001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.commons.configuration2.interpol; 018 019import java.util.ArrayList; 020import java.util.Objects; 021 022import org.apache.commons.configuration2.ex.ConfigurationRuntimeException; 023import org.apache.commons.configuration2.io.ConfigurationLogger; 024import org.apache.commons.jexl2.Expression; 025import org.apache.commons.jexl2.JexlContext; 026import org.apache.commons.jexl2.JexlEngine; 027import org.apache.commons.jexl2.MapContext; 028import org.apache.commons.lang3.ClassUtils; 029import org.apache.commons.lang3.StringUtils; 030import org.apache.commons.text.StringSubstitutor; 031import org.apache.commons.text.lookup.StringLookup; 032 033/** 034 * Lookup that allows expressions to be evaluated. 035 * 036 * <pre> 037 * ExprLookup.Variables vars = new ExprLookup.Variables(); 038 * vars.add(new ExprLookup.Variable("String", org.apache.commons.lang.StringUtils.class)); 039 * vars.add(new ExprLookup.Variable("Util", new Utility("Hello"))); 040 * vars.add(new ExprLookup.Variable("System", "Class:java.lang.System")); 041 * XMLConfiguration config = new XMLConfiguration(TEST_FILE); 042 * config.setLogger(log); 043 * ExprLookup lookup = new ExprLookup(vars); 044 * lookup.setConfiguration(config); 045 * String str = lookup.lookup("'$[element] ' + String.trimToEmpty('$[space.description]')"); 046 * </pre> 047 * 048 * In the example above TEST_FILE contains xml that looks like: 049 * <pre> 050 * <configuration> 051 * <element>value</element> 052 * <space xml:space="preserve"> 053 * <description xml:space="default"> Some text </description> 054 * </space> 055 * </configuration> 056 * </pre> 057 * 058 * The result will be "value Some text". 059 * 060 * This lookup uses Apache Commons Jexl and requires that the dependency be added to any 061 * projects which use this. 062 * 063 * @since 1.7 064 */ 065public class ExprLookup implements Lookup 066{ 067 /** Prefix to identify a Java Class object */ 068 private static final String CLASS = "Class:"; 069 070 /** The default prefix for subordinate lookup expressions */ 071 private static final String DEFAULT_PREFIX = "$["; 072 073 /** The default suffix for subordinate lookup expressions */ 074 private static final String DEFAULT_SUFFIX = "]"; 075 076 /** The ConfigurationInterpolator used by this object. */ 077 private ConfigurationInterpolator interpolator; 078 079 /** The StringSubstitutor for performing replace operations. */ 080 private StringSubstitutor substitutor; 081 082 /** The logger used by this instance. */ 083 private ConfigurationLogger logger; 084 085 /** The engine. */ 086 private final JexlEngine engine = new JexlEngine(); 087 088 /** The variables maintained by this object. */ 089 private Variables variables; 090 091 /** The String to use to start subordinate lookup expressions */ 092 private String prefixMatcher = DEFAULT_PREFIX; 093 094 /** The String to use to terminate subordinate lookup expressions */ 095 private String suffixMatcher = DEFAULT_SUFFIX; 096 097 /** 098 * The default constructor. Will get used when the Lookup is constructed via 099 * configuration. 100 */ 101 public ExprLookup() 102 { 103 } 104 105 /** 106 * Constructor for use by applications. 107 * @param list The list of objects to be accessible in expressions. 108 */ 109 public ExprLookup(final Variables list) 110 { 111 setVariables(list); 112 } 113 114 /** 115 * Constructor for use by applications. 116 * @param list The list of objects to be accessible in expressions. 117 * @param prefix The prefix to use for subordinate lookups. 118 * @param suffix The suffix to use for subordinate lookups. 119 */ 120 public ExprLookup(final Variables list, final String prefix, final String suffix) 121 { 122 this(list); 123 setVariablePrefixMatcher(prefix); 124 setVariableSuffixMatcher(suffix); 125 } 126 127 /** 128 * Set the prefix to use to identify subordinate expressions. This cannot be the 129 * same as the prefix used for the primary expression. 130 * @param prefix The String identifying the beginning of the expression. 131 */ 132 public void setVariablePrefixMatcher(final String prefix) 133 { 134 prefixMatcher = prefix; 135 } 136 137 /** 138 * Set the suffix to use to identify subordinate expressions. This cannot be the 139 * same as the suffix used for the primary expression. 140 * @param suffix The String identifying the end of the expression. 141 */ 142 public void setVariableSuffixMatcher(final String suffix) 143 { 144 suffixMatcher = suffix; 145 } 146 147 /** 148 * Add the Variables that will be accessible within expressions. 149 * @param list The list of Variables. 150 */ 151 public void setVariables(final Variables list) 152 { 153 variables = new Variables(list); 154 } 155 156 /** 157 * Returns the list of Variables that are accessible within expressions. 158 * This method returns a copy of the variables managed by this lookup; so 159 * modifying this object has no impact on this lookup. 160 * 161 * @return the List of Variables that are accessible within expressions. 162 */ 163 public Variables getVariables() 164 { 165 return new Variables(variables); 166 } 167 168 /** 169 * Returns the logger used by this object. 170 * 171 * @return the {@code Log} 172 * @since 2.0 173 */ 174 public ConfigurationLogger getLogger() 175 { 176 return logger; 177 } 178 179 /** 180 * Sets the logger to be used by this object. If no logger is passed in, no 181 * log output is generated. 182 * 183 * @param logger the {@code Log} 184 * @since 2.0 185 */ 186 public void setLogger(final ConfigurationLogger logger) 187 { 188 this.logger = logger; 189 } 190 191 /** 192 * Returns the {@code ConfigurationInterpolator} used by this object. 193 * 194 * @return the {@code ConfigurationInterpolator} 195 * @since 2.0 196 */ 197 public ConfigurationInterpolator getInterpolator() 198 { 199 return interpolator; 200 } 201 202 /** 203 * Sets the {@code ConfigurationInterpolator} to be used by this object. 204 * 205 * @param interpolator the {@code ConfigurationInterpolator} (may be 206 * <b>null</b>) 207 * @since 2.0 208 */ 209 public void setInterpolator(final ConfigurationInterpolator interpolator) 210 { 211 this.interpolator = interpolator; 212 installSubstitutor(interpolator); 213 } 214 215 /** 216 * Evaluates the expression. 217 * @param var The expression. 218 * @return The String result of the expression. 219 */ 220 @Override 221 public String lookup(final String var) 222 { 223 if (substitutor == null) 224 { 225 return var; 226 } 227 228 String result = substitutor.replace(var); 229 try 230 { 231 final Expression exp = engine.createExpression(result); 232 final Object exprResult = exp.evaluate(createContext()); 233 result = exprResult != null ? String.valueOf(exprResult) : null; 234 } 235 catch (final Exception e) 236 { 237 final ConfigurationLogger l = getLogger(); 238 if (l != null) 239 { 240 l.debug("Error encountered evaluating " + result + ": " + e); 241 } 242 } 243 244 return result; 245 } 246 247 /** 248 * Creates a {@code StringSubstitutor} object which uses the passed in 249 * {@code ConfigurationInterpolator} as lookup object. 250 * 251 * @param ip the {@code ConfigurationInterpolator} to be used 252 */ 253 private void installSubstitutor(final ConfigurationInterpolator ip) 254 { 255 if (ip == null) 256 { 257 substitutor = null; 258 } 259 else 260 { 261 final StringLookup variableResolver = key -> Objects.toString(ip.resolve(key), null); 262 substitutor = 263 new StringSubstitutor(variableResolver, prefixMatcher, 264 suffixMatcher, StringSubstitutor.DEFAULT_ESCAPE); 265 } 266 } 267 268 /** 269 * Creates a new {@code JexlContext} and initializes it with the variables 270 * managed by this Lookup object. 271 * 272 * @return the newly created context 273 */ 274 private JexlContext createContext() 275 { 276 final JexlContext ctx = new MapContext(); 277 initializeContext(ctx); 278 return ctx; 279 } 280 281 /** 282 * Initializes the specified context with the variables managed by this 283 * Lookup object. 284 * 285 * @param ctx the context to be initialized 286 */ 287 private void initializeContext(final JexlContext ctx) 288 { 289 for (final Variable var : variables) 290 { 291 ctx.set(var.getName(), var.getValue()); 292 } 293 } 294 295 /** 296 * List wrapper used to allow the Variables list to be created as beans in 297 * DefaultConfigurationBuilder. 298 */ 299 public static class Variables extends ArrayList<Variable> 300 { 301 /** 302 * The serial version UID. 303 */ 304 private static final long serialVersionUID = 20111205L; 305 306 /** 307 * Creates a new empty instance of {@code Variables}. 308 */ 309 public Variables() 310 { 311 super(); 312 } 313 314 /** 315 * Creates a new instance of {@code Variables} and copies the content of 316 * the given object. 317 * 318 * @param vars the {@code Variables} object to be copied 319 */ 320 public Variables(final Variables vars) 321 { 322 super(vars); 323 } 324 325 public Variable getVariable() 326 { 327 return size() > 0 ? get(size() - 1) : null; 328 } 329 330 } 331 332 /** 333 * The key and corresponding object that will be made available to the 334 * JexlContext for use in expressions. 335 */ 336 public static class Variable 337 { 338 /** The name to be used in expressions */ 339 private String key; 340 341 /** The object to be accessed in expressions */ 342 private Object value; 343 344 public Variable() 345 { 346 } 347 348 public Variable(final String name, final Object value) 349 { 350 setName(name); 351 setValue(value); 352 } 353 354 public String getName() 355 { 356 return key; 357 } 358 359 public void setName(final String name) 360 { 361 this.key = name; 362 } 363 364 public Object getValue() 365 { 366 return value; 367 } 368 369 public void setValue(final Object value) throws ConfigurationRuntimeException 370 { 371 try 372 { 373 if (!(value instanceof String)) 374 { 375 this.value = value; 376 return; 377 } 378 final String val = (String) value; 379 final String name = StringUtils.removeStartIgnoreCase(val, CLASS); 380 final Class<?> clazz = ClassUtils.getClass(name); 381 if (name.length() == val.length()) 382 { 383 this.value = clazz.newInstance(); 384 } 385 else 386 { 387 this.value = clazz; 388 } 389 } 390 catch (final Exception e) 391 { 392 throw new ConfigurationRuntimeException("Unable to create " + value, e); 393 } 394 395 } 396 } 397}