| /* |
| * 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.tools; |
| |
| import java.util.Collection; |
| import java.util.Comparator; |
| import java.util.LinkedList; |
| import java.util.List; |
| import java.util.TreeSet; |
| import org.eclipse.persistence.jpa.jpql.AbstractGrammarValidator; |
| import org.eclipse.persistence.jpa.jpql.AbstractSemanticValidator; |
| import org.eclipse.persistence.jpa.jpql.Assert; |
| import org.eclipse.persistence.jpa.jpql.JPQLQueryProblem; |
| import org.eclipse.persistence.jpa.jpql.parser.Expression; |
| 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.tools.spi.IManagedTypeProvider; |
| 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.ITypeRepository; |
| |
| /** |
| * This helper can perform the following operations over a JPQL query: |
| * <ul> |
| * <li>Calculates the result type of a query: {@link #getResultType()};</li> |
| * <li>Calculates the type of an input parameter: {@link #getParameterType(String)}.</li> |
| * <li>Calculates the possible choices to complete the query from a given |
| * position (used for content assist): {@link #buildContentAssistProposals(int)}.</li> |
| * <li>Validates the query by introspecting it grammatically and semantically: |
| * <ul> |
| * <li>{@link #validate()},</li> |
| * <li>{@link #validateGrammar()},</li> |
| * <li>{@link #validateSemantic()}.</li> |
| * </ul> |
| * </li> |
| * <li>Refactoring support: |
| * <ul> |
| * <li>{@link #buildBasicRefactoringTool()} provides support for generating the delta of the |
| * refactoring operation through a collection of {@link TextEdit} objects.</li> |
| * <li>{@link #buildRefactoringTool()} provides support for refactoring the JPQL query through |
| * the editable {@link org.eclipse.persistence.jpa.jpql.tools.model.query.StateObject StateObject} |
| * and once all refactoring operations have been executed, the {@link |
| * org.eclipse.persistence.jpa.jpql.tools.model.IJPQLQueryFormatter IJPQLQueryFormatter} will |
| * generate a new string representation of the JPQL query.</li> |
| * </ul> |
| * </li> |
| * </ul> |
| * <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. |
| * |
| * @version 2.5 |
| * @since 2.3 |
| * @author Pascal Filion |
| */ |
| @SuppressWarnings("nls") |
| public abstract class AbstractJPQLQueryHelper { |
| |
| /** |
| * This visitor is responsible to gather the possible proposals based on the position of the |
| * caret within the JPQL query. |
| */ |
| private AbstractContentAssistVisitor contentAssistVisitor; |
| |
| /** |
| * This visitor is responsible to visit the entire parsed tree representation of the JPQL query |
| * and to validate its content based on the JPQL grammar. |
| */ |
| private AbstractGrammarValidator grammarValidator; |
| |
| /** |
| * The {@link JPQLGrammar} that will determine how to parse JPQL queries. |
| */ |
| private JPQLGrammar jpqlGrammar; |
| |
| /** |
| * The context used to query information about the JPQL query. |
| */ |
| private JPQLQueryContext queryContext; |
| |
| /** |
| * This visitor is responsible to visit the entire parsed tree representation of the JPQL query |
| * and to validate the semantic of the information. |
| */ |
| private AbstractSemanticValidator semanticValidator; |
| |
| /** |
| * Creates a new <code>AbstractJPQLQueryHelper</code>. |
| * |
| * @param jpqlGrammar The {@link JPQLGrammar} that will determine how to parse JPQL queries |
| */ |
| public AbstractJPQLQueryHelper(JPQLGrammar jpqlGrammar) { |
| super(); |
| Assert.isNotNull(jpqlGrammar, "The JPQLGrammar cannot be null"); |
| this.jpqlGrammar = jpqlGrammar; |
| } |
| |
| /** |
| * Creates a new <code>AbstractJPQLQueryHelper</code>. |
| * |
| * @param queryContext The context used to query information about the JPQL query |
| * @exception NullPointerException The JPQLQueryContext cannot be <code>null</code> |
| */ |
| protected AbstractJPQLQueryHelper(JPQLQueryContext queryContext) { |
| super(); |
| |
| Assert.isNotNull(queryContext, "The JPQLQueryContext cannot be null"); |
| |
| this.queryContext = queryContext; |
| this.jpqlGrammar = queryContext.getGrammar(); |
| } |
| |
| /** |
| * Creates the concrete instance of the tool that can refactor the content of a JPQL query. This |
| * version simply provides the delta of the refactoring operations. |
| * |
| * @return The concrete instance of {@link RefactoringTool} |
| * @see #buildRefactoringTool |
| * @since 2.5 |
| */ |
| public abstract BasicRefactoringTool buildBasicRefactoringTool(); |
| |
| /** |
| * Retrieves the possibles choices that can complete the query from the given position within |
| * the query. |
| * <p> |
| * <b>Note:</b> Disposing of the internal data is not done automatically. |
| * |
| * @param position The position within the query for which a list of possible choices are created |
| * for completing the query |
| * @return The list of valid proposals regrouped by categories |
| */ |
| public ContentAssistProposals buildContentAssistProposals(int position) { |
| return buildContentAssistProposals(position, ContentAssistExtension.NULL_HELPER); |
| } |
| |
| /** |
| * Retrieves the possibles choices that can complete the query from the given position within |
| * the query. |
| * <p> |
| * <b>Note:</b> Disposing of the internal data is not done automatically. |
| * |
| * @param position The position within the query for which a list of possible choices are created |
| * for completing the query |
| * @param extension This extension can be used to provide additional information that is outside |
| * the scope of simply providing JPA metadata information, such as table names, column names, |
| * class names |
| * @return The list of valid proposals regrouped by categories |
| * @since 2.5 |
| */ |
| public ContentAssistProposals buildContentAssistProposals(int position, ContentAssistExtension extension) { |
| return getContentAssistVisitor().buildProposals(position, extension); |
| } |
| |
| /** |
| * Creates the concrete instance of the content assist visitor that will give the possible |
| * choices based on the position of the cursor within the JPQL query. |
| * |
| * @param queryContext The context used to query information about the JPQL query |
| * @return A new concrete instance of {@link AbstractContentAssistVisitor} |
| */ |
| protected abstract AbstractContentAssistVisitor buildContentAssistVisitor(JPQLQueryContext queryContext); |
| |
| /** |
| * Creates the concrete instance of the validator that will grammatically validate the JPQL query. |
| * |
| * @param jpqlGrammar The context used to query information about the JPQL query |
| * @return A new concrete instance of {@link AbstractGrammarValidator} |
| */ |
| protected abstract AbstractGrammarValidator buildGrammarValidator(JPQLGrammar jpqlGrammar); |
| |
| /** |
| * Creates a context that will be used to store and retrieve information about the JPQL query. |
| * |
| * @param jpqlGrammar The JPQL grammar that is required for dictating how the JPQL query will be |
| * parsed. It is also used by validation and by the content assist |
| * @return A new {@link JPQLQueryContext} |
| */ |
| protected abstract JPQLQueryContext buildJPQLQueryContext(JPQLGrammar jpqlGrammar); |
| |
| /** |
| * Creates the {@link Comparator} that can sort {@link IType ITypes} based on the numerical |
| * priority. |
| * |
| * @return {@link NumericTypeComparator} |
| */ |
| protected Comparator<IType> buildNumericTypeComparator() { |
| return new NumericTypeComparator(getTypeHelper()); |
| } |
| |
| /** |
| * Creates the concrete instance of the tool that can refactor the content of a JPQL query. This |
| * version provides a way to manipulate the editable version of the JPQL query ({@link |
| * org.eclipse.persistence.jpa.jpql.tools.model.query.StateObject StateObject} and simply |
| * outputs the result of the refactoring operations, i.e. the updated JPQL query). |
| * |
| * @return The concrete instance of {@link RefactoringTool} |
| * @see #buildBasicRefactoringTool() |
| * @since 2.4 |
| */ |
| public abstract RefactoringTool buildRefactoringTool(); |
| |
| /** |
| * Creates the concrete instance of the validator that will semantically validate the JPQL query. |
| * |
| * @param queryContext The context used to query information about the JPQL query |
| * @return A new concrete instance of {@link AbstractSemanticValidator} |
| */ |
| protected abstract AbstractSemanticValidator buildSemanticValidator(JPQLQueryContext queryContext); |
| |
| /** |
| * Disposes of the internal data. |
| */ |
| public void dispose() { |
| |
| if (queryContext != null) { |
| queryContext.dispose(); |
| } |
| |
| // Not required but during debugging, this is important to be reset |
| if (contentAssistVisitor != null) { |
| contentAssistVisitor.dispose(); |
| } |
| } |
| |
| /** |
| * Returns the visitor that can visit a JPQL query and based on the position of the cursor within |
| * the JPQL query and determines the valid proposals. |
| * |
| * @return A concrete instance of {@link AbstractContentAssistVisitor} |
| * @see #buildContentAssistVisitor(JPQLQueryContext) |
| */ |
| protected AbstractContentAssistVisitor getContentAssistVisitor() { |
| if (contentAssistVisitor == null) { |
| contentAssistVisitor = buildContentAssistVisitor(getQueryContext()); |
| } |
| return contentAssistVisitor; |
| } |
| |
| /** |
| * Returns the JPQL grammar that will be used to define how to parse a JPQL query. |
| * |
| * @return The grammar that was used to parse this {@link Expression} |
| * @since 2.4 |
| */ |
| public JPQLGrammar getGrammar() { |
| return jpqlGrammar; |
| } |
| |
| protected AbstractGrammarValidator getGrammarValidator() { |
| if (grammarValidator == null) { |
| grammarValidator = buildGrammarValidator(jpqlGrammar); |
| } |
| return grammarValidator; |
| } |
| |
| /** |
| * Returns the root of the parsed tree representation of the JPQL query. |
| * |
| * @return The parsed JPQL query |
| */ |
| public JPQLExpression getJPQLExpression() { |
| return getQueryContext().getJPQLExpression(); |
| } |
| |
| /** |
| * Retrieves, if it can be determined, the type of the given input parameter with the given name. |
| * The type will be guessed based on its location within expression. |
| * <p> |
| * Note: Both named and positional input parameter can be used. |
| * |
| * @param parameterName The name of the input parameter to retrieve its type, which needs to be |
| * prepended by ':' or '?' |
| * @return Either the closest type of the input parameter or <code>null</code> if the type |
| * couldn't be determined |
| */ |
| public IType getParameterType(String parameterName) { |
| |
| // Retrieve the input parameter's qualifier (':' or '?') |
| char character = parameterName.length() > 0 ? parameterName.charAt(0) : '\0'; |
| |
| // Does not begin with either ':' or '?' |
| if ((character != ':') && (character != '?')) { |
| return getTypeHelper().objectType(); |
| } |
| |
| // Find the InputParameters with the given parameter name |
| Collection<InputParameter> inputParameters = getQueryContext().findInputParameters(parameterName); |
| |
| // No InputParameter was found |
| if (inputParameters.isEmpty()) { |
| return getTypeHelper().objectType(); |
| } |
| |
| // Now find the closest type for each location |
| TreeSet<IType> types = new TreeSet<>(buildNumericTypeComparator()); |
| |
| for (InputParameter inputParameter : inputParameters) { |
| IType type = queryContext.getParameterType(inputParameter); |
| |
| // A type is ignored if it cannot be determined and it can't affect the calculation |
| // if the same input parameter is used elsewhere. Example: |
| // SELECT e FROM Employee e WHERE :name IS NOT NULL AND e.name = 'JPQL' |
| // The first :name cannot be used to calculate the type |
| if (type.isResolvable()) { |
| types.add(type); |
| } |
| } |
| |
| return types.isEmpty() ? getTypeHelper().objectType() : types.first(); |
| } |
| |
| /** |
| * Returns the string representation of the parsed tree. |
| * |
| * @return The string created from the parsed tree representation of the original JPQL query |
| */ |
| public String getParsedJPQLQuery() { |
| return getJPQLExpression().toParsedText(); |
| } |
| |
| /** |
| * Returns the provider for managed types (entities, embeddables, mapped superclasses). |
| * |
| * @return The container of managed types |
| */ |
| public IManagedTypeProvider getProvider() { |
| return getQuery().getProvider(); |
| } |
| |
| /** |
| * Returns the external form representing a named query. |
| * |
| * @return The external form representing a named query |
| */ |
| public IQuery getQuery() { |
| return getQueryContext().getQuery(); |
| } |
| |
| /** |
| * Returns the {@link JPQLQueryContext} that contains information about the JPQL query. |
| * |
| * @return The {@link JPQLQueryContext} that contains information contained in the JPQL query |
| */ |
| public JPQLQueryContext getQueryContext() { |
| if (queryContext == null) { |
| queryContext = buildJPQLQueryContext(jpqlGrammar); |
| } |
| return queryContext; |
| } |
| |
| /** |
| * Calculates the type of the query result of the JPQL query. |
| * <p> |
| * See {@link org.eclipse.persistence.jpa.jpql.tools.resolver.Resolver Resolver} |
| * to understand how the type is calculated. |
| * |
| * @return The result type of the JPQL query if it could accurately be calculated or the |
| * {@link IType} for <code>Object</code> if it could not be calculated |
| */ |
| public IType getResultType() { |
| |
| IType type = queryContext.getType(getJPQLExpression()); |
| |
| if (!type.isResolvable()) { |
| type = getTypeHelper().objectType(); |
| } |
| |
| return type; |
| } |
| |
| protected AbstractSemanticValidator getSemanticValidator() { |
| if (semanticValidator == null) { |
| semanticValidator = buildSemanticValidator(getQueryContext()); |
| } |
| return semanticValidator; |
| } |
| |
| /** |
| * Returns the {@link IType} representing the given Java type. |
| * |
| * @param type The Java type for which its external form is requested |
| * @return The external form for the given Java type |
| */ |
| public IType getType(Class<?> type) { |
| return getTypeRepository().getType(type); |
| } |
| |
| /** |
| * Returns a helper that gives access to the most common {@link IType types}. |
| * |
| * @return A helper containing a collection of methods related to {@link IType} |
| */ |
| public TypeHelper getTypeHelper() { |
| return getTypeRepository().getTypeHelper(); |
| } |
| |
| /** |
| * Returns the repository that gives access to the application's types. |
| * |
| * @return The repository for classes, interfaces, enum types and annotations |
| */ |
| public ITypeRepository getTypeRepository() { |
| return getProvider().getTypeRepository(); |
| } |
| |
| /** |
| * Sets the parsed tree representation of the JPQL query. If the expression was parsed outside of |
| * the scope of this context, then this method has to be invoked before {@link #setQuery(IQuery)} |
| * because the JPQL query is automatically parsed by that method. |
| * |
| * @param jpqlExpression The parsed representation of the JPQL query to manipulate |
| * @see #setQuery(IQuery) |
| */ |
| public void setJPQLExpression(JPQLExpression jpqlExpression) { |
| getQueryContext().setJPQLExpression(jpqlExpression); |
| } |
| |
| /** |
| * Sets the external form of the JPQL query, which will be parsed and information will be |
| * extracted for later access. |
| * |
| * @param query The external form of the JPQL query |
| */ |
| public void setQuery(IQuery query) { |
| getQueryContext().setQuery(query); |
| } |
| |
| /** |
| * Validates the query by introspecting it grammatically and semantically. |
| * |
| * @return The non-<code>null</code> list that will be used to store the {@link JPQLQueryProblem |
| * problems} if any was found |
| */ |
| public List<JPQLQueryProblem> validate() { |
| List<JPQLQueryProblem> problems = new LinkedList<>(); |
| validate(getJPQLExpression(), problems); |
| return problems; |
| } |
| |
| /** |
| * Validates the query by introspecting it grammatically and semantically. |
| * |
| * @param expression The parsed tree representation of the JPQL fragment to validate |
| * @param problems A non-<code>null</code> list that will be used to store the {@link |
| * JPQLQueryProblem problems} if any was found |
| */ |
| public void validate(Expression expression, List<JPQLQueryProblem> problems) { |
| validateGrammar(expression, problems); |
| validateSemantic(expression, problems); |
| } |
| |
| /** |
| * Validates the query by only introspecting it grammatically. |
| * |
| * @return The non-<code>null</code> list that will be used to store the {@link JPQLQueryProblem |
| * problems} if any was found |
| */ |
| public List<JPQLQueryProblem> validateGrammar() { |
| List<JPQLQueryProblem> problems = new LinkedList<>(); |
| validateGrammar(getJPQLExpression(), problems); |
| return problems; |
| } |
| |
| /** |
| * Validates the query by only introspecting it grammatically. |
| * |
| * @param expression The parsed tree representation of the query |
| * @param problems A non-<code>null</code> list that will be used to store the {@link |
| * JPQLQueryProblem problems} if any was found |
| */ |
| public void validateGrammar(Expression expression, List<JPQLQueryProblem> problems) { |
| AbstractGrammarValidator visitor = getGrammarValidator(); |
| try { |
| visitor.setProblems(problems); |
| expression.accept(visitor); |
| } |
| finally { |
| visitor.dispose(); |
| } |
| } |
| |
| /** |
| * Validates the query by only introspecting it semantically. |
| * |
| * @return The non-<code>null</code> list that will be used to store the {@link JPQLQueryProblem |
| * problems} if any was found |
| */ |
| public List<JPQLQueryProblem> validateSemantic() { |
| List<JPQLQueryProblem> problems = new LinkedList<>(); |
| validateSemantic(getJPQLExpression(), problems); |
| return problems; |
| } |
| |
| /** |
| * Validates the query by only introspecting it semantically. |
| * |
| * @param expression The parsed tree representation of the query |
| * @param problems A non-<code>null</code> list that will be used to store the {@link |
| * JPQLQueryProblem problems} if any was found |
| */ |
| public void validateSemantic(Expression expression, List<JPQLQueryProblem> problems) { |
| AbstractSemanticValidator visitor = getSemanticValidator(); |
| try { |
| visitor.setProblems(problems); |
| expression.accept(visitor); |
| } |
| finally { |
| visitor.dispose(); |
| } |
| } |
| } |