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