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