/* * Copyright 2014 - 2017 Blazebit. * * Licensed 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 com.blazebit.persistence.impl; import com.blazebit.lang.StringUtils; import com.blazebit.lang.ValueRetriever; import com.blazebit.persistence.JoinOnBuilder; import com.blazebit.persistence.JoinType; import com.blazebit.persistence.impl.builder.predicate.JoinOnBuilderImpl; import com.blazebit.persistence.impl.builder.predicate.PredicateBuilderEndedListenerImpl; import com.blazebit.persistence.impl.expression.ArrayExpression; import com.blazebit.persistence.impl.expression.Expression; import com.blazebit.persistence.impl.expression.ExpressionFactory; import com.blazebit.persistence.impl.expression.FunctionExpression; import com.blazebit.persistence.impl.expression.GeneralCaseExpression; import com.blazebit.persistence.impl.expression.ListIndexExpression; import com.blazebit.persistence.impl.expression.MapEntryExpression; import com.blazebit.persistence.impl.expression.MapKeyExpression; import com.blazebit.persistence.impl.expression.MapValueExpression; import com.blazebit.persistence.impl.expression.NumericLiteral; import com.blazebit.persistence.impl.expression.ParameterExpression; import com.blazebit.persistence.impl.expression.PathElementExpression; import com.blazebit.persistence.impl.expression.PathExpression; import com.blazebit.persistence.impl.expression.PathReference; import com.blazebit.persistence.impl.expression.PropertyExpression; import com.blazebit.persistence.impl.expression.QualifiedExpression; import com.blazebit.persistence.impl.expression.SimplePathReference; import com.blazebit.persistence.impl.expression.StringLiteral; import com.blazebit.persistence.impl.expression.TreatExpression; import com.blazebit.persistence.impl.expression.VisitorAdapter; import com.blazebit.persistence.impl.expression.modifier.ExpressionModifier; import com.blazebit.persistence.impl.function.entity.ValuesEntity; import com.blazebit.persistence.impl.predicate.CompoundPredicate; import com.blazebit.persistence.impl.predicate.EqPredicate; import com.blazebit.persistence.impl.predicate.Predicate; import com.blazebit.persistence.impl.predicate.PredicateBuilder; import com.blazebit.persistence.impl.transform.ExpressionModifierVisitor; import com.blazebit.persistence.impl.util.SqlUtils; import com.blazebit.persistence.spi.DbmsDialect; import com.blazebit.persistence.spi.DbmsModificationState; import com.blazebit.persistence.spi.DbmsStatementType; import com.blazebit.persistence.spi.JpaProvider; import com.blazebit.persistence.spi.ValuesStrategy; import javax.persistence.Query; import javax.persistence.metamodel.Attribute; import javax.persistence.metamodel.EmbeddableType; import javax.persistence.metamodel.EntityType; import javax.persistence.metamodel.IdentifiableType; import javax.persistence.metamodel.ListAttribute; import javax.persistence.metamodel.ManagedType; import javax.persistence.metamodel.MapAttribute; import javax.persistence.metamodel.SingularAttribute; import javax.persistence.metamodel.Type; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.IdentityHashMap; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; /** * @author Moritz Becker * @author Christian Beikov * @since 1.0 */ public class JoinManager extends AbstractManager<ExpressionModifier> { private static final Logger LOG = Logger.getLogger(JoinManager.class.getName()); // we might have multiple nodes that depend on the same unresolved alias, // hence we need a List of NodeInfos. // e.g. SELECT a.X, a.Y FROM A a // a is unresolved for both X and Y private final List<JoinNode> rootNodes = new ArrayList<JoinNode>(1); private final Set<JoinNode> entityFunctionNodes = new LinkedHashSet<JoinNode>(); // root entity class private final String joinRestrictionKeyword; private final MainQuery mainQuery; private final AliasManager aliasManager; private final EntityMetamodelImpl metamodel; // needed for model-aware joins private final JoinManager parent; private final JoinOnBuilderEndedListener joinOnBuilderListener; private final SubqueryInitiatorFactory subqueryInitFactory; private final ExpressionFactory expressionFactory; // helper collections for join rendering private final Set<JoinNode> collectionJoinNodes = Collections.newSetFromMap(new IdentityHashMap<JoinNode, Boolean>()); private final Set<JoinNode> renderedJoins = Collections.newSetFromMap(new IdentityHashMap<JoinNode, Boolean>()); private final Set<JoinNode> markedJoinNodes = Collections.newSetFromMap(new IdentityHashMap<JoinNode, Boolean>()); JoinManager(MainQuery mainQuery, ResolvingQueryGenerator queryGenerator, AliasManager aliasManager, JoinManager parent, ExpressionFactory expressionFactory) { super(queryGenerator, mainQuery.parameterManager, null); this.mainQuery = mainQuery; this.aliasManager = aliasManager; this.metamodel = mainQuery.metamodel; this.parent = parent; this.joinRestrictionKeyword = " " + mainQuery.jpaProvider.getOnClause() + " "; this.joinOnBuilderListener = new JoinOnBuilderEndedListener(); this.subqueryInitFactory = new SubqueryInitiatorFactory(mainQuery, aliasManager, this); this.expressionFactory = expressionFactory; } void applyFrom(JoinManager joinManager) { for (JoinNode node : joinManager.rootNodes) { JoinNode rootNode = applyFrom(node); if (node.getValueQuery() != null) { // TODO: At the moment the value type is without meaning ParameterManager.ParameterImpl<?> param = joinManager.parameterManager.getParameter(node.getAlias()); ValuesParameterBinder binder = ((ParameterManager.ValuesParameterWrapper) param.getParameterValue()).getBinder(); parameterManager.registerValuesParameter(rootNode.getAlias(), null, binder.getParameterNames(), binder.getPathExpressions()); entityFunctionNodes.add(rootNode); } } } private JoinNode applyFrom(JoinNode node) { String rootAlias = node.getAlias(); boolean implicit = node.getAliasInfo().isImplicit(); JoinAliasInfo rootAliasInfo = new JoinAliasInfo(rootAlias, rootAlias, implicit, true, aliasManager); JoinNode rootNode; if (node.getCorrelationParent() != null) { throw new UnsupportedOperationException("Cloning subqueries not yet implemented!"); } else { rootNode = node.cloneRootNode(rootAliasInfo); } rootAliasInfo.setJoinNode(rootNode); rootNodes.add(rootNode); // register root alias in aliasManager aliasManager.registerAliasInfo(rootAliasInfo); for (JoinTreeNode treeNode : node.getNodes().values()) { applyFrom(rootNode, treeNode); } if (!node.getTreatedJoinNodes().isEmpty()) { throw new UnsupportedOperationException("Cloning joins with treat joins is not yet implemented!"); } return rootNode; } private void applyFrom(JoinNode parent, JoinTreeNode treeNode) { JoinTreeNode newTreeNode = parent.getOrCreateTreeNode(treeNode.getRelationName(), treeNode.getAttribute()); for (Map.Entry<String, JoinNode> nodeEntry : treeNode.getJoinNodes().entrySet()) { JoinNode newNode = applyFrom(parent, newTreeNode, nodeEntry.getKey(), nodeEntry.getValue()); newTreeNode.addJoinNode(newNode, nodeEntry.getValue() == treeNode.getDefaultNode()); } } private JoinNode applyFrom(JoinNode parent, JoinTreeNode treeNode, String alias, JoinNode oldNode) { boolean implicit = oldNode.getAliasInfo().isImplicit(); String currentJoinPath = parent.getAliasInfo().getAbsolutePath() + "." + treeNode.getRelationName(); JoinAliasInfo newAliasInfo = new JoinAliasInfo(alias, currentJoinPath, implicit, false, aliasManager); aliasManager.registerAliasInfo(newAliasInfo); JoinNode node = oldNode.cloneJoinNode(parent, treeNode, newAliasInfo); newAliasInfo.setJoinNode(node); if (oldNode.getOnPredicate() != null) { node.setOnPredicate(subqueryInitFactory.reattachSubqueries(oldNode.getOnPredicate().clone(true))); } for (JoinTreeNode oldTreeNode : oldNode.getNodes().values()) { applyFrom(node, oldTreeNode); } if (!oldNode.getTreatedJoinNodes().isEmpty()) { throw new UnsupportedOperationException("Cloning joins with treat joins is not yet implemented!"); } return node; } @Override public ClauseType getClauseType() { return ClauseType.JOIN; } Set<JoinNode> getKeyRestrictedLeftJoins() { if (!mainQuery.jpaProvider.needsJoinSubqueryRewrite()) { return Collections.emptySet(); } Set<JoinNode> keyRestrictedLeftJoins = new HashSet<JoinNode>(); acceptVisitor(new KeyRestrictedLeftJoinCollectingVisitor(mainQuery.jpaProvider, keyRestrictedLeftJoins)); return keyRestrictedLeftJoins; } static class KeyRestrictedLeftJoinCollectingVisitor extends VisitorAdapter implements JoinNodeVisitor { final JpaProvider jpaProvider; final Set<JoinNode> keyRestrictedLeftJoins; public KeyRestrictedLeftJoinCollectingVisitor(JpaProvider jpaProvider, Set<JoinNode> keyRestrictedLeftJoins) { this.jpaProvider = jpaProvider; this.keyRestrictedLeftJoins = keyRestrictedLeftJoins; } @Override public void visit(JoinNode node) { if (node.getJoinType() == JoinType.LEFT && node.getOnPredicate() != null) { node.getOnPredicate().accept(this); } } @Override public void visit(MapKeyExpression expression) { super.visit(expression); visitKeyOrIndexExpression(expression.getPath()); } @Override public void visit(ListIndexExpression expression) { super.visit(expression); visitKeyOrIndexExpression(expression.getPath()); } private void visitKeyOrIndexExpression(PathExpression pathExpression) { JoinNode node = (JoinNode) pathExpression.getBaseNode(); Attribute<?, ?> attribute = node.getParentTreeNode().getAttribute(); // Exclude element collections as they are not problematic if (attribute.getPersistentAttributeType() != Attribute.PersistentAttributeType.ELEMENT_COLLECTION) { // There are weird mappings possible, we have to check if the attribute is a join table if (jpaProvider.isJoinTable(attribute)) { keyRestrictedLeftJoins.add(node); } } } } String addRootValues(Class<?> clazz, Class<?> valueClazz, String rootAlias, int valueCount, String valuesFunction, String castedParameter, boolean identifiableReference) { if (rootAlias == null) { throw new IllegalArgumentException("Illegal empty alias for the VALUES clause: " + clazz.getName()); } ValuesStrategy strategy = mainQuery.dbmsDialect.getValuesStrategy(); String dummyTable = mainQuery.dbmsDialect.getDummyTable(); // TODO: we should do batching to avoid filling query caches ManagedType<?> managedType = mainQuery.metamodel.getManagedType(clazz); Set<Attribute<?, ?>> attributeSet; if (identifiableReference) { attributeSet = (Set<Attribute<?, ?>>) (Set<?>) Collections.singleton(JpaUtils.getIdAttribute((EntityType<?>) managedType)); } else { Set<Attribute<?, ?>> originalAttributeSet = (Set<Attribute<?, ?>>) managedType.getAttributes(); attributeSet = new LinkedHashSet<>(originalAttributeSet.size()); for (Attribute<?, ?> attr : originalAttributeSet) { // Filter out collection attributes if (!attr.isCollection()) { attributeSet.add(attr); } } } String[][] parameterNames = new String[valueCount][attributeSet.size()]; ValueRetriever<Object, Object>[] pathExpressions = new ValueRetriever[attributeSet.size()]; StringBuilder valuesSb = new StringBuilder(20 + valueCount * attributeSet.size() * 3); Query valuesExampleQuery = getValuesExampleQuery(clazz, identifiableReference, rootAlias, valuesFunction, castedParameter, attributeSet, parameterNames, pathExpressions, valuesSb, strategy, dummyTable); parameterManager.registerValuesParameter(rootAlias, valueClazz, parameterNames, pathExpressions); String exampleQuerySql = mainQuery.cbf.getExtendedQuerySupport().getSql(mainQuery.em, valuesExampleQuery); String exampleQuerySqlAlias = mainQuery.cbf.getExtendedQuerySupport().getSqlAlias(mainQuery.em, valuesExampleQuery, "e"); StringBuilder whereClauseSb = new StringBuilder(exampleQuerySql.length()); String filterNullsTableAlias = "fltr_nulls_tbl_als_"; String valuesAliases = getValuesAliases(exampleQuerySqlAlias, attributeSet.size(), exampleQuerySql, whereClauseSb, filterNullsTableAlias, strategy, dummyTable); if (strategy == ValuesStrategy.SELECT_VALUES) { valuesSb.insert(0, valuesAliases); valuesSb.append(')'); valuesAliases = null; } else if (strategy == ValuesStrategy.SELECT_UNION) { valuesSb.insert(0, valuesAliases); mainQuery.dbmsDialect.appendExtendedSql(valuesSb, DbmsStatementType.SELECT, true, true, null, Integer.toString(valueCount + 1), "1", null, null); valuesSb.append(')'); valuesAliases = null; } boolean filterNulls = mainQuery.getQueryConfiguration().isValuesClauseFilterNullsEnabled(); if (filterNulls) { valuesSb.insert(0, "(select * from "); valuesSb.append(' '); valuesSb.append(filterNullsTableAlias); if (valuesAliases != null) { valuesSb.append(valuesAliases); valuesAliases = null; } valuesSb.append(whereClauseSb); valuesSb.append(')'); } String valuesClause = valuesSb.toString(); JoinAliasInfo rootAliasInfo = new JoinAliasInfo(rootAlias, rootAlias, true, true, aliasManager); JoinNode rootNode = JoinNode.createValuesRootNode(managedType, valuesFunction, valueCount, attributeSet.size(), valuesExampleQuery, valuesClause, valuesAliases, rootAliasInfo); rootAliasInfo.setJoinNode(rootNode); rootNodes.add(rootNode); // register root alias in aliasManager aliasManager.registerAliasInfo(rootAliasInfo); entityFunctionNodes.add(rootNode); return rootAlias; } private String getValuesAliases(String tableAlias, int attributeCount, String exampleQuerySql, StringBuilder whereClauseSb, String filterNullsTableAlias, ValuesStrategy strategy, String dummyTable) { int startIndex = SqlUtils.indexOfSelect(exampleQuerySql); int endIndex = exampleQuerySql.indexOf(" from "); StringBuilder sb; if (strategy == ValuesStrategy.VALUES) { sb = new StringBuilder((endIndex - startIndex) - (tableAlias.length() + 3) * attributeCount); sb.append('('); } else if (strategy == ValuesStrategy.SELECT_VALUES) { sb = new StringBuilder(endIndex - startIndex); sb.append("(select "); } else if (strategy == ValuesStrategy.SELECT_UNION) { sb = new StringBuilder((endIndex - startIndex) - (tableAlias.length() + 3) * attributeCount); sb.append("(select "); } else { throw new IllegalArgumentException("Unsupported values strategy: " + strategy); } whereClauseSb.append(" where"); String[] columnNames = SqlUtils.getSelectItemColumns(exampleQuerySql, startIndex); for (int i = 0; i < columnNames.length; i++) { whereClauseSb.append(' '); if (i > 0) { whereClauseSb.append("or "); } whereClauseSb.append(filterNullsTableAlias); whereClauseSb.append('.'); whereClauseSb.append(columnNames[i]); whereClauseSb.append(" is not null"); if (strategy == ValuesStrategy.SELECT_VALUES) { // TODO: This naming is actually H2 specific sb.append('c'); sb.append(i + 1); sb.append(' '); } else if (strategy == ValuesStrategy.SELECT_UNION) { sb.append("null as "); } sb.append(columnNames[i]); sb.append(','); } if (strategy == ValuesStrategy.VALUES) { sb.setCharAt(sb.length() - 1, ')'); } else if (strategy == ValuesStrategy.SELECT_VALUES) { sb.setCharAt(sb.length() - 1, ' '); sb.append(" from "); } else if (strategy == ValuesStrategy.SELECT_UNION) { sb.setCharAt(sb.length() - 1, ' '); if (dummyTable != null) { sb.append(" from "); sb.append(dummyTable); } } return sb.toString(); } static class SimpleValueRetriever implements ValueRetriever<Object, Object> { @Override public Object getValue(Object target) { return target; } } private static String getCastedParameters(StringBuilder sb, DbmsDialect dbmsDialect, String[] types) { sb.setLength(0); if (dbmsDialect.needsCastParameters()) { for (int i = 0; i < types.length; i++) { sb.append(dbmsDialect.cast("?", types[i])); sb.append(','); } } else { for (int i = 0; i < types.length; i++) { sb.append("?,"); } } return sb.substring(0, sb.length() - 1); } private Query getValuesExampleQuery(Class<?> clazz, boolean identifiableReference, String prefix, String treatFunction, String castedParameter, Set<Attribute<?, ?>> attributeSet, String[][] parameterNames, ValueRetriever<?, ?>[] pathExpressions, StringBuilder valuesSb, ValuesStrategy strategy, String dummyTable) { int valueCount = parameterNames.length; String[] attributes = new String[attributeSet.size()]; String[] attributeParameter = new String[attributeSet.size()]; // This size estimation roughly assumes a maximum attribute name length of 15 StringBuilder sb = new StringBuilder(50 + valueCount * prefix.length() * attributeSet.size() * 50); sb.append("SELECT "); if (clazz == ValuesEntity.class) { sb.append("e."); attributes[0] = attributeSet.iterator().next().getName(); attributeParameter[0] = mainQuery.dbmsDialect.needsCastParameters() ? castedParameter : "?"; pathExpressions[0] = new SimpleValueRetriever(); sb.append(attributes[0]); sb.append(','); } else if (identifiableReference) { sb.append("e."); Attribute<?, ?> attribute = attributeSet.iterator().next(); attributes[0] = attribute.getName(); String[] columnTypes = metamodel.getAttributeColumnTypeMapping(clazz).get(attribute.getName()).getValue(); attributeParameter[0] = getCastedParameters(new StringBuilder(), mainQuery.dbmsDialect, columnTypes); pathExpressions[0] = com.blazebit.reflection.ExpressionUtils.getExpression(clazz, attributes[0]); sb.append(attributes[0]); sb.append(','); } else { Iterator<Attribute<?, ?>> iter = attributeSet.iterator(); Map<String, Map.Entry<AttributePath, String[]>> mapping = metamodel.getAttributeColumnTypeMapping(clazz); StringBuilder paramBuilder = new StringBuilder(); for (int i = 0; i < attributes.length; i++) { sb.append("e."); Attribute<?, ?> attribute = iter.next(); attributes[i] = attribute.getName(); Map.Entry<AttributePath, String[]> entry = mapping.get(attribute.getName()); String[] columnTypes = entry.getValue(); attributeParameter[i] = getCastedParameters(paramBuilder, mainQuery.dbmsDialect, columnTypes); pathExpressions[i] = com.blazebit.reflection.ExpressionUtils.getExpression(clazz, attributes[i]); sb.append(attributes[i]); // When the class for which we want a VALUES clause has *ToOne relations, we need to put their ids into the select // otherwise we would fetch all of the types attributes, but the VALUES clause can only ever contain the id if (attribute.getPersistentAttributeType() != Attribute.PersistentAttributeType.BASIC && attribute.getPersistentAttributeType() != Attribute.PersistentAttributeType.EMBEDDED) { ManagedType<?> managedAttributeType = metamodel.managedType(entry.getKey().getAttributeClass()); Attribute<?, ?> attributeTypeIdAttribute = JpaUtils.getIdAttribute((IdentifiableType<?>) managedAttributeType); sb.append('.'); sb.append(attributeTypeIdAttribute.getName()); } sb.append(','); } } sb.setCharAt(sb.length() - 1, ' '); sb.append("FROM "); sb.append(clazz.getName()); sb.append(" e WHERE 1=1"); if (strategy == ValuesStrategy.SELECT_VALUES || strategy == ValuesStrategy.VALUES) { valuesSb.append("(VALUES "); } else if (strategy == ValuesStrategy.SELECT_UNION) { // Nothing to do here } else { throw new IllegalArgumentException("Unsupported values strategy: " + strategy); } for (int i = 0; i < valueCount; i++) { if (strategy == ValuesStrategy.SELECT_UNION) { valuesSb.append(" union all select "); } else { valuesSb.append('('); } for (int j = 0; j < attributes.length; j++) { sb.append(" OR "); if (treatFunction != null) { sb.append(treatFunction); sb.append('('); sb.append("e."); sb.append(attributes[j]); sb.append(')'); } else { sb.append("e."); sb.append(attributes[j]); } sb.append(" = "); sb.append(':'); int start = sb.length(); sb.append(prefix); sb.append('_'); sb.append(attributes[j]); sb.append('_').append(i); String paramName = sb.substring(start, sb.length()); parameterNames[i][j] = paramName; valuesSb.append(attributeParameter[j]); valuesSb.append(','); } if (strategy == ValuesStrategy.SELECT_UNION) { valuesSb.setCharAt(valuesSb.length() - 1, ' '); if (dummyTable != null) { valuesSb.append("from "); valuesSb.append(dummyTable); valuesSb.append(' '); } } else { valuesSb.setCharAt(valuesSb.length() - 1, ')'); valuesSb.append(','); } } if (strategy == ValuesStrategy.SELECT_UNION) { valuesSb.setCharAt(valuesSb.length() - 1, ' '); } else { valuesSb.setCharAt(valuesSb.length() - 1, ')'); } String exampleQueryString = sb.toString(); Query q = mainQuery.em.createQuery(exampleQueryString); return q; } String addRoot(EntityType<?> entityType, String rootAlias) { if (rootAlias == null) { // TODO: not sure if other JPA providers support case sensitive queries like hibernate StringBuilder sb = new StringBuilder(entityType.getName()); sb.setCharAt(0, Character.toLowerCase(sb.charAt(0))); String alias = sb.toString(); if (aliasManager.getAliasInfo(alias) == null) { rootAlias = alias; } else { rootAlias = aliasManager.generatePostfixedAlias(alias); } } JoinAliasInfo rootAliasInfo = new JoinAliasInfo(rootAlias, rootAlias, true, true, aliasManager); JoinNode rootNode = JoinNode.createRootNode(entityType, rootAliasInfo); rootAliasInfo.setJoinNode(rootNode); rootNodes.add(rootNode); // register root alias in aliasManager aliasManager.registerAliasInfo(rootAliasInfo); return rootAlias; } String addRoot(String correlationPath, String rootAlias) { Expression expr = expressionFactory.createJoinPathExpression(correlationPath); PathElementExpression elementExpr; EntityType<?> treatEntityType = null; JoinResult result; JoinNode correlationParent = null; if (expr instanceof PathExpression) { PathExpression pathExpression = (PathExpression) expr; if (isJoinableSelectAlias(pathExpression, false, false)) { throw new IllegalArgumentException("No select alias allowed in join path"); } List<PathElementExpression> pathElements = pathExpression.getExpressions(); elementExpr = pathElements.get(pathElements.size() - 1); if (pathElements.size() > 1) { result = implicitJoin(null, pathExpression, null, 0, pathElements.size() - 1, true); correlationParent = result.baseNode; } else { result = new JoinResult(null, null, null); } } else if (expr instanceof TreatExpression) { TreatExpression treatExpression = (TreatExpression) expr; Expression expression = treatExpression.getExpression(); if (expression instanceof PathExpression) { PathExpression pathExpression = (PathExpression) expression; List<PathElementExpression> pathElements = pathExpression.getExpressions(); elementExpr = pathElements.get(pathElements.size() - 1); result = implicitJoin(null, pathExpression, null, 0, pathElements.size() - 1, true); correlationParent = result.baseNode; treatEntityType = metamodel.entity(treatExpression.getType()); } else { throw new IllegalArgumentException("Unexpected expression type[" + expression.getClass().getSimpleName() + "] in treat expression: " + treatExpression); } } else { throw new IllegalArgumentException("Correlation join path [" + correlationPath + "] is not a valid join path"); } if (elementExpr instanceof ArrayExpression) { throw new IllegalArgumentException("Array expressions are not allowed!"); } if (correlationParent == null) { correlationParent = getRootNodeOrFail("Could not join correlation path [", correlationPath, "] because it did not use an absolute path but multiple root nodes are available!"); } if (correlationParent.getAliasInfo().getAliasOwner() == aliasManager) { throw new IllegalArgumentException("The correlation path '" + correlationPath + "' does not seem to be part of a parent query!"); } String correlatedAttribute; Expression correlatedAttributeExpr; if (result.hasField()) { correlatedAttribute = result.joinFields(elementExpr.toString()); correlatedAttributeExpr = expressionFactory.createSimpleExpression(correlatedAttribute, false); } else { correlatedAttribute = elementExpr.toString(); correlatedAttributeExpr = elementExpr; } AttributeHolder joinResult = JpaUtils.getAttributeForJoining(metamodel, correlationParent.getType(), correlatedAttributeExpr, null); Class<?> attributeType = joinResult.getAttributeJavaType(); if (rootAlias == null) { StringBuilder sb = new StringBuilder(attributeType.getSimpleName()); sb.setCharAt(0, Character.toLowerCase(sb.charAt(0))); String alias = sb.toString(); if (aliasManager.getAliasInfo(alias) == null) { rootAlias = alias; } else { rootAlias = aliasManager.generatePostfixedAlias(alias); } } ManagedType<?> managedType = metamodel.managedType(attributeType); JoinAliasInfo rootAliasInfo = new JoinAliasInfo(rootAlias, rootAlias, true, true, aliasManager); JoinNode rootNode = JoinNode.createCorrelationRootNode(correlationParent, correlatedAttribute, managedType, treatEntityType, rootAliasInfo); rootAliasInfo.setJoinNode(rootNode); rootNodes.add(rootNode); // register root alias in aliasManager aliasManager.registerAliasInfo(rootAliasInfo); return rootAlias; } void removeRoot() { // We only use this to remove implicit root nodes JoinNode rootNode = rootNodes.remove(0); aliasManager.unregisterAliasInfoForBottomLevel(rootNode.getAliasInfo()); } JoinNode getRootNodeOrFail(String string) { return getRootNodeOrFail(string, "", ""); } JoinNode getRootNodeOrFail(String prefix, Object middle, String suffix) { if (rootNodes.size() > 1) { throw new IllegalArgumentException(prefix + middle + suffix); } return rootNodes.get(0); } JoinNode getRootNode(Expression expression) { String alias; if (expression instanceof PropertyExpression) { alias = expression.toString(); } else { return null; } List<JoinNode> nodes = rootNodes; int size = nodes.size(); for (int i = 0; i < size; i++) { JoinNode node = nodes.get(i); if (alias.equals(node.getAliasInfo().getAlias())) { return node; } } return null; } public List<JoinNode> getRoots() { return rootNodes; } boolean hasCollections() { List<JoinNode> nodes = rootNodes; int size = nodes.size(); for (int i = 0; i < size; i++) { if (nodes.get(i).hasCollections()) { return true; } } return false; } boolean hasJoins() { List<JoinNode> nodes = rootNodes; int size = nodes.size(); for (int i = 0; i < size; i++) { JoinNode n = nodes.get(i); if (!n.getNodes().isEmpty() || !n.getEntityJoinNodes().isEmpty()) { return true; } if (!n.getTreatedJoinNodes().isEmpty()) { for (JoinNode treatedNode : n.getTreatedJoinNodes().values()) { if (!treatedNode.getNodes().isEmpty() || !treatedNode.getEntityJoinNodes().isEmpty()) { return true; } } } } return false; } boolean hasEntityFunctions() { return entityFunctionNodes.size() > 0; } public Set<JoinNode> getCollectionJoins() { if (rootNodes.isEmpty()) { return Collections.EMPTY_SET; } else { Set<JoinNode> collectionJoins = rootNodes.get(0).getCollectionJoins(); for (int i = 1; i < rootNodes.size(); i++) { collectionJoins.addAll(rootNodes.get(i).getCollectionJoins()); } return collectionJoins; } } Set<JoinNode> getEntityFunctionNodes() { return entityFunctionNodes; } public JoinManager getParent() { return parent; } public SubqueryInitiatorFactory getSubqueryInitFactory() { return subqueryInitFactory; } Set<JoinNode> buildClause(StringBuilder sb, Set<ClauseType> clauseExclusions, String aliasPrefix, boolean collectCollectionJoinNodes, boolean externalRepresenation, List<String> whereConjuncts, Map<Class<?>, Map<String, DbmsModificationState>> explicitVersionEntities, Set<JoinNode> nodesToFetch) { final boolean renderFetches = !clauseExclusions.contains(ClauseType.SELECT); StringBuilder tempSb = null; collectionJoinNodes.clear(); renderedJoins.clear(); sb.append(" FROM "); // TODO: we might have dependencies to other from clause elements which should also be accounted for List<JoinNode> nodes = rootNodes; int size = nodes.size(); for (int i = 0; i < size; i++) { if (i != 0) { sb.append(", "); } JoinNode rootNode = nodes.get(i); JoinNode correlationParent = rootNode.getCorrelationParent(); if (externalRepresenation && rootNode.getValueCount() > 0) { ManagedType<?> type = rootNode.getManagedType(); final int attributeCount = rootNode.getAttributeCount(); if (type.getJavaType() != ValuesEntity.class) { if (type instanceof EntityType<?>) { sb.append(((EntityType) type).getName()); } else { sb.append(type.getJavaType().getSimpleName()); } } sb.append("(VALUES"); for (int valueNumber = 0; valueNumber < rootNode.getValueCount(); valueNumber++) { sb.append(" ("); for (int j = 0; j < attributeCount; j++) { sb.append("?,"); } sb.setCharAt(sb.length() - 1, ')'); sb.append(','); } sb.setCharAt(sb.length() - 1, ')'); } else if (externalRepresenation && explicitVersionEntities.get(rootNode.getType()) != null) { DbmsModificationState state = explicitVersionEntities.get(rootNode.getType()).get(rootNode.getAlias()); EntityType<?> type = rootNode.getEntityType(); if (state == DbmsModificationState.NEW) { sb.append("NEW("); } else { sb.append("OLD("); } sb.append(type.getName()); sb.append(')'); } else { if (correlationParent != null) { renderCorrelationJoinPath(sb, correlationParent.getAliasInfo(), rootNode); } else { EntityType<?> type = rootNode.getEntityType(); sb.append(type.getName()); } } sb.append(' '); if (aliasPrefix != null) { sb.append(aliasPrefix); } sb.append(rootNode.getAliasInfo().getAlias()); renderedJoins.add(rootNode); // TODO: not sure if needed since applyImplicitJoins will already invoke that rootNode.registerDependencies(); applyJoins(sb, rootNode.getAliasInfo(), rootNode.getNodes(), clauseExclusions, aliasPrefix, collectCollectionJoinNodes, renderFetches, nodesToFetch, whereConjuncts); for (JoinNode treatedNode : rootNode.getTreatedJoinNodes().values()) { applyJoins(sb, treatedNode.getAliasInfo(), treatedNode.getNodes(), clauseExclusions, aliasPrefix, collectCollectionJoinNodes, renderFetches, nodesToFetch, whereConjuncts); } if (!rootNode.getEntityJoinNodes().isEmpty()) { // TODO: Fix this with #216 boolean isCollection = true; if (mainQuery.jpaProvider.supportsEntityJoin()) { applyJoins(sb, rootNode.getAliasInfo(), new ArrayList<JoinNode>(rootNode.getEntityJoinNodes()), isCollection, clauseExclusions, aliasPrefix, collectCollectionJoinNodes, renderFetches, nodesToFetch, whereConjuncts); } else { Set<JoinNode> entityNodes = rootNode.getEntityJoinNodes(); for (JoinNode entityNode : entityNodes) { // Collect the join nodes referring to collections if (collectCollectionJoinNodes && isCollection) { collectionJoinNodes.add(entityNode); } sb.append(", "); EntityType<?> type = entityNode.getEntityType(); sb.append(type.getName()); sb.append(' '); if (aliasPrefix != null) { sb.append(aliasPrefix); } sb.append(entityNode.getAliasInfo().getAlias()); // TODO: not sure if needed since applyImplicitJoins will already invoke that entityNode.registerDependencies(); if (entityNode.getOnPredicate() != null && !entityNode.getOnPredicate().getChildren().isEmpty()) { if (tempSb == null) { tempSb = new StringBuilder(); } else { tempSb.setLength(0); } queryGenerator.setClauseType(ClauseType.JOIN); queryGenerator.setQueryBuffer(tempSb); SimpleQueryGenerator.BooleanLiteralRenderingContext oldBooleanLiteralRenderingContext = queryGenerator.setBooleanLiteralRenderingContext(SimpleQueryGenerator.BooleanLiteralRenderingContext.PREDICATE); queryGenerator.generate(entityNode.getOnPredicate()); queryGenerator.setBooleanLiteralRenderingContext(oldBooleanLiteralRenderingContext); queryGenerator.setClauseType(null); whereConjuncts.add(tempSb.toString()); } renderedJoins.add(entityNode); applyJoins(sb, entityNode.getAliasInfo(), entityNode.getNodes(), clauseExclusions, aliasPrefix, collectCollectionJoinNodes, renderFetches, nodesToFetch, whereConjuncts); for (JoinNode treatedNode : entityNode.getTreatedJoinNodes().values()) { applyJoins(sb, treatedNode.getAliasInfo(), treatedNode.getNodes(), clauseExclusions, aliasPrefix, collectCollectionJoinNodes, renderFetches, nodesToFetch, whereConjuncts); } } } } } return collectionJoinNodes; } void verifyBuilderEnded() { joinOnBuilderListener.verifyBuilderEnded(); } void acceptVisitor(JoinNodeVisitor v) { List<JoinNode> nodes = rootNodes; int size = nodes.size(); for (int i = 0; i < size; i++) { nodes.get(i).accept(v); } } public boolean acceptVisitor(Expression.ResultVisitor<Boolean> aggregateDetector, boolean stopValue) { Boolean stop = Boolean.valueOf(stopValue); List<JoinNode> nodes = rootNodes; int size = nodes.size(); for (int i = 0; i < size; i++) { if (stop.equals(nodes.get(i).accept(new AbortableOnClauseJoinNodeVisitor(aggregateDetector, stopValue)))) { return true; } } return false; } @Override public void apply(ExpressionModifierVisitor<? super ExpressionModifier> visitor) { List<JoinNode> nodes = rootNodes; int size = nodes.size(); for (int i = 0; i < size; i++) { nodes.get(i).accept(visitor); } } private void renderJoinNode(StringBuilder sb, JoinAliasInfo joinBase, JoinNode node, String aliasPrefix, boolean renderFetches, Set<JoinNode> nodesToFetch, List<String> whereConjuncts) { if (!renderedJoins.contains(node)) { // We determine the nodes that should be fetched by analyzing the fetch owners during implicit joining final boolean fetch = nodesToFetch.contains(node) && renderFetches; // Don't render key joins unless fetching is specified on it if (node.isQualifiedJoin() && !fetch) { renderedJoins.add(node); return; } // We only render treat joins, but not treated join nodes. These treats are just "optional casts" that don't affect joining if (node.isTreatedJoinNode()) { renderedJoins.add(node); return; } switch (node.getJoinType()) { case INNER: sb.append(" JOIN "); break; case LEFT: sb.append(" LEFT JOIN "); break; case RIGHT: sb.append(" RIGHT JOIN "); break; default: throw new IllegalArgumentException("Unknown join type: " + node.getJoinType()); } if (fetch) { sb.append("FETCH "); } if (aliasPrefix != null) { sb.append(aliasPrefix); } String onCondition = renderJoinPath(sb, joinBase, node, whereConjuncts); sb.append(' '); if (aliasPrefix != null) { sb.append(aliasPrefix); } sb.append(node.getAliasInfo().getAlias()); renderedJoins.add(node); if (node.getOnPredicate() != null && !node.getOnPredicate().getChildren().isEmpty()) { sb.append(joinRestrictionKeyword); // Always render the ON condition in parenthesis to workaround an EclipseLink bug in entity join parsing sb.append('('); if (onCondition != null) { sb.append(onCondition).append(" AND "); } queryGenerator.setClauseType(ClauseType.JOIN); queryGenerator.setQueryBuffer(sb); SimpleQueryGenerator.BooleanLiteralRenderingContext oldBooleanLiteralRenderingContext = queryGenerator.setBooleanLiteralRenderingContext(SimpleQueryGenerator.BooleanLiteralRenderingContext.PREDICATE); queryGenerator.setRenderedJoinNodes(renderedJoins); queryGenerator.generate(node.getOnPredicate()); queryGenerator.setRenderedJoinNodes(null); queryGenerator.setBooleanLiteralRenderingContext(oldBooleanLiteralRenderingContext); queryGenerator.setClauseType(null); sb.append(')'); } else if (onCondition != null) { sb.append(joinRestrictionKeyword); sb.append('('); sb.append(onCondition); sb.append(')'); } } } private void renderCorrelationJoinPath(StringBuilder sb, JoinAliasInfo joinBase, JoinNode node) { if (node.getTreatType() != null) { final boolean renderTreat = mainQuery.jpaProvider.supportsTreatJoin() && (!mainQuery.jpaProvider.supportsSubtypeRelationResolving() || node.getJoinType() == JoinType.INNER); if (renderTreat) { sb.append("TREAT("); renderAlias(sb, joinBase.getJoinNode(), mainQuery.jpaProvider.supportsRootTreat()); sb.append('.'); sb.append(node.getCorrelationPath()); sb.append(" AS "); sb.append(node.getTreatType().getName()); sb.append(')'); } else if (mainQuery.jpaProvider.supportsSubtypeRelationResolving()) { sb.append(joinBase.getAlias()).append('.').append(node.getCorrelationPath()); } else { throw new IllegalArgumentException("Treat should not be used as the JPA provider does not support subtype property access!"); } } else { JoinNode baseNode = joinBase.getJoinNode(); if (baseNode.getTreatType() != null) { if (mainQuery.jpaProvider.supportsRootTreatJoin()) { baseNode.appendAlias(sb, true); } else if (mainQuery.jpaProvider.supportsSubtypeRelationResolving()) { baseNode.appendAlias(sb, false); } else { throw new IllegalArgumentException("Treat should not be used as the JPA provider does not support subtype property access!"); } } else { baseNode.appendAlias(sb, false); } sb.append('.').append(node.getCorrelationPath()); } } private String renderJoinPath(StringBuilder sb, JoinAliasInfo joinBase, JoinNode node, List<String> whereConjuncts) { if (node.getTreatType() != null) { // We render the treat join only if it makes sense. If we have e.g. a left join and the provider supports // implicit relation resolving then there is no point in rendering the treat join. On the contrary, that might lead to wrong results final boolean renderTreat = mainQuery.jpaProvider.supportsTreatJoin() && (!mainQuery.jpaProvider.supportsSubtypeRelationResolving() || node.getJoinType() == JoinType.INNER); final String onCondition; final JoinNode baseNode = joinBase.getJoinNode(); final String treatType = node.getTreatType().getName(); final String relationName = node.getParentTreeNode().getRelationName(); JpaProvider.ConstraintType constraintType = mainQuery.jpaProvider.requiresTreatFilter(baseNode.getManagedType(), relationName, node.getJoinType()); if (constraintType != JpaProvider.ConstraintType.NONE) { String constraint = "TYPE(" + node.getAlias() + ") = " + treatType; if (constraintType == JpaProvider.ConstraintType.WHERE) { whereConjuncts.add(constraint); onCondition = null; } else { onCondition = constraint; } } else { onCondition = null; } if (renderTreat) { sb.append("TREAT("); renderAlias(sb, baseNode, mainQuery.jpaProvider.supportsRootTreatTreatJoin()); sb.append('.'); sb.append(relationName); sb.append(" AS "); sb.append(treatType); sb.append(')'); } else if (mainQuery.jpaProvider.supportsSubtypeRelationResolving()) { sb.append(joinBase.getAlias()).append('.').append(node.getParentTreeNode().getRelationName()); } else { throw new IllegalArgumentException("Treat should not be used as the JPA provider does not support subtype property access!"); } return onCondition; } else if (node.getCorrelationPath() == null && node.getAliasInfo().isRootNode()) { sb.append(node.getEntityType().getName()); } else if (node.isQualifiedJoin()) { sb.append(node.getQualificationExpression()); sb.append('('); sb.append(joinBase.getJoinNode().getAlias()); sb.append(')'); } else { renderAlias(sb, joinBase.getJoinNode(), mainQuery.jpaProvider.supportsRootTreatJoin()); sb.append('.').append(node.getParentTreeNode().getRelationName()); } return null; } private void renderAlias(StringBuilder sb, JoinNode baseNode, boolean supportsTreat) { if (baseNode.getTreatType() != null) { if (supportsTreat) { baseNode.appendAlias(sb, true); } else if (mainQuery.jpaProvider.supportsSubtypeRelationResolving()) { baseNode.appendAlias(sb, false); } else { throw new IllegalArgumentException("Treat should not be used as the JPA provider does not support subtype property access!"); } } else { baseNode.appendAlias(sb, false); } } private void renderReverseDependency(StringBuilder sb, JoinNode dependency, String aliasPrefix, boolean renderFetches, Set<JoinNode> nodesToFetch, List<String> whereConjuncts) { if (dependency.getParent() != null) { renderReverseDependency(sb, dependency.getParent(), aliasPrefix, renderFetches, nodesToFetch, whereConjuncts); if (!dependency.getDependencies().isEmpty()) { markedJoinNodes.add(dependency); try { for (JoinNode dep : dependency.getDependencies()) { if (markedJoinNodes.contains(dep)) { throw new IllegalStateException("Cyclic join dependency detected at absolute path [" + dep.getAliasInfo().getAbsolutePath() + "] with alias [" + dep.getAliasInfo().getAlias() + "]"); } // render reverse dependencies renderReverseDependency(sb, dep, aliasPrefix, renderFetches, nodesToFetch, whereConjuncts); } } finally { markedJoinNodes.remove(dependency); } } renderJoinNode(sb, dependency.getParent().getAliasInfo(), dependency, aliasPrefix, renderFetches, nodesToFetch, whereConjuncts); } } private void applyJoins(StringBuilder sb, JoinAliasInfo joinBase, Map<String, JoinTreeNode> nodes, Set<ClauseType> clauseExclusions, String aliasPrefix, boolean collectCollectionJoinNodes, boolean renderFetches, Set<JoinNode> nodesToFetch, List<String> whereConjuncts) { for (Map.Entry<String, JoinTreeNode> nodeEntry : nodes.entrySet()) { JoinTreeNode treeNode = nodeEntry.getValue(); List<JoinNode> stack = new ArrayList<JoinNode>(); stack.addAll(treeNode.getJoinNodes().descendingMap().values()); applyJoins(sb, joinBase, stack, treeNode.isCollection(), clauseExclusions, aliasPrefix, collectCollectionJoinNodes, renderFetches, nodesToFetch, whereConjuncts); } } private void applyJoins(StringBuilder sb, JoinAliasInfo joinBase, List<JoinNode> stack, boolean isCollection, Set<ClauseType> clauseExclusions, String aliasPrefix, boolean collectCollectionJoinNodes, boolean renderFetches, Set<JoinNode> nodesToFetch, List<String> whereConjuncts) { while (!stack.isEmpty()) { JoinNode node = stack.remove(stack.size() - 1); // If the clauses in which a join node occurs are all excluded or the join node is not mandatory for the cardinality, we skip it if (!clauseExclusions.isEmpty() && clauseExclusions.containsAll(node.getClauseDependencies()) && !node.isCardinalityMandatory()) { continue; } stack.addAll(node.getEntityJoinNodes()); stack.addAll(node.getTreatedJoinNodes().values()); // We have to render any dependencies this join node has before actually rendering itself if (!node.getDependencies().isEmpty()) { renderReverseDependency(sb, node, aliasPrefix, renderFetches, nodesToFetch, whereConjuncts); } // Collect the join nodes referring to collections if (collectCollectionJoinNodes && isCollection) { collectionJoinNodes.add(node); } // Finally render this join node renderJoinNode(sb, joinBase, node, aliasPrefix, renderFetches, nodesToFetch, whereConjuncts); // Render child nodes recursively if (!node.getNodes().isEmpty()) { applyJoins(sb, node.getAliasInfo(), node.getNodes(), clauseExclusions, aliasPrefix, collectCollectionJoinNodes, renderFetches, nodesToFetch, whereConjuncts); } } } private boolean isExternal(PathExpression path) { PathElementExpression firstElem = path.getExpressions().get(0); return isExternal(path, firstElem); } private boolean isExternal(TreatExpression treatExpression) { Expression expression = treatExpression.getExpression(); if (expression instanceof PathExpression) { PathExpression path = (PathExpression) expression; PathElementExpression firstElem = path.getExpressions().get(0); return isExternal(path, firstElem); } else if (expression instanceof FunctionExpression) { // Can only be key or value function PathExpression path = (PathExpression) ((FunctionExpression) expression).getExpressions().get(0); PathElementExpression firstElem = path.getExpressions().get(0); return isExternal(path, firstElem); } else { throw new IllegalArgumentException("Unexpected expression type[" + expression.getClass().getSimpleName() + "] in treat expression: " + treatExpression); } } private boolean isExternal(PathExpression path, PathElementExpression firstElem) { String startAlias; if (firstElem instanceof ArrayExpression) { startAlias = ((ArrayExpression) firstElem).getBase().toString(); } else if (firstElem instanceof TreatExpression) { Expression treatedExpression = ((TreatExpression) firstElem).getExpression(); if (treatedExpression instanceof PathExpression) { treatedExpression = ((PathExpression) treatedExpression).getExpressions().get(0); } if (treatedExpression instanceof ArrayExpression) { startAlias = ((ArrayExpression) treatedExpression).getBase().toString(); } else if (treatedExpression instanceof TreatExpression) { startAlias = ((TreatExpression) treatedExpression).getExpression().toString(); } else { startAlias = treatedExpression.toString(); } } else { startAlias = firstElem.toString(); } AliasInfo aliasInfo = aliasManager.getAliasInfo(startAlias); if (aliasInfo == null) { return false; } if (parent != null && aliasInfo.getAliasOwner() != aliasManager) { // the alias exists but originates from the parent query builder // an external select alias must not be de-referenced if (path.getExpressions().size() > 1) { // But if check if the expression really is just an alias reference or the if (aliasInfo instanceof SelectInfo) { throw new ExternalAliasDereferencingException("Start alias [" + startAlias + "] of path [" + path.toString() + "] is external and must not be dereferenced"); } } // the alias is external so we do not have to treat it return true; } else if (aliasInfo.getAliasOwner() == aliasManager) { // the alias originates from the current query builder and is therefore not external return false; } else { throw new IllegalStateException("Alias [" + aliasInfo.getAlias() + "] originates from an unknown query"); } } public boolean isJoinableSelectAlias(PathExpression pathExpr, boolean fromSelect, boolean fromSubquery) { return getJoinableSelectAlias(pathExpr, fromSelect, fromSubquery) != null; } public Expression getJoinableSelectAlias(PathExpression pathExpr, boolean fromSelect, boolean fromSubquery) { // We can skip this check if the first element is not a simple property if (!(pathExpr.getExpressions().get(0) instanceof PropertyExpression)) { return null; } boolean singlePathElement = pathExpr.getExpressions().size() == 1; String startAlias = pathExpr.getExpressions().get(0).toString(); AliasInfo aliasInfo = aliasManager.getAliasInfo(startAlias); if (aliasInfo == null) { return null; } if (aliasInfo instanceof SelectInfo && !fromSelect && !fromSubquery) { // select alias if (!singlePathElement) { throw new IllegalStateException("Path starting with select alias not allowed"); } // might be joinable Expression expression = ((SelectInfo) aliasInfo).getExpression(); // If the expression the alias refers to and the expression are the same, we are resolving an ambiguous alias expression if (expression == pathExpr) { return null; } return expression; } return null; } <X> JoinOnBuilder<X> joinOn(X result, String base, Class<?> clazz, String alias, JoinType type) { PathExpression basePath = expressionFactory.createPathExpression(base); EntityType<?> entityType = metamodel.entity(clazz); if (alias == null || alias.isEmpty()) { throw new IllegalArgumentException("Invalid empty alias!"); } if (type != JoinType.INNER && !mainQuery.jpaProvider.supportsEntityJoin()) { throw new IllegalArgumentException("The JPA provider does not support entity joins and an emulation for non-inner entity joins is not implemented!"); } List<PathElementExpression> propertyExpressions = basePath.getExpressions(); JoinNode baseNode; if (propertyExpressions.size() > 1) { AliasInfo aliasInfo = aliasManager.getAliasInfo(propertyExpressions.get(0).toString()); if (aliasInfo == null || !(aliasInfo instanceof JoinAliasInfo)) { throw new IllegalArgumentException("The base '" + base + "' is not a valid join alias!"); } baseNode = ((JoinAliasInfo) aliasInfo).getJoinNode(); for (int i = 1; i < propertyExpressions.size(); i++) { String relationName = propertyExpressions.get(i).toString(); JoinTreeNode treeNode = baseNode.getNodes().get(relationName); if (treeNode == null) { break; } baseNode = treeNode.getDefaultNode(); if (baseNode == null) { break; } } if (baseNode == null) { throw new IllegalArgumentException("The base '" + base + "' is not a valid join alias!"); } } else { AliasInfo aliasInfo = aliasManager.getAliasInfo(base); if (aliasInfo == null || !(aliasInfo instanceof JoinAliasInfo)) { throw new IllegalArgumentException("The base '" + base + "' is not a valid join alias!"); } baseNode = ((JoinAliasInfo) aliasInfo).getJoinNode(); } JoinAliasInfo joinAliasInfo = new JoinAliasInfo(alias, null, false, true, aliasManager); JoinNode entityJoinNode = JoinNode.createEntityJoinNode(baseNode, type, entityType, joinAliasInfo); joinAliasInfo.setJoinNode(entityJoinNode); baseNode.addEntityJoin(entityJoinNode); aliasManager.registerAliasInfo(joinAliasInfo); joinOnBuilderListener.joinNode = entityJoinNode; return joinOnBuilderListener.startBuilder(new JoinOnBuilderImpl<X>(result, joinOnBuilderListener, parameterManager, expressionFactory, subqueryInitFactory)); } <X> JoinOnBuilder<X> joinOn(X result, String path, String alias, JoinType type, boolean defaultJoin) { joinOnBuilderListener.joinNode = join(path, alias, type, false, defaultJoin); return joinOnBuilderListener.startBuilder(new JoinOnBuilderImpl<X>(result, joinOnBuilderListener, parameterManager, expressionFactory, subqueryInitFactory)); } JoinNode join(String path, String alias, JoinType type, boolean fetch, boolean defaultJoin) { Expression expr = expressionFactory.createJoinPathExpression(path); PathElementExpression elementExpr; String treatType = null; JoinResult result; JoinNode current; if (expr instanceof PathExpression) { PathExpression pathExpression = (PathExpression) expr; if (isExternal(pathExpression) || isJoinableSelectAlias(pathExpression, false, false)) { throw new IllegalArgumentException("No external path or select alias allowed in join path"); } List<PathElementExpression> pathElements = pathExpression.getExpressions(); elementExpr = pathElements.get(pathElements.size() - 1); result = implicitJoin(null, pathExpression, null, 0, pathElements.size() - 1, false); current = result.baseNode; } else if (expr instanceof TreatExpression) { TreatExpression treatExpression = (TreatExpression) expr; if (isExternal(treatExpression)) { throw new IllegalArgumentException("No external path or select alias allowed in join path"); } Expression expression = treatExpression.getExpression(); if (expression instanceof PathExpression) { PathExpression pathExpression = (PathExpression) expression; List<PathElementExpression> pathElements = pathExpression.getExpressions(); elementExpr = pathElements.get(pathElements.size() - 1); result = implicitJoin(null, pathExpression, null, 0, pathElements.size() - 1, false); current = result.baseNode; treatType = treatExpression.getType(); } else { throw new IllegalArgumentException("Unexpected expression type[" + expression.getClass().getSimpleName() + "] in treat expression: " + treatExpression); } } else { throw new IllegalArgumentException("Join path [" + path + "] is not a path"); } if (elementExpr instanceof ArrayExpression) { throw new IllegalArgumentException("Array expressions are not allowed!"); } else if (elementExpr instanceof MapKeyExpression) { MapKeyExpression mapKeyExpression = (MapKeyExpression) elementExpr; boolean fromSubquery = false; boolean fromSelectAlias = false; boolean joinRequired = true; current = joinMapKey(mapKeyExpression, alias, null, fromSubquery, fromSelectAlias, joinRequired, fetch, false, defaultJoin); result = new JoinResult(current, null, current.getType()); } else { List<String> joinRelationAttributes = result.addToList(new ArrayList<String>()); joinRelationAttributes.add(elementExpr.toString()); current = current == null ? getRootNodeOrFail("Could not join path [", path, "] because it did not use an absolute path but multiple root nodes are available!") : current; result = createOrUpdateNode(current, joinRelationAttributes, treatType, alias, type, false, defaultJoin); } if (fetch) { fetchPath(result.baseNode); } return result.baseNode; } public void implicitJoin(Expression expression, boolean objectLeafAllowed, String targetType, ClauseType fromClause, boolean fromSubquery, boolean fromSelectAlias, boolean joinRequired, boolean idRemovable) { implicitJoin(expression, objectLeafAllowed, targetType, fromClause, fromSubquery, fromSelectAlias, joinRequired, idRemovable, false); } @SuppressWarnings("checkstyle:methodlength") public void implicitJoin(Expression expression, boolean objectLeafAllowed, String targetTypeName, ClauseType fromClause, boolean fromSubquery, boolean fromSelectAlias, boolean joinRequired, boolean idRemovable, boolean fetch) { PathExpression pathExpression; if (expression instanceof PathExpression) { pathExpression = (PathExpression) expression; Expression aliasedExpression; // If joinable select alias, it is guaranteed to have only a single element if ((aliasedExpression = getJoinableSelectAlias(pathExpression, fromClause == ClauseType.SELECT, fromSubquery)) != null) { // this check is necessary to prevent infinite recursion in the case of e.g. SELECT name AS name if (!fromSelectAlias) { // we have to do this implicit join because we might have to adjust the selectOnly flag in the referenced join nodes implicitJoin(aliasedExpression, true, null, fromClause, fromSubquery, true, joinRequired, false); } return; } else if (isExternal(pathExpression)) { // try to set base node and field for the external expression based // on existing joins in the super query parent.implicitJoin(pathExpression, true, targetTypeName, fromClause, true, fromSelectAlias, joinRequired, false); return; } // First try to implicit join indices of array expressions since we will need their base nodes List<PathElementExpression> pathElements = pathExpression.getExpressions(); int pathElementSize = pathElements.size(); for (int i = 0; i < pathElementSize; i++) { PathElementExpression pathElem = pathElements.get(i); if (pathElem instanceof ArrayExpression) { implicitJoin(((ArrayExpression) pathElem).getIndex(), false, null, fromClause, fromSubquery, fromSelectAlias, joinRequired, false); } } PathElementExpression elementExpr = pathElements.get(pathElements.size() - 1); boolean singleValuedAssociationIdExpression = false; JoinNode current = null; List<String> resultFields = new ArrayList<String>(); JoinResult currentResult; JoinNode possibleRoot; int startIndex = 0; // Skip root speculation if this is just a single element path if (pathElements.size() > 1 && (possibleRoot = getRootNode(pathElements.get(0))) != null) { startIndex = 1; current = possibleRoot; } if (pathElements.size() > startIndex + 1) { int maybeSingularAssociationIndex = pathElements.size() - 2; int maybeSingularAssociationIdIndex = pathElements.size() - 1; currentResult = implicitJoin(current, pathExpression, fromClause, startIndex, maybeSingularAssociationIndex, false); current = currentResult.baseNode; resultFields = currentResult.addToList(resultFields); singleValuedAssociationIdExpression = isSingleValuedAssociationId(currentResult, pathElements, idRemovable); if (singleValuedAssociationIdExpression) { if (!mainQuery.jpaProvider.supportsSingleValuedAssociationIdExpressions()) { if (idRemovable) { // remove the id part only if we come from a predicate elementExpr = null; if (current == null) { // This is the case when we use a join alias like "alias.id" // We need to resolve the base since it might not be the root node AliasInfo a = aliasManager.getAliasInfo(pathElements.get(maybeSingularAssociationIndex).toString()); // We know this can only be a join node alias current = ((JoinAliasInfo) a).getJoinNode(); resultFields = Collections.emptyList(); } } else { // Need a normal join currentResult = implicitJoin(current, pathExpression, fromClause, maybeSingularAssociationIndex, pathElements.size() - 1, false); current = currentResult.baseNode; resultFields = currentResult.addToList(resultFields); singleValuedAssociationIdExpression = false; } } } else { if (currentResult.hasField()) { // currentResult.typeName? // Redo the joins for embeddables by moving the start index back currentResult = implicitJoin(current, pathExpression, fromClause, maybeSingularAssociationIndex - currentResult.fields.size(), maybeSingularAssociationIdIndex, false); if (currentResult.fields != resultFields) { resultFields.clear(); } } else { currentResult = implicitJoin(current, pathExpression, fromClause, maybeSingularAssociationIndex, maybeSingularAssociationIdIndex, false); } current = currentResult.baseNode; resultFields = currentResult.addToList(resultFields); } } else { // Single element expression like "alias", "relation", "property" or "alias.relation" currentResult = implicitJoin(current, pathExpression, fromClause, startIndex, pathElements.size() - 1, false); current = currentResult.baseNode; resultFields = currentResult.addToList(resultFields); if (idRemovable) { if (current != null) { // If there is a "base node" i.e. a current, the expression has 2 elements if (isId(current.getType(), elementExpr)) { // We remove the "id" part elementExpr = null; // Treat it like a single valued association id expression singleValuedAssociationIdExpression = true; } } else { // There is no base node, this is a expression with 1 element // Either relative or a direct alias String elementExpressionString; if (elementExpr instanceof ArrayExpression) { elementExpressionString = ((ArrayExpression) elementExpr).getBase().toString(); } else { elementExpressionString = elementExpr.toString(); } AliasInfo a = aliasManager.getAliasInfo(elementExpressionString); if (a == null) { // If the element expression is an alias, there is nothing to replace current = getRootNodeOrFail("Could not join path [", expression, "] because it did not use an absolute path but multiple root nodes are available!"); if (isId(current.getType(), elementExpr)) { // We replace the "id" part with the alias elementExpr = new PropertyExpression(current.getAlias()); } } } } } JoinResult result; AliasInfo aliasInfo; // The case of a simple join alias usage if (pathElements.size() == 1 && !fromSelectAlias && (aliasInfo = aliasManager.getAliasInfoForBottomLevel(elementExpr.toString())) != null) { // No need to assert the resultFields here since they can't appear anyways if we enter this branch if (aliasInfo instanceof SelectInfo) { if (targetTypeName != null) { throw new IllegalArgumentException("The select alias '" + aliasInfo.getAlias() + "' can not be used for a treat expression!."); } // We actually allow usage of select aliases in expressions, but JPA doesn't, so we have to resolve them here Expression selectExpr = ((SelectInfo) aliasInfo).getExpression(); if (!(selectExpr instanceof PathExpression)) { throw new RuntimeException("The select expression '" + selectExpr.toString() + "' is not a simple path expression! No idea how to implicit join that."); } // join the expression behind a select alias once when it is encountered the first time if (((PathExpression) selectExpr).getBaseNode() == null) { implicitJoin(selectExpr, objectLeafAllowed, null, fromClause, fromSubquery, true, joinRequired, false); } PathExpression selectPathExpr = (PathExpression) selectExpr; PathReference reference = selectPathExpr.getPathReference(); result = new JoinResult((JoinNode) selectPathExpr.getBaseNode(), Arrays.asList(selectPathExpr.getField()), reference.getType()); } else { JoinNode pathJoinNode = ((JoinAliasInfo) aliasInfo).getJoinNode(); if (targetTypeName != null) { // Treated root path ManagedType<?> targetType = metamodel.managedType(targetTypeName); result = new JoinResult(pathJoinNode, null, targetType.getJavaType()); } else { // Naked join alias usage like in "KEY(joinAlias)" result = new JoinResult(pathJoinNode, null, pathJoinNode.getType()); } } } else if (pathElements.size() == 1 && elementExpr instanceof QualifiedExpression) { QualifiedExpression qualifiedExpression = (QualifiedExpression) elementExpr; JoinNode baseNode; if (elementExpr instanceof MapKeyExpression) { baseNode = joinMapKey((MapKeyExpression) elementExpr, null, fromClause, fromSubquery, fromSelectAlias, true, fetch, true, true); } else if (elementExpr instanceof ListIndexExpression) { baseNode = joinListIndex((ListIndexExpression) elementExpr, null, fromClause, fromSubquery, fromSelectAlias, true, fetch, true, true); } else if (elementExpr instanceof MapEntryExpression) { baseNode = joinMapEntry((MapEntryExpression) elementExpr, null, fromClause, fromSubquery, fromSelectAlias, true, fetch, true, true); } else if (elementExpr instanceof MapValueExpression) { implicitJoin(qualifiedExpression.getPath(), objectLeafAllowed, targetTypeName, fromClause, fromSubquery, fromSelectAlias, joinRequired, false, fetch); baseNode = (JoinNode) qualifiedExpression.getPath().getBaseNode(); } else { throw new IllegalArgumentException("Unknown qualified expression type: " + elementExpr); } result = new JoinResult(baseNode, null, baseNode.getType()); } else { // current might be null if (current == null) { current = getRootNodeOrFail("Could not join path [", expression, "] because it did not use an absolute path but multiple root nodes are available!"); } if (singleValuedAssociationIdExpression) { String associationName = pathElements.get(pathElements.size() - 2).toString(); AliasInfo singleValuedAssociationRootAliasInfo = null; JoinTreeNode treeNode; if (currentResult.hasField()) { associationName = currentResult.joinFields(associationName); } else if (pathElements.size() == 2) { // If this path is composed of only two elements, the association name could represent an alias singleValuedAssociationRootAliasInfo = aliasManager.getAliasInfoForBottomLevel(associationName); } if (singleValuedAssociationRootAliasInfo != null) { JoinNode singleValuedAssociationRoot = ((JoinAliasInfo) singleValuedAssociationRootAliasInfo).getJoinNode(); if (elementExpr != null) { AttributeHolder attributeHolder = JpaUtils.getAttributeForJoining( metamodel, singleValuedAssociationRoot.getType(), elementExpr, singleValuedAssociationRoot.getAlias() ); Class<?> type = attributeHolder.getAttributeJavaType(); result = new JoinResult(singleValuedAssociationRoot, Arrays.asList(elementExpr.toString()), type); } else { result = new JoinResult(singleValuedAssociationRoot, null, singleValuedAssociationRoot.getType()); } } else { treeNode = current.getNodes().get(associationName); if (treeNode != null && treeNode.getDefaultNode() != null) { if (elementExpr != null) { AttributeHolder attributeHolder = JpaUtils.getAttributeForJoining( metamodel, treeNode.getDefaultNode().getType(), elementExpr, treeNode.getDefaultNode().getAlias() ); Class<?> type = attributeHolder.getAttributeJavaType(); result = new JoinResult(treeNode.getDefaultNode(), Arrays.asList(elementExpr.toString()), type); } else { result = new JoinResult(treeNode.getDefaultNode(), null, treeNode.getDefaultNode().getType()); } } else { if (elementExpr != null) { String elementString = elementExpr.toString(); Expression resultExpr = expressionFactory.createSimpleExpression(associationName + '.' + elementString, false); AttributeHolder attributeHolder = JpaUtils.getAttributeForJoining( metamodel, current.getType(), resultExpr, current.getAlias() ); Class<?> type = attributeHolder.getAttributeJavaType(); result = new JoinResult(current, Arrays.asList(associationName, elementString), type); } else { Expression resultExpr = expressionFactory.createSimpleExpression(associationName, false); AttributeHolder attributeHolder = JpaUtils.getAttributeForJoining( metamodel, current.getType(), resultExpr, current.getAlias() ); Class<?> type = attributeHolder.getAttributeJavaType(); result = new JoinResult(current, Arrays.asList(associationName), type); } } } } else if (elementExpr instanceof ArrayExpression) { // TODO: Not sure if necessary if (!resultFields.isEmpty()) { throw new IllegalArgumentException("The join path [" + pathExpression + "] has a non joinable part [" + StringUtils.join(".", resultFields) + "]"); } ArrayExpression arrayExpr = (ArrayExpression) elementExpr; String joinRelationName = arrayExpr.getBase().toString(); // Find a node by a predicate match JoinNode matchingNode; if (pathElements.size() == 1 && (aliasInfo = aliasManager.getAliasInfoForBottomLevel(joinRelationName)) != null) { // The first node is allowed to be a join alias if (aliasInfo instanceof SelectInfo) { throw new IllegalArgumentException("Illegal reference to the select alias '" + joinRelationName + "'"); } current = ((JoinAliasInfo) aliasInfo).getJoinNode(); generateAndApplyOnPredicate(current, arrayExpr); } else if ((matchingNode = findNode(current, joinRelationName, arrayExpr)) != null) { // We found a join node for the same join relation with the same array expression predicate current = matchingNode; } else { String joinAlias = getJoinAlias(arrayExpr); currentResult = createOrUpdateNode(current, Arrays.asList(joinRelationName), null, joinAlias, null, true, false); current = currentResult.baseNode; // TODO: Not sure if necessary if (currentResult.hasField()) { throw new IllegalArgumentException("The join path [" + pathExpression + "] has a non joinable part [" + currentResult.joinFields() + "]"); } generateAndApplyOnPredicate(current, arrayExpr); } result = new JoinResult(current, null, current.getType()); } else if (!pathExpression.isUsedInCollectionFunction()) { if (resultFields.isEmpty()) { result = implicitJoinSingle(current, elementExpr.toString(), objectLeafAllowed, joinRequired); } else { resultFields.add(elementExpr.toString()); String attributeName = StringUtils.join(".", resultFields); // Validates and gets the path type getPathType(current.getType(), attributeName, pathExpression); result = implicitJoinSingle(current, attributeName, objectLeafAllowed, joinRequired); } } else { if (resultFields.isEmpty()) { String attributeName = elementExpr.toString(); Class<?> type = getPathType(current.getType(), attributeName, pathExpression); result = new JoinResult(current, Arrays.asList(attributeName), type); } else { resultFields.add(elementExpr.toString()); String attributeName = StringUtils.join(".", resultFields); Class<?> type = getPathType(current.getType(), attributeName, pathExpression); result = new JoinResult(current, resultFields, type); } } } if (fetch) { fetchPath(result.baseNode); } // Don't forget to update the clause dependencies!! if (fromClause != null) { updateClauseDependencies(result.baseNode, fromClause, new HashSet<JoinNode>()); } if (result.isLazy()) { pathExpression.setPathReference(new LazyPathReference(result.baseNode, result.joinFields(), result.type)); } else { pathExpression.setPathReference(new SimplePathReference(result.baseNode, result.joinFields(), result.type)); } } else if (expression instanceof FunctionExpression) { List<Expression> expressions = ((FunctionExpression) expression).getExpressions(); int size = expressions.size(); for (int i = 0; i < size; i++) { implicitJoin(expressions.get(i), objectLeafAllowed, null, fromClause, fromSubquery, fromSelectAlias, joinRequired, false); } } else if (expression instanceof MapKeyExpression) { MapKeyExpression mapKeyExpression = (MapKeyExpression) expression; joinMapKey(mapKeyExpression, null, fromClause, fromSubquery, fromSelectAlias, joinRequired, fetch, true, true); } else if (expression instanceof QualifiedExpression) { implicitJoin(((QualifiedExpression) expression).getPath(), objectLeafAllowed, null, fromClause, fromSubquery, fromSelectAlias, joinRequired, false); } else if (expression instanceof ArrayExpression || expression instanceof GeneralCaseExpression || expression instanceof TreatExpression) { // TODO: Having a treat expression actually makes sense here for fetchOnly // NOTE: I haven't found a use case for this yet, so I'd like to throw an exception instead of silently not supporting this throw new IllegalArgumentException("Unsupported expression for implicit joining found: " + expression); } else { // Other expressions don't need handling } } private JoinNode getFetchOwner(JoinNode node) { while (node.isFetch()) { node = node.getParent(); } return node; } private static class LazyPathReference implements PathReference { private final JoinNode baseNode; private final String field; private final Class<?> type; public LazyPathReference(JoinNode baseNode, String field, Class<?> type) { this.baseNode = baseNode; this.field = field; this.type = type; } @Override public JoinNode getBaseNode() { JoinTreeNode subNode = baseNode.getNodes().get(field); if (subNode != null && subNode.getDefaultNode() != null) { return subNode.getDefaultNode(); } return baseNode; } @Override public String getField() { JoinTreeNode subNode = baseNode.getNodes().get(field); if (subNode != null && subNode.getDefaultNode() != null) { return null; } return field; } @Override public Class<?> getType() { return type; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((baseNode == null) ? 0 : baseNode.hashCode()); result = prime * result + ((field == null) ? 0 : field.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null) { return false; } if (!(obj instanceof PathReference)) { return false; } PathReference other = (PathReference) obj; if (baseNode == null) { if (other.getBaseNode() != null) { return false; } } else if (!baseNode.equals(other.getBaseNode())) { return false; } if (field == null) { if (other.getField() != null) { return false; } } else if (!field.equals(other.getField())) { return false; } return true; } } private Class<?> getPathType(Class<?> baseType, String expression, PathExpression pathExpression) { try { return JpaUtils.getAttributeForJoining(metamodel, baseType, expressionFactory.createPathExpression(expression), null).getAttributeJavaType(); } catch (IllegalArgumentException ex) { throw new IllegalArgumentException("The join path [" + pathExpression + "] has a non joinable part [" + expression + "]"); } } private boolean isSingleValuedAssociationId(JoinResult joinResult, List<PathElementExpression> pathElements, boolean idRemovable) { JoinNode parent = joinResult.baseNode; int maybeSingularAssociationIndex = pathElements.size() - 2; int maybeSingularAssociationIdIndex = pathElements.size() - 1; Type<?> baseType; AttributeHolder maybeSingularAssociationJoinResult; PathElementExpression maybeSingularAssociationNameExpression = pathElements.get(maybeSingularAssociationIndex); String maybeSingularAssociationName = getSimpleName(maybeSingularAssociationNameExpression); if (parent == null) { // This is the case when we have exactly 2 path elements if (maybeSingularAssociationNameExpression instanceof TreatExpression) { // When we dereference a treat expression, we simply say this can never be a single valued association id return false; } else { AliasInfo a = aliasManager.getAliasInfo(maybeSingularAssociationName); if (a == null) { // if the path element is no alias we can do some optimizations parent = getRootNodeOrFail("Ambiguous join path [", maybeSingularAssociationName, "] because of multiple root nodes!"); baseType = parent.getManagedType(); maybeSingularAssociationJoinResult = JpaUtils.getAttributeForJoining(metamodel, baseType.getJavaType(), maybeSingularAssociationNameExpression, parent.getAlias()); } else if (!(a instanceof JoinAliasInfo)) { throw new IllegalArgumentException("Can't dereference select alias in the expression!"); } else { // If there is a JoinAliasInfo for the path element, we have to use the alias // We can only "consider" this path a single valued association id when we are about to "remove" the id part if (idRemovable) { Class<?> maybeSingularAssociationClass = ((JoinAliasInfo) a).getJoinNode().getType(); PathElementExpression maybeSingularAssociationIdExpression = pathElements.get(maybeSingularAssociationIdIndex); return isId(maybeSingularAssociationClass, maybeSingularAssociationIdExpression); } else { // Otherwise we return false in order to signal that a normal implicit join should be done return false; } } } } else { if (joinResult.hasField()) { Expression fieldExpression = expressionFactory.createPathExpression(joinResult.joinFields()); AttributeHolder result = JpaUtils.getAttributeForJoining(metamodel, parent.getType(), fieldExpression, parent.getAlias()); baseType = metamodel.type(result.getAttributeJavaType()); } else { baseType = parent.getNodeType(); } maybeSingularAssociationJoinResult = JpaUtils.getAttributeForJoining(metamodel, baseType.getJavaType(), maybeSingularAssociationNameExpression, null); } Attribute<?, ?> maybeSingularAssociation = maybeSingularAssociationJoinResult.getAttribute(); if (maybeSingularAssociation == null) { // A naked root treat like TREAT(alias AS Subtype) has no attribute return false; } if (maybeSingularAssociation.getPersistentAttributeType() != Attribute.PersistentAttributeType.MANY_TO_ONE && maybeSingularAssociation.getPersistentAttributeType() != Attribute.PersistentAttributeType.ONE_TO_ONE ) { // Attributes that are not ManyToOne or OneToOne can't possibly be single value association sources return false; } if (maybeSingularAssociation instanceof MapKeyAttribute<?, ?>) { // Skip the foreign join column check for map keys // They aren't allowed as join sources in the JPA providers yet so we can only render them directly } else if (baseType instanceof EmbeddableType<?>) { // Get the base type. This is important if the path is "deeper" i.e. when having embeddables baseType = parent.getNodeType(); String attributePath = joinResult.joinFields(maybeSingularAssociationName); if (mainQuery.jpaProvider.isForeignJoinColumn((ManagedType<?>) baseType, attributePath)) { return false; } } else if (mainQuery.jpaProvider.isForeignJoinColumn((ManagedType<?>) baseType, maybeSingularAssociation.getName())) { return false; } Class<?> maybeSingularAssociationClass = maybeSingularAssociationJoinResult.getAttributeJavaType(); PathElementExpression maybeSingularAssociationIdExpression = pathElements.get(maybeSingularAssociationIdIndex); return isId(maybeSingularAssociationClass, maybeSingularAssociationIdExpression); } private boolean isId(Class<?> managedTypeClass, Expression idExpression) { AttributeHolder maybeSingularAssociationIdJoinResult = JpaUtils.getAttributeForJoining(metamodel, managedTypeClass, idExpression, null); Attribute<?, ?> maybeSingularAssociationId = maybeSingularAssociationIdJoinResult.getAttribute(); if (!(maybeSingularAssociationId instanceof SingularAttribute<?, ?>)) { return false; } if (!((SingularAttribute<?, ?>) maybeSingularAssociationId).isId()) { return false; } return true; } private String getSimpleName(PathElementExpression element) { if (element == null) { return null; } else if (element instanceof ArrayExpression) { return ((ArrayExpression) element).getBase().getProperty(); } else { return element.toString(); } } private String getJoinAlias(ArrayExpression expr) { StringBuilder sb = new StringBuilder(expr.getBase().toString()); Expression indexExpr = expr.getIndex(); if (indexExpr instanceof ParameterExpression) { ParameterExpression indexParamExpr = (ParameterExpression) indexExpr; sb.append('_'); sb.append(indexParamExpr.getName()); } else if (indexExpr instanceof PathExpression) { PathExpression indexPathExpr = (PathExpression) indexExpr; sb.append('_'); sb.append(((JoinNode) indexPathExpr.getBaseNode()).getAliasInfo().getAlias()); if (indexPathExpr.getField() != null) { sb.append('_'); sb.append(indexPathExpr.getField().replaceAll("\\.", "_")); } } else if (indexExpr instanceof NumericLiteral) { sb.append('_'); sb.append(((NumericLiteral) indexExpr).getValue()); } else if (indexExpr instanceof StringLiteral) { sb.append('_'); sb.append(((StringLiteral) indexExpr).getValue()); } else { throw new IllegalStateException("Invalid array index expression " + indexExpr.toString()); } return sb.toString(); } private EqPredicate getArrayExpressionPredicate(JoinNode joinNode, ArrayExpression arrayExpr) { PathExpression keyPath = new PathExpression(new ArrayList<PathElementExpression>(), true); keyPath.getExpressions().add(new PropertyExpression(joinNode.getAliasInfo().getAlias())); keyPath.setPathReference(new SimplePathReference(joinNode, null, joinNode.getType())); Attribute<?, ?> arrayBaseAttribute = joinNode.getParentTreeNode().getAttribute(); Expression keyExpression; if (arrayBaseAttribute instanceof ListAttribute<?, ?>) { keyExpression = new ListIndexExpression(keyPath); } else { keyExpression = new MapKeyExpression(keyPath); } return new EqPredicate(keyExpression, arrayExpr.getIndex()); } private void registerDependencies(final JoinNode joinNode, CompoundPredicate onExpression) { onExpression.accept(new VisitorAdapter() { @Override public void visit(PathExpression pathExpr) { // prevent loop dependencies to the same join node if (pathExpr.getBaseNode() != joinNode) { joinNode.getDependencies().add((JoinNode) pathExpr.getBaseNode()); } } }); } private void generateAndApplyOnPredicate(JoinNode joinNode, ArrayExpression arrayExpr) { EqPredicate valueKeyFilterPredicate = getArrayExpressionPredicate(joinNode, arrayExpr); if (joinNode.getOnPredicate() != null) { CompoundPredicate currentPred = joinNode.getOnPredicate(); // Only add the predicate if it isn't contained yet if (!findPredicate(currentPred, valueKeyFilterPredicate)) { currentPred.getChildren().add(valueKeyFilterPredicate); registerDependencies(joinNode, currentPred); } } else { CompoundPredicate onAndPredicate = new CompoundPredicate(CompoundPredicate.BooleanOperator.AND); onAndPredicate.getChildren().add(valueKeyFilterPredicate); joinNode.setOnPredicate(onAndPredicate); registerDependencies(joinNode, onAndPredicate); } } private JoinResult implicitJoin(JoinNode current, PathExpression pathExpression, ClauseType fromClause, int start, int end, boolean allowParentAliases) { List<PathElementExpression> pathElements = pathExpression.getExpressions(); List<String> resultFields = new ArrayList<String>(); PathElementExpression elementExpr; for (int i = start; i < end; i++) { AliasInfo aliasInfo; elementExpr = pathElements.get(i); if (elementExpr instanceof ArrayExpression) { ArrayExpression arrayExpr = (ArrayExpression) elementExpr; String joinRelationName; List<String> joinRelationAttributes; if (!resultFields.isEmpty()) { resultFields.add(arrayExpr.getBase().toString()); joinRelationAttributes = resultFields; resultFields = new ArrayList<String>(); joinRelationName = StringUtils.join(".", joinRelationAttributes); } else { joinRelationName = arrayExpr.getBase().toString(); joinRelationAttributes = Arrays.asList(joinRelationName); } current = current == null ? getRootNodeOrFail("Ambiguous join path [", joinRelationName, "] because of multiple root nodes!") : current; // Find a node by a predicate match JoinNode matchingNode = findNode(current, joinRelationName, arrayExpr); if (matchingNode != null) { current = matchingNode; } else if (i == 0 && (aliasInfo = aliasManager.getAliasInfoForBottomLevel(joinRelationName)) != null) { // The first node is allowed to be a join alias if (aliasInfo instanceof SelectInfo) { throw new IllegalArgumentException("Illegal reference to the select alias '" + joinRelationName + "'"); } current = ((JoinAliasInfo) aliasInfo).getJoinNode(); generateAndApplyOnPredicate(current, arrayExpr); } else { String joinAlias = getJoinAlias(arrayExpr); final JoinResult result = createOrUpdateNode(current, joinRelationAttributes, null, joinAlias, null, true, false); current = result.baseNode; resultFields = result.addToList(resultFields); generateAndApplyOnPredicate(current, arrayExpr); } } else if (elementExpr instanceof TreatExpression) { if (i != 0 || current != null) { throw new IllegalArgumentException("A treat expression should be the first element in a path!"); } TreatExpression treatExpression = (TreatExpression) elementExpr; boolean fromSubquery = false; boolean fromSelectAlias = false; boolean joinRequired = false; boolean fetch = false; if (treatExpression.getExpression() instanceof PathExpression) { PathExpression treatedPathExpression = (PathExpression) treatExpression.getExpression(); implicitJoin(treatedPathExpression, true, treatExpression.getType(), fromClause, fromSubquery, fromSelectAlias, true, false, fetch); JoinNode treatedJoinNode = (JoinNode) treatedPathExpression.getBaseNode(); EntityType<?> treatType = metamodel.getEntity(treatExpression.getType()); current = treatedJoinNode.getTreatedJoinNode(treatType); } else { throw new UnsupportedOperationException("Unsupported treated expression type: " + treatExpression.getExpression().getClass()); } } else if (elementExpr instanceof MapKeyExpression) { MapKeyExpression mapKeyExpression = (MapKeyExpression) elementExpr; boolean fromSubquery = false; boolean fromSelectAlias = false; boolean joinRequired = true; boolean fetch = false; current = joinMapKey(mapKeyExpression, null, fromClause, fromSubquery, fromSelectAlias, joinRequired, fetch, true, true); } else if (elementExpr instanceof MapValueExpression) { MapValueExpression mapValueExpression = (MapValueExpression) elementExpr; boolean fromSubquery = false; boolean fromSelectAlias = false; boolean joinRequired = true; boolean fetch = false; implicitJoin(mapValueExpression.getPath(), true, null, fromClause, fromSubquery, fromSelectAlias, joinRequired, fetch); current = (JoinNode) mapValueExpression.getPath().getBaseNode(); } else if (pathElements.size() == 1 && (aliasInfo = aliasManager.getAliasInfoForBottomLevel(elementExpr.toString())) != null) { if (aliasInfo instanceof SelectInfo) { throw new IllegalArgumentException("Can't dereference a select alias"); } else { // Join alias usage like in "joinAlias.relationName" current = ((JoinAliasInfo) aliasInfo).getJoinNode(); } } else { if (!resultFields.isEmpty()) { resultFields.add(elementExpr.toString()); JoinResult currentResult = createOrUpdateNode(current, resultFields, null, null, null, true, true); current = currentResult.baseNode; if (!currentResult.hasField()) { resultFields.clear(); } } else { final JoinResult result = implicitJoinSingle(current, elementExpr.toString(), allowParentAliases); if (current != result.baseNode) { current = result.baseNode; } resultFields = result.addToList(resultFields); } } } if (resultFields.isEmpty()) { return new JoinResult(current, null, current == null ? null : current.getType()); } else { StringBuilder sb = new StringBuilder(); sb.append(resultFields.get(0)); for (int i = 1; i < resultFields.size(); i++) { sb.append('.'); sb.append(resultFields.get(i)); } Expression expression = expressionFactory.createSimpleExpression(sb.toString(), false); Class<?> type = JpaUtils.getAttributeForJoining(metamodel, current.getType(), expression, current.getAlias()).getAttributeJavaType(); return new JoinResult(current, resultFields, type); } } private JoinNode joinMapKey(MapKeyExpression mapKeyExpression, String alias, ClauseType fromClause, boolean fromSubquery, boolean fromSelectAlias, boolean joinRequired, boolean fetch, boolean implicit, boolean defaultJoin) { implicitJoin(mapKeyExpression.getPath(), true, null, fromClause, fromSubquery, fromSelectAlias, joinRequired, false, fetch); JoinNode current = (JoinNode) mapKeyExpression.getPath().getBaseNode(); String joinRelationName = "KEY(" + current.getParentTreeNode().getRelationName() + ")"; MapAttribute<?, ?, ?> mapAttribute = (MapAttribute<?, ?, ?>) current.getParentTreeNode().getAttribute(); Attribute<?, ?> keyAttribute = new MapKeyAttribute<>(mapAttribute); String aliasToUse = alias == null ? current.getParentTreeNode().getRelationName().replaceAll("\\.", "_") + "_key" : alias; Type<?> joinRelationType = metamodel.type(mapAttribute.getKeyJavaType()); current = getOrCreate(current, joinRelationName, joinRelationType, null, aliasToUse, JoinType.LEFT, "Ambiguous implicit join", implicit, true, keyAttribute); return current; } private JoinNode joinMapEntry(MapEntryExpression mapEntryExpression, String alias, ClauseType fromClause, boolean fromSubquery, boolean fromSelectAlias, boolean joinRequired, boolean fetch, boolean implicit, boolean defaultJoin) { implicitJoin(mapEntryExpression.getPath(), true, null, fromClause, fromSubquery, fromSelectAlias, joinRequired, false, fetch); JoinNode current = (JoinNode) mapEntryExpression.getPath().getBaseNode(); String joinRelationName = "ENTRY(" + current.getParentTreeNode().getRelationName() + ")"; MapAttribute<?, ?, ?> mapAttribute = (MapAttribute<?, ?, ?>) current.getParentTreeNode().getAttribute(); Attribute<?, ?> entryAttribute = new MapEntryAttribute<>(mapAttribute); String aliasToUse = alias == null ? current.getParentTreeNode().getRelationName().replaceAll("\\.", "_") + "_entry" : alias; Type<?> joinRelationType = metamodel.type(Map.Entry.class); current = getOrCreate(current, joinRelationName, joinRelationType, null, aliasToUse, JoinType.LEFT, "Ambiguous implicit join", implicit, true, entryAttribute); return current; } private JoinNode joinListIndex(ListIndexExpression listIndexExpression, String alias, ClauseType fromClause, boolean fromSubquery, boolean fromSelectAlias, boolean joinRequired, boolean fetch, boolean implicit, boolean defaultJoin) { implicitJoin(listIndexExpression.getPath(), true, null, fromClause, fromSubquery, fromSelectAlias, joinRequired, false, fetch); JoinNode current = (JoinNode) listIndexExpression.getPath().getBaseNode(); String joinRelationName = "INDEX(" + current.getParentTreeNode().getRelationName() + ")"; ListAttribute<?, ?> listAttribute = (ListAttribute<?, ?>) current.getParentTreeNode().getAttribute(); Attribute<?, ?> indexAttribute = new ListIndexAttribute<>(listAttribute); String aliasToUse = alias == null ? current.getParentTreeNode().getRelationName().replaceAll("\\.", "_") + "_index" : alias; Type<?> joinRelationType = metamodel.type(Integer.class); current = getOrCreate(current, joinRelationName, joinRelationType, null, aliasToUse, JoinType.LEFT, "Ambiguous implicit join", implicit, true, indexAttribute); return current; } private JoinResult implicitJoinSingle(JoinNode baseNode, String attributeName, boolean allowParentAliases) { if (baseNode == null) { // When no base is given, check if the attribute name is an alias AliasInfo aliasInfo = allowParentAliases ? aliasManager.getAliasInfo(attributeName) : aliasManager.getAliasInfoForBottomLevel(attributeName); if (aliasInfo != null && aliasInfo instanceof JoinAliasInfo) { JoinNode node = ((JoinAliasInfo) aliasInfo).getJoinNode(); // if it is, we can just return the join node return new JoinResult(node, null, node.getType()); } } // If we have no base node, root is assumed if (baseNode == null) { baseNode = getRootNodeOrFail("Ambiguous join path [", attributeName, "] because of multiple root nodes!"); } // check if the path is joinable, assuming it is relative to the root (implicit root prefix) return createOrUpdateNode(baseNode, Arrays.asList(attributeName), null, null, null, true, true); } private JoinResult implicitJoinSingle(JoinNode baseNode, String attributeName, boolean objectLeafAllowed, boolean joinRequired) { JoinNode newBaseNode; String field; Class<?> type; boolean lazy = false; // The given path may be relative to the root or it might be an alias if (objectLeafAllowed) { Type<?> baseNodeType = baseNode.getNodeType(); AttributeHolder attributeHolder = JpaUtils.getAttributeForJoining(metamodel, baseNodeType.getJavaType(), expressionFactory.createJoinPathExpression(attributeName), baseNode.getAlias()); Attribute<?, ?> attr = attributeHolder.getAttribute(); if (attr == null) { throw new IllegalArgumentException("Field with name '" + attributeName + "' was not found within managed type " + baseNodeType.getJavaType().getName()); } if (joinRequired || attr.isCollection()) { final JoinResult newBaseNodeResult = implicitJoinSingle(baseNode, attributeName, false); newBaseNode = newBaseNodeResult.baseNode; // check if the last path element was also joined if (newBaseNode != baseNode) { field = null; type = newBaseNode.getType(); } else { field = attributeName; type = attributeHolder.getAttributeJavaType(); } } else { newBaseNode = baseNode; field = attributeName; type = attributeHolder.getAttributeJavaType(); lazy = true; } } else { Class<?> baseNodeType = baseNode.getType(); AttributeHolder attributeHolder = JpaUtils.getAttributeForJoining(metamodel, baseNodeType, expressionFactory.createJoinPathExpression(attributeName), baseNode.getAlias()); Attribute<?, ?> attr = attributeHolder.getAttribute(); if (attr == null) { throw new IllegalArgumentException("Field with name " + attributeName + " was not found within class " + baseNodeType.getName()); } if (JpaUtils.isJoinable(attr)) { throw new IllegalArgumentException("No object leaf allowed but " + attributeName + " is an object leaf"); } newBaseNode = baseNode; field = attributeName; type = attr.getJavaType(); } return new JoinResult(newBaseNode, field == null ? null : Arrays.asList(field), type, lazy); } private void updateClauseDependencies(JoinNode baseNode, ClauseType clauseDependency, Set<JoinNode> seenNodes) { if (!seenNodes.add(baseNode)) { // Cyclic dependency throw new IllegalStateException("Cyclic join dependency: " + seenNodes); } JoinNode current = baseNode; while (current != null) { // update the ON clause dependent nodes to also have a clause dependency for (JoinNode dependency : current.getDependencies()) { updateClauseDependencies(dependency, clauseDependency, seenNodes); } current.getClauseDependencies().add(clauseDependency); // If the parent node was a dependency, we are done with cycle checking // as it has been checked by the recursive call before if (current.getDependencies().contains(current.getParent())) { break; } current = current.getParent(); } } private JoinType getModelAwareType(JoinNode baseNode, Attribute<?, ?> attr) { if (baseNode.getJoinType() == JoinType.LEFT) { return JoinType.LEFT; } if ((attr.getPersistentAttributeType() == Attribute.PersistentAttributeType.MANY_TO_ONE || attr.getPersistentAttributeType() == Attribute.PersistentAttributeType.ONE_TO_ONE) && ((SingularAttribute<?, ?>) attr).isOptional() == false) { return JoinType.INNER; } else { return JoinType.LEFT; } } private JoinResult createOrUpdateNode(JoinNode baseNode, List<String> joinRelationAttributes, String treatType, String alias, JoinType joinType, boolean implicit, boolean defaultJoin) { Class<?> baseNodeType = baseNode.getType(); String joinRelationName = StringUtils.join(".", joinRelationAttributes); AttributeHolder attrJoinResult = JpaUtils.getAttributeForJoining(metamodel, baseNodeType, expressionFactory.createJoinPathExpression(joinRelationName), baseNode.getAlias()); Attribute<?, ?> attr = attrJoinResult.getAttribute(); if (attr == null) { throw new IllegalArgumentException("Field with name " + joinRelationName + " was not found within class " + baseNodeType.getName()); } if (!JpaUtils.isJoinable(attr)) { if (LOG.isLoggable(Level.FINE)) { LOG.fine(new StringBuilder("Field with name ").append(joinRelationName) .append(" of class ") .append(baseNodeType.getName()) .append(" is parseable and therefore it has not to be fetched explicitly.") .toString()); } return new JoinResult(baseNode, joinRelationAttributes, attrJoinResult.getAttributeJavaType()); } if (implicit) { String aliasToUse = alias == null ? attr.getName() : alias; alias = aliasManager.generatePostfixedAlias(aliasToUse); } if (joinType == null) { joinType = getModelAwareType(baseNode, attr); } Type<?> joinRelationType = metamodel.type(attrJoinResult.getAttributeJavaType()); JoinNode newNode = getOrCreate(baseNode, joinRelationName, joinRelationType, treatType, alias, joinType, "Ambiguous implicit join", implicit, defaultJoin, attr); return new JoinResult(newNode, null, newNode.getType()); } private void checkAliasIsAvailable(String alias, String currentJoinPath, String errorMessage) { AliasInfo oldAliasInfo = aliasManager.getAliasInfoForBottomLevel(alias); if (oldAliasInfo instanceof SelectInfo) { throw new IllegalStateException("Alias [" + oldAliasInfo.getAlias() + "] already used as select alias"); } JoinAliasInfo oldJoinAliasInfo = (JoinAliasInfo) oldAliasInfo; if (oldJoinAliasInfo != null) { if (!oldJoinAliasInfo.getAbsolutePath().equals(currentJoinPath)) { throw new IllegalArgumentException(errorMessage); } else { throw new RuntimeException("Probably a programming error if this happens. An alias[" + alias + "] for the same join path[" + currentJoinPath + "] is available but the join node is not!"); } } } private JoinNode getOrCreate(JoinNode baseNode, String joinRelationName, Type<?> joinRelationType, String treatType, String alias, JoinType type, String errorMessage, boolean implicit, boolean defaultJoin, Attribute<?, ?> attribute) { JoinTreeNode treeNode = baseNode.getOrCreateTreeNode(joinRelationName, attribute); JoinNode node = treeNode.getJoinNode(alias, defaultJoin); String qualificationExpression = null; if (attribute instanceof QualifiedAttribute) { qualificationExpression = ((QualifiedAttribute) attribute).getQualificationExpression(); } EntityType<?> treatJoinType; String currentJoinPath; if (treatType != null) { // Verify it's a valid type treatJoinType = metamodel.getEntity(treatType); currentJoinPath = "TREAT(" + baseNode.getAliasInfo().getAbsolutePath() + "." + joinRelationName + " AS " + treatJoinType.getName() + ")"; } else { treatJoinType = null; currentJoinPath = baseNode.getAliasInfo().getAbsolutePath() + "." + joinRelationName; } if (node == null) { // a join node for the join relation does not yet exist checkAliasIsAvailable(alias, currentJoinPath, errorMessage); // the alias might have to be postfixed since it might already exist in parent queries if (implicit && aliasManager.getAliasInfo(alias) != null) { alias = aliasManager.generatePostfixedAlias(alias); } JoinAliasInfo newAliasInfo = new JoinAliasInfo(alias, currentJoinPath, implicit, false, aliasManager); aliasManager.registerAliasInfo(newAliasInfo); node = JoinNode.createAssociationJoinNode(baseNode, treeNode, type, joinRelationType, treatJoinType, qualificationExpression, newAliasInfo); newAliasInfo.setJoinNode(node); treeNode.addJoinNode(node, defaultJoin); } else { JoinAliasInfo nodeAliasInfo = node.getAliasInfo(); if (!alias.equals(nodeAliasInfo.getAlias())) { // Aliases for the same join paths don't match if (nodeAliasInfo.isImplicit() && !implicit) { // Overwrite implicit aliases aliasManager.unregisterAliasInfoForBottomLevel(nodeAliasInfo); // we must alter the nodeAliasInfo instance since this instance is also set on the join node // TODO: we must update the key for the JoinNode in the respective JoinTreeNode nodeAliasInfo.setAlias(alias); nodeAliasInfo.setImplicit(false); // We can only change the join type if the existing node is implicit and the update on the node is not implicit node.setJoinType(type); aliasManager.registerAliasInfo(nodeAliasInfo); } else if (!nodeAliasInfo.isImplicit() && !implicit) { throw new IllegalArgumentException("Alias conflict [" + nodeAliasInfo.getAlias() + "=" + nodeAliasInfo.getAbsolutePath() + ", " + alias + "=" + currentJoinPath + "]"); } } if (treatJoinType != null) { if (node.getTreatType() == null) { node = node.getTreatedJoinNode(treatJoinType); } else if (!treatJoinType.equals(node.getTreatType())) { throw new IllegalArgumentException("A join node [" + nodeAliasInfo.getAlias() + "=" + nodeAliasInfo.getAbsolutePath() + "] " + "for treat type [" + treatType + "] conflicts with the existing treat type [" + node.getTreatType() + "]"); } } } return node; } private JoinNode findNode(JoinNode baseNode, String joinRelationName, ArrayExpression arrayExpression) { JoinTreeNode treeNode = baseNode.getNodes().get(joinRelationName); if (treeNode == null) { return null; } for (JoinNode node : treeNode.getJoinNodes().values()) { Predicate pred = getArrayExpressionPredicate(node, arrayExpression); CompoundPredicate compoundPredicate = node.getOnPredicate(); if (findPredicate(compoundPredicate, pred)) { return node; } } return null; } private boolean findPredicate(CompoundPredicate compoundPredicate, Predicate pred) { if (compoundPredicate != null) { List<Predicate> children = compoundPredicate.getChildren(); int size = children.size(); for (int i = 0; i < size; i++) { if (pred.equals(children.get(i))) { return true; } } } return false; } /** * Fetch the given node only. * * @param node */ private void fetchPath(JoinNode node) { node.setFetch(true); // fetches implicitly need to be selected node.getClauseDependencies().add(ClauseType.SELECT); } // TODO: needs equals-hashCode implementation private static class JoinResult { final JoinNode baseNode; final List<String> fields; final Class<?> type; final boolean lazy; public JoinResult(JoinNode baseNode, List<String> fields, Class<?> type) { this.baseNode = baseNode; this.fields = fields; this.type = type; this.lazy = false; } public JoinResult(JoinNode baseNode, List<String> fields, Class<?> type, boolean lazy) { this.baseNode = baseNode; this.fields = fields; this.type = type; this.lazy = lazy; } private boolean hasField() { return fields != null && !fields.isEmpty(); } private String joinFields(String field) { if (fields == null || fields.isEmpty()) { return field; } StringBuilder sb = new StringBuilder(); sb.append(fields.get(0)); for (int i = 1; i < fields.size(); i++) { sb.append('.'); sb.append(fields.get(i)); } if (field != null) { sb.append('.'); sb.append(field); } return sb.toString(); } private String joinFields() { return joinFields(null); } private List<String> addToList(List<String> resultFields) { if (hasField()) { if (resultFields != fields) { resultFields.addAll(fields); } } return resultFields; } private boolean isLazy() { return lazy; } } private class JoinOnBuilderEndedListener extends PredicateBuilderEndedListenerImpl { private JoinNode joinNode; @Override public void onBuilderEnded(PredicateBuilder builder) { super.onBuilderEnded(builder); Predicate predicate = builder.getPredicate(); predicate.accept(new VisitorAdapter() { private boolean isKeyFunction; @Override public void visit(ListIndexExpression expression) { boolean old = isKeyFunction; this.isKeyFunction = true; super.visit(expression); this.isKeyFunction = old; } @Override public void visit(MapKeyExpression expression) { boolean old = isKeyFunction; this.isKeyFunction = true; super.visit(expression); this.isKeyFunction = old; } @Override public void visit(PathExpression expression) { expression.setCollectionKeyPath(isKeyFunction); super.visit(expression); } }); joinNode.setOnPredicate((CompoundPredicate) predicate); } } }