# AssemblyHelper.java

    /*
     * Copyright (c) 2010, Rickard Öberg. All Rights Reserved.
     *
     * Licensed under the Apache License, Version 2.0 (the "License");
     * you may not use this file except in compliance with the License.
     * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
     * Unless required by applicable law or agreed to in writing, software
     * distributed under the License is distributed on an "AS IS" BASIS,
     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     * See the License for the specific language governing permissions and
     * limitations under the License.
     *
     */

    package org.qi4j.runtime.bootstrap;

    import java.lang.annotation.Annotation;
    import java.lang.reflect.InvocationHandler;
    import java.lang.reflect.Method;
    import java.lang.reflect.Modifier;
    import java.lang.reflect.Type;
    import java.util.HashMap;
    import java.util.Map;
    import org.qi4j.api.common.AppliesTo;
    import org.qi4j.api.common.AppliesToFilter;
    import org.qi4j.api.common.ConstructionException;
    import org.qi4j.api.constraint.Constraint;
    import org.qi4j.runtime.composite.ConcernModel;
    import org.qi4j.runtime.composite.ConstraintDeclaration;
    import org.qi4j.runtime.composite.FragmentClassLoader;
    import org.qi4j.runtime.composite.MixinModel;
    import org.qi4j.runtime.composite.SideEffectModel;

    /**
     * This helper is used when building the application model. It keeps track
     * of already created classloaders and various models
     */
    public class AssemblyHelper
    {
        Map<Class, Class> instantiationClasses = new HashMap<>();
        Map<Class, ConstraintDeclaration> constraintDeclarations = new HashMap<>();
        Map<ClassLoader, FragmentClassLoader> modifierClassLoaders = new HashMap<>();
        Map<Class<?>, AppliesToFilter> appliesToInstances = new HashMap<>();

        public MixinModel getMixinModel( Class mixinClass )
        {
            return new MixinModel( mixinClass, instantiationClass( mixinClass ) );
        }

        public ConcernModel getConcernModel( Class concernClass )
        {
            return new ConcernModel( concernClass, instantiationClass( concernClass ) );
        }

        public SideEffectModel getSideEffectModel( Class sideEffectClass )
        {
            return new SideEffectModel( sideEffectClass, instantiationClass( sideEffectClass ) );
        }

        private Class instantiationClass( Class fragmentClass )
        {
            Class instantiationClass = fragmentClass;
            if( !InvocationHandler.class.isAssignableFrom( fragmentClass ) )
            {
                instantiationClass = instantiationClasses.get( fragmentClass );

                if( instantiationClass == null )
                {
                    try
                    {
                        FragmentClassLoader fragmentLoader = getModifierClassLoader( fragmentClass.getClassLoader() );
                        instantiationClass = fragmentLoader.loadFragmentClass( fragmentClass );
                        instantiationClasses.put( fragmentClass, instantiationClass );
                    }
                    catch( ClassNotFoundException | VerifyError e )
                    {
                        throw new ConstructionException( "Could not generate mixin subclass " + fragmentClass.getName(), e );
                    }
                }
            }
            return instantiationClass;
        }

        private FragmentClassLoader getModifierClassLoader( ClassLoader classLoader )
        {
            FragmentClassLoader cl = modifierClassLoaders.get( classLoader );
            if( cl == null )
            {
                cl = new FragmentClassLoader( classLoader );
                modifierClassLoaders.put( classLoader, cl );
            }
            return cl;
        }

        public boolean appliesTo( Class<?> fragmentClass, Method method, Iterable<Class<?>> types, Class<?> mixinClass )
        {
            AppliesToFilter appliesToFilter = appliesToInstances.get( fragmentClass );
            if( appliesToFilter == null )
            {
                appliesToFilter = createAppliesToFilter( fragmentClass );
                appliesToInstances.put( fragmentClass, appliesToFilter );
            }
            for( Class<?> compositeType : types )
            {
                if( appliesToFilter.appliesTo( method, mixinClass, compositeType, fragmentClass ) )
                {
                    return true;
                }
            }
            return false;
        }

        public AppliesToFilter createAppliesToFilter( Class<?> fragmentClass )
        {
            AppliesToFilter result = null;
            if( !InvocationHandler.class.isAssignableFrom( fragmentClass ) )
            {
                result = new TypedFragmentAppliesToFilter();
                if( Modifier.isAbstract( fragmentClass.getModifiers() ) )
                {
                    result = new AndAppliesToFilter( result, new ImplementsMethodAppliesToFilter() );
                }
            }
            result = applyAppliesTo( result, fragmentClass );
            if( result == null )
            {
                return AppliesToFilter.ALWAYS;
            }
            return result;
        }

        private AppliesToFilter applyAppliesTo( AppliesToFilter existing, Class<?> modifierClass )
        {
            AppliesTo appliesTo = modifierClass.getAnnotation( AppliesTo.class );
            if( appliesTo != null )
            {
                // Use "or" for all filters specified in the annotation
                AppliesToFilter appliesToAnnotation = null;
                for( Class<?> appliesToClass : appliesTo.value() )
                {
                    AppliesToFilter filter;
                    if( AppliesToFilter.class.isAssignableFrom( appliesToClass ) )
                    {
                        try
                        {
                            filter = (AppliesToFilter) appliesToClass.newInstance();
                        }
                        catch( Exception e )
                        {
                            throw new ConstructionException( e );
                        }
                    }
                    else if( Annotation.class.isAssignableFrom( appliesToClass ) )
                    {
                        filter = new AnnotationAppliesToFilter( appliesToClass );
                    }
                    else // Type check
                    {
                        filter = new TypeCheckAppliesToFilter( appliesToClass );
                    }

                    if( appliesToAnnotation == null )
                    {
                        appliesToAnnotation = filter;
                    }
                    else
                    {
                        appliesToAnnotation = new OrAppliesToFilter( appliesToAnnotation, filter );
                    }
                }
                // Add to the rest of the rules using "and"
                if( existing == null )
                {
                    return appliesToAnnotation;
                }
                else
                {
                    return new AndAppliesToFilter( existing, appliesToAnnotation );
                }
            }
            return existing;
        }

        public boolean appliesTo( Class<? extends Constraint<?, ?>> constraint,
                                  Class<? extends Annotation> annotationType,
                                  Type valueType
        )
        {
            ConstraintDeclaration constraintDeclaration = constraintDeclarations.get( constraint );
            if( constraintDeclaration == null )
            {
                constraintDeclaration = new ConstraintDeclaration( constraint );
                constraintDeclarations.put( constraint, constraintDeclaration );
            }

            return constraintDeclaration.appliesTo( annotationType, valueType );
        }
    }