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:
parent
c4ff9e4339
commit
5032e44182
7 changed files with 239 additions and 63 deletions
1
TODO
1
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
|
||||
|
||||
|
|
|
@ -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 ) ) {
|
||||
|
|
25
src/main/java/info/ebenoit/ebul/cmp/NameProvider.java
Normal file
25
src/main/java/info/ebenoit/ebul/cmp/NameProvider.java
Normal 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
|
||||
}
|
|
@ -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 <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
|
||||
* 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 */
|
||||
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
|
||||
* 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
|
||||
*/
|
||||
|
|
|
@ -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( );
|
||||
|
||||
}
|
|
@ -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( ) );
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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 );
|
||||
}
|
||||
|
||||
|
||||
|
|
Loading…
Reference in a new issue