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