blob: 6a2528edbfcec0118f5e82ca3441684f28c85655 [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
//
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);
}
}
}
}