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