/******************************************************************************* * Copyright (c) 2011, 2015 Oracle and/or its affiliates. All rights reserved. * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v1.0 and Eclipse Distribution License v. 1.0 * which accompanies this distribution. * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html * and the Eclipse Distribution License is available at * http://www.eclipse.org/org/documents/edl-v10.php. * * Contributors: * Oracle - initial API and implementation * ******************************************************************************/ package org.eclipse.persistence.internal.jpa.jpql; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Set; import org.eclipse.persistence.jpa.jpql.ExpressionTools; import org.eclipse.persistence.jpa.jpql.JPQLQueryDeclaration.Type; import org.eclipse.persistence.jpa.jpql.LiteralType; import org.eclipse.persistence.jpa.jpql.parser.AbstractEclipseLinkExpressionVisitor; import org.eclipse.persistence.jpa.jpql.parser.AbstractSchemaName; import org.eclipse.persistence.jpa.jpql.parser.CollectionExpression; import org.eclipse.persistence.jpa.jpql.parser.CollectionMemberDeclaration; import org.eclipse.persistence.jpa.jpql.parser.CollectionValuedPathExpression; import org.eclipse.persistence.jpa.jpql.parser.DeleteClause; import org.eclipse.persistence.jpa.jpql.parser.DeleteStatement; import org.eclipse.persistence.jpa.jpql.parser.Expression; import org.eclipse.persistence.jpa.jpql.parser.FromClause; import org.eclipse.persistence.jpa.jpql.parser.IdentificationVariable; import org.eclipse.persistence.jpa.jpql.parser.IdentificationVariableDeclaration; import org.eclipse.persistence.jpa.jpql.parser.JPQLExpression; import org.eclipse.persistence.jpa.jpql.parser.Join; import org.eclipse.persistence.jpa.jpql.parser.RangeVariableDeclaration; import org.eclipse.persistence.jpa.jpql.parser.ResultVariable; import org.eclipse.persistence.jpa.jpql.parser.SelectClause; import org.eclipse.persistence.jpa.jpql.parser.SelectStatement; import org.eclipse.persistence.jpa.jpql.parser.SimpleFromClause; import org.eclipse.persistence.jpa.jpql.parser.SimpleSelectClause; import org.eclipse.persistence.jpa.jpql.parser.SimpleSelectStatement; import org.eclipse.persistence.jpa.jpql.parser.SubExpression; import org.eclipse.persistence.jpa.jpql.parser.TableVariableDeclaration; import org.eclipse.persistence.jpa.jpql.parser.UpdateClause; import org.eclipse.persistence.jpa.jpql.parser.UpdateStatement; /** * This visitor visits the declaration clause of the JPQL query and creates the list of * {@link Declaration Declarations}. * * @version 2.5 * @since 2.4 * @author Pascal Filion */ @SuppressWarnings("nls") final class DeclarationResolver { /** * The first {@link Declaration} that was created when visiting the declaration clause. */ private Declaration baseDeclaration; /** * The {@link Declaration} objects mapped to their identification variable. */ private List<Declaration> declarations; /** * The parent {@link DeclarationResolver} which represents the superquery's declaration or * <code>null</code> if this is used for the top-level query. */ private DeclarationResolver parent; /** * Determines whether the {@link Declaration Declaration} objects were created after visiting the * query's declaration clause. */ private boolean populated; /** * The {@link JPQLQueryContext} is used to query information about the application metadata and * cached information. */ private JPQLQueryContext queryContext; /** * The result variables used to identify select expressions. */ private Collection<IdentificationVariable> resultVariables; /** * Creates a new <code>DeclarationResolver</code>. * * @param queryContext The context used to query information about the application metadata and * cached information * @param parent The parent {@link DeclarationResolver} which represents the superquery's declaration */ DeclarationResolver(JPQLQueryContext queryContext, DeclarationResolver parent) { super(); initialize(queryContext, parent); } /** * Adds a "virtual" range variable declaration that will be used when parsing a JPQL fragment. * * @param entityName The name of the entity to be accessible with the given variable name * @param variableName The identification variable used to navigate to the entity */ void addRangeVariableDeclaration(String entityName, String variableName) { // This method should only be used by HermesParser.buildSelectionCriteria(), // initializes these variables right away since this method should only be // called by HermesParser.buildSelectionCriteria() populated = true; resultVariables = Collections.emptySet(); // Create the "virtual" range variable declaration RangeVariableDeclaration rangeVariableDeclaration = new RangeVariableDeclaration( entityName, variableName ); // Make sure the identification variable was not declared more than once, // this could cause issues when trying to resolve it RangeDeclaration declaration = new RangeDeclaration(queryContext); declaration.rootPath = entityName; declaration.baseExpression = rangeVariableDeclaration; declaration.identificationVariable = (IdentificationVariable) rangeVariableDeclaration.getIdentificationVariable(); declarations.add(declaration); // Make sure it is marked as the base declaration and the base Expression is created if (baseDeclaration == null) { baseDeclaration = declaration; // Make sure the base Expression is initialized, which will cache it // into the right context as well (the top-level context) declaration.getQueryExpression(); } } /** * Converts the given {@link Declaration} from being set as a range variable declaration to * a path expression declaration. * <p> * In this query "<code>UPDATE Employee SET firstName = 'MODIFIED' WHERE (SELECT COUNT(m) FROM * managedEmployees m) > 0</code>" <em>managedEmployees</em> is an unqualified collection-valued * path expression (<code>employee.managedEmployees</code>). * * @param declaration The {@link Declaration} that was parsed to range over an abstract schema * name but is actually ranging over a path expression * @param outerVariableName The identification variable coming from the parent identification * variable declaration */ void convertUnqualifiedDeclaration(RangeDeclaration declaration, String outerVariableName) { QualifyRangeDeclarationVisitor visitor = new QualifyRangeDeclarationVisitor(); // Convert the declaration expression into a derived declaration visitor.declaration = declaration; visitor.outerVariableName = outerVariableName; visitor.queryContext = queryContext.getCurrentContext(); declaration.declarationExpression.accept(visitor); // Now replace the old declaration with the new one int index = declarations.indexOf(declaration); declarations.set(index, visitor.declaration); // Update the base declaration if (baseDeclaration == declaration) { baseDeclaration = visitor.declaration; } } /** * Retrieves the {@link Declaration} for which the given variable name is used to navigate to the * "root" object. * * @param variableName The name of the identification variable that is used to navigate a "root" * object * @return The {@link Declaration} containing the information about the identification variable * declaration */ Declaration getDeclaration(String variableName) { for (Declaration declaration : declarations) { if (declaration.getVariableName().equalsIgnoreCase(variableName)) { return declaration; } } return null; } /** * Returns the ordered list of {@link Declaration Declarations}. * * @return The {@link Declaration Declarations} of the current query that was parsed */ List<Declaration> getDeclarations() { return declarations; } /** * Returns the first {@link Declaration} that was created after visiting the declaration clause. * * @return The first {@link Declaration} object */ Declaration getFirstDeclaration() { return baseDeclaration; } /** * Returns the parsed representation of a <b>JOIN FETCH</b> that were defined in the same * declaration than the given range identification variable name. * * @param variableName The name of the identification variable that should be used to define an entity * @return The <b>JOIN FETCH</b> expressions used in the same declaration or an empty collection * if none was defined */ Collection<Join> getJoinFetches(String variableName) { Declaration declaration = getDeclaration(variableName); if ((declaration != null) && (declaration.getType() == Type.RANGE)) { RangeDeclaration rangeDeclaration = (RangeDeclaration) declaration; if (rangeDeclaration.hasJoins()) { return rangeDeclaration.getJoinFetches(); } } return null; } /** * Returns the parent of this {@link DeclarationResolver}. * * @return The parent of this {@link DeclarationResolver} if this is used for a subquery or * <code>null</code> if this is used for the top-level query */ DeclarationResolver getParent() { return parent; } /** * Returns the variables that got defined in the select expression. This only applies to JPQL * queries built for JPA 2.0 or later. * * @return The variables identifying the select expressions, if any was defined or an empty set * if none were defined */ Collection<IdentificationVariable> getResultVariables() { if (parent != null) { return parent.getResultVariables(); } if (resultVariables == null) { ResultVariableVisitor visitor = new ResultVariableVisitor(); queryContext.getJPQLExpression().accept(visitor); resultVariables = visitor.resultVariables; } return resultVariables; } /** * Initializes this <code>DeclarationResolver</code>. * * @param queryContext The context used to query information about the query * @param parent The parent {@link DeclarationResolver}, which is not <code>null</code> when this * resolver is created for a subquery */ private void initialize(JPQLQueryContext queryContext, DeclarationResolver parent) { this.parent = parent; this.queryContext = queryContext; this.declarations = new LinkedList<Declaration>(); } /** * Determines whether the given identification variable is defining a <b>JOIN</b> expression or * in a <code>IN</code> expressions for a collection-valued field. If the search didn't find the * identification in this resolver, then it will traverse the parent hierarchy. * * @param variableName The identification variable to check for what it maps * @return <code>true</code> if the given identification variable maps a collection-valued field * defined in a <code>JOIN</code> or <code>IN</code> expression; <code>false</code> otherwise */ boolean isCollectionIdentificationVariable(String variableName) { boolean result = isCollectionIdentificationVariableImp(variableName); if (!result && (parent != null)) { result = parent.isCollectionIdentificationVariableImp(variableName); } return result; } /** * Determines whether the given identification variable is defining a <b>JOIN</b> expression or * in a <code>IN</code> expressions for a collection-valued field. The search does not traverse * the parent hierarchy. * * @param variableName The identification variable to check for what it maps * @return <code>true</code> if the given identification variable maps a collection-valued field * defined in a <code>JOIN</code> or <code>IN</code> expression; <code>false</code> otherwise */ boolean isCollectionIdentificationVariableImp(String variableName) { for (Declaration declaration : declarations) { switch (declaration.getType()) { case COLLECTION: { if (declaration.getVariableName().equalsIgnoreCase(variableName)) { return true; } return false; } case RANGE: case DERIVED: { AbstractRangeDeclaration rangeDeclaration = (AbstractRangeDeclaration) declaration; // Check the JOIN expressions for (Join join : rangeDeclaration.getJoins()) { String joinVariableName = queryContext.literal( join.getIdentificationVariable(), LiteralType.IDENTIFICATION_VARIABLE ); if (joinVariableName.equalsIgnoreCase(variableName)) { // Make sure the JOIN expression maps a collection mapping Declaration joinDeclaration = queryContext.getDeclaration(joinVariableName); return joinDeclaration.getMapping().isCollectionMapping(); } } } default: continue; } } return false; } /** * Determines whether the given variable name is an identification variable name used to define * an abstract schema name. * * @param variableName The name of the variable to verify if it's defined in a range variable * declaration in the current query or any parent query * @return <code>true</code> if the variable name is mapping an abstract schema name; <code>false</code> * if it's defined in a collection member declaration */ boolean isRangeIdentificationVariable(String variableName) { boolean result = isRangeIdentificationVariableImp(variableName); if (!result && (parent != null)) { result = parent.isRangeIdentificationVariableImp(variableName); } return result; } private boolean isRangeIdentificationVariableImp(String variableName) { Declaration declaration = getDeclaration(variableName); return (declaration != null) && declaration.getType().isRange(); } /** * Determines whether the given variable is a result variable or not. * * @param variableName The variable to check if it used to identify a select expression * @return <code>true</code> if the given variable is defined as a result variable; * <code>false</code> otherwise */ boolean isResultVariable(String variableName) { // Only the top-level SELECT query has result variables if (parent != null) { return parent.isResultVariable(variableName); } for (IdentificationVariable resultVariable : getResultVariables()) { if (resultVariable.getText().equalsIgnoreCase(variableName)) { return true; } } return false; } /** * Visits the given {@link Expression} (which is either the top-level query or a subquery) and * retrieve the information from its declaration clause. * * @param expression The {@link Expression} to visit in order to retrieve the information * contained in the given query's declaration */ void populate(Expression expression) { if (!populated) { populated = true; populateImp(expression); } } private void populateImp(Expression expression) { DeclarationVisitor visitor = new DeclarationVisitor(); visitor.queryContext = queryContext; visitor.declarations = declarations; expression.accept(visitor); baseDeclaration = visitor.baseDeclaration; } private static class DeclarationVisitor extends AbstractEclipseLinkExpressionVisitor { /** * The first {@link Declaration} that was created when visiting the declaration clause. */ private Declaration baseDeclaration; /** * This flag is used to determine what to do in {@link #visit(SimpleSelectStatement)}. */ private boolean buildingDeclaration; /** * The {@link Declaration} being populated. */ private Declaration currentDeclaration; /** * The list of {@link Declaration} objects to which new ones will be added by traversing the * declaration clause. */ List<Declaration> declarations; /** * The {@link JPQLQueryContext} is used to query information about the application metadata and * cached information. */ JPQLQueryContext queryContext; /** * {@inheritDoc} */ @Override public void visit(AbstractSchemaName expression) { String rootPath = expression.getText(); // Abstract schema name (entity name) if (rootPath.indexOf('.') == -1) { currentDeclaration = new RangeDeclaration(queryContext); } else { // Check to see if the "root" path is a class name before assuming it's a derived path Class<?> type = queryContext.getType(rootPath); // Fully qualified class name if (type != null) { RangeDeclaration declaration = new RangeDeclaration(queryContext); declaration.type = type; currentDeclaration = declaration; } // Derived path expression (for subqueries) else { currentDeclaration = new DerivedDeclaration(queryContext); } } currentDeclaration.rootPath = rootPath; } /** * {@inheritDoc} */ @Override public void visit(CollectionExpression expression) { expression.acceptChildren(this); } /** * {@inheritDoc} */ @Override public void visit(CollectionMemberDeclaration expression) { Declaration declaration = new CollectionDeclaration(queryContext); declaration.baseExpression = expression.getCollectionValuedPathExpression(); declaration.rootPath = declaration.baseExpression.toActualText(); declaration.declarationExpression = expression; declarations.add(declaration); // A derived collection member declaration does not have an identification variable if (!expression.isDerived()) { IdentificationVariable identificationVariable = (IdentificationVariable) expression.getIdentificationVariable(); declaration.identificationVariable = identificationVariable; } // This collection member declaration is the first defined, // it is then the base Declaration if (baseDeclaration == null) { baseDeclaration = declaration; } } /** * {@inheritDoc} */ @Override public void visit(CollectionValuedPathExpression expression) { String rootPath = expression.toParsedText(); // Check to see if the "root" path is a class name before assuming it's a derived path Class<?> type = queryContext.getType(rootPath); // Fully qualified class name if (type != null) { RangeDeclaration declaration = new RangeDeclaration(queryContext); declaration.type = type; currentDeclaration = declaration; } // Derived path expression (for subqueries) else { currentDeclaration = new DerivedDeclaration(queryContext); } currentDeclaration.rootPath = rootPath; } /** * {@inheritDoc} */ @Override public void visit(DeleteClause expression) { try { expression.getRangeVariableDeclaration().accept(this); } finally { currentDeclaration = null; } } /** * {@inheritDoc} */ @Override public void visit(DeleteStatement expression) { expression.getDeleteClause().accept(this); } /** * {@inheritDoc} */ @Override public void visit(FromClause expression) { expression.getDeclaration().accept(this); } /** * {@inheritDoc} */ @Override public void visit(IdentificationVariableDeclaration expression) { try { // Visit the RangeVariableDeclaration, it will create the right Declaration expression.getRangeVariableDeclaration().accept(this); currentDeclaration.declarationExpression = expression; // Now visit the JOIN expressions expression.getJoins().accept(this); } finally { currentDeclaration = null; } } /** * {@inheritDoc} */ @Override public void visit(Join expression) { ((AbstractRangeDeclaration) currentDeclaration).addJoin(expression); if (!expression.hasFetch() || expression.hasIdentificationVariable()) { IdentificationVariable identificationVariable = (IdentificationVariable) expression.getIdentificationVariable(); JoinDeclaration declaration = new JoinDeclaration(queryContext); declaration.baseExpression = expression; declaration.identificationVariable = identificationVariable; declarations.add(declaration); } } /** * {@inheritDoc} */ @Override public void visit(JPQLExpression expression) { expression.getQueryStatement().accept(this); } /** * {@inheritDoc} */ @Override public void visit(RangeVariableDeclaration expression) { // Traverse the "root" object, it will create the right Declaration buildingDeclaration = true; expression.getRootObject().accept(this); buildingDeclaration = false; // Cache more information currentDeclaration.identificationVariable = (IdentificationVariable) expression.getIdentificationVariable(); currentDeclaration.baseExpression = expression; declarations.add(currentDeclaration); // This range variable declaration is the first defined, // it is then the base declaration if (baseDeclaration == null) { baseDeclaration = currentDeclaration; } } /** * {@inheritDoc} */ @Override public void visit(SelectStatement expression) { expression.getFromClause().accept(this); } /** * {@inheritDoc} */ @Override public void visit(SimpleFromClause expression) { expression.getDeclaration().accept(this); } /** * {@inheritDoc} */ @Override public void visit(SimpleSelectClause expression) { expression.getSelectExpression().accept(this); } /** * {@inheritDoc} */ @Override public void visit(SimpleSelectStatement expression) { // The parent query is using a subquery in the FROM clause if (buildingDeclaration) { currentDeclaration = new SubqueryDeclaration(queryContext); currentDeclaration.rootPath = ExpressionTools.EMPTY_STRING; } // Simply traversing the tree to create the declarations else { expression.getFromClause().accept(this); } } /** * {@inheritDoc} */ @Override public void visit(SubExpression expression) { expression.getExpression().accept(this); } /** * {@inheritDoc} */ @Override public void visit(TableVariableDeclaration expression) { TableDeclaration declaration = new TableDeclaration(queryContext); declaration.declarationExpression = expression; declaration.baseExpression = expression.getTableExpression(); declaration.rootPath = declaration.baseExpression.toParsedText(); declaration.identificationVariable = (IdentificationVariable) expression.getIdentificationVariable(); declarations.add(declaration); } /** * {@inheritDoc} */ @Override public void visit(UpdateClause expression) { try { expression.getRangeVariableDeclaration().accept(this); } finally { currentDeclaration = null; } } /** * {@inheritDoc} */ @Override public void visit(UpdateStatement expression) { expression.getUpdateClause().accept(this); } } private static class QualifyRangeDeclarationVisitor extends AbstractEclipseLinkExpressionVisitor { /** * The {@link Declaration} being modified. */ AbstractRangeDeclaration declaration; /** * The identification variable coming from the parent identification variable declaration. */ String outerVariableName; /** * The {@link JPQLQueryContext} is used to query information about the application metadata and * cached information. */ JPQLQueryContext queryContext; /** * {@inheritDoc} */ @Override public void visit(CollectionValuedPathExpression expression) { // Create the path because CollectionValuedPathExpression.toParsedText() // does not contain the virtual identification variable StringBuilder rootPath = new StringBuilder(); rootPath.append(outerVariableName); rootPath.append("."); rootPath.append(expression.toParsedText()); declaration.rootPath = rootPath.toString(); } /** * {@inheritDoc} */ @Override public void visit(IdentificationVariableDeclaration expression) { expression.getRangeVariableDeclaration().accept(this); declaration.declarationExpression = expression; } /** * {@inheritDoc} */ @Override public void visit(RangeVariableDeclaration expression) { DerivedDeclaration derivedDeclaration = new DerivedDeclaration(queryContext); derivedDeclaration.joins = declaration.joins; derivedDeclaration.rootPath = declaration.rootPath; derivedDeclaration.baseExpression = declaration.baseExpression; derivedDeclaration.identificationVariable = declaration.identificationVariable; declaration = derivedDeclaration; expression.setVirtualIdentificationVariable(outerVariableName, declaration.rootPath); expression.getRootObject().accept(this); } } /** * This visitor traverses the <code><b>SELECT</b></code> clause and retrieves the result variables. */ private static class ResultVariableVisitor extends AbstractEclipseLinkExpressionVisitor { Set<IdentificationVariable> resultVariables; /** * Creates a new <code>ResultVariableVisitor</code>. */ public ResultVariableVisitor() { super(); resultVariables = new HashSet<IdentificationVariable>(); } /** * {@inheritDoc} */ @Override public void visit(CollectionExpression expression) { expression.acceptChildren(this); } /** * {@inheritDoc} */ @Override public void visit(JPQLExpression expression) { expression.getQueryStatement().accept(this); } /** * {@inheritDoc} */ @Override public void visit(ResultVariable expression) { IdentificationVariable identificationVariable = (IdentificationVariable) expression.getResultVariable(); resultVariables.add(identificationVariable); } /** * {@inheritDoc} */ @Override public void visit(SelectClause expression) { expression.getSelectExpression().accept(this); } /** * {@inheritDoc} */ @Override public void visit(SelectStatement expression) { expression.getSelectClause().accept(this); } } }