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-2014 ForgeRock AS.
016 */
017
018package org.forgerock.openig.el;
019
020import java.beans.FeatureDescriptor;
021import java.util.ArrayList;
022import java.util.Collections;
023import java.util.Iterator;
024import java.util.List;
025import java.util.Map;
026
027import javax.el.ELContext;
028import javax.el.ELException;
029import javax.el.ELResolver;
030import javax.el.FunctionMapper;
031import javax.el.ValueExpression;
032import javax.el.VariableMapper;
033
034import org.forgerock.openig.resolver.Resolver;
035import org.forgerock.openig.resolver.Resolvers;
036
037import de.odysseus.el.ExpressionFactoryImpl;
038
039/**
040 * An Unified Expression Language expression. Creating an expression is the equivalent to
041 * compiling it. Once created, an expression can be evaluated within a supplied scope. An
042 * expression can safely be evaluated concurrently in multiple threads.
043 */
044public class Expression {
045
046    /** The underlying EL expression(s) that this object represents. */
047    private final List<ValueExpression> valueExpression;
048
049    /**
050     * Constructs an expression for later evaluation.
051     *
052     * @param expression the expression to parse.
053     * @throws ExpressionException if the expression was not syntactically correct.
054     */
055    public Expression(String expression) throws ExpressionException {
056        try {
057            // An expression with no pattern will just return the original String so we will always have at least one
058            // item in the array.
059            String[] split = expression.split("[\\\\]");
060            valueExpression = new ArrayList<ValueExpression>(split.length);
061            for (String component : split) {
062                valueExpression.add(new ExpressionFactoryImpl().createValueExpression(
063                        new XLContext(null), component, Object.class));
064            }
065        } catch (ELException ele) {
066            throw new ExpressionException(ele);
067        }
068    }
069
070    /**
071     * Evaluates the expression within the specified scope and returns the resulting object, or
072     * {@code null} if it does not resolve a value.
073     *
074     * @param scope the scope to evaluate the expression within.
075     * @return the result of the expression evaluation, or {@code null} if does not resolve a value.
076     */
077    public Object eval(final Object scope) {
078
079        XLContext context = new XLContext(scope);
080
081        try {
082            // When there are multiple expressions to evaluate it is because original expression had \'s so result
083            // should include them back in again, the result will always be a String.
084            if (valueExpression.size() > 1) {
085                StringBuilder result = new StringBuilder();
086                for (ValueExpression expression : valueExpression) {
087                    if (result.length() > 0) {
088                        result.append("\\");
089                    }
090                    result.append(expression.getValue(context));
091                }
092                return result.toString();
093            } else {
094                return valueExpression.get(0).getValue(context);
095            }
096        } catch (ELException ele) {
097            // unresolved element yields null value
098            return null;
099        }
100    }
101
102    /**
103     * Evaluates the expression within the specified scope and returns the resulting object
104     * if it matches the specified type, or {@code null} if it does not resolve or match.
105     *
106     * @param scope the scope to evaluate the expression within.
107     * @param type the type of object the evaluation is expected to yield.
108     * @param <T> expected result type
109     * @return the result of the expression evaluation, or {@code null} if it does not resolve or match the type.
110     */
111    @SuppressWarnings("unchecked")
112    public <T> T eval(Object scope, Class<T> type) {
113        Object value = eval(scope);
114        return (value != null && type.isInstance(value) ? (T) value : null);
115    }
116
117    /**
118     * Sets the result of an evaluated expression to a specified value. The expression is
119     * treated as an <em>lvalue</em>, the expression resolves to an object whose value will be
120     * set. If the expression does not resolve to an object or cannot otherwise be written to
121     * (e.g. read-only), then this method will have no effect.
122     *
123     * @param scope the scope to evaluate the expression within.
124     * @param value the value to set in the result of the expression evaluation.
125     */
126    public void set(Object scope, Object value) {
127        try {
128            // cannot set multiple items, truncate the List
129            while (valueExpression.size() > 1) {
130                valueExpression.remove(1);
131            }
132            valueExpression.get(0).setValue(new XLContext(scope), value);
133        } catch (ELException ele) {
134            // unresolved elements are simply ignored
135        }
136    }
137
138    private static class XLContext extends ELContext {
139        private final ELResolver elResolver;
140        private final FunctionMapper fnMapper = new Functions();
141
142        public XLContext(Object scope) {
143            elResolver = new XLResolver(scope);
144        }
145
146        @Override
147        public ELResolver getELResolver() {
148            return elResolver;
149        }
150
151        @Override
152        public FunctionMapper getFunctionMapper() {
153            return fnMapper;
154        }
155
156        @Override
157        public VariableMapper getVariableMapper() {
158            return null;
159        }
160    }
161
162    private static class XLResolver extends ELResolver {
163        private final Object scope;
164
165        public XLResolver(final Object scope) {
166            // Resolvers.get() don't support null value
167            this.scope = (scope == null) ? new Object() : scope;
168        }
169
170        @Override
171        public Object getValue(ELContext context, Object base, Object property) {
172            context.setPropertyResolved(true);
173
174            // deal with readonly implicit objects
175            if (base == null) {
176                String name = property.toString();
177                if ("system".equals(name)) {
178                    return readOnlySystemProperties();
179                } else if ("env".equals(name)) {
180                    return readOnlyEnvironmentVariables();
181                }
182            }
183
184            Object value = Resolvers.get((base == null ? scope : base), property);
185            return (value != Resolver.UNRESOLVED ? value : null);
186        }
187
188        @Override
189        public Class<?> getType(ELContext context, Object base, Object property) {
190            context.setPropertyResolved(true);
191            return Object.class;
192        }
193
194        @Override
195        public void setValue(ELContext context, Object base, Object property, Object value) {
196            context.setPropertyResolved(true);
197            Resolvers.put((base == null ? scope : base), property, value);
198        }
199
200        @Override
201        public boolean isReadOnly(ELContext context, Object base, Object property) {
202            context.setPropertyResolved(true);
203            // attempts to write to read-only values are merely ignored
204            return false;
205        }
206
207        @Override
208        public Iterator<FeatureDescriptor> getFeatureDescriptors(ELContext context, Object base) {
209            return null;
210        }
211
212        @Override
213        public Class<?> getCommonPropertyType(ELContext context, Object base) {
214            return (base == null ? String.class : Object.class);
215        }
216
217        private Map<String, String> readOnlyEnvironmentVariables() {
218            return Collections.unmodifiableMap(System.getenv());
219        }
220
221        private Map<Object, Object> readOnlySystemProperties() {
222            return Collections.unmodifiableMap(System.getProperties());
223        }
224
225    }
226}