Changes for component-provided names

* The component registration record may now include a "name provider" (a
function returning a string based on an instance of the component),
which will be used to determine the component's name.
* NameProvider annotation indicates a method that returns the
component's name
This commit is contained in:
Emmanuel BENOîT 2015-09-17 13:15:41 +02:00
parent c4ff9e4339
commit 5032e44182
7 changed files with 239 additions and 63 deletions

1
TODO
View file

@ -3,7 +3,6 @@ To Do:
* Registry tests * Registry tests
* Registry doc * Registry doc
* General usage documentation * General usage documentation
* Uncouple component-provided names from the library
* Automatically-updated singletons * Automatically-updated singletons
* Document exceptions * Document exceptions

View file

@ -9,6 +9,7 @@ import java.util.Set;
import info.ebenoit.ebul.func.FunctionException; import info.ebenoit.ebul.func.FunctionException;
import info.ebenoit.ebul.func.ThrowingConsumer; import info.ebenoit.ebul.func.ThrowingConsumer;
import info.ebenoit.ebul.func.ThrowingFunction;
@ -85,9 +86,15 @@ public final class ComponentState
} }
this.component = component; this.component = component;
@SuppressWarnings( "unchecked" )
ThrowingFunction< Object , String > np = (ThrowingFunction< Object , String >) ci.getNameProvider( );
String name; String name;
if ( component instanceof ParametricComponent ) { if ( np != null ) {
name = ( (ParametricComponent) component ).getComponentName( ); try {
name = np.apply( component );
} catch ( final FunctionException e ) {
throw new ComponentCreationException( e.getCause( ) );
}
} else { } else {
name = ci.getName( ); name = ci.getName( );
if ( "".equals( name ) ) { if ( "".equals( name ) ) {

View file

@ -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.
*
* <p>
* 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 <a href="mailto:ebenoit@ebenoit.info">E. Benoît</a>
*/
@Retention( RetentionPolicy.RUNTIME )
@Target( ElementType.METHOD )
public @interface NameProvider
{
// EMPTY
}

View file

@ -14,6 +14,7 @@ import java.util.HashSet;
import info.ebenoit.ebul.func.ThrowingBiConsumer; import info.ebenoit.ebul.func.ThrowingBiConsumer;
import info.ebenoit.ebul.func.ThrowingConsumer; import info.ebenoit.ebul.func.ThrowingConsumer;
import info.ebenoit.ebul.func.ThrowingFunction;
import info.ebenoit.ebul.func.ThrowingSupplier; import info.ebenoit.ebul.func.ThrowingSupplier;
import info.ebenoit.ebul.reflection.Annotations; import info.ebenoit.ebul.reflection.Annotations;
import info.ebenoit.ebul.reflection.Classes; import info.ebenoit.ebul.reflection.Classes;
@ -32,6 +33,8 @@ public final class NewComponentInfo< T >
{ {
/** Member finder that looks for the default constructor */ /** Member finder that looks for the default constructor */
private static final MemberFinder< Constructor< ? > > CONSTRUCTOR_FINDER; 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 */ /** Member finder that looks for lifecycle methods */
private static final MemberFinder< Method > LC_METHOD_FINDER; private static final MemberFinder< Method > LC_METHOD_FINDER;
/** Member finder that looks for dependency injections using fields */ /** Member finder that looks for dependency injections using fields */
@ -45,6 +48,10 @@ public final class NewComponentInfo< T >
NewComponentInfo.CONSTRUCTOR_FINDER NewComponentInfo.CONSTRUCTOR_FINDER
.setMemberFilter( ( final Constructor< ? > c ) -> c.getParameterCount( ) == 0 ); .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 ); LC_METHOD_FINDER = new MemberFinder< >( MemberFinder.METHOD_EXTRACTOR , true );
NewComponentInfo.LC_METHOD_FINDER.setClassesFirst( true ); NewComponentInfo.LC_METHOD_FINDER.setClassesFirst( true );
NewComponentInfo.LC_METHOD_FINDER 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 * Scans a package for classes that correspond to annotated components and processes them using
* {@link #fromClass(Class)} * {@link #fromClass(Class)}
* *
* @param name * @param name
* the name of the package to scan * the name of the package to scan
* @param recursive * @param recursive
@ -112,22 +119,27 @@ public final class NewComponentInfo< T >
// Sets the new component's name // Sets the new component's name
final Component aComp = klass.getAnnotation( Component.class ); final Component aComp = klass.getAnnotation( Component.class );
final boolean isAnon = klass.isAnnotationPresent( Anonymous.class ); final boolean isAnon = klass.isAnnotationPresent( Anonymous.class );
final Method nameProvider = NewComponentInfo.findNameProvider( klass );
if ( isAnon ) { if ( isAnon ) {
if ( ParametricComponent.class.isAssignableFrom( klass ) ) { if ( nameProvider != null ) {
throw new ComponentDefinitionException( "parametric component can't be anonymous" ); throw new ComponentDefinitionException( "component with name provider can't be anonymous" );
} }
if ( aComp != null && !"".equals( aComp.value( ) ) ) { if ( aComp != null && !"".equals( aComp.value( ) ) ) {
throw new ComponentDefinitionException( "named component can't be anonymous" ); throw new ComponentDefinitionException( "named component can't be anonymous" );
} }
info.setName( null ); info.setName( null );
} else if ( aComp != null ) { } else if ( aComp != null ) {
if ( !"".equals( aComp.value( ) ) && ParametricComponent.class.isAssignableFrom( klass ) ) { if ( !"".equals( aComp.value( ) ) && nameProvider != null ) {
throw new ComponentDefinitionException( "parametric component can't be named" ); throw new ComponentDefinitionException( "component with name provider can't be named explicitely" );
} }
info.setName( aComp.value( ) ); info.setName( aComp.value( ) );
} else { } else {
info.setName( "" ); info.setName( "" );
} }
if ( nameProvider != null ) {
info.setNameProvider( o -> (String) nameProvider.invoke( o ) );
}
// Autostart flag // Autostart flag
info.setAutostart( klass.isAnnotationPresent( Autostart.class ) ); 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 <code>null</code> 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 * Finds lifecycle methods in all classes and interfaces a component class extends / implements and adds the
* corresponding actions to the information record. * corresponding actions to the information record.
@ -337,6 +385,9 @@ public final class NewComponentInfo< T >
/** The component's name, or <code>null</code> if it must be guessed */ /** The component's name, or <code>null</code> if it must be guessed */
private String name; 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 <code>null</code> if this component is not a * The name of a component this new component acts as a driver for, or <code>null</code> if this component is not a
* driver. * driver.
@ -374,7 +425,7 @@ public final class NewComponentInfo< T >
* Initialises a component based on a component supplier * Initialises a component based on a component supplier
* *
* @param 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 ) 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 * @return the name of the component for which the current component acts as a driver for
*/ */

View file

@ -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 <a href="mailto:ebenoit@ebenoit.info">E. Benoît</a>
*/
public interface ParametricComponent
{
/**
* Determine the name of the current instance of this component.
*
* @return the name of the component
*/
public String getComponentName( );
}

View file

@ -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 private static class PCmpTest
implements ParametricComponent
{ {
private final String componentName; private final String componentName;
@ -59,7 +58,6 @@ public class TestComponentState
} }
@Override
public String getComponentName( ) public String getComponentName( )
{ {
return this.componentName; 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 @Test
public void testInitialiseWithParametricComponent( ) public void testInitialiseWithParametricComponent( )
{ {
final Object object = new PCmpTest( "ParametricName" ); final PCmpTest object = new PCmpTest( "ParametricName" );
final NewComponentInfo< Object > ci = new NewComponentInfo< Object >( object ); final NewComponentInfo< PCmpTest > ci = new NewComponentInfo< PCmpTest >( object ) //
.setNameProvider( o -> o.getComponentName( ) );
final ComponentState cs = new ComponentState( this.reg , ci ); final ComponentState cs = new ComponentState( this.reg , ci );
Assert.assertEquals( "ParametricName" , cs.getName( ) ); 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 @Test
public void testInitialiseWithParametricComponentAndName( ) public void testInitialiseWithParametricComponentAndName( )
{ {
final Object object = new PCmpTest( "ParametricName" ); final PCmpTest object = new PCmpTest( "ParametricName" );
final NewComponentInfo< Object > ci = new NewComponentInfo< Object >( object ).setName( "Test" ); final NewComponentInfo< PCmpTest > ci = new NewComponentInfo< PCmpTest >( object ) //
.setNameProvider( o -> o.getComponentName( ) ) //
.setName( "Test" );
final ComponentState cs = new ComponentState( this.reg , ci ); final ComponentState cs = new ComponentState( this.reg , ci );
Assert.assertEquals( "ParametricName" , cs.getName( ) ); Assert.assertEquals( "ParametricName" , cs.getName( ) );
} }
@ -268,10 +269,10 @@ public class TestComponentState
@Test @Test
public void testToStringAnon( ) public void testToStringAnon( )
{ {
final Object object = new PCmpTest( null ); final Object object = new Object( );
final NewComponentInfo< Object > ci = new NewComponentInfo< Object >( object ); final NewComponentInfo< Object > ci = new NewComponentInfo< Object >( object );
final ComponentState cs = new ComponentState( this.reg , ci ); 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( ) );
} }

View file

@ -85,13 +85,12 @@ public class TestNewComponentInfo
// EMPTY // EMPTY
} }
/** Test anonymous component that implements {@link ParametricComponent} (invalid!) */ /** Test anonymous component that has a name provider (invalid!) */
@Anonymous @Anonymous
private static class TestCmpAnonPN private static class TestCmpAnonPN
implements ParametricComponent
{ {
@Override @NameProvider
public String getComponentName( ) public String getComponentName( )
{ {
return null; return null;
@ -103,23 +102,15 @@ public class TestNewComponentInfo
@Component( "Fail!" ) @Component( "Fail!" )
@Anonymous @Anonymous
private static class TestCmpAnonNamed private static class TestCmpAnonNamed
implements ParametricComponent
{ {
// EMPTY
@Override
public String getComponentName( )
{
return null;
}
} }
/** Test component with a parametric name */ /** Test component with a name provider */
private static class TestCmpPN private static class TestCmpPN
implements ParametricComponent
{ {
@Override @NameProvider
public String getComponentName( ) public String getComponentName( )
{ {
return "TestCmpPN"; 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" ) @Component( "Fail" )
private static class TestCmpPNFail private static class TestCmpPNFail1
implements ParametricComponent
{ {
@Override @NameProvider
public String getComponentName( ) public String getComponentName( )
{ {
return "TestCmpPNFail"; 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 */ /** Test component that has lifecycle methods */
private static class TestCmp5 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 * 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 ) @Test( expected = ComponentDefinitionException.class )
public void testFromClassAnonPN( ) 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 @Test
public void testFromClassPN1( ) public void testFromClassPN1( )
@ -529,13 +574,57 @@ 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} but also
* but also has an annotation-specified name * has an annotation-specified name
*/ */
@Test( expected = ComponentDefinitionException.class ) @Test( expected = ComponentDefinitionException.class )
public void testFromClassPNFail1( ) 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 );
} }