[OLINGO-568] SearchParser negative tests
This commit is contained in:
parent
cef72e45ab
commit
8457c0f60a
|
@ -6,9 +6,9 @@
|
|||
* 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
|
||||
|
@ -21,6 +21,7 @@ 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.core.uri.parser.search.SearchQueryToken.Token;
|
||||
import org.apache.olingo.server.core.uri.queryoption.SearchOptionImpl;
|
||||
|
||||
import java.util.Iterator;
|
||||
|
@ -31,13 +32,11 @@ public class SearchParser {
|
|||
private Iterator<SearchQueryToken> tokens;
|
||||
private SearchQueryToken token;
|
||||
|
||||
public SearchOption parse(String path, String value) {
|
||||
public SearchOption parse(String path, String value) throws SearchParserException, SearchTokenizerException {
|
||||
SearchTokenizer tokenizer = new SearchTokenizer();
|
||||
SearchExpression searchExpression;
|
||||
try {
|
||||
tokens = tokenizer.tokenize(value).iterator();
|
||||
nextToken();
|
||||
searchExpression = processSearchExpression(null);
|
||||
searchExpression = parseInternal(tokenizer.tokenize(value));
|
||||
} catch (SearchTokenizerException e) {
|
||||
return null;
|
||||
}
|
||||
|
@ -46,32 +45,45 @@ public class SearchParser {
|
|||
return searchOption;
|
||||
}
|
||||
|
||||
protected SearchExpression parseInternal(List<SearchQueryToken> tokens) {
|
||||
protected SearchExpression parseInternal(List<SearchQueryToken> 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) {
|
||||
if(token == null) {
|
||||
private SearchExpression processSearchExpression(SearchExpression left) throws SearchParserException {
|
||||
if (token == null) {
|
||||
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)) {
|
||||
if (isToken(SearchQueryToken.Token.OPEN)) {
|
||||
processOpen();
|
||||
expression = processSearchExpression(left);
|
||||
validateToken(SearchQueryToken.Token.CLOSE);
|
||||
processClose();
|
||||
} else if(isTerm()) {
|
||||
} else if (isTerm()) {
|
||||
expression = processTerm();
|
||||
}
|
||||
|
||||
if(isToken(SearchQueryToken.Token.AND) || isTerm()) {
|
||||
expression = processAnd(expression);
|
||||
} else if(isToken(SearchQueryToken.Token.OR)) {
|
||||
expression = processOr(expression);
|
||||
} else if(isEof()) {
|
||||
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;
|
||||
|
@ -88,15 +100,17 @@ public class SearchParser {
|
|||
}
|
||||
|
||||
private boolean isToken(SearchQueryToken.Token toCheckToken) {
|
||||
if(token == null) {
|
||||
if (token == null) {
|
||||
return false;
|
||||
}
|
||||
return token.getToken() == toCheckToken;
|
||||
}
|
||||
|
||||
private void validateToken(SearchQueryToken.Token toValidateToken) {
|
||||
if(!isToken(toValidateToken)) {
|
||||
throw illegalState();
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -108,23 +122,27 @@ public class SearchParser {
|
|||
nextToken();
|
||||
}
|
||||
|
||||
private SearchExpression processAnd(SearchExpression left) {
|
||||
if(isToken(SearchQueryToken.Token.AND)) {
|
||||
private SearchExpression processAnd(SearchExpression left) throws SearchParserException {
|
||||
if (isToken(SearchQueryToken.Token.AND)) {
|
||||
nextToken();
|
||||
}
|
||||
SearchExpression se = left;
|
||||
if(isTerm()) {
|
||||
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) {
|
||||
if(isToken(SearchQueryToken.Token.OR)) {
|
||||
public SearchExpression processOr(SearchExpression left) throws SearchParserException {
|
||||
if (isToken(SearchQueryToken.Token.OR)) {
|
||||
nextToken();
|
||||
}
|
||||
SearchExpression se = processSearchExpression(left);
|
||||
|
@ -135,30 +153,31 @@ public class SearchParser {
|
|||
return new RuntimeException();
|
||||
}
|
||||
|
||||
private SearchExpression processNot() {
|
||||
private SearchExpression processNot() throws SearchParserException {
|
||||
nextToken();
|
||||
SearchExpression searchExpression = processTerm();
|
||||
if(searchExpression.isSearchTerm()) {
|
||||
if (isToken(Token.WORD) || isToken(Token.PHRASE)) {
|
||||
SearchExpression searchExpression = processTerm();
|
||||
return new SearchUnaryImpl(searchExpression.asSearchTerm());
|
||||
}
|
||||
throw illegalState();
|
||||
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();
|
||||
if (tokens.hasNext()) {
|
||||
token = tokens.next();
|
||||
} else {
|
||||
token = null;
|
||||
}
|
||||
}
|
||||
|
||||
private SearchExpression processTerm() {
|
||||
if(isToken(SearchQueryToken.Token.NOT)) {
|
||||
private SearchExpression processTerm() throws SearchParserException {
|
||||
if (isToken(SearchQueryToken.Token.NOT)) {
|
||||
return processNot();
|
||||
}
|
||||
if(isToken(SearchQueryToken.Token.PHRASE)) {
|
||||
if (isToken(SearchQueryToken.Token.PHRASE)) {
|
||||
return processPhrase();
|
||||
} else if(isToken(SearchQueryToken.Token.WORD)) {
|
||||
} else if (isToken(SearchQueryToken.Token.WORD)) {
|
||||
return processWord();
|
||||
}
|
||||
throw illegalState();
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -6,9 +6,9 @@
|
|||
* 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
|
||||
|
@ -24,18 +24,18 @@ import java.util.List;
|
|||
/**
|
||||
* <code>
|
||||
* searchExpr = ( OPEN BWS searchExpr BWS CLOSE
|
||||
* / searchTerm
|
||||
* ) [ searchOrExpr
|
||||
* / searchAndExpr
|
||||
* ]
|
||||
* / searchTerm
|
||||
* ) [ searchOrExpr
|
||||
* / searchAndExpr
|
||||
* ]
|
||||
*
|
||||
* searchOrExpr = RWS 'OR' RWS searchExpr
|
||||
* searchAndExpr = RWS [ 'AND' RWS ] searchExpr
|
||||
* 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
|
||||
* 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
|
||||
* </code>
|
||||
*/
|
||||
public class SearchTokenizer {
|
||||
|
@ -65,7 +65,8 @@ public class SearchTokenizer {
|
|||
}
|
||||
|
||||
public State forbidden(char c) throws SearchTokenizerException {
|
||||
throw new SearchTokenizerException("Forbidden character for " + this.getClass().getName() + "->" + c);
|
||||
throw new SearchTokenizerException("Forbidden character for " + this.getClass().getName() + "->" + c,
|
||||
SearchTokenizerException.MessageKeys.FORBIDDEN_CHARACTER, "" + c);
|
||||
}
|
||||
|
||||
public State finish() {
|
||||
|
@ -97,20 +98,20 @@ public class SearchTokenizer {
|
|||
}
|
||||
|
||||
/**
|
||||
* unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
|
||||
* other-delims = "!" / "(" / ")" / "*" / "+" / "," / ";"
|
||||
* 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" )
|
||||
* 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 )
|
||||
* qchar-no-AMP-DQUOTE = qchar-unescaped / escape ( escape / quotation-mark )
|
||||
*
|
||||
* escape = "\" / "%5C" ; reverse solidus U+005C
|
||||
* quotation-mark = DQUOTE / "%22"
|
||||
* escape = "\" / "%5C" ; reverse solidus U+005C
|
||||
* quotation-mark = DQUOTE / "%22"
|
||||
*
|
||||
* ALPHA = %x41-5A / %x61-7A
|
||||
* DIGIT = %x30-39
|
||||
* ALPHA = %x41-5A / %x61-7A
|
||||
* DIGIT = %x30-39
|
||||
* DQUOTE = %x22
|
||||
*
|
||||
* @param character which is checked
|
||||
|
@ -137,10 +138,10 @@ public class SearchTokenizer {
|
|||
|| '0' <= character && character <= '9'; // case 0..9
|
||||
}
|
||||
|
||||
//BWS = *( SP / HTAB / "%20" / "%09" ) ; "bad" whitespace
|
||||
//RWS = 1*( SP / HTAB / "%20" / "%09" ) ; "required" whitespace
|
||||
// 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" )
|
||||
// ( SP / HTAB / "%20" / "%09" )
|
||||
// TODO mibo: add missing whitespaces
|
||||
return character == ' ' || character == '\t';
|
||||
}
|
||||
|
@ -158,29 +159,35 @@ public class SearchTokenizer {
|
|||
|
||||
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.");
|
||||
if (isFinished()) {
|
||||
throw new SearchTokenizerException(toString() + " is already finished.",
|
||||
SearchTokenizerException.MessageKeys.ALREADY_FINISHED);
|
||||
}
|
||||
literal.append(c);
|
||||
return this;
|
||||
|
@ -191,13 +198,14 @@ public class SearchTokenizer {
|
|||
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) {
|
||||
} else if (c == CHAR_CLOSE) {
|
||||
return new CloseState();
|
||||
} else {
|
||||
return new SearchTermState().init(c);
|
||||
|
@ -214,9 +222,10 @@ public class SearchTokenizer {
|
|||
public SearchTermState() {
|
||||
super(Token.TERM);
|
||||
}
|
||||
|
||||
@Override
|
||||
public State nextChar(char c) throws SearchTokenizerException {
|
||||
if(c == CHAR_N) {
|
||||
if (c == CHAR_N) {
|
||||
return new NotState(c);
|
||||
} else if (c == QUOTATION_MARK) {
|
||||
return new SearchPhraseState(c);
|
||||
|
@ -225,6 +234,7 @@ public class SearchTokenizer {
|
|||
}
|
||||
return forbidden(c);
|
||||
}
|
||||
|
||||
@Override
|
||||
public State init(char c) throws SearchTokenizerException {
|
||||
return nextChar(c);
|
||||
|
@ -234,15 +244,16 @@ public class SearchTokenizer {
|
|||
private class SearchWordState extends LiteralState {
|
||||
public SearchWordState(char c) throws SearchTokenizerException {
|
||||
super(Token.WORD, c);
|
||||
if(!isAllowedWord(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)) {
|
||||
if (!isAllowedWord(aChar)) {
|
||||
forbidden(aChar);
|
||||
}
|
||||
}
|
||||
|
@ -271,7 +282,7 @@ public class SearchTokenizer {
|
|||
private class SearchPhraseState extends LiteralState {
|
||||
public SearchPhraseState(char c) throws SearchTokenizerException {
|
||||
super(Token.PHRASE, c);
|
||||
if(c != QUOTATION_MARK) {
|
||||
if (c != QUOTATION_MARK) {
|
||||
forbidden(c);
|
||||
}
|
||||
}
|
||||
|
@ -286,7 +297,7 @@ public class SearchTokenizer {
|
|||
finish();
|
||||
allowed(c);
|
||||
return new SearchExpressionState();
|
||||
} else if(isFinished()) {
|
||||
} else if (isFinished()) {
|
||||
return new SearchExpressionState().init(c);
|
||||
}
|
||||
return forbidden(c);
|
||||
|
@ -298,6 +309,7 @@ public class SearchTokenizer {
|
|||
super(Token.OPEN);
|
||||
finish();
|
||||
}
|
||||
|
||||
@Override
|
||||
public State nextChar(char c) throws SearchTokenizerException {
|
||||
finish();
|
||||
|
@ -323,56 +335,40 @@ public class SearchTokenizer {
|
|||
private class NotState extends LiteralState {
|
||||
public NotState(char c) throws SearchTokenizerException {
|
||||
super(Token.NOT, c);
|
||||
if(c != CHAR_N) {
|
||||
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)) {
|
||||
} 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) {
|
||||
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)) {
|
||||
} else if (literal.length() == 3 && isWhitespace(c)) {
|
||||
finish();
|
||||
return new BeforeSearchExpressionRwsState();
|
||||
} else {
|
||||
|
@ -381,12 +377,34 @@ public class SearchTokenizer {
|
|||
}
|
||||
}
|
||||
|
||||
// RWS 'OR' RWS searchExpr
|
||||
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)) {
|
||||
|
@ -401,11 +419,12 @@ public class SearchTokenizer {
|
|||
public BeforePhraseOrWordRwsState() {
|
||||
super(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public State nextChar(char c) throws SearchTokenizerException {
|
||||
if (isWhitespace(c)) {
|
||||
return allowed(c);
|
||||
} else if(c == '"') {
|
||||
} else if (c == '"') {
|
||||
return new SearchPhraseState(c);
|
||||
} else {
|
||||
return new SearchWordState(c);
|
||||
|
@ -417,6 +436,7 @@ public class SearchTokenizer {
|
|||
public RwsState() {
|
||||
super(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public State nextChar(char c) throws SearchTokenizerException {
|
||||
if (isWhitespace(c)) {
|
||||
|
@ -438,10 +458,10 @@ public class SearchTokenizer {
|
|||
* @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)
|
||||
* (based on OData search query ABNF)
|
||||
*/
|
||||
public List<SearchQueryToken> tokenize(final String searchQuery)
|
||||
throws SearchTokenizerException {
|
||||
throws SearchTokenizerException {
|
||||
|
||||
char[] chars = searchQuery.trim().toCharArray();
|
||||
|
||||
|
@ -455,7 +475,7 @@ public class SearchTokenizer {
|
|||
state = next;
|
||||
}
|
||||
|
||||
if(state.close().isFinished()) {
|
||||
if (state.close().isFinished()) {
|
||||
states.add(state);
|
||||
}
|
||||
|
||||
|
|
|
@ -18,11 +18,30 @@
|
|||
*/
|
||||
package org.apache.olingo.server.core.uri.parser.search;
|
||||
|
||||
public class SearchTokenizerException extends Exception {
|
||||
import org.apache.olingo.server.core.uri.parser.UriParserSyntaxException;
|
||||
|
||||
public class SearchTokenizerException extends UriParserSyntaxException {
|
||||
|
||||
private static final long serialVersionUID = -8295456415309640166L;
|
||||
|
||||
public SearchTokenizerException(String message) {
|
||||
super(message);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -33,7 +33,7 @@ import org.junit.Test;
|
|||
public class SearchParserAndTokenizerTest {
|
||||
|
||||
@Test
|
||||
public void basicParsing() throws SearchTokenizerException {
|
||||
public void basicParsing() throws Exception {
|
||||
SearchExpressionValidator.init("a")
|
||||
.validate(with("a"));
|
||||
SearchExpressionValidator.init("a AND b")
|
||||
|
@ -172,17 +172,18 @@ public class SearchParserAndTokenizerTest {
|
|||
Assert.fail("Expected exception " + exception.getClass().getSimpleName() + " was not thrown.");
|
||||
}
|
||||
|
||||
private void validate(SearchExpression expectedSearchExpression) throws SearchTokenizerException {
|
||||
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 {
|
||||
private void validate(String expectedSearchExpression) throws SearchTokenizerException, SearchParserException {
|
||||
final SearchExpression searchExpression = getSearchExpression();
|
||||
Assert.assertEquals(expectedSearchExpression, searchExpression.toString());
|
||||
}
|
||||
|
||||
private SearchExpression getSearchExpression() {
|
||||
private SearchExpression getSearchExpression() throws SearchParserException, SearchTokenizerException {
|
||||
SearchParser tokenizer = new SearchParser();
|
||||
SearchOption result = tokenizer.parse(null, searchQuery);
|
||||
Assert.assertNotNull(result);
|
||||
|
@ -195,5 +196,4 @@ public class SearchParserAndTokenizerTest {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ 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;
|
||||
|
||||
|
@ -29,35 +30,35 @@ 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.Ignore;
|
||||
import org.junit.Test;
|
||||
|
||||
public class SearchParserTest extends SearchParser {
|
||||
|
||||
@Test
|
||||
public void simple() {
|
||||
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
|
||||
// TODO: Check if quotation marks should be part of the string we deliver
|
||||
assertEquals("phrase1", se.asSearchTerm().getSearchTerm());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void simpleAnd() {
|
||||
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());
|
||||
|
@ -65,16 +66,16 @@ public class SearchParserTest extends SearchParser {
|
|||
assertEquals("phrase1", se.asSearchBinary().getLeftOperand().asSearchTerm().getSearchTerm());
|
||||
assertEquals("phrase2", se.asSearchBinary().getRightOperand().asSearchTerm().getSearchTerm());
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void simpleOr() {
|
||||
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());
|
||||
|
@ -82,16 +83,16 @@ public class SearchParserTest extends SearchParser {
|
|||
assertEquals("phrase1", se.asSearchBinary().getLeftOperand().asSearchTerm().getSearchTerm());
|
||||
assertEquals("phrase2", se.asSearchBinary().getRightOperand().asSearchTerm().getSearchTerm());
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void simpleImplicitAnd() {
|
||||
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());
|
||||
|
@ -99,59 +100,119 @@ public class SearchParserTest extends SearchParser {
|
|||
assertEquals("phrase1", se.asSearchBinary().getLeftOperand().asSearchTerm().getSearchTerm());
|
||||
assertEquals("phrase2", se.asSearchBinary().getRightOperand().asSearchTerm().getSearchTerm());
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void simpleBrackets() {
|
||||
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() {
|
||||
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() {
|
||||
//word1 AND (word2 AND word3)
|
||||
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() {
|
||||
//(word1 AND word2) AND word3
|
||||
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() {
|
||||
//word1 AND word2 OR word3
|
||||
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
|
||||
// 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());
|
||||
}
|
||||
|
||||
private SearchExpression run(SearchQueryToken.Token... tokenArray) {
|
||||
@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 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<SearchQueryToken> tokenList = prepareTokens(tokenArray);
|
||||
SearchExpression se = parseInternal(tokenList);
|
||||
assertNotNull(se);
|
||||
|
@ -172,6 +233,7 @@ public class SearchParserTest extends SearchParser {
|
|||
when(token.getLiteral()).thenReturn("phrase" + phraseNumber);
|
||||
phraseNumber++;
|
||||
}
|
||||
when(token.toString()).thenReturn("" + tokenArray[i]);
|
||||
tokenList.add(token);
|
||||
}
|
||||
return tokenList;
|
||||
|
|
Loading…
Reference in New Issue