| /* |
| * 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.parser; |
| |
| import java.util.Arrays; |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.Locale; |
| import java.util.Map; |
| import java.util.Set; |
| import org.eclipse.persistence.jpa.jpql.Assert; |
| import org.eclipse.persistence.jpa.jpql.JPAVersion; |
| |
| /** |
| * This registry contains the necessary information used by Hermes parser. When parsing a JPQL query, |
| * the {@link JPQLGrammar} given to {@link JPQLExpression} will give access to this registry. |
| * <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 JPQLGrammar |
| * @see ExpressionFactory |
| * @see JPQLQueryBNF |
| * |
| * @version 2.5 |
| * @since 2.4 |
| * @author Pascal Filion |
| */ |
| @SuppressWarnings("nls") |
| public class ExpressionRegistry { |
| |
| /** |
| * The map of {@link ExpressionFactory ExpressionFactories} that have been registered and |
| * required for parsing a JPQL query, they are mapped with their unique identifier. |
| */ |
| private Map<String, ExpressionFactory> expressionFactories; |
| |
| /** |
| * The set of the JPQL identifiers defined by the grammar. |
| */ |
| private Map<String, IdentifierRole> identifiers; |
| |
| /** |
| * This table specify in which JPA version the identifiers was introduced. |
| */ |
| private Map<String, JPAVersion> identifierVersions; |
| |
| /** |
| * The {@link JPQLQueryBNF} unique identifiers mapped to the only instance of the BNF rule. |
| */ |
| private Map<String, JPQLQueryBNF> queryBNFs; |
| |
| /** |
| * Creates the only instance of <code>ExpressionRegistry</code>. |
| */ |
| public ExpressionRegistry() { |
| super(); |
| initialize(); |
| } |
| |
| /** |
| * Adds to the given query BNF a child BNF. |
| * |
| * @param queryBNFId The unique identifier of the parent BNF to add a child BNF |
| * @param childQueryBNFId The unique identifier of the child to add to the parent BNF |
| * @exception NullPointerException The {@link JPQLQueryBNF} identified by the given ID does not exist |
| */ |
| public void addChildBNF(String queryBNFId, String childQueryBNFId) { |
| JPQLQueryBNF queryBNF = queryBNFs.get(queryBNFId); |
| Assert.isNotNull(queryBNF, "The JPQLQueryBNF identified with '" + queryBNFId + "' does not exist."); |
| queryBNF.registerChild(childQueryBNFId); |
| } |
| |
| /** |
| * Adds to the given unique identifier of an {@link ExpressionFactory} to the given query BNF. |
| * |
| * @param queryBNFId The unique identifier of the parent BNF |
| * @param childExpressionFactoryId The unique identifier of the {@link ExpressionFactory} to add |
| * to the given query BNF |
| * @exception NullPointerException The {@link JPQLQueryBNF} identified by the given ID does not exist |
| */ |
| public void addChildFactory(String queryBNFId, String childExpressionFactoryId) { |
| JPQLQueryBNF queryBNF = queryBNFs.get(queryBNFId); |
| Assert.isNotNull(queryBNF, "The JPQLQueryBNF identified with '" + queryBNFId + "' does not exist."); |
| queryBNF.registerExpressionFactory(childExpressionFactoryId); |
| } |
| |
| /** |
| * Adds the given JPQL identifier to this factory. |
| * |
| * @param expressionFactoryId The unique identifier of the {@link ExpressionFactory} to add more |
| * JPQL identifiers |
| * @param identifier The JPQL identifier this factory will parse |
| */ |
| public void addIdentifier(String expressionFactoryId, String identifier) { |
| getExpressionFactory(expressionFactoryId).addIdentifier(identifier); |
| } |
| |
| /** |
| * Adds the given JPQL identifiers to this factory. |
| * |
| * @param expressionFactoryId The unique identifier of the {@link ExpressionFactory} to add more |
| * JPQL identifiers |
| * @param identifiers The JPQL identifiers this factory will parse |
| */ |
| public void addIdentifiers(String expressionFactoryId, String... identifiers) { |
| getExpressionFactory(expressionFactoryId).addIdentifiers(identifiers); |
| } |
| |
| /** |
| * Retrieves the {@link ExpressionFactory} that is responsible for creating the {@link Expression} |
| * object that represents the given JPQL identifier. |
| * |
| * @param identifier The JPQL identifier for which its factory is searched |
| * @return Either the {@link ExpressionFactory} that creates the {@link Expression} or |
| * <code>null</code> if none was found |
| */ |
| public ExpressionFactory expressionFactoryForIdentifier(String identifier) { |
| |
| identifier = identifier.toUpperCase(Locale.ROOT); |
| |
| if (Expression.SELECT.equalsIgnoreCase(identifier)) { |
| return getExpressionFactory(SimpleSelectStatementFactory.ID); |
| } |
| |
| for (ExpressionFactory expressionFactory : expressionFactories.values()) { |
| boolean found = Arrays.binarySearch(expressionFactory.identifiers(), identifier) > -1; |
| if (found) { |
| return expressionFactory; |
| } |
| } |
| |
| return null; |
| } |
| |
| /** |
| * Retrieves the registered {@link ExpressionFactory} that was registered for the given unique identifier. |
| * |
| * @param expressionFactoryId The unique identifier of the {@link ExpressionFactory} to retrieve |
| * @return The {@link ExpressionFactory} mapped with the given unique identifier |
| */ |
| public ExpressionFactory getExpressionFactory(String expressionFactoryId) { |
| return expressionFactories.get(expressionFactoryId); |
| } |
| |
| /** |
| * Retrieves the {@link IdentifierRole} of the given JPQL identifier. A role helps to describe |
| * the purpose of the identifier in a JPQL query. |
| * |
| * @param identifier The JPQL identifier for which its role is returned |
| * @return The {@link IdentifierRole} of the given JPQL identifier |
| */ |
| public IdentifierRole getIdentifierRole(String identifier) { |
| return identifiers.get(identifier.toUpperCase(Locale.ROOT)); |
| } |
| |
| /** |
| * Returns the JPQL identifiers that are supported by the {@link JPQLGrammar JPQL grammar}. |
| * |
| * @return The supported JPQL identifiers |
| */ |
| public Set<String> getIdentifiers() { |
| return Collections.unmodifiableSet(identifiers.keySet()); |
| } |
| |
| /** |
| * Retrieves the JPQL identifiers that are supported by the BNF rule with the given unique |
| * identifier. The JPQL identifiers are retrieved by scanning the {@link ExpressionFactory} |
| * registered with the BNF rule and its child BNF rules. |
| * |
| * @return The list of JPQL identifiers that are supported by a BNF rule |
| */ |
| public Iterable<String> getIdentifiers(String queryBNFId) { |
| return getQueryBNF(queryBNFId).getIdentifiers(); |
| } |
| |
| /** |
| * Retrieves the JPA version in which the identifier was first introduced. |
| * |
| * @return The version in which the identifier was introduced |
| */ |
| public JPAVersion getIdentifierVersion(String identifier) { |
| JPAVersion version = identifierVersions.get(identifier.toUpperCase(Locale.ROOT)); |
| return (version != null) ? version : JPAVersion.VERSION_1_0; |
| } |
| |
| /** |
| * Retrieves the BNF object that was registered for the given unique identifier. |
| * |
| * @param queryBNFId The unique identifier of the {@link JPQLQueryBNF} to retrieve |
| * @return The {@link JPQLQueryBNF} representing a section of the grammar |
| */ |
| public JPQLQueryBNF getQueryBNF(String queryBNFId) { |
| return queryBNFs.get(queryBNFId); |
| } |
| |
| /** |
| * Instantiates the only instance of various API used by the parser. |
| */ |
| protected void initialize() { |
| queryBNFs = new HashMap<>(); |
| identifiers = new HashMap<>(); |
| expressionFactories = new HashMap<>(); |
| identifierVersions = new HashMap<>(); |
| } |
| |
| /** |
| * Determines if the given word is a JPQL identifier. The check is case insensitive. |
| * |
| * @param text The string value to test if it's a JPQL identifier based on what is registered |
| * with this <code>ExpressionRegistry</code> |
| * @return <code>true</code> if the word is an identifier, <code>false</code> otherwise |
| */ |
| public boolean isIdentifier(String text) { |
| return identifiers.containsKey(text.toUpperCase(Locale.ROOT)); |
| } |
| |
| /** |
| * Registers the given {@link JPQLQueryBNF}. The BNF will be registered using its unique |
| * identifier. |
| * |
| * @param queryBNF The {@link JPQLQueryBNF} to store |
| * @exception NullPointerException The {@link JPQLQueryBNF} cannot be <code>null</code> |
| */ |
| public void registerBNF(JPQLQueryBNF queryBNF) { |
| |
| Assert.isNotNull(queryBNF, "The JPQLQueryBNF cannot be null"); |
| |
| queryBNFs.put(queryBNF.getId(), queryBNF); |
| queryBNF.setExpressionRegistry(this); |
| } |
| |
| /** |
| * Registers the given {@link ExpressionFactory} by storing it for all its identifiers. |
| * |
| * @param expressionFactory The {@link ExpressionFactory} to store |
| * @exception NullPointerException The {@link ExpressionFactory} cannot be <code>null</code> |
| */ |
| public void registerFactory(ExpressionFactory expressionFactory) { |
| |
| Assert.isNotNull(expressionFactory, "The ExpressionFactory cannot be null"); |
| |
| expressionFactories.put(expressionFactory.getId(), expressionFactory); |
| expressionFactory.setExpressionRegistry(this); |
| } |
| |
| /** |
| * Registers the {@link IdentifierRole} for the given JPQL identifier. |
| * |
| * @param identifier The JPQL identifier to register its role within a JPQL query |
| * @param role The role of the given JPQL identifier |
| * @exception NullPointerException The JPQL identifier and the {@link IdentifierRole} cannot be |
| * <code>null</code> |
| */ |
| public void registerIdentifierRole(String identifier, IdentifierRole role) { |
| |
| Assert.isNotNull(identifier, "The JPQL identifier cannot be null"); |
| Assert.isNotNull(role, "The IdentifierRole cannot be null"); |
| |
| identifiers.put(identifier, role); |
| } |
| |
| /** |
| * Registers the {@link JPAVersion} for which the given JPQL identifier was defined. |
| * |
| * @param identifier The JPQL identifier to register in which version it was added to the grammar |
| * @param version The version when the JPQL identifier was added to the grammar |
| * @exception NullPointerException The JPQL identifier and the {@link JPAVersion} cannot be |
| * <code>null</code> |
| */ |
| public void registerIdentifierVersion(String identifier, JPAVersion version) { |
| |
| Assert.isNotNull(identifier, "The JPQL identifier cannot be null"); |
| Assert.isNotNull(version, "The JPAVersion cannot be null"); |
| |
| identifierVersions.put(identifier, version); |
| } |
| |
| /** |
| * When parsing the query and no {@link JPQLQueryBNF JPQLQueryBNFs} can help to parse the query, |
| * then it will fall back on the given one. |
| * |
| * @param queryBNFId The unique identifier of the BNF to modify its fallback BNF unique identifier |
| * @param fallbackBNFId The unique identifier of the {@link JPQLQueryBNF} to use in the last resort |
| * @exception NullPointerException The {@link JPQLQueryBNF} identified by the given ID does not exist |
| */ |
| public void setFallbackBNFId(String queryBNFId, String fallbackBNFId) { |
| JPQLQueryBNF queryBNF = queryBNFs.get(queryBNFId); |
| Assert.isNotNull(queryBNF, "The JPQLQueryBNF identified with '" + queryBNFId + "' does not exist."); |
| queryBNF.setFallbackBNFId(queryBNFId); |
| } |
| |
| /** |
| * Sets the unique identifier of the {@link ExpressionFactory} to use when the fall back BNF |
| * ID is not <code>null</code>. This will be used to parse a portion of the query when the |
| * registered {@link ExpressionFactory expression factories} cannot parse it. |
| * <p> |
| * Note: This method is only called if {@link JPQLQueryBNF#getFallbackBNFId() JPQLQueryBNF. |
| * getFallbackBNFId()} does not return <code>null</code>. |
| * |
| * @param queryBNFId The unique identifier of the BNF to modify its fallback expression factory |
| * unique identifier |
| * @exception NullPointerException The {@link JPQLQueryBNF} identified by the given ID does not exist |
| */ |
| public void setFallbackExpressionFactoryId(String queryBNFId, String fallbackExpressionFactoryId) { |
| JPQLQueryBNF queryBNF = queryBNFs.get(queryBNFId); |
| Assert.isNotNull(queryBNF, "The JPQLQueryBNF identified with '" + queryBNFId + "' does not exist."); |
| queryBNF.setFallbackExpressionFactoryId(fallbackExpressionFactoryId); |
| } |
| |
| /** |
| * Determines whether the <code>Expression</code> handles a collection of sub-expressions that |
| * are separated by commas. |
| * |
| * @param queryBNFId The unique identifier of the {@link JPQLQueryBNF} |
| * @param handleCollection <code>true</code> if the sub-expression to parse might have several |
| * sub-expressions separated by commas; <code>false</code> otherwise |
| */ |
| public void setHandleCollection(String queryBNFId, boolean handleCollection) { |
| JPQLQueryBNF queryBNF = queryBNFs.get(queryBNFId); |
| Assert.isNotNull(queryBNF, "The JPQLQueryBNF identified with '" + queryBNFId + "' does not exist."); |
| queryBNF.setHandleCollection(handleCollection); |
| } |
| |
| /** |
| * Sets whether the BNF with the given ID supports nested array or not. A nested array is a sub- |
| * expression with its child being a collection expression: (item_1, item_2, ..., item_n). |
| * |
| * @param queryBNFId The unique identifier of the {@link JPQLQueryBNF} |
| * @param handleNestedArray <code>true</code> if the expression represented by this BNF can be |
| * a nested array; <code>false</code> otherwise |
| * @since 2.5 |
| */ |
| public void setHandleNestedArray(String queryBNFId, boolean handleNestedArray) { |
| JPQLQueryBNF queryBNF = queryBNFs.get(queryBNFId); |
| Assert.isNotNull(queryBNF, "The JPQLQueryBNF identified with '" + queryBNFId + "' does not exist."); |
| queryBNF.setHandleNestedArray(handleNestedArray); |
| } |
| |
| /** |
| * Sets whether the query BNF with the given ID handles parsing a sub-expression, i.e. parsing an |
| * expression encapsulated by parenthesis. Which in fact would be handled by the fallback {@link |
| * ExpressionFactory}. The default behavior is to not handle it. |
| * <p> |
| * A good example for using this option is when an {@link Expression} cannot use any {@link |
| * ExpressionFactory} for creating a child object, parsing will use the fallback {@link |
| * ExpressionFactory}, if one was specified. So when this is set to <code>true</code>, the |
| * fallback {@link ExpressionFactory} will be immediately invoked. |
| * <p> |
| * Let's say we want to parse "SELECT e FROM (SELECT a FROM Address a) e", {@link FromClause} |
| * cannot use a factory for parsing the entity name (that's what usually the <code>FROM</code> |
| * clause has) so it uses the fallback factory to create {@link IdentificationVariableDeclaration}. |
| * Then <code>IdentificationVariableDeclaration</code> also cannot use any factory to create its |
| * child object so it uses the fallback factory to create {@link RangeVariableDeclaration}. |
| * By changing the status of for handling the sub-expression for the BNFs for those objects, then |
| * a subquery can be created by <code>RangeVariableDeclaration</code>. |
| * |
| * <pre><code>FromClause |
| * |- IdentificationVariableDeclaration |
| * |- RangeVariableDeclaration |
| * |- SubExpression(subquery)</code></pre> |
| * |
| * In order to get this working, the following would have to be done into the grammar: |
| * |
| * <pre><code> public class MyJPQLGrammar extends AbstractJPQLGrammar { |
| * @Override |
| * protected void initializeBNFs() { |
| * setHandleSubExpression(InternalFromClauseBNF.ID, true); |
| * setHandleSubExpression(InternalSimpleFromClauseBNF.ID, true); |
| * setHandleSubExpression(IdentificationVariableDeclarationBNF.ID, true); |
| * setHandleSubExpression(RangeVariableDeclarationBNF.ID, true); |
| * } |
| * }</code></pre> |
| * |
| * @param queryBNFId The unique identifier of the {@link JPQLQueryBNF} |
| * @param handleSubExpression <code>true</code> to let the creation of a sub-expression be |
| * created by the fallback {@link ExpressionFactory} registered with this BNF; <code>false</code> |
| * otherwise (which is the default value) |
| */ |
| public void setHandleSubExpression(String queryBNFId, boolean handleSubExpression) { |
| JPQLQueryBNF queryBNF = queryBNFs.get(queryBNFId); |
| Assert.isNotNull(queryBNF, "The JPQLQueryBNF identified with '" + queryBNFId + "' does not exist."); |
| queryBNF.setHandleSubExpression(handleSubExpression); |
| } |
| |
| /** |
| * Unregisters the given {@link JPQLQueryBNF}. |
| * |
| * @param queryBNF The {@link JPQLQueryBNF} to unregister |
| * @exception NullPointerException The {@link JPQLQueryBNF} cannot be <code>null</code> |
| */ |
| public void unregisterBNF(JPQLQueryBNF queryBNF) { |
| |
| Assert.isNotNull(queryBNF, "The JPQLQueryBNF cannot be null"); |
| |
| queryBNFs.remove(queryBNF.getId()); |
| queryBNF.setExpressionRegistry(null); |
| } |
| } |