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 }