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 * 050 * <pre> 051 * <configuration> 052 * <element>value</element> 053 * <space xml:space="preserve"> 054 * <description xml:space="default"> Some text </description> 055 * </space> 056 * </configuration> 057 * </pre> 058 * 059 * The result will be "value Some text". 060 * 061 * This lookup uses Apache Commons Jexl and requires that the dependency be added to any projects which use this. 062 * 063 * @since 1.7 064 */ 065public class ExprLookup implements Lookup { 066 /** Prefix to identify a Java Class object */ 067 private static final String CLASS = "Class:"; 068 069 /** The default prefix for subordinate lookup expressions */ 070 private static final String DEFAULT_PREFIX = "$["; 071 072 /** The default suffix for subordinate lookup expressions */ 073 private static final String DEFAULT_SUFFIX = "]"; 074 075 /** The ConfigurationInterpolator used by this object. */ 076 private ConfigurationInterpolator interpolator; 077 078 /** The StringSubstitutor for performing replace operations. */ 079 private StringSubstitutor substitutor; 080 081 /** The logger used by this instance. */ 082 private ConfigurationLogger logger; 083 084 /** The engine. */ 085 private final JexlEngine engine = new JexlEngine(); 086 087 /** The variables maintained by this object. */ 088 private Variables variables; 089 090 /** The String to use to start subordinate lookup expressions */ 091 private String prefixMatcher = DEFAULT_PREFIX; 092 093 /** The String to use to terminate subordinate lookup expressions */ 094 private String suffixMatcher = DEFAULT_SUFFIX; 095 096 /** 097 * The default constructor. Will get used when the Lookup is constructed via configuration. 098 */ 099 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}