/*
* 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.Iterator;
import java.util.Map;
import org.apache.commons.betwixt.AttributeDescriptor;
import org.apache.commons.betwixt.BindingConfiguration;
import org.apache.commons.betwixt.ElementDescriptor;
import org.apache.commons.betwixt.TextDescriptor;
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.betwixt.io.read.ElementMapping;
import org.apache.commons.betwixt.io.read.ReadConfiguration;
import org.apache.commons.betwixt.io.read.ReadContext;
import org.apache.commons.digester.Digester;
import org.apache.commons.digester.Rule;
import org.apache.commons.digester.RuleSet;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.xml.sax.Attributes;
/** <p>Sets <code>Betwixt</code> digestion rules for a bean class.</p>
*
* @author <a href="mailto:rdonkin@apache.org">Robert Burrell Donkin</a>
* @author <a href="mailto:martin@mvdb.net">Martin van den Bemt</a>
* @since 0.5
*/
public class BeanRuleSet implements RuleSet {
/** Logger */
private static Log log = LogFactory.getLog( BeanRuleSet.class );
/**
* Set log to be used by <code>BeanRuleSet</code> instances
* @param aLog the <code>Log</code> implementation for this class to log to
*/
public static void setLog(Log aLog) {
log = aLog;
}
/** Use this to introspect beans */
private XMLIntrospector introspector;
/** The base path under which the rules will be attached */
private String basePath;
/** The element descriptor for the base */
private ElementDescriptor baseElementDescriptor;
/** The bean based */
private Class baseBeanClass;
/** The (empty) base context from which all Contexts
with beans are (directly or indirectly) obtained */
private ReadContext baseContext;
/** allows an attribute to be specified to overload the types of beans used */
private String classNameAttribute = "className";
/**
* Base constructor.
*
* @param introspector the <code>XMLIntrospector</code> used to introspect
* @param basePath specifies the (Digester-style) path under which the rules will be attached
* @param baseElementDescriptor the <code>ElementDescriptor</code> used to create the rules
* @param baseBeanClass the <code>Class</code> whose mapping rules will be created
* @param matchIDs should ID/IDREFs be used to match beans?
* @deprecated 0.5 use constructor which takes a ReadContext instead
*/
public BeanRuleSet(
XMLIntrospector introspector,
String basePath,
ElementDescriptor baseElementDescriptor,
Class baseBeanClass,
boolean matchIDs) {
this.introspector = introspector;
this.basePath = basePath;
this.baseElementDescriptor = baseElementDescriptor;
this.baseBeanClass = baseBeanClass;
BindingConfiguration bindingConfiguration = new BindingConfiguration();
bindingConfiguration.setMapIDs( matchIDs );
baseContext = new ReadContext( log , bindingConfiguration, new ReadConfiguration() );
}
/**
* Base constructor.
*
* @param introspector the <code>XMLIntrospector</code> used to introspect
* @param basePath specifies the (Digester-style) path under which the rules will be attached
* @param baseElementDescriptor the <code>ElementDescriptor</code> used to create the rules
* @param baseBeanClass the <code>Class</code> whose mapping rules will be created
* @param context the root Context that bean carrying Contexts should be obtained from,
* not null
* @deprecated 0.5 use the constructor which takes a ReadContext instead
*/
public BeanRuleSet(
XMLIntrospector introspector,
String basePath,
ElementDescriptor baseElementDescriptor,
Class baseBeanClass,
Context context) {
this.introspector = introspector;
this.basePath = basePath;
this.baseElementDescriptor = baseElementDescriptor;
this.baseBeanClass = baseBeanClass;
this.baseContext = new ReadContext( context, new ReadConfiguration() );
}
/**
* Base constructor.
*
* @param introspector the <code>XMLIntrospector</code> used to introspect
* @param basePath specifies the (Digester-style) path under which the rules will be attached
* @param baseElementDescriptor the <code>ElementDescriptor</code> used to create the rules
* @param baseBeanClass the <code>Class</code> whose mapping rules will be created
* @param baseContext the root Context that bean carrying Contexts should be obtained from,
* not null
*/
public BeanRuleSet(
XMLIntrospector introspector,
String basePath,
ElementDescriptor baseElementDescriptor,
Class baseBeanClass,
ReadContext baseContext) {
this.introspector = introspector;
this.basePath = basePath;
this.baseElementDescriptor = baseElementDescriptor;
this.baseBeanClass = baseBeanClass;
this.baseContext = baseContext;
}
/**
* 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 baseContext.getClassNameAttribute();
}
/**
* 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
* @deprecated 0.5 set the <code>ReadContext</code> property instead
*/
public void setClassNameAttribute(String classNameAttribute) {
baseContext.setClassNameAttribute(classNameAttribute);
}
//-------------------------------- Ruleset implementation
/**
* <p>Return namespace associated with this ruleset</p>
*
* <p><strong>Note</strong> namespaces are not currently supported.</p>
*
* @return null
*/
public String getNamespaceURI() {
return null;
}
/**
* Add rules for bean to given <code>Digester</code>.
*
* @param digester the <code>Digester</code> to which the rules for the bean will be added
*/
public void addRuleInstances(Digester digester) {
if (log.isTraceEnabled()) {
log.trace("Adding rules to:" + digester);
}
ReadingContext readContext = new ReadingContext( digester );
}
/**
* <p>A set of associated rules that maps a bean graph.
* An instance will be created each time {@link #addRuleInstances} is called.</p>
*
* <p>When an instance is constructed, rules are created and added to digester.</p>
*/
private class ReadingContext {
/** The rules in this context indexed by path */
private Map rulesByPath = new HashMap();
/**
* Creates rules for bean and adds them to digester
* @param digester the <code>Digester</code>
* to which the bean mapping rules will be added
*/
ReadingContext(Digester digester) {
ReadContext context = new ReadContext( baseContext );
// if the classloader is not set, set to the digester classloader
if ( context.getClassLoader() == null ) {
context.setClassLoader( digester.getClassLoader() );
}
BeanRule rule
= new BeanRule( basePath + "/" , baseElementDescriptor, baseBeanClass, context );
addRule( basePath, rule , baseElementDescriptor, context );
if ( log.isDebugEnabled() ) {
log.debug( "Added root rule to path: " + basePath + " class: " + baseBeanClass );
}
Iterator it = rulesByPath.entrySet().iterator();
while (it.hasNext()) {
Map.Entry entry = (Map.Entry) it.next();
if ( log.isTraceEnabled() ) {
log.trace("Added rule:" + entry.getValue() + " @path:" + entry.getKey());
}
digester.addRule( (String) entry.getKey() , (Rule) entry.getValue() );
}
}
/**
* 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
* @param context the <code>Context</code> against which beans will be evaluated
*/
private void addChildRules(
String prefix,
ElementDescriptor currentDescriptor,
ReadContext context ) {
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);
if (log.isTraceEnabled()) {
log.trace("Added wrapped rule for " + childDescriptor);
}
} else {
// we have a element/element type of thing..
ElementDescriptor[] desc = currentDescriptor.getElementDescriptors();
if (desc.length == 1) {
path = "*/"+desc[0].getQualifiedName();
}
if (log.isTraceEnabled()) {
log.trace("Added not wrapped rule for " + childDescriptor);
}
}
addRule( path, childDescriptor, context );
continue;
}
if ( childDescriptor.getUpdater() != null ) {
if (
log.isTraceEnabled()
&& childDescriptor.getUpdater() instanceof MethodUpdater) {
log.trace("Element has updater "
+ ((MethodUpdater) childDescriptor.getUpdater()).getMethod().getName());
}
if ( childDescriptor.isPrimitiveType() ) {
addPrimitiveTypeRule( path, childDescriptor, context );
} 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;
if (log.isTraceEnabled()) {
log.trace(
"Descriptor wraps elements in collection, path:"
+ path);
}
} else {
path = prefix
+ (prefix.endsWith("/")?"":"/") + grandChildQName;
if (log.isTraceEnabled()) {
log.trace(
"Descriptor does not wrap elements in collection, path:"
+ path);
}
}
}
}
// maybe we are adding a primitve type to a collection/array
Class beanClass = childDescriptor.getSingularPropertyType();
if ( XMLIntrospectorHelper.isPrimitiveType( beanClass ) ) {
addPrimitiveTypeRule( path, childDescriptor, context );
} else {
addRule( path, childDescriptor, context );
}
}
} else {
if ( log.isTraceEnabled() ) {
log.trace("Element does not have updater: " + childDescriptor);
}
if (childDescriptor.hasAttributes()) {
if ( log.isTraceEnabled() ) {
log.trace( "Element has attributes, so adding rule anyway : "
+ childDescriptor );
}
addRule(path,childDescriptor, context);
}
}
ElementDescriptor[] grandChildren = childDescriptor.getElementDescriptors();
if ( grandChildren != null && grandChildren.length > 0 ) {
if ( log.isTraceEnabled() ) {
log.trace("Adding grand children @path:" + path);
}
addChildRules( path + '/', childDescriptor, context );
} else if ( log.isTraceEnabled() ) {
log.trace( "No children for " + childDescriptor);
}
}
}
}
/** 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.
*/
ElementDescriptor getElementDescriptor( ElementDescriptor propertyDescriptor ) {
Class beanClass = propertyDescriptor.getSingularPropertyType();
if ( beanClass != null && !Map.class.isAssignableFrom( beanClass ) ) {
if (log.isTraceEnabled()) {
log.trace("Filling descriptor for: " + beanClass);
}
try {
XMLBeanInfo xmlInfo = introspector.introspect( beanClass );
if (log.isTraceEnabled()) {
log.trace("Is wrapped? "
+ xmlInfo.getElementDescriptor().isWrapCollectionsInElement());
}
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
* @param context the <code>ReadContext</code> against which the elements will be evaluated
*/
void addPrimitiveTypeRule(
String path,
final ElementDescriptor childDescriptor,
final ReadContext context) {
Rule rule = new Rule() {
public void body(String text) throws Exception {
childDescriptor.getUpdater().update( context, text );
}
};
add( path, rule );
}
/**
* Adds a new Digester rule to process the text as a primitive type
*
* @param path digester path where this rule will be attached
* @param elementDescriptor update this <code>ElementDescriptor</code> with the body text
* @param context the <code>ReadContext</code> against which the elements will be evaluated
*/
private void addRule(
String path,
ElementDescriptor elementDescriptor,
ReadContext context ) {
BeanRule rule = new BeanRule( path + '/', elementDescriptor, context );
addRule( path, rule, elementDescriptor, context );
}
/**
* Safely add a rule with given path.
*
* @param path the digester path to add rule at
* @param rule the <code>Rule</code> to add
* @param elementDescriptor the <code>ElementDescriptor</code>
* associated with this rule
* @param context the <code>ReadContext</code> against which the elements
* will be evaluated
*/
private void addRule(
String path,
Rule rule,
ElementDescriptor elementDescriptor,
ReadContext context) {
if ( add( path, rule ) ) {
// stop infinite recursion by allowing only one rule per path
addChildRules( path + '/', elementDescriptor, context );
}
}
/**
* Add a rule at given path.
*
* @param path add rule at this path
* @param rule the <code>Rule</code> to add
*
* @return true if this rule was successfully add at given path
*/
private boolean add( String path, Rule rule ) {
// only one bean rule allowed per path
if ( ! rulesByPath.containsKey( path ) ) {
if ( log.isDebugEnabled() ) {
log.debug( "Added rule for path: " + path + " rule: " + rule );
if (log.isTraceEnabled()) {
log.trace( rulesByPath );
}
}
rulesByPath.put( path, rule );
return true;
} 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:" + rulesByPath.get(path) );
}
}
return false;
}
/**
* Rule that creates bean and updates methods.
*/
private class BeanRule extends Rule {
/** The descriptor of this element */
private ElementDescriptor descriptor;
/** The Context used when evaluating Updaters */
private ReadContext context;
/** 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;
/**
* Constructor uses standard qualified name from <code>ElementDescriptor</code>.
*
* @param descriptor the <code>ElementDescriptor</code> describing the element mapped
* @param beanClass the <code>Class</code> to be created
* @param context the <code>ReadContext</code> for this rule
*/
public BeanRule( ElementDescriptor descriptor, Class beanClass, ReadContext context ) {
this( descriptor.getQualifiedName() + "/", descriptor, beanClass, context );
}
/**
* Construct a rule based on singular property type of <code>ElementDescriptor</code>.
*
* @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 BeanRule(
String pathPrefix,
ElementDescriptor descriptor,
ReadContext context ) {
this(
pathPrefix,
descriptor,
descriptor.getSingularPropertyType(),
context );
}
/**
* 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
*/
private BeanRule(
String pathPrefix,
ElementDescriptor descriptor,
Class beanClass,
ReadContext context ) {
this.descriptor = descriptor;
this.context = context;
this.beanClass = beanClass;
this.pathPrefix = pathPrefix;
if (log.isTraceEnabled()) {
log.trace("Created bean create rule");
log.trace("Descriptor=" + descriptor);
log.trace("Class=" + beanClass);
log.trace("Path prefix=" + pathPrefix);
}
}
// Rule interface
//-------------------------------------------------------------------------
/**
* @see Rule#begin(String, String, Attributes)
*/
public void begin(String namespace, String name, Attributes attributes) {
if ( log.isDebugEnabled() ) {
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 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 );
if ( typeDescriptor.getUpdater() == null && beanClass == null ) {
// we try to get the instance from the context.
instance = context.getBean();
if ( instance == null ) {
return;
}
} else {
instance = createBean( namespace, name, attributes );
if ( instance != null ) {
createdBean = true;
context.setBean( instance );
digester.push( instance );
} else {
// we don't do anything if the instance is null.
return;
}
}
// 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();
if ( log.isTraceEnabled() ) {
log.trace("Updater : "+updater);
}
if ( updater != null && value != null ) {
updater.update( context, value );
}
}
}
if ( log.isTraceEnabled() ) {
log.trace("Created bean " + instance);
log.trace("Path prefix: " + pathPrefix);
}
// add bean for ID matching
if ( context.getMapIDs() ) {
// 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 ) {
context.putBean( id, instance );
}
}
}
/**
* @see Rule#body(String, String, String)
*/
public void body(String namespace, String name, String text) {
if ( log.isTraceEnabled() ) {
log.trace("Body with text " + text);
}
if ( digester.getCount() > 0 ) {
Context bodyContext = context.newContext( digester.peek() );
// Take the first content descriptor
ElementDescriptor typeDescriptor = getElementDescriptor( descriptor );
TextDescriptor descriptor = typeDescriptor.getPrimaryBodyTextDescriptor();
if ( descriptor != null ) {
if ( log.isTraceEnabled() ) {
log.trace("Setting mixed content for:");
log.trace(descriptor);
}
Updater updater = descriptor.getUpdater();
if ( log.isTraceEnabled() ) {
log.trace( "Updating mixed content with:" );
log.trace( updater );
}
if ( updater != null && text != null ) {
updater.update( bodyContext, text );
}
}
}
}
/**
* 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 (log.isTraceEnabled()) {
log.trace("Popped " + top);
}
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() {
//
// Clear indexed beans so that we're ready to process next document
//
baseContext.clearBeans();
}
// Implementation methods
//-------------------------------------------------------------------------
/**
* Factory method to create new bean instances
*
* @param namespace the namespace for the element
* @param name the local name
* @param attributes the <code>Attributes</code> used to match <code>ID/IDREF</code>
* @return the created bean
*/
protected Object createBean( String namespace, String name, Attributes attributes ) {
// todo: recycle element mappings
ElementMapping mapping = new ElementMapping();
mapping.setType( beanClass );
mapping.setNamespace( namespace );
mapping.setName( name );
mapping.setAttributes( attributes );
mapping.setDescriptor( descriptor );
Object newInstance = context.getBeanCreationChain().create( mapping, context );
return newInstance;
}
/**
* Return something meaningful for logging.
*
* @return something useful for logging
*/
public String toString() {
return "BeanRule [path prefix=" + pathPrefix + " descriptor=" + descriptor + "]";
}
}
}
}