package org.infinispan.objectfilter.impl.syntax.parser;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import org.antlr.runtime.tree.Tree;
import org.infinispan.objectfilter.SortField;
import org.infinispan.objectfilter.impl.logging.Log;
import org.infinispan.objectfilter.impl.ql.AggregationFunction;
import org.infinispan.objectfilter.impl.ql.JoinType;
import org.infinispan.objectfilter.impl.ql.PropertyPath;
import org.infinispan.objectfilter.impl.ql.QueryRendererDelegate;
import org.infinispan.objectfilter.impl.syntax.ComparisonExpr;
import org.infinispan.objectfilter.impl.syntax.ConstantValueExpr;
import org.infinispan.objectfilter.impl.syntax.IndexedFieldProvider;
import org.jboss.logging.Logger;
/**
* Transform the parsed tree into a {@link IckleParsingResult} containing the {@link
* org.infinispan.objectfilter.impl.syntax.BooleanExpr}s representing the WHERE and HAVING clauses of the query.
*
* @author Adrian Nistor
* @since 9.0
*/
final class QueryRendererDelegateImpl<TypeMetadata> implements QueryRendererDelegate<TypeDescriptor<TypeMetadata>> {
private static final Log log = Logger.getMessageLogger(Log.class, QueryRendererDelegateImpl.class.getName());
private enum Phase {
SELECT,
FROM,
WHERE,
GROUP_BY,
HAVING,
ORDER_BY
}
/**
* The current parsing phase
*/
private Phase phase;
private String targetTypeName;
private TypeMetadata targetEntityMetadata;
private IndexedFieldProvider.FieldIndexingMetadata fieldIndexingMetadata;
private PropertyPath<TypeDescriptor<TypeMetadata>> propertyPath;
private AggregationFunction aggregationFunction;
private final ExpressionBuilder<TypeMetadata> whereBuilder;
private final ExpressionBuilder<TypeMetadata> havingBuilder;
/**
* Persister space: keep track of aliases and entity names.
*/
private final Map<String, String> aliasToEntityType = new HashMap<>();
private final Map<String, PropertyPath<TypeDescriptor<TypeMetadata>>> aliasToPropertyPath = new HashMap<>();
private String alias;
private final Map<String, Object> namedParameters = new HashMap<>(5);
private final ObjectPropertyHelper<TypeMetadata> propertyHelper;
private List<PropertyPath<TypeDescriptor<TypeMetadata>>> groupBy;
private List<SortField> sortFields;
private List<PropertyPath<TypeDescriptor<TypeMetadata>>> projections;
private List<Class<?>> projectedTypes;
private final String queryString;
QueryRendererDelegateImpl(String queryString, ObjectPropertyHelper<TypeMetadata> propertyHelper) {
this.queryString = queryString;
this.propertyHelper = propertyHelper;
this.whereBuilder = new ExpressionBuilder<>(propertyHelper);
this.havingBuilder = new ExpressionBuilder<>(propertyHelper);
}
/**
* See rule entityName
*/
@Override
public void registerPersisterSpace(String entityName, Tree aliasTree) {
String aliasText = aliasTree.getText();
String previous = aliasToEntityType.put(aliasText, entityName);
if (previous != null && !previous.equalsIgnoreCase(entityName)) {
throw new UnsupportedOperationException("Alias reuse currently not supported: alias " + aliasText + " already assigned to type " + previous);
}
if (targetTypeName != null) {
throw new IllegalStateException("Can't target multiple types: " + targetTypeName + " already selected before " + entityName);
}
targetTypeName = entityName;
targetEntityMetadata = propertyHelper.getEntityMetadata(targetTypeName);
if (targetEntityMetadata == null) {
throw log.getUnknownEntity(targetTypeName);
}
fieldIndexingMetadata = propertyHelper.getIndexedFieldProvider().get(targetEntityMetadata);
whereBuilder.setEntityType(targetEntityMetadata);
havingBuilder.setEntityType(targetEntityMetadata);
}
public void registerEmbeddedAlias(String alias, PropertyPath<TypeDescriptor<TypeMetadata>> propertyPath) {
PropertyPath<TypeDescriptor<TypeMetadata>> previous = aliasToPropertyPath.put(alias, propertyPath);
if (previous != null) {
throw new UnsupportedOperationException("Alias reuse currently not supported: alias " + alias + " already assigned to type " + previous);
}
}
@Override
public boolean isUnqualifiedPropertyReference() {
return true;
}
@Override
public boolean isPersisterReferenceAlias() {
return aliasToEntityType.containsKey(alias);
}
@Override
public void activateFromStrategy(JoinType joinType, Tree associationFetchTree, Tree propertyFetchTree, Tree alias) {
phase = Phase.FROM;
this.alias = alias.getText();
}
@Override
public void activateSelectStrategy() {
phase = Phase.SELECT;
}
@Override
public void activateWhereStrategy() {
phase = Phase.WHERE;
}
@Override
public void activateGroupByStrategy() {
phase = Phase.GROUP_BY;
}
@Override
public void activateHavingStrategy() {
phase = Phase.HAVING;
}
@Override
public void activateOrderByStrategy() {
phase = Phase.ORDER_BY;
}
@Override
public void deactivateStrategy() {
phase = null;
alias = null;
propertyPath = null;
aggregationFunction = null;
}
@Override
public void activateOR() {
if (phase == Phase.WHERE) {
whereBuilder.pushOr();
} else if (phase == Phase.HAVING) {
havingBuilder.pushOr();
} else {
throw new IllegalStateException();
}
}
@Override
public void activateAND() {
if (phase == Phase.WHERE) {
whereBuilder.pushAnd();
} else if (phase == Phase.HAVING) {
havingBuilder.pushAnd();
} else {
throw new IllegalStateException();
}
}
@Override
public void activateNOT() {
if (phase == Phase.WHERE) {
whereBuilder.pushNot();
} else if (phase == Phase.HAVING) {
havingBuilder.pushNot();
} else {
throw new IllegalStateException();
}
}
@Override
public void predicateLess(String value) {
addComparisonPredicate(value, ComparisonExpr.Type.LESS);
}
@Override
public void predicateLessOrEqual(String value) {
addComparisonPredicate(value, ComparisonExpr.Type.LESS_OR_EQUAL);
}
/**
* This implements the equality predicate; the comparison
* predicate could be a constant, a subfunction or
* some random type parameter.
* The tree node has all details but with current tree rendering
* it just passes it's text so we have to figure out the options again.
*/
@Override
public void predicateEquals(String value) {
addComparisonPredicate(value, ComparisonExpr.Type.EQUAL);
}
@Override
public void predicateNotEquals(String value) {
activateNOT();
addComparisonPredicate(value, ComparisonExpr.Type.EQUAL);
deactivateBoolean();
}
@Override
public void predicateGreaterOrEqual(String value) {
addComparisonPredicate(value, ComparisonExpr.Type.GREATER_OR_EQUAL);
}
@Override
public void predicateGreater(String value) {
addComparisonPredicate(value, ComparisonExpr.Type.GREATER);
}
private void addComparisonPredicate(String value, ComparisonExpr.Type comparisonType) {
ensureLeftSideIsAPropertyPath();
PropertyPath<TypeDescriptor<TypeMetadata>> property = resolveAlias(propertyPath);
if (property.isEmpty()) {
throw log.getPredicatesOnEntityAliasNotAllowedException(propertyPath.asStringPath());
}
Object comparisonValue = parameterValue(value);
checkAnalyzed(property, false);
if (phase == Phase.WHERE) {
whereBuilder.addComparison(property, comparisonType, comparisonValue);
} else if (phase == Phase.HAVING) {
havingBuilder.addComparison(property, comparisonType, comparisonValue);
} else {
throw new IllegalStateException();
}
}
@Override
public void predicateIn(List<String> list) {
ensureLeftSideIsAPropertyPath();
List<Object> values = new ArrayList<>(list.size());
for (String string : list) {
values.add(parameterValue(string));
}
PropertyPath<TypeDescriptor<TypeMetadata>> property = resolveAlias(propertyPath);
if (property.isEmpty()) {
throw log.getPredicatesOnEntityAliasNotAllowedException(propertyPath.asStringPath());
}
if (phase == Phase.WHERE) {
whereBuilder.addIn(property, values);
} else if (phase == Phase.HAVING) {
havingBuilder.addIn(property, values);
} else {
throw new IllegalStateException();
}
}
@Override
public void predicateBetween(String lowerValue, String upperValue) {
ensureLeftSideIsAPropertyPath();
Object lowerComparisonValue = parameterValue(lowerValue);
Object upperComparisonValue = parameterValue(upperValue);
PropertyPath<TypeDescriptor<TypeMetadata>> property = resolveAlias(propertyPath);
if (property.isEmpty()) {
throw log.getPredicatesOnEntityAliasNotAllowedException(propertyPath.asStringPath());
}
checkAnalyzed(property, false);
if (phase == Phase.WHERE) {
whereBuilder.addRange(property, lowerComparisonValue, upperComparisonValue);
} else if (phase == Phase.HAVING) {
havingBuilder.addRange(property, lowerComparisonValue, upperComparisonValue);
} else {
throw new IllegalStateException();
}
}
@Override
public void predicateLike(String patternValue, Character escapeCharacter) {
ensureLeftSideIsAPropertyPath();
Object pattern = parameterValue(patternValue);
PropertyPath<TypeDescriptor<TypeMetadata>> property = resolveAlias(propertyPath);
if (property.isEmpty()) {
throw log.getPredicatesOnEntityAliasNotAllowedException(propertyPath.asStringPath());
}
checkAnalyzed(property, false);
if (phase == Phase.WHERE) {
whereBuilder.addLike(property, pattern, escapeCharacter);
} else if (phase == Phase.HAVING) {
havingBuilder.addLike(property, pattern, escapeCharacter);
} else {
throw new IllegalStateException();
}
}
@Override
public void predicateIsNull() {
ensureLeftSideIsAPropertyPath();
PropertyPath<TypeDescriptor<TypeMetadata>> property = resolveAlias(propertyPath);
if (property.isEmpty()) {
throw log.getPredicatesOnEntityAliasNotAllowedException(propertyPath.asStringPath());
}
checkAnalyzed(property, false);
if (phase == Phase.WHERE) {
whereBuilder.addIsNull(property);
} else if (phase == Phase.HAVING) {
havingBuilder.addIsNull(property);
} else {
throw new IllegalStateException();
}
}
@Override
public void predicateConstantBoolean(boolean booleanConstant) {
if (phase == Phase.WHERE) {
whereBuilder.addConstantBoolean(booleanConstant);
} else if (phase == Phase.HAVING) {
havingBuilder.addConstantBoolean(booleanConstant);
} else {
throw new IllegalStateException();
}
}
@Override
public void predicateFullTextTerm(String term, String fuzzyFlop) {
ensureLeftSideIsAPropertyPath();
PropertyPath<TypeDescriptor<TypeMetadata>> property = resolveAlias(propertyPath);
if (property.isEmpty()) {
throw log.getPredicatesOnEntityAliasNotAllowedException(propertyPath.asStringPath());
}
String comparisonValue = (String) parameterValue(term);
if (phase == Phase.WHERE) {
checkAnalyzed(property, true);
whereBuilder.addFullTextTerm(property, comparisonValue, fuzzyFlop == null ? null : (fuzzyFlop.equals("~") ? 2 : Integer.parseInt(fuzzyFlop)));
} else if (phase == Phase.HAVING) {
throw log.getFullTextQueriesNotAllowedInHavingClauseException();
} else {
throw new IllegalStateException();
}
}
@Override
public void predicateFullTextRegexp(String term) {
ensureLeftSideIsAPropertyPath();
PropertyPath<TypeDescriptor<TypeMetadata>> property = resolveAlias(propertyPath);
if (property.isEmpty()) {
throw log.getPredicatesOnEntityAliasNotAllowedException(propertyPath.asStringPath());
}
if (phase == Phase.WHERE) {
checkAnalyzed(property, true);
whereBuilder.addFullTextRegexp(property, term);
} else if (phase == Phase.HAVING) {
throw log.getFullTextQueriesNotAllowedInHavingClauseException();
} else {
throw new IllegalStateException();
}
}
@Override
public void predicateFullTextRange(boolean includeLower, String lower, String upper, boolean includeUpper) {
ensureLeftSideIsAPropertyPath();
if (phase == Phase.WHERE) {
PropertyPath<TypeDescriptor<TypeMetadata>> property = resolveAlias(propertyPath);
if (property.isEmpty()) {
throw log.getPredicatesOnEntityAliasNotAllowedException(propertyPath.asStringPath());
}
Object from = lower != null ? parameterValue(lower) : null;
Object to = upper != null ? parameterValue(upper) : null;
checkIndexed(property);
whereBuilder.addFullTextRange(property, includeLower, from, to, includeUpper);
} else if (phase == Phase.HAVING) {
throw log.getFullTextQueriesNotAllowedInHavingClauseException();
} else {
throw new IllegalStateException();
}
}
private void checkAnalyzed(PropertyPath<?> propertyPath, boolean expectAnalyzed) {
if (fieldIndexingMetadata.isAnalyzed(propertyPath.asArrayPath())) {
if (!expectAnalyzed) {
throw log.getQueryOnAnalyzedPropertyNotSupportedException(targetTypeName, propertyPath.asStringPath());
}
} else {
if (expectAnalyzed) {
throw log.getFullTextQueryOnNotAalyzedPropertyNotSupportedException(targetTypeName, propertyPath.asStringPath());
}
}
}
private void checkIndexed(PropertyPath<?> propertyPath) {
if (!fieldIndexingMetadata.isIndexed(propertyPath.asArrayPath())) {
throw log.getFullTextQueryOnNotIndexedPropertyNotSupportedException(targetTypeName, propertyPath.asStringPath());
}
}
@Override
public void activateFullTextBoost(float boost) {
if (phase == Phase.WHERE) {
whereBuilder.pushFullTextBoost(boost);
} else if (phase == Phase.HAVING) {
throw log.getFullTextQueriesNotAllowedInHavingClauseException();
} else {
throw new IllegalStateException();
}
}
@Override
public void deactivateFullTextBoost() {
if (phase == Phase.WHERE) {
whereBuilder.pop();
} else if (phase == Phase.HAVING) {
throw log.getFullTextQueriesNotAllowedInHavingClauseException();
} else {
throw new IllegalStateException();
}
}
@Override
public void activateFullTextOccur(Occur occur) {
if (phase == Phase.WHERE) {
whereBuilder.pushFullTextOccur(occur);
} else if (phase == Phase.HAVING) {
throw log.getFullTextQueriesNotAllowedInHavingClauseException();
} else {
throw new IllegalStateException();
}
}
@Override
public void deactivateFullTextOccur() {
if (phase == Phase.WHERE) {
whereBuilder.pop();
} else if (phase == Phase.HAVING) {
throw log.getFullTextQueriesNotAllowedInHavingClauseException();
} else {
throw new IllegalStateException();
}
}
/**
* Sets a property path representing one property in the SELECT, GROUP BY, WHERE or HAVING clause of a given query.
*
* @param propertyPath the property path to set
*/
@Override
public void setPropertyPath(PropertyPath<TypeDescriptor<TypeMetadata>> propertyPath) {
if (aggregationFunction != null) {
if (propertyPath == null && aggregationFunction != AggregationFunction.COUNT && aggregationFunction != AggregationFunction.COUNT_DISTINCT) {
throw log.getAggregationCanOnlyBeAppliedToPropertyReferencesException(aggregationFunction.name());
}
propertyPath = new AggregationPropertyPath<>(aggregationFunction, propertyPath.getNodes());
}
if (phase == Phase.SELECT) {
if (projections == null) {
projections = new ArrayList<>(5);
projectedTypes = new ArrayList<>(5);
}
PropertyPath<TypeDescriptor<TypeMetadata>> projection;
Class<?> propertyType;
if (propertyPath.getLength() == 1 && propertyPath.isAlias()) {
projection = new PropertyPath<>(Collections.singletonList(new PropertyPath.PropertyReference<>("__HSearch_This", null, true))); //todo [anistor] this is a leftover from hsearch ???? this represents the entity itself. see org.hibernate.search.ProjectionConstants
propertyType = null;
} else {
projection = resolveAlias(propertyPath);
propertyType = propertyHelper.getPrimitivePropertyType(targetEntityMetadata, projection.asArrayPath());
}
projections.add(projection);
projectedTypes.add(propertyType);
} else {
this.propertyPath = propertyPath;
}
}
@Override
public void activateAggregation(AggregationFunction aggregationFunction) {
if (phase == Phase.WHERE) {
throw log.getNoAggregationsInWhereClauseException(aggregationFunction.name());
}
if (phase == Phase.GROUP_BY) {
throw log.getNoAggregationsInGroupByClauseException(aggregationFunction.name());
}
this.aggregationFunction = aggregationFunction;
propertyPath = null;
}
@Override
public void deactivateAggregation() {
aggregationFunction = null;
}
/**
* Add field sort criteria.
*
* @param collateName optional collation name
* @param isAscending sort direction
*/
@Override
public void sortSpecification(String collateName, boolean isAscending) {
// collationName is ignored for now
PropertyPath<TypeDescriptor<TypeMetadata>> property = resolveAlias(propertyPath);
checkAnalyzed(property, false); //todo [anistor] cannot sort on analyzed field?
if (sortFields == null) {
sortFields = new ArrayList<>(5);
}
sortFields.add(new IckleParsingResult.SortFieldImpl<>(property, isAscending));
}
/**
* Add 'group by' criteria.
*
* @param collateName optional collation name
*/
@Override
public void groupingValue(String collateName) {
// collationName is ignored for now
if (groupBy == null) {
groupBy = new ArrayList<>(5);
}
groupBy.add(resolveAlias(propertyPath));
}
private Object parameterValue(String value) {
if (value.startsWith(":")) {
// it's a named parameter
String paramName = value.substring(1).trim(); //todo [anistor] trim should not be required!
ConstantValueExpr.ParamPlaceholder namedParam = (ConstantValueExpr.ParamPlaceholder) namedParameters.get(paramName);
if (namedParam == null) {
namedParam = new ConstantValueExpr.ParamPlaceholder(paramName);
namedParameters.put(paramName, namedParam);
}
return namedParam;
} else {
// it's a literal value given in the query string
List<String> path = propertyPath.getNodeNamesWithoutAlias();
// create the complete path in case it's a join
PropertyPath<TypeDescriptor<TypeMetadata>> fullPath = propertyPath;
while (fullPath.isAlias()) {
PropertyPath<TypeDescriptor<TypeMetadata>> resolved = aliasToPropertyPath.get(fullPath.getFirst().getPropertyName());
if (resolved == null) {
break;
}
path.addAll(0, resolved.getNodeNamesWithoutAlias());
fullPath = resolved;
}
return propertyHelper.convertToPropertyType(targetEntityMetadata, path.toArray(new String[path.size()]), value);
}
}
@Override
public void deactivateBoolean() {
if (phase == Phase.WHERE) {
whereBuilder.pop();
} else if (phase == Phase.HAVING) {
havingBuilder.pop();
} else {
throw new IllegalStateException();
}
}
private PropertyPath<TypeDescriptor<TypeMetadata>> resolveAlias(PropertyPath<TypeDescriptor<TypeMetadata>> path) {
List<PropertyPath.PropertyReference<TypeDescriptor<TypeMetadata>>> resolved = resolveAliasPath(path);
return path instanceof AggregationPropertyPath ?
new AggregationPropertyPath<>(((AggregationPropertyPath) path).getAggregationFunction(), resolved) : new PropertyPath<>(resolved);
}
private List<PropertyPath.PropertyReference<TypeDescriptor<TypeMetadata>>> resolveAliasPath(PropertyPath<TypeDescriptor<TypeMetadata>> path) {
if (path.isAlias()) {
String alias = path.getFirst().getPropertyName();
if (aliasToEntityType.containsKey(alias)) {
// Alias for entity
return path.getNodesWithoutAlias();
} else if (aliasToPropertyPath.containsKey(alias)) {
// Alias for embedded
PropertyPath<TypeDescriptor<TypeMetadata>> propertyPath = aliasToPropertyPath.get(alias);
List<PropertyPath.PropertyReference<TypeDescriptor<TypeMetadata>>> resolvedAlias = resolveAliasPath(propertyPath);
resolvedAlias.addAll(path.getNodesWithoutAlias());
return resolvedAlias;
} else {
// Alias not found
throw log.getUnknownAliasException(alias);
}
}
// It does not start with an alias
return path.getNodesWithoutAlias();
}
@Override
public void registerJoinAlias(Tree alias, PropertyPath<TypeDescriptor<TypeMetadata>> path) {
if (!aliasToPropertyPath.containsKey(alias.getText())) {
aliasToPropertyPath.put(alias.getText(), path);
}
}
private void ensureLeftSideIsAPropertyPath() {
if (propertyPath == null) {
throw log.getLeftSideMustBeAPropertyPath();
}
}
public IckleParsingResult<TypeMetadata> getResult() {
return new IckleParsingResult<>(
queryString,
Collections.unmodifiableSet(new HashSet<>(namedParameters.keySet())),
whereBuilder.build(),
havingBuilder.build(),
targetTypeName,
targetEntityMetadata,
projections == null ? null : projections.toArray(new PropertyPath[projections.size()]),
projectedTypes == null ? null : projectedTypes.toArray(new Class<?>[projectedTypes.size()]),
groupBy == null ? null : groupBy.toArray(new PropertyPath[groupBy.size()]),
sortFields == null ? null : sortFields.toArray(new SortField[sortFields.size()]));
}
}