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 ca6835cd9 TAP5-2742: page cache invalidation on template change working
ca6835cd9 is described below

commit ca6835cd9670b2aa5b53c5957fcae469e881ee14
Author: Thiago H. de Paula Figueiredo <thi...@arsmachina.com.br>
AuthorDate: Sat Nov 26 10:51:37 2022 -0300

    TAP5-2742: page cache invalidation on template change working
    
    plus some additional logging for when invalidations are done and page
    instances created
---
 583_RELEASE_NOTES.md                               | 10 ++-
 .../commons/services/InvalidationEventHub.java     |  8 ++
 .../apache/tapestry5/commons/util/MultiKey.java    | 10 ++-
 .../internal/event/InvalidationEventHubImpl.java   | 18 ++++-
 .../services/ComponentClassResolverImpl.java       |  9 ++-
 .../services/ComponentDependencyRegistryImpl.java  | 23 ++++--
 .../services/ComponentInstantiatorSourceImpl.java  |  8 +-
 .../services/ComponentMessagesSourceImpl.java      | 13 ++--
 .../services/ComponentTemplateSourceImpl.java      | 55 ++++++++++----
 .../InternalComponentInvalidationEventHubImpl.java |  5 +-
 .../internal/services/MessagesSourceImpl.java      |  6 +-
 .../internal/services/PageSourceImpl.java          | 55 +++++++++++++-
 .../tapestry5/internal/services/ReloadHelper.java  |  2 +-
 .../services/ResourceDigestManagerImpl.java        |  6 ++
 .../services/assets/ResourceChangeTrackerImpl.java |  5 +-
 .../event/InvalidationEventHubImplTest.java        |  7 +-
 .../ComponentDependencyRegistryImplTest.java       |  5 ++
 .../services/ComponentMessagesSourceImplTest.java  |  8 +-
 .../services/ComponentTemplateSourceImplTest.java  | 14 ++--
 .../internal/AbstractReloadableObjectCreator.java  |  2 +-
 .../ioc/internal/util/URLChangeTracker.java        | 85 +++++++++++++++++++---
 21 files changed, 283 insertions(+), 71 deletions(-)

diff --git a/583_RELEASE_NOTES.md b/583_RELEASE_NOTES.md
index 2e6bea9dd..34e9fdf3d 100644
--- a/583_RELEASE_NOTES.md
+++ b/583_RELEASE_NOTES.md
@@ -1,7 +1,13 @@
 Scratch pad for changes destined for the 5.8.3 release notes page.
 
-# Non-backward-compatible changes
+# Added methods
+
+* add(URL url, String memo) to URLChangeTracker
+* getChangeResourcesMemos() to URLChangeTracker
+* getValues() to MultiKey
+
+# Non-backward-compatible changes (but that probably won't cause problems)
 
 * New addInvalidationCallback(Function<List<String>, List<String>> callback) 
method in InvalidationEventHub
 * New getEmbeddedElementIds() method in ComponentPageElement (internal service)
-* New getLogicalName() method in ComponentClassResolver.
\ No newline at end of file
+* New getLogicalName() method in ComponentClassResolver.
diff --git 
a/commons/src/main/java/org/apache/tapestry5/commons/services/InvalidationEventHub.java
 
b/commons/src/main/java/org/apache/tapestry5/commons/services/InvalidationEventHub.java
index 85e3fbb69..4e0f37ec5 100644
--- 
a/commons/src/main/java/org/apache/tapestry5/commons/services/InvalidationEventHub.java
+++ 
b/commons/src/main/java/org/apache/tapestry5/commons/services/InvalidationEventHub.java
@@ -71,4 +71,12 @@ public interface InvalidationEventHub
      */
     @IncompatibleChange(release = "5.8.3", details = "Added method")
     void addInvalidationCallback(Function<List<String>, List<String>> 
function);
+    
+   /** 
+    * Notify resource-specific invalidations to listeners.
+    * @since 5.8.3
+    */
+   @IncompatibleChange(release = "5.8.3", details = "Added method")
+   void fireInvalidationEvent(List<String> resources);
+
 }
diff --git 
a/commons/src/main/java/org/apache/tapestry5/commons/util/MultiKey.java 
b/commons/src/main/java/org/apache/tapestry5/commons/util/MultiKey.java
index bd595fc98..b9d29b8de 100644
--- a/commons/src/main/java/org/apache/tapestry5/commons/util/MultiKey.java
+++ b/commons/src/main/java/org/apache/tapestry5/commons/util/MultiKey.java
@@ -82,5 +82,13 @@ public final class MultiKey
 
         return builder.toString();
     }
-
+    
+    /**
+     * Returns a copy of the values array.
+     * @since 5.8.3
+     */
+    public Object[] getValues() {
+        return values.clone();
+    }
+    
 }
diff --git 
a/tapestry-core/src/main/java/org/apache/tapestry5/internal/event/InvalidationEventHubImpl.java
 
b/tapestry-core/src/main/java/org/apache/tapestry5/internal/event/InvalidationEventHubImpl.java
index 46186ee00..aab89d38c 100644
--- 
a/tapestry-core/src/main/java/org/apache/tapestry5/internal/event/InvalidationEventHubImpl.java
+++ 
b/tapestry-core/src/main/java/org/apache/tapestry5/internal/event/InvalidationEventHubImpl.java
@@ -27,6 +27,7 @@ import 
org.apache.tapestry5.commons.internal.util.TapestryException;
 import org.apache.tapestry5.commons.services.InvalidationEventHub;
 import org.apache.tapestry5.commons.services.InvalidationListener;
 import org.apache.tapestry5.commons.util.CollectionFactory;
