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 *     https://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.Strings;
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    /**
068     * The key and corresponding object that will be made available to the JexlContext for use in expressions.
069     */
070    public static class Variable {
071
072        /** The name to be used in expressions. */
073        private String key;
074
075        /** The object to be accessed in expressions. */
076        private Object value;
077
078        /**
079         * Constructs a new instance.
080         */
081        public Variable() {
082        }
083
084        /**
085         * Constructs a new instance.
086         *
087         * @param name The name to be used in expressions.
088         * @param value The object to be accessed in expressions.
089         */
090        public Variable(final String name, final Object value) {
091            setName(name);
092            setValue(value);
093        }
094
095        /**
096         * Gets the name to be used in expressions.
097         *
098         * @return the name to be used in expressions.
099         */
100        public String getName() {
101            return key;
102        }
103
104        /**
105         * Sets the value to be used in expressions.
106         *
107         * @return the value to be used in expressions.
108         */
109        public Object getValue() {
110            return value;
111        }
112
113        /**
114         * Sets the name to be used in expressions.
115         *
116         * @param name the name to be used in expressions.
117         */
118        public void setName(final String name) {
119            this.key = name;
120        }
121
122        /**
123         * Sets the value to be used in expressions.
124         *
125         * @param value The object to be accessed in expressions.
126         * @throws ConfigurationRuntimeException Wraps an exception creating the value.
127         */
128        public void setValue(final Object value) throws ConfigurationRuntimeException {
129            try {
130                if (!(value instanceof String)) {
131                    this.value = value;
132                    return;
133                }
134                final String val = (String) value;
135                final String name = Strings.CI.removeStart(val, CLASS);
136                final Class<?> clazz = ClassUtils.getClass(name);
137                if (name.length() == val.length()) {
138                    this.value = clazz.getConstructor().newInstance();
139                } else {
140                    this.value = clazz;
141                }
142            } catch (final Exception e) {
143                throw new ConfigurationRuntimeException(e, "Unable to create %s", value);
144            }
145
146        }
147    }
148
149    /**
150     * List wrapper used to allow the Variables list to be created as beans in DefaultConfigurationBuilder.
151     */
152    public static class Variables extends ArrayList<Variable> {
153
154        /**
155         * The serial version UID.
156         */
157        private static final long serialVersionUID = 20111205L;
158
159        /**
160         * Creates a new empty instance of {@code Variables}.
161         */
162        public Variables() {
163        }
164
165        /**
166         * Creates a new instance of {@code Variables} and copies the content of the given object.
167         *
168         * @param vars the {@code Variables} object to be copied
169         */
170        public Variables(final Variables vars) {
171            super(vars);
172        }
173
174        /**
175         * Gets the variable or null if empty.
176         *
177         * @return the variable or null if empty.
178         */
179        public Variable getVariable() {
180            return !isEmpty() ? get(size() - 1) : null;
181        }
182
183    }
184
185    /** Prefix to identify a Java Class object */
186    private static final String CLASS = "Class:";
187
188    /** The default prefix for subordinate lookup expressions */
189    private static final String DEFAULT_PREFIX = "$[";
190
191    /** The default suffix for subordinate lookup expressions */
192    private static final String DEFAULT_SUFFIX = "]";
193
194    /** The ConfigurationInterpolator used by this object. */
195    private ConfigurationInterpolator interpolator;
196
197    /** The StringSubstitutor for performing replace operations. */
198    private StringSubstitutor substitutor;
199
200    /** The logger used by this instance. */
201    private ConfigurationLogger logger;
202
203    /** The engine. */
204    private final JexlEngine engine = new JexlEngine();
205
206    /** The variables maintained by this object. */
207    private Variables variables;
208
209    /** The String to use to start subordinate lookup expressions */
210    private String prefixMatcher = DEFAULT_PREFIX;
211
212    /** The String to use to terminate subordinate lookup expressions */
213    private String suffixMatcher = DEFAULT_SUFFIX;
214
215    /**
216     * Constructs a new instance. Will get used when the Lookup is constructed via configuration.
217     */
218    public ExprLookup() {
219    }
220
221    /**
222     * Constructor for use by applications.
223     *
224     * @param list The list of objects to be accessible in expressions.
225     */
226    public ExprLookup(final Variables list) {
227        setVariables(list);
228    }
229
230    /**
231     * Constructor for use by applications.
232     *
233     * @param list The list of objects to be accessible in expressions.
234     * @param prefix The prefix to use for subordinate lookups.
235     * @param suffix The suffix to use for subordinate lookups.
236     */
237    public ExprLookup(final Variables list, final String prefix, final String suffix) {
238        this(list);
239        setVariablePrefixMatcher(prefix);
240        setVariableSuffixMatcher(suffix);
241    }
242
243    /**
244     * Creates a new {@code JexlContext} and initializes it with the variables managed by this Lookup object.
245     *
246     * @return the newly created context
247     */
248    private JexlContext createContext() {
249        final JexlContext ctx = new MapContext();
250        initializeContext(ctx);
251        return ctx;
252    }
253
254    /**
255     * Gets the {@code ConfigurationInterpolator} used by this object.
256     *
257     * @return the {@code ConfigurationInterpolator}
258     * @since 2.0
259     */
260    public ConfigurationInterpolator getInterpolator() {
261        return interpolator;
262    }
263
264    /**
265     * Gets the logger used by this object.
266     *
267     * @return the {@code Log}
268     * @since 2.0
269     */
270    public ConfigurationLogger getLogger() {
271        return logger;
272    }
273
274    /**
275     * Gets the list of Variables that are accessible within expressions. This method returns a copy of the variables
276     * managed by this lookup; so modifying this object has no impact on this lookup.
277     *
278     * @return the List of Variables that are accessible within expressions.
279     */
280    public Variables getVariables() {
281        return new Variables(variables);
282    }
283
284    /**
285     * Initializes the specified context with the variables managed by this Lookup object.
286     *
287     * @param ctx the context to be initialized
288     */
289    private void initializeContext(final JexlContext ctx) {
290        variables.forEach(var -> ctx.set(var.getName(), var.getValue()));
291    }
292
293    /**
294     * Creates a {@code StringSubstitutor} object which uses the passed in {@code ConfigurationInterpolator} as lookup
295     * object.
296     *
297     * @param ip the {@code ConfigurationInterpolator} to be used
298     */
299    private void installSubstitutor(final ConfigurationInterpolator ip) {
300        if (ip == null) {
301            substitutor = null;
302        } else {
303            final StringLookup variableResolver = key -> Objects.toString(ip.resolve(key), null);
304            substitutor = new StringSubstitutor(variableResolver, prefixMatcher, suffixMatcher, StringSubstitutor.DEFAULT_ESCAPE);
305        }
306    }
307
308    /**
309     * Evaluates the expression.
310     *
311     * @param var The expression.
312     * @return The String result of the expression.
313     */
314    @Override
315    public String lookup(final String var) {
316        if (substitutor == null) {
317            return var;
318        }
319
320        String result = substitutor.replace(var);
321        try {
322            final Expression exp = engine.createExpression(result);
323            final Object exprResult = exp.evaluate(createContext());
324            result = exprResult != null ? String.valueOf(exprResult) : null;
325        } catch (final Exception e) {
326            final ConfigurationLogger l = getLogger();
327            if (l != null) {
328                l.debug("Error encountered evaluating " + result + ": " + e);
329            }
330        }
331
332        return result;
333    }
334
335    /**
336     * Sets the {@code ConfigurationInterpolator} to be used by this object.
337     *
338     * @param interpolator the {@code ConfigurationInterpolator} (may be <strong>null</strong>)
339     * @since 2.0
340     */
341    public void setInterpolator(final ConfigurationInterpolator interpolator) {
342        this.interpolator = interpolator;
343        installSubstitutor(interpolator);
344    }
345
346    /**
347     * Sets the logger to be used by this object. If no logger is passed in, no log output is generated.
348     *
349     * @param logger the {@code Log}
350     * @since 2.0
351     */
352    public void setLogger(final ConfigurationLogger logger) {
353        this.logger = logger;
354    }
355
356    /**
357     * Sets the prefix to use to identify subordinate expressions. This cannot be the same as the prefix used for the primary
358     * expression.
359     *
360     * @param prefix The String identifying the beginning of the expression.
361     */
362    public void setVariablePrefixMatcher(final String prefix) {
363        prefixMatcher = prefix;
364    }
365
366    /**
367     * Add the Variables that will be accessible within expressions.
368     *
369     * @param list The list of Variables.
370     */
371    public void setVariables(final Variables list) {
372        variables = new Variables(list);
373    }
374
375    /**
376     * Sets the suffix to use to identify subordinate expressions. This cannot be the same as the suffix used for the primary
377     * expression.
378     *
379     * @param suffix The String identifying the end of the expression.
380     */
381    public void setVariableSuffixMatcher(final String suffix) {
382        suffixMatcher = suffix;
383    }
384}