001: /*
002: * Licensed to the Apache Software Foundation (ASF) under one or more
003: * contributor license agreements. See the NOTICE file distributed with
004: * this work for additional information regarding copyright ownership.
005: * The ASF licenses this file to You under the Apache License, Version 2.0
006: * (the "License"); you may not use this file except in compliance with
007: * the License. You may obtain a copy of the License at
008: *
009: * http://www.apache.org/licenses/LICENSE-2.0
010: *
011: * Unless required by applicable law or agreed to in writing, software
012: * distributed under the License is distributed on an "AS IS" BASIS,
013: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014: * See the License for the specific language governing permissions and
015: * limitations under the License.
016: */
017: package org.apache.commons.betwixt.io;
018:
019: import java.util.HashMap;
020: import java.util.List;
021: import java.util.Map;
022:
023: import org.apache.commons.betwixt.AttributeDescriptor;
024: import org.apache.commons.betwixt.ElementDescriptor;
025: import org.apache.commons.betwixt.XMLBeanInfo;
026: import org.apache.commons.betwixt.XMLIntrospector;
027: import org.apache.commons.betwixt.digester.XMLIntrospectorHelper;
028: import org.apache.commons.betwixt.expression.Context;
029: import org.apache.commons.betwixt.expression.MethodUpdater;
030: import org.apache.commons.betwixt.expression.Updater;
031: import org.apache.commons.digester.Rule;
032: import org.apache.commons.digester.Rules;
033: import org.apache.commons.logging.Log;
034: import org.apache.commons.logging.LogFactory;
035: import org.xml.sax.Attributes;
036:
037: /** <p><code>BeanCreateRule</code> is a Digester Rule for creating beans
038: * from the betwixt XML metadata.</p>
039: *
040: * @author <a href="mailto:jstrachan@apache.org">James Strachan</a>
041: * @author <a href="mailto:martin@mvdb.net">Martin van den Bemt</a>
042: * @deprecated 0.5 this Rule does not allowed good integration with other Rules -
043: * use {@link BeanRuleSet} instead.
044: */
045: public class BeanCreateRule extends Rule {
046:
047: /** Logger */
048: private static Log log = LogFactory.getLog(BeanCreateRule.class);
049:
050: /**
051: * Set log to be used by <code>BeanCreateRule</code> instances
052: * @param aLog the <code>Log</code> implementation for this class to log to
053: */
054: public static void setLog(Log aLog) {
055: log = aLog;
056: }
057:
058: /** The descriptor of this element */
059: private ElementDescriptor descriptor;
060: /** The Context used when evaluating Updaters */
061: private Context context;
062: /** Have we added our child rules to the digester? */
063: private boolean addedChildren;
064: /** In this begin-end loop did we actually create a new bean */
065: private boolean createdBean;
066: /** The type of the bean to create */
067: private Class beanClass;
068: /** The prefix added to digester rules */
069: private String pathPrefix;
070: /** Use id's to match beans? */
071: private boolean matchIDs = true;
072: /** allows an attribute to be specified to overload the types of beans used */
073: private String classNameAttribute = "className";
074:
075: /**
076: * Convenience constructor which uses <code>ID's</code> for matching.
077: *
078: * @param descriptor the <code>ElementDescriptor</code> describing the element mapped
079: * @param beanClass the <code>Class</code> to be created
080: * @param pathPrefix the digester style path
081: */
082: public BeanCreateRule(ElementDescriptor descriptor,
083: Class beanClass, String pathPrefix) {
084: this (descriptor, beanClass, pathPrefix, true);
085: }
086:
087: /**
088: * Constructor taking a class.
089: *
090: * @param descriptor the <code>ElementDescriptor</code> describing the element mapped
091: * @param beanClass the <code>Class</code> to be created
092: * @param pathPrefix the digester style path
093: * @param matchIDs should <code>ID</code>/<code>IDREF</code>'s be used for matching
094: */
095: public BeanCreateRule(ElementDescriptor descriptor,
096: Class beanClass, String pathPrefix, boolean matchIDs) {
097: this (descriptor, beanClass, new Context(), pathPrefix, matchIDs);
098: }
099:
100: /**
101: * Convenience constructor which uses <code>ID's</code> for matching.
102: *
103: * @param descriptor the <code>ElementDescriptor</code> describing the element mapped
104: * @param beanClass the <code>Class</code> to be created
105: */
106: public BeanCreateRule(ElementDescriptor descriptor, Class beanClass) {
107: this (descriptor, beanClass, true);
108: }
109:
110: /**
111: * Constructor uses standard qualified name.
112: *
113: * @param descriptor the <code>ElementDescriptor</code> describing the element mapped
114: * @param beanClass the <code>Class</code> to be created
115: * @param matchIDs should <code>ID</code>/<code>IDREF</code>'s be used for matching
116: */
117: public BeanCreateRule(ElementDescriptor descriptor,
118: Class beanClass, boolean matchIDs) {
119: this (descriptor, beanClass,
120: descriptor.getQualifiedName() + "/", matchIDs);
121: }
122:
123: /**
124: * Convenience constructor which uses <code>ID's</code> for match.
125: *
126: * @param descriptor the <code>ElementDescriptor</code> describing the element mapped
127: * @param context the <code>Context</code> to be used to evaluate expressions
128: * @param pathPrefix the digester path prefix
129: */
130: public BeanCreateRule(ElementDescriptor descriptor,
131: Context context, String pathPrefix) {
132: this (descriptor, context, pathPrefix, true);
133: }
134:
135: /**
136: * Constructor taking a context.
137: *
138: * @param descriptor the <code>ElementDescriptor</code> describing the element mapped
139: * @param context the <code>Context</code> to be used to evaluate expressions
140: * @param pathPrefix the digester path prefix
141: * @param matchIDs should <code>ID</code>/<code>IDREF</code>'s be used for matching
142: */
143: public BeanCreateRule(ElementDescriptor descriptor,
144: Context context, String pathPrefix, boolean matchIDs) {
145: this (descriptor, descriptor.getSingularPropertyType(), context,
146: pathPrefix, matchIDs);
147: }
148:
149: /**
150: * Base constructor (used by other constructors).
151: *
152: * @param descriptor the <code>ElementDescriptor</code> describing the element mapped
153: * @param beanClass the <code>Class</code> of the bean to be created
154: * @param context the <code>Context</code> to be used to evaluate expressions
155: * @param pathPrefix the digester path prefix
156: * @param matchIDs should <code>ID</code>/<code>IDREF</code>'s be used for matching
157: */
158: private BeanCreateRule(ElementDescriptor descriptor,
159: Class beanClass, Context context, String pathPrefix,
160: boolean matchIDs) {
161: this .descriptor = descriptor;
162: this .context = context;
163: this .beanClass = beanClass;
164: this .pathPrefix = pathPrefix;
165: this .matchIDs = matchIDs;
166: if (log.isTraceEnabled()) {
167: log.trace("Created bean create rule");
168: log.trace("Descriptor=" + descriptor);
169: log.trace("Class=" + beanClass);
170: log.trace("Path prefix=" + pathPrefix);
171: }
172: }
173:
174: // Rule interface
175: //-------------------------------------------------------------------------
176:
177: /**
178: * Process the beginning of this element.
179: *
180: * @param attributes The attribute list of this element
181: */
182: public void begin(Attributes attributes) {
183: log.debug("Called with descriptor: " + descriptor
184: + " propertyType: " + descriptor.getPropertyType());
185:
186: if (log.isTraceEnabled()) {
187: int attributesLength = attributes.getLength();
188: if (attributesLength > 0) {
189: log.trace("Attributes:");
190: }
191: for (int i = 0, size = attributesLength; i < size; i++) {
192: log.trace("Local:" + attributes.getLocalName(i));
193: log.trace("URI:" + attributes.getURI(i));
194: log.trace("QName:" + attributes.getQName(i));
195: }
196: }
197:
198: // XXX: if a single rule instance gets reused and nesting occurs
199: // XXX: we should probably use a stack of booleans to test if we created a bean
200: // XXX: or let digester take nulls, which would be easier for us ;-)
201: createdBean = false;
202:
203: Object instance = null;
204: if (beanClass != null) {
205: instance = createBean(attributes);
206: if (instance != null) {
207: createdBean = true;
208:
209: context.setBean(instance);
210: digester.push(instance);
211:
212: // if we are a reference to a type we should lookup the original
213: // as this ElementDescriptor will be 'hollow' and have no child attributes/elements.
214: // XXX: this should probably be done by the NodeDescriptors...
215: ElementDescriptor typeDescriptor = getElementDescriptor(descriptor);
216: //ElementDescriptor typeDescriptor = descriptor;
217:
218: // iterate through all attributes
219: AttributeDescriptor[] attributeDescriptors = typeDescriptor
220: .getAttributeDescriptors();
221: if (attributeDescriptors != null) {
222: for (int i = 0, size = attributeDescriptors.length; i < size; i++) {
223: AttributeDescriptor attributeDescriptor = attributeDescriptors[i];
224:
225: // The following isn't really the right way to find the attribute
226: // but it's quite robust.
227: // The idea is that you try both namespace and local name first
228: // and if this returns null try the qName.
229: String value = attributes.getValue(
230: attributeDescriptor.getURI(),
231: attributeDescriptor.getLocalName());
232:
233: if (value == null) {
234: value = attributes
235: .getValue(attributeDescriptor
236: .getQualifiedName());
237: }
238:
239: if (log.isTraceEnabled()) {
240: log.trace("Attr URL:"
241: + attributeDescriptor.getURI());
242: log.trace("Attr LocalName:"
243: + attributeDescriptor
244: .getLocalName());
245: log.trace(value);
246: }
247:
248: Updater updater = attributeDescriptor
249: .getUpdater();
250: log.trace(updater);
251: if (updater != null && value != null) {
252: updater.update(context, value);
253: }
254: }
255: }
256:
257: addChildRules();
258:
259: // add bean for ID matching
260: if (matchIDs) {
261: // XXX need to support custom ID attribute names
262: // XXX i have a feeling that the current mechanism might need to change
263: // XXX so i'm leaving this till later
264: String id = attributes.getValue("id");
265: if (id != null) {
266: getBeansById().put(id, instance);
267: }
268: }
269: }
270: }
271: }
272:
273: /**
274: * Process the end of this element.
275: */
276: public void end() {
277: if (createdBean) {
278:
279: // force any setters of the parent bean to be called for this new bean instance
280: Updater updater = descriptor.getUpdater();
281: Object instance = context.getBean();
282:
283: Object top = digester.pop();
284: if (digester.getCount() == 0) {
285: context.setBean(null);
286: } else {
287: context.setBean(digester.peek());
288: }
289:
290: if (updater != null) {
291: if (log.isDebugEnabled()) {
292: log.debug("Calling updater for: " + descriptor
293: + " with: " + instance + " on bean: "
294: + context.getBean());
295: }
296: updater.update(context, instance);
297: } else {
298: if (log.isDebugEnabled()) {
299: log.debug("No updater for: " + descriptor
300: + " with: " + instance + " on bean: "
301: + context.getBean());
302: }
303: }
304: }
305: }
306:
307: /**
308: * Tidy up.
309: */
310: public void finish() {
311: }
312:
313: // Properties
314: //-------------------------------------------------------------------------
315:
316: /**
317: * The name of the attribute which can be specified in the XML to override the
318: * type of a bean used at a certain point in the schema.
319: *
320: * <p>The default value is 'className'.</p>
321: *
322: * @return The name of the attribute used to overload the class name of a bean
323: */
324: public String getClassNameAttribute() {
325: return classNameAttribute;
326: }
327:
328: /**
329: * Sets the name of the attribute which can be specified in
330: * the XML to override the type of a bean used at a certain
331: * point in the schema.
332: *
333: * <p>The default value is 'className'.</p>
334: *
335: * @param classNameAttribute The name of the attribute used to overload the class name of a bean
336: */
337: public void setClassNameAttribute(String classNameAttribute) {
338: this .classNameAttribute = classNameAttribute;
339: }
340:
341: // Implementation methods
342: //-------------------------------------------------------------------------
343:
344: /**
345: * Factory method to create new bean instances
346: *
347: * @param attributes the <code>Attributes</code> used to match <code>ID/IDREF</code>
348: * @return the created bean
349: */
350: protected Object createBean(Attributes attributes) {
351: //
352: // See if we've got an IDREF
353: //
354: // XXX This should be customizable but i'm not really convinced by the existing system
355: // XXX maybe it's going to have to change so i'll use 'idref' for nows
356: //
357: if (matchIDs) {
358: String idref = attributes.getValue("idref");
359: if (idref != null) {
360: // XXX need to check up about ordering
361: // XXX this is a very simple system that assumes that id occurs before idrefs
362: // XXX would need some thought about how to implement a fuller system
363: log.trace("Found IDREF");
364: Object bean = getBeansById().get(idref);
365: if (bean != null) {
366: if (log.isTraceEnabled()) {
367: log.trace("Matched bean " + bean);
368: }
369: return bean;
370: }
371: log.trace("No match found");
372: }
373: }
374:
375: Class theClass = beanClass;
376: try {
377:
378: String className = attributes.getValue(classNameAttribute);
379: if (className != null) {
380: // load the class we should instantiate
381: theClass = getDigester().getClassLoader().loadClass(
382: className);
383: }
384: if (log.isTraceEnabled()) {
385: log.trace("Creating instance of " + theClass);
386: }
387: return theClass.newInstance();
388:
389: } catch (Exception e) {
390: log.warn("Could not create instance of type: "
391: + theClass.getName());
392: return null;
393: }
394: }
395:
396: /** Adds the rules to the digester for all child elements */
397: protected void addChildRules() {
398: if (!addedChildren) {
399: addedChildren = true;
400:
401: addChildRules(pathPrefix, descriptor);
402: }
403: }
404:
405: /**
406: * Add child rules for given descriptor at given prefix
407: *
408: * @param prefix add child rules at this (digester) path prefix
409: * @param currentDescriptor add child rules for this descriptor
410: */
411: protected void addChildRules(String prefix,
412: ElementDescriptor currentDescriptor) {
413:
414: if (log.isTraceEnabled()) {
415: log.trace("Adding child rules for " + currentDescriptor
416: + "@" + prefix);
417: }
418:
419: // if we are a reference to a type we should lookup the original
420: // as this ElementDescriptor will be 'hollow' and have no child attributes/elements.
421: // XXX: this should probably be done by the NodeDescriptors...
422: ElementDescriptor typeDescriptor = getElementDescriptor(currentDescriptor);
423: //ElementDescriptor typeDescriptor = descriptor;
424:
425: ElementDescriptor[] childDescriptors = typeDescriptor
426: .getElementDescriptors();
427: if (childDescriptors != null) {
428: for (int i = 0, size = childDescriptors.length; i < size; i++) {
429: final ElementDescriptor childDescriptor = childDescriptors[i];
430: if (log.isTraceEnabled()) {
431: log.trace("Processing child " + childDescriptor);
432: }
433:
434: String qualifiedName = childDescriptor
435: .getQualifiedName();
436: if (qualifiedName == null) {
437: log.trace("Ignoring");
438: continue;
439: }
440: String path = prefix + qualifiedName;
441: // this code is for making sure that recursive elements
442: // can also be used..
443:
444: if (qualifiedName.equals(currentDescriptor
445: .getQualifiedName())
446: && currentDescriptor.getPropertyName() != null) {
447: log
448: .trace("Creating generic rule for recursive elements");
449: int index = -1;
450: if (childDescriptor.isWrapCollectionsInElement()) {
451: index = prefix.indexOf(qualifiedName);
452: if (index == -1) {
453: // shouldn't happen..
454: log.debug("Oops - this shouldn't happen");
455: continue;
456: }
457: int removeSlash = prefix.endsWith("/") ? 1 : 0;
458: path = "*/"
459: + prefix.substring(index, prefix
460: .length()
461: - removeSlash);
462: } else {
463: // we have a element/element type of thing..
464: ElementDescriptor[] desc = currentDescriptor
465: .getElementDescriptors();
466: if (desc.length == 1) {
467: path = "*/" + desc[0].getQualifiedName();
468: }
469: }
470: Rule rule = new BeanCreateRule(childDescriptor,
471: context, path, matchIDs);
472: addRule(path, rule);
473: continue;
474: }
475: if (childDescriptor.getUpdater() != null) {
476: if (log.isTraceEnabled()) {
477: log.trace("Element has updater "
478: + ((MethodUpdater) childDescriptor
479: .getUpdater()).getMethod()
480: .getName());
481: }
482: if (childDescriptor.isPrimitiveType()) {
483: addPrimitiveTypeRule(path, childDescriptor);
484:
485: } else {
486: // add the first child to the path
487: ElementDescriptor[] grandChildren = childDescriptor
488: .getElementDescriptors();
489: if (grandChildren != null
490: && grandChildren.length > 0) {
491: ElementDescriptor grandChild = grandChildren[0];
492: String grandChildQName = grandChild
493: .getQualifiedName();
494: if (grandChildQName != null
495: && grandChildQName.length() > 0) {
496: if (childDescriptor
497: .isWrapCollectionsInElement()) {
498: path += '/' + grandChildQName;
499:
500: } else {
501: path = prefix
502: + (prefix.endsWith("/") ? ""
503: : "/")
504: + grandChildQName;
505: }
506: }
507: }
508:
509: // maybe we are adding a primitve type to a collection/array
510: Class beanClass = childDescriptor
511: .getSingularPropertyType();
512: if (XMLIntrospectorHelper
513: .isPrimitiveType(beanClass)) {
514: addPrimitiveTypeRule(path, childDescriptor);
515:
516: } else {
517: Rule rule = new BeanCreateRule(
518: childDescriptor, context,
519: path + '/', matchIDs);
520: addRule(path, rule);
521: }
522: }
523: } else {
524: log.trace("Element does not have updater");
525: }
526:
527: ElementDescriptor[] grandChildren = childDescriptor
528: .getElementDescriptors();
529: if (grandChildren != null && grandChildren.length > 0) {
530: log.trace("Adding grand children");
531: addChildRules(path + '/', childDescriptor);
532: }
533: }
534: }
535: }
536:
537: /**
538: * Get the associated bean reader.
539: *
540: * @return the <code>BeanReader</code digesting the xml
541: */
542: protected BeanReader getBeanReader() {
543: // XXX this breaks the rule contact
544: // XXX maybe the reader should be passed in the constructor
545: return (BeanReader) getDigester();
546: }
547:
548: /**
549: * Allows the navigation from a reference to a property object to the descriptor defining what
550: * the property is. In other words, doing the join from a reference to a type to lookup its descriptor.
551: * This could be done automatically by the NodeDescriptors. Refer to TODO.txt for more info.
552: *
553: * @param propertyDescriptor find descriptor for property object referenced by this descriptor
554: * @return descriptor for the singular property class type referenced.
555: */
556: protected ElementDescriptor getElementDescriptor(
557: ElementDescriptor propertyDescriptor) {
558: Class beanClass = propertyDescriptor.getSingularPropertyType();
559: if (beanClass != null) {
560: XMLIntrospector introspector = getBeanReader()
561: .getXMLIntrospector();
562: try {
563: XMLBeanInfo xmlInfo = introspector
564: .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,
582: final ElementDescriptor childDescriptor) {
583: Rule rule = new Rule() {
584: public void body(String text) throws Exception {
585: childDescriptor.getUpdater().update(context, text);
586: }
587: };
588: addRule(path, rule);
589: }
590:
591: /**
592: * Safely add a rule with given path.
593: *
594: * @param path the digester path to add rule at
595: * @param rule the <code>Rule</code> to add
596: */
597: protected void addRule(String path, Rule rule) {
598: Rules rules = digester.getRules();
599: List matches = rules.match(null, path);
600: if (matches.isEmpty()) {
601: if (log.isDebugEnabled()) {
602: log.debug("Adding digester rule for path: " + path
603: + " rule: " + rule);
604: }
605: digester.addRule(path, rule);
606:
607: } else {
608: if (log.isDebugEnabled()) {
609: log.debug("Ignoring duplicate digester rule for path: "
610: + path + " rule: " + rule);
611: log.debug("New rule (not added): " + rule);
612: log.debug("Existing rule:" + matches.get(0));
613: }
614: }
615: }
616:
617: /**
618: * Get the map used to index beans (previously read in) by id.
619: * This is stored in the evaluation context.
620: *
621: * @return map indexing beans created by id
622: */
623: protected Map getBeansById() {
624: //
625: // we need a single index for beans read in by id
626: // so that we can use them for idref-matching
627: // store this in the context
628: //
629: Map beansById = (Map) context.getVariable("beans-index");
630: if (beansById == null) {
631: // lazy creation
632: beansById = new HashMap();
633: context.setVariable("beans-index", beansById);
634: log.trace("Created new index-by-id map");
635: }
636:
637: return beansById;
638: }
639:
640: /**
641: * Return something meaningful for logging.
642: *
643: * @return something useful for logging
644: */
645: public String toString() {
646: return "BeanCreateRule [path prefix=" + pathPrefix
647: + " descriptor=" + descriptor + "]";
648: }
649:
650: }
|