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    }