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}