1 /*
2 * Copyright (c) 2002-2006 by OpenSymphony
3 * All rights reserved.
4 */
5 package com.opensymphony.xwork2.ognl;
6
7 import com.opensymphony.xwork2.conversion.impl.XWorkConverter;
8 import com.opensymphony.xwork2.inject.Inject;
9 import com.opensymphony.xwork2.util.CompoundRoot;
10 import com.opensymphony.xwork2.util.logging.Logger;
11 import com.opensymphony.xwork2.util.logging.LoggerFactory;
12 import com.opensymphony.xwork2.util.reflection.ReflectionException;
13 import ognl;
14
15 import java.beans.BeanInfo;
16 import java.beans.IntrospectionException;
17 import java.beans.Introspector;
18 import java.beans.PropertyDescriptor;
19 import java.lang.reflect.Method;
20 import java.util.Collection;
21 import java.util.HashMap;
22 import java.util.Map;
23 import java.util.concurrent.ConcurrentHashMap;
24
25
26 /**
27 * Utility class that provides common access to the Ognl APIs for
28 * setting and getting properties from objects (usually Actions).
29 *
30 * @author Jason Carreira
31 */
32 public class OgnlUtil {
33
34 private static final Logger LOG = LoggerFactory.getLogger(OgnlUtil.class);
35 private ConcurrentHashMap<String, Object> expressions = new ConcurrentHashMap<String, Object>();
36 private final ConcurrentHashMap<Class, BeanInfo> beanInfoCache = new ConcurrentHashMap<Class, BeanInfo>();
37
38 private TypeConverter defaultConverter;
39 static boolean devMode = false;
40 static boolean enableExpressionCache = true;
41
42 @Inject
43 public void setXWorkConverter(XWorkConverter conv) {
44 this.defaultConverter = new OgnlTypeConverterWrapper(conv);
45 }
46
47 @Inject("devMode")
48 public static void setDevMode(String mode) {
49 devMode = "true".equals(mode);
50 }
51
52 @Inject("enableOGNLExpressionCache")
53 public static void setEnableExpressionCache(String cache) {
54 enableExpressionCache = "true".equals(cache);
55 }
56
57 /**
58 * Sets the object's properties using the default type converter, defaulting to not throw
59 * exceptions for problems setting the properties.
60 *
61 * @param props the properties being set
62 * @param o the object
63 * @param context the action context
64 */
65 public void setProperties(Map<String, ?> props, Object o, Map<String, Object> context) {
66 setProperties(props, o, context, false);
67 }
68
69 /**
70 * Sets the object's properties using the default type converter.
71 *
72 * @param props the properties being set
73 * @param o the object
74 * @param context the action context
75 * @param throwPropertyExceptions boolean which tells whether it should throw exceptions for
76 * problems setting the properties
77 */
78 public void setProperties(Map<String, ?> props, Object o, Map<String, Object> context, boolean throwPropertyExceptions) throws ReflectionException{
79 if (props == null) {
80 return;
81 }
82
83 Ognl.setTypeConverter(context, getTypeConverterFromContext(context));
84
85 Object oldRoot = Ognl.getRoot(context);
86 Ognl.setRoot(context, o);
87
88 for (Map.Entry<String, ?> entry : props.entrySet()) {
89 String expression = entry.getKey();
90 internalSetProperty(expression, entry.getValue(), o, context, throwPropertyExceptions);
91 }
92
93 Ognl.setRoot(context, oldRoot);
94 }
95
96 /**
97 * Sets the properties on the object using the default context, defaulting to not throwing
98 * exceptions for problems setting the properties.
99 *
100 * @param properties
101 * @param o
102 */
103 public void setProperties(Map<String, ?> properties, Object o) {
104 setProperties(properties, o, false);
105 }
106
107 /**
108 * Sets the properties on the object using the default context.
109 *
110 * @param properties the property map to set on the object
111 * @param o the object to set the properties into
112 * @param throwPropertyExceptions boolean which tells whether it should throw exceptions for
113 * problems setting the properties
114 */
115 public void setProperties(Map<String, ?> properties, Object o, boolean throwPropertyExceptions) {
116 Map context = Ognl.createDefaultContext(o);
117 setProperties(properties, o, context, throwPropertyExceptions);
118 }
119
120 /**
121 * Sets the named property to the supplied value on the Object, defaults to not throwing
122 * property exceptions.
123 *
124 * @param name the name of the property to be set
125 * @param value the value to set into the named property
126 * @param o the object upon which to set the property
127 * @param context the context which may include the TypeConverter
128 */
129 public void setProperty(String name, Object value, Object o, Map<String, Object> context) {
130 setProperty(name, value, o, context, false);
131 }
132
133 /**
134 * Sets the named property to the supplied value on the Object.
135 *
136 * @param name the name of the property to be set
137 * @param value the value to set into the named property
138 * @param o the object upon which to set the property
139 * @param context the context which may include the TypeConverter
140 * @param throwPropertyExceptions boolean which tells whether it should throw exceptions for
141 * problems setting the property
142 */
143 public void setProperty(String name, Object value, Object o, Map<String, Object> context, boolean throwPropertyExceptions) {
144 Ognl.setTypeConverter(context, getTypeConverterFromContext(context));
145
146 Object oldRoot = Ognl.getRoot(context);
147 Ognl.setRoot(context, o);
148
149 internalSetProperty(name, value, o, context, throwPropertyExceptions);
150
151 Ognl.setRoot(context, oldRoot);
152 }
153
154 /**
155 * Looks for the real target with the specified property given a root Object which may be a
156 * CompoundRoot.
157 *
158 * @return the real target or null if no object can be found with the specified property
159 */
160 public Object getRealTarget(String property, Map<String, Object> context, Object root) throws OgnlException {
161 //special keyword, they must be cutting the stack
162 if ("top".equals(property)) {
163 return root;
164 }
165
166 if (root instanceof CompoundRoot) {
167 // find real target
168 CompoundRoot cr = (CompoundRoot) root;
169
170 try {
171 for (Object target : cr) {
172 if (
173 OgnlRuntime.hasSetProperty((OgnlContext) context, target, property)
174 ||
175 OgnlRuntime.hasGetProperty((OgnlContext) context, target, property)
176 ||
177 OgnlRuntime.getIndexedPropertyType((OgnlContext) context, target.getClass(), property) != OgnlRuntime.INDEXED_PROPERTY_NONE
178 ) {
179 return target;
180 }
181 }
182 } catch (IntrospectionException ex) {
183 throw new ReflectionException("Cannot figure out real target class", ex);
184 }
185
186 return null;
187 }
188
189 return root;
190 }
191
192
193 /**
194 * Wrapper around Ognl.setValue() to handle type conversion for collection elements.
195 * Ideally, this should be handled by OGNL directly.
196 */
197 public void setValue(String name, Map<String, Object> context, Object root, Object value) throws OgnlException {
198 Ognl.setValue(compile(name), context, root, value);
199 }
200
201 public Object getValue(String name, Map<String, Object> context, Object root) throws OgnlException {
202 return Ognl.getValue(compile(name), context, root);
203 }
204
205 public Object getValue(String name, Map<String, Object> context, Object root, Class resultType) throws OgnlException {
206 return Ognl.getValue(compile(name), context, root, resultType);
207 }
208
209
210 public Object compile(String expression) throws OgnlException {
211 if (enableExpressionCache) {
212 Object o = expressions.get(expression);
213 if (o == null) {
214 o = Ognl.parseExpression(expression);
215 expressions.put(expression, o);
216 }
217 return o;
218 } else
219 return Ognl.parseExpression(expression);
220 }
221
222 /**
223 * Copies the properties in the object "from" and sets them in the object "to"
224 * using specified type converter, or {@link com.opensymphony.xwork2.conversion.impl.XWorkConverter} if none
225 * is specified.
226 *
227 * @param from the source object
228 * @param to the target object
229 * @param context the action context we're running under
230 * @param exclusions collection of method names to excluded from copying ( can be null)
231 * @param inclusions collection of method names to included copying (can be null)
232 * note if exclusions AND inclusions are supplied and not null nothing will get copied.
233 */
234 public void copy(Object from, Object to, Map<String, Object> context, Collection<String> exclusions, Collection<String> inclusions) {
235 if (from == null || to == null) {
236 LOG.warn("Attempting to copy from or to a null source. This is illegal and is bein skipped. This may be due to an error in an OGNL expression, action chaining, or some other event.");
237
238 return;
239 }
240
241 TypeConverter conv = getTypeConverterFromContext(context);
242 Map contextFrom = Ognl.createDefaultContext(from);
243 Ognl.setTypeConverter(contextFrom, conv);
244 Map contextTo = Ognl.createDefaultContext(to);
245 Ognl.setTypeConverter(contextTo, conv);
246
247 PropertyDescriptor[] fromPds;
248 PropertyDescriptor[] toPds;
249
250 try {
251 fromPds = getPropertyDescriptors(from);
252 toPds = getPropertyDescriptors(to);
253 } catch (IntrospectionException e) {
254 LOG.error("An error occured", e);
255
256 return;
257 }
258
259 Map<String, PropertyDescriptor> toPdHash = new HashMap<String, PropertyDescriptor>();
260
261 for (PropertyDescriptor toPd : toPds) {
262 toPdHash.put(toPd.getName(), toPd);
263 }
264
265 for (PropertyDescriptor fromPd : fromPds) {
266 if (fromPd.getReadMethod() != null) {
267 boolean copy = true;
268 if (exclusions != null && exclusions.contains(fromPd.getName())) {
269 copy = false;
270 } else if (inclusions != null && !inclusions.contains(fromPd.getName())) {
271 copy = false;
272 }
273
274 if (copy == true) {
275 PropertyDescriptor toPd = toPdHash.get(fromPd.getName());
276 if ((toPd != null) && (toPd.getWriteMethod() != null)) {
277 try {
278 Object expr = compile(fromPd.getName());
279 Object value = Ognl.getValue(expr, contextFrom, from);
280 Ognl.setValue(expr, contextTo, to, value);
281 } catch (OgnlException e) {
282 // ignore, this is OK
283 }
284 }
285
286 }
287
288 }
289
290 }
291 }
292
293
294 /**
295 * Copies the properties in the object "from" and sets them in the object "to"
296 * using specified type converter, or {@link com.opensymphony.xwork2.conversion.impl.XWorkConverter} if none
297 * is specified.
298 *
299 * @param from the source object
300 * @param to the target object
301 * @param context the action context we're running under
302 */
303 public void copy(Object from, Object to, Map<String, Object> context) {
304 copy(from, to, context, null, null);
305 }
306
307 /**
308 * Get's the java beans property descriptors for the given source.
309 *
310 * @param source the source object.
311 * @return property descriptors.
312 * @throws IntrospectionException is thrown if an exception occurs during introspection.
313 */
314 public PropertyDescriptor[] getPropertyDescriptors(Object source) throws IntrospectionException {
315 BeanInfo beanInfo = getBeanInfo(source);
316 return beanInfo.getPropertyDescriptors();
317 }
318
319
320 /**
321 * Get's the java beans property descriptors for the given class.
322 *
323 * @param clazz the source object.
324 * @return property descriptors.
325 * @throws IntrospectionException is thrown if an exception occurs during introspection.
326 */
327 public PropertyDescriptor[] getPropertyDescriptors(Class clazz) throws IntrospectionException {
328 BeanInfo beanInfo = getBeanInfo(clazz);
329 return beanInfo.getPropertyDescriptors();
330 }
331
332 /**
333 * Creates a Map with read properties for the given source object.
334 * <p/>
335 * If the source object does not have a read property (i.e. write-only) then
336 * the property is added to the map with the value <code>here is no read method for property-name</code>.
337 *
338 * @param source the source object.
339 * @return a Map with (key = read property name, value = value of read property).
340 * @throws IntrospectionException is thrown if an exception occurs during introspection.
341 * @throws OgnlException is thrown by OGNL if the property value could not be retrieved
342 */
343 public Map getBeanMap(Object source) throws IntrospectionException, OgnlException {
344 Map beanMap = new HashMap();
345 Map sourceMap = Ognl.createDefaultContext(source);
346 PropertyDescriptor[] propertyDescriptors = getPropertyDescriptors(source);
347 for (PropertyDescriptor propertyDescriptor : propertyDescriptors) {
348 String propertyName = propertyDescriptor.getDisplayName();
349 Method readMethod = propertyDescriptor.getReadMethod();
350 if (readMethod != null) {
351 Object expr = compile(propertyName);
352 Object value = Ognl.getValue(expr, sourceMap, source);
353 beanMap.put(propertyName, value);
354 } else {
355 beanMap.put(propertyName, "There is no read method for " + propertyName);
356 }
357 }
358 return beanMap;
359 }
360
361 /**
362 * Get's the java bean info for the given source object. Calls getBeanInfo(Class c).
363 *
364 * @param from the source object.
365 * @return java bean info.
366 * @throws IntrospectionException is thrown if an exception occurs during introspection.
367 */
368 public BeanInfo getBeanInfo(Object from) throws IntrospectionException {
369 return getBeanInfo(from.getClass());
370 }
371
372
373 /**
374 * Get's the java bean info for the given source.
375 *
376 * @param clazz the source class.
377 * @return java bean info.
378 * @throws IntrospectionException is thrown if an exception occurs during introspection.
379 */
380 public BeanInfo getBeanInfo(Class clazz) throws IntrospectionException {
381 synchronized (beanInfoCache) {
382 BeanInfo beanInfo;
383 beanInfo = beanInfoCache.get(clazz);
384 if (beanInfo == null) {
385 beanInfo = Introspector.getBeanInfo(clazz, Object.class);
386 beanInfoCache.put(clazz, beanInfo);
387 }
388 return beanInfo;
389 }
390 }
391
392 void internalSetProperty(String name, Object value, Object o, Map<String, Object> context, boolean throwPropertyExceptions) throws ReflectionException{
393 try {
394 setValue(name, context, o, value);
395 } catch (OgnlException e) {
396 Throwable reason = e.getReason();
397 String msg = "Caught OgnlException while setting property '" + name + "' on type '" + o.getClass().getName() + "'.";
398 Throwable exception = (reason == null) ? e : reason;
399
400 if (throwPropertyExceptions) {
401 throw new ReflectionException(msg, exception);
402 } else {
403 if (devMode) {
404 LOG.warn(msg, exception);
405 }
406 }
407 }
408 }
409
410 TypeConverter getTypeConverterFromContext(Map<String, Object> context) {
411 /*ValueStack stack = (ValueStack) context.get(ActionContext.VALUE_STACK);
412 Container cont = (Container)stack.getContext().get(ActionContext.CONTAINER);
413 if (cont != null) {
414 return new OgnlTypeConverterWrapper(cont.getInstance(XWorkConverter.class));
415 } else {
416 throw new IllegalArgumentException("Cannot find type converter in context map");
417 }
418 */
419 return defaultConverter;
420 }
421 }