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