View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *     http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.configuration2.interpol;
18  
19  import java.util.ArrayList;
20  import java.util.Objects;
21  
22  import org.apache.commons.configuration2.ex.ConfigurationRuntimeException;
23  import org.apache.commons.configuration2.io.ConfigurationLogger;
24  import org.apache.commons.jexl2.Expression;
25  import org.apache.commons.jexl2.JexlContext;
26  import org.apache.commons.jexl2.JexlEngine;
27  import org.apache.commons.jexl2.MapContext;
28  import org.apache.commons.lang3.ClassUtils;
29  import org.apache.commons.lang3.StringUtils;
30  import org.apache.commons.text.StringSubstitutor;
31  import org.apache.commons.text.lookup.StringLookup;
32  
33  /**
34   * Lookup that allows expressions to be evaluated.
35   *
36   * <pre>
37   * ExprLookup.Variables vars = new ExprLookup.Variables();
38   * vars.add(new ExprLookup.Variable("String", org.apache.commons.lang.StringUtils.class));
39   * vars.add(new ExprLookup.Variable("Util", new Utility("Hello")));
40   * vars.add(new ExprLookup.Variable("System", "Class:java.lang.System"));
41   * XMLConfiguration config = new XMLConfiguration(TEST_FILE);
42   * config.setLogger(log);
43   * ExprLookup lookup = new ExprLookup(vars);
44   * lookup.setConfiguration(config);
45   * String str = lookup.lookup("'$[element] ' + String.trimToEmpty('$[space.description]')");
46   * </pre>
47   *
48   * In the example above TEST_FILE contains xml that looks like:
49   *
50   * <pre>
51   * &lt;configuration&gt;
52   *   &lt;element&gt;value&lt;/element&gt;
53   *   &lt;space xml:space="preserve"&gt;
54   *     &lt;description xml:space="default"&gt;     Some text      &lt;/description&gt;
55   *   &lt;/space&gt;
56   * &lt;/configuration&gt;
57   * </pre>
58   *
59   * The result will be "value Some text".
60   *
61   * This lookup uses Apache Commons Jexl and requires that the dependency be added to any projects which use this.
62   *
63   * @since 1.7
64   */
65  public class ExprLookup implements Lookup {
66      /**
67       * The key and corresponding object that will be made available to the JexlContext for use in expressions.
68       */
69      public static class Variable {
70          /** The name to be used in expressions. */
71          private String key;
72  
73          /** The object to be accessed in expressions. */
74          private Object value;
75  
76          /**
77           * Constructs a new instance.
78           */
79          public Variable() {
80          }
81  
82          /**
83           * Constructs a new instance.
84           *
85           * @param name The name to be used in expressions.
86           * @param value The object to be accessed in expressions.
87           */
88          public Variable(final String name, final Object value) {
89              setName(name);
90              setValue(value);
91          }
92  
93          /**
94           * Gets the name to be used in expressions.
95           *
96           * @return the name to be used in expressions.
97           */
98          public String getName() {
99              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 }