    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    /**
    067     * The key and corresponding object that will be made available to the JexlContext for use in expressions.
    068     */
    069    public static class Variable {
    070        /** The name to be used in expressions */
    071        private String key;
    072
    073        /** The object to be accessed in expressions */
    074        private Object value;
    075
    076        public Variable() {
    077        }
    078
    079        public Variable(final String name, final Object value) {
    080            setName(name);
    081            setValue(value);
    082        }
    083
    084        public String getName() {
    085            return key;
    086        }
    087
    088        public Object getValue() {
    089            return value;
    090        }
    091
    092        public void setName(final String name) {
    093            this.key = name;
    094        }
    095
    096        public void setValue(final Object value) throws ConfigurationRuntimeException {
    097            try {
    098                if (!(value instanceof String)) {
    099                    this.value = value;
    100                    return;
    101                }
    102                final String val = (String) value;
    103                final String name = StringUtils.removeStartIgnoreCase(val, CLASS);
    104                final Class<?> clazz = ClassUtils.getClass(name);
    105                if (name.length() == val.length()) {
    106                    this.value = clazz.getConstructor().newInstance();
    107                } else {
    108                    this.value = clazz;
    109                }
    110            } catch (final Exception e) {
    111                throw new ConfigurationRuntimeException("Unable to create " + value, e);
    112            }
    113
    114        }
    115    }
    116
    117    /**
    118     * List wrapper used to allow the Variables list to be created as beans in DefaultConfigurationBuilder.
    119     */
    120    public static class Variables extends ArrayList<Variable> {
    121        /**
    122         * The serial version UID.
    123         */
    124        private static final long serialVersionUID = 20111205L;
    125
    126        /**
    127         * Creates a new empty instance of {@code Variables}.
    128         */
    129        public Variables() {
    130        }
    131
    132        /**
    133         * Creates a new instance of {@code Variables} and copies the content of the given object.
    134         *
    135         * @param vars the {@code Variables} object to be copied
    136         */
    137        public Variables(final Variables vars) {
    138            super(vars);
    139        }
    140
    141        public Variable getVariable() {
    142            return !isEmpty() ? get(size() - 1) : null;
    143        }
    144
    145    }
    146
    147    /** Prefix to identify a Java Class object */
    148    private static final String CLASS = "Class:";
    149
    150    /** The default prefix for subordinate lookup expressions */
    151    private static final String DEFAULT_PREFIX = "$[";
    152
    153    /** The default suffix for subordinate lookup expressions */
    154    private static final String DEFAULT_SUFFIX = "]";
    155
    156    /** The ConfigurationInterpolator used by this object. */
    157    private ConfigurationInterpolator interpolator;
    158
    159    /** The StringSubstitutor for performing replace operations. */
    160    private StringSubstitutor substitutor;
    161
    162    /** The logger used by this instance. */
    163    private ConfigurationLogger logger;
    164
    165    /** The engine. */
    166    private final JexlEngine engine = new JexlEngine();
    167
    168    /** The variables maintained by this object. */
    169    private Variables variables;
    170
    171    /** The String to use to start subordinate lookup expressions */
    172    private String prefixMatcher = DEFAULT_PREFIX;
    173
    174    /** The String to use to terminate subordinate lookup expressions */
    175    private String suffixMatcher = DEFAULT_SUFFIX;
    176
    177    /**
    178     * The default constructor. Will get used when the Lookup is constructed via configuration.
    179     */
    180    public ExprLookup() {
    181    }
    182
    183    /**
    184     * Constructor for use by applications.
    185     *
    186     * @param list The list of objects to be accessible in expressions.
    187     */
    188    public ExprLookup(final Variables list) {
    189        setVariables(list);
    190    }
    191
    192    /**
    193     * Constructor for use by applications.
    194     *
    195     * @param list The list of objects to be accessible in expressions.
    196     * @param prefix The prefix to use for subordinate lookups.
    197     * @param suffix The suffix to use for subordinate lookups.
    198     */
    199    public ExprLookup(final Variables list, final String prefix, final String suffix) {
    200        this(list);
    201        setVariablePrefixMatcher(prefix);
    202        setVariableSuffixMatcher(suffix);
    203    }
    204
    205    /**
    206     * Creates a new {@code JexlContext} and initializes it with the variables managed by this Lookup object.
    207     *
    208     * @return the newly created context
    209     */
    210    private JexlContext createContext() {
    211        final JexlContext ctx = new MapContext();
    212        initializeContext(ctx);
    213        return ctx;
    214    }
    215
    216    /**
    217     * Gets the {@code ConfigurationInterpolator} used by this object.
    218     *
    219     * @return the {@code ConfigurationInterpolator}
    220     * @since 2.0
    221     */
    222    public ConfigurationInterpolator getInterpolator() {
    223        return interpolator;
    224    }
    225
    226    /**
    227     * Gets the logger used by this object.
    228     *
    229     * @return the {@code Log}
    230     * @since 2.0
    231     */
    232    public ConfigurationLogger getLogger() {
    233        return logger;
    234    }
    235
    236    /**
    237     * Gets the list of Variables that are accessible within expressions. This method returns a copy of the variables
    238     * managed by this lookup; so modifying this object has no impact on this lookup.
    239     *
    240     * @return the List of Variables that are accessible within expressions.
    241     */
    242    public Variables getVariables() {
    243        return new Variables(variables);
    244    }
    245
    246    /**
    247     * Initializes the specified context with the variables managed by this Lookup object.
    248     *
    249     * @param ctx the context to be initialized
    250     */
    251    private void initializeContext(final JexlContext ctx) {
    252        variables.forEach(var -> ctx.set(var.getName(), var.getValue()));
    253    }
    254
    255    /**
    256     * Creates a {@code StringSubstitutor} object which uses the passed in {@code ConfigurationInterpolator} as lookup
    257     * object.
    258     *
    259     * @param ip the {@code ConfigurationInterpolator} to be used
    260     */
    261    private void installSubstitutor(final ConfigurationInterpolator ip) {
    262        if (ip == null) {
    263            substitutor = null;
    264        } else {
    265            final StringLookup variableResolver = key -> Objects.toString(ip.resolve(key), null);
    266            substitutor = new StringSubstitutor(variableResolver, prefixMatcher, suffixMatcher, StringSubstitutor.DEFAULT_ESCAPE);
    267        }
    268    }
    269
    270    /**
    271     * Evaluates the expression.
    272     *
    273     * @param var The expression.
    274     * @return The String result of the expression.
    275     */
    276    @Override
    277    public String lookup(final String var) {
    278        if (substitutor == null) {
    279            return var;
    280        }
    281
    282        String result = substitutor.replace(var);
    283        try {
    284            final Expression exp = engine.createExpression(result);
    285            final Object exprResult = exp.evaluate(createContext());
    286            result = exprResult != null ? String.valueOf(exprResult) : null;
    287        } catch (final Exception e) {
    288            final ConfigurationLogger l = getLogger();
    289            if (l != null) {
    290                l.debug("Error encountered evaluating " + result + ": " + e);
    291            }
    292        }
    293
    294        return result;
    295    }
    296
    297    /**
    298     * Sets the {@code ConfigurationInterpolator} to be used by this object.
    299     *
    300     * @param interpolator the {@code ConfigurationInterpolator} (may be <b>null</b>)
    301     * @since 2.0
    302     */
    303    public void setInterpolator(final ConfigurationInterpolator interpolator) {
    304        this.interpolator = interpolator;
    305        installSubstitutor(interpolator);
    306    }
    307
    308    /**
    309     * Sets the logger to be used by this object. If no logger is passed in, no log output is generated.
    310     *
    311     * @param logger the {@code Log}
    312     * @since 2.0
    313     */
    314    public void setLogger(final ConfigurationLogger logger) {
    315        this.logger = logger;
    316    }
    317
    318    /**
    319     * Sets the prefix to use to identify subordinate expressions. This cannot be the same as the prefix used for the primary
    320     * expression.
    321     *
    322     * @param prefix The String identifying the beginning of the expression.
    323     */
    324    public void setVariablePrefixMatcher(final String prefix) {
    325        prefixMatcher = prefix;
    326    }
    327
    328    /**
    329     * Add the Variables that will be accessible within expressions.
    330     *
    331     * @param list The list of Variables.
    332     */
    333    public void setVariables(final Variables list) {
    334        variables = new Variables(list);
    335    }
    336
    337    /**
    338     * Sets the suffix to use to identify subordinate expressions. This cannot be the same as the suffix used for the primary
    339     * expression.
    340     *
    341     * @param suffix The String identifying the end of the expression.
    342     */
    343    public void setVariableSuffixMatcher(final String suffix) {
    344        suffixMatcher = suffix;
    345    }
    346}