    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 * &lt;configuration&gt;
    051 *   &lt;element&gt;value&lt;/element&gt;
    052 *   &lt;space xml:space="preserve"&gt;
    053 *     &lt;description xml:space="default"&gt;     Some text      &lt;/description&gt;
    054 *   &lt;/space&gt;
    055 * &lt;/configuration&gt;
    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}