    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;
    020
    021import org.apache.commons.configuration2.ex.ConfigurationRuntimeException;
    022import org.apache.commons.configuration2.io.ConfigurationLogger;
    023import org.apache.commons.jexl2.Expression;
    024import org.apache.commons.jexl2.JexlContext;
    025import org.apache.commons.jexl2.JexlEngine;
    026import org.apache.commons.jexl2.MapContext;
    027import org.apache.commons.lang3.ClassUtils;
    028import org.apache.commons.lang3.StringUtils;
    029import org.apache.commons.text.StringSubstitutor;
    030import org.apache.commons.text.lookup.StringLookup;
    031
    032/**
    033 * Lookup that allows expressions to be evaluated.
    034 *
    035 * <pre>
    036 *     ExprLookup.Variables vars = new ExprLookup.Variables();
    037 *     vars.add(new ExprLookup.Variable("String", org.apache.commons.lang.StringUtils.class));
    038 *     vars.add(new ExprLookup.Variable("Util", new Utility("Hello")));
    039 *     vars.add(new ExprLookup.Variable("System", "Class:java.lang.System"));
    040 *     XMLConfiguration config = new XMLConfiguration(TEST_FILE);
    041 *     config.setLogger(log);
    042 *     ExprLookup lookup = new ExprLookup(vars);
    043 *     lookup.setConfiguration(config);
    044 *     String str = lookup.lookup("'$[element] ' + String.trimToEmpty('$[space.description]')");
    045 * </pre>
    046 *
    047 * In the example above TEST_FILE contains xml that looks like:
    048 * <pre>
    049 * &lt;configuration&gt;
    050 *   &lt;element&gt;value&lt;/element&gt;
    051 *   &lt;space xml:space="preserve"&gt;
    052 *     &lt;description xml:space="default"&gt;     Some text      &lt;/description&gt;
    053 *   &lt;/space&gt;
    054 * &lt;/configuration&gt;
    055 * </pre>
    056 *
    057 * The result will be "value Some text".
    058 *
    059 * This lookup uses Apache Commons Jexl and requires that the dependency be added to any
    060 * projects which use this.
    061 *
    062 * @since 1.7
    063 * @author <a
    064 * href="http://commons.apache.org/configuration/team-list.html">Commons Configuration team</a>
    065 * @version $Id: ExprLookup.java 1842196 2018-09-27 22:30:20Z ggregory $
    066 */
    067public class ExprLookup implements Lookup
    068{
    069    /** Prefix to identify a Java Class object */
    070    private static final String CLASS = "Class:";
    071
    072    /** The default prefix for subordinate lookup expressions */
    073    private static final String DEFAULT_PREFIX = "$[";
    074
    075    /** The default suffix for subordinate lookup expressions */
    076    private static final String DEFAULT_SUFFIX = "]";
    077
    078    /** The ConfigurationInterpolator used by this object. */
    079    private ConfigurationInterpolator interpolator;
    080
    081    /** The StringSubstitutor for performing replace operations. */
    082    private StringSubstitutor substitutor;
    083
    084    /** The logger used by this instance. */
    085    private ConfigurationLogger logger;
    086
    087    /** The engine. */
    088    private final JexlEngine engine = new JexlEngine();
    089
    090    /** The variables maintained by this object. */
    091    private Variables variables;
    092
    093    /** The String to use to start subordinate lookup expressions */
    094    private String prefixMatcher = DEFAULT_PREFIX;
    095
    096    /** The String to use to terminate subordinate lookup expressions */
    097    private String suffixMatcher = DEFAULT_SUFFIX;
    098
    099    /**
    100     * The default constructor. Will get used when the Lookup is constructed via
    101     * configuration.
    102     */
    103    public ExprLookup()
    104    {
    105    }
    106
    107    /**
    108     * Constructor for use by applications.
    109     * @param list The list of objects to be accessible in expressions.
    110     */
    111    public ExprLookup(final Variables list)
    112    {
    113        setVariables(list);
    114    }
    115
    116    /**
    117     * Constructor for use by applications.
    118     * @param list The list of objects to be accessible in expressions.
    119     * @param prefix The prefix to use for subordinate lookups.
    120     * @param suffix The suffix to use for subordinate lookups.
    121     */
    122    public ExprLookup(final Variables list, final String prefix, final String suffix)
    123    {
    124        this(list);
    125        setVariablePrefixMatcher(prefix);
    126        setVariableSuffixMatcher(suffix);
    127    }
    128
    129    /**
    130     * Set the prefix to use to identify subordinate expressions. This cannot be the
    131     * same as the prefix used for the primary expression.
    132     * @param prefix The String identifying the beginning of the expression.
    133     */
    134    public void setVariablePrefixMatcher(final String prefix)
    135    {
    136        prefixMatcher = prefix;
    137    }
    138
    139    /**
    140     * Set the suffix to use to identify subordinate expressions. This cannot be the
    141     * same as the suffix used for the primary expression.
    142     * @param suffix The String identifying the end of the expression.
    143     */
    144    public void setVariableSuffixMatcher(final String suffix)
    145    {
    146        suffixMatcher = suffix;
    147    }
    148
    149    /**
    150     * Add the Variables that will be accessible within expressions.
    151     * @param list The list of Variables.
    152     */
    153    public void setVariables(final Variables list)
    154    {
    155        variables = new Variables(list);
    156    }
    157
    158    /**
    159     * Returns the list of Variables that are accessible within expressions.
    160     * This method returns a copy of the variables managed by this lookup; so
    161     * modifying this object has no impact on this lookup.
    162     *
    163     * @return the List of Variables that are accessible within expressions.
    164     */
    165    public Variables getVariables()
    166    {
    167        return new Variables(variables);
    168    }
    169
    170    /**
    171     * Returns the logger used by this object.
    172     *
    173     * @return the {@code Log}
    174     * @since 2.0
    175     */
    176    public ConfigurationLogger getLogger()
    177    {
    178        return logger;
    179    }
    180
    181    /**
    182     * Sets the logger to be used by this object. If no logger is passed in, no
    183     * log output is generated.
    184     *
    185     * @param logger the {@code Log}
    186     * @since 2.0
    187     */
    188    public void setLogger(final ConfigurationLogger logger)
    189    {
    190        this.logger = logger;
    191    }
    192
    193    /**
    194     * Returns the {@code ConfigurationInterpolator} used by this object.
    195     *
    196     * @return the {@code ConfigurationInterpolator}
    197     * @since 2.0
    198     */
    199    public ConfigurationInterpolator getInterpolator()
    200    {
    201        return interpolator;
    202    }
    203
    204    /**
    205     * Sets the {@code ConfigurationInterpolator} to be used by this object.
    206     *
    207     * @param interpolator the {@code ConfigurationInterpolator} (may be
    208     *        <b>null</b>)
    209     * @since 2.0
    210     */
    211    public void setInterpolator(final ConfigurationInterpolator interpolator)
    212    {
    213        this.interpolator = interpolator;
    214        installSubstitutor(interpolator);
    215    }
    216
    217    /**
    218     * Evaluates the expression.
    219     * @param var The expression.
    220     * @return The String result of the expression.
    221     */
    222    @Override
    223    public String lookup(final String var)
    224    {
    225        if (substitutor == null)
    226        {
    227            return var;
    228        }
    229
    230        String result = substitutor.replace(var);
    231        try
    232        {
    233            final Expression exp = engine.createExpression(result);
    234            final Object exprResult = exp.evaluate(createContext());
    235            result = (exprResult != null) ? String.valueOf(exprResult) : null;
    236        }
    237        catch (final Exception e)
    238        {
    239            final ConfigurationLogger l = getLogger();
    240            if (l != null)
    241            {
    242                l.debug("Error encountered evaluating " + result + ": " + e);
    243            }
    244        }
    245
    246        return result;
    247    }
    248
    249    /**
    250     * Creates a {@code StringSubstitutor} object which uses the passed in
    251     * {@code ConfigurationInterpolator} as lookup object.
    252     *
    253     * @param ip the {@code ConfigurationInterpolator} to be used
    254     */
    255    private void installSubstitutor(final ConfigurationInterpolator ip)
    256    {
    257        if (ip == null)
    258        {
    259            substitutor = null;
    260        }
    261        else
    262        {
    263            final StringLookup variableResolver = new StringLookup()
    264            {
    265                @Override
    266                public String lookup(final String key)
    267                {
    268                    final Object value = ip.resolve(key);
    269                    return value != null ? value.toString() : null;
    270                }
    271            };
    272            substitutor =
    273                    new StringSubstitutor(variableResolver, prefixMatcher,
    274                            suffixMatcher, StringSubstitutor.DEFAULT_ESCAPE);
    275        }
    276    }
    277
    278    /**
    279     * Creates a new {@code JexlContext} and initializes it with the variables
    280     * managed by this Lookup object.
    281     *
    282     * @return the newly created context
    283     */
    284    private JexlContext createContext()
    285    {
    286        final JexlContext ctx = new MapContext();
    287        initializeContext(ctx);
    288        return ctx;
    289    }
    290
    291    /**
    292     * Initializes the specified context with the variables managed by this
    293     * Lookup object.
    294     *
    295     * @param ctx the context to be initialized
    296     */
    297    private void initializeContext(final JexlContext ctx)
    298    {
    299        for (final Variable var : variables)
    300        {
    301            ctx.set(var.getName(), var.getValue());
    302        }
    303    }
    304
    305    /**
    306     * List wrapper used to allow the Variables list to be created as beans in
    307     * DefaultConfigurationBuilder.
    308     */
    309    public static class Variables extends ArrayList<Variable>
    310    {
    311        /**
    312         * The serial version UID.
    313         */
    314        private static final long serialVersionUID = 20111205L;
    315
    316        /**
    317         * Creates a new empty instance of {@code Variables}.
    318         */
    319        public Variables()
    320        {
    321            super();
    322        }
    323
    324        /**
    325         * Creates a new instance of {@code Variables} and copies the content of
    326         * the given object.
    327         *
    328         * @param vars the {@code Variables} object to be copied
    329         */
    330        public Variables(final Variables vars)
    331        {
    332            super(vars);
    333        }
    334
    335        public Variable getVariable()
    336        {
    337            return size() > 0 ? get(size() - 1) : null;
    338        }
    339
    340    }
    341
    342    /**
    343     * The key and corresponding object that will be made available to the
    344     * JexlContext for use in expressions.
    345     */
    346    public static class Variable
    347    {
    348        /** The name to be used in expressions */
    349        private String key;
    350
    351        /** The object to be accessed in expressions */
    352        private Object value;
    353
    354        public Variable()
    355        {
    356        }
    357
    358        public Variable(final String name, final Object value)
    359        {
    360            setName(name);
    361            setValue(value);
    362        }
    363
    364        public String getName()
    365        {
    366            return key;
    367        }
    368
    369        public void setName(final String name)
    370        {
    371            this.key = name;
    372        }
    373
    374        public Object getValue()
    375        {
    376            return value;
    377        }
    378
    379        public void setValue(final Object value) throws ConfigurationRuntimeException
    380        {
    381            try
    382            {
    383                if (!(value instanceof String))
    384                {
    385                    this.value = value;
    386                    return;
    387                }
    388                final String val = (String) value;
    389                final String name = StringUtils.removeStartIgnoreCase(val, CLASS);
    390                final Class<?> clazz = ClassUtils.getClass(name);
    391                if (name.length() == val.length())
    392                {
    393                    this.value = clazz.newInstance();
    394                }
    395                else
    396                {
    397                    this.value = clazz;
    398                }
    399            }
    400            catch (final Exception e)
    401            {
    402                throw new ConfigurationRuntimeException("Unable to create " + value, e);
    403            }
    404
    405        }
    406    }
    407}