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            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(), PlasticUtils.toTypeName(objectType), InternalUtils
275                                                .joinSorted(beanMap.keySet()));
276
277                                throw new IllegalArgumentException(message);
278                        }
279                    }
280                };
281
282                final ObjectProvider springBeanProviderInvoker = new ObjectProvider()
283                {
284                    public <T> T provide(final Class<T> objectType,
285                                         final AnnotationProvider annotationProvider, final ObjectLocator locator)
286                    {
287                        return tracker.invoke(
288                                "Resolving dependency by searching Spring ApplicationContext",
289                                new Invokable<T>()
290                                {
291                                    public T invoke()
292                                    {
293                                        return springBeanProvider.provide(objectType,
294                                                annotationProvider, locator);
295                                    }
296                                });
297                    }
298                };
299
300                ObjectProvider outerCheck = new ObjectProvider()
301                {
302                    public <T> T provide(Class<T> objectType,
303                                         AnnotationProvider annotationProvider, ObjectLocator locator)
304                    {
305                        // I think the following line is the only reason we put the
306                        // SpringBeanProvider here,
307                        // rather than in SpringModule.
308
309                        if (!applicationContextCreated.get())
310                            return null;
311
312                        return springBeanProviderInvoker.provide(objectType, annotationProvider,
313                                locator);
314                    }
315                };
316
317
318                configuration.add("SpringBean", outerCheck, "after:AnnotationBasedContributions", "after:ServiceOverride");
319            }
320        };
321
322        return def;
323    }
324
325    /**
326     * Returns an empty set.
327     */
328    public Set<DecoratorDef> getDecoratorDefs()
329    {
330        return Collections.emptySet();
331    }
332
333    public String getLoggerName()
334    {
335        return SpringModuleDef.class.getName();
336    }
337
338    public ServiceDef getServiceDef(String serviceId)
339    {
340        return services.get(serviceId);
341    }
342
343    public Set<String> getServiceIds()
344    {
345        return services.keySet();
346    }
347}