+import org.slf4j.Logger;
 
 /**
  * Base implementation class for classes (especially services) that need to 
manage a list of
@@ -36,7 +37,9 @@ public class InvalidationEventHubImpl implements 
InvalidationEventHub
 {
     private final List<Function<List<String>, List<String>>> callbacks;
     
-    protected InvalidationEventHubImpl(boolean productionMode)
+    private final Logger logger;
+    
+    protected InvalidationEventHubImpl(boolean productionMode, Logger logger)
     {
         if (productionMode)
         {
@@ -45,6 +48,7 @@ public class InvalidationEventHubImpl implements 
InvalidationEventHub
         {
             callbacks = CollectionFactory.newThreadSafeList();
         }
+        this.logger = logger;
     }
 
     /**
@@ -58,7 +62,7 @@ public class InvalidationEventHubImpl implements 
InvalidationEventHub
     /**
      * Notifies all listeners/callbacks.
      */
-    protected final void fireInvalidationEvent(List<String> resources)
+    public final void fireInvalidationEvent(List<String> resources)
     {
         if (callbacks == null)
         {
@@ -67,10 +71,19 @@ public class InvalidationEventHubImpl implements 
InvalidationEventHub
         
         final Set<String> alreadyProcessed = new HashSet<>();
         
+        int level = 1;
         do 
         {
             final Set<String> extraResources = new HashSet<>();
             Set<String> actuallyNewResources;
+            if (!resources.isEmpty())
+            {
+                logger.info("Invalidating {} resource(s) at level {}: {}", 
resources.size(), level, String.join(", ", resources));
+            }
+            else
+            {
+                logger.info("Invalidating all resources");
+            }
             for (Function<List<String>, List<String>> callback : callbacks)
             {
                 final List<String> newResources = callback.apply(resources);
@@ -83,6 +96,7 @@ public class InvalidationEventHubImpl implements 
InvalidationEventHub
                 extraResources.addAll(actuallyNewResources);
                 alreadyProcessed.addAll(newResources);
             }
+            level++;
             resources = new ArrayList<>(extraResources);
         }
         while (!resources.isEmpty());
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 f8c6d6df1..349755fc7 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
@@ -820,13 +820,14 @@ public class ComponentClassResolverImpl implements 
ComponentClassResolver, Inval
     @Override
     public String getLogicalName(String className) 
     {
-        String result = getData().pageClassNameToLogicalName.get(className);
+        final Data thisData = getData();
+        String result = thisData.pageClassNameToLogicalName.get(className);
         if (result == null)
         {
-            result = getKeyByValue(getData().componentToClassName, className);
+            result = getKeyByValue(thisData.componentToClassName, className);
         }
-        else {
-            result = getKeyByValue(getData().mixinToClassName, className);
+        if (result == null ){
+            result = getKeyByValue(thisData.mixinToClassName, className);
         }
 
         return result;
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 a7d7dcae8..1810b9804 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
@@ -176,18 +176,27 @@ public class ComponentDependencyRegistryImpl implements 
ComponentDependencyRegis
     // Protected just for testing
     List<String> listen(List<String> resources)
     {
-        List<String> furtherDependents = new ArrayList<>();
-        for (String resource : resources) 
+        List<String> furtherDependents;
+        if (resources.isEmpty())
         {
-            final Set<String> dependents = getDependents(resource);
-            for (String furtherDependent : dependents) 
+            clear();
+            furtherDependents = Collections.emptyList();
+        }
+        else
+        {
+            furtherDependents = new ArrayList<>();
+            for (String resource : resources) 
             {
-                if (!resources.contains(furtherDependent) && 
!furtherDependents.contains(furtherDependent))
+                final Set<String> dependents = getDependents(resource);
+                for (String furtherDependent : dependents) 
                 {
-                    furtherDependents.add(furtherDependent);
+                    if (!resources.contains(furtherDependent) && 
!furtherDependents.contains(furtherDependent))
+                    {
+                        furtherDependents.add(furtherDependent);
+                    }
                 }
+                clear(resource);
             }
-            clear(resource);
         }
         return furtherDependents;
     }
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 09c568f41..c3e91f270 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,7 @@
 
 package org.apache.tapestry5.internal.services;
 
+import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -172,9 +173,10 @@ public final class ComponentInstantiatorSourceImpl 
implements ComponentInstantia
 
     public synchronized void checkForUpdates()
     {
-        if (changeTracker.containsChanges())
+        final Set<String> changedResources = 
changeTracker.getChangedResourcesMemos();
+        if (!changedResources.isEmpty())
         {
-            invalidationHub.classInControlledPackageHasChanged();
+            invalidationHub.fireInvalidationEvent(new 
ArrayList<>(changedResources));
         }
     }
 
@@ -306,7 +308,7 @@ public final class ComponentInstantiatorSourceImpl 
implements ComponentInstantia
                         Resource baseResource = new ClasspathResource(parent, 
PlasticInternalUtils
                                 .toClassPath(className));
 
-                        changeTracker.add(baseResource.toURL());
+                        changeTracker.add(baseResource.toURL(), className);
 
                         if (isRoot)
                         {
diff --git 
a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ComponentMessagesSourceImpl.java
 
b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ComponentMessagesSourceImpl.java
index 1a97c9551..223e0dca6 100644
--- 
a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ComponentMessagesSourceImpl.java
+++ 
b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ComponentMessagesSourceImpl.java
@@ -36,6 +36,7 @@ import 
org.apache.tapestry5.services.messages.PropertiesFileParser;
 import org.apache.tapestry5.services.pageload.ComponentRequestSelectorAnalyzer;
 import org.apache.tapestry5.services.pageload.ComponentResourceLocator;
 import org.apache.tapestry5.services.pageload.ComponentResourceSelector;
+import org.slf4j.Logger;
 
 public class ComponentMessagesSourceImpl implements ComponentMessagesSource, 
UpdateListener
 {
@@ -81,26 +82,26 @@ public class ComponentMessagesSourceImpl implements 
ComponentMessagesSource, Upd
                                        boolean productionMode, List<Resource> 
appCatalogResources, PropertiesFileParser parser,
                                        ComponentResourceLocator 
resourceLocator, ClasspathURLConverter classpathURLConverter,
                                        ComponentRequestSelectorAnalyzer 
componentRequestSelectorAnalyzer,
-                                       ThreadLocale threadLocale)
+                                       ThreadLocale threadLocale, Logger 
logger)
     {
-        this(productionMode, appCatalogResources, resourceLocator, parser, new 
URLChangeTracker(classpathURLConverter), componentRequestSelectorAnalyzer, 
threadLocale);
+        this(productionMode, appCatalogResources, resourceLocator, parser, new 
URLChangeTracker(classpathURLConverter), componentRequestSelectorAnalyzer, 
threadLocale, logger);
     }
 
     ComponentMessagesSourceImpl(boolean productionMode, Resource 
appCatalogResource,
                                 ComponentResourceLocator resourceLocator, 
PropertiesFileParser parser, 
                                 URLChangeTracker tracker, 
ComponentRequestSelectorAnalyzer componentRequestSelectorAnalyzer,
-                                ThreadLocale threadLocale)
+                                ThreadLocale threadLocale, Logger logger)
     {
-        this(productionMode, Arrays.asList(appCatalogResource), 
resourceLocator, parser, tracker, componentRequestSelectorAnalyzer, 
threadLocale);
+        this(productionMode, Arrays.asList(appCatalogResource), 
resourceLocator, parser, tracker, componentRequestSelectorAnalyzer, 
threadLocale, logger);
     }
 
     ComponentMessagesSourceImpl(boolean productionMode, List<Resource> 
appCatalogResources,
                                 ComponentResourceLocator resourceLocator, 
PropertiesFileParser parser, 
                                 URLChangeTracker tracker, 
ComponentRequestSelectorAnalyzer componentRequestSelectorAnalyzer,
-                                ThreadLocale threadLocale)
+                                ThreadLocale threadLocale, Logger logger)
     {
         messagesSource = new MessagesSourceImpl(productionMode, productionMode 
? null : tracker, resourceLocator,
-                parser);
+                parser, logger);
 
         appCatalogBundle = createAppCatalogBundle(appCatalogResources);
         this.componentRequestSelectorAnalyzer = 
componentRequestSelectorAnalyzer;
diff --git 
a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ComponentTemplateSourceImpl.java
 
b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ComponentTemplateSourceImpl.java
index 71bac204b..f8e2b83ae 100644
--- 
a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ComponentTemplateSourceImpl.java
+++ 
b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ComponentTemplateSourceImpl.java
@@ -12,6 +12,15 @@
 
 package org.apache.tapestry5.internal.services;
 
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+
 import org.apache.tapestry5.TapestryConstants;
 import org.apache.tapestry5.commons.Location;
 import org.apache.tapestry5.commons.Resource;
@@ -34,12 +43,7 @@ import org.apache.tapestry5.model.ComponentModel;
 import org.apache.tapestry5.services.pageload.ComponentRequestSelectorAnalyzer;
 import org.apache.tapestry5.services.pageload.ComponentResourceLocator;
 import org.apache.tapestry5.services.pageload.ComponentResourceSelector;
-import org.apache.tapestry5.services.templates.ComponentTemplateLocator;
-
-import java.util.Collections;
-import java.util.List;
-import java.util.Locale;
-import java.util.Map;
+import org.slf4j.Logger;
 
 /**
  * Service implementation that manages a cache of parsed component templates.
@@ -56,6 +60,8 @@ public final class ComponentTemplateSourceImpl extends 
InvalidationEventHubImpl
     private final ComponentRequestSelectorAnalyzer 
componentRequestSelectorAnalyzer;
     
     private final ThreadLocale threadLocale;
+    
+    private final Logger logger;
 
     /**
      * Caches from a key (combining component name and locale) to a resource. 
Often, many different keys will point to
@@ -112,22 +118,23 @@ public final class ComponentTemplateSourceImpl extends 
InvalidationEventHubImpl
                                        boolean productionMode, TemplateParser 
parser, ComponentResourceLocator locator,
                                        ClasspathURLConverter 
classpathURLConverter,
                                        ComponentRequestSelectorAnalyzer 
componentRequestSelectorAnalyzer,
-                                       ThreadLocale threadLocale)
+                                       ThreadLocale threadLocale, Logger 
logger)
     {
-        this(productionMode, parser, locator, new 
URLChangeTracker(classpathURLConverter), componentRequestSelectorAnalyzer, 
threadLocale);
+        this(productionMode, parser, locator, new 
URLChangeTracker(classpathURLConverter), componentRequestSelectorAnalyzer, 
threadLocale, logger);
     }
 
     ComponentTemplateSourceImpl(boolean productionMode, TemplateParser parser, 
ComponentResourceLocator locator,
                                 URLChangeTracker tracker, 
ComponentRequestSelectorAnalyzer componentRequestSelectorAnalyzer,
-                                ThreadLocale threadLocale)
+                                ThreadLocale threadLocale, Logger logger)
     {
-        super(productionMode);
+        super(productionMode, logger);
 
         this.parser = parser;
         this.locator = locator;
         this.tracker = tracker;
         this.componentRequestSelectorAnalyzer = 
componentRequestSelectorAnalyzer;
         this.threadLocale = threadLocale;
+        this.logger = logger;
     }
 
     @PostInjection
@@ -170,7 +177,7 @@ public final class ComponentTemplateSourceImpl extends 
InvalidationEventHubImpl
 
         if (result == null)
         {
-            result = parseTemplate(resource);
+            result = parseTemplate(resource, 
componentModel.getComponentClassName());
             templates.put(resource, result);
         }
 
@@ -196,7 +203,7 @@ public final class ComponentTemplateSourceImpl extends 
InvalidationEventHubImpl
         }
     }
 
-    private ComponentTemplate parseTemplate(Resource r)
+    private ComponentTemplate parseTemplate(Resource r, String className)
     {
         // In a race condition, we may parse the same template more than once. 
This will likely add
         // the resource to the tracker multiple times. Not likely this will 
cause a big issue.
@@ -204,7 +211,7 @@ public final class ComponentTemplateSourceImpl extends 
InvalidationEventHubImpl
         if (!r.exists())
             return missingTemplate;
 
-        tracker.add(r.toURL());
+        tracker.add(r.toURL(), className);
 
         return parser.parseTemplate(r);
     }
@@ -235,12 +242,30 @@ public final class ComponentTemplateSourceImpl extends 
InvalidationEventHubImpl
      * Checks to see if any parsed resource has changed. If so, then all 
internal caches are cleared, and an
      * invalidation event is fired. This is brute force ... a more targeted 
dependency management strategy may come
      * later.
+     * Actually, TAP5-2742 did exactly that! :D
      */
     public void checkForUpdates()
     {
-        if (tracker.containsChanges())
+        final Set<String> changedResourcesMemos = 
tracker.getChangedResourcesMemos();
+        if (!changedResourcesMemos.isEmpty())
         {
-            invalidate();
+            logger.info("Changed template(s) found: {}", String.join(", ", 
changedResourcesMemos));
+            
+            final Iterator<Entry<MultiKey, Resource>> 
templateResourcesIterator = templateResources.entrySet().iterator();
+            for (String className : changedResourcesMemos) 
+            {
+                while (templateResourcesIterator.hasNext())
+                {
+                    final MultiKey key = 
templateResourcesIterator.next().getKey();
+                    if (className.equals((String) key.getValues()[0]))
+                    {
+                        templates.remove(templateResources.get(key));
+                        templateResourcesIterator.remove();
+                    }
+                }
+            }
+            
+            fireInvalidationEvent(new ArrayList<>(changedResourcesMemos));
         }
     }
 
diff --git 
a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/InternalComponentInvalidationEventHubImpl.java
 
b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/InternalComponentInvalidationEventHubImpl.java
index e536a7766..fb1864edc 100644
--- 
a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/InternalComponentInvalidationEventHubImpl.java
+++ 
b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/InternalComponentInvalidationEventHubImpl.java
@@ -18,14 +18,15 @@ import 
org.apache.tapestry5.http.TapestryHttpSymbolConstants;
 import org.apache.tapestry5.internal.event.InvalidationEventHubImpl;
 import org.apache.tapestry5.ioc.annotations.PostInjection;
 import org.apache.tapestry5.ioc.annotations.Symbol;
+import org.slf4j.Logger;
 
 public class InternalComponentInvalidationEventHubImpl extends 
InvalidationEventHubImpl implements
         InternalComponentInvalidationEventHub
 {
     public 
InternalComponentInvalidationEventHubImpl(@Symbol(TapestryHttpSymbolConstants.PRODUCTION_MODE)
-                                                     boolean productionMode)
+                                                     boolean productionMode, 
Logger logger)
     {
-        super(productionMode);
+        super(productionMode, logger);
     }
 
     @PostInjection
diff --git 
a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/MessagesSourceImpl.java
 
b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/MessagesSourceImpl.java
index adeddc167..43b7e70d1 100644
--- 
a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/MessagesSourceImpl.java
+++ 
b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/MessagesSourceImpl.java
@@ -23,6 +23,7 @@ import 
org.apache.tapestry5.ioc.internal.util.URLChangeTracker;
 import org.apache.tapestry5.services.messages.PropertiesFileParser;
 import org.apache.tapestry5.services.pageload.ComponentResourceLocator;
 import org.apache.tapestry5.services.pageload.ComponentResourceSelector;
+import org.slf4j.Logger;
 
 import java.util.Collections;
 import java.util.List;
@@ -68,9 +69,10 @@ public class MessagesSourceImpl extends 
InvalidationEventHubImpl implements Mess
     private final Map<String, String> emptyMap = Collections.emptyMap();
 
     public MessagesSourceImpl(boolean productionMode, URLChangeTracker tracker,
-                              ComponentResourceLocator resourceLocator, 
PropertiesFileParser propertiesFileParser)
+                              ComponentResourceLocator resourceLocator, 
PropertiesFileParser propertiesFileParser,
+                              Logger logger)
     {
-        super(productionMode);
+        super(productionMode, logger);
 
         this.tracker = tracker;
         this.propertiesFileParser = propertiesFileParser;
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 e9db70ad9..33736dade 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
@@ -22,13 +22,19 @@ import 
org.apache.tapestry5.internal.services.assets.ResourceChangeTracker;
 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.services.ComponentClassResolver;
 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.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
@@ -38,6 +44,10 @@ public class PageSourceImpl implements PageSource
     private final PageLoader pageLoader;
 
     private final ComponentDependencyRegistry componentDependencyRegistry;
+    
+    private final ComponentClassResolver componentClassResolver;
+    
+    private final Logger logger;
 
     private static final class CachedPageKey
     {
@@ -73,11 +83,15 @@ public class PageSourceImpl implements PageSource
     private final Map<CachedPageKey, SoftReference<Page>> pageCache = 
CollectionFactory.newConcurrentMap();
 
     public PageSourceImpl(PageLoader pageLoader, 
ComponentRequestSelectorAnalyzer selectorAnalyzer,
-            ComponentDependencyRegistry componentDependencyRegistry)
+            ComponentDependencyRegistry componentDependencyRegistry,
+            ComponentClassResolver componentClassResolver,
+            Logger logger)
     {
         this.pageLoader = pageLoader;
         this.selectorAnalyzer = selectorAnalyzer;
         this.componentDependencyRegistry = componentDependencyRegistry;
+        this.componentClassResolver = componentClassResolver;
+        this.logger = logger;
     }
 
     public Page getPage(String canonicalPageName)
@@ -121,9 +135,9 @@ public class PageSourceImpl implements PageSource
                                   @ComponentMessages InvalidationEventHub 
messagesHub,
                                   ResourceChangeTracker resourceChangeTracker)
     {
-        classesHub.clearOnInvalidation(pageCache);
-        templatesHub.clearOnInvalidation(pageCache);
-        messagesHub.clearOnInvalidation(pageCache);
+        classesHub.addInvalidationCallback(this::listen);
+        templatesHub.addInvalidationCallback(this::listen);
+        messagesHub.addInvalidationCallback(this::listen);
 
         // Because Assets can be injected into pages, and Assets are 
invalidated when
         // an Asset's value is changed (partly due to the change, in 5.4, to 
include the asset's
@@ -131,9 +145,42 @@ public class PageSourceImpl implements PageSource
         // any Resource, it is necessary to discard all page instances.
         resourceChangeTracker.clearOnInvalidation(pageCache);
     }
+    
+    private List<String> listen(List<String> resources)
+    {
+    
+        if (resources.isEmpty())
+        {
+            clearCache();
+        }
+        else
+        {
+            String pageName;
+            for (String className : resources)
+            {
+                pageName = componentClassResolver.getLogicalName(className);
+                if (pageName != null && !pageName.isEmpty())
+                {
+                    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)) 
+                        {
+                            logger.info("Clearing cached page '{}'", pageName);
+                            iterator.remove();
+                        }
+                    }
+                }
+            }
+        }
+            
+        return Collections.emptyList();
+    }
 
     public void clearCache()
     {
+        logger.info("Clearing page cache");
         pageCache.clear();
     }
 
