blob: a3b9d1bdc82edc16b12264b9b5c964d8e3445c20 [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.tests.jpql.parser;
import org.eclipse.persistence.jpa.jpql.WordParser;
import org.eclipse.persistence.jpa.jpql.parser.*;
import org.eclipse.persistence.jpa.jpql.tools.model.IJPQLQueryFormatter.IdentifierStyle;
import static org.eclipse.persistence.jpa.jpql.parser.AbstractExpression.*;
import static org.eclipse.persistence.jpa.jpql.parser.Expression.*;
import static org.junit.Assert.*;
/**
* This builder creates the parsed tree representation of a JPQL query and verifies the generated
* strings ({@link org.eclipse.persistence.jpa.jpql.parser.Expression#toActualText()
* Expression.toActualText()} and {@link org.eclipse.persistence.jpa.jpql.parser.Expression#toParsedText()
* Expression.toParsedText()}) were generated correctly.
*
* @version 2.4
* @since 2.3
* @author Pascal Filion
*/
@SuppressWarnings({"nls", "unused" /* For the extra import statement, see bug 330740 */})
public final class JPQLQueryBuilder {
/**
* Parses the given JPQL query and tests its generated string with the given query, which will be
* formatted first.
*
* @param jpqlQuery The JPQL query to parse into a parsed tree
* @param jpqlGrammar The JPQL grammar that defines how to parse the given JPQL query
* @param tolerant Determines if the parsing system should be tolerant, meaning if it should try
* to parse invalid or incomplete queries
* @return The parsed tree representation of the given JPQL query
*/
public static JPQLExpression buildQuery(String jpqlQuery, JPQLGrammar jpqlGrammar, boolean tolerant) {
return buildQuery(jpqlQuery, jpqlGrammar, JPQLQueryStringFormatter.DEFAULT, tolerant);
}
/**
* Parses the given JPQL query and tests its generated string with the given query, which will be
* formatted first.
*
* @param jpqlQuery The JPQL query to parse into a parsed tree
* @param jpqlGrammar The JPQL grammar that defines how to parse the given JPQL query
* @param formatter This formatter is used to personalized the formatting of the JPQL query
* before it is used to test the generated string
* @param tolerant Determines if the parsing system should be tolerant, meaning if it should try
* to parse invalid or incomplete queries
* @return The parsed tree representation of the given JPQL query
*/
public static JPQLExpression buildQuery(String jpqlQuery,
JPQLGrammar jpqlGrammar,
JPQLQueryStringFormatter formatter,
boolean tolerant) {
return buildQuery(jpqlQuery, jpqlGrammar, JPQLStatementBNF.ID, formatter, tolerant);
}
/**
* Parses the given JPQL query and tests its generated string with the given query, which will be
* formatted first.
*
* @param jpqlQuery The JPQL query to parse into a parsed tree
* @param jpqlGrammar The JPQL grammar that defines how to parse the given JPQL query
* @param jpqlQueryBNFId The unique identifier of the {@link JPQLQueryBNF JPQLQueryBNF}
* @param tolerant Determines if the parsing system should be tolerant, meaning if it should try
* to parse invalid or incomplete queries
* @return The parsed tree representation of the given JPQL query
*/
public static JPQLExpression buildQuery(String jpqlQuery,
JPQLGrammar jpqlGrammar,
String jpqlQueryBNFId,
boolean tolerant) {
return buildQuery(jpqlQuery, jpqlGrammar, jpqlQueryBNFId, JPQLQueryStringFormatter.DEFAULT, tolerant);
}
/**
* Parses the given JPQL query and tests its generated string with the given query, which will be
* formatted first. Both the parsed and actual generated strings will be tested.
*
* @param jpqlQuery The JPQL query to parse into a parsed tree
* @param jpqlGrammar The JPQL grammar that defines how to parse the given JPQL query
* @param jpqlQueryBNFId The unique identifier of the {@link JPQLQueryBNF JPQLQueryBNF}
* @param formatter This formatter is used to personalized the formatting of the JPQL query
* before it is used to test the generated string
* @param tolerant Determines if the parsing system should be tolerant, meaning if it should try
* to parse invalid or incomplete queries
* @return The parsed tree representation of the given JPQL query
*/
public static JPQLExpression buildQuery(String jpqlQuery,
JPQLGrammar jpqlGrammar,
String jpqlQueryBNFId,
JPQLQueryStringFormatter formatter,
boolean tolerant) {
// Format the JPQL query to reflect how the parsed tree outputs the query back as a string
String parsedJPQLQuery = toParsedText(jpqlQuery, jpqlGrammar);
String actualJPQLQuery = toActualText(jpqlQuery, jpqlGrammar);
// Format the JPQL query with this formatter so the invoker can tweak the default formatting
parsedJPQLQuery = formatter.format(parsedJPQLQuery);
actualJPQLQuery = formatter.format(actualJPQLQuery);
// Parse the JPQL query
JPQLExpression jpqlExpression = new JPQLExpression(jpqlQuery, jpqlGrammar, jpqlQueryBNFId, tolerant);
// Make sure the JPQL query was correctly parsed and the
// generated string matches the original JPQL query
assertEquals(parsedJPQLQuery, jpqlExpression.toParsedText());
assertEquals(actualJPQLQuery, jpqlExpression.toActualText());
// If the JPQL query is parsed with tolerance turned off, then the query should
// be completely parsed and there should not be any unknown ending statement
if (!tolerant && (jpqlQueryBNFId == JPQLStatementBNF.ID)) {
assertFalse(
"A valid JPQL query cannot have an unknown ending fragment:" + jpqlQueryBNFId,
jpqlExpression.hasUnknownEndingStatement()
);
}
return jpqlExpression;
}
/**
* Formats the given JPQL query by converting it to what
* {@link Expression#toActualText() Expression.toActualText()} would return.
* <p>
* For instance, "Select e From Employee e" will be converted to "Select e From Employee e".
*
* @param jpqlQuery The string to format
* @param jpqlGrammar The {@link JPQLGrammar} is used to properly format the string
* @return The converted string
* @see #toParsedText(String, JPQLGrammar)
* @see #toText(String, JPQLGrammar, boolean, IdentifierStyle)
*/
public static String toActualText(String jpqlQuery, JPQLGrammar jpqlGrammar) {
return toText(jpqlQuery, jpqlGrammar, true, IdentifierStyle.UPPERCASE);
}
/**
* Formats the given JPQL query by converting it to what
* {@link Expression#toParsedText() Expression.toParsedText()} would return.
* <p>
* For instance, "Select e From Employee e" will be converted to "SELECT e FROM Employee e".
*
* @param jpqlQuery The string to format
* @param jpqlGrammar The {@link JPQLGrammar} is used to properly format the string
* @return The formatted JPQL query
* @see #toActualText(String, JPQLGrammar)
* @see #toText(String, JPQLGrammar, boolean, IdentifierStyle)
*/
public static String toParsedText(String jpqlQuery, JPQLGrammar jpqlGrammar) {
return toText(jpqlQuery, jpqlGrammar, false, IdentifierStyle.UPPERCASE);
}
/**
* Formats the given JPQL query by replacing multiple whitespace with a single whitespace. The
* JPQL identifiers will be converted to uppercase if <em>exactMatch</em> is <code>true</code>
* otherwise they will remain unchanged.
*
* @param jpqlQuery The string to format
* @param jpqlGrammar The {@link JPQLGrammar} is used to properly format the string
* @return The formatted JPQL query
* @see #toActualText(String, JPQLGrammar)
* @see #toParsedText(String, JPQLGrammar)
*/
public static String toText(String jpqlQuery,
JPQLGrammar jpqlGrammar,
boolean exactMatch,
IdentifierStyle style) {
ExpressionRegistry registry = jpqlGrammar.getExpressionRegistry();
StringBuilder sb = new StringBuilder();
WordParser wordParser = new WordParser(jpqlQuery);
boolean singleQuoteParsed = false;
Boolean fromClause = null;
int whitespaceParsed = 0;
for (int index = 0, count = jpqlQuery.length(); index < count; index++) {
char character = jpqlQuery.charAt(index);
// '
if (character == SINGLE_QUOTE) {
// Entering string literal
if (!singleQuoteParsed) {
singleQuoteParsed = true;
}
else {
// Make sure the single quote is not escaped
char nextCharacter = (index + 1 < count) ? jpqlQuery.charAt(index + 1) : '\0';
// Exiting the string literal
if (nextCharacter != SINGLE_QUOTE) {
singleQuoteParsed = false;
}
// Skip the escaped '
else {
sb.append(character);
index++;
}
}
whitespaceParsed = 0;
}
// Anything outside of string literal
else if (!singleQuoteParsed) {
// Will skip whitespace after the first one but not inside string literals
if (Character.isWhitespace(character)) {
// Leading whitespace is always removed
if (sb.length() == 0) {
continue;
}
// Make sure the whitespace is a real space
character = SPACE;
// '( ' will always be converted to '('
char previousCharacter = (index > 0) ? jpqlQuery.charAt(index - 1) : '\0';
if (previousCharacter == LEFT_PARENTHESIS) {
// Skip any subsequent whitespace
while (index < count) {
previousCharacter = jpqlQuery.charAt(index + 1);
if (!Character.isWhitespace(previousCharacter)) {
break;
}
index++;
}
// Function without a closing parenthesis should have a whitespace after (
// Example: "... ABS( FROM Employee e"
if (!wordParser.startsWithIdentifier(FROM, index + 1) &&
!wordParser.startsWithIdentifier(WHERE, index + 1) &&
!wordParser.startsWithIdentifier(GROUP_BY, index + 1) &&
!wordParser.startsWithIdentifier(ORDER_BY, index + 1)) {
continue;
}
}
whitespaceParsed++;
// Capitalize JPQL identifiers
if (!exactMatch && (whitespaceParsed == 1)) {
String identifier = wordParser.partialWord(index);
int length = identifier.length();
if ((length > 0) && registry.isIdentifier(identifier)) {
// The word "ORDER"/"GROUP" is not the entity name but the identifier ORDER BY
if ((fromClause == Boolean.TRUE) &&
(identifier.equalsIgnoreCase("ORDER") ||
identifier.equalsIgnoreCase("GROUP")) &&
(wordParser.startsWithIgnoreCase(ORDER_BY, index - 5) ||
wordParser.startsWithIgnoreCase(GROUP_BY, index - 5))) {
fromClause = null;
}
// Special case where Order/Group should not be capitalized when it's the entity name
if (!((fromClause == Boolean.TRUE) &&
(identifier.equalsIgnoreCase("ORDER") ||
identifier.equalsIgnoreCase("GROUP")))) {
identifier = style.formatIdentifier(identifier);
int offset = sb.length();
sb.replace(offset - length, offset, identifier);
}
// The FROM clause should be parsed soon
if ((fromClause == null) && SELECT.equalsIgnoreCase(identifier)) {
fromClause = Boolean.FALSE;
}
// Entering the FROM clause
else if ((fromClause == Boolean.FALSE) && FROM.equalsIgnoreCase(identifier)) {
fromClause = Boolean.TRUE;
}
// Exiting the FROM clause
else if ((fromClause == Boolean.TRUE) &&
(WHERE .equalsIgnoreCase(identifier) ||
HAVING .equalsIgnoreCase(identifier) ||
"ORDER".equalsIgnoreCase(identifier) ||
"GROUP".equalsIgnoreCase(identifier))) {
fromClause = null;
}
}
}
// Skip any subsequent whitespace
if (whitespaceParsed > 1) {
continue;
}
}
// '('
else if (character == LEFT_PARENTHESIS) {
String previousWord = wordParser.partialWord(index - whitespaceParsed);
// Remove the previous character, which is a whitespace and only if
// it's after a function like "ABS (", which will be converted to "ABS("
// but "WHERE (" will remain "WHERE (" (same with NEW)
if (!NEW.equalsIgnoreCase(previousWord)) {
if ((whitespaceParsed > 0) && !previousWord.equalsIgnoreCase(NEW)) {
IdentifierRole role = registry.getIdentifierRole(previousWord);
if ((role == IdentifierRole.FUNCTION) || IN.equalsIgnoreCase(previousWord)) {
int offset = sb.length();
sb.delete(offset - 1, offset);
}
}
else {
int length = previousWord.length();
// Capitalize JPQL identifiers
if (!exactMatch && (length > 0) && registry.isIdentifier(previousWord)) {
previousWord = style.formatIdentifier(previousWord);
int offset = sb.length();
sb.replace(offset - length, offset, previousWord);
}
}
}
whitespaceParsed = 0;
}
// ')'
// ','
// Remove any whitespace before ')' or ','
else if (character == RIGHT_PARENTHESIS ||
character == COMMA) {
if (whitespaceParsed > 0) {
int offset = sb.length();
sb.delete(offset - 1, offset);
}
// Capitalize JPQL identifiers
else if (!exactMatch) {
String identifier = wordParser.partialWord(index);
int length = identifier.length();
if ((length > 0) && registry.isIdentifier(identifier)) {
identifier = style.formatIdentifier(identifier);
int offset = sb.length();
sb.replace(offset - length, offset, identifier);
}
}
// Add a whitespace after ','
if ((character == COMMA) && (index + 1 < count)) {
char nextCharacter = jpqlQuery.charAt(index + 1);
// But not if the next character is ')' or ','
if ((nextCharacter != COMMA) &&
(nextCharacter != RIGHT_PARENTHESIS) &&
!Character.isWhitespace(nextCharacter)) {
sb.append(character);
character = SPACE;
}
}
whitespaceParsed = 0;
}
// Add a whitespace before and after *, /
else if ((character == '*') ||
(character == '/')) {
// Add a whitespace before
if (whitespaceParsed == 0) {
sb.append(' ');
}
// Add a whitespace after
if (index + 1 < count) {
char nextCharacter = jpqlQuery.charAt(index + 1);
if (!Character.isWhitespace(nextCharacter)) {
sb.append(character);
character = SPACE;
}
}
whitespaceParsed = 0;
}
// !=
else if (character == '!') {
if (index + 1 < count) {
char nextCharacter = jpqlQuery.charAt(index + 1);
// != needs to have whitespace around it
if ((nextCharacter == '=') && (whitespaceParsed == 0)) {
sb.append(' ');
}
}
whitespaceParsed = 0;
}
// Add a whitespace before and after <, <=, =, >=, >, <>, !=
else if ((character == '=') ||
(character == '<') ||
(character == '>')) {
char previousCharacter = jpqlQuery.charAt(index - 1);
if (character == '=' &&
previousCharacter == '!') {
// Don't do anything for !=
}
// Add a whitespace before
else if ((previousCharacter != '>') &&
(previousCharacter != '<') &&
(whitespaceParsed == 0)) {
sb.append(' ');
}
// Add a whitespace after
if (index + 1 < count) {
char nextCharacter = jpqlQuery.charAt(index + 1);
// Don't add a whitespace if it's <=, >=, <>, !=
if ((nextCharacter != '=') &&
(nextCharacter != '>') &&
!Character.isWhitespace(nextCharacter)) {
sb.append(character);
character = SPACE;
}
}
whitespaceParsed = 0;
}
else {
whitespaceParsed = 0;
}
}
sb.append(character);
// At the end of the query, make sure the last JPQL identifier is capitalized if required
if (!exactMatch &&
(index + 1 == count) &&
(character != SINGLE_QUOTE) &&
(whitespaceParsed == 0)) {
String previousWord = wordParser.partialWord(index + 1);
int length = previousWord.length();
// Capitalize JPQL identifiers
if ((length > 0) && registry.isIdentifier(previousWord)) {
previousWord = style.formatIdentifier(previousWord);
int offset = sb.length();
sb.replace(offset - length, offset, previousWord);
}
}
}
return sb.toString();
}
}