| /* |
| * 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 |
| // |
| package org.eclipse.persistence.jpa.jpql; |
| |
| import org.eclipse.persistence.jpa.jpql.parser.AbstractEclipseLinkExpressionVisitor; |
| import org.eclipse.persistence.jpa.jpql.parser.AnonymousExpressionVisitor; |
| import org.eclipse.persistence.jpa.jpql.parser.AsOfClause; |
| import org.eclipse.persistence.jpa.jpql.parser.BadExpression; |
| import org.eclipse.persistence.jpa.jpql.parser.CastExpression; |
| import org.eclipse.persistence.jpa.jpql.parser.CollectionExpression; |
| import org.eclipse.persistence.jpa.jpql.parser.CollectionValuedPathExpression; |
| import org.eclipse.persistence.jpa.jpql.parser.ConnectByClause; |
| import org.eclipse.persistence.jpa.jpql.parser.DatabaseType; |
| import org.eclipse.persistence.jpa.jpql.parser.EclipseLinkExpressionVisitor; |
| import org.eclipse.persistence.jpa.jpql.parser.Expression; |
| import org.eclipse.persistence.jpa.jpql.parser.ExtractExpression; |
| import org.eclipse.persistence.jpa.jpql.parser.FunctionExpression; |
| import org.eclipse.persistence.jpa.jpql.parser.HierarchicalQueryClause; |
| import org.eclipse.persistence.jpa.jpql.parser.InExpression; |
| import org.eclipse.persistence.jpa.jpql.parser.NullExpression; |
| import org.eclipse.persistence.jpa.jpql.parser.OrderSiblingsByClause; |
| import org.eclipse.persistence.jpa.jpql.parser.RangeVariableDeclaration; |
| import org.eclipse.persistence.jpa.jpql.parser.RegexpExpression; |
| import org.eclipse.persistence.jpa.jpql.parser.SimpleSelectClause; |
| import org.eclipse.persistence.jpa.jpql.parser.SimpleSelectStatement; |
| import org.eclipse.persistence.jpa.jpql.parser.StartWithClause; |
| import org.eclipse.persistence.jpa.jpql.parser.StateFieldPathExpression; |
| import org.eclipse.persistence.jpa.jpql.parser.TableExpression; |
| import org.eclipse.persistence.jpa.jpql.parser.TableVariableDeclaration; |
| import org.eclipse.persistence.jpa.jpql.parser.UnionClause; |
| |
| import static org.eclipse.persistence.jpa.jpql.JPQLQueryProblemMessages.*; |
| |
| /** |
| * This validator is responsible to gather the problems found in a JPQL query by validating the |
| * content to make sure it is semantically valid for EclipseLink. The grammar is not validated by |
| * this visitor. |
| * <p> |
| * For instance, the function <b>AVG</b> accepts a state field path. The property it represents has |
| * to be of numeric type. <b>AVG(e.name)</b> is parsable but is not semantically valid because the |
| * type of name is a string (the property signature is: "<code>private String name</code>"). |
| * <p> |
| * <b>Note:</b> EclipseLink does not validate types, but leaves it to the database. This is because |
| * some databases such as Oracle allow different types to different functions and perform implicit |
| * type conversion. i.e. <code>CONCAT('test', 2)</code> returns <code>'test2'</code>. Also the |
| * <b>FUNC</b> function has an unknown type, so should be allowed with any function. |
| * <p> |
| * Provisional API: This interface is part of an interim API that is still under development and |
| * expected to change significantly before reaching stability. It is available at this early stage |
| * to solicit feedback from pioneering adopters on the understanding that any code that uses this |
| * API will almost certainly be broken (repeatedly) as the API evolves. |
| * |
| * @see EclipseLinkGrammarValidator |
| * |
| * @version 2.5.1 |
| * @since 2.4 |
| * @author Pascal Filion |
| */ |
| public class AbstractEclipseLinkSemanticValidator extends AbstractSemanticValidator |
| implements EclipseLinkExpressionVisitor { |
| |
| /** |
| * The following extension can be used to give access to non-JPA metadata artifacts, such as |
| * database tables and columns. |
| */ |
| private EclipseLinkSemanticValidatorExtension extension; |
| |
| /** |
| * This visitor calculates the number of items the subquery <code><b>SELECT</b></code> clause has. |
| */ |
| private SubquerySelectItemCalculator subquerySelectItemCalculator; |
| |
| /** |
| * This visitor determines if the {@link Expression} being visited is a {@link TableExpression}. |
| */ |
| private TableExpressionVisitor tableExpressionVisitor; |
| |
| /** |
| * Creates a new <code>AbstractEclipseLinkSemanticValidator</code>. |
| * |
| * @param helper The given helper allows this validator to access the JPA artifacts without using |
| * Hermes SPI |
| * @param extension The following extension can be used to give access to non-JPA metadata |
| * artifacts, such as database tables and columns |
| * @exception NullPointerException The given {@link SemanticValidatorHelper} cannot be <code>null</code> |
| */ |
| protected AbstractEclipseLinkSemanticValidator(SemanticValidatorHelper helper, |
| EclipseLinkSemanticValidatorExtension extension) { |
| |
| super(helper); |
| this.extension = extension; |
| } |
| |
| @Override |
| protected LiteralVisitor buildLiteralVisitor() { |
| return new EclipseLinkLiteralVisitor(); |
| } |
| |
| @Override |
| protected OwningClauseVisitor buildOwningClauseVisitor() { |
| return new EclipseLinkOwningClauseVisitor(); |
| } |
| |
| protected SubquerySelectItemCalculator buildSubquerySelectItemCalculator() { |
| return new SubquerySelectItemCalculator(); |
| } |
| |
| protected TableExpressionVisitor buildTableExpressionVisitor() { |
| return new TableExpressionVisitor(); |
| } |
| |
| @Override |
| protected TopLevelFirstDeclarationVisitor buildTopLevelFirstDeclarationVisitor() { |
| return new TopLevelFirstDeclarationVisitor(this); |
| } |
| |
| protected JPQLQueryDeclaration getDeclaration(String variableName) { |
| |
| for (JPQLQueryDeclaration declaration : helper.getAllDeclarations()) { |
| |
| if (declaration.getVariableName().equalsIgnoreCase(variableName)) { |
| return declaration; |
| } |
| } |
| |
| return null; |
| } |
| |
| /** |
| * Returns the extension that gives access to non-JPA metadata artifacts, such as database tables |
| * and columns. |
| * |
| * @return An extension giving access to non-JPA metadata artifacts or {@link |
| * EclipseLinkSemanticValidatorExtension#NULL_EXTENSION} if no extension was provided |
| */ |
| protected EclipseLinkSemanticValidatorExtension getExtension() { |
| return extension; |
| } |
| |
| protected SubquerySelectItemCalculator getSubquerySelectItemCalculator() { |
| if (subquerySelectItemCalculator == null) { |
| subquerySelectItemCalculator = buildSubquerySelectItemCalculator(); |
| } |
| return subquerySelectItemCalculator; |
| } |
| |
| protected TableExpressionVisitor getTableExpressionVisitor() { |
| if (tableExpressionVisitor == null) { |
| tableExpressionVisitor = buildTableExpressionVisitor(); |
| } |
| return tableExpressionVisitor; |
| } |
| |
| protected boolean isTableExpression(Expression expression) { |
| TableExpressionVisitor visitor = getTableExpressionVisitor(); |
| try { |
| visitor.expression = expression; |
| expression.accept(visitor); |
| return visitor.valid; |
| } |
| finally { |
| visitor.valid = false; |
| visitor.expression = null; |
| } |
| } |
| |
| @Override |
| protected PathType selectClausePathExpressionPathType() { |
| return PathType.ANY_FIELD_INCLUDING_COLLECTION; |
| } |
| |
| /** |
| * Retrieves the number of select items the given subquery has. |
| * |
| * @param subquery The {@link Expression} to visit, which should represents a subquery |
| * @return The number of select items or 0 if the subquery is malformed or incomplete |
| */ |
| protected int subquerySelectItemCount(Expression subquery) { |
| SubquerySelectItemCalculator visitor = getSubquerySelectItemCalculator(); |
| try { |
| visitor.count = 0; |
| subquery.accept(visitor); |
| return visitor.count; |
| } |
| finally { |
| visitor.count = 0; |
| } |
| } |
| |
| @Override |
| protected void validateFunctionExpression(FunctionExpression expression) { |
| super.validateFunctionExpression(expression); |
| |
| // No need to validate if no extension was provided |
| // Validate the column name |
| if (extension != EclipseLinkSemanticValidatorExtension.NULL_EXTENSION && |
| (expression.getIdentifier() == Expression.COLUMN) && |
| expression.hasExpression()) { |
| |
| String columnName = expression.getUnquotedFunctionName(); |
| String variableName = literal(expression.getExpression(), LiteralType.IDENTIFICATION_VARIABLE); |
| |
| if (ExpressionTools.stringIsNotEmpty(variableName) && |
| ExpressionTools.stringIsNotEmpty(columnName)) { |
| |
| // Retrieve the declaration associated with the identification variable |
| JPQLQueryDeclaration declaration = getDeclaration(variableName); |
| |
| // Only a range variable declaration can be used |
| if (declaration.getType().isRange()) { |
| String entityName = literal(declaration.getBaseExpression(), LiteralType.ABSTRACT_SCHEMA_NAME); |
| |
| if (entityName != ExpressionTools.EMPTY_STRING) { |
| |
| // Retrieve the primary table of the entity |
| String tableName = extension.getEntityTable(entityName); |
| |
| // The column is not on the primary table |
| if (ExpressionTools.stringIsNotEmpty(tableName) && |
| !extension.columnExists(tableName, expression.getUnquotedFunctionName())) { |
| |
| int startPosition = position(expression) + |
| Expression.COLUMN.length() + |
| (expression.hasLeftParenthesis() ? 1 : 0); |
| |
| int endPosition = startPosition + |
| expression.getFunctionName().length(); |
| |
| addProblem( |
| expression, |
| startPosition, |
| endPosition, |
| FunctionExpression_UnknownColumn, |
| columnName, |
| tableName |
| ); |
| } |
| } |
| } |
| } |
| } |
| } |
| |
| @Override |
| protected void validateInExpression(InExpression expression) { |
| super.validateInExpression(expression); |
| |
| // Make sure the number of items matches if the left expression |
| // is a nested array and the IN items expression is a subquery |
| Expression inItems = expression.getInItems(); |
| |
| if (isSubquery(inItems)) { |
| int nestedArraySize = nestedArraySize(expression.getExpression()); |
| int subquerySelectItemsSize = subquerySelectItemCount(inItems); |
| |
| if ((nestedArraySize > -1) && (subquerySelectItemsSize != nestedArraySize) || |
| (nestedArraySize == -1) && (subquerySelectItemsSize > 1)) { |
| |
| addProblem(expression, InExpression_InvalidItemCount); |
| } |
| } |
| } |
| |
| @Override |
| protected void validateRangeVariableDeclarationRootObject(RangeVariableDeclaration expression) { |
| |
| Expression rootObject = expression.getRootObject(); |
| |
| // Special case, the path expression could be a fully qualified class name, |
| // make sure to not validate it as collection-valued path expression |
| CollectionValuedPathExpression pathExpression = getCollectionValuedPathExpression(rootObject); |
| |
| if (pathExpression != null) { |
| String path = pathExpression.toActualText(); |
| |
| // The path expression is not a fully qualified class name |
| if (helper.getType(path) == null) { |
| pathExpression.accept(this); |
| } |
| } |
| else { |
| rootObject.accept(this); |
| } |
| } |
| |
| @Override |
| protected PathType validPathExpressionTypeForCountFunction() { |
| return PathType.ANY_FIELD_INCLUDING_COLLECTION; |
| } |
| |
| @Override |
| protected PathType validPathExpressionTypeForInExpression() { |
| // Loosen up the JPA spec restriction because ANTLR parser used to allow it |
| return PathType.ANY_FIELD_INCLUDING_COLLECTION; |
| } |
| |
| @Override |
| protected PathType validPathExpressionTypeForInItem() { |
| return PathType.ANY_FIELD_INCLUDING_COLLECTION; |
| } |
| |
| @Override |
| protected Boolean validateThirdPartyStateFieldPathExpression(StateFieldPathExpression expression) { |
| |
| Boolean valid = null; |
| |
| // Retrieve the identification variable |
| String variableName = literal(expression.getIdentificationVariable(), LiteralType.IDENTIFICATION_VARIABLE); |
| |
| if (variableName != ExpressionTools.EMPTY_STRING) { |
| |
| // Now find the associated declaration |
| JPQLQueryDeclaration declaration = getDeclaration(variableName); |
| |
| if (declaration != null) { |
| |
| // The path expression is composed with an identification variable mapping a subquery |
| if (declaration.getType() == JPQLQueryDeclaration.Type.SUBQUERY) { |
| valid = Boolean.TRUE; |
| } |
| else { |
| Expression baseExpression = declaration.getBaseExpression(); |
| |
| // If the base expression is a TableExpression, then we can |
| // continue to resolve the table and column names |
| if ((baseExpression != null) && isTableExpression(baseExpression)) { |
| |
| // No need to validate if no extension was provided, |
| // but mark valid to true so it's not validated otherwise |
| if (extension == EclipseLinkSemanticValidatorExtension.NULL_EXTENSION) { |
| valid = Boolean.TRUE; |
| } |
| else { |
| valid = Boolean.FALSE; |
| |
| // Retrieve the table name |
| String tableName = literal(baseExpression, LiteralType.STRING_LITERAL); |
| |
| if (tableName != ExpressionTools.EMPTY_STRING) { |
| tableName = ExpressionTools.unquote(tableName); |
| |
| // If the table name can be resolved, then validate the column name |
| if ((tableName != null) && extension.tableExists(tableName)) { |
| String columnName = expression.getPath(1); |
| |
| // The column cannot be found on the table |
| if (!extension.columnExists(tableName, columnName)) { |
| addProblem( |
| expression, |
| StateFieldPathExpression_UnknownColumn, |
| columnName, |
| tableName |
| ); |
| } |
| else { |
| valid = Boolean.TRUE; |
| } |
| } |
| } |
| } |
| } |
| } |
| } |
| } |
| |
| return valid; |
| } |
| |
| @Override |
| protected PathType validPathExpressionTypeForStringExpression() { |
| // Loosen up the JPA spec restriction because ANTLR parser used to allow it |
| return PathType.ANY_FIELD_INCLUDING_COLLECTION; |
| } |
| |
| @Override |
| public void visit(AsOfClause expression) { |
| super.visit(expression); |
| // Nothing to validate semantically |
| } |
| |
| @Override |
| public void visit(CastExpression expression) { |
| super.visit(expression); |
| // Nothing to validate semantically |
| } |
| |
| @Override |
| public void visit(ConnectByClause expression) { |
| super.visit(expression); |
| // Nothing to validate semantically |
| } |
| |
| @Override |
| public void visit(DatabaseType expression) { |
| super.visit(expression); |
| // Nothing to validate semantically |
| } |
| |
| @Override |
| public void visit(ExtractExpression expression) { |
| super.visit(expression); |
| // Nothing to validate semantically |
| } |
| |
| @Override |
| public void visit(HierarchicalQueryClause expression) { |
| super.visit(expression); |
| // Nothing to validate semantically |
| } |
| |
| @Override |
| public void visit(OrderSiblingsByClause expression) { |
| super.visit(expression); |
| // TODO |
| } |
| |
| @Override |
| public void visit(RegexpExpression expression) { |
| super.visit(expression); |
| // Nothing to validate semantically |
| } |
| |
| @Override |
| public void visit(StartWithClause expression) { |
| super.visit(expression); |
| // Nothing to validate semantically |
| } |
| |
| @Override |
| public void visit(TableExpression expression) { |
| super.visit(expression); |
| |
| // No need to validate if no extension was defined |
| if (extension != EclipseLinkSemanticValidatorExtension.NULL_EXTENSION) { |
| Expression tableNameExpression = expression.getExpression(); |
| String tableName = literal(tableNameExpression, LiteralType.STRING_LITERAL); |
| |
| if (tableName != ExpressionTools.EMPTY_STRING) { |
| tableName = ExpressionTools.unquote(tableName); |
| |
| if ((tableName.length() > 0) && !extension.tableExists(tableName)) { |
| addProblem(tableNameExpression, JPQLQueryProblemMessages.TableExpression_InvalidTableName); |
| } |
| } |
| } |
| } |
| |
| @Override |
| public void visit(TableVariableDeclaration expression) { |
| super.visit(expression); |
| // Nothing to validate semantically |
| } |
| |
| @Override |
| public void visit(UnionClause expression) { |
| super.visit(expression); |
| // Nothing to validate semantically |
| } |
| |
| // Made static final for performance reasons. |
| /** |
| * This visitor retrieves the clause owning the visited {@link Expression}. |
| */ |
| public static final class EclipseLinkOwningClauseVisitor extends OwningClauseVisitor { |
| |
| public UnionClause unionClause; |
| |
| /** |
| * Creates a new <code>EclipseLinkOwningClauseVisitor</code>. |
| */ |
| public EclipseLinkOwningClauseVisitor() { |
| super(); |
| } |
| |
| @Override |
| public void dispose() { |
| super.dispose(); |
| unionClause = null; |
| } |
| |
| public void visit(UnionClause expression) { |
| this.unionClause = expression; |
| } |
| } |
| |
| // Made static final for performance reasons. |
| protected static final class SubquerySelectItemCalculator extends AnonymousExpressionVisitor { |
| |
| public int count; |
| |
| @Override |
| public void visit(BadExpression expression) { |
| count = 0; |
| } |
| |
| @Override |
| public void visit(CollectionExpression expression) { |
| count = expression.childrenSize(); |
| } |
| |
| @Override |
| protected void visit(Expression expression) { |
| count = 1; |
| } |
| |
| @Override |
| public void visit(NullExpression expression) { |
| count = 0; |
| } |
| |
| @Override |
| public void visit(SimpleSelectClause expression) { |
| expression.getSelectExpression().accept(this); |
| } |
| |
| @Override |
| public void visit(SimpleSelectStatement expression) { |
| expression.getSelectClause().accept(this); |
| } |
| } |
| |
| // Made static final for performance reasons. |
| protected static final class TableExpressionVisitor extends AbstractEclipseLinkExpressionVisitor { |
| |
| /** |
| * The {@link Expression} being visited. |
| */ |
| protected Expression expression; |
| |
| /** |
| * <code>true</code> if the {@link Expression} being visited is a {@link TableExpression}. |
| */ |
| protected boolean valid; |
| |
| @Override |
| public void visit(TableExpression expression) { |
| valid = (this.expression == expression); |
| } |
| } |
| |
| // Made static final for performance reasons. |
| protected static final class TopLevelFirstDeclarationVisitor extends AbstractSemanticValidator.TopLevelFirstDeclarationVisitor { |
| |
| private final AbstractEclipseLinkSemanticValidator validator; |
| |
| private TopLevelFirstDeclarationVisitor(AbstractEclipseLinkSemanticValidator validator) { |
| this.validator = validator; |
| } |
| |
| @Override |
| public void visit(CollectionValuedPathExpression expression) { |
| |
| // Derived path is not allowed, this could although be a fully |
| // qualified class name, which was added to EclipseLink 2.4 |
| EclipseLinkVersion version = EclipseLinkVersion.value(validator.getProviderVersion()); |
| valid = version.isNewerThanOrEqual(EclipseLinkVersion.VERSION_2_4); |
| |
| if (valid) { |
| Object type = validator.helper.getType(expression.toActualText()); |
| valid = validator.helper.isTypeResolvable(type); |
| } |
| } |
| } |
| } |