diff --git 
a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ReloadHelper.java
 
b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ReloadHelper.java
index 91a8fec2c..d0266b088 100644
--- 
a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ReloadHelper.java
+++ 
b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ReloadHelper.java
@@ -15,7 +15,7 @@
 package org.apache.tapestry5.internal.services;
 
 /**
- * Forces a reload of all caches and invalidates the component class cache. 
This is only allowed
+ * Forces a reload of all caches and invalidates the component class cache. 
This is only allowed when production mode is off.
  *
  * @since 5.4
  */
diff --git 
a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ResourceDigestManagerImpl.java
 
b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ResourceDigestManagerImpl.java
index 08ca9c066..eb0e78939 100644
--- 
a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ResourceDigestManagerImpl.java
+++ 
b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ResourceDigestManagerImpl.java
@@ -49,4 +49,10 @@ public class ResourceDigestManagerImpl implements 
ResourceDigestManager
     public void addInvalidationCallback(Function<List<String>, List<String>> 
function) 
     {
     }
+
+    @Override
+    public void fireInvalidationEvent(List<String> resources) 
+    {
+    }
+    
 }
diff --git 
a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/assets/ResourceChangeTrackerImpl.java
 
b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/assets/ResourceChangeTrackerImpl.java
index 917af3a7e..de9fbc166 100644
--- 
a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/assets/ResourceChangeTrackerImpl.java
+++ 
b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/assets/ResourceChangeTrackerImpl.java
@@ -23,6 +23,7 @@ import 
org.apache.tapestry5.ioc.internal.util.URLChangeTracker;
 import org.apache.tapestry5.ioc.services.ClasspathURLConverter;
 import org.apache.tapestry5.ioc.services.UpdateListener;
 import org.apache.tapestry5.ioc.services.UpdateListenerHub;
