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