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}