blob: 90cc2921e0b3e15ae23bd184f205159913fc9a59 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.ignite.springdata22.repository.query;
import java.util.HashSet;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import org.jetbrains.annotations.Nullable;
import org.springframework.data.util.Streamable;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import static java.util.regex.Pattern.CASE_INSENSITIVE;
import static java.util.regex.Pattern.DOTALL;
import static java.util.regex.Pattern.compile;
/**
* Simple utility class to create queries.
*
* @author Oliver Gierke
* @author Kevin Raymond
* @author Thomas Darimont
* @author Komi Innocent
* @author Christoph Strobl
* @author Mark Paluch
* @author Sébastien Péralta
* @author Jens Schauder
* @author Nils Borrmann
* @author Reda.Housni -Alaoui
*/
public abstract class QueryUtils {
/**
* The constant COUNT_QUERY_STRING.
*/
public static final String COUNT_QUERY_STRING = "select count(%s) from %s x";
/**
* The constant DELETE_ALL_QUERY_STRING.
*/
public static final String DELETE_ALL_QUERY_STRING = "delete from %s x";
/**
* Used Regex/Unicode categories (see http://www.unicode.org/reports/tr18/#General_Category_Property): Z Separator
* Cc Control Cf Format P Punctuation
*/
private static final String IDENTIFIER = "[._[\\P{Z}&&\\P{Cc}&&\\P{Cf}&&\\P{P}]]+";
/**
* The Colon no double colon.
*/
static final String COLON_NO_DOUBLE_COLON = "(?<![:\\\\]):";
/**
* The Identifier group.
*/
static final String IDENTIFIER_GROUP = String.format("(%s)", IDENTIFIER);
/** */
private static final String COUNT_REPLACEMENT_TEMPLATE = "select count(%s) $5$6$7";
/** */
private static final String SIMPLE_COUNT_VALUE = "$2";
/** */
private static final String COMPLEX_COUNT_VALUE = "$3$6";
/** */
private static final String ORDER_BY_PART = "(?iu)\\s+order\\s+by\\s+.*$";
/** */
private static final Pattern ALIAS_MATCH;
/** */
private static final Pattern COUNT_MATCH;
/** */
private static final Pattern PROJECTION_CLAUSE = Pattern
.compile("select\\s+(.+)\\s+from", Pattern.CASE_INSENSITIVE);
/** */
private static final String JOIN = "join\\s+(fetch\\s+)?" + IDENTIFIER + "\\s+(as\\s+)?" + IDENTIFIER_GROUP;
/** */
private static final Pattern JOIN_PATTERN = Pattern.compile(JOIN, Pattern.CASE_INSENSITIVE);
/** */
private static final String EQUALS_CONDITION_STRING = "%s.%s = :%s";
/** */
private static final Pattern NAMED_PARAMETER = Pattern.compile(
COLON_NO_DOUBLE_COLON + IDENTIFIER + "|\\#" + IDENTIFIER, CASE_INSENSITIVE);
/** */
private static final Pattern CONSTRUCTOR_EXPRESSION;
/** */
private static final int QUERY_JOIN_ALIAS_GROUP_INDEX = 3;
/** */
private static final int VARIABLE_NAME_GROUP_INDEX = 4;
/** */
private static final Pattern FUNCTION_PATTERN;
static {
StringBuilder builder = new StringBuilder();
builder.append("(?<=from)"); // from as starting delimiter
builder.append("(?:\\s)+"); // at least one space separating
builder.append(IDENTIFIER_GROUP); // Entity name, can be qualified (any
builder.append("(?:\\sas)*"); // exclude possible "as" keyword
builder.append("(?:\\s)+"); // at least one space separating
builder.append("(?!(?:where))(\\w+)"); // the actual alias
ALIAS_MATCH = compile(builder.toString(), CASE_INSENSITIVE);
builder = new StringBuilder();
builder.append("(select\\s+((distinct )?(.+?)?)\\s+)?(from\\s+");
builder.append(IDENTIFIER);
builder.append("(?:\\s+as)?\\s+)");
builder.append(IDENTIFIER_GROUP);
builder.append("(.*)");
COUNT_MATCH = compile(builder.toString(), CASE_INSENSITIVE);
builder = new StringBuilder();
builder.append("select");
builder.append("\\s+"); // at least one space separating
builder.append("(.*\\s+)?"); // anything in between (e.g. distinct) at least one space separating
builder.append("new");
builder.append("\\s+"); // at least one space separating
builder.append(IDENTIFIER);
builder.append("\\s*"); // zero to unlimited space separating
builder.append("\\(");
builder.append(".*");
builder.append("\\)");
CONSTRUCTOR_EXPRESSION = compile(builder.toString(), CASE_INSENSITIVE + DOTALL);
builder = new StringBuilder();
// any function call including parameters within the brackets
builder.append("\\w+\\s*\\([\\w\\.,\\s'=]+\\)");
// the potential alias
builder.append("\\s+[as|AS]+\\s+(([\\w\\.]+))");
FUNCTION_PATTERN = compile(builder.toString());
}
/**
* Private constructor to prevent instantiation.
*/
private QueryUtils() {
// No-op.
}
/**
* Returns the query string to execute an exists query for the given id attributes.
*
* @param entityName the name of the entity to create the query for, must not be {@literal null}.
* @param cntQryPlaceHolder the placeholder for the count clause, must not be {@literal null}.
* @param idAttrs the id attributes for the entity, must not be {@literal null}.
* @return the exists query string
*/
public static String getExistsQueryString(String entityName,
String cntQryPlaceHolder,
Iterable<String> idAttrs) {
String whereClause = Streamable.of(idAttrs).stream() //
.map(idAttribute -> String.format(EQUALS_CONDITION_STRING, "x", idAttribute,
idAttribute)) //
.collect(Collectors.joining(" AND ", " WHERE ", ""));
return String.format(COUNT_QUERY_STRING, cntQryPlaceHolder, entityName) + whereClause;
}
/**
* Returns the query string for the given class name.
*
* @param template must not be {@literal null}.
* @param entityName must not be {@literal null}.
* @return the template with placeholders replaced by the {@literal entityName}. Guaranteed to be not {@literal
* null}.
*/
public static String getQueryString(String template, String entityName) {
Assert.hasText(entityName, "Entity name must not be null or empty!");
return String.format(template, entityName);
}
/**
* Returns the aliases used for {@code left (outer) join}s.
*
* @param qry a query string to extract the aliases of joins from. Must not be {@literal null}.
* @return a {@literal Set} of aliases used in the query. Guaranteed to be not {@literal null}.
*/
static Set<String> getOuterJoinAliases(String qry) {
Set<String> result = new HashSet<>();
Matcher matcher = JOIN_PATTERN.matcher(qry);
while (matcher.find()) {
String alias = matcher.group(QUERY_JOIN_ALIAS_GROUP_INDEX);
if (StringUtils.hasText(alias))
result.add(alias);
}
return result;
}
/**
* Returns the aliases used for aggregate functions like {@code SUM, COUNT, ...}.
*
* @param qry a {@literal String} containing a query. Must not be {@literal null}.
* @return a {@literal Set} containing all found aliases. Guaranteed to be not {@literal null}.
*/
static Set<String> getFunctionAliases(String qry) {
Set<String> result = new HashSet<>();
Matcher matcher = FUNCTION_PATTERN.matcher(qry);
while (matcher.find()) {
String alias = matcher.group(1);
if (StringUtils.hasText(alias))
result.add(alias);
}
return result;
}
/**
* Resolves the alias for the entity to be retrieved from the given JPA query.
*
* @param qry must not be {@literal null}.
* @return Might return {@literal null}.
*/
@Nullable
static String detectAlias(String qry) {
Matcher matcher = ALIAS_MATCH.matcher(qry);
return matcher.find() ? matcher.group(2) : null;
}
/**
* Creates a count projected query from the given original query.
*
* @param originalQry must not be {@literal null}.
* @param cntProjection may be {@literal null}.
* @return a query String to be used a count query for pagination. Guaranteed to be not {@literal null}.
*/
static String createCountQueryFor(String originalQry, @Nullable String cntProjection) {
Assert.hasText(originalQry, "OriginalQuery must not be null or empty!");
Matcher matcher = COUNT_MATCH.matcher(originalQry);
String countQuery;
if (cntProjection == null) {
String variable = matcher.matches() ? matcher.group(VARIABLE_NAME_GROUP_INDEX) : null;
boolean useVariable = variable != null && StringUtils.hasText(variable) && !variable.startsWith("new")
&& !variable.startsWith("count(") && !variable.contains(",");
String replacement = useVariable ? SIMPLE_COUNT_VALUE : COMPLEX_COUNT_VALUE;
countQuery = matcher.replaceFirst(String.format(COUNT_REPLACEMENT_TEMPLATE, replacement));
}
else
countQuery = matcher.replaceFirst(String.format(COUNT_REPLACEMENT_TEMPLATE, cntProjection));
return countQuery.replaceFirst(ORDER_BY_PART, "");
}
/**
* Returns whether the given JPQL query contains a constructor expression.
*
* @param qry must not be {@literal null} or empty.
* @return boolean
*/
public static boolean hasConstructorExpression(String qry) {
Assert.hasText(qry, "Query must not be null or empty!");
return CONSTRUCTOR_EXPRESSION.matcher(qry).find();
}
/**
* Returns the projection part of the query, i.e. everything between {@code select} and {@code from}.
*
* @param qry must not be {@literal null} or empty.
* @return projection
*/
public static String getProjection(String qry) {
Assert.hasText(qry, "Query must not be null or empty!");
Matcher matcher = PROJECTION_CLAUSE.matcher(qry);
String projection = matcher.find() ? matcher.group(1) : "";
return projection.trim();
}
}