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