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


The following commit(s) were added to refs/heads/better-page-invalidation by 
this push:
     new 52113b4ca TAP5-2742: first pass at multiple classloader support
52113b4ca is described below

commit 52113b4ca244f8f8acac6447d5c10b2b49b3b07c
Author: Thiago H. de Paula Figueiredo <[email protected]>
AuthorDate: Thu Feb 16 01:03:18 2023 -0300

    TAP5-2742: first pass at multiple classloader support
    
    for live class reloading
---
 583_RELEASE_NOTES.md                               |   1 +
 .../services/PropertyConduitSourceImpl.java        |   2 +-
 .../services/PlasticProxyFactoryImpl.java          |   6 +
 .../commons/services/PlasticProxyFactory.java      |  21 +-
 .../apache/tapestry5/plastic/PlasticManager.java   |  11 ++
 .../internal/services/ComponentClassCacheImpl.java |   2 +-
 .../services/ComponentDependencyRegistry.java      |  22 ++-
 .../services/ComponentDependencyRegistryImpl.java  |  37 +++-
 .../services/ComponentInstantiatorSourceImpl.java  | 215 ++++++++++++++++++---
 .../internal/services/PageSourceImpl.java          |  33 ++--
 .../apache/tapestry5/modules/PageLoadModule.java   |   3 +
 .../services/pageload/PageClassloaderContext.java  | 211 ++++++++++++++++++++
 .../pageload/PageClassloaderContextManager.java    |  57 ++++++
 .../PageClassloaderContextManagerImpl.java         | 153 +++++++++++++++
 .../tapestry5/services/pageload/package-info.java  |   2 +-
 .../ComponentDependencyRegistryImplTest.java       |  52 ++++-
 16 files changed, 767 insertions(+), 61 deletions(-)

diff --git a/583_RELEASE_NOTES.md b/583_RELEASE_NOTES.md
index 577db3087..e7923cc55 100644
--- a/583_RELEASE_NOTES.md
+++ b/583_RELEASE_NOTES.md
@@ -6,6 +6,7 @@ Scratch pad for changes destined for the 5.8.3 release notes 
page.
 * getChangeResourcesMemos() to URLChangeTracker
 * getValues() to MultiKey
 * New getLogicalName() method in ComponentClassResolver.
+* New getPlasticManager() method in PlasticProxyFactory
 
 # Non-backward-compatible changes (but that probably won't cause problems)
 
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 8a304a5ce..66f106660 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
@@ -1524,7 +1524,7 @@ public class PropertyConduitSourceImpl implements 
PropertyConduitSource
                     break;
             }
 
-            return proxyFactory.createProxy(InternalPropertyConduit.class,
+            return 
proxyFactory.getProxyFactory(rootClass.getName()).createProxy(InternalPropertyConduit.class,
                     new PropertyConduitBuilder(rootClass, expression, 
tree)).newInstance();
         } catch (Exception ex)
         {
diff --git 
a/beanmodel/src/main/java/org/apache/tapestry5/beanmodel/services/PlasticProxyFactoryImpl.java
 
b/beanmodel/src/main/java/org/apache/tapestry5/beanmodel/services/PlasticProxyFactoryImpl.java
index abc5c762c..1e6a50785 100644
--- 
a/beanmodel/src/main/java/org/apache/tapestry5/beanmodel/services/PlasticProxyFactoryImpl.java
+++ 
b/beanmodel/src/main/java/org/apache/tapestry5/beanmodel/services/PlasticProxyFactoryImpl.java
@@ -297,4 +297,10 @@ public class PlasticProxyFactoryImpl implements 
PlasticProxyFactory
         manager.removePlasticClassListener(listener);
     }
 
+    @Override
+    public PlasticManager getPlasticManager() 
+    {
+        return manager;
+    }
+
 }
diff --git 
a/commons/src/main/java/org/apache/tapestry5/commons/services/PlasticProxyFactory.java
 
b/commons/src/main/java/org/apache/tapestry5/commons/services/PlasticProxyFactory.java
index 3b8b9bc47..0237cc7a4 100644
--- 
a/commons/src/main/java/org/apache/tapestry5/commons/services/PlasticProxyFactory.java
+++ 
b/commons/src/main/java/org/apache/tapestry5/commons/services/PlasticProxyFactory.java
@@ -1,4 +1,4 @@
-// Copyright 2011, 2012 The Apache Software Foundation
+// Copyright 2011, 2012, 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.
@@ -24,6 +24,7 @@ import org.apache.tapestry5.plastic.ClassInstantiator;
 import org.apache.tapestry5.plastic.PlasticClassListenerHub;
 import org.apache.tapestry5.plastic.PlasticClassTransformation;
 import org.apache.tapestry5.plastic.PlasticClassTransformer;
+import org.apache.tapestry5.plastic.PlasticManager;
 
 /**
  * A service used to create proxies of varying types. As a secondary concern, 
manages to identify the
@@ -169,6 +170,24 @@ public interface PlasticProxyFactory extends 
PlasticClassListenerHub
      *
      * @since 5.3.3
      */
+    @IncompatibleChange(release = "5.3.3", details = "Added method")
     void clearCache();
