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