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: 557796 $ $Date: 2007-07-19 23:28:49 +0100 (Thu, 19 Jul 2007) $
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 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 = null;
260 try {
261 beanClass = bean.getClass();
262 newBean = beanClass.newInstance();
263 } catch (Exception e) {
264 // unable to instantiate
265 throw new CloneNotSupportedException
266 ("Unable to instantiate the underlying bean \"" +
267 beanClass.getName() + "\": " + e);
268 }
269
270 try {
271 newMap.setBean(newBean);
272 } catch (Exception exception) {
273 throw new CloneNotSupportedException
274 ("Unable to set bean in the cloned bean map: " +
275 exception);
276 }
277
278 try {
279 // copy only properties that are readable and writable. If its
280 // not readable, we can't get the value from the old map. If
281 // its not writable, we can't write a value into the new map.
282 Iterator readableKeys = readMethods.keySet().iterator();
283 while(readableKeys.hasNext()) {
284 Object key = readableKeys.next();
285 if(getWriteMethod(key) != null) {
286 newMap.put(key, get(key));
287 }
288 }
289 } catch (Exception exception) {
290 throw new CloneNotSupportedException
291 ("Unable to copy bean values to cloned bean map: " +
292 exception);
293 }
294
295 return newMap;
296 }
297
298 /**
299 * Puts all of the writable properties from the given BeanMap into this
300 * BeanMap. Read-only and Write-only properties will be ignored.
301 *
302 * @param map the BeanMap whose properties to put
303 */
304 public void putAllWriteable(BeanMap map) {
305 Iterator readableKeys = map.readMethods.keySet().iterator();
306 while (readableKeys.hasNext()) {
307 Object key = readableKeys.next();
308 if (getWriteMethod(key) != null) {
309 this.put(key, map.get(key));
310 }
311 }
312 }
313
314
315 /**
316 * This method reinitializes the bean map to have default values for the
317 * bean's properties. This is accomplished by constructing a new instance
318 * of the bean which the map uses as its underlying data source. This
319 * behavior for <code>clear()</code> differs from the Map contract in that
320 * the mappings are not actually removed from the map (the mappings for a
321 * BeanMap are fixed).
322 */
323 public void clear() {
324 if(bean == null) {
325 return;
326 }
327
328 Class beanClass = null;
329 try {
330 beanClass = bean.getClass();
331 bean = beanClass.newInstance();
332 }
333 catch (Exception e) {
334 throw new UnsupportedOperationException( "Could not create new instance of class: " + beanClass );
335 }
336 }
337
338 /**
339 * Returns true if the bean defines a property with the given name.
340 * <p>
341 * The given name must be a <code>String</code>; if not, this method
342 * returns false. This method will also return false if the bean
343 * does not define a property with that name.
344 * <p>
345 * Write-only properties will not be matched as the test operates against
346 * property read methods.
347 *
348 * @param name the name of the property to check
349 * @return false if the given name is null or is not a <code>String</code>;
350 * false if the bean does not define a property with that name; or
351 * true if the bean does define a property with that name
352 */
353 public boolean containsKey(Object name) {
354 Method method = getReadMethod(name);
355 return method != null;
356 }
357
358 /**
359 * Returns true if the bean defines a property whose current value is
360 * the given object.
361 *
362 * @param value the value to check
363 * @return false true if the bean has at least one property whose
364 * current value is that object, false otherwise
365 */
366 public boolean containsValue(Object value) {
367 // use default implementation
368 return super.containsValue(value);
369 }
370
371 /**
372 * Returns the value of the bean's property with the given name.
373 * <p>
374 * The given name must be a {@link String} and must not be
375 * null; otherwise, this method returns <code>null</code>.
376 * If the bean defines a property with the given name, the value of
377 * that property is returned. Otherwise, <code>null</code> is
378 * returned.
379 * <p>
380 * Write-only properties will not be matched as the test operates against
381 * property read methods.
382 *
383 * @param name the name of the property whose value to return
384 * @return the value of the property with that name
385 */
386 public Object get(Object name) {
387 if ( bean != null ) {
388 Method method = getReadMethod( name );
389 if ( method != null ) {
390 try {
391 return method.invoke( bean, NULL_ARGUMENTS );
392 }
393 catch ( IllegalAccessException e ) {
394 logWarn( e );
395 }
396 catch ( IllegalArgumentException e ) {
397 logWarn( e );
398 }
399 catch ( InvocationTargetException e ) {
400 logWarn( e );
401 }
402 catch ( NullPointerException e ) {
403 logWarn( e );
404 }
405 }
406 }
407 return null;
408 }
409
410 /**
411 * Sets the bean property with the given name to the given value.
412 *
413 * @param name the name of the property to set
414 * @param value the value to set that property to
415 * @return the previous value of that property
416 * @throws IllegalArgumentException if the given name is null;
417 * if the given name is not a {@link String}; if the bean doesn't
418 * define a property with that name; or if the bean property with
419 * that name is read-only
420 * @throws ClassCastException if an error occurs creating the method args
421 */
422 public Object put(Object name, Object value) throws IllegalArgumentException, ClassCastException {
423 if ( bean != null ) {
424 Object oldValue = get( name );
425 Method method = getWriteMethod( name );
426 if ( method == null ) {
427 throw new IllegalArgumentException( "The bean of type: "+
428 bean.getClass().getName() + " has no property called: " + name );
429 }
430 try {
431 Object[] arguments = createWriteMethodArguments( method, value );
432 method.invoke( bean, arguments );
433
434 Object newValue = get( name );
435 firePropertyChange( name, oldValue, newValue );
436 }
437 catch ( InvocationTargetException e ) {
438 logInfo( e );
439 throw new IllegalArgumentException( e.getMessage() );
440 }
441 catch ( IllegalAccessException e ) {
442 logInfo( e );
443 throw new IllegalArgumentException( e.getMessage() );
444 }
445 return oldValue;
446 }
447 return null;
448 }
449
450 /**
451 * Returns the number of properties defined by the bean.
452 *
453 * @return the number of properties defined by the bean
454 */
455 public int size() {
456 return readMethods.size();
457 }
458
459
460 /**
461 * Get the keys for this BeanMap.
462 * <p>
463 * Write-only properties are <b>not</b> included in the returned set of
464 * property names, although it is possible to set their value and to get
465 * their type.
466 *
467 * @return BeanMap keys. The Set returned by this method is not
468 * modifiable.
469 */
470 public Set keySet() {
471 return UnmodifiableSet.decorate(readMethods.keySet());
472 }
473
474 /**
475 * Gets a Set of MapEntry objects that are the mappings for this BeanMap.
476 * <p>
477 * Each MapEntry can be set but not removed.
478 *
479 * @return the unmodifiable set of mappings
480 */
481 public Set entrySet() {
482 return UnmodifiableSet.decorate(new AbstractSet() {
483 public Iterator iterator() {
484 return entryIterator();
485 }
486 public int size() {
487 return BeanMap.this.readMethods.size();
488 }
489 });
490 }
491
492 /**
493 * Returns the values for the BeanMap.
494 *
495 * @return values for the BeanMap. The returned collection is not
496 * modifiable.
497 */
498 public Collection values() {
499 ArrayList answer = new ArrayList( readMethods.size() );
500 for ( Iterator iter = valueIterator(); iter.hasNext(); ) {
501 answer.add( iter.next() );
502 }
503 return UnmodifiableList.decorate(answer);
504 }
505
506
507 // Helper methods
508 //-------------------------------------------------------------------------
509
510 /**
511 * Returns the type of the property with the given name.
512 *
513 * @param name the name of the property
514 * @return the type of the property, or <code>null</code> if no such
515 * property exists
516 */
517 public Class getType(String name) {
518 return (Class) types.get( name );
519 }
520
521 /**
522 * Convenience method for getting an iterator over the keys.
523 * <p>
524 * Write-only properties will not be returned in the iterator.
525 *
526 * @return an iterator over the keys
527 */
528 public Iterator keyIterator() {
529 return readMethods.keySet().iterator();
530 }
531
532 /**
533 * Convenience method for getting an iterator over the values.
534 *
535 * @return an iterator over the values
536 */
537 public Iterator valueIterator() {
538 final Iterator iter = keyIterator();
539 return new Iterator() {
540 public boolean hasNext() {
541 return iter.hasNext();
542 }
543 public Object next() {
544 Object key = iter.next();
545 return get(key);
546 }
547 public void remove() {
548 throw new UnsupportedOperationException( "remove() not supported for BeanMap" );
549 }
550 };
551 }
552
553 /**
554 * Convenience method for getting an iterator over the entries.
555 *
556 * @return an iterator over the entries
557 */
558 public Iterator entryIterator() {
559 final Iterator iter = keyIterator();
560 return new Iterator() {
561 public boolean hasNext() {
562 return iter.hasNext();
563 }
564 public Object next() {
565 Object key = iter.next();
566 Object value = get(key);
567 return new Entry( BeanMap.this, key, value );
568 }
569 public void remove() {
570 throw new UnsupportedOperationException( "remove() not supported for BeanMap" );
571 }
572 };
573 }
574
575
576 // Properties
577 //-------------------------------------------------------------------------
578
579 /**
580 * Returns the bean currently being operated on. The return value may
581 * be null if this map is empty.
582 *
583 * @return the bean being operated on by this map
584 */
585 public Object getBean() {
586 return bean;
587 }
588
589 /**
590 * Sets the bean to be operated on by this map. The given value may
591 * be null, in which case this map will be empty.
592 *
593 * @param newBean the new bean to operate on
594 */
595 public void setBean( Object newBean ) {
596 bean = newBean;
597 reinitialise();
598 }
599
600 /**
601 * Returns the accessor for the property with the given name.
602 *
603 * @param name the name of the property
604 * @return the accessor method for the property, or null
605 */
606 public Method getReadMethod(String name) {
607 return (Method) readMethods.get(name);
608 }
609
610 /**
611 * Returns the mutator for the property with the given name.
612 *
613 * @param name the name of the property
614 * @return the mutator method for the property, or null
615 */
616 public Method getWriteMethod(String name) {
617 return (Method) writeMethods.get(name);
618 }
619
620
621 // Implementation methods
622 //-------------------------------------------------------------------------
623
624 /**
625 * Returns the accessor for the property with the given name.
626 *
627 * @param name the name of the property
628 * @return null if the name is null; null if the name is not a
629 * {@link String}; null if no such property exists; or the accessor
630 * method for that property
631 */
632 protected Method getReadMethod( Object name ) {
633 return (Method) readMethods.get( name );
634 }
635
636 /**
637 * Returns the mutator for the property with the given name.
638 *
639 * @param name the name of the
640 * @return null if the name is null; null if the name is not a
641 * {@link String}; null if no such property exists; null if the
642 * property is read-only; or the mutator method for that property
643 */
644 protected Method getWriteMethod( Object name ) {
645 return (Method) writeMethods.get( name );
646 }
647
648 /**
649 * Reinitializes this bean. Called during {@link #setBean(Object)}.
650 * Does introspection to find properties.
651 */
652 protected void reinitialise() {
653 readMethods.clear();
654 writeMethods.clear();
655 types.clear();
656 initialise();
657 }
658
659 private void initialise() {
660 if(getBean() == null) {
661 return;
662 }
663
664 Class beanClass = getBean().getClass();
665 try {
666 //BeanInfo beanInfo = Introspector.getBeanInfo( bean, null );
667 BeanInfo beanInfo = Introspector.getBeanInfo( beanClass );
668 PropertyDescriptor[] propertyDescriptors = beanInfo.getPropertyDescriptors();
669 if ( propertyDescriptors != null ) {
670 for ( int i = 0; i < propertyDescriptors.length; i++ ) {
671 PropertyDescriptor propertyDescriptor = propertyDescriptors[i];
672 if ( propertyDescriptor != null ) {
673 String name = propertyDescriptor.getName();
674 Method readMethod = propertyDescriptor.getReadMethod();
675 Method writeMethod = propertyDescriptor.getWriteMethod();
676 Class aType = propertyDescriptor.getPropertyType();
677
678 if ( readMethod != null ) {
679 readMethods.put( name, readMethod );
680 }
681 if ( writeMethod != null ) {
682 writeMethods.put( name, writeMethod );
683 }
684 types.put( name, aType );
685 }
686 }
687 }
688 }
689 catch ( IntrospectionException e ) {
690 logWarn( e );
691 }
692 }
693
694 /**
695 * Called during a successful {@link #put(Object,Object)} operation.
696 * Default implementation does nothing. Override to be notified of
697 * property changes in the bean caused by this map.
698 *
699 * @param key the name of the property that changed
700 * @param oldValue the old value for that property
701 * @param newValue the new value for that property
702 */
703 protected void firePropertyChange( Object key, Object oldValue, Object newValue ) {
704 }
705
706 // Implementation classes
707 //-------------------------------------------------------------------------
708
709 /**
710 * Map entry used by {@link BeanMap}.
711 */
712 protected static class Entry extends AbstractMapEntry {
713 private BeanMap owner;
714
715 /**
716 * Constructs a new <code>Entry</code>.
717 *
718 * @param owner the BeanMap this entry belongs to
719 * @param key the key for this entry
720 * @param value the value for this entry
721 */
722 protected Entry( BeanMap owner, Object key, Object value ) {
723 super( key, value );
724 this.owner = owner;
725 }
726
727 /**
728 * Sets the value.
729 *
730 * @param value the new value for the entry
731 * @return the old value for the entry
732 */
733 public Object setValue(Object value) {
734 Object key = getKey();
735 Object oldValue = owner.get( key );
736
737 owner.put( key, value );
738 Object newValue = owner.get( key );
739 super.setValue( newValue );
740 return oldValue;
741 }
742 }
743
744 /**
745 * Creates an array of parameters to pass to the given mutator method.
746 * If the given object is not the right type to pass to the method
747 * directly, it will be converted using {@link #convertType(Class,Object)}.
748 *
749 * @param method the mutator method
750 * @param value the value to pass to the mutator method
751 * @return an array containing one object that is either the given value
752 * or a transformed value
753 * @throws IllegalAccessException if {@link #convertType(Class,Object)}
754 * raises it
755 * @throws IllegalArgumentException if any other exception is raised
756 * by {@link #convertType(Class,Object)}
757 * @throws ClassCastException if an error occurs creating the method args
758 */
759 protected Object[] createWriteMethodArguments( Method method, Object value )
760 throws IllegalAccessException, ClassCastException {
761 try {
762 if ( value != null ) {
763 Class[] types = method.getParameterTypes();
764 if ( types != null && types.length > 0 ) {
765 Class paramType = types[0];
766 if ( ! paramType.isAssignableFrom( value.getClass() ) ) {
767 value = convertType( paramType, value );
768 }
769 }
770 }
771 Object[] answer = { value };
772 return answer;
773 }
774 catch ( InvocationTargetException e ) {
775 logInfo( e );
776 throw new IllegalArgumentException( e.getMessage() );
777 }
778 catch ( InstantiationException e ) {
779 logInfo( e );
780 throw new IllegalArgumentException( e.getMessage() );
781 }
782 }
783
784 /**
785 * Converts the given value to the given type. First, reflection is
786 * is used to find a public constructor declared by the given class
787 * that takes one argument, which must be the precise type of the
788 * given value. If such a constructor is found, a new object is
789 * created by passing the given value to that constructor, and the
790 * newly constructed object is returned.<P>
791 *
792 * If no such constructor exists, and the given type is a primitive
793 * type, then the given value is converted to a string using its
794 * {@link Object#toString() toString()} method, and that string is
795 * parsed into the correct primitive type using, for instance,
796 * {@link Integer#valueOf(String)} to convert the string into an
797 * <code>int</code>.<P>
798 *
799 * If no special constructor exists and the given type is not a
800 * primitive type, this method returns the original value.
801 *
802 * @param newType the type to convert the value to
803 * @param value the value to convert
804 * @return the converted value
805 * @throws NumberFormatException if newType is a primitive type, and
806 * the string representation of the given value cannot be converted
807 * to that type
808 * @throws InstantiationException if the constructor found with
809 * reflection raises it
810 * @throws InvocationTargetException if the constructor found with
811 * reflection raises it
812 * @throws IllegalAccessException never
813 * @throws IllegalArgumentException never
814 */
815 protected Object convertType( Class newType, Object value )
816 throws InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
817
818 // try call constructor
819 Class[] types = { value.getClass() };
820 try {
821 Constructor constructor = newType.getConstructor( types );
822 Object[] arguments = { value };
823 return constructor.newInstance( arguments );
824 }
825 catch ( NoSuchMethodException e ) {
826 // try using the transformers
827 Transformer transformer = getTypeTransformer( newType );
828 if ( transformer != null ) {
829 return transformer.transform( value );
830 }
831 return value;
832 }
833 }
834
835 /**
836 * Returns a transformer for the given primitive type.
837 *
838 * @param aType the primitive type whose transformer to return
839 * @return a transformer that will convert strings into that type,
840 * or null if the given type is not a primitive type
841 */
842 protected Transformer getTypeTransformer( Class aType ) {
843 return (Transformer) typeTransformers.get( aType );
844 }
845
846 /**
847 * Logs the given exception to <code>System.out</code>. Used to display
848 * warnings while accessing/mutating the bean.
849 *
850 * @param ex the exception to log
851 */
852 protected void logInfo(Exception ex) {
853 // Deliberately do not use LOG4J or Commons Logging to avoid dependencies
854 System.out.println( "INFO: Exception: " + ex );
855 }
856
857 /**
858 * Logs the given exception to <code>System.err</code>. Used to display
859 * errors while accessing/mutating the bean.
860 *
861 * @param ex the exception to log
862 */
863 protected void logWarn(Exception ex) {
864 // Deliberately do not use LOG4J or Commons Logging to avoid dependencies
865 System.out.println( "WARN: Exception: " + ex );
866 ex.printStackTrace();
867 }
868 }