+import org.slf4j.Logger;
 
 public class ResourceChangeTrackerImpl extends InvalidationEventHubImpl 
implements ResourceChangeTracker,
         UpdateListener
@@ -38,9 +39,9 @@ public class ResourceChangeTrackerImpl extends 
InvalidationEventHubImpl implemen
 
     public ResourceChangeTrackerImpl(ClasspathURLConverter 
classpathURLConverter,
                                      
@Symbol(TapestryHttpSymbolConstants.PRODUCTION_MODE)
-                                     boolean productionMode)
+                                     boolean productionMode, Logger logger)
     {
-        super(productionMode);
+        super(productionMode, logger);
 
         // Use granularity of seconds (not milliseconds) since that works 
properly
         // with response headers for identifying last modified. Don't track
diff --git 
a/tapestry-core/src/test/java/org/apache/tapestry5/internal/event/InvalidationEventHubImplTest.java
 
b/tapestry-core/src/test/java/org/apache/tapestry5/internal/event/InvalidationEventHubImplTest.java
index 7112126d9..9edb441c3 100644
--- 
a/tapestry-core/src/test/java/org/apache/tapestry5/internal/event/InvalidationEventHubImplTest.java
+++ 
b/tapestry-core/src/test/java/org/apache/tapestry5/internal/event/InvalidationEventHubImplTest.java
@@ -19,6 +19,7 @@ import java.util.function.Function;
 
 import org.apache.tapestry5.commons.internal.util.TapestryException;
 import org.apache.tapestry5.internal.services.ComponentTemplateSourceImplTest;
+import org.slf4j.LoggerFactory;
 import org.testng.Assert;
 import org.testng.annotations.Test;
 
@@ -36,7 +37,7 @@ public class InvalidationEventHubImplTest
     @Test
     public void add_invalidation_callback_with_parameter() 
     {
-        InvalidationEventHubImpl invalidationEventHub = new 
InvalidationEventHubImpl(false);
+        InvalidationEventHubImpl invalidationEventHub = new 
InvalidationEventHubImpl(false, 
LoggerFactory.getLogger(InvalidationEventHubImpl.class));
         final String firstInitialElement = "a";
         final String secondInitialElement = "b";
         final List<String> initialResources = 
Arrays.asList(firstInitialElement, secondInitialElement);
@@ -65,9 +66,9 @@ public class InvalidationEventHubImplTest
     @Test(expectedExceptions = TapestryException.class)
     public void null_check_for_callback_method() 
     {
-        InvalidationEventHubImpl invalidationEventHub = new 
InvalidationEventHubImpl(false);
+        InvalidationEventHubImpl invalidationEventHub = new 
InvalidationEventHubImpl(false, 
LoggerFactory.getLogger(InvalidationEventHubImpl.class));
         invalidationEventHub.addInvalidationCallback((s) -> null);
-        invalidationEventHub.fireInvalidationEvent();
+        invalidationEventHub.fireInvalidationEvent(Arrays.asList("a"));
     }
     
 }
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 e6fc2bd86..4a1c9e92a 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
@@ -53,6 +53,11 @@ public class ComponentDependencyRegistryImplTest
         Collections.sort(result);
         assertEquals(result, Arrays.asList("d", "dd"));
         
+        final List<String> returnValue = 
componentDependencyRegistry.listen(Collections.emptyList());
+        assertEquals(returnValue, Collections.emptyList());
+        assertEquals(componentDependencyRegistry.getDependents("bar").size(), 
0);
+        
assertEquals(componentDependencyRegistry.getDependencies("foo").size(), 0);
+        
     }
     
     @Test
