/*
* Copyright (c) 2009, SQL Power Group Inc.
*
* This file is part of SQL Power Library.
*
* SQL Power Library is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* SQL Power Library 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package ca.sqlpower.object;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.collections.map.MultiValueMap;
import org.apache.log4j.Logger;
import org.olap4j.OlapConnection;
import org.olap4j.PreparedOlapStatement;
/**
* This is a helper class for resolving variables. It is a delegating
* implementation of {@link SPVariableResolver} that walks up the {@link SPObject}
* tree and tries the best it can to find a resolver for the provided
* variable key.
*
* <p>Typically, a class that needs such an object has to instanciate it
* and pass a node to the constructor. This node will serve as a starting
* point for variable resolution. The helper will walk up the tree and
* ask all the {@link SPVariableResolver} it finds on it's way if they
* can resolve the variable.
*
* <p>There is an option available to make this helper walk down the tree
* once it reaches the root. It will therefore iterate over all children,
* starting at the root, until it finds a resolver for the given variable key.
*
* <b>Be aware that this mode is very costly in computational times</b> yet
* it might be required if the variable comes from a node that is not directly
* in the path of the source node to the root of the tree.
*
* <p>There is also a flag to make this helper aggregate all results in finds
* in the tree. This means that even if it does find a resolver for a variable,
* it will keep searching and add to the collections of resolved values for the
* variable. It will search the whole tree for all
* {@link SPVariableResolver} instances and resolve with everything it finds.
*
* <p>For example, if you have database queries which provide variables,
* the helper will indirectly trigger the execution of each of those queries in order
* to obtain the column names and thus decide if it can resolve a given variable.
* One easy way to optimize the performance of such operations is to
* use namespaces. This will prevent effective resolution of matches if
* the namespace is not supported by the encountered {@link SPVariableResolver}.
*
* @see {@link SPVariableResolver}
* @author Luc Boudreau
*/
public class SPVariableHelper implements SPVariableResolver {
private final static Pattern varPattern = Pattern.compile("\\$\\{([^\\}]+)\\}");
private static final Logger logger = Logger.getLogger(SPVariableHelper.class);
/**
* Tells if we want to search everywhere in the tree when we are
* resolving collections of variable values.
*/
private boolean globalCollectionResolve = false;
/**
* This is the node onto which this helper is bonded. Searches will
* always start at this node, go up the tree to the root, then go down if
* {@link SPVariableHelper#walkDown} is true.
*/
private final SPObject contextSource;
/**
* Builds a variable helper to help resolve variables as values.
* @param contextSource The source node from which to start
* resolving variables.
*/
public SPVariableHelper(SPObject contextSource) {
this.contextSource = contextSource;
}
/**
* Returns the node onto which this helper is pinned.
*/
public SPObject getContextSource() {
return contextSource;
}
public String substitute(String textWithVars) {
return SPVariableHelper.substitute(textWithVars, this);
}
/**
* Substitutes any number of variable references in the given string, returning
* the resultant string with all variable references replaced by the corresponding
* variable values.
*
* @param textWithVars
* @param variableContext
* @return
*/
public static String substitute(String textWithVars, SPVariableHelper variableHelper) {
logger.debug("Performing variable substitution on " + textWithVars);
// Make sure that the registry is ready.
SPResolverRegistry.init(variableHelper.getContextSource());
StringBuilder text = new StringBuilder();
Matcher matcher = varPattern.matcher(textWithVars);
int currentIndex = 0;
while (!matcher.hitEnd()) {
if (matcher.find()) {
String variableName = matcher.group(1);
Object variableValue;
if (variableName.equals("$")) {
variableValue = "$";
} else {
variableValue = variableHelper.resolve(variableName);
}
logger.debug("Found variable " + variableName + " = " + variableValue);
text.append(textWithVars.substring(currentIndex, matcher.start()));
text.append(variableValue);
currentIndex = matcher.end();
}
}
text.append(textWithVars.substring(currentIndex));
return text.toString();
}
/**
* Helper method that takes a connection and a SQL statement which includes variable and
* converts all that in a nifty prepared statement ready for execution, on time for Christmas.
* @param connection A connection object to use in order to generate the prepared statement.
* @param sql A SQL string which might include variables.
* @return A {@link PreparedStatement} object ready for execution.
* @throws SQLException Might get thrown if we cannot generate a {@link PreparedStatement} with the supplied connection.
*/
public PreparedStatement substituteForDb(Connection connection, String sql) throws SQLException {
return SPVariableHelper.substituteForDb(connection, sql, this);
}
/**
* Helper method that takes a connection and a SQL statement which includes variable and
* converts all that in a nifty prepared statement ready for execution, on time for Christmas.
* @param connection A connection object to use in order to generate the prepared statement.
* @param sql A SQL string which might include variables.
* @param variableHelper A {@link SPVariableHelper} object to resolve the variables.
* @return A {@link PreparedStatement} object ready for execution.
* @throws SQLException Might get thrown if we cannot generate a {@link PreparedStatement} with the supplied connection.
*/
public static PreparedStatement substituteForDb(Connection connection, String sql, SPVariableHelper variableHelper) throws SQLException {
// Make sure that the registry is ready.
SPResolverRegistry.init(variableHelper.getContextSource());
StringBuilder text = new StringBuilder();
Matcher matcher = varPattern.matcher(sql);
List<Object> vars = new LinkedList<Object>();
// First, change all vars to '?' markers.
int currentIndex = 0;
while (!matcher.hitEnd()) {
if (matcher.find()) {
String variableName = matcher.group(1);
if (variableName.equals("$")) {
vars.add("$");
} else {
vars.add(variableHelper.resolve(variableName));
}
text.append(sql.substring(currentIndex, matcher.start()));
text.append("?");
currentIndex = matcher.end();
}
}
text.append(sql.substring(currentIndex));
// Now generate a prepared statement and inject it's variables.
PreparedStatement ps = connection.prepareStatement(text.toString());
for (int i = 0; i < vars.size(); i++) {
ps.setObject(i+1, vars.get(i));
}
return ps;
}
/**
* Helper method that takes a connection and a MDX statement which includes variable and
* converts all that in a nifty prepared statement ready for execution, on time for Christmas.
* @param connection A connection object to use in order to generate the prepared statement.
* @param sql A MDX string which might include variables.
* @return A {@link PreparedStatement} object ready for execution.
* @throws SQLException Might get thrown if we cannot generate a {@link PreparedStatement} with the supplied connection.
*/
public PreparedOlapStatement substituteForDb(
OlapConnection connection,
String mdxQuery) throws SQLException
{
return substituteForDb(connection, mdxQuery, this);
}
/**
* Helper method that takes a connection and a MDX statement which includes variable and
* converts all that in a nifty prepared statement ready for execution, on time for Christmas.
* @param connection A connection object to use in order to generate the prepared statement.
* @param sql A MDX string which might include variables.
* @param variableHelper A {@link SPVariableHelper} object to resolve the variables.
* @return A {@link PreparedStatement} object ready for execution.
* @throws SQLException Might get thrown if we cannot generate a {@link PreparedStatement} with the supplied connection.
*/
public static PreparedOlapStatement substituteForDb(
OlapConnection connection,
String mdxQuery,
SPVariableHelper variableHelper) throws SQLException
{
// Make sure that the registry is ready.
SPResolverRegistry.init(variableHelper.getContextSource());
StringBuilder text = new StringBuilder();
Matcher matcher = varPattern.matcher(mdxQuery);
List<Object> vars = new LinkedList<Object>();
// First, change all vars to '?' markers.
int currentIndex = 0;
while (!matcher.hitEnd()) {
if (matcher.find()) {
String variableName = matcher.group(1);
if (variableName.equals("$")) {
vars.add("$");
} else {
vars.add(variableHelper.resolve(variableName));
}
text.append(mdxQuery.substring(currentIndex, matcher.start()));
text.append("?");
currentIndex = matcher.end();
}
}
text.append(mdxQuery.substring(currentIndex));
// Now generate a prepared statement and inject it's variables.
PreparedOlapStatement ps = connection.prepareOlapStatement(text.toString());
for (int i = 0; i < vars.size(); i++) {
ps.setObject(i+1, vars.get(i));
}
return ps;
}
/**
* Returns the namespace of a variable. If there is
* no variable namespace, null is returned.
* @param varDef The complete variable key lookup value. Something like : '1234-1234::myVar->defValue'
* @return The namespace value, '1234-1234' in the above example, or null if none.
*/
public static String getNamespace(String varDef) {
int index = varDef.indexOf(NAMESPACE_DELIMITER);
if (index != -1) {
return varDef.substring(0, index);
}
return null;
}
/**
* Returns the variable name without the namespace nor
* the default value.
* @param varDef The complete variable key lookup value. Something like : '1234-1234::myVar->defValue'
* @return Only the key part, 'myVar' in the above example.
*/
public static String getKey(String varDef) {
String returnValue = varDef;
int namespaceIndex = varDef.indexOf(NAMESPACE_DELIMITER);
if (namespaceIndex != -1) {
returnValue = returnValue.substring(namespaceIndex + NAMESPACE_DELIMITER.length(), varDef.length());
}
int defValueIndex = returnValue.indexOf(DEFAULT_VALUE_DELIMITER);
if (defValueIndex != -1) {
returnValue = returnValue.substring(0, defValueIndex);
}
return returnValue;
}
/**
* Extracts the default value from an inserted variable key.
* @param varDef The complete variable key lookup value. Something like : '1234-1234::myVar->defValue'
* @return Only the default value part. 'defValue' in the above example.
*/
public static String getDefaultValue(String varDef) {
int defValueIndex = varDef.indexOf(DEFAULT_VALUE_DELIMITER);
if (defValueIndex != -1) {
return varDef.substring(defValueIndex + DEFAULT_VALUE_DELIMITER.length(), varDef.length());
} else {
return null;
}
}
/**
* Returns an inserted variable lookup key stripped from it's default
* value part.
* @param varDef The complete variable key lookup value. Something like : '1234-1234::myVar->defValue'
* @return Would return '1234-1234::myVar' in the above example
*/
public static String stripDefaultValue(String varDef) {
int defValueIndex = varDef.indexOf(DEFAULT_VALUE_DELIMITER);
if (defValueIndex != -1) {
return varDef.substring(0, defValueIndex);
} else {
return varDef;
}
}
/**
* Searches and returns the first resolver for a given namespace
* it can find in the tree. If none can be found, NULL is returned.
* @param namespace The namespace for which we want the resolver.
* @return Either a proper resolver for the given namespace or null
* if none can be found.
*/
public SPVariableResolver getResolverForNamespace(String namespace) {
return SPResolverRegistry.getResolver(this.contextSource, namespace);
}
/**
* Tells if we want to search everywhere in the tree when we are
* resolving collections of variable values.
*
* <p>Setting this property to true makes means that when you call
* {@link SPVariableHelper#resolveCollection(String)} or
* {@link SPVariableHelper#matches(String, String)}, even if it finds
* a resolver for the provided key, the search will continue and
* all resolvers on the tree will append to the returned results.
* Setting it to false (the default behavior) makes it stop and return
* the results as soon as one resolver has resolved the variable.
*/
public void setGlobalCollectionResolve(boolean globalCollectionResolve) {
this.globalCollectionResolve = globalCollectionResolve;
}
// ************************* Resolver Implementation *****************************//
public Object resolve(String key) {
return this.resolve(stripDefaultValue(key), getDefaultValue(key));
}
public Object resolve(String key, Object defaultValue) {
String namespace = getNamespace(key);
try {
if (namespace != null) {
SPVariableResolver resolver =
SPResolverRegistry.getResolver(this.contextSource, namespace);
if (resolver==null) {
return defaultValue;
} else {
return resolver.resolve(key, defaultValue);
}
}
SPObject node = this.contextSource;
while (true) {
if (node instanceof SPVariableResolverProvider) {
SPVariableResolver resolver = ((SPVariableResolverProvider)node).getVariableResolver();
if (resolver.resolves(key)) {
return resolver.resolve(key, defaultValue);
}
}
node = node.getParent();
if (node == null) return defaultValue;
}
} catch (StackOverflowError soe) {
throw new RecursiveVariableException();
}
}
public Collection<Object> resolveCollection(String key) {
return this.resolveCollection(stripDefaultValue(key), getDefaultValue(key));
}
public Collection<Object> resolveCollection(String key, Object defaultValue) {
LinkedHashSet<Object> results = new LinkedHashSet<Object>();
String namespace = getNamespace(key);
try {
if (namespace != null) {
List<SPVariableResolver> resolvers = SPResolverRegistry.getResolvers(this.contextSource, namespace);
for (SPVariableResolver resolver : resolvers) {
if (resolver.resolves(key)) {
results.addAll(resolver.resolveCollection(key));
if (!globalCollectionResolve) {
break;
}
}
}
} else {
SPObject node = this.contextSource;
while (true) {
if (node instanceof SPVariableResolverProvider) {
SPVariableResolver resolver = ((SPVariableResolverProvider)node).getVariableResolver();
if (resolver.resolves(key)) {
results.addAll(resolver.resolveCollection(key));
if (!globalCollectionResolve) {
break;
}
}
}
node = node.getParent();
if (node == null) break;
}
}
if (results.size() == 0) {
if (defaultValue == null) {
return Collections.emptySet();
} else {
return Collections.singleton(defaultValue);
}
} else {
return results;
}
} catch (StackOverflowError soe) {
throw new RecursiveVariableException();
}
}
public boolean resolves(String key) {
String namespace = getNamespace(key);
if (namespace != null) {
return SPResolverRegistry.getResolver(this.contextSource, namespace) != null;
} else {
SPObject node = this.contextSource;
while (true) {
if (node instanceof SPVariableResolverProvider) {
SPVariableResolver resolver = ((SPVariableResolverProvider)node).getVariableResolver();
if (resolver.resolves(key)) {
return true;
}
}
node = node.getParent();
if (node == null) return false;
}
}
}
public boolean resolvesNamespace(String namespace) {
return SPResolverRegistry.getResolver(this.contextSource, namespace) != null;
}
public Collection<Object> matches(String key, String partialValue) {
Collection<Object> matches = new HashSet<Object>();
String namespace = getNamespace(key);
try {
if (namespace != null) {
for (SPVariableResolver resolver : SPResolverRegistry.getResolvers(contextSource, namespace)) {
if (resolver.resolves(key)) {
matches.addAll(resolver.matches(key, partialValue));
if (!globalCollectionResolve) {
break;
}
}
}
return matches;
} else {
SPObject node = this.contextSource;
while (true) {
if (node instanceof SPVariableResolverProvider) {
SPVariableResolver resolver = ((SPVariableResolverProvider)node).getVariableResolver();
if (resolver.resolves(key)) {
matches.addAll(resolver.matches(key, partialValue));
if (!globalCollectionResolve) {
break;
}
}
}
node = node.getParent();
if (node == null) break;
}
return matches;
}
} catch (StackOverflowError soe) {
throw new RecursiveVariableException();
}
}
public Collection<String> keySet(String namespace) {
List<String> results = new ArrayList<String>();
if (namespace != null) {
for (SPVariableResolver resolver : SPResolverRegistry.getResolvers(this.contextSource, namespace)) {
if (resolver.resolvesNamespace(namespace)) {
results.addAll(resolver.keySet(namespace));
}
}
return results;
} else {
SPObject node = this.contextSource;
while (true) {
if (node instanceof SPVariableResolverProvider) {
SPVariableResolver resolver = ((SPVariableResolverProvider)node).getVariableResolver();
results.addAll(resolver.keySet(namespace));
}
node = node.getParent();
if (node == null) break;
}
return results;
}
}
public String getNamespace() {
throw new UnsupportedOperationException("SPVariableHelper is not bound to a namespace.");
}
/**
* Creates a list of user friendly names->namespaces.
* If you want only the namespaces, do {@link MultiValueMap#values()}
* @return
*/
public MultiValueMap getNamespaces() {
return SPResolverRegistry.getNamespaces(this.contextSource);
}
public String getUserFriendlyName() {
return null;
}
/**
* Wraps the RuntimeException to identify recursive variables resolutions.
*/
public class RecursiveVariableException extends RuntimeException {
}
public void delete(String key) {
throw new UnsupportedOperationException("SPVariableHelper cannot store variables.");
}
public void store(String key, Object value) {
throw new UnsupportedOperationException("SPVariableHelper cannot store variables.");
}
public void update(String key, Object value) {
throw new UnsupportedOperationException("SPVariableHelper cannot store variables.");
}
}