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.newConcurrentMap(); 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 Instantiator getInstantiator(final String className) 204 { 205 return classToInstantiator.computeIfAbsent(className, this::createInstantiatorForClass); 206 } 207 208 private Instantiator createInstantiatorForClass(final String className) 209 { 210 return tracker.invoke(String.format("Creating instantiator for component class %s", className), 211 new Invokable<Instantiator>() 212 { 213 public Instantiator invoke() 214 { 215 // Force the creation of the class (and the transformation of the class). This will first 216 // trigger transformations of any base classes. 217 218 final ClassInstantiator<Component> plasticInstantiator = manager.getClassInstantiator(className); 219 220 final ComponentModel model = classToModel.get(className); 221 222 return new Instantiator() 223 { 224 public Component newInstance(InternalComponentResources resources) 225 { 226 return plasticInstantiator.with(ComponentResources.class, resources) 227 .with(InternalComponentResources.class, resources).newInstance(); 228 } 229 230 public ComponentModel getModel() 231 { 232 return model; 233 } 234 235 @Override 236 public String toString() 237 { 238 return String.format("[Instantiator[%s]", className); 239 } 240 }; 241 } 242 }); 243 } 244 245 public boolean exists(String className) 246 { 247 return parent.getResource(PlasticInternalUtils.toClassPath(className)) != null; 248 } 249 250 public PlasticProxyFactory getProxyFactory() 251 { 252 return proxyFactory; 253 } 254 255 public void transform(final PlasticClass plasticClass) 256 { 257 tracker.run(String.format("Running component class transformations on %s", plasticClass.getClassName()), 258 new Runnable() 259 { 260 public void run() 261 { 262 String className = plasticClass.getClassName(); 263 String parentClassName = plasticClass.getSuperClassName(); 264 265 // The parent model may not exist, if the super class is not in a controlled package. 266 267 ComponentModel parentModel = classToModel.get(parentClassName); 268 269 final boolean isRoot = parentModel == null; 270 271 if (isRoot 272 && !(parentClassName.equals("java.lang.Object") || parentClassName 273 .equals("groovy.lang.GroovyObjectSupport"))) 274 { 275 String suggestedPackageName = buildSuggestedPackageName(className); 276 277 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)); 278 } 279 280 // Tapestry 5.2 was more sensitive that the parent class have a public no-args constructor. 281 // Plastic 282 // doesn't care, and we don't have the tools to dig that information out. 283 284 Logger logger = loggerSource.getLogger(className); 285 286 Resource baseResource = new ClasspathResource(parent, PlasticInternalUtils 287 .toClassPath(className)); 288 289 changeTracker.add(baseResource.toURL()); 290 291 if (isRoot) 292 { 293 implementComponentInterface(plasticClass); 294 } 295 296 boolean isPage = resolver.isPage(className); 297 298 boolean superClassImplementsPageLifecycle = plasticClass.isInterfaceImplemented(PageLifecycleListener.class); 299 300 String libraryName = resolver.getLibraryNameForClass(className); 301 302 final MutableComponentModel model = new MutableComponentModelImpl(className, logger, baseResource, 303 parentModel, isPage, libraryName); 304 305 TransformationSupportImpl transformationSupport = new TransformationSupportImpl(plasticClass, isRoot, model); 306 307 transformerChain.transform(plasticClass, transformationSupport, model); 308 309 transformationSupport.commit(); 310 311 if (!superClassImplementsPageLifecycle && plasticClass.isInterfaceImplemented(PageLifecycleListener.class)) 312 { 313 plasticClass.onConstruct(REGISTER_AS_PAGE_LIFECYCLE_LISTENER); 314 } 315 316 classToModel.put(className, model); 317 } 318 }); 319 } 320 321 private void implementComponentInterface(PlasticClass plasticClass) 322 { 323 plasticClass.introduceInterface(Component.class); 324 325 final PlasticField resourcesField = plasticClass.introduceField(InternalComponentResources.class, 326 "internalComponentResources").injectFromInstanceContext(); 327 328 plasticClass.introduceMethod(GET_COMPONENT_RESOURCES, new InstructionBuilderCallback() 329 { 330 public void doBuild(InstructionBuilder builder) 331 { 332 builder.loadThis().getField(resourcesField).returnResult(); 333 } 334 }); 335 } 336 337 public <T> ClassInstantiator<T> configureInstantiator(String className, ClassInstantiator<T> instantiator) 338 { 339 return instantiator; 340 } 341 342 private String buildSuggestedPackageName(String className) 343 { 344 for (String subpackage : InternalConstants.SUBPACKAGES) 345 { 346 String term = "." + subpackage + "."; 347 348 int pos = className.indexOf(term); 349 350 // Keep the leading '.' in the subpackage name and tack on "base". 351 352 if (pos > 0) 353 return className.substring(0, pos + 1) + InternalConstants.BASE_SUBPACKAGE; 354 } 355 356 // Is this even reachable? className should always be in a controlled package and so 357 // some subpackage above should have matched. 358 359 return null; 360 } 361 362 public void classWillLoad(PlasticClassEvent event) 363 { 364 Logger logger = loggerSource.getLogger("tapestry.transformer." + event.getPrimaryClassName()); 365 366 if (logger.isDebugEnabled()) 367 logger.debug(event.getDissasembledBytecode()); 368 } 369 370 private class TransformationSupportImpl implements TransformationSupport 371 { 372 private final PlasticClass plasticClass; 373 374 private final boolean root; 375 376 private final MutableComponentModel model; 377 378 private final List<MethodAdvice> eventHandlerAdvice = CollectionFactory.newList(); 379 380 public TransformationSupportImpl(PlasticClass plasticClass, boolean root, MutableComponentModel model) 381 { 382 this.plasticClass = plasticClass; 383 this.root = root; 384 this.model = model; 385 } 386 387 /** 388 * Commits any stored changes to the PlasticClass; this is used to defer adding advice to the dispatch method. 389 */ 390 public void commit() 391 { 392 if (!eventHandlerAdvice.isEmpty()) 393 { 394 PlasticMethod dispatchMethod = plasticClass.introduceMethod(TransformConstants.DISPATCH_COMPONENT_EVENT_DESCRIPTION); 395 for (MethodAdvice advice : eventHandlerAdvice) 396 { 397 dispatchMethod.addAdvice(advice); 398 } 399 } 400 } 401 402 public Class toClass(String typeName) 403 { 404 try 405 { 406 return PlasticInternalUtils.toClass(manager.getClassLoader(), typeName); 407 } catch (ClassNotFoundException ex) 408 { 409 throw new RuntimeException(String.format( 410 "Unable to convert type '%s' to a Class: %s", typeName, 411 ExceptionUtils.toMessage(ex)), ex); 412 } 413 } 414 415 public boolean isRootTransformation() 416 { 417 return root; 418 } 419 420 public void addEventHandler(final String eventType, final int minContextValues, final String operationDescription, final ComponentEventHandler handler) 421 { 422 assert InternalUtils.isNonBlank(eventType); 423 assert minContextValues >= 0; 424 assert handler != null; 425 426 model.addEventHandler(eventType); 427 428 MethodAdvice advice = new EventMethodAdvice(tracker, eventType, minContextValues, operationDescription, handler); 429 430 // The advice is added at the very end, after the logic provided by the OnEventWorker 431 432 eventHandlerAdvice.add(advice); 433 } 434 } 435 436 private static class EventMethodAdvice implements MethodAdvice 437 { 438 final OperationTracker tracker; 439 final String eventType; 440 final int minContextValues; 441 final String operationDescription; 442 final ComponentEventHandler handler; 443 444 public EventMethodAdvice(OperationTracker tracker, String eventType, int minContextValues, String operationDescription, ComponentEventHandler handler) 445 { 446 this.tracker = tracker; 447 this.eventType = eventType; 448 this.minContextValues = minContextValues; 449 this.operationDescription = operationDescription; 450 this.handler = handler; 451 } 452 453 public void advise(final MethodInvocation invocation) 454 { 455 final ComponentEvent event = (ComponentEvent) invocation.getParameter(0); 456 457 boolean matches = !event.isAborted() && event.matches(eventType, "", minContextValues); 458 459 if (matches) 460 { 461 tracker.run(operationDescription, new Runnable() 462 { 463 public void run() 464 { 465 Component instance = (Component) invocation.getInstance(); 466 467 handler.handleEvent(instance, event); 468 } 469 }); 470 } 471 472 // Order of operations is key here. This logic takes precedence; base class event dispatch and event handler methods 473 // in the class come AFTER. 474 475 invocation.proceed(); 476 477 if (matches) 478 { 479 invocation.setReturnValue(true); 480 } 481 } 482 } 483}