diff --git a/proposal/myrmidon/src/java/org/apache/myrmidon/components/configurer/AttributeSetter.java b/proposal/myrmidon/src/java/org/apache/myrmidon/components/configurer/AttributeSetter.java new file mode 100644 index 000000000..af0334785 --- /dev/null +++ b/proposal/myrmidon/src/java/org/apache/myrmidon/components/configurer/AttributeSetter.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) The Apache Software Foundation. All rights reserved. + * + * This software is published under the terms of the Apache Software License + * version 1.1, a copy of which has been included with this distribution in + * the LICENSE.txt file. + */ +package org.apache.myrmidon.components.configurer; + +import org.apache.avalon.framework.configuration.ConfigurationException; + +/** + * Used to set an attribute or text content of an object. + * + * @author Adam Murdoch + * @version $Revision$ $Date$ + */ +public interface AttributeSetter +{ + /** + * Returns the attribute type. + */ + Class getType(); + + /** + * Sets the value of the attribute. + * + * @param object The object to set the attribute of. + * @param value The value of the attribute. Must be assignable to the class + * returned by {@link #getType}. + * @throw ConfigurationException If the value could not be set. + */ + void setAttribute( Object object, Object value ) + throws ConfigurationException; +} diff --git a/proposal/myrmidon/src/java/org/apache/myrmidon/components/configurer/DefaultAttributeSetter.java b/proposal/myrmidon/src/java/org/apache/myrmidon/components/configurer/DefaultAttributeSetter.java new file mode 100644 index 000000000..5a9ec3d61 --- /dev/null +++ b/proposal/myrmidon/src/java/org/apache/myrmidon/components/configurer/DefaultAttributeSetter.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) The Apache Software Foundation. All rights reserved. + * + * This software is published under the terms of the Apache Software License + * version 1.1, a copy of which has been included with this distribution in + * the LICENSE.txt file. + */ +package org.apache.myrmidon.components.configurer; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import org.apache.avalon.excalibur.i18n.ResourceManager; +import org.apache.avalon.excalibur.i18n.Resources; +import org.apache.avalon.framework.configuration.ConfigurationException; + +/** + * A default attribute setter implementation, which uses reflection to + * set the attribute value. + * + * @author Adam Murdoch + * @version $Revision$ $Date$ + */ +class DefaultAttributeSetter + implements AttributeSetter +{ + private final Method m_method; + private final Class m_type; + + private static final Resources REZ = + ResourceManager.getPackageResources( DefaultAttributeSetter.class ); + + public DefaultAttributeSetter( final Method method ) + { + m_method = method; + m_type = method.getParameterTypes()[ 0 ]; + } + + /** + * Returns the attribute type. + */ + public Class getType() + { + return m_type; + } + + /** + * Sets the value of the attribute. + */ + public void setAttribute( final Object object, final Object value ) + throws ConfigurationException + { + try + { + m_method.invoke( object, new Object[]{value} ); + } + catch( final InvocationTargetException ite ) + { + final Throwable cause = ite.getTargetException(); + throw new ConfigurationException( cause.getMessage(), cause ); + } + catch( final Exception e ) + { + throw new ConfigurationException( e.getMessage(), e ); + } + } +} diff --git a/proposal/myrmidon/src/java/org/apache/myrmidon/components/configurer/DefaultConfigurer.java b/proposal/myrmidon/src/java/org/apache/myrmidon/components/configurer/DefaultConfigurer.java index e36888e99..4564418ab 100644 --- a/proposal/myrmidon/src/java/org/apache/myrmidon/components/configurer/DefaultConfigurer.java +++ b/proposal/myrmidon/src/java/org/apache/myrmidon/components/configurer/DefaultConfigurer.java @@ -7,12 +7,10 @@ */ package org.apache.myrmidon.components.configurer; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; import org.apache.avalon.excalibur.i18n.ResourceManager; import org.apache.avalon.excalibur.i18n.Resources; -import org.apache.avalon.excalibur.property.PropertyException; import org.apache.avalon.excalibur.property.PropertyUtil; import org.apache.avalon.framework.component.ComponentException; import org.apache.avalon.framework.component.ComponentManager; @@ -23,7 +21,6 @@ import org.apache.avalon.framework.configuration.ConfigurationException; import org.apache.avalon.framework.context.Context; import org.apache.avalon.framework.logger.AbstractLogEnabled; import org.apache.avalon.framework.logger.LogEnabled; -import org.apache.myrmidon.converter.ConverterException; import org.apache.myrmidon.interfaces.configurer.Configurer; import org.apache.myrmidon.interfaces.converter.MasterConverter; @@ -42,18 +39,13 @@ public class DefaultConfigurer ///Compile time constant to turn on extreme debugging private final static boolean DEBUG = false; - /* - * TODO: Should reserved names be "configurable" ? - */ - ///Element names that are reserved - private final static String[] RESERVED_ELEMENTS = - { - "content" - }; - ///Converter to use for converting between values private MasterConverter m_converter; + ///Cached object configurers. This is a map from Class to the + ///ObjectConfigurer for that class. + private Map m_configurerCache = new HashMap(); + public void compose( final ComponentManager componentManager ) throws ComponentException { @@ -81,7 +73,7 @@ public class DefaultConfigurer if( DEBUG ) { final String message = REZ.getString( "configuring-object.notice", object ); - getLogger().debug( "Configuring " + object ); + getLogger().debug( message ); } if( object instanceof Configurable ) @@ -89,9 +81,10 @@ public class DefaultConfigurer if( DEBUG ) { final String message = REZ.getString( "configurable.notice" ); - getLogger().debug( "Configuring object via Configurable interface" ); + getLogger().debug( message ); } + // Let the object configure itself ( (Configurable)object ).configure( configuration ); } else @@ -102,408 +95,257 @@ public class DefaultConfigurer getLogger().debug( message ); } + // Locate the configurer for this object + final ObjectConfigurer configurer = getConfigurer( object.getClass() ); + + // Set each of the attributes final String[] attributes = configuration.getAttributeNames(); for( int i = 0; i < attributes.length; i++ ) { final String name = attributes[ i ]; final String value = configuration.getAttribute( name ); - if( DEBUG ) - { - final String message = REZ.getString( "configure-attribute.notice", name, value ); - getLogger().debug( message ); - } - - configureAttribute( object, name, value, context ); + // Set the attribute + setAttribute( configurer, object, name, value, context ); } - final Configuration[] children = configuration.getChildren(); - - for( int i = 0; i < children.length; i++ ) + // Set the text content + final String content = configuration.getValue( null ); + if( null != content && content.length() > 0 ) { - final Configuration child = children[ i ]; - - if( DEBUG ) - { - final String message = - REZ.getString( "configure-subelement.notice", child.getName() ); - getLogger().debug( message ); - } - - configureElement( object, child, context ); + setContent( configurer, object, content, context ); } - final String content = configuration.getValue( null ); - if( null != content ) + // Create and configure each of the child elements + final Configuration[] children = configuration.getChildren(); + for( int i = 0; i < children.length; i++ ) { - if( !content.trim().equals( "" ) ) - { - if( DEBUG ) - { - final String message = - REZ.getString( "configure-content.notice", content ); - getLogger().debug( message ); - } - - configureContent( object, content, context ); - } + final Configuration childConfig = children[ i ]; + configureElement( configurer, object, childConfig, context ); } } } /** - * Configure named attribute of object in a particular context. - * This configuring can be done in different ways for different - * configurers. - * - * @param object the object - * @param name the attribute name - * @param value the attribute value - * @param context the Context - * @exception ConfigurationException if an error occurs + * Sets the text content of an object. */ - public void configure( final Object object, - final String name, - final String value, - final Context context ) + private void setContent( final ObjectConfigurer configurer, + final Object object, + final String content, + final Context context ) throws ConfigurationException { - configureAttribute( object, name, value, context ); + if( DEBUG ) + { + final String message = + REZ.getString( "configure-content.notice", content ); + getLogger().debug( message ); + } + + // Set the content + final AttributeSetter setter = configurer.getContentSetter(); + if( null == setter ) + { + final String message = REZ.getString( "content-not-supported.error" ); + throw new ConfigurationException( message ); + } + try + { + setValue( setter, object, content, context ); + } + catch( final Exception e ) + { + final String message = REZ.getString( "bad-set-content.error" ); + throw new ConfigurationException( message, e ); + } } /** - * Try to configure content of an object. - * - * @param object the object - * @param content the content value to be set - * @param context the Context - * @exception ConfigurationException if an error occurs + * Creates and configures a nested element */ - private void configureContent( final Object object, - final String content, + private void configureElement( final ObjectConfigurer configurer, + final Object object, + final Configuration childConfig, final Context context ) throws ConfigurationException { - setValue( object, "addContent", content, context ); - } + final String childName = childConfig.getName(); - private void configureAttribute( final Object object, - final String name, - final String value, - final Context context ) - throws ConfigurationException - { - final String methodName = getMethodNameFor( name ); - setValue( object, methodName, value, context ); - } - - private void setValue( final Object object, - final String methodName, - final String value, - final Context context ) - throws ConfigurationException - { - // OMFG the rest of this is soooooooooooooooooooooooooooooooo - // slow. Need to cache results per class etc. - - final Class clazz = object.getClass(); - final Method[] methods = getMethodsFor( clazz, methodName ); - if( 0 == methods.length ) + if( DEBUG ) { final String message = - REZ.getString( "no-attribute-method.error", methodName ); - throw new ConfigurationException( message ); + REZ.getString( "configure-subelement.notice", childName ); + getLogger().debug( message ); } - setValue( object, value, context, methods ); - } + // Locate the configurer for the child element + final ElementConfigurer childConfigurer = configurer.getElement( childName ); + if( null == childConfigurer ) + { + final String message = REZ.getString( "unknown-subelement.error", childName ); + throw new ConfigurationException( message ); + } - private void setValue( final Object object, - final String value, - final Context context, - final Method methods[] ) - throws ConfigurationException - { try { - final Object objectValue = - PropertyUtil.resolveProperty( value, context, false ); + // Create the child element + final Object child = childConfigurer.createElement( object ); - setValue( object, objectValue, methods, context ); + // Configure the child element + configure( child, childConfig, context ); + + // Set the child element + childConfigurer.addElement( object, child ); } - catch( final PropertyException pe ) + catch( final ConfigurationException ce ) { final String message = - REZ.getString( "bad-property-resolve.error", value ); - throw new ConfigurationException( message, pe ); + REZ.getString( "bad-configure-subelement.error", childName ); + throw new ConfigurationException( message, ce ); } } - private void setValue( final Object object, - Object value, - final Method methods[], + /** + * Configure named attribute of object in a particular context. + * This configuring can be done in different ways for different + * configurers. + * + * @param object the object + * @param name the attribute name + * @param value the attribute value + * @param context the Context + * @exception ConfigurationException if an error occurs + */ + public void configure( final Object object, + final String name, + final String value, final Context context ) throws ConfigurationException { - final Class sourceClass = value.getClass(); - final String source = sourceClass.getName(); + // Locate the configurer for this object + final ObjectConfigurer configurer = getConfigurer( object.getClass() ); - for( int i = 0; i < methods.length; i++ ) - { - if( setValue( object, value, methods[ i ], sourceClass, source, context ) ) - { - return; - } - } - - final String message = - REZ.getString( "no-can-convert.error", methods[ 0 ].getName(), source ); - throw new ConfigurationException( message ); + // Set the attribute value + setAttribute( configurer, object, name, value, context ); } - private boolean setValue( final Object object, - Object value, - final Method method, - final Class sourceClass, - final String source, - final Context context ) + /** + * Sets an attribute value. + */ + private void setAttribute( final ObjectConfigurer configurer, + final Object object, + final String name, + final String value, + final Context context ) throws ConfigurationException { - Class parameterType = method.getParameterTypes()[ 0 ]; - if( parameterType.isPrimitive() ) - { - parameterType = getComplexTypeFor( parameterType ); - } - - try + if( DEBUG ) { - value = m_converter.convert( parameterType, value, context ); + final String message = REZ.getString( "configure-attribute.notice", + name, + value ); + getLogger().debug( message ); } - catch( final ConverterException ce ) - { - if( DEBUG ) - { - final String message = REZ.getString( "no-converter.error" ); - getLogger().debug( message, ce ); - } - throw new ConfigurationException( ce.getMessage(), ce ); - } - catch( final Exception e ) + // Locate the setter for this attribute + final AttributeSetter setter = configurer.getAttributeSetter( name ); + if( null == setter ) { - final String message = - REZ.getString( "bad-convert-for-attribute.error", method.getName() ); - throw new ConfigurationException( message, e ); - } - - if( null == value ) - { - return false; + final String message = REZ.getString( "unknown-attribute.error", name ); + throw new ConfigurationException( message ); } + // Set the value try { - method.invoke( object, new Object[]{value} ); - } - catch( final IllegalAccessException iae ) - { - //should never happen .... - final String message = REZ.getString( "illegal-access.error" ); - throw new ConfigurationException( message, iae ); - } - catch( final InvocationTargetException ite ) - { - final String message = REZ.getString( "invoke-target.error", method.getName() ); - throw new ConfigurationException( message, ite ); + setValue( setter, object, value, context ); } - - return true; - } - - private Class getComplexTypeFor( final Class clazz ) - { - if( String.class == clazz ) - return String.class; - else if( Integer.TYPE.equals( clazz ) ) - return Integer.class; - else if( Long.TYPE.equals( clazz ) ) - return Long.class; - else if( Short.TYPE.equals( clazz ) ) - return Short.class; - else if( Byte.TYPE.equals( clazz ) ) - return Byte.class; - else if( Boolean.TYPE.equals( clazz ) ) - return Boolean.class; - else if( Float.TYPE.equals( clazz ) ) - return Float.class; - else if( Double.TYPE.equals( clazz ) ) - return Double.class; - else + catch( final Exception e ) { - final String message = REZ.getString( "no-complex-type.error", clazz.getName() ); - throw new IllegalArgumentException( message ); + final String message = REZ.getString( "bad-set-attribute.error", name ); + throw new ConfigurationException( message, e ); } } - private Method[] getMethodsFor( final Class clazz, final String methodName ) + /** + * Sets an attribute value, or an element's text content. + */ + private void setValue( final AttributeSetter setter, + final Object object, + final String value, + final Context context ) + throws Exception { - final Method methods[] = clazz.getMethods(); - final ArrayList matches = new ArrayList(); + // Resolve property references in the attribute value + Object objValue = PropertyUtil.resolveProperty( value, context, false ); - for( int i = 0; i < methods.length; i++ ) + // Convert the value to the appropriate type + Class clazz = setter.getType(); + if( clazz.isPrimitive() ) { - final Method method = methods[ i ]; - if( methodName.equals( method.getName() ) && - Method.PUBLIC == ( method.getModifiers() & Method.PUBLIC ) ) - { - if( method.getReturnType().equals( Void.TYPE ) ) - { - final Class parameters[] = method.getParameterTypes(); - if( 1 == parameters.length ) - { - matches.add( method ); - } - } - } + clazz = getComplexTypeFor( clazz ); } - return (Method[])matches.toArray( new Method[ 0 ] ); + objValue = m_converter.convert( clazz, objValue, context ); + + // Set the value + setter.setAttribute( object, objValue ); } - private Method[] getCreateMethodsFor( final Class clazz, final String methodName ) + /** + * Locates the configurer for a particular class. + */ + private ObjectConfigurer getConfigurer( final Class clazz ) + throws ConfigurationException { - final Method methods[] = clazz.getMethods(); - final ArrayList matches = new ArrayList(); - - for( int i = 0; i < methods.length; i++ ) + ObjectConfigurer configurer = + (ObjectConfigurer)m_configurerCache.get( clazz ); + if( null == configurer ) { - final Method method = methods[ i ]; - if( methodName.equals( method.getName() ) && - Method.PUBLIC == ( method.getModifiers() & Method.PUBLIC ) ) - { - final Class returnType = method.getReturnType(); - if( !returnType.equals( Void.TYPE ) && - !returnType.isPrimitive() ) - { - final Class parameters[] = method.getParameterTypes(); - if( 0 == parameters.length ) - { - matches.add( method ); - } - } - } + configurer = DefaultObjectConfigurer.getConfigurer( clazz ); + m_configurerCache.put( clazz, configurer ); } - - return (Method[])matches.toArray( new Method[ 0 ] ); - } - - private String getMethodNameFor( final String attribute ) - { - return "set" + getJavaNameFor( attribute.toLowerCase() ); + return configurer; } - private String getJavaNameFor( final String name ) + private Class getComplexTypeFor( final Class clazz ) { - final StringBuffer sb = new StringBuffer(); - - int index = name.indexOf( '-' ); - int last = 0; - - while( -1 != index ) + if( String.class == clazz ) { - final String word = name.substring( last, index ).toLowerCase(); - sb.append( Character.toUpperCase( word.charAt( 0 ) ) ); - sb.append( word.substring( 1, word.length() ) ); - last = index + 1; - index = name.indexOf( '-', last ); + return String.class; } - - index = name.length(); - final String word = name.substring( last, index ).toLowerCase(); - sb.append( Character.toUpperCase( word.charAt( 0 ) ) ); - sb.append( word.substring( 1, word.length() ) ); - - return sb.toString(); - } - - private void configureElement( final Object object, - final Configuration configuration, - final Context context ) - throws ConfigurationException - { - final String name = configuration.getName(); - final String javaName = getJavaNameFor( name ); - - // OMFG the rest of this is soooooooooooooooooooooooooooooooo - // slow. Need to cache results per class etc. - final Class clazz = object.getClass(); - Method methods[] = getMethodsFor( clazz, "add" + javaName ); - - if( 0 != methods.length ) + else if( Integer.TYPE.equals( clazz ) ) { - //guess it is first method ???? - addElement( object, methods[ 0 ], configuration, context ); + return Integer.class; } - else + else if( Long.TYPE.equals( clazz ) ) { - methods = getCreateMethodsFor( clazz, "create" + javaName ); - - if( 0 == methods.length ) - { - final String message = - REZ.getString( "no-element-method.error", javaName ); - throw new ConfigurationException( message ); - } - - //guess it is first method ???? - createElement( object, methods[ 0 ], configuration, context ); + return Long.class; } - } - - private void createElement( final Object object, - final Method method, - final Configuration configuration, - final Context context ) - throws ConfigurationException - { - try + else if( Short.TYPE.equals( clazz ) ) { - final Object created = method.invoke( object, new Object[ 0 ] ); - configure( created, configuration, context ); + return Short.class; } - catch( final ConfigurationException ce ) + else if( Byte.TYPE.equals( clazz ) ) { - throw ce; + return Byte.class; } - catch( final Exception e ) + else if( Boolean.TYPE.equals( clazz ) ) { - final String message = REZ.getString( "subelement-create.error" ); - throw new ConfigurationException( message, e ); + return Boolean.class; } - } - - private void addElement( final Object object, - final Method method, - final Configuration configuration, - final Context context ) - throws ConfigurationException - { - try + else if( Float.TYPE.equals( clazz ) ) { - final Class clazz = method.getParameterTypes()[ 0 ]; - final Object created = clazz.newInstance(); - - configure( created, configuration, context ); - method.invoke( object, new Object[]{created} ); + return Float.class; } - catch( final ConfigurationException ce ) + else if( Double.TYPE.equals( clazz ) ) { - throw ce; + return Double.class; } - catch( final Exception e ) + else { - final String message = REZ.getString( "subelement-create.error" ); - throw new ConfigurationException( message, e ); + final String message = REZ.getString( "no-complex-type.error", clazz.getName() ); + throw new IllegalArgumentException( message ); } } } diff --git a/proposal/myrmidon/src/java/org/apache/myrmidon/components/configurer/DefaultElementConfigurer.java b/proposal/myrmidon/src/java/org/apache/myrmidon/components/configurer/DefaultElementConfigurer.java new file mode 100644 index 000000000..8e6f0d5f8 --- /dev/null +++ b/proposal/myrmidon/src/java/org/apache/myrmidon/components/configurer/DefaultElementConfigurer.java @@ -0,0 +1,101 @@ +/* + * Copyright (C) The Apache Software Foundation. All rights reserved. + * + * This software is published under the terms of the Apache Software License + * version 1.1, a copy of which has been included with this distribution in + * the LICENSE.txt file. + */ +package org.apache.myrmidon.components.configurer; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import org.apache.avalon.excalibur.i18n.ResourceManager; +import org.apache.avalon.excalibur.i18n.Resources; +import org.apache.avalon.framework.configuration.ConfigurationException; + +/** + * The default element configurer implementation, which uses reflection to + * create and/or add nested elements. + * + * @author Adam Murdoch + * @version $Revision$ $Date$ + */ +class DefaultElementConfigurer + implements ElementConfigurer +{ + private static final Resources REZ = + ResourceManager.getPackageResources( DefaultElementConfigurer.class ); + + private final Class m_type; + private final Method m_createMethod; + private final Method m_addMethod; + + public DefaultElementConfigurer( final Class type, + final Method createMethod, + final Method addMethod ) + { + m_type = type; + m_createMethod = createMethod; + m_addMethod = addMethod; + } + + /** + * Returns the type of the element. + */ + public Class getType() + { + return m_type; + } + + /** + * Creates a nested element. + */ + public Object createElement( final Object parent ) + throws ConfigurationException + { + try + { + if( null != m_createMethod ) + { + return m_createMethod.invoke( parent, null ); + } + else + { + return m_type.newInstance(); + } + } + catch( final InvocationTargetException ite ) + { + final Throwable cause = ite.getTargetException(); + throw new ConfigurationException( cause.getMessage(), cause ); + } + catch( final Exception e ) + { + throw new ConfigurationException( e.getMessage(), e ); + } + } + + /** + * Sets the nested element, after it has been configured. + */ + public void addElement( final Object parent, final Object child ) + throws ConfigurationException + { + try + { + if( null != m_addMethod ) + { + m_addMethod.invoke( parent, new Object[]{child} ); + } + } + catch( final InvocationTargetException ite ) + { + final Throwable cause = ite.getTargetException(); + throw new ConfigurationException( cause.getMessage(), cause ); + } + catch( final Exception e ) + { + throw new ConfigurationException( e.getMessage(), e ); + } + } +} diff --git a/proposal/myrmidon/src/java/org/apache/myrmidon/components/configurer/DefaultObjectConfigurer.java b/proposal/myrmidon/src/java/org/apache/myrmidon/components/configurer/DefaultObjectConfigurer.java new file mode 100644 index 000000000..0172c89a2 --- /dev/null +++ b/proposal/myrmidon/src/java/org/apache/myrmidon/components/configurer/DefaultObjectConfigurer.java @@ -0,0 +1,380 @@ +/* + * Copyright (C) The Apache Software Foundation. All rights reserved. + * + * This software is published under the terms of the Apache Software License + * version 1.1, a copy of which has been included with this distribution in + * the LICENSE.txt file. + */ +package org.apache.myrmidon.components.configurer; + +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.apache.avalon.excalibur.i18n.ResourceManager; +import org.apache.avalon.excalibur.i18n.Resources; +import org.apache.avalon.framework.configuration.ConfigurationException; + +/** + * An object configurer which uses reflection to determine the attributes + * and elements of a class. + * + * @author Adam Murdoch + * @version $Revision$ $Date$ + */ +public class DefaultObjectConfigurer + implements ObjectConfigurer +{ + private static final Resources REZ = + ResourceManager.getPackageResources( DefaultObjectConfigurer.class ); + + private final Class m_class; + + /** + * Map from lowercase attribute name -> AttributeSetter. + */ + private final Map m_attrs = new HashMap(); + + /** + * Map from lowercase element name -> ElementSetter. + */ + private final Map m_elements = new HashMap(); + + /** + * Content setter. + */ + private AttributeSetter m_contentSetter; + + /** + * Creates an object configurer for a particular class. The newly + * created configurer will not handle any attributes, elements, or content. + * Use the various enable methods to enable handling of these. + */ + public DefaultObjectConfigurer( final Class classInfo ) + { + m_class = classInfo; + } + + /** + * Enables all attributes, elements and content handling. + */ + public void enableAll() + throws ConfigurationException + { + enableAttributes(); + enableElements(); + enableContent(); + } + + /** + * Enables all attributes. + */ + public void enableAttributes() + throws ConfigurationException + { + // Find all 'set' methods which take a single parameter, and return void. + final List methods = new ArrayList(); + findMethodsWithPrefix( "set", methods ); + final Iterator iterator = methods.iterator(); + while( iterator.hasNext() ) + { + final Method method = (Method)iterator.next(); + if( method.getReturnType() != Void.TYPE || + method.getParameterTypes().length != 1 ) + { + continue; + } + + // Extract the attribute name + final String attrName = extractName( "set", method.getName() ); + + // Enable the attribute + enableAttribute( attrName, method ); + } + } + + /** + * Enables all elements. + */ + public void enableElements() + throws ConfigurationException + { + final Map creators = findCreators(); + final Map adders = findAdders(); + + // Add the elements + final Set elemNames = new HashSet(); + elemNames.addAll( creators.keySet() ); + elemNames.addAll( adders.keySet() ); + + final Iterator iterator = elemNames.iterator(); + while( iterator.hasNext() ) + { + final String elemName = (String)iterator.next(); + final Method createMethod = (Method)creators.get( elemName ); + final Method addMethod = (Method)adders.get( elemName ); + + // Determine and check the return type + Class type; + if( createMethod != null && addMethod != null ) + { + // Make sure the add method is more general than the create + // method + type = createMethod.getReturnType(); + final Class addType = addMethod.getParameterTypes()[ 0 ]; + if( !addType.isAssignableFrom( type ) ) + { + final String message = + REZ.getString( "incompatible-element-types.error", + elemName, + m_class.getName() ); + throw new ConfigurationException( message ); + } + } + else if( createMethod != null ) + { + type = createMethod.getReturnType(); + } + else + { + type = addMethod.getParameterTypes()[ 0 ]; + } + + final DefaultElementConfigurer configurer = + new DefaultElementConfigurer( type, createMethod, addMethod ); + m_elements.put( elemName, configurer ); + } + } + + /** + * Locate all 'add' methods which return void, and take a non-primitive type + */ + private Map findAdders() + throws ConfigurationException + { + final Map adders = new HashMap(); + final List methodSet = new ArrayList(); + findMethodsWithPrefix( "add", methodSet ); + + final Iterator iterator = methodSet.iterator(); + while( iterator.hasNext() ) + { + final Method method = (Method)iterator.next(); + final String methodName = method.getName(); + if( method.getReturnType() != Void.TYPE || + method.getParameterTypes().length != 1 || + method.getParameterTypes()[ 0 ].isPrimitive() ) + { + continue; + } + + // TODO - un-hard-code this + if( methodName.equals( "addContent" ) ) + { + continue; + } + + // Extract element name + final String elemName = extractName( "add", methodName ); + + // Add to the adders map + if( adders.containsKey( elemName ) ) + { + final String message = + REZ.getString( "multiple-adder-methods-for-element.error", + m_class.getName(), + elemName ); + throw new ConfigurationException( message ); + } + adders.put( elemName, method ); + } + return adders; + } + + /** + * Find all 'create' methods, which return a non-primitive type, + * and take no parameters. + */ + private Map findCreators() + throws ConfigurationException + { + final Map creators = new HashMap(); + final List methodSet = new ArrayList(); + findMethodsWithPrefix( "create", methodSet ); + + final Iterator iterator = methodSet.iterator(); + while( iterator.hasNext() ) + { + final Method method = (Method)iterator.next(); + final String methodName = method.getName(); + if( method.getReturnType().isPrimitive() || + method.getParameterTypes().length != 0 ) + { + continue; + } + + // Extract element name + final String elemName = extractName( "create", methodName ); + + // Add to the creators map + if( creators.containsKey( elemName ) ) + { + final String message = + REZ.getString( "multiple-creator-methods-for-element.error", + m_class.getName(), + elemName ); + throw new ConfigurationException( message ); + } + creators.put( elemName, method ); + } + return creators; + } + + /** + * Enables content. + */ + public void enableContent() + throws ConfigurationException + { + // Locate any 'addContent' methods, which return void, and take + // a single parameter. + final Method[] methods = m_class.getMethods(); + for( int i = 0; i < methods.length; i++ ) + { + final Method method = methods[ i ]; + final String methodName = method.getName(); + if( Modifier.isStatic( method.getModifiers() ) || + !methodName.equals( "addContent" ) || + method.getReturnType() != Void.TYPE || + method.getParameterTypes().length != 1 ) + { + continue; + } + + if( null != m_contentSetter ) + { + final String message = + REZ.getString( "multiple-content-setter-methods.error", m_class.getName() ); + throw new ConfigurationException( message ); + } + + m_contentSetter = new DefaultAttributeSetter( method ); + } + } + + /** + * Locates the configurer for a particular class. + */ + public static ObjectConfigurer getConfigurer( final Class classInfo ) + throws ConfigurationException + { + final DefaultObjectConfigurer configurer = new DefaultObjectConfigurer( classInfo ); + configurer.enableAll(); + return configurer; + } + + /** + * Returns the class. + */ + public Class getType() + { + return m_class; + } + + /** + * Returns a configurer for an attribute of this class. + */ + public AttributeSetter getAttributeSetter( final String name ) + { + return (AttributeSetter)m_attrs.get( name ); + } + + /** + * Returns a configurer for an element of this class. + */ + public ElementConfigurer getElement( final String name ) + { + return (ElementConfigurer)m_elements.get( name ); + } + + /** + * Returns a configurer for the content of this class. + */ + public AttributeSetter getContentSetter() + { + return m_contentSetter; + } + + /** + * Enables an attribute. + */ + private void enableAttribute( final String attrName, + final Method method ) + throws ConfigurationException + { + if( m_attrs.containsKey( attrName ) ) + { + final String message = + REZ.getString( "multiple-setter-methods-for-attribute.error", + m_class.getName(), + attrName ); + throw new ConfigurationException( message ); + } + final DefaultAttributeSetter setter = new DefaultAttributeSetter( method ); + m_attrs.put( attrName, setter ); + } + + /** + * Extracts an attribute/element name from a Java method name. + * Removes the prefix, inserts '-' before each uppercase character + * (except the first), then converts all to lowercase. + */ + private String extractName( final String prefix, final String methodName ) + { + final StringBuffer sb = new StringBuffer( methodName ); + sb.delete( 0, prefix.length() ); + for( int i = 0; i < sb.length(); i++ ) + { + char ch = sb.charAt( i ); + if( Character.isUpperCase( ch ) ) + { + if( i > 0 ) + { + sb.insert( i, '-' ); + i++; + } + sb.setCharAt( i, Character.toLowerCase( ch ) ); + } + } + return sb.toString(); + } + + /** + * Locates all non-static methods whose name starts with a particular + * prefix. + */ + private void findMethodsWithPrefix( final String prefix, final Collection matches ) + { + final int prefixLen = prefix.length(); + final Method[] methods = m_class.getMethods(); + for( int i = 0; i < methods.length; i++ ) + { + final Method method = methods[ i ]; + final String methodName = method.getName(); + if( Modifier.isStatic( method.getModifiers() ) || + methodName.length() <= prefixLen || + !methodName.startsWith( prefix ) ) + { + continue; + } + + matches.add( method ); + } + } +} diff --git a/proposal/myrmidon/src/java/org/apache/myrmidon/components/configurer/ElementConfigurer.java b/proposal/myrmidon/src/java/org/apache/myrmidon/components/configurer/ElementConfigurer.java new file mode 100644 index 000000000..1cac23363 --- /dev/null +++ b/proposal/myrmidon/src/java/org/apache/myrmidon/components/configurer/ElementConfigurer.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) The Apache Software Foundation. All rights reserved. + * + * This software is published under the terms of the Apache Software License + * version 1.1, a copy of which has been included with this distribution in + * the LICENSE.txt file. + */ +package org.apache.myrmidon.components.configurer; + +import org.apache.avalon.framework.configuration.ConfigurationException; + +/** + * Configures an element of an object. + * + * @author Adam Murdoch + * @version $Revision$ $Date$ + */ +public interface ElementConfigurer +{ + /** + * Returns the type of the element. + */ + Class getType(); + + /** + * Creates an object for an element. + * + * @param parent The parent object. + * @return An object which is assignable to the type returned by + * {@link #getType}. + * @throws ConfigurationException If the object cannot be created. + */ + Object createElement( Object parent ) + throws ConfigurationException; + + /** + * Attaches an element object to its parent, after it has been configured. + * + * @param parent The parent object. + * + * @param child The element object. This must be assignable to the type + * returned by {@link #getType}. + * @throws ConfigurationException If the object cannot be attached. + */ + void addElement( Object parent, Object child ) + throws ConfigurationException; +} diff --git a/proposal/myrmidon/src/java/org/apache/myrmidon/components/configurer/ObjectConfigurer.java b/proposal/myrmidon/src/java/org/apache/myrmidon/components/configurer/ObjectConfigurer.java new file mode 100644 index 000000000..945d90a73 --- /dev/null +++ b/proposal/myrmidon/src/java/org/apache/myrmidon/components/configurer/ObjectConfigurer.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) The Apache Software Foundation. All rights reserved. + * + * This software is published under the terms of the Apache Software License + * version 1.1, a copy of which has been included with this distribution in + * the LICENSE.txt file. + */ +package org.apache.myrmidon.components.configurer; + +/** + * Configures objects of a particular class. + * + * @author Adam Murdoch + * @version $Revision$ $Date$ + */ +public interface ObjectConfigurer +{ + /** + * Returns the class. + */ + Class getType(); + + /** + * Returns a configurer for an attribute of this class. + * + * @param name The attribute name. + * @return A configurer for the attribute. Returns null if the attribute + * is not valid for this class. + */ + AttributeSetter getAttributeSetter( String name ); + + /** + * Returns a configurer for an element of this class. + * + * @param name The element name. + * @return A configurer for the element. Returns null if the element + * is not valid for this class. + */ + ElementConfigurer getElement( String name ); + + /** + * Returns a configurer for the content of this class. + * + * @return A configurer for the content. Returns null if the class does + * not allow text content. + */ + AttributeSetter getContentSetter(); +} diff --git a/proposal/myrmidon/src/java/org/apache/myrmidon/components/configurer/Resources.properties b/proposal/myrmidon/src/java/org/apache/myrmidon/components/configurer/Resources.properties index ce049719f..750042a56 100644 --- a/proposal/myrmidon/src/java/org/apache/myrmidon/components/configurer/Resources.properties +++ b/proposal/myrmidon/src/java/org/apache/myrmidon/components/configurer/Resources.properties @@ -1,18 +1,20 @@ configuring-object.notice=Configuring {0}. configurable.notice=Configuring object via Configurable interface. -reflection.notice=Configuring object via Configurable reflection. -configure-attribute.notice=Configuring attribute name={0} value={1}. -configure-subelement.notice=Configuring subelement name={0}. -configure-content.notice=Configuring content {0}. +reflection.notice=Configuring object via ObjectConfigurer. +configure-content.notice=Configuring content with "{0}". +configure-subelement.notice=Configuring subelement "{0}". +configure-attribute.notice=Configuring attribute name="{0}" value="{1}". -reserved-attribute.error=Can not specify reserved attribute {0}. -no-attribute-method.error=Unable to set attribute via {0} due to not finding any appropriate mutator method. -bad-property-resolve.error=Error resolving property {0}. -no-can-convert.error=Unable to set attribute via {0} as could not convert {1} to a matching type. -no-converter.error=Failed to find converter. -bad-convert-for-attribute.error=Error converting attribute for {0}. -no-element-method.error=Unable to set element {0} due to not finding any appropriate mutator method. -illegal-access.error=Error retrieving methods with correct access specifiers. -invoke-target.error=Error calling method attribute {0}. +content-not-supported.error=Text content is not supported for this element. +bad-set-content.error=Could not set text content. +unknown-subelement.error=Unknown element "{0}". +bad-configure-subelement.error=Could not configure element "{0}". +unknown-attribute.error=Unknown attribute "{0}". +bad-set-attribute.error=Could not set attribute "{0}". no-complex-type.error=Can not get complex type for non-primitive type {0}. -subelement-create.error=Error creating sub-element. + +multiple-creator-methods-for-element.error=Multiple creator methods found in class {0} for element "{0}". +multiple-adder-methods-for-element.error=Multiple adder methods found in class {0} for element "{0}". +incompatible-element-types.error=Incompatible creator and adder types for element "{0}" of class {1}. +multiple-content-setter-methods.error=Multiple content setter methods found in class {0}. +multiple-setter-methods-for-attribute.error=Multiple setter methods found in class {0} for attribute "{1}".