1 /*
2 * $Header: /home/cvs/jakarta-commons/betwixt/src/java/org/apache/commons/betwixt/io/BeanCreateRule.java,v 1.17.2.1 2003/01/19 16:57:52 mvdb Exp $
3 * $Revision: 1.17.2.1 $
4 * $Date: 2003/01/19 16:57:52 $
5 *
6 * ====================================================================
7 *
8 * The Apache Software License, Version 1.1
9 *
10 * Copyright (c) 1999-2003 The Apache Software Foundation. All rights
11 * reserved.
12 *
13 * Redistribution and use in source and binary forms, with or without
14 * modification, are permitted provided that the following conditions
15 * are met:
16 *
17 * 1. Redistributions of source code must retain the above copyright
18 * notice, this list of conditions and the following disclaimer.
19 *
20 * 2. Redistributions in binary form must reproduce the above copyright
21 * notice, this list of conditions and the following disclaimer in
22 * the documentation and/or other materials provided with the
23 * distribution.
24 *
25 * 3. The end-user documentation included with the redistribution, if
26 * any, must include the following acknowlegement:
27 * "This product includes software developed by the
28 * Apache Software Foundation (http://www.apache.org/)."
29 * Alternately, this acknowlegement may appear in the software itself,
30 * if and wherever such third-party acknowlegements normally appear.
31 *
32 * 4. The names "The Jakarta Project", "Commons", and "Apache Software
33 * Foundation" must not be used to endorse or promote products derived
34 * from this software without prior written permission. For written
35 * permission, please contact apache@apache.org.
36 *
37 * 5. Products derived from this software may not be called "Apache"
38 * nor may "Apache" appear in their names without prior written
39 * permission of the Apache Group.
40 *
41 * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
42 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
43 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
44 * DISCLAIMED. IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR
45 * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
46 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
47 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
48 * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
49 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
50 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
51 * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
52 * SUCH DAMAGE.
53 * ====================================================================
54 *
55 * This software consists of voluntary contributions made by many
56 * individuals on behalf of the Apache Software Foundation. For more
57 * information on the Apache Software Foundation, please see
58 * <http://www.apache.org/>.
59 *
60 * $Id: BeanCreateRule.java,v 1.17.2.1 2003/01/19 16:57:52 mvdb Exp $
61 */
62 package org.apache.commons.betwixt.io;
63
64 import java.util.HashMap;
65 import java.util.List;
66 import java.util.Map;
67
68 import org.apache.commons.betwixt.AttributeDescriptor;
69 import org.apache.commons.betwixt.ElementDescriptor;
70 import org.apache.commons.betwixt.XMLBeanInfo;
71 import org.apache.commons.betwixt.XMLIntrospector;
72 import org.apache.commons.betwixt.digester.XMLIntrospectorHelper;
73 import org.apache.commons.betwixt.expression.Context;
74 import org.apache.commons.betwixt.expression.MethodUpdater;
75 import org.apache.commons.betwixt.expression.Updater;
76 import org.apache.commons.digester.Rule;
77 import org.apache.commons.digester.Rules;
78 import org.apache.commons.logging.Log;
79 import org.apache.commons.logging.LogFactory;
80 import org.xml.sax.Attributes;
81
82 /*** <p><code>BeanCreateRule</code> is a Digester Rule for creating beans
83 * from the betwixt XML metadata.</p>
84 *
85 * @author <a href="mailto:jstrachan@apache.org">James Strachan</a>
86 * @author <a href="mailto:martin@mvdb.net">Martin van den Bemt</a>
87 * @version $Revision: 1.17.2.1 $
88 */
89 public class BeanCreateRule extends Rule {
90
91 /*** Logger */
92 private static Log log = LogFactory.getLog( BeanCreateRule.class );
93
94 /***
95 * Set log to be used by <code>BeanCreateRule</code> instances
96 * @param aLog the <code>Log</code> implementation for this class to log to
97 */
98 public static void setLog(Log aLog) {
99 log = aLog;
100 }
101
102 /*** The descriptor of this element */
103 private ElementDescriptor descriptor;
104 /*** The Context used when evaluating Updaters */
105 private Context context;
106 /*** Have we added our child rules to the digester? */
107 private boolean addedChildren;
108 /*** In this begin-end loop did we actually create a new bean */
109 private boolean createdBean;
110 /*** The type of the bean to create */
111 private Class beanClass;
112 /*** The prefix added to digester rules */
113 private String pathPrefix;
114 /*** Use id's to match beans? */
115 private boolean matchIDs = true;
116
117 /***
118 * Convenience constructor which uses <code>ID's</code> for matching.
119 *
120 * @param descriptor the <code>ElementDescriptor</code> describing the element mapped
121 * @param beanClass the <code>Class</code> to be created
122 * @param pathPrefix the digester style path
123 */
124 public BeanCreateRule(
125 ElementDescriptor descriptor,
126 Class beanClass,
127 String pathPrefix ) {
128 this( descriptor, beanClass, pathPrefix, true );
129 }
130
131 /***
132 * Constructor taking a class.
133 *
134 * @param descriptor the <code>ElementDescriptor</code> describing the element mapped
135 * @param beanClass the <code>Class</code> to be created
136 * @param pathPrefix the digester style path
137 * @param matchIDs should <code>ID</code>/<code>IDREF</code>'s be used for matching
138 */
139 public BeanCreateRule(
140 ElementDescriptor descriptor,
141 Class beanClass,
142 String pathPrefix,
143 boolean matchIDs ) {
144 this(
145 descriptor,
146 beanClass,
147 new Context(),
148 pathPrefix,
149 matchIDs);
150 }
151
152 /***
153 * Convenience constructor which uses <code>ID's</code> for matching.
154 *
155 * @param descriptor the <code>ElementDescriptor</code> describing the element mapped
156 * @param beanClass the <code>Class</code> to be created
157 */
158 public BeanCreateRule( ElementDescriptor descriptor, Class beanClass ) {
159 this( descriptor, beanClass, true );
160 }
161
162 /***
163 * Constructor uses standard qualified name.
164 *
165 * @param descriptor the <code>ElementDescriptor</code> describing the element mapped
166 * @param beanClass the <code>Class</code> to be created
167 * @param matchIDs should <code>ID</code>/<code>IDREF</code>'s be used for matching
168 */
169 public BeanCreateRule( ElementDescriptor descriptor, Class beanClass, boolean matchIDs ) {
170 this( descriptor, beanClass, descriptor.getQualifiedName() + "/" , matchIDs );
171 }
172
173 /***
174 * Convenience constructor which uses <code>ID's</code> for match.
175 *
176 * @param descriptor the <code>ElementDescriptor</code> describing the element mapped
177 * @param context the <code>Context</code> to be used to evaluate expressions
178 * @param pathPrefix the digester path prefix
179 */
180 public BeanCreateRule(
181 ElementDescriptor descriptor,
182 Context context,
183 String pathPrefix ) {
184 this( descriptor, context, pathPrefix, true );
185 }
186
187 /***
188 * Constructor taking a context.
189 *
190 * @param descriptor the <code>ElementDescriptor</code> describing the element mapped
191 * @param context the <code>Context</code> to be used to evaluate expressions
192 * @param pathPrefix the digester path prefix
193 * @param matchIDs should <code>ID</code>/<code>IDREF</code>'s be used for matching
194 */
195 public BeanCreateRule(
196 ElementDescriptor descriptor,
197 Context context,
198 String pathPrefix,
199 boolean matchIDs ) {
200 this(
201 descriptor,
202 descriptor.getSingularPropertyType(),
203 context,
204 pathPrefix,
205 matchIDs );
206 }
207
208 /***
209 * Base constructor (used by other constructors).
210 *
211 * @param descriptor the <code>ElementDescriptor</code> describing the element mapped
212 * @param beanClass the <code>Class</code> of the bean to be created
213 * @param context the <code>Context</code> to be used to evaluate expressions
214 * @param pathPrefix the digester path prefix
215 * @param matchIDs should <code>ID</code>/<code>IDREF</code>'s be used for matching
216 */
217 private BeanCreateRule(
218 ElementDescriptor descriptor,
219 Class beanClass,
220 Context context,
221 String pathPrefix,
222 boolean matchIDs ) {
223 this.descriptor = descriptor;
224 this.context = context;
225 this.beanClass = beanClass;
226 this.pathPrefix = pathPrefix;
227 this.matchIDs = matchIDs;
228 if (log.isTraceEnabled()) {
229 log.trace("Created bean create rule");
230 log.trace("Descriptor=" + descriptor);
231 log.trace("Class=" + beanClass);
232 log.trace("Path prefix=" + pathPrefix);
233 }
234 }
235
236
237
238 // Rule interface
239 //-------------------------------------------------------------------------
240
241 /***
242 * Process the beginning of this element.
243 *
244 * @param attributes The attribute list of this element
245 */
246 public void begin(Attributes attributes) {
247 log.debug( "Called with descriptor: " + descriptor
248 + " propertyType: " + descriptor.getPropertyType() );
249
250 if (log.isTraceEnabled()) {
251 int attributesLength = attributes.getLength();
252 if (attributesLength > 0) {
253 log.trace("Attributes:");
254 }
255 for (int i=0, size=attributesLength; i<size; i++) {
256 log.trace("Local:" + attributes.getLocalName(i));
257 log.trace("URI:" + attributes.getURI(i));
258 log.trace("QName:" + attributes.getQName(i));
259 }
260 }
261
262
263
264 // XXX: if a single rule instance gets reused and nesting occurs
265 // XXX: we should probably use a stack of booleans to test if we created a bean
266 // XXX: or let digester take nulls, which would be easier for us ;-)
267 createdBean = false;
268
269 Object instance = null;
270 if ( beanClass != null ) {
271 instance = createBean(attributes);
272 if ( instance != null ) {
273 createdBean = true;
274
275 context.setBean( instance );
276 digester.push(instance);
277
278
279 // if we are a reference to a type we should lookup the original
280 // as this ElementDescriptor will be 'hollow' and have no child attributes/elements.
281 // XXX: this should probably be done by the NodeDescriptors...
282 ElementDescriptor typeDescriptor = getElementDescriptor( descriptor );
283 //ElementDescriptor typeDescriptor = descriptor;
284
285 // iterate through all attributes
286 AttributeDescriptor[] attributeDescriptors
287 = typeDescriptor.getAttributeDescriptors();
288 if ( attributeDescriptors != null ) {
289 for ( int i = 0, size = attributeDescriptors.length; i < size; i++ ) {
290 AttributeDescriptor attributeDescriptor = attributeDescriptors[i];
291
292 // The following isn't really the right way to find the attribute
293 // but it's quite robust.
294 // The idea is that you try both namespace and local name first
295 // and if this returns null try the qName.
296 String value = attributes.getValue(
297 attributeDescriptor.getURI(),
298 attributeDescriptor.getLocalName()
299 );
300
301 if (value == null) {
302 value = attributes.getValue(attributeDescriptor.getQualifiedName());
303 }
304
305 if (log.isTraceEnabled()) {
306 log.trace("Attr URL:" + attributeDescriptor.getURI());
307 log.trace("Attr LocalName:" + attributeDescriptor.getLocalName() );
308 log.trace(value);
309 }
310
311 Updater updater = attributeDescriptor.getUpdater();
312 log.trace(updater);
313 if ( updater != null && value != null ) {
314 updater.update( context, value );
315 }
316 }
317 }
318
319 addChildRules();
320
321 // add bean for ID matching
322 if ( matchIDs ) {
323 // XXX need to support custom ID attribute names
324 // XXX i have a feeling that the current mechanism might need to change
325 // XXX so i'm leaving this till later
326 String id = attributes.getValue( "id" );
327 if ( id != null ) {
328 getBeansById().put( id, instance );
329 }
330 }
331 }
332 }
333 }
334
335 /***
336 * Process the end of this element.
337 */
338 public void end() {
339 if ( createdBean ) {
340
341 // force any setters of the parent bean to be called for this new bean instance
342 Updater updater = descriptor.getUpdater();
343 Object instance = context.getBean();
344
345 Object top = digester.pop();
346 if (digester.getCount() == 0) {
347 context.setBean(null);
348 }else{
349 context.setBean( digester.peek() );
350 }
351
352 if ( updater != null ) {
353 if ( log.isDebugEnabled() ) {
354 log.debug( "Calling updater for: " + descriptor + " with: "
355 + instance + " on bean: " + context.getBean() );
356 }
357 updater.update( context, instance );
358 } else {
359 if ( log.isDebugEnabled() ) {
360 log.debug( "No updater for: " + descriptor + " with: "
361 + instance + " on bean: " + context.getBean() );
362 }
363 }
364 }
365 }
366
367 /***
368 * Tidy up.
369 */
370 public void finish() {}
371
372
373 // Implementation methods
374 //-------------------------------------------------------------------------
375
376 /***
377 * Factory method to create new bean instances
378 *
379 * @param attributes the <code>Attributes</code> used to match <code>ID/IDREF</code>
380 * @return the created bean
381 */
382 protected Object createBean(Attributes attributes) {
383 //
384 // See if we've got an IDREF
385 //
386 // XXX This should be customizable but i'm not really convinced by the existing system
387 // XXX maybe it's going to have to change so i'll use 'idref' for nows
388 //
389 if ( matchIDs ) {
390 String idref = attributes.getValue( "idref" );
391 if ( idref != null ) {
392 // XXX need to check up about ordering
393 // XXX this is a very simple system that assumes that id occurs before idrefs
394 // XXX would need some thought about how to implement a fuller system
395 log.trace( "Found IDREF" );
396 Object bean = getBeansById().get( idref );
397 if ( bean != null ) {
398 if (log.isTraceEnabled()) {
399 log.trace( "Matched bean " + bean );
400 }
401 return bean;
402 }
403 log.trace( "No match found" );
404 }
405 }
406
407 try {
408 if (log.isTraceEnabled()) {
409 log.trace( "Creating instance of " + beanClass );
410 }
411 return beanClass.newInstance();
412
413 } catch (Exception e) {
414 log.warn( "Could not create instance of type: " + beanClass.getName() );
415 return null;
416 }
417 }
418
419 /*** Adds the rules to the digester for all child elements */
420 protected void addChildRules() {
421 if ( ! addedChildren ) {
422 addedChildren = true;
423
424 addChildRules( pathPrefix, descriptor );
425 }
426 }
427
428 /***
429 * Add child rules for given descriptor at given prefix
430 *
431 * @param prefix add child rules at this (digester) path prefix
432 * @param currentDescriptor add child rules for this descriptor
433 */
434 protected void addChildRules(String prefix, ElementDescriptor currentDescriptor ) {
435 BeanReader digester = getBeanReader();
436
437 if (log.isTraceEnabled()) {
438 log.trace("Adding child rules for " + currentDescriptor + "@" + prefix);
439 }
440
441 // if we are a reference to a type we should lookup the original
442 // as this ElementDescriptor will be 'hollow' and have no child attributes/elements.
443 // XXX: this should probably be done by the NodeDescriptors...
444 ElementDescriptor typeDescriptor = getElementDescriptor( currentDescriptor );
445 //ElementDescriptor typeDescriptor = descriptor;
446
447
448 ElementDescriptor[] childDescriptors = typeDescriptor.getElementDescriptors();
449 if ( childDescriptors != null ) {
450 for ( int i = 0, size = childDescriptors.length; i < size; i++ ) {
451 final ElementDescriptor childDescriptor = childDescriptors[i];
452 if (log.isTraceEnabled()) {
453 log.trace("Processing child " + childDescriptor);
454 }
455
456 String propertyName = childDescriptor.getPropertyName();
457 String qualifiedName = childDescriptor.getQualifiedName();
458 if ( qualifiedName == null ) {
459 log.trace( "Ignoring" );
460 continue;
461 }
462 String path = prefix + qualifiedName;
463 // this code is for making sure that recursive elements
464 // can also be used..
465
466 if ( qualifiedName.equals( currentDescriptor.getQualifiedName() )
467 && currentDescriptor.getPropertyName() != null ) {
468 log.trace("Creating generic rule for recursive elements");
469 int index = -1;
470 if (childDescriptor.isWrapCollectionsInElement()) {
471 index = prefix.indexOf(qualifiedName);
472 if (index == -1) {
473 // shouldn't happen..
474 log.debug( "Oops - this shouldn't happen" );
475 continue;
476 }
477 int removeSlash = prefix.endsWith("/")?1:0;
478 path = "*/" + prefix.substring(index, prefix.length()-removeSlash);
479 }else{
480 // we have a element/element type of thing..
481 ElementDescriptor[] desc = currentDescriptor.getElementDescriptors();
482 if (desc.length == 1) {
483 path = "*/"+desc[0].getQualifiedName();
484 }
485 }
486 Rule rule = new BeanCreateRule( childDescriptor, context, path, matchIDs);
487 addRule(path, rule);
488 continue;
489 }
490 if ( childDescriptor.getUpdater() != null ) {
491 if (log.isTraceEnabled()) {
492 log.trace("Element has updater "
493 + ((MethodUpdater) childDescriptor.getUpdater()).getMethod().getName());
494 }
495 if ( childDescriptor.isPrimitiveType() ) {
496 addPrimitiveTypeRule(path, childDescriptor);
497
498 } else {
499 // add the first child to the path
500 ElementDescriptor[] grandChildren = childDescriptor.getElementDescriptors();
501 if ( grandChildren != null && grandChildren.length > 0 ) {
502 ElementDescriptor grandChild = grandChildren[0];
503 String grandChildQName = grandChild.getQualifiedName();
504 if ( grandChildQName != null && grandChildQName.length() > 0 ) {
505 if (childDescriptor.isWrapCollectionsInElement()) {
506 path += '/' + grandChildQName;
507
508 } else {
509 path = prefix + (prefix.endsWith("/")?"":"/") + grandChildQName;
510 }
511 }
512 }
513
514 // maybe we are adding a primitve type to a collection/array
515 Class beanClass = childDescriptor.getSingularPropertyType();
516 if ( XMLIntrospectorHelper.isPrimitiveType( beanClass ) ) {
517 addPrimitiveTypeRule(path, childDescriptor);
518
519 } else {
520 Rule rule = new BeanCreateRule(
521 childDescriptor,
522 context,
523 path + '/',
524 matchIDs );
525 addRule( path, rule );
526 }
527 }
528 } else {
529 log.trace("Element does not have updater");
530 }
531
532 ElementDescriptor[] grandChildren = childDescriptor.getElementDescriptors();
533 if ( grandChildren != null && grandChildren.length > 0 ) {
534 log.trace("Adding grand children");
535 addChildRules( path + '/', childDescriptor );
536 }
537 }
538 }
539 }
540
541 /***
542 * Get the associated bean reader.
543 *
544 * @return the <code>BeanReader</code digesting the xml
545 */
546 protected BeanReader getBeanReader() {
547 // XXX this breaks the rule contact
548 // XXX maybe the reader should be passed in the constructor
549 return (BeanReader) getDigester();
550 }
551
552 /*** Allows the navigation from a reference to a property object to the descriptor defining what
553 * the property is. i.e. doing the join from a reference to a type to lookup its descriptor.
554 * This could be done automatically by the NodeDescriptors. Refer to TODO.txt for more info.
555 *
556 * @param propertyDescriptor find descriptor for property object referenced by this descriptor
557 * @return descriptor for the singular property class type referenced.
558 */
559 protected ElementDescriptor getElementDescriptor( ElementDescriptor propertyDescriptor ) {
560 Class beanClass = propertyDescriptor.getSingularPropertyType();
561 if ( beanClass != null ) {
562 XMLIntrospector introspector = getBeanReader().getXMLIntrospector();
563 try {
564 XMLBeanInfo xmlInfo = introspector.introspect( beanClass );
565 return xmlInfo.getElementDescriptor();
566
567 } catch (Exception e) {
568 log.warn( "Could not introspect class: " + beanClass, e );
569 }
570 }
571 // could not find a better descriptor so use the one we've got
572 return propertyDescriptor;
573 }
574
575 /***
576 * Adds a new Digester rule to process the text as a primitive type
577 *
578 * @param path digester path where this rule will be attached
579 * @param childDescriptor update this <code>ElementDescriptor</code> with the body text
580 */
581 protected void addPrimitiveTypeRule(String path, final ElementDescriptor childDescriptor) {
582 Rule rule = new Rule() {
583 public void body(String text) throws Exception {
584 childDescriptor.getUpdater().update( context, text );
585 }
586 };
587 addRule( path, rule );
588 }
589
590 /***
591 * Safely add a rule with given path.
592 *
593 * @param path the digester path to add rule at
594 * @param rule the <code>Rule</code> to add
595 */
596 protected void addRule(String path, Rule rule) {
597 Rules rules = digester.getRules();
598 List matches = rules.match(null, path);
599 if ( matches.isEmpty() ) {
600 if ( log.isDebugEnabled() ) {
601 log.debug( "Adding digester rule for path: " + path + " rule: " + rule );
602 }
603 digester.addRule( path, rule );
604
605 } else {
606 if ( log.isDebugEnabled() ) {
607 log.debug( "Ignoring duplicate digester rule for path: "
608 + path + " rule: " + rule );
609 log.debug( "New rule (not added): " + rule );
610 log.debug( "Existing rule:" + matches.get(0) );
611 }
612 }
613 }
614
615 /***
616 * Get the map used to index beans (previously read in) by id.
617 * This is stored in the evaluation context.
618 *
619 * @return map indexing beans created by id
620 */
621 protected Map getBeansById() {
622 //
623 // we need a single index for beans read in by id
624 // so that we can use them for idref-matching
625 // store this in the context
626 //
627 Map beansById = (Map) context.getVariable( "beans-index" );
628 if ( beansById == null ) {
629 // lazy creation
630 beansById = new HashMap();
631 context.setVariable( "beans-index", beansById );
632 log.trace( "Created new index-by-id map" );
633 }
634
635 return beansById;
636 }
637
638 /***
639 * Return something meaningful for logging.
640 *
641 * @return something useful for logging
642 */
643 public String toString() {
644 return "BeanCreateRule [path prefix=" + pathPrefix + " descriptor=" + descriptor + "]";
645 }
646
647 }
This page was automatically generated by Maven