diff --git a/fit/src/test/java/org/apache/olingo/fit/tecsvc/client/SystemQueryOptionITCase.java b/fit/src/test/java/org/apache/olingo/fit/tecsvc/client/SystemQueryOptionITCase.java index 4341a5390..e605836e4 100644 --- a/fit/src/test/java/org/apache/olingo/fit/tecsvc/client/SystemQueryOptionITCase.java +++ b/fit/src/test/java/org/apache/olingo/fit/tecsvc/client/SystemQueryOptionITCase.java @@ -21,10 +21,11 @@ package org.apache.olingo.fit.tecsvc.client; import static org.junit.Assert.assertEquals; import static org.junit.Assert.fail; +import java.io.IOException; import java.net.URI; +import java.util.List; import org.apache.olingo.client.api.communication.ODataClientErrorException; -import org.apache.olingo.client.api.communication.ODataServerErrorException; import org.apache.olingo.client.api.communication.request.retrieve.ODataEntitySetRequest; import org.apache.olingo.client.api.communication.response.ODataRetrieveResponse; import org.apache.olingo.client.api.domain.ClientEntity; @@ -301,18 +302,41 @@ public class SystemQueryOptionITCase extends AbstractParamTecSvcITCase { } @Test - public void negativeSearch() { + public void basicSearch() { ODataEntitySetRequest request = getClient().getRetrieveRequestFactory() .getEntitySetRequest(getClient().newURIBuilder(SERVICE_URI) .appendEntitySetSegment(ES_ALL_PRIM) - .search("ABC") + .search("Second") .build()); setCookieHeader(request); - try { - request.execute(); - fail(); - } catch (ODataServerErrorException e) { - assertEquals("HTTP/1.1 501 Not Implemented", e.getMessage()); - } + ODataRetrieveResponse response = request.execute(); + List entities = response.getBody().getEntities(); + assertEquals(1, entities.size()); + } + + @Test + public void andSearch() { + ODataEntitySetRequest request = getClient().getRetrieveRequestFactory() + .getEntitySetRequest(getClient().newURIBuilder(SERVICE_URI) + .appendEntitySetSegment(ES_ALL_PRIM) + .search("Second AND positive") + .build()); + setCookieHeader(request); + ODataRetrieveResponse response = request.execute(); + List entities = response.getBody().getEntities(); + assertEquals(0, entities.size()); + } + + @Test + public void orSearch() { + ODataEntitySetRequest request = getClient().getRetrieveRequestFactory() + .getEntitySetRequest(getClient().newURIBuilder(SERVICE_URI) + .appendEntitySetSegment(ES_ALL_PRIM) + .search("Second OR positive") + .build()); + setCookieHeader(request); + ODataRetrieveResponse response = request.execute(); + List entities = response.getBody().getEntities(); + assertEquals(2, entities.size()); } } diff --git a/lib/server-api/src/main/java/org/apache/olingo/server/api/uri/queryoption/search/SearchExpression.java b/lib/server-api/src/main/java/org/apache/olingo/server/api/uri/queryoption/search/SearchExpression.java index ed66f5feb..983919cca 100644 --- a/lib/server-api/src/main/java/org/apache/olingo/server/api/uri/queryoption/search/SearchExpression.java +++ b/lib/server-api/src/main/java/org/apache/olingo/server/api/uri/queryoption/search/SearchExpression.java @@ -19,5 +19,17 @@ package org.apache.olingo.server.api.uri.queryoption.search; public interface SearchExpression { - //No additional methods needed for now. + + boolean isSearchTerm(); + + SearchTerm asSearchTerm(); + + boolean isSearchBinary(); + + SearchBinary asSearchBinary(); + + boolean isSearchUnary(); + + SearchUnary asSearchUnary(); + } diff --git a/lib/server-api/src/main/java/org/apache/olingo/server/api/uri/queryoption/search/SearchUnary.java b/lib/server-api/src/main/java/org/apache/olingo/server/api/uri/queryoption/search/SearchUnary.java index 5d8e143e4..c26630878 100644 --- a/lib/server-api/src/main/java/org/apache/olingo/server/api/uri/queryoption/search/SearchUnary.java +++ b/lib/server-api/src/main/java/org/apache/olingo/server/api/uri/queryoption/search/SearchUnary.java @@ -18,8 +18,9 @@ */ package org.apache.olingo.server.api.uri.queryoption.search; -public interface SearchUnary { +public interface SearchUnary extends SearchExpression { - SearchExpression getOperand(); + SearchUnaryOperatorKind getOperator(); + SearchTerm getOperand(); } diff --git a/lib/server-core/src/main/java/org/apache/olingo/server/core/uri/parser/Parser.java b/lib/server-core/src/main/java/org/apache/olingo/server/core/uri/parser/Parser.java index c5857c76c..d12b85370 100644 --- a/lib/server-core/src/main/java/org/apache/olingo/server/core/uri/parser/Parser.java +++ b/lib/server-core/src/main/java/org/apache/olingo/server/core/uri/parser/Parser.java @@ -58,6 +58,7 @@ import org.apache.olingo.server.core.uri.antlr.UriParserParser.MetadataEOFContex import org.apache.olingo.server.core.uri.antlr.UriParserParser.OrderByEOFContext; import org.apache.olingo.server.core.uri.antlr.UriParserParser.PathSegmentEOFContext; import org.apache.olingo.server.core.uri.antlr.UriParserParser.SelectEOFContext; +import org.apache.olingo.server.core.uri.parser.search.SearchParser; import org.apache.olingo.server.core.uri.queryoption.AliasQueryOptionImpl; import org.apache.olingo.server.core.uri.queryoption.CountOptionImpl; import org.apache.olingo.server.core.uri.queryoption.CustomQueryOptionImpl; @@ -80,7 +81,7 @@ public class Parser { int logLevel = 0; private enum ParserEntryRules { - All, Batch, CrossJoin, Entity, ExpandItems, FilterExpression, Metadata, PathSegment, Orderby, Select + All, Batch, CrossJoin, Entity, ExpandItems, FilterExpression, Metadata, PathSegment, Orderby, Select, Search } public Parser setLogLevel(final int logLevel) { @@ -218,8 +219,8 @@ public class Parser { systemOption = (OrderByOptionImpl) uriParseTreeVisitor.visitOrderByEOF(ctxOrderByExpression); } else if (option.name.equals(SystemQueryOptionKind.SEARCH.toString())) { - throw new UriParserSemanticException("System query option '$search' not implemented!", - UriParserSemanticException.MessageKeys.NOT_IMPLEMENTED, "System query option '$search"); + SearchParser searchParser = new SearchParser(); + systemOption = searchParser.parse(option.value); } else if (option.name.equals(SystemQueryOptionKind.SELECT.toString())) { SelectEOFContext ctxSelectEOF = (SelectEOFContext) parseRule(option.value, ParserEntryRules.Select); @@ -386,6 +387,9 @@ public class Parser { case Select: ret = parser.selectEOF(); break; + case Search: + ret = parser.searchInline(); + break; default: break; @@ -443,6 +447,9 @@ public class Parser { case Select: ret = parser.selectEOF(); break; + case Search: + ret = parser.searchInline(); + break; default: break; } @@ -501,7 +508,7 @@ public class Parser { } else { out.append(index); } - out.append(nL); + out.append(nL); } out.append(']'); System.out.println("tokens: " + out.toString()); diff --git a/lib/server-core/src/main/java/org/apache/olingo/server/core/uri/parser/search/SearchBinaryImpl.java b/lib/server-core/src/main/java/org/apache/olingo/server/core/uri/parser/search/SearchBinaryImpl.java new file mode 100644 index 000000000..418d9e75a --- /dev/null +++ b/lib/server-core/src/main/java/org/apache/olingo/server/core/uri/parser/search/SearchBinaryImpl.java @@ -0,0 +1,56 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.olingo.server.core.uri.parser.search; + +import org.apache.olingo.server.api.uri.queryoption.search.SearchBinary; +import org.apache.olingo.server.api.uri.queryoption.search.SearchBinaryOperatorKind; +import org.apache.olingo.server.api.uri.queryoption.search.SearchExpression; + +public class SearchBinaryImpl extends SearchExpressionImpl implements SearchBinary { + + private final SearchBinaryOperatorKind operator; + private final SearchExpression left; + private final SearchExpression right; + + public SearchBinaryImpl(SearchExpression left, SearchBinaryOperatorKind operator, SearchExpression right) { + this.left = left; + this.operator = operator; + this.right = right; + } + + @Override + public SearchBinaryOperatorKind getOperator() { + return operator; + } + + @Override + public SearchExpression getLeftOperand() { + return left; + } + + @Override + public SearchExpression getRightOperand() { + return right; + } + + @Override + public String toString() { + return "{" + left + " " + operator.name() + " " + right + '}'; + } +} diff --git a/lib/server-core/src/main/java/org/apache/olingo/server/core/uri/parser/search/SearchExpressionImpl.java b/lib/server-core/src/main/java/org/apache/olingo/server/core/uri/parser/search/SearchExpressionImpl.java new file mode 100644 index 000000000..ee5a19773 --- /dev/null +++ b/lib/server-core/src/main/java/org/apache/olingo/server/core/uri/parser/search/SearchExpressionImpl.java @@ -0,0 +1,58 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.olingo.server.core.uri.parser.search; + +import org.apache.olingo.server.api.uri.queryoption.search.SearchBinary; +import org.apache.olingo.server.api.uri.queryoption.search.SearchExpression; +import org.apache.olingo.server.api.uri.queryoption.search.SearchTerm; +import org.apache.olingo.server.api.uri.queryoption.search.SearchUnary; + +public abstract class SearchExpressionImpl implements SearchExpression { + + @Override + public boolean isSearchTerm() { + return this instanceof SearchTerm; + } + + @Override + public SearchTerm asSearchTerm() { + return isSearchTerm() ? (SearchTerm) this : null; + } + + @Override + public boolean isSearchBinary() { + return this instanceof SearchBinary; + } + + @Override + public SearchBinary asSearchBinary() { + return isSearchBinary() ? (SearchBinary) this : null; + } + + @Override + public boolean isSearchUnary() { + return this instanceof SearchUnary; + } + + @Override + public SearchUnary asSearchUnary() { + return isSearchUnary() ? (SearchUnary) this : null; + } + +} diff --git a/lib/server-core/src/main/java/org/apache/olingo/server/core/uri/parser/search/SearchParser.java b/lib/server-core/src/main/java/org/apache/olingo/server/core/uri/parser/search/SearchParser.java new file mode 100644 index 000000000..a9fe332dd --- /dev/null +++ b/lib/server-core/src/main/java/org/apache/olingo/server/core/uri/parser/search/SearchParser.java @@ -0,0 +1,196 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.olingo.server.core.uri.parser.search; + +import org.apache.olingo.server.api.uri.queryoption.SearchOption; +import org.apache.olingo.server.api.uri.queryoption.search.SearchBinaryOperatorKind; +import org.apache.olingo.server.api.uri.queryoption.search.SearchExpression; +import org.apache.olingo.server.api.uri.queryoption.search.SearchTerm; +import org.apache.olingo.server.core.uri.parser.search.SearchQueryToken.Token; +import org.apache.olingo.server.core.uri.queryoption.SearchOptionImpl; + +import java.util.Iterator; +import java.util.List; + +public class SearchParser { + + private Iterator tokens; + private SearchQueryToken token; + + public SearchOption parse(String searchQuery) throws SearchParserException, SearchTokenizerException { + SearchTokenizer tokenizer = new SearchTokenizer(); + SearchExpression searchExpression; + try { + searchExpression = parse(tokenizer.tokenize(searchQuery)); + } catch (SearchTokenizerException e) { + return null; + } + final SearchOptionImpl searchOption = new SearchOptionImpl(); + searchOption.setSearchExpression(searchExpression); + return searchOption; + } + + protected SearchExpression parse(List tokens) throws SearchParserException { + this.tokens = tokens.iterator(); + nextToken(); + if (token == null) { + throw new SearchParserException("No search String", SearchParserException.MessageKeys.NO_EXPRESSION_FOUND); + } + return processSearchExpression(null); + } + + private SearchExpression processSearchExpression(SearchExpression left) throws SearchParserException { + if (isEof()) { + return left; + } + + if (left == null && (isToken(SearchQueryToken.Token.AND) || isToken(SearchQueryToken.Token.OR))) { + throw new SearchParserException(token.getToken() + " needs a left operand.", + SearchParserException.MessageKeys.INVALID_BINARY_OPERATOR_POSITION, token.getToken().toString()); + } + + SearchExpression expression = left; + if (isToken(SearchQueryToken.Token.OPEN)) { + processOpen(); + expression = processSearchExpression(left); + validateToken(SearchQueryToken.Token.CLOSE); + processClose(); + } else if (isTerm()) { + expression = processTerm(); + } + + if (expression == null) { + throw new SearchParserException("Brackets must contain an expression.", + SearchParserException.MessageKeys.NO_EXPRESSION_FOUND); + } + + if (isToken(SearchQueryToken.Token.AND) || isToken(SearchQueryToken.Token.OPEN) || isTerm()) { + expression = processAnd(expression); + } else if (isToken(SearchQueryToken.Token.OR)) { + expression = processOr(expression); + } else if (isEof()) { + return expression; + } + return expression; + } + + private boolean isTerm() { + return isToken(SearchQueryToken.Token.NOT) + || isToken(SearchQueryToken.Token.PHRASE) + || isToken(SearchQueryToken.Token.WORD); + } + + private boolean isEof() { + return token == null; + } + + private boolean isToken(SearchQueryToken.Token toCheckToken) { + return token != null && token.getToken() == toCheckToken; + } + + private void validateToken(SearchQueryToken.Token toValidateToken) throws SearchParserException { + if (!isToken(toValidateToken)) { + String actualToken = token == null ? "null" : token.getToken().toString(); + throw new SearchParserException("Expected " + toValidateToken + " but was " + actualToken, + SearchParserException.MessageKeys.EXPECTED_DIFFERENT_TOKEN, toValidateToken.toString(), actualToken); + } + } + + private void processClose() { + nextToken(); + } + + private void processOpen() { + nextToken(); + } + + private SearchExpression processAnd(SearchExpression left) throws SearchParserException { + if (isToken(SearchQueryToken.Token.AND)) { + nextToken(); + } + SearchExpression se = left; + if (isTerm()) { + se = processTerm(); + se = new SearchBinaryImpl(left, SearchBinaryOperatorKind.AND, se); + return processSearchExpression(se); + } else { + if (isToken(SearchQueryToken.Token.AND) || isToken(SearchQueryToken.Token.OR)) { + throw new SearchParserException("Operators must not be followed by an AND or an OR", + SearchParserException.MessageKeys.INVALID_OPERATOR_AFTER_AND, token.getToken().toString()); + } + se = processSearchExpression(se); + return new SearchBinaryImpl(left, SearchBinaryOperatorKind.AND, se); + } + } + + public SearchExpression processOr(SearchExpression left) throws SearchParserException { + if (isToken(SearchQueryToken.Token.OR)) { + nextToken(); + } + SearchExpression se = processSearchExpression(left); + return new SearchBinaryImpl(left, SearchBinaryOperatorKind.OR, se); + } + + private SearchExpression processNot() throws SearchParserException { + nextToken(); + if (isToken(Token.WORD) || isToken(Token.PHRASE)) { + return new SearchUnaryImpl(processWordOrPhrase()); + } + throw new SearchParserException("NOT must be followed by a term not a " + token.getToken(), + SearchParserException.MessageKeys.INVALID_NOT_OPERAND, token.getToken().toString()); + } + + private void nextToken() { + if (tokens.hasNext()) { + token = tokens.next(); + } else { + token = null; + } + } + + private SearchExpression processTerm() throws SearchParserException { + if (isToken(SearchQueryToken.Token.NOT)) { + return processNot(); + } + return processWordOrPhrase(); + } + + private SearchTerm processWordOrPhrase() throws SearchParserException { + if (isToken(Token.PHRASE)) { + return processPhrase(); + } else if (isToken(Token.WORD)) { + return processWord(); + } + throw new SearchParserException("Expected PHRASE||WORD found: " + token.getToken(), + SearchParserException.MessageKeys.EXPECTED_DIFFERENT_TOKEN, + Token.PHRASE.name() + "" + Token.WORD.name(), token.getToken().toString()); + } + + private SearchTerm processWord() { + String literal = token.getLiteral(); + nextToken(); + return new SearchTermImpl(literal); + } + + private SearchTerm processPhrase() { + String literal = token.getLiteral(); + nextToken(); + return new SearchTermImpl(literal.substring(1,literal.length()-1)); + } +} diff --git a/lib/server-core/src/main/java/org/apache/olingo/server/core/uri/parser/search/SearchParserException.java b/lib/server-core/src/main/java/org/apache/olingo/server/core/uri/parser/search/SearchParserException.java new file mode 100644 index 000000000..78a12beb3 --- /dev/null +++ b/lib/server-core/src/main/java/org/apache/olingo/server/core/uri/parser/search/SearchParserException.java @@ -0,0 +1,54 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.olingo.server.core.uri.parser.search; + +import org.apache.olingo.server.core.uri.parser.UriParserSyntaxException; + +public class SearchParserException extends UriParserSyntaxException { + + private static final long serialVersionUID = 5781553037561337795L; + + public static enum MessageKeys implements MessageKey { + /** parameter: operatorkind */ + INVALID_BINARY_OPERATOR_POSITION, + /** parameter: operatorkind */ + INVALID_NOT_OPERAND, + /** parameters: expectedToken actualToken */ + EXPECTED_DIFFERENT_TOKEN, + NO_EXPRESSION_FOUND, + /** parameter: operatorkind */ + INVALID_OPERATOR_AFTER_AND; + + @Override + public String getKey() { + return name(); + } + } + + public SearchParserException(final String developmentMessage, final MessageKey messageKey, + final String... parameters) { + super(developmentMessage, messageKey, parameters); + } + + public SearchParserException(final String developmentMessage, final Throwable cause, final MessageKey messageKey, + final String... parameters) { + super(developmentMessage, cause, messageKey, parameters); + } + +} diff --git a/lib/server-core/src/main/java/org/apache/olingo/server/core/uri/parser/search/SearchQueryToken.java b/lib/server-core/src/main/java/org/apache/olingo/server/core/uri/parser/search/SearchQueryToken.java new file mode 100644 index 000000000..3fb66f1ac --- /dev/null +++ b/lib/server-core/src/main/java/org/apache/olingo/server/core/uri/parser/search/SearchQueryToken.java @@ -0,0 +1,26 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.olingo.server.core.uri.parser.search; + +public interface SearchQueryToken { + enum Token {OPEN, NOT, AND, OR, WORD, PHRASE, CLOSE} + + Token getToken(); + String getLiteral(); +} diff --git a/lib/server-core/src/main/java/org/apache/olingo/server/core/uri/parser/search/SearchTermImpl.java b/lib/server-core/src/main/java/org/apache/olingo/server/core/uri/parser/search/SearchTermImpl.java new file mode 100644 index 000000000..efd728028 --- /dev/null +++ b/lib/server-core/src/main/java/org/apache/olingo/server/core/uri/parser/search/SearchTermImpl.java @@ -0,0 +1,39 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.olingo.server.core.uri.parser.search; + +import org.apache.olingo.server.api.uri.queryoption.search.SearchTerm; + +public class SearchTermImpl extends SearchExpressionImpl implements SearchTerm { + private final String term; + + public SearchTermImpl(String term) { + this.term = term; + } + + @Override + public String getSearchTerm() { + return term; + } + + @Override + public String toString() { + return "'" + term + "'"; + } +} diff --git a/lib/server-core/src/main/java/org/apache/olingo/server/core/uri/parser/search/SearchTokenizer.java b/lib/server-core/src/main/java/org/apache/olingo/server/core/uri/parser/search/SearchTokenizer.java new file mode 100644 index 000000000..fb0ad9452 --- /dev/null +++ b/lib/server-core/src/main/java/org/apache/olingo/server/core/uri/parser/search/SearchTokenizer.java @@ -0,0 +1,571 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.olingo.server.core.uri.parser.search; + +import java.util.ArrayList; +import java.util.List; + +/** + * + * searchExpr = ( OPEN BWS searchExpr BWS CLOSE + * / searchTerm + * ) [ searchOrExpr + * / searchAndExpr + * ] + * + * searchOrExpr = RWS 'OR' RWS searchExpr + * searchAndExpr = RWS [ 'AND' RWS ] searchExpr + * + * searchTerm = [ 'NOT' RWS ] ( searchPhrase / searchWord ) + * searchPhrase = quotation-mark 1*qchar-no-AMP-DQUOTE quotation-mark + * searchWord = 1*ALPHA ; Actually: any character from the Unicode categories L or Nl, + * ; but not the words AND, OR, and NOT + * + */ +public class SearchTokenizer { + + private static abstract class State implements SearchQueryToken { + private Token token = null; + private boolean finished = false; + + protected static final char QUOTATION_MARK = '\"'; + protected static final char CHAR_N = 'N'; + protected static final char CHAR_O = 'O'; + protected static final char CHAR_T = 'T'; + protected static final char CHAR_A = 'A'; + protected static final char CHAR_D = 'D'; + protected static final char CHAR_R = 'R'; + protected static final char CHAR_CLOSE = ')'; + protected static final char CHAR_OPEN = '('; + + public State(Token t) { + token = t; + } + + protected abstract State nextChar(char c) throws SearchTokenizerException; + + public State allowed(char c) { + return this; + } + + public State forbidden(char c) throws SearchTokenizerException { + throw new SearchTokenizerException("Forbidden character for " + this.getClass().getName() + "->" + c, + SearchTokenizerException.MessageKeys.FORBIDDEN_CHARACTER, "" + c); + } + + public State finish() { + this.finished = true; + return this; + } + + public boolean isFinished() { + return finished; + } + + public Token getToken() { + return token; + } + + public State close() { + return this; + } + + static boolean isAllowedWord(final char character) { + // TODO mibo: add missing allowed characters + int type = Character.getType(character); + return (type == Character.LETTER_NUMBER + || type == Character.LOWERCASE_LETTER + || type == Character.MODIFIER_LETTER + || type == Character.OTHER_LETTER + || type == Character.TITLECASE_LETTER + || type == Character.UPPERCASE_LETTER); + } + + /** + * searchPhrase = quotation-mark 1*qchar-no-AMP-DQUOTE quotation-mark + * + * qchar-no-AMP-DQUOTE = qchar-unescaped / escape ( escape / quotation-mark ) + * + * qchar-unescaped = unreserved / pct-encoded-unescaped / other-delims / ":" / "@" / "/" / "?" / "$" / "'" / "=" + * + * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" + * + * escape = "\" / "%5C" ; reverse solidus U+005C + * + * pct-encoded-unescaped = "%" ( "0" / "1" / "3" / "4" / "6" / "7" / "8" / "9" / A-to-F ) HEXDIG + * / "%" "2" ( "0" / "1" / "3" / "4" / "5" / "6" / "7" / "8" / "9" / A-to-F ) + * / "%" "5" ( DIGIT / "A" / "B" / "D" / "E" / "F" ) + * + * other-delims = "!" / "(" / ")" / "*" / "+" / "," / ";" + * + * quotation-mark = DQUOTE / "%22" + * + * ALPHA = %x41-5A / %x61-7A + * DIGIT = %x30-39 + * DQUOTE = %x22 + * + * @param character which is checked + * @return true if character is allowed for a phrase + */ + static boolean isAllowedPhrase(final char character) { + // FIXME mibo: check missing + return isQCharUnescaped(character) || isEscaped(character); + } + + /** + * escape = "\" / "%5C" ; reverse solidus U+005C + * @param character which is checked + * @return true if character is allowed + */ + private static boolean isEscaped(char character) { + // TODO: mibo(151117): check how to implement + return false; + } + + /** + * qchar-unescaped = unreserved / pct-encoded-unescaped / other-delims / ":" / "@" / "/" / "?" / "$" / "'" / "=" + * @param character which is checked + * @return true if character is allowed + */ + private static boolean isQCharUnescaped(char character) { + return isUnreserved(character) + || isPctEncodedUnescaped(character) + || isOtherDelims(character) + || character == ':' + || character == '@' + || character == '/' + || character == '$' + || character == '\'' + || character == '='; + } + + /** + * other-delims = "!" / "(" / ")" / "*" / "+" / "," / ";" + * @param character which is checked + * @return true if character is allowed + */ + private static boolean isOtherDelims(char character) { + return character == '!' + || character == '(' + || character == ')' + || character == '*' + || character == '+' + || character == ',' + || character == ';'; + } + + /** + * pct-encoded-unescaped = "%" ( "0" / "1" / "3" / "4" / "6" / "7" / "8" / "9" / A-to-F ) HEXDIG + * / "%" "2" ( "0" / "1" / "3" / "4" / "5" / "6" / "7" / "8" / "9" / A-to-F ) + * / "%" "5" ( DIGIT / "A" / "B" / "D" / "E" / "F" ) + * + * HEXDIG = DIGIT / A-to-F + * + * @param character which is checked + * @return true if character is allowed + */ + private static boolean isPctEncodedUnescaped(char character) { + String hex = Integer.toHexString((int) character); + char aschar[] = hex.toCharArray(); + if(aschar[0] == '%') { + if(aschar[1] == '2') { + return aschar[2] != '2' && isHexDigit(aschar[2]); + } else if(aschar[1] == '5') { + return aschar[2] != 'C' && isHexDigit(aschar[2]); + } else if(isHexDigit(aschar[1])) { + return isHexDigit(aschar[2]); + } + } + return false; + } + + private static boolean isHexDigit(char character) { + return 'A' <= character && character <= 'F' // case A..F + || '0' <= character && character <= '9'; // case 0..9 + } + + /** + * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" + * @param character which is checked + * @return true if character is allowed + */ + private static boolean isUnreserved(char character) { + return isAlphaOrDigit(character) + || character == '-' + || character == '.' + || character == '_' + || character == '~'; + } + + /** + * ALPHA = %x41-5A / %x61-7A + * DIGIT = %x30-39 + * @param character which is checked + * @return true if character is allowed + */ + private static boolean isAlphaOrDigit(char character) { + return 'A' <= character && character <= 'Z' // case A..Z + || 'a' <= character && character <= 'z' // case a..z + || '0' <= character && character <= '9'; // case 0..9 + } + + // BWS = *( SP / HTAB / "%20" / "%09" ) ; "bad" whitespace + // RWS = 1*( SP / HTAB / "%20" / "%09" ) ; "required" whitespace + static boolean isWhitespace(final char character) { + // ( SP / HTAB / "%20" / "%09" ) + // TODO mibo: add missing whitespaces + return character == ' ' || character == '\t'; + } + + @Override + public String getLiteral() { + return token.toString(); + } + + @Override + public String toString() { + return this.getToken().toString() + "=>{" + getLiteral() + "}"; + } + } + + private static abstract class LiteralState extends State { + protected final StringBuilder literal = new StringBuilder(); + + public LiteralState(Token t) { + super(t); + } + + public LiteralState(Token t, char c) throws SearchTokenizerException { + super(t); + init(c); + } + + public LiteralState(Token t, String initLiteral) { + super(t); + literal.append(initLiteral); + } + + public State allowed(char c) { + literal.append(c); + return this; + } + + @Override + public String getLiteral() { + return literal.toString(); + } + + public State init(char c) throws SearchTokenizerException { + if (isFinished()) { + throw new SearchTokenizerException(toString() + " is already finished.", + SearchTokenizerException.MessageKeys.ALREADY_FINISHED); + } + literal.append(c); + return this; + } + } + + private class SearchExpressionState extends LiteralState { + public SearchExpressionState() { + super(null); + } + + @Override + public State nextChar(char c) throws SearchTokenizerException { + if (c == CHAR_OPEN) { + return new OpenState(); + } else if (isWhitespace(c)) { + return new RwsState(); + } else if (c == CHAR_CLOSE) { + return new CloseState(); + } else { + return new SearchTermState().init(c); + } + } + + @Override + public State init(char c) throws SearchTokenizerException { + return nextChar(c); + } + } + + private class SearchTermState extends LiteralState { + public SearchTermState() { + super(null); + } + + @Override + public State nextChar(char c) throws SearchTokenizerException { + if (c == CHAR_N) { + return new NotState(c); + } else if (c == QUOTATION_MARK) { + return new SearchPhraseState(c); + } else if (isAllowedWord(c)) { + return new SearchWordState(c); + } + return forbidden(c); + } + + @Override + public State init(char c) throws SearchTokenizerException { + return nextChar(c); + } + } + + private class SearchWordState extends LiteralState { + public SearchWordState(char c) throws SearchTokenizerException { + super(Token.WORD, c); + if (!isAllowedWord(c)) { + forbidden(c); + } + } + + public SearchWordState(State toConsume) throws SearchTokenizerException { + super(Token.WORD, toConsume.getLiteral()); + char[] chars = literal.toString().toCharArray(); + for (char aChar : chars) { + if (!isAllowedWord(aChar)) { + forbidden(aChar); + } + } + } + + @Override + public State nextChar(char c) throws SearchTokenizerException { + if (isAllowedWord(c)) { + return allowed(c); + } else if (c == CHAR_CLOSE) { + finish(); + return new CloseState(); + } else if (isWhitespace(c)) { + finish(); + return new RwsState(); + } + return forbidden(c); + } + + @Override + public State close() { + return finish(); + } + } + + private class SearchPhraseState extends LiteralState { + public SearchPhraseState(char c) throws SearchTokenizerException { + super(Token.PHRASE, c); + if (c != QUOTATION_MARK) { + forbidden(c); + } + } + + @Override + public State nextChar(char c) throws SearchTokenizerException { + if (isAllowedPhrase(c)) { + return allowed(c); + } else if (isWhitespace(c)) { + return allowed(c); + } else if (c == QUOTATION_MARK) { + finish(); + allowed(c); + return new SearchExpressionState(); + } else if (isFinished()) { + return new SearchExpressionState().init(c); + } + return forbidden(c); + } + } + + private class OpenState extends State { + public OpenState() { + super(Token.OPEN); + finish(); + } + + @Override + public State nextChar(char c) throws SearchTokenizerException { + finish(); + if (isWhitespace(c)) { + return forbidden(c); + } + return new SearchExpressionState().init(c); + } + } + + private class CloseState extends State { + public CloseState() { + super(Token.CLOSE); + finish(); + } + + @Override + public State nextChar(char c) throws SearchTokenizerException { + return new SearchExpressionState().init(c); + } + } + + private class NotState extends LiteralState { + public NotState(char c) throws SearchTokenizerException { + super(Token.NOT, c); + if (c != CHAR_N) { + forbidden(c); + } + } + + @Override + public State nextChar(char c) throws SearchTokenizerException { + if (literal.length() == 1 && c == CHAR_O) { + return allowed(c); + } else if (literal.length() == 2 && c == CHAR_T) { + return allowed(c); + } else if (literal.length() == 3 && isWhitespace(c)) { + finish(); + return new BeforePhraseOrWordRwsState(); + } + return forbidden(c); + } + } + + private class AndState extends LiteralState { + public AndState(char c) throws SearchTokenizerException { + super(Token.AND, c); + if (c != CHAR_A) { + forbidden(c); + } + } + + @Override + public State nextChar(char c) throws SearchTokenizerException { + if (literal.length() == 1 && c == CHAR_N) { + return allowed(c); + } else if (literal.length() == 2 && c == CHAR_D) { + return allowed(c); + } else if (literal.length() == 3 && isWhitespace(c)) { + finish(); + return new BeforeSearchExpressionRwsState(); + } else { + return new SearchWordState(this); + } + } + } + + private class OrState extends LiteralState { + public OrState(char c) throws SearchTokenizerException { + super(Token.OR, c); + if (c != CHAR_O) { + forbidden(c); + } + } + + @Override + public State nextChar(char c) throws SearchTokenizerException { + if (literal.length() == 1 && (c == CHAR_R)) { + return allowed(c); + } else if (literal.length() == 2 && isWhitespace(c)) { + finish(); + return new BeforeSearchExpressionRwsState(); + } else { + return new SearchWordState(this); + } + } + } + + // RWS 'OR' RWS searchExpr + // RWS [ 'AND' RWS ] searchExpr + private class BeforeSearchExpressionRwsState extends State { + public BeforeSearchExpressionRwsState() { + super(null); + } + + @Override + public State nextChar(char c) throws SearchTokenizerException { + if (isWhitespace(c)) { + return allowed(c); + } else { + return new SearchExpressionState().init(c); + } + } + } + + private class BeforePhraseOrWordRwsState extends State { + public BeforePhraseOrWordRwsState() { + super(null); + } + + @Override + public State nextChar(char c) throws SearchTokenizerException { + if (isWhitespace(c)) { + return allowed(c); + } else if (c == '"') { + return new SearchPhraseState(c); + } else { + return new SearchWordState(c); + } + } + } + + private class RwsState extends State { + public RwsState() { + super(null); + } + + @Override + public State nextChar(char c) throws SearchTokenizerException { + if (isWhitespace(c)) { + return allowed(c); + } else if (c == CHAR_O) { + return new OrState(c); + } else if (c == CHAR_A) { + return new AndState(c); + } else { + return new SearchExpressionState().init(c); + } + } + } + + /** + * Take the search query and split into according SearchQueryToken. + * Before split into tokens the given search query is 'trimmed'. + * + * @param searchQuery search query to be tokenized + * @return list of tokens + * @throws SearchTokenizerException if something in query is not valid + * (based on OData search query ABNF) + */ + public List tokenize(final String searchQuery) + throws SearchTokenizerException { + + char[] chars = searchQuery.trim().toCharArray(); + + State state = new SearchExpressionState(); + List states = new ArrayList(); + for (char aChar : chars) { + State next = state.nextChar(aChar); + if (state.isFinished()) { + states.add(state); + } + state = next; + } + + if (state.close().isFinished()) { + states.add(state); + } + + return states; + } +} diff --git a/lib/server-core/src/main/java/org/apache/olingo/server/core/uri/parser/search/SearchTokenizerException.java b/lib/server-core/src/main/java/org/apache/olingo/server/core/uri/parser/search/SearchTokenizerException.java new file mode 100644 index 000000000..fb20efe37 --- /dev/null +++ b/lib/server-core/src/main/java/org/apache/olingo/server/core/uri/parser/search/SearchTokenizerException.java @@ -0,0 +1,47 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.olingo.server.core.uri.parser.search; + +import org.apache.olingo.server.core.uri.parser.UriParserSyntaxException; + +public class SearchTokenizerException extends UriParserSyntaxException { + + private static final long serialVersionUID = -8295456415309640166L; + + public static enum MessageKeys implements MessageKey { + /** parameter: character */ + FORBIDDEN_CHARACTER, + ALREADY_FINISHED; + + @Override + public String getKey() { + return name(); + } + } + + public SearchTokenizerException(final String developmentMessage, final MessageKey messageKey, + final String... parameters) { + super(developmentMessage, messageKey, parameters); + } + + public SearchTokenizerException(final String developmentMessage, final Throwable cause, final MessageKey messageKey, + final String... parameters) { + super(developmentMessage, cause, messageKey, parameters); + } +} diff --git a/lib/server-core/src/main/java/org/apache/olingo/server/core/uri/parser/search/SearchUnaryImpl.java b/lib/server-core/src/main/java/org/apache/olingo/server/core/uri/parser/search/SearchUnaryImpl.java new file mode 100644 index 000000000..51e3a2449 --- /dev/null +++ b/lib/server-core/src/main/java/org/apache/olingo/server/core/uri/parser/search/SearchUnaryImpl.java @@ -0,0 +1,46 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.olingo.server.core.uri.parser.search; + +import org.apache.olingo.server.api.uri.queryoption.search.SearchTerm; +import org.apache.olingo.server.api.uri.queryoption.search.SearchUnary; +import org.apache.olingo.server.api.uri.queryoption.search.SearchUnaryOperatorKind; + +public class SearchUnaryImpl extends SearchExpressionImpl implements SearchUnary { + private final SearchTerm operand; + + public SearchUnaryImpl(SearchTerm operand) { + this.operand = operand; + } + + @Override + public SearchUnaryOperatorKind getOperator() { + return SearchUnaryOperatorKind.NOT; + } + + @Override + public SearchTerm getOperand() { + return operand; + } + + @Override + public String toString() { + return "{" + getOperator().name() + " " + operand + '}'; + } +} diff --git a/lib/server-core/src/main/java/org/apache/olingo/server/core/uri/queryoption/SearchOptionImpl.java b/lib/server-core/src/main/java/org/apache/olingo/server/core/uri/queryoption/SearchOptionImpl.java index ec42147c6..51323a852 100644 --- a/lib/server-core/src/main/java/org/apache/olingo/server/core/uri/queryoption/SearchOptionImpl.java +++ b/lib/server-core/src/main/java/org/apache/olingo/server/core/uri/queryoption/SearchOptionImpl.java @@ -24,13 +24,18 @@ import org.apache.olingo.server.api.uri.queryoption.search.SearchExpression; public class SearchOptionImpl extends SystemQueryOptionImpl implements SearchOption { + private SearchExpression searchExpression; + public SearchOptionImpl() { setKind(SystemQueryOptionKind.SEARCH); } @Override public SearchExpression getSearchExpression() { - return null; + return searchExpression; } + public void setSearchExpression(SearchExpression searchExpression) { + this.searchExpression = searchExpression; + } } diff --git a/lib/server-core/src/test/java/org/apache/olingo/server/core/uri/parser/search/SearchParserAndTokenizerTest.java b/lib/server-core/src/test/java/org/apache/olingo/server/core/uri/parser/search/SearchParserAndTokenizerTest.java new file mode 100644 index 000000000..23cac8e3d --- /dev/null +++ b/lib/server-core/src/test/java/org/apache/olingo/server/core/uri/parser/search/SearchParserAndTokenizerTest.java @@ -0,0 +1,198 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.olingo.server.core.uri.parser.search; + +import static org.apache.olingo.server.api.uri.queryoption.search.SearchBinaryOperatorKind.AND; +import static org.apache.olingo.server.api.uri.queryoption.search.SearchBinaryOperatorKind.OR; + +import java.lang.reflect.Field; + +import org.apache.olingo.server.api.uri.queryoption.SearchOption; +import org.apache.olingo.server.api.uri.queryoption.search.SearchExpression; +import org.apache.olingo.server.api.uri.queryoption.search.SearchUnary; +import org.junit.Assert; +import org.junit.Ignore; +import org.junit.Test; + +public class SearchParserAndTokenizerTest { + + @Test + public void basicParsing() throws Exception { + SearchExpressionValidator.init("a") + .validate(with("a")); + SearchExpressionValidator.init("a AND b") + .validate(with("a", and("b"))); + SearchExpressionValidator.init("a AND b AND c") + .validate("{{'a' AND 'b'} AND 'c'}"); + SearchExpressionValidator.init("a OR b") + .validate(with("a", or("b"))); + SearchExpressionValidator.init("a OR b OR c") + .validate(with("a", or("b", or("c")))); + } + + @Test + public void mixedParsing() throws Exception { + SearchExpressionValidator.init("a AND b OR c") + .validate("{{'a' AND 'b'} OR 'c'}"); + SearchExpressionValidator.init("a OR b AND c") + .validate("{'a' OR {'b' AND 'c'}}"); + } + + @Test + public void notParsing() throws Exception { + SearchExpressionValidator.init("NOT a AND b OR c") + .validate("{{{NOT 'a'} AND 'b'} OR 'c'}"); + SearchExpressionValidator.init("a OR b AND NOT c") + .validate("{'a' OR {'b' AND {NOT 'c'}}}"); + } + + @Test + public void parenthesesParsing() throws Exception { + SearchExpressionValidator.init("a AND (b OR c)") + .validate("{'a' AND {'b' OR 'c'}}"); + SearchExpressionValidator.init("(a OR b) AND NOT c") + .validate("{{'a' OR 'b'} AND {NOT 'c'}}"); + } + + @Ignore + @Test + public void sebuilder() { + System.out.println(with("c", or("a", and("b"))).toString()); + System.out.println(with("a", and("b", and("c"))).toString()); + System.out.println(with("a").toString()); + System.out.println(with(not("a")).toString()); + System.out.println(with("a", and("b")).toString()); + System.out.println(with("a", or("b")).toString()); + System.out.println(with("a", and(not("b"))).toString()); + } + + private static SearchExpression with(String term) { + return new SearchTermImpl(term); + } + + private static SearchExpression with(String left, SearchExpression right) { + setLeftField(left, right); + return right; + } + + private static SearchUnary with(SearchUnary unary) { + return unary; + } + + private static SearchExpression or(String left, SearchExpression right) { + SearchExpression or = or(right); + setLeftField(left, right); + return or; + } + + private static SearchExpression and(String left, SearchExpression right) { + SearchExpression and = and(right); + setLeftField(left, right); + return and; + } + + private static SearchExpression or(SearchExpression right) { + return new SearchBinaryImpl(null, OR, right); + } + + private static SearchExpression and(SearchExpression right) { + return new SearchBinaryImpl(null, AND, right); + } + + private static SearchExpression and(String right) { + return and(new SearchTermImpl(right)); + } + + private static SearchExpression or(String right) { + return or(new SearchTermImpl(right)); + } + + private static SearchUnary not(String term) { + return new SearchUnaryImpl(new SearchTermImpl(term)); + } + + private static void setLeftField(String left, SearchExpression se) { + try { + Field field = null; + if (se instanceof SearchUnaryImpl) { + field = SearchBinaryImpl.class.getDeclaredField("operand"); + } else if (se instanceof SearchBinaryImpl) { + field = SearchBinaryImpl.class.getDeclaredField("left"); + } else { + Assert.fail("Unexpected exception: " + se.getClass()); + } + field.setAccessible(true); + field.set(se, new SearchTermImpl(left)); + } catch (Exception e) { + Assert.fail("Unexpected exception: " + e.getClass()); + } + } + + private static class SearchExpressionValidator { + private boolean log; + private final String searchQuery; + + private SearchExpressionValidator(String searchQuery) { + this.searchQuery = searchQuery; + } + + private static SearchExpressionValidator init(String searchQuery) { + return new SearchExpressionValidator(searchQuery); + } + + @SuppressWarnings("unused") + private SearchExpressionValidator enableLogging() { + log = true; + return this; + } + + private void validate(Class exception) throws SearchTokenizerException { + try { + new SearchTokenizer().tokenize(searchQuery); + } catch (Exception e) { + Assert.assertEquals(exception, e.getClass()); + return; + } + Assert.fail("Expected exception " + exception.getClass().getSimpleName() + " was not thrown."); + } + + private void validate(SearchExpression expectedSearchExpression) throws SearchTokenizerException, + SearchParserException { + final SearchExpression searchExpression = getSearchExpression(); + Assert.assertEquals(expectedSearchExpression.toString(), searchExpression.toString()); + } + + private void validate(String expectedSearchExpression) throws SearchTokenizerException, SearchParserException { + final SearchExpression searchExpression = getSearchExpression(); + Assert.assertEquals(expectedSearchExpression, searchExpression.toString()); + } + + private SearchExpression getSearchExpression() throws SearchParserException, SearchTokenizerException { + SearchParser tokenizer = new SearchParser(); + SearchOption result = tokenizer.parse(searchQuery); + Assert.assertNotNull(result); + final SearchExpression searchExpression = result.getSearchExpression(); + Assert.assertNotNull(searchExpression); + if (log) { + System.out.println(searchExpression); + } + return searchExpression; + } + } +} diff --git a/lib/server-core/src/test/java/org/apache/olingo/server/core/uri/parser/search/SearchParserTest.java b/lib/server-core/src/test/java/org/apache/olingo/server/core/uri/parser/search/SearchParserTest.java new file mode 100644 index 000000000..ee10e1a22 --- /dev/null +++ b/lib/server-core/src/test/java/org/apache/olingo/server/core/uri/parser/search/SearchParserTest.java @@ -0,0 +1,248 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.olingo.server.core.uri.parser.search; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.olingo.server.api.uri.queryoption.search.SearchBinaryOperatorKind; +import org.apache.olingo.server.api.uri.queryoption.search.SearchExpression; +import org.apache.olingo.server.core.uri.parser.search.SearchParserException.MessageKeys; +import org.apache.olingo.server.core.uri.parser.search.SearchQueryToken.Token; +import org.junit.Test; + +public class SearchParserTest extends SearchParser { + + @Test + public void simple() throws Exception { + SearchExpression se = run(Token.WORD); + assertEquals("'word1'", se.toString()); + assertTrue(se.isSearchTerm()); + assertEquals("word1", se.asSearchTerm().getSearchTerm()); + + se = run(Token.PHRASE); + assertEquals("'phrase1'", se.toString()); + assertTrue(se.isSearchTerm()); + // TODO: Check if quotation marks should be part of the string we deliver + assertEquals("phrase1", se.asSearchTerm().getSearchTerm()); + } + + @Test + public void simpleAnd() throws Exception { + SearchExpression se = run(Token.WORD, Token.AND, Token.WORD); + assertEquals("{'word1' AND 'word2'}", se.toString()); + assertTrue(se.isSearchBinary()); + assertEquals(SearchBinaryOperatorKind.AND, se.asSearchBinary().getOperator()); + assertEquals("word1", se.asSearchBinary().getLeftOperand().asSearchTerm().getSearchTerm()); + assertEquals("word2", se.asSearchBinary().getRightOperand().asSearchTerm().getSearchTerm()); + + se = run(Token.PHRASE, Token.AND, Token.PHRASE); + assertEquals("{'phrase1' AND 'phrase2'}", se.toString()); + assertTrue(se.isSearchBinary()); + assertEquals(SearchBinaryOperatorKind.AND, se.asSearchBinary().getOperator()); + assertEquals("phrase1", se.asSearchBinary().getLeftOperand().asSearchTerm().getSearchTerm()); + assertEquals("phrase2", se.asSearchBinary().getRightOperand().asSearchTerm().getSearchTerm()); + } + + @Test + public void simpleOr() throws Exception { + SearchExpression se = run(Token.WORD, Token.OR, Token.WORD); + assertEquals("{'word1' OR 'word2'}", se.toString()); + assertTrue(se.isSearchBinary()); + assertEquals(SearchBinaryOperatorKind.OR, se.asSearchBinary().getOperator()); + assertEquals("word1", se.asSearchBinary().getLeftOperand().asSearchTerm().getSearchTerm()); + assertEquals("word2", se.asSearchBinary().getRightOperand().asSearchTerm().getSearchTerm()); + + se = run(Token.PHRASE, Token.OR, Token.PHRASE); + assertEquals("{'phrase1' OR 'phrase2'}", se.toString()); + assertTrue(se.isSearchBinary()); + assertEquals(SearchBinaryOperatorKind.OR, se.asSearchBinary().getOperator()); + assertEquals("phrase1", se.asSearchBinary().getLeftOperand().asSearchTerm().getSearchTerm()); + assertEquals("phrase2", se.asSearchBinary().getRightOperand().asSearchTerm().getSearchTerm()); + } + + @Test + public void simpleImplicitAnd() throws Exception { + SearchExpression se = run(Token.WORD, Token.WORD); + assertEquals("{'word1' AND 'word2'}", se.toString()); + assertTrue(se.isSearchBinary()); + assertEquals(SearchBinaryOperatorKind.AND, se.asSearchBinary().getOperator()); + assertEquals("word1", se.asSearchBinary().getLeftOperand().asSearchTerm().getSearchTerm()); + assertEquals("word2", se.asSearchBinary().getRightOperand().asSearchTerm().getSearchTerm()); + + se = run(Token.PHRASE, Token.PHRASE); + assertEquals("{'phrase1' AND 'phrase2'}", se.toString()); + assertTrue(se.isSearchBinary()); + assertEquals(SearchBinaryOperatorKind.AND, se.asSearchBinary().getOperator()); + assertEquals("phrase1", se.asSearchBinary().getLeftOperand().asSearchTerm().getSearchTerm()); + assertEquals("phrase2", se.asSearchBinary().getRightOperand().asSearchTerm().getSearchTerm()); + } + + @Test + public void simpleBrackets() throws Exception { + SearchExpression se = run(Token.OPEN, Token.WORD, Token.CLOSE); + assertEquals("'word1'", se.toString()); + assertTrue(se.isSearchTerm()); + assertEquals("word1", se.asSearchTerm().getSearchTerm()); + + se = run(Token.OPEN, Token.PHRASE, Token.CLOSE); + assertEquals("'phrase1'", se.toString()); + assertTrue(se.isSearchTerm()); + assertEquals("phrase1", se.asSearchTerm().getSearchTerm()); + } + + @Test + public void simpleNot() throws Exception { + SearchExpression se = run(Token.NOT, Token.WORD); + assertEquals("{NOT 'word1'}", se.toString()); + assertTrue(se.isSearchUnary()); + assertEquals("word1", se.asSearchUnary().getOperand().asSearchTerm().getSearchTerm()); + + se = run(Token.NOT, Token.PHRASE); + assertEquals("{NOT 'phrase1'}", se.toString()); + assertTrue(se.isSearchUnary()); + assertEquals("phrase1", se.asSearchUnary().getOperand().asSearchTerm().getSearchTerm()); + } + + @Test + public void precedenceLast() throws Exception { + // word1 AND (word2 AND word3) + SearchExpression se = run(Token.WORD, Token.AND, Token.OPEN, Token.WORD, Token.AND, Token.WORD, Token.CLOSE); + assertEquals("{'word1' AND {'word2' AND 'word3'}}", se.toString()); + } + + @Test + public void precedenceFirst() throws Exception { + // (word1 AND word2) AND word3 + SearchExpression se = run(Token.OPEN, Token.WORD, Token.AND, Token.WORD, Token.CLOSE, Token.AND, Token.WORD); + assertEquals("{{'word1' AND 'word2'} AND 'word3'}", se.toString()); + } + + @Test + public void combinationAndOr() throws Exception { + // word1 AND word2 OR word3 + SearchExpression se = run(Token.WORD, Token.AND, Token.WORD, Token.OR, Token.WORD); + assertEquals("{{'word1' AND 'word2'} OR 'word3'}", se.toString()); + // word1 OR word2 AND word3 + se = run(Token.WORD, Token.OR, Token.WORD, Token.AND, Token.WORD); + assertEquals("{'word1' OR {'word2' AND 'word3'}}", se.toString()); + } + + @Test + public void unnecessaryBrackets() throws Exception { + // (word1) (word2) + SearchExpression se = run(Token.OPEN, Token.WORD, Token.CLOSE, Token.OPEN, Token.WORD, Token.CLOSE); + assertEquals("{'word1' AND 'word2'}", se.toString()); + } + + @Test + public void complex() throws Exception { + // ((word1 word2) word3) OR word4 + SearchExpression se = + run(Token.OPEN, Token.OPEN, Token.WORD, Token.WORD, Token.CLOSE, Token.WORD, Token.CLOSE, Token.OR, Token.WORD); + assertEquals("{{{'word1' AND 'word2'} AND 'word3'} OR 'word4'}", se.toString()); + } + + @Test + public void doubleNot() throws Exception { + SearchExpression se = run(Token.NOT, Token.WORD, Token.AND, Token.NOT, Token.PHRASE); + assertEquals("{{NOT 'word1'} AND {NOT 'phrase1'}}", se.toString()); + } + + @Test + public void notAnd() throws Exception { + runEx(SearchParserException.MessageKeys.INVALID_NOT_OPERAND, Token.NOT, Token.AND); + } + + + @Test + public void notNotWord() throws Exception { + runEx(SearchParserException.MessageKeys.INVALID_NOT_OPERAND, Token.NOT, Token.NOT, Token.WORD); + } + + @Test + public void doubleAnd() throws Exception { + runEx(SearchParserException.MessageKeys.INVALID_OPERATOR_AFTER_AND, Token.WORD, Token.AND, Token.AND, Token.WORD); + } + + @Test + public void singleAnd() { + runEx(SearchParserException.MessageKeys.INVALID_BINARY_OPERATOR_POSITION, Token.AND); + } + + @Test + public void singleOpenBracket() { + runEx(SearchParserException.MessageKeys.EXPECTED_DIFFERENT_TOKEN, Token.OPEN); + } + + @Test + public void emptyBrackets() { + runEx(SearchParserException.MessageKeys.NO_EXPRESSION_FOUND, Token.OPEN, Token.CLOSE); + } + + @Test + public void empty() { + Token[] emptyArray = new Token[0]; + runEx(SearchParserException.MessageKeys.NO_EXPRESSION_FOUND, emptyArray); + } + + private void runEx(MessageKeys key, Token... tokenArray) { + try { + run(tokenArray); + fail("Expected UriParserSyntaxException with key " + key); + } catch (SearchParserException e) { + assertEquals(key, e.getMessageKey()); + } + } + + private SearchExpression run(SearchQueryToken.Token... tokenArray) throws SearchParserException { + List tokenList = prepareTokens(tokenArray); + SearchExpression se = parse(tokenList); + assertNotNull(se); + return se; + } + + public List prepareTokens(SearchQueryToken.Token... tokenArray) { + ArrayList tokenList = new ArrayList(); + int wordNumber = 1; + int phraseNumber = 1; + for (Token aTokenArray : tokenArray) { + SearchQueryToken token = mock(SearchQueryToken.class); + when(token.getToken()).thenReturn(aTokenArray); + if (aTokenArray == Token.WORD) { + when(token.getLiteral()).thenReturn("word" + wordNumber); + wordNumber++; + } else if (aTokenArray == Token.PHRASE) { + when(token.getLiteral()).thenReturn("\"phrase" + phraseNumber + "\""); + phraseNumber++; + } + when(token.toString()).thenReturn("" + aTokenArray); + tokenList.add(token); + } + return tokenList; + } + +} \ No newline at end of file diff --git a/lib/server-core/src/test/java/org/apache/olingo/server/core/uri/parser/search/SearchTokenizerTest.java b/lib/server-core/src/test/java/org/apache/olingo/server/core/uri/parser/search/SearchTokenizerTest.java new file mode 100644 index 000000000..8408e93e1 --- /dev/null +++ b/lib/server-core/src/test/java/org/apache/olingo/server/core/uri/parser/search/SearchTokenizerTest.java @@ -0,0 +1,500 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.olingo.server.core.uri.parser.search; + +import org.junit.Assert; +import org.junit.Ignore; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import static org.apache.olingo.server.core.uri.parser.search.SearchQueryToken.Token.*; + +public class SearchTokenizerTest { + + @Test + public void parseBasics() throws Exception { + SearchTokenizer tokenizer = new SearchTokenizer(); + List result; + + // + result = tokenizer.tokenize("abc"); + Assert.assertNotNull(result); + + Assert.assertEquals(WORD, result.get(0).getToken()); + + result = tokenizer.tokenize("NOT abc"); + Assert.assertNotNull(result); + + Assert.assertEquals(NOT, result.get(0).getToken()); + Assert.assertEquals(WORD, result.get(1).getToken()); + + result = tokenizer.tokenize("(abc)"); + Assert.assertNotNull(result); + + Assert.assertEquals(OPEN, result.get(0).getToken()); + Assert.assertEquals(WORD, result.get(1).getToken()); + Assert.assertEquals(CLOSE, result.get(2).getToken()); + + result = tokenizer.tokenize("((abc))"); + Assert.assertNotNull(result); + + Assert.assertEquals(OPEN, result.get(0).getToken()); + Assert.assertEquals(WORD, result.get(2).getToken()); + Assert.assertEquals(CLOSE, result.get(4).getToken()); + } + + @Test + public void parseWords() throws Exception { + SearchTokenizer tokenizer = new SearchTokenizer(); + List result; + + // + result = tokenizer.tokenize("abc"); + Assert.assertNotNull(result); + + Assert.assertEquals(WORD, result.get(0).getToken()); + + // + result = tokenizer.tokenize("anotherWord\u1234"); + Assert.assertNotNull(result); + + Assert.assertEquals(WORD, result.get(0).getToken()); + } + + @Test + public void parsePhrase() throws Exception { + SearchTokenizer tokenizer = new SearchTokenizer(); + List result; + + SearchValidator.init("abc AND \"x-y_z\" AND olingo").validate(); + + // + result = tokenizer.tokenize("\"abc\""); + Assert.assertNotNull(result); + + Assert.assertEquals(PHRASE, result.get(0).getToken()); + + // + result = tokenizer.tokenize("\"9988 abs\""); + Assert.assertNotNull(result); + + Assert.assertEquals(PHRASE, result.get(0).getToken()); + Assert.assertEquals("\"9988 abs\"", result.get(0).getLiteral()); + + // + result = tokenizer.tokenize("\"99_88.\""); + Assert.assertNotNull(result); + + Assert.assertEquals(PHRASE, result.get(0).getToken()); + Assert.assertEquals("\"99_88.\"", result.get(0).getLiteral()); + + SearchValidator.init("abc or \"xyz\"").addExpected(WORD, WORD, PHRASE).validate(); + } + + /** + * https://tools.oasis-open.org/version-control/browse/wsvn/odata/trunk/spec/ABNF/odata-abnf-testcases.xml + * @throws Exception + */ + @Test + @Ignore("Test must be moved to SearchParserTest and SearchParserAndTokenizerTest") + public void parsePhraseAbnfTestcases() throws Exception { + // + SearchValidator.init("\"blue%20green\"").validate(); + // + SearchValidator.init("\"blue%20green%22").validate(); + // + // $search="blue\"green" + SearchValidator.init("\"blue\\\"green\"").validate(); + + // + // $search="blue\\green" + SearchValidator.init("\"blue\\\\green\"").validate(); + + // + SearchValidator.init("\"blue\"green\"").validate(); + + // + SearchValidator.init("\"blue%22green\"").validate(); + +// +// $search=blue green +// SearchValidator.init("\"blue%20green\"").validate(); + // +// SearchValidator.init("blue%20green").validate(); + } + + + @Test + public void parseNot() throws Exception { + SearchTokenizer tokenizer = new SearchTokenizer(); + List result; + + result = tokenizer.tokenize("NOT abc"); + Assert.assertNotNull(result); + + Assert.assertEquals(NOT, result.get(0).getToken()); + Assert.assertEquals(WORD, result.get(1).getToken()); + + SearchValidator.init("not abc").addExpected(WORD, WORD).validate(); + SearchValidator.init("NOT abc").addExpected(NOT, WORD).validate(); + SearchValidator.init("NOT \"abc\"").addExpected(NOT, PHRASE).validate(); + SearchValidator.init("NOT (sdf)").validate(SearchTokenizerException.class); + } + + @Test + public void parseOr() throws Exception { + SearchTokenizer tokenizer = new SearchTokenizer(); + List result; + + result = tokenizer.tokenize("abc OR xyz"); + Assert.assertNotNull(result); + + Assert.assertEquals(WORD, result.get(0).getToken()); + Assert.assertEquals(OR, result.get(1).getToken()); + Assert.assertEquals(WORD, result.get(2).getToken()); + + result = tokenizer.tokenize("abc OR xyz OR olingo"); + Assert.assertNotNull(result); + + Assert.assertEquals(WORD, result.get(0).getToken()); + Assert.assertEquals(OR, result.get(1).getToken()); + Assert.assertEquals(WORD, result.get(2).getToken()); + Assert.assertEquals(OR, result.get(3).getToken()); + Assert.assertEquals(WORD, result.get(4).getToken()); + + SearchValidator.init("abc or xyz").addExpected(WORD, WORD, WORD).validate(); + } + + @Test + public void parseImplicitAnd() throws SearchTokenizerException { + SearchValidator.init("a b").addExpected(WORD, WORD).validate(); + SearchValidator.init("a b OR c").addExpected(WORD, WORD, OR, WORD).validate(); + SearchValidator.init("a bc OR c").addExpected(WORD, WORD, OR, WORD).validate(); + SearchValidator.init("a bc c").addExpected(WORD, WORD, WORD).validate(); + SearchValidator.init("(a OR x) bc c").addExpected(OPEN, WORD, OR, WORD, CLOSE, WORD, WORD).validate(); + } + + @Test + public void parseAnd() throws Exception { + SearchTokenizer tokenizer = new SearchTokenizer(); + List result; + + result = tokenizer.tokenize("abc AND xyz"); + Assert.assertNotNull(result); + + Assert.assertEquals(WORD, result.get(0).getToken()); + Assert.assertEquals(AND, result.get(1).getToken()); + Assert.assertEquals(WORD, result.get(2).getToken()); + + // no lower case allowed for AND + result = tokenizer.tokenize("abc and xyz"); + Assert.assertNotNull(result); + Assert.assertEquals(3, result.size()); + + Assert.assertEquals(WORD, result.get(0).getToken()); + Assert.assertEquals(WORD, result.get(1).getToken()); + Assert.assertEquals(WORD, result.get(2).getToken()); + + // implicit AND + result = tokenizer.tokenize("abc xyz"); + Assert.assertNotNull(result); + + Assert.assertEquals(WORD, result.get(0).getToken()); + Assert.assertEquals(WORD, result.get(1).getToken()); + + result = tokenizer.tokenize("abc AND xyz AND olingo"); + Assert.assertNotNull(result); + + Assert.assertEquals(WORD, result.get(0).getToken()); + Assert.assertEquals(AND, result.get(1).getToken()); + Assert.assertEquals(WORD, result.get(2).getToken()); + Assert.assertEquals(AND, result.get(3).getToken()); + Assert.assertEquals(WORD, result.get(4).getToken()); + + result = tokenizer.tokenize("abc AND \"x-y_z\" AND olingo"); + Assert.assertNotNull(result); + + Assert.assertEquals(WORD, result.get(0).getToken()); + Assert.assertEquals(AND, result.get(1).getToken()); + Assert.assertEquals(PHRASE, result.get(2).getToken()); + Assert.assertEquals("\"x-y_z\"", result.get(2).getLiteral()); + Assert.assertEquals(AND, result.get(3).getToken()); + Assert.assertEquals(WORD, result.get(4).getToken()); + } + + @Test + public void parseAndOr() throws Exception { + SearchTokenizer tokenizer = new SearchTokenizer(); + List result; + + result = tokenizer.tokenize("abc AND xyz OR olingo"); + Assert.assertNotNull(result); + + Assert.assertEquals(WORD, result.get(0).getToken()); + Assert.assertEquals(AND, result.get(1).getToken()); + Assert.assertEquals(WORD, result.get(2).getToken()); + Assert.assertEquals(OR, result.get(3).getToken()); + Assert.assertEquals(WORD, result.get(4).getToken()); + + SearchValidator.init("abc AND ANDsomething") + .addExpected(WORD, AND, WORD).validate(); + } + + + @Test + public void parseCombinations() throws Exception { + SearchTokenizer tokenizer = new SearchTokenizer(); + List result; + + result = tokenizer.tokenize("abc AND NOT xyz OR olingo"); + Assert.assertNotNull(result); + + Iterator it = result.iterator(); + Assert.assertEquals(WORD, it.next().getToken()); + Assert.assertEquals(AND, it.next().getToken()); + Assert.assertEquals(NOT, it.next().getToken()); + Assert.assertEquals(WORD, it.next().getToken()); + Assert.assertEquals(OR, it.next().getToken()); + Assert.assertEquals(WORD, it.next().getToken()); + + SearchValidator.init("foo AND bar OR foo AND baz OR that AND bar OR that AND baz") + .addExpected(WORD, "foo").addExpected(AND) + .addExpected(WORD, "bar").addExpected(OR) + .addExpected(WORD, "foo").addExpected(AND) + .addExpected(WORD, "baz").addExpected(OR) + .addExpected(WORD, "that").addExpected(AND) + .addExpected(WORD, "bar").addExpected(OR) + .addExpected(WORD, "that").addExpected(AND) + .addExpected(WORD, "baz") + .validate(); + + + SearchValidator.init("(foo OR that) AND (bar OR baz)") + .addExpected(OPEN) + .addExpected(WORD, "foo").addExpected(OR).addExpected(WORD, "that") + .addExpected(CLOSE).addExpected(AND).addExpected(OPEN) + .addExpected(WORD, "bar").addExpected(OR).addExpected(WORD, "baz") + .addExpected(CLOSE) + .validate(); + } + + + @Test + public void parseSpecial() throws Exception { + SearchTokenizer tokenizer = new SearchTokenizer(); + List result; + Iterator it; + + result = tokenizer.tokenize("NOT abc AND nothing"); + + it = result.iterator(); + Assert.assertEquals(NOT, it.next().getToken()); + Assert.assertEquals(WORD, it.next().getToken()); + Assert.assertEquals(AND, it.next().getToken()); + Assert.assertEquals(WORD, it.next().getToken()); + + result = tokenizer.tokenize("abc AND andsomething"); + + it = result.iterator(); + Assert.assertEquals(WORD, it.next().getToken()); + Assert.assertEquals(AND, it.next().getToken()); + Assert.assertEquals(WORD, it.next().getToken()); + + SearchValidator.init("abc AND ANDsomething") + .addExpected(WORD, AND, WORD).validate(); + + SearchValidator.init("abc ANDsomething") + .addExpected(WORD, WORD).validate(); + + SearchValidator.init("abc ORsomething") + .addExpected(WORD, WORD).validate(); + + SearchValidator.init("abc OR orsomething") + .addExpected(WORD, OR, WORD).validate(); + + SearchValidator.init("abc OR ORsomething") + .addExpected(WORD, OR, WORD).validate(); + } + + @Test + public void unicodeInWords() throws Exception { + // Ll, Lm, Lo, Lt, Lu, Nl + SearchValidator.init("abc OR Ll\u01E3Lm\u02B5Lo\u1BE4Lt\u01F2Lu\u03D3Nl\u216F") + .addExpected(WORD, OR, WORD).validate(); + } + + /** + * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" + * other-delims = "!" / "(" / ")" / "*" / "+" / "," / ";" + * qchar-unescaped = unreserved / pct-encoded-unescaped / other-delims / ":" / "@" / "/" / "?" / "$" / "'" / "=" + * pct-encoded-unescaped = "%" ( "0" / "1" / "3" / "4" / "6" / "7" / "8" / "9" / A-to-F ) HEXDIG + * / "%" "2" ( "0" / "1" / "3" / "4" / "5" / "6" / "7" / "8" / "9" / A-to-F ) + * / "%" "5" ( DIGIT / "A" / "B" / "D" / "E" / "F" ) + * + * qchar-no-AMP-DQUOTE = qchar-unescaped / escape ( escape / quotation-mark ) + * + * escape = "\" / "%5C" ; reverse solidus U+005C + * quotation-mark = DQUOTE / "%22" + * ALPHA = %x41-5A / %x61-7A + * DIGIT = %x30-39 + * DQUOTE = %x22 + * + * @throws Exception + */ + @Test + public void characterInPhrase() throws Exception { + SearchValidator.init("\"123\" OR \"ALPHA-._~\"") + .addExpected(PHRASE, OR, PHRASE).validate(); + } + + @Test + public void moreMixedTests() throws SearchTokenizerException { + validate("abc"); + validate("NOT abc"); + + validate("abc AND def"); + validate("abc OR def"); + validate("abc def", WORD, WORD); + + validate("abc AND def AND ghi", WORD, AND, WORD, AND, WORD); + validate("abc AND def OR ghi"); + validate("abc AND def ghi"); + + validate("abc OR def AND ghi", WORD, OR, WORD, AND, WORD); + validate("abc OR def OR ghi", WORD, OR, WORD, OR, WORD); + validate("abc OR def ghi", WORD, OR, WORD, WORD); + + validate("abc def AND ghi"); + validate("abc def OR ghi"); + validate("abc def ghi"); + + // mixed not + SearchValidator.init(" abc def AND ghi").validate(WORD, WORD, AND, WORD); + validate("NOT abc NOT def OR NOT ghi", NOT, WORD, NOT, WORD, OR, NOT, WORD); + validate(" abc def NOT ghi", WORD, WORD, NOT, WORD); + + // parenthesis + validate("(abc)", OPEN, WORD, CLOSE); + validate("(abc AND def)", OPEN, WORD, AND, WORD, CLOSE); + validate("(abc AND def) OR ghi", OPEN, WORD, AND, WORD, CLOSE, OR, WORD); + validate("(abc AND def) ghi", OPEN, WORD, AND, WORD, CLOSE, WORD); + validate("abc AND (def OR ghi)", WORD, AND, OPEN, WORD, OR, WORD, CLOSE); + validate("abc AND (def ghi)", WORD, AND, OPEN, WORD, WORD, CLOSE); + } + + @Test + public void parseInvalid() throws SearchTokenizerException { + SearchValidator.init("abc AND OR something").validate(); + SearchValidator.init("abc AND \"something\" )").validate(); + // + SearchValidator.init("( abc AND) OR something").validate(SearchTokenizerException.class); + } + + public void validate(String query) throws SearchTokenizerException { + new SearchValidator(query).validate(); + } + + public void validate(String query, SearchQueryToken.Token ... tokens) throws SearchTokenizerException { + SearchValidator sv = new SearchValidator(query); + for (SearchQueryToken.Token token : tokens) { + sv.addExpected(token); + } + sv.validate(); + } + + private static class SearchValidator { + private List validations = new ArrayList(); + private boolean log; + private final String searchQuery; + + public void validate(SearchQueryToken.Token... tokens) throws SearchTokenizerException { + addExpected(tokens); + validate(); + } + + private class Tuple { + final SearchQueryToken.Token token; + final String literal; + public Tuple(SearchQueryToken.Token token, String literal) { + this.token = token; + this.literal = literal; + } + public Tuple(SearchQueryToken.Token token) { + this(token, null); + } + } + + private SearchValidator(String searchQuery) { + this.searchQuery = searchQuery; + } + + private static SearchValidator init(String searchQuery) { + return new SearchValidator(searchQuery); + } + + @SuppressWarnings("unused") + private SearchValidator enableLogging() { + log = true; + return this; + } + private SearchValidator addExpected(SearchQueryToken.Token token, String literal) { + validations.add(new Tuple(token, literal)); + return this; + } + private SearchValidator addExpected(SearchQueryToken.Token ... token) { + for (SearchQueryToken.Token t : token) { + validations.add(new Tuple(t)); + } + return this; + } + private void validate(Class exception) throws SearchTokenizerException { + try { + new SearchTokenizer().tokenize(searchQuery); + } catch (Exception e) { + Assert.assertEquals(exception, e.getClass()); + return; + } + Assert.fail("Expected exception " + exception.getClass().getSimpleName() + " was not thrown."); + } + + private void validate() throws SearchTokenizerException { + SearchTokenizer tokenizer = new SearchTokenizer(); + List result = tokenizer.tokenize(searchQuery); + Assert.assertNotNull(result); + if(log) { + System.out.println(result); + } + if(validations.size() != 0) { + Assert.assertEquals(validations.size(), result.size()); + + Iterator validationIt = validations.iterator(); + for (SearchQueryToken iToken : result) { + Tuple validation = validationIt.next(); + Assert.assertEquals(validation.token, iToken.getToken()); + if(validation.literal != null) { + Assert.assertEquals(validation.literal, iToken.getLiteral()); + } + } + } + } + } +} \ No newline at end of file diff --git a/lib/server-tecsvc/src/main/java/org/apache/olingo/server/tecsvc/processor/TechnicalEntityProcessor.java b/lib/server-tecsvc/src/main/java/org/apache/olingo/server/tecsvc/processor/TechnicalEntityProcessor.java index 74cdd51f1..923bf293e 100644 --- a/lib/server-tecsvc/src/main/java/org/apache/olingo/server/tecsvc/processor/TechnicalEntityProcessor.java +++ b/lib/server-tecsvc/src/main/java/org/apache/olingo/server/tecsvc/processor/TechnicalEntityProcessor.java @@ -67,6 +67,7 @@ import org.apache.olingo.server.tecsvc.processor.queryoptions.ExpandSystemQueryO import org.apache.olingo.server.tecsvc.processor.queryoptions.options.CountHandler; import org.apache.olingo.server.tecsvc.processor.queryoptions.options.FilterHandler; import org.apache.olingo.server.tecsvc.processor.queryoptions.options.OrderByHandler; +import org.apache.olingo.server.tecsvc.processor.queryoptions.options.SearchHandler; import org.apache.olingo.server.tecsvc.processor.queryoptions.options.ServerSidePagingHandler; import org.apache.olingo.server.tecsvc.processor.queryoptions.options.SkipHandler; import org.apache.olingo.server.tecsvc.processor.queryoptions.options.TopHandler; @@ -486,6 +487,7 @@ public class TechnicalEntityProcessor extends TechnicalProcessor OrderByHandler.applyOrderByOption(uriInfo.getOrderByOption(), entitySet, uriInfo, serviceMetadata.getEdm()); SkipHandler.applySkipSystemQueryHandler(uriInfo.getSkipOption(), entitySet); TopHandler.applyTopSystemQueryOption(uriInfo.getTopOption(), entitySet); + SearchHandler.applySearchSystemQueryOption(uriInfo.getSearchOption(), entitySet); final Integer pageSize = odata.createPreferences(request.getHeaders(HttpHeader.PREFER)).getMaxPageSize(); final Integer serverPageSize = ServerSidePagingHandler.applyServerSidePaging(uriInfo.getSkipTokenOption(), diff --git a/lib/server-tecsvc/src/main/java/org/apache/olingo/server/tecsvc/processor/TechnicalProcessor.java b/lib/server-tecsvc/src/main/java/org/apache/olingo/server/tecsvc/processor/TechnicalProcessor.java index b63bdd899..28cf63def 100644 --- a/lib/server-tecsvc/src/main/java/org/apache/olingo/server/tecsvc/processor/TechnicalProcessor.java +++ b/lib/server-tecsvc/src/main/java/org/apache/olingo/server/tecsvc/processor/TechnicalProcessor.java @@ -233,8 +233,7 @@ public abstract class TechnicalProcessor implements Processor { } protected void validateOptions(final UriInfoResource uriInfo) throws ODataApplicationException { - if (uriInfo.getIdOption() != null - || uriInfo.getSearchOption() != null) { + if (uriInfo.getIdOption() != null) { throw new ODataApplicationException("Not all of the specified options are supported.", HttpStatusCode.NOT_IMPLEMENTED.getStatusCode(), Locale.ROOT); } diff --git a/lib/server-tecsvc/src/main/java/org/apache/olingo/server/tecsvc/processor/queryoptions/options/SearchHandler.java b/lib/server-tecsvc/src/main/java/org/apache/olingo/server/tecsvc/processor/queryoptions/options/SearchHandler.java new file mode 100644 index 000000000..e56e08308 --- /dev/null +++ b/lib/server-tecsvc/src/main/java/org/apache/olingo/server/tecsvc/processor/queryoptions/options/SearchHandler.java @@ -0,0 +1,93 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.olingo.server.tecsvc.processor.queryoptions.options; + +import org.apache.olingo.commons.api.data.Entity; +import org.apache.olingo.commons.api.data.EntityCollection; +import org.apache.olingo.commons.api.data.Property; +import org.apache.olingo.commons.api.http.HttpStatusCode; +import org.apache.olingo.server.api.ODataApplicationException; +import org.apache.olingo.server.api.uri.queryoption.SearchOption; +import org.apache.olingo.server.api.uri.queryoption.search.SearchBinary; +import org.apache.olingo.server.api.uri.queryoption.search.SearchBinaryOperatorKind; +import org.apache.olingo.server.api.uri.queryoption.search.SearchExpression; +import org.apache.olingo.server.api.uri.queryoption.search.SearchTerm; + +import java.util.Iterator; +import java.util.List; +import java.util.Locale; + +public class SearchHandler { + public static void applySearchSystemQueryOption(final SearchOption searchOption, final EntityCollection entitySet) + throws ODataApplicationException { + + if (searchOption != null) { + Iterator it = entitySet.getEntities().iterator(); + while(it.hasNext()) { + boolean keep = false; + Entity next = it.next(); + List propertyList = next.getProperties(); + for (Property property : propertyList) { + SearchExpression se = searchOption.getSearchExpression(); + if(isTrue(se, property)) { + keep = true; + break; + } + } + if(!keep) { + it.remove(); + } + } + } + } + + private static boolean isTrue(SearchTerm term, Property property) { + if(property.isPrimitive() && !property.isNull()) { + // TODO: mibo(151117): pass EDM information to do correct 'string' convertation + String propertyString = property.asPrimitive().toString(); + return propertyString != null && propertyString.contains(term.getSearchTerm()); + } + return false; + } + + private static boolean isTrue(SearchBinary binary, Property property) throws ODataApplicationException { + SearchExpression left = binary.getLeftOperand(); + SearchExpression right = binary.getRightOperand(); + if(binary.getOperator() == SearchBinaryOperatorKind.AND) { + return isTrue(left, property) && isTrue(right, property); + } else if(binary.getOperator() == SearchBinaryOperatorKind.OR) { + return isTrue(left, property) || isTrue(right, property); + } else { + throw new ODataApplicationException("Found unknown SearchBinaryOperatorKind: " + binary.getOperator(), + HttpStatusCode.INTERNAL_SERVER_ERROR.getStatusCode(), Locale.ROOT); + } + } + + private static boolean isTrue(SearchExpression searchExpression, Property property) throws ODataApplicationException { + if(searchExpression.isSearchBinary()) { + return isTrue(searchExpression.asSearchBinary(), property); + } else if(searchExpression.isSearchTerm()) { + return isTrue(searchExpression.asSearchTerm(), property); + } else if(searchExpression.isSearchUnary()) { + return !isTrue(searchExpression.asSearchUnary().getOperand(), property); + } + throw new ODataApplicationException("Found unknown SearchExpression: " + searchExpression, + HttpStatusCode.INTERNAL_SERVER_ERROR.getStatusCode(), Locale.ROOT); + } +} diff --git a/lib/server-test/src/test/java/org/apache/olingo/server/core/uri/antlr/TestFullResourcePath.java b/lib/server-test/src/test/java/org/apache/olingo/server/core/uri/antlr/TestFullResourcePath.java index e0f573876..9f66d66ed 100644 --- a/lib/server-test/src/test/java/org/apache/olingo/server/core/uri/antlr/TestFullResourcePath.java +++ b/lib/server-test/src/test/java/org/apache/olingo/server/core/uri/antlr/TestFullResourcePath.java @@ -1193,9 +1193,10 @@ public class TestFullResourcePath { .isExValidation(UriValidationException.MessageKeys.SYSTEM_QUERY_OPTION_NOT_ALLOWED); // $search is currently not implemented. Please change this exception if the implementation is done. - testUri.runEx("FICRTCollETMixPrimCollCompTwoParam(ParameterInt16=1,ParameterString='1')", "$search=test") - .isExSemantic(MessageKeys.NOT_IMPLEMENTED); - + // FIXME (151106:mibo): check after finish of OLINGO-568 +// testUri.runEx("FICRTCollETMixPrimCollCompTwoParam(ParameterInt16=1,ParameterString='1')", "$search=test") +// .isExSemantic(MessageKeys.NOT_IMPLEMENTED); + testUri.run("ESAllPrim/olingo.odata.test1.BFNESAllPrimRTCTAllPrim()") .isKind(UriInfoKind.resource) .goPath().first() @@ -5455,12 +5456,12 @@ public class TestFullResourcePath { testUri.run("ESTwoKeyNav", "$search= abc def NOT ghi"); // parenthesis - testUri.run("ESTwoKeyNav", "$search= (abc)"); - testUri.run("ESTwoKeyNav", "$search= (abc AND def)"); - testUri.run("ESTwoKeyNav", "$search= (abc AND def) OR ghi "); - testUri.run("ESTwoKeyNav", "$search= (abc AND def) ghi "); - testUri.run("ESTwoKeyNav", "$search= abc AND (def OR ghi)"); - testUri.run("ESTwoKeyNav", "$search= abc AND (def ghi)"); + testUri.run("ESTwoKeyNav", "$search=(abc)"); + testUri.run("ESTwoKeyNav", "$search=(abc AND def)"); + testUri.run("ESTwoKeyNav", "$search=(abc AND def) OR ghi "); + testUri.run("ESTwoKeyNav", "$search=(abc AND def) ghi "); + testUri.run("ESTwoKeyNav", "$search=abc AND (def OR ghi)"); + testUri.run("ESTwoKeyNav", "$search=abc AND (def ghi)"); } @Test