    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
    015    package org.apache.tapestry5.internal.spring;
    016
    017    import org.apache.tapestry5.internal.AbstractContributionDef;
    018    import org.apache.tapestry5.ioc.*;
    019    import org.apache.tapestry5.ioc.annotations.Primary;
    020    import org.apache.tapestry5.ioc.def.ContributionDef;
    021    import org.apache.tapestry5.ioc.def.DecoratorDef;
    022    import org.apache.tapestry5.ioc.def.ModuleDef;
    023    import org.apache.tapestry5.ioc.def.ServiceDef;
    024    import org.apache.tapestry5.ioc.internal.util.CollectionFactory;
    025    import org.apache.tapestry5.ioc.internal.util.InternalUtils;
    026    import org.apache.tapestry5.ioc.services.ClassFabUtils;
    027    import org.apache.tapestry5.ioc.services.RegistryShutdownHub;
    028    import org.apache.tapestry5.spring.ApplicationContextCustomizer;
    029    import org.apache.tapestry5.spring.SpringConstants;
    030    import org.springframework.beans.factory.BeanFactoryUtils;
    031    import org.springframework.context.ApplicationContext;
    032    import org.springframework.core.SpringVersion;
    033    import org.springframework.web.context.ConfigurableWebApplicationContext;
    034    import org.springframework.web.context.WebApplicationContext;
    035    import org.springframework.web.context.support.WebApplicationContextUtils;
    036
    037    import javax.servlet.ServletContext;
    038    import java.util.Collections;
    039    import java.util.Map;
    040    import java.util.Set;
    041    import 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     */
    048    public 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                public ObjectCreator createServiceCreator(final ServiceBuilderResources resources)
    113                {
    114                    if (compatibilityMode)
    115                        return new StaticObjectCreator(externalContext,
    116                                "externally configured Spring ApplicationContext");
    117
    118                    ApplicationContextCustomizer customizer = resources.getService(
    119                            "ApplicationContextCustomizer", ApplicationContextCustomizer.class);
    120
    121                    return constructObjectCreatorForApplicationContext(resources, customizer);
    122                }
    123
    124                public String getServiceId()
    125                {
    126                    return SERVICE_ID;
    127                }
    128
    129                public Set<Class> getMarkers()
    130                {
    131                    return Collections.emptySet();
    132                }
    133
    134                public Class getServiceInterface()
    135                {
    136                    return compatibilityMode ? externalContext.getClass()
    137                            : ConfigurableWebApplicationContext.class;
    138                }
    139
    140                public String getServiceScope()
    141                {
    142                    return ScopeConstants.DEFAULT;
    143                }
    144
    145                public boolean isEagerLoad()
    146                {
    147                    return false;
    148                }
    149            };
    150
    151            services.put(SERVICE_ID, applicationContextServiceDef);
    152        }
    153
    154        private void addServiceDefsForSpringBeans(ApplicationContext context)
    155        {
    156            for (final String beanName : context.getBeanDefinitionNames())
    157            {
    158                String trueName = beanName.startsWith("&") ? beanName.substring(1) : beanName;
    159
    160                services.put(trueName, new SpringBeanServiceDef(trueName, context));
    161            }
    162        }
    163
    164        private ObjectCreator constructObjectCreatorForApplicationContext(
    165                final ServiceBuilderResources resources, @Primary
    166        ApplicationContextCustomizer customizer)
    167        {
    168            final CustomizingContextLoader loader = new CustomizingContextLoader(customizer);
    169
    170            final Runnable shutdownListener = new Runnable()
    171            {
    172                public void run()
    173                {
    174                    loader.closeWebApplicationContext(servletContext);
    175                }
    176            };
    177
    178            final RegistryShutdownHub shutdownHub = resources.getService(RegistryShutdownHub.class);
    179
    180            return new ObjectCreator()
    181            {
    182                public Object createObject()
    183                {
    184                    return resources.getTracker().invoke(
    185                            "Creating Spring ApplicationContext via ContextLoader",
    186                            new Invokable<Object>()
    187                            {
    188                                public Object invoke()
    189                                {
    190                                    resources.getLogger().info(
    191                                            String.format("Starting Spring (version %s)", SpringVersion
    192                                                    .getVersion()));
    193
    194                                    WebApplicationContext context = loader
    195                                            .initWebApplicationContext(servletContext);
    196
    197                                    shutdownHub.addRegistryShutdownListener(shutdownListener);
    198
    199                                    applicationContextCreated.set(true);
    200
    201                                    return context;
    202                                }
    203                            });
    204                }
    205
    206                @Override
    207                public String toString()
    208                {
    209                    return "ObjectCreator for Spring ApplicationContext";
    210                }
    211            };
    212        }
    213
    214        public Class getBuilderClass()
    215        {
    216            return null;
    217        }
    218
    219        /**
    220         * Returns a contribution, "SpringBean", to the MasterObjectProvider service. It is ordered
    221         * after the built-in
    222         * contributions.
    223         */
    224        public Set<ContributionDef> getContributionDefs()
    225        {
    226            ContributionDef def = createContributionToMasterObjectProvider();
    227
    228            return CollectionFactory.newSet(def);
    229        }
    230
    231        private ContributionDef createContributionToMasterObjectProvider()
    232        {
    233
    234            ContributionDef def = new AbstractContributionDef()
    235            {
    236                public String getServiceId()
    237                {
    238                    return "MasterObjectProvider";
    239                }
    240
    241                @Override
    242                public void contribute(ModuleBuilderSource moduleSource, ServiceResources resources,
    243                                       OrderedConfiguration configuration)
    244                {
    245                    final OperationTracker tracker = resources.getTracker();
    246
    247                    final ApplicationContext context = resources.getService(SERVICE_ID,
    248                            ApplicationContext.class);
    249
    250                    final ObjectProvider springBeanProvider = new ObjectProvider()
    251                    {
    252                        public <T> T provide(Class<T> objectType,
    253                                             AnnotationProvider annotationProvider, ObjectLocator locator)
    254                        {
    255
    256                            Map beanMap = context.getBeansOfType(objectType);
    257
    258                            switch (beanMap.size())
    259                            {
    260                                case 0:
    261                                    return null;
    262
    263                                case 1:
    264
    265                                    Object bean = beanMap.values().iterator().next();
    266
    267                                    return objectType.cast(bean);
    268
    269                                default:
    270
    271                                    String message = String
    272                                            .format(
    273                                                    "Spring context contains %d beans assignable to type %s: %s.",
    274                                                    beanMap.size(), ClassFabUtils
    275                                                    .toJavaClassName(objectType), InternalUtils
    276                                                    .joinSorted(beanMap.keySet()));
    277
    278                                    throw new IllegalArgumentException(message);
    279                            }
    280                        }
    281                    };
    282
    283                    final ObjectProvider springBeanProviderInvoker = new ObjectProvider()
    284                    {
    285                        public <T> T provide(final Class<T> objectType,
    286                                             final AnnotationProvider annotationProvider, final ObjectLocator locator)
    287                        {
    288                            return tracker.invoke(
    289                                    "Resolving dependency by searching Spring ApplicationContext",
    290                                    new Invokable<T>()
    291                                    {
    292                                        public T invoke()
    293                                        {
    294                                            return springBeanProvider.provide(objectType,
    295                                                    annotationProvider, locator);
    296                                        }
    297                                    });
    298                        }
    299                    };
    300
    301                    ObjectProvider outerCheck = new ObjectProvider()
    302                    {
    303                        public <T> T provide(Class<T> objectType,
    304                                             AnnotationProvider annotationProvider, ObjectLocator locator)
    305                        {
    306                            // I think the following line is the only reason we put the
    307                            // SpringBeanProvider here,
    308                            // rather than in SpringModule.
    309
    310                            if (!applicationContextCreated.get())
    311                                return null;
    312
    313                            return springBeanProviderInvoker.provide(objectType, annotationProvider,
    314                                    locator);
    315                        }
    316                    };
    317
    318
    319                    configuration.add("SpringBean", outerCheck, "after:AnnotationBasedContributions", "after:ServiceOverride");
    320                }
    321            };
    322
    323            return def;
    324        }
    325
    326        /**
    327         * Returns an empty set.
    328         */
    329        public Set<DecoratorDef> getDecoratorDefs()
    330        {
    331            return Collections.emptySet();
    332        }
    333
    334        public String getLoggerName()
    335        {
    336            return SpringModuleDef.class.getName();
    337        }
    338
    339        public ServiceDef getServiceDef(String serviceId)
    340        {
    341            return services.get(serviceId);
    342        }
    343
    344        public Set<String> getServiceIds()
    345        {
    346            return services.keySet();
    347        }
    348    }