diff --git 
a/tapestry-core/src/test/java/org/apache/tapestry5/internal/services/ComponentMessagesSourceImplTest.java
 
b/tapestry-core/src/test/java/org/apache/tapestry5/internal/services/ComponentMessagesSourceImplTest.java
index 7a0247805..a1e610454 100644
--- 
a/tapestry-core/src/test/java/org/apache/tapestry5/internal/services/ComponentMessagesSourceImplTest.java
+++ 
b/tapestry-core/src/test/java/org/apache/tapestry5/internal/services/ComponentMessagesSourceImplTest.java
@@ -33,6 +33,8 @@ import org.apache.tapestry5.model.ComponentModel;
 import org.apache.tapestry5.services.messages.ComponentMessagesSource;
 import org.apache.tapestry5.services.pageload.ComponentRequestSelectorAnalyzer;
 import org.apache.tapestry5.services.pageload.ComponentResourceLocator;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 import org.testng.annotations.AfterClass;
 import org.testng.annotations.BeforeClass;
 import org.testng.annotations.Test;
@@ -63,6 +65,8 @@ public class ComponentMessagesSourceImplTest extends 
InternalBaseTestCase
     private ComponentMessagesSourceImpl source;
 
     private ComponentResourceLocator resourceLocator;
+    
+    private Logger logger = 
LoggerFactory.getLogger(ComponentMessagesSourceImplTest.class);
 
     @BeforeClass
     public void setup()
