* Setting a component information record's name to null will lead to an anonymous component, while setting it to an empty string will lead to an component whose name is that of its class. * New Anonymous annotation to indicate that a component is meant to be anonymous (conflicts with ParametricComponent or valued Component annotations).
549 lines
18 KiB
Java
549 lines
18 KiB
Java
package info.ebenoit.ebul.cmp;
|
|
|
|
|
|
import java.lang.reflect.Constructor;
|
|
import java.lang.reflect.Field;
|
|
import java.lang.reflect.Method;
|
|
import java.lang.reflect.Modifier;
|
|
import java.util.ArrayList;
|
|
import java.util.EnumMap;
|
|
import java.util.EnumSet;
|
|
import java.util.HashMap;
|
|
import java.util.HashSet;
|
|
|
|
import info.ebenoit.ebul.func.ThrowingBiConsumer;
|
|
import info.ebenoit.ebul.func.ThrowingConsumer;
|
|
import info.ebenoit.ebul.func.ThrowingSupplier;
|
|
import info.ebenoit.ebul.reflection.Annotations;
|
|
import info.ebenoit.ebul.reflection.MemberFinder;
|
|
|
|
|
|
|
|
/**
|
|
* This class carries information about a component to be added to the registry. Instances can be fed directly to the
|
|
* registry, or generated from a class' annotations.
|
|
*
|
|
* @author <a href="mailto:ebenoit@ebenoit.info">E. Benoît</a>
|
|
*/
|
|
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 lifecycle methods */
|
|
private static final MemberFinder< Method > LC_METHOD_FINDER;
|
|
/** Member finder that looks for dependency injections using fields */
|
|
private static final MemberFinder< Field > DI_FIELD_FINDER;
|
|
/** Member finder that looks for dependency injections using methods */
|
|
private static final MemberFinder< Method > DI_METHOD_FINDER;
|
|
|
|
|
|
static {
|
|
CONSTRUCTOR_FINDER = new MemberFinder< >( ( final Class< ? > c ) -> c.getDeclaredConstructors( ) , false );
|
|
NewComponentInfo.CONSTRUCTOR_FINDER
|
|
.setMemberFilter( ( final Constructor< ? > c ) -> c.getParameterCount( ) == 0 );
|
|
|
|
LC_METHOD_FINDER = new MemberFinder< >( MemberFinder.METHOD_EXTRACTOR , true );
|
|
NewComponentInfo.LC_METHOD_FINDER.setClassesFirst( true );
|
|
NewComponentInfo.LC_METHOD_FINDER
|
|
.setMemberFilter( m -> m.getDeclaredAnnotation( LifecycleMethod.class ) != null );
|
|
|
|
DI_FIELD_FINDER = new MemberFinder< >( MemberFinder.FIELD_EXTRACTOR , false );
|
|
NewComponentInfo.DI_FIELD_FINDER.setMemberFilter( f -> f.getDeclaredAnnotation( UseComponent.class ) != null );
|
|
|
|
DI_METHOD_FINDER = new MemberFinder< >( MemberFinder.METHOD_EXTRACTOR , true );
|
|
NewComponentInfo.DI_METHOD_FINDER.setMemberFilter( m -> m.getDeclaredAnnotation( UseComponent.class ) != null );
|
|
}
|
|
|
|
|
|
/**
|
|
* Creates a new component information record based on a component's annotated class.
|
|
*
|
|
* @param klass
|
|
* the class to extract the information from
|
|
* @return the generated information record
|
|
* @throws ComponentDefinitionException
|
|
* if the class cannot be used as a component
|
|
*/
|
|
public static < T > NewComponentInfo< T > fromClass( final Class< T > klass )
|
|
throws ComponentDefinitionException
|
|
{
|
|
final Constructor< ? > constructor = NewComponentInfo.getDefaultConstructor( klass );
|
|
constructor.setAccessible( true );
|
|
final NewComponentInfo< T > info = new NewComponentInfo< T >( ( ) -> {
|
|
return klass.cast( constructor.newInstance( ) );
|
|
} );
|
|
|
|
// Sets the new component's name
|
|
final Component aComp = klass.getAnnotation( Component.class );
|
|
final boolean isAnon = klass.isAnnotationPresent( Anonymous.class );
|
|
if ( isAnon ) {
|
|
if ( ParametricComponent.class.isAssignableFrom( klass ) ) {
|
|
throw new ComponentDefinitionException( "parametric component 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" );
|
|
}
|
|
info.setName( aComp.value( ) );
|
|
} else {
|
|
info.setName( "" );
|
|
}
|
|
|
|
// Autostart flag
|
|
info.setAutostart( klass.isAnnotationPresent( Autostart.class ) );
|
|
|
|
// Driver for other component?
|
|
final DriverFor driverFor = klass.getAnnotation( DriverFor.class );
|
|
if ( driverFor != null ) {
|
|
info.setDriverFor( driverFor.value( ) );
|
|
}
|
|
|
|
// Explicit dependencies
|
|
for ( final Class< ? super T > c : Annotations.findParentsWith( klass , Dependencies.class ) ) {
|
|
final Dependencies dependencies = c.getDeclaredAnnotation( Dependencies.class );
|
|
for ( final String dependency : dependencies.value( ) ) {
|
|
info.addDependency( dependency );
|
|
}
|
|
}
|
|
|
|
// Find lifecycle methods and dependency injections
|
|
NewComponentInfo.findLifecycleActions( info , klass );
|
|
NewComponentInfo.findDependencyInjectorFields( info , klass );
|
|
NewComponentInfo.findDependencyInjectorMethods( info , klass );
|
|
|
|
return info;
|
|
}
|
|
|
|
|
|
/**
|
|
* Finds the default constructor for the specified class
|
|
*
|
|
* @param klass
|
|
* the class
|
|
* @return the default constructor (even if it is private)
|
|
* @throws ComponentDefinitionException
|
|
* if there is no default constructor
|
|
*/
|
|
private static < T > Constructor< ? > getDefaultConstructor( final Class< T > klass )
|
|
throws ComponentDefinitionException
|
|
{
|
|
final ArrayList< Constructor< ? > > constructors = new ArrayList< >( );
|
|
NewComponentInfo.CONSTRUCTOR_FINDER.find( constructors , klass );
|
|
if ( constructors.isEmpty( ) ) {
|
|
throw new ComponentDefinitionException( "could not find default constructor in " + klass );
|
|
}
|
|
|
|
final Constructor< ? > constructor = constructors.get( 0 );
|
|
if ( constructor.getDeclaringClass( ) != klass ) {
|
|
throw new ComponentDefinitionException( "could not find default constructor in " + klass );
|
|
}
|
|
return constructor;
|
|
}
|
|
|
|
|
|
/**
|
|
* Finds lifecycle methods in all classes and interfaces a component class extends / implements and adds the
|
|
* corresponding actions to the information record.
|
|
*
|
|
* <p>
|
|
* FIXME: inefficient algorithm
|
|
*
|
|
* @param info
|
|
* the information record
|
|
* @param klass
|
|
* the component class
|
|
* @throws ComponentDefinitionException
|
|
* if a class or interface in the hierarchy declares more than one method for the same lifecycle stage
|
|
*/
|
|
private static < T > void findLifecycleActions( final NewComponentInfo< T > info , final Class< T > klass )
|
|
throws ComponentDefinitionException
|
|
{
|
|
final ArrayList< Method > output = new ArrayList< >( );
|
|
NewComponentInfo.LC_METHOD_FINDER.find( output , klass );
|
|
|
|
final EnumMap< LifecycleStage , Method > methods = new EnumMap< >( LifecycleStage.class );
|
|
final HashMap< Class< ? > , EnumSet< LifecycleStage > > checks = new HashMap< >( );
|
|
final int nFound = output.size( );
|
|
for ( int i = nFound - 1 ; i >= 0 ; i-- ) {
|
|
final Method m = output.get( i );
|
|
final Class< ? > mc = m.getDeclaringClass( );
|
|
EnumSet< LifecycleStage > present = checks.get( mc );
|
|
if ( present == null ) {
|
|
present = EnumSet.noneOf( LifecycleStage.class );
|
|
checks.put( mc , present );
|
|
}
|
|
|
|
final LifecycleStage lma = m.getDeclaredAnnotation( LifecycleMethod.class ).value( );
|
|
if ( !present.add( lma ) ) {
|
|
throw new ComponentDefinitionException( "in class " + mc + ": duplicate lifecycle method for " + lma );
|
|
}
|
|
methods.put( lma , m );
|
|
}
|
|
|
|
for ( final EnumMap.Entry< LifecycleStage , Method > entry : methods.entrySet( ) ) {
|
|
final Method m = entry.getValue( );
|
|
m.setAccessible( true );
|
|
info.setLifecycleAction( entry.getKey( ) , o -> m.invoke( o ) );
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Finds fields annotated with {@link UseComponent} and add dependency injection actions setting these fields to the
|
|
* information structure.
|
|
*
|
|
* @param info
|
|
* the information structure
|
|
* @param klass
|
|
* the component class
|
|
* @throws ComponentDefinitionException
|
|
* if a static or final field is annotated with {@link UseComponent}, or if the field's type is invalid
|
|
* (primitive or array type)
|
|
*/
|
|
private static < T > void findDependencyInjectorFields( final NewComponentInfo< T > info , final Class< T > klass )
|
|
throws ComponentDefinitionException
|
|
{
|
|
final ArrayList< Field > fields = new ArrayList< >( );
|
|
NewComponentInfo.DI_FIELD_FINDER.find( fields , klass );
|
|
for ( final Field f : fields ) {
|
|
if ( ( f.getModifiers( ) & Modifier.STATIC ) != 0 ) {
|
|
throw new ComponentDefinitionException( "in class " + f.getDeclaringClass( ) + ": static field "
|
|
+ f.getName( ) + " annotated with UseComponent" );
|
|
}
|
|
if ( ( f.getModifiers( ) & Modifier.FINAL ) != 0 ) {
|
|
throw new ComponentDefinitionException( "in class " + f.getDeclaringClass( ) + ": final field "
|
|
+ f.getName( ) + " annotated with UseComponent" );
|
|
}
|
|
|
|
DependencyInfo dep;
|
|
try {
|
|
dep = NewComponentInfo.depFrom( f.getType( ) , f.getDeclaredAnnotation( UseComponent.class ) );
|
|
} catch ( final ComponentDefinitionException e ) {
|
|
throw new ComponentDefinitionException(
|
|
"in class " + f.getDeclaringClass( ) + ", field " + f.getName( ) + ": " + e.getMessage( ) );
|
|
}
|
|
f.setAccessible( true );
|
|
info.addDependencyInjector( dep , ( o , d ) -> f.set( o , d ) );
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Finds methods annotated with {@link UseComponent} and add dependency injection actions calling these methods to
|
|
* the information structure.
|
|
*
|
|
* @param info
|
|
* the information structure
|
|
* @param klass
|
|
* the component class
|
|
* @throws ComponentDefinitionException
|
|
* if a static method or a method with more than one parameter is annotated with {@link UseComponent},
|
|
* or if the method parameter's type is invalid (primitive or array type)
|
|
*/
|
|
private static < T > void findDependencyInjectorMethods( final NewComponentInfo< T > info , final Class< T > klass )
|
|
throws ComponentDefinitionException
|
|
{
|
|
final ArrayList< Method > methods = new ArrayList< >( );
|
|
NewComponentInfo.DI_METHOD_FINDER.find( methods , klass );
|
|
for ( final Method m : methods ) {
|
|
if ( ( m.getModifiers( ) & Modifier.STATIC ) != 0 ) {
|
|
throw new ComponentDefinitionException( "in class " + m.getDeclaringClass( ) + ": static method "
|
|
+ m.getName( ) + " annotated with UseComponent" );
|
|
}
|
|
if ( m.getParameterCount( ) != 1 ) {
|
|
throw new ComponentDefinitionException( "in class " + m.getDeclaringClass( ) + ": method "
|
|
+ m.getName( ) + " has more than 1 parameter" );
|
|
}
|
|
|
|
DependencyInfo dep;
|
|
try {
|
|
dep = NewComponentInfo.depFrom( m.getParameterTypes( )[ 0 ] ,
|
|
m.getDeclaredAnnotation( UseComponent.class ) );
|
|
} catch ( final ComponentDefinitionException e ) {
|
|
throw new ComponentDefinitionException(
|
|
"in class " + m.getDeclaringClass( ) + ", field " + m.getName( ) + ": " + e.getMessage( ) );
|
|
}
|
|
m.setAccessible( true );
|
|
info.addDependencyInjector( dep , ( o , d ) -> m.invoke( o , d ) );
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Create a dependency from an {@link UseComponent}-annotated field or method
|
|
*
|
|
* @param type
|
|
* the type of the field or of the method's parameter
|
|
* @param annotation
|
|
* the {@link UseComponent} annotation
|
|
* @return the dependency information record
|
|
* @throws ComponentDefinitionException
|
|
* if the type is invalid (primitive or array type)
|
|
*/
|
|
private static DependencyInfo depFrom( final Class< ? > type , final UseComponent annotation )
|
|
throws ComponentDefinitionException
|
|
{
|
|
if ( "".equals( annotation.value( ) ) ) {
|
|
return new DependencyInfo( type );
|
|
}
|
|
DependencyInfo.checkClassValidity( type );
|
|
return new DependencyInfo( annotation.value( ) );
|
|
}
|
|
|
|
/** The component's instance, or <code>null</code> if a supplier is being used */
|
|
private final T component;
|
|
/** The component's supplier, or <code>null</code> if an instance has been supplied */
|
|
private final ThrowingSupplier< T > supplier;
|
|
|
|
/** The component's name, or <code>null</code> if it must be guessed */
|
|
private String name;
|
|
/**
|
|
* The name of a component this new component acts as a driver for, or <code>null</code> if this component is not a
|
|
* driver.
|
|
*/
|
|
private String driverFor;
|
|
/** The set of dependency names for the new component */
|
|
private final HashSet< DependencyInfo > dependencies;
|
|
|
|
/** Lifecylce actions for the new component */
|
|
private final EnumMap< LifecycleStage , ThrowingConsumer< ? super T > > lcActions;
|
|
/** Dependency injectors */
|
|
private final HashMap< DependencyInfo , ArrayList< ThrowingBiConsumer< Object , Object > > > injectors;
|
|
|
|
/** Whether the component should be activated automatically */
|
|
private boolean autostart;
|
|
|
|
|
|
/**
|
|
* Initialises a component based on a existing instance
|
|
*
|
|
* @param component
|
|
* the component's instance
|
|
*/
|
|
public NewComponentInfo( final T component )
|
|
{
|
|
this.component = component;
|
|
this.supplier = null;
|
|
this.dependencies = new HashSet< >( );
|
|
this.lcActions = new EnumMap< >( LifecycleStage.class );
|
|
this.injectors = new HashMap< >( );
|
|
}
|
|
|
|
|
|
/**
|
|
* Initialises a component based on a component supplier
|
|
*
|
|
* @param supplier
|
|
* the supplier to obtain the component from
|
|
*/
|
|
public NewComponentInfo( final ThrowingSupplier< T > supplier )
|
|
{
|
|
this.component = null;
|
|
this.supplier = supplier;
|
|
this.dependencies = new HashSet< >( );
|
|
this.lcActions = new EnumMap< >( LifecycleStage.class );
|
|
this.injectors = new HashMap< >( );
|
|
}
|
|
|
|
|
|
/**
|
|
* @return the component, or <code>null</code> if a supplier is in use
|
|
*/
|
|
public T getComponent( )
|
|
{
|
|
return this.component;
|
|
}
|
|
|
|
|
|
/**
|
|
* @return the component supplier, or <code>null</code> if an instance was provided
|
|
*/
|
|
public ThrowingSupplier< T > getSupplier( )
|
|
{
|
|
return this.supplier;
|
|
}
|
|
|
|
|
|
/**
|
|
* Sets the new component's name
|
|
*
|
|
* @param name
|
|
* the new component's name. May be <code>null</code> for an anonymous component or {@code ""} for a
|
|
* component whose class name will serve as its name.
|
|
* @return the current object
|
|
*/
|
|
public NewComponentInfo< T > setName( final String name )
|
|
{
|
|
this.name = name;
|
|
return this;
|
|
}
|
|
|
|
|
|
/**
|
|
* @return the name of the new component. May be <code>null</code> for an anonymous component or {@code ""} for a
|
|
* component whose class name will serve as its name.
|
|
*/
|
|
public String getName( )
|
|
{
|
|
return this.name;
|
|
}
|
|
|
|
|
|
/**
|
|
* @return the name of the component for which the current component acts as a driver for
|
|
*/
|
|
public String getDriverFor( )
|
|
{
|
|
return this.driverFor;
|
|
}
|
|
|
|
|
|
/**
|
|
* Sets the name of the component for which the current component acts as a driver for
|
|
*
|
|
* @param driverFor
|
|
* the name of the main component
|
|
* @return the current object
|
|
*/
|
|
public NewComponentInfo< T > setDriverFor( final String driverFor )
|
|
{
|
|
this.driverFor = driverFor;
|
|
this.dependencies.add( new DependencyInfo( driverFor ) );
|
|
return this;
|
|
}
|
|
|
|
|
|
/**
|
|
* Sets the action for one of the component's lifecycle stages
|
|
*
|
|
* @param stage
|
|
* the lifecyle stage for which the action is being set
|
|
* @param action
|
|
* the action to perform
|
|
* @return the current object
|
|
*/
|
|
public NewComponentInfo< T > setLifecycleAction( final LifecycleStage stage ,
|
|
final ThrowingConsumer< ? super T > action )
|
|
{
|
|
this.lcActions.put( stage , action );
|
|
return this;
|
|
}
|
|
|
|
|
|
/**
|
|
* Reads the action for one of the component's lifecycle stages
|
|
*
|
|
* @param stage
|
|
* the lifecycle stage for which the action must be returned
|
|
* @return the action corresponding to the specified lifecycle stage
|
|
*/
|
|
public ThrowingConsumer< ? super T > getLifecycleAction( final LifecycleStage stage )
|
|
{
|
|
return this.lcActions.get( stage );
|
|
}
|
|
|
|
|
|
/**
|
|
* @return the modifiable set of dependencies
|
|
*/
|
|
public HashSet< DependencyInfo > getDependencies( )
|
|
{
|
|
return this.dependencies;
|
|
}
|
|
|
|
|
|
/**
|
|
* Adds a dependency to the component
|
|
*
|
|
* @param name
|
|
* the dependency's name
|
|
* @return the current object
|
|
*/
|
|
public NewComponentInfo< T > addDependency( final String name )
|
|
{
|
|
this.dependencies.add( new DependencyInfo( name ) );
|
|
return this;
|
|
}
|
|
|
|
|
|
/**
|
|
* Adds a dependency to the component
|
|
*
|
|
* @param klass
|
|
* the dependency's type
|
|
* @return the current object
|
|
*/
|
|
public NewComponentInfo< T > addDependency( final Class< ? > klass )
|
|
{
|
|
this.dependencies.add( new DependencyInfo( klass ) );
|
|
return this;
|
|
}
|
|
|
|
|
|
/**
|
|
* @return <code>true</code> if the component is supposed to start automatically
|
|
*/
|
|
public boolean getAutostart( )
|
|
{
|
|
return this.autostart;
|
|
}
|
|
|
|
|
|
/**
|
|
* Sets the autostart flag
|
|
*
|
|
* @param autostart
|
|
* a flag indicating whether the component should be started automatically
|
|
* @return the current object
|
|
*/
|
|
public NewComponentInfo< T > setAutostart( final boolean autostart )
|
|
{
|
|
this.autostart = autostart;
|
|
return this;
|
|
}
|
|
|
|
|
|
/**
|
|
* Accesses the map of dependency injectors. Note that any direct change should be reflected on the dependencies
|
|
* manually.
|
|
*
|
|
* @return the modifiable map of dependency injectors, with the dependency information as the key and a list of
|
|
* functions that inject the dependency into the target component as the value.
|
|
*/
|
|
public HashMap< DependencyInfo , ArrayList< ThrowingBiConsumer< Object , Object > > > getInjectors( )
|
|
{
|
|
return this.injectors;
|
|
}
|
|
|
|
|
|
/**
|
|
* Adds a dependency injector and registers the new dependency.
|
|
*
|
|
* @param dependency
|
|
* the name of the component being depended upon
|
|
* @param injector
|
|
* a function that accepts two parameters: the instance into which the dependency is being injected, and
|
|
* the instance of the component being injected
|
|
* @return the current object
|
|
*/
|
|
public NewComponentInfo< T > addDependencyInjector( final DependencyInfo dependency ,
|
|
final ThrowingBiConsumer< ? super T , Object > injector )
|
|
{
|
|
ArrayList< ThrowingBiConsumer< Object , Object > > injectors = this.injectors.get( dependency );
|
|
if ( injectors == null ) {
|
|
injectors = new ArrayList< >( );
|
|
this.injectors.put( dependency , injectors );
|
|
}
|
|
|
|
@SuppressWarnings( "unchecked" )
|
|
ThrowingBiConsumer< Object , Object > ci = (ThrowingBiConsumer< Object , Object >) injector;
|
|
injectors.add( ci );
|
|
|
|
this.dependencies.add( dependency );
|
|
return this;
|
|
}
|
|
}
|