package org.apache.commons.betwixt;
/*
* 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.
*/
import java.beans.BeanDescriptor;
import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Method;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.apache.commons.beanutils.DynaBean;
import org.apache.commons.beanutils.DynaClass;
import org.apache.commons.beanutils.DynaProperty;
import org.apache.commons.betwixt.digester.XMLBeanInfoDigester;
import org.apache.commons.betwixt.digester.XMLIntrospectorHelper;
import org.apache.commons.betwixt.expression.EmptyExpression;
import org.apache.commons.betwixt.expression.IteratorExpression;
import org.apache.commons.betwixt.expression.MapEntryAdder;
import org.apache.commons.betwixt.expression.MethodUpdater;
import org.apache.commons.betwixt.expression.StringExpression;
import org.apache.commons.betwixt.registry.DefaultXMLBeanInfoRegistry;
import org.apache.commons.betwixt.registry.XMLBeanInfoRegistry;
import org.apache.commons.betwixt.strategy.ClassNormalizer;
import org.apache.commons.betwixt.strategy.DefaultNameMapper;
import org.apache.commons.betwixt.strategy.DefaultPluralStemmer;
import org.apache.commons.betwixt.strategy.NameMapper;
import org.apache.commons.betwixt.strategy.PluralStemmer;
import org.apache.commons.betwixt.strategy.TypeBindingStrategy;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
* <p><code>XMLIntrospector</code> an introspector of beans to create a
* XMLBeanInfo instance.</p>
*
* <p>By default, <code>XMLBeanInfo</code> caching is switched on.
* This means that the first time that a request is made for a <code>XMLBeanInfo</code>
* for a particular class, the <code>XMLBeanInfo</code> is cached.
* Later requests for the same class will return the cached value.</p>
*
* <p>Note :</p>
* <p>This class makes use of the <code>java.bean.Introspector</code>
* class, which contains a BeanInfoSearchPath. To make sure betwixt can
* do his work correctly, this searchpath is completely ignored during
* processing. The original values will be restored after processing finished
* </p>
*
* @author <a href="mailto:jstrachan@apache.org">James Strachan</a>
* @author <a href="mailto:martin@mvdb.net">Martin van den Bemt</a>
*/
public class XMLIntrospector {
/**
* Log used for logging (Doh!)
* @deprecated 0.6 use the {@link #getLog()} property instead
*/
protected Log log = LogFactory.getLog( XMLIntrospector.class );
/** Maps classes to <code>XMLBeanInfo</code>'s */
private XMLBeanInfoRegistry registry = new DefaultXMLBeanInfoRegistry();
/** Digester used to parse the XML descriptor files */
private XMLBeanInfoDigester digester;
/** Configuration to be used for introspection*/
private IntrospectionConfiguration configuration;
/** Base constructor */
public XMLIntrospector() {
this(new IntrospectionConfiguration());
}
/**
* Construct allows a custom configuration to be set on construction.
* This allows <code>IntrospectionConfiguration</code> subclasses
* to be easily used.
* @param configuration IntrospectionConfiguration, not null
*/
public XMLIntrospector(IntrospectionConfiguration configuration) {
setConfiguration(configuration);
}
// Properties
//-------------------------------------------------------------------------
/**
* <p>Gets the current logging implementation. </p>
* @return the Log implementation which this class logs to
*/
public Log getLog() {
return getConfiguration().getIntrospectionLog();
}
/**
* <p>Sets the current logging implementation.</p>
* @param log the Log implementation to use for logging
*/
public void setLog(Log log) {
getConfiguration().setIntrospectionLog(log);
}
/**
* <p>Gets the current registry implementation.
* The registry is checked to see if it has an <code>XMLBeanInfo</code> for a class
* before introspecting.
* After standard introspection is complete, the instance will be passed to the registry.</p>
*
* <p>This allows finely grained control over the caching strategy.
* It also allows the standard introspection mechanism
* to be overridden on a per class basis.</p>
*
* @return the XMLBeanInfoRegistry currently used
*/
public XMLBeanInfoRegistry getRegistry() {
return registry;
}
/**
* <p>Sets the <code>XMLBeanInfoRegistry</code> implementation.
* The registry is checked to see if it has an <code>XMLBeanInfo</code> for a class
* before introspecting.
* After standard introspection is complete, the instance will be passed to the registry.</p>
*
* <p>This allows finely grained control over the caching strategy.
* It also allows the standard introspection mechanism
* to be overridden on a per class basis.</p>
*
* @param registry the XMLBeanInfoRegistry to use
*/
public void setRegistry(XMLBeanInfoRegistry registry) {
this.registry = registry;
}
/**
* Gets the configuration to be used for introspection.
* The various introspection-time strategies
* and configuration variables have been consolidated as properties
* of this bean.
* This allows the configuration to be more easily shared.
* @return IntrospectionConfiguration, not null
*/
public IntrospectionConfiguration getConfiguration() {
return configuration;
}
/**
* Sets the configuration to be used for introspection.
* The various introspection-time strategies
* and configuration variables have been consolidated as properties
* of this bean.
* This allows the configuration to be more easily shared.
* @param configuration IntrospectionConfiguration, not null
*/
public void setConfiguration(IntrospectionConfiguration configuration) {
this.configuration = configuration;
}
/**
* Gets the <code>ClassNormalizer</code> strategy.
* This is used to determine the Class to be introspected
* (the normalized Class).
*
* @return the <code>ClassNormalizer</code> used to determine the Class to be introspected
* for a given Object.
* @deprecated 0.6 use getConfiguration().getClassNormalizer
* @since 0.5
*/
public ClassNormalizer getClassNormalizer() {
return getConfiguration().getClassNormalizer();
}
/**
* Sets the <code>ClassNormalizer</code> strategy.
* This is used to determine the Class to be introspected
* (the normalized Class).
*
* @param classNormalizer the <code>ClassNormalizer</code> to be used to determine
* the Class to be introspected for a given Object.
* @deprecated 0.6 use getConfiguration().setClassNormalizer
* @since 0.5
*
*/
public void setClassNormalizer(ClassNormalizer classNormalizer) {
getConfiguration().setClassNormalizer(classNormalizer);
}
/**
* Is <code>XMLBeanInfo</code> caching enabled?
*
* @deprecated 0.5 replaced by XMlBeanInfoRegistry
* @return true if caching is enabled
*/
public boolean isCachingEnabled() {
return true;
}
/**
* Set whether <code>XMLBeanInfo</code> caching should be enabled.
*
* @deprecated 0.5 replaced by XMlBeanInfoRegistry
* @param cachingEnabled ignored
*/
public void setCachingEnabled(boolean cachingEnabled) {
//
}
/**
* Should attributes (or elements) be used for primitive types.
* @return true if primitive types will be mapped to attributes in the introspection
* @deprecated 0.6 use getConfiguration().isAttributesForPrimitives
*/
public boolean isAttributesForPrimitives() {
return getConfiguration().isAttributesForPrimitives();
}
/**
* Set whether attributes (or elements) should be used for primitive types.
* @param attributesForPrimitives pass trus to map primitives to attributes,
* pass false to map primitives to elements
* @deprecated 0.6 use getConfiguration().setAttributesForPrimitives
*/
public void setAttributesForPrimitives(boolean attributesForPrimitives) {
getConfiguration().setAttributesForPrimitives(attributesForPrimitives);
}
/**
* Should collections be wrapped in an extra element?
*
* @return whether we should we wrap collections in an extra element?
* @deprecated 0.6 use getConfiguration().isWrapCollectionsInElement
*/
public boolean isWrapCollectionsInElement() {
return getConfiguration().isWrapCollectionsInElement();
}
/**
* Sets whether we should we wrap collections in an extra element.
*
* @param wrapCollectionsInElement pass true if collections should be wrapped in a
* parent element
* @deprecated 0.6 use getConfiguration().setWrapCollectionsInElement
*/
public void setWrapCollectionsInElement(boolean wrapCollectionsInElement) {
getConfiguration().setWrapCollectionsInElement(wrapCollectionsInElement);
}
/**
* Get singular and plural matching strategy.
*
* @return the strategy used to detect matching singular and plural properties
* @deprecated 0.6 use getConfiguration().getPluralStemmer
*/
public PluralStemmer getPluralStemmer() {
return getConfiguration().getPluralStemmer();
}
/**
* Sets the strategy used to detect matching singular and plural properties
*
* @param pluralStemmer the PluralStemmer used to match singular and plural
* @deprecated 0.6 use getConfiguration().setPluralStemmer
*/
public void setPluralStemmer(PluralStemmer pluralStemmer) {
getConfiguration().setPluralStemmer(pluralStemmer);
}
/**
* Gets the name mapper strategy.
*
* @return the strategy used to convert bean type names into element names
* @deprecated 0.5 getNameMapper is split up in
* {@link #getElementNameMapper()} and {@link #getAttributeNameMapper()}
*/
public NameMapper getNameMapper() {
return getElementNameMapper();
}
/**
* Sets the strategy used to convert bean type names into element names
* @param nameMapper the NameMapper strategy to be used
* @deprecated 0.5 setNameMapper is split up in
* {@link #setElementNameMapper(NameMapper)} and {@link #setAttributeNameMapper(NameMapper)}
*/
public void setNameMapper(NameMapper nameMapper) {
setElementNameMapper(nameMapper);
}
/**
* Gets the name mapping strategy used to convert bean names into elements.
*
* @return the strategy used to convert bean type names into element
* names. If no element mapper is currently defined then a default one is created.
* @deprecated 0.6 use getConfiguration().getElementNameMapper
*/
public NameMapper getElementNameMapper() {
return getConfiguration().getElementNameMapper();
}
/**
* Sets the strategy used to convert bean type names into element names
* @param nameMapper the NameMapper to use for the conversion
* @deprecated 0.6 use getConfiguration().setElementNameMapper
*/
public void setElementNameMapper(NameMapper nameMapper) {
getConfiguration().setElementNameMapper( nameMapper );
}
/**
* Gets the name mapping strategy used to convert bean names into attributes.
*
* @return the strategy used to convert bean type names into attribute
* names. If no attributeNamemapper is known, it will default to the ElementNameMapper
* @deprecated 0.6 getConfiguration().getAttributeNameMapper
*/
public NameMapper getAttributeNameMapper() {
return getConfiguration().getAttributeNameMapper();
}
/**
* Sets the strategy used to convert bean type names into attribute names
* @param nameMapper the NameMapper to use for the convertion
* @deprecated 0.6 use getConfiguration().setAttributeNameMapper
*/
public void setAttributeNameMapper(NameMapper nameMapper) {
getConfiguration().setAttributeNameMapper( nameMapper );
}
/**
* Should the original <code>java.reflect.Introspector</code> bean info search path be used?
* By default it will be false.
*
* @return boolean if the beanInfoSearchPath should be used.
* @deprecated 0.6 use getConfiguration().useBeanInfoSearchPath
*/
public boolean useBeanInfoSearchPath() {
return getConfiguration().useBeanInfoSearchPath();
}
/**
* Specifies if you want to use the beanInfoSearchPath
* @see java.beans.Introspector for more details
* @param useBeanInfoSearchPath
* @deprecated 0.6 use getConfiguration().setUseBeanInfoSearchPath
*/
public void setUseBeanInfoSearchPath(boolean useBeanInfoSearchPath) {
getConfiguration().setUseBeanInfoSearchPath( useBeanInfoSearchPath );
}
// Methods
//-------------------------------------------------------------------------
/**
* Flush existing cached <code>XMLBeanInfo</code>'s.
*
* @deprecated 0.5 use flushable registry instead
*/
public void flushCache() {}
/** Create a standard <code>XMLBeanInfo</code> by introspection
* The actual introspection depends only on the <code>BeanInfo</code>
* associated with the bean.
*
* @param bean introspect this bean
* @return XMLBeanInfo describing bean-xml mapping
* @throws IntrospectionException when the bean introspection fails
*/
public XMLBeanInfo introspect(Object bean) throws IntrospectionException {
if (getLog().isDebugEnabled()) {
getLog().debug( "Introspecting..." );
getLog().debug(bean);
}
if ( bean instanceof DynaBean ) {
// allow DynaBean implementations to be overridden by .betwixt files
XMLBeanInfo xmlBeanInfo = findByXMLDescriptor( bean.getClass() );
if (xmlBeanInfo != null) {
return xmlBeanInfo;
}
// this is DynaBean use the DynaClass for introspection
return introspect( ((DynaBean) bean).getDynaClass() );
} else {
// normal bean so normal introspection
Class normalClass = getClassNormalizer().getNormalizedClass( bean );
return introspect( normalClass );
}
}
/**
* Creates XMLBeanInfo by reading the DynaProperties of a DynaBean.
* Customizing DynaBeans using betwixt is not supported.
*
* @param dynaClass the DynaBean to introspect
*
* @return XMLBeanInfo for the DynaClass
*/
public XMLBeanInfo introspect(DynaClass dynaClass) {
// for now this method does not do much, since XMLBeanInfoRegistry cannot
// use a DynaClass as a key
// TODO: add caching for DynaClass XMLBeanInfo
// need to work out if this is possible
// this line allows subclasses to change creation strategy
XMLBeanInfo xmlInfo = createXMLBeanInfo( dynaClass );
// populate the created info with
DynaClassBeanType beanClass = new DynaClassBeanType( dynaClass );
populate( xmlInfo, beanClass );
return xmlInfo;
}
/** Create a standard <code>XMLBeanInfo</code> by introspection.
* The actual introspection depends only on the <code>BeanInfo</code>
* associated with the bean.
*
* @param aClass introspect this class
* @return XMLBeanInfo describing bean-xml mapping
* @throws IntrospectionException when the bean introspection fails
*/
public XMLBeanInfo introspect(Class aClass) throws IntrospectionException {
// we first reset the beaninfo searchpath.
String[] searchPath = null;
if ( !getConfiguration().useBeanInfoSearchPath() ) {
searchPath = Introspector.getBeanInfoSearchPath();
Introspector.setBeanInfoSearchPath(new String[] { });
}
XMLBeanInfo xmlInfo = registry.get( aClass );
if ( xmlInfo == null ) {
// lets see if we can find an XML descriptor first
if ( getLog().isDebugEnabled() ) {
getLog().debug( "Attempting to lookup an XML descriptor for class: " + aClass );
}
xmlInfo = findByXMLDescriptor( aClass );
if ( xmlInfo == null ) {
BeanInfo info = Introspector.getBeanInfo( aClass );
xmlInfo = introspect( info );
}
if ( xmlInfo != null ) {
registry.put( aClass, xmlInfo );
}
} else {
getLog().trace( "Used cached XMLBeanInfo." );
}
if ( getLog().isTraceEnabled() ) {
getLog().trace( xmlInfo );
}
if ( !getConfiguration().useBeanInfoSearchPath() ) {
// we restore the beaninfo searchpath.
Introspector.setBeanInfoSearchPath( searchPath );
}
return xmlInfo;
}
/** Create a standard <code>XMLBeanInfo</code> by introspection.
* The actual introspection depends only on the <code>BeanInfo</code>
* associated with the bean.
*
* @param beanInfo the BeanInfo the xml-bean mapping is based on
* @return XMLBeanInfo describing bean-xml mapping
* @throws IntrospectionException when the bean introspection fails
*/
public XMLBeanInfo introspect(BeanInfo beanInfo) throws IntrospectionException {
XMLBeanInfo xmlBeanInfo = createXMLBeanInfo( beanInfo );
populate( xmlBeanInfo, new JavaBeanType( beanInfo ) );
return xmlBeanInfo;
}
/**
* Populates the given <code>XMLBeanInfo</code> based on the given type of bean.
*
* @param xmlBeanInfo populate this, not null
* @param bean the type definition for the bean, not null
*/
private void populate(XMLBeanInfo xmlBeanInfo, BeanType bean) {
String name = bean.getBeanName();
ElementDescriptor elementDescriptor = new ElementDescriptor();
elementDescriptor.setLocalName(
getElementNameMapper().mapTypeToElementName( name ) );
elementDescriptor.setPropertyType( bean.getElementType() );
if (getLog().isTraceEnabled()) {
getLog().trace("Populating:" + bean);
}
// add default string value for primitive types
if ( bean.isPrimitiveType() ) {
getLog().trace("Bean is primitive");
elementDescriptor.setTextExpression( StringExpression.getInstance() );
} else if ( bean.isLoopType() ) {
getLog().trace("Bean is loop");
ElementDescriptor loopDescriptor = new ElementDescriptor();
loopDescriptor.setContextExpression(
new IteratorExpression( EmptyExpression.getInstance() )
);
if ( bean.isMapType() ) {
loopDescriptor.setQualifiedName( "entry" );
}
elementDescriptor.setElementDescriptors( new ElementDescriptor[] { loopDescriptor } );
} else {
getLog().trace("Bean is standard type");
List elements = new ArrayList();
List attributes = new ArrayList();
List contents = new ArrayList();
addProperties( bean.getProperties(), elements, attributes, contents );
int size = elements.size();
if ( size > 0 ) {
ElementDescriptor[] descriptors = new ElementDescriptor[size];
elements.toArray( descriptors );
elementDescriptor.setElementDescriptors( descriptors );
}
size = attributes.size();
if ( size > 0 ) {
AttributeDescriptor[] descriptors = new AttributeDescriptor[size];
attributes.toArray( descriptors );
elementDescriptor.setAttributeDescriptors( descriptors );
}
size = contents.size();
if ( size > 0 ) {
if ( size > 0 ) {
Descriptor[] descriptors = new Descriptor[size];
contents.toArray( descriptors );
elementDescriptor.setContentDescriptors( descriptors );
}
}
}
xmlBeanInfo.setElementDescriptor( elementDescriptor );
// default any addProperty() methods
defaultAddMethods( elementDescriptor, bean.getElementType() );
if (getLog().isTraceEnabled()) {
getLog().trace("Populated descriptor:");
getLog().trace(elementDescriptor);
}
}
/**
* Creates XMLBeanInfo for the given DynaClass.
*
* @param dynaClass the class describing a DynaBean
*
* @return XMLBeanInfo that describes the properties of the given
* DynaClass
*/
protected XMLBeanInfo createXMLBeanInfo(DynaClass dynaClass) {
// XXX is the chosen class right?
XMLBeanInfo beanInfo = new XMLBeanInfo(dynaClass.getClass());
return beanInfo;
}
/**
* Create a XML descriptor from a bean one.
* Go through and work out whether it's a loop property, a primitive or a standard.
* The class property is ignored.
*
* @param propertyDescriptor create a <code>NodeDescriptor</code> for this property
* @param useAttributesForPrimitives write primitives as attributes (rather than elements)
* @return a correctly configured <code>NodeDescriptor</code> for the property
* @throws IntrospectionException when bean introspection fails
* @deprecated 0.5 use {@link #createXMLDescriptor}.
*/
public Descriptor createDescriptor(
PropertyDescriptor propertyDescriptor,
boolean useAttributesForPrimitives
) throws IntrospectionException {
return createXMLDescriptor( new BeanProperty( propertyDescriptor ) );
}
/**
* Create a XML descriptor from a bean one.
* Go through and work out whether it's a loop property, a primitive or a standard.
* The class property is ignored.
*
* @param beanProperty the BeanProperty specifying the property
* @return a correctly configured <code>NodeDescriptor</code> for the property
* @since 0.5
*/
public Descriptor createXMLDescriptor( BeanProperty beanProperty ) {
return beanProperty.createXMLDescriptor( configuration );
}
/**
* Add any addPropety(PropertyType) methods as Updaters
* which are often used for 1-N relationships in beans.
* <br>
* The tricky part here is finding which ElementDescriptor corresponds
* to the method. e.g. a property 'items' might have an Element descriptor
* which the method addItem() should match to.
* <br>
* So the algorithm we'll use
* by default is to take the decapitalized name of the property being added
* and find the first ElementDescriptor that matches the property starting with
* the string. This should work for most use cases.
* e.g. addChild() would match the children property.
* <br>
* TODO this probably needs refactoring. It probably belongs in the bean wrapper
* (so that it'll work properly with dyna-beans) and so that the operations can
* be optimized by caching. Multiple hash maps are created and getMethods is
* called multiple times. This is relatively expensive and so it'd be better
* to push into a proper class and cache.
* <br>
* TODO this probably does work properly with DynaBeans: need to push
* implementation into an class and expose it on BeanType.
*
* @param introspector use this <code>XMLIntrospector</code> for introspection
* @param rootDescriptor add defaults to this descriptor
* @param beanClass the <code>Class</code> to which descriptor corresponds
*/
public void defaultAddMethods(
ElementDescriptor rootDescriptor,
Class beanClass ) {
// lets iterate over all methods looking for one of the form
// add*(PropertyType)
if ( beanClass != null ) {
ArrayList singleParameterAdders = new ArrayList();
ArrayList twinParameterAdders = new ArrayList();
Method[] methods = beanClass.getMethods();
for ( int i = 0, size = methods.length; i < size; i++ ) {
Method method = methods[i];
String name = method.getName();
if ( name.startsWith( "add" )) {
// TODO: should we filter out non-void returning methods?
// some beans will return something as a helper
Class[] types = method.getParameterTypes();
if ( types != null) {
if ( getLog().isTraceEnabled() ) {
getLog().trace("Searching for match for " + method);
}
switch (types.length)
{
case 1:
singleParameterAdders.add(method);
break;
case 2:
twinParameterAdders.add(method);
break;
default:
// ignore
break;
}
}
}
}
Map elementsByPropertyName = makeElementDescriptorMap( rootDescriptor );
for (Iterator it=singleParameterAdders.iterator();it.hasNext();) {
Method singleParameterAdder = (Method) it.next();
setIteratorAdder(elementsByPropertyName, singleParameterAdder);
}
for (Iterator it=twinParameterAdders.iterator();it.hasNext();) {
Method twinParameterAdder = (Method) it.next();
setMapAdder(elementsByPropertyName, twinParameterAdder);
}
}
}
/**
* Sets the adder method where the corresponding property is an iterator
* @param rootDescriptor
* @param singleParameterAdder
*/
private void setIteratorAdder(
Map elementsByPropertyName,
Method singleParameterAdderMethod) {
String adderName = singleParameterAdderMethod.getName();
String propertyName = Introspector.decapitalize(adderName.substring(3));
ElementDescriptor matchingDescriptor = getMatchForAdder(propertyName, elementsByPropertyName);
if (matchingDescriptor != null) {
//TODO defensive code: probably should check descriptor type
Class singularType = singleParameterAdderMethod.getParameterTypes()[0];
if (getLog().isTraceEnabled()) {
getLog().trace(adderName + "->" + propertyName);
}
// this may match a standard collection or iteration
getLog().trace("Matching collection or iteration");
matchingDescriptor.setUpdater( new MethodUpdater( singleParameterAdderMethod ) );
matchingDescriptor.setSingularPropertyType( singularType );
matchingDescriptor.setHollow(!isPrimitiveType(singularType));
String localName = matchingDescriptor.getLocalName();
if ( localName == null || localName.length() == 0 ) {
matchingDescriptor.setLocalName(
getElementNameMapper()
.mapTypeToElementName( propertyName ) );
}
if ( getLog().isDebugEnabled() ) {
getLog().debug( "!! " + singleParameterAdderMethod);
getLog().debug( "!! " + singularType);
}
}
}
/**
* Sets the adder where the corresponding property type is an map
* @param rootDescriptor
* @param singleParameterAdder
*/
private void setMapAdder(
Map elementsByPropertyName,
Method twinParameterAdderMethod) {
String adderName = twinParameterAdderMethod.getName();
String propertyName = Introspector.decapitalize(adderName.substring(3));
ElementDescriptor matchingDescriptor = getMatchForAdder(propertyName, elementsByPropertyName);
if ( matchingDescriptor != null
&& Map.class.isAssignableFrom( matchingDescriptor.getPropertyType() )) {
// this may match a map
getLog().trace("Matching map");
ElementDescriptor[] children
= matchingDescriptor.getElementDescriptors();
// see if the descriptor's been set up properly
if ( children.length == 0 ) {
getLog().info(
"'entry' descriptor is missing for map. "
+ "Updaters cannot be set");
} else {
Class[] types = twinParameterAdderMethod.getParameterTypes();
Class keyType = types[0];
Class valueType = types[1];
// loop through children
// adding updaters for key and value
MapEntryAdder adder = new MapEntryAdder(twinParameterAdderMethod);
for (
int n=0,
noOfGrandChildren = children.length;
n < noOfGrandChildren;
n++ ) {
if ( "key".equals( children[n].getLocalName() ) ) {
children[n].setUpdater( adder.getKeyUpdater() );
children[n].setSingularPropertyType( keyType );
if (children[n].getPropertyType() == null) {
children[n].setPropertyType( valueType );
}
if ( isPrimitiveType(keyType) ) {
children[n].setHollow(false);
}
if ( getLog().isTraceEnabled() ) {
getLog().trace( "Key descriptor: " + children[n]);
}
} else if ( "value".equals( children[n].getLocalName() ) ) {
children[n].setUpdater( adder.getValueUpdater() );
children[n].setSingularPropertyType( valueType );
if (children[n].getPropertyType() == null) {
children[n].setPropertyType( valueType );
}
if ( isPrimitiveType( valueType) ) {
children[n].setHollow(false);
}
if ( isLoopType( valueType )) {
// need to attach a hollow descriptor
// don't know the element name
// so use null name (to match anything)
ElementDescriptor loopDescriptor = new ElementDescriptor();
loopDescriptor.setHollow(true);
loopDescriptor.setSingularPropertyType( valueType );
loopDescriptor.setPropertyType( valueType );
children[n].addElementDescriptor(loopDescriptor);
}
if ( getLog().isTraceEnabled() ) {
getLog().trace( "Value descriptor: " + children[n]);
}
}
}
}
}
}
/**
* Gets an ElementDescriptor for the property matching the adder
* @param adderName
* @param rootDescriptor
* @return
*/
private ElementDescriptor getMatchForAdder(
String propertyName,
Map elementsByPropertyName) {
ElementDescriptor matchingDescriptor = null;
if (propertyName.length() > 0) {
if ( getLog().isTraceEnabled() ) {
getLog().trace( "findPluralDescriptor( " + propertyName
+ " ):root property name=" + propertyName );
}
PluralStemmer stemmer = getPluralStemmer();
matchingDescriptor = stemmer.findPluralDescriptor( propertyName, elementsByPropertyName );
if ( getLog().isTraceEnabled() ) {
getLog().trace(
"findPluralDescriptor( " + propertyName
+ " ):ElementDescriptor=" + matchingDescriptor );
}
}
return matchingDescriptor;
}
// Implementation methods
//-------------------------------------------------------------------------
/**
* Creates a map where the keys are the property names and the values are the ElementDescriptors
*/
private Map makeElementDescriptorMap( ElementDescriptor rootDescriptor ) {
Map result = new HashMap();
String rootPropertyName = rootDescriptor.getPropertyName();
if (rootPropertyName != null) {
result.put(rootPropertyName, rootDescriptor);
}
makeElementDescriptorMap( rootDescriptor, result );
return result;
}
/**
* Creates a map where the keys are the property names and the values are the ElementDescriptors
*
* @param rootDescriptor the values of the maps are the children of this
* <code>ElementDescriptor</code> index by their property names
* @param map the map to which the elements will be added
*/
private void makeElementDescriptorMap( ElementDescriptor rootDescriptor, Map map ) {
ElementDescriptor[] children = rootDescriptor.getElementDescriptors();
if ( children != null ) {
for ( int i = 0, size = children.length; i < size; i++ ) {
ElementDescriptor child = children[i];
String propertyName = child.getPropertyName();
if ( propertyName != null ) {
map.put( propertyName, child );
}
makeElementDescriptorMap( child, map );
}
}
}
/**
* A Factory method to lazily create a new strategy
* to detect matching singular and plural properties.
*
* @return new defualt PluralStemmer implementation
* @deprecated 0.6 this method has been moved into IntrospectionConfiguration.
* Those who need to vary this should subclass that class instead
*/
protected PluralStemmer createPluralStemmer() {
return new DefaultPluralStemmer();
}
/**
* A Factory method to lazily create a strategy
* used to convert bean type names into element names.
*
* @return new default NameMapper implementation
* @deprecated 0.6 this method has been moved into IntrospectionConfiguration.
* Those who need to vary this should subclass that class instead
*/
protected NameMapper createNameMapper() {
return new DefaultNameMapper();
}
/**
* Attempt to lookup the XML descriptor for the given class using the
* classname + ".betwixt" using the same ClassLoader used to load the class
* or return null if it could not be loaded
*
* @param aClass digester .betwixt file for this class
* @return XMLBeanInfo digested from the .betwixt file if one can be found.
* Otherwise null.
*/
protected synchronized XMLBeanInfo findByXMLDescriptor( Class aClass ) {
// trim the package name
String name = aClass.getName();
int idx = name.lastIndexOf( '.' );
if ( idx >= 0 ) {
name = name.substring( idx + 1 );
}
name += ".betwixt";
URL url = aClass.getResource( name );
if ( url != null ) {
try {
String urlText = url.toString();
if ( getLog().isDebugEnabled( )) {
getLog().debug( "Parsing Betwixt XML descriptor: " + urlText );
}
// synchronized method so this digester is only used by
// one thread at once
if ( digester == null ) {
digester = new XMLBeanInfoDigester();
digester.setXMLIntrospector( this );
}
digester.setBeanClass( aClass );
return (XMLBeanInfo) digester.parse( urlText );
} catch (Exception e) {
getLog().warn( "Caught exception trying to parse: " + name, e );
}
}
if ( getLog().isTraceEnabled() ) {
getLog().trace( "Could not find betwixt file " + name );
}
return null;
}
/**
* Loop through properties and process each one
*
* @param beanInfo the BeanInfo whose properties will be processed
* @param elements ElementDescriptor list to which elements will be added
* @param attributes AttributeDescriptor list to which attributes will be added
* @param contents Descriptor list to which mixed content will be added
* @throws IntrospectionException if the bean introspection fails
* @deprecated 0.5 use {@link #addProperties(BeanProperty[], List, List,List)}
*/
protected void addProperties(
BeanInfo beanInfo,
List elements,
List attributes,
List contents)
throws
IntrospectionException {
PropertyDescriptor[] descriptors = beanInfo.getPropertyDescriptors();
if ( descriptors != null ) {
for ( int i = 0, size = descriptors.length; i < size; i++ ) {
addProperty(beanInfo, descriptors[i], elements, attributes, contents);
}
}
if (getLog().isTraceEnabled()) {
getLog().trace(elements);
getLog().trace(attributes);
getLog().trace(contents);
}
}
/**
* Loop through properties and process each one
*
* @param beanProperties the properties to be processed
* @param elements ElementDescriptor list to which elements will be added
* @param attributes AttributeDescriptor list to which attributes will be added
* @param contents Descriptor list to which mixed content will be added
* @since 0.5
*/
protected void addProperties(
BeanProperty[] beanProperties,
List elements,
List attributes,
List contents) {
if ( beanProperties != null ) {
if (getLog().isTraceEnabled()) {
getLog().trace(beanProperties.length + " properties to be added");
}
for ( int i = 0, size = beanProperties.length; i < size; i++ ) {
addProperty(beanProperties[i], elements, attributes, contents);
}
}
if (getLog().isTraceEnabled()) {
getLog().trace("After properties have been added (elements, attributes, contents):");
getLog().trace(elements);
getLog().trace(attributes);
getLog().trace(contents);
}
}
/**
* Process a property.
* Go through and work out whether it's a loop property, a primitive or a standard.
* The class property is ignored.
*
* @param beanInfo the BeanInfo whose property is being processed
* @param propertyDescriptor the PropertyDescriptor to process
* @param elements ElementDescriptor list to which elements will be added
* @param attributes AttributeDescriptor list to which attributes will be added
* @param contents Descriptor list to which mixed content will be added
* @throws IntrospectionException if the bean introspection fails
* @deprecated 0.5 BeanInfo is no longer required.
* Use {@link #addProperty(PropertyDescriptor, List, List, List)} instead.
*/
protected void addProperty(
BeanInfo beanInfo,
PropertyDescriptor propertyDescriptor,
List elements,
List attributes,
List contents)
throws
IntrospectionException {
addProperty( propertyDescriptor, elements, attributes, contents);
}
/**
* Process a property.
* Go through and work out whether it's a loop property, a primitive or a standard.
* The class property is ignored.
*
* @param propertyDescriptor the PropertyDescriptor to process
* @param elements ElementDescriptor list to which elements will be added
* @param attributes AttributeDescriptor list to which attributes will be added
* @param contents Descriptor list to which mixed content will be added
* @throws IntrospectionException if the bean introspection fails
* @deprecated 0.5 use {@link #addProperty(BeanProperty, List, List, List)} instead
*/
protected void addProperty(
PropertyDescriptor propertyDescriptor,
List elements,
List attributes,
List contents)
throws
IntrospectionException {
addProperty(new BeanProperty( propertyDescriptor ), elements, attributes, contents);
}
/**
* Process a property.
* Go through and work out whether it's a loop property, a primitive or a standard.
* The class property is ignored.
*
* @param beanProperty the bean property to process
* @param elements ElementDescriptor list to which elements will be added
* @param attributes AttributeDescriptor list to which attributes will be added
* @param contents Descriptor list to which mixed content will be added
* @since 0.5
*/
protected void addProperty(
BeanProperty beanProperty,
List elements,
List attributes,
List contents) {
Descriptor nodeDescriptor = createXMLDescriptor(beanProperty);
if (nodeDescriptor == null) {
return;
}
if (nodeDescriptor instanceof ElementDescriptor) {
elements.add(nodeDescriptor);
} else if (nodeDescriptor instanceof AttributeDescriptor) {
attributes.add(nodeDescriptor);
} else {
contents.add(nodeDescriptor);
}
}
/**
* Loop through properties and process each one
*
* @param beanInfo the BeanInfo whose properties will be processed
* @param elements ElementDescriptor list to which elements will be added
* @param attributes AttributeDescriptor list to which attributes will be added
* @throws IntrospectionException if the bean introspection fails
* @deprecated 0.5 this method does not support mixed content.
* Use {@link #addProperties(BeanInfo, List, List, List)} instead.
*/
protected void addProperties(
BeanInfo beanInfo,
List elements,
List attributes)
throws
IntrospectionException {
PropertyDescriptor[] descriptors = beanInfo.getPropertyDescriptors();
if ( descriptors != null ) {
for ( int i = 0, size = descriptors.length; i < size; i++ ) {
addProperty(beanInfo, descriptors[i], elements, attributes);
}
}
if (getLog().isTraceEnabled()) {
getLog().trace(elements);
getLog().trace(attributes);
}
}
/**
* Process a property.
* Go through and work out whether it's a loop property, a primitive or a standard.
* The class property is ignored.
*
* @param beanInfo the BeanInfo whose property is being processed
* @param propertyDescriptor the PropertyDescriptor to process
* @param elements ElementDescriptor list to which elements will be added
* @param attributes AttributeDescriptor list to which attributes will be added
* @throws IntrospectionException if the bean introspection fails
* @deprecated 0.5 this method does not support mixed content.
* Use {@link #addProperty(BeanInfo, PropertyDescriptor, List, List, List)} instead.
*/
protected void addProperty(
BeanInfo beanInfo,
PropertyDescriptor propertyDescriptor,
List elements,
List attributes)
throws
IntrospectionException {
NodeDescriptor nodeDescriptor = XMLIntrospectorHelper
.createDescriptor(propertyDescriptor,
isAttributesForPrimitives(),
this);
if (nodeDescriptor == null) {
return;
}
if (nodeDescriptor instanceof ElementDescriptor) {
elements.add(nodeDescriptor);
} else {
attributes.add(nodeDescriptor);
}
}
/**
* Factory method to create XMLBeanInfo instances
*
* @param beanInfo the BeanInfo from which the XMLBeanInfo will be created
* @return XMLBeanInfo describing the bean-xml mapping
*/
protected XMLBeanInfo createXMLBeanInfo( BeanInfo beanInfo ) {
XMLBeanInfo xmlBeanInfo = new XMLBeanInfo( beanInfo.getBeanDescriptor().getBeanClass() );
return xmlBeanInfo;
}
/**
* Is this class a loop?
*
* @param type the Class to test
* @return true if the type is a loop type
*/
public boolean isLoopType(Class type) {
return XMLIntrospectorHelper.isLoopType(type);
}
/**
* Is this class a primitive?
* TODO: this method will probably be deprecated when primitive types
* are subsumed into the simple type concept
* @param type the Class to test
* @return true for primitive types
*/
public boolean isPrimitiveType(Class type) {
TypeBindingStrategy.BindingType bindingType
= configuration.getTypeBindingStrategy().bindingType( type ) ;
boolean result = (bindingType.equals(TypeBindingStrategy.BindingType.PRIMITIVE));
return result;
}
/** Some type of pseudo-bean */
private abstract class BeanType {
/**
* Gets the name for this bean type
* @return the bean type name, not null
*/
public abstract String getBeanName();
/**
* Gets the type to be used by the associated element
* @return a Class that is the type not null
*/
public abstract Class getElementType();
/**
* Is this type a primitive?
* @return true if this type should be treated by betwixt as a primitive
*/
public abstract boolean isPrimitiveType();
/**
* is this type a map?
* @return true this should be treated as a map.
*/
public abstract boolean isMapType();
/**
* Is this type a loop?
* @return true if this should be treated as a loop
*/
public abstract boolean isLoopType();
/**
* Gets the properties associated with this bean.
* @return the BeanProperty's, not null
*/
public abstract BeanProperty[] getProperties();
/**
* Create string representation
* @return something useful for logging
*/
public String toString() {
return "Bean[name=" + getBeanName() + ", type=" + getElementType();
}
}
/** Supports standard Java Beans */
private class JavaBeanType extends BeanType {
/** Introspected bean */
private BeanInfo beanInfo;
/** Bean class */
private Class beanClass;
/** Bean name */
private String name;
/** Bean properties */
private BeanProperty[] properties;
/**
* Constructs a BeanType for a standard Java Bean
* @param beanInfo the BeanInfo describing the standard Java Bean, not null
*/
public JavaBeanType(BeanInfo beanInfo) {
this.beanInfo = beanInfo;
BeanDescriptor beanDescriptor = beanInfo.getBeanDescriptor();
beanClass = beanDescriptor.getBeanClass();
name = beanDescriptor.getName();
// Array's contain a bad character
if (beanClass.isArray()) {
// called all array's Array
name = "Array";
}
}
/** @see BeanType #getElementType */
public Class getElementType() {
return beanClass;
}
/** @see BeanType#getBeanName */
public String getBeanName() {
return name;
}
/** @see BeanType#isPrimitiveType */
public boolean isPrimitiveType() {
return XMLIntrospector.this.isPrimitiveType( beanClass );
}
/** @see BeanType#isLoopType */
public boolean isLoopType() {
return XMLIntrospectorHelper.isLoopType( beanClass );
}
/** @see BeanType#isMapType */
public boolean isMapType() {
return Map.class.isAssignableFrom( beanClass );
}
/** @see BeanType#getProperties */
public BeanProperty[] getProperties() {
// lazy creation
if ( properties == null ) {
ArrayList propertyDescriptors = new ArrayList();
// add base bean info
PropertyDescriptor[] descriptors = beanInfo.getPropertyDescriptors();
if ( descriptors != null ) {
for (int i=0, size=descriptors.length; i<size; i++) {
propertyDescriptors.add( descriptors[i] );
}
}
// add properties from additional bean infos
BeanInfo[] additionals = beanInfo.getAdditionalBeanInfo();
if ( additionals != null ) {
for ( int i=0, outerSize=additionals.length; i<outerSize; i++ ) {
BeanInfo additionalInfo = additionals[i];
descriptors = beanInfo.getPropertyDescriptors();
if ( descriptors != null ) {
for (int j=0, innerSize=descriptors.length; j<innerSize; j++) {
propertyDescriptors.add( descriptors[j] );
}
}
}
}
// what happens when size is zero?
properties = new BeanProperty[ propertyDescriptors.size() ];
int count = 0;
for ( Iterator it = propertyDescriptors.iterator(); it.hasNext(); count++) {
PropertyDescriptor propertyDescriptor = (PropertyDescriptor) it.next();
properties[count] = new BeanProperty( propertyDescriptor );
}
}
return properties;
}
}
/** Implementation for DynaClasses */
private class DynaClassBeanType extends BeanType {
/** BeanType for this DynaClass */
private DynaClass dynaClass;
/** Properties extracted in constuctor */
private BeanProperty[] properties;
/**
* Constructs a BeanType for a DynaClass
* @param dynaClass not null
*/
public DynaClassBeanType(DynaClass dynaClass) {
this.dynaClass = dynaClass;
DynaProperty[] dynaProperties = dynaClass.getDynaProperties();
properties = new BeanProperty[dynaProperties.length];
for (int i=0, size=dynaProperties.length; i<size; i++) {
properties[i] = new BeanProperty(dynaProperties[i]);
}
}
/** @see BeanType#getBeanName */
public String getBeanName() {
return dynaClass.getName();
}
/** @see BeanType#getElementType */
public Class getElementType() {
return DynaClass.class;
}
/** @see BeanType#isPrimitiveType */
public boolean isPrimitiveType() {
return false;
}
/** @see BeanType#isMapType */
public boolean isMapType() {
return false;
}
/** @see BeanType#isLoopType */
public boolean isLoopType() {
return false;
}
/** @see BeanType#getProperties */
public BeanProperty[] getProperties() {
return properties;
}
}
}