@@ -70,7 +74,7 @@ public class ComponentMessagesSourceImplTest extends 
InternalBaseTestCase
         resourceLocator = getService(ComponentResourceLocator.class);
 
         source = new ComponentMessagesSourceImpl(false, 
simpleComponentResource.forFile("AppCatalog.properties"),
-                resourceLocator, new PropertiesFileParserImpl(), tracker, 
componentRequestSelectorAnalyzer, threadLocale);
+                resourceLocator, new PropertiesFileParserImpl(), tracker, 
componentRequestSelectorAnalyzer, threadLocale, logger);
     }
 
     @AfterClass
@@ -240,7 +244,7 @@ public class ComponentMessagesSourceImplTest extends 
InternalBaseTestCase
         List<Resource> resources = Arrays.asList(resource);
 
         ComponentMessagesSource source = new ComponentMessagesSourceImpl(true, 
resources,
-                new PropertiesFileParserImpl(), resourceLocator, converter, 
componentRequestSelectorAnalyzer, threadLocale);
+                new PropertiesFileParserImpl(), resourceLocator, converter, 
componentRequestSelectorAnalyzer, threadLocale, logger);
 
         Messages messages = source.getMessages(model, Locale.ENGLISH);
 
diff --git 
a/tapestry-core/src/test/java/org/apache/tapestry5/internal/services/ComponentTemplateSourceImplTest.java
 
