All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.codehaus.plexus.component.builder.XBeanComponentBuilder Maven / Gradle / Ivy

There is a newer version: 2.1.1
Show newest version
/*
 * Copyright 2001-2006 Codehaus Foundation.
 *
 * 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.codehaus.plexus.component.builder;

import static org.apache.xbean.recipe.RecipeHelper.toClass;

import org.apache.xbean.recipe.AbstractRecipe;
import org.apache.xbean.recipe.ConstructionException;
import org.apache.xbean.recipe.ObjectRecipe;
import org.apache.xbean.recipe.Option;
import org.apache.xbean.recipe.RecipeHelper;
import org.codehaus.plexus.MutablePlexusContainer;
import org.codehaus.plexus.PlexusConstants;
import org.codehaus.plexus.PlexusContainer;
import static org.codehaus.plexus.PlexusConstants.PLEXUS_DEFAULT_HINT;
import org.codehaus.plexus.classworlds.realm.ClassRealm;
import org.codehaus.plexus.component.MapOrientedComponent;
import static org.codehaus.plexus.component.ComponentStack.setComponentStackProperty;
import static org.codehaus.plexus.component.CastUtils.isAssignableFrom;
import org.codehaus.plexus.component.collections.LiveMap;
import org.codehaus.plexus.component.collections.LiveList;
import org.codehaus.plexus.component.configurator.BasicComponentConfigurator;
import org.codehaus.plexus.component.configurator.ComponentConfigurationException;
import org.codehaus.plexus.component.configurator.ComponentConfigurator;
import org.codehaus.plexus.component.configurator.converters.ConfigurationConverter;
import org.codehaus.plexus.component.configurator.converters.composite.MapConverter;
import org.codehaus.plexus.component.configurator.converters.lookup.ConverterLookup;
import org.codehaus.plexus.component.configurator.converters.lookup.DefaultConverterLookup;
import org.codehaus.plexus.component.configurator.converters.special.ClassRealmConverter;
import org.codehaus.plexus.component.configurator.expression.DefaultExpressionEvaluator;
import org.codehaus.plexus.component.factory.ComponentFactory;
import org.codehaus.plexus.component.factory.ComponentInstantiationException;
import org.codehaus.plexus.component.factory.UndefinedComponentFactoryException;
import org.codehaus.plexus.component.factory.java.JavaComponentFactory;
import org.codehaus.plexus.component.manager.ComponentManager;
import org.codehaus.plexus.component.repository.ComponentDescriptor;
import org.codehaus.plexus.component.repository.ComponentRequirement;
import org.codehaus.plexus.component.repository.ComponentRequirementList;
import org.codehaus.plexus.component.repository.exception.ComponentLifecycleException;
import org.codehaus.plexus.component.repository.exception.ComponentLookupException;
import org.codehaus.plexus.configuration.PlexusConfiguration;
import org.codehaus.plexus.configuration.PlexusConfigurationException;
import org.codehaus.plexus.logging.Logger;
import org.codehaus.plexus.personality.plexus.lifecycle.phase.PhaseExecutionException;
import org.codehaus.plexus.util.StringUtils;

import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

public class XBeanComponentBuilder implements ComponentBuilder {
    private ComponentManager componentManager;

    public XBeanComponentBuilder() {
    }

    public XBeanComponentBuilder(ComponentManager componentManager) {
        setComponentManager(componentManager);
    }

    public ComponentManager getComponentManager() {
        return componentManager;
    }

    public void setComponentManager(ComponentManager componentManager) {
        this.componentManager = componentManager;
    }

    protected MutablePlexusContainer getContainer() {
        return componentManager.getContainer();
    }

    public T build(ComponentDescriptor descriptor, ClassRealm realm, ComponentBuildListener listener) throws ComponentInstantiationException, ComponentLifecycleException {
        if (listener != null) {
            listener.beforeComponentCreate(descriptor, realm);
        }

        T component = createComponentInstance(descriptor, realm);

        if (listener != null) {
            listener.componentCreated(descriptor, component, realm);
        }

        startComponentLifecycle(component, realm);

        if (listener != null) {
            listener.componentConfigured(descriptor, component, realm);
        }

        return component;
    }

    protected T createComponentInstance(ComponentDescriptor descriptor, ClassRealm realm) throws ComponentInstantiationException {
        MutablePlexusContainer container = getContainer();
        if (realm == null) {
            realm = descriptor.getRealm();
        }

        ClassLoader oldClassLoader = Thread.currentThread().getContextClassLoader();
        Thread.currentThread().setContextClassLoader(realm);
        try {
            ObjectRecipe recipe = createObjectRecipe(descriptor, realm);

            T instance;
            ComponentFactory componentFactory = container.getComponentFactoryManager().findComponentFactory(descriptor.getComponentFactory());
            if (JavaComponentFactory.class.equals(componentFactory.getClass())) {
                // xbean-reflect will create object and do injection
                instance = (T) recipe.create();
            } else {
                // todo figure out how to easily let xbean use the factory to construct the component
                // use object factory to construct component and then inject into that object
                instance = (T) componentFactory.newInstance(descriptor, realm, container);
                recipe.setProperties( instance );
            }

            // todo figure out how to easily let xbean do this map oriented stuff (if it is actually used in plexus)
            if ( instance instanceof MapOrientedComponent) {
                MapOrientedComponent mapOrientedComponent = (MapOrientedComponent) instance;
                processMapOrientedComponent(descriptor, mapOrientedComponent, realm);
            }

            return instance;
        }
        catch ( ConstructionException e )
        {
            Throwable cause = unwrapConstructionException( e );

            // do not rewrap
            if (cause instanceof ComponentInstantiationException)
            {
                throw (ComponentInstantiationException) cause;
            }

            // reuse original exception message.. the ones from XBean contain a lot of information
            if ( cause != null )
            {
                // wrap real cause if we got one
                throw new ComponentInstantiationException( e.getMessage(), cause );
            }
            else
            {
                throw new ComponentInstantiationException( e.getMessage() );
            }
        }
        catch ( UndefinedComponentFactoryException e )
        {
            throw new ComponentInstantiationException( e );
        }
        finally
        {
            Thread.currentThread().setContextClassLoader( oldClassLoader );
        }
    }

    public ObjectRecipe createObjectRecipe(ComponentDescriptor descriptor, ClassRealm realm) throws ComponentInstantiationException {
        String factoryMethod = null;
        String[] constructorArgNames = null;
        Class[] constructorArgTypes = null;

        ObjectRecipe recipe = new ObjectRecipe(descriptor.getImplementationClass(),
                factoryMethod,
                constructorArgNames,
                constructorArgTypes);
        recipe.allow(Option.FIELD_INJECTION);
        recipe.allow(Option.PRIVATE_PROPERTIES);

        // MapOrientedComponents don't get normal injection
        if (!MapOrientedComponent.class.isAssignableFrom(descriptor.getImplementationClass())) {
            for (ComponentRequirement requirement : descriptor.getRequirements() ) {
                String name = requirement.getFieldName();
                RequirementRecipe requirementRecipe = new RequirementRecipe(descriptor, requirement, getContainer(), name == null);

                if (name != null) {
                    recipe.setProperty(name, requirementRecipe);
                } else {
                    recipe.setAutoMatchProperty(requirement.getRole(), requirementRecipe);
                }
            }

            // add configuration data
            if (shouldConfigure(descriptor )) {
                PlexusConfiguration configuration = descriptor.getConfiguration();
                if (configuration != null) {
                    for (String name : configuration.getAttributeNames()) {
                        String value;
                        try {
                            value = configuration.getAttribute(name);
                        } catch (PlexusConfigurationException e) {
                            throw new ComponentInstantiationException("Error getting value for attribute " + name, e);
                        }
                        name = fromXML(name);
                        recipe.setProperty(name, value);
                    }
                    for (PlexusConfiguration child : configuration.getChildren()) {
                        String name = child.getName();
                        name = fromXML(name);
                        if ( child.getChildCount() > 0 )
                        {
                            recipe.setProperty( name, new PlexusConfigurationRecipe( child ) );
                        }
                        else if ( StringUtils.isNotEmpty( child.getValue() ) )
                        {
                            recipe.setProperty( name, child.getValue() );
                        }
                    }
                }
            }
        }
        return recipe;
    }

    protected boolean shouldConfigure( ComponentDescriptor descriptor ) {
        String configuratorId = descriptor.getComponentConfigurator();

        if (StringUtils.isEmpty(configuratorId)) {
            return true;
        }

        try {
            ComponentConfigurator componentConfigurator = getContainer().lookup(ComponentConfigurator.class, configuratorId);
            return componentConfigurator == null || componentConfigurator.getClass().equals(BasicComponentConfigurator.class);
        } catch (ComponentLookupException e) {
        }

        return true;
    }

    protected String fromXML(String elementName) {
        return StringUtils.lowercaseFirstLetter(StringUtils.removeAndHump(elementName, "-"));
    }

    protected void startComponentLifecycle(Object component, ClassRealm realm) throws ComponentLifecycleException {
        try {
            componentManager.start(component);
        } catch (Exception e) {
            Throwable cause = e;
            // if we got a PhaseExecutionException, unwrap it
            if ( e instanceof PhaseExecutionException && e.getCause() != null )
            {
                cause = e.getCause();
            }

            throw new ComponentLifecycleException("Error invoking start method", cause);
        }
    }

    public static class RequirementRecipe extends AbstractRecipe {
        private ComponentDescriptor componentDescriptor;
        private ComponentRequirement requirement;
        private MutablePlexusContainer container;
        private boolean autoMatch;

        public RequirementRecipe(ComponentDescriptor componentDescriptor, ComponentRequirement requirement, MutablePlexusContainer container, boolean autoMatch) {
            this.componentDescriptor = componentDescriptor;
            this.requirement = requirement;
            this.container = container;
            this.autoMatch = autoMatch;
        }

        public boolean canCreate(Type expectedType) {
            if (!autoMatch)
            {
                return true;
            }

            Class propertyType = toClass(expectedType);

            // Never auto match array, map or collection
            if (propertyType.isArray() || Map.class.isAssignableFrom(propertyType) || Collection.class.isAssignableFrom(propertyType) || requirement instanceof ComponentRequirementList) {
                return false;
            }

            // if the type to be created is an instance of the expected type, return true
            try {

                String roleHint = requirement.getRoleHint();
                Class roleType = getInterfaceClass( container, requirement.getRole(), roleHint );

                for ( ComponentDescriptor descriptor : container.getComponentDescriptorList( roleType ) )
                {
                    if ( descriptor.getRoleHint().equals( roleHint ) && isAssignableFrom( propertyType, descriptor.getImplementationClass() ) )
                    {
                        return true;
                    }
                }
            } catch (Exception e) {
            }

            return false;
        }

        @Override
        protected Object internalCreate(Type expectedType, boolean lazyRefAllowed) throws ConstructionException
        {
            Class propertyType = toClass(expectedType);

            // push requirement property name on the stack
            setComponentStackProperty( requirement.getFieldName() );
            try
            {
                Class roleType = getInterfaceClass( container, requirement.getRole(), requirement.getRoleHint() );
                List roleHints = null;
                if ( requirement instanceof ComponentRequirementList )
                {
                    roleHints = ( (ComponentRequirementList) requirement ).getRoleHints();
                }

                Object assignment;
                if ( propertyType.isArray() )
                {
                    assignment = new ArrayList( container.lookupList( roleType, roleHints ) );
                }

                // Map.class.isAssignableFrom( clazz ) doesn't make sense, since Map.class doesn't really
                // have a meaningful superclass.
                else
                {
                    if ( Map.class.equals( propertyType ) )
                    {
                        // todo this is a lazy map

                        // get component type
                        Type keyType = Object.class;
                        Type valueType = Object.class;
                        Type[] typeParameters = RecipeHelper.getTypeParameters( Collection.class, expectedType );
                        if ( typeParameters != null && typeParameters.length == 2 )
                        {
                            if ( typeParameters[0] instanceof Class )
                            {
                                keyType = typeParameters[0];
                            }
                            if ( typeParameters[1] instanceof Class )
                            {
                                valueType = typeParameters[1];
                            }
                        }

                        // if no generic type, load the roll as a class
                        Class valueClass = toClass( valueType );
                        if ( valueClass.equals( Object.class ) )
                        {
                            valueClass = roleType;
                        }

                        // todo verify key type is String

                        assignment = new LiveMap( container,
                            roleType,
                            roleHints,
                            componentDescriptor.getHumanReadableKey() );
                    }
                    // List.class.isAssignableFrom( clazz ) doesn't make sense, since List.class doesn't really
                    // have a meaningful superclass other than Collection.class, which we'll handle next.
                    else if ( List.class.equals( propertyType ) )
                    {
                        // get component type
                        Type[] typeParameters = RecipeHelper.getTypeParameters( Collection.class, expectedType );
                        Type componentType = Object.class;
                        if ( typeParameters != null && typeParameters.length == 1 && typeParameters[0] instanceof Class )
                        {
                            componentType = typeParameters[0];
                        }

                        // if no generic type, load the roll as a class
                        Class componentClass = toClass( componentType );
                        if ( componentClass.equals( Object.class ) )
                        {
                            componentClass = roleType;
                        }

                        assignment = new LiveList( container,
                            roleType,
                            roleHints,
                            componentDescriptor.getHumanReadableKey() );
                    }
                    // Set.class.isAssignableFrom( clazz ) doesn't make sense, since Set.class doesn't really
                    // have a meaningful superclass other than Collection.class, and that would make this
                    // if-else cascade unpredictable (both List and Set extend Collection, so we'll put another
                    // check in for Collection.class.
                    else if ( Set.class.equals( propertyType ) || Collection.class.isAssignableFrom( propertyType ) )
                    {
                        // todo why isn't this lazy as above?
                        assignment = container.lookupMap( roleType, roleHints );
                    }
                    else if ( Logger.class.equals( propertyType ) )
                    {
                        // todo magic reference types should not be handled here
                        assignment = container.getLoggerManager().getLoggerForComponent(
                            componentDescriptor.getRole() );
                    }
                    else if ( PlexusContainer.class.equals( propertyType ) )
                    {
                        // todo magic reference types should not be handled here
                        assignment = container;
                    }
                    else
                    {
                        String roleHint = requirement.getRoleHint();
                        assignment = container.lookup( roleType, roleHint );
                    }
                }

                return assignment;
            }
            catch ( ComponentLookupException e )
            {
                // simply wrap exception, so it can be unwrapped and rethrown
                throw new ConstructionException( e );
            }
            finally
            {
                setComponentStackProperty( null );
            }
        }

        @Override
        public String toString() {
            return "RequirementRecipe[fieldName=" + requirement.getFieldName() + ", role=" + componentDescriptor.getRole() + "]";
        }
    }

    private class PlexusConfigurationRecipe extends AbstractRecipe
    {
        private final PlexusConfiguration child;

        public PlexusConfigurationRecipe( PlexusConfiguration child )
        {
            this.child = child;
        }

        public boolean canCreate( Type type )
        {
            try
            {
                ConverterLookup lookup = createConverterLookup();
                lookup.lookupConverterForType( toClass( type ) );
                return true;
            }
            catch ( ComponentConfigurationException e )
            {
                return false;
            }
        }

        @Override
        protected Object internalCreate( Type expectedType, boolean lazyRefAllowed ) throws ConstructionException
        {
            setComponentStackProperty( child.getName() );
            try
            {
                ConverterLookup lookup = createConverterLookup();
                ConfigurationConverter converter = lookup.lookupConverterForType( toClass( expectedType ) );

                // todo this will not work for static factories
                ObjectRecipe caller = (ObjectRecipe) RecipeHelper.getCaller();
                Class parentClass = toClass( caller.getType() );

                Object value = converter.fromConfiguration(
                    lookup,
                    child,
                    toClass( expectedType ),
                    parentClass,
                    Thread.currentThread().getContextClassLoader(),
                    new DefaultExpressionEvaluator() );

                return value;
            }
            catch ( ComponentConfigurationException e )
            {
                // simply wrap exception, so it can be unwrapped and rethrown
                throw new ConstructionException( e );
            }
            finally
            {
                setComponentStackProperty( null );
            }
        }

        private ConverterLookup createConverterLookup()
        {
            ClassRealm realm = (ClassRealm) Thread.currentThread().getContextClassLoader();
            ConverterLookup lookup = new DefaultConverterLookup();
            lookup.registerConverter( new ClassRealmConverter( realm ) );
            return lookup;
        }
    }


    private void processMapOrientedComponent(ComponentDescriptor descriptor, MapOrientedComponent mapOrientedComponent, ClassRealm realm) throws ComponentInstantiationException {
        MutablePlexusContainer container = getContainer();

        for (ComponentRequirement requirement : descriptor.getRequirements()) {
            String role = requirement.getRole();
            String hint = requirement.getRoleHint();
            String mappingType = requirement.getFieldMappingType();


            try
            {
                // if the hint is not empty (and not default), we don't care about mapping type...
                // it's a single-value, not a collection.
                Object value;
                if (StringUtils.isNotEmpty(hint) && !hint.equals(PlexusConstants.PLEXUS_DEFAULT_HINT)) {
                    value = container.lookup(role, hint);
                } else if ("single".equals(mappingType)) {
                    value = container.lookup(role, hint);
                } else if ("map".equals(mappingType)) {
                    value = container.lookupMap(role);
                } else if ("set".equals(mappingType)) {
                    value = new HashSet(container.lookupList(role));
                } else {
                    value = container.lookup(role, hint);
                }

                mapOrientedComponent.addComponentRequirement(requirement, value);
            }
            catch ( ComponentLookupException e )
            {
                throw new ComponentInstantiationException( "Error looking up requirement of MapOrientedComponent ", e );
            }
            catch ( ComponentConfigurationException e )
            {
                throw new ComponentInstantiationException( "Error adding requirement to MapOrientedComponent ", e );
            }
        }

        MapConverter converter = new MapConverter();
        ConverterLookup converterLookup = new DefaultConverterLookup();
        DefaultExpressionEvaluator expressionEvaluator = new DefaultExpressionEvaluator();
        PlexusConfiguration configuration = container.getConfigurationSource().getConfiguration( descriptor );

        if ( configuration != null )
        {
            try
            {
                Map context = (Map) converter.fromConfiguration(converterLookup,
                                                                configuration,
                                                                null,
                                                                null,
                                                                realm,
                                                                expressionEvaluator,
                                                                null );

                mapOrientedComponent.setComponentConfiguration( context );
            }
            catch ( ComponentConfigurationException e )
            {
                throw new ComponentInstantiationException( "Error adding configuration to MapOrientedComponent ", e );
            }
        }
    }

    private static Class getInterfaceClass( PlexusContainer container, String role, String hint )
    {
        if ( hint == null ) hint = PLEXUS_DEFAULT_HINT;

        try
        {
            ClassRealm realm = container.getLookupRealm();

            if ( realm != null )
            {
                return realm.loadClass( role );
            }
        }
        catch ( Throwable e )
        {
        }

        try
        {
            ClassLoader loader = Thread.currentThread().getContextClassLoader();
            if ( loader != null )
            {
                return loader.loadClass( role );
            }
        }
        catch ( Throwable e )
        {
        }

        try
        {
            ComponentDescriptor cd = container.getComponentDescriptor( role, hint );
            if ( cd != null )
            {
                ClassLoader loader = cd.getImplementationClass().getClassLoader();
                if ( loader != null )
                {
                    return loader.loadClass( role );
                }
            }
        }
        catch ( Throwable ignored )
        {
        }

        return Object.class;
    }

    /**
     * There are a few bugs in XBean reflect where a constuction exception is wrapped with another constuction exception.
     */
    private static Throwable unwrapConstructionException( ConstructionException e )
    {
        Throwable cause = e;
        while ( cause instanceof ConstructionException && e.getCause() != null)
        {
            cause = cause.getCause();
        }
        return cause;
    }
}