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