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}