b/tapestry-core/src/test/java/org/apache/tapestry5/internal/services/ComponentTemplateSourceImplTest.java
index 63a89d586..e44a4580a 100644
--- 
a/tapestry-core/src/test/java/org/apache/tapestry5/internal/services/ComponentTemplateSourceImplTest.java
+++ 
b/tapestry-core/src/test/java/org/apache/tapestry5/internal/services/ComponentTemplateSourceImplTest.java
@@ -35,6 +35,8 @@ import 
org.apache.tapestry5.services.pageload.ComponentRequestSelectorAnalyzer;
 import org.apache.tapestry5.services.pageload.ComponentResourceLocator;
 import org.apache.tapestry5.services.pageload.ComponentResourceSelector;
 import org.apache.tapestry5.services.templates.ComponentTemplateLocator;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 import org.testng.annotations.Test;
 
 public class ComponentTemplateSourceImplTest extends InternalBaseTestCase
@@ -53,6 +55,8 @@ public class ComponentTemplateSourceImplTest extends 
InternalBaseTestCase
 
     private final ComponentRequestSelectorAnalyzer 
componentRequestSelectorAnalyzer = 
         new DefaultComponentRequestSelectorAnalyzer(threadLocale);
+    
+    private final Logger logger = 
LoggerFactory.getLogger(ComponentTemplateSourceImplTest.class);
 
     /**
      * Creates a new class loader, whose parent is the thread's context class 
loader, but adds a single classpath root
@@ -111,7 +115,7 @@ public class ComponentTemplateSourceImplTest extends 
InternalBaseTestCase
 
         replay();
 
-        ComponentTemplateSource source = new ComponentTemplateSourceImpl(true, 
parser, locator, converter, componentRequestSelectorAnalyzer, threadLocale);
+        ComponentTemplateSource source = new ComponentTemplateSourceImpl(true, 
parser, locator, converter, componentRequestSelectorAnalyzer, threadLocale, 
logger);
 
         assertSame(source.getTemplate(model, english), template);
 
@@ -160,7 +164,7 @@ public class ComponentTemplateSourceImplTest extends 
InternalBaseTestCase
 
         replay();
 
-        ComponentTemplateSourceImpl source = new 
ComponentTemplateSourceImpl(false, parser, locator, converter, 
componentRequestSelectorAnalyzer, threadLocale);
+        ComponentTemplateSourceImpl source = new 
ComponentTemplateSourceImpl(false, parser, locator, converter, 
componentRequestSelectorAnalyzer, threadLocale, logger);
         source.addInvalidationListener(listener);
 
         assertSame(source.getTemplate(model, Locale.ENGLISH), template);
@@ -228,7 +232,7 @@ public class ComponentTemplateSourceImplTest extends 
InternalBaseTestCase
 
         replay();
 
-        ComponentTemplateSourceImpl source = new 
ComponentTemplateSourceImpl(true, parser, locator, converter, 
componentRequestSelectorAnalyzer, threadLocale);
+        ComponentTemplateSourceImpl source = new 
ComponentTemplateSourceImpl(true, parser, locator, converter, 
componentRequestSelectorAnalyzer, threadLocale, logger);
 
         assertSame(source.getTemplate(model, Locale.ENGLISH), template);
 
@@ -266,7 +270,7 @@ public class ComponentTemplateSourceImplTest extends 
InternalBaseTestCase
 
         replay();
 
-        ComponentTemplateSourceImpl source = new 
ComponentTemplateSourceImpl(true, parser, locator, converter, 
componentRequestSelectorAnalyzer, threadLocale);
+        ComponentTemplateSourceImpl source = new 
ComponentTemplateSourceImpl(true, parser, locator, converter, 
componentRequestSelectorAnalyzer, threadLocale, logger);
 
         ComponentTemplate template = source.getTemplate(model, Locale.ENGLISH);
 
@@ -298,7 +302,7 @@ public class ComponentTemplateSourceImplTest extends 
InternalBaseTestCase
 
         replay();
 
-        ComponentTemplateSource source = new ComponentTemplateSourceImpl(true, 
parser, locator, converter, componentRequestSelectorAnalyzer, threadLocale);
+        ComponentTemplateSource source = new ComponentTemplateSourceImpl(true, 
parser, locator, converter, componentRequestSelectorAnalyzer, threadLocale, 
logger);
 
         assertSame(source.getTemplate(model, english), template);
 
diff --git 
a/tapestry-ioc/src/main/java/org/apache/tapestry5/ioc/internal/AbstractReloadableObjectCreator.java
 
b/tapestry-ioc/src/main/java/org/apache/tapestry5/ioc/internal/AbstractReloadableObjectCreator.java
index 0efb4fd24..fa9d56b0e 100644
--- 
a/tapestry-ioc/src/main/java/org/apache/tapestry5/ioc/internal/AbstractReloadableObjectCreator.java
+++ 
b/tapestry-ioc/src/main/java/org/apache/tapestry5/ioc/internal/AbstractReloadableObjectCreator.java
@@ -284,7 +284,7 @@ public abstract class AbstractReloadableObjectCreator 
implements ObjectCreator,
 
         if (isFileURL(url))
         {
-            changeTracker.add(url);
+            changeTracker.add(url, className);
         }
     }
 
diff --git 
a/tapestry-ioc/src/main/java/org/apache/tapestry5/ioc/internal/util/URLChangeTracker.java
 
b/tapestry-ioc/src/main/java/org/apache/tapestry5/ioc/internal/util/URLChangeTracker.java
index aa2c85df8..2e78e0697 100644
--- 
a/tapestry-ioc/src/main/java/org/apache/tapestry5/ioc/internal/util/URLChangeTracker.java
+++ 
b/tapestry-ioc/src/main/java/org/apache/tapestry5/ioc/internal/util/URLChangeTracker.java
@@ -22,7 +22,9 @@ import java.io.File;
 import java.io.IOException;
 import java.net.URISyntaxException;
 import java.net.URL;
+import java.util.HashSet;
 import java.util.Map;
+import java.util.Set;
 
 /**
  * Given a (growing) set of URLs, can periodically check to see if any of the 
underlying resources has changed. This
@@ -35,7 +37,7 @@ public class URLChangeTracker
 {
     private static final long FILE_DOES_NOT_EXIST_TIMESTAMP = -1L;
 
-    private final Map<File, Long> fileToTimestamp = 
CollectionFactory.newConcurrentMap();
+    private final Map<File, TrackingInfo> fileToTimestamp = 
CollectionFactory.newConcurrentMap();
 
     private final boolean granularitySeconds;
 
@@ -135,6 +137,23 @@ public class URLChangeTracker
      *         null
      */
     public long add(URL url)
