    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 *     https://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.Strings;
    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 * &lt;configuration&gt;
    052 *   &lt;element&gt;value&lt;/element&gt;
    053 *   &lt;space xml:space="preserve"&gt;
    054 *     &lt;description xml:space="default"&gt;     Some text      &lt;/description&gt;
    055 *   &lt;/space&gt;
    056 * &lt;/configuration&gt;
    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
    067    /**
    068     * The key and corresponding object that will be made available to the JexlContext for use in expressions.
    069     */
    070    public static class Variable {
    071
    072        /** The name to be used in expressions. */
    073        private String key;
    074
    075        /** The object to be accessed in expressions. */
    076        private Object value;
    077
    078        /**
    079         * Constructs a new instance.
    080         */
    081        public Variable() {
    082        }
    083
    084        /**
    085         * Constructs a new instance.
    086         *
    087         * @param name The name to be used in expressions.
    088         * @param value The object to be accessed in expressions.
    089         */
    090        public Variable(final String name, final Object value) {
    091            setName(name);
    092            setValue(value);
    093        }
    094
    095        /**
    096         * Gets the name to be used in expressions.
    097         *
    098         * @return the name to be used in expressions.
    099         */
    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}