diff --git a/TODO b/TODO index 2e1a1cf..0ecc9eb 100644 --- a/TODO +++ b/TODO @@ -3,7 +3,6 @@ To Do: * Registry tests * Registry doc * General usage documentation - * Uncouple component-provided names from the library * Automatically-updated singletons * Document exceptions diff --git a/src/main/java/info/ebenoit/ebul/cmp/ComponentState.java b/src/main/java/info/ebenoit/ebul/cmp/ComponentState.java index f5083c7..f2d9104 100644 --- a/src/main/java/info/ebenoit/ebul/cmp/ComponentState.java +++ b/src/main/java/info/ebenoit/ebul/cmp/ComponentState.java @@ -9,6 +9,7 @@ import java.util.Set; import info.ebenoit.ebul.func.FunctionException; import info.ebenoit.ebul.func.ThrowingConsumer; +import info.ebenoit.ebul.func.ThrowingFunction; @@ -85,9 +86,15 @@ public final class ComponentState } this.component = component; + @SuppressWarnings( "unchecked" ) + ThrowingFunction< Object , String > np = (ThrowingFunction< Object , String >) ci.getNameProvider( ); String name; - if ( component instanceof ParametricComponent ) { - name = ( (ParametricComponent) component ).getComponentName( ); + if ( np != null ) { + try { + name = np.apply( component ); + } catch ( final FunctionException e ) { + throw new ComponentCreationException( e.getCause( ) ); + } } else { name = ci.getName( ); if ( "".equals( name ) ) { diff --git a/src/main/java/info/ebenoit/ebul/cmp/NameProvider.java b/src/main/java/info/ebenoit/ebul/cmp/NameProvider.java new file mode 100644 index 0000000..398e703 --- /dev/null +++ b/src/main/java/info/ebenoit/ebul/cmp/NameProvider.java @@ -0,0 +1,25 @@ +package info.ebenoit.ebul.cmp; + + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + + + +/** + * This annotation indicates that a component's name is determined by the method it annotates. + * + *

+ * This annotation must be used on a method that returns a String and has no parameters. This annotation must be unique + * for a whole class hierarchy. + * + * @author E. Benoît + */ +@Retention( RetentionPolicy.RUNTIME ) +@Target( ElementType.METHOD ) +public @interface NameProvider +{ + // EMPTY +} diff --git a/src/main/java/info/ebenoit/ebul/cmp/NewComponentInfo.java b/src/main/java/info/ebenoit/ebul/cmp/NewComponentInfo.java index 4bc6a9a..2ea5ebe 100644 --- a/src/main/java/info/ebenoit/ebul/cmp/NewComponentInfo.java +++ b/src/main/java/info/ebenoit/ebul/cmp/NewComponentInfo.java @@ -14,6 +14,7 @@ import java.util.HashSet; import info.ebenoit.ebul.func.ThrowingBiConsumer; import info.ebenoit.ebul.func.ThrowingConsumer; +import info.ebenoit.ebul.func.ThrowingFunction; import info.ebenoit.ebul.func.ThrowingSupplier; import info.ebenoit.ebul.reflection.Annotations; import info.ebenoit.ebul.reflection.Classes; @@ -32,6 +33,8 @@ public final class NewComponentInfo< T > { /** Member finder that looks for the default constructor */ private static final MemberFinder< Constructor< ? > > CONSTRUCTOR_FINDER; + /** Member finder that looks for name-providing methods */ + private static final MemberFinder< Method > NAME_METHOD_FINDER; /** Member finder that looks for lifecycle methods */ private static final MemberFinder< Method > LC_METHOD_FINDER; /** Member finder that looks for dependency injections using fields */ @@ -45,6 +48,10 @@ public final class NewComponentInfo< T > NewComponentInfo.CONSTRUCTOR_FINDER .setMemberFilter( ( final Constructor< ? > c ) -> c.getParameterCount( ) == 0 ); + NAME_METHOD_FINDER = new MemberFinder< >( MemberFinder.METHOD_EXTRACTOR , true ); + NewComponentInfo.NAME_METHOD_FINDER + .setMemberFilter( m -> m.getDeclaredAnnotation( NameProvider.class ) != null ); + LC_METHOD_FINDER = new MemberFinder< >( MemberFinder.METHOD_EXTRACTOR , true ); NewComponentInfo.LC_METHOD_FINDER.setClassesFirst( true ); NewComponentInfo.LC_METHOD_FINDER @@ -61,7 +68,7 @@ public final class NewComponentInfo< T > /** * Scans a package for classes that correspond to annotated components and processes them using * {@link #fromClass(Class)} - * + * * @param name * the name of the package to scan * @param recursive @@ -112,22 +119,27 @@ public final class NewComponentInfo< T > // Sets the new component's name final Component aComp = klass.getAnnotation( Component.class ); final boolean isAnon = klass.isAnnotationPresent( Anonymous.class ); + final Method nameProvider = NewComponentInfo.findNameProvider( klass ); + if ( isAnon ) { - if ( ParametricComponent.class.isAssignableFrom( klass ) ) { - throw new ComponentDefinitionException( "parametric component can't be anonymous" ); + if ( nameProvider != null ) { + throw new ComponentDefinitionException( "component with name provider can't be anonymous" ); } if ( aComp != null && !"".equals( aComp.value( ) ) ) { throw new ComponentDefinitionException( "named component can't be anonymous" ); } info.setName( null ); } else if ( aComp != null ) { - if ( !"".equals( aComp.value( ) ) && ParametricComponent.class.isAssignableFrom( klass ) ) { - throw new ComponentDefinitionException( "parametric component can't be named" ); + if ( !"".equals( aComp.value( ) ) && nameProvider != null ) { + throw new ComponentDefinitionException( "component with name provider can't be named explicitely" ); } info.setName( aComp.value( ) ); } else { info.setName( "" ); } + if ( nameProvider != null ) { + info.setNameProvider( o -> (String) nameProvider.invoke( o ) ); + } // Autostart flag info.setAutostart( klass.isAnnotationPresent( Autostart.class ) ); @@ -181,6 +193,42 @@ public final class NewComponentInfo< T > } + /** + * Finds a method annotated with {@link NameProvider} in the class hierarchy + * + * @param klass + * the class to examine + * @return the name provider, or null if there is no name provider + * @throws ComponentDefinitionException + * if there are multiple methods annotated with {@link NameProvider}, if the method is static, if it + * doesn't return a string or if it has arguments + */ + private static < T > Method findNameProvider( final Class< T > klass ) + throws ComponentDefinitionException + { + final ArrayList< Method > nameProviders = new ArrayList< >( ); + NewComponentInfo.NAME_METHOD_FINDER.find( nameProviders , klass ); + if ( nameProviders.size( ) > 1 ) { + throw new ComponentDefinitionException( "multiple name providers in hierarchy" ); + } + if ( nameProviders.size( ) == 0 ) { + return null; + } + + final Method nameProvider = nameProviders.get( 0 ); + if ( ( nameProvider.getModifiers( ) & Modifier.STATIC ) != 0 ) { + throw new ComponentDefinitionException( "name provider cannot be static" ); + } + if ( nameProvider.getReturnType( ) != String.class ) { + throw new ComponentDefinitionException( "name provider must return a string" ); + } + if ( nameProvider.getParameterCount( ) != 0 ) { + throw new ComponentDefinitionException( "name provider must have no arguments" ); + } + return nameProvider; + } + + /** * Finds lifecycle methods in all classes and interfaces a component class extends / implements and adds the * corresponding actions to the information record. @@ -337,6 +385,9 @@ public final class NewComponentInfo< T > /** The component's name, or null if it must be guessed */ private String name; + /** A method that can be used to determine a component's name */ + private ThrowingFunction< ? super T , String > nameProvider; + /** * The name of a component this new component acts as a driver for, or null if this component is not a * driver. @@ -374,7 +425,7 @@ public final class NewComponentInfo< T > * Initialises a component based on a component supplier * * @param supplier - * the supplier to obtain the component from + * the supplier to use in order to obtain the component's instance */ public NewComponentInfo( final ThrowingSupplier< T > supplier ) { @@ -429,6 +480,30 @@ public final class NewComponentInfo< T > } + /** + * @return the name provider, if there is one + */ + public ThrowingFunction< ? super T , String > getNameProvider( ) + { + return this.nameProvider; + } + + + /** + * Sets the name provider function. + * + * @param nameProvider + * the name provider function + * @return the current object + */ + public NewComponentInfo< T > setNameProvider( final ThrowingFunction< ? super T , String > nameProvider ) + { + this.nameProvider = nameProvider; + this.name = ""; + return this; + } + + /** * @return the name of the component for which the current component acts as a driver for */ diff --git a/src/main/java/info/ebenoit/ebul/cmp/ParametricComponent.java b/src/main/java/info/ebenoit/ebul/cmp/ParametricComponent.java deleted file mode 100644 index ec433ab..0000000 --- a/src/main/java/info/ebenoit/ebul/cmp/ParametricComponent.java +++ /dev/null @@ -1,20 +0,0 @@ -package info.ebenoit.ebul.cmp; - - -/** - * This interface must be implemented by components which are loaded dynamically depending on configuration (for example - * VFS drivers). Note that such components need not be annotated with {@link Component}, and that they never start - * automatically. - * - * @author E. Benoît - */ -public interface ParametricComponent -{ - /** - * Determine the name of the current instance of this component. - * - * @return the name of the component - */ - public String getComponentName( ); - -} diff --git a/src/test/java/info/ebenoit/ebul/cmp/TestComponentState.java b/src/test/java/info/ebenoit/ebul/cmp/TestComponentState.java index 24b7878..3e91065 100644 --- a/src/test/java/info/ebenoit/ebul/cmp/TestComponentState.java +++ b/src/test/java/info/ebenoit/ebul/cmp/TestComponentState.java @@ -45,9 +45,8 @@ public class TestComponentState } } - /** Class used to test parametric component handling */ + /** Class used to test name provider handling */ private static class PCmpTest - implements ParametricComponent { private final String componentName; @@ -59,7 +58,6 @@ public class TestComponentState } - @Override public String getComponentName( ) { return this.componentName; @@ -215,23 +213,26 @@ public class TestComponentState } - /** Test: initialising a {@link ComponentState} with {@link ParametricComponent} */ + /** Test: initialising a {@link ComponentState} with a name provider */ @Test public void testInitialiseWithParametricComponent( ) { - final Object object = new PCmpTest( "ParametricName" ); - final NewComponentInfo< Object > ci = new NewComponentInfo< Object >( object ); + final PCmpTest object = new PCmpTest( "ParametricName" ); + final NewComponentInfo< PCmpTest > ci = new NewComponentInfo< PCmpTest >( object ) // + .setNameProvider( o -> o.getComponentName( ) ); final ComponentState cs = new ComponentState( this.reg , ci ); Assert.assertEquals( "ParametricName" , cs.getName( ) ); } - /** Test: initialising a {@link ComponentState} with {@link ParametricComponent} overrides any configured name */ + /** Test: initialising a {@link ComponentState} with a name provider overrides any configured name */ @Test public void testInitialiseWithParametricComponentAndName( ) { - final Object object = new PCmpTest( "ParametricName" ); - final NewComponentInfo< Object > ci = new NewComponentInfo< Object >( object ).setName( "Test" ); + final PCmpTest object = new PCmpTest( "ParametricName" ); + final NewComponentInfo< PCmpTest > ci = new NewComponentInfo< PCmpTest >( object ) // + .setNameProvider( o -> o.getComponentName( ) ) // + .setName( "Test" ); final ComponentState cs = new ComponentState( this.reg , ci ); Assert.assertEquals( "ParametricName" , cs.getName( ) ); } @@ -268,10 +269,10 @@ public class TestComponentState @Test public void testToStringAnon( ) { - final Object object = new PCmpTest( null ); + final Object object = new Object( ); final NewComponentInfo< Object > ci = new NewComponentInfo< Object >( object ); final ComponentState cs = new ComponentState( this.reg , ci ); - Assert.assertEquals( "anonymous component of type " + PCmpTest.class.getCanonicalName( ) , cs.toString( ) ); + Assert.assertEquals( "anonymous component of type " + Object.class.getCanonicalName( ) , cs.toString( ) ); } diff --git a/src/test/java/info/ebenoit/ebul/cmp/TestNewComponentInfo.java b/src/test/java/info/ebenoit/ebul/cmp/TestNewComponentInfo.java index f21da50..9691cc9 100644 --- a/src/test/java/info/ebenoit/ebul/cmp/TestNewComponentInfo.java +++ b/src/test/java/info/ebenoit/ebul/cmp/TestNewComponentInfo.java @@ -85,13 +85,12 @@ public class TestNewComponentInfo // EMPTY } - /** Test anonymous component that implements {@link ParametricComponent} (invalid!) */ + /** Test anonymous component that has a name provider (invalid!) */ @Anonymous private static class TestCmpAnonPN - implements ParametricComponent { - @Override + @NameProvider public String getComponentName( ) { return null; @@ -103,23 +102,15 @@ public class TestNewComponentInfo @Component( "Fail!" ) @Anonymous private static class TestCmpAnonNamed - implements ParametricComponent { - - @Override - public String getComponentName( ) - { - return null; - } - + // EMPTY } - /** Test component with a parametric name */ + /** Test component with a name provider */ private static class TestCmpPN - implements ParametricComponent { - @Override + @NameProvider public String getComponentName( ) { return "TestCmpPN"; @@ -127,13 +118,12 @@ public class TestNewComponentInfo } - /** Test component with a parametric name and an annotation-specified name */ + /** Test component with a name provider and an annotation-specified name (invalid!) */ @Component( "Fail" ) - private static class TestCmpPNFail - implements ParametricComponent + private static class TestCmpPNFail1 { - @Override + @NameProvider public String getComponentName( ) { return "TestCmpPNFail"; @@ -141,6 +131,61 @@ public class TestNewComponentInfo } + /** Test component with a static name provider (invalid) */ + private static class TestCmpPNFail2 + { + + @NameProvider + static public String getComponentName( ) + { + return "TestCmpPNFail"; + } + + } + + /** Test component with a name provider that doesn't return a string (invalid) */ + private static class TestCmpPNFail3 + { + + @NameProvider + static public Object getComponentName( ) + { + return "TestCmpPNFail"; + } + + } + + /** Test component with a name provider that has arguments (invalid) */ + private static class TestCmpPNFail4 + { + + @NameProvider + static public String getComponentName( String fail ) + { + return "TestCmpPNFail"; + } + + } + + /** Test component with multiple name providers (invalid) */ + private static class TestCmpPNFail5 + { + + @NameProvider + static public String getComponentName( ) + { + return "TestCmpPNFail"; + } + + + @NameProvider + static public String getComponentNameToo( String fail ) + { + return "TestCmpPNFail"; + } + + } + /** Test component that has lifecycle methods */ private static class TestCmp5 { @@ -497,7 +542,7 @@ public class TestNewComponentInfo /** * Test: {@link NewComponentInfo#fromClass(Class)} on a component class with the {@link Anonymous} annotation that - * implements {@link ParametricComponent} + * includes a {@link NameProvider} */ @Test( expected = ComponentDefinitionException.class ) public void testFromClassAnonPN( ) @@ -518,7 +563,7 @@ public class TestNewComponentInfo /** - * Test: {@link NewComponentInfo#fromClass(Class)} on a component class that implements {@link ParametricComponent} + * Test: {@link NewComponentInfo#fromClass(Class)} on a component class that uses a {@link NameProvider} */ @Test public void testFromClassPN1( ) @@ -529,13 +574,57 @@ public class TestNewComponentInfo /** - * Test: {@link NewComponentInfo#fromClass(Class)} on a component class that implements {@link ParametricComponent} - * but also has an annotation-specified name + * Test: {@link NewComponentInfo#fromClass(Class)} on a component class that uses a {@link NameProvider} but also + * has an annotation-specified name */ @Test( expected = ComponentDefinitionException.class ) public void testFromClassPNFail1( ) { - NewComponentInfo.fromClass( TestCmpPNFail.class ); + NewComponentInfo.fromClass( TestCmpPNFail1.class ); + } + + + /** + * Test: {@link NewComponentInfo#fromClass(Class)} on a component class that uses {@link NameProvider} on a static + * method + */ + @Test( expected = ComponentDefinitionException.class ) + public void testFromClassPNFail2( ) + { + NewComponentInfo.fromClass( TestCmpPNFail2.class ); + } + + + /** + * Test: {@link NewComponentInfo#fromClass(Class)} on a component class that uses {@link NameProvider} on a method + * that doesn't return a string + */ + @Test( expected = ComponentDefinitionException.class ) + public void testFromClassPNFail3( ) + { + NewComponentInfo.fromClass( TestCmpPNFail3.class ); + } + + + /** + * Test: {@link NewComponentInfo#fromClass(Class)} on a component class that uses {@link NameProvider} on a method + * that has arguments + */ + @Test( expected = ComponentDefinitionException.class ) + public void testFromClassPNFail4( ) + { + NewComponentInfo.fromClass( TestCmpPNFail4.class ); + } + + + /** + * Test: {@link NewComponentInfo#fromClass(Class)} on a component class that uses {@link NameProvider} on more than + * one method + */ + @Test( expected = ComponentDefinitionException.class ) + public void testFromClassPNFail5( ) + { + NewComponentInfo.fromClass( TestCmpPNFail5.class ); }