blob: b98a16b4e7815e5be32f5cd718e5bb068495421d [file] [log] [blame]
/*
* Copyright (c) 2011, 2021 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 v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0,
* or the Eclipse Distribution License v. 1.0 which is available at
* http://www.eclipse.org/org/documents/edl-v10.php.
*
* SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
*/
// 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<>();
}
/**
* 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;
@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;
}
@Override
public void visit(CollectionExpression expression) {
expression.acceptChildren(this);
}
@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;
}
}
@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;
}
@Override
public void visit(DeleteClause expression) {
try {
expression.getRangeVariableDeclaration().accept(this);
}
finally {
currentDeclaration = null;
}
}
@Override
public void visit(DeleteStatement expression) {
expression.getDeleteClause().accept(this);
}
@Override
public void visit(FromClause expression) {
expression.getDeclaration().accept(this);
}
@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;
}
}
@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);
}
}
@Override
public void visit(JPQLExpression expression) {
expression.getQueryStatement().accept(this);
}
@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;
}
}
@Override
public void visit(SelectStatement expression) {
expression.getFromClause().accept(this);
}
@Override
public void visit(SimpleFromClause expression) {
expression.getDeclaration().accept(this);
}
@Override
public void visit(SimpleSelectClause expression) {
expression.getSelectExpression().accept(this);
}
@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);
}
}
@Override
public void visit(SubExpression expression) {
expression.getExpression().accept(this);
}
@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);
}
@Override
public void visit(UpdateClause expression) {
try {
expression.getRangeVariableDeclaration().accept(this);
}
finally {
currentDeclaration = null;
}
}
@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;
@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();
}
@Override
public void visit(IdentificationVariableDeclaration expression) {
expression.getRangeVariableDeclaration().accept(this);
declaration.declarationExpression = expression;
}
@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<>();
}
@Override
public void visit(CollectionExpression expression) {
expression.acceptChildren(this);
}
@Override
public void visit(JPQLExpression expression) {
expression.getQueryStatement().accept(this);
}
@Override
public void visit(ResultVariable expression) {
IdentificationVariable identificationVariable = (IdentificationVariable) expression.getResultVariable();
resultVariables.add(identificationVariable);
}
@Override
public void visit(SelectClause expression) {
expression.getSelectExpression().accept(this);
}
@Override
public void visit(SelectStatement expression) {
expression.getSelectClause().accept(this);
}
}
}