001 /*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements. See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License. You may obtain a copy of the License at
008 *
009 * http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017 package org.apache.commons.beanutils;
018
019 import java.beans.BeanInfo;
020 import java.beans.IntrospectionException;
021 import java.beans.Introspector;
022 import java.beans.PropertyDescriptor;
023 import java.lang.reflect.Constructor;
024 import java.lang.reflect.InvocationTargetException;
025 import java.lang.reflect.Method;
026 import java.util.AbstractMap;
027 import java.util.AbstractSet;
028 import java.util.ArrayList;
029 import java.util.Collection;
030 import java.util.Collections;
031 import java.util.HashMap;
032 import java.util.Iterator;
033 import java.util.Map;
034 import java.util.Set;
035
036 import org.apache.commons.collections.list.UnmodifiableList;
037 import org.apache.commons.collections.keyvalue.AbstractMapEntry;
038 import org.apache.commons.collections.set.UnmodifiableSet;
039 import org.apache.commons.collections.Transformer;
040
041 /**
042 * An implementation of Map for JavaBeans which uses introspection to
043 * get and put properties in the bean.
044 * <p>
045 * If an exception occurs during attempts to get or set a property then the
046 * property is considered non existent in the Map
047 *
048 * @version $Revision: 812176 $ $Date: 2009-09-07 15:59:25 +0100 (Mon, 07 Sep 2009) $
049 *
050 * @author James Strachan
051 * @author Stephen Colebourne
052 */
053 public class BeanMap extends AbstractMap implements Cloneable {
054
055 private transient Object bean;
056
057 private transient HashMap readMethods = new HashMap();
058 private transient HashMap writeMethods = new HashMap();
059 private transient HashMap types = new HashMap();
060
061 /**
062 * An empty array. Used to invoke accessors via reflection.
063 */
064 public static final Object[] NULL_ARGUMENTS = {};
065
066 /**
067 * Maps primitive Class types to transformers. The transformer
068 * transform strings into the appropriate primitive wrapper.
069 *
070 * N.B. private & unmodifiable replacement for the (public & static) defaultTransformers instance.
071 */
072 private static final Map typeTransformers = Collections.unmodifiableMap(createTypeTransformers());
073
074 /**
075 * This HashMap has been made unmodifiable to prevent issues when
076 * loaded in a shared ClassLoader enviroment.
077 *
078 * @see "http://issues.apache.org/jira/browse/BEANUTILS-112"
079 * @deprecated Use {@link BeanMap#getTypeTransformer(Class)} method
080 */
081 public static HashMap defaultTransformers = new HashMap() {
082 public void clear() {
083 throw new UnsupportedOperationException();
084 }
085 public boolean containsKey(Object key) {
086 return typeTransformers.containsKey(key);
087 }
088 public boolean containsValue(Object value) {
089 return typeTransformers.containsValue(value);
090 }
091 public Set entrySet() {
092 return typeTransformers.entrySet();
093 }
094 public Object get(Object key) {
095 return typeTransformers.get(key);
096 }
097 public boolean isEmpty() {
098 return false;
099 }
100 public Set keySet() {
101 return typeTransformers.keySet();
102 }
103 public Object put(Object key, Object value) {
104 throw new UnsupportedOperationException();
105 }
106 public void putAll(Map m) {
107 throw new UnsupportedOperationException();
108 }
109 public Object remove(Object key) {
110 throw new UnsupportedOperationException();
111 }
112 public int size() {
113 return typeTransformers.size();
114 }
115 public Collection values() {
116 return typeTransformers.values();
117 }
118 };
119
120 private static Map createTypeTransformers() {
121 Map defaultTransformers = new HashMap();
122 defaultTransformers.put(
123 Boolean.TYPE,
124 new Transformer() {
125 public Object transform( Object input ) {
126 return Boolean.valueOf( input.toString() );
127 }
128 }
129 );
130 defaultTransformers.put(
131 Character.TYPE,
132 new Transformer() {
133 public Object transform( Object input ) {
134 return new Character( input.toString().charAt( 0 ) );
135 }
136 }
137 );
138 defaultTransformers.put(
139 Byte.TYPE,
140 new Transformer() {
141 public Object transform( Object input ) {
142 return Byte.valueOf( input.toString() );
143 }
144 }
145 );
146 defaultTransformers.put(
147 Short.TYPE,
148 new Transformer() {
149 public Object transform( Object input ) {
150 return Short.valueOf( input.toString() );
151 }
152 }
153 );
154 defaultTransformers.put(
155 Integer.TYPE,
156 new Transformer() {
157 public Object transform( Object input ) {
158 return Integer.valueOf( input.toString() );
159 }
160 }
161 );
162 defaultTransformers.put(
163 Long.TYPE,
164 new Transformer() {
165 public Object transform( Object input ) {
166 return Long.valueOf( input.toString() );
167 }
168 }
169 );
170 defaultTransformers.put(
171 Float.TYPE,
172 new Transformer() {
173 public Object transform( Object input ) {
174 return Float.valueOf( input.toString() );
175 }
176 }
177 );
178 defaultTransformers.put(
179 Double.TYPE,
180 new Transformer() {
181 public Object transform( Object input ) {
182 return Double.valueOf( input.toString() );
183 }
184 }
185 );
186 return defaultTransformers;
187 }
188
189
190 // Constructors
191 //-------------------------------------------------------------------------
192
193 /**
194 * Constructs a new empty <code>BeanMap</code>.
195 */
196 public BeanMap() {
197 }
198
199 /**
200 * Constructs a new <code>BeanMap</code> that operates on the
201 * specified bean. If the given bean is <code>null</code>, then
202 * this map will be empty.
203 *
204 * @param bean the bean for this map to operate on
205 */
206 public BeanMap(Object bean) {
207 this.bean = bean;
208 initialise();
209 }
210
211 // Map interface
212 //-------------------------------------------------------------------------
213
214 /**
215 * Renders a string representation of this object.
216 * @return a <code>String</code> representation of this object
217 */
218 public String toString() {
219 return "BeanMap<" + String.valueOf(bean) + ">";
220 }
221
222 /**
223 * Clone this bean map using the following process:
224 *
225 * <ul>
226 * <li>If there is no underlying bean, return a cloned BeanMap without a
227 * bean.
228 *
229 * <li>Since there is an underlying bean, try to instantiate a new bean of
230 * the same type using Class.newInstance().
231 *
232 * <li>If the instantiation fails, throw a CloneNotSupportedException
233 *
234 * <li>Clone the bean map and set the newly instantiated bean as the
235 * underlying bean for the bean map.
236 *
237 * <li>Copy each property that is both readable and writable from the
238 * existing object to a cloned bean map.
239 *
240 * <li>If anything fails along the way, throw a
241 * CloneNotSupportedException.
242 *
243 * <ul>
244 *
245 * @return a cloned instance of this bean map
246 * @throws CloneNotSupportedException if the underlying bean
247 * cannot be cloned
248 */
249 public Object clone() throws CloneNotSupportedException {
250 BeanMap newMap = (BeanMap)super.clone();
251
252 if(bean == null) {
253 // no bean, just an empty bean map at the moment. return a newly
254 // cloned and empty bean map.
255 return newMap;
256 }
257
258 Object newBean = null;
259 Class beanClass = bean.getClass(); // Cannot throw Exception
260 try {
261 newBean = beanClass.newInstance();
262 } catch (Exception e) {
263 // unable to instantiate
264 throw new CloneNotSupportedException
265 ("Unable to instantiate the underlying bean \"" +
266 beanClass.getName() + "\": " + e);
267 }
268
269 try {
270 newMap.setBean(newBean);
271 } catch (Exception exception) {
272 throw new CloneNotSupportedException
273 ("Unable to set bean in the cloned bean map: " +
274 exception);
275 }
276
277 try {
278 // copy only properties that are readable and writable. If its
279 // not readable, we can't get the value from the old map. If
280 // its not writable, we can't write a value into the new map.
281 Iterator readableKeys = readMethods.keySet().iterator();
282 while(readableKeys.hasNext()) {
283 Object key = readableKeys.next();
284 if(getWriteMethod(key) != null) {
285 newMap.put(key, get(key));
286 }
287 }
288 } catch (Exception exception) {
289 throw new CloneNotSupportedException
290 ("Unable to copy bean values to cloned bean map: " +
291 exception);
292 }
293
294 return newMap;
295 }
296
297 /**
298 * Puts all of the writable properties from the given BeanMap into this
299 * BeanMap. Read-only and Write-only properties will be ignored.
300 *
301 * @param map the BeanMap whose properties to put
302 */
303 public void putAllWriteable(BeanMap map) {
304 Iterator readableKeys = map.readMethods.keySet().iterator();
305 while (readableKeys.hasNext()) {
306 Object key = readableKeys.next();
307 if (getWriteMethod(key) != null) {
308 this.put(key, map.get(key));
309 }
310 }
311 }
312
313
314 /**
315 * This method reinitializes the bean map to have default values for the
316 * bean's properties. This is accomplished by constructing a new instance
317 * of the bean which the map uses as its underlying data source. This
318 * behavior for <code>clear()</code> differs from the Map contract in that
319 * the mappings are not actually removed from the map (the mappings for a
320 * BeanMap are fixed).
321 */
322 public void clear() {
323 if(bean == null) {
324 return;
325 }
326
327 Class beanClass = null;
328 try {
329 beanClass = bean.getClass();
330 bean = beanClass.newInstance();
331 }
332 catch (Exception e) {
333 throw new UnsupportedOperationException( "Could not create new instance of class: " + beanClass );
334 }
335 }
336
337 /**
338 * Returns true if the bean defines a property with the given name.
339 * <p>
340 * The given name must be a <code>String</code>; if not, this method
341 * returns false. This method will also return false if the bean
342 * does not define a property with that name.
343 * <p>
344 * Write-only properties will not be matched as the test operates against
345 * property read methods.
346 *
347 * @param name the name of the property to check
348 * @return false if the given name is null or is not a <code>String</code>;
349 * false if the bean does not define a property with that name; or
350 * true if the bean does define a property with that name
351 */
352 public boolean containsKey(Object name) {
353 Method method = getReadMethod(name);
354 return method != null;
355 }
356
357 /**
358 * Returns true if the bean defines a property whose current value is
359 * the given object.
360 *
361 * @param value the value to check
362 * @return false true if the bean has at least one property whose
363 * current value is that object, false otherwise
364 */
365 public boolean containsValue(Object value) {
366 // use default implementation
367 return super.containsValue(value);
368 }
369
370 /**
371 * Returns the value of the bean's property with the given name.
372 * <p>
373 * The given name must be a {@link String} and must not be
374 * null; otherwise, this method returns <code>null</code>.
375 * If the bean defines a property with the given name, the value of
376 * that property is returned. Otherwise, <code>null</code> is
377 * returned.
378 * <p>
379 * Write-only properties will not be matched as the test operates against
380 * property read methods.
381 *
382 * @param name the name of the property whose value to return
383 * @return the value of the property with that name
384 */
385 public Object get(Object name) {
386 if ( bean != null ) {
387 Method method = getReadMethod( name );
388 if ( method != null ) {
389 try {
390 return method.invoke( bean, NULL_ARGUMENTS );
391 }
392 catch ( IllegalAccessException e ) {
393 logWarn( e );
394 }
395 catch ( IllegalArgumentException e ) {
396 logWarn( e );
397 }
398 catch ( InvocationTargetException e ) {
399 logWarn( e );
400 }
401 catch ( NullPointerException e ) {
402 logWarn( e );
403 }
404 }
405 }
406 return null;
407 }
408
409 /**
410 * Sets the bean property with the given name to the given value.
411 *
412 * @param name the name of the property to set
413 * @param value the value to set that property to
414 * @return the previous value of that property
415 * @throws IllegalArgumentException if the given name is null;
416 * if the given name is not a {@link String}; if the bean doesn't
417 * define a property with that name; or if the bean property with
418 * that name is read-only
419 * @throws ClassCastException if an error occurs creating the method args
420 */
421 public Object put(Object name, Object value) throws IllegalArgumentException, ClassCastException {
422 if ( bean != null ) {
423 Object oldValue = get( name );
424 Method method = getWriteMethod( name );
425 if ( method == null ) {
426 throw new IllegalArgumentException( "The bean of type: "+
427 bean.getClass().getName() + " has no property called: " + name );
428 }
429 try {
430 Object[] arguments = createWriteMethodArguments( method, value );
431 method.invoke( bean, arguments );
432
433 Object newValue = get( name );
434 firePropertyChange( name, oldValue, newValue );
435 }
436 catch ( InvocationTargetException e ) {
437 logInfo( e );
438 throw new IllegalArgumentException( e.getMessage() );
439 }
440 catch ( IllegalAccessException e ) {
441 logInfo( e );
442 throw new IllegalArgumentException( e.getMessage() );
443 }
444 return oldValue;
445 }
446 return null;
447 }
448
449 /**
450 * Returns the number of properties defined by the bean.
451 *
452 * @return the number of properties defined by the bean
453 */
454 public int size() {
455 return readMethods.size();
456 }
457
458
459 /**
460 * Get the keys for this BeanMap.
461 * <p>
462 * Write-only properties are <b>not</b> included in the returned set of
463 * property names, although it is possible to set their value and to get
464 * their type.
465 *
466 * @return BeanMap keys. The Set returned by this method is not
467 * modifiable.
468 */
469 public Set keySet() {
470 return UnmodifiableSet.decorate(readMethods.keySet());
471 }
472
473 /**
474 * Gets a Set of MapEntry objects that are the mappings for this BeanMap.
475 * <p>
476 * Each MapEntry can be set but not removed.
477 *
478 * @return the unmodifiable set of mappings
479 */
480 public Set entrySet() {
481 return UnmodifiableSet.decorate(new AbstractSet() {
482 public Iterator iterator() {
483 return entryIterator();
484 }
485 public int size() {
486 return BeanMap.this.readMethods.size();
487 }
488 });
489 }
490
491 /**
492 * Returns the values for the BeanMap.
493 *
494 * @return values for the BeanMap. The returned collection is not
495 * modifiable.
496 */
497 public Collection values() {
498 ArrayList answer = new ArrayList( readMethods.size() );
499 for ( Iterator iter = valueIterator(); iter.hasNext(); ) {
500 answer.add( iter.next() );
501 }
502 return UnmodifiableList.decorate(answer);
503 }
504
505
506 // Helper methods
507 //-------------------------------------------------------------------------
508
509 /**
510 * Returns the type of the property with the given name.
511 *
512 * @param name the name of the property
513 * @return the type of the property, or <code>null</code> if no such
514 * property exists
515 */
516 public Class getType(String name) {
517 return (Class) types.get( name );
518 }
519
520 /**
521 * Convenience method for getting an iterator over the keys.
522 * <p>
523 * Write-only properties will not be returned in the iterator.
524 *
525 * @return an iterator over the keys
526 */
527 public Iterator keyIterator() {
528 return readMethods.keySet().iterator();
529 }
530
531 /**
532 * Convenience method for getting an iterator over the values.
533 *
534 * @return an iterator over the values
535 */
536 public Iterator valueIterator() {
537 final Iterator iter = keyIterator();
538 return new Iterator() {
539 public boolean hasNext() {
540 return iter.hasNext();
541 }
542 public Object next() {
543 Object key = iter.next();
544 return get(key);
545 }
546 public void remove() {
547 throw new UnsupportedOperationException( "remove() not supported for BeanMap" );
548 }
549 };
550 }
551
552 /**
553 * Convenience method for getting an iterator over the entries.
554 *
555 * @return an iterator over the entries
556 */
557 public Iterator entryIterator() {
558 final Iterator iter = keyIterator();
559 return new Iterator() {
560 public boolean hasNext() {
561 return iter.hasNext();
562 }
563 public Object next() {
564 Object key = iter.next();
565 Object value = get(key);
566 return new Entry( BeanMap.this, key, value );
567 }
568 public void remove() {
569 throw new UnsupportedOperationException( "remove() not supported for BeanMap" );
570 }
571 };
572 }
573
574
575 // Properties
576 //-------------------------------------------------------------------------
577
578 /**
579 * Returns the bean currently being operated on. The return value may
580 * be null if this map is empty.
581 *
582 * @return the bean being operated on by this map
583 */
584 public Object getBean() {
585 return bean;
586 }
587
588 /**
589 * Sets the bean to be operated on by this map. The given value may
590 * be null, in which case this map will be empty.
591 *
592 * @param newBean the new bean to operate on
593 */
594 public void setBean( Object newBean ) {
595 bean = newBean;
596 reinitialise();
597 }
598
599 /**
600 * Returns the accessor for the property with the given name.
601 *
602 * @param name the name of the property
603 * @return the accessor method for the property, or null
604 */
605 public Method getReadMethod(String name) {
606 return (Method) readMethods.get(name);
607 }
608
609 /**
610 * Returns the mutator for the property with the given name.
611 *
612 * @param name the name of the property
613 * @return the mutator method for the property, or null
614 */
615 public Method getWriteMethod(String name) {
616 return (Method) writeMethods.get(name);
617 }
618
619
620 // Implementation methods
621 //-------------------------------------------------------------------------
622
623 /**
624 * Returns the accessor for the property with the given name.
625 *
626 * @param name the name of the property
627 * @return null if the name is null; null if the name is not a
628 * {@link String}; null if no such property exists; or the accessor
629 * method for that property
630 */
631 protected Method getReadMethod( Object name ) {
632 return (Method) readMethods.get( name );
633 }
634
635 /**
636 * Returns the mutator for the property with the given name.
637 *
638 * @param name the name of the
639 * @return null if the name is null; null if the name is not a
640 * {@link String}; null if no such property exists; null if the
641 * property is read-only; or the mutator method for that property
642 */
643 protected Method getWriteMethod( Object name ) {
644 return (Method) writeMethods.get( name );
645 }
646
647 /**
648 * Reinitializes this bean. Called during {@link #setBean(Object)}.
649 * Does introspection to find properties.
650 */
651 protected void reinitialise() {
652 readMethods.clear();
653 writeMethods.clear();
654 types.clear();
655 initialise();
656 }
657
658 private void initialise() {
659 if(getBean() == null) {
660 return;
661 }
662
663 Class beanClass = getBean().getClass();
664 try {
665 //BeanInfo beanInfo = Introspector.getBeanInfo( bean, null );
666 BeanInfo beanInfo = Introspector.getBeanInfo( beanClass );
667 PropertyDescriptor[] propertyDescriptors = beanInfo.getPropertyDescriptors();
668 if ( propertyDescriptors != null ) {
669 for ( int i = 0; i < propertyDescriptors.length; i++ ) {
670 PropertyDescriptor propertyDescriptor = propertyDescriptors[i];
671 if ( propertyDescriptor != null ) {
672 String name = propertyDescriptor.getName();
673 Method readMethod = propertyDescriptor.getReadMethod();
674 Method writeMethod = propertyDescriptor.getWriteMethod();
675 Class aType = propertyDescriptor.getPropertyType();
676
677 if ( readMethod != null ) {
678 readMethods.put( name, readMethod );
679 }
680 if ( writeMethod != null ) {
681 writeMethods.put( name, writeMethod );
682 }
683 types.put( name, aType );
684 }
685 }
686 }
687 }
688 catch ( IntrospectionException e ) {
689 logWarn( e );
690 }
691 }
692
693 /**
694 * Called during a successful {@link #put(Object,Object)} operation.
695 * Default implementation does nothing. Override to be notified of
696 * property changes in the bean caused by this map.
697 *
698 * @param key the name of the property that changed
699 * @param oldValue the old value for that property
700 * @param newValue the new value for that property
701 */
702 protected void firePropertyChange( Object key, Object oldValue, Object newValue ) {
703 }
704
705 // Implementation classes
706 //-------------------------------------------------------------------------
707
708 /**
709 * Map entry used by {@link BeanMap}.
710 */
711 protected static class Entry extends AbstractMapEntry {
712 private BeanMap owner;
713
714 /**
715 * Constructs a new <code>Entry</code>.
716 *
717 * @param owner the BeanMap this entry belongs to
718 * @param key the key for this entry
719 * @param value the value for this entry
720 */
721 protected Entry( BeanMap owner, Object key, Object value ) {
722 super( key, value );
723 this.owner = owner;
724 }
725
726 /**
727 * Sets the value.
728 *
729 * @param value the new value for the entry
730 * @return the old value for the entry
731 */
732 public Object setValue(Object value) {
733 Object key = getKey();
734 Object oldValue = owner.get( key );
735
736 owner.put( key, value );
737 Object newValue = owner.get( key );
738 super.setValue( newValue );
739 return oldValue;
740 }
741 }
742
743 /**
744 * Creates an array of parameters to pass to the given mutator method.
745 * If the given object is not the right type to pass to the method
746 * directly, it will be converted using {@link #convertType(Class,Object)}.
747 *
748 * @param method the mutator method
749 * @param value the value to pass to the mutator method
750 * @return an array containing one object that is either the given value
751 * or a transformed value
752 * @throws IllegalAccessException if {@link #convertType(Class,Object)}
753 * raises it
754 * @throws IllegalArgumentException if any other exception is raised
755 * by {@link #convertType(Class,Object)}
756 * @throws ClassCastException if an error occurs creating the method args
757 */
758 protected Object[] createWriteMethodArguments( Method method, Object value )
759 throws IllegalAccessException, ClassCastException {
760 try {
761 if ( value != null ) {
762 Class[] types = method.getParameterTypes();
763 if ( types != null && types.length > 0 ) {
764 Class paramType = types[0];
765 if ( ! paramType.isAssignableFrom( value.getClass() ) ) {
766 value = convertType( paramType, value );
767 }
768 }
769 }
770 Object[] answer = { value };
771 return answer;
772 }
773 catch ( InvocationTargetException e ) {
774 logInfo( e );
775 throw new IllegalArgumentException( e.getMessage() );
776 }
777 catch ( InstantiationException e ) {
778 logInfo( e );
779 throw new IllegalArgumentException( e.getMessage() );
780 }
781 }
782
783 /**
784 * Converts the given value to the given type. First, reflection is
785 * is used to find a public constructor declared by the given class
786 * that takes one argument, which must be the precise type of the
787 * given value. If such a constructor is found, a new object is
788 * created by passing the given value to that constructor, and the
789 * newly constructed object is returned.<P>
790 *
791 * If no such constructor exists, and the given type is a primitive
792 * type, then the given value is converted to a string using its
793 * {@link Object#toString() toString()} method, and that string is
794 * parsed into the correct primitive type using, for instance,
795 * {@link Integer#valueOf(String)} to convert the string into an
796 * <code>int</code>.<P>
797 *
798 * If no special constructor exists and the given type is not a
799 * primitive type, this method returns the original value.
800 *
801 * @param newType the type to convert the value to
802 * @param value the value to convert
803 * @return the converted value
804 * @throws NumberFormatException if newType is a primitive type, and
805 * the string representation of the given value cannot be converted
806 * to that type
807 * @throws InstantiationException if the constructor found with
808 * reflection raises it
809 * @throws InvocationTargetException if the constructor found with
810 * reflection raises it
811 * @throws IllegalAccessException never
812 * @throws IllegalArgumentException never
813 */
814 protected Object convertType( Class newType, Object value )
815 throws InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
816
817 // try call constructor
818 Class[] types = { value.getClass() };
819 try {
820 Constructor constructor = newType.getConstructor( types );
821 Object[] arguments = { value };
822 return constructor.newInstance( arguments );
823 }
824 catch ( NoSuchMethodException e ) {
825 // try using the transformers
826 Transformer transformer = getTypeTransformer( newType );
827 if ( transformer != null ) {
828 return transformer.transform( value );
829 }
830 return value;
831 }
832 }
833
834 /**
835 * Returns a transformer for the given primitive type.
836 *
837 * @param aType the primitive type whose transformer to return
838 * @return a transformer that will convert strings into that type,
839 * or null if the given type is not a primitive type
840 */
841 protected Transformer getTypeTransformer( Class aType ) {
842 return (Transformer) typeTransformers.get( aType );
843 }
844
845 /**
846 * Logs the given exception to <code>System.out</code>. Used to display
847 * warnings while accessing/mutating the bean.
848 *
849 * @param ex the exception to log
850 */
851 protected void logInfo(Exception ex) {
852 // Deliberately do not use LOG4J or Commons Logging to avoid dependencies
853 System.out.println( "INFO: Exception: " + ex );
854 }
855
856 /**
857 * Logs the given exception to <code>System.err</code>. Used to display
858 * errors while accessing/mutating the bean.
859 *
860 * @param ex the exception to log
861 */
862 protected void logWarn(Exception ex) {
863 // Deliberately do not use LOG4J or Commons Logging to avoid dependencies
864 System.out.println( "WARN: Exception: " + ex );
865 ex.printStackTrace();
866 }
867 }