All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.eclipse.persistence.jpa.jpql.parser.AbstractExpression Maven / Gradle / Ivy

There is a newer version: 5.0.0-B03
Show newest version
/*
 * 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.parser;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import org.eclipse.persistence.jpa.jpql.ExpressionTools;
import org.eclipse.persistence.jpa.jpql.JPAVersion;
import org.eclipse.persistence.jpa.jpql.WordParser;
import org.eclipse.persistence.jpa.jpql.WordParser.WordType;
import org.eclipse.persistence.jpa.jpql.utility.iterable.ListIterable;
import org.eclipse.persistence.jpa.jpql.utility.iterable.SnapshotCloneListIterable;

/**
 * This is the abstract definition of all the parts used to create the tree hierarchy representing
 * the parsed JPQL query.
 *
 * @see ExpressionFactory
 * @see JPQLGrammar
 *
 * @version 2.6
 * @since 2.3
 * @author Pascal Filion
 */
@SuppressWarnings("nls")
public abstract class AbstractExpression implements Expression {

    /**
     * The string representation of this {@link AbstractExpression} (which includes its children).
     * The string includes characters that are considered virtual, i.e. that was parsed when the
     * query is incomplete and is needed for functionality like content assist.
     *
     * @see #toActualText()
     */
    private String actualText;

    /**
     * The children of this {@link AbstractExpression}.
     *
     * @see #children()
     */
    private List children;

    /**
     * The position of this {@link AbstractExpression} in relation to its parent hierarchy by
     * calculating the length of the string representation of what comes before.
     */
    private int offset;

    /**
     * The string representation of this {@link AbstractExpression}.
     *
     * @see #orderedChildren()
     */
    private List orderedChildren;

    /**
     * The parent of this {@link AbstractExpression} or null if this object is {@link
     * JPQLExpression} - the root of the parsed tree hierarchy.
     */
    private AbstractExpression parent;

    /**
     * The string representation of this {@link AbstractExpression} (which includes its children).
     * The string does not include characters that are considered virtual, i.e. that was parsed when
     * the query is incomplete.
     *
     * @see #toParsedText()
     */
    private String parsedText;

    /**
     * This attribute can be used to store the {@link AbstractExpression}'s JPQL identifier or a literal.
     */
    private String text;

    /**
     * The constant for ','.
     */
    public static final char COMMA = ',';

    /**
     * The constant for '.'.
     */
    public static final char DOT = '.';

    /**
     * The constant for '"'.
     */
    public static final char DOUBLE_QUOTE = '\"';

    /**
     * The constant for '{'.
     */
    public static final char LEFT_CURLY_BRACKET = '{';

    /**
     * The constant for '('.
     */
    public static final char LEFT_PARENTHESIS = '(';

    /**
     * The constant for a character that is not defined.
     */
    public static final char NOT_DEFINED = '\0';

    /**
     * The constant for '}'.
     */
    public static final char RIGHT_CURLY_BRACKET = '}';

    /**
     * The constant for ')'.
     */
    public static final char RIGHT_PARENTHESIS = ')';

    /**
     * The constant for '''.
     */
    public static final char SINGLE_QUOTE = '\'';

    /**
     * The constant for ' '.
     */
    public static final char SPACE = ' ';

    /**
     * The constant for '_'.
     */
    public static final char UNDERSCORE = '_';

    /**
     * Creates a new AbstractExpression.
     *
     * @param parent The parent of this expression
     */
    protected AbstractExpression(AbstractExpression parent) {
        this(parent, ExpressionTools.EMPTY_STRING);
    }

    /**
     * Creates a new AbstractExpression.
     *
     * @param parent The parent of this expression
     * @param text The text to be stored in this expression, null cannot be passed
     */
    protected AbstractExpression(AbstractExpression parent, String text) {
        super();
        this.offset = -1;
        this.text   = text;
        this.parent = parent;
    }

