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