blob: b3e7440e419ef913a98164005d85ded376ecc716 [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.internal.jpa.jpql;
import java.text.MessageFormat;
import java.util.Collection;
import java.util.LinkedList;
import java.util.Map;
import java.util.ResourceBundle;
import org.eclipse.persistence.config.ParserValidationType;
import org.eclipse.persistence.exceptions.JPQLException;
import org.eclipse.persistence.expressions.Expression;
import org.eclipse.persistence.internal.expressions.ParameterExpression;
import org.eclipse.persistence.internal.queries.JPQLCallQueryMechanism;
import org.eclipse.persistence.internal.sessions.AbstractSession;
import org.eclipse.persistence.jpa.jpql.EclipseLinkGrammarValidator;
import org.eclipse.persistence.jpa.jpql.JPQLQueryProblem;
import org.eclipse.persistence.jpa.jpql.JPQLQueryProblemResourceBundle;
import org.eclipse.persistence.jpa.jpql.parser.AbstractExpressionVisitor;
import org.eclipse.persistence.jpa.jpql.parser.ConditionalExpressionBNF;
import org.eclipse.persistence.jpa.jpql.parser.DefaultEclipseLinkJPQLGrammar;
import org.eclipse.persistence.jpa.jpql.parser.DeleteStatement;
import org.eclipse.persistence.jpa.jpql.parser.InputParameter;
import org.eclipse.persistence.jpa.jpql.parser.JPQLExpression;
import org.eclipse.persistence.jpa.jpql.parser.JPQLGrammar;
import org.eclipse.persistence.jpa.jpql.parser.JPQLGrammar1_0;
import org.eclipse.persistence.jpa.jpql.parser.JPQLGrammar2_0;
import org.eclipse.persistence.jpa.jpql.parser.JPQLGrammar2_1;
import org.eclipse.persistence.jpa.jpql.parser.SelectStatement;
import org.eclipse.persistence.jpa.jpql.parser.UpdateStatement;
import org.eclipse.persistence.queries.DatabaseQuery;
import org.eclipse.persistence.queries.DatabaseQuery.ParameterType;
import org.eclipse.persistence.queries.DeleteAllQuery;
import org.eclipse.persistence.queries.JPAQueryBuilder;
import org.eclipse.persistence.queries.ObjectLevelReadQuery;
import org.eclipse.persistence.queries.ReadAllQuery;
import org.eclipse.persistence.queries.ReportQuery;
import org.eclipse.persistence.queries.UpdateAllQuery;
import static org.eclipse.persistence.jpa.jpql.JPQLQueryProblemMessages.*;
/**
* This class compiles a JPQL query into a {@link DatabaseQuery}. If validation is not turned off,
* then the JPQL query will be validated based on the grammar related to the validation level and
* will also be validated based on the semantic (context).
* <p>
* The validation level determines how to validate the JPQL query. It checks if any specific feature
* is allowed. For instance, if the JPQL query has functions defined for EclipseLink grammar but
* the validation level is set for generic JPA, then an exception will be thrown indicating the
* function cannot be used.
*
* @see JPQLExpression
*
* @version 2.5
* @since 2.3
* @author John Bracken
* @author Pascal Filion
*/
@SuppressWarnings("nls")
public final class HermesParser implements JPAQueryBuilder {
/**
* Determines how to validate the JPQL query grammatically.
*/
private String validationLevel;
/**
* Creates a new <code>HermesParser</code>.
*/
public HermesParser() {
super();
validationLevel = ParserValidationType.DEFAULT;
}
/**
* Registers the input parameters derived from the JPQL expression with the {@link DatabaseQuery}.
*
* @param queryContext The {@link JPQLQueryContext} containing the information about the JPQL query
* @param databaseQuery The EclipseLink {@link DatabaseQuery} where the input parameter types are added
*/
private void addArguments(JPQLQueryContext queryContext, DatabaseQuery databaseQuery) {
if (queryContext.inputParameters != null) {
for (Map.Entry<InputParameter, Expression> entry : queryContext.inputParameters.entrySet()) {
ParameterExpression parameter = (ParameterExpression) entry.getValue();
databaseQuery.addArgument(
parameter.getField().getName(),
(Class<?>) parameter.getType(),
entry.getKey().isPositional() ? ParameterType.POSITIONAL : ParameterType.NAMED
);
}
}
}
/**
* Creates a {@link JPQLException} indicating the problems with the JPQL query.
*
* @param queryContext The {@link JPQLQueryContext} containing the information about the JPQL query
* @param problems The {@link JPQLQueryProblem problems} found in the JPQL query that are
* translated into an exception
* @param messageKey The key used to retrieve the localized message
* @return The {@link JPQLException} indicating the problems with the JPQL query
*/
private JPQLException buildException(JPQLQueryContext queryContext,
Collection<JPQLQueryProblem> problems,
String messageKey) {
ResourceBundle bundle = resourceBundle();
StringBuilder sb = new StringBuilder();
for (JPQLQueryProblem problem : problems) {
// Retrieve the localized message
String message;
try {
message = bundle.getString(problem.getMessageKey());
}
catch (NullPointerException e) {
// In case the resource bundle was not updated
message = problem.getMessageKey();
}
// Now format the localized message
String[] arguments = problem.getMessageArguments();
if (arguments.length > 0) {
message = MessageFormat.format(message, (Object[]) arguments);
}
// Append the description
sb.append("\n");
sb.append("[");
sb.append(problem.getStartPosition());
sb.append(", ");
sb.append(problem.getEndPosition());
sb.append("] ");
sb.append(message);
}
String errorMessage = bundle.getString(messageKey);
errorMessage = MessageFormat.format(errorMessage, queryContext.getJPQLQuery(), sb);
return new JPQLException(errorMessage);
}
@Override
public DatabaseQuery buildQuery(CharSequence jpqlQuery, AbstractSession session) {
return populateQueryImp(jpqlQuery, null, session);
}
@Override
public Expression buildSelectionCriteria(String entityName,
String selectionCriteria,
AbstractSession session) {
try {
// Create the parsed tree representation of the selection criteria
JPQLExpression jpqlExpression = new JPQLExpression(
selectionCriteria,
DefaultEclipseLinkJPQLGrammar.instance(),
ConditionalExpressionBNF.ID,
isTolerant()
);
// Caches the info and add a virtual range variable declaration
JPQLQueryContext queryContext = new JPQLQueryContext(jpqlGrammar());
queryContext.cache(session, null, jpqlExpression, selectionCriteria);
queryContext.addRangeVariableDeclaration(entityName, "this");
// Validate the JPQL query, which will use the JPQL grammar matching the validation
// level, for now, only validate the query statement because there could be an unknown
// ending that is an order by clause
validate(queryContext, jpqlExpression.getQueryStatement());
// Create the Expression representing the selection criteria
return queryContext.buildExpression(jpqlExpression.getQueryStatement());
}
catch (JPQLException exception) {
throw exception;
}
catch (Exception exception) {
throw buildUnexpectedException(selectionCriteria, exception);
}
}
private JPQLException buildUnexpectedException(CharSequence jpqlQuery, Exception exception) {
String errorMessage = resourceBundle().getString(HermesParser_UnexpectedException_ErrorMessage);
errorMessage = MessageFormat.format(errorMessage, jpqlQuery);
return new JPQLException(errorMessage, exception);
}
/**
* Determines whether the JPQL query should be parsed with tolerance turned on or off, i.e. if
* validation is turned off, then it's assumed the JPQL query is grammatically valid and complete.
* In this case, it will be parsed with tolerance turned off resulting in better performance.
*
* @return <code>true</code> if the query might be incomplete or invalid; <code>false</code> if
* the query is complete and grammatically valid
*/
private boolean isTolerant() {
return validationLevel != ParserValidationType.None;
}
/**
* Returns the {@link JPQLGrammar} that will help to validate the JPQL query grammatically and
* semantically (contextually). It will also checks if any specific feature added to that grammar
* is allowed. For instance, if the JPQL query has functions defined for EclipseLink grammar but
* the validation level is set for generic JPA, then an exception will be thrown.
*
* @return The {@link JPQLGrammar} written for a specific JPA version or for the current version
* of EclipseLink
*/
private JPQLGrammar jpqlGrammar() {
if (validationLevel == ParserValidationType.EclipseLink) {
return DefaultEclipseLinkJPQLGrammar.instance();
}
if (validationLevel == ParserValidationType.JPA10) {
return JPQLGrammar1_0.instance();
}
if (validationLevel == ParserValidationType.JPA20) {
return JPQLGrammar2_0.instance();
}
if (validationLevel == ParserValidationType.JPA21) {
return JPQLGrammar2_1.instance();
}
return DefaultEclipseLinkJPQLGrammar.instance();
}
@Override
public void populateQuery(CharSequence jpqlQuery, DatabaseQuery query, AbstractSession session) {
populateQueryImp(jpqlQuery, query, session);
}
private DatabaseQuery populateQueryImp(CharSequence jpqlQuery,
DatabaseQuery query,
AbstractSession session) {
try {
// Parse the JPQL query with the most recent JPQL grammar
JPQLExpression jpqlExpression = new JPQLExpression(
jpqlQuery,
DefaultEclipseLinkJPQLGrammar.instance(),
isTolerant()
);
// Create a context that caches the information contained in the JPQL query
// (especially from the FROM clause)
JPQLQueryContext queryContext = new JPQLQueryContext(jpqlGrammar());
queryContext.cache(session, query, jpqlExpression, jpqlQuery);
// Validate the JPQL query, which will use the JPQL grammar matching the validation level
validate(queryContext, jpqlExpression);
// Create the DatabaseQuery by visiting the parsed tree
DatabaseQueryVisitor visitor = new DatabaseQueryVisitor(queryContext, jpqlQuery);
jpqlExpression.accept(visitor);
// Add the input parameter types to the DatabaseQuery
if (query == null) {
query = queryContext.getDatabaseQuery();
addArguments(queryContext, query);
}
return query;
}
catch (JPQLException exception) {
throw exception;
}
catch (Exception exception) {
throw buildUnexpectedException(jpqlQuery, exception);
}
}
private ResourceBundle resourceBundle() {
return ResourceBundle.getBundle(JPQLQueryProblemResourceBundle.class.getName());
}
@Override
public void setValidationLevel(String validationLevel) {
this.validationLevel = validationLevel;
}
/**
* Grammatically and semantically validates the JPQL query. If the query is not valid, then an
* exception will be thrown.
*
* @param queryContext The context used to query information about the application metadata and
* cached information
* @param expression The {@link org.eclipse.persistence.jpa.jpql.parser.Expression Expression} to
* validate grammatically and semantically
*/
private void validate(JPQLQueryContext queryContext,
org.eclipse.persistence.jpa.jpql.parser.Expression expression) {
if (validationLevel != ParserValidationType.None) {
Collection<JPQLQueryProblem> problems = new LinkedList<>();
// Validate the JPQL query grammatically (based on the JPQL grammar)
EclipseLinkGrammarValidator grammar = new EclipseLinkGrammarValidator(jpqlGrammar());
grammar.setProblems(problems);
expression.accept(grammar);
if (!problems.isEmpty()) {
throw buildException(
queryContext,
problems,
HermesParser_GrammarValidator_ErrorMessage
);
}
// Validate the JPQL query semantically (contextually)
EclipseLinkSemanticValidator semantic = new EclipseLinkSemanticValidator(queryContext);
semantic.setProblems(problems);
expression.accept(semantic);
if (!problems.isEmpty()) {
throw buildException(
queryContext,
problems,
HermesParser_SemanticValidator_ErrorMessage
);
}
}
}
/**
* This visitor traverses the parsed tree and create the right EclipseLink query and populates it.
*/
private static class DatabaseQueryVisitor extends AbstractExpressionVisitor {
private final String jpqlQuery;
private final JPQLQueryContext queryContext;
DatabaseQueryVisitor(JPQLQueryContext queryContext, CharSequence jpqlQuery) {
super();
this.jpqlQuery = jpqlQuery.toString();
this.queryContext = queryContext;
}
private ReadAllQuery buildReadAllQuery(SelectStatement expression) {
ReadAllQueryBuilder visitor = new ReadAllQueryBuilder(queryContext);
expression.accept(visitor);
return visitor.query;
}
private AbstractObjectLevelReadQueryVisitor buildVisitor(ObjectLevelReadQuery query) {
if (query.isReportQuery()) {
return new ReportQueryVisitor(queryContext, (ReportQuery) query);
}
if (query.isReadAllQuery()) {
return new ReadAllQueryVisitor(queryContext, (ReadAllQuery) query);
}
return new ObjectLevelReadQueryVisitor(queryContext, query);
}
@Override
public void visit(DeleteStatement expression) {
DeleteAllQuery query = queryContext.getDatabaseQuery();
// Create and prepare the query
if (query == null) {
query = new DeleteAllQuery();
queryContext.setDatabasQuery(query);
query.setJPQLString(jpqlQuery);
((JPQLCallQueryMechanism) query.getQueryMechanism()).getJPQLCall().setIsParsed(true);
}
query.setSession(queryContext.getSession());
query.setShouldDeferExecutionInUOW(false);
// Now populate it
DeleteQueryVisitor visitor = new DeleteQueryVisitor(queryContext, query);
expression.accept(visitor);
}
@Override
public void visit(JPQLExpression expression) {
expression.getQueryStatement().accept(this);
}
@Override
public void visit(SelectStatement expression) {
ObjectLevelReadQuery query = queryContext.getDatabaseQuery();
// Create and prepare the query
if (query == null) {
query = buildReadAllQuery(expression);
queryContext.setDatabasQuery(query);
query.setJPQLString(jpqlQuery);
((JPQLCallQueryMechanism) query.getQueryMechanism()).getJPQLCall().setIsParsed(true);
}
// Now populate it
expression.accept(buildVisitor(query));
}
@Override
public void visit(UpdateStatement expression) {
UpdateAllQuery query = queryContext.getDatabaseQuery();
// Create and prepare the query
if (query == null) {
query = new UpdateAllQuery();
queryContext.setDatabasQuery(query);
query.setJPQLString(jpqlQuery);
((JPQLCallQueryMechanism) query.getQueryMechanism()).getJPQLCall().setIsParsed(true);
}
query.setSession(queryContext.getSession());
query.setShouldDeferExecutionInUOW(false);
// Now populate it
UpdateQueryVisitor visitor = new UpdateQueryVisitor(queryContext, query);
expression.accept(visitor);
}
}
}