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.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 * <configuration>
52 * <element>value</element>
53 * <space xml:space="preserve">
54 * <description xml:space="default"> Some text </description>
55 * </space>
56 * </configuration>
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 }