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 * <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 /**
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 }