blob: 08e934a3da4f88630fed5fc8c5e12baa317fccf6 [file] [log] [blame]
/*
* Copyright (c) 2006, 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
//
// 09/02/2019-3.0 Alexandre Jacob
// - 527415: Fix code when locale is tr, az or lt
package org.eclipse.persistence.jpa.jpql.tools.resolver;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import org.eclipse.persistence.jpa.jpql.ExpressionTools;
import org.eclipse.persistence.jpa.jpql.LiteralType;
import org.eclipse.persistence.jpa.jpql.parser.AbstractExpressionVisitor;
import org.eclipse.persistence.jpa.jpql.parser.AbstractSchemaName;
import org.eclipse.persistence.jpa.jpql.parser.AnonymousExpressionVisitor;
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.NullExpression;
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.UpdateClause;
import org.eclipse.persistence.jpa.jpql.parser.UpdateStatement;
import org.eclipse.persistence.jpa.jpql.tools.JPQLQueryContext;
import org.eclipse.persistence.jpa.jpql.tools.spi.IMapping;
import org.eclipse.persistence.jpa.jpql.tools.spi.IQuery;
import org.eclipse.persistence.jpa.jpql.tools.spi.IType;
import org.eclipse.persistence.jpa.jpql.tools.spi.ITypeDeclaration;
/**
* This {@link Resolver} is responsible to visit the current query (which is either the top-level
* query or a subquery) and gathers the information from the declaration clause. For a <b>SELECT</b>
* or <b>DELETE</b> clause, the information will be retrieved from the <b>FROM</b> clause. For an
* <code>UDPATE</code> clause, it will be retrieved from the unique identification range variable
* declaration.
*
* @version 2.5
* @since 2.3
* @author Pascal Filion
*/
public class DeclarationResolver extends Resolver {
/**
* The {@link Declaration Declarations} of the current query that was parsed.
*/
private List<Declaration> declarations;
/**
* This visitor is responsible to visit the current query's declaration and populate this
* resolver with the list of declarations.
*/
private DeclarationVisitor declarationVisitor;
/**
* This visitor is responsible to convert the abstract schema name into a path expression.
*/
private QualifyRangeDeclarationVisitor qualifyRangeDeclarationVisitor;
/**
* The context used to query information about the query.
*/
private JPQLQueryContext queryContext;
/**
* The identification variable names mapped to their resolvers.
*/
private Map<String, Resolver> resolvers;
/**
* The variables identifying the select expressions, if any was defined or an empty set if none
* were defined.
*/
private Map<IdentificationVariable, String> resultVariables;
/**
* This visitor resolves the "root" object represented by the given {@link Expression}. This will
* also handle using a subquery in the <code><b>FROM</b></code> clause. This is only supported by
* EclipseLink.
*/
private RootObjectExpressionVisitor rootObjectExpressionVisitor;
/**
* Creates a new <code>DeclarationResolver</code>.
*
* @param parent The parent resolver if this is used for a subquery or null if it's used for the
* top-level query
* @param queryContext The context used to query information about the query
*/
public DeclarationResolver(DeclarationResolver parent, JPQLQueryContext queryContext) {
super(parent);
initialize(queryContext);
}
/**
* Adds the given {@link Declaration} at the end of the list.
*
* @param declaration The {@link Declaration} representing a single variable declaration
*/
protected final void addDeclaration(Declaration declaration) {
declarations.add(declaration);
}
/**
* Registers a range variable declaration that will be used when a JPQL fragment is parsed.
*
* @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
*/
public void addRangeVariableDeclaration(String entityName, String variableName) {
// Always make the identification variable be upper case since it's
// case insensitive, the get will also use upper case
String internalVariableName = variableName.toUpperCase(Locale.ROOT);
// Make sure the identification variable was not declared more than once,
// this could cause issues when trying to resolve it
if (!resolvers.containsKey(internalVariableName)) {
// Resolve the expression and map it with the identification variable
Resolver resolver = new EntityResolver(this, entityName);
resolver = new IdentificationVariableResolver(resolver, variableName);
resolvers.put(internalVariableName, resolver);
RangeDeclaration declaration = new RangeDeclaration();
declaration.rootPath = entityName;
declaration.identificationVariable = new IdentificationVariable(null, variableName);
declarations.add(declaration);
}
}
protected DeclarationVisitor buildDeclarationVisitor() {
return new DeclarationVisitor();
}
protected RootObjectExpressionVisitor buildRootObjectExpressionVisitor() {
return new RootObjectExpressionVisitor();
}
@Override
protected IType buildType() {
return getTypeHelper().unknownType();
}
@Override
protected ITypeDeclaration buildTypeDeclaration() {
return getTypeHelper().unknownTypeDeclaration();
}
@Override
protected void checkParent(Resolver parent) {
// Don't do anything, this is the root
}
/**
* 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) {@literal >} 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
*/
public void convertUnqualifiedDeclaration(Declaration declaration, String outerVariableName) {
QualifyRangeDeclarationVisitor visitor = qualifyRangeDeclarationVisitor();
try {
visitor.oldDeclaration = declaration;
visitor.outerVariableName = outerVariableName;
declaration.declarationExpression.accept(visitor);
}
finally {
visitor.oldDeclaration = null;
visitor.newDeclaration = null;
visitor.outerVariableName = null;
}
}
/**
* Disposes the internal data.
*/
public void dispose() {
resolvers .clear();
declarations .clear();
resultVariables.clear();
}
/**
* 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
* @since 2.5
*/
public 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
*/
public List<Declaration> getDeclarations() {
return declarations;
}
protected DeclarationVisitor getDeclarationVisitor() {
if (declarationVisitor == null) {
declarationVisitor = buildDeclarationVisitor();
}
return declarationVisitor;
}
@Override
public DeclarationResolver getParent() {
return (DeclarationResolver) super.getParent();
}
@Override
public IQuery getQuery() {
return queryContext.getQuery();
}
/**
* Returns the {@link JPQLQueryContext} that is used by this visitor.
*
* @return The {@link JPQLQueryContext} holding onto the JPQL query and the cached information
*/
protected JPQLQueryContext getQueryContext() {
return queryContext;
}
/**
* Retrieves the {@link Resolver} mapped with the given identification variable. If the
* identification is not defined in the declaration traversed by this resolver, than the search
* will traverse the parent hierarchy.
*
* @param variableName The identification variable that maps a {@link Resolver}
* @return The {@link Resolver} mapped with the given identification variable
*/
public Resolver getResolver(String variableName) {
variableName = variableName.toUpperCase(Locale.ROOT);
Resolver resolver = getResolverImp(variableName);
if ((resolver == null) && (getParent() != null)) {
resolver = getParent().getResolver(variableName);
}
if (resolver == null) {
resolver = new NullResolver(this);
resolvers.put(variableName, resolver);
}
return resolver;
}
/**
* Retrieves the {@link Resolver} mapped with the given identification variable. The search does
* not traverse the parent hierarchy.
*
* @param variableName The identification variable that maps a {@link Resolver}
* @return The {@link Resolver} mapped with the given identification variable
*/
protected Resolver getResolverImp(String variableName) {
return resolvers.get(variableName);
}
/**
* 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
*/
public Set<String> getResultVariables() {
return new HashSet<String>(resultVariables.values());
}
/**
* Returns the map of result variables that got used to define a select expression. This only
* applies to JPQL queries built for JPA 2.0.
*
* @return The variables identifying the select expressions, if any was defined or an empty map
* if none were defined
*/
public Map<IdentificationVariable, String> getResultVariablesMap() {
return resultVariables;
}
protected RootObjectExpressionVisitor getRootObjectExpressionVisitor() {
if (rootObjectExpressionVisitor == null) {
rootObjectExpressionVisitor = buildRootObjectExpressionVisitor();
}
return rootObjectExpressionVisitor;
}
/**
* Determines whether the JPQL expression has <b>JOIN</b> expressions.
*
* @return <code>true</code> if the query or subquery being traversed contains <b>JOIN</b>
* expressions; <code>false</code> otherwise
*/
public boolean hasJoins() {
for (Declaration declaration : declarations) {
if (declaration.hasJoins()) {
return true;
}
}
return false;
}
/**
* Initializes this <code>DeclarationResolver</code>.
*
* @param queryContext The context used to query information about the query
*/
protected void initialize(JPQLQueryContext queryContext) {
this.queryContext = queryContext;
this.resolvers = new HashMap<String, Resolver>();
this.declarations = new LinkedList<Declaration>();
this.resultVariables = new HashMap<IdentificationVariable, String>();
}
/**
* 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
*/
public boolean isCollectionIdentificationVariable(String variableName) {
boolean result = isCollectionIdentificationVariableImp(variableName);
if (!result && (getParent() != null)) {
result = getParent().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
*/
protected 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
Resolver resolver = getResolver(variableName);
IMapping mapping = (resolver != null) ? resolver.getMapping() : null;
return (mapping != null) && mapping.isCollection();
}
}
}
default:
continue;
}
}
return false;
}
/**
* Determines whether the given variable name is an identification variable name mapping an
* entity. The search traverses the parent resolver if it is not found in this resolver, which
* represents the current JPQL query.
*
* @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
*/
public boolean isRangeIdentificationVariable(String variableName) {
boolean result = isRangeIdentificationVariableImp(variableName);
if (!result && (getParent() != null)) {
result = getParent().isRangeIdentificationVariableImp(variableName);
}
return result;
}
/**
* Determines whether the given variable name is an identification variable name mapping an
* entity. The search only searches in this resolver, which represents the current JPQL query.
*
* @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
*/
protected boolean isRangeIdentificationVariableImp(String variableName) {
for (Declaration declaration : declarations) {
if (declaration.getType().isRange() &&
declaration.getVariableName().equalsIgnoreCase(variableName)) {
return true;
}
}
return false;
}
/**
* Determines if the given variable is a result variable.
*
* @param variable The variable to check if it's a result variable
* @return <code>true</code> if the given variable is defined as a result variable; <code>false</code>
* otherwise
*/
public boolean isResultVariable(String variable) {
return resultVariables.containsValue(variable.toUpperCase(Locale.ROOT));
}
/**
* Visits the current query (which is either the top-level query or a subquery) and gathers the
* information from the declaration clause.
*
* @param expression The {@link Expression} to visit in order to retrieve the information
* contained in the given query's declaration
*/
public void populate(Expression expression) {
DeclarationVisitor visitor = getDeclarationVisitor();
try {
expression.accept(visitor);
}
finally {
visitor.currentDeclaration = null;
}
}
protected QualifyRangeDeclarationVisitor qualifyRangeDeclarationVisitor() {
if (qualifyRangeDeclarationVisitor == null) {
qualifyRangeDeclarationVisitor = new QualifyRangeDeclarationVisitor();
}
return qualifyRangeDeclarationVisitor;
}
/**
* Resolves the "root" object represented by the given {@link Expression}. This will also handle
* using a subquery in the <code><b>FROM</b></code> clause. This is only supported by EclipseLink.
*
* @param expression The {@link Expression} to visit, which represents the "root" object of a
* declaration
* @return The {@link Resolver} for the given {@link Expression}
*/
protected Resolver resolveRootObject(Expression expression) {
RootObjectExpressionVisitor visitor = getRootObjectExpressionVisitor();
try {
expression.accept(visitor);
return visitor.resolver;
}
finally {
visitor.resolver = null;
}
}
protected String visitDeclaration(Expression expression, Expression identificationVariable) {
// Visit the identification variable expression and retrieve the identification variable name
String variableName = queryContext.literal(
identificationVariable,
LiteralType.IDENTIFICATION_VARIABLE
);
// If it's not empty, then we can create a Resolver
if (variableName != ExpressionTools.EMPTY_STRING) {
// Always make the identification variable be upper case since it's
// case insensitive, the get will also use upper case
String internalVariableName = variableName.toUpperCase(Locale.ROOT);
// Make sure the identification variable was not declared more than once,
// this could cause issues when trying to resolve it
if (!resolvers.containsKey(internalVariableName)) {
// Resolve the expression and map it with the identification variable
Resolver resolver = resolveRootObject(expression);
resolver = new IdentificationVariableResolver(resolver, variableName);
resolvers.put(internalVariableName, resolver);
}
return variableName;
}
return ExpressionTools.EMPTY_STRING;
}
protected class DeclarationVisitor extends AbstractExpressionVisitor {
/**
* This flag is used to determine what to do in {@link #visit(SimpleSelectStatement)}.
*/
protected boolean buildingDeclaration;
/**
* Flag used to determine if the {@link IdentificationVariable} to visit is a result variable.
*/
protected boolean collectResultVariable;
/**
* The {@link Declaration} being populated.
*/
protected Declaration currentDeclaration;
@Override
public void visit(AbstractSchemaName expression) {
currentDeclaration = new RangeDeclaration();
currentDeclaration.rootPath = expression.getText();
declarations.add(currentDeclaration);
}
@Override
public void visit(CollectionExpression expression) {
expression.acceptChildren(this);
}
@Override
public void visit(CollectionMemberDeclaration expression) {
CollectionDeclaration declaration = new CollectionDeclaration();
declaration.declarationExpression = expression;
declaration.baseExpression = expression.getCollectionValuedPathExpression();
declaration.rootPath = declaration.baseExpression.toActualText();
declarations.add(declaration);
// Retrieve the IdentificationVariable
Expression identificationVariable = expression.getIdentificationVariable();
String variableName = visitDeclaration(expression, identificationVariable);
if (variableName != ExpressionTools.EMPTY_STRING) {
declaration.identificationVariable = (IdentificationVariable) identificationVariable;
}
}
@Override
public void visit(CollectionValuedPathExpression expression) {
currentDeclaration = new DerivedDeclaration();
currentDeclaration.rootPath = expression.toActualText();
declarations.add(currentDeclaration);
}
@Override
public void visit(DeleteClause expression) {
try {
buildingDeclaration = true;
expression.getRangeVariableDeclaration().accept(this);
buildingDeclaration = false;
currentDeclaration.declarationExpression = expression;
}
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(IdentificationVariable expression) {
if (collectResultVariable) {
resultVariables.put(expression, expression.getText().toUpperCase(Locale.ROOT));
}
}
@Override
public void visit(IdentificationVariableDeclaration expression) {
try {
expression.getRangeVariableDeclaration().accept(this);
expression.getJoins().accept(this);
currentDeclaration.declarationExpression = expression;
}
finally {
currentDeclaration = null;
}
}
@Override
public void visit(Join expression) {
AbstractRangeDeclaration rangeDeclaration = (AbstractRangeDeclaration) currentDeclaration;
Expression identificationVariable = expression.getIdentificationVariable();
visitDeclaration(expression, identificationVariable);
rangeDeclaration.addJoin(expression);
}
@Override
public void visit(JPQLExpression expression) {
expression.getQueryStatement().accept(this);
}
@Override
public void visit(NullExpression expression) {
if (buildingDeclaration) {
currentDeclaration = new UnknownDeclaration();
currentDeclaration.baseExpression = expression;
currentDeclaration.rootPath = ExpressionTools.EMPTY_STRING;
declarations.add(currentDeclaration);
}
}
@Override
public void visit(RangeVariableDeclaration expression) {
// Visit the "root" object, which will create the right Declaration
Expression rootObject = expression.getRootObject();
buildingDeclaration = true;
rootObject.accept(this);
buildingDeclaration = false;
// Set the base expression
currentDeclaration.baseExpression = expression;
// Set the identification variable
Expression identificationVariable = expression.getIdentificationVariable();
String variableName = visitDeclaration(rootObject, identificationVariable);
if (variableName != ExpressionTools.EMPTY_STRING) {
currentDeclaration.identificationVariable = (IdentificationVariable) identificationVariable;
}
}
@Override
public void visit(ResultVariable expression) {
collectResultVariable = true;
expression.getResultVariable().accept(this);
collectResultVariable = false;
}
@Override
public void visit(SelectClause expression) {
expression.getSelectExpression().accept(this);
}
@Override
public void visit(SelectStatement expression) {
expression.getFromClause().accept(this);
expression.getSelectClause().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) {
expression.getFromClause().accept(this);
}
@Override
public void visit(UpdateClause expression) {
try {
buildingDeclaration = true;
expression.getRangeVariableDeclaration().accept(this);
buildingDeclaration = false;
currentDeclaration.declarationExpression = expression;
}
finally {
currentDeclaration = null;
}
}
@Override
public void visit(UpdateStatement expression) {
expression.getUpdateClause().accept(this);
}
}
protected static class QualifyRangeDeclarationVisitor extends AbstractExpressionVisitor {
/**
* The new {@link Declaration}.
*/
protected Declaration newDeclaration;
/**
* The {@link Declaration} being modified.
*/
protected Declaration oldDeclaration;
/**
* The identification variable coming from the parent identification variable declaration.
*/
protected String outerVariableName;
@Override
public void visit(CollectionValuedPathExpression expression) {
newDeclaration.rootPath = expression.toActualText();
newDeclaration.baseExpression = expression;
}
@Override
public void visit(IdentificationVariableDeclaration expression) {
expression.getRangeVariableDeclaration().accept(this);
}
@Override
public void visit(RangeVariableDeclaration expression) {
newDeclaration = new DerivedDeclaration();
expression.setVirtualIdentificationVariable(outerVariableName, newDeclaration.rootPath);
expression.getRootObject().accept(this);
}
}
/**
* This visitor takes care to support a subquery defined as a "root" object.
*/
protected class RootObjectExpressionVisitor extends AnonymousExpressionVisitor {
/**
* The {@link Resolver} of the "root" object.
*/
protected Resolver resolver;
@Override
protected void visit(Expression expression) {
resolver = queryContext.getResolver(expression);
}
@Override
public void visit(SimpleSelectStatement expression) {
resolver = new FromSubqueryResolver(
DeclarationResolver.this,
DeclarationResolver.this.queryContext,
expression
);
}
@Override
public void visit(SubExpression expression) {
expression.getExpression().accept(this);
}
}
}