+    
+    /**
+     * Returns the {@linkplain PlasticManager} instance used by this 
PlasticProxyFactory.
+     * @since 5.8.3
+     */
+    @IncompatibleChange(release = "5.8.3", details = "Added method")
+    PlasticManager getPlasticManager();
+
+    /**
+     * Returns the {@linkplain PlasticProxyFactory} instance to be used for a 
given class.
+     * Default implementation returns <code>this</code>.
+     * @since 5.8.3
+     */
+    default PlasticProxyFactory getProxyFactory(String className)
+    {
+        return this;
+    }
 
 }
diff --git 
a/plastic/src/main/java/org/apache/tapestry5/plastic/PlasticManager.java 
b/plastic/src/main/java/org/apache/tapestry5/plastic/PlasticManager.java
index 74acf21c1..1d42bd6b7 100644
--- a/plastic/src/main/java/org/apache/tapestry5/plastic/PlasticManager.java
+++ b/plastic/src/main/java/org/apache/tapestry5/plastic/PlasticManager.java
@@ -345,4 +345,15 @@ public class PlasticManager implements 
PlasticClassListenerHub
     {
         pool.removePlasticClassListener(listener);
     }
+
+    /**
+     * Returns whether a given class will have it classloading intercepted for
+     * live class reloading.
+     * @since 5.8.3
+     */
+    public boolean shouldInterceptClassLoading(String className) 
+    {
+        return pool.shouldInterceptClassLoading(className);
+    }
+    
 }
diff --git 
a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ComponentClassCacheImpl.java
 
b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ComponentClassCacheImpl.java
index 349189707..504f612ee 100644
--- 
a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ComponentClassCacheImpl.java
+++ 
b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ComponentClassCacheImpl.java
@@ -105,7 +105,7 @@ public class ComponentClassCacheImpl implements 
ComponentClassCache
 
     private Class lookupClassForType(String className)
     {
-        ClassLoader componentLoader = plasticFactory.getClassLoader();
+        ClassLoader componentLoader = 
plasticFactory.getProxyFactory(className).getClassLoader();
         try
         {
             return PlasticInternalUtils.toClass(componentLoader, className);
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 dfa89611d..c4ab81a6f 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
@@ -33,6 +33,12 @@ import org.apache.tapestry5.plastic.PlasticField;
  * @since 5.8.3
  */
 public interface ComponentDependencyRegistry {
+
+    /**
+     * Name of the file where the dependency information is stored between 
webapp runs.
+     */
+    String FILENAME = "tapestryComponentDependencies.json";
+
     
     /**
      * Register all the dependencies of a given component.
@@ -80,10 +86,20 @@ public interface ComponentDependencyRegistry {
      * @see #FILENAME
      */
     void writeFile();
-
+    
     /**
-     * Name of the file where the dependency information is stored between 
webapp runs.
+     * Tells whether this registry already contans a given class name.
      */
-    String FILENAME = "tapestryComponentDependencies.json";
+    boolean contains(String className);
+    
+    /**
+     * Returns the set of all class names in the registry.
+     */
+    Set<String> getClassNames();
+    
+    /**
+     * Returns the set of all root classes (i.e. ones with no dependencies).
+     */
+    Set<String> getRootClasses();
 
 }
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 1765985b7..08689c4f3 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
@@ -242,8 +242,20 @@ public class ComponentDependencyRegistryImpl implements 
ComponentDependencyRegis
         add(getClassName(component), getClassName(dependency));
     }
     
-    // Protected just for testing
-    void add(String component, String dependency) 
+    // Just for unit tests
+    void add(String component, String dependency, boolean 
markAsAlreadyProcessed)
+    {
+        if (markAsAlreadyProcessed)
+        {
+            alreadyProcessed.add(component);
+        }
+        if (dependency != null)
+        {
+            add(component, dependency);
+        }
+    }
+    
+    private void add(String component, String dependency) 
     {
         Objects.requireNonNull(component, "Parameter component cannot be 
null");
         Objects.requireNonNull(dependency, "Parameter dependency cannot be 
null");
@@ -258,7 +270,7 @@ public class ComponentDependencyRegistryImpl implements 
ComponentDependencyRegis
             dependents.add(component);
         }
     }
-
+    
     @Override
     public void listen(InvalidationEventHub invalidationEventHub) 
     {
@@ -315,5 +327,24 @@ public class ComponentDependencyRegistryImpl implements 
ComponentDependencyRegis
             }
         } 
     }
+
+    @Override
+    public boolean contains(String className) 
+    {
+        return alreadyProcessed.contains(className);
+    }
+
+    @Override
+    public Set<String> getClassNames() 
+    {
+        return Collections.unmodifiableSet(new HashSet<>(alreadyProcessed));
+    }
+
+    @Override
+    public Set<String> getRootClasses() {
+        return alreadyProcessed.stream()
+                .filter(c -> getDependencies(c).isEmpty())
+                .collect(Collectors.toSet());
+    }
     
 }
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 cff65325e..a2ff8d87c 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
@@ -12,6 +12,10 @@
 
 package org.apache.tapestry5.internal.services;
 
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Method;
+import java.util.Collections;
+import java.util.HashSet;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
@@ -22,6 +26,8 @@ import java.util.stream.Collectors;
 
 import org.apache.tapestry5.ComponentResources;
 import org.apache.tapestry5.beanmodel.services.PlasticProxyFactoryImpl;
