Classes in this File | Line Coverage | Branch Coverage | Complexity | ||||
BeanUtilsBean |
|
| 6.4;6.4 | ||||
BeanUtilsBean$1 |
|
| 6.4;6.4 |
1 | /* | |
2 | * Licensed to the Apache Software Foundation (ASF) under one or more | |
3 | * contributor license agreements. See the NOTICE file distributed with | |
4 | * this work for additional information regarding copyright ownership. | |
5 | * The ASF licenses this file to You under the Apache License, Version 2.0 | |
6 | * (the "License"); you may not use this file except in compliance with | |
7 | * the License. You may obtain a copy of the License at | |
8 | * | |
9 | * http://www.apache.org/licenses/LICENSE-2.0 | |
10 | * | |
11 | * Unless required by applicable law or agreed to in writing, software | |
12 | * distributed under the License is distributed on an "AS IS" BASIS, | |
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
14 | * See the License for the specific language governing permissions and | |
15 | * limitations under the License. | |
16 | */ | |
17 | ||
18 | ||
19 | package org.apache.commons.beanutils; | |
20 | ||
21 | ||
22 | import java.beans.IndexedPropertyDescriptor; | |
23 | import java.beans.PropertyDescriptor; | |
24 | import java.lang.reflect.Array; | |
25 | import java.lang.reflect.InvocationTargetException; | |
26 | import java.lang.reflect.Method; | |
27 | import java.util.ArrayList; | |
28 | import java.util.Collection; | |
29 | import java.util.HashMap; | |
30 | import java.util.Iterator; | |
31 | import java.util.Map; | |
32 | ||
33 | import org.apache.commons.beanutils.expression.Resolver; | |
34 | import org.apache.commons.logging.Log; | |
35 | import org.apache.commons.logging.LogFactory; | |
36 | ||
37 | ||
38 | /** | |
39 | * <p>JavaBean property population methods.</p> | |
40 | * | |
41 | * <p>This class provides implementations for the utility methods in | |
42 | * {@link BeanUtils}. | |
43 | * Different instances can be used to isolate caches between classloaders | |
44 | * and to vary the value converters registered.</p> | |
45 | * | |
46 | * @author Craig R. McClanahan | |
47 | * @author Ralph Schaer | |
48 | * @author Chris Audley | |
49 | * @author Rey Francois | |
50 | * @author Gregor Rayman | |
51 | * @version $Revision: 834031 $ $Date: 2009-11-09 07:26:52 -0500 (Mon, 09 Nov 2009) $ | |
52 | * @see BeanUtils | |
53 | * @since 1.7 | |
54 | */ | |
55 | ||
56 | public class BeanUtilsBean { | |
57 | ||
58 | ||
59 | // ------------------------------------------------------ Private Class Variables | |
60 | ||
61 | /** | |
62 | * Contains <code>BeanUtilsBean</code> instances indexed by context classloader. | |
63 | */ | |
64 | private static final ContextClassLoaderLocal | |
65 | 1 | BEANS_BY_CLASSLOADER = new ContextClassLoaderLocal() { |
66 | // Creates the default instance used when the context classloader is unavailable | |
67 | protected Object initialValue() { | |
68 | 4 | return new BeanUtilsBean(); |
69 | } | |
70 | }; | |
71 | ||
72 | /** | |
73 | * Gets the instance which provides the functionality for {@link BeanUtils}. | |
74 | * This is a pseudo-singleton - an single instance is provided per (thread) context classloader. | |
75 | * This mechanism provides isolation for web apps deployed in the same container. | |
76 | * | |
77 | * @return The (pseudo-singleton) BeanUtils bean instance | |
78 | */ | |
79 | public static BeanUtilsBean getInstance() { | |
80 | 1946 | return (BeanUtilsBean) BEANS_BY_CLASSLOADER.get(); |
81 | } | |
82 | ||
83 | /** | |
84 | * Sets the instance which provides the functionality for {@link BeanUtils}. | |
85 | * This is a pseudo-singleton - an single instance is provided per (thread) context classloader. | |
86 | * This mechanism provides isolation for web apps deployed in the same container. | |
87 | * | |
88 | * @param newInstance The (pseudo-singleton) BeanUtils bean instance | |
89 | */ | |
90 | public static void setInstance(BeanUtilsBean newInstance) { | |
91 | 128 | BEANS_BY_CLASSLOADER.set(newInstance); |
92 | 128 | } |
93 | ||
94 | // --------------------------------------------------------- Attributes | |
95 | ||
96 | /** | |
97 | * Logging for this instance | |
98 | */ | |
99 | 192 | private Log log = LogFactory.getLog(BeanUtils.class); |
100 | ||
101 | /** Used to perform conversions between object types when setting properties */ | |
102 | private ConvertUtilsBean convertUtilsBean; | |
103 | ||
104 | /** Used to access properties*/ | |
105 | private PropertyUtilsBean propertyUtilsBean; | |
106 | ||
107 | /** A reference to Throwable's initCause method, or null if it's not there in this JVM */ | |
108 | 1 | private static final Method INIT_CAUSE_METHOD = getInitCauseMethod(); |
109 | ||
110 | // --------------------------------------------------------- Constuctors | |
111 | ||
112 | /** | |
113 | * <p>Constructs an instance using new property | |
114 | * and conversion instances.</p> | |
115 | */ | |
116 | public BeanUtilsBean() { | |
117 | 131 | this(new ConvertUtilsBean(), new PropertyUtilsBean()); |
118 | 131 | } |
119 | ||
120 | /** | |
121 | * <p>Constructs an instance using given conversion instances | |
122 | * and new {@link PropertyUtilsBean} instance.</p> | |
123 | * | |
124 | * @param convertUtilsBean use this <code>ConvertUtilsBean</code> | |
125 | * to perform conversions from one object to another | |
126 | * | |
127 | * @since 1.8.0 | |
128 | */ | |
129 | public BeanUtilsBean(ConvertUtilsBean convertUtilsBean) { | |
130 | 55 | this(convertUtilsBean, new PropertyUtilsBean()); |
131 | 55 | } |
132 | ||
133 | /** | |
134 | * <p>Constructs an instance using given property and conversion instances.</p> | |
135 | * | |
136 | * @param convertUtilsBean use this <code>ConvertUtilsBean</code> | |
137 | * to perform conversions from one object to another | |
138 | * @param propertyUtilsBean use this <code>PropertyUtilsBean</code> | |
139 | * to access properties | |
140 | */ | |
141 | public BeanUtilsBean( | |
142 | ConvertUtilsBean convertUtilsBean, | |
143 | 192 | PropertyUtilsBean propertyUtilsBean) { |
144 | ||
145 | 192 | this.convertUtilsBean = convertUtilsBean; |
146 | 192 | this.propertyUtilsBean = propertyUtilsBean; |
147 | 192 | } |
148 | ||
149 | // --------------------------------------------------------- Public Methods | |
150 | ||
151 | /** | |
152 | * <p>Clone a bean based on the available property getters and setters, | |
153 | * even if the bean class itself does not implement Cloneable.</p> | |
154 | * | |
155 | * <p> | |
156 | * <strong>Note:</strong> this method creates a <strong>shallow</strong> clone. | |
157 | * In other words, any objects referred to by the bean are shared with the clone | |
158 | * rather than being cloned in turn. | |
159 | * </p> | |
160 | * | |
161 | * @param bean Bean to be cloned | |
162 | * @return the cloned bean | |
163 | * | |
164 | * @exception IllegalAccessException if the caller does not have | |
165 | * access to the property accessor method | |
166 | * @exception InstantiationException if a new instance of the bean's | |
167 | * class cannot be instantiated | |
168 | * @exception InvocationTargetException if the property accessor method | |
169 | * throws an exception | |
170 | * @exception NoSuchMethodException if an accessor method for this | |
171 | * property cannot be found | |
172 | */ | |
173 | public Object cloneBean(Object bean) | |
174 | throws IllegalAccessException, InstantiationException, | |
175 | InvocationTargetException, NoSuchMethodException { | |
176 | ||
177 | 1 | if (log.isDebugEnabled()) { |
178 | 0 | log.debug("Cloning bean: " + bean.getClass().getName()); |
179 | } | |
180 | 1 | Object newBean = null; |
181 | 1 | if (bean instanceof DynaBean) { |
182 | 1 | newBean = ((DynaBean) bean).getDynaClass().newInstance(); |
183 | } else { | |
184 | 0 | newBean = bean.getClass().newInstance(); |
185 | } | |
186 | 1 | getPropertyUtils().copyProperties(newBean, bean); |
187 | 1 | return (newBean); |
188 | ||
189 | } | |
190 | ||
191 | ||
192 | /** | |
193 | * <p>Copy property values from the origin bean to the destination bean | |
194 | * for all cases where the property names are the same. For each | |
195 | * property, a conversion is attempted as necessary. All combinations of | |
196 | * standard JavaBeans and DynaBeans as origin and destination are | |
197 | * supported. Properties that exist in the origin bean, but do not exist | |
198 | * in the destination bean (or are read-only in the destination bean) are | |
199 | * silently ignored.</p> | |
200 | * | |
201 | * <p>If the origin "bean" is actually a <code>Map</code>, it is assumed | |
202 | * to contain String-valued <strong>simple</strong> property names as the keys, pointing at | |
203 | * the corresponding property values that will be converted (if necessary) | |
204 | * and set in the destination bean. <strong>Note</strong> that this method | |
205 | * is intended to perform a "shallow copy" of the properties and so complex | |
206 | * properties (for example, nested ones) will not be copied.</p> | |
207 | * | |
208 | * <p>This method differs from <code>populate()</code>, which | |
209 | * was primarily designed for populating JavaBeans from the map of request | |
210 | * parameters retrieved on an HTTP request, is that no scalar->indexed | |
211 | * or indexed->scalar manipulations are performed. If the origin property | |
212 | * is indexed, the destination property must be also.</p> | |
213 | * | |
214 | * <p>If you know that no type conversions are required, the | |
215 | * <code>copyProperties()</code> method in {@link PropertyUtils} will | |
216 | * execute faster than this method.</p> | |
217 | * | |
218 | * <p><strong>FIXME</strong> - Indexed and mapped properties that do not | |
219 | * have getter and setter methods for the underlying array or Map are not | |
220 | * copied by this method.</p> | |
221 | * | |
222 | * @param dest Destination bean whose properties are modified | |
223 | * @param orig Origin bean whose properties are retrieved | |
224 | * | |
225 | * @exception IllegalAccessException if the caller does not have | |
226 | * access to the property accessor method | |
227 | * @exception IllegalArgumentException if the <code>dest</code> or | |
228 | * <code>orig</code> argument is null or if the <code>dest</code> | |
229 | * property type is different from the source type and the relevant | |
230 | * converter has not been registered. | |
231 | * @exception InvocationTargetException if the property accessor method | |
232 | * throws an exception | |
233 | */ | |
234 | public void copyProperties(Object dest, Object orig) | |
235 | throws IllegalAccessException, InvocationTargetException { | |
236 | ||
237 | // Validate existence of the specified beans | |
238 | 11 | if (dest == null) { |
239 | 0 | throw new IllegalArgumentException |
240 | ("No destination bean specified"); | |
241 | } | |
242 | 11 | if (orig == null) { |
243 | 0 | throw new IllegalArgumentException("No origin bean specified"); |
244 | } | |
245 | 11 | if (log.isDebugEnabled()) { |
246 | 0 | log.debug("BeanUtils.copyProperties(" + dest + ", " + |
247 | orig + ")"); | |
248 | } | |
249 | ||
250 | // Copy the properties, converting as necessary | |
251 | 11 | if (orig instanceof DynaBean) { |
252 | 4 | DynaProperty[] origDescriptors = |
253 | ((DynaBean) orig).getDynaClass().getDynaProperties(); | |
254 | 71 | for (int i = 0; i < origDescriptors.length; i++) { |
255 | 67 | String name = origDescriptors[i].getName(); |
256 | // Need to check isReadable() for WrapDynaBean | |
257 | // (see Jira issue# BEANUTILS-61) | |
258 | 67 | if (getPropertyUtils().isReadable(orig, name) && |
259 | getPropertyUtils().isWriteable(dest, name)) { | |
260 | 56 | Object value = ((DynaBean) orig).get(name); |
261 | 56 | copyProperty(dest, name, value); |
262 | } | |
263 | } | |
264 | 4 | } else if (orig instanceof Map) { |
265 | 4 | Iterator entries = ((Map) orig).entrySet().iterator(); |
266 | 35 | while (entries.hasNext()) { |
267 | 31 | Map.Entry entry = (Map.Entry) entries.next(); |
268 | 31 | String name = (String)entry.getKey(); |
269 | 31 | if (getPropertyUtils().isWriteable(dest, name)) { |
270 | 30 | copyProperty(dest, name, entry.getValue()); |
271 | } | |
272 | 31 | } |
273 | 4 | } else /* if (orig is a standard JavaBean) */ { |
274 | 3 | PropertyDescriptor[] origDescriptors = |
275 | getPropertyUtils().getPropertyDescriptors(orig); | |
276 | 90 | for (int i = 0; i < origDescriptors.length; i++) { |
277 | 87 | String name = origDescriptors[i].getName(); |
278 | 87 | if ("class".equals(name)) { |
279 | 3 | continue; // No point in trying to set an object's class |
280 | } | |
281 | 84 | if (getPropertyUtils().isReadable(orig, name) && |
282 | getPropertyUtils().isWriteable(dest, name)) { | |
283 | try { | |
284 | 58 | Object value = |
285 | getPropertyUtils().getSimpleProperty(orig, name); | |
286 | 52 | copyProperty(dest, name, value); |
287 | 6 | } catch (NoSuchMethodException e) { |
288 | // Should not happen | |
289 | 52 | } |
290 | } | |
291 | } | |
292 | } | |
293 | ||
294 | 11 | } |
295 | ||
296 | ||
297 | /** | |
298 | * <p>Copy the specified property value to the specified destination bean, | |
299 | * performing any type conversion that is required. If the specified | |
300 | * bean does not have a property of the specified name, or the property | |
301 | * is read only on the destination bean, return without | |
302 | * doing anything. If you have custom destination property types, register | |
303 | * {@link Converter}s for them by calling the <code>register()</code> | |
304 | * method of {@link ConvertUtils}.</p> | |
305 | * | |
306 | * <p><strong>IMPLEMENTATION RESTRICTIONS</strong>:</p> | |
307 | * <ul> | |
308 | * <li>Does not support destination properties that are indexed, | |
309 | * but only an indexed setter (as opposed to an array setter) | |
310 | * is available.</li> | |
311 | * <li>Does not support destination properties that are mapped, | |
312 | * but only a keyed setter (as opposed to a Map setter) | |
313 | * is available.</li> | |
314 | * <li>The desired property type of a mapped setter cannot be | |
315 | * determined (since Maps support any data type), so no conversion | |
316 | * will be performed.</li> | |
317 | * </ul> | |
318 | * | |
319 | * @param bean Bean on which setting is to be performed | |
320 | * @param name Property name (can be nested/indexed/mapped/combo) | |
321 | * @param value Value to be set | |
322 | * | |
323 | * @exception IllegalAccessException if the caller does not have | |
324 | * access to the property accessor method | |
325 | * @exception InvocationTargetException if the property accessor method | |
326 | * throws an exception | |
327 | */ | |
328 | public void copyProperty(Object bean, String name, Object value) | |
329 | throws IllegalAccessException, InvocationTargetException { | |
330 | ||
331 | // Trace logging (if enabled) | |
332 | 254 | if (log.isTraceEnabled()) { |
333 | 0 | StringBuffer sb = new StringBuffer(" copyProperty("); |
334 | 0 | sb.append(bean); |
335 | 0 | sb.append(", "); |
336 | 0 | sb.append(name); |
337 | 0 | sb.append(", "); |
338 | 0 | if (value == null) { |
339 | 0 | sb.append("<NULL>"); |
340 | 0 | } else if (value instanceof String) { |
341 | 0 | sb.append((String) value); |
342 | 0 | } else if (value instanceof String[]) { |
343 | 0 | String[] values = (String[]) value; |
344 | 0 | sb.append('['); |
345 | 0 | for (int i = 0; i < values.length; i++) { |
346 | 0 | if (i > 0) { |
347 | 0 | sb.append(','); |
348 | } | |
349 | 0 | sb.append(values[i]); |
350 | } | |
351 | 0 | sb.append(']'); |
352 | 0 | } else { |
353 | 0 | sb.append(value.toString()); |
354 | } | |
355 | 0 | sb.append(')'); |
356 | 0 | log.trace(sb.toString()); |
357 | } | |
358 | ||
359 | // Resolve any nested expression to get the actual target bean | |
360 | 254 | Object target = bean; |
361 | 254 | Resolver resolver = getPropertyUtils().getResolver(); |
362 | 281 | while (resolver.hasNested(name)) { |
363 | try { | |
364 | 27 | target = getPropertyUtils().getProperty(target, resolver.next(name)); |
365 | 27 | name = resolver.remove(name); |
366 | 0 | } catch (NoSuchMethodException e) { |
367 | 0 | return; // Skip this property setter |
368 | 27 | } |
369 | } | |
370 | 254 | if (log.isTraceEnabled()) { |
371 | 0 | log.trace(" Target bean = " + target); |
372 | 0 | log.trace(" Target name = " + name); |
373 | } | |
374 | ||
375 | // Declare local variables we will require | |
376 | 254 | String propName = resolver.getProperty(name); // Simple name of target property |
377 | 254 | Class type = null; // Java type of target property |
378 | 254 | int index = resolver.getIndex(name); // Indexed subscript value (if any) |
379 | 254 | String key = resolver.getKey(name); // Mapped key value (if any) |
380 | ||
381 | // Calculate the target property type | |
382 | 254 | if (target instanceof DynaBean) { |
383 | 47 | DynaClass dynaClass = ((DynaBean) target).getDynaClass(); |
384 | 47 | DynaProperty dynaProperty = dynaClass.getDynaProperty(propName); |
385 | 47 | if (dynaProperty == null) { |
386 | 0 | return; // Skip this property setter |
387 | } | |
388 | 47 | type = dynaProperty.getType(); |
389 | 47 | } else { |
390 | 207 | PropertyDescriptor descriptor = null; |
391 | try { | |
392 | 207 | descriptor = |
393 | getPropertyUtils().getPropertyDescriptor(target, name); | |
394 | 207 | if (descriptor == null) { |
395 | 0 | return; // Skip this property setter |
396 | } | |
397 | 0 | } catch (NoSuchMethodException e) { |
398 | 0 | return; // Skip this property setter |
399 | 207 | } |
400 | 207 | type = descriptor.getPropertyType(); |
401 | 207 | if (type == null) { |
402 | // Most likely an indexed setter on a POJB only | |
403 | 8 | if (log.isTraceEnabled()) { |
404 | 0 | log.trace(" target type for property '" + |
405 | propName + "' is null, so skipping ths setter"); | |
406 | } | |
407 | 8 | return; |
408 | } | |
409 | } | |
410 | 246 | if (log.isTraceEnabled()) { |
411 | 0 | log.trace(" target propName=" + propName + ", type=" + |
412 | type + ", index=" + index + ", key=" + key); | |
413 | } | |
414 | ||
415 | // Convert the specified value to the required type and store it | |
416 | 246 | if (index >= 0) { // Destination must be indexed |
417 | 14 | value = convert(value, type.getComponentType()); |
418 | try { | |
419 | 14 | getPropertyUtils().setIndexedProperty(target, propName, |
420 | index, value); | |
421 | 0 | } catch (NoSuchMethodException e) { |
422 | 0 | throw new InvocationTargetException |
423 | (e, "Cannot set " + propName); | |
424 | 14 | } |
425 | 232 | } else if (key != null) { // Destination must be mapped |
426 | // Maps do not know what the preferred data type is, | |
427 | // so perform no conversions at all | |
428 | // FIXME - should we create or support a TypedMap? | |
429 | try { | |
430 | 3 | getPropertyUtils().setMappedProperty(target, propName, |
431 | key, value); | |
432 | 0 | } catch (NoSuchMethodException e) { |
433 | 0 | throw new InvocationTargetException |
434 | (e, "Cannot set " + propName); | |
435 | 3 | } |
436 | } else { // Destination must be simple | |
437 | 229 | value = convert(value, type); |
438 | try { | |
439 | 229 | getPropertyUtils().setSimpleProperty(target, propName, value); |
440 | 0 | } catch (NoSuchMethodException e) { |
441 | 0 | throw new InvocationTargetException |
442 | (e, "Cannot set " + propName); | |
443 | 229 | } |
444 | } | |
445 | ||
446 | 246 | } |
447 | ||
448 | ||
449 | /** | |
450 | * <p>Return the entire set of properties for which the specified bean | |
451 | * provides a read method. This map contains the to <code>String</code> | |
452 | * converted property values for all properties for which a read method | |
453 | * is provided (i.e. where the getReadMethod() returns non-null).</p> | |
454 | * | |
455 | * <p>This map can be fed back to a call to | |
456 | * <code>BeanUtils.populate()</code> to reconsitute the same set of | |
457 | * properties, modulo differences for read-only and write-only | |
458 | * properties, but only if there are no indexed properties.</p> | |
459 | * | |
460 | * <p><strong>Warning:</strong> if any of the bean property implementations | |
461 | * contain (directly or indirectly) a call to this method then | |
462 | * a stack overflow may result. For example: | |
463 | * <code><pre> | |
464 | * class MyBean | |
465 | * { | |
466 | * public Map getParameterMap() | |
467 | * { | |
468 | * BeanUtils.describe(this); | |
469 | * } | |
470 | * } | |
471 | * </pre></code> | |
472 | * will result in an infinite regression when <code>getParametersMap</code> | |
473 | * is called. It is recommended that such methods are given alternative | |
474 | * names (for example, <code>parametersMap</code>). | |
475 | * </p> | |
476 | * @param bean Bean whose properties are to be extracted | |
477 | * @return Map of property descriptors | |
478 | * | |
479 | * @exception IllegalAccessException if the caller does not have | |
480 | * access to the property accessor method | |
481 | * @exception InvocationTargetException if the property accessor method | |
482 | * throws an exception | |
483 | * @exception NoSuchMethodException if an accessor method for this | |
484 | * property cannot be found | |
485 | */ | |
486 | public Map describe(Object bean) | |
487 | throws IllegalAccessException, InvocationTargetException, | |
488 | NoSuchMethodException { | |
489 | ||
490 | 5 | if (bean == null) { |
491 | // return (Collections.EMPTY_MAP); | |
492 | 0 | return (new java.util.HashMap()); |
493 | } | |
494 | ||
495 | 5 | if (log.isDebugEnabled()) { |
496 | 0 | log.debug("Describing bean: " + bean.getClass().getName()); |
497 | } | |
498 | ||
499 | 5 | Map description = new HashMap(); |
500 | 5 | if (bean instanceof DynaBean) { |
501 | 0 | DynaProperty[] descriptors = |
502 | ((DynaBean) bean).getDynaClass().getDynaProperties(); | |
503 | 0 | for (int i = 0; i < descriptors.length; i++) { |
504 | 0 | String name = descriptors[i].getName(); |
505 | 0 | description.put(name, getProperty(bean, name)); |
506 | } | |
507 | 0 | } else { |
508 | 5 | PropertyDescriptor[] descriptors = |
509 | getPropertyUtils().getPropertyDescriptors(bean); | |
510 | 5 | Class clazz = bean.getClass(); |
511 | 71 | for (int i = 0; i < descriptors.length; i++) { |
512 | 66 | String name = descriptors[i].getName(); |
513 | 66 | if (getPropertyUtils().getReadMethod(clazz, descriptors[i]) != null) { |
514 | 56 | description.put(name, getProperty(bean, name)); |
515 | } | |
516 | } | |
517 | } | |
518 | 5 | return (description); |
519 | ||
520 | } | |
521 | ||
522 | ||
523 | /** | |
524 | * Return the value of the specified array property of the specified | |
525 | * bean, as a String array. | |
526 | * | |
527 | * @param bean Bean whose property is to be extracted | |
528 | * @param name Name of the property to be extracted | |
529 | * @return The array property value | |
530 | * | |
531 | * @exception IllegalAccessException if the caller does not have | |
532 | * access to the property accessor method | |
533 | * @exception InvocationTargetException if the property accessor method | |
534 | * throws an exception | |
535 | * @exception NoSuchMethodException if an accessor method for this | |
536 | * property cannot be found | |
537 | */ | |
538 | public String[] getArrayProperty(Object bean, String name) | |
539 | throws IllegalAccessException, InvocationTargetException, | |
540 | NoSuchMethodException { | |
541 | ||
542 | 14 | Object value = getPropertyUtils().getProperty(bean, name); |
543 | 14 | if (value == null) { |
544 | 0 | return (null); |
545 | 14 | } else if (value instanceof Collection) { |
546 | 0 | ArrayList values = new ArrayList(); |
547 | 0 | Iterator items = ((Collection) value).iterator(); |
548 | 0 | while (items.hasNext()) { |
549 | 0 | Object item = items.next(); |
550 | 0 | if (item == null) { |
551 | 0 | values.add((String) null); |
552 | } else { | |
553 | // convert to string using convert utils | |
554 | 0 | values.add(getConvertUtils().convert(item)); |
555 | } | |
556 | 0 | } |
557 | 0 | return ((String[]) values.toArray(new String[values.size()])); |
558 | 14 | } else if (value.getClass().isArray()) { |
559 | 10 | int n = Array.getLength(value); |
560 | 10 | String[] results = new String[n]; |
561 | 52 | for (int i = 0; i < n; i++) { |
562 | 42 | Object item = Array.get(value, i); |
563 | 42 | if (item == null) { |
564 | 0 | results[i] = null; |
565 | } else { | |
566 | // convert to string using convert utils | |
567 | 42 | results[i] = getConvertUtils().convert(item); |
568 | } | |
569 | } | |
570 | 10 | return (results); |
571 | } else { | |
572 | 4 | String[] results = new String[1]; |
573 | 4 | results[0] = getConvertUtils().convert(value); |
574 | 4 | return (results); |
575 | } | |
576 | ||
577 | } | |
578 | ||
579 | ||
580 | /** | |
581 | * Return the value of the specified indexed property of the specified | |
582 | * bean, as a String. The zero-relative index of the | |
583 | * required value must be included (in square brackets) as a suffix to | |
584 | * the property name, or <code>IllegalArgumentException</code> will be | |
585 | * thrown. | |
586 | * | |
587 | * @param bean Bean whose property is to be extracted | |
588 | * @param name <code>propertyname[index]</code> of the property value | |
589 | * to be extracted | |
590 | * @return The indexed property's value, converted to a String | |
591 | * | |
592 | * @exception IllegalAccessException if the caller does not have | |
593 | * access to the property accessor method | |
594 | * @exception InvocationTargetException if the property accessor method | |
595 | * throws an exception | |
596 | * @exception NoSuchMethodException if an accessor method for this | |
597 | * property cannot be found | |
598 | */ | |
599 | public String getIndexedProperty(Object bean, String name) | |
600 | throws IllegalAccessException, InvocationTargetException, | |
601 | NoSuchMethodException { | |
602 | ||
603 | 8 | Object value = getPropertyUtils().getIndexedProperty(bean, name); |
604 | 8 | return (getConvertUtils().convert(value)); |
605 | ||
606 | } | |
607 | ||
608 | ||
609 | /** | |
610 | * Return the value of the specified indexed property of the specified | |
611 | * bean, as a String. The index is specified as a method parameter and | |
612 | * must *not* be included in the property name expression | |
613 | * | |
614 | * @param bean Bean whose property is to be extracted | |
615 | * @param name Simple property name of the property value to be extracted | |
616 | * @param index Index of the property value to be extracted | |
617 | * @return The indexed property's value, converted to a String | |
618 | * | |
619 | * @exception IllegalAccessException if the caller does not have | |
620 | * access to the property accessor method | |
621 | * @exception InvocationTargetException if the property accessor method | |
622 | * throws an exception | |
623 | * @exception NoSuchMethodException if an accessor method for this | |
624 | * property cannot be found | |
625 | */ | |
626 | public String getIndexedProperty(Object bean, | |
627 | String name, int index) | |
628 | throws IllegalAccessException, InvocationTargetException, | |
629 | NoSuchMethodException { | |
630 | ||
631 | 8 | Object value = getPropertyUtils().getIndexedProperty(bean, name, index); |
632 | 8 | return (getConvertUtils().convert(value)); |
633 | ||
634 | } | |
635 | ||
636 | ||
637 | /** | |
638 | * Return the value of the specified indexed property of the specified | |
639 | * bean, as a String. The String-valued key of the required value | |
640 | * must be included (in parentheses) as a suffix to | |
641 | * the property name, or <code>IllegalArgumentException</code> will be | |
642 | * thrown. | |
643 | * | |
644 | * @param bean Bean whose property is to be extracted | |
645 | * @param name <code>propertyname(index)</code> of the property value | |
646 | * to be extracted | |
647 | * @return The mapped property's value, converted to a String | |
648 | * | |
649 | * @exception IllegalAccessException if the caller does not have | |
650 | * access to the property accessor method | |
651 | * @exception InvocationTargetException if the property accessor method | |
652 | * throws an exception | |
653 | * @exception NoSuchMethodException if an accessor method for this | |
654 | * property cannot be found | |
655 | */ | |
656 | public String getMappedProperty(Object bean, String name) | |
657 | throws IllegalAccessException, InvocationTargetException, | |
658 | NoSuchMethodException { | |
659 | ||
660 | 0 | Object value = getPropertyUtils().getMappedProperty(bean, name); |
661 | 0 | return (getConvertUtils().convert(value)); |
662 | ||
663 | } | |
664 | ||
665 | ||
666 | /** | |
667 | * Return the value of the specified mapped property of the specified | |
668 | * bean, as a String. The key is specified as a method parameter and | |
669 | * must *not* be included in the property name expression | |
670 | * | |
671 | * @param bean Bean whose property is to be extracted | |
672 | * @param name Simple property name of the property value to be extracted | |
673 | * @param key Lookup key of the property value to be extracted | |
674 | * @return The mapped property's value, converted to a String | |
675 | * | |
676 | * @exception IllegalAccessException if the caller does not have | |
677 | * access to the property accessor method | |
678 | * @exception InvocationTargetException if the property accessor method | |
679 | * throws an exception | |
680 | * @exception NoSuchMethodException if an accessor method for this | |
681 | * property cannot be found | |
682 | */ | |
683 | public String getMappedProperty(Object bean, | |
684 | String name, String key) | |
685 | throws IllegalAccessException, InvocationTargetException, | |
686 | NoSuchMethodException { | |
687 | ||
688 | 0 | Object value = getPropertyUtils().getMappedProperty(bean, name, key); |
689 | 0 | return (getConvertUtils().convert(value)); |
690 | ||
691 | } | |
692 | ||
693 | ||
694 | /** | |
695 | * Return the value of the (possibly nested) property of the specified | |
696 | * name, for the specified bean, as a String. | |
697 | * | |
698 | * @param bean Bean whose property is to be extracted | |
699 | * @param name Possibly nested name of the property to be extracted | |
700 | * @return The nested property's value, converted to a String | |
701 | * | |
702 | * @exception IllegalAccessException if the caller does not have | |
703 | * access to the property accessor method | |
704 | * @exception IllegalArgumentException if a nested reference to a | |
705 | * property returns null | |
706 | * @exception InvocationTargetException if the property accessor method | |
707 | * throws an exception | |
708 | * @exception NoSuchMethodException if an accessor method for this | |
709 | * property cannot be found | |
710 | */ | |
711 | public String getNestedProperty(Object bean, String name) | |
712 | throws IllegalAccessException, InvocationTargetException, | |
713 | NoSuchMethodException { | |
714 | ||
715 | 72 | Object value = getPropertyUtils().getNestedProperty(bean, name); |
716 | 71 | return (getConvertUtils().convert(value)); |
717 | ||
718 | } | |
719 | ||
720 | ||
721 | /** | |
722 | * Return the value of the specified property of the specified bean, | |
723 | * no matter which property reference format is used, as a String. | |
724 | * | |
725 | * @param bean Bean whose property is to be extracted | |
726 | * @param name Possibly indexed and/or nested name of the property | |
727 | * to be extracted | |
728 | * @return The property's value, converted to a String | |
729 | * | |
730 | * @exception IllegalAccessException if the caller does not have | |
731 | * access to the property accessor method | |
732 | * @exception InvocationTargetException if the property accessor method | |
733 | * throws an exception | |
734 | * @exception NoSuchMethodException if an accessor method for this | |
735 | * property cannot be found | |
736 | */ | |
737 | public String getProperty(Object bean, String name) | |
738 | throws IllegalAccessException, InvocationTargetException, | |
739 | NoSuchMethodException { | |
740 | ||
741 | 69 | return (getNestedProperty(bean, name)); |
742 | ||
743 | } | |
744 | ||
745 | ||
746 | /** | |
747 | * Return the value of the specified simple property of the specified | |
748 | * bean, converted to a String. | |
749 | * | |
750 | * @param bean Bean whose property is to be extracted | |
751 | * @param name Name of the property to be extracted | |
752 | * @return The property's value, converted to a String | |
753 | * | |
754 | * @exception IllegalAccessException if the caller does not have | |
755 | * access to the property accessor method | |
756 | * @exception InvocationTargetException if the property accessor method | |
757 | * throws an exception | |
758 | * @exception NoSuchMethodException if an accessor method for this | |
759 | * property cannot be found | |
760 | */ | |
761 | public String getSimpleProperty(Object bean, String name) | |
762 | throws IllegalAccessException, InvocationTargetException, | |
763 | NoSuchMethodException { | |
764 | ||
765 | 5 | Object value = getPropertyUtils().getSimpleProperty(bean, name); |
766 | 5 | return (getConvertUtils().convert(value)); |
767 | ||
768 | } | |
769 | ||
770 | ||
771 | /** | |
772 | * <p>Populate the JavaBeans properties of the specified bean, based on | |
773 | * the specified name/value pairs. This method uses Java reflection APIs | |
774 | * to identify corresponding "property setter" method names, and deals | |
775 | * with setter arguments of type <code>String</code>, <code>boolean</code>, | |
776 | * <code>int</code>, <code>long</code>, <code>float</code>, and | |
777 | * <code>double</code>. In addition, array setters for these types (or the | |
778 | * corresponding primitive types) can also be identified.</p> | |
779 | * | |
780 | * <p>The particular setter method to be called for each property is | |
781 | * determined using the usual JavaBeans introspection mechanisms. Thus, | |
782 | * you may identify custom setter methods using a BeanInfo class that is | |
783 | * associated with the class of the bean itself. If no such BeanInfo | |
784 | * class is available, the standard method name conversion ("set" plus | |
785 | * the capitalized name of the property in question) is used.</p> | |
786 | * | |
787 | * <p><strong>NOTE</strong>: It is contrary to the JavaBeans Specification | |
788 | * to have more than one setter method (with different argument | |
789 | * signatures) for the same property.</p> | |
790 | * | |
791 | * <p><strong>WARNING</strong> - The logic of this method is customized | |
792 | * for extracting String-based request parameters from an HTTP request. | |
793 | * It is probably not what you want for general property copying with | |
794 | * type conversion. For that purpose, check out the | |
795 | * <code>copyProperties()</code> method instead.</p> | |
796 | * | |
797 | * @param bean JavaBean whose properties are being populated | |
798 | * @param properties Map keyed by property name, with the | |
799 | * corresponding (String or String[]) value(s) to be set | |
800 | * | |
801 | * @exception IllegalAccessException if the caller does not have | |
802 | * access to the property accessor method | |
803 | * @exception InvocationTargetException if the property accessor method | |
804 | * throws an exception | |
805 | */ | |
806 | public void populate(Object bean, Map properties) | |
807 | throws IllegalAccessException, InvocationTargetException { | |
808 | ||
809 | // Do nothing unless both arguments have been specified | |
810 | 19 | if ((bean == null) || (properties == null)) { |
811 | 0 | return; |
812 | } | |
813 | 19 | if (log.isDebugEnabled()) { |
814 | 0 | log.debug("BeanUtils.populate(" + bean + ", " + |
815 | properties + ")"); | |
816 | } | |
817 | ||
818 | // Loop through the property name/value pairs to be set | |
819 | 19 | Iterator entries = properties.entrySet().iterator(); |
820 | 84 | while (entries.hasNext()) { |
821 | ||
822 | // Identify the property name and value(s) to be assigned | |
823 | 65 | Map.Entry entry = (Map.Entry)entries.next(); |
824 | 65 | String name = (String) entry.getKey(); |
825 | 65 | if (name == null) { |
826 | 0 | continue; |
827 | } | |
828 | ||
829 | // Perform the assignment for this property | |
830 | 65 | setProperty(bean, name, entry.getValue()); |
831 | ||
832 | 65 | } |
833 | ||
834 | 19 | } |
835 | ||
836 | ||
837 | /** | |
838 | * <p>Set the specified property value, performing type conversions as | |
839 | * required to conform to the type of the destination property.</p> | |
840 | * | |
841 | * <p>If the property is read only then the method returns | |
842 | * without throwing an exception.</p> | |
843 | * | |
844 | * <p>If <code>null</code> is passed into a property expecting a primitive value, | |
845 | * then this will be converted as if it were a <code>null</code> string.</p> | |
846 | * | |
847 | * <p><strong>WARNING</strong> - The logic of this method is customized | |
848 | * to meet the needs of <code>populate()</code>, and is probably not what | |
849 | * you want for general property copying with type conversion. For that | |
850 | * purpose, check out the <code>copyProperty()</code> method instead.</p> | |
851 | * | |
852 | * <p><strong>WARNING</strong> - PLEASE do not modify the behavior of this | |
853 | * method without consulting with the Struts developer community. There | |
854 | * are some subtleties to its functionality that are not documented in the | |
855 | * Javadoc description above, yet are vital to the way that Struts utilizes | |
856 | * this method.</p> | |
857 | * | |
858 | * @param bean Bean on which setting is to be performed | |
859 | * @param name Property name (can be nested/indexed/mapped/combo) | |
860 | * @param value Value to be set | |
861 | * | |
862 | * @exception IllegalAccessException if the caller does not have | |
863 | * access to the property accessor method | |
864 | * @exception InvocationTargetException if the property accessor method | |
865 | * throws an exception | |
866 | */ | |
867 | public void setProperty(Object bean, String name, Object value) | |
868 | throws IllegalAccessException, InvocationTargetException { | |
869 | ||
870 | // Trace logging (if enabled) | |
871 | 215 | if (log.isTraceEnabled()) { |
872 | 0 | StringBuffer sb = new StringBuffer(" setProperty("); |
873 | 0 | sb.append(bean); |
874 | 0 | sb.append(", "); |
875 | 0 | sb.append(name); |
876 | 0 | sb.append(", "); |
877 | 0 | if (value == null) { |
878 | 0 | sb.append("<NULL>"); |
879 | 0 | } else if (value instanceof String) { |
880 | 0 | sb.append((String) value); |
881 | 0 | } else if (value instanceof String[]) { |
882 | 0 | String[] values = (String[]) value; |
883 | 0 | sb.append('['); |
884 | 0 | for (int i = 0; i < values.length; i++) { |
885 | 0 | if (i > 0) { |
886 | 0 | sb.append(','); |
887 | } | |
888 | 0 | sb.append(values[i]); |
889 | } | |
890 | 0 | sb.append(']'); |
891 | 0 | } else { |
892 | 0 | sb.append(value.toString()); |
893 | } | |
894 | 0 | sb.append(')'); |
895 | 0 | log.trace(sb.toString()); |
896 | } | |
897 | ||
898 | // Resolve any nested expression to get the actual target bean | |
899 | 215 | Object target = bean; |
900 | 215 | Resolver resolver = getPropertyUtils().getResolver(); |
901 | 234 | while (resolver.hasNested(name)) { |
902 | try { | |
903 | 19 | target = getPropertyUtils().getProperty(target, resolver.next(name)); |
904 | 19 | name = resolver.remove(name); |
905 | 0 | } catch (NoSuchMethodException e) { |
906 | 0 | return; // Skip this property setter |
907 | 19 | } |
908 | } | |
909 | 215 | if (log.isTraceEnabled()) { |
910 | 0 | log.trace(" Target bean = " + target); |
911 | 0 | log.trace(" Target name = " + name); |
912 | } | |
913 | ||
914 | // Declare local variables we will require | |
915 | 215 | String propName = resolver.getProperty(name); // Simple name of target property |
916 | 215 | Class type = null; // Java type of target property |
917 | 215 | int index = resolver.getIndex(name); // Indexed subscript value (if any) |
918 | 215 | String key = resolver.getKey(name); // Mapped key value (if any) |
919 | ||
920 | // Calculate the property type | |
921 | 215 | if (target instanceof DynaBean) { |
922 | 48 | DynaClass dynaClass = ((DynaBean) target).getDynaClass(); |
923 | 48 | DynaProperty dynaProperty = dynaClass.getDynaProperty(propName); |
924 | 48 | if (dynaProperty == null) { |
925 | 0 | return; // Skip this property setter |
926 | } | |
927 | 48 | type = dynaProperty.getType(); |
928 | 48 | } else if (target instanceof Map) { |
929 | 2 | type = Object.class; |
930 | 165 | } else if (target != null && target.getClass().isArray() && index >= 0) { |
931 | 2 | type = Array.get(target, index).getClass(); |
932 | } else { | |
933 | 163 | PropertyDescriptor descriptor = null; |
934 | try { | |
935 | 163 | descriptor = |
936 | getPropertyUtils().getPropertyDescriptor(target, name); | |
937 | 162 | if (descriptor == null) { |
938 | 0 | return; // Skip this property setter |
939 | } | |
940 | 0 | } catch (NoSuchMethodException e) { |
941 | 0 | return; // Skip this property setter |
942 | 162 | } |
943 | 162 | if (descriptor instanceof MappedPropertyDescriptor) { |
944 | 6 | if (((MappedPropertyDescriptor) descriptor).getMappedWriteMethod() == null) { |
945 | 0 | if (log.isDebugEnabled()) { |
946 | 0 | log.debug("Skipping read-only property"); |
947 | } | |
948 | 0 | return; // Read-only, skip this property setter |
949 | } | |
950 | 6 | type = ((MappedPropertyDescriptor) descriptor). |
951 | getMappedPropertyType(); | |
952 | 156 | } else if (index >= 0 && descriptor instanceof IndexedPropertyDescriptor) { |
953 | 12 | if (((IndexedPropertyDescriptor) descriptor).getIndexedWriteMethod() == null) { |
954 | 0 | if (log.isDebugEnabled()) { |
955 | 0 | log.debug("Skipping read-only property"); |
956 | } | |
957 | 0 | return; // Read-only, skip this property setter |
958 | } | |
959 | 12 | type = ((IndexedPropertyDescriptor) descriptor). |
960 | getIndexedPropertyType(); | |
961 | 144 | } else if (key != null) { |
962 | 1 | if (descriptor.getReadMethod() == null) { |
963 | 0 | if (log.isDebugEnabled()) { |
964 | 0 | log.debug("Skipping read-only property"); |
965 | } | |
966 | 0 | return; // Read-only, skip this property setter |
967 | } | |
968 | 1 | type = (value == null) ? Object.class : value.getClass(); |
969 | } else { | |
970 | 143 | if (descriptor.getWriteMethod() == null) { |
971 | 2 | if (log.isDebugEnabled()) { |
972 | 0 | log.debug("Skipping read-only property"); |
973 | } | |
974 | 2 | return; // Read-only, skip this property setter |
975 | } | |
976 | 141 | type = descriptor.getPropertyType(); |
977 | } | |
978 | } | |
979 | ||
980 | // Convert the specified value to the required type | |
981 | 212 | Object newValue = null; |
982 | 212 | if (type.isArray() && (index < 0)) { // Scalar value into array |
983 | 20 | if (value == null) { |
984 | 3 | String[] values = new String[1]; |
985 | 3 | values[0] = null; |
986 | 3 | newValue = getConvertUtils().convert(values, type); |
987 | 3 | } else if (value instanceof String) { |
988 | 8 | newValue = getConvertUtils().convert(value, type); |
989 | 9 | } else if (value instanceof String[]) { |
990 | 5 | newValue = getConvertUtils().convert((String[]) value, type); |
991 | } else { | |
992 | 4 | newValue = convert(value, type); |
993 | } | |
994 | 192 | } else if (type.isArray()) { // Indexed value into array |
995 | 10 | if (value instanceof String || value == null) { |
996 | 8 | newValue = getConvertUtils().convert((String) value, |
997 | type.getComponentType()); | |
998 | 2 | } else if (value instanceof String[]) { |
999 | 0 | newValue = getConvertUtils().convert(((String[]) value)[0], |
1000 | type.getComponentType()); | |
1001 | } else { | |
1002 | 2 | newValue = convert(value, type.getComponentType()); |
1003 | } | |
1004 | } else { // Value into scalar | |
1005 | 182 | if (value instanceof String) { |
1006 | 70 | newValue = getConvertUtils().convert((String) value, type); |
1007 | 112 | } else if (value instanceof String[]) { |
1008 | 0 | newValue = getConvertUtils().convert(((String[]) value)[0], |
1009 | type); | |
1010 | } else { | |
1011 | 112 | newValue = convert(value, type); |
1012 | } | |
1013 | } | |
1014 | ||
1015 | // Invoke the setter method | |
1016 | try { | |
1017 | 210 | getPropertyUtils().setProperty(target, name, newValue); |
1018 | 0 | } catch (NoSuchMethodException e) { |
1019 | 0 | throw new InvocationTargetException |
1020 | (e, "Cannot set " + propName); | |
1021 | 210 | } |
1022 | ||
1023 | 210 | } |
1024 | ||
1025 | /** | |
1026 | * Gets the <code>ConvertUtilsBean</code> instance used to perform the conversions. | |
1027 | * | |
1028 | * @return The ConvertUtils bean instance | |
1029 | */ | |
1030 | public ConvertUtilsBean getConvertUtils() { | |
1031 | 1094 | return convertUtilsBean; |
1032 | } | |
1033 | ||
1034 | /** | |
1035 | * Gets the <code>PropertyUtilsBean</code> instance used to access properties. | |
1036 | * | |
1037 | * @return The ConvertUtils bean instance | |
1038 | */ | |
1039 | public PropertyUtilsBean getPropertyUtils() { | |
1040 | 2989 | return propertyUtilsBean; |
1041 | } | |
1042 | ||
1043 | /** | |
1044 | * If we're running on JDK 1.4 or later, initialize the cause for the given throwable. | |
1045 | * | |
1046 | * @param throwable The throwable. | |
1047 | * @param cause The cause of the throwable. | |
1048 | * @return true if the cause was initialized, otherwise false. | |
1049 | * @since 1.8.0 | |
1050 | */ | |
1051 | public boolean initCause(Throwable throwable, Throwable cause) { | |
1052 | 72 | if (INIT_CAUSE_METHOD != null && cause != null) { |
1053 | try { | |
1054 | 72 | INIT_CAUSE_METHOD.invoke(throwable, new Object[] { cause }); |
1055 | 72 | return true; |
1056 | 0 | } catch (Throwable e) { |
1057 | 0 | return false; // can't initialize cause |
1058 | } | |
1059 | } | |
1060 | 0 | return false; |
1061 | } | |
1062 | ||
1063 | /** | |
1064 | * <p>Convert the value to an object of the specified class (if | |
1065 | * possible).</p> | |
1066 | * | |
1067 | * @param value Value to be converted (may be null) | |
1068 | * @param type Class of the value to be converted to | |
1069 | * @return The converted value | |
1070 | * | |
1071 | * @exception ConversionException if thrown by an underlying Converter | |
1072 | * @since 1.8.0 | |
1073 | */ | |
1074 | protected Object convert(Object value, Class type) { | |
1075 | 228 | Converter converter = getConvertUtils().lookup(type); |
1076 | 228 | if (converter != null) { |
1077 | 212 | log.trace(" USING CONVERTER " + converter); |
1078 | 212 | return converter.convert(type, value); |
1079 | } else { | |
1080 | 16 | return value; |
1081 | } | |
1082 | } | |
1083 | ||
1084 | /** | |
1085 | * Returns a <code>Method<code> allowing access to | |
1086 | * {@link Throwable#initCause(Throwable)} method of {@link Throwable}, | |
1087 | * or <code>null</code> if the method | |
1088 | * does not exist. | |
1089 | * | |
1090 | * @return A <code>Method<code> for <code>Throwable.initCause</code>, or | |
1091 | * <code>null</code> if unavailable. | |
1092 | */ | |
1093 | private static Method getInitCauseMethod() { | |
1094 | try { | |
1095 | 1 | Class[] paramsClasses = new Class[] { Throwable.class }; |
1096 | 1 | return Throwable.class.getMethod("initCause", paramsClasses); |
1097 | 0 | } catch (NoSuchMethodException e) { |
1098 | 0 | Log log = LogFactory.getLog(BeanUtils.class); |
1099 | 0 | if (log.isWarnEnabled()) { |
1100 | 0 | log.warn("Throwable does not have initCause() method in JDK 1.3"); |
1101 | } | |
1102 | 0 | return null; |
1103 | 0 | } catch (Throwable e) { |
1104 | 0 | Log log = LogFactory.getLog(BeanUtils.class); |
1105 | 0 | if (log.isWarnEnabled()) { |
1106 | 0 | log.warn("Error getting the Throwable initCause() method", e); |
1107 | } | |
1108 | 0 | return null; |
1109 | } | |
1110 | } | |
1111 | } |