001 // Copyright 2006, 2007, 2008, 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.services; 016 017 import org.apache.tapestry5.ComponentResources; 018 import org.apache.tapestry5.SymbolConstants; 019 import org.apache.tapestry5.internal.InternalComponentResources; 020 import org.apache.tapestry5.internal.InternalConstants; 021 import org.apache.tapestry5.internal.model.MutableComponentModelImpl; 022 import org.apache.tapestry5.internal.plastic.PlasticInternalUtils; 023 import org.apache.tapestry5.ioc.Invokable; 024 import org.apache.tapestry5.ioc.LoggerSource; 025 import org.apache.tapestry5.ioc.OperationTracker; 026 import org.apache.tapestry5.ioc.Resource; 027 import org.apache.tapestry5.ioc.annotations.PostInjection; 028 import org.apache.tapestry5.ioc.annotations.Primary; 029 import org.apache.tapestry5.ioc.annotations.Symbol; 030 import org.apache.tapestry5.ioc.internal.services.ClassFactoryImpl; 031 import org.apache.tapestry5.ioc.internal.services.PlasticProxyFactoryImpl; 032 import org.apache.tapestry5.ioc.internal.util.ClasspathResource; 033 import org.apache.tapestry5.ioc.internal.util.CollectionFactory; 034 import org.apache.tapestry5.ioc.internal.util.InternalUtils; 035 import org.apache.tapestry5.ioc.internal.util.URLChangeTracker; 036 import org.apache.tapestry5.ioc.services.Builtin; 037 import org.apache.tapestry5.ioc.services.ClassFactory; 038 import org.apache.tapestry5.ioc.services.ClasspathURLConverter; 039 import org.apache.tapestry5.ioc.services.PlasticProxyFactory; 040 import org.apache.tapestry5.model.ComponentModel; 041 import org.apache.tapestry5.model.MutableComponentModel; 042 import org.apache.tapestry5.plastic.*; 043 import org.apache.tapestry5.plastic.PlasticManager.PlasticManagerBuilder; 044 import org.apache.tapestry5.runtime.Component; 045 import org.apache.tapestry5.runtime.ComponentEvent; 046 import org.apache.tapestry5.runtime.ComponentResourcesAware; 047 import org.apache.tapestry5.runtime.PageLifecycleListener; 048 import org.apache.tapestry5.services.*; 049 import org.apache.tapestry5.services.transform.ComponentClassTransformWorker2; 050 import org.apache.tapestry5.services.transform.ControlledPackageType; 051 import org.apache.tapestry5.services.transform.TransformationSupport; 052 import org.slf4j.Logger; 053 054 import java.util.Map; 055 import java.util.Set; 056 057 /** 058 * A wrapper around a {@link PlasticManager} that allows certain classes to be modified as they are loaded. 059 */ 060 public final class ComponentInstantiatorSourceImpl implements ComponentInstantiatorSource, UpdateListener, 061 InvalidationListener, PlasticManagerDelegate, PlasticClassListener 062 { 063 private final Set<String> controlledPackageNames = CollectionFactory.newSet(); 064 065 private final URLChangeTracker changeTracker; 066 067 private final ClassLoader parent; 068 069 private final ComponentClassTransformWorker2 transformerChain; 070 071 private final LoggerSource loggerSource; 072 073 private final Logger logger; 074 075 private final OperationTracker tracker; 076 077 private final InternalComponentInvalidationEventHub invalidationHub; 078 079 private final boolean productionMode; 080 081 private final ComponentClassResolver resolver; 082 083 // These change whenever the invalidation event hub sends an invalidation notification 084 085 private volatile ClassFactory classFactory; 086 087 private volatile PlasticProxyFactory proxyFactory; 088 089 private volatile PlasticManager manager; 090 091 /** 092 * Map from class name to Instantiator. 093 */ 094 private final Map<String, Instantiator> classToInstantiator = CollectionFactory.newMap(); 095 096 private final Map<String, ComponentModel> classToModel = CollectionFactory.newMap(); 097 098 private final MethodDescription GET_COMPONENT_RESOURCES = PlasticUtils.getMethodDescription( 099 ComponentResourcesAware.class, "getComponentResources"); 100 101 private final ConstructorCallback REGISTER_AS_PAGE_LIFECYCLE_LISTENER = new ConstructorCallback() 102 { 103 public void onConstruct(Object instance, InstanceContext context) 104 { 105 InternalComponentResources resources = context.get(InternalComponentResources.class); 106 107 resources.addPageLifecycleListener((PageLifecycleListener) instance); 108 } 109 }; 110 111 public ComponentInstantiatorSourceImpl(Logger logger, 112 113 LoggerSource loggerSource, 114 115 @Builtin 116 PlasticProxyFactory proxyFactory, 117 118 @Primary 119 ComponentClassTransformWorker2 transformerChain, 120 121 ClasspathURLConverter classpathURLConverter, 122 123 OperationTracker tracker, 124 125 Map<String, ControlledPackageType> configuration, 126 127 @Symbol(SymbolConstants.PRODUCTION_MODE) 128 boolean productionMode, 129 130 ComponentClassResolver resolver, 131 132 InternalComponentInvalidationEventHub invalidationHub) 133 { 134 this.parent = proxyFactory.getClassLoader(); 135 this.transformerChain = transformerChain; 136 this.logger = logger; 137 this.loggerSource = loggerSource; 138 this.changeTracker = new URLChangeTracker(classpathURLConverter); 139 this.tracker = tracker; 140 this.invalidationHub = invalidationHub; 141 this.productionMode = productionMode; 142 this.resolver = resolver; 143 144 // For now, we just need the keys of the configuration. When there are more types of controlled 145 // packages, we'll need to do more. 146 147 controlledPackageNames.addAll(configuration.keySet()); 148 149 initializeService(); 150 } 151 152 @PostInjection 153 public void listenForUpdates(UpdateListenerHub hub) 154 { 155 invalidationHub.addInvalidationListener(this); 156 hub.addUpdateListener(this); 157 } 158 159 public synchronized void checkForUpdates() 160 { 161 if (changeTracker.containsChanges()) 162 { 163 invalidationHub.classInControlledPackageHasChanged(); 164 } 165 } 166 167 public void forceComponentInvalidation() 168 { 169 changeTracker.clear(); 170 invalidationHub.classInControlledPackageHasChanged(); 171 } 172 173 public void objectWasInvalidated() 174 { 175 changeTracker.clear(); 176 classToInstantiator.clear(); 177 178 // Release the existing class pool, loader and so forth. 179 // Create a new one. 180 181 initializeService(); 182 } 183 184 /** 185 * Invoked at object creation, or when there are updates to class files (i.e., invalidation), to create a new set of 186 * Javassist class pools and loaders. 187 */ 188 private void initializeService() 189 { 190 PlasticManagerBuilder builder = PlasticManager.withClassLoader(parent).delegate(this) 191 .packages(controlledPackageNames); 192 193 if (!productionMode) 194 { 195 builder.enable(TransformationOption.FIELD_WRITEBEHIND); 196 } 197 198 manager = builder.create(); 199 200 manager.addPlasticClassListener(this); 201 202 classFactory = new ClassFactoryImpl(manager.getClassLoader(), logger); 203 204 proxyFactory = new PlasticProxyFactoryImpl(manager, logger); 205 206 classToInstantiator.clear(); 207 classToModel.clear(); 208 } 209 210 public synchronized Instantiator getInstantiator(final String className) 211 { 212 Instantiator result = classToInstantiator.get(className); 213 214 if (result == null) 215 { 216 result = createInstantiatorForClass(className); 217 218 classToInstantiator.put(className, result); 219 } 220 221 return result; 222 } 223 224 private Instantiator createInstantiatorForClass(final String className) 225 { 226 return tracker.invoke(String.format("Creating instantiator for component class %s", className), 227 new Invokable<Instantiator>() 228 { 229 public Instantiator invoke() 230 { 231 // Force the creation of the class (and the transformation of the class). This will first 232 // trigger transformations of any base classes. 233 234 final ClassInstantiator<Component> plasticInstantiator = manager.getClassInstantiator(className); 235 236 final ComponentModel model = classToModel.get(className); 237 238 return new Instantiator() 239 { 240 public Component newInstance(InternalComponentResources resources) 241 { 242 return plasticInstantiator.with(ComponentResources.class, resources) 243 .with(InternalComponentResources.class, resources).newInstance(); 244 } 245 246 public ComponentModel getModel() 247 { 248 return model; 249 } 250 251 @Override 252 public String toString() 253 { 254 return String.format("[Instantiator[%s]", className); 255 } 256 }; 257 } 258 }); 259 } 260 261 public boolean exists(String className) 262 { 263 return parent.getResource(PlasticInternalUtils.toClassPath(className)) != null; 264 } 265 266 public ClassFactory getClassFactory() 267 { 268 return classFactory; 269 } 270 271 public PlasticProxyFactory getProxyFactory() 272 { 273 return proxyFactory; 274 } 275 276 public void transform(final PlasticClass plasticClass) 277 { 278 tracker.run(String.format("Running component class transformations on %s", plasticClass.getClassName()), 279 new Runnable() 280 { 281 public void run() 282 { 283 String className = plasticClass.getClassName(); 284 String parentClassName = plasticClass.getSuperClassName(); 285 286 // The parent model may not exist, if the super class is not in a controlled package. 287 288 ComponentModel parentModel = classToModel.get(parentClassName); 289 290 final boolean isRoot = parentModel == null; 291 292 if (isRoot 293 && !(parentClassName.equals("java.lang.Object") || parentClassName 294 .equals("groovy.lang.GroovyObjectSupport"))) 295 { 296 String suggestedPackageName = buildSuggestedPackageName(className); 297 298 throw new RuntimeException(ServicesMessages.baseClassInWrongPackage(parentClassName, 299 className, suggestedPackageName)); 300 } 301 302 // Tapestry 5.2 was more sensitive that the parent class have a public no-args constructor. 303 // Plastic 304 // doesn't care, and we don't have the tools to dig that information out. 305 306 Logger logger = loggerSource.getLogger(className); 307 308 Resource baseResource = new ClasspathResource(parent, PlasticInternalUtils 309 .toClassPath(className)); 310 311 changeTracker.add(baseResource.toURL()); 312 313 if (isRoot) 314 { 315 implementComponentInterface(plasticClass); 316 } 317 318 boolean isPage = resolver.isPage(className); 319 320 boolean superClassImplementsPageLifecycle = plasticClass.isInterfaceImplemented(PageLifecycleListener.class); 321 322 final MutableComponentModel model = new MutableComponentModelImpl(className, logger, baseResource, 323 parentModel, isPage); 324 325 transformerChain.transform(plasticClass, new TransformationSupportImpl(plasticClass, isRoot, model), model); 326 327 if (!superClassImplementsPageLifecycle && plasticClass.isInterfaceImplemented(PageLifecycleListener.class)) 328 { 329 plasticClass.onConstruct(REGISTER_AS_PAGE_LIFECYCLE_LISTENER); 330 } 331 332 classToModel.put(className, model); 333 } 334 }); 335 } 336 337 private void implementComponentInterface(PlasticClass plasticClass) 338 { 339 plasticClass.introduceInterface(Component.class); 340 341 final PlasticField resourcesField = plasticClass.introduceField(InternalComponentResources.class, 342 "internalComponentResources").injectFromInstanceContext(); 343 344 plasticClass.introduceMethod(GET_COMPONENT_RESOURCES, new InstructionBuilderCallback() 345 { 346 public void doBuild(InstructionBuilder builder) 347 { 348 builder.loadThis().getField(resourcesField).returnResult(); 349 } 350 }); 351 } 352 353 public <T> ClassInstantiator<T> configureInstantiator(String className, ClassInstantiator<T> instantiator) 354 { 355 return instantiator; 356 } 357 358 private String buildSuggestedPackageName(String className) 359 { 360 for (String subpackage : InternalConstants.SUBPACKAGES) 361 { 362 String term = "." + subpackage + "."; 363 364 int pos = className.indexOf(term); 365 366 // Keep the leading '.' in the subpackage name and tack on "base". 367 368 if (pos > 0) 369 return className.substring(0, pos + 1) + InternalConstants.BASE_SUBPACKAGE; 370 } 371 372 // Is this even reachable? className should always be in a controlled package and so 373 // some subpackage above should have matched. 374 375 return null; 376 } 377 378 public void classWillLoad(PlasticClassEvent event) 379 { 380 Logger logger = loggerSource.getLogger("tapestry.transformer." + event.getPrimaryClassName()); 381 382 if (logger.isDebugEnabled()) 383 logger.debug(event.getDissasembledBytecode()); 384 } 385 386 private class TransformationSupportImpl implements TransformationSupport 387 { 388 private final PlasticClass plasticClass; 389 390 private final boolean root; 391 392 private final MutableComponentModel model; 393 394 public TransformationSupportImpl(PlasticClass plasticClass, boolean root, MutableComponentModel model) 395 { 396 this.plasticClass = plasticClass; 397 this.root = root; 398 this.model = model; 399 } 400 401 public Class toClass(String typeName) 402 { 403 try 404 { 405 return PlasticInternalUtils.toClass(manager.getClassLoader(), typeName); 406 } catch (ClassNotFoundException ex) 407 { 408 throw new RuntimeException(String.format( 409 "Unable to convert type '%s' to a Class: %s", typeName, 410 InternalUtils.toMessage(ex)), ex); 411 } 412 } 413 414 public boolean isRootTransformation() 415 { 416 return root; 417 } 418 419 public void addEventHandler(final String eventType, final int minContextValues, final String operationDescription, final ComponentEventHandler handler) 420 { 421 assert InternalUtils.isNonBlank(eventType); 422 assert minContextValues >= 0; 423 assert handler != null; 424 425 model.addEventHandler(eventType); 426 427 MethodAdvice advice = new MethodAdvice() 428 { 429 public void advise(MethodInvocation invocation) 430 { 431 final ComponentEvent event = (ComponentEvent) invocation.getParameter(0); 432 433 boolean matches = !event.isAborted() && event.matches(eventType, "", minContextValues); 434 435 if (matches) 436 { 437 final Component instance = (Component) invocation.getInstance(); 438 439 tracker.invoke(operationDescription, new Invokable<Object>() 440 { 441 public Object invoke() 442 { 443 handler.handleEvent(instance, event); 444 445 return null; 446 } 447 }); 448 } 449 450 // Order of operations is key here. This logic takes precedence; base class event dispatch and event handler methods 451 // in the class come AFTER. 452 453 invocation.proceed(); 454 455 if (matches) 456 { 457 invocation.setReturnValue(true); 458 } 459 } 460 }; 461 462 plasticClass.introduceMethod(TransformConstants.DISPATCH_COMPONENT_EVENT_DESCRIPTION).addAdvice(advice); 463 } 464 } 465 }