/*
* Copyright 2010, Andrew M Gibson
*
* www.andygibson.net
*
* This file is part of DataValve.
*
* DataValve is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* DataValve is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
*
* You should have received a copy of the GNU Lesser General Public License
* along with DataValve. If not, see <http://www.gnu.org/licenses/>.
*
*/
package org.fluttercode.datavalve.provider.util;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.fluttercode.datavalve.params.Parameter;
import org.fluttercode.datavalve.params.ParameterParser;
import org.fluttercode.datavalve.params.ParameterValues;
import org.fluttercode.datavalve.params.RegexParameterParser;
import org.fluttercode.datavalve.provider.ParameterizedDataProvider;
import org.fluttercode.datavalve.provider.QueryDataProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Builds a {@link DataQuery} object based on the following elements :
*
* <ul>
* <li>a <code>baseStatement</code> that defines the selection (required)</li>
* <li>A {@link ParameterizedDataProvider} reference for evaluating parameters</li>
* <li>an <code>orderBy</code> statement (optional)</li>
* <li>a flag indicating if the order is ascending (only used if orderBy is set)
* </li>
* </ul>
* If the {@link #provider} reference also implements the
* {@link QueryDataProvider}, then it also processes the restrictions as part of
* the final query.
* <p>
* The process involves parameterizing each line of the query (statement plus
* restrictions) and then
*
* @author Andy Gibson
*
*/
public class DataQueryBuilder {
private int parameterId = 0;
// private ParameterizedDataProvider<? extends Object> provider;
private static Pattern commaSplitter = Pattern.compile(",");
private StringBuilder statement = new StringBuilder();
private StringBuilder restrictions = new StringBuilder();
private Pattern logicalOpPattern = Pattern.compile(
"\\A[ (]*\\b(and|or)\\b[ (]*.*", Pattern.CASE_INSENSITIVE);
private ParameterParser parameterParser = new RegexParameterParser();
private boolean orderedParams = false;
private boolean allowNullParameters = false;
private ParameterizedDataProvider<? extends Object> provider;
private String orderBy;
private boolean orderAscending = true;
private String baseStatement;
private static Logger log = LoggerFactory.getLogger(DataQueryBuilder.class);
/**
* Clears the final statement and restriction values before building another
* query.
*/
private void clear() {
statement = new StringBuilder();
restrictions = new StringBuilder();
}
public DataQuery build() {
if (provider == null) {
throw new NullPointerException(
"Cannot build Data Query without setting the provider");
}
if (baseStatement == null || baseStatement.length() == 0) {
throw new IllegalArgumentException(
"Cannot build Data Query without a base statement");
}
clear();
DataQuery query = new DataQuery();
addLineToQuery(parameterizeLine(baseStatement, query, true));
// is this a query? If so, lets add the restrictions for this query
if (provider instanceof QueryDataProvider<?>) {
QueryDataProvider<? extends Object> queryProvider = (QueryDataProvider<? extends Object>) provider;
for (String restriction : queryProvider.getRestrictions()) {
addRestrictionToQuery(parameterizeLine(restriction, query,
allowNullParameters));
}
}
query.setStatement(buildFinalStatement());
return query;
}
/**
* Process this query line by getting the parameter expressions, evaluating
* them. The parameters are renamed and the line is added to the query and
* the evaluated parameters are added to the list of parameters. Optionally,
* the restriction may be included even if it contains null parameters.
*
* @param line
* The query line to process
*
* @return The statement line with the new parameter names
*/
protected final String parameterizeLine(String line, DataQuery query,
boolean includeNullParameters) {
log.debug("processing restriction '{}'", line);
// if this is a null or empty string, just return
if (line == null || line.length() == 0) {
return null;
}
// get the parameter expressions in this line
String[] expressions = extractExpressions(line);
// if there are no expressions, just add the restriction and leave
if (expressions.length == 0) {
log.debug("Found no restrictions, exiting");
return line;
}
// build the parameters for this restriction line
ParameterValues params = buildParameterList(expressions, query);
// are we missing some parameter values? If so we are done if we are not
// including missing parameters
if (params.hasNullParameters() && !includeNullParameters) {
return null;
}
// Process each parameter
for (Parameter param : params) {
// calculate new name for parameter (param_(int) or '?')
String newName = getNewParameterName();
// replace the param in the restriction with the new name with the
// necessary prefix ":" or blank if we are using ordered parameters
String prefixedNewName = getNewParameterNamePrefix() + newName;
log.debug("Replacing {} with {} in expression");
String oldValue = param.getName();
oldValue = oldValue.replace("{", "\\{");
oldValue = oldValue.replace("}", "\\}");
line = line.replaceFirst(oldValue, prefixedNewName);
// set the new name of the parameter in the parameter info
param.setName(newName);
// add the parameter to the final list
query.getParameters().add(param);
}
// finally, add the restriction to the list
log.debug("Adding restriction {}", line);
return line;
}
/**
* Appends a line to the final query text. Accepts a null parameter but does
* not add it.
*
* @param line
* Line to append to the query
*/
protected void addLineToQuery(String line) {
if (line != null) {
statement.append(line);
}
}
/**
* Adds a restriction to the final query text. Checks to see if the line
* starts with a logical operator (and/or) and if not, prefixes with ' AND
* '.
*
* @param line
* Line to append to the query.
*/
protected void addRestrictionToQuery(String line) {
if (line != null) {
// only worry about logical operators if its not empty.
if (restrictions.length() != 0) {
if (!startsWithLogicalOperator(line)) {
restrictions.append(" AND");
}
// add a space to separate this next statement
restrictions.append(" ");
}
restrictions.append(line);
}
}
/**
* Returns a new parameter name based on whether this query is using ordered
* parameters or named parameters. For ordered parameters, we always just
* return a "?" but with named parameters, we return a new unique name.
*
* @return A new parameter name
*/
protected String getNewParameterName() {
// return ? for jdbc type parameters
if (orderedParams) {
return "?";
} else {
return "param_" + parameterId++;
}
}
/**
* Returns the prefix for parameters which returns a colon if it is a named
* parameter and an empty string if we are using ordered parameters.
*
* @return prefix of the new parameter.
*/
protected String getNewParameterNamePrefix() {
return orderedParams ? "" : ":";
}
/**
* Takes a list of string parameter expressions and resolves them, storing
* the results in the {@link ParameterValues} list.
*
* @param expressions
* List of expressions to resolve
* @return List of resolved parameter values.
*/
private ParameterValues buildParameterList(String[] expressions,
DataQuery query) {
ParameterValues results = new ParameterValues();
if (provider != null) {
for (String expression : expressions) {
Object value = provider.resolveParameter(expression);
results.add(expression, value);
}
}
return results;
}
/**
* Extracts the parameter expressions contained in a query restriction into
* a string array. The parameters are extracted using a parameterParser
* which can be changed at run time.
*
* @param restriction
* the query restriction we want to extract expressions from
* @return a string array holding the expressions.
*/
protected String[] extractExpressions(String restriction) {
if (parameterParser == null) {
throw new IllegalStateException(
"Parameter parser is null in query builder");
}
return parameterParser.extractParameters(restriction);
}
public ParameterParser getParameterParser() {
return parameterParser;
}
public void setParameterParser(ParameterParser parameterParser) {
this.parameterParser = parameterParser;
}
/**
* @param s
* Restriction line to check
* @return True if the line starts with AND or OR
*/
public boolean startsWithLogicalOperator(String s) {
Matcher m = logicalOpPattern.matcher(s);
return m.matches();
}
/**
* Takes the statement and restrictions and builds a final statement which
* is returned to the caller.
*
* @return The complete Query statement from the statement and restrictions.
*/
protected String buildFinalStatement() {
if (restrictions.length() > 0) {
statement.append(" WHERE ");
statement.append(restrictions);
}
statement.append(buildOrderBy());
return statement.toString();
}
private String buildOrderBy() {
// parse out fields and add order
if (orderBy == null || orderBy.length() == 0) {
return "";
}
String[] fields = commaSplitter.split(orderBy);
String order = "";
// concatenate the fields with the direction, put commas in between
for (String field : fields) {
if (order.length() != 0) {
order = order + ", ";
}
order = order + field + (isOrderAscending() ? " ASC" : " DESC");
}
return " ORDER BY " + order;
}
protected String buildFinalStatement(String orderBy) {
if (orderBy != null && orderBy.length() != 0) {
return buildFinalStatement() + " ORDER BY " + orderBy;
}
return buildFinalStatement();
}
public boolean getAllowNullParameters() {
return allowNullParameters;
}
public void setAllowNullParameters(boolean allowNullParameters) {
this.allowNullParameters = allowNullParameters;
}
public boolean isOrderAscending() {
return orderAscending;
}
public boolean isOrderedParams() {
return orderedParams;
}
public void setOrderBy(String orderBy) {
this.orderBy = orderBy;
}
public void setBaseStatement(String baseStatement) {
this.baseStatement = baseStatement;
}
public ParameterizedDataProvider<? extends Object> getProvider() {
return provider;
}
public void setProvider(ParameterizedDataProvider<? extends Object> provider) {
this.provider = provider;
}
public String getOrderBy() {
return orderBy;
}
public String getBaseStatement() {
return baseStatement;
}
public void setOrderedParams(boolean orderedParams) {
this.orderedParams = orderedParams;
}
public void setOrderAscending(boolean orderAscending) {
this.orderAscending = orderAscending;
}
}