/******************************************************************************* | |
* Copyright (c) 2006, 2013 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 v1.0 and Eclipse Distribution License v. 1.0 | |
* which accompanies this distribution. | |
* The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html | |
* and the Eclipse Distribution License is available at | |
* http://www.eclipse.org/org/documents/edl-v10.php. | |
* | |
* 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.5 | |
* @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 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 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 Pattern INTEGER_REGEXP = Pattern.compile("^[-+]?[0-9]+$"); | |
/** | |
* The {@link Pattern} representing the regular expression of a numerical value as a long. | |
*/ | |
public static 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><IDENTIFIER>(</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 position 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 | |
* @return 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); | |
} | |
} |