| /* |
| * 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(); |
| } |
| } |