com.sap.cds.adapter.odata.v2.search.SearchParser Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of cds-adapter-odata-v2 Show documentation
Show all versions of cds-adapter-odata-v2 Show documentation
OData V2 adapter for CDS Services Java
/**************************************************************************
* (C) 2019-2024 SAP SE or an SAP affiliate company. All rights reserved. *
**************************************************************************/
package com.sap.cds.adapter.odata.v2.search;
import static com.sap.cds.ql.CQL.and;
import static com.sap.cds.ql.CQL.or;
import java.util.Iterator;
import java.util.List;
import java.util.StringJoiner;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.base.CharMatcher;
import com.sap.cds.adapter.odata.v2.search.SearchQueryToken.Token;
import com.sap.cds.impl.builder.model.SearchTermPredicate;
import com.sap.cds.ql.CQL;
import com.sap.cds.ql.cqn.CqnPredicate;
import com.sap.cds.services.utils.CdsErrorStatuses;
import com.sap.cds.services.utils.ErrorStatusException;
/*
* Rewritten grammar
*
* SearchExpr ::= ExprOR
* ExprOR ::= ExprAnd ('OR' ExprAnd)*
* ExprAnd ::= Term ('AND'? Term)*
* Term ::= ('NOT')? (Word | Phrase)
* | '(' Expr ')'
*/
public class SearchParser {
private final static Logger logger = LoggerFactory.getLogger(SearchParser.class);
private Iterator tokens;
private SearchQueryToken token;
// related to https://github.tools.sap/cap/issues/issues/11607
//
// FE V4 encloses search terms with quotes, therefore they are treated as literals.
// FE V2, however, does not enclose search terms with quotes, which leads to the issues
// observed in https://github.tools.sap/cap/issues/issues/11607.
//
// In order to work around this, we enclose non-ascii characters with quotes manually,
// if they aren't already.
//
// Unfortunately we cannot do this for every search term, as logical operators, such
// as NOT, AND, OR would stop working for scenarios where OData v2 is used without
// a FE V2 UI (e.g. calling the OData V2 endpoints using search directly).
private String encloseNonAsciiTermsWithQuotes(String query) {
String[] terms = query.split(" ");
StringJoiner joiner = new StringJoiner(" ");
for (String term : terms) {
String res = term;
boolean isAscii = CharMatcher.ascii().matchesAllOf(term);
if (!isAscii && !term.startsWith("\"")) {
res = "\"" + term + "\"";
}
joiner.add(res);
}
return joiner.toString();
}
public CqnPredicate parse(final String searchQuery) {
SearchTokenizer tokenizer = new SearchTokenizer();
String query = encloseNonAsciiTermsWithQuotes(searchQuery);
return parse(tokenizer.tokenize(query));
}
protected CqnPredicate parse(final List tokens) {
this.tokens = tokens.iterator();
nextToken();
if (token == null) {
logger.error("No search String");
throw new ErrorStatusException(CdsErrorStatuses.SEARCH_PARSING_FAILED);
}
CqnPredicate searchExpression = processSearchExpression();
if (!isEof()) {
logger.error("Token left after end of search query parsing.");
throw new ErrorStatusException(CdsErrorStatuses.SEARCH_PARSING_FAILED);
}
return searchExpression;
}
private CqnPredicate processSearchExpression() {
return processExprOr();
}
private CqnPredicate processExprOr() {
CqnPredicate left = processExprAnd();
while (isToken(Token.OR)) {
nextToken(); // Match OR
final CqnPredicate right = processExprAnd();
left = or(left, right);
}
return left;
}
private CqnPredicate processExprAnd() {
CqnPredicate left = processTerm();
while (isToken(Token.AND) || isTerm()) {
if (isToken(Token.AND)) {
nextToken(); // Match AND
}
final CqnPredicate right = processTerm();
left = and(left, right);
}
return left;
}
private CqnPredicate processTerm() {
if (isToken(SearchQueryToken.Token.OPEN)) {
nextToken(); // Match OPEN
final CqnPredicate expr = processExprOr();
processClose();
return expr;
} else {
// ('NOT')? (Word | Phrase)
if (isToken(SearchQueryToken.Token.NOT)) {
return processNot();
}
return processWordOrPhrase();
}
}
private void processClose() {
if (isToken(Token.CLOSE)) {
nextToken();
} else {
logger.error("Missing close bracket after open bracket.");
throw new ErrorStatusException(CdsErrorStatuses.SEARCH_PARSING_FAILED);
}
}
private CqnPredicate processNot() {
nextToken();
if (isToken(Token.WORD) || isToken(Token.PHRASE)) {
return CQL.not(processWordOrPhrase());
}
final String tokenAsString = getTokenAsString();
logger.error("NOT must be followed by a term not a " + tokenAsString);
throw new ErrorStatusException(CdsErrorStatuses.SEARCH_PARSING_FAILED);
}
private CqnPredicate processWordOrPhrase() {
if (isToken(Token.PHRASE)) {
return processPhrase();
} else if (isToken(Token.WORD)) {
return processWord();
}
final String tokenName = getTokenAsString();
logger.error("Expected PHRASE||WORD found: " + tokenName);
throw new ErrorStatusException(CdsErrorStatuses.SEARCH_PARSING_FAILED);
}
private CqnPredicate processWord() {
String literal = token.getLiteral();
nextToken();
return new SearchTermPredicate(literal);
}
private CqnPredicate processPhrase() {
String literal = token.getLiteral();
nextToken();
return new SearchTermPredicate(literal.substring(1, literal.length() - 1));
}
private boolean isTerm() {
return isToken(SearchQueryToken.Token.NOT) || isToken(SearchQueryToken.Token.PHRASE)
|| isToken(SearchQueryToken.Token.WORD) || isToken(SearchQueryToken.Token.OPEN);
}
private boolean isEof() {
return token == null;
}
private boolean isToken(final SearchQueryToken.Token toCheckToken) {
return token != null && token.getToken() == toCheckToken;
}
private void nextToken() {
if (tokens.hasNext()) {
token = tokens.next();
} else {
token = null;
}
}
private String getTokenAsString() {
return token == null ? "" : token.getToken().name();
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy