blob: 49fb04c6da288adc996a2c8075ca68878765e008 [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;
import java.util.regex.Pattern;
/**
* A utility class containing various methods related to the Hermes parser.
* <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.
*
* @version 2.6
* @since 2.3
* @author Pascal Filion
*/
@SuppressWarnings("nls")
public final class ExpressionTools {
/**
* The {@link Pattern} representing the regular expression of a numerical value as a double.
*/
public static final Pattern DOUBLE_REGEXP = Pattern.compile("^[-+]?[0-9]*(\\.[0-9]+)?([dD]|([eE][-+]?[0-9]+))?$");
/**
* The constant of an empty array.
*/
public static final Object[] EMPTY_ARRAY = new Object[0];
/**
* The constant for an empty string.
*/
public static final String EMPTY_STRING = "";
/**
* The constant of an empty String array.
*/
public static final String[] EMPTY_STRING_ARRAY = new String[0];
/**
* The {@link Pattern} representing the regular expression of a numerical value as a float.
*/
public static final Pattern FLOAT_REGEXP = Pattern.compile("^[-+]?[0-9]*(\\.[0-9]+)?[fF]$");
/**
* The {@link Pattern} representing the regular expression of a numerical value as an integer.
*/
public static final Pattern INTEGER_REGEXP = Pattern.compile("^[-+]?[0-9]+$");
/**
* The {@link Pattern} representing the regular expression of a numerical value as a long.
*/
public static final Pattern LONG_REGEXP = Pattern.compile("^[-+]?[0-9]+[lL]?$");
/**
* ExpressionTools cannot be instantiated.
*/
private ExpressionTools() {
super();
throw new IllegalAccessError("ExpressionTools cannot be instantiated");
}
/**
* Converts the escape characters contained in the given {@link CharSequence} to their literal
* representation. For example, '\b' is converted to '\\b'.
*
* @param value The sequence of characters to convert any escape character
* @param position This is a one element array that needs to be adjusted when an escape
* character is converted
* @return The new sequence of characters that does not contain any escape character but it's
* literal representation
*/
public static String escape(CharSequence value, int[] position) {
StringBuilder sb = new StringBuilder(value.length());
int originalPosition = position[0];
for (int index = 0, count = value.length(); index < count; index++) {
char character = value.charAt(index);
switch (character) {
case '\b':
sb.append("\\b");
if (index < originalPosition) position[0]++;
break;
case '\t':
sb.append("\\t");
if (index < originalPosition) position[0]++;
break;
case '\n':
sb.append("\\n");
if (index < originalPosition) position[0]++;
break;
case '\f':
sb.append("\\f");
if (index < originalPosition) position[0]++;
break;
case '\r':
sb.append("\\r");
if (index < originalPosition) position[0]++;
break;
case '\"':
sb.append("\\\"");
if (index < originalPosition) position[0]++;
break;
case '\\':
sb.append("\\\\");
if (index < originalPosition) position[0]++;
break;
case '\0':
sb.append("\\0");
if (index < originalPosition) position[0]++;
break;
case '\1':
sb.append("\\1");
if (index < originalPosition) position[0]++;
break;
case '\2':
sb.append("\\2");
if (index < originalPosition) position[0]++;
break;
case '\3':
sb.append("\\3");
if (index < originalPosition) position[0]++;
break;
case '\4':
sb.append("\\4");
if (index < originalPosition) position[0]++;
break;
case '\5':
sb.append("\\5");
if (index < originalPosition) position[0]++;
break;
case '\6':
sb.append("\\6");
if (index < originalPosition) position[0]++;
break;
case '\7':
sb.append("\\7");
if (index < originalPosition) position[0]++;
break;
default:
sb.append(character);
}
}
return sb.toString();
}
/**
* Determines whether the JPQL fragment is an expression of the form <code>&lt;IDENTIFIER&gt;(</code>.
*
* @param wordParser The text to parse based on the current position of the cursor
* @param identifier The identifier to verify if it's for an expression or for possibly for a
* variable name
* @return <code>true</code> if the identifier is followed by '('; <code>false</code> otherwise
*/
public static boolean isFunctionExpression(WordParser wordParser, String identifier) {
// Skip the identifier
int count = identifier.length();
wordParser.moveForward(identifier);
// Check to see if ( is following the identifier
int whitespace = wordParser.skipLeadingWhitespace();
boolean function = wordParser.startsWith('(');
// Revert the changes
wordParser.moveBackward(count + whitespace);
return function;
}
/**
* Determines whether the given character should be escaped when being part of a string, i.e.
* '\r' should be "\\r".
*
* @param character The character to check if it can directly be used in a string or should be escaped
* @return <code>true</code> if the given character cannot be used directly in a string
* @since 2.5
*/
public static boolean isJavaEscapedCharacter(char character) {
switch (character) {
case '\b':
case '\t':
case '\n':
case '\f':
case '\r':
case '\"':
case '\\':
case '\0':
case '\1':
case '\2':
case '\3':
case '\4':
case '\5':
case '\6':
case '\7': return true;
default: return false;
}
}
/**
* Determines whether the given character is the character used to identify an input parameter,
* either a named parameter or position parameter.
*
* @param character The character to check if it's a parameter
* @return <code>true</code> if the given character is either : or ?; <code>false</code> otherwise
*/
public static boolean isParameter(char character) {
return character == ':' ||
character == '?';
}
/**
* Determines whether the given character is the single or double quote.
*
* @param character The character to check if it's a quote
* @return <code>true</code> if the given character is either ' or "; <code>false</code> otherwise
*/
public static boolean isQuote(char character) {
return character == '\'' ||
character == '\"';
}
/**
* Returns the given string literal by wrapping it with single quotes. Any single quote within
* the string will be escaped with another single quote.
*
* @param text The original text to quote
* @return The quoted text
* @since 2.4
*/
public static String quote(String text) {
text = text.replace("'", "''");
text = "'" + text + "'";
return text;
}
/**
* Adjusts the positions contained by the given array of either one or two elements, which is
* based on the given <em>query1</em>, by translating those positions to be at the equivalent
* position within <em>query2</em>.
* <p>
* <b>Important:</b> The two JPQL queries should contain the same characters but the amount of
* whitespace can differ. One cannot have the escape characters and the other one has the Unicode
* characters.
*
* @param query1 The query where the positions are pointing
* @param positions An array of either one or two elements positioned within <em>query1</em>
* @param query2 The query for which the positions might need adjustment
* @since 2.5
*/
public static void reposition(CharSequence query1, int[] positions, CharSequence query2) {
// Nothing to adjust
if ((positions[0] <= 0) && (positions.length == 1)) {
return;
}
int queryLength1 = query1.length();
int queryLength2 = query2.length();
// Queries 1 and 2 have the same length, no translation required
if (queryLength1 == queryLength2) {
return;
}
int index1 = 0;
int index2 = 0;
boolean position1Done = false;
boolean position2Done = false;
while ((index1 < queryLength1) && (index2 < queryLength2) && (!position1Done || !position2Done)) {
char character1 = Character.toUpperCase(query1.charAt(index1));
char character2 = Character.toUpperCase(query2.charAt(index2));
boolean whitespace1 = Character.isWhitespace(character1);
boolean whitespace2 = Character.isWhitespace(character2);
if (character1 != character2) {
// Query 2 does not have a whitespace but query 1 does
if (whitespace1 && !whitespace2) {
index1++;
if (!position1Done) {
positions[0]--;
}
if (!position2Done && (positions.length > 1)) {
positions[1]--;
}
}
// Query 1 does not have a whitespace but the query 2 does
else if (!whitespace1 && whitespace2) {
index2++;
if (!position1Done) {
positions[0]++;
}
if (!position2Done && (positions.length > 1)) {
positions[1]++;
}
}
// Continue with the next character
else {
index1++;
index2++;
}
}
// Continue with the next character
else {
index1++;
index2++;
}
// Check to see if the position has been fully updated
if ((!whitespace1 && !whitespace2) || (queryLength1 > queryLength2)) {
if (positions[0] <= index2) {
position1Done = true;
}
if ((positions.length > 1) && positions[1] <= index2) {
position2Done = true;
}
}
}
// Make sure the positions are not outside of length of query2
positions[0] = Math.min(positions[0], queryLength2);
positions[0] = Math.max(positions[0], 0);
if (positions.length > 1) {
positions[1] = Math.min(positions[1], queryLength2);
positions[1] = Math.max(positions[1], 0);
}
}
/**
* Adjusts the position, which is based on the given <em>query1</em>, by translating it to be at
* the equivalent position within <em>query2</em>.
* <p>
* <b>Important:</b> The two JPQL queries should contain the same characters but the amount of
* whitespace can differ. One cannot have the escape characters and the other one has the Unicode
* characters.
*
* @param query1 The query where the positions are pointing
* @param position1 The position of a cursor within <em>query1</em>
* @param query2 The query for which the position might need adjustment
* @return The adjusted position by moving it based on the difference between <em>query1</em> and
* <em>query2</em>
* @see #reposition(CharSequence, int[], CharSequence)
*/
public static int repositionCursor(CharSequence query1, int position1, CharSequence query2) {
int[] positions = { position1, position1 };
reposition(query1, positions, query2);
return positions[0];
}
/**
* Re-adjusts the given positions, which is based on the non-escaped version of the given
* <em>query</em>, by making sure it is pointing at the same position within <em>query</em>,
* which contains references (escape characters).
* <p>
* The escape characters are either \b, \t, \n, \f, \r, \", \' and \0 through \7.
* <p>
* <b>Important:</b> The given query should contain the exact same amount of whitespace than the
* query used to calculate the given positions.
*
* @param query The query that may contain escape characters
* @param positions The position within the non-escaped version of the given query, which is
* either a single element position or two positions that is used as a text range. After execution
* contains the adjusted positions by moving it based on the difference between the escape and
* non-escaped versions of the query
* @since 2.5
*/
public static void repositionJava(CharSequence query, int[] positions) {
if ((query == null) || (query.length() == 0)) {
return;
}
StringBuilder sb = new StringBuilder(query);
for (int index = 0, count = sb.length(); index < count; index++) {
char character = sb.charAt(index);
if (isJavaEscapedCharacter(character)) {
// Translate both positions because the special
// character is written with its escape character
if (index < positions[0]) {
positions[0]++;
positions[1]++;
}
// Only translate the end position because the start
// position is before the current index
else if (index < positions[1]) {
positions[1]++;
}
}
}
}
/**
* Determines whether the given string starts with the given prefix and ignores the case. If the
* prefix is <code>null</code> or an empty string, then <code>true</code> is always returned.
*
* @param string The string to check if its beginning matches the prefix
* @param prefix The prefix used to test matching the beginning of the sequence of characters
* @return <code>true</code> if the given string begins with the given prefix and ignores the
* case; <code>false</code> otherwise
*
* @since 2.5
*/
public static boolean startWithIgnoreCase(String string, String prefix) {
if (stringIsEmpty(prefix)) {
return true;
}
return string.regionMatches(true, 0, prefix, 0, prefix.length());
}
/**
* Determines whether the specified string is <code>null</code>, empty, or contains only
* whitespace characters.
*
* @param text The sequence of character to test if it is <code>null</code> or only contains
* whitespace
* @return <code>true</code> if the given string is <code>null</code> or only contains whitespace;
* <code>false</code> otherwise
*/
public static boolean stringIsEmpty(CharSequence text) {
if ((text == null) || (text.length() == 0)) {
return true;
}
for (int i = text.length(); i-- > 0;) {
if (!Character.isWhitespace(text.charAt(i))) {
return false;
}
}
return true;
}
/**
* Determines whether the specified string is NOT <code>null</code>, NOT empty, or contains at
* least one non-whitespace character.
*
* @param text The sequence of character to test if it is NOT <code>null</code> or does not only
* contain whitespace
* @return <code>true</code> if the given string is NOT <code>null</code> or has at least one
* non-whitespace character; <code>false</code> otherwise
*/
public static boolean stringIsNotEmpty(CharSequence text) {
return !stringIsEmpty(text);
}
/**
* Determines whether the two sequence of characters are different, with the appropriate
* <code>null</code> checks and the case is ignored.
*
* @param value1 The first value to check for equality and equivalency
* @param value2 The second value to check for equality and equivalency
* @return <code>true</code> if both values are different; <code>false</code> otherwise
*/
public static boolean stringsAreDifferentIgnoreCase(CharSequence value1, CharSequence value2) {
return !stringsAreEqualIgnoreCase(value1, value2);
}
/**
* Determines whether the two sequence of characters are equal or equivalent, with the
* appropriate <code>null</code> checks and the case is ignored.
*
* @param value1 The first value to check for equality and equivalency
* @param value2 The second value to check for equality and equivalency
* @return <code>true</code> if both values are <code>null</code>, equal or equivalent;
* <code>false</code> otherwise
*/
public static boolean stringsAreEqualIgnoreCase(CharSequence value1, CharSequence value2) {
// Both are equal or both are null
if ((value1 == value2) || (value1 == null) && (value2 == null)) {
return true;
}
// One is null but the other is not
if ((value1 == null) || (value2 == null)) {
return false;
}
return value1.toString().equalsIgnoreCase(value2.toString());
}
/**
* Converts the string representation of the escape characters contained by the given {@link
* CharSequence} into the actual escape characters. For example, the string '\\b' is converted
* into the character value '\b'.
*
* @param value The sequence of characters to convert to an escaped version
* @param position This is a one element array that needs to be adjusted when an escape
* character is converted
* @return The new sequence of characters that contains escape characters rather than their
* string representation
*/
public static String unescape(CharSequence value, int[] position) {
StringBuilder sb = new StringBuilder(value.length());
int originalPosition = position[0];
for (int index = 0, count = value.length(); index < count; index++) {
char character = value.charAt(index);
if ((character == '\\') && (index + 1 < count)) {
character = value.charAt(++index);
switch (character) {
// Standard escape character
case 'b': sb.append("\b"); if (index <= originalPosition) position[0]--; break;
case 't': sb.append("\t"); if (index <= originalPosition) position[0]--; break;
case 'n': sb.append("\n"); if (index <= originalPosition) position[0]--; break;
case 'f': sb.append("\f"); if (index <= originalPosition) position[0]--; break;
case 'r': sb.append("\r"); if (index <= originalPosition) position[0]--; break;
case '"': sb.append("\""); if (index <= originalPosition) position[0]--; break;
case '\\': sb.append("\\"); if (index <= originalPosition) position[0]--; break;
case '0': sb.append("\0"); if (index <= originalPosition) position[0]--; break;
case '1': sb.append("\1"); if (index <= originalPosition) position[0]--; break;
case '2': sb.append("\2"); if (index <= originalPosition) position[0]--; break;
case '3': sb.append("\3"); if (index <= originalPosition) position[0]--; break;
case '4': sb.append("\4"); if (index <= originalPosition) position[0]--; break;
case '5': sb.append("\5"); if (index <= originalPosition) position[0]--; break;
case '6': sb.append("\6"); if (index <= originalPosition) position[0]--; break;
case '7': sb.append("\7"); if (index <= originalPosition) position[0]--; break;
// Unicode
case 'u': {
// Convert the hexadecimal digit into a char
String hexadecimals = value.subSequence(index + 1, index + 5).toString();
char unicode = (char) Integer.parseInt(hexadecimals, 16);
sb.append(unicode);
// Adjust the position and make sure if the position is within the unicode
// value, then it's only adjusted to be at the beginning of the unicode value
if ((originalPosition > index - 1) && (originalPosition <= index + 5)) {
position[0] -= (originalPosition - index + 1);
}
else if (index <= originalPosition) {
position[0] -= 5;
}
index += 4;
break;
}
// Non-escape character
default: {
sb.append(character);
}
}
}
else {
sb.append(character);
}
}
return sb.toString();
}
/**
* Returns the string literal without the single or double quotes. Any two consecutive single
* quotes will be converted into a single quote.
*
* @param text The original text to unquote if it has ' at the beginning and the end
* @return The unquoted text
*/
public static String unquote(String text) {
// Nothing to unquote
if ((text == null) || (text.length() == 0)) {
return text;
}
int startIndex = 0;
int endIndex = text.length();
// Skip the leading single quote
if (isQuote(text.charAt(0))) {
startIndex = 1;
}
// Skip the trailing single quote
if ((endIndex - 1 >= startIndex) && isQuote(text.charAt(endIndex - 1))) {
endIndex--;
}
text = text.substring(startIndex, endIndex);
text = text.replace("''", "'");
return text;
}
/**
* Determines whether the values are different, with the appropriate <code>null</code> checks.
*
* @param value1 The first value to check for equality and equivalency
* @param value2 The second value to check for equality and equivalency
* @return <code>true</code> if both values are different; <code>true</code> if they are both
* <code>null</code>, equal or equivalent
*/
public static boolean valuesAreDifferent(Object value1, Object value2) {
return !valuesAreEqual(value1, value2);
}
/**
* Determines whether the values are equal or equivalent, with the appropriate <code>null</code>
* checks.
*
* @param value1 The first value to check for equality and equivalency
* @param value2 The second value to check for equality and equivalency
* @return <code>true</code> if both values are <code>null</code>, equal or equivalent;
* <code>false</code> otherwise
*/
public static boolean valuesAreEqual(Object value1, Object value2) {
// Both are equal or both are null
if ((value1 == value2) || (value1 == null) && (value2 == null)) {
return true;
}
// One is null but the other is not
if ((value1 == null) || (value2 == null)) {
return false;
}
return value1.equals(value2);
}
}