You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tapestry.apache.org by th...@apache.org on 2022/11/17 22:59:59 UTC
[tapestry-5] 01/01: TAP5-2742: Initial work on smarter page cache invalidation
This is an automated email from the ASF dual-hosted git repository.
thiagohp pushed a commit to branch better-page-invalidation
in repository https://gitbox.apache.org/repos/asf/tapestry-5.git
commit 8f9b33380dd95bef2df6bb45174043d30e3acdfa
Author: Thiago H. de Paula Figueiredo <th...@arsmachina.com.br>
AuthorDate: Thu Nov 17 19:58:17 2022 -0300
TAP5-2742: Initial work on smarter page cache invalidation
---
583_RELEASE_NOTES.md | 7 +
.../commons/services/InvalidationEventHub.java | 16 ++
.../tapestry5/corelib/pages/PageCatalog.java | 139 ++++++++++++++-
.../internal/event/InvalidationEventHubImpl.java | 46 ++++-
.../services/ComponentClassResolverImpl.java | 35 +++-
.../services/ComponentDependencyRegistry.java | 62 +++++++
.../services/ComponentDependencyRegistryImpl.java | 191 +++++++++++++++++++++
.../internal/services/PageSourceImpl.java | 8 +-
.../services/ResourceDigestManagerImpl.java | 7 +
.../internal/structure/ComponentPageElement.java | 10 ++
.../structure/ComponentPageElementImpl.java | 24 ++-
.../apache/tapestry5/modules/TapestryModule.java | 13 ++
.../tapestry5/services/ComponentClassResolver.java | 6 +-
.../apache/tapestry5/corelib/pages/PageCatalog.tml | 28 ++-
.../event/InvalidationEventHubImplTest.java | 71 ++++++++
.../ComponentDependencyRegistryImplTest.java | 41 +++++
16 files changed, 675 insertions(+), 29 deletions(-)
diff --git a/583_RELEASE_NOTES.md b/583_RELEASE_NOTES.md
new file mode 100644
index 000000000..2e6bea9dd
--- /dev/null
+++ b/583_RELEASE_NOTES.md
@@ -0,0 +1,7 @@
+Scratch pad for changes destined for the 5.8.3 release notes page.
+
+# Non-backward-compatible changes
+
+* 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
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 d5831721e..85e3fbb69 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
@@ -12,7 +12,11 @@
package org.apache.tapestry5.commons.services;
+import java.util.List;
import java.util.Map;
+import java.util.function.Function;
+
+import org.apache.tapestry5.ioc.annotations.IncompatibleChange;
/**
* An object which manages a list of {@link org.apache.tapestry5.commons.services.InvalidationListener}s. There are multiple
@@ -55,4 +59,16 @@ public interface InvalidationEventHub
* @since 5.4
*/
void clearOnInvalidation(Map<?,?> map);
+
+ /**
+ * Adds a callback, as a function that receives a list of strings and also returns a list of strings,
+ * that is invoked when one or more listed underlying tracked resource have changed.
+ * An empty list should be considered as all resources being changed and any caches needing to be cleared.
+ * The return value of the function should be a non-null, but possibly empty, list of other resources that also
+ * need to be invalidated in a recursive fashion.
+ * This method does nothing in production mode.
+ * @since 5.8.3
+ */
+ @IncompatibleChange(release = "5.8.3", details = "Added method")
+ void addInvalidationCallback(Function<List<String>, List<String>> function);
}
diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/corelib/pages/PageCatalog.java b/tapestry-core/src/main/java/org/apache/tapestry5/corelib/pages/PageCatalog.java
index abaedb62d..72b79b5cc 100644
--- a/tapestry-core/src/main/java/org/apache/tapestry5/corelib/pages/PageCatalog.java
+++ b/tapestry-core/src/main/java/org/apache/tapestry5/corelib/pages/PageCatalog.java
@@ -14,31 +14,47 @@
package org.apache.tapestry5.corelib.pages;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+
+import org.apache.tapestry5.MarkupWriter;
import org.apache.tapestry5.alerts.AlertManager;
-import org.apache.tapestry5.annotations.*;
+import org.apache.tapestry5.annotations.InjectComponent;
+import org.apache.tapestry5.annotations.Persist;
+import org.apache.tapestry5.annotations.Property;
+import org.apache.tapestry5.annotations.UnknownActivationContextCheck;
+import org.apache.tapestry5.annotations.WhitelistAccessOnly;
import org.apache.tapestry5.beaneditor.Validate;
import org.apache.tapestry5.beanmodel.BeanModel;
import org.apache.tapestry5.beanmodel.services.BeanModelSource;
import org.apache.tapestry5.commons.Messages;
import org.apache.tapestry5.commons.util.CollectionFactory;
import org.apache.tapestry5.corelib.components.Zone;
-import org.apache.tapestry5.func.*;
+import org.apache.tapestry5.dom.Element;
+import org.apache.tapestry5.func.F;
+import org.apache.tapestry5.func.Flow;
+import org.apache.tapestry5.func.Mapper;
+import org.apache.tapestry5.func.Predicate;
+import org.apache.tapestry5.func.Reducer;
import org.apache.tapestry5.http.TapestryHttpSymbolConstants;
+import org.apache.tapestry5.http.services.Request;
import org.apache.tapestry5.internal.PageCatalogTotals;
+import org.apache.tapestry5.internal.services.ComponentDependencyRegistry;
import org.apache.tapestry5.internal.services.PageSource;
import org.apache.tapestry5.internal.services.ReloadHelper;
+import org.apache.tapestry5.internal.structure.ComponentPageElement;
import org.apache.tapestry5.internal.structure.Page;
import org.apache.tapestry5.ioc.OperationTracker;
import org.apache.tapestry5.ioc.annotations.Inject;
import org.apache.tapestry5.ioc.annotations.Symbol;
import org.apache.tapestry5.ioc.internal.util.InternalUtils;
+import org.apache.tapestry5.runtime.Component;
import org.apache.tapestry5.services.ComponentClassResolver;
import org.apache.tapestry5.services.pageload.ComponentResourceSelector;
-import java.util.Collection;
-import java.util.List;
-import java.util.Set;
-
/**
* Lists out the currently loaded pages, using a {@link org.apache.tapestry5.corelib.components.Grid}.
* Provides an option to force all pages to be loaded. In development mode, includes an option to clear the page cache.
@@ -64,6 +80,9 @@ public class PageCatalog
@Inject
private ComponentClassResolver resolver;
+
+ @Inject
+ private ComponentDependencyRegistry componentDependencyRegistry;
@Inject
private AlertManager alertManager;
@@ -71,8 +90,17 @@ public class PageCatalog
@Property
private Page page;
+ @Property
+ private Page selectedPage;
+
+ @Property
+ private String dependency;
+
@InjectComponent
private Zone pagesZone;
+
+ @InjectComponent
+ private Zone pageStructureZone;
@Persist
private Set<String> failures;
@@ -90,13 +118,16 @@ public class PageCatalog
@Inject
private BeanModelSource beanModelSource;
-
+
@Inject
private Messages messages;
@Property
public static BeanModel<Page> model;
+ @Inject
+ private Request request;
+
void pageLoaded()
{
model = beanModelSource.createDisplayModel(Page.class, messages);
@@ -291,4 +322,98 @@ public class PageCatalog
{
return String.format("%,.3f ms", millis);
}
+
+ public List<String> getDependencies()
+ {
+ List<String> dependencies = new ArrayList<>(componentDependencyRegistry.getDependencies(getSelectedPageClassName()));
+ Collections.sort(dependencies);
+ return dependencies;
+ }
+
+ public Object onPageStructure(String name)
+ {
+ selectedPage = pageSource.getPage(name);
+ return request.isXHR() ? pageStructureZone.getBody() : null;
+ }
+
+ public String getDisplayLogicalName()
+ {
+ return getDisplayLogicalName(dependency);
+ }
+
+ public String getPageClassName()
+ {
+
+ return getClassName(page);
+ }
+
+ public String getSelectedPageClassName()
+ {
+ return getClassName(selectedPage);
+ }
+
+ private String getClassName(Page page)
+ {
+ return page.getRootComponent().getComponentResources().getComponentModel().getComponentClassName();
+ }
+
+ private String getClassName(Component component)
+ {
+ return component.getComponentResources().getComponentModel().getComponentClassName();
+ }
+
+ public void onComponentTree(MarkupWriter writer)
+ {
+ render(selectedPage.getRootElement(), writer);
+ }
+
+ private void render(ComponentPageElement componentPageElement, MarkupWriter writer)
+ {
+ final Element li = writer.element("li");
+ final String className = getClassName(componentPageElement.getComponent());
+ final Set<String> embeddedElementIds = componentPageElement.getEmbeddedElementIds();
+
+ if (componentPageElement.getComponent().getComponentResources().getComponentModel().isPage())
+ {
+ li.text(componentPageElement.getPageName());
+ }
+ else {
+ li.text(String.format("%s (%s)", getDisplayLogicalName(className), componentPageElement.getId()));
+ }
+
+ if (!embeddedElementIds.isEmpty())
+ {
+ writer.element("ul");
+ for (String id : embeddedElementIds)
+ {
+ render(componentPageElement.getEmbeddedElement(id), writer);
+ }
+ writer.end();
+ }
+
+ writer.end();
+ }
+
+ private String getDisplayLogicalName(final String className)
+ {
+ final String logicalName = resolver.getLogicalName(className);
+ String displayName = logicalName;
+ if (logicalName == null || logicalName.trim().length() == 0)
+ {
+ if (className.contains(".base."))
+ {
+ displayName = "(base class)";
+ }
+ if (className.contains(".mixins."))
+ {
+ displayName = "(mixin)";
+ }
+ }
+ return displayName;
+ }
+
+ public String getLogicalName(String className)
+ {
+ return resolver.getLogicalName(className);
+ }
}
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 b8e458616..b635730b3 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
@@ -14,12 +14,18 @@
package org.apache.tapestry5.internal.event;
+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 java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
import java.util.List;
import java.util.Map;
+import java.util.Set;
+import java.util.function.Function;
/**
* Base implementation class for classes (especially services) that need to manage a list of
@@ -27,8 +33,8 @@ import java.util.Map;
*/
public class InvalidationEventHubImpl implements InvalidationEventHub
{
- private final List<Runnable> callbacks;
-
+ private final List<Function<List<String>, List<String>>> callbacks;
+
protected InvalidationEventHubImpl(boolean productionMode)
{
if (productionMode)
@@ -44,19 +50,37 @@ public class InvalidationEventHubImpl implements InvalidationEventHub
* Notifies all listeners/callbacks.
*/
protected final void fireInvalidationEvent()
+ {
+ fireInvalidationEvent(Collections.emptyList());
+ }
+
+ /**
+ * Notifies all listeners/callbacks.
+ */
+ protected final void fireInvalidationEvent(List<String> resources)
{
if (callbacks == null)
{
return;
}
-
- for (Runnable callback : callbacks)
+
+ do
{
- callback.run();
+ Set<String> extraResources = new HashSet<>();
+ for (Function<List<String>, List<String>> callback : callbacks)
+ {
+ final List<String> newResources = callback.apply(resources);
+ if (newResources == null) {
+ throw new TapestryException("InvalidationEventHub callback functions cannot return null", null);
+ }
+ extraResources.addAll(newResources);
+ }
+ resources = new ArrayList<>(extraResources);
}
+ while (!resources.isEmpty());
}
- public final void addInvalidationCallback(Runnable callback)
+ public final void addInvalidationCallback(final Runnable callback)
{
assert callback != null;
@@ -64,7 +88,10 @@ public class InvalidationEventHubImpl implements InvalidationEventHub
// ignore the callback.
if (callbacks != null)
{
- callbacks.add(callback);
+ callbacks.add((r) -> {
+ callback.run();
+ return Collections.emptyList();
+ });
}
}
@@ -94,4 +121,9 @@ public class InvalidationEventHubImpl implements InvalidationEventHub
});
}
+ @Override
+ public void addInvalidationCallback(Function<List<String>, List<String>> callback) {
+ callbacks.add(callback);
+ }
+
}
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 8ba9c0a3d..f8c6d6df1 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
@@ -12,6 +12,14 @@
package org.apache.tapestry5.internal.services;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Formatter;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.regex.Pattern;
+
import org.apache.tapestry5.SymbolConstants;
import org.apache.tapestry5.commons.services.InvalidationListener;
import org.apache.tapestry5.commons.util.AvailableValues;
@@ -27,9 +35,6 @@ import org.apache.tapestry5.services.LibraryMapping;
import org.apache.tapestry5.services.transform.ControlledPackageType;
import org.slf4j.Logger;
-import java.util.*;
-import java.util.regex.Pattern;
-
public class ComponentClassResolverImpl implements ComponentClassResolver, InvalidationListener
{
private static final String CORE_LIBRARY_PREFIX = "core/";
@@ -812,4 +817,28 @@ public class ComponentClassResolverImpl implements ComponentClassResolver, Inval
return libraryMappings;
}
+ @Override
+ public String getLogicalName(String className)
+ {
+ String result = getData().pageClassNameToLogicalName.get(className);
+ if (result == null)
+ {
+ result = getKeyByValue(getData().componentToClassName, className);
+ }
+ else {
+ result = getKeyByValue(getData().mixinToClassName, className);
+ }
+
+ return result;
+ }
+
+ private String getKeyByValue(Map<String, String> map, String value)
+ {
+ return map.entrySet().stream()
+ .filter(e -> e.getValue().equals(value))
+ .map(e -> e.getKey())
+ .findAny()
+ .orElse(null);
+ }
+
}
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
new file mode 100644
index 000000000..d8e827c8a
--- /dev/null
+++ b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ComponentDependencyRegistry.java
@@ -0,0 +1,62 @@
+// Copyright 2022 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.internal.services;
+
+import java.util.Set;
+
+import org.apache.tapestry5.commons.services.InvalidationEventHub;
+import org.apache.tapestry5.internal.structure.ComponentPageElement;
+
+
+/**
+ * Internal service that registers direct dependencies between components (including components, pages and
+ * base classes). Even though methods receive {@link ComponentPageElement} parameters, dependencies
+ * are tracked using their fully qualified classs names.
+ *
+ * @since 5.8.3
+ */
+public interface ComponentDependencyRegistry {
+
+ /**
+ * Register all the dependencies of a given component.
+ */
+ void register(ComponentPageElement resources);
+
+ /**
+ * Clears all dependency information for a given component.
+ */
+ void clear(String className);
+
+ /**
+ * Clears all dependency information.
+ */
+ void clear();
+
+ /**
+ * Returns the fully qualified names of the direct dependencies of a given component.
+ */
+ Set<String> getDependents(String className);
+
+ /**
+ * Returns the fully qualified names of the direct dependencies of a given component.
+ */
+ Set<String> getDependencies(String className);
+
+ /**
+ * Signs up this registry to invalidation events from a given hub.
+ */
+ void listen(InvalidationEventHub invalidationEventHub);
+
+}
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
new file mode 100644
index 000000000..8c4c7d0da
--- /dev/null
+++ b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ComponentDependencyRegistryImpl.java
@@ -0,0 +1,191 @@
+// Copyright 2022 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.internal.services;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import org.apache.tapestry5.ComponentResources;
+import org.apache.tapestry5.commons.services.InvalidationEventHub;
+import org.apache.tapestry5.internal.structure.ComponentPageElement;
+import org.apache.tapestry5.model.ComponentModel;
+import org.apache.tapestry5.model.EmbeddedComponentModel;
+import org.apache.tapestry5.runtime.Component;
+
+
+public class ComponentDependencyRegistryImpl implements ComponentDependencyRegistry
+{
+
+ // Key is a component, values are the components that depend on it.
+ final private Map<String, Set<String>> map;
+
+ // Cache to check which classes were already processed or not.
+ final private Set<String> alreadyProcessed;
+
+ public ComponentDependencyRegistryImpl()
+ {
+ map = new HashMap<>();
+ alreadyProcessed = new HashSet<>();
+ }
+
+ @Override
+ public void register(ComponentPageElement componentPageElement)
+ {
+ final String componentClassName = getClassName(componentPageElement);
+
+ if (!alreadyProcessed.contains(componentClassName))
+ {
+ synchronized (map)
+ {
+
+ // Components in the tree (i.e. declared in the template
+ for (String id : componentPageElement.getEmbeddedElementIds())
+ {
+ final ComponentPageElement child = componentPageElement.getEmbeddedElement(id);
+ add(componentPageElement, child);
+ register(child);
+ }
+
+ // Mixins, class level
+ final ComponentResources componentResources = componentPageElement.getComponentResources();
+ final ComponentModel componentModel = componentResources.getComponentModel();
+ for (String mixinClassName : componentModel.getMixinClassNames())
+ {
+ add(componentClassName, mixinClassName);
+ }
+
+ // Mixins applied to embedded component instances
+ final List<String> embeddedComponentIds = componentModel.getEmbeddedComponentIds();
+ for (String id : embeddedComponentIds)
+ {
+ final EmbeddedComponentModel embeddedComponentModel = componentResources
+ .getComponentModel()
+ .getEmbeddedComponentModel(id);
+ final List<String> mixinClassNames = embeddedComponentModel
+ .getMixinClassNames();
+ for (String mixinClassName : mixinClassNames) {
+ add(componentClassName, mixinClassName);
+ }
+ }
+
+ // Superclass
+ final Component component = componentPageElement.getComponent();
+ Class<?> parent = component.getClass().getSuperclass();
+ if (parent != null && !Object.class.equals(parent))
+ {
+ add(componentClassName, parent.getName());
+ }
+
+ alreadyProcessed.add(componentClassName);
+
+ }
+
+ }
+
+ }
+
+ private String getClassName(ComponentPageElement component)
+ {
+ return component.getComponentResources().getComponentModel().getComponentClassName();
+ }
+
+ @Override
+ public void clear(String className)
+ {
+ synchronized (map)
+ {
+ alreadyProcessed.remove(className);
+ map.put(className, null);
+ final Collection<Set<String>> allDependentSets = map.values();
+ for (Set<String> dependents : allDependentSets)
+ {
+ if (dependents != null)
+ {
+ dependents.remove(className);
+ }
+ }
+ }
+ }
+
+ @Override
+ public void clear() {
+ map.clear();
+ alreadyProcessed.clear();
+ }
+
+ @Override
+ public Set<String> getDependents(String className)
+ {
+ return map.get(className);
+ }
+
+ @Override
+ public Set<String> getDependencies(String className)
+ {
+ return map.entrySet().stream()
+ .filter(e -> e.getValue().contains(className))
+ .map(e -> e.getKey())
+ .collect(Collectors.toSet());
+ }
+
+ private void add(ComponentPageElement component, ComponentPageElement dependency)
+ {
+ add(getClassName(component), getClassName(dependency));
+ }
+
+ private void add(String component, String dependency)
+ {
+ synchronized (map)
+ {
+ Set<String> dependents = map.get(dependency);
+ if (dependents == null)
+ {
+ dependents = new HashSet<>();
+ map.put(dependency, dependents);
+ }
+ dependents.add(component);
+ }
+ }
+
+ @Override
+ public void listen(InvalidationEventHub invalidationEventHub)
+ {
+ invalidationEventHub.addInvalidationCallback(this::listen);
+ }
+
+ private List<String> listen(List<String> resources)
+ {
+ List<String> furtherDependents = new ArrayList<>();
+ for (String resource : resources)
+ {
+ final Set<String> dependents = map.get(resource);
+ for (String furtherDependent : dependents)
+ {
+ if (!resources.contains(furtherDependent) && !furtherDependents.contains(furtherDependent))
+ {
+ furtherDependents.add(furtherDependent);
+ }
+ }
+ clear(resource);
+ }
+ return furtherDependents;
+ }
+
+}
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 cc15c1658..e9db70ad9 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
@@ -37,6 +37,8 @@ public class PageSourceImpl implements PageSource
private final PageLoader pageLoader;
+ private final ComponentDependencyRegistry componentDependencyRegistry;
+
private static final class CachedPageKey
{
final String pageName;
@@ -70,10 +72,12 @@ public class PageSourceImpl implements PageSource
private final Map<CachedPageKey, SoftReference<Page>> pageCache = CollectionFactory.newConcurrentMap();
- public PageSourceImpl(PageLoader pageLoader, ComponentRequestSelectorAnalyzer selectorAnalyzer)
+ public PageSourceImpl(PageLoader pageLoader, ComponentRequestSelectorAnalyzer selectorAnalyzer,
+ ComponentDependencyRegistry componentDependencyRegistry)
{
this.pageLoader = pageLoader;
this.selectorAnalyzer = selectorAnalyzer;
+ this.componentDependencyRegistry = componentDependencyRegistry;
}
public Page getPage(String canonicalPageName)
@@ -106,6 +110,8 @@ public class PageSourceImpl implements PageSource
ref = new SoftReference<Page>(page);
pageCache.put(key, ref);
+
+ componentDependencyRegistry.register(page.getRootElement());
}
}
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 11aa54c3d..08ca9c066 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
@@ -17,7 +17,9 @@ package org.apache.tapestry5.internal.services;
import org.apache.tapestry5.commons.Resource;
import org.apache.tapestry5.commons.services.InvalidationListener;
+import java.util.List;
import java.util.Map;
+import java.util.function.Function;
public class ResourceDigestManagerImpl implements ResourceDigestManager
{
@@ -42,4 +44,9 @@ public class ResourceDigestManagerImpl implements ResourceDigestManager
public void clearOnInvalidation(Map<?, ?> map)
{
}
+
+ @Override
+ public void addInvalidationCallback(Function<List<String>, List<String>> function)
+ {
+ }
}
diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/internal/structure/ComponentPageElement.java b/tapestry-core/src/main/java/org/apache/tapestry5/internal/structure/ComponentPageElement.java
index 0152e401e..1e17fe90b 100644
--- a/tapestry-core/src/main/java/org/apache/tapestry5/internal/structure/ComponentPageElement.java
+++ b/tapestry-core/src/main/java/org/apache/tapestry5/internal/structure/ComponentPageElement.java
@@ -12,6 +12,8 @@
package org.apache.tapestry5.internal.structure;
+import java.util.Set;
+
import org.apache.tapestry5.Binding;
import org.apache.tapestry5.Block;
import org.apache.tapestry5.ComponentResources;
@@ -20,6 +22,7 @@ import org.apache.tapestry5.commons.Location;
import org.apache.tapestry5.internal.InternalComponentResources;
import org.apache.tapestry5.internal.InternalComponentResourcesCommon;
import org.apache.tapestry5.internal.services.Instantiator;
+import org.apache.tapestry5.ioc.annotations.IncompatibleChange;
import org.apache.tapestry5.runtime.Component;
import org.apache.tapestry5.runtime.ComponentEvent;
import org.apache.tapestry5.runtime.RenderCommand;
@@ -102,6 +105,13 @@ public interface ComponentPageElement extends ComponentResourcesCommon, Internal
* if no component exists with the given id
*/
ComponentPageElement getEmbeddedElement(String id);
+
+ /**
+ * Returns the ids of all embedded elements defined within the component.
+ * @since 5.8.3
+ */
+ @IncompatibleChange(release = "5.8.3", details = "Added method")
+ Set<String> getEmbeddedElementIds();
/**
* Returns the {@link org.apache.tapestry5.ComponentResources} for a mixin attached to this component element. Mixin
diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/internal/structure/ComponentPageElementImpl.java b/tapestry-core/src/main/java/org/apache/tapestry5/internal/structure/ComponentPageElementImpl.java
index 151b5c8af..36fd6bc6e 100644
--- a/tapestry-core/src/main/java/org/apache/tapestry5/internal/structure/ComponentPageElementImpl.java
+++ b/tapestry-core/src/main/java/org/apache/tapestry5/internal/structure/ComponentPageElementImpl.java
@@ -842,15 +842,7 @@ public class ComponentPageElementImpl extends BaseLocatable implements Component
if (embeddedElement == null)
{
- Set<String> ids = CollectionFactory.newSet();
-
- if (children != null)
- {
- for (ComponentPageElement child : children)
- {
- ids.add(child.getId());
- }
- }
+ Set<String> ids = getEmbeddedElementIds();
throw new UnknownValueException(String.format("Component %s does not contain embedded component '%s'.",
getCompleteId(), embeddedId), new AvailableValues("Embedded components", ids));
@@ -859,6 +851,20 @@ public class ComponentPageElementImpl extends BaseLocatable implements Component
return embeddedElement;
}
+ @Override
+ public Set<String> getEmbeddedElementIds() {
+ Set<String> ids = CollectionFactory.newSet();
+
+ if (children != null)
+ {
+ for (ComponentPageElement child : children)
+ {
+ ids.add(child.getId());
+ }
+ }
+ return ids;
+ }
+
public String getId()
{
return id;
diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/modules/TapestryModule.java b/tapestry-core/src/main/java/org/apache/tapestry5/modules/TapestryModule.java
index 34b9bec4c..f47bc52b6 100644
--- a/tapestry-core/src/main/java/org/apache/tapestry5/modules/TapestryModule.java
+++ b/tapestry-core/src/main/java/org/apache/tapestry5/modules/TapestryModule.java
@@ -163,6 +163,7 @@ import org.apache.tapestry5.internal.services.*;
import org.apache.tapestry5.internal.services.ajax.AjaxFormUpdateFilter;
import org.apache.tapestry5.internal.services.ajax.AjaxResponseRendererImpl;
import org.apache.tapestry5.internal.services.ajax.MultiZoneUpdateEventResultProcessor;
+import org.apache.tapestry5.internal.services.assets.ResourceChangeTracker;
import org.apache.tapestry5.internal.services.exceptions.ExceptionReportWriterImpl;
import org.apache.tapestry5.internal.services.exceptions.ExceptionReporterImpl;
import org.apache.tapestry5.internal.services.linktransform.LinkTransformerImpl;
@@ -2766,6 +2767,18 @@ public final class TapestryModule
{
configuration.add(appRootPackage + ".rest.entities");
}
+
+ public static ComponentDependencyRegistry buildComponentDependencyRegistry(
+ InternalComponentInvalidationEventHub internalComponentInvalidationEventHub,
+ ResourceChangeTracker resourceChangeTracker,
+ ComponentTemplateSource componentTemplateSource)
+ {
+ ComponentDependencyRegistry componentDependencyRegistry = new ComponentDependencyRegistryImpl();
+ componentDependencyRegistry.listen(internalComponentInvalidationEventHub);
+ componentDependencyRegistry.listen(resourceChangeTracker);
+ componentDependencyRegistry.listen(componentTemplateSource.getInvalidationEventHub());
+ return componentDependencyRegistry;
+ }
private static final class TapestryCoreComponentLibraryInfoSource implements
ComponentLibraryInfoSource
diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/services/ComponentClassResolver.java b/tapestry-core/src/main/java/org/apache/tapestry5/services/ComponentClassResolver.java
index e84d175e9..e94489a5e 100644
--- a/tapestry-core/src/main/java/org/apache/tapestry5/services/ComponentClassResolver.java
+++ b/tapestry-core/src/main/java/org/apache/tapestry5/services/ComponentClassResolver.java
@@ -179,9 +179,13 @@ public interface ComponentClassResolver
/**
* Returns the library mappings.
- * @return
*/
@IncompatibleChange(release = "5.4", details = "Added method")
Collection<LibraryMapping> getLibraryMappings();
+ /**
+ * Returns the logical name for a page, component or mixin fully classified class name.
+ */
+ @IncompatibleChange(release = "5.8.3", details = "Added method")
+ public String getLogicalName(String className);
}
diff --git a/tapestry-core/src/main/resources/org/apache/tapestry5/corelib/pages/PageCatalog.tml b/tapestry-core/src/main/resources/org/apache/tapestry5/corelib/pages/PageCatalog.tml
index 5e7c7a2f3..0424beab1 100644
--- a/tapestry-core/src/main/resources/org/apache/tapestry5/corelib/pages/PageCatalog.tml
+++ b/tapestry-core/src/main/resources/org/apache/tapestry5/corelib/pages/PageCatalog.tml
@@ -12,6 +12,11 @@
<t:beandisplay t:id="totals"/>
<t:grid source="pages" row="page" model="model">
+ <p:componentCountCell>
+ ${page.stats.componentCount}
+ <a href="#" t:type="EventLink" t:event="pageStructure" t:zone="pageStructureZone"
+ t:context="page.name">Structure info</a>
+ </p:componentCountCell>
<p:assemblyTimeCell>
${formatElapsed(page.stats.assemblyTime)}
</p:assemblyTimeCell>
@@ -37,7 +42,28 @@
</t:if>
<t:actionlink t:id="runGC" zone="pages" class="btn btn-default">Run the GC</t:actionlink>
</div>
-
+
+
+ <t:zone t:id="pageStructureZone">
+ <div class="panel panel-default vert-offset" t:type="If" t:test="selectedPage">
+ <div class="panel-heading">Component dependency information for ${selectedPage.name} (just direct dependencies)</div>
+ <div class="panel-body">
+ <ul>
+ <li t:type="Loop" t:value="dependency" t:source="dependencies">
+ ${displayLogicalName} (${dependency})
+ </li>
+ </ul>
+ </div>
+ </div>
+ <div class="panel panel-default vert-offset" t:type="If" t:test="selectedPage">
+ <div class="panel-heading">${selectedPage.name}'s component tree</div>
+ <div class="panel-body">
+ <ul>
+ <t:trigger t:event="componentTree"/>
+ </ul>
+ </div>
+ </div>
+ </t:zone>
<div class="panel panel-default vert-offset">
<div class="panel-heading">Load single page</div>
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
new file mode 100644
index 000000000..6cf69d58a
--- /dev/null
+++ b/tapestry-core/src/test/java/org/apache/tapestry5/internal/event/InvalidationEventHubImplTest.java
@@ -0,0 +1,71 @@
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package org.apache.tapestry5.internal.event;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Function;
+
+import org.apache.tapestry5.commons.internal.util.TapestryException;
+import org.apache.tapestry5.internal.services.ComponentTemplateSourceImplTest;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+/**
+ * Tests the parts of {@link InvalidationEventHubImpl} that {@link ComponentTemplateSourceImplTest}
+ * doesn't. This is mostly for the resource-specific invalidations in
+ * {@link InvalidationEventHubImpl#addInvalidationCallback(java.util.function.Function)}
+ */
+public class InvalidationEventHubImplTest
+{
+
+ /**
+ * Tests {@link InvalidationEventHubImpl#addInvalidationCallback(java.util.function.Function)}.
+ */
+ @Test
+ public void add_invalidation_callback_with_parameter()
+ {
+ InvalidationEventHubImpl invalidationEventHub = new InvalidationEventHubImpl(false);
+ final String firstInitialElement = "a";
+ final String secondInitialElement = "b";
+ final List<String> initialResources = Arrays.asList(firstInitialElement, secondInitialElement);
+ final AtomicInteger callCount = new AtomicInteger(0);
+ Function<List<String>, List<String>> callback = (r) -> {
+ callCount.incrementAndGet();
+ if (r.size() == 2 && r.get(0).equals(firstInitialElement) && r.get(1).equals(secondInitialElement)) {
+ return Arrays.asList(firstInitialElement.toUpperCase(), secondInitialElement.toUpperCase());
+ }
+ else if (r.size() == 2 && r.get(0).equals(firstInitialElement.toUpperCase()) && r.get(1).equals(secondInitialElement.toUpperCase())) {
+ return Arrays.asList("something", "else");
+ }
+ else {
+ return Collections.emptyList();
+ }
+ };
+
+ invalidationEventHub.addInvalidationCallback(callback);
+ invalidationEventHub.fireInvalidationEvent(initialResources);
+ Assert.assertEquals(callCount.get(), 3, "Wrong call count");
+
+ }
+
+ @Test(expectedExceptions = TapestryException.class)
+ public void null_check_for_callback_method()
+ {
+ InvalidationEventHubImpl invalidationEventHub = new InvalidationEventHubImpl(false);
+ invalidationEventHub.addInvalidationCallback((s) -> null);
+ invalidationEventHub.fireInvalidationEvent();
+ }
+
+}
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
new file mode 100644
index 000000000..794ffd7dc
--- /dev/null
+++ b/tapestry-core/src/test/java/org/apache/tapestry5/internal/services/ComponentDependencyRegistryImplTest.java
@@ -0,0 +1,41 @@
+// Copyright 2022 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.internal.services;
+
+import org.apache.tapestry5.internal.test.InternalBaseTestCase;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
+
+/**
+ * Tests for the bean editor model source itself, as well as the model classes.
+ */
+public abstract class ComponentDependencyRegistryImplTest extends InternalBaseTestCase
+{
+
+ private ComponentDependencyRegistry componentDependencyRegistry;
+
+ @BeforeClass
+ public void setup()
+ {
+ componentDependencyRegistry = new ComponentDependencyRegistryImpl();
+ }
+
+ @Test
+ public void register()
+ {
+ componentDependencyRegistry.register(null);
+ }
+
+}