blob: a267d0064cbc48a220f1d9e2697b688050a398d1 [file] [log] [blame]
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package org.apache.tapestry5.internal.services;
import org.apache.tapestry5.ComponentResources;
import org.apache.tapestry5.SymbolConstants;
import org.apache.tapestry5.internal.InternalComponentResources;
import org.apache.tapestry5.internal.InternalConstants;
import org.apache.tapestry5.internal.model.MutableComponentModelImpl;
import org.apache.tapestry5.internal.plastic.PlasticInternalUtils;
import org.apache.tapestry5.ioc.Invokable;
import org.apache.tapestry5.ioc.LoggerSource;
import org.apache.tapestry5.ioc.OperationTracker;
import org.apache.tapestry5.ioc.Resource;
import org.apache.tapestry5.ioc.annotations.PostInjection;
import org.apache.tapestry5.ioc.annotations.Primary;
import org.apache.tapestry5.ioc.annotations.Symbol;
import org.apache.tapestry5.ioc.internal.services.PlasticProxyFactoryImpl;
import org.apache.tapestry5.ioc.internal.util.ClasspathResource;
import org.apache.tapestry5.ioc.internal.util.CollectionFactory;
import org.apache.tapestry5.ioc.internal.util.InternalUtils;
import org.apache.tapestry5.ioc.internal.util.URLChangeTracker;
import org.apache.tapestry5.ioc.services.Builtin;
import org.apache.tapestry5.ioc.services.ClasspathURLConverter;
import org.apache.tapestry5.ioc.services.PlasticProxyFactory;
import org.apache.tapestry5.ioc.util.ExceptionUtils;
import org.apache.tapestry5.model.ComponentModel;
import org.apache.tapestry5.model.MutableComponentModel;
import org.apache.tapestry5.plastic.*;
import org.apache.tapestry5.plastic.PlasticManager.PlasticManagerBuilder;
import org.apache.tapestry5.runtime.Component;
import org.apache.tapestry5.runtime.ComponentEvent;
import org.apache.tapestry5.runtime.ComponentResourcesAware;
import org.apache.tapestry5.runtime.PageLifecycleListener;
import org.apache.tapestry5.services.*;
import org.apache.tapestry5.services.transform.ComponentClassTransformWorker2;
import org.apache.tapestry5.services.transform.ControlledPackageType;
import org.apache.tapestry5.services.transform.TransformationSupport;
import org.slf4j.Logger;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* A wrapper around a {@link PlasticManager} that allows certain classes to be modified as they are loaded.
*/
public final class ComponentInstantiatorSourceImpl implements ComponentInstantiatorSource, UpdateListener,
Runnable, PlasticManagerDelegate, PlasticClassListener
{
private final Set<String> controlledPackageNames = CollectionFactory.newSet();
private final URLChangeTracker changeTracker;
private final ClassLoader parent;
private final ComponentClassTransformWorker2 transformerChain;
private final LoggerSource loggerSource;
private final Logger logger;
private final OperationTracker tracker;
private final InternalComponentInvalidationEventHub invalidationHub;
private final boolean productionMode;
private final ComponentClassResolver resolver;
private volatile PlasticProxyFactory proxyFactory;
private volatile PlasticManager manager;
/**
* Map from class name to Instantiator.
*/
private final Map<String, Instantiator> classToInstantiator = CollectionFactory.newConcurrentMap();
private final Map<String, ComponentModel> classToModel = CollectionFactory.newMap();
private final MethodDescription GET_COMPONENT_RESOURCES = PlasticUtils.getMethodDescription(
ComponentResourcesAware.class, "getComponentResources");
private final ConstructorCallback REGISTER_AS_PAGE_LIFECYCLE_LISTENER = new ConstructorCallback()
{
public void onConstruct(Object instance, InstanceContext context)
{
InternalComponentResources resources = context.get(InternalComponentResources.class);
resources.addPageLifecycleListener((PageLifecycleListener) instance);
}
};
public ComponentInstantiatorSourceImpl(Logger logger,
LoggerSource loggerSource,
@Builtin
PlasticProxyFactory proxyFactory,
@Primary
ComponentClassTransformWorker2 transformerChain,
ClasspathURLConverter classpathURLConverter,
OperationTracker tracker,
Map<String, ControlledPackageType> configuration,
@Symbol(SymbolConstants.PRODUCTION_MODE)
boolean productionMode,
ComponentClassResolver resolver,
InternalComponentInvalidationEventHub invalidationHub)
{
this.parent = proxyFactory.getClassLoader();
this.transformerChain = transformerChain;
this.logger = logger;
this.loggerSource = loggerSource;
this.changeTracker = new URLChangeTracker(classpathURLConverter);
this.tracker = tracker;
this.invalidationHub = invalidationHub;
this.productionMode = productionMode;
this.resolver = resolver;
// For now, we just need the keys of the configuration. When there are more types of controlled
// packages, we'll need to do more.
controlledPackageNames.addAll(configuration.keySet());
initializeService();
}
@PostInjection
public void listenForUpdates(UpdateListenerHub hub)
{
invalidationHub.addInvalidationCallback(this);
hub.addUpdateListener(this);
}
public synchronized void checkForUpdates()
{
if (changeTracker.containsChanges())
{
invalidationHub.classInControlledPackageHasChanged();
}
}
public void forceComponentInvalidation()
{
changeTracker.clear();
invalidationHub.classInControlledPackageHasChanged();
}
public void run()
{
changeTracker.clear();
classToInstantiator.clear();
proxyFactory.clearCache();
// Release the existing class pool, loader and so forth.
// Create a new one.
initializeService();
}
/**
* Invoked at object creation, or when there are updates to class files (i.e., invalidation), to create a new set of
* Javassist class pools and loaders.
*/
private void initializeService()
{
PlasticManagerBuilder builder = PlasticManager.withClassLoader(parent).delegate(this)
.packages(controlledPackageNames);
if (!productionMode)
{
builder.enable(TransformationOption.FIELD_WRITEBEHIND);
}
manager = builder.create();
manager.addPlasticClassListener(this);
proxyFactory = new PlasticProxyFactoryImpl(manager, logger);
classToInstantiator.clear();
classToModel.clear();
}
public Instantiator getInstantiator(final String className)
{
return classToInstantiator.computeIfAbsent(className, this::createInstantiatorForClass);
}
private Instantiator createInstantiatorForClass(final String className)
{
return tracker.invoke(String.format("Creating instantiator for component class %s", className),
new Invokable<Instantiator>()
{
public Instantiator invoke()
{
// Force the creation of the class (and the transformation of the class). This will first
// trigger transformations of any base classes.
final ClassInstantiator<Component> plasticInstantiator = manager.getClassInstantiator(className);
final ComponentModel model = classToModel.get(className);
return new Instantiator()
{
public Component newInstance(InternalComponentResources resources)
{
return plasticInstantiator.with(ComponentResources.class, resources)
.with(InternalComponentResources.class, resources).newInstance();
}
public ComponentModel getModel()
{
return model;
}
@Override
public String toString()
{
return String.format("[Instantiator[%s]", className);
}
};
}
});
}
public boolean exists(String className)
{
return parent.getResource(PlasticInternalUtils.toClassPath(className)) != null;
}
public PlasticProxyFactory getProxyFactory()
{
return proxyFactory;
}
public void transform(final PlasticClass plasticClass)
{
tracker.run(String.format("Running component class transformations on %s", plasticClass.getClassName()),
new Runnable()
{
public void run()
{
String className = plasticClass.getClassName();
String parentClassName = plasticClass.getSuperClassName();
// The parent model may not exist, if the super class is not in a controlled package.
ComponentModel parentModel = classToModel.get(parentClassName);
final boolean isRoot = parentModel == null;
if (isRoot
&& !(parentClassName.equals("java.lang.Object") || parentClassName
.equals("groovy.lang.GroovyObjectSupport")))
{
String suggestedPackageName = buildSuggestedPackageName(className);
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));
}
// Tapestry 5.2 was more sensitive that the parent class have a public no-args constructor.
// Plastic
// doesn't care, and we don't have the tools to dig that information out.
Logger logger = loggerSource.getLogger(className);
Resource baseResource = new ClasspathResource(parent, PlasticInternalUtils
.toClassPath(className));
changeTracker.add(baseResource.toURL());
if (isRoot)
{
implementComponentInterface(plasticClass);
}
boolean isPage = resolver.isPage(className);
boolean superClassImplementsPageLifecycle = plasticClass.isInterfaceImplemented(PageLifecycleListener.class);
String libraryName = resolver.getLibraryNameForClass(className);
final MutableComponentModel model = new MutableComponentModelImpl(className, logger, baseResource,
parentModel, isPage, libraryName);
TransformationSupportImpl transformationSupport = new TransformationSupportImpl(plasticClass, isRoot, model);
transformerChain.transform(plasticClass, transformationSupport, model);
transformationSupport.commit();
if (!superClassImplementsPageLifecycle && plasticClass.isInterfaceImplemented(PageLifecycleListener.class))
{
plasticClass.onConstruct(REGISTER_AS_PAGE_LIFECYCLE_LISTENER);
}
classToModel.put(className, model);
}
});
}
private void implementComponentInterface(PlasticClass plasticClass)
{
plasticClass.introduceInterface(Component.class);
final PlasticField resourcesField = plasticClass.introduceField(InternalComponentResources.class,
"internalComponentResources").injectFromInstanceContext();
plasticClass.introduceMethod(GET_COMPONENT_RESOURCES, new InstructionBuilderCallback()
{
public void doBuild(InstructionBuilder builder)
{
builder.loadThis().getField(resourcesField).returnResult();
}
});
}
public <T> ClassInstantiator<T> configureInstantiator(String className, ClassInstantiator<T> instantiator)
{
return instantiator;
}
private String buildSuggestedPackageName(String className)
{
for (String subpackage : InternalConstants.SUBPACKAGES)
{
String term = "." + subpackage + ".";
int pos = className.indexOf(term);
// Keep the leading '.' in the subpackage name and tack on "base".
if (pos > 0)
return className.substring(0, pos + 1) + InternalConstants.BASE_SUBPACKAGE;
}
// Is this even reachable? className should always be in a controlled package and so
// some subpackage above should have matched.
return null;
}
public void classWillLoad(PlasticClassEvent event)
{
Logger logger = loggerSource.getLogger("tapestry.transformer." + event.getPrimaryClassName());
if (logger.isDebugEnabled())
logger.debug(event.getDissasembledBytecode());
}
private class TransformationSupportImpl implements TransformationSupport
{
private final PlasticClass plasticClass;
private final boolean root;
private final MutableComponentModel model;
private final List<MethodAdvice> eventHandlerAdvice = CollectionFactory.newList();
public TransformationSupportImpl(PlasticClass plasticClass, boolean root, MutableComponentModel model)
{
this.plasticClass = plasticClass;
this.root = root;
this.model = model;
}
/**
* Commits any stored changes to the PlasticClass; this is used to defer adding advice to the dispatch method.
*/
public void commit()
{
if (!eventHandlerAdvice.isEmpty())
{
PlasticMethod dispatchMethod = plasticClass.introduceMethod(TransformConstants.DISPATCH_COMPONENT_EVENT_DESCRIPTION);
for (MethodAdvice advice : eventHandlerAdvice)
{
dispatchMethod.addAdvice(advice);
}
}
}
public Class toClass(String typeName)
{
try
{
return PlasticInternalUtils.toClass(manager.getClassLoader(), typeName);
} catch (ClassNotFoundException ex)
{
throw new RuntimeException(String.format(
"Unable to convert type '%s' to a Class: %s", typeName,
ExceptionUtils.toMessage(ex)), ex);
}
}
public boolean isRootTransformation()
{
return root;
}
public void addEventHandler(final String eventType, final int minContextValues, final String operationDescription, final ComponentEventHandler handler)
{
assert InternalUtils.isNonBlank(eventType);
assert minContextValues >= 0;
assert handler != null;
model.addEventHandler(eventType);
MethodAdvice advice = new EventMethodAdvice(tracker, eventType, minContextValues, operationDescription, handler);
// The advice is added at the very end, after the logic provided by the OnEventWorker
eventHandlerAdvice.add(advice);
}
}
private static class EventMethodAdvice implements MethodAdvice
{
final OperationTracker tracker;
final String eventType;
final int minContextValues;
final String operationDescription;
final ComponentEventHandler handler;
public EventMethodAdvice(OperationTracker tracker, String eventType, int minContextValues, String operationDescription, ComponentEventHandler handler)
{
this.tracker = tracker;
this.eventType = eventType;
this.minContextValues = minContextValues;
this.operationDescription = operationDescription;
this.handler = handler;
}
public void advise(final MethodInvocation invocation)
{
final ComponentEvent event = (ComponentEvent) invocation.getParameter(0);
boolean matches = !event.isAborted() && event.matches(eventType, "", minContextValues);
if (matches)
{
tracker.run(operationDescription, new Runnable()
{
public void run()
{
Component instance = (Component) invocation.getInstance();
handler.handleEvent(instance, event);
}
});
}
// Order of operations is key here. This logic takes precedence; base class event dispatch and event handler methods
// in the class come AFTER.
invocation.proceed();
if (matches)
{
invocation.setReturnValue(true);
}
}
}
}