1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 package org.apache.commons.betwixt.io;
17
18 import java.util.HashMap;
19 import java.util.List;
20 import java.util.Map;
21
22 import org.apache.commons.betwixt.AttributeDescriptor;
23 import org.apache.commons.betwixt.ElementDescriptor;
24 import org.apache.commons.betwixt.XMLBeanInfo;
25 import org.apache.commons.betwixt.XMLIntrospector;
26 import org.apache.commons.betwixt.digester.XMLIntrospectorHelper;
27 import org.apache.commons.betwixt.expression.Context;
28 import org.apache.commons.betwixt.expression.MethodUpdater;
29 import org.apache.commons.betwixt.expression.Updater;
30 import org.apache.commons.digester.Rule;
31 import org.apache.commons.digester.Rules;
32 import org.apache.commons.logging.Log;
33 import org.apache.commons.logging.LogFactory;
34 import org.xml.sax.Attributes;
35
36 /*** <p><code>BeanCreateRule</code> is a Digester Rule for creating beans
37 * from the betwixt XML metadata.</p>
38 *
39 * @author <a href="mailto:jstrachan@apache.org">James Strachan</a>
40 * @author <a href="mailto:martin@mvdb.net">Martin van den Bemt</a>
41 * @deprecated 0.5 this Rule does not allowed good integration with other Rules -
42 * use {@link BeanRuleSet} instead.
43 */
44 public class BeanCreateRule extends Rule {
45
46 /*** Logger */
47 private static Log log = LogFactory.getLog( BeanCreateRule.class );
48
49 /***
50 * Set log to be used by <code>BeanCreateRule</code> instances
51 * @param aLog the <code>Log</code> implementation for this class to log to
52 */
53 public static void setLog(Log aLog) {
54 log = aLog;
55 }
56
57 /*** The descriptor of this element */
58 private ElementDescriptor descriptor;
59 /*** The Context used when evaluating Updaters */
60 private Context context;
61 /*** Have we added our child rules to the digester? */
62 private boolean addedChildren;
63 /*** In this begin-end loop did we actually create a new bean */
64 private boolean createdBean;
65 /*** The type of the bean to create */
66 private Class beanClass;
67 /*** The prefix added to digester rules */
68 private String pathPrefix;
69 /*** Use id's to match beans? */
70 private boolean matchIDs = true;
71 /*** allows an attribute to be specified to overload the types of beans used */
72 private String classNameAttribute = "className";
73
74 /***
75 * Convenience constructor which uses <code>ID's</code> for matching.
76 *
77 * @param descriptor the <code>ElementDescriptor</code> describing the element mapped
78 * @param beanClass the <code>Class</code> to be created
79 * @param pathPrefix the digester style path
80 */
81 public BeanCreateRule(
82 ElementDescriptor descriptor,
83 Class beanClass,
84 String pathPrefix ) {
85 this( descriptor, beanClass, pathPrefix, true );
86 }
87
88 /***
89 * Constructor taking a class.
90 *
91 * @param descriptor the <code>ElementDescriptor</code> describing the element mapped
92 * @param beanClass the <code>Class</code> to be created
93 * @param pathPrefix the digester style path
94 * @param matchIDs should <code>ID</code>/<code>IDREF</code>'s be used for matching
95 */
96 public BeanCreateRule(
97 ElementDescriptor descriptor,
98 Class beanClass,
99 String pathPrefix,
100 boolean matchIDs ) {
101 this(
102 descriptor,
103 beanClass,
104 new Context(),
105 pathPrefix,
106 matchIDs);
107 }
108
109 /***
110 * Convenience constructor which uses <code>ID's</code> for matching.
111 *
112 * @param descriptor the <code>ElementDescriptor</code> describing the element mapped
113 * @param beanClass the <code>Class</code> to be created
114 */
115 public BeanCreateRule( ElementDescriptor descriptor, Class beanClass ) {
116 this( descriptor, beanClass, true );
117 }
118
119 /***
120 * Constructor uses standard qualified name.
121 *
122 * @param descriptor the <code>ElementDescriptor</code> describing the element mapped
123 * @param beanClass the <code>Class</code> to be created
124 * @param matchIDs should <code>ID</code>/<code>IDREF</code>'s be used for matching
125 */
126 public BeanCreateRule( ElementDescriptor descriptor, Class beanClass, boolean matchIDs ) {
127 this( descriptor, beanClass, descriptor.getQualifiedName() + "/" , matchIDs );
128 }
129
130 /***
131 * Convenience constructor which uses <code>ID's</code> for match.
132 *
133 * @param descriptor the <code>ElementDescriptor</code> describing the element mapped
134 * @param context the <code>Context</code> to be used to evaluate expressions
135 * @param pathPrefix the digester path prefix
136 */
137 public BeanCreateRule(
138 ElementDescriptor descriptor,
139 Context context,
140 String pathPrefix ) {
141 this( descriptor, context, pathPrefix, true );
142 }
143
144 /***
145 * Constructor taking a context.
146 *
147 * @param descriptor the <code>ElementDescriptor</code> describing the element mapped
148 * @param context the <code>Context</code> to be used to evaluate expressions
149 * @param pathPrefix the digester path prefix
150 * @param matchIDs should <code>ID</code>/<code>IDREF</code>'s be used for matching
151 */
152 public BeanCreateRule(
153 ElementDescriptor descriptor,
154 Context context,
155 String pathPrefix,
156 boolean matchIDs ) {
157 this(
158 descriptor,
159 descriptor.getSingularPropertyType(),
160 context,
161 pathPrefix,
162 matchIDs );
163 }
164
165 /***
166 * Base constructor (used by other constructors).
167 *
168 * @param descriptor the <code>ElementDescriptor</code> describing the element mapped
169 * @param beanClass the <code>Class</code> of the bean to be created
170 * @param context the <code>Context</code> to be used to evaluate expressions
171 * @param pathPrefix the digester path prefix
172 * @param matchIDs should <code>ID</code>/<code>IDREF</code>'s be used for matching
173 */
174 private BeanCreateRule(
175 ElementDescriptor descriptor,
176 Class beanClass,
177 Context context,
178 String pathPrefix,
179 boolean matchIDs ) {
180 this.descriptor = descriptor;
181 this.context = context;
182 this.beanClass = beanClass;
183 this.pathPrefix = pathPrefix;
184 this.matchIDs = matchIDs;
185 if (log.isTraceEnabled()) {
186 log.trace("Created bean create rule");
187 log.trace("Descriptor=" + descriptor);
188 log.trace("Class=" + beanClass);
189 log.trace("Path prefix=" + pathPrefix);
190 }
191 }
192
193
194
195
196
197
198 /***
199 * Process the beginning of this element.
200 *
201 * @param attributes The attribute list of this element
202 */
203 public void begin(Attributes attributes) {
204 log.debug( "Called with descriptor: " + descriptor
205 + " propertyType: " + descriptor.getPropertyType() );
206
207 if (log.isTraceEnabled()) {
208 int attributesLength = attributes.getLength();
209 if (attributesLength > 0) {
210 log.trace("Attributes:");
211 }
212 for (int i=0, size=attributesLength; i<size; i++) {
213 log.trace("Local:" + attributes.getLocalName(i));
214 log.trace("URI:" + attributes.getURI(i));
215 log.trace("QName:" + attributes.getQName(i));
216 }
217 }
218
219
220
221
222
223
224 createdBean = false;
225
226 Object instance = null;
227 if ( beanClass != null ) {
228 instance = createBean(attributes);
229 if ( instance != null ) {
230 createdBean = true;
231
232 context.setBean( instance );
233 digester.push(instance);
234
235
236
237
238
239 ElementDescriptor typeDescriptor = getElementDescriptor( descriptor );
240
241
242
243 AttributeDescriptor[] attributeDescriptors
244 = typeDescriptor.getAttributeDescriptors();
245 if ( attributeDescriptors != null ) {
246 for ( int i = 0, size = attributeDescriptors.length; i < size; i++ ) {
247 AttributeDescriptor attributeDescriptor = attributeDescriptors[i];
248
249
250
251
252
253 String value = attributes.getValue(
254 attributeDescriptor.getURI(),
255 attributeDescriptor.getLocalName()
256 );
257
258 if (value == null) {
259 value = attributes.getValue(attributeDescriptor.getQualifiedName());
260 }
261
262 if (log.isTraceEnabled()) {
263 log.trace("Attr URL:" + attributeDescriptor.getURI());
264 log.trace("Attr LocalName:" + attributeDescriptor.getLocalName() );
265 log.trace(value);
266 }
267
268 Updater updater = attributeDescriptor.getUpdater();
269 log.trace(updater);
270 if ( updater != null && value != null ) {
271 updater.update( context, value );
272 }
273 }
274 }
275
276 addChildRules();
277
278
279 if ( matchIDs ) {
280
281
282
283 String id = attributes.getValue( "id" );
284 if ( id != null ) {
285 getBeansById().put( id, instance );
286 }
287 }
288 }
289 }
290 }
291
292 /***
293 * Process the end of this element.
294 */
295 public void end() {
296 if ( createdBean ) {
297
298
299 Updater updater = descriptor.getUpdater();
300 Object instance = context.getBean();
301
302 Object top = digester.pop();
303 if (digester.getCount() == 0) {
304 context.setBean(null);
305 }else{
306 context.setBean( digester.peek() );
307 }
308
309 if ( updater != null ) {
310 if ( log.isDebugEnabled() ) {
311 log.debug( "Calling updater for: " + descriptor + " with: "
312 + instance + " on bean: " + context.getBean() );
313 }
314 updater.update( context, instance );
315 } else {
316 if ( log.isDebugEnabled() ) {
317 log.debug( "No updater for: " + descriptor + " with: "
318 + instance + " on bean: " + context.getBean() );
319 }
320 }
321 }
322 }
323
324 /***
325 * Tidy up.
326 */
327 public void finish() {}
328
329
330
331
332
333
334 /***
335 * The name of the attribute which can be specified in the XML to override the
336 * type of a bean used at a certain point in the schema.
337 *
338 * <p>The default value is 'className'.</p>
339 *
340 * @return The name of the attribute used to overload the class name of a bean
341 */
342 public String getClassNameAttribute() {
343 return classNameAttribute;
344 }
345
346 /***
347 * Sets the name of the attribute which can be specified in
348 * the XML to override the type of a bean used at a certain
349 * point in the schema.
350 *
351 * <p>The default value is 'className'.</p>
352 *
353 * @param classNameAttribute The name of the attribute used to overload the class name of a bean
354 */
355 public void setClassNameAttribute(String classNameAttribute) {
356 this.classNameAttribute = classNameAttribute;
357 }
358
359
360
361
362 /***
363 * Factory method to create new bean instances
364 *
365 * @param attributes the <code>Attributes</code> used to match <code>ID/IDREF</code>
366 * @return the created bean
367 */
368 protected Object createBean(Attributes attributes) {
369
370
371
372
373
374
375 if ( matchIDs ) {
376 String idref = attributes.getValue( "idref" );
377 if ( idref != null ) {
378
379
380
381 log.trace( "Found IDREF" );
382 Object bean = getBeansById().get( idref );
383 if ( bean != null ) {
384 if (log.isTraceEnabled()) {
385 log.trace( "Matched bean " + bean );
386 }
387 return bean;
388 }
389 log.trace( "No match found" );
390 }
391 }
392
393 Class theClass = beanClass;
394 try {
395
396 String className = attributes.getValue(classNameAttribute);
397 if (className != null) {
398
399 theClass = getDigester().getClassLoader().loadClass(className);
400 }
401 if (log.isTraceEnabled()) {
402 log.trace( "Creating instance of " + theClass );
403 }
404 return theClass.newInstance();
405
406 } catch (Exception e) {
407 log.warn( "Could not create instance of type: " + theClass.getName() );
408 return null;
409 }
410 }
411
412 /*** Adds the rules to the digester for all child elements */
413 protected void addChildRules() {
414 if ( ! addedChildren ) {
415 addedChildren = true;
416
417 addChildRules( pathPrefix, descriptor );
418 }
419 }
420
421 /***
422 * Add child rules for given descriptor at given prefix
423 *
424 * @param prefix add child rules at this (digester) path prefix
425 * @param currentDescriptor add child rules for this descriptor
426 */
427 protected void addChildRules(String prefix, ElementDescriptor currentDescriptor ) {
428
429 if (log.isTraceEnabled()) {
430 log.trace("Adding child rules for " + currentDescriptor + "@" + prefix);
431 }
432
433
434
435
436 ElementDescriptor typeDescriptor = getElementDescriptor( currentDescriptor );
437
438
439
440 ElementDescriptor[] childDescriptors = typeDescriptor.getElementDescriptors();
441 if ( childDescriptors != null ) {
442 for ( int i = 0, size = childDescriptors.length; i < size; i++ ) {
443 final ElementDescriptor childDescriptor = childDescriptors[i];
444 if (log.isTraceEnabled()) {
445 log.trace("Processing child " + childDescriptor);
446 }
447
448 String qualifiedName = childDescriptor.getQualifiedName();
449 if ( qualifiedName == null ) {
450 log.trace( "Ignoring" );
451 continue;
452 }
453 String path = prefix + qualifiedName;
454
455
456
457 if ( qualifiedName.equals( currentDescriptor.getQualifiedName() )
458 && currentDescriptor.getPropertyName() != null ) {
459 log.trace("Creating generic rule for recursive elements");
460 int index = -1;
461 if (childDescriptor.isWrapCollectionsInElement()) {
462 index = prefix.indexOf(qualifiedName);
463 if (index == -1) {
464
465 log.debug( "Oops - this shouldn't happen" );
466 continue;
467 }
468 int removeSlash = prefix.endsWith("/")?1:0;
469 path = "*/" + prefix.substring(index, prefix.length()-removeSlash);
470 }else{
471
472 ElementDescriptor[] desc = currentDescriptor.getElementDescriptors();
473 if (desc.length == 1) {
474 path = "*/"+desc[0].getQualifiedName();
475 }
476 }
477 Rule rule = new BeanCreateRule( childDescriptor, context, path, matchIDs);
478 addRule(path, rule);
479 continue;
480 }
481 if ( childDescriptor.getUpdater() != null ) {
482 if (log.isTraceEnabled()) {
483 log.trace("Element has updater "
484 + ((MethodUpdater) childDescriptor.getUpdater()).getMethod().getName());
485 }
486 if ( childDescriptor.isPrimitiveType() ) {
487 addPrimitiveTypeRule(path, childDescriptor);
488
489 } else {
490
491 ElementDescriptor[] grandChildren = childDescriptor.getElementDescriptors();
492 if ( grandChildren != null && grandChildren.length > 0 ) {
493 ElementDescriptor grandChild = grandChildren[0];
494 String grandChildQName = grandChild.getQualifiedName();
495 if ( grandChildQName != null && grandChildQName.length() > 0 ) {
496 if (childDescriptor.isWrapCollectionsInElement()) {
497 path += '/' + grandChildQName;
498
499 } else {
500 path = prefix + (prefix.endsWith("/")?"":"/") + grandChildQName;
501 }
502 }
503 }
504
505
506 Class beanClass = childDescriptor.getSingularPropertyType();
507 if ( XMLIntrospectorHelper.isPrimitiveType( beanClass ) ) {
508 addPrimitiveTypeRule(path, childDescriptor);
509
510 } else {
511 Rule rule = new BeanCreateRule(
512 childDescriptor,
513 context,
514 path + '/',
515 matchIDs );
516 addRule( path, rule );
517 }
518 }
519 } else {
520 log.trace("Element does not have updater");
521 }
522
523 ElementDescriptor[] grandChildren = childDescriptor.getElementDescriptors();
524 if ( grandChildren != null && grandChildren.length > 0 ) {
525 log.trace("Adding grand children");
526 addChildRules( path + '/', childDescriptor );
527 }
528 }
529 }
530 }
531
532 /***
533 * Get the associated bean reader.
534 *
535 * @return the <code>BeanReader</code digesting the xml
536 */
537 protected BeanReader getBeanReader() {
538
539
540 return (BeanReader) getDigester();
541 }
542
543 /*** Allows the navigation from a reference to a property object to the descriptor defining what
544 * the property is. i.e. doing the join from a reference to a type to lookup its descriptor.
545 * This could be done automatically by the NodeDescriptors. Refer to TODO.txt for more info.
546 *
547 * @param propertyDescriptor find descriptor for property object referenced by this descriptor
548 * @return descriptor for the singular property class type referenced.
549 */
550 protected ElementDescriptor getElementDescriptor( ElementDescriptor propertyDescriptor ) {
551 Class beanClass = propertyDescriptor.getSingularPropertyType();
552 if ( beanClass != null ) {
553 XMLIntrospector introspector = getBeanReader().getXMLIntrospector();
554 try {
555 XMLBeanInfo xmlInfo = introspector.introspect( beanClass );
556 return xmlInfo.getElementDescriptor();
557
558 } catch (Exception e) {
559 log.warn( "Could not introspect class: " + beanClass, e );
560 }
561 }
562
563 return propertyDescriptor;
564 }
565
566 /***
567 * Adds a new Digester rule to process the text as a primitive type
568 *
569 * @param path digester path where this rule will be attached
570 * @param childDescriptor update this <code>ElementDescriptor</code> with the body text
571 */
572 protected void addPrimitiveTypeRule(String path, final ElementDescriptor childDescriptor) {
573 Rule rule = new Rule() {
574 public void body(String text) throws Exception {
575 childDescriptor.getUpdater().update( context, text );
576 }
577 };
578 addRule( path, rule );
579 }
580
581 /***
582 * Safely add a rule with given path.
583 *
584 * @param path the digester path to add rule at
585 * @param rule the <code>Rule</code> to add
586 */
587 protected void addRule(String path, Rule rule) {
588 Rules rules = digester.getRules();
589 List matches = rules.match(null, path);
590 if ( matches.isEmpty() ) {
591 if ( log.isDebugEnabled() ) {
592 log.debug( "Adding digester rule for path: " + path + " rule: " + rule );
593 }
594 digester.addRule( path, rule );
595
596 } else {
597 if ( log.isDebugEnabled() ) {
598 log.debug( "Ignoring duplicate digester rule for path: "
599 + path + " rule: " + rule );
600 log.debug( "New rule (not added): " + rule );
601 log.debug( "Existing rule:" + matches.get(0) );
602 }
603 }
604 }
605
606 /***
607 * Get the map used to index beans (previously read in) by id.
608 * This is stored in the evaluation context.
609 *
610 * @return map indexing beans created by id
611 */
612 protected Map getBeansById() {
613
614
615
616
617
618 Map beansById = (Map) context.getVariable( "beans-index" );
619 if ( beansById == null ) {
620
621 beansById = new HashMap();
622 context.setVariable( "beans-index", beansById );
623 log.trace( "Created new index-by-id map" );
624 }
625
626 return beansById;
627 }
628
629 /***
630 * Return something meaningful for logging.
631 *
632 * @return something useful for logging
633 */
634 public String toString() {
635 return "BeanCreateRule [path prefix=" + pathPrefix + " descriptor=" + descriptor + "]";
636 }
637
638 }