+    {
+        return add(url, null);
+    }
+    /**
+     * Stores a new URL and associated memo (most probably a related class 
name)
+     * into the tracker, or returns the previous time stamp for a previously 
added URL. Filters out all
+     * non-file URLs.
+     * 
+     * @param url
+     *            of the resource to add, or null if not known
+     * @param memo
+     *            some information about the tracked URL, most probably the 
associated class name
+     * @return the current timestamp for the URL (possibly rounded off for 
granularity reasons), or 0 if the URL is
+     *         null
+     * @since 5.8.3
+     */
+    public long add(URL url, String memo)
     {
         if (url == null)
             return 0;
@@ -147,14 +166,14 @@ public class URLChangeTracker
         File resourceFile = toFileFromFileProtocolURL(converted);
 
         if (fileToTimestamp.containsKey(resourceFile))
-            return fileToTimestamp.get(resourceFile);
+            return fileToTimestamp.get(resourceFile).timestamp;
 
         long timestamp = readTimestamp(resourceFile);
 
         // A quick and imperfect fix for TAPESTRY-1918. When a file
         // is added, add the directory containing the file as well.
 
-        fileToTimestamp.put(resourceFile, timestamp);
+        fileToTimestamp.put(resourceFile, new TrackingInfo(timestamp, memo));
 
         if (trackFolderChanges)
         {
@@ -163,7 +182,7 @@ public class URLChangeTracker
             if (!fileToTimestamp.containsKey(dir))
             {
                 long dirTimestamp = readTimestamp(dir);
-                fileToTimestamp.put(dir, dirTimestamp);
+                fileToTimestamp.put(dir, new TrackingInfo(dirTimestamp, null));
             }
         }
 
@@ -205,20 +224,48 @@ public class URLChangeTracker
         // concurrently, but CheckForUpdatesFilter ensures that it will be 
invoked
         // synchronously.
 
-        for (Map.Entry<File, Long> entry : fileToTimestamp.entrySet())
+        for (Map.Entry<File, TrackingInfo> entry : fileToTimestamp.entrySet())
         {
             long newTimestamp = readTimestamp(entry.getKey());
-            long current = entry.getValue();
+            long current = entry.getValue().timestamp;
 
             if (current == newTimestamp)
                 continue;
 
             result = true;
-            entry.setValue(newTimestamp);
+            entry.getValue().timestamp = newTimestamp;
         }
 
         return result;
     }
+    
+    /**
+     * Re-acquires the last updated timestamp for each URL and returns the 
memo value for all files with a changed timestamp.
+     */
+    public Set<String> getChangedResourcesMemos()
+    {
+        
+        Set<String> changedResourcesMemos = new HashSet<>();
+
+        for (Map.Entry<File, TrackingInfo> entry : fileToTimestamp.entrySet())
+        {
+            long newTimestamp = readTimestamp(entry.getKey());
+            final TrackingInfo value = entry.getValue();
+            long current = value.timestamp;
+
+            if (current != newTimestamp)
+            {
+                if (value.memo != null)
+                {
+                    changedResourcesMemos.add(value.memo);
+                }
+                value.timestamp = newTimestamp;
+            }
+        }
+
+        return changedResourcesMemos;
+    }
+
 
     /**
      * Returns the time that the specified file was last modified, possibly 
rounded down to the nearest second.
@@ -249,9 +296,9 @@ public class URLChangeTracker
      */
     public void forceChange()
     {
-        for (Map.Entry<File, Long> e : fileToTimestamp.entrySet())
+        for (Map.Entry<File, TrackingInfo> e : fileToTimestamp.entrySet())
         {
-            e.setValue(0l);
+            e.getValue().timestamp = 0l;
         }
     }
 
@@ -262,5 +309,25 @@ public class URLChangeTracker
     {
         return fileToTimestamp.size();
     }
+    
+    private static final class TrackingInfo
+    {
+        
+        private long timestamp;
+        private String memo;
+
+        public TrackingInfo(long timestamp, String memo) 
+        {
+            this.timestamp = timestamp;
+            this.memo = memo;
+        }
+
+        @Override
+        public String toString() 
+        {
+            return "Info [timestamp=" + timestamp + ", memo=" + memo + "]";
+        }
+        
+    }
 
 }


Reply via email to