ExprLookup.java
/*
* 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.commons.configuration2.interpol;
import java.util.ArrayList;
import java.util.Objects;
import org.apache.commons.configuration2.ex.ConfigurationRuntimeException;
import org.apache.commons.configuration2.io.ConfigurationLogger;
import org.apache.commons.jexl2.Expression;
import org.apache.commons.jexl2.JexlContext;
import org.apache.commons.jexl2.JexlEngine;
import org.apache.commons.jexl2.MapContext;
import org.apache.commons.lang3.ClassUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.text.StringSubstitutor;
import org.apache.commons.text.lookup.StringLookup;
/**
* Lookup that allows expressions to be evaluated.
*
* <pre>
* ExprLookup.Variables vars = new ExprLookup.Variables();
* vars.add(new ExprLookup.Variable("String", org.apache.commons.lang.StringUtils.class));
* vars.add(new ExprLookup.Variable("Util", new Utility("Hello")));
* vars.add(new ExprLookup.Variable("System", "Class:java.lang.System"));
* XMLConfiguration config = new XMLConfiguration(TEST_FILE);
* config.setLogger(log);
* ExprLookup lookup = new ExprLookup(vars);
* lookup.setConfiguration(config);
* String str = lookup.lookup("'$[element] ' + String.trimToEmpty('$[space.description]')");
* </pre>
*
* In the example above TEST_FILE contains xml that looks like:
*
* <pre>
* <configuration>
* <element>value</element>
* <space xml:space="preserve">
* <description xml:space="default"> Some text </description>
* </space>
* </configuration>
* </pre>
*
* The result will be "value Some text".
*
* This lookup uses Apache Commons Jexl and requires that the dependency be added to any projects which use this.
*
* @since 1.7
*/
public class ExprLookup implements Lookup {
/**
* The key and corresponding object that will be made available to the JexlContext for use in expressions.
*/
public static class Variable {
/** The name to be used in expressions */
private String key;
/** The object to be accessed in expressions */
private Object value;
public Variable() {
}
public Variable(final String name, final Object value) {
setName(name);
setValue(value);
}
public String getName() {
return key;
}
public Object getValue() {
return value;
}
public void setName(final String name) {
this.key = name;
}
public void setValue(final Object value) throws ConfigurationRuntimeException {
try {
if (!(value instanceof String)) {
this.value = value;
return;
}
final String val = (String) value;
final String name = StringUtils.removeStartIgnoreCase(val, CLASS);
final Class<?> clazz = ClassUtils.getClass(name);
if (name.length() == val.length()) {
this.value = clazz.getConstructor().newInstance();
} else {
this.value = clazz;
}
} catch (final Exception e) {
throw new ConfigurationRuntimeException("Unable to create " + value, e);
}
}
}
/**
* List wrapper used to allow the Variables list to be created as beans in DefaultConfigurationBuilder.
*/
public static class Variables extends ArrayList<Variable> {
/**
* The serial version UID.
*/
private static final long serialVersionUID = 20111205L;
/**
* Creates a new empty instance of {@code Variables}.
*/
public Variables() {
}
/**
* Creates a new instance of {@code Variables} and copies the content of the given object.
*
* @param vars the {@code Variables} object to be copied
*/
public Variables(final Variables vars) {
super(vars);
}
public Variable getVariable() {
return !isEmpty() ? get(size() - 1) : null;
}
}
/** Prefix to identify a Java Class object */
private static final String CLASS = "Class:";
/** The default prefix for subordinate lookup expressions */
private static final String DEFAULT_PREFIX = "$[";
/** The default suffix for subordinate lookup expressions */
private static final String DEFAULT_SUFFIX = "]";
/** The ConfigurationInterpolator used by this object. */
private ConfigurationInterpolator interpolator;
/** The StringSubstitutor for performing replace operations. */
private StringSubstitutor substitutor;
/** The logger used by this instance. */
private ConfigurationLogger logger;
/** The engine. */
private final JexlEngine engine = new JexlEngine();
/** The variables maintained by this object. */
private Variables variables;
/** The String to use to start subordinate lookup expressions */
private String prefixMatcher = DEFAULT_PREFIX;
/** The String to use to terminate subordinate lookup expressions */
private String suffixMatcher = DEFAULT_SUFFIX;
/**
* The default constructor. Will get used when the Lookup is constructed via configuration.
*/
public ExprLookup() {
}
/**
* Constructor for use by applications.
*
* @param list The list of objects to be accessible in expressions.
*/
public ExprLookup(final Variables list) {
setVariables(list);
}
/**
* Constructor for use by applications.
*
* @param list The list of objects to be accessible in expressions.
* @param prefix The prefix to use for subordinate lookups.
* @param suffix The suffix to use for subordinate lookups.
*/
public ExprLookup(final Variables list, final String prefix, final String suffix) {
this(list);
setVariablePrefixMatcher(prefix);
setVariableSuffixMatcher(suffix);
}
/**
* Creates a new {@code JexlContext} and initializes it with the variables managed by this Lookup object.
*
* @return the newly created context
*/
private JexlContext createContext() {
final JexlContext ctx = new MapContext();
initializeContext(ctx);
return ctx;
}
/**
* Gets the {@code ConfigurationInterpolator} used by this object.
*
* @return the {@code ConfigurationInterpolator}
* @since 2.0
*/
public ConfigurationInterpolator getInterpolator() {
return interpolator;
}
/**
* Gets the logger used by this object.
*
* @return the {@code Log}
* @since 2.0
*/
public ConfigurationLogger getLogger() {
return logger;
}
/**
* Gets the list of Variables that are accessible within expressions. This method returns a copy of the variables
* managed by this lookup; so modifying this object has no impact on this lookup.
*
* @return the List of Variables that are accessible within expressions.
*/
public Variables getVariables() {
return new Variables(variables);
}
/**
* Initializes the specified context with the variables managed by this Lookup object.
*
* @param ctx the context to be initialized
*/
private void initializeContext(final JexlContext ctx) {
variables.forEach(var -> ctx.set(var.getName(), var.getValue()));
}
/**
* Creates a {@code StringSubstitutor} object which uses the passed in {@code ConfigurationInterpolator} as lookup
* object.
*
* @param ip the {@code ConfigurationInterpolator} to be used
*/
private void installSubstitutor(final ConfigurationInterpolator ip) {
if (ip == null) {
substitutor = null;
} else {
final StringLookup variableResolver = key -> Objects.toString(ip.resolve(key), null);
substitutor = new StringSubstitutor(variableResolver, prefixMatcher, suffixMatcher, StringSubstitutor.DEFAULT_ESCAPE);
}
}
/**
* Evaluates the expression.
*
* @param var The expression.
* @return The String result of the expression.
*/
@Override
public String lookup(final String var) {
if (substitutor == null) {
return var;
}
String result = substitutor.replace(var);
try {
final Expression exp = engine.createExpression(result);
final Object exprResult = exp.evaluate(createContext());
result = exprResult != null ? String.valueOf(exprResult) : null;
} catch (final Exception e) {
final ConfigurationLogger l = getLogger();
if (l != null) {
l.debug("Error encountered evaluating " + result + ": " + e);
}
}
return result;
}
/**
* Sets the {@code ConfigurationInterpolator} to be used by this object.
*
* @param interpolator the {@code ConfigurationInterpolator} (may be <b>null</b>)
* @since 2.0
*/
public void setInterpolator(final ConfigurationInterpolator interpolator) {
this.interpolator = interpolator;
installSubstitutor(interpolator);
}
/**
* Sets the logger to be used by this object. If no logger is passed in, no log output is generated.
*
* @param logger the {@code Log}
* @since 2.0
*/
public void setLogger(final ConfigurationLogger logger) {
this.logger = logger;
}
/**
* Sets the prefix to use to identify subordinate expressions. This cannot be the same as the prefix used for the primary
* expression.
*
* @param prefix The String identifying the beginning of the expression.
*/
public void setVariablePrefixMatcher(final String prefix) {
prefixMatcher = prefix;
}
/**
* Add the Variables that will be accessible within expressions.
*
* @param list The list of Variables.
*/
public void setVariables(final Variables list) {
variables = new Variables(list);
}
/**
* Sets the suffix to use to identify subordinate expressions. This cannot be the same as the suffix used for the primary
* expression.
*
* @param suffix The String identifying the end of the expression.
*/
public void setVariableSuffixMatcher(final String suffix) {
suffixMatcher = suffix;
}
}