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