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; } }