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