This is an automated email from the ASF dual-hosted git repository. thiagohp pushed a commit to branch better-page-invalidation in repository https://gitbox.apache.org/repos/asf/tapestry-5.git
commit 449b248c6b3e7ee179cbd08ba717424983ec26a9 Author: Thiago H. de Paula Figueiredo <thi...@arsmachina.com.br> AuthorDate: Sun Mar 19 19:45:46 2023 -0300 Partial work commit for better page invalidation part 2 --- 583_RELEASE_NOTES.md | 3 + .../services/PropertyConduitSourceImpl.java | 1 + .../util/DifferentClassVersionsException.java | 18 +- .../tapestry5/internal/bindings/PropBinding.java | 9 + .../internal/bindings/PropBindingFactory.java | 29 +- .../internal/renderers/RequestRenderer.java | 120 +++++- .../services/ComponentClassResolverImpl.java | 20 +- .../services/ComponentDependencyRegistry.java | 21 +- .../services/ComponentDependencyRegistryImpl.java | 449 ++++++++++++++++++++- .../services/ComponentInstantiatorSourceImpl.java | 27 +- .../internal/services/PageSourceImpl.java | 42 +- .../internal/services/RequestErrorFilter.java | 85 ++-- .../internal/services/RequestPageCacheImpl.java | 86 +++- .../internal/transform/InjectComponentWorker.java | 3 +- .../apache/tapestry5/modules/InternalModule.java | 2 +- .../apache/tapestry5/modules/TapestryModule.java | 22 +- .../tapestry5/services/ComponentClassResolver.java | 6 + .../services/pageload/PageClassloaderContext.java | 106 ++++- .../pageload/PageClassloaderContextManager.java | 37 +- .../PageClassloaderContextManagerImpl.java | 415 +++++++++++++++++-- .../apache/tapestry5/corelib/pages/PageCatalog.tml | 1 + .../integration/app1/components/Border.java | 4 + .../app1/pages/InstanceMixinDependencies.java | 30 ++ .../integration/app1/services/AppModule.java | 6 +- .../ComponentDependencyRegistryImplTest.java | 260 +++++++++++- tapestry-core/src/test/resources/log4j.properties | 2 + .../integration/app1/components/Border.tml | 1 + 27 files changed, 1651 insertions(+), 154 deletions(-) diff --git a/583_RELEASE_NOTES.md b/583_RELEASE_NOTES.md index e7923cc55..7db54d7a8 100644 --- a/583_RELEASE_NOTES.md +++ b/583_RELEASE_NOTES.md @@ -14,3 +14,6 @@ Scratch pad for changes destined for the 5.8.3 release notes page. * New getEmbeddedElementIds() method in ComponentPageElement (internal service) * New registerClassName() method in ResourceChangeTracker (internal service) * New clearClassName() method in ResourceChangeTracker (internal service) + +# Overall notes +* Before \ No newline at end of file diff --git a/beanmodel/src/main/java/org/apache/tapestry5/beanmodel/internal/services/PropertyConduitSourceImpl.java b/beanmodel/src/main/java/org/apache/tapestry5/beanmodel/internal/services/PropertyConduitSourceImpl.java index 66f106660..4b474491d 100644 --- a/beanmodel/src/main/java/org/apache/tapestry5/beanmodel/internal/services/PropertyConduitSourceImpl.java +++ b/beanmodel/src/main/java/org/apache/tapestry5/beanmodel/internal/services/PropertyConduitSourceImpl.java @@ -1390,6 +1390,7 @@ public class PropertyConduitSourceImpl implements PropertyConduitSource @PostInjection public void listenForInvalidations(@ComponentClasses InvalidationEventHub hub) { + System.out.println("WWWWWW hub " + hub); hub.addInvalidationCallback(this::listen); } diff --git a/commons/src/main/java/org/apache/tapestry5/commons/util/DifferentClassVersionsException.java b/commons/src/main/java/org/apache/tapestry5/commons/util/DifferentClassVersionsException.java index 6bc062b18..ab490f71d 100644 --- a/commons/src/main/java/org/apache/tapestry5/commons/util/DifferentClassVersionsException.java +++ b/commons/src/main/java/org/apache/tapestry5/commons/util/DifferentClassVersionsException.java @@ -27,11 +27,17 @@ public class DifferentClassVersionsException extends TapestryException private static final long serialVersionUID = 1L; private final String className; + + private final ClassLoader classLoader1; + + private final ClassLoader classLoader2; - public DifferentClassVersionsException(String message, String className) + public DifferentClassVersionsException(String message, String className, ClassLoader classLoader1, ClassLoader classLoader2) { super(message, null); this.className = className; + this.classLoader1 = classLoader1; + this.classLoader2 = classLoader2; } public String getClassName() @@ -39,4 +45,14 @@ public class DifferentClassVersionsException extends TapestryException return className; } + public ClassLoader getClassLoader1() + { + return classLoader1; + } + + public ClassLoader getClassLoader2() + { + return classLoader2; + } + } diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/internal/bindings/PropBinding.java b/tapestry-core/src/main/java/org/apache/tapestry5/internal/bindings/PropBinding.java index 8a7778153..973c42b41 100644 --- a/tapestry-core/src/main/java/org/apache/tapestry5/internal/bindings/PropBinding.java +++ b/tapestry-core/src/main/java/org/apache/tapestry5/internal/bindings/PropBinding.java @@ -16,6 +16,7 @@ package org.apache.tapestry5.internal.bindings; import java.lang.annotation.Annotation; import java.lang.reflect.Type; +import java.util.concurrent.atomic.AtomicInteger; import org.apache.tapestry5.beanmodel.PropertyConduit; import org.apache.tapestry5.beanmodel.PropertyConduit2; @@ -39,6 +40,10 @@ public class PropBinding extends AbstractBinding implements InternalPropBinding private boolean invariant; private final String expression; + + // TODO remove + private static final AtomicInteger COUNTER = new AtomicInteger(); + private final int id; public PropBinding(final Location location, final Object root, final PropertyConduit conduit, final String expression, final String toString) { @@ -50,6 +55,10 @@ public class PropBinding extends AbstractBinding implements InternalPropBinding this.toString = toString; invariant = conduit.getAnnotation(Invariant.class) != null; + + System.out.println("TTTTT new PropBinding: " + root.getClass().getSimpleName() + ":" + expression); + id = COUNTER.getAndIncrement(); + } /** diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/internal/bindings/PropBindingFactory.java b/tapestry-core/src/main/java/org/apache/tapestry5/internal/bindings/PropBindingFactory.java index 4fe2bf14a..c8e51ffcb 100644 --- a/tapestry-core/src/main/java/org/apache/tapestry5/internal/bindings/PropBindingFactory.java +++ b/tapestry-core/src/main/java/org/apache/tapestry5/internal/bindings/PropBindingFactory.java @@ -18,7 +18,10 @@ import org.apache.tapestry5.beanmodel.PropertyConduit; import org.apache.tapestry5.beanmodel.services.PropertyConduitSource; import org.apache.tapestry5.commons.Location; import org.apache.tapestry5.commons.internal.services.StringInterner; +import org.apache.tapestry5.commons.internal.util.TapestryException; import org.apache.tapestry5.services.BindingFactory; +import org.apache.tapestry5.services.pageload.PageClassloaderContext; +import org.apache.tapestry5.services.pageload.PageClassloaderContextManager; /** * Binding factory for reading and updating JavaBean properties. @@ -31,18 +34,25 @@ public class PropBindingFactory implements BindingFactory private final PropertyConduitSource source; private final StringInterner interner; + + private final PageClassloaderContextManager pageClassloaderContextManager; - public PropBindingFactory(PropertyConduitSource propertyConduitSource, StringInterner interner) + public PropBindingFactory(PropertyConduitSource propertyConduitSource, StringInterner interner, + PageClassloaderContextManager pageClassloaderContextManager) { source = propertyConduitSource; this.interner = interner; + this.pageClassloaderContextManager = pageClassloaderContextManager; } public Binding newBinding(String description, ComponentResources container, ComponentResources component, String expression, Location location) { + + // TODO: need to get correct classloader here, probably Object target = container.getComponent(); Class targetClass = target.getClass(); + targetClass = getClassLoaderAppropriateClass(targetClass); PropertyConduit conduit = source.create(targetClass, expression); @@ -51,4 +61,21 @@ public class PropBindingFactory implements BindingFactory return new PropBinding(location, target, conduit, expression, toString); } + + private Class getClassLoaderAppropriateClass(Class targetClass) + { + final String className = targetClass.getName(); + try + { + final PageClassloaderContext context = pageClassloaderContextManager.get(className); + System.out.printf("XXXXX Target class (before): %s classloader : %s\n", targetClass.getSimpleName(), targetClass.getClassLoader()); + targetClass = context.getProxyFactory() + .getClassLoader().loadClass(className); + System.out.printf("XXXXX Target class (after) : %s classloader : %s context %s\n", targetClass.getSimpleName(), targetClass.getClassLoader(), context.getName()); + } catch (ClassNotFoundException e) + { + throw new TapestryException(e.getMessage(), e); + } + return targetClass; + } } diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/internal/renderers/RequestRenderer.java b/tapestry-core/src/main/java/org/apache/tapestry5/internal/renderers/RequestRenderer.java index b5ebd0c2d..326ca7226 100644 --- a/tapestry-core/src/main/java/org/apache/tapestry5/internal/renderers/RequestRenderer.java +++ b/tapestry-core/src/main/java/org/apache/tapestry5/internal/renderers/RequestRenderer.java @@ -15,16 +15,26 @@ package org.apache.tapestry5.internal.renderers; import org.apache.tapestry5.MarkupWriter; +import org.apache.tapestry5.SymbolConstants; import org.apache.tapestry5.commons.util.CollectionFactory; import org.apache.tapestry5.http.TapestryHttpSymbolConstants; import org.apache.tapestry5.http.services.Context; import org.apache.tapestry5.http.services.Request; +import org.apache.tapestry5.internal.services.PageSource; +import org.apache.tapestry5.internal.structure.Page; import org.apache.tapestry5.ioc.annotations.Primary; import org.apache.tapestry5.ioc.annotations.Symbol; import org.apache.tapestry5.ioc.internal.util.InternalUtils; +import org.apache.tapestry5.services.ComponentClassResolver; import org.apache.tapestry5.services.ObjectRenderer; +import org.apache.tapestry5.services.pageload.PageClassloaderContext; +import org.apache.tapestry5.services.pageload.PageClassloaderContextManager; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; import java.util.List; +import java.util.stream.Collectors; public class RequestRenderer implements ObjectRenderer<Request> { @@ -33,12 +43,31 @@ public class RequestRenderer implements ObjectRenderer<Request> private final String contextPath; private final ObjectRenderer masterObjectRenderer; - - public RequestRenderer(@Primary ObjectRenderer masterObjectRenderer, Context context, @Symbol(TapestryHttpSymbolConstants.CONTEXT_PATH) String contextPath) + + private final boolean productionMode; + + private final PageClassloaderContextManager pageClassloaderContextManager; + + private final PageSource pageSource; + + private final ComponentClassResolver componentClassResolver; + + public RequestRenderer( + @Primary ObjectRenderer masterObjectRenderer, + Context context, + @Symbol(TapestryHttpSymbolConstants.CONTEXT_PATH) String contextPath, + @Symbol(SymbolConstants.PRODUCTION_MODE) boolean productionMode, + PageClassloaderContextManager pageClassloaderContextManager, + PageSource pageSource, + ComponentClassResolver componentClassResolver) { this.masterObjectRenderer = masterObjectRenderer; this.context = context; this.contextPath = contextPath; + this.productionMode = productionMode; + this.pageClassloaderContextManager = pageClassloaderContextManager; + this.pageSource = pageSource; + this.componentClassResolver = componentClassResolver; } public void render(Request request, MarkupWriter writer) @@ -48,6 +77,8 @@ public class RequestRenderer implements ObjectRenderer<Request> headers(request, writer); attributes(request, writer); context(writer); + pageClassloaderContext(writer); + pages(writer); } private void coreProperties(Request request, MarkupWriter writer) @@ -243,4 +274,89 @@ public class RequestRenderer implements ObjectRenderer<Request> writer.end(); // dl } + private void pageClassloaderContext(MarkupWriter writer) + { + if (!productionMode) + { + section(writer, "Page Classloader Context"); + writer.element("ul"); + render(pageClassloaderContextManager.getRoot(), writer); + writer.end(); // ul + } + } + + private void render(PageClassloaderContext context, MarkupWriter writer) + { + if (context != null) + { + + writer.element("li"); + + writer.element("p"); + writer.element("em"); + writer.write(context.getName()); + writer.write(", "); + writer.write(context.getClassLoader().toString()); + writer.end(); // em + writer.end(); // p + + writer.element("p"); + writer.write(context.getClassNames().stream().collect(Collectors.joining(", "))); + writer.end(); // p + + if (!context.getChildren().isEmpty()) + { + writer.element("ul"); + for (PageClassloaderContext child : context.getChildren()) + { + render(child, writer); + } + writer.end(); // ul + } + writer.end(); // li + + } + + } + + private void pages(MarkupWriter writer) + { + if (!productionMode) + { + section(writer, "Pages"); + writer.element("table", "class", "table table-condensed table-hover table-striped exception-report-threads"); + writer.element("thead"); + + writer.element("td"); + writer.write("Name"); + writer.end(); //td Name + + writer.element("td"); + writer.write("Context"); + writer.end(); //td Context + + writer.end(); // thead + + writer.element("tbody"); + + List<Page> pages = new ArrayList<>(pageSource.getAllPages()); + Collections.sort(pages, Comparator.comparing(Page::getName)); + + for (Page page : pages) { + writer.element("tr"); + writer.element("td"); + writer.write(page.getName()); + writer.end(); // td + writer.element("td"); + writer.write(pageClassloaderContextManager.getRoot().findByClassName(componentClassResolver.getClassName(page.getName())).toString()); + writer.end(); // td + writer.end(); // tr + } + + writer.end(); // tbody + + writer.end(); // table + } + } + } diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ComponentClassResolverImpl.java b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ComponentClassResolverImpl.java index 349755fc7..77651e95c 100644 --- a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ComponentClassResolverImpl.java +++ b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ComponentClassResolverImpl.java @@ -826,13 +826,31 @@ public class ComponentClassResolverImpl implements ComponentClassResolver, Inval { result = getKeyByValue(thisData.componentToClassName, className); } - if (result == null ){ + if (result == null ) + { result = getKeyByValue(thisData.mixinToClassName, className); } return result; } + @Override + public String getClassName(String logicalName) + { + final Data thisData = getData(); + String result = getKeyByValue(thisData.pageClassNameToLogicalName, logicalName); + if (result == null) + { + result = thisData.componentToClassName.get(logicalName); + } + if (result == null ) + { + result = thisData.mixinToClassName.get(logicalName); + } + return result; + } + + private String getKeyByValue(Map<String, String> map, String value) { return map.entrySet().stream() diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ComponentDependencyRegistry.java b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ComponentDependencyRegistry.java index c4ab81a6f..e2f10469a 100644 --- a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ComponentDependencyRegistry.java +++ b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ComponentDependencyRegistry.java @@ -39,7 +39,11 @@ public interface ComponentDependencyRegistry { */ String FILENAME = "tapestryComponentDependencies.json"; - + /** + * Register all the dependencies of a given class. + */ + void register(Class<?> clasz); + /** * Register all the dependencies of a given component. */ @@ -101,5 +105,20 @@ public interface ComponentDependencyRegistry { * Returns the set of all root classes (i.e. ones with no dependencies). */ Set<String> getRootClasses(); + + /** + * Returns whether stored dependency information is present. + */ + boolean isStoredDependencyInformationPresent(); + + /** + * Tells this service to ignore invalidations in this thread. + */ + void disableInvalidations(); + + /** + * Tells this service to stop ignoring invalidations in this thread. + */ + void enableInvalidations(); } diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ComponentDependencyRegistryImpl.java b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ComponentDependencyRegistryImpl.java index 08689c4f3..a694cdb71 100644 --- a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ComponentDependencyRegistryImpl.java +++ b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ComponentDependencyRegistryImpl.java @@ -19,34 +19,56 @@ import java.io.File; import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; +import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.Set; +import java.util.function.Consumer; import java.util.stream.Collectors; import org.apache.tapestry5.ComponentResources; import org.apache.tapestry5.annotations.InjectComponent; import org.apache.tapestry5.annotations.InjectPage; +import org.apache.tapestry5.annotations.Mixin; +import org.apache.tapestry5.annotations.MixinClasses; +import org.apache.tapestry5.annotations.Mixins; +import org.apache.tapestry5.commons.Resource; import org.apache.tapestry5.commons.internal.util.TapestryException; import org.apache.tapestry5.commons.services.InvalidationEventHub; +import org.apache.tapestry5.internal.TapestryInternalUtils; +import org.apache.tapestry5.internal.parser.ComponentTemplate; +import org.apache.tapestry5.internal.parser.StartComponentToken; +import org.apache.tapestry5.internal.parser.TemplateToken; import org.apache.tapestry5.internal.structure.ComponentPageElement; +import org.apache.tapestry5.ioc.Orderable; +import org.apache.tapestry5.ioc.internal.util.ClasspathResource; +import org.apache.tapestry5.ioc.internal.util.InternalUtils; import org.apache.tapestry5.json.JSONArray; import org.apache.tapestry5.json.JSONObject; import org.apache.tapestry5.model.ComponentModel; import org.apache.tapestry5.model.EmbeddedComponentModel; import org.apache.tapestry5.model.MutableComponentModel; +import org.apache.tapestry5.model.ParameterModel; import org.apache.tapestry5.plastic.PlasticField; +import org.apache.tapestry5.plastic.PlasticManager; import org.apache.tapestry5.runtime.Component; +import org.apache.tapestry5.services.ComponentClassResolver; +import org.apache.tapestry5.services.pageload.PageClassloaderContextManager; +import org.apache.tapestry5.services.templates.ComponentTemplateLocator; +import org.slf4j.Logger; public class ComponentDependencyRegistryImpl implements ComponentDependencyRegistry { + final private PageClassloaderContextManager pageClassloaderContextManager; + private static final String META_ATTRIBUTE = "injectedComponentDependencies"; private static final String META_ATTRIBUTE_SEPARATOR = ","; @@ -58,11 +80,34 @@ public class ComponentDependencyRegistryImpl implements ComponentDependencyRegis final private Set<String> alreadyProcessed; final private File storedDependencies; - - public ComponentDependencyRegistryImpl() + + final private static ThreadLocal<Boolean> INVALIDATIONS_ENABLED = ThreadLocal.withInitial(() -> Boolean.TRUE); + + final private PlasticManager plasticManager; + + final private ComponentClassResolver resolver; + + final private TemplateParser templateParser; + + @SuppressWarnings("deprecation") + final private ComponentTemplateLocator componentTemplateLocator; + + final private boolean storedDependencyInformationPresent; + + public ComponentDependencyRegistryImpl( + final PageClassloaderContextManager pageClassloaderContextManager, + final PlasticManager plasticManager, + final ComponentClassResolver componentClassResolver, + final TemplateParser templateParser, + final ComponentTemplateLocator componentTemplateLocator) { + this.pageClassloaderContextManager = pageClassloaderContextManager; map = new HashMap<>(); alreadyProcessed = new HashSet<>(); + this.plasticManager = plasticManager; + this.resolver = componentClassResolver; + this.templateParser = templateParser; + this.componentTemplateLocator = componentTemplateLocator; storedDependencies = new File(FILENAME); if (storedDependencies.exists()) @@ -84,7 +129,11 @@ public class ComponentDependencyRegistryImpl implements ComponentDependencyRegis .stream() .map(o -> (String) o) .collect(Collectors.toSet()); - map.put(className, new HashSet<>(dependencies)); + for (String dependency : dependencies) + { + add(className, dependency); + alreadyProcessed.add(dependency); + } alreadyProcessed.add(className); } } catch (IOException e) @@ -93,13 +142,200 @@ public class ComponentDependencyRegistryImpl implements ComponentDependencyRegis } } + storedDependencyInformationPresent = !map.isEmpty(); + + } + + @Override + public void register(Class<?> component) + { + + final Set<Class<?>> furtherDependencies = new HashSet<>(); + Consumer<Class<?>> processClass = furtherDependencies::add; + Consumer<String> processClassName = s -> { + try { + furtherDependencies.add(component.getClassLoader().loadClass(s)); + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + }; + + // Components declared in the template + registerTemplate(component, processClassName); + + // Dependencies from injecting or component-declaring annotations: + // @InjectPage, @InjectComponent + for (Field field : component.getDeclaredFields()) + { + + // Component and page injection annotation + if (field.isAnnotationPresent(InjectPage.class) || + field.isAnnotationPresent(InjectComponent.class)) + { + final Class<?> dependency = field.getType(); + add(component, dependency); + processClass.accept(dependency); + } + + // @Component + registerComponentInstance(field, processClassName); + + // Mixins, class level: @Mixin + registerMixin(field, processClassName); + + // Mixins applied to embedded component instances through @MixinClasses or @Mixins + registerComponentInstanceMixins(field, processClass, processClassName); + } + + // Superclass + Class superclass = component.getSuperclass(); + if (isTransformed(superclass)) + { + processClass.accept(superclass); + add(component, superclass); + } + + alreadyProcessed.add(component.getName()); + + for (Class<?> dependency : furtherDependencies) + { + // Avoid infinite recursion + final String dependencyClassName = dependency.getName(); + if (!alreadyProcessed.contains(dependencyClassName) + && !plasticManager.shouldInterceptClassLoading(dependency.getName())) + { + register(dependency); + } + } + + } + + /** + * Notice only the main template (i.e. not the locale- or axis-specific ones) + * are checked here. They hopefully will be covered when the ComponentModel-based + * component dependency processing is done. + * @param component + * @param processClassName + */ + @SuppressWarnings("deprecation") + private void registerTemplate(Class<?> component, Consumer<String> processClassName) + { + // TODO: implement caching of template dependency information, probably + // by listening separaterly to ComponentTemplateSource to invalidate caches + // just when template changes. + + ComponentModel mock = new ComponentModelMock(component, resolver.isPage(component.getName())); + final Resource templateResource = componentTemplateLocator.locateTemplate(mock, Locale.getDefault()); + String dependency; + if (templateResource != null) + { + final String className = component.getName(); + final ComponentTemplate template = templateParser.parseTemplate(templateResource); + for (TemplateToken token: template.getTokens()) + { + if (token instanceof StartComponentToken) + { + StartComponentToken componentToken = (StartComponentToken) token; + String logicalName = componentToken.getComponentType(); + if (logicalName != null) + { + dependency = resolver.resolveComponentTypeToClassName(logicalName); + add(className, dependency); + processClassName.accept(dependency); + } + for (String mixin : TapestryInternalUtils.splitAtCommas(componentToken.getMixins())) + { + dependency = resolver.resolveMixinTypeToClassName(mixin); + add(className, dependency); + processClassName.accept(dependency); + } + } + } + } + } + + private void registerComponentInstance(Field field, Consumer<String> processClassName) + { + if (field.isAnnotationPresent(org.apache.tapestry5.annotations.Component.class)) + { + org.apache.tapestry5.annotations.Component component = + field.getAnnotation(org.apache.tapestry5.annotations.Component.class); + + final String typeFromAnnotation = component.type().trim(); + String dependency; + if (typeFromAnnotation.isEmpty()) + { + dependency = field.getType().getName(); + } + else + { + dependency = resolver.resolveComponentTypeToClassName(typeFromAnnotation); + } + add(field.getDeclaringClass().getName(), dependency); + processClassName.accept(dependency); + } + } + + private void registerMixin(Field field, Consumer<String> processClassName) { + if (field.isAnnotationPresent(Mixin.class)) + { + // Logic adapted from MixinWorker + String mixinType = field.getAnnotation(Mixin.class).value(); + String mixinClassName = InternalUtils.isBlank(mixinType) ? + getFieldTypeClassName(field) : + resolver.resolveMixinTypeToClassName(mixinType); + + add(getDeclaringClassName(field), mixinClassName); + processClassName.accept(mixinClassName); + } + } + + private String getDeclaringClassName(Field field) { + return field.getDeclaringClass().getName(); + } + + private String getFieldTypeClassName(Field field) { + return field.getType().getName(); + } + + private void registerComponentInstanceMixins(Field field, Consumer<Class<?>> processClass, Consumer<String> processClassName) + { + + if (field.isAnnotationPresent(org.apache.tapestry5.annotations.Component.class)) + { + + MixinClasses mixinClasses = field.getAnnotation(MixinClasses.class); + if (mixinClasses != null) + { + for (Class dependency : mixinClasses.value()) + { + add(field.getDeclaringClass(), dependency); + processClass.accept(dependency); + } + } + + Mixins mixins = field.getAnnotation(Mixins.class); + if (mixins != null) + { + for (String mixin : mixins.value()) + { + // Logic adapted from MixinsWorker + Orderable<String> typeAndOrder = TapestryInternalUtils.mixinTypeAndOrder(mixin); + final String dependency = resolver.resolveMixinTypeToClassName(typeAndOrder.getTarget()); + add(getDeclaringClassName(field), dependency); + processClassName.accept(dependency); + } + } + + } + } @Override public void register(ComponentPageElement componentPageElement) { final String componentClassName = getClassName(componentPageElement); - + if (!alreadyProcessed.contains(componentClassName)) { synchronized (map) @@ -255,6 +491,14 @@ public class ComponentDependencyRegistryImpl implements ComponentDependencyRegis } } + private void add(Class component, Class dependency) + { + if (plasticManager.shouldInterceptClassLoading(dependency.getName())) + { + add(component.getName(), dependency.getName()); + } + } + private void add(String component, String dependency) { Objects.requireNonNull(component, "Parameter component cannot be null"); @@ -281,12 +525,18 @@ public class ComponentDependencyRegistryImpl implements ComponentDependencyRegis List<String> listen(List<String> resources) { List<String> furtherDependents; - if (resources.isEmpty()) + if (!INVALIDATIONS_ENABLED.get()) + { + furtherDependents = Collections.emptyList(); + } + else if (resources.isEmpty()) { clear(); furtherDependents = Collections.emptyList(); } - else + // Don't invalidate component dependency information when + // PageClassloaderContextManager is merging contexts + else if (!pageClassloaderContextManager.isMerging()) { furtherDependents = new ArrayList<>(); for (String resource : resources) @@ -302,6 +552,10 @@ public class ComponentDependencyRegistryImpl implements ComponentDependencyRegis clear(resource); } } + else + { + furtherDependents = Collections.emptyList(); + } return furtherDependents; } @@ -347,4 +601,187 @@ public class ComponentDependencyRegistryImpl implements ComponentDependencyRegis .collect(Collectors.toSet()); } + private boolean isTransformed(Class clasz) + { + return plasticManager.shouldInterceptClassLoading(clasz.getName()); + } + + @Override + public boolean isStoredDependencyInformationPresent() + { + return storedDependencyInformationPresent; + } + + @Override + public void disableInvalidations() + { + INVALIDATIONS_ENABLED.set(false); + } + + @Override + public void enableInvalidations() + { + INVALIDATIONS_ENABLED.set(true); + } + + /** + * Only really implemented method is {@link ComponentModel#getBaseResource()} + */ + private class ComponentModelMock implements ComponentModel + { + + final private Resource baseResource; + final private boolean isPage; + final private String componentClassName; + + public ComponentModelMock(Class<?> component, boolean isPage) + { + componentClassName = component.getName(); + String templateLocation = componentClassName.replace('.', '/'); + baseResource = new ClasspathResource(templateLocation); + + this.isPage = isPage; + } + + @Override + public Resource getBaseResource() + { + return baseResource; + } + + @Override + public String getLibraryName() + { + return null; + } + + @Override + public boolean isPage() + { + return isPage; + } + + @Override + public String getComponentClassName() + { + return componentClassName; + } + + @Override + public List<String> getEmbeddedComponentIds() + { + return null; + } + + @Override + public EmbeddedComponentModel getEmbeddedComponentModel(String componentId) + { + return null; + } + + @Override + public String getFieldPersistenceStrategy(String fieldName) + { + return null; + } + + @Override + public Logger getLogger() + { + return null; + } + + @Override + public List<String> getMixinClassNames() + { + return null; + } + + @Override + public ParameterModel getParameterModel(String parameterName) + { + return null; + } + + @Override + public boolean isFormalParameter(String parameterName) + { + return false; + } + + @Override + public List<String> getParameterNames() + { + return null; + } + + @Override + public List<String> getDeclaredParameterNames() + { + return null; + } + + @Override + public List<String> getPersistentFieldNames() + { + return null; + } + + @Override + public boolean isRootClass() + { + return false; + } + + @Override + public boolean getSupportsInformalParameters() + { + return false; + } + + @Override + public ComponentModel getParentModel() + { + return null; + } + + @Override + public boolean isMixinAfter() + { + return false; + } + + @Override + public String getMeta(String key) + { + return null; + } + + @Override + public Set<Class> getHandledRenderPhases() + { + return null; + } + + @Override + public boolean handlesEvent(String eventType) + { + return false; + } + + @Override + public String[] getOrderForMixin(String mixinClassName) + { + return null; + } + + @Override + public boolean handleActivationEventContext() + { + return false; + } + + } + + } diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ComponentInstantiatorSourceImpl.java b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ComponentInstantiatorSourceImpl.java index a2ff8d87c..588c99dde 100644 --- a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ComponentInstantiatorSourceImpl.java +++ b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ComponentInstantiatorSourceImpl.java @@ -181,6 +181,11 @@ public final class ComponentInstantiatorSourceImpl implements ComponentInstantia controlledPackageNames.addAll(configuration.keySet()); initializeService(); + + pageClassloaderContextManager.initialize( + rootPageClassloaderContext, + ComponentInstantiatorSourceImpl.this::createPlasticProxyFactory); + } @PostInjection @@ -231,6 +236,7 @@ public final class ComponentInstantiatorSourceImpl implements ComponentInstantia } } + // TODO: fix this // final Iterator<Entry<String, ComponentModel>> classToModelIterator = classToModel.entrySet().iterator(); // while (classToModelIterator.hasNext()) // { @@ -317,8 +323,7 @@ public final class ComponentInstantiatorSourceImpl implements ComponentInstantia // Force the creation of the class (and the transformation of the class). This will first // trigger transformations of any base classes. - final PageClassloaderContext context = pageClassloaderContextManager.get( - className, rootPageClassloaderContext, ComponentInstantiatorSourceImpl.this::createPlasticProxyFactory); + final PageClassloaderContext context = pageClassloaderContextManager.get(className); final ClassInstantiator<Component> plasticInstantiator = context.getPlasticManager().getClassInstantiator(className); @@ -512,8 +517,7 @@ public final class ComponentInstantiatorSourceImpl implements ComponentInstantia { try { - final PageClassloaderContext context = pageClassloaderContextManager.get( - typeName, rootPageClassloaderContext, ComponentInstantiatorSourceImpl.this::createPlasticProxyFactory); + final PageClassloaderContext context = pageClassloaderContextManager.get(typeName); return PlasticInternalUtils.toClass(context.getPlasticManager().getClassLoader(), typeName); } catch (ClassNotFoundException ex) { @@ -717,7 +721,7 @@ public final class ComponentInstantiatorSourceImpl implements ComponentInstantia @Override public PlasticManager getPlasticManager() { - throw new UnsupportedOperationException(); + return rootPageClassloaderContext.getProxyFactory().getPlasticManager(); } @Override @@ -726,19 +730,8 @@ public final class ComponentInstantiatorSourceImpl implements ComponentInstantia PageClassloaderContext context = rootPageClassloaderContext.findByClassName(className); if (context == null) { - context = pageClassloaderContextManager.get( - className, rootPageClassloaderContext, ComponentInstantiatorSourceImpl.this::createPlasticProxyFactory); + context = pageClassloaderContextManager.get(className); } -// System.out.println("Class: " + className); -// System.out.println("Root classloader : " + rootPageClassloaderContext); -// System.out.println("Parent classloader : " + parent); -// if (!rootPageClassloaderContext.getChildren().isEmpty()) -// { -// System.out.println("Unknown classloader : " + rootPageClassloaderContext.getChildren().iterator().next()); -// } -// System.out.println("Returned context : " + context); -// System.out.println("Returned classloader: " + context.getProxyFactory().getClassLoader()); -// System.out.println(); return context.getProxyFactory(); } diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/PageSourceImpl.java b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/PageSourceImpl.java index ec1b5a0a9..79b2a51dd 100644 --- a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/PageSourceImpl.java +++ b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/PageSourceImpl.java @@ -38,6 +38,8 @@ import org.apache.tapestry5.services.ComponentMessages; import org.apache.tapestry5.services.ComponentTemplates; import org.apache.tapestry5.services.pageload.ComponentRequestSelectorAnalyzer; import org.apache.tapestry5.services.pageload.ComponentResourceSelector; +import org.apache.tapestry5.services.pageload.PageClassloaderContext; +import org.apache.tapestry5.services.pageload.PageClassloaderContextManager; import org.slf4j.Logger; public class PageSourceImpl implements PageSource @@ -50,6 +52,8 @@ public class PageSourceImpl implements PageSource private final ComponentClassResolver componentClassResolver; + private final PageClassloaderContextManager pageClassloaderContextManager; + private final Logger logger; final private boolean productionMode; @@ -83,6 +87,13 @@ public class PageSourceImpl implements PageSource return pageName.equals(other.pageName) && selector.equals(other.selector); } + + @Override + public String toString() { + return "CachedPageKey [pageName=" + pageName + ", selector=" + selector + "]"; + } + + } private final Map<CachedPageKey, SoftReference<Page>> pageCache = CollectionFactory.newConcurrentMap(); @@ -90,6 +101,7 @@ public class PageSourceImpl implements PageSource public PageSourceImpl(PageLoader pageLoader, ComponentRequestSelectorAnalyzer selectorAnalyzer, ComponentDependencyRegistry componentDependencyRegistry, ComponentClassResolver componentClassResolver, + PageClassloaderContextManager pageClassloaderContextManager, @Symbol(SymbolConstants.PRODUCTION_MODE) boolean productionMode, Logger logger) { @@ -98,6 +110,7 @@ public class PageSourceImpl implements PageSource this.componentDependencyRegistry = componentDependencyRegistry; this.componentClassResolver = componentClassResolver; this.productionMode = productionMode; + this.pageClassloaderContextManager = pageClassloaderContextManager; this.logger = logger; } @@ -126,7 +139,27 @@ public class PageSourceImpl implements PageSource // different threads. The last built one will "evict" the others from the page cache, // and the earlier ones will be GCed. - page = pageLoader.loadPage(canonicalPageName, selector); +// if (!productionMode) +// { +// // Make sure you get a fresh version of the class before processing its +// // dependencies +// final String className = componentClassResolver.resolvePageNameToClassName(canonicalPageName); +// pageClassloaderContextManager.clear(className); +// final PageClassloaderContext context = pageClassloaderContextManager.get(className); +// final Class<?> clasz; +// try { +// clasz = context.getProxyFactory().getClassLoader().loadClass(className); +// pageClassloaderContextManager.invalidateAndFireInvalidationEvents(context); +// componentDependencyRegistry.register(clasz); +// pageClassloaderContextManager.get(className); +// } catch (ClassNotFoundException e) +// { +// logger.error(e.getMessage(), e); +// } +// +// } + + page = pageLoader.loadPage(canonicalPageName, selector); ref = new SoftReference<Page>(page); @@ -174,14 +207,15 @@ public class PageSourceImpl implements PageSource String pageName; for (String className : resources) { - pageName = componentClassResolver.getLogicalName(className); - if (pageName != null && !pageName.isEmpty()) + if (componentClassResolver.isPage(className)) { + pageName = componentClassResolver.resolvePageClassNameToPageName(className); final Iterator<Entry<CachedPageKey, SoftReference<Page>>> iterator = pageCache.entrySet().iterator(); while (iterator.hasNext()) { final Entry<CachedPageKey, SoftReference<Page>> entry = iterator.next(); - if (entry.getKey().pageName.equalsIgnoreCase(pageName)) + final String entryPageName = entry.getKey().pageName; + if (entryPageName.equalsIgnoreCase(pageName)) { logger.info("Clearing cached page '{}'", pageName); iterator.remove(); diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/RequestErrorFilter.java b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/RequestErrorFilter.java index 30831509a..32fa090ef 100644 --- a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/RequestErrorFilter.java +++ b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/RequestErrorFilter.java @@ -20,6 +20,8 @@ import org.apache.tapestry5.services.ComponentEventLinkEncoder; import org.apache.tapestry5.services.ComponentEventRequestParameters; import org.apache.tapestry5.services.PageRenderRequestParameters; import org.apache.tapestry5.services.RequestExceptionHandler; +import org.apache.tapestry5.services.pageload.PageClassloaderContext; +import org.apache.tapestry5.services.pageload.PageClassloaderContextManager; /** * Filter for the {@link org.apache.tapestry5.http.services.RequestHandler} pipeline used to intercept and report @@ -32,18 +34,21 @@ public class RequestErrorFilter implements RequestFilter private final InvalidationEventHub classesInvalidationHub; private final ComponentEventLinkEncoder componentEventLinkEncoder; private final ComponentInstantiatorSource componentInstantiatorSource; + private final PageClassloaderContextManager pageClassloaderContextManager; private final static String QUERY_PARAMETER = "RequestErrorFilterRedirected"; private final static Pattern CCE_PATTERN = Pattern.compile("((.*)\\scannot be cast to (.*))(.*)"); public RequestErrorFilter(InternalRequestGlobals internalRequestGlobals, RequestExceptionHandler exceptionHandler, @ComponentClasses InvalidationEventHub classesInvalidationHub, ComponentEventLinkEncoder componentEventLinkEncoder, - ComponentInstantiatorSource componentInstantiatorSource) + ComponentInstantiatorSource componentInstantiatorSource, + PageClassloaderContextManager pageClassloaderContextManager) { this.internalRequestGlobals = internalRequestGlobals; this.exceptionHandler = exceptionHandler; this.classesInvalidationHub = classesInvalidationHub; this.componentEventLinkEncoder = componentEventLinkEncoder; this.componentInstantiatorSource = componentInstantiatorSource; + this.pageClassloaderContextManager = pageClassloaderContextManager; } public boolean service(Request request, Response response, RequestHandler handler) throws IOException @@ -60,49 +65,51 @@ public class RequestErrorFilter implements RequestFilter catch (Throwable ex) { - if (request.getParameter(QUERY_PARAMETER) == null) - { - - Throwable rootCause = ex.getCause(); - String classToInvalidate = getClassToInvalidate(rootCause); - - if (classToInvalidate != null) - { - - final List<String> classesToInvalidate = - Arrays.asList(classToInvalidate, ExceptionReport.class.getName()); - componentInstantiatorSource.invalidate(classesToInvalidate); - classesInvalidationHub.fireInvalidationEvent(classesToInvalidate); - - Link link = null; - - final ComponentEventRequestParameters componentEventParameters = componentEventLinkEncoder.decodeComponentEventRequest(request); - if (componentEventParameters != null) - { - link = componentEventLinkEncoder.createComponentEventLink(componentEventParameters, false); - } - - final PageRenderRequestParameters pageRenderParameters = componentEventLinkEncoder.decodePageRenderRequest(request); - if (pageRenderParameters != null) - { - link = componentEventLinkEncoder.createPageRenderLink(pageRenderParameters); - } - - if (link != null) - { - link.addParameter(QUERY_PARAMETER, "true"); - response.sendRedirect(link); - return true; - } - - } - - } +// if (request.getParameter(QUERY_PARAMETER) == null) +// { +// +// Throwable rootCause = ex.getCause(); +// String classToInvalidate = getClassToInvalidate(rootCause); +// +// if (classToInvalidate != null) +// { +// +// final List<String> classesToInvalidate = +// Arrays.asList(classToInvalidate, ExceptionReport.class.getName()); +// componentInstantiatorSource.invalidate(classesToInvalidate); +// classesInvalidationHub.fireInvalidationEvent(classesToInvalidate); +// +// Link link = null; +// +// final ComponentEventRequestParameters componentEventParameters = componentEventLinkEncoder.decodeComponentEventRequest(request); +// if (componentEventParameters != null) +// { +// link = componentEventLinkEncoder.createComponentEventLink(componentEventParameters, false); +// } +// +// final PageRenderRequestParameters pageRenderParameters = componentEventLinkEncoder.decodePageRenderRequest(request); +// if (pageRenderParameters != null) +// { +// link = componentEventLinkEncoder.createPageRenderLink(pageRenderParameters); +// } +// +// if (link != null) +// { +// link.addParameter(QUERY_PARAMETER, "true"); +// response.sendRedirect(link); +// return true; +// } +// +// } +// +// } // Most of the time, we've got exception linked up the kazoo ... but when ClassLoaders // get involved, things go screwy. Exceptions when transforming classes can cause // a NoClassDefFoundError with no cause; here we're trying to link the cause back in. // TAPESTRY-2078 + + System.out.println("YYYYYY" + pageClassloaderContextManager.getRoot().toRecursiveString()); Throwable exceptionToReport = attachNewCause(ex, internalRequestGlobals.getClassLoaderException()); diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/RequestPageCacheImpl.java b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/RequestPageCacheImpl.java index 457b40949..a0c06d4f5 100644 --- a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/RequestPageCacheImpl.java +++ b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/RequestPageCacheImpl.java @@ -14,20 +14,26 @@ package org.apache.tapestry5.internal.services; +import java.util.Map; +import java.util.Set; + +import org.apache.tapestry5.SymbolConstants; import org.apache.tapestry5.commons.util.CollectionFactory; import org.apache.tapestry5.commons.util.ExceptionUtils; +import org.apache.tapestry5.corelib.pages.ExceptionReport; import org.apache.tapestry5.http.services.RequestGlobals; import org.apache.tapestry5.internal.InternalConstants; import org.apache.tapestry5.internal.structure.Page; import org.apache.tapestry5.ioc.ScopeConstants; import org.apache.tapestry5.ioc.annotations.PostInjection; import org.apache.tapestry5.ioc.annotations.Scope; +import org.apache.tapestry5.ioc.annotations.Symbol; import org.apache.tapestry5.ioc.services.PerthreadManager; import org.apache.tapestry5.services.ComponentClassResolver; +import org.apache.tapestry5.services.pageload.PageClassloaderContext; +import org.apache.tapestry5.services.pageload.PageClassloaderContextManager; import org.slf4j.Logger; -import java.util.Map; - /** * In Tapestry 5.1, the implementation of this worked with the page pool (a pool of page instances, reserved * to individual requests/threads). Page pooling was deprecated in 5.2 and removed in 5.3. @@ -36,6 +42,8 @@ import java.util.Map; */ @Scope(ScopeConstants.PERTHREAD) public class RequestPageCacheImpl implements RequestPageCache, Runnable + +/// This should have a listener too! { private final Logger logger; @@ -46,13 +54,25 @@ public class RequestPageCacheImpl implements RequestPageCache, Runnable private final RequestGlobals requestGlobals; private final Map<String, Page> cache = CollectionFactory.newMap(); - - public RequestPageCacheImpl(Logger logger, ComponentClassResolver resolver, PageSource pageSource, RequestGlobals requestGlobals) + + private final PageClassloaderContextManager pageClassloaderContextManager; + + private final ComponentDependencyRegistry componentDependencyRegistry; + + private final boolean productionMode; + + public RequestPageCacheImpl(Logger logger, ComponentClassResolver resolver, PageSource pageSource, RequestGlobals requestGlobals, + PageClassloaderContextManager pageClassloaderContextManager, + ComponentDependencyRegistry componentDependencyRegistry, + @Symbol(SymbolConstants.PRODUCTION_MODE) boolean productionMode) { this.logger = logger; this.resolver = resolver; this.pageSource = pageSource; this.requestGlobals = requestGlobals; + this.pageClassloaderContextManager = pageClassloaderContextManager; + this.productionMode = productionMode; + this.componentDependencyRegistry = componentDependencyRegistry; } @PostInjection @@ -83,13 +103,69 @@ public class RequestPageCacheImpl implements RequestPageCache, Runnable if (page == null) { +// if (!productionMode) +// { +// final String className = resolver.getClassName(canonical); +// if (!componentDependencyRegistry.contains(className)) +// { +// final PageClassloaderContext context = pageClassloaderContextManager.get(className); +// +// try { +// Class<?> clasz = context.getProxyFactory().getClassLoader().loadClass(className); +// componentDependencyRegistry.register(clasz); +// if (context.isUnknown()) +// { +// pageClassloaderContextManager.invalidateAndFireInvalidationEvents(context); +// } +// componentDependencyRegistry.register(clasz); +// if (context.isUnknown()) +// { +// pageClassloaderContextManager.get(className); +// } +// } catch (ClassNotFoundException e) { +// logger.warn("Exception while loading class " + className, e); +// } +// } +// } + page = pageSource.getPage(canonical); + +// // In production mode, we don't have the unknown context nor +// // page invalidation. +// if (!productionMode) +// { +// +// // If the page is in the unknown page classloader context, it means we don't +// // know its dependencies yet. Since we just got an instance assembled, +// // let's throw it away and rebuild with known depedencies. This way, +// // we avoid ClassCastExceptions later on classes used by it. +// final String className = page.getRootComponent().getComponentResources().getComponentModel().getComponentClassName(); +// +// final PageClassloaderContext context = pageClassloaderContextManager.get(className); +// if (context.isUnknown() && false) +// { +// System.out.println("XXXXX Before unknown invalidation"); +// System.out.println(root.toRecursiveString()); +// componentDependencyRegistry.disableInvalidations(); +// pageClassloaderContextManager.invalidateAndFireInvalidationEvents(context); +// componentDependencyRegistry.enableInvalidations(); +// // Force processing the classloader context for this page so it's ready +// // when the new page instance is created +// pageClassloaderContextManager.get(className); +// page = pageSource.getPage(canonical); +// } +// +// } try { page.attached(); - } catch (Throwable t) + } + catch (Throwable t) { + System.out.println("XXXX " + pageClassloaderContextManager.getRoot()); + cache.remove(canonical); + pageClassloaderContextManager.get(ExceptionReport.class.getName()); throw new RuntimeException(String.format("Unable to attach page %s: %s", canonical, ExceptionUtils.toMessage(t)), t); } diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/internal/transform/InjectComponentWorker.java b/tapestry-core/src/main/java/org/apache/tapestry5/internal/transform/InjectComponentWorker.java index 7f9f6c26b..2240a418b 100644 --- a/tapestry-core/src/main/java/org/apache/tapestry5/internal/transform/InjectComponentWorker.java +++ b/tapestry-core/src/main/java/org/apache/tapestry5/internal/transform/InjectComponentWorker.java @@ -98,7 +98,8 @@ public class InjectComponentWorker implements ComponentClassTransformWorker2 if (embedded.getClass().getName().equals(className)) { logger.warn(message); - throw new DifferentClassVersionsException(message, className); + throw new DifferentClassVersionsException(message, className, + fieldType.getClassLoader(), embedded.getClass().getClassLoader()); } else { diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/modules/InternalModule.java b/tapestry-core/src/main/java/org/apache/tapestry5/modules/InternalModule.java index 0b9b73806..122068aba 100644 --- a/tapestry-core/src/main/java/org/apache/tapestry5/modules/InternalModule.java +++ b/tapestry-core/src/main/java/org/apache/tapestry5/modules/InternalModule.java @@ -116,7 +116,7 @@ public class InternalModule binder.bind(AjaxFormUpdateController.class); binder.bind(ResourceDigestManager.class, ResourceDigestManagerImpl.class); // Remove in Tapestry 5.5 binder.bind(RequestPageCache.class, RequestPageCacheImpl.class); - binder.bind(ComponentInstantiatorSource.class); + binder.bind(ComponentInstantiatorSource.class).eagerLoad(); binder.bind(InternalComponentInvalidationEventHub.class); binder.bind(PageSource.class, PageSourceImpl.class); binder.bind(PageLoader.class, PageLoaderImpl.class).preventReloading(); diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/modules/TapestryModule.java b/tapestry-core/src/main/java/org/apache/tapestry5/modules/TapestryModule.java index c4f5b4efe..9c4a55827 100644 --- a/tapestry-core/src/main/java/org/apache/tapestry5/modules/TapestryModule.java +++ b/tapestry-core/src/main/java/org/apache/tapestry5/modules/TapestryModule.java @@ -363,10 +363,11 @@ import org.apache.tapestry5.services.messages.PropertiesFileParser; import org.apache.tapestry5.services.meta.FixedExtractor; import org.apache.tapestry5.services.meta.MetaDataExtractor; import org.apache.tapestry5.services.meta.MetaWorker; +import org.apache.tapestry5.services.pageload.PageClassloaderContextManager; import org.apache.tapestry5.services.pageload.PreloaderMode; +import org.apache.tapestry5.services.rest.MappedEntityManager; import org.apache.tapestry5.services.rest.OpenApiDescriptionGenerator; import org.apache.tapestry5.services.rest.OpenApiTypeDescriber; -import org.apache.tapestry5.services.rest.MappedEntityManager; import org.apache.tapestry5.services.security.ClientWhitelist; import org.apache.tapestry5.services.security.WhitelistAnalyzer; import org.apache.tapestry5.services.templates.ComponentTemplateLocator; @@ -517,7 +518,7 @@ public final class TapestryModule binder.bind(AjaxResponseRenderer.class, AjaxResponseRendererImpl.class); binder.bind(AlertManager.class, AlertManagerImpl.class); binder.bind(ValidationDecoratorFactory.class, ValidationDecoratorFactoryImpl.class); - binder.bind(PropertyConduitSource.class, PropertyConduitSourceImpl.class); + binder.bind(PropertyConduitSource.class, PropertyConduitSourceImpl.class).eagerLoad(); binder.bind(ClientWhitelist.class, ClientWhitelistImpl.class); binder.bind(MetaDataLocator.class, MetaDataLocatorImpl.class); binder.bind(ComponentClassCache.class, ComponentClassCacheImpl.class); @@ -2772,9 +2773,20 @@ public final class TapestryModule public static ComponentDependencyRegistry buildComponentDependencyRegistry( InternalComponentInvalidationEventHub internalComponentInvalidationEventHub, ResourceChangeTracker resourceChangeTracker, - ComponentTemplateSource componentTemplateSource) - { - ComponentDependencyRegistry componentDependencyRegistry = new ComponentDependencyRegistryImpl(); + ComponentTemplateSource componentTemplateSource, + PageClassloaderContextManager pageClassloaderContextManager, + ComponentInstantiatorSource componentInstantiatorSource, + ComponentClassResolver componentClassResolver, + TemplateParser templateParser, + ComponentTemplateLocator componentTemplateLocator) + { + ComponentDependencyRegistry componentDependencyRegistry = + new ComponentDependencyRegistryImpl( + pageClassloaderContextManager, + componentInstantiatorSource.getProxyFactory().getPlasticManager(), + componentClassResolver, + templateParser, + componentTemplateLocator); componentDependencyRegistry.listen(internalComponentInvalidationEventHub); componentDependencyRegistry.listen(resourceChangeTracker); componentDependencyRegistry.listen(componentTemplateSource.getInvalidationEventHub()); diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/services/ComponentClassResolver.java b/tapestry-core/src/main/java/org/apache/tapestry5/services/ComponentClassResolver.java index e94489a5e..62e81f63d 100644 --- a/tapestry-core/src/main/java/org/apache/tapestry5/services/ComponentClassResolver.java +++ b/tapestry-core/src/main/java/org/apache/tapestry5/services/ComponentClassResolver.java @@ -188,4 +188,10 @@ public interface ComponentClassResolver */ @IncompatibleChange(release = "5.8.3", details = "Added method") public String getLogicalName(String className); + + /** + * Returns the class name for a page, component or class given its logical name. + */ + @IncompatibleChange(release = "5.8.3", details = "Added method") + String getClassName(String logicalName); } diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/services/pageload/PageClassloaderContext.java b/tapestry-core/src/main/java/org/apache/tapestry5/services/pageload/PageClassloaderContext.java index 4fe237e34..1b17f69a9 100644 --- a/tapestry-core/src/main/java/org/apache/tapestry5/services/pageload/PageClassloaderContext.java +++ b/tapestry-core/src/main/java/org/apache/tapestry5/services/pageload/PageClassloaderContext.java @@ -13,6 +13,8 @@ // limitations under the License. package org.apache.tapestry5.services.pageload; +import java.util.ArrayList; +import java.util.Collections; import java.util.HashSet; import java.util.Objects; import java.util.Set; @@ -43,6 +45,12 @@ public class PageClassloaderContext private final PlasticManager plasticManager; private final PlasticProxyFactory proxyFactory; + + /** + * Name of the <code>unknown</code> context (i.e. the one for controlled classes + * without dependency information at the moment). + */ + public static final String UNKOWN_CONTEXT_NAME = "unknown"; public PageClassloaderContext(String name, PageClassloaderContext parent, @@ -114,11 +122,22 @@ public class PageClassloaderContext classNames.add(className); } - public void addChildren(PageClassloaderContext context) + /** + * Adds a child context. + */ + public void addChild(PageClassloaderContext context) { children.add(context); } - + + /** + * Removes a child context. + */ + public void removeChildren(PageClassloaderContext context) + { + children.remove(context); + } + /** * Searches for the context that contains the given class in itself and recursivel in its children. */ @@ -156,32 +175,67 @@ public class PageClassloaderContext */ public void invalidate() { - LOGGER.info("Invalidating page classloader context '{}' (class loader {}, classes : {})", + for (PageClassloaderContext child : new ArrayList<>(children)) + { + child.invalidate(); + } + LOGGER.debug("Invalidating page classloader context '{}' (class loader {}, classes : {})", name, proxyFactory.getClassLoader(), classNames); classNames.clear(); parent.getChildren().remove(this); proxyFactory.clearCache(); - for (PageClassloaderContext child : children) + } + + /** + * Returns whether this is the root context. + */ + public boolean isRoot() + { + return parent == null; + } + + /** + * Returns whether this is the <code>unknwon</code> context. + * @see #UNKOWN_CONTEXT_NAME + */ + public boolean isUnknown() + { + return name.equals(UNKOWN_CONTEXT_NAME); + } + + /** + * Returns the set of descendents (children and their children recursively + * of this context. + */ + public Set<PageClassloaderContext> getDescendents() + { + Set<PageClassloaderContext> descendents; + if (children.isEmpty()) { - child.invalidate(); + descendents = Collections.emptySet(); + } + else + { + descendents = new HashSet<>(children); + for (PageClassloaderContext child : children) + { + descendents.addAll(child.getDescendents()); + } } + return descendents; } @Override - public int hashCode() - { + public int hashCode() { return Objects.hash(name); } - + @Override - public boolean equals(Object obj) - { - if (this == obj) - { + public boolean equals(Object obj) { + if (this == obj) { return true; } - if (!(obj instanceof PageClassloaderContext)) - { + if (!(obj instanceof PageClassloaderContext)) { return false; } PageClassloaderContext other = (PageClassloaderContext) obj; @@ -194,10 +248,30 @@ public class PageClassloaderContext return "PageClassloaderContext [name=" + name + ", parent=" + (parent != null ? parent.getName() : "null" ) + ", classLoader=" + afterAt(proxyFactory.getClassLoader().toString()) + - ", object id" + afterAt(super.toString()) + -// ", classNames=" + classNames + + (isRoot() ? "" : ", classNames=" + classNames) + "]"; } + + public String toRecursiveString() + { + StringBuilder builder = new StringBuilder(); + toRecursiveString(builder, ""); + return builder.toString(); + } + + private void toRecursiveString(StringBuilder builder, String tabs) + { + builder.append(tabs); + builder.append(name); + builder.append(" : "); + builder.append(afterAt(proxyFactory.getClassLoader().toString())); + builder.append(" : "); + builder.append(classNames); + builder.append("\n"); + for (PageClassloaderContext child : children) { + child.toRecursiveString(builder, tabs + "\t"); + } + } private static String afterAt(String string) { diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/services/pageload/PageClassloaderContextManager.java b/tapestry-core/src/main/java/org/apache/tapestry5/services/pageload/PageClassloaderContextManager.java index d91857444..8ead155d3 100644 --- a/tapestry-core/src/main/java/org/apache/tapestry5/services/pageload/PageClassloaderContextManager.java +++ b/tapestry-core/src/main/java/org/apache/tapestry5/services/pageload/PageClassloaderContextManager.java @@ -17,6 +17,7 @@ import java.util.Set; import java.util.function.Function; import org.apache.tapestry5.commons.services.PlasticProxyFactory; +import org.apache.tapestry5.internal.services.ComponentDependencyRegistry; import org.apache.tapestry5.internal.services.ComponentInstantiatorSource; /** @@ -38,20 +39,44 @@ public interface PageClassloaderContextManager * {@linkplain} ClassLoader} and returns a new {@linkplain PlasticProxyFactory}. * @return the {@link PageClassloaderContext} associated with that class. */ - PageClassloaderContext get( - String className, - PageClassloaderContext root, - Function<ClassLoader, PlasticProxyFactory> plasticProxyFactoryProvider); + PageClassloaderContext get(String className); /** - * Invalidates one page classloader context and returns a set containing the names + * Invalidates page classloader contexts and returns a set containing the names * of all classes that should be invalidated. */ - Set<String> invalidate(PageClassloaderContext context); + Set<String> invalidate(PageClassloaderContext... contexts); + + /** + * Invalidates page classloader contexts and invalidates the classes in the context as well. + */ + void invalidateAndFireInvalidationEvents(PageClassloaderContext... contexts); + + /** + * Returns the root context. + */ + PageClassloaderContext getRoot(); /** * Clears any state held by this manager. */ void clear(); + /** + * Returns whether contexts are being merged. + */ + boolean isMerging(); + + /** + * Removes one specific class from this manager, invalidating the context where + * it is. + */ + void clear(String className); + + /** + * Initializes this service with the root context and a Plastic proxy factory provider. + * Method can only be called once. None of the parameters may be null. + */ + void initialize(PageClassloaderContext root, Function<ClassLoader, PlasticProxyFactory> plasticProxyFactoryProvider); + } diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/services/pageload/PageClassloaderContextManagerImpl.java b/tapestry-core/src/main/java/org/apache/tapestry5/services/pageload/PageClassloaderContextManagerImpl.java index e382f01b2..94771139b 100644 --- a/tapestry-core/src/main/java/org/apache/tapestry5/services/pageload/PageClassloaderContextManagerImpl.java +++ b/tapestry-core/src/main/java/org/apache/tapestry5/services/pageload/PageClassloaderContextManagerImpl.java @@ -13,16 +13,27 @@ // limitations under the License. package org.apache.tapestry5.services.pageload; +import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; +import java.util.List; +import java.util.Objects; import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Function; import java.util.function.Supplier; +import java.util.stream.Collectors; import org.apache.tapestry5.SymbolConstants; +import org.apache.tapestry5.commons.services.InvalidationEventHub; import org.apache.tapestry5.commons.services.PlasticProxyFactory; import org.apache.tapestry5.internal.services.ComponentDependencyRegistry; +import org.apache.tapestry5.internal.services.InternalComponentInvalidationEventHub; +import org.apache.tapestry5.ioc.annotations.ComponentClasses; import org.apache.tapestry5.ioc.annotations.Symbol; +import org.apache.tapestry5.services.ComponentClassResolver; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * Default {@linkplain PageClassloaderContextManager} implementation. @@ -32,26 +43,63 @@ import org.apache.tapestry5.ioc.annotations.Symbol; public class PageClassloaderContextManagerImpl implements PageClassloaderContextManager { - private static final String UNKOWN_CONTEXT_NAME = "unknown"; + private static final Logger LOGGER = LoggerFactory.getLogger(PageClassloaderContextManager.class); private final ComponentDependencyRegistry componentDependencyRegistry; + private final ComponentClassResolver componentClassResolver; + + private final InternalComponentInvalidationEventHub invalidationHub; + + private final InvalidationEventHub componentClassesInvalidationEventHub; + private final boolean productionMode; + private final static ThreadLocal<Integer> NESTED_MERGE_COUNT = ThreadLocal.withInitial(() -> 0); + + private final static ThreadLocal<Boolean> INVALIDATING_CONTEXT = ThreadLocal.withInitial(() -> false); + + private static final AtomicInteger MERGED_COUNTER = new AtomicInteger(1); + + private Function<ClassLoader, PlasticProxyFactory> plasticProxyFactoryProvider; + + private PageClassloaderContext root; + public PageClassloaderContextManagerImpl( final ComponentDependencyRegistry componentDependencyRegistry, + final ComponentClassResolver componentClassResolver, + final InternalComponentInvalidationEventHub invalidationHub, + final @ComponentClasses InvalidationEventHub componentClassesInvalidationEventHub, @Symbol(SymbolConstants.PRODUCTION_MODE) final boolean productionMode) { super(); this.componentDependencyRegistry = componentDependencyRegistry; + this.componentClassResolver = componentClassResolver; + this.invalidationHub = invalidationHub; + this.componentClassesInvalidationEventHub = componentClassesInvalidationEventHub; this.productionMode = productionMode; + invalidationHub.addInvalidationCallback(this::listen); + NESTED_MERGE_COUNT.set(0); } - + @Override - public PageClassloaderContext get( - final String className, + public void initialize( final PageClassloaderContext root, final Function<ClassLoader, PlasticProxyFactory> plasticProxyFactoryProvider) + { + if (this.root != null) + { + throw new IllegalStateException("PageClassloaderContextManager.initialize() can only be called once"); + } + Objects.requireNonNull(root); + Objects.requireNonNull(plasticProxyFactoryProvider); + this.root = root; + this.plasticProxyFactoryProvider = plasticProxyFactoryProvider; + LOGGER.debug("Root context: {}", root); + } + + @Override + public PageClassloaderContext get(final String className) { PageClassloaderContext context; @@ -63,11 +111,30 @@ public class PageClassloaderContextManagerImpl implements PageClassloaderContext else { - context = root.findByClassName(className); + final String enclosingClassName = getEnclosingClassName(className); + context = root.findByClassName(enclosingClassName); if (context == null) { - context = processUsingDependencies(className, root, () -> getUnknownContext(root, plasticProxyFactoryProvider)); + Set<String> classesToInvalidate = new HashSet<>(); + + context = processUsingDependencies( + enclosingClassName, + root, + () -> getUnknownContext(root, plasticProxyFactoryProvider), + plasticProxyFactoryProvider, + classesToInvalidate); + + if (!classesToInvalidate.isEmpty()) + { + invalidate(classesToInvalidate); + } + + if (!className.equals(enclosingClassName)) + { + context.addClass(className); + } + } } @@ -76,13 +143,23 @@ public class PageClassloaderContextManagerImpl implements PageClassloaderContext } + /** + * If the given class is an inner class, returns the enclosing class. + * Otherwise, returns the class name unchanged. + */ + private String getEnclosingClassName(String className) + { + int index = className.indexOf('$'); + return index <= 0 ? className : className.substring(0, index); + } + private PageClassloaderContext getUnknownContext(final PageClassloaderContext root, final Function<ClassLoader, PlasticProxyFactory> plasticProxyFactoryProvider) { PageClassloaderContext unknownContext = null; for (PageClassloaderContext child : root.getChildren()) { - if (child.getName().equals(UNKOWN_CONTEXT_NAME)) + if (child.getName().equals(PageClassloaderContext.UNKOWN_CONTEXT_NAME)) { unknownContext = child; break; @@ -91,17 +168,34 @@ public class PageClassloaderContextManagerImpl implements PageClassloaderContext if (unknownContext == null) { - unknownContext = new PageClassloaderContext(UNKOWN_CONTEXT_NAME, root, + unknownContext = new PageClassloaderContext(PageClassloaderContext.UNKOWN_CONTEXT_NAME, root, Collections.emptySet(), plasticProxyFactoryProvider.apply(root.getClassLoader())); - root.addChildren(unknownContext); + root.addChild(unknownContext); + LOGGER.debug("Unknown context: {}", unknownContext); } return unknownContext; } + + private PageClassloaderContext processUsingDependencies( + String className, + PageClassloaderContext root, + Supplier<PageClassloaderContext> unknownContextProvider, + Function<ClassLoader, PlasticProxyFactory> plasticProxyFactoryProvider, Set<String> classesToInvalidate) + { + return processUsingDependencies(className, root, unknownContextProvider, plasticProxyFactoryProvider, classesToInvalidate, new HashSet<>()); + } - private PageClassloaderContext processUsingDependencies(String className, PageClassloaderContext root, Supplier<PageClassloaderContext> unknownContextProvider) + + private PageClassloaderContext processUsingDependencies( + String className, + PageClassloaderContext root, + Supplier<PageClassloaderContext> unknownContextProvider, + Function<ClassLoader, PlasticProxyFactory> plasticProxyFactoryProvider, Set<String> classesToInvalidate, + Set<String> alreadyProcessed) { PageClassloaderContext context = root.findByClassName(className); + Set<String> circularDependencies = new HashSet<>(1); if (context == null) { @@ -110,33 +204,295 @@ public class PageClassloaderContextManagerImpl implements PageClassloaderContext if (!root.getPlasticManager().shouldInterceptClassLoading(className)) { context = root; + } else { + if ( + !componentDependencyRegistry.contains(className) && + componentDependencyRegistry.getDependents(className).isEmpty()) + { + // Make sure you get a fresh version of the class before processing its + // dependencies + Class<?> clasz; + PlasticProxyFactory throwaway = plasticProxyFactoryProvider.apply(root.getClassLoader()); + try { + clasz = throwaway.getClassLoader().loadClass(className); + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + componentDependencyRegistry.register(clasz); + alreadyProcessed.remove(className); + return processUsingDependencies(className, root, unknownContextProvider, plasticProxyFactoryProvider, classesToInvalidate, alreadyProcessed); + } + else + { + + alreadyProcessed.add(className); + + // Sorting dependencies alphabetically so we have consistent results. + List<String> dependencies = new ArrayList<>(componentDependencyRegistry.getDependencies(className)); + Collections.sort(dependencies); + + // Process dependencies depth-first + for (String dependency : dependencies) + { + // Avoid infinite recursion loops + if (!alreadyProcessed.contains(dependency)) + { + processUsingDependencies(dependency, root, unknownContextProvider, + plasticProxyFactoryProvider, classesToInvalidate, alreadyProcessed); + } + } + + // Collect context dependencies + Set<PageClassloaderContext> contextDependencies = new HashSet<>(); + for (String dependency : dependencies) + { + // Direct circular dependency + if (componentDependencyRegistry.getDependencies(dependency).contains(className)) + { + circularDependencies.add(dependency); + } + else + { + PageClassloaderContext dependencyContext = root.findByClassName(dependency); + if (dependencyContext == null) + { + dependencyContext = processUsingDependencies(dependency, root, unknownContextProvider, + plasticProxyFactoryProvider, classesToInvalidate, alreadyProcessed); + + } + contextDependencies.add(dependencyContext); + } + } + + if (contextDependencies.size() == 0) + { + context = new PageClassloaderContext( + getContextName(className), + root, + Collections.singleton(className), + plasticProxyFactoryProvider.apply(root.getClassLoader())); + } + else + { + PageClassloaderContext parentContext; + if (contextDependencies.size() == 1) + { + parentContext = contextDependencies.iterator().next(); + } + else + { + parentContext = merge(contextDependencies, plasticProxyFactoryProvider, root, classesToInvalidate); + } + context = new PageClassloaderContext( + getContextName(className), + parentContext, + Collections.singleton(className), + plasticProxyFactoryProvider.apply(root.getClassLoader())); + } + + context.getParent().addChild(context); + + LOGGER.debug("New context: {}", context); + + } } - // If we don't have dependency information about this class, it goes - // into the "unknown" context. - else if (!componentDependencyRegistry.contains(className)) + + } + context.addClass(className); + for (String circularDependency : circularDependencies) + { + context.addClass(circularDependency); + } + return context; + } + + private PageClassloaderContext merge( + Set<PageClassloaderContext> contextDependencies, + Function<ClassLoader, PlasticProxyFactory> plasticProxyFactoryProvider, + PageClassloaderContext root, Set<String> classesToInvalidate) + { + + NESTED_MERGE_COUNT.set(NESTED_MERGE_COUNT.get() + 1); + + if (LOGGER.isDebugEnabled()) + { + + LOGGER.debug("Nested merge count going up to {}", NESTED_MERGE_COUNT.get()); + + String classes; + StringBuilder builder = new StringBuilder(); + builder.append("Merging the following page classloader contexts into one:\n"); + for (PageClassloaderContext context : contextDependencies) { - context = unknownContextProvider.get(); + classes = context.getClassNames().stream() + .map(this::getContextName) + .sorted() + .collect(Collectors.joining(", ")); + builder.append(String.format("\t%s (parent %s) (%s)\n", context.getName(), context.getParent().getName(), classes)); } - // TODO: implement this correctly by creating and merging new contexts - // as needed. - else + LOGGER.debug(builder.toString().trim()); + } + + Set<PageClassloaderContext> allContextsIncludingDescendents = new HashSet<>(); + for (PageClassloaderContext context : contextDependencies) + { + allContextsIncludingDescendents.add(context); + allContextsIncludingDescendents.addAll(context.getDescendents()); + } + + PageClassloaderContext merged; + + // Collect the classes in these dependencies, then invalidate the contexts + + Set<PageClassloaderContext> furtherDependencies = new HashSet<>(); + + Set<String> classNames = new HashSet<>(); + + for (PageClassloaderContext context : contextDependencies) + { + if (!context.isRoot()) { - context = unknownContextProvider.get(); + classNames.addAll(context.getClassNames()); + } + final PageClassloaderContext parent = context.getParent(); + // We don't want the merged context to have a further dependency on + // the root context (it's not mergeable) nor on itself. + if (!parent.isRoot() && !allContextsIncludingDescendents.contains(parent)) + { + furtherDependencies.add(parent); } - } - context.addClass(className); - return context; + + final List<PageClassloaderContext> contextsToInvalidate = contextDependencies.stream() + .filter(c -> !c.isRoot()) + .collect(Collectors.toList()); + + if (!contextsToInvalidate.isEmpty()) + { + classesToInvalidate.addAll(invalidate(contextsToInvalidate.toArray(new PageClassloaderContext[contextsToInvalidate.size()]))); + } + + PageClassloaderContext parent; + + // No context dependencies, so parent is going to be the root one + if (furtherDependencies.size() == 0) + { + parent = root; + } + else + { + // Single shared context dependency, so it's our parent + if (furtherDependencies.size() == 1) + { + parent = furtherDependencies.iterator().next(); + } + // No single context dependency, so we'll need to recursively merge it + // so we can have a single parent. + else + { + parent = merge(furtherDependencies, plasticProxyFactoryProvider, root, classesToInvalidate); + LOGGER.debug("New context: {}", parent); + } + } + + merged = new PageClassloaderContext( + "merged " + MERGED_COUNTER.getAndIncrement(), + parent, + classNames, + plasticProxyFactoryProvider.apply(parent.getClassLoader())); + + parent.addChild(merged); + + NESTED_MERGE_COUNT.set(NESTED_MERGE_COUNT.get() - 1); + if (LOGGER.isDebugEnabled()) + { + LOGGER.debug("Nested merge count going down to {}", NESTED_MERGE_COUNT.get()); + } + + return merged; + } + + @Override + public void clear(String className) + { + final PageClassloaderContext context = root.findByClassName(className); + if (context != null) + { +// invalidationHub.fireInvalidationEvent(new ArrayList<>(invalidate(context))); + invalidate(context); + } + } + + private String getContextName(String className) + { + String contextName = componentClassResolver.getLogicalName(className); + if (contextName == null) + { + contextName = className; + } + return contextName; } @Override - public Set<String> invalidate(PageClassloaderContext context) + public Set<String> invalidate(PageClassloaderContext ... contexts) { Set<String> classNames = new HashSet<>(); - addClassNames(context, classNames); - context.invalidate(); + for (PageClassloaderContext context : contexts) { + addClassNames(context, classNames); + context.invalidate(); + if (context.getParent() != null) + { + context.getParent().removeChildren(context); + } + } return classNames; } + + private List<String> listen(List<String> resources) + { +// System.out.println("XXXXXX Before PageClassloaderContextManager listen: " + root.toRecursiveString()); + if (INVALIDATING_CONTEXT.get()) + { + return Collections.emptyList(); + } + Set<PageClassloaderContext> contextsToInvalidate = new HashSet<>(); + for (String resource : resources) + { + PageClassloaderContext context = root.findByClassName(resource); + if (context != null && !context.isRoot()) + { + contextsToInvalidate.add(context); + } + } + + Set<String> furtherResources = invalidate(contextsToInvalidate.toArray( + new PageClassloaderContext[contextsToInvalidate.size()])); + + // We don't want to invalidate resources more than once + furtherResources.removeAll(resources); + + return new ArrayList<>(furtherResources); + } + + @Override + public void invalidateAndFireInvalidationEvents(PageClassloaderContext... contexts) { + final Set<String> classNames = invalidate(contexts); + INVALIDATING_CONTEXT.set(true); + invalidate(classNames); + INVALIDATING_CONTEXT.set(false); + } + + private void invalidate(Set<String> classesToInvalidate) { + if (!classesToInvalidate.isEmpty()) + { + LOGGER.debug("Invalidating classes {}", classesToInvalidate); + INVALIDATING_CONTEXT.set(true); + final ArrayList<String> classesToInvalidateAsList = new ArrayList<>(classesToInvalidate); + invalidationHub.fireInvalidationEvent(classesToInvalidateAsList); + componentClassesInvalidationEventHub.fireInvalidationEvent(classesToInvalidateAsList); + INVALIDATING_CONTEXT.set(false); + } + } private void addClassNames(PageClassloaderContext context, Set<String> classNames) { classNames.addAll(context.getClassNames()); @@ -145,6 +501,17 @@ public class PageClassloaderContextManagerImpl implements PageClassloaderContext } } + @Override + public PageClassloaderContext getRoot() { + return root; + } + + @Override + public boolean isMerging() + { + return NESTED_MERGE_COUNT.get() > 0; + } + @Override public void clear() { diff --git a/tapestry-core/src/main/resources/org/apache/tapestry5/corelib/pages/PageCatalog.tml b/tapestry-core/src/main/resources/org/apache/tapestry5/corelib/pages/PageCatalog.tml index cc7ae7056..078ac9f7d 100644 --- a/tapestry-core/src/main/resources/org/apache/tapestry5/corelib/pages/PageCatalog.tml +++ b/tapestry-core/src/main/resources/org/apache/tapestry5/corelib/pages/PageCatalog.tml @@ -43,6 +43,7 @@ </t:actionlink> </t:if> <t:actionlink t:id="runGC" zone="pages" class="btn btn-default">Run the GC</t:actionlink> + <t:actionlink t:id="storeDependencyInformation" zone="pages" class="btn btn-warning">Store dependency information</t:actionlink> </div> diff --git a/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/components/Border.java b/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/components/Border.java index 9214e2ddc..0d5007513 100644 --- a/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/components/Border.java +++ b/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/components/Border.java @@ -17,6 +17,7 @@ import org.apache.tapestry5.annotations.Import; import org.apache.tapestry5.annotations.Property; import org.apache.tapestry5.http.services.Request; import org.apache.tapestry5.ioc.annotations.Inject; +import org.apache.tapestry5.services.pageload.PageClassloaderContextManager; import java.util.Calendar; @@ -33,6 +34,9 @@ public class Border @Inject private ComponentResources resources; + + @Inject @Property + private PageClassloaderContextManager pccm; public static final int year; diff --git a/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/InstanceMixinDependencies.java b/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/InstanceMixinDependencies.java new file mode 100644 index 000000000..53146858d --- /dev/null +++ b/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/InstanceMixinDependencies.java @@ -0,0 +1,30 @@ +// Copyright 2023 The Apache Software Foundation +// +// 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.integration.app1.pages; + +import org.apache.tapestry5.annotations.Component; +import org.apache.tapestry5.annotations.MixinClasses; +import org.apache.tapestry5.annotations.Mixins; +import org.apache.tapestry5.corelib.components.TextField; +import org.apache.tapestry5.integration.app1.mixins.EchoValue; + +public class InstanceMixinDependencies +{ + @Component + @Mixins("echovalue2::before:echovalue3") + @MixinClasses(value={EchoValue.class},order={"after:echovalue2;after:echovalue3"}) + private TextField order3; + +} diff --git a/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/services/AppModule.java b/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/services/AppModule.java index ecb207c5f..c9b5b7505 100644 --- a/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/services/AppModule.java +++ b/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/services/AppModule.java @@ -395,9 +395,9 @@ public class AppModule @Contribute(PagePreloader.class) public static void setupPagePreload(Configuration<String> configuration) { - configuration.add("index"); - configuration.add("core/exceptionreport"); - configuration.add("core/t5dashboard"); +// configuration.add("index"); +// configuration.add("core/exceptionreport"); +// configuration.add("core/t5dashboard"); } diff --git a/tapestry-core/src/test/java/org/apache/tapestry5/internal/services/ComponentDependencyRegistryImplTest.java b/tapestry-core/src/test/java/org/apache/tapestry5/internal/services/ComponentDependencyRegistryImplTest.java index b470b5cce..847494f96 100644 --- a/tapestry-core/src/test/java/org/apache/tapestry5/internal/services/ComponentDependencyRegistryImplTest.java +++ b/tapestry-core/src/test/java/org/apache/tapestry5/internal/services/ComponentDependencyRegistryImplTest.java @@ -14,17 +14,57 @@ package org.apache.tapestry5.internal.services; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; -import static org.testng.Assert.assertEquals; import java.io.File; +import java.net.URL; import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; -import org.testng.annotations.BeforeClass; +import org.apache.tapestry5.commons.MappedConfiguration; +import org.apache.tapestry5.corelib.base.AbstractComponentEventLink; +import org.apache.tapestry5.corelib.base.AbstractField; +import org.apache.tapestry5.corelib.base.AbstractLink; +import org.apache.tapestry5.corelib.base.AbstractTextField; +import org.apache.tapestry5.corelib.components.ActionLink; +import org.apache.tapestry5.corelib.components.Any; +import org.apache.tapestry5.corelib.components.BeanEditForm; +import org.apache.tapestry5.corelib.components.EventLink; +import org.apache.tapestry5.corelib.components.If; +import org.apache.tapestry5.corelib.components.TextField; +import org.apache.tapestry5.corelib.components.Zone; +import org.apache.tapestry5.corelib.mixins.RenderDisabled; +import org.apache.tapestry5.integration.app1.components.Border; +import org.apache.tapestry5.integration.app1.components.ErrorComponent; +import org.apache.tapestry5.integration.app1.components.OuterAny; +import org.apache.tapestry5.integration.app1.components.TextOnlyOnDisabledTextField; +import org.apache.tapestry5.integration.app1.mixins.AltTitleDefault; +import org.apache.tapestry5.integration.app1.mixins.EchoValue; +import org.apache.tapestry5.integration.app1.mixins.EchoValue2; +import org.apache.tapestry5.integration.app1.mixins.TextOnlyOnDisabled; +import org.apache.tapestry5.integration.app1.pages.AlertsDemo; +import org.apache.tapestry5.integration.app1.pages.BlockCaller; +import org.apache.tapestry5.integration.app1.pages.BlockHolder; +import org.apache.tapestry5.integration.app1.pages.EmbeddedComponentTypeConflict; +import org.apache.tapestry5.integration.app1.pages.InstanceMixinDependencies; +import org.apache.tapestry5.integration.app1.pages.MixinParameterDefault; +import org.apache.tapestry5.internal.services.templates.DefaultTemplateLocator; +import org.apache.tapestry5.ioc.internal.QuietOperationTracker; +import org.apache.tapestry5.modules.TapestryModule; +import org.apache.tapestry5.plastic.PlasticManager; +import org.apache.tapestry5.services.ComponentClassResolver; +import org.apache.tapestry5.services.pageload.PageClassloaderContextManager; +import org.apache.tapestry5.services.templates.ComponentTemplateLocator; +import org.easymock.EasyMock; +import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; /** @@ -35,19 +75,78 @@ public class ComponentDependencyRegistryImplTest private ComponentDependencyRegistryImpl componentDependencyRegistry; - @BeforeClass + private PageClassloaderContextManager pageClassloaderContextManager; + + private ComponentClassResolver resolver; + + private PlasticManager plasticManager; + + private TemplateParser templateParser; + + @SuppressWarnings("deprecation") + private ComponentTemplateLocator componentTemplateLocator; + + @BeforeMethod public void setup() { assertFalse( String.format("During testing, %s shouldn't exist", ComponentDependencyRegistry.FILENAME), new File(ComponentDependencyRegistry.FILENAME).exists()); - componentDependencyRegistry = new ComponentDependencyRegistryImpl(); + + MockMappedConfiguration<String, URL> templateConfiguration = new MockMappedConfiguration<String, URL>(); + TapestryModule.contributeTemplateParser(templateConfiguration); + templateParser = new TemplateParserImpl(templateConfiguration.map, false, new QuietOperationTracker()); + + componentTemplateLocator = new DefaultTemplateLocator(); + + resolver = EasyMock.createMock(ComponentClassResolver.class); + + expectResolveComponent(TextField.class); + expectResolveComponent(Border.class); + expectResolveComponent(BeanEditForm.class); + expectResolveComponent(Zone.class); + expectResolveComponent(ActionLink.class); + expectResolveComponent(If.class); + expectResolveComponent(ErrorComponent.class); + expectResolveComponent(EventLink.class); + + EasyMock.expect(resolver.resolveMixinTypeToClassName("textonlyondisabled")) + .andReturn(TextOnlyOnDisabled.class.getName()).anyTimes(); + EasyMock.expect(resolver.resolveMixinTypeToClassName("echovalue2")) + .andReturn(EchoValue2.class.getName()).anyTimes(); + EasyMock.expect(resolver.resolveMixinTypeToClassName("alttitledefault")) + .andReturn(AltTitleDefault.class.getName()).anyTimes(); + + pageClassloaderContextManager = EasyMock.createMock(PageClassloaderContextManager.class); + plasticManager = EasyMock.createMock(PlasticManager.class); + EasyMock.expect(plasticManager.shouldInterceptClassLoading(EasyMock.anyString())) + .andAnswer(() -> { + String className = (String) EasyMock.getCurrentArguments()[0]; + return className.contains(".pages.") || className.contains(".mixins.") || + className.contains(".components.") || className.contains(".base."); + }).anyTimes(); + componentDependencyRegistry = new ComponentDependencyRegistryImpl( + pageClassloaderContextManager, plasticManager, resolver, templateParser, componentTemplateLocator); + EasyMock.replay(pageClassloaderContextManager, plasticManager, resolver); + } + + private void expectResolveComponent(final Class<?> clasz) { + EasyMock.expect(resolver.resolveComponentTypeToClassName(clasz.getSimpleName())) + .andReturn(clasz.getName()).anyTimes(); + EasyMock.expect(resolver.resolveComponentTypeToClassName(clasz.getSimpleName().toLowerCase())) + .andReturn(clasz.getName()).anyTimes(); + } + + private void configurePCCM(boolean merging) + { + EasyMock.reset(pageClassloaderContextManager); + EasyMock.expect(pageClassloaderContextManager.isMerging()).andReturn(merging).anyTimes(); + EasyMock.replay(pageClassloaderContextManager); } @Test(timeOut = 5000) public void listen() { - add("foo", "bar"); add("d", "a"); add("dd", "aa"); @@ -55,12 +154,21 @@ public class ComponentDependencyRegistryImplTest add("dd", "a"); final List<String> resources = Arrays.asList("a", "aa", "none"); - final List<String> result = componentDependencyRegistry.listen(resources); + + configurePCCM(true); + List<String> result = componentDependencyRegistry.listen(resources); + assertEquals(0, result.size()); + + configurePCCM(false); + result = componentDependencyRegistry.listen(resources); Collections.sort(result); assertEquals(result, Arrays.asList("d", "dd")); + assertEquals("bar", componentDependencyRegistry.getDependencies("foo").iterator().next()); + assertEquals("foo", componentDependencyRegistry.getDependents("bar").iterator().next()); - final List<String> returnValue = componentDependencyRegistry.listen(Collections.emptyList()); - assertEquals(returnValue, Collections.emptyList()); + configurePCCM(false); + result = componentDependencyRegistry.listen(Collections.emptyList()); + assertEquals(result, Collections.emptyList()); assertEquals(componentDependencyRegistry.getDependents("bar").size(), 0); assertEquals(componentDependencyRegistry.getDependencies("foo").size(), 0); @@ -77,11 +185,15 @@ public class ComponentDependencyRegistryImplTest final String fulano = "a"; final String beltrano = "aa"; - assertEquals(componentDependencyRegistry.getDependencies(foo), Collections.emptySet(), - "getDependents() should never return null"); + assertEquals( + "getDependents() should never return null", + Collections.emptySet(), + componentDependencyRegistry.getDependencies(foo)); - assertEquals(componentDependencyRegistry.getDependents(foo), Collections.emptySet(), - "getDependents() should never return null"); + assertEquals( + "getDependents() should never return null", + Collections.emptySet(), + componentDependencyRegistry.getDependents(foo)); add(foo, bar); add(something, fulano); @@ -92,17 +204,17 @@ public class ComponentDependencyRegistryImplTest add(fulano, null); add(beltrano, null); - assertEquals(componentDependencyRegistry.getDependencies(other), new HashSet<>(Arrays.asList(fulano, beltrano))); - assertEquals(componentDependencyRegistry.getDependencies(something), new HashSet<>(Arrays.asList(fulano))); - assertEquals(componentDependencyRegistry.getDependencies(fulano), new HashSet<>(Arrays.asList())); - assertEquals(componentDependencyRegistry.getDependencies(foo), new HashSet<>(Arrays.asList(bar))); - assertEquals(componentDependencyRegistry.getDependencies(bar), new HashSet<>(Arrays.asList())); + assertEquals(new HashSet<>(Arrays.asList(fulano, beltrano)), componentDependencyRegistry.getDependencies(other)); + assertEquals(new HashSet<>(Arrays.asList(fulano)), componentDependencyRegistry.getDependencies(something)); + assertEquals(new HashSet<>(Arrays.asList()), componentDependencyRegistry.getDependencies(fulano)); + assertEquals(new HashSet<>(Arrays.asList(bar)), componentDependencyRegistry.getDependencies(foo)); + assertEquals(new HashSet<>(Arrays.asList()), componentDependencyRegistry.getDependencies(bar)); - assertEquals(componentDependencyRegistry.getDependents(bar), new HashSet<>(Arrays.asList(foo))); - assertEquals(componentDependencyRegistry.getDependents(fulano), new HashSet<>(Arrays.asList(other, something))); - assertEquals(componentDependencyRegistry.getDependents(foo), new HashSet<>(Arrays.asList())); + assertEquals(new HashSet<>(Arrays.asList(foo)), componentDependencyRegistry.getDependents(bar)); + assertEquals(new HashSet<>(Arrays.asList(other, something)), componentDependencyRegistry.getDependents(fulano)); + assertEquals(new HashSet<>(Arrays.asList()), componentDependencyRegistry.getDependents(foo)); - assertEquals(componentDependencyRegistry.getRootClasses(), new HashSet<>(Arrays.asList(bar, fulano, beltrano))); + assertEquals(new HashSet<>(Arrays.asList(bar, fulano, beltrano)), componentDependencyRegistry.getRootClasses()); assertTrue(componentDependencyRegistry.contains(foo)); assertTrue(componentDependencyRegistry.contains(bar)); @@ -122,9 +234,115 @@ public class ComponentDependencyRegistryImplTest } + @Test + public void register() + { + + componentDependencyRegistry.clear(); + + // Superclass + componentDependencyRegistry.register(EventLink.class); + + // Superclass, recursively + assertDependencies(AbstractComponentEventLink.class, AbstractLink.class); + assertDependencies(AbstractLink.class); + + // @InjectPage and circular dependencies + componentDependencyRegistry.register(BlockCaller.class); + assertDependencies(BlockCaller.class, BlockHolder.class); + assertDependencies(BlockHolder.class, BlockCaller.class); + + // @InjectComponent + // Components declared in templates + componentDependencyRegistry.register(AlertsDemo.class); + assertDependencies(AlertsDemo.class, Zone.class, // @InjectComponent + // Components declared in template + Border.class, BeanEditForm.class, Zone.class, If.class, + ActionLink.class, ErrorComponent.class, EventLink.class); + + // Mixins defined in in templates (t:mixins="..."). + componentDependencyRegistry.register(MixinParameterDefault.class); + assertDependencies(MixinParameterDefault.class, AltTitleDefault.class, // Mixin + ActionLink.class, Border.class); // Components declared in template); + + // @Component, type() not defined + componentDependencyRegistry.register(OuterAny.class); + assertDependencies(OuterAny.class, Any.class); + + // @Component, type() defined + componentDependencyRegistry.register(EmbeddedComponentTypeConflict.class); + assertDependencies(EmbeddedComponentTypeConflict.class, TextField.class); + + // @Mixin, type() not defined + componentDependencyRegistry.register(AbstractTextField.class); + assertDependencies(AbstractTextField.class, + RenderDisabled.class, AbstractField.class); + + // @Mixin, type() defined + componentDependencyRegistry.register(TextOnlyOnDisabledTextField.class); + assertDependencies(TextOnlyOnDisabledTextField.class, + TextOnlyOnDisabled.class, TextField.class); + + // @MixinClasses and @Mixins + componentDependencyRegistry.register(InstanceMixinDependencies.class); + assertDependencies(InstanceMixinDependencies.class, + EchoValue.class, EchoValue2.class, TextField.class); + + + } + + private void assertDependencies(Class clasz, Class... dependencies) { + assertEquals( + setOf(dependencies), + componentDependencyRegistry.getDependencies(clasz.getName())); + } + + private static Set<String> setOf(Class ... classes) + { + return Arrays.asList(classes).stream() + .map(Class::getName) + .collect(Collectors.toSet()); + } + + private static Set<String> setOf(String ... strings) + { + return new HashSet<>(Arrays.asList(strings)); + } + private void add(String component, String dependency) { componentDependencyRegistry.add(component, dependency, true); } + private static final class MockMappedConfiguration<String, URL> implements MappedConfiguration<String, URL> + { + + private final Map<String, URL> map = new HashMap<>(); + + @Override + public void add(String key, URL value) + { + map.put(key, value); + } + + @Override + public void override(String key, URL value) + { + throw new UnsupportedOperationException(); + } + + @Override + public void addInstance(String key, Class<? extends URL> clazz) + { + throw new UnsupportedOperationException(); + } + + @Override + public void overrideInstance(String key, Class<? extends URL> clazz) + { + throw new UnsupportedOperationException(); + } + + } + } diff --git a/tapestry-core/src/test/resources/log4j.properties b/tapestry-core/src/test/resources/log4j.properties index 7090fd495..6e07ce841 100644 --- a/tapestry-core/src/test/resources/log4j.properties +++ b/tapestry-core/src/test/resources/log4j.properties @@ -8,6 +8,8 @@ log4j.appender.A1.layout=org.apache.log4j.PatternLayout log4j.appender.A1.layout.ConversionPattern=%t [%p] %c{1} %m%n log4j.category.org.apache.tapestry5.integration.app2=debug +log4j.category.org.apache.tapestry5.services.pageload.PageClassloaderContextManager=debug +log4j.category.org.apache.tapestry5.services.pageload.PageClassloaderContext=debug # log4j.category.tapestry.render=debug diff --git a/tapestry-core/src/test/resources/org/apache/tapestry5/integration/app1/components/Border.tml b/tapestry-core/src/test/resources/org/apache/tapestry5/integration/app1/components/Border.tml index 007f7a017..2a2941e0e 100644 --- a/tapestry-core/src/test/resources/org/apache/tapestry5/integration/app1/components/Border.tml +++ b/tapestry-core/src/test/resources/org/apache/tapestry5/integration/app1/components/Border.tml @@ -44,6 +44,7 @@ <t:devtool class="pull-right"/> <t:remove>alert-class is for the AlertsTest.check_informal_parameters test</t:remove> + <pre>${pccm.root.toRecursiveString().trim()}</pre> <t:alerts class="alert-class"/> <t:body/>