    /**
     * The given {@link ExpressionVisitor} needs to visit this class but it is defined by a third-
     * party provider. This method will programmatically invoke the visit method defined on
     * the visitor. The method signature should be:
     *
     * 
{public|protected|private} void visit(ThirdPartyExpression expression)
*

* or * *

{public|protected|private} void visit(Expression expression)
*

* Note: The package protected visibility (default) should be used with care, if the code * is running inside OSGi, then the method will not be accessible, even through reflection. * * @param visitor The {@link ExpressionVisitor} to visit this {@link Expression} programmatically * @return true if the call was successfully executed; false otherwise * @since 2.4 */ protected boolean acceptUnknownVisitor(ExpressionVisitor visitor) { try { try { acceptUnknownVisitor(visitor, visitor.getClass(), getClass()); } catch (NoSuchMethodException e) { // Try with Expression as the parameter type acceptUnknownVisitor(visitor, visitor.getClass(), Expression.class); } return true; } catch (NoSuchMethodException e) { // Ignore, just do nothing return false; } catch (IllegalAccessException e) { // Ignore, just do nothing return false; } catch (InvocationTargetException e) { Throwable cause = e.getCause(); RuntimeException actual; if (cause instanceof RuntimeException) { actual = (RuntimeException) cause; } else { actual = new RuntimeException(cause); } throw actual; } } /** * The given {@link ExpressionVisitor} needs to visit this class but it is defined by a third- * party provider. This method will programmatically invoke the visit method defined on * the visitor. The method signature should be: * *

{public|protected|private} void visit(ThirdPartyExpression expression)
*

* or * *

{public|protected|private} void visit(Expression expression)
*

* * @param visitor The {@link ExpressionVisitor} to visit this {@link Expression} programmatically * @param type The type found in the hierarchy of the given {@link ExpressionVisitor} that will * be used to retrieve the visit method * @param parameterType The parameter type of the visit method * @see #acceptUnknownVisitor(ExpressionVisitor) * @since 2.4 */ protected void acceptUnknownVisitor(ExpressionVisitor visitor, Class type, Class parameterType) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException{ try { Method visitMethod = type.getDeclaredMethod("visit", parameterType); visitMethod.setAccessible(true); visitMethod.invoke(visitor, this); } catch (NoSuchMethodException e) { type = type.getSuperclass(); if (type == Object.class) { throw e; } else { acceptUnknownVisitor(visitor, type, parameterType); } } } /** * Adds the children of this {@link AbstractExpression} to the given collection. * * @param children The collection used to store the children */ protected void addChildrenTo(Collection children) { } /** * Adds the children of this {@link AbstractExpression} to the given list. * * @param children The list used to store the string representation of this {@link AbstractExpression} */ protected void addOrderedChildrenTo(List children) { } /** * No factories were found to create an {@link Expression} with the content of {@link WordParser}, * this method will retrieve the fallback {@link ExpressionFactory} defined in the given {@link * JPQLQueryBNF BNF}. * * @param wordParser The text to parse based on the current position of the cursor * @param word The word that was retrieved from the given text, which is the first word in the text * @param queryBNF The {@link JPQLQueryBNF} used to determine how to parse from the current * position of the cursor within the JPQL query * @param expression The {@link Expression} that has just been parsed or null * @param tolerant Determines whether the parsing system should be tolerant, meaning if it should * try to parse invalid or incomplete queries * @return The {@link Expression} representing the given sub-query */ protected final AbstractExpression buildExpressionFromFallingBack(WordParser wordParser, String word, JPQLQueryBNF queryBNF, AbstractExpression expression, boolean tolerant) { ExpressionFactory factory = findFallBackExpressionFactory(queryBNF); if (factory == null) { return null; } // When parsing an invalid or incomplete query, it is possible two literals would be parsed // but in some cases, a CollectionExpression should not be created and the parsing should // actually stop here. Example: BETWEEN 10 20, when parsing 20, it should not be parsed as // part of the lower bound expression if (tolerant && (factory.getId() == LiteralExpressionFactory.ID) && shouldSkipLiteral(expression)) { return null; } return factory.buildExpression(this, wordParser, word, queryBNF, expression, tolerant); } /** * Creates a new null-{@link Expression} parented with this one. * * @return A new null version of an {@link Expression} */ protected final AbstractExpression buildNullExpression() { return new NullExpression(this); } /** * Creates a new {@link Expression} wrapping the given character value. * * @param value The character to wrap as a {@link Expression} * @return The {@link Expression} representation of the given identifier where the owning * {@link Expression} is this one */ protected final Expression buildStringExpression(char value) { return buildStringExpression(String.valueOf(value)); } /** * Creates a new {@link Expression} wrapping the given string value. * * @param value The string to wrap as a Expression * @return The {@link Expression} representation of the given identifier where the owning * {@link Expression} is this one */ protected final Expression buildStringExpression(String value) { return new DefaultStringExpression(this, value); } /** * Creates an {@link Expression} that contains a malformed expression. * * @param text The text causing the expression to be malformed * @return A new {@link Expression} where {@link #toActualText()} returns the given text */ protected final AbstractExpression buildUnknownExpression(String text) { return new UnknownExpression(this, text); } /** * Calculates the position of the given {@link Expression} by calculating the length of what is before. * * @param expression The {@link Expression} for which its position within the parsed tree needs * to be determined * @param length The current cursor position within the JPQL query while digging into the tree * until the search reaches the expression * @return The length of the string representation for what is coming before the given {@link Expression} * @since 2.4 */ protected final int calculatePosition(Expression expression, int length) { Expression parent = expression.getParent(); // Reach the root if (parent == null) { return length; } // Traverse the child expression until the expression for (Expression childExpression : parent.orderedChildren()) { // Continue to calculate the position by going up the hierarchy if (childExpression == expression) { return calculatePosition(parent, length); } length += childExpression.getLength(); } // It should never reach this throw new RuntimeException("The position of the Expression could not be calculated: " + expression); } @Override public final ListIterable children() { if (children == null) { children = new LinkedList(); addChildrenTo(children); } return new SnapshotCloneListIterable(children); } /** * Retrieve the {@link ExpressionFactory} from the given {@link JPQLQueryBNF} by following the * path of fallback {@link JPQLQueryBNF JPQLQueryBNFs} and then returns the {@link ExpressionFactory} * from the leaf {@link JPQLQueryBNF}. * * @param queryBNF The {@link JPQLQueryBNF} for which its associated fallback {@link ExpressionFactory} * will be searched * @return Either the fallback {@link ExpressionFactory} linked to the given {@link JPQLQueryBNF} * or null if none was declared */ protected final ExpressionFactory findFallBackExpressionFactory(JPQLQueryBNF queryBNF) { String fallBackBNFId = queryBNF.getFallbackBNFId(); // No fall back BNF is defined, then nothing can be done if (fallBackBNFId == null) { return null; } JPQLQueryBNF fallBackQueryBNF = getQueryBNF(fallBackBNFId); // Traverse the fall back BNF because it has its own fall back BNF if (fallBackQueryBNF != queryBNF && fallBackQueryBNF.getFallbackBNFId() != null) { return findFallBackExpressionFactory(fallBackQueryBNF); } // Retrieve the factory associated with the fall back BNF return getExpressionRegistry().getExpressionFactory(fallBackQueryBNF.getFallbackExpressionFactoryId()); } @Override public JPQLQueryBNF findQueryBNF(Expression expression) { return getQueryBNF(); } /** * 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 * @see ExpressionRegistry#getExpressionFactory(String) */ protected final ExpressionFactory getExpressionFactory(String expressionFactoryId) { return getExpressionRegistry().getExpressionFactory(expressionFactoryId); } /** * Returns the registry containing the {@link JPQLQueryBNF JPQLQueryBNFs} and the {@link * org.eclipse.persistence.jpa.jpql.parser.ExpressionFactory ExpressionFactories} that are used * to properly parse a JPQL query. * * @return The registry containing the information related to the JPQL grammar */ protected final ExpressionRegistry getExpressionRegistry() { return getGrammar().getExpressionRegistry(); } @Override public JPQLGrammar getGrammar() { return getRoot().getGrammar(); } /** * 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) { return getRoot().getIdentifierVersion(identifier); } /** * Returns the version of the Java Persistence to support. * * @return The JPA version supported by the grammar * @see JPQLGrammar */ protected JPAVersion getJPAVersion() { return getRoot().getJPAVersion(); } @Override public final int getLength() { return toActualText().length(); } @Override public final int getOffset() { if (offset == -1) { offset = calculatePosition(this, 0); } return offset; } @Override public final AbstractExpression getParent() { return parent; } /** * 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 getExpressionRegistry().getQueryBNF(queryBNFID); } @Override public final JPQLExpression getRoot() { return (parent == null) ? (JPQLExpression) this : parent.getRoot(); } /** * Returns the encapsulated text of this {@link AbstractExpression}, which can be used in various * ways, it can be a keyword, a literal, etc. * * @return Either the JPQL identifier for this {@link AbstractExpression}, the literal it * encapsulates or an empty string */ protected String getText() { return text; } /** * Determines whether the given {@link JPQLQueryBNF} handles aggregate expressions. * * @param queryBNF The {@link JPQLQueryBNF} used to determine if the parsing should handle * aggregate expressions * @return true if the given BNF handles aggregate expressions; false * otherwise */ protected boolean handleAggregate(JPQLQueryBNF queryBNF) { return queryBNF.handleAggregate(); } /** * Determines whether the given {@link JPQLQueryBNF} handles a collection of sub-expressions that * are separated by commas. * * @param queryBNF The {@link JPQLQueryBNF} used to determine if the parsing should handle * collection of sub-expressions * @return true if the sub-expression to parse might have several sub-expressions * separated by commas; false otherwise */ protected boolean handleCollection(JPQLQueryBNF queryBNF) { return queryBNF.handleCollection(); } @Override public boolean isAncestor(Expression expression) { if (expression == this) { return true; } if (expression == null) { return false; } return isAncestor(expression.getParent()); } /** * Determines if the given word is a JPQL identifier. The check is case insensitive. * * @param word The word to test if it is a JPQL identifier * @return true if the word is an identifier, false otherwise * @see ExpressionRegistry#isIdentifier(String) */ protected final boolean isIdentifier(String word) { return getExpressionRegistry().isIdentifier(word); } /** * Determines whether this expression is a null {@link Expression} or any other subclass. * * @return false by default */ protected boolean isNull() { return false; } /** * Determines whether the parsing is complete based on what is left in the given text. The text * is never empty. * * @param wordParser The text to parse based on the current position of the cursor * @param word The word that was retrieved from the given text, which is the first word in the text * @param expression The {@link Expression} that has already been parsed * @return true if the text no longer can't be parsed by the current expression; * false if more can be parsed */ protected boolean isParsingComplete(WordParser wordParser, String word, Expression expression) { // TODO: MAYBE MOVE THIS TO THE JPQL GRAMMAR SO GENERIC JPA DOES // NOT HAVE KNOWLEDGE OF ECLIPSELINK SPECIFIC FUNCTIONS return word.equalsIgnoreCase(FROM) || word.equalsIgnoreCase(WHERE) || word.equalsIgnoreCase(HAVING) || wordParser.startsWithIdentifier(GROUP_BY) || wordParser.startsWithIdentifier(ORDER_BY) || wordParser.startsWithIdentifier(AS_OF) || wordParser.startsWithIdentifier(START_WITH) || wordParser.startsWithIdentifier(CONNECT_BY) || wordParser.startsWithIdentifier(ORDER_SIBLINGS_BY) || word.equalsIgnoreCase(UNION) || word.equalsIgnoreCase(INTERSECT) || word.equalsIgnoreCase(EXCEPT); } /** * Determines if the parser is in tolerant mode or is in fast mode. When the tolerant is turned * on, it means the parser will attempt to parse incomplete or invalid queries. * * @return true if the parsing system should parse invalid or incomplete queries; * false when the query is well-formed and valid */ protected boolean isTolerant() { return getRoot().isTolerant(); } /** * Determines whether this expression is an unknown {@link Expression} or any other subclass. * * @return false by default */ protected boolean isUnknown() { return false; } /** * Determines whether this {@link AbstractExpression} is virtual, meaning it's not part of the * query but is required for proper navigability. * * @return true if this {@link AbstractExpression} was virtually created to fully * qualify path expression; false if it was parsed */ protected boolean isVirtual() { return false; } @Override public final ListIterable orderedChildren() { if (orderedChildren == null) { orderedChildren = new LinkedList(); addOrderedChildrenTo(orderedChildren); } return new SnapshotCloneListIterable(orderedChildren); } /** * Parses the query by starting at the current position, which is part of the given {@link WordParser}. * * @param wordParser The text to parse based on the current position of the cursor * @param tolerant Determines whether the parsing system should be tolerant, meaning if it should * try to parse invalid or incomplete queries */ protected abstract void parse(WordParser wordParser, boolean tolerant); /** * Parses the given text by using the specified BNF. * * @param wordParser The text to parse based on the current position of the cursor * @param queryBNFId The unique identifier of the {@link JPQLQueryBNF} that is used to determine * how to parse the text at the current cursor position within the JPQL query * @param tolerant Determines whether the parsing system should be tolerant, meaning if it should * try to parse invalid or incomplete queries * @return The {@link Expression} representing the given sub-query */ @SuppressWarnings("null") protected AbstractExpression parse(WordParser wordParser, String queryBNFId, boolean tolerant) { // Quick check so we don't create some objects for no reasons if (tolerant && wordParser.isTail()) { return null; } // // NOTE: This method could look better but for performance reason, it is a single method, // which reduces the number of objects created and methods called. // JPQLQueryBNF queryBNF = getQueryBNF(queryBNFId); int count = 0; boolean beginning = !tolerant; char character = wordParser.character(); AbstractExpression child; AbstractExpression expression = null; Info rootInfo = null; Info currentInfo = null; // Parse the string until the position of the cursor is at // the end of the string or until the parsing is complete while (!wordParser.isTail()) { child = null; // // Step 1 // // Right away create a SubExpression and parse the encapsulated expression if (character == LEFT_PARENTHESIS) { // If the JPQLQueryBNF handles parsing the sub-expression, then delegate the parsing // to its fallback ExpressionFactory if (queryBNF.handleSubExpression()) { expression = buildExpressionFromFallingBack( wordParser, ExpressionTools.EMPTY_STRING, queryBNF, expression, tolerant ); } else { expression = new SubExpression(this, queryBNF); expression.parse(wordParser, tolerant); // Make sure this is not the root and if the parent handles parsing the sub- // expression, then the Expression needs to be returned without further parsing if ((parent != null) && parent.getQueryBNF().handleSubExpression()) { return expression; } } // Something has been parsed, which means it's not the beginning anymore beginning = false; // Continue to the next character/word count = wordParser.skipLeadingWhitespace(); character = wordParser.character(); // Store the SubExpression currentInfo = (rootInfo == null) ? (rootInfo = new Info()) : (currentInfo.next = new Info(currentInfo)); currentInfo.expression = expression; currentInfo.space = count > 0; } // Retrieve the next word, including any arithmetic symbols String word = wordParser.word(); // A word was parsed, attempt to parse it using first the factory, then the fallback factory if (word.length() > 0) { // Nothing more to parse if (!tolerant && !beginning && isParsingComplete(wordParser, word, expression) || tolerant && isParsingComplete(wordParser, word, expression)) { break; } // // Step 2 // // Parse using the ExpressionFactory that is mapped with a JPQL identifier (word) if (shouldParseWithFactoryFirst() && (wordParser.getWordType() == WordType.WORD)) { ExpressionFactory factory = queryBNF.getExpressionFactory(word); if (factory != null) { child = factory.buildExpression(this, wordParser, word, queryBNF, expression, tolerant); if (child != null) { // The new expression is a child of the previous expression, // remove it from the collection since it's already parented if ((expression != null) && child.isAncestor(expression)) { if (currentInfo == rootInfo) { rootInfo = null; currentInfo = null; } else if (currentInfo != null) { currentInfo = currentInfo.previous; } } // Something has been parsed, which means it's not the beginning anymore beginning = false; // Continue with the next character/word count = wordParser.skipLeadingWhitespace(); character = wordParser.character(); // The new expression becomes the previous expression expression = child; } } } // // Step 3 // // No factories could be used, use the fall back ExpressionFactory if (child == null) { child = buildExpressionFromFallingBack(wordParser, word, queryBNF, expression, tolerant); if (child != null) { // The new expression is a child of the previous expression, // remove it from the collection since it's already parented if ((expression != null) && child.isAncestor(expression)) { if (currentInfo == rootInfo) { rootInfo = null; currentInfo = null; } else if (currentInfo != null) { currentInfo = currentInfo.previous; } } // Something has been parsed, which means it's not the beginning anymore beginning = false; // Continue with the next character/word count = wordParser.skipLeadingWhitespace(); character = wordParser.character(); // The new expression becomes the previous expression expression = child; } } // // Step 4 // // If nothing was parsed, then attempt to parse the fragment by retrieving the factory // directory from the JPQL grammar and not from the one registered with the current BNF if (tolerant && (child == null)) { ExpressionRegistry expressionRegistry = getExpressionRegistry(); if (expressionRegistry.getIdentifierRole(word) != IdentifierRole.AGGREGATE) { ExpressionFactory factory = expressionRegistry.expressionFactoryForIdentifier(word); if (factory != null) { child = factory.buildExpression(this, wordParser, word, queryBNF, expression, tolerant); if (child != null) { child = new BadExpression(this, child); // The new expression is a child of the previous expression, // remove it from the collection since it's already parented if ((expression != null) && child.isAncestor(expression)) { if (currentInfo == rootInfo) { rootInfo = null; currentInfo = null; } else if (currentInfo != null) { currentInfo = currentInfo.previous; } } // Something has been parsed, which means it's not the beginning anymore beginning = false; // Continue with the next character/word count = wordParser.skipLeadingWhitespace(); character = wordParser.character(); // The new expression becomes the previous expression expression = child; } } } } } // Nothing could be parsed, break here so the parent can continue parsing. // Example: AVG() and we're parsing what's inside the parenthesis (nothing). // But if it's (,), then we have to create a collection of "null" expressions // separated by a comma if ((child == null) && (character != COMMA)) { break; } // Store the child but skip a very special case, which happens when parsing // two subqueries in a collection expression. Example: (SELECT ... ), (SELECT ... ) if ((expression == null) || (child != null)) { currentInfo = (rootInfo == null) ? (rootInfo = new Info()) : (currentInfo.next = new Info(currentInfo)); currentInfo.expression = child; currentInfo.space = count > 1; } // Nothing else to parse if (wordParser.isTail()) { break; } // ',' if (character == COMMA) { // The current expression does not handle collection, then stop the // parsing here so the parent can continue to parse if (!handleCollection(queryBNF) || tolerant && isParsingComplete(wordParser, word, expression)) { break; } // Skip the comma wordParser.moveForward(1); currentInfo.comma = true; // Remove leading whitespace count = wordParser.skipLeadingWhitespace(); currentInfo.space = count > 0; character = wordParser.character(); expression = null; // Special case: ((), (), ()) if (character == LEFT_PARENTHESIS) { continue; } // No more text, the query ends with a comma word = wordParser.word(); boolean stopParsing = tolerant && (word.length() == 0 || isParsingComplete(wordParser, word, null)); if (wordParser.isTail() || stopParsing) { // Make sure the space is not re-added at the end of the query count = 0; // Add a null Expression since the expression ends with a comma currentInfo = (rootInfo == null) ? (rootInfo = new Info()) : (currentInfo.next = new Info(currentInfo)); // Nothing else to parse if (stopParsing) { break; } } // Nothing more to parse if (character == RIGHT_PARENTHESIS) { break; } } else { // Continue parsing the collection expression if (character != RIGHT_PARENTHESIS && handleAggregate(queryBNF)) { currentInfo.space = count > 0; } // Nothing more to parse else { break; } } } if (count > 0) { currentInfo.space = currentInfo.comma; if (!currentInfo.comma) { wordParser.moveBackward(count); } } // Nothing was parsed if (currentInfo == null) { return null; } // Simply return the single expression if (currentInfo == rootInfo && !currentInfo.comma && !currentInfo.space) { return currentInfo.expression; } // Return a collection of expressions return new CollectionExpression( this, rootInfo.buildChildren(), rootInfo.buildCommas(), rootInfo.buildSpaces() ); } /** * Right away parses the text by retrieving the {@link ExpressionFactory} for the first word that * is extracted from {@link WordParser} at the current location. * * @param wordParser The text to parse based on the current position of the cursor * @param queryBNFId The unique identifier of the {@link JPQLQueryBNF} that is used to determine * how to parse the text at the current cursor position within the JPQL query * @param tolerant Determines whether the parsing system should be tolerant, meaning if it should * try to parse invalid or incomplete queries * @return The {@link Expression} representing the given sub-query */ protected AbstractExpression parseUsingExpressionFactory(WordParser wordParser, String queryBNFId, boolean tolerant) { String word = wordParser.word(); JPQLQueryBNF queryBNF = getQueryBNF(queryBNFId); ExpressionFactory factory = queryBNF.getExpressionFactory(word); if (factory == null) { return null; } return factory.buildExpression(this, wordParser, word, queryBNF, null, tolerant); } @Override public void populatePosition(QueryPosition queryPosition, int position) { queryPosition.addPosition(this, position); // The position is at the beginning of this expression if (position == 0) { queryPosition.setExpression(this); } else { // Traverse the children in order to find where the cursor is located for (Expression expression : orderedChildren()) { String expressionText = expression.toParsedText(); // The position is in the Expression, traverse it if (position <= expressionText.length()) { expression.populatePosition(queryPosition, position); return; } // Continue with the next child by adjusting the position position -= expressionText.length(); } throw new IllegalStateException("A problem was encountered while calculating the position."); } } /** * Rebuilds the actual parsed text if it has been cached. */ protected final void rebuildActualText() { if (actualText != null) { toActualText(); } } /** * Rebuilds the parsed parsed text if it has been cached. */ protected final void rebuildParsedText() { if (parsedText != null) { toParsedText(); } } /** * Re-parents this {@link Expression} to be a child of the given {@link Expression}. * * @param parent The new parent of this object */ protected final void setParent(AbstractExpression parent) { this.parent = parent; } /** * Sets the text of this {@link Expression}. * * @param text The immutable text wrapped by this {@link Expression}, which cannot be null */ protected final void setText(String text) { this.text = text; } /** * Determines whether the parsing of the query should be performed using the {@link ExpressionFactory * factories} first or it should automatically fallback to the fallback factory. * * @return true is returned by default so the factories are used before falling back */ protected boolean shouldParseWithFactoryFirst() { return true; } /** * When parsing an invalid or incomplete query, it is possible two literals would be parsed but * in some cases, a CollectionExpression should not be created and the parsing should actually * stop here. Example: BETWEEN 10 20, when parsing 20, it should not be parsed as part of the * lower bound expression. * * @param expression The {@link Expression} that has just been parsed or null * @return true */ protected boolean shouldSkipLiteral(AbstractExpression expression) { return (expression != null); } @Override public String toActualText() { if (actualText == null) { StringBuilder writer = new StringBuilder(); toParsedText(writer, true); actualText = writer.toString(); } return actualText; } @Override public String toParsedText() { if (parsedText == null) { StringBuilder writer = new StringBuilder(); toParsedText(writer, false); parsedText = writer.toString(); } return parsedText; } /** * Generates a string representation of this {@link Expression}, including its children, * if it has any. * * @param writer The buffer used to append this {@link Expression}'s string representation * @param actual Determines whether the string representation should represent what was parsed, * i.e. include any "virtual" whitespace (such as ending whitespace) and the actual case of the * JPQL identifiers */ protected abstract void toParsedText(StringBuilder writer, boolean actual); @Override public final String toString() { // toString() should only be called during debugging, thus the cached parsed text // should always be recreated in order to reflect the current state while debugging parsedText = null; return toParsedText(); } /** * Rather than creating three lists when performing a single parsing operation ({@link * AbstractExpression#parse(WordParser, String, boolean) parse(WordParser, String, boolean)}), * this class will act as a simple chained list and postponing the creation of those three lists * only when needed (which is when the parsed expression needs to be a collection of expressions). */ private static class Info { /** * Flag indicating a comma follows the parsed {@link #expression}. */ boolean comma; /** * The parsed {@link Expression}, which is the first one that was parsed if {@link #next} is * null, meaning this Info object is the root of the chained list. */ AbstractExpression expression; /** * When more than one expression needs to be parsed, it will be chained. */ Info next; /** * The parent within the chain of this one. */ Info previous; /** * Flag indicating a whitespace follows the parsed {@link #expression}. */ boolean space; /** * Creates a new Info. */ Info() { super(); } /** * Creates a new Info. * * @param previous The parent within the chain of this one */ Info(Info previous) { super(); this.previous = previous; } private void addChild(ArrayList children) { children.add(expression); if (next != null) { next.addChild(children); } } private void addComma(ArrayList children) { children.add(comma); if (next != null) { next.addComma(children); } } private void addSpace(ArrayList children) { children.add(space); if (next != null) { next.addSpace(children); } } List buildChildren() { ArrayList children = new ArrayList(); addChild(children); return children; } List buildCommas() { ArrayList children = new ArrayList(); addComma(children); return children; } List buildSpaces() { ArrayList children = new ArrayList(); addSpace(children); return children; } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy