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