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 <[email protected]> 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); + } + +}
