From 8457c0f60a084f92f7a98c61039c9a5540980a3c Mon Sep 17 00:00:00 2001 From: Christian Amend Date: Fri, 13 Nov 2015 14:56:29 +0100 Subject: [PATCH] [OLINGO-568] SearchParser negative tests --- .../core/uri/parser/search/SearchParser.java | 89 ++++++----- .../parser/search/SearchParserException.java | 54 +++++++ .../uri/parser/search/SearchTokenizer.java | 144 ++++++++++-------- .../search/SearchTokenizerException.java | 25 ++- .../search/SearchParserAndTokenizerTest.java | 10 +- .../uri/parser/search/SearchParserTest.java | 118 ++++++++++---- 6 files changed, 307 insertions(+), 133 deletions(-) create mode 100644 lib/server-core/src/main/java/org/apache/olingo/server/core/uri/parser/search/SearchParserException.java 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 index d5109a5b4..2cd03c662 100644 --- 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 @@ -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 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 tokens) { + protected SearchExpression parseInternal(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) { - 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(); 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/SearchTokenizer.java b/lib/server-core/src/main/java/org/apache/olingo/server/core/uri/parser/search/SearchTokenizer.java index a9a589539..1e3b2efc3 100644 --- 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 @@ -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; /** * * 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 * */ 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 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); } 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 index 451632b4c..fb20efe37 100644 --- 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 @@ -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); } } 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 index 271b617b8..dd5ab70d1 100644 --- 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 @@ -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 { } } - } 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 index 89b59b407..0902e8a17 100644 --- 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 @@ -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 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;