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