    001// Copyright 2007, 2008, 2009, 2010, 2011 The Apache Software Foundation
    002//
    003// Licensed under the Apache License, Version 2.0 (the "License");
    004// you may not use this file except in compliance with the License.
    005// You may obtain a copy of the License at
    006//
    007// http://www.apache.org/licenses/LICENSE-2.0
    008//
    009// Unless required by applicable law or agreed to in writing, software
    010// distributed under the License is distributed on an "AS IS" BASIS,
    011// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    012// See the License for the specific language governing permissions and
    013// limitations under the License.
    014
    015package org.apache.tapestry5.internal.spring;
    016
    017import org.apache.tapestry5.internal.AbstractContributionDef;
    018import org.apache.tapestry5.ioc.*;
    019import org.apache.tapestry5.ioc.annotations.Primary;
    020import org.apache.tapestry5.ioc.def.ContributionDef;
    021import org.apache.tapestry5.ioc.def.DecoratorDef;
    022import org.apache.tapestry5.ioc.def.ModuleDef;
    023import org.apache.tapestry5.ioc.def.ServiceDef;
    024import org.apache.tapestry5.ioc.internal.util.CollectionFactory;
    025import org.apache.tapestry5.ioc.internal.util.InternalUtils;
    026import org.apache.tapestry5.ioc.services.RegistryShutdownHub;
    027import org.apache.tapestry5.plastic.PlasticUtils;
    028import org.apache.tapestry5.spring.ApplicationContextCustomizer;
    029import org.apache.tapestry5.spring.SpringConstants;
    030import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
    031import org.springframework.context.ApplicationContext;
    032import org.springframework.context.ConfigurableApplicationContext;
    033import org.springframework.core.SpringVersion;
    034import org.springframework.web.context.ConfigurableWebApplicationContext;
    035import org.springframework.web.context.WebApplicationContext;
    036import org.springframework.web.context.support.WebApplicationContextUtils;
    037
    038import javax.servlet.ServletContext;
    039import java.util.Collections;
    040import java.util.Map;
    041import java.util.Set;
    042import java.util.concurrent.atomic.AtomicBoolean;
    043
    044/**
    045 * A wrapper that converts a Spring {@link ApplicationContext} into a set of service definitions,
    046 * compatible with
    047 * Tapestry 5 IoC, for the beans defined in the context, as well as the context itself.
    048 */
    049public class SpringModuleDef implements ModuleDef
    050{
    051    static final String SERVICE_ID = "ApplicationContext";
    052
    053    private final Map<String, ServiceDef> services = CollectionFactory.newMap();
    054
    055    private final boolean compatibilityMode;
    056
    057    private final AtomicBoolean applicationContextCreated = new AtomicBoolean(false);
    058
    059    private final ServletContext servletContext;
    060
    061    private ApplicationContext locateExternalContext()
    062    {
    063        ApplicationContext context = locateApplicationContext(servletContext);
    064
    065        applicationContextCreated.set(true);
    066
    067        return context;
    068    }
    069
    070    /**
    071     * Invoked to obtain the Spring ApplicationContext, presumably stored in the ServletContext.
    072     * This method is only used in Tapestry 5.0 compatibility mode (in Tapestry 5.1 and above,
    073     * the default is for Tapestry to <em>create</em> the ApplicationContext).
    074     *
    075     * @param servletContext used to locate the ApplicationContext
    076     * @return the ApplicationContext itself
    077     * @throws RuntimeException if the ApplicationContext could not be located or is otherwise invalid
    078     * @since 5.2.0
    079     */
    080    protected ApplicationContext locateApplicationContext(ServletContext servletContext)
    081    {
    082        ApplicationContext context = WebApplicationContextUtils.getWebApplicationContext(servletContext);
    083
    084        if (context == null)
    085        {
    086            throw new NullPointerException(
    087                    String
    088                            .format(
    089                                    "No Spring ApplicationContext stored in the ServletContext as attribute '%s'. "
    090                                            + "You should either re-enable Tapestry as the creator of the ApplicationContext, or "
    091                                            + "add a Spring ContextLoaderListener to web.xml.",
    092                                    WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE));
    093        }
    094
    095        return context;
    096    }
    097
    098    public SpringModuleDef(ServletContext servletContext)
    099    {
    100        this.servletContext = servletContext;
    101
    102        compatibilityMode = Boolean.parseBoolean(servletContext
    103                .getInitParameter(SpringConstants.USE_EXTERNAL_SPRING_CONTEXT));
    104
    105        final ApplicationContext externalContext = compatibilityMode ? locateExternalContext()
    106                : null;
    107
    108        if (compatibilityMode)
    109            addServiceDefsForSpringBeans(externalContext);
    110
    111        ServiceDef applicationContextServiceDef = new ServiceDef()
    112        {
    113            @Override
    114            public ObjectCreator createServiceCreator(final ServiceBuilderResources resources)
    115            {
    116                if (compatibilityMode)
    117                    return new StaticObjectCreator(externalContext,
    118                            "externally configured Spring ApplicationContext");
    119
    120                ApplicationContextCustomizer customizer = resources.getService(
    121                        "ApplicationContextCustomizer", ApplicationContextCustomizer.class);
    122
    123                return constructObjectCreatorForApplicationContext(resources, customizer);
    124            }
    125
    126            @Override
    127            public String getServiceId()
    128            {
    129                return SERVICE_ID;
    130            }
    131
    132            @Override
    133            public Set<Class> getMarkers()
    134            {
    135                return Collections.emptySet();
    136            }
    137
    138            @Override
    139            public Class getServiceInterface()
    140            {
    141                return compatibilityMode ? externalContext.getClass()
    142                        : ConfigurableWebApplicationContext.class;
    143            }
    144
    145            @Override
    146            public String getServiceScope()
    147            {
    148                return ScopeConstants.DEFAULT;
    149            }
    150
    151            @Override
    152            public boolean isEagerLoad()
    153            {
    154                return false;
    155            }
    156        };
    157
    158        services.put(SERVICE_ID, applicationContextServiceDef);
    159    }
    160
    161    private void addServiceDefsForSpringBeans(ApplicationContext context)
    162    {
    163        ConfigurableListableBeanFactory beanFactory = null;
    164        if (context instanceof ConfigurableApplicationContext)
    165        {
    166            beanFactory = ((ConfigurableApplicationContext) context).getBeanFactory();
    167        }
    168
    169        for (final String beanName : context.getBeanDefinitionNames())
    170        {
    171            boolean isAbstract = false;
    172            if (beanFactory != null)
    173            {
    174                isAbstract = beanFactory.getBeanDefinition(beanName).isAbstract();
    175            }
    176
    177            if (!isAbstract)
    178            {
    179                String trueName = beanName.startsWith("&") ? beanName.substring(1) : beanName;
    180
    181                services.put(trueName, new SpringBeanServiceDef(trueName, context));
    182            }
    183        }
    184    }
    185
    186    private ObjectCreator constructObjectCreatorForApplicationContext(
    187            final ServiceBuilderResources resources, @Primary
    188    ApplicationContextCustomizer customizer)
    189    {
    190        final CustomizingContextLoader loader = new CustomizingContextLoader(customizer);
    191
    192        final Runnable shutdownListener = new Runnable()
    193        {
    194            @Override
    195            public void run()
    196            {
    197                loader.closeWebApplicationContext(servletContext);
    198            }
    199        };
    200
    201        final RegistryShutdownHub shutdownHub = resources.getService(RegistryShutdownHub.class);
    202
    203        return new ObjectCreator()
    204        {
    205            @Override
    206            public Object createObject()
    207            {
    208                return resources.getTracker().invoke(
    209                        "Creating Spring ApplicationContext via ContextLoader",
    210                        new Invokable<Object>()
    211                        {
    212                            @Override
    213                            public Object invoke()
    214                            {
    215                                resources.getLogger().info(
    216                                        String.format("Starting Spring (version %s)", SpringVersion
    217                                                .getVersion()));
    218
    219                                WebApplicationContext context = loader
    220                                        .initWebApplicationContext(servletContext);
    221
    222                                shutdownHub.addRegistryShutdownListener(shutdownListener);
    223
    224                                applicationContextCreated.set(true);
    225
    226                                return context;
    227                            }
    228                        });
    229            }
    230
    231            @Override
    232            public String toString()
    233            {
    234                return "ObjectCreator for Spring ApplicationContext";
    235            }
    236        };
    237    }
    238
    239    @Override
    240    public Class getBuilderClass()
    241    {
    242        return null;
    243    }
    244
    245    /**
    246     * Returns a contribution, "SpringBean", to the MasterObjectProvider service. It is ordered
    247     * after the built-in
    248     * contributions.
    249     */
    250    @Override
    251    public Set<ContributionDef> getContributionDefs()
    252    {
    253        ContributionDef def = createContributionToMasterObjectProvider();
    254
    255        return CollectionFactory.newSet(def);
    256    }
    257
    258    private ContributionDef createContributionToMasterObjectProvider()
    259    {
    260
    261        ContributionDef def = new AbstractContributionDef()
    262        {
    263            @Override
    264            public String getServiceId()
    265            {
    266                return "MasterObjectProvider";
    267            }
    268
    269            @Override
    270            public void contribute(ModuleBuilderSource moduleSource, ServiceResources resources,
    271                                   OrderedConfiguration configuration)
    272            {
    273                final OperationTracker tracker = resources.getTracker();
    274
    275                final ApplicationContext context = resources.getService(SERVICE_ID,
    276                        ApplicationContext.class);
    277
    278                final ObjectProvider springBeanProvider = new ObjectProvider()
    279                {
    280                    @Override
    281                    public <T> T provide(Class<T> objectType,
    282                                         AnnotationProvider annotationProvider, ObjectLocator locator)
    283                    {
    284
    285                        Map beanMap = context.getBeansOfType(objectType);
    286
    287                        switch (beanMap.size())
    288                        {
    289                            case 0:
    290                                return null;
    291
    292                            case 1:
    293
    294                                Object bean = beanMap.values().iterator().next();
    295
    296                                return objectType.cast(bean);
    297
    298                            default:
    299
    300                                String message = String
    301                                        .format(
    302                                                "Spring context contains %d beans assignable to type %s: %s.",
    303                                                beanMap.size(), PlasticUtils.toTypeName(objectType), InternalUtils
    304                                                .joinSorted(beanMap.keySet()));
    305
    306                                throw new IllegalArgumentException(message);
    307                        }
    308                    }
    309                };
    310
    311                final ObjectProvider springBeanProviderInvoker = new ObjectProvider()
    312                {
    313                    @Override
    314                    public <T> T provide(final Class<T> objectType,
    315                                         final AnnotationProvider annotationProvider, final ObjectLocator locator)
    316                    {
    317                        return tracker.invoke(
    318                                "Resolving dependency by searching Spring ApplicationContext",
    319                                new Invokable<T>()
    320                                {
    321                                    @Override
    322                                    public T invoke()
    323                                    {
    324                                        return springBeanProvider.provide(objectType,
    325                                                annotationProvider, locator);
    326                                    }
    327                                });
    328                    }
    329                };
    330
    331                ObjectProvider outerCheck = new ObjectProvider()
    332                {
    333                    @Override
    334                    public <T> T provide(Class<T> objectType,
    335                                         AnnotationProvider annotationProvider, ObjectLocator locator)
    336                    {
    337                        // I think the following line is the only reason we put the
    338                        // SpringBeanProvider here,
    339                        // rather than in SpringModule.
    340
    341                        if (!applicationContextCreated.get())
    342                            return null;
    343
    344                        return springBeanProviderInvoker.provide(objectType, annotationProvider,
    345                                locator);
    346                    }
    347                };
    348
    349
    350                configuration.add("SpringBean", outerCheck, "after:AnnotationBasedContributions", "after:ServiceOverride");
    351            }
    352        };
    353
    354        return def;
    355    }
    356
    357    /**
    358     * Returns an empty set.
    359     */
    360    @Override
    361    public Set<DecoratorDef> getDecoratorDefs()
    362    {
    363        return Collections.emptySet();
    364    }
    365
    366    @Override
    367    public String getLoggerName()
    368    {
    369        return SpringModuleDef.class.getName();
    370    }
    371
    372    @Override
    373    public ServiceDef getServiceDef(String serviceId)
    374    {
    375        return services.get(serviceId);
    376    }
    377
    378    @Override
    379    public Set<String> getServiceIds()
    380    {
    381        return services.keySet();
    382    }
    383}