001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *     http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.commons.configuration2.interpol;
018
019import java.util.ArrayList;
020import java.util.Objects;
021
022import org.apache.commons.configuration2.ex.ConfigurationRuntimeException;
023import org.apache.commons.configuration2.io.ConfigurationLogger;
024import org.apache.commons.jexl2.Expression;
025import org.apache.commons.jexl2.JexlContext;
026import org.apache.commons.jexl2.JexlEngine;
027import org.apache.commons.jexl2.MapContext;
028import org.apache.commons.lang3.ClassUtils;
029import org.apache.commons.lang3.StringUtils;
030import org.apache.commons.text.StringSubstitutor;
031import org.apache.commons.text.lookup.StringLookup;
032
033/**
034 * Lookup that allows expressions to be evaluated.
035 *
036 * <pre>
037 * ExprLookup.Variables vars = new ExprLookup.Variables();
038 * vars.add(new ExprLookup.Variable("String", org.apache.commons.lang.StringUtils.class));
039 * vars.add(new ExprLookup.Variable("Util", new Utility("Hello")));
040 * vars.add(new ExprLookup.Variable("System", "Class:java.lang.System"));
041 * XMLConfiguration config = new XMLConfiguration(TEST_FILE);
042 * config.setLogger(log);
043 * ExprLookup lookup = new ExprLookup(vars);
044 * lookup.setConfiguration(config);
045 * String str = lookup.lookup("'$[element] ' + String.trimToEmpty('$[space.description]')");
046 * </pre>
047 *
048 * In the example above TEST_FILE contains xml that looks like:
049 *
050 * <pre>
051 * &lt;configuration&gt;
052 *   &lt;element&gt;value&lt;/element&gt;
053 *   &lt;space xml:space="preserve"&gt;
054 *     &lt;description xml:space="default"&gt;     Some text      &lt;/description&gt;
055 *   &lt;/space&gt;
056 * &lt;/configuration&gt;
057 * </pre>
058 *
059 * The result will be "value Some text".
060 *
061 * This lookup uses Apache Commons Jexl and requires that the dependency be added to any projects which use this.
062 *
063 * @since 1.7
064 */
065public class ExprLookup implements Lookup {
066    /**
067     * The key and corresponding object that will be made available to the JexlContext for use in expressions.
068     */
069    public static class Variable {
070        /** The name to be used in expressions */
071        private String key;
072
073        /** The object to be accessed in expressions */
074        private Object value;
075
076        public Variable() {
077        }
078
079        public Variable(final String name, final Object value) {
080            setName(name);
081            setValue(value);
082        }
083
084        public String getName() {
085            return key;
086        }
087
088        public Object getValue() {
089            return value;
090        }
091
092        public void setName(final String name) {
093            this.key = name;
094        }
095
096        public void setValue(final Object value) throws ConfigurationRuntimeException {
097            try {
098                if (!(value instanceof String)) {
099                    this.value = value;
100                    return;
101                }
102                final String val = (String) value;
103                final String name = StringUtils.removeStartIgnoreCase(val, CLASS);
104                final Class<?> clazz = ClassUtils.getClass(name);
105                if (name.length() == val.length()) {
106                    this.value = clazz.getConstructor().newInstance();
107                } else {
108                    this.value = clazz;
109                }
110            } catch (final Exception e) {
111                throw new ConfigurationRuntimeException("Unable to create " + value, e);
112            }
113
114        }
115    }
116
117    /**
118     * List wrapper used to allow the Variables list to be created as beans in DefaultConfigurationBuilder.
119     */
120    public static class Variables extends ArrayList<Variable> {
121        /**
122         * The serial version UID.
123         */
124        private static final long serialVersionUID = 20111205L;
125
126        /**
127         * Creates a new empty instance of {@code Variables}.
128         */
129        public Variables() {
130        }
131
132        /**
133         * Creates a new instance of {@code Variables} and copies the content of the given object.
134         *
135         * @param vars the {@code Variables} object to be copied
136         */
137        public Variables(final Variables vars) {
138            super(vars);
139        }
140
141        public Variable getVariable() {
142            return !isEmpty() ? get(size() - 1) : null;
143        }
144
145    }
146
147    /** Prefix to identify a Java Class object */
148    private static final String CLASS = "Class:";
149
150    /** The default prefix for subordinate lookup expressions */
151    private static final String DEFAULT_PREFIX = "$[";
152
153    /** The default suffix for subordinate lookup expressions */
154    private static final String DEFAULT_SUFFIX = "]";
155
156    /** The ConfigurationInterpolator used by this object. */
157    private ConfigurationInterpolator interpolator;
158
159    /** The StringSubstitutor for performing replace operations. */
160    private StringSubstitutor substitutor;
161
162    /** The logger used by this instance. */
163    private ConfigurationLogger logger;
164
165    /** The engine. */
166    private final JexlEngine engine = new JexlEngine();
167
168    /** The variables maintained by this object. */
169    private Variables variables;
170
171    /** The String to use to start subordinate lookup expressions */
172    private String prefixMatcher = DEFAULT_PREFIX;
173
174    /** The String to use to terminate subordinate lookup expressions */
175    private String suffixMatcher = DEFAULT_SUFFIX;
176
177    /**
178     * The default constructor. Will get used when the Lookup is constructed via configuration.
179     */
180    public ExprLookup() {
181    }
182
183    /**
184     * Constructor for use by applications.
185     *
186     * @param list The list of objects to be accessible in expressions.
187     */
188    public ExprLookup(final Variables list) {
189        setVariables(list);
190    }
191
192    /**
193     * Constructor for use by applications.
194     *
195     * @param list The list of objects to be accessible in expressions.
196     * @param prefix The prefix to use for subordinate lookups.
197     * @param suffix The suffix to use for subordinate lookups.
198     */
199    public ExprLookup(final Variables list, final String prefix, final String suffix) {
200        this(list);
201        setVariablePrefixMatcher(prefix);
202        setVariableSuffixMatcher(suffix);
203    }
204
205    /**
206     * Creates a new {@code JexlContext} and initializes it with the variables managed by this Lookup object.
207     *
208     * @return the newly created context
209     */
210    private JexlContext createContext() {
211        final JexlContext ctx = new MapContext();
212        initializeContext(ctx);
213        return ctx;
214    }
215
216    /**
217     * Gets the {@code ConfigurationInterpolator} used by this object.
218     *
219     * @return the {@code ConfigurationInterpolator}
220     * @since 2.0
221     */
222    public ConfigurationInterpolator getInterpolator() {
223        return interpolator;
224    }
225
226    /**
227     * Gets the logger used by this object.
228     *
229     * @return the {@code Log}
230     * @since 2.0
231     */
232    public ConfigurationLogger getLogger() {
233        return logger;
234    }
235
236    /**
237     * Gets the list of Variables that are accessible within expressions. This method returns a copy of the variables
238     * managed by this lookup; so modifying this object has no impact on this lookup.
239     *
240     * @return the List of Variables that are accessible within expressions.
241     */
242    public Variables getVariables() {
243        return new Variables(variables);
244    }
245
246    /**
247     * Initializes the specified context with the variables managed by this Lookup object.
248     *
249     * @param ctx the context to be initialized
250     */
251    private void initializeContext(final JexlContext ctx) {
252        variables.forEach(var -> ctx.set(var.getName(), var.getValue()));
253    }
254
255    /**
256     * Creates a {@code StringSubstitutor} object which uses the passed in {@code ConfigurationInterpolator} as lookup
257     * object.
258     *
259     * @param ip the {@code ConfigurationInterpolator} to be used
260     */
261    private void installSubstitutor(final ConfigurationInterpolator ip) {
262        if (ip == null) {
263            substitutor = null;
264        } else {
265            final StringLookup variableResolver = key -> Objects.toString(ip.resolve(key), null);
266            substitutor = new StringSubstitutor(variableResolver, prefixMatcher, suffixMatcher, StringSubstitutor.DEFAULT_ESCAPE);
267        }
268    }
269
270    /**
271     * Evaluates the expression.
272     *
273     * @param var The expression.
274     * @return The String result of the expression.
275     */
276    @Override
277    public String lookup(final String var) {
278        if (substitutor == null) {
279            return var;
280        }
281
282        String result = substitutor.replace(var);
283        try {
284            final Expression exp = engine.createExpression(result);
285            final Object exprResult = exp.evaluate(createContext());
286            result = exprResult != null ? String.valueOf(exprResult) : null;
287        } catch (final Exception e) {
288            final ConfigurationLogger l = getLogger();
289            if (l != null) {
290                l.debug("Error encountered evaluating " + result + ": " + e);
291            }
292        }
293
294        return result;
295    }
296
297    /**
298     * Sets the {@code ConfigurationInterpolator} to be used by this object.
299     *
300     * @param interpolator the {@code ConfigurationInterpolator} (may be <b>null</b>)
301     * @since 2.0
302     */
303    public void setInterpolator(final ConfigurationInterpolator interpolator) {
304        this.interpolator = interpolator;
305        installSubstitutor(interpolator);
306    }
307
308    /**
309     * Sets the logger to be used by this object. If no logger is passed in, no log output is generated.
310     *
311     * @param logger the {@code Log}
312     * @since 2.0
313     */
314    public void setLogger(final ConfigurationLogger logger) {
315        this.logger = logger;
316    }
317
318    /**
319     * Sets the prefix to use to identify subordinate expressions. This cannot be the same as the prefix used for the primary
320     * expression.
321     *
322     * @param prefix The String identifying the beginning of the expression.
323     */
324    public void setVariablePrefixMatcher(final String prefix) {
325        prefixMatcher = prefix;
326    }
327
328    /**
329     * Add the Variables that will be accessible within expressions.
330     *
331     * @param list The list of Variables.
332     */
333    public void setVariables(final Variables list) {
334        variables = new Variables(list);
335    }
336
337    /**
338     * Sets the suffix to use to identify subordinate expressions. This cannot be the same as the suffix used for the primary
339     * expression.
340     *
341     * @param suffix The String identifying the end of the expression.
342     */
343    public void setVariableSuffixMatcher(final String suffix) {
344        suffixMatcher = suffix;
345    }
346}