All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.sap.cds.adapter.odata.v2.search.SearchParser Maven / Gradle / Ivy

There is a newer version: 3.6.0
Show newest version
/**************************************************************************
 * (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