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