+import org.apache.tapestry5.commons.Location;
+import org.apache.tapestry5.commons.ObjectCreator;
 import org.apache.tapestry5.commons.Resource;
 import org.apache.tapestry5.commons.services.PlasticProxyFactory;
 import org.apache.tapestry5.commons.util.CollectionFactory;
@@ -57,6 +63,8 @@ import org.apache.tapestry5.plastic.MethodInvocation;
 import org.apache.tapestry5.plastic.PlasticClass;
 import org.apache.tapestry5.plastic.PlasticClassEvent;
 import org.apache.tapestry5.plastic.PlasticClassListener;
+import org.apache.tapestry5.plastic.PlasticClassTransformation;
+import org.apache.tapestry5.plastic.PlasticClassTransformer;
 import org.apache.tapestry5.plastic.PlasticField;
 import org.apache.tapestry5.plastic.PlasticManager;
 import org.apache.tapestry5.plastic.PlasticManager.PlasticManagerBuilder;
@@ -71,6 +79,8 @@ import org.apache.tapestry5.runtime.PageLifecycleListener;
 import org.apache.tapestry5.services.ComponentClassResolver;
 import org.apache.tapestry5.services.ComponentEventHandler;
 import org.apache.tapestry5.services.TransformConstants;
+import org.apache.tapestry5.services.pageload.PageClassloaderContext;
+import org.apache.tapestry5.services.pageload.PageClassloaderContextManager;
 import org.apache.tapestry5.services.transform.ComponentClassTransformWorker2;
 import org.apache.tapestry5.services.transform.ControlledPackageType;
 import org.apache.tapestry5.services.transform.TransformationSupport;
@@ -101,10 +111,12 @@ public final class ComponentInstantiatorSourceImpl 
implements ComponentInstantia
     private final boolean productionMode;
 
     private final ComponentClassResolver resolver;
-
-    private volatile PlasticProxyFactory proxyFactory;
-
-    private volatile PlasticManager manager;
+    
+    private final PageClassloaderContextManager pageClassloaderContextManager;
+    
+    private PageClassloaderContext rootPageClassloaderContext;
+    
+    private PlasticProxyFactoryProxy plasticProxyFactoryProxy;
 
     /**
      * Map from class name to Instantiator.
@@ -147,7 +159,10 @@ public final class ComponentInstantiatorSourceImpl 
implements ComponentInstantia
 
                                            ComponentClassResolver resolver,
 
-                                           
InternalComponentInvalidationEventHub invalidationHub)
+                                           
InternalComponentInvalidationEventHub invalidationHub,
+                                           
+                                           PageClassloaderContextManager 
pageClassloaderContextManager                                         
+            )
     {
         this.parent = proxyFactory.getClassLoader();
         this.transformerChain = transformerChain;
@@ -158,6 +173,7 @@ public final class ComponentInstantiatorSourceImpl 
implements ComponentInstantia
         this.invalidationHub = invalidationHub;
         this.productionMode = productionMode;
         this.resolver = resolver;
+        this.pageClassloaderContextManager = pageClassloaderContextManager;
 
         // For now, we just need the keys of the configuration. When there are 
more types of controlled
         // packages, we'll need to do more.
@@ -182,14 +198,23 @@ public final class ComponentInstantiatorSourceImpl 
implements ComponentInstantia
             
             final List<String> classNames = 
changedResources.stream().map(ClassName::getClassName).collect(Collectors.toList());
             
+            final Set<String> classesToInvalidate = new HashSet<>();
+            
+            for (String className : classNames) 
+            {
+                final PageClassloaderContext context = 
rootPageClassloaderContext.findByClassName(className);
+                if (context != rootPageClassloaderContext && context != null)
+                {
+                    
classesToInvalidate.addAll(pageClassloaderContextManager.invalidate(context));
+                }
+            }
+            
+            classNames.clear();
+            classNames.addAll(classesToInvalidate);
+            
             invalidate(classNames);
-
-            invalidationHub.fireInvalidationEvent(changedResources.stream()
-                    .map(ClassNameHolder::getClassName)
-                    .collect(Collectors.toList()));
             
-            // TODO Remove this when multiple classloaders are figured out
-            initializeService();
+            invalidationHub.fireInvalidationEvent(classNames);
             
         }
     }
@@ -220,14 +245,16 @@ public final class ComponentInstantiatorSourceImpl 
implements ComponentInstantia
     {
         changeTracker.clear();
         invalidationHub.classInControlledPackageHasChanged();
+        pageClassloaderContextManager.clear();
     }
 
     public void run()
     {
         changeTracker.clear();
         classToInstantiator.clear();
-        proxyFactory.clearCache();
+        // TODO fix this
 //        classToModel.clear();
+        pageClassloaderContextManager.clear();
         initializeService();
     }
 
@@ -238,30 +265,41 @@ public final class ComponentInstantiatorSourceImpl 
implements ComponentInstantia
      */
     private void initializeService()
     {
-        if (manager == null)
+        
+        pageClassloaderContextManager.clear();
+        
+        if (rootPageClassloaderContext == null)
         {
             logger.info("Initializing page pool");
+            
+            pageClassloaderContextManager.clear();
+            
+            PlasticProxyFactory proxyFactory = 
createPlasticProxyFactory(parent);
+            rootPageClassloaderContext = new PageClassloaderContext(
+                    "root", null, Collections.emptySet(), proxyFactory);
         }
         else 
         {
             logger.info("Restarting page pool");
         }
-        PlasticManagerBuilder builder = 
PlasticManager.withClassLoader(parent).delegate(this)
-                .packages(controlledPackageNames);
 
+        classToInstantiator.clear();
+        classToModel.clear();
+    }
+
+    private PlasticProxyFactory createPlasticProxyFactory(final ClassLoader 
parentClassloader) 
+    {
+        PlasticManagerBuilder builder = 
PlasticManager.withClassLoader(parentClassloader)
+                .delegate(this)
+                .packages(controlledPackageNames);
         if (!productionMode)
         {
             builder.enable(TransformationOption.FIELD_WRITEBEHIND);
         }
-
-        manager = builder.create();
-
-        manager.addPlasticClassListener(this);
-
-        proxyFactory = new PlasticProxyFactoryImpl(manager, logger);
-
-        classToInstantiator.clear();
-        classToModel.clear();
+        PlasticManager plasticManager = builder.create();
+        plasticManager.addPlasticClassListener(this);
+        PlasticProxyFactory proxyFactory = new 
PlasticProxyFactoryImpl(plasticManager, logger);
+        return proxyFactory;
     }
 
     public Instantiator getInstantiator(final String className)
@@ -279,7 +317,10 @@ 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 ClassInstantiator<Component> plasticInstantiator 
= manager.getClassInstantiator(className);
+                        final PageClassloaderContext context = 
pageClassloaderContextManager.get(
+                                className, rootPageClassloaderContext, 
ComponentInstantiatorSourceImpl.this::createPlasticProxyFactory);
+                        final ClassInstantiator<Component> plasticInstantiator 
= 
+                                
context.getPlasticManager().getClassInstantiator(className);
 
                         final ComponentModel model = 
classToModel.get(className);
 
@@ -313,9 +354,13 @@ public final class ComponentInstantiatorSourceImpl 
implements ComponentInstantia
 
     public PlasticProxyFactory getProxyFactory()
     {
-        return proxyFactory;
+        if (plasticProxyFactoryProxy == null)
+        {
+            plasticProxyFactoryProxy = new PlasticProxyFactoryProxy();
+        }
+        return plasticProxyFactoryProxy;
     }
-
+    
     public void transform(final PlasticClass plasticClass)
     {
         tracker.run(String.format("Running component class transformations on 
%s", plasticClass.getClassName()),
@@ -467,7 +512,9 @@ public final class ComponentInstantiatorSourceImpl 
implements ComponentInstantia
         {
             try
             {
-                return PlasticInternalUtils.toClass(manager.getClassLoader(), 
typeName);
+                final PageClassloaderContext context = 
pageClassloaderContextManager.get(
+                        typeName, rootPageClassloaderContext, 
ComponentInstantiatorSourceImpl.this::createPlasticProxyFactory);
+                return 
PlasticInternalUtils.toClass(context.getPlasticManager().getClassLoader(), 
typeName);
             } catch (ClassNotFoundException ex)
             {
                 throw new RuntimeException(String.format(
@@ -586,5 +633,115 @@ public final class ComponentInstantiatorSourceImpl 
implements ComponentInstantia
         }
         
     }
-    
+
+    private class PlasticProxyFactoryProxy implements PlasticProxyFactory
+    {
+
+        @Override
+        public void addPlasticClassListener(PlasticClassListener listener) 
+        {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public void removePlasticClassListener(PlasticClassListener listener) 
+        {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public ClassLoader getClassLoader() {
+            return 
rootPageClassloaderContext.getProxyFactory().getClassLoader();
+        }
+
+        @Override
+        public <T> ClassInstantiator<T> createProxy(Class<T> interfaceType, 
PlasticClassTransformer callback) 
+        {
+            return 
getProxyFactory(interfaceType.getName()).createProxy(interfaceType, callback);
+        }
+
+        @Override
+        public <T> ClassInstantiator<T> createProxy(Class<T> interfaceType,
+                Class<? extends T> implementationType,
+                PlasticClassTransformer callback,
+                boolean introduceInterface) 
+        {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public <T> ClassInstantiator<T> createProxy(Class<T> interfaceType, 
Class<? extends T> implementationType, PlasticClassTransformer callback) 
+        {
+            throw new UnsupportedOperationException();            
+        }
+
+        @Override
+        public <T> PlasticClassTransformation<T> 
createProxyTransformation(Class<T> interfaceType) 
+        {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public <T> PlasticClassTransformation<T> 
createProxyTransformation(Class<T> interfaceType, Class<? extends T> 
implementationType) 
+        {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public <T> T createProxy(Class<T> interfaceType, ObjectCreator<T> 
creator, String description) 
+        {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public <T> T createProxy(Class<T> interfaceType, Class<? extends T> 
implementationType, ObjectCreator<T> creator, String description) 
+        {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public Location getMethodLocation(Method method) {
+            return 
getProxyFactory(method.getDeclaringClass().getName()).getMethodLocation(method);
+        }
+
+        @Override
+        public Location getConstructorLocation(Constructor constructor) 
+        {
+            return 
getProxyFactory(constructor.getDeclaringClass().getName()).getConstructorLocation(constructor);
+        }
+
+        @Override
+        public void clearCache() {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public PlasticManager getPlasticManager() {
+            throw new UnsupportedOperationException();
+        }
+        
+        @Override
+        public PlasticProxyFactory getProxyFactory(String className)
+        {
+            PageClassloaderContext context = 
rootPageClassloaderContext.findByClassName(className);
+            if (context == null)
+            {
+                context = pageClassloaderContextManager.get(
+                        className, rootPageClassloaderContext, 
ComponentInstantiatorSourceImpl.this::createPlasticProxyFactory);
+            }
+//            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 37986cfa8..ec1b5a0a9 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
@@ -14,6 +14,15 @@
 
 package org.apache.tapestry5.internal.services;
 
+import java.lang.ref.SoftReference;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+
+import org.apache.tapestry5.SymbolConstants;
 import org.apache.tapestry5.commons.services.InvalidationEventHub;
 import org.apache.tapestry5.commons.util.CollectionFactory;
 import org.apache.tapestry5.func.F;
@@ -23,6 +32,7 @@ import 
org.apache.tapestry5.internal.structure.ComponentPageElement;
 import org.apache.tapestry5.internal.structure.Page;
 import org.apache.tapestry5.ioc.annotations.ComponentClasses;
 import org.apache.tapestry5.ioc.annotations.PostInjection;
+import org.apache.tapestry5.ioc.annotations.Symbol;
 import org.apache.tapestry5.services.ComponentClassResolver;
 import org.apache.tapestry5.services.ComponentMessages;
 import org.apache.tapestry5.services.ComponentTemplates;
@@ -30,14 +40,6 @@ import 
org.apache.tapestry5.services.pageload.ComponentRequestSelectorAnalyzer;
 import org.apache.tapestry5.services.pageload.ComponentResourceSelector;
 import org.slf4j.Logger;
 
-import java.lang.ref.SoftReference;
-import java.util.Collections;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Map;
-import java.util.Map.Entry;
-import java.util.Set;
-
 public class PageSourceImpl implements PageSource
 {
     private final ComponentRequestSelectorAnalyzer selectorAnalyzer;
@@ -49,7 +51,9 @@ public class PageSourceImpl implements PageSource
     private final ComponentClassResolver componentClassResolver;
     
     private final Logger logger;
-
+    
+    final private boolean productionMode;
+    
     private static final class CachedPageKey
     {
         final String pageName;
@@ -86,12 +90,14 @@ public class PageSourceImpl implements PageSource
     public PageSourceImpl(PageLoader pageLoader, 
ComponentRequestSelectorAnalyzer selectorAnalyzer,
             ComponentDependencyRegistry componentDependencyRegistry,
             ComponentClassResolver componentClassResolver,
+            @Symbol(SymbolConstants.PRODUCTION_MODE) boolean productionMode,
             Logger logger)
     {
         this.pageLoader = pageLoader;
         this.selectorAnalyzer = selectorAnalyzer;
         this.componentDependencyRegistry = componentDependencyRegistry;
         this.componentClassResolver = componentClassResolver;
+        this.productionMode = productionMode;
         this.logger = logger;
     }
 
@@ -126,9 +132,12 @@ public class PageSourceImpl implements PageSource
 
             pageCache.put(key, ref);
             
-            final ComponentPageElement rootElement = page.getRootElement();
-            componentDependencyRegistry.clear(rootElement);
-            componentDependencyRegistry.register(rootElement);
+            if (!productionMode)
+            {
+                final ComponentPageElement rootElement = page.getRootElement();
+                componentDependencyRegistry.clear(rootElement);
+                componentDependencyRegistry.register(rootElement);
+            }
             
         }
     }
diff --git 
a/tapestry-core/src/main/java/org/apache/tapestry5/modules/PageLoadModule.java 
b/tapestry-core/src/main/java/org/apache/tapestry5/modules/PageLoadModule.java
index 597a38afc..a55a05833 100644
--- 
a/tapestry-core/src/main/java/org/apache/tapestry5/modules/PageLoadModule.java
+++ 
b/tapestry-core/src/main/java/org/apache/tapestry5/modules/PageLoadModule.java
@@ -26,6 +26,8 @@ import org.apache.tapestry5.ioc.annotations.Symbol;
 import org.apache.tapestry5.services.Core;
 import org.apache.tapestry5.services.pageload.ComponentRequestSelectorAnalyzer;
 import org.apache.tapestry5.services.pageload.ComponentResourceLocator;
+import org.apache.tapestry5.services.pageload.PageClassloaderContextManager;
+import 
org.apache.tapestry5.services.pageload.PageClassloaderContextManagerImpl;
 import org.apache.tapestry5.services.pageload.PagePreloader;
 import org.apache.tapestry5.services.pageload.PreloaderMode;
 
@@ -41,6 +43,7 @@ public class PageLoadModule
         binder.bind(ComponentResourceLocator.class, 
DefaultComponentResourceLocator.class);
         binder.bind(ComponentTemplateSource.class, 
ComponentTemplateSourceImpl.class);
         binder.bind(PagePreloader.class, PagePreloaderImpl.class);
+        binder.bind(PageClassloaderContextManager.class, 
PageClassloaderContextManagerImpl.class);
     }
 
     @Startup
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
new file mode 100644
index 000000000..4fe237e34
--- /dev/null
+++ 
b/tapestry-core/src/main/java/org/apache/tapestry5/services/pageload/PageClassloaderContext.java
@@ -0,0 +1,211 @@
+// 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.services.pageload;
+
+import java.util.HashSet;
+import java.util.Objects;
+import java.util.Set;
+
+import org.apache.tapestry5.commons.services.PlasticProxyFactory;
+import org.apache.tapestry5.plastic.PlasticManager;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Class that encapsulates a classloader context for Tapestry's live class 
reloading.
+ * Each instance contains basically a classloader, a set of classnames, a 
parent
+ * context (possibly null) and child contexts (possibly empty).
+ */
+public class PageClassloaderContext 
+{
+    
+    private static final Logger LOGGER = 
LoggerFactory.getLogger(PageClassloaderContext.class);
+    
+    private final String name;
+    
+    private final PageClassloaderContext parent;
+    
+    private final Set<String> classNames = new HashSet<>();
+    
+    private final Set<PageClassloaderContext> children;
+    
+    private final PlasticManager plasticManager;
+    
+    private final PlasticProxyFactory proxyFactory;
+    
+    public PageClassloaderContext(String name, 
+            PageClassloaderContext parent, 
+            Set<String> classNames, 
+            PlasticProxyFactory plasticProxyFactory) 
+    {
+        super();
+        this.name = name;
+        this.parent = parent;
+        this.classNames.addAll(classNames);
+        this.plasticManager = plasticProxyFactory.getPlasticManager();
+        this.proxyFactory = plasticProxyFactory;
+        children = new HashSet<>();
+    }
+
+    /**
+     * Returns the name of this context.
+     */
+    public String getName() 
+    {
+        return name;
+    }
+    
+    /**
+     * Returns the parent of this context.
+     */
+    public PageClassloaderContext getParent() 
+    {
+        return parent;
+    }
+
+    /**
+     * Returns the set of classes that belong in this context.
+     */
+    public Set<String> getClassNames() 
+    {
+        return classNames;
+    }
+    
+    /**
+     * Returns the children of this context.
+     */
+    public Set<PageClassloaderContext> getChildren() 
+    {
+        return children;
+    }
+
+    /**
+     * Returns this context's {@linkplain PlasticManager} instance.
+     */
+    public PlasticManager getPlasticManager() 
+    {
+        return plasticManager;
+    }
+    
+    /**
+     * Returns this context's {@linkplain PlasticProxyFactory} instance.
+     */
+    public PlasticProxyFactory getProxyFactory() 
+    {
+        return proxyFactory;
+    }
+    
+    /**
+     * Adds a class to this context.
+     */
+    public void addClass(String className)
+    {
+        classNames.add(className);
+    }
+    
+    public void addChildren(PageClassloaderContext context)
+    {
+        children.add(context);
+    }
+    
+    /**
+     * Searches for the context that contains the given class in itself and 
recursivel in its children.
+     */
+    public PageClassloaderContext findByClassName(String className)
+    {
+        PageClassloaderContext context = null;
+        if (classNames.contains(className))
+        {
+            context = this;
+        }
+        else
+        {
+            for (PageClassloaderContext child : children) {
+                context = child.findByClassName(className);
+                if (context != null)
+                {
+                    break;
+                }
+            }
+        }
+        return context;
+    }
+    
+    /**
+     * Returns the {@linkplain ClassLoader} associated with this context.
+     */
+    public ClassLoader getClassLoader()
+    {
+        return proxyFactory.getClassLoader();
+    }
+
+    /**
+     * Invalidates this context and its children recursively. This shouldn't
+     * be called directly, just through {@link 
PageClassloaderContextManager#invalidate(PageClassloaderContext)}.
+     */
+    public void invalidate() 
+    {
+        LOGGER.info("Invalidating page classloader context '{}' (class loader 
{}, classes : {})", 
+                name, proxyFactory.getClassLoader(), classNames);
+        classNames.clear();
+        parent.getChildren().remove(this);
+        proxyFactory.clearCache();
+        for (PageClassloaderContext child : children) 
+        {
+            child.invalidate();
+        }
+    }
+
+    @Override
+    public int hashCode() 
+    {
+        return Objects.hash(name);
+    }
+
+    @Override
+    public boolean equals(Object obj) 
+    {
+        if (this == obj) 
+        {
+            return true;
+        }
+        if (!(obj instanceof PageClassloaderContext)) 
+        {
+            return false;
+        }
+        PageClassloaderContext other = (PageClassloaderContext) obj;
+        return Objects.equals(name, other.name);
+    }
+
+    @Override
+    public String toString() 
+    {
+        return "PageClassloaderContext [name=" + name + 
+                ", parent=" + (parent != null ? parent.getName() : "null" ) + 
+                ", classLoader=" + 
afterAt(proxyFactory.getClassLoader().toString()) +
+                ", object id" + afterAt(super.toString()) +
+//                ", classNames=" + classNames + 
+                "]";
+    }
+
+    private static String afterAt(String string) 
+    {
+        int index = string.indexOf('@');
+        if (index > 0)
+        {
+            string = string.substring(index + 1);
+        }
+        return 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
new file mode 100644
index 000000000..d91857444
--- /dev/null
+++ 
b/tapestry-core/src/main/java/org/apache/tapestry5/services/pageload/PageClassloaderContextManager.java
@@ -0,0 +1,57 @@
+// 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.services.pageload;
+
+import java.util.Set;
+import java.util.function.Function;
+
+import org.apache.tapestry5.commons.services.PlasticProxyFactory;
+import org.apache.tapestry5.internal.services.ComponentInstantiatorSource;
+
+/**
+ * Service that creates {@linkplain PageClassloaderContext} instances (except 
the root one)
+ * when a class in a controlled page is first used in the Tapestry page pool. 
Existing 
+ * contexts may be reused for a given class, specially when in production mode.
+ * 
+ * @see ComponentInstantiatorSource
+ * @since 5.8.3
+ */
+public interface PageClassloaderContextManager 
+{
+
+    /**
+     * Processes a class, given its class name and the root context.
+     * @param className the class fully qualified name.
+     * @param root the root {@link PageClassloaderContext}.
+     * @param plasticProxyFactoryProvider a function that receives a 
+     * {@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);
+    
+    /**
+     * Invalidates one page classloader context and returns a set containing 
the names
+     * of all classes that should be invalidated.
+     */
+    Set<String> invalidate(PageClassloaderContext context);
+    
+    /**
+     * Clears any state held by this manager.
+     */
+    void clear();
+    
+}
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
new file mode 100644
index 000000000..e382f01b2
--- /dev/null
+++ 
b/tapestry-core/src/main/java/org/apache/tapestry5/services/pageload/PageClassloaderContextManagerImpl.java
@@ -0,0 +1,153 @@
+// 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.services.pageload;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+import org.apache.tapestry5.SymbolConstants;
+import org.apache.tapestry5.commons.services.PlasticProxyFactory;
+import org.apache.tapestry5.internal.services.ComponentDependencyRegistry;
+import org.apache.tapestry5.ioc.annotations.Symbol;
+
+/**
+ * Default {@linkplain PageClassloaderContextManager} implementation.
+ *
+ * @since 5.8.3
+ */
+public class PageClassloaderContextManagerImpl implements 
PageClassloaderContextManager
+{
+    
+    private static final String UNKOWN_CONTEXT_NAME = "unknown";
+    
+    private final ComponentDependencyRegistry componentDependencyRegistry;
+    
+    private final boolean productionMode;
+    
+    public PageClassloaderContextManagerImpl(
+            final ComponentDependencyRegistry componentDependencyRegistry, 
+            @Symbol(SymbolConstants.PRODUCTION_MODE) final boolean 
productionMode) 
+    {
+        super();
+        this.componentDependencyRegistry = componentDependencyRegistry;
+        this.productionMode = productionMode;
+    }
+
+    @Override
+    public PageClassloaderContext get(
+            final String className, 
+            final PageClassloaderContext root,
+            final Function<ClassLoader, PlasticProxyFactory> 
plasticProxyFactoryProvider)
+    {
+        PageClassloaderContext context;
+        
+        if (productionMode)
+        {
+            root.addClass(className);
+            context = root;
+        }
+        else
+        {
+            
+            context = root.findByClassName(className);
+            
+            if (context == null)
+            {
+                context = processUsingDependencies(className, root, () -> 
getUnknownContext(root, plasticProxyFactoryProvider));
+            }
+            
+        }
+        
+        return context;
+        
+    }
+
+    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))
+            {
+                unknownContext = child;
+                break;
+            }
+        }
+        
+        if (unknownContext == null)
+        {
+            unknownContext = new PageClassloaderContext(UNKOWN_CONTEXT_NAME, 
root, 
+                    Collections.emptySet(), 
+                    plasticProxyFactoryProvider.apply(root.getClassLoader()));
+            root.addChildren(unknownContext);
+        }
+        return unknownContext;
+    }
+
+    private PageClassloaderContext processUsingDependencies(String className, 
PageClassloaderContext root, Supplier<PageClassloaderContext> 
unknownContextProvider) 
+    {
+        PageClassloaderContext context = root.findByClassName(className);
+        if (context == null)
+        {
+            
+            // Class isn't in a controlled package, so it doesn't get 
transformed
+            // and should go for the root context, which is never thrown out.
+            if 
(!root.getPlasticManager().shouldInterceptClassLoading(className))
+            {
+                context = root;
+            }
+            // If we don't have dependency information about this class, it 
goes
+            // into the "unknown" context.
+            else if (!componentDependencyRegistry.contains(className))
+            {
+                context = unknownContextProvider.get();
+            }
+            // TODO: implement this correctly by creating and merging new 
contexts
+            // as needed.
+            else 
+            {
+                context = unknownContextProvider.get();
+            }
+            
+        }
+        context.addClass(className);
+        return context;
+    }
+
+    @Override
+    public Set<String> invalidate(PageClassloaderContext context) 
+    {
+        Set<String> classNames = new HashSet<>();
+        addClassNames(context, classNames);
+        context.invalidate();
+        return classNames;
+    }
+
+    private void addClassNames(PageClassloaderContext context, Set<String> 
classNames) {
+        classNames.addAll(context.getClassNames());
+        for (PageClassloaderContext child : context.getChildren()) {
+            addClassNames(child, classNames);
+        }
+    }
+
+    @Override
+    public void clear() 
+    {
+    }
+
+}
diff --git 
a/tapestry-core/src/main/java/org/apache/tapestry5/services/pageload/package-info.java
 
b/tapestry-core/src/main/java/org/apache/tapestry5/services/pageload/package-info.java
index f80452ba0..5c462eb25 100644
--- 
a/tapestry-core/src/main/java/org/apache/tapestry5/services/pageload/package-info.java
+++ 
b/tapestry-core/src/main/java/org/apache/tapestry5/services/pageload/package-info.java
@@ -1,4 +1,4 @@
-// Copyright 2011 The Apache Software Foundation
+// Copyright 2011-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.
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 4a1c9e92a..b470b5cce 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,8 +14,11 @@
 
 package org.apache.tapestry5.internal.services;
 
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
 import static org.testng.Assert.assertEquals;
 
+import java.io.File;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashSet;
@@ -35,6 +38,9 @@ public class ComponentDependencyRegistryImplTest
     @BeforeClass
     public void setup()
     {
+        assertFalse(
+                String.format("During testing, %s shouldn't exist", 
ComponentDependencyRegistry.FILENAME), 
+                new File(ComponentDependencyRegistry.FILENAME).exists());
         componentDependencyRegistry = new ComponentDependencyRegistryImpl();
     }
     
@@ -42,11 +48,11 @@ public class ComponentDependencyRegistryImplTest
     public void listen()
     {
         
-        componentDependencyRegistry.add("foo", "bar");
-        componentDependencyRegistry.add("d", "a");
-        componentDependencyRegistry.add("dd", "aa");
-        componentDependencyRegistry.add("dd", "a");
-        componentDependencyRegistry.add("dd", "a");
+        add("foo", "bar");
+        add("d", "a");
+        add("dd", "aa");
+        add("dd", "a");
+        add("dd", "a");
         
         final List<String> resources = Arrays.asList("a", "aa", "none");
         final List<String> result = 
componentDependencyRegistry.listen(resources);
@@ -77,11 +83,14 @@ public class ComponentDependencyRegistryImplTest
         assertEquals(componentDependencyRegistry.getDependents(foo), 
Collections.emptySet(), 
                 "getDependents() should never return null");
 
-        componentDependencyRegistry.add(foo, bar);
-        componentDependencyRegistry.add(something, fulano);
-        componentDependencyRegistry.add(other, beltrano);
-        componentDependencyRegistry.add(other, fulano);
-        componentDependencyRegistry.add(other, fulano);
+        add(foo, bar);
+        add(something, fulano);
+        add(other, beltrano);
+        add(other, fulano);
+        add(other, fulano);
+        add(bar, null);
+        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)));
@@ -93,6 +102,29 @@ public class ComponentDependencyRegistryImplTest
         assertEquals(componentDependencyRegistry.getDependents(fulano), new 
HashSet<>(Arrays.asList(other, something)));
         assertEquals(componentDependencyRegistry.getDependents(foo), new 
HashSet<>(Arrays.asList()));
         
+        assertEquals(componentDependencyRegistry.getRootClasses(), new 
HashSet<>(Arrays.asList(bar, fulano, beltrano)));
+        
+        assertTrue(componentDependencyRegistry.contains(foo));
+        assertTrue(componentDependencyRegistry.contains(bar));
+        assertTrue(componentDependencyRegistry.contains(other));
+        assertTrue(componentDependencyRegistry.contains(something));
+        assertTrue(componentDependencyRegistry.contains(fulano));
+        assertTrue(componentDependencyRegistry.contains(beltrano));
+        assertFalse(componentDependencyRegistry.contains("blah"));
+
+        assertTrue(componentDependencyRegistry.getClassNames().contains(foo));
+        assertTrue(componentDependencyRegistry.getClassNames().contains(bar));
+        
assertTrue(componentDependencyRegistry.getClassNames().contains(other));
+        
assertTrue(componentDependencyRegistry.getClassNames().contains(something));
+        
assertTrue(componentDependencyRegistry.getClassNames().contains(fulano));
+        
assertTrue(componentDependencyRegistry.getClassNames().contains(beltrano));
+        
assertFalse(componentDependencyRegistry.getClassNames().contains("blah"));
+        
+    }
+    
+    private void add(String component, String dependency)
+    {
+        componentDependencyRegistry.add(component, dependency, true);
     }
 
 }

Reply via email to