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}