- /*
- * Copyright 2001-2004 The Apache Software Foundation.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- package org.apache.commons.betwixt.io;
-
- import java.util.HashMap;
- import java.util.List;
- import java.util.Map;
-
- import org.apache.commons.betwixt.AttributeDescriptor;
- import org.apache.commons.betwixt.ElementDescriptor;
- import org.apache.commons.betwixt.XMLBeanInfo;
- import org.apache.commons.betwixt.XMLIntrospector;
- import org.apache.commons.betwixt.digester.XMLIntrospectorHelper;
- import org.apache.commons.betwixt.expression.Context;
- import org.apache.commons.betwixt.expression.MethodUpdater;
- import org.apache.commons.betwixt.expression.Updater;
- import org.apache.commons.digester.Rule;
- import org.apache.commons.digester.Rules;
- import org.apache.commons.logging.Log;
- import org.apache.commons.logging.LogFactory;
- import org.xml.sax.Attributes;
-
- /** <p><code>BeanCreateRule</code> is a Digester Rule for creating beans
- * from the betwixt XML metadata.</p>
- *
- * @author <a href="mailto:jstrachan@apache.org">James Strachan</a>
- * @author <a href="mailto:martin@mvdb.net">Martin van den Bemt</a>
- * @deprecated 0.5 this Rule does not allowed good integration with other Rules -
- * use {@link BeanRuleSet} instead.
- */
- public class BeanCreateRule extends Rule {
-
- /** Logger */
- private static Log log = LogFactory.getLog( BeanCreateRule.class );
-
- /**
- * Set log to be used by <code>BeanCreateRule</code> instances
- * @param aLog the <code>Log</code> implementation for this class to log to
- */
- public static void setLog(Log aLog) {
- log = aLog;
- }
-
- /** The descriptor of this element */
- private ElementDescriptor descriptor;
- /** The Context used when evaluating Updaters */
- private Context context;
- /** Have we added our child rules to the digester? */
- private boolean addedChildren;
- /** In this begin-end loop did we actually create a new bean */
- private boolean createdBean;
- /** The type of the bean to create */
- private Class beanClass;
- /** The prefix added to digester rules */
- private String pathPrefix;
- /** Use id's to match beans? */
- private boolean matchIDs = true;
- /** allows an attribute to be specified to overload the types of beans used */
- private String classNameAttribute = "className";
-
- /**
- * Convenience constructor which uses <code>ID's</code> for matching.
- *
- * @param descriptor the <code>ElementDescriptor</code> describing the element mapped
- * @param beanClass the <code>Class</code> to be created
- * @param pathPrefix the digester style path
- */
- public BeanCreateRule(
- ElementDescriptor descriptor,
- Class beanClass,
- String pathPrefix ) {
- this( descriptor, beanClass, pathPrefix, true );
- }
-
- /**
- * Constructor taking a class.
- *
- * @param descriptor the <code>ElementDescriptor</code> describing the element mapped
- * @param beanClass the <code>Class</code> to be created
- * @param pathPrefix the digester style path
- * @param matchIDs should <code>ID</code>/<code>IDREF</code>'s be used for matching
- */
- public BeanCreateRule(
- ElementDescriptor descriptor,
- Class beanClass,
- String pathPrefix,
- boolean matchIDs ) {
- this(
- descriptor,
- beanClass,
- new Context(),
- pathPrefix,
- matchIDs);
- }
-
- /**
- * Convenience constructor which uses <code>ID's</code> for matching.
- *
- * @param descriptor the <code>ElementDescriptor</code> describing the element mapped
- * @param beanClass the <code>Class</code> to be created
- */
- public BeanCreateRule( ElementDescriptor descriptor, Class beanClass ) {
- this( descriptor, beanClass, true );
- }
-
- /**
- * Constructor uses standard qualified name.
- *
- * @param descriptor the <code>ElementDescriptor</code> describing the element mapped
- * @param beanClass the <code>Class</code> to be created
- * @param matchIDs should <code>ID</code>/<code>IDREF</code>'s be used for matching
- */
- public BeanCreateRule( ElementDescriptor descriptor, Class beanClass, boolean matchIDs ) {
- this( descriptor, beanClass, descriptor.getQualifiedName() + "/" , matchIDs );
- }
-
- /**
- * Convenience constructor which uses <code>ID's</code> for match.
- *
- * @param descriptor the <code>ElementDescriptor</code> describing the element mapped
- * @param context the <code>Context</code> to be used to evaluate expressions
- * @param pathPrefix the digester path prefix
- */
- public BeanCreateRule(
- ElementDescriptor descriptor,
- Context context,
- String pathPrefix ) {
- this( descriptor, context, pathPrefix, true );
- }
-
- /**
- * Constructor taking a context.
- *
- * @param descriptor the <code>ElementDescriptor</code> describing the element mapped
- * @param context the <code>Context</code> to be used to evaluate expressions
- * @param pathPrefix the digester path prefix
- * @param matchIDs should <code>ID</code>/<code>IDREF</code>'s be used for matching
- */
- public BeanCreateRule(
- ElementDescriptor descriptor,
- Context context,
- String pathPrefix,
- boolean matchIDs ) {
- this(
- descriptor,
- descriptor.getSingularPropertyType(),
- context,
- pathPrefix,
- matchIDs );
- }
-
- /**
- * Base constructor (used by other constructors).
- *
- * @param descriptor the <code>ElementDescriptor</code> describing the element mapped
- * @param beanClass the <code>Class</code> of the bean to be created
- * @param context the <code>Context</code> to be used to evaluate expressions
- * @param pathPrefix the digester path prefix
- * @param matchIDs should <code>ID</code>/<code>IDREF</code>'s be used for matching
- */
- private BeanCreateRule(
- ElementDescriptor descriptor,
- Class beanClass,
- Context context,
- String pathPrefix,
- boolean matchIDs ) {
- this.descriptor = descriptor;
- this.context = context;
- this.beanClass = beanClass;
- this.pathPrefix = pathPrefix;
- this.matchIDs = matchIDs;
- if (log.isTraceEnabled()) {
- log.trace("Created bean create rule");
- log.trace("Descriptor=" + descriptor);
- log.trace("Class=" + beanClass);
- log.trace("Path prefix=" + pathPrefix);
- }
- }
-
-
-
- // Rule interface
- //-------------------------------------------------------------------------
-
- /**
- * Process the beginning of this element.
- *
- * @param attributes The attribute list of this element
- */
- public void begin(Attributes attributes) {
- log.debug( "Called with descriptor: " + descriptor
- + " propertyType: " + descriptor.getPropertyType() );
-
- if (log.isTraceEnabled()) {
- int attributesLength = attributes.getLength();
- if (attributesLength > 0) {
- log.trace("Attributes:");
- }
- for (int i=0, size=attributesLength; i<size; i++) {
- log.trace("Local:" + attributes.getLocalName(i));
- log.trace("URI:" + attributes.getURI(i));
- log.trace("QName:" + attributes.getQName(i));
- }
- }
-
-
-
- // XXX: if a single rule instance gets reused and nesting occurs
- // XXX: we should probably use a stack of booleans to test if we created a bean
- // XXX: or let digester take nulls, which would be easier for us ;-)
- createdBean = false;
-
- Object instance = null;
- if ( beanClass != null ) {
- instance = createBean(attributes);
- if ( instance != null ) {
- createdBean = true;
-
- context.setBean( instance );
- digester.push(instance);
-
-
- // if we are a reference to a type we should lookup the original
- // as this ElementDescriptor will be 'hollow' and have no child attributes/elements.
- // XXX: this should probably be done by the NodeDescriptors...
- ElementDescriptor typeDescriptor = getElementDescriptor( descriptor );
- //ElementDescriptor typeDescriptor = descriptor;
-
- // iterate through all attributes
- AttributeDescriptor[] attributeDescriptors
- = typeDescriptor.getAttributeDescriptors();
- if ( attributeDescriptors != null ) {
- for ( int i = 0, size = attributeDescriptors.length; i < size; i++ ) {
- AttributeDescriptor attributeDescriptor = attributeDescriptors[i];
-
- // The following isn't really the right way to find the attribute
- // but it's quite robust.
- // The idea is that you try both namespace and local name first
- // and if this returns null try the qName.
- String value = attributes.getValue(
- attributeDescriptor.getURI(),
- attributeDescriptor.getLocalName()
- );
-
- if (value == null) {
- value = attributes.getValue(attributeDescriptor.getQualifiedName());
- }
-
- if (log.isTraceEnabled()) {
- log.trace("Attr URL:" + attributeDescriptor.getURI());
- log.trace("Attr LocalName:" + attributeDescriptor.getLocalName() );
- log.trace(value);
- }
-
- Updater updater = attributeDescriptor.getUpdater();
- log.trace(updater);
- if ( updater != null && value != null ) {
- updater.update( context, value );
- }
- }
- }
-
- addChildRules();
-
- // add bean for ID matching
- if ( matchIDs ) {
- // XXX need to support custom ID attribute names
- // XXX i have a feeling that the current mechanism might need to change
- // XXX so i'm leaving this till later
- String id = attributes.getValue( "id" );
- if ( id != null ) {
- getBeansById().put( id, instance );
- }
- }
- }
- }
- }
-
- /**
- * Process the end of this element.
- */
- public void end() {
- if ( createdBean ) {
-
- // force any setters of the parent bean to be called for this new bean instance
- Updater updater = descriptor.getUpdater();
- Object instance = context.getBean();
-
- Object top = digester.pop();
- if (digester.getCount() == 0) {
- context.setBean(null);
- }else{
- context.setBean( digester.peek() );
- }
-
- if ( updater != null ) {
- if ( log.isDebugEnabled() ) {
- log.debug( "Calling updater for: " + descriptor + " with: "
- + instance + " on bean: " + context.getBean() );
- }
- updater.update( context, instance );
- } else {
- if ( log.isDebugEnabled() ) {
- log.debug( "No updater for: " + descriptor + " with: "
- + instance + " on bean: " + context.getBean() );
- }
- }
- }
- }
-
- /**
- * Tidy up.
- */
- public void finish() {}
-
-
- // Properties
- //-------------------------------------------------------------------------
-
-
- /**
- * The name of the attribute which can be specified in the XML to override the
- * type of a bean used at a certain point in the schema.
- *
- * <p>The default value is 'className'.</p>
- *
- * @return The name of the attribute used to overload the class name of a bean
- */
- public String getClassNameAttribute() {
- return classNameAttribute;
- }
-
- /**
- * Sets the name of the attribute which can be specified in
- * the XML to override the type of a bean used at a certain
- * point in the schema.
- *
- * <p>The default value is 'className'.</p>
- *
- * @param classNameAttribute The name of the attribute used to overload the class name of a bean
- */
- public void setClassNameAttribute(String classNameAttribute) {
- this.classNameAttribute = classNameAttribute;
- }
-
- // Implementation methods
- //-------------------------------------------------------------------------
-
- /**
- * Factory method to create new bean instances
- *
- * @param attributes the <code>Attributes</code> used to match <code>ID/IDREF</code>
- * @return the created bean
- */
- protected Object createBean(Attributes attributes) {
- //
- // See if we've got an IDREF
- //
- // XXX This should be customizable but i'm not really convinced by the existing system
- // XXX maybe it's going to have to change so i'll use 'idref' for nows
- //
- if ( matchIDs ) {
- String idref = attributes.getValue( "idref" );
- if ( idref != null ) {
- // XXX need to check up about ordering
- // XXX this is a very simple system that assumes that id occurs before idrefs
- // XXX would need some thought about how to implement a fuller system
- log.trace( "Found IDREF" );
- Object bean = getBeansById().get( idref );
- if ( bean != null ) {
- if (log.isTraceEnabled()) {
- log.trace( "Matched bean " + bean );
- }
- return bean;
- }
- log.trace( "No match found" );
- }
- }
-
- Class theClass = beanClass;
- try {
-
- String className = attributes.getValue(classNameAttribute);
- if (className != null) {
- // load the class we should instantiate
- theClass = getDigester().getClassLoader().loadClass(className);
- }
- if (log.isTraceEnabled()) {
- log.trace( "Creating instance of " + theClass );
- }
- return theClass.newInstance();
-
- } catch (Exception e) {
- log.warn( "Could not create instance of type: " + theClass.getName() );
- return null;
- }
- }
-
- /** Adds the rules to the digester for all child elements */
- protected void addChildRules() {
- if ( ! addedChildren ) {
- addedChildren = true;
-
- addChildRules( pathPrefix, descriptor );
- }
- }
-
- /**
- * Add child rules for given descriptor at given prefix
- *
- * @param prefix add child rules at this (digester) path prefix
- * @param currentDescriptor add child rules for this descriptor
- */
- protected void addChildRules(String prefix, ElementDescriptor currentDescriptor ) {
-
- if (log.isTraceEnabled()) {
- log.trace("Adding child rules for " + currentDescriptor + "@" + prefix);
- }
-
- // if we are a reference to a type we should lookup the original
- // as this ElementDescriptor will be 'hollow' and have no child attributes/elements.
- // XXX: this should probably be done by the NodeDescriptors...
- ElementDescriptor typeDescriptor = getElementDescriptor( currentDescriptor );
- //ElementDescriptor typeDescriptor = descriptor;
-
-
- ElementDescriptor[] childDescriptors = typeDescriptor.getElementDescriptors();
- if ( childDescriptors != null ) {
- for ( int i = 0, size = childDescriptors.length; i < size; i++ ) {
- final ElementDescriptor childDescriptor = childDescriptors[i];
- if (log.isTraceEnabled()) {
- log.trace("Processing child " + childDescriptor);
- }
-
- String qualifiedName = childDescriptor.getQualifiedName();
- if ( qualifiedName == null ) {
- log.trace( "Ignoring" );
- continue;
- }
- String path = prefix + qualifiedName;
- // this code is for making sure that recursive elements
- // can also be used..
-
- if ( qualifiedName.equals( currentDescriptor.getQualifiedName() )
- && currentDescriptor.getPropertyName() != null ) {
- log.trace("Creating generic rule for recursive elements");
- int index = -1;
- if (childDescriptor.isWrapCollectionsInElement()) {
- index = prefix.indexOf(qualifiedName);
- if (index == -1) {
- // shouldn't happen..
- log.debug( "Oops - this shouldn't happen" );
- continue;
- }
- int removeSlash = prefix.endsWith("/")?1:0;
- path = "*/" + prefix.substring(index, prefix.length()-removeSlash);
- }else{
- // we have a element/element type of thing..
- ElementDescriptor[] desc = currentDescriptor.getElementDescriptors();
- if (desc.length == 1) {
- path = "*/"+desc[0].getQualifiedName();
- }
- }
- Rule rule = new BeanCreateRule( childDescriptor, context, path, matchIDs);
- addRule(path, rule);
- continue;
- }
- if ( childDescriptor.getUpdater() != null ) {
- if (log.isTraceEnabled()) {
- log.trace("Element has updater "
- + ((MethodUpdater) childDescriptor.getUpdater()).getMethod().getName());
- }
- if ( childDescriptor.isPrimitiveType() ) {
- addPrimitiveTypeRule(path, childDescriptor);
-
- } else {
- // add the first child to the path
- ElementDescriptor[] grandChildren = childDescriptor.getElementDescriptors();
- if ( grandChildren != null && grandChildren.length > 0 ) {
- ElementDescriptor grandChild = grandChildren[0];
- String grandChildQName = grandChild.getQualifiedName();
- if ( grandChildQName != null && grandChildQName.length() > 0 ) {
- if (childDescriptor.isWrapCollectionsInElement()) {
- path += '/' + grandChildQName;
-
- } else {
- path = prefix + (prefix.endsWith("/")?"":"/") + grandChildQName;
- }
- }
- }
-
- // maybe we are adding a primitve type to a collection/array
- Class beanClass = childDescriptor.getSingularPropertyType();
- if ( XMLIntrospectorHelper.isPrimitiveType( beanClass ) ) {
- addPrimitiveTypeRule(path, childDescriptor);
-
- } else {
- Rule rule = new BeanCreateRule(
- childDescriptor,
- context,
- path + '/',
- matchIDs );
- addRule( path, rule );
- }
- }
- } else {
- log.trace("Element does not have updater");
- }
-
- ElementDescriptor[] grandChildren = childDescriptor.getElementDescriptors();
- if ( grandChildren != null && grandChildren.length > 0 ) {
- log.trace("Adding grand children");
- addChildRules( path + '/', childDescriptor );
- }
- }
- }
- }
-
- /**
- * Get the associated bean reader.
- *
- * @return the <code>BeanReader</code digesting the xml
- */
- protected BeanReader getBeanReader() {
- // XXX this breaks the rule contact
- // XXX maybe the reader should be passed in the constructor
- return (BeanReader) getDigester();
- }
-
- /** Allows the navigation from a reference to a property object to the descriptor defining what
- * the property is. i.e. doing the join from a reference to a type to lookup its descriptor.
- * This could be done automatically by the NodeDescriptors. Refer to TODO.txt for more info.
- *
- * @param propertyDescriptor find descriptor for property object referenced by this descriptor
- * @return descriptor for the singular property class type referenced.
- */
- protected ElementDescriptor getElementDescriptor( ElementDescriptor propertyDescriptor ) {
- Class beanClass = propertyDescriptor.getSingularPropertyType();
- if ( beanClass != null ) {
- XMLIntrospector introspector = getBeanReader().getXMLIntrospector();
- try {
- XMLBeanInfo xmlInfo = introspector.introspect( beanClass );
- return xmlInfo.getElementDescriptor();
-
- } catch (Exception e) {
- log.warn( "Could not introspect class: " + beanClass, e );
- }
- }
- // could not find a better descriptor so use the one we've got
- return propertyDescriptor;
- }
-
- /**
- * Adds a new Digester rule to process the text as a primitive type
- *
- * @param path digester path where this rule will be attached
- * @param childDescriptor update this <code>ElementDescriptor</code> with the body text
- */
- protected void addPrimitiveTypeRule(String path, final ElementDescriptor childDescriptor) {
- Rule rule = new Rule() {
- public void body(String text) throws Exception {
- childDescriptor.getUpdater().update( context, text );
- }
- };
- addRule( path, rule );
- }
-
- /**
- * Safely add a rule with given path.
- *
- * @param path the digester path to add rule at
- * @param rule the <code>Rule</code> to add
- */
- protected void addRule(String path, Rule rule) {
- Rules rules = digester.getRules();
- List matches = rules.match(null, path);
- if ( matches.isEmpty() ) {
- if ( log.isDebugEnabled() ) {
- log.debug( "Adding digester rule for path: " + path + " rule: " + rule );
- }
- digester.addRule( path, rule );
-
- } else {
- if ( log.isDebugEnabled() ) {
- log.debug( "Ignoring duplicate digester rule for path: "
- + path + " rule: " + rule );
- log.debug( "New rule (not added): " + rule );
- log.debug( "Existing rule:" + matches.get(0) );
- }
- }
- }
-
- /**
- * Get the map used to index beans (previously read in) by id.
- * This is stored in the evaluation context.
- *
- * @return map indexing beans created by id
- */
- protected Map getBeansById() {
- //
- // we need a single index for beans read in by id
- // so that we can use them for idref-matching
- // store this in the context
- //
- Map beansById = (Map) context.getVariable( "beans-index" );
- if ( beansById == null ) {
- // lazy creation
- beansById = new HashMap();
- context.setVariable( "beans-index", beansById );
- log.trace( "Created new index-by-id map" );
- }
-
- return beansById;
- }
-
- /**
- * Return something meaningful for logging.
- *
- * @return something useful for logging
- */
- public String toString() {
- return "BeanCreateRule [path prefix=" + pathPrefix + " descriptor=" + descriptor + "]";
- }
-
- }