blob: bbeadc05a1db5a071a990f43378f10679575679e [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.parser;
import java.util.Collection;
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;
/**
* A <code>JPQLExpression</code> is the root of the parsed tree representation of a JPQL query. The
* query is parsed based on what was registered in the {@link JPQLGrammar}'s {@link ExpressionRegistry}.
* <p>
* A JPQL statement may be either a <b>SELECT</b> statement, an <b>UPDATE</b> statement, or a
* <b>DELETE FROM</b> statement.
*
* <div><b>BNF:</b> <code>QL_statement ::= {@link SelectStatement select_statement} |
* {@link UpdateStatement update_statement} |
* {@link DeleteStatement delete_statement}</code></div>
* <p>
* It is possible to parse a portion of a JPQL query. The ID of the {@link JPQLQueryBNF} is used to
* parse that portion and {@link #getQueryStatement()} then returns only the parsed tree representation
* of that JPQL fragment.
*
* @version 2.5
* @since 2.3
* @author Pascal Filion
*/
@SuppressWarnings("nls")
public final class JPQLExpression extends AbstractExpression {
/**
* The JPQL grammar that defines how to parse a JPQL query.
*/
private JPQLGrammar jpqlGrammar;
/**
* By default, this is {@link JPQLStatementBNF#ID} but it can be any other unique identifier of
* a {@link JPQLQueryBNF} when a portion of a JPQL query needs to be parsed.
*/
private String queryBNFId;
/**
* The tree representation of the query.
*/
private AbstractExpression queryStatement;
/**
* Determines if the parsing system should be tolerant, meaning if it should try to parse invalid
* or incomplete queries.
*/
private boolean tolerant;
/**
* If the expression could not be fully parsed, meaning some unknown text is trailing the query,
* this will contain it.
*/
private AbstractExpression unknownEndingStatement;
/**
* Creates a new <code>JPQLExpression</code>, which is the root of the JPQL parsed tree.
*
* @param query The string representation of the JPQL query to parse
* @param jpqlGrammar The JPQL grammar that defines how to parse a JPQL query
*/
public JPQLExpression(CharSequence query, JPQLGrammar jpqlGrammar) {
this(query, jpqlGrammar, false);
}
/**
* Creates a new <code>JPQLExpression</code>, which is the root of the JPQL parsed tree.
*
* @param query The string representation of the JPQL query to parse
* @param jpqlGrammar The JPQL grammar that defines how to parse a JPQL query
* @param tolerant Determines if the parsing system should be tolerant, meaning if it should try
* to parse invalid or incomplete queries
*/
public JPQLExpression(CharSequence query, JPQLGrammar jpqlGrammar, boolean tolerant) {
this(query, jpqlGrammar, JPQLStatementBNF.ID, tolerant);
}
/**
* Creates a new <code>JPQLExpression</code> that will parse the given fragment of a JPQL query.
* This means {@link #getQueryStatement()} will not return a query statement (select, delete or
* update) but only the parsed tree representation of the fragment if the query BNF can pare it.
* If the fragment of the JPQL query could not be parsed using the given {@link JPQLQueryBNF},
* then {@link #getUnknownEndingStatement()} will contain the non-parsable fragment.
*
* @param jpqlFragment A fragment of a JPQL query, which is a portion of a complete JPQL query
* @param jpqlGrammar The JPQL grammar that defines how to parse a JPQL query
* @param queryBNFId The unique identifier of the {@link org.eclipse.persistence.jpa.jpql.parser.JPQLQueryBNF JPQLQueryBNF}
* @param tolerant Determines if the parsing system should be tolerant, meaning if it should try
* to parse invalid or incomplete queries
* @since 2.4
*/
public JPQLExpression(CharSequence jpqlFragment,
JPQLGrammar jpqlGrammar,
String queryBNFId,
boolean tolerant) {
this(jpqlGrammar, queryBNFId, tolerant);
parse(new WordParser(jpqlFragment), tolerant);
}
/**
* Creates a new <code>JPQLExpression</code>, which is the root of the JPQL parsed tree.
*
* @param jpqlGrammar The JPQL grammar that defines how to parse a JPQL query
* @param tolerant Determines if the parsing system should be tolerant, meaning if it should try
* to parse invalid or incomplete queries
*/
private JPQLExpression(JPQLGrammar jpqlGrammar, String queryBNFId, boolean tolerant) {
super(null);
this.queryBNFId = queryBNFId;
this.tolerant = tolerant;
this.jpqlGrammar = jpqlGrammar;
}
@Override
public void accept(ExpressionVisitor visitor) {
visitor.visit(this);
}
@Override
public void acceptChildren(ExpressionVisitor visitor) {
getQueryStatement().accept(visitor);
getUnknownEndingStatement().accept(visitor);
}
@Override
protected void addChildrenTo(Collection<Expression> children) {
children.add(getQueryStatement());
children.add(getUnknownEndingStatement());
}
@Override
protected void addOrderedChildrenTo(List<Expression> children) {
if (queryStatement != null) {
children.add(queryStatement);
}
if (unknownEndingStatement != null) {
children.add(unknownEndingStatement);
}
}
/**
* Creates a map of the position of the cursor within each {@link Expression} of the parsed tree.
*
* @param actualQuery The actual query is a string representation of the query that may contain
* extra whitespace
* @param position The position of the cursor in the actual query, which is used to retrieve the
* deepest {@link Expression}. The position will be adjusted to fit into the beautified version
* of the query
* @return A new {@link QueryPosition}
*/
public QueryPosition buildPosition(String actualQuery, int position) {
// Adjust the position by not counting extra whitespace
position = ExpressionTools.repositionCursor(actualQuery, position, toActualText());
QueryPosition queryPosition = new QueryPosition(position);
populatePosition(queryPosition, position);
return queryPosition;
}
/**
* Returns the deepest {@link Expression} for the given position.
*
* @param actualQuery The actual query is the text version of the query that may contain extra
* whitespace and different formatting than the trim down version generated by the parsed tree
* @param position The position in the actual query used to retrieve the {@link Expression}
* @return The {@link Expression} located at the given position in the given query
*/
public Expression getExpression(String actualQuery, int position) {
QueryPosition queryPosition = buildPosition(actualQuery, position);
return queryPosition.getExpression();
}
@Override
public JPQLGrammar getGrammar() {
return jpqlGrammar;
}
@Override
public JPAVersion getJPAVersion() {
return jpqlGrammar.getJPAVersion();
}
@Override
public JPQLQueryBNF getQueryBNF() {
return getQueryBNF(queryBNFId);
}
/**
* Returns the {@link Expression} representing the query, which is either a <b>SELECT</b>, a
* <b>DELETE</b> or an <b>UPDATE</b> clause.
*
* @return The expression representing the Java Persistence query
*/
public Expression getQueryStatement() {
if (queryStatement == null) {
queryStatement = buildNullExpression();
}
return queryStatement;
}
/**
* Returns the {@link Expression} that may contain a portion of the query that could not be
* parsed, this happens when the query is either incomplete or malformed.
*
* @return The expression used when the ending of the query is unknown or malformed
*/
public Expression getUnknownEndingStatement() {
if (unknownEndingStatement == null) {
unknownEndingStatement = buildNullExpression();
}
return unknownEndingStatement;
}
/**
* Determines whether a query was parsed. The query may be incomplete but it started with one of
* the three clauses (<b>SELECT</b>, <b>DELETE FROM</b>, or <b>UPDATE</b>).
*
* @return <code>true</code> the query was parsed; <code>false</code> otherwise
*/
public boolean hasQueryStatement() {
return queryStatement != null &&
!queryStatement.isNull();
}
/**
* Determines whether the query that got parsed had some malformed or unknown information.
*
* @return <code>true</code> if the query could not be parsed correctly
* because it is either incomplete or malformed
*/
public boolean hasUnknownEndingStatement() {
return unknownEndingStatement != null &&
!unknownEndingStatement.isNull();
}
@Override
protected boolean isTolerant() {
return tolerant;
}
@Override
protected void parse(WordParser wordParser, boolean tolerant) {
// Skip leading whitespace
wordParser.skipLeadingWhitespace();
// Parse the query, which can be invalid/incomplete or complete and valid
// Make sure to use this statement if it's a JPQL fragment as well
if (tolerant || (queryBNFId != JPQLStatementBNF.ID)) {
// If the query BNF is not the "root" BNF, then we need to parse
// it with a broader check when parsing
if (queryBNFId == JPQLStatementBNF.ID) {
queryStatement = parseUsingExpressionFactory(wordParser, queryBNFId, tolerant);
}
else {
queryStatement = parse(wordParser, queryBNFId, tolerant);
}
int count = wordParser.skipLeadingWhitespace();
// The JPQL query is invalid or incomplete, the remaining will be added
// to the unknown ending statement
if ((queryStatement == null) || !wordParser.isTail()) {
wordParser.moveBackward(count);
unknownEndingStatement = buildUnknownExpression(wordParser.substring());
}
// The JPQL query has some ending whitespace, keep one (used by content assist)
else if (!wordParser.isTail() || (tolerant && (count > 0))) {
unknownEndingStatement = buildUnknownExpression(" ");
}
// The JPQL query or fragment is invalid
else if (queryStatement.isUnknown()) {
unknownEndingStatement = buildUnknownExpression(queryStatement.toParsedText());
queryStatement = null;
}
}
// Quickly parse the valid query
else {
switch (wordParser.character()) {
case 'd': case 'D': queryStatement = new DeleteStatement(this); break;
case 'u': case 'U': queryStatement = new UpdateStatement(this); break;
case 's': case 'S': queryStatement = new SelectStatement(this); break;
}
if (queryStatement != null) {
queryStatement.parse(wordParser, tolerant);
}
else {
queryStatement = parse(wordParser, queryBNFId, tolerant);
}
}
}
@Override
protected void toParsedText(StringBuilder writer, boolean actual) {
if (queryStatement != null) {
queryStatement.toParsedText(writer, actual);
}
if (unknownEndingStatement != null) {
unknownEndingStatement.toParsedText(writer, actual);
}
}
}