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