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