Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
com.blazebit.persistence.impl.JoinManager Maven / Gradle / Ivy
/*
* Copyright 2014 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 java.util.*;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.persistence.Query;
import javax.persistence.metamodel.*;
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.*;
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.ExpressionTransformer;
import com.blazebit.persistence.spi.JpaProvider;
import com.blazebit.persistence.impl.util.*;
import com.blazebit.persistence.spi.ValuesStrategy;
/**
* @author Moritz Becker
* @since 1.0
*/
public class JoinManager extends AbstractManager {
private final static 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 rootNodes = new ArrayList(1);
private final Set entityFunctionNodes = new LinkedHashSet();
// root entity class
private final String joinRestrictionKeyword;
private final MainQuery mainQuery;
private final AliasManager aliasManager;
private final EntityMetamodel metamodel; // needed for model-aware joins
private final JoinManager parent;
private final JoinOnBuilderEndedListener joinOnBuilderListener;
private SubqueryInitiatorFactory subqueryInitFactory;
private final ExpressionFactory expressionFactory;
// helper collections for join rendering
private final Set collectionJoinNodes = Collections.newSetFromMap(new IdentityHashMap());
private final Set renderedJoins = Collections.newSetFromMap(new IdentityHashMap());
private final Set markedJoinNodes = Collections.newSetFromMap(new IdentityHashMap());
JoinManager(MainQuery mainQuery, ResolvingQueryGenerator queryGenerator, AliasManager aliasManager, JoinManager parent, ExpressionFactory expressionFactory) {
super(queryGenerator, mainQuery.parameterManager);
this.mainQuery = mainQuery;
this.aliasManager = aliasManager;
this.metamodel = mainQuery.metamodel;
this.parent = parent;
this.joinRestrictionKeyword = " " + mainQuery.jpaProvider.getOnClause() + " ";
this.joinOnBuilderListener = new JoinOnBuilderEndedListener();
this.expressionFactory = expressionFactory;
}
@Override
public ClauseType getClauseType() {
return ClauseType.JOIN;
}
Set getKeyRestrictedLeftJoins() {
if (!mainQuery.jpaProvider.needsJoinSubqueryRewrite()) {
return Collections.emptySet();
}
Set keyRestrictedLeftJoins = new HashSet();
acceptVisitor(new KeyRestrictedLeftJoinCollectingVisitor(mainQuery.jpaProvider, keyRestrictedLeftJoins));
return keyRestrictedLeftJoins;
}
static class KeyRestrictedLeftJoinCollectingVisitor extends VisitorAdapter implements JoinNodeVisitor {
final JpaProvider jpaProvider;
final Set keyRestrictedLeftJoins;
public KeyRestrictedLeftJoinCollectingVisitor(JpaProvider jpaProvider, Set 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(FunctionExpression expression) {
super.visit(expression);
if ("KEY".equals(expression.getFunctionName()) || "INDEX".equals(expression.getFunctionName())) {
// We know it can only be a path
PathExpression pathExpression = (PathExpression) expression.getExpressions().get(0);
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 treatFunction, String castedParameter) {
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
Set> attributeSet = (Set>) mainQuery.metamodel.getManagedType(clazz).getAttributes();
String[][] parameterNames = new String[valueCount][attributeSet.size()];
ValueRetriever[] pathExpressions = new ValueRetriever[attributeSet.size()];
StringBuilder valuesSb = new StringBuilder(20 + valueCount * attributeSet.size() * 3);
Query valuesExampleQuery = getValuesExampleQuery(clazz, rootAlias, treatFunction, 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");
String valuesAliases = getValuesAliases(exampleQuerySqlAlias, attributeSet.size(), exampleQuerySql, 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);
valuesSb.append("limit 1,");
valuesSb.append(valueCount);
valuesSb.append(')');
valuesAliases = null;
}
String valuesClause = valuesSb.toString();
JoinAliasInfo rootAliasInfo = new JoinAliasInfo(rootAlias, rootAlias, true, true, aliasManager);
JoinNode rootNode = new JoinNode(rootAliasInfo, clazz, treatFunction, valueCount, valuesExampleQuery, valuesClause, valuesAliases);
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, ValuesStrategy strategy, String dummyTable) {
final String select = "select ";
int startIndex = exampleQuerySql.indexOf(select) + select.length();
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);
}
// Search for table alias usages
final String searchAlias = tableAlias + ".";
int searchIndex = startIndex;
int attributeNumber = 1;
while ((searchIndex = exampleQuerySql.indexOf(searchAlias, searchIndex)) > -1 && searchIndex < endIndex) {
searchIndex += searchAlias.length();
if (strategy == ValuesStrategy.SELECT_VALUES) {
// TODO: This naming is actually H2 specific
sb.append('c');
sb.append(attributeNumber++);
sb.append(' ');
} else if (strategy == ValuesStrategy.SELECT_UNION) {
sb.append("null as ");
}
// Append chars until non-identifier char was found
for (; searchIndex < endIndex; searchIndex++) {
final char c = exampleQuerySql.charAt(searchIndex);
// NOTE: We expect users to be sane and only allow letters, numbers and underscore in an identifier
if (Character.isLetterOrDigit(c) || c == '_') {
sb.append(c);
} else {
sb.append(',');
break;
}
}
}
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 {
@Override
public Object getValue(Object target) {
return target;
}
}
private Query getValuesExampleQuery(Class> clazz, String prefix, String treatFunction, String castedParameter, Set> 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 {
Iterator> iter = attributeSet.iterator();
for (int i = 0; i < attributes.length; i++) {
sb.append("e.");
Attribute, ?> attribute = iter.next();
attributes[i] = attribute.getName();
attributeParameter[i] = mainQuery.dbmsDialect.needsCastParameters() ? mainQuery.dbmsDialect.cast("?", mainQuery.jpaProvider.getColumnType(attribute)) : "?";
pathExpressions[i] = com.blazebit.reflection.ExpressionUtils.getExpression(clazz, attributes[i]);
sb.append(attributes[i]);
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);
}
} 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> clazz, String rootAlias) {
if (rootAlias == null) {
// TODO: not sure if other JPA providers support case sensitive queries like hibernate
StringBuilder sb = new StringBuilder(clazz.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 = new JoinNode(null, null, null, rootAliasInfo, null, clazz.getJavaType(), null);
rootAliasInfo.setJoinNode(rootNode);
rootNodes.add(rootNode);
// register root alias in aliasManager
aliasManager.registerAliasInfo(rootAliasInfo);
return rootAlias;
}
String addRoot(String correlationPath, String rootAlias) {
// TODO: TREAT support is missing
String[] parts = correlationPath.split("\\.");
String correlationParentAlias = parts[0];
String correlationPathNoAlias = correlationPath.substring(parts[0].length() + 1);
// We assume that this is a subquery join manager here
AliasInfo aliasInfo = aliasManager.getAliasInfo(correlationParentAlias);
if (aliasInfo == null || !(aliasInfo instanceof JoinAliasInfo) || aliasInfo.getAliasOwner() == aliasManager) {
throw new IllegalArgumentException("No join node for the alias '" + correlationParentAlias + "' could be found in a parent query!");
}
JoinNode correlationParent = ((JoinAliasInfo) aliasInfo).getJoinNode();
Class> attributeType;
if (correlationPathNoAlias.indexOf('.') < 0) {
Attribute, ?> attribute = JpaUtils.getAttribute(metamodel.managedType(correlationParent.getPropertyClass()), correlationPathNoAlias);
attributeType = JpaUtils.resolveFieldClass(correlationParent.getPropertyClass(), attribute);
} else {
ManagedType> managedType = MetamodelUtils.resolveManagedTargetType(metamodel, correlationParent.getPropertyClass(), correlationPath.substring(correlationParentAlias.length() + 1, correlationPath.lastIndexOf('.')));
Attribute, ?> secondLastAttribute = MetamodelUtils.resolveTargetAttribute(metamodel, correlationParent.getPropertyClass(), correlationPathNoAlias);
attributeType = JpaUtils.resolveFieldClass(managedType.getJavaType(), secondLastAttribute);
}
if (rootAlias == null) {
// TODO: not sure if other JPA providers support case sensitive queries like hibernate
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);
}
}
// TODO: Implement treat support for correlated subqueries
String treatType = null;
JoinAliasInfo rootAliasInfo = new JoinAliasInfo(rootAlias, rootAlias, true, true, aliasManager);
JoinNode rootNode = new JoinNode(correlationParent, correlationPathNoAlias, treatType, rootAliasInfo, attributeType, null);
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) {
if (rootNodes.size() > 1) {
throw new IllegalArgumentException(string);
}
return rootNodes.get(0);
}
JoinNode getRootNode(Expression expression) {
String alias;
if (expression instanceof PropertyExpression) {
alias = expression.toString();
} else {
return null;
}
List 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 getRoots() {
return rootNodes;
}
boolean hasCollections() {
List 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 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;
}
}
return false;
}
boolean hasEntityFunctions() {
return entityFunctionNodes.size() > 0;
}
public Set getCollectionJoins() {
if (rootNodes.isEmpty()) {
return Collections.EMPTY_SET;
} else {
Set collectionJoins = rootNodes.get(0).getCollectionJoins();
for (int i = 1; i < rootNodes.size(); i++) {
collectionJoins.addAll(rootNodes.get(i).getCollectionJoins());
}
return collectionJoins;
}
}
Set getEntityFunctionNodes() {
return entityFunctionNodes;
}
private void fillCollectionJoinsNodesRec(JoinNode node, Set collectionNodes) {
for (JoinTreeNode treeNode : node.getNodes().values()) {
if (treeNode.isCollection()) {
collectionNodes.addAll(treeNode.getJoinNodes().values());
for (JoinNode childNode : treeNode.getJoinNodes().values()) {
fillCollectionJoinsNodesRec(childNode, collectionNodes);
}
}
}
}
public JoinManager getParent() {
return parent;
}
void setSubqueryInitFactory(SubqueryInitiatorFactory subqueryInitFactory) {
this.subqueryInitFactory = subqueryInitFactory;
}
Set buildClause(StringBuilder sb, Set clauseExclusions, String aliasPrefix, boolean collectCollectionJoinNodes, boolean externalRepresenation) {
collectionJoinNodes.clear();
renderedJoins.clear();
sb.append(" FROM ");
// TODO: we might have dependencies to other from clause elements which should also be accounted for
List 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 = metamodel.getManagedType(rootNode.getPropertyClass());
int count = type.getAttributes().size();
if (rootNode.getPropertyClass() != 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 < count; j++) {
sb.append("?,");
}
sb.setCharAt(sb.length() - 1, ')');
sb.append(',');
}
sb.setCharAt(sb.length() - 1, ')');
} else {
if (correlationParent != null) {
sb.append(correlationParent.getAliasInfo().getAlias());
sb.append('.');
sb.append(rootNode.getCorrelationPath());
} else {
EntityType> type = metamodel.entity(rootNode.getPropertyClass());
sb.append(type.getName());
}
}
sb.append(' ');
if (aliasPrefix != null) {
sb.append(aliasPrefix);
}
sb.append(rootNode.getAliasInfo().getAlias());
// TODO: not sure if needed since applyImplicitJoins will already invoke that
rootNode.registerDependencies();
applyJoins(sb, rootNode.getAliasInfo(), rootNode.getNodes(), clauseExclusions, aliasPrefix, collectCollectionJoinNodes);
if (!rootNode.getEntityJoinNodes().isEmpty()) {
// TODO: Fix this with #216
boolean isCollection = true;
applyJoins(sb, rootNode.getAliasInfo(), new ArrayList(rootNode.getEntityJoinNodes()), isCollection, clauseExclusions, aliasPrefix, collectCollectionJoinNodes);
}
}
return collectionJoinNodes;
}
void verifyBuilderEnded() {
joinOnBuilderListener.verifyBuilderEnded();
}
void acceptVisitor(JoinNodeVisitor v) {
List nodes = rootNodes;
int size = nodes.size();
for (int i = 0; i < size; i++) {
nodes.get(i).accept(v);
}
}
public boolean acceptVisitor(AggregateDetectionVisitor aggregateDetector, boolean stopValue) {
Boolean stop = Boolean.valueOf(stopValue);
List 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 applyTransformer(ExpressionTransformer transformer) {
List nodes = rootNodes;
int size = nodes.size();
for (int i = 0; i < size; i++) {
nodes.get(i).accept(new OnClauseJoinNodeVisitor(new PredicateManager.TransformationVisitor(transformer, ClauseType.JOIN)));
}
}
private void renderJoinNode(StringBuilder sb, JoinAliasInfo joinBase, JoinNode node, String aliasPrefix) {
if (!renderedJoins.contains(node)) {
switch (node.getJoinType()) {
case INNER:
sb.append(" JOIN ");
break;
case LEFT:
sb.append(" LEFT JOIN ");
break;
case RIGHT:
sb.append(" RIGHT JOIN ");
break;
}
if (node.isFetch()) {
sb.append("FETCH ");
}
if (aliasPrefix != null) {
sb.append(aliasPrefix);
}
if (node.getTreatType() != null) {
if (mainQuery.jpaProvider.supportsTreatJoin()) {
sb.append("TREAT(");
renderParentAlias(sb, node, joinBase.getAlias());
sb.append(node.getParentTreeNode().getRelationName());
sb.append(" AS ");
sb.append(node.getTreatType());
sb.append(") ");
} else if (mainQuery.jpaProvider.supportsSubtypePropertyResolving()) {
sb.append(joinBase.getAlias()).append('.').append(node.getParentTreeNode().getRelationName()).append(' ');
} else {
throw new IllegalArgumentException("Treat should not be used as the JPA provider does not support subtype property access!");
}
} else if (node.getAliasInfo().isRootNode()) {
sb.append(metamodel.entity(node.getPropertyClass()).getName()).append(' ');
} else {
renderParentAlias(sb, node, joinBase.getAlias());
sb.append(node.getParentTreeNode().getRelationName()).append(' ');
}
if (aliasPrefix != null) {
sb.append(aliasPrefix);
}
sb.append(node.getAliasInfo().getAlias());
if (node.getOnPredicate() != null && !node.getOnPredicate().getChildren().isEmpty()) {
sb.append(joinRestrictionKeyword);
queryGenerator.setQueryBuffer(sb);
SimpleQueryGenerator.BooleanLiteralRenderingContext oldBooleanLiteralRenderingContext = queryGenerator.setBooleanLiteralRenderingContext(SimpleQueryGenerator.BooleanLiteralRenderingContext.PREDICATE);
node.getOnPredicate().accept(queryGenerator);
queryGenerator.setBooleanLiteralRenderingContext(oldBooleanLiteralRenderingContext);
}
renderedJoins.add(node);
}
}
private void renderParentAlias(StringBuilder sb, JoinNode parentNode, String alias) {
if (parentNode.getParentTreatType() != null) {
if (mainQuery.jpaProvider.supportsRootTreatJoin()) {
sb.append("TREAT(");
sb.append(alias);
sb.append(" AS ");
sb.append(parentNode.getParentTreatType());
sb.append(").");
} else if (mainQuery.jpaProvider.supportsSubtypePropertyResolving()) {
sb.append(alias).append('.');
} else {
throw new IllegalArgumentException("Treat should not be used as the JPA provider does not support subtype property access!");
}
} else {
sb.append(alias).append('.');
}
}
private void renderReverseDependency(StringBuilder sb, JoinNode dependency, String aliasPrefix) {
if (dependency.getParent() != null) {
renderReverseDependency(sb, dependency.getParent(), aliasPrefix);
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);
}
} finally {
markedJoinNodes.remove(dependency);
}
}
renderJoinNode(sb, dependency.getParent().getAliasInfo(), dependency, aliasPrefix);
}
}
private void applyJoins(StringBuilder sb, JoinAliasInfo joinBase, Map nodes, Set clauseExclusions, String aliasPrefix, boolean collectCollectionJoinNodes) {
for (Map.Entry nodeEntry : nodes.entrySet()) {
JoinTreeNode treeNode = nodeEntry.getValue();
List stack = new ArrayList();
stack.addAll(treeNode.getJoinNodes().descendingMap().values());
applyJoins(sb, joinBase, stack, treeNode.isCollection(), clauseExclusions, aliasPrefix, collectCollectionJoinNodes);
}
}
private void applyJoins(StringBuilder sb, JoinAliasInfo joinBase, List stack, boolean isCollection, Set clauseExclusions, String aliasPrefix, boolean collectCollectionJoinNodes) {
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());
// We have to render any dependencies this join node has before actually rendering itself
if (!node.getDependencies().isEmpty()) {
renderReverseDependency(sb, node, aliasPrefix);
}
// Collect the join nodes referring to collections
if (collectCollectionJoinNodes && isCollection) {
collectionJoinNodes.add(node);
}
// Finally render this join node
renderJoinNode(sb, joinBase, node, aliasPrefix);
// Render child nodes recursively
if (!node.getNodes().isEmpty()) {
applyJoins(sb, node.getAliasInfo(), node.getNodes(), clauseExclusions, aliasPrefix, collectCollectionJoinNodes);
}
}
}
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(Expression 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() == parent.aliasManager) {
// the alias exists but originates from the parent query builder
// an external select alias must not be dereferenced
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 an is therefore not external
return false;
} else {
throw new IllegalStateException("Alias [" + aliasInfo.getAlias() + "] originates from an unknown query");
}
}
private boolean isJoinableSelectAlias(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 false;
}
boolean singlePathElement = pathExpr.getExpressions().size() == 1;
String startAlias = pathExpr.getExpressions().get(0).toString();
AliasInfo aliasInfo = aliasManager.getAliasInfo(startAlias);
if (aliasInfo == null) {
return false;
}
if (aliasInfo instanceof SelectInfo && !fromSelect && !fromSubquery) {
// select alias
if (!singlePathElement) {
throw new IllegalStateException("Path starting with select alias not allowed");
}
// might be joinable
return true;
}
return false;
}
JoinOnBuilder 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 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 = new JoinNode(baseNode, null, null, joinAliasInfo, type, entityType.getJavaType(), null);
joinAliasInfo.setJoinNode(entityJoinNode);
baseNode.addEntityJoin(entityJoinNode);
aliasManager.registerAliasInfo(joinAliasInfo);
joinOnBuilderListener.joinNode = entityJoinNode;
return joinOnBuilderListener.startBuilder(new JoinOnBuilderImpl(result, joinOnBuilderListener, parameterManager, expressionFactory, subqueryInitFactory));
}
JoinOnBuilder joinOn(X result, String path, String alias, JoinType type, boolean defaultJoin) {
joinOnBuilderListener.joinNode = join(path, alias, type, false, defaultJoin);
return joinOnBuilderListener.startBuilder(new JoinOnBuilderImpl(result, joinOnBuilderListener, parameterManager, expressionFactory, subqueryInitFactory));
}
JoinNode join(String path, String alias, JoinType type, boolean fetch, boolean defaultJoin) {
Expression expr = expressionFactory.createJoinPathExpression(path);
PathElementExpression elementExpr;
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 pathElements = pathExpression.getExpressions();
elementExpr = pathElements.get(pathElements.size() - 1);
result = implicitJoin(null, pathExpression, 0, pathElements.size() - 1);
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 pathElements = pathExpression.getExpressions();
elementExpr = pathElements.get(pathElements.size() - 1);
result = implicitJoin(null, pathExpression, 0, pathElements.size() - 1);
current = result.baseNode;
} 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 {
String treatType = null;
if (expr instanceof TreatExpression) {
treatType = ((TreatExpression) expr).getType();
}
List joinRelationAttributes = result.addToList(new ArrayList());
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, result.typeName, 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) {
implicitJoin(expression, objectLeafAllowed, targetType, fromClause, fromSubquery, fromSelectAlias, joinRequired, false);
}
public void implicitJoin(Expression expression, boolean objectLeafAllowed, String targetTypeName, ClauseType fromClause, boolean fromSubquery, boolean fromSelectAlias, boolean joinRequired, boolean fetch) {
PathExpression pathExpression;
if (expression instanceof PathExpression) {
pathExpression = (PathExpression) expression;
// If joinable select alias, it is guaranteed to have only a single element
if (isJoinableSelectAlias(pathExpression, fromClause == ClauseType.SELECT, fromSubquery)) {
String alias = pathExpression.getExpressions().get(0).toString();
Expression expr = ((SelectInfo) aliasManager.getAliasInfo(alias)).getExpression();
// 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(expr, true, targetTypeName, fromClause, fromSubquery, true, joinRequired);
}
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);
return;
}
// First try to implicit join indices of array expressions since we will need their base nodes
List 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);
}
}
PathElementExpression elementExpr = pathElements.get(pathElements.size() - 1);
boolean singleValuedAssociationIdExpression = false;
JoinNode current = null;
String currentTreatType = null;
List resultFields = new ArrayList();
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, startIndex, maybeSingularAssociationIndex);
current = currentResult.baseNode;
resultFields = currentResult.addToList(resultFields);
singleValuedAssociationIdExpression = isSingleValuedAssociationId(currentResult, pathElements);
if (singleValuedAssociationIdExpression) {
} else {
if (currentResult.hasField()) {
// currentResult.typeName?
// Redo the joins for embeddables by moving the start index back
currentResult = implicitJoin(current, pathExpression, maybeSingularAssociationIndex - currentResult.fields.size(), maybeSingularAssociationIdIndex);
if (currentResult.fields != resultFields) {
resultFields.clear();
}
} else {
currentResult = implicitJoin(current, pathExpression, maybeSingularAssociationIndex, maybeSingularAssociationIdIndex);
}
current = currentResult.baseNode;
currentTreatType = currentResult.typeName;
resultFields = currentResult.addToList(resultFields);
}
} else {
currentResult = implicitJoin(current, pathExpression, startIndex, pathElements.size() - 1);
current = currentResult.baseNode;
currentTreatType = currentResult.typeName;
resultFields = currentResult.addToList(resultFields);
}
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);
}
PathExpression selectPathExpr = (PathExpression) selectExpr;
result = new JoinResult((JoinNode) selectPathExpr.getBaseNode(), Arrays.asList(selectPathExpr.getField()));
} else {
JoinNode pathJoinNode = ((JoinAliasInfo) aliasInfo).getJoinNode();
if (targetTypeName != null) {
// Treated root path
ManagedType> targetType = metamodel.managedType(targetTypeName);
result = new JoinResult(pathJoinNode, null, targetTypeName);
} else {
// Naked join alias usage like in "KEY(joinAlias)"
result = new JoinResult(pathJoinNode, null);
}
}
} else {
// current might be null
if (current == null) {
if (rootNodes.size() > 1) {
throw new IllegalArgumentException("Could not join path [" + expression + "] because it did not use an absolute path but multiple root nodes are available!");
}
current = rootNodes.get(0);
}
if (singleValuedAssociationIdExpression) {
String associationName = pathElements.get(pathElements.size() - 2).toString();
AliasInfo a = null;
JoinTreeNode treeNode;
if (currentResult.hasField()) {
associationName = currentResult.joinFields(associationName);
} else {
a = aliasManager.getAliasInfoForBottomLevel(associationName);
}
if (a != null) {
result = new JoinResult(((JoinAliasInfo) a).getJoinNode(), Arrays.asList(elementExpr.toString()));
} else {
treeNode = current.getNodes().get(associationName);
if (treeNode != null && treeNode.getDefaultNode() != null) {
result = new JoinResult(treeNode.getDefaultNode(), Arrays.asList(elementExpr.toString()));
} else {
result = new JoinResult(current, Arrays.asList(associationName, elementExpr.toString()));
}
}
} 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, currentTreatType, 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);
} else if (!pathExpression.isUsedInCollectionFunction()) {
if (resultFields.isEmpty()) {
result = implicitJoinSingle(current, currentTreatType, elementExpr.toString(), objectLeafAllowed, joinRequired);
} else {
resultFields.add(elementExpr.toString());
if (!validPath(JpaUtils.getManagedType(metamodel, current.getPropertyClass(), currentTreatType), resultFields)) {
throw new IllegalArgumentException("The join path [" + pathExpression + "] has a non joinable part ["
+ StringUtils.join(".", resultFields) + "]");
}
result = implicitJoinSingle(current, currentTreatType, StringUtils.join(".", resultFields), objectLeafAllowed, joinRequired);
}
} else {
if (resultFields.isEmpty()) {
result = new JoinResult(current, Arrays.asList(elementExpr.toString()));
} else {
resultFields.add(elementExpr.toString());
if (!validPath(JpaUtils.getManagedType(metamodel, current.getPropertyClass(), currentTreatType), resultFields)) {
throw new IllegalArgumentException("The join path [" + pathExpression + "] has a non joinable part ["
+ StringUtils.join(".", resultFields) + "]");
}
result = new JoinResult(current, resultFields);
}
}
}
if (fetch) {
fetchPath(result.baseNode);
}
// Don't forget to update the clause dependencies!!
if (fromClause != null) {
updateClauseDependencies(result.baseNode, fromClause);
}
if (result.isLazy()) {
pathExpression.setPathReference(new LazyPathReference(result.baseNode, result.joinFields(), result.typeName));
} else {
pathExpression.setPathReference(new SimplePathReference(result.baseNode, result.joinFields(), result.typeName));
}
if (result.hasTreatedSubpath) {
pathExpression.setHasTreatedSubpath(true);
}
} else if (expression instanceof FunctionExpression) {
List 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);
}
} else if (expression instanceof ArrayExpression || expression instanceof GeneralCaseExpression || expression instanceof TreatExpression) {
// 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 type for implicit joining found: " + expression.getClass());
}
}
private static class LazyPathReference implements PathReference {
private final JoinNode baseNode;
private final String field;
private final String typeName;
public LazyPathReference(JoinNode baseNode, String field, String typeName) {
this.baseNode = baseNode;
this.field = field;
this.typeName = typeName;
}
@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 String getTreatTypeName() {
return typeName;
}
@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());
result = prime * result + ((typeName == null) ? 0 : typeName.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;
if (typeName == null) {
if (other.getTreatTypeName() != null)
return false;
} else if (!typeName.equals(other.getTreatTypeName()))
return false;
return true;
}
}
private boolean validPath(ManagedType> t, List pathElements) {
for (int i = 0; i < pathElements.size(); i++) {
String element = pathElements.get(i);
Set> attributes = JpaUtils.getAttributesPolymorphic(metamodel, t, element);
if (attributes.isEmpty()) {
return false;
} else if (attributes.size() == 1) {
t = JpaUtils.getManagedTypeOrNull(metamodel, attributes.iterator().next().getJavaType());
} else {
// Only consider a path valid when all possible paths along the polymorphic hierarchy are valid
for (Attribute, ?> attr : attributes) {
if (!validPath(JpaUtils.getManagedTypeOrNull(metamodel, attr.getJavaType()), pathElements.subList(i, pathElements.size() - 1))) {
return false;
}
}
return true;
}
}
return true;
}
private boolean isSingleValuedAssociationId(JoinResult joinResult, List pathElements) {
JoinNode parent = joinResult.baseNode;
String parentTypeName = joinResult.typeName;
int maybeSingularAssociationIndex = pathElements.size() - 2;
int maybeSingularAssociationIdIndex = pathElements.size() - 1;
ManagedType> baseType;
Set> maybeSingularAssociationAttributes;
String maybeSingularAssociationName = getSimpleName(pathElements.get(maybeSingularAssociationIndex));
if (parent == null) {
// This is the case when we have exactly 2 path elements
AliasInfo a = aliasManager.getAliasInfo(maybeSingularAssociationName);
if (a == null) {
// if the path element is no alias we can do some optimizations
baseType = metamodel.managedType(getRootNodeOrFail("Ambiguous join path [" + maybeSingularAssociationName + "] because of multiple root nodes!").getPropertyClass());
maybeSingularAssociationAttributes = JpaUtils.getAttributesPolymorphic(metamodel, baseType, maybeSingularAssociationName);
} 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
// So we return false in order to signal that a normal implicit join should be done
return false;
}
} else {
Class> parentClass = parent.getPropertyClass();
baseType = JpaUtils.getManagedType(metamodel, parentClass, parentTypeName);
if (joinResult.hasField()) {
Attribute, ?> fieldAttribute = JpaUtils.getPolymorphicAttribute(metamodel, baseType, joinResult.joinFields());
baseType = metamodel.managedType(fieldAttribute.getJavaType());
}
maybeSingularAssociationAttributes = JpaUtils.getAttributesPolymorphic(metamodel, baseType, maybeSingularAssociationName);
}
if (maybeSingularAssociationAttributes.isEmpty()) {
return false;
}
for (Attribute, ?> maybeSingularAssociation : maybeSingularAssociationAttributes) {
if (maybeSingularAssociation.getPersistentAttributeType() != Attribute.PersistentAttributeType.MANY_TO_ONE
// TODO: to be able to support ONE_TO_ONE we need to know where the FK is
// && maybeSingularAssociation.getPersistentAttributeType() != Attribute.PersistentAttributeType.ONE_TO_ONE
) {
return false;
}
Class> maybeSingularAssociationClass = JpaUtils.resolveFieldClass(baseType.getJavaType(), maybeSingularAssociation);
ManagedType> maybeSingularAssociationType = metamodel.managedType(maybeSingularAssociationClass);
String maybeSingularAssociationIdName = getSimpleName(pathElements.get(maybeSingularAssociationIdIndex));
Set> maybeSingularAssociationIdAttributes = JpaUtils.getAttributesPolymorphic(metamodel, maybeSingularAssociationType, maybeSingularAssociationIdName);
if (maybeSingularAssociationIdAttributes.isEmpty()) {
return false;
}
for (Attribute, ?> maybeSingularAssociationId : maybeSingularAssociationIdAttributes) {
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(), true);
keyPath.getExpressions().add(new PropertyExpression(joinNode.getAliasInfo().getAlias()));
keyPath.setPathReference(new SimplePathReference(joinNode, null, null));
final String accessFunction;
Attribute, ?> arrayBaseAttribute = joinNode.getParentTreeNode().getAttribute();
if (arrayBaseAttribute instanceof ListAttribute, ?>) {
accessFunction = "INDEX";
} else {
accessFunction = "KEY";
}
FunctionExpression keyExpression = new FunctionExpression(accessFunction, Arrays.asList((Expression) 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, int start, int end) {
List pathElements = pathExpression.getExpressions();
List resultFields = new ArrayList();
String currentTargetType = null;
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 joinRelationAttributes;
if (!resultFields.isEmpty()) {
resultFields.add(arrayExpr.getBase().toString());
joinRelationAttributes = resultFields;
resultFields = new ArrayList();
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, currentTargetType, joinRelationAttributes, null, joinAlias, null, true, false);
current = result.baseNode;
resultFields = result.addToList(resultFields);
generateAndApplyOnPredicate(current, arrayExpr);
}
// Reset target type
currentTargetType = null;
} 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;
// TODO: reuse existing treated join node or create one? not sure if it wasn't better to just pass it through to the persistence provider
if (treatExpression.getExpression() instanceof PathExpression) {
PathExpression treatedPathExpression = (PathExpression) treatExpression.getExpression();
implicitJoin(treatedPathExpression, true, treatExpression.getType(), null, fromSubquery, fromSelectAlias, true, fetch);
JoinNode treatedJoinNode = (JoinNode) treatedPathExpression.getBaseNode();
current = treatedJoinNode;
currentTargetType = treatExpression.getType();
} else {
throw new UnsupportedOperationException("Unsupported treated expression type: " + treatExpression.getExpression().getClass());
}
} 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();
}
// Reset target type
currentTargetType = null;
} else {
if (!resultFields.isEmpty()) {
resultFields.add(elementExpr.toString());
JoinResult currentResult = createOrUpdateNode(current, currentTargetType, resultFields, null, null, null, true, true);
current = currentResult.baseNode;
if (!currentResult.hasField()) {
resultFields.clear();
}
} else {
final JoinResult result = implicitJoinSingle(current, elementExpr.toString());
current = result.baseNode;
resultFields = result.addToList(resultFields);
}
// Reset target type
currentTargetType = null;
}
}
if (resultFields.isEmpty()) {
return new JoinResult(current, null, currentTargetType);
} else {
return new JoinResult(current, resultFields, currentTargetType);
}
}
private JoinResult implicitJoinSingle(JoinNode baseNode, String attributeName) {
if (baseNode == null) {
// When no base is given, check if the attribute name is an alias
AliasInfo aliasInfo = aliasManager.getAliasInfoForBottomLevel(attributeName);
if (aliasInfo != null && aliasInfo instanceof JoinAliasInfo) {
// if it is, we can just return the join node
return new JoinResult(((JoinAliasInfo) aliasInfo).getJoinNode(), null);
}
}
// 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, null, Arrays.asList(attributeName), null, null, null, true, true);
}
private JoinResult implicitJoinSingle(JoinNode baseNode, String treatTypeName, String attributeName, boolean objectLeafAllowed, boolean joinRequired) {
JoinNode newBaseNode;
String field;
boolean hasTreatedSubpath = false;
boolean lazy = false;
// The given path may be relative to the root or it might be an alias
if (objectLeafAllowed) {
Class> baseNodeClass = baseNode.getPropertyClass();
String typeName;
ManagedType> baseNodeType;
if (treatTypeName != null) {
typeName = treatTypeName;
baseNodeType = metamodel.managedType(treatTypeName);
} else {
typeName = baseNodeClass.getSimpleName();
baseNodeType = metamodel.managedType(baseNodeClass);
}
Attribute, ?> attr = JpaUtils.getSimpleAttributeForImplicitJoining(metamodel, baseNodeType, attributeName);
if (attr == null) {
throw new IllegalArgumentException("Field with name '" + attributeName + "' was not found within managed type " + typeName);
}
if (joinRequired || attr.isCollection()) {
final JoinResult newBaseNodeResult = implicitJoinSingle(baseNode, attributeName);
newBaseNode = newBaseNodeResult.baseNode;
// check if the last path element was also joined
if (newBaseNode != baseNode) {
field = null;
} else {
hasTreatedSubpath = treatTypeName != null;
field = attributeName;
}
} else {
newBaseNode = baseNode;
field = attributeName;
lazy = true;
hasTreatedSubpath = treatTypeName != null;
}
} else {
Class> baseNodeType = baseNode.getPropertyClass();
Attribute, ?> attr = JpaUtils.getSimpleAttributeForImplicitJoining(metamodel, metamodel.managedType(baseNodeType), attributeName);
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;
}
return new JoinResult(newBaseNode, field == null ? null : Arrays.asList(field), lazy, hasTreatedSubpath);
}
private void updateClauseDependencies(JoinNode baseNode, ClauseType clauseDependency) {
JoinNode current = baseNode;
while (current != null) {
// setSelectOnlyFalse for all JoinNodes that are used in the WITH clause of the current node
for (JoinNode dependency : current.getDependencies()) {
updateClauseDependencies(dependency, clauseDependency);
}
current.getClauseDependencies().add(clauseDependency);
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, String baseNodeTreatType, List joinRelationAttributes, String treatType, String alias, JoinType joinType, boolean implicit, boolean defaultJoin) {
Class> baseNodeType = baseNode.getPropertyClass();
ManagedType> type = metamodel.managedType(baseNodeType);
String joinRelationName = StringUtils.join(".", joinRelationAttributes);
AttributeJoinResult attrJoinResult = JpaUtils.getAttributeForJoining(metamodel, type, joinRelationName);
Attribute, ?> attr = attrJoinResult.getAttribute();
if (attr == null) {
throw new IllegalArgumentException("Field with name " + joinRelationName + " was not found within class " + baseNodeType.getName());
}
Class> resolvedFieldClass = JpaUtils.resolveFieldClass(attrJoinResult.getContainingClass(), attr);
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);
}
if (implicit) {
String aliasToUse = alias == null ? attr.getName() : alias;
alias = aliasManager.generatePostfixedAlias(aliasToUse);
}
if (joinType == null) {
joinType = getModelAwareType(baseNode, attr);
}
if (baseNodeTreatType != null) {
// Verify it's a valid type
metamodel.managedType(baseNodeTreatType).getJavaType();
}
if (treatType != null) {
// Verify it's a valid type
metamodel.managedType(treatType).getJavaType();
}
JoinNode newNode = getOrCreate(baseNode, baseNodeTreatType, joinRelationName, resolvedFieldClass, treatType, alias, joinType, "Ambiguous implicit join", implicit, defaultJoin, attr);
return new JoinResult(newNode, null);
}
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 baseNodeTreatType, String joinRelationName, Class> joinRelationClass, 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 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 = new JoinNode(baseNode, treeNode, baseNodeTreatType, newAliasInfo, type, joinRelationClass, treatType);
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 (treatType != null) {
if (!treatType.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 children = compoundPredicate.getChildren();
int size = children.size();
for (int i = 0; i < size; i++) {
if (pred.equals(children.get(i))) {
return true;
}
}
}
return false;
}
/**
* Base node will NOT be fetched
*
* @param node
*/
private void fetchPath(JoinNode node) {
JoinNode currentNode = node;
while (currentNode != null) {
currentNode.setFetch(true);
// fetches implicitly need to be selected
currentNode.getClauseDependencies().add(ClauseType.SELECT);
currentNode = currentNode.getParent();
}
}
// TODO: needs equals-hashCode implementation
private static class JoinResult {
final JoinNode baseNode;
final List fields;
final String typeName;
final boolean lazy;
final boolean hasTreatedSubpath;
public JoinResult(JoinNode baseNode, List fields) {
this(baseNode, fields, null);
}
public JoinResult(JoinNode baseNode, List fields, String typeName) {
this.baseNode = baseNode;
this.fields = fields;
this.typeName = typeName;
this.lazy = false;
this.hasTreatedSubpath = false;
}
public JoinResult(JoinNode baseNode, List fields, boolean lazy, boolean hasTreatedSubpath) {
this(baseNode, fields, null, lazy, hasTreatedSubpath);
}
public JoinResult(JoinNode baseNode, List fields, String typeName, boolean lazy, boolean hasTreatedSubpath) {
this.baseNode = baseNode;
this.fields = fields;
this.typeName = typeName;
this.lazy = lazy;
this.hasTreatedSubpath = hasTreatedSubpath;
}
private boolean hasField() {
return fields != null && !fields.isEmpty();
}
private String joinFields(String field) {
if (fields == null || fields.isEmpty()) {
return null;
}
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 addToList(List 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(FunctionExpression expression) {
boolean old = isKeyFunction;
this.isKeyFunction = com.blazebit.persistence.impl.util.ExpressionUtils.isKeyFunction(expression) || com.blazebit.persistence.impl.util.ExpressionUtils.isIndexFunction(expression);
super.visit(expression);
this.isKeyFunction = old;
}
@Override
public void visit(PathExpression expression) {
expression.setCollectionKeyPath(isKeyFunction);
super.visit(expression);
}
});
joinNode.setOnPredicate((CompoundPredicate) predicate);
}
}
}