1 /*
2 * Copyright (c) 2002-2006 by OpenSymphony
3 * All rights reserved.
4 */
5 package com.opensymphony.xwork2.ognl;
6
7 import java.io.Serializable;
8 import java.util.HashMap;
9 import java.util.Map;
10 import java.util.Set;
11 import java.util.LinkedHashSet;
12 import java.beans.IntrospectionException;
13 import java.beans.PropertyDescriptor;
14 import java.lang.reflect.Method;
15
16 import ognl.Ognl;
17 import ognl.OgnlContext;
18 import ognl.OgnlException;
19 import ognl.PropertyAccessor;
20
21 import com.opensymphony.xwork2.ActionContext;
22 import com.opensymphony.xwork2.TextProvider;
23 import com.opensymphony.xwork2.XWorkException;
24 import com.opensymphony.xwork2.ActionSupport;
25 import com.opensymphony.xwork2.conversion.impl.XWorkConverter;
26 import com.opensymphony.xwork2.inject.Container;
27 import com.opensymphony.xwork2.inject.Inject;
28 import com.opensymphony.xwork2.ognl.accessor.CompoundRootAccessor;
29 import com.opensymphony.xwork2.util.CompoundRoot;
30 import com.opensymphony.xwork2.util.ValueStack;
31 import com.opensymphony.xwork2.util.logging.Logger;
32 import com.opensymphony.xwork2.util.logging.LoggerFactory;
33 import com.opensymphony.xwork2.util.logging.LoggerUtils;
34 import com.opensymphony.xwork2.util.reflection.ReflectionContextState;
35
36 /**
37 * Ognl implementation of a value stack that allows for dynamic Ognl expressions to be evaluated against it. When
38 * evaluating an expression, the stack will be searched down the stack, from the latest objects pushed in to the
39 * earliest, looking for a bean with a getter or setter for the given property or a method of the given name (depending
40 * on the expression being evaluated).
41 *
42 * @author Patrick Lightbody
43 * @author tm_jee
44 * @version $Date: 2007-10-15 19:04:43 +0200 (Mon, 15 Oct 2007) $ $Id: OgnlValueStack.java 1647 2007-10-15 17:04:43Z rainerh $
45 */
46 public class OgnlValueStack implements Serializable, ValueStack {
47
48 private static final long serialVersionUID = 370737852934925530L;
49
50 private static Logger LOG = LoggerFactory.getLogger(OgnlValueStack.class);
51 private boolean devMode;
52
53 public static void link(Map context, Class clazz, String name) {
54 context.put("__link", new Object[]{clazz, name});
55 }
56
57
58 CompoundRoot root;
59 transient Map context;
60 Class defaultType;
61 Map overrides;
62 transient OgnlUtil ognlUtil;
63
64 protected OgnlValueStack(XWorkConverter xworkConverter, CompoundRootAccessor accessor, TextProvider prov, boolean allowStaticAccess) {
65 setRoot(xworkConverter, accessor, new CompoundRoot(), allowStaticAccess);
66 push(prov);
67 }
68
69
70 protected OgnlValueStack(ValueStack vs, XWorkConverter xworkConverter, CompoundRootAccessor accessor, boolean allowStaticAccess) {
71 setRoot(xworkConverter, accessor, new CompoundRoot(vs.getRoot()), allowStaticAccess);
72 }
73
74 @Inject
75 public void setOgnlUtil(OgnlUtil ognlUtil) {
76 this.ognlUtil = ognlUtil;
77 }
78
79 protected void setRoot(XWorkConverter xworkConverter,
80 CompoundRootAccessor accessor, CompoundRoot compoundRoot, boolean allowStaticMethodAccess) {
81 this.root = compoundRoot;
82 this.context = Ognl.createDefaultContext(this.root, accessor, new OgnlTypeConverterWrapper(xworkConverter),
83 new StaticMemberAccess(allowStaticMethodAccess));
84 context.put(VALUE_STACK, this);
85 Ognl.setClassResolver(context, accessor);
86 ((OgnlContext) context).setTraceEvaluations(false);
87 ((OgnlContext) context).setKeepLastEvaluation(false);
88 }
89
90 @Inject("devMode")
91 public void setDevMode(String mode) {
92 devMode = "true".equalsIgnoreCase(mode);
93 }
94
95 /* (non-Javadoc)
96 * @see com.opensymphony.xwork2.util.ValueStack#getContext()
97 */
98 public Map getContext() {
99 return context;
100 }
101
102 /* (non-Javadoc)
103 * @see com.opensymphony.xwork2.util.ValueStack#setDefaultType(java.lang.Class)
104 */
105 public void setDefaultType(Class defaultType) {
106 this.defaultType = defaultType;
107 }
108
109 /* (non-Javadoc)
110 * @see com.opensymphony.xwork2.util.ValueStack#setExprOverrides(java.util.Map)
111 */
112 public void setExprOverrides(Map overrides) {
113 if (this.overrides == null) {
114 this.overrides = overrides;
115 } else {
116 this.overrides.putAll(overrides);
117 }
118 }
119
120 /* (non-Javadoc)
121 * @see com.opensymphony.xwork2.util.ValueStack#getExprOverrides()
122 */
123 public Map getExprOverrides() {
124 return this.overrides;
125 }
126
127 /* (non-Javadoc)
128 * @see com.opensymphony.xwork2.util.ValueStack#getRoot()
129 */
130 public CompoundRoot getRoot() {
131 return root;
132 }
133
134 /* (non-Javadoc)
135 * @see com.opensymphony.xwork2.util.ValueStack#setValue(java.lang.String, java.lang.Object)
136 */
137 public void setValue(String expr, Object value) {
138 setValue(expr, value, devMode);
139 }
140
141 /* (non-Javadoc)
142 * @see com.opensymphony.xwork2.util.ValueStack#setValue(java.lang.String, java.lang.Object, boolean)
143 */
144 public void setValue(String expr, Object value, boolean throwExceptionOnFailure) {
145 Map context = getContext();
146
147 try {
148 context.put(XWorkConverter.CONVERSION_PROPERTY_FULLNAME, expr);
149 context.put(REPORT_ERRORS_ON_NO_PROP, (throwExceptionOnFailure) ? Boolean.TRUE : Boolean.FALSE);
150 ognlUtil.setValue(expr, context, root, value);
151 } catch (OgnlException e) {
152 if (throwExceptionOnFailure) {
153 e.printStackTrace(System.out);
154 System.out.println("expr: " + expr + " val: " + value + " context: " + context + " root:" + root + " value: " + value);
155 String msg = "Error setting expression '" + expr + "' with value '" + value + "'";
156 throw new XWorkException(msg, e);
157 } else {
158 if (LOG.isDebugEnabled()) {
159 LOG.debug("Error setting value", e);
160 }
161 }
162 } catch (RuntimeException re) { //XW-281
163 if (throwExceptionOnFailure) {
164 StringBuffer msg = new StringBuffer();
165 msg.append("Error setting expression '");
166 msg.append(expr);
167 msg.append("' with value ");
168
169 if (value instanceof Object[]) {
170 Object[] valueArray = (Object[]) value;
171 msg.append("[");
172 for (int index = 0; index < valueArray.length; index++) {
173 msg.append("'");
174 msg.append(valueArray[index]);
175 msg.append("'");
176
177 if (index < (valueArray.length + 1))
178 msg.append(", ");
179 }
180 msg.append("]");
181 } else {
182 msg.append("'");
183 msg.append(value);
184 msg.append("'");
185 }
186
187 throw new XWorkException(msg.toString(), re);
188 } else {
189 if (LOG.isDebugEnabled()) {
190 LOG.debug("Error setting value", re);
191 }
192 }
193 } finally {
194 ReflectionContextState.clear(context);
195 context.remove(XWorkConverter.CONVERSION_PROPERTY_FULLNAME);
196 context.remove(REPORT_ERRORS_ON_NO_PROP);
197 }
198 }
199
200 /* (non-Javadoc)
201 * @see com.opensymphony.xwork2.util.ValueStack#findString(java.lang.String)
202 */
203 public String findString(String expr) {
204 return (String) findValue(expr, String.class);
205 }
206
207 /* (non-Javadoc)
208 * @see com.opensymphony.xwork2.util.ValueStack#findValue(java.lang.String)
209 */
210 public Object findValue(String expr) {
211 try {
212 if (expr == null) {
213 return null;
214 }
215
216 if ((overrides != null) && overrides.containsKey(expr)) {
217 expr = (String) overrides.get(expr);
218 }
219
220 if (defaultType != null) {
221 return findValue(expr, defaultType);
222 }
223
224 Object value = ognlUtil.getValue(expr, context, root);
225 if (value != null) {
226 return value;
227 } else {
228 checkForInvalidProperties(expr);
229 return findInContext(expr);
230 }
231 } catch (OgnlException e) {
232 checkForInvalidProperties(expr);
233 return findInContext(expr);
234 } catch (Exception e) {
235 logLookupFailure(expr, e);
236
237 return findInContext(expr);
238 } finally {
239 ReflectionContextState.clear(context);
240 }
241 }
242
243 /* (non-Javadoc)
244 * @see com.opensymphony.xwork2.util.ValueStack#findValue(java.lang.String, java.lang.Class)
245 */
246 public Object findValue(String expr, Class asType) {
247 try {
248 if (expr == null) {
249 return null;
250 }
251
252 if ((overrides != null) && overrides.containsKey(expr)) {
253 expr = (String) overrides.get(expr);
254 }
255
256 Object value = ognlUtil.getValue(expr, context, root, asType);
257 if (value != null) {
258 return value;
259 } else {
260 return findInContext(expr);
261 }
262 } catch (OgnlException e) {
263 return findInContext(expr);
264 } catch (Exception e) {
265 logLookupFailure(expr, e);
266
267 return findInContext(expr);
268 } finally {
269 ReflectionContextState.clear(context);
270 }
271 }
272
273 private Object findInContext(String name) {
274 return getContext().get(name);
275 }
276
277
278 /**
279 * This method looks for matching methods/properties in an action to warn the user if
280 * they specified a property that doesn't exist.
281 * @param expr the property expression
282 */
283 private void checkForInvalidProperties(String expr) {
284 if (expr.contains("(") && expr.contains(")")) {
285 LOG.warn("Could not find method [" + expr + "]");
286 } else if (findInContext(expr) == null) {
287 // find objects with Action in them and inspect matching getters
288 Set availableProperties = new LinkedHashSet();
289 for (Object o : root) {
290 if (o instanceof ActionSupport || o.getClass().getSimpleName().endsWith("Action")) {
291 try {
292 findAvailableProperties(o.getClass(), expr, availableProperties, null);
293 } catch (IntrospectionException ise) {
294 // ignore
295 }
296 }
297 }
298 if (!availableProperties.contains(expr)) {
299 LOG.warn("Could not find property [" + expr + "]");
300 }
301 }
302 }
303
304 /**
305 * Look for available properties on an existing class.
306 * @param c the class to search on
307 * @param expr the property expression
308 * @param availableProperties a set of properties found
309 * @param parent a parent property
310 * @throws IntrospectionException when Ognl can't get property descriptors
311 */
312 private void findAvailableProperties(Class c, String expr, Set availableProperties, String parent) throws IntrospectionException {
313 PropertyDescriptor[] descriptors = ognlUtil.getPropertyDescriptors(c);
314 for (PropertyDescriptor pd : descriptors) {
315 String name = pd.getDisplayName();
316 if (parent != null && expr.indexOf(".") > -1) {
317 name = expr.substring(0, expr.indexOf(".") + 1) + name;
318 }
319 if (expr.startsWith(name)) {
320 availableProperties.add((parent != null) ? parent + "." + name : name);
321 if (expr.equals(name)) break; // no need to go any further
322 if (expr.indexOf(".") > -1) {
323 String property = expr.substring(expr.indexOf(".") + 1);
324 // if there is a nested property (indicated by a dot), chop it off so we can look for method name
325 String rawProperty = (property.indexOf(".") > -1) ? property.substring(0, property.indexOf(".")) : property;
326 String methodToLookFor = "get" + rawProperty.substring(0, 1).toUpperCase() + rawProperty.substring(1);
327 Method[] methods = pd.getPropertyType().getDeclaredMethods();
328 for (Method method : methods) {
329 if (method.getName().equals(methodToLookFor)) {
330 availableProperties.add(name + "." + rawProperty);
331 Class returnType = method.getReturnType();
332 findAvailableProperties(returnType, property, availableProperties, name);
333 }
334 }
335
336 }
337 }
338 }
339 }
340
341 /**
342 * Log a failed lookup, being more verbose when devMode=true.
343 *
344 * @param expr The failed expression
345 * @param e The thrown exception.
346 */
347 private void logLookupFailure(String expr, Exception e) {
348 String msg = LoggerUtils.format("Caught an exception while evaluating expression '#0' against value stack", expr);
349 if (devMode && LOG.isWarnEnabled()) {
350 LOG.warn(msg, e);
351 LOG.warn("NOTE: Previous warning message was issued due to devMode set to true.");
352 } else if (LOG.isDebugEnabled()) {
353 LOG.debug(msg, e);
354 }
355 }
356
357 /* (non-Javadoc)
358 * @see com.opensymphony.xwork2.util.ValueStack#peek()
359 */
360 public Object peek() {
361 return root.peek();
362 }
363
364 /* (non-Javadoc)
365 * @see com.opensymphony.xwork2.util.ValueStack#pop()
366 */
367 public Object pop() {
368 return root.pop();
369 }
370
371 /* (non-Javadoc)
372 * @see com.opensymphony.xwork2.util.ValueStack#push(java.lang.Object)
373 */
374 public void push(Object o) {
375 root.push(o);
376 }
377
378 /* (non-Javadoc)
379 * @see com.opensymphony.xwork2.util.ValueStack#set(java.lang.String, java.lang.Object)
380 */
381 public void set(String key, Object o) {
382 //set basically is backed by a Map
383 //pushed on the stack with a key
384 //being put on the map and the
385 //Object being the value
386
387 Map setMap = null;
388
389 //check if this is a Map
390 //put on the stack for setting
391 //if so just use the old map (reduces waste)
392 Object topObj = peek();
393 if (topObj instanceof Map
394 && ((Map) topObj).get(MAP_IDENTIFIER_KEY) != null) {
395
396 setMap = (Map) topObj;
397 } else {
398 setMap = new HashMap();
399 //the map identifier key ensures
400 //that this map was put there
401 //for set purposes and not by a user
402 //whose data we don't want to touch
403 setMap.put(MAP_IDENTIFIER_KEY, "");
404 push(setMap);
405 }
406 setMap.put(key, o);
407
408 }
409
410
411 private static final String MAP_IDENTIFIER_KEY = "com.opensymphony.xwork2.util.OgnlValueStack.MAP_IDENTIFIER_KEY";
412
413 /* (non-Javadoc)
414 * @see com.opensymphony.xwork2.util.ValueStack#size()
415 */
416 public int size() {
417 return root.size();
418 }
419
420 private Object readResolve() {
421 // TODO: this should be done better
422 ActionContext ac = ActionContext.getContext();
423 Container cont = ac.getContainer();
424 XWorkConverter xworkConverter = cont.getInstance(XWorkConverter.class);
425 CompoundRootAccessor accessor = (CompoundRootAccessor) cont.getInstance(PropertyAccessor.class, CompoundRoot.class.getName());
426 TextProvider prov = cont.getInstance(TextProvider.class, "system");
427 boolean allow = "true".equals(cont.getInstance(String.class, "allowStaticMethodAccess"));
428 OgnlValueStack aStack = new OgnlValueStack(xworkConverter, accessor, prov, allow);
429 aStack.setOgnlUtil(cont.getInstance(OgnlUtil.class));
430 aStack.setRoot(xworkConverter, accessor, this.root, allow);
431
432 return aStack;
433 }
434
435
436 }