
org.bitbucket.brunneng.qb.QueryBuilder Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of query-builder Show documentation
Show all versions of query-builder Show documentation
Compact tool for building SQL, JPA or hibernate queries.
Supports utilities for spring pagination and sorting.
Supports extending to other types of query languages.
Has no transitive dependencies - use only what you need.
The newest version!
package org.bitbucket.brunneng.qb;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* The most general query builder.
* Builds query by multiple invocations of different
* append... methods.
* Default parameter prefix is ":", however it can be changed in subclasses.
* Supports generic parameter placeholder ":v", which will be replaced by meaningful name.
* Possible collisions of parameter names is automatically resolved by adding incrementing counter as suffix.
*/
public class QueryBuilder {
private final static String DEFAULT_PARAMETER_PREFIX = ":";
private final static String DEFAULT_PARAMETER_SHORT_NAME = "v";
private static final Pattern INCREMENT_NAME_PATTERN = Pattern.compile("[0-9]$");
private final List operators = new ArrayList<>();
protected boolean ignoreCaseOfOperators = true;
protected final StringBuilder query = new StringBuilder();
private final Map parameters = new LinkedHashMap<>();
private String parameterPrefix = DEFAULT_PARAMETER_PREFIX;
private String parameterShortName = DEFAULT_PARAMETER_SHORT_NAME;
private boolean autoAddSpaceBetweenQueryParts = true;
protected String identifierPattern = "[A-Za-z][A-Za-z0-9_]*";
public void setParameterPrefix(String parameterPrefix) {
this.parameterPrefix = parameterPrefix;
}
public void setParameterShortName(String parameterShortName) {
this.parameterShortName = parameterShortName;
}
/**
* Appends the queryPart without any additional checks.
* @param queryPart part of query to append
* @return this query builder
*/
public QueryBuilder append(String queryPart) {
queryPart = preprocessQueryPart(queryPart);
queryPart = tryFrameWithSpacesIfNeeded(queryPart);
query.append(queryPart);
return this;
}
protected void registerOperator(String operator) {
operators.add(operator);
}
/**
* @return the current query
*/
public String getQuery() {
postprocessQueryBeforeGet(query);
return query.toString();
}
protected void postprocessQueryBeforeGet(StringBuilder query) {
}
/**
* @return the current parameters
*/
public Map getParameters() {
return Collections.unmodifiableMap(parameters);
}
/**
* Appends the queryPart with corresponding value if the value is not null. Otherwise - do nothing.
* @param queryPart the part of query to be appended
* @param value the value of parameter, which is present in the query part
* @return the same query builder
*/
public QueryBuilder append(String queryPart, Object value) {
if (value != null) {
appendInternal(queryPart, value);
}
return this;
}
/**
* Appends the *queryPart* with corresponding collection value if the collection is not empty.
* Otherwise - do nothing.
* @param queryPart the part of query to be appended
* @param value the value of parameter, which is present in the query part
* @return the same query builder
*/
public QueryBuilder appendIfNotEmpty(String queryPart, Collection> value) {
if (value != null && !value.isEmpty()) {
appendInternal(queryPart, value);
}
return this;
}
/**
* Appends the *queryPart* with corresponding string value if the string is not empty. Otherwise - do nothing.
* @param queryPart the part of query to be appended
* @param value the value of parameter, which is present in the query part
* @return the same query builder
*/
public QueryBuilder appendIfNotEmpty(String queryPart, String value) {
if (isNotEmpty(value)) {
appendInternal(queryPart, value);
}
return this;
}
/**
* Appends the *queryPart* with corresponding string value if the string is not blank. Otherwise - do nothing.
* @param queryPart the part of query to be appended
* @param value the value of parameter, which is present in the query part
* @return the same query builder
*/
public QueryBuilder appendIfNotBlank(String queryPart, String value) {
if (value != null && !value.trim().isEmpty()) {
appendInternal(queryPart, value);
}
return this;
}
protected void appendInternal(String queryPart, Object value) {
if (value == null) {
throw new IllegalArgumentException("Value should be not null!");
}
if (queryPart == null || queryPart.isEmpty()) {
throw new IllegalArgumentException("queryPart should be not empty!");
}
queryPart = preprocessQueryPart(queryPart);
queryPart = tryFrameWithSpacesIfNeeded(queryPart);
List newParams = findParameters(queryPart);
if (newParams.isEmpty()) {
throw new IllegalArgumentException(String.format(
"Can't detect parameter in `%s`, it should start from %s and be valid identifier",
queryPart, parameterPrefix));
}
int dIndex = 0;
for (ParameterInfo param : newParams) {
String paramName = param.name;
int paramStartIndex = param.index + dIndex;
if (paramName.equals(parameterShortName)) {
paramName = constructFullNameOfShortParameterInternal(queryPart, paramStartIndex);
}
while (parameters.containsKey(paramName)) {
paramName = incrementName(paramName);
}
if (!paramName.equals(param.name)) {
String newQueryPart = queryPart.substring(0, paramStartIndex) + parameterPrefix + paramName +
queryPart.substring(paramStartIndex + parameterPrefix.length() + param.name.length());
dIndex = newQueryPart.length() - queryPart.length();
queryPart = newQueryPart;
}
parameters.put(paramName, value);
}
query.append(queryPart);
}
static String incrementName(String name) {
Matcher matcher = INCREMENT_NAME_PATTERN.matcher(name);
if (matcher.find()) {
String partWithoutIndex = name.substring(0, matcher.start());
int index = Integer.parseInt(matcher.group());
return partWithoutIndex + (index + 1);
}
return name + "2";
}
private List findParameters(String queryPart) {
List res = new ArrayList<>();
Pattern pattern = Pattern.compile(parameterPrefix + identifierPattern);
Matcher matcher = pattern.matcher(queryPart);
while (matcher.find()) {
String name = matcher.group().substring(parameterPrefix.length());
res.add(new ParameterInfo(matcher.start(), name));
}
return res;
}
String constructFullNameOfShortParameter(String queryPart, int paramIndex) {
List params = findParameters(queryPart);
ParameterInfo parameterInfo = paramIndex < params.size() ? params.get(paramIndex) : null;
if (parameterInfo == null || !parameterInfo.name.equals(parameterShortName)) {
return null;
}
int i = parameterInfo.index;
return constructFullNameOfShortParameterInternal(queryPart, i);
}
protected boolean isNotEmpty(String value) {
return value != null && !value.isEmpty();
}
private String constructFullNameOfShortParameterInternal(String queryPart, int startIndex) {
List chars = new ArrayList<>();
int i = startIndex - 1;
String res = null;
A: while (i >= 0) {
char ch = queryPart.charAt(i);
if (Character.isLetter(ch)) {
if (i > 0 && !Character.isLetter(queryPart.charAt(i - 1)) &&
hasNextLettersGoingBack(queryPart, i - 1)) {
chars.add(Character.toUpperCase(ch));
}
else {
chars.add(ch);
}
}
else if (Character.isDigit(ch) || ch == '_') {
chars.add(ch);
}
else if (Character.isSpaceChar(ch) && !chars.isEmpty()) {
res = getReversedString(chars);
for (String operator : operators) {
if ((ignoreCaseOfOperators && operator.equalsIgnoreCase(res)) ||
(!ignoreCaseOfOperators && operator.equals(res))) {
chars.clear();
res = null;
continue A;
}
}
break;
}
i--;
}
if (res == null && !chars.isEmpty()) {
res = getReversedString(chars);
}
if (res != null && !Character.isLetter(res.charAt(0))) {
res = "v" + res;
}
return res;
}
private static boolean hasNextLettersGoingBack(String queryPart, int i) {
while (i >= 0) {
final char ch = queryPart.charAt(i);
if (Character.isSpaceChar(ch)) {
return false;
}
else if (Character.isLetter(ch)) {
return true;
}
i--;
}
return false;
}
protected String preprocessQueryPart(String queryPart) {
return queryPart;
}
protected String tryFrameWithSpacesIfNeeded(String queryPart) {
return tryFrameWithSpacesIfNeeded(queryPart, query.length());
}
protected String tryFrameWithSpacesIfNeeded(String queryPart, int insertPosition) {
if (autoAddSpaceBetweenQueryParts && query.length() > 0) {
if (insertPosition > 0 && !Character.isSpaceChar(queryPart.charAt(0)) &&
!Character.isSpaceChar(query.charAt(insertPosition - 1))) {
queryPart = " " + queryPart;
}
if (insertPosition < query.length() && !Character.isSpaceChar(queryPart.charAt(queryPart.length() - 1)) &&
!Character.isSpaceChar(query.charAt(insertPosition))) {
queryPart = queryPart + " ";
}
}
return queryPart;
}
/**
* @param autoAddSpaceBetweenQueryParts should this query builder automatically add additional space,
* between appended query parts? (by default 'true')
*/
public void setAutoAddSpaceBetweenQueryParts(boolean autoAddSpaceBetweenQueryParts) {
this.autoAddSpaceBetweenQueryParts = autoAddSpaceBetweenQueryParts;
}
/**
* @return new {@link QueryWithParams} from current query and parameters.
*/
public QueryWithParams getQueryWithParams() {
return new QueryWithParams(getQuery(), parameters);
}
private static String getReversedString(List chars) {
Collections.reverse(chars);
StringBuilder sb = new StringBuilder();
for (char ch : chars) {
sb.append(ch);
}
return sb.toString();
}
private static class ParameterInfo {
private final int index;
private final String name;
ParameterInfo(int index, String name) {
this.index = index;
this.name = name;
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy