1 package org.apache.commons.betwixt.digester;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 import java.beans.IntrospectionException;
20 import java.beans.Introspector;
21 import java.beans.PropertyDescriptor;
22 import java.lang.reflect.Method;
23 import java.util.Collection;
24 import java.util.Date;
25 import java.util.Enumeration;
26 import java.util.HashMap;
27 import java.util.Iterator;
28 import java.util.Map;
29
30 import org.apache.commons.betwixt.AttributeDescriptor;
31 import org.apache.commons.betwixt.ElementDescriptor;
32 import org.apache.commons.betwixt.NodeDescriptor;
33 import org.apache.commons.betwixt.XMLIntrospector;
34 import org.apache.commons.betwixt.expression.IteratorExpression;
35 import org.apache.commons.betwixt.expression.MapEntryAdder;
36 import org.apache.commons.betwixt.expression.MethodExpression;
37 import org.apache.commons.betwixt.expression.MethodUpdater;
38 import org.apache.commons.betwixt.strategy.PluralStemmer;
39 import org.apache.commons.logging.Log;
40 import org.apache.commons.logging.LogFactory;
41
42 /***
43 * <p><code>XMLIntrospectorHelper</code> a helper class for
44 * common code shared between the digestor and introspector.</p>
45 *
46 * TODO this class will be deprecated soon
47 * need to move the isLoop and isPrimitiveType but probably need to
48 * think about whether they need replacing with something different.
49 * @author <a href="mailto:jstrachan@apache.org">James Strachan</a>
50 * @author <a href="mailto:martin@mvdb.net">Martin van den Bemt</a>
51 */
52 public class XMLIntrospectorHelper {
53
54 /*** Log used for logging (Doh!) */
55 protected static Log log = LogFactory.getLog( XMLIntrospectorHelper.class );
56
57 /*** Base constructor */
58 public XMLIntrospectorHelper() {
59 }
60
61 /***
62 * <p>Gets the current logging implementation.</p>
63 *
64 * @return current log
65 */
66 public static Log getLog() {
67 return log;
68 }
69
70 /***
71 * <p>Sets the current logging implementation.</p>
72 *
73 * @param aLog use this <code>Log</code>
74 */
75 public static void setLog(Log aLog) {
76 log = aLog;
77 }
78
79
80
81 /***
82 * Process a property.
83 * Go through and work out whether it's a loop property, a primitive or a standard.
84 * The class property is ignored.
85 *
86 * @param propertyDescriptor create a <code>NodeDescriptor</code> for this property
87 * @param useAttributesForPrimitives write primitives as attributes (rather than elements)
88 * @param introspector use this <code>XMLIntrospector</code>
89 * @return a correctly configured <code>NodeDescriptor</code> for the property
90 * @throws IntrospectionException when bean introspection fails
91 * @deprecated 0.5 this method has been replaced by {@link XMLIntrospector#createDescriptor}
92 */
93 public static NodeDescriptor createDescriptor(
94 PropertyDescriptor propertyDescriptor,
95 boolean useAttributesForPrimitives,
96 XMLIntrospector introspector
97 ) throws IntrospectionException {
98 String name = propertyDescriptor.getName();
99 Class type = propertyDescriptor.getPropertyType();
100
101 if (log.isTraceEnabled()) {
102 log.trace("Creating descriptor for property: name="
103 + name + " type=" + type);
104 }
105
106 NodeDescriptor nodeDescriptor = null;
107 Method readMethod = propertyDescriptor.getReadMethod();
108 Method writeMethod = propertyDescriptor.getWriteMethod();
109
110 if ( readMethod == null ) {
111 if (log.isTraceEnabled()) {
112 log.trace( "No read method for property: name="
113 + name + " type=" + type);
114 }
115 return null;
116 }
117
118 if ( log.isTraceEnabled() ) {
119 log.trace( "Read method=" + readMethod.getName() );
120 }
121
122
123
124
125 if ( Class.class.equals( type ) && "class".equals( name ) ) {
126 log.trace( "Ignoring class property" );
127 return null;
128 }
129 if ( isPrimitiveType( type ) ) {
130 if (log.isTraceEnabled()) {
131 log.trace( "Primitive type: " + name);
132 }
133 if ( useAttributesForPrimitives ) {
134 if (log.isTraceEnabled()) {
135 log.trace( "Adding property as attribute: " + name );
136 }
137 nodeDescriptor = new AttributeDescriptor();
138 } else {
139 if (log.isTraceEnabled()) {
140 log.trace( "Adding property as element: " + name );
141 }
142 nodeDescriptor = new ElementDescriptor(true);
143 }
144 nodeDescriptor.setTextExpression( new MethodExpression( readMethod ) );
145
146 if ( writeMethod != null ) {
147 nodeDescriptor.setUpdater( new MethodUpdater( writeMethod ) );
148 }
149 } else if ( isLoopType( type ) ) {
150 if (log.isTraceEnabled()) {
151 log.trace("Loop type: " + name);
152 log.trace("Wrap in collections? " + introspector.isWrapCollectionsInElement());
153 }
154 ElementDescriptor loopDescriptor = new ElementDescriptor();
155 loopDescriptor.setContextExpression(
156 new IteratorExpression( new MethodExpression( readMethod ) )
157 );
158 loopDescriptor.setWrapCollectionsInElement(
159 introspector.isWrapCollectionsInElement());
160
161
162 if ( Map.class.isAssignableFrom( type ) ) {
163 loopDescriptor.setQualifiedName( "entry" );
164
165 loopDescriptor.addElementDescriptor( new ElementDescriptor( "key" ) );
166 loopDescriptor.addElementDescriptor( new ElementDescriptor( "value" ) );
167 }
168
169 ElementDescriptor elementDescriptor = new ElementDescriptor();
170 elementDescriptor.setWrapCollectionsInElement(
171 introspector.isWrapCollectionsInElement());
172 elementDescriptor.setElementDescriptors( new ElementDescriptor[] { loopDescriptor } );
173
174 nodeDescriptor = elementDescriptor;
175 } else {
176 if (log.isTraceEnabled()) {
177 log.trace( "Standard property: " + name);
178 }
179 ElementDescriptor elementDescriptor = new ElementDescriptor();
180 elementDescriptor.setContextExpression( new MethodExpression( readMethod ) );
181 if ( writeMethod != null ) {
182 elementDescriptor.setUpdater( new MethodUpdater( writeMethod ) );
183 }
184
185 nodeDescriptor = elementDescriptor;
186 }
187
188 if (nodeDescriptor instanceof AttributeDescriptor) {
189
190 nodeDescriptor.setLocalName(
191 introspector.getAttributeNameMapper().mapTypeToElementName( name ) );
192 } else {
193 nodeDescriptor.setLocalName(
194 introspector.getElementNameMapper().mapTypeToElementName( name ) );
195 }
196
197 nodeDescriptor.setPropertyName( propertyDescriptor.getName() );
198 nodeDescriptor.setPropertyType( type );
199
200
201
202
203
204 if (log.isTraceEnabled()) {
205 log.trace("Created descriptor:");
206 log.trace(nodeDescriptor);
207 }
208 return nodeDescriptor;
209 }
210
211 /***
212 * Configure an <code>ElementDescriptor</code> from a <code>PropertyDescriptor</code>.
213 * This uses default element updater (the write method of the property).
214 *
215 * @param elementDescriptor configure this <code>ElementDescriptor</code>
216 * @param propertyDescriptor configure from this <code>PropertyDescriptor</code>
217 * @deprecated 0.6 unused
218 */
219 public static void configureProperty(
220 ElementDescriptor elementDescriptor,
221 PropertyDescriptor propertyDescriptor ) {
222
223 configureProperty( elementDescriptor, propertyDescriptor, null, null);
224 }
225
226 /***
227 * Configure an <code>ElementDescriptor</code> from a <code>PropertyDescriptor</code>.
228 * A custom update method may be set.
229 *
230 * @param elementDescriptor configure this <code>ElementDescriptor</code>
231 * @param propertyDescriptor configure from this <code>PropertyDescriptor</code>
232 * @param updateMethodName the name of the custom updater method to user.
233 * If null, then then
234 * @param beanClass the <code>Class</code> from which the update method should be found.
235 * This may be null only when <code>updateMethodName</code> is also null.
236 * @since 0.5
237 * @deprecated 0.6 moved into ElementRule
238 */
239 public static void configureProperty(
240 ElementDescriptor elementDescriptor,
241 PropertyDescriptor propertyDescriptor,
242 String updateMethodName,
243 Class beanClass ) {
244
245 Class type = propertyDescriptor.getPropertyType();
246 Method readMethod = propertyDescriptor.getReadMethod();
247 Method writeMethod = propertyDescriptor.getWriteMethod();
248
249 elementDescriptor.setLocalName( propertyDescriptor.getName() );
250 elementDescriptor.setPropertyType( type );
251
252
253
254
255
256 if ( readMethod == null ) {
257 log.trace( "No read method" );
258 return;
259 }
260
261 if ( log.isTraceEnabled() ) {
262 log.trace( "Read method=" + readMethod.getName() );
263 }
264
265
266
267
268 if ( Class.class.equals( type ) && "class".equals( propertyDescriptor.getName() ) ) {
269 log.trace( "Ignoring class property" );
270 return;
271 }
272 if ( isPrimitiveType( type ) ) {
273 elementDescriptor.setTextExpression( new MethodExpression( readMethod ) );
274
275 } else if ( isLoopType( type ) ) {
276 log.trace("Loop type ??");
277
278
279
280 elementDescriptor.setContextExpression(
281 new IteratorExpression( new MethodExpression( readMethod ) )
282 );
283
284 writeMethod = null;
285 } else {
286 log.trace( "Standard property" );
287 elementDescriptor.setContextExpression( new MethodExpression( readMethod ) );
288 }
289
290
291 if (updateMethodName == null) {
292
293 if ( writeMethod != null ) {
294 elementDescriptor.setUpdater( new MethodUpdater( writeMethod ) );
295 }
296
297 } else {
298
299 if ( log.isTraceEnabled() ) {
300 log.trace( "Finding custom method: " );
301 log.trace( " on:" + beanClass );
302 log.trace( " name:" + updateMethodName );
303 }
304
305 Method updateMethod = null;
306 Method[] methods = beanClass.getMethods();
307 for ( int i = 0, size = methods.length; i < size; i++ ) {
308 Method method = methods[i];
309 if ( updateMethodName.equals( method.getName() ) ) {
310
311
312 if (methods[i].getParameterTypes().length == 1) {
313
314 updateMethod = methods[i];
315 if ( log.isTraceEnabled() ) {
316 log.trace("Matched method:" + updateMethod);
317 }
318
319 break;
320 }
321 }
322 }
323
324 if (updateMethod == null) {
325 if ( log.isInfoEnabled() ) {
326
327 log.info("No method with name '" + updateMethodName + "' found for update");
328 }
329 } else {
330
331 elementDescriptor.setUpdater( new MethodUpdater( updateMethod ) );
332 elementDescriptor.setSingularPropertyType( updateMethod.getParameterTypes()[0] );
333 if ( log.isTraceEnabled() ) {
334 log.trace( "Set custom updater on " + elementDescriptor);
335 }
336 }
337 }
338 }
339
340 /***
341 * Configure an <code>AttributeDescriptor</code> from a <code>PropertyDescriptor</code>
342 *
343 * @param attributeDescriptor configure this <code>AttributeDescriptor</code>
344 * @param propertyDescriptor configure from this <code>PropertyDescriptor</code>
345 * @deprecated 0.6 moved into AttributeRule
346 */
347 public static void configureProperty(
348 AttributeDescriptor attributeDescriptor,
349 PropertyDescriptor propertyDescriptor ) {
350 Class type = propertyDescriptor.getPropertyType();
351 Method readMethod = propertyDescriptor.getReadMethod();
352 Method writeMethod = propertyDescriptor.getWriteMethod();
353
354 if ( readMethod == null ) {
355 log.trace( "No read method" );
356 return;
357 }
358
359 if ( log.isTraceEnabled() ) {
360 log.trace( "Read method=" + readMethod );
361 }
362
363
364
365
366 if ( Class.class.equals( type ) && "class".equals( propertyDescriptor.getName() ) ) {
367 log.trace( "Ignoring class property" );
368 return;
369 }
370 if ( isLoopType( type ) ) {
371 log.warn( "Using loop type for an attribute. Type = "
372 + type.getName() + " attribute: " + attributeDescriptor.getQualifiedName() );
373 }
374
375 log.trace( "Standard property" );
376 attributeDescriptor.setTextExpression( new MethodExpression( readMethod ) );
377
378 if ( writeMethod != null ) {
379 attributeDescriptor.setUpdater( new MethodUpdater( writeMethod ) );
380 }
381
382 attributeDescriptor.setLocalName( propertyDescriptor.getName() );
383 attributeDescriptor.setPropertyType( type );
384
385
386
387
388 }
389
390
391 /***
392 * Add any addPropety(PropertyType) methods as Updaters
393 * which are often used for 1-N relationships in beans.
394 * <br>
395 * The tricky part here is finding which ElementDescriptor corresponds
396 * to the method. e.g. a property 'items' might have an Element descriptor
397 * which the method addItem() should match to.
398 * <br>
399 * So the algorithm we'll use
400 * by default is to take the decapitalized name of the property being added
401 * and find the first ElementDescriptor that matches the property starting with
402 * the string. This should work for most use cases.
403 * e.g. addChild() would match the children property.
404 *
405 * @param introspector use this <code>XMLIntrospector</code> for introspection
406 * @param rootDescriptor add defaults to this descriptor
407 * @param beanClass the <code>Class</code> to which descriptor corresponds
408 * @deprecated 0.6 use the method in XMLIntrospector instead
409 */
410 public static void defaultAddMethods(
411 XMLIntrospector introspector,
412 ElementDescriptor rootDescriptor,
413 Class beanClass ) {
414
415
416 if ( beanClass != null ) {
417 Method[] methods = beanClass.getMethods();
418 for ( int i = 0, size = methods.length; i < size; i++ ) {
419 Method method = methods[i];
420 String name = method.getName();
421 if ( name.startsWith( "add" ) ) {
422
423
424 Class[] types = method.getParameterTypes();
425 if ( types != null) {
426 if ( log.isTraceEnabled() ) {
427 log.trace("Searching for match for " + method);
428 }
429
430 if ( ( types.length == 1 ) || types.length == 2 ) {
431 String propertyName = Introspector.decapitalize( name.substring(3) );
432 if (propertyName.length() == 0)
433 continue;
434 if ( log.isTraceEnabled() ) {
435 log.trace( name + "->" + propertyName );
436 }
437
438
439
440
441
442 ElementDescriptor descriptor =
443 findGetCollectionDescriptor(
444 introspector,
445 rootDescriptor,
446 propertyName );
447
448 if ( log.isDebugEnabled() ) {
449 log.debug( "!! " + propertyName + " -> " + descriptor );
450 log.debug( "!! " + name + " -> "
451 + (descriptor!=null?descriptor.getPropertyName():"") );
452 }
453 if ( descriptor != null ) {
454 boolean isMapDescriptor
455 = Map.class.isAssignableFrom( descriptor.getPropertyType() );
456 if ( !isMapDescriptor && types.length == 1 ) {
457
458 log.trace("Matching collection or iteration");
459
460 descriptor.setUpdater( new MethodUpdater( method ) );
461 descriptor.setSingularPropertyType( types[0] );
462
463 if ( log.isDebugEnabled() ) {
464 log.debug( "!! " + method);
465 log.debug( "!! " + types[0]);
466 }
467
468
469 ElementDescriptor[] children
470 = descriptor.getElementDescriptors();
471 if ( children != null && children.length > 0 ) {
472 ElementDescriptor child = children[0];
473 String localName = child.getLocalName();
474 if ( localName == null || localName.length() == 0 ) {
475 child.setLocalName(
476 introspector.getElementNameMapper()
477 .mapTypeToElementName( propertyName ) );
478 }
479 }
480
481 } else if ( isMapDescriptor && types.length == 2 ) {
482
483 log.trace("Matching map");
484 ElementDescriptor[] children
485 = descriptor.getElementDescriptors();
486
487 if ( children.length == 0 ) {
488
489 log.info(
490 "'entry' descriptor is missing for map. "
491 + "Updaters cannot be set");
492
493 } else {
494
495
496 ElementDescriptor[] grandchildren
497 = children[0].getElementDescriptors();
498 MapEntryAdder adder = new MapEntryAdder(method);
499 for (
500 int n=0,
501 noOfGrandChildren = grandchildren.length;
502 n < noOfGrandChildren;
503 n++ ) {
504 if ( "key".equals(
505 grandchildren[n].getLocalName() ) ) {
506
507 grandchildren[n].setUpdater(
508 adder.getKeyUpdater() );
509 grandchildren[n].setSingularPropertyType(
510 types[0] );
511 if ( log.isTraceEnabled() ) {
512 log.trace(
513 "Key descriptor: " + grandchildren[n]);
514 }
515
516 } else if (
517 "value".equals(
518 grandchildren[n].getLocalName() ) ) {
519
520 grandchildren[n].setUpdater(
521 adder.getValueUpdater() );
522 grandchildren[n].setSingularPropertyType(
523 types[1] );
524 if ( log.isTraceEnabled() ) {
525 log.trace(
526 "Value descriptor: " + grandchildren[n]);
527 }
528 }
529 }
530 }
531 }
532 } else {
533 if ( log.isDebugEnabled() ) {
534 log.debug(
535 "Could not find an ElementDescriptor with property name: "
536 + propertyName + " to attach the add method: " + method
537 );
538 }
539 }
540 }
541 }
542 }
543 }
544 }
545 }
546
547 /***
548 * Is this a loop type class?
549 *
550 * @param type is this <code>Class</code> a loop type?
551 * @return true if the type is a loop type, or if type is null
552 */
553 public static boolean isLoopType(Class type) {
554
555 if (type == null) {
556 log.trace("isLoopType: type is null");
557 return false;
558 }
559 return type.isArray()
560 || Map.class.isAssignableFrom( type )
561 || Collection.class.isAssignableFrom( type )
562 || Enumeration.class.isAssignableFrom( type )
563 || Iterator.class.isAssignableFrom( type );
564 }
565
566
567 /***
568 * Is this a primitive type?
569 *
570 * TODO: this method will probably be removed when primitive types
571 * are subsumed into the simple type concept.
572 * This needs moving into XMLIntrospector so that the list of simple
573 * type can be varied.
574 * @param type is this <code>Class<code> a primitive type?
575 * @return true for primitive types
576 * @deprecated 0.6 replaced by {@link org.apache.commons.betwixt.strategy.TypeBindingStrategy}
577 */
578 public static boolean isPrimitiveType(Class type) {
579 if ( type == null ) {
580 return false;
581
582 } else if ( type.isPrimitive() ) {
583 return true;
584
585 } else if ( type.equals( Object.class ) ) {
586 return false;
587 }
588 return type.getName().startsWith( "java.lang." )
589 || Number.class.isAssignableFrom( type )
590 || String.class.isAssignableFrom( type )
591 || Date.class.isAssignableFrom( type )
592 || java.sql.Date.class.isAssignableFrom( type )
593 || java.sql.Time.class.isAssignableFrom( type )
594 || java.sql.Timestamp.class.isAssignableFrom( type )
595 || java.math.BigDecimal.class.isAssignableFrom( type )
596 || java.math.BigInteger.class.isAssignableFrom( type );
597 }
598
599
600
601
602 /***
603 * Attempts to find the element descriptor for the getter property that
604 * typically matches a collection or array. The property name is used
605 * to match. e.g. if an addChild() method is detected the
606 * descriptor for the 'children' getter property should be returned.
607 *
608 * @param introspector use this <code>XMLIntrospector</code>
609 * @param rootDescriptor the <code>ElementDescriptor</code> whose child element will be
610 * searched for a match
611 * @param propertyName the name of the 'adder' method to match
612 * @return <code>ElementDescriptor</code> for the matching getter
613 * @deprecated 0.6 moved into XMLIntrospector
614 */
615 protected static ElementDescriptor findGetCollectionDescriptor(
616 XMLIntrospector introspector,
617 ElementDescriptor rootDescriptor,
618 String propertyName ) {
619
620 Map map = new HashMap();
621
622 if ( log.isTraceEnabled() ) {
623 log.trace( "findPluralDescriptor( " + propertyName
624 + " ):root property name=" + rootDescriptor.getPropertyName() );
625 }
626
627 if (rootDescriptor.getPropertyName() != null) {
628 map.put(propertyName, rootDescriptor);
629 }
630 makeElementDescriptorMap( rootDescriptor, map );
631
632 PluralStemmer stemmer = introspector.getPluralStemmer();
633 ElementDescriptor elementDescriptor = stemmer.findPluralDescriptor( propertyName, map );
634
635 if ( log.isTraceEnabled() ) {
636 log.trace(
637 "findPluralDescriptor( " + propertyName
638 + " ):ElementDescriptor=" + elementDescriptor );
639 }
640
641 return elementDescriptor;
642 }
643
644 /***
645 * Creates a map where the keys are the property names and the values are the ElementDescriptors
646 *
647 * @param rootDescriptor the values of the maps are the children of this
648 * <code>ElementDescriptor</code> index by their property names
649 * @param map the map to which the elements will be added
650 * @deprecated 0.6 moved into XMLIntrospector
651 */
652 protected static void makeElementDescriptorMap( ElementDescriptor rootDescriptor, Map map ) {
653 ElementDescriptor[] children = rootDescriptor.getElementDescriptors();
654 if ( children != null ) {
655 for ( int i = 0, size = children.length; i < size; i++ ) {
656 ElementDescriptor child = children[i];
657 String propertyName = child.getPropertyName();
658 if ( propertyName != null ) {
659 map.put( propertyName, child );
660 }
661 makeElementDescriptorMap( child, map );
662 }
663 }
664 }
665
666 /***
667 * Traverse the tree of element descriptors and find the oldValue and swap it with the newValue.
668 * This would be much easier to do if ElementDescriptor supported a parent relationship.
669 *
670 * @param rootDescriptor traverse child graph for this <code>ElementDescriptor</code>
671 * @param oldValue replace this <code>ElementDescriptor</code>
672 * @param newValue replace with this <code>ElementDescriptor</code>
673 * @deprecated 0.6 now unused
674 */
675 protected static void swapDescriptor(
676 ElementDescriptor rootDescriptor,
677 ElementDescriptor oldValue,
678 ElementDescriptor newValue ) {
679 ElementDescriptor[] children = rootDescriptor.getElementDescriptors();
680 if ( children != null ) {
681 for ( int i = 0, size = children.length; i < size; i++ ) {
682 ElementDescriptor child = children[i];
683 if ( child == oldValue ) {
684 children[i] = newValue;
685 break;
686 }
687 swapDescriptor( child, oldValue, newValue );
688 }
689 }
690 }
691 }