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}