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}