001/*
002 * The contents of this file are subject to the terms of the Common Development and
003 * Distribution License (the License). You may not use this file except in compliance with the
004 * License.
005 *
006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
007 * specific language governing permission and limitations under the License.
008 *
009 * When distributing Covered Software, include this CDDL Header Notice in each file and include
010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
011 * Header, with the fields enclosed by brackets [] replaced by your own identifying
012 * information: "Portions Copyright [year] [name of copyright owner]".
013 *
014 * Copyright 2010-2011 ApexIdentity Inc.
015 * Portions Copyright 2011-2015 ForgeRock AS.
016 */
017
018package org.forgerock.openig.el;
019
020import static org.forgerock.openig.el.Bindings.bindings;
021
022import java.beans.FeatureDescriptor;
023import java.util.Collections;
024import java.util.Iterator;
025import java.util.Map;
026
027import javax.el.BeanELResolver;
028import javax.el.ELContext;
029import javax.el.ELException;
030import javax.el.ELResolver;
031import javax.el.FunctionMapper;
032import javax.el.ValueExpression;
033import javax.el.VariableMapper;
034
035import org.forgerock.http.util.Loader;
036import org.forgerock.openig.resolver.Resolver;
037import org.forgerock.openig.resolver.Resolvers;
038import org.forgerock.util.Reject;
039
040import de.odysseus.el.ExpressionFactoryImpl;
041
042/**
043 * An Unified Expression Language expression. Creating an expression is the equivalent to
044 * compiling it. Once created, an expression can be evaluated within a supplied scope. An
045 * expression can safely be evaluated concurrently in multiple threads.
046 *
047 * @param <T> expected result type
048 */
049public final class Expression<T> {
050
051    /** The underlying EL expression that this object represents. */
052    private final ValueExpression valueExpression;
053
054    /** The original string used to create this expression. */
055    private final String original;
056
057    /** The expected type of this expression. */
058    private Class<T> expectedType;
059
060    /** The expression plugins configured in META-INF/services. */
061    private static final Map<String, ExpressionPlugin> PLUGINS =
062            Collections.unmodifiableMap(Loader.loadMap(String.class, ExpressionPlugin.class));
063
064    /**
065     * Factory method to create an Expression.
066     *
067     * @param <T> expected result type
068     * @param expression The expression to parse.
069     * @param expectedType The expected result type of the expression.
070     * @return An expression based on the given string.
071     * @throws ExpressionException
072     *             if the expression was not syntactically correct.
073     */
074    public static <T> Expression<T> valueOf(String expression, Class<T> expectedType) throws ExpressionException {
075        return new Expression<>(expression, expectedType);
076    }
077
078    /**
079     * Constructs an expression for later evaluation.
080     *
081     * @param expression the expression to parse.
082     * @param expectedType The expected result type of the expression.
083     * @throws ExpressionException if the expression was not syntactically correct.
084     */
085    private Expression(String expression, Class<T> expectedType) throws ExpressionException {
086        original = expression;
087        this.expectedType = expectedType;
088        try {
089            ExpressionFactoryImpl exprFactory = new ExpressionFactoryImpl();
090            /*
091             * We still use Object.class but use the expectedType in the evaluation. If we use the expectedType instead
092             * of Object.class at the creation, then we had some breaking changes :
093             * - "not a boolean" as Boolean.class => before : null, after : false
094             * - "${null}" as String.class => before : null, after : the empty String
095             * - accessing a missing bean property as an Integer => before : null, after : 0
096             *
097             * But note that by still using Object.class prevents from using our own TypeConverter.
098             */
099            valueExpression = exprFactory.createValueExpression(new XLContext(null), expression, Object.class);
100        } catch (ELException ele) {
101            throw new ExpressionException(ele);
102        }
103    }
104
105    /**
106     * Evaluates the expression within the specified bindings and returns the resulting object if it matches the
107     * specified type, or {@code null} if it does not resolve or match.
108     *
109     * @param bindings
110     *            the bindings to evaluate the expression within.
111     * @return the result of the expression evaluation, or {@code null} if it does not resolve or match the type.
112     */
113    public T eval(final Bindings bindings) {
114        try {
115            Object value = valueExpression.getValue(new XLContext(bindings.asMap()));
116            return (value != null && expectedType.isInstance(value) ? expectedType.cast(value) : null);
117        } catch (ELException ele) {
118            // unresolved element yields null value
119            return null;
120        }
121
122    }
123
124    /**
125     * Convenient method to eval an Expression that does not need a scope.
126     * @return the result of the expression evaluation, or {@code null} if it does not resolve or match the type.
127     */
128    public T eval() {
129        return eval(bindings());
130    }
131
132    /**
133     * Sets the result of an evaluated expression to a specified value. The expression is
134     * treated as an <em>lvalue</em>, the expression resolves to an object whose value will be
135     * set. If the expression does not resolve to an object or cannot otherwise be written to
136     * (e.g. read-only), then this method will have no effect.
137     *
138     * @param bindings the bindings to evaluate the expression within.
139     * @param value the value to set in the result of the expression evaluation.
140     */
141    public void set(Bindings bindings, Object value) {
142        Reject.ifNull(bindings);
143        try {
144            valueExpression.setValue(new XLContext(bindings.asMap()), value);
145        } catch (ELException ele) {
146            // unresolved elements are simply ignored
147        }
148    }
149
150    private static class XLContext extends ELContext {
151        private final ELResolver elResolver;
152
153        public XLContext(Object scope) {
154            elResolver = new XLResolver(scope);
155        }
156
157        @Override
158        public ELResolver getELResolver() {
159            return elResolver;
160        }
161
162        @Override
163        public FunctionMapper getFunctionMapper() {
164            return MethodsMapper.INSTANCE;
165        }
166
167        @Override
168        public VariableMapper getVariableMapper() {
169            return null;
170        }
171    }
172
173    private static class XLResolver extends ELResolver {
174        private static final BeanELResolver RESOLVER = new BeanELResolver(true);
175        private final Object scope;
176
177        public XLResolver(final Object scope) {
178            // Resolvers.get() don't support null value
179            this.scope = (scope == null) ? new Object() : scope;
180        }
181
182        @Override
183        public Object getValue(ELContext context, Object base, Object property) {
184            context.setPropertyResolved(true);
185
186            // deal with readonly implicit objects
187            if (base == null) {
188                ExpressionPlugin node = Expression.PLUGINS.get(property.toString());
189                if (node != null) {
190                    return node.getObject();
191                }
192            }
193
194            Object value = Resolvers.get((base == null ? scope : base), property);
195            return (value != Resolver.UNRESOLVED ? value : null);
196        }
197
198        @Override
199        public Class<?> getType(ELContext context, Object base, Object property) {
200            context.setPropertyResolved(true);
201            return Object.class;
202        }
203
204        @Override
205        public void setValue(ELContext context, Object base, Object property, Object value) {
206            context.setPropertyResolved(true);
207            Resolvers.put((base == null ? scope : base), property, value);
208        }
209
210        @Override
211        public boolean isReadOnly(ELContext context, Object base, Object property) {
212            context.setPropertyResolved(true);
213            // attempts to write to read-only values are merely ignored
214            return false;
215        }
216
217        @Override
218        public Iterator<FeatureDescriptor> getFeatureDescriptors(ELContext context, Object base) {
219            return null;
220        }
221
222        @Override
223        public Class<?> getCommonPropertyType(ELContext context, Object base) {
224            return (base == null ? String.class : Object.class);
225        }
226
227        @Override
228        public Object invoke(final ELContext context,
229                             final Object base,
230                             final Object method,
231                             final Class<?>[] paramTypes,
232                             final Object[] params) {
233            return RESOLVER.invoke(context, base, method, paramTypes, params);
234        }
235    }
236
237    /**
238     * Returns the original string used to create this expression, unmodified.
239     * <p>
240     * Note to implementors: That returned value must be usable in
241     * Expression.valueOf() to create an equivalent Expression(somehow cloning
242     * this instance)
243     * </p>
244     *
245     * @return The original string used to create this expression.
246     */
247    @Override
248    public String toString() {
249        return original;
250    }
251}