diff --git a/583_RELEASE_NOTES.md b/583_RELEASE_NOTES.md new file mode 100644 index 0000000000..55a4de5014 --- /dev/null +++ b/583_RELEASE_NOTES.md @@ -0,0 +1,25 @@ +Scratch pad for changes destined for the 5.8.3 release notes page. + +# New configuration symbol + +* SymbolConstants.MULTIPLE_CLASSLOADERS: when set to true (default false), enables +multiple classloaders for smarter page cache invalidation. + +# Added methods + +* add(URL url, String memo) to URLChangeTracker +* getChangeResourcesMemos() to URLChangeTracker +* getValues() to MultiKey +* New getLogicalName() method in ComponentClassResolver. +* New getPlasticManager() method in PlasticProxyFactory +* New getPageNames() method in BeanBlockOverrideSource + +# Non-backward-compatible changes (but that probably won't cause problems) + +* New addInvalidationCallback(Function, List> callback) method in InvalidationEventHub +* New getEmbeddedElementIds() method in ComponentPageElement (internal service) +* New registerClassName() method in ResourceChangeTracker (internal service) +* New clearClassName() method in ResourceChangeTracker (internal service) + +# Overall notes +* Before \ No newline at end of file diff --git a/beanmodel/src/main/java/org/apache/tapestry5/beanmodel/internal/services/PropertyConduitSourceImpl.java b/beanmodel/src/main/java/org/apache/tapestry5/beanmodel/internal/services/PropertyConduitSourceImpl.java index 158132a396..5ac0aac721 100644 --- a/beanmodel/src/main/java/org/apache/tapestry5/beanmodel/internal/services/PropertyConduitSourceImpl.java +++ b/beanmodel/src/main/java/org/apache/tapestry5/beanmodel/internal/services/PropertyConduitSourceImpl.java @@ -41,9 +41,12 @@ import java.lang.reflect.Modifier; import java.lang.reflect.Type; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; +import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Map.Entry; import org.antlr.runtime.ANTLRInputStream; import org.antlr.runtime.CommonTokenStream; @@ -1387,9 +1390,42 @@ public PropertyConduitSourceImpl(PropertyAccess access, @ComponentLayer @PostInjection public void listenForInvalidations(@ComponentClasses InvalidationEventHub hub) { - hub.clearOnInvalidation(cache); + hub.addInvalidationCallback(this::listen); + } + + private List listen(List resources) + { + + if (resources.isEmpty()) + { + cache.clear(); + } + else + { + + final Iterator> iterator = cache.entrySet().iterator(); + + while (iterator.hasNext()) + { + + final Entry entry = iterator.next(); + + for (String resource : resources) { + @SuppressWarnings("rawtypes") + final Class clasz = (Class) entry.getKey().getValues()[0]; + if (clasz.getName().equals(resource)) + { + iterator.remove(); + } + } + + } + + } + + return Collections.emptyList(); + } - public PropertyConduit create(Class rootClass, String expression) { @@ -1488,7 +1524,7 @@ private PropertyConduit build(final Class rootClass, String expression) break; } - return proxyFactory.createProxy(InternalPropertyConduit.class, + return proxyFactory.getProxyFactory(rootClass.getName()).createProxy(InternalPropertyConduit.class, new PropertyConduitBuilder(rootClass, expression, tree)).newInstance(); } catch (Exception ex) { diff --git a/beanmodel/src/main/java/org/apache/tapestry5/beanmodel/services/PlasticProxyFactoryImpl.java b/beanmodel/src/main/java/org/apache/tapestry5/beanmodel/services/PlasticProxyFactoryImpl.java index abc5c762ce..1e6a507857 100644 --- a/beanmodel/src/main/java/org/apache/tapestry5/beanmodel/services/PlasticProxyFactoryImpl.java +++ b/beanmodel/src/main/java/org/apache/tapestry5/beanmodel/services/PlasticProxyFactoryImpl.java @@ -297,4 +297,10 @@ public void removePlasticClassListener(PlasticClassListener listener) manager.removePlasticClassListener(listener); } + @Override + public PlasticManager getPlasticManager() + { + return manager; + } + } 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 d5831721e8..4e0f37ec57 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,24 @@ 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> function); + + /** + * Notify resource-specific invalidations to listeners. + * @since 5.8.3 + */ + @IncompatibleChange(release = "5.8.3", details = "Added method") + void fireInvalidationEvent(List resources); + } diff --git a/commons/src/main/java/org/apache/tapestry5/commons/services/PlasticProxyFactory.java b/commons/src/main/java/org/apache/tapestry5/commons/services/PlasticProxyFactory.java index 3b8b9bc473..0237cc7a44 100644 --- a/commons/src/main/java/org/apache/tapestry5/commons/services/PlasticProxyFactory.java +++ b/commons/src/main/java/org/apache/tapestry5/commons/services/PlasticProxyFactory.java @@ -1,4 +1,4 @@ -// Copyright 2011, 2012 The Apache Software Foundation +// Copyright 2011, 2012, 2023 The Apache Software Foundation // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -24,6 +24,7 @@ import org.apache.tapestry5.plastic.PlasticClassListenerHub; import org.apache.tapestry5.plastic.PlasticClassTransformation; import org.apache.tapestry5.plastic.PlasticClassTransformer; +import org.apache.tapestry5.plastic.PlasticManager; /** * A service used to create proxies of varying types. As a secondary concern, manages to identify the @@ -169,6 +170,24 @@ public interface PlasticProxyFactory extends PlasticClassListenerHub * * @since 5.3.3 */ + @IncompatibleChange(release = "5.3.3", details = "Added method") void clearCache(); + + /** + * Returns the {@linkplain PlasticManager} instance used by this PlasticProxyFactory. + * @since 5.8.3 + */ + @IncompatibleChange(release = "5.8.3", details = "Added method") + PlasticManager getPlasticManager(); + + /** + * Returns the {@linkplain PlasticProxyFactory} instance to be used for a given class. + * Default implementation returns this. + * @since 5.8.3 + */ + default PlasticProxyFactory getProxyFactory(String className) + { + return this; + } } diff --git a/commons/src/main/java/org/apache/tapestry5/commons/util/DifferentClassVersionsException.java b/commons/src/main/java/org/apache/tapestry5/commons/util/DifferentClassVersionsException.java new file mode 100644 index 0000000000..ab490f71d9 --- /dev/null +++ b/commons/src/main/java/org/apache/tapestry5/commons/util/DifferentClassVersionsException.java @@ -0,0 +1,58 @@ +// Copyright 2021 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.commons.util; + +import org.apache.tapestry5.commons.internal.util.TapestryException; + +/** + * Exception used when trying to assemble a page but different versions of the same class are found. + * + * @since 5.8.3 + */ +public class DifferentClassVersionsException extends TapestryException +{ + + private static final long serialVersionUID = 1L; + + private final String className; + + private final ClassLoader classLoader1; + + private final ClassLoader classLoader2; + + public DifferentClassVersionsException(String message, String className, ClassLoader classLoader1, ClassLoader classLoader2) + { + super(message, null); + this.className = className; + this.classLoader1 = classLoader1; + this.classLoader2 = classLoader2; + } + + public String getClassName() + { + return className; + } + + public ClassLoader getClassLoader1() + { + return classLoader1; + } + + public ClassLoader getClassLoader2() + { + return classLoader2; + } + +} 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 bd595fc989..b9d29b8dec 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 String toString() return builder.toString(); } - + + /** + * Returns a copy of the values array. + * @since 5.8.3 + */ + public Object[] getValues() { + return values.clone(); + } + } diff --git a/plastic/src/main/java/org/apache/tapestry5/internal/plastic/PlasticClassLoader.java b/plastic/src/main/java/org/apache/tapestry5/internal/plastic/PlasticClassLoader.java index a4ba43cc14..85a263b759 100644 --- a/plastic/src/main/java/org/apache/tapestry5/internal/plastic/PlasticClassLoader.java +++ b/plastic/src/main/java/org/apache/tapestry5/internal/plastic/PlasticClassLoader.java @@ -14,6 +14,11 @@ package org.apache.tapestry5.internal.plastic; +import java.util.function.Function; +import java.util.function.Predicate; + +import org.apache.tapestry5.plastic.PlasticUtils; + public class PlasticClassLoader extends ClassLoader { static @@ -23,11 +28,16 @@ public class PlasticClassLoader extends ClassLoader } private final ClassLoaderDelegate delegate; - - public PlasticClassLoader(ClassLoader parent, ClassLoaderDelegate delegate) + + private Predicate filter; + + private Function> alternativeClassloading; + + private String tag; + + public PlasticClassLoader(ClassLoader parent, ClassLoaderDelegate delegate) { super(parent); - this.delegate = delegate; } @@ -41,10 +51,23 @@ protected Class loadClass(String name, boolean resolve) throws ClassNotFoundE if (loadedClass != null) return loadedClass; - if (delegate.shouldInterceptClassLoading(name)) + if (shouldInterceptClassLoading(name)) { - Class c = delegate.loadAndTransformClass(name); - + Class c = null; + if ((filter != null && filter.test(name)) || (filter == null && delegate.shouldInterceptClassLoading(name))) + { + c = delegate.loadAndTransformClass(name); + } + else if (alternativeClassloading != null) + { + c = alternativeClassloading.apply(name); + } + + if (c == null) + { + return super.loadClass(name, resolve); + } + if (resolve) resolveClass(c); @@ -56,6 +79,11 @@ protected Class loadClass(String name, boolean resolve) throws ClassNotFoundE } } + private boolean shouldInterceptClassLoading(String name) { + return delegate.shouldInterceptClassLoading( + PlasticUtils.getEnclosingClassName(name)); + } + public synchronized Class defineClassWithBytecode(String className, byte[] bytecode) { synchronized(getClassLoadingLock(className)) @@ -63,4 +91,44 @@ public synchronized Class defineClassWithBytecode(String className, byte[] by return defineClass(className, bytecode, 0, bytecode.length); } } + + /** + * When alternatingClassloader is set, this classloader delegates to it the + * call to {@linkplain ClassLoader#loadClass(String)}. If it returns a non-null object, + * it's returned by loadClass(String). Otherwise, it returns + * super.loadClass(name). + * @since 5.8.3 + */ + public void setAlternativeClassloading(Function> alternateClassloading) + { + this.alternativeClassloading = alternateClassloading; + } + + /** + * @since 5.8.3 + */ + public void setTag(String tag) + { + this.tag = tag; + } + + /** + * When a filter is set, only classes accepted by it will be loaded by this classloader. + * Instead, it will be delegated to alternate classloading first and the parent classloader + * in case the alternate doesn't handle it. + * @since 5.8.3 + */ + public void setFilter(Predicate filter) + { + this.filter = filter; + } + + @Override + public String toString() + { + final String superToString = super.toString(); + final String id = superToString.substring(superToString.indexOf('@')).trim(); + return String.format("PlasticClassLoader[%s, tag=%s, parent=%s]", id, tag, getParent()); + } + } diff --git a/plastic/src/main/java/org/apache/tapestry5/internal/plastic/PlasticClassPool.java b/plastic/src/main/java/org/apache/tapestry5/internal/plastic/PlasticClassPool.java index 8ee547f238..9daa47d518 100644 --- a/plastic/src/main/java/org/apache/tapestry5/internal/plastic/PlasticClassPool.java +++ b/plastic/src/main/java/org/apache/tapestry5/internal/plastic/PlasticClassPool.java @@ -14,22 +14,45 @@ package org.apache.tapestry5.internal.plastic; -import org.apache.tapestry5.internal.plastic.asm.ClassReader; -import org.apache.tapestry5.internal.plastic.asm.ClassWriter; -import org.apache.tapestry5.internal.plastic.asm.Opcodes; -import org.apache.tapestry5.internal.plastic.asm.tree.*; -import org.apache.tapestry5.plastic.*; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import java.io.BufferedInputStream; import java.io.IOException; import java.io.InputStream; import java.lang.annotation.Annotation; import java.lang.reflect.Modifier; -import java.util.*; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.ListIterator; +import java.util.Map; +import java.util.Set; +import java.util.Stack; import java.util.concurrent.CopyOnWriteArrayList; +import org.apache.tapestry5.internal.plastic.asm.ClassReader; +import org.apache.tapestry5.internal.plastic.asm.ClassWriter; +import org.apache.tapestry5.internal.plastic.asm.Opcodes; +import org.apache.tapestry5.internal.plastic.asm.tree.AbstractInsnNode; +import org.apache.tapestry5.internal.plastic.asm.tree.AnnotationNode; +import org.apache.tapestry5.internal.plastic.asm.tree.ClassNode; +import org.apache.tapestry5.internal.plastic.asm.tree.FieldInsnNode; +import org.apache.tapestry5.internal.plastic.asm.tree.InsnList; +import org.apache.tapestry5.internal.plastic.asm.tree.MethodInsnNode; +import org.apache.tapestry5.internal.plastic.asm.tree.MethodNode; +import org.apache.tapestry5.plastic.AnnotationAccess; +import org.apache.tapestry5.plastic.ClassInstantiator; +import org.apache.tapestry5.plastic.ClassType; +import org.apache.tapestry5.plastic.PlasticClassEvent; +import org.apache.tapestry5.plastic.PlasticClassListener; +import org.apache.tapestry5.plastic.PlasticClassListenerHub; +import org.apache.tapestry5.plastic.PlasticClassTransformation; +import org.apache.tapestry5.plastic.PlasticConstants; +import org.apache.tapestry5.plastic.PlasticManagerDelegate; +import org.apache.tapestry5.plastic.TransformationOption; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + /** * Responsible for managing a class loader that allows ASM {@link ClassNode}s * to be instantiated as runtime classes. @@ -63,6 +86,10 @@ public class PlasticClassPool implements ClassLoaderDelegate, Opcodes, PlasticCl private final StaticContext emptyStaticContext = new StaticContext(); private final List listeners = new CopyOnWriteArrayList(); + + private PlasticClassPool parent; + + private Collection children = new ArrayList<>(); private final Cache typeName2Category = new Cache() { @@ -482,9 +509,24 @@ private InternalPlasticClassTransformation createTransformation(String baseClass if (shouldInterceptClassLoading(baseClassName)) { loader.loadClass(baseClassName); + + PlasticClassPool current = this; - BaseClassDef def = baseClassDefs.get(baseClassName); - + BaseClassDef def = current.baseClassDefs.get(baseClassName); + + while (def == null && current.parent != null) + { + current = current.parent; + def = current.baseClassDefs.get(baseClassName); + } + + // Usually, when df is still null, that's because the superclass + // is a page class too + if (def == null) + { + def = findBaseClassDef(baseClassName, current); + } + assert def != null; return new PlasticClassImpl(classNode, implementationClassNode, this, def.inheritanceData, def.staticContext, proxy); @@ -495,6 +537,23 @@ private InternalPlasticClassTransformation createTransformation(String baseClass return new PlasticClassImpl(classNode, implementationClassNode, this, emptyInheritanceData, emptyStaticContext, proxy); } + private BaseClassDef findBaseClassDef(String baseClassName, PlasticClassPool plasticClassPool) + { + BaseClassDef def = plasticClassPool.baseClassDefs.get(baseClassName); + if (def == null) + { + for (PlasticClassPool child : plasticClassPool.children) + { + def = child.findBaseClassDef(baseClassName, child); + if (def != null) + { + break; + } + } + } + return def; + } + /** * Constructs a class node by reading the raw bytecode for a class and instantiating a ClassNode * (via {@link ClassReader#accept(org.apache.tapestry5.internal.plastic.asm.ClassVisitor, int)}). @@ -669,6 +728,16 @@ public void removePlasticClassListener(PlasticClassListener listener) listeners.remove(listener); } + + /** + * Sets the parent of this instance. Only used to look up baseClassDefs. + * @since 5.8.3 + */ + public void setParent(PlasticClassPool parent) + { + this.parent = parent; + parent.children.add(this); + } boolean isEnabled(TransformationOption option) { diff --git a/plastic/src/main/java/org/apache/tapestry5/plastic/PlasticManager.java b/plastic/src/main/java/org/apache/tapestry5/plastic/PlasticManager.java index 74acf21c10..49249db25e 100644 --- a/plastic/src/main/java/org/apache/tapestry5/plastic/PlasticManager.java +++ b/plastic/src/main/java/org/apache/tapestry5/plastic/PlasticManager.java @@ -345,4 +345,24 @@ public void removePlasticClassListener(PlasticClassListener listener) { pool.removePlasticClassListener(listener); } + + /** + * Returns whether a given class will have it classloading intercepted for + * live class reloading. + * @since 5.8.3 + */ + public boolean shouldInterceptClassLoading(String className) + { + return pool.shouldInterceptClassLoading(className); + } + + /** + * Returns the Plastic class pool used by this instance. + * @since 5.8.3 + */ + public PlasticClassPool getPool() + { + return pool; + } + } diff --git a/plastic/src/main/java/org/apache/tapestry5/plastic/PlasticUtils.java b/plastic/src/main/java/org/apache/tapestry5/plastic/PlasticUtils.java index 6ed424e30c..2fab09efd8 100644 --- a/plastic/src/main/java/org/apache/tapestry5/plastic/PlasticUtils.java +++ b/plastic/src/main/java/org/apache/tapestry5/plastic/PlasticUtils.java @@ -141,4 +141,14 @@ public static boolean isPrimitive(String typeName) { return PrimitiveType.getByName(typeName) != null; } + + /** + * If the given class is an inner class, returns the enclosing class. + * Otherwise, returns the class name unchanged. + */ + public static String getEnclosingClassName(String className) + { + int index = className.indexOf('$'); + return index <= 0 ? className : className.substring(0, index); + } } diff --git a/plastic/src/test/groovy/org/apache/tapestry5/internal/plastic/PlasticUtilsTests.groovy b/plastic/src/test/groovy/org/apache/tapestry5/internal/plastic/PlasticUtilsTests.groovy index 8a2f4ea515..801f74cf2c 100644 --- a/plastic/src/test/groovy/org/apache/tapestry5/internal/plastic/PlasticUtilsTests.groovy +++ b/plastic/src/test/groovy/org/apache/tapestry5/internal/plastic/PlasticUtilsTests.groovy @@ -155,4 +155,21 @@ class PlasticUtilsTests extends Specification "int" | true "java.lang.Integer" | false } + + def "getEnclosingClass #name should be #expected"() + { + + expect: + + PlasticUtils.getEnclosingClassName(name) == expected + + where: + + name | expected + + "org.apache.tapestry5.corelib.components.PropertyEditor" | "org.apache.tapestry5.corelib.components.PropertyEditor" + "org.apache.tapestry5.corelib.components.PropertyEditor\$CleanupEnvironment" | "org.apache.tapestry5.corelib.components.PropertyEditor" + + } + } diff --git a/tapestry-beanvalidator/src/test/java/org/apache/tapestry5/beanvalidator/integration/TapestryBeanValidationIntegrationTests.java b/tapestry-beanvalidator/src/test/java/org/apache/tapestry5/beanvalidator/integration/TapestryBeanValidationIntegrationTests.java index 6e945fcfd4..f89eb9953c 100644 --- a/tapestry-beanvalidator/src/test/java/org/apache/tapestry5/beanvalidator/integration/TapestryBeanValidationIntegrationTests.java +++ b/tapestry-beanvalidator/src/test/java/org/apache/tapestry5/beanvalidator/integration/TapestryBeanValidationIntegrationTests.java @@ -110,6 +110,7 @@ public void form_validation() throws Exception public void beaneditform_validation() throws Exception { openLinks("BeanEditForm Validation Demo"); + openLinks("BeanEditForm Validation Demo");// TODO: remove this // Ugly hack to fix the "Unable to locate element: //input[@type='submit']" error. // I have no idea why it's failing here but not in other tests and pages. @@ -143,6 +144,8 @@ public void client_validation() throws Exception { openLinks("Client Validation Demo"); + Thread.sleep(2000); // For some reason, without this sleep, the submit button isn't found + //@NotNull click(SUBMIT); diff --git a/tapestry-beanvalidator/src/test/webapp/RadioGroupWithValidation.tml b/tapestry-beanvalidator/src/test/webapp/RadioGroupWithValidation.tml index f1123a14c8..27fe900b18 100644 --- a/tapestry-beanvalidator/src/test/webapp/RadioGroupWithValidation.tml +++ b/tapestry-beanvalidator/src/test/webapp/RadioGroupWithValidation.tml @@ -20,11 +20,11 @@

test 2

- + 1
2
3
-
+
diff --git a/tapestry-core/.gitignore b/tapestry-core/.gitignore index f739972ea5..2483330532 100644 --- a/tapestry-core/.gitignore +++ b/tapestry-core/.gitignore @@ -2,3 +2,4 @@ docs /.externalToolBuilders src/main/generated src/test/generated +/tapestryComponentDependencies.json diff --git a/tapestry-core/src/main/coffeescript/META-INF/modules/t5/core/graphviz.coffee b/tapestry-core/src/main/coffeescript/META-INF/modules/t5/core/graphviz.coffee new file mode 100644 index 0000000000..e1fbe3ee2f --- /dev/null +++ b/tapestry-core/src/main/coffeescript/META-INF/modules/t5/core/graphviz.coffee @@ -0,0 +1,29 @@ +# Copyright 2023 The Apache Software Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http:#www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# ## t5/core/graphviz +# +# Support to the core/Graphviz Tapestry component. +define ["https://cdn.jsdelivr.net/npm/@hpcc-js/wasm/dist/graphviz.umd.js"], + (hpccWasm) -> + render = (value, id, showDownloadLink) -> + hpccWasm.Graphviz.load().then (graphviz) -> + svg = graphviz.dot value + div = document.getElementById id + layout = graphviz.layout(value, "svg", "dot") + div.innerHTML = layout + if showDownloadLink + link = document.getElementById (id + "-download") + link.setAttribute "href", "data:image/svg+xml;charset=utf-8," + encodeURIComponent(layout) + return render diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/SymbolConstants.java b/tapestry-core/src/main/java/org/apache/tapestry5/SymbolConstants.java index d761905fbc..ca3b542f8d 100644 --- a/tapestry-core/src/main/java/org/apache/tapestry5/SymbolConstants.java +++ b/tapestry-core/src/main/java/org/apache/tapestry5/SymbolConstants.java @@ -33,6 +33,7 @@ import org.apache.tapestry5.services.assets.ResourceMinimizer; import org.apache.tapestry5.services.compatibility.Trait; import org.apache.tapestry5.services.javascript.JavaScriptStack; +import org.apache.tapestry5.services.pageload.PageClassLoaderContextManager; import org.apache.tapestry5.services.rest.OpenApiDescriptionGenerator; /** @@ -780,5 +781,14 @@ public class SymbolConstants * @since 5.8.2 */ public static final String CORS_MAX_AGE = TapestryHttpSymbolConstants.CORS_MAX_AGE; + + /** + * Defines whether multiple classloaders will be used instead of one for smarter page invalidation. + * This is ignored when in production mode. + * Default value is false. + * @see PageClassLoaderContextManager + * @since 5.8.3 + */ + public static final String MULTIPLE_CLASSLOADERS = "tapestry.multiple-classloaders"; } diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/corelib/components/Graphviz.java b/tapestry-core/src/main/java/org/apache/tapestry5/corelib/components/Graphviz.java new file mode 100644 index 0000000000..8fd74f4c81 --- /dev/null +++ b/tapestry-core/src/main/java/org/apache/tapestry5/corelib/components/Graphviz.java @@ -0,0 +1,104 @@ +// Copyright 2023 The Apache Software Foundation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package org.apache.tapestry5.corelib.components; + +import org.apache.tapestry5.BindingConstants; +import org.apache.tapestry5.ComponentResources; +import org.apache.tapestry5.MarkupWriter; +import org.apache.tapestry5.annotations.Environmental; +import org.apache.tapestry5.annotations.Parameter; +import org.apache.tapestry5.annotations.Property; +import org.apache.tapestry5.commons.Messages; +import org.apache.tapestry5.ioc.annotations.Inject; +import org.apache.tapestry5.services.ajax.AjaxResponseRenderer; +import org.apache.tapestry5.services.javascript.JavaScriptSupport; + +/** + * Component that renders a Graphviz graph using + * @hpcc-js/wasm. It's mostly + * intended to be used internally at Tapestry, hence the lack of options. + * + * @tapestrydoc + */ +public class Graphviz +{ + + /** + * A Graphviz graph described in its DOT language. + */ + @Parameter(required = true, allowNull = false) + @Property + private String value; + + /** + * Defines whether a link to download the graph as an SVG file should be provided. + */ + @Parameter(defaultPrefix = BindingConstants.LITERAL, value = "false") + private boolean showDownloadLink; + + /** + * Defines whether a the Graphviz source should be shown. + */ + @Parameter(defaultPrefix = BindingConstants.LITERAL, value = "false") + private boolean showSource; + + @Environmental + private JavaScriptSupport javaScriptSupport; + + @Inject + private AjaxResponseRenderer ajaxResponseRenderer; + + @Inject + private ComponentResources resources; + + @Inject + private Messages messages; + + // Read value only once if showSource = true + private String cachedValue; + + void setupRender(MarkupWriter writer) + { + + cachedValue = value; + String elementName = resources.getElementName(); + if (elementName == null) + { + elementName = "div"; + } + + final String id = javaScriptSupport.allocateClientId(resources); + writer.element(elementName, "id", id); + writer.end(); + + javaScriptSupport.require("t5/core/graphviz").with(cachedValue, id, showDownloadLink); + + if (showDownloadLink) + { + writer.element("a", "href", "#", "id", id + "-download", "download", id + ".svg"); + writer.write(messages.get("download-graphviz-image")); + writer.end(); + } + + if (showSource) + { + writer.element("pre", "id", id + "-source"); + writer.write(cachedValue); + writer.end(); + } + + } + +} diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/corelib/pages/ExceptionReport.java b/tapestry-core/src/main/java/org/apache/tapestry5/corelib/pages/ExceptionReport.java index ced7b69600..f11d805488 100644 --- a/tapestry-core/src/main/java/org/apache/tapestry5/corelib/pages/ExceptionReport.java +++ b/tapestry-core/src/main/java/org/apache/tapestry5/corelib/pages/ExceptionReport.java @@ -20,6 +20,7 @@ import org.apache.tapestry5.annotations.Property; import org.apache.tapestry5.annotations.UnknownActivationContextCheck; import org.apache.tapestry5.beanmodel.services.*; +import org.apache.tapestry5.commons.services.InvalidationEventHub; import org.apache.tapestry5.commons.util.CollectionFactory; import org.apache.tapestry5.corelib.base.AbstractInternalPage; import org.apache.tapestry5.func.F; @@ -33,6 +34,7 @@ import org.apache.tapestry5.internal.TapestryInternalUtils; import org.apache.tapestry5.internal.services.PageActivationContextCollector; import org.apache.tapestry5.internal.services.ReloadHelper; +import org.apache.tapestry5.ioc.annotations.ComponentClasses; import org.apache.tapestry5.ioc.annotations.Inject; import org.apache.tapestry5.ioc.annotations.Symbol; import org.apache.tapestry5.ioc.internal.util.InternalUtils; @@ -42,6 +44,8 @@ import java.net.MalformedURLException; import java.net.URL; +import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.regex.Pattern; @@ -115,6 +119,10 @@ public class ExceptionReport extends AbstractInternalPage implements ExceptionRe @Inject private ComponentResources resources; + + @Inject + @ComponentClasses + private InvalidationEventHub classesInvalidationHub; private String failurePage; @@ -242,6 +250,8 @@ public List getActionLinks() Object onReloadFirst(EventContext reloadContext) { + + classesInvalidationHub.fireInvalidationEvent(Collections.emptyList()); reloadHelper.forceReload(); return linkSource.createPageRenderLinkWithContext(urlEncoder.decode(request.getParameter("loadPage")), reloadContext); 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 abaedb62d0..a8472be8a8 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,30 +14,55 @@ package org.apache.tapestry5.corelib.pages; +import java.util.ArrayList; +import java.util.Arrays; +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.SymbolConstants; 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.services.InvalidationEventHub; 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.ComponentDependencyGraphvizGenerator; +import org.apache.tapestry5.internal.services.ComponentDependencyRegistry; +import org.apache.tapestry5.internal.services.ComponentDependencyRegistry.DependencyType; 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.ComponentClasses; 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.ajax.AjaxResponseRenderer; +import org.apache.tapestry5.services.javascript.JavaScriptSupport; import org.apache.tapestry5.services.pageload.ComponentResourceSelector; - -import java.util.Collection; -import java.util.List; -import java.util.Set; +import org.apache.tapestry5.services.pageload.PageClassLoaderContextManager; /** * Lists out the currently loaded pages, using a {@link org.apache.tapestry5.corelib.components.Grid}. @@ -55,6 +80,11 @@ public class PageCatalog @Inject @Symbol(TapestryHttpSymbolConstants.PRODUCTION_MODE) private boolean productionMode; + + @Property + @Inject + @Symbol(SymbolConstants.MULTIPLE_CLASSLOADERS) + private boolean multipleClassLoaders; @Inject private PageSource pageSource; @@ -64,6 +94,9 @@ public class PageCatalog @Inject private ComponentClassResolver resolver; + + @Inject + private ComponentDependencyRegistry componentDependencyRegistry; @Inject private AlertManager alertManager; @@ -71,8 +104,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 failures; @@ -90,13 +132,35 @@ public class PageCatalog @Inject private BeanModelSource beanModelSource; - + @Inject private Messages messages; @Property public static BeanModel model; + @Inject + private Request request; + + @Inject + @ComponentClasses + private InvalidationEventHub classesInvalidationEventHub; + + @Inject + private JavaScriptSupport javaScriptSupport; + + @Inject + private ComponentDependencyGraphvizGenerator componentDependencyGraphvizGenerator; + + @Inject + private ComponentClassResolver componentClassResolver; + + @Inject + private AjaxResponseRenderer ajaxResponseRenderer; + + @Inject + private PageClassLoaderContextManager pageClassLoaderContextManager; + void pageLoaded() { model = beanModelSource.createDisplayModel(Page.class, messages); @@ -105,6 +169,7 @@ void pageLoaded() model.addExpression("assemblyTime", "stats.assemblyTime"); model.addExpression("componentCount", "stats.componentCount"); model.addExpression("weight", "stats.weight"); + model.add("clear", null); model.reorder("name", "selector", "assemblyTime", "componentCount", "weight"); } @@ -153,7 +218,21 @@ public Collection getPages() { return pageSource.getAllPages(); } - + + void onActionFromPreloadPageClassLoaderContexts() + { + pageClassLoaderContextManager.preload(); + } + + Object onClearPage(String className) + { + final String logicalName = resolver.getLogicalName(className); + classesInvalidationEventHub.fireInvalidationEvent(Arrays.asList(className)); + alertManager.warn(String.format("Page %s (%s) has been cleared from the page cache", + className, logicalName)); + return pagesZone.getBody(); + } + Object onSuccessFromSinglePageLoad() { boolean found = !F.flow(getPages()).filter(new Predicate() @@ -270,6 +349,19 @@ Object onActionFromClearCaches() return pagesZone.getBody(); } + + Object onActionFromStoreDependencyInformation() + { + + componentDependencyRegistry.writeFile(); + + alertManager.warn(String.format( + "Component dependency information written to %s.", + ComponentDependencyRegistry.FILENAME)); + + return pagesZone.getBody(); + + } Object onActionFromRunGC() { @@ -291,4 +383,110 @@ public String formatElapsed(double millis) { return String.format("%,.3f ms", millis); } + + public List getDependencies() + { + final String selectedPageClassName = getSelectedPageClassName(); + List dependencies = new ArrayList<>(); + dependencies.addAll( + componentDependencyRegistry.getDependencies(selectedPageClassName, DependencyType.USAGE)); + dependencies.addAll( + componentDependencyRegistry.getDependencies(selectedPageClassName, DependencyType.INJECT_PAGE)); + dependencies.addAll( + componentDependencyRegistry.getDependencies(selectedPageClassName, DependencyType.SUPERCLASS)); + Collections.sort(dependencies); + return dependencies; + } + + public void onPageStructure(String pageName) + { + selectedPage = pageSource.getPage(pageName); + ajaxResponseRenderer.addRender("pageStructureZone", pageStructureZone.getBody()); + } + + 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 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); + } + + public String getGraphvizValue() + { + return componentDependencyGraphvizGenerator.generate(getClassName(selectedPage)); + } + } diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/corelib/pages/PageDependencyGraph.java b/tapestry-core/src/main/java/org/apache/tapestry5/corelib/pages/PageDependencyGraph.java new file mode 100644 index 0000000000..46a86222ab --- /dev/null +++ b/tapestry-core/src/main/java/org/apache/tapestry5/corelib/pages/PageDependencyGraph.java @@ -0,0 +1,48 @@ +// Copyright 2023 The Apache Software Foundation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package org.apache.tapestry5.corelib.pages; + +import java.util.Set; +import java.util.stream.Collectors; + +import org.apache.tapestry5.annotations.UnknownActivationContextCheck; +import org.apache.tapestry5.annotations.WhitelistAccessOnly; +import org.apache.tapestry5.internal.services.ComponentDependencyGraphvizGenerator; +import org.apache.tapestry5.internal.services.ComponentDependencyRegistry; +import org.apache.tapestry5.ioc.annotations.Inject; + +/** + * Shows graph showing the dependencies of all already loaded pages and its compnoents, mixins and base classes. + */ +@UnknownActivationContextCheck(false) +@WhitelistAccessOnly +public class PageDependencyGraph +{ + + @Inject + private ComponentDependencyGraphvizGenerator componentDependencyGraphvizGenerator; + + @Inject + private ComponentDependencyRegistry componentDependencyRegistry; + + public String getGraphvizValue() + { + final Set pages = componentDependencyRegistry.getClassNames().stream() + .filter(c -> c.contains(".pages")) + .collect(Collectors.toSet()); + return componentDependencyGraphvizGenerator.generate(pages.toArray(new String[pages.size()])); + } + +} diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/internal/bindings/PropBindingFactory.java b/tapestry-core/src/main/java/org/apache/tapestry5/internal/bindings/PropBindingFactory.java index 4fe2bf14a4..0d95b52547 100644 --- a/tapestry-core/src/main/java/org/apache/tapestry5/internal/bindings/PropBindingFactory.java +++ b/tapestry-core/src/main/java/org/apache/tapestry5/internal/bindings/PropBindingFactory.java @@ -18,7 +18,10 @@ import org.apache.tapestry5.beanmodel.services.PropertyConduitSource; import org.apache.tapestry5.commons.Location; import org.apache.tapestry5.commons.internal.services.StringInterner; +import org.apache.tapestry5.commons.internal.util.TapestryException; import org.apache.tapestry5.services.BindingFactory; +import org.apache.tapestry5.services.pageload.PageClassLoaderContext; +import org.apache.tapestry5.services.pageload.PageClassLoaderContextManager; /** * Binding factory for reading and updating JavaBean properties. @@ -31,18 +34,24 @@ public class PropBindingFactory implements BindingFactory private final PropertyConduitSource source; private final StringInterner interner; + + private final PageClassLoaderContextManager pageClassLoaderContextManager; - public PropBindingFactory(PropertyConduitSource propertyConduitSource, StringInterner interner) + public PropBindingFactory(PropertyConduitSource propertyConduitSource, StringInterner interner, + PageClassLoaderContextManager pageClassLoaderContextManager) { source = propertyConduitSource; this.interner = interner; + this.pageClassLoaderContextManager = pageClassLoaderContextManager; } public Binding newBinding(String description, ComponentResources container, ComponentResources component, String expression, Location location) { + Object target = container.getComponent(); Class targetClass = target.getClass(); + targetClass = getClassLoaderAppropriateClass(targetClass); PropertyConduit conduit = source.create(targetClass, expression); @@ -51,4 +60,19 @@ public Binding newBinding(String description, ComponentResources container, return new PropBinding(location, target, conduit, expression, toString); } + + private Class getClassLoaderAppropriateClass(Class targetClass) + { + final String className = targetClass.getName(); + try + { + final PageClassLoaderContext context = pageClassLoaderContextManager.get(className); + targetClass = context.getProxyFactory() + .getClassLoader().loadClass(className); + } catch (ClassNotFoundException e) + { + throw new TapestryException(e.getMessage(), e); + } + return targetClass; + } } 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 b8e4586164..a18df83d87 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,20 @@ package org.apache.tapestry5.internal.event; +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; +import java.util.stream.Collectors; + +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.List; -import java.util.Map; +import org.slf4j.Logger; /** * Base implementation class for classes (especially services) that need to manage a list of @@ -27,9 +35,11 @@ */ public class InvalidationEventHubImpl implements InvalidationEventHub { - private final List callbacks; - - protected InvalidationEventHubImpl(boolean productionMode) + private final List, List>> callbacks; + + private final Logger logger; + + protected InvalidationEventHubImpl(boolean productionMode, Logger logger) { if (productionMode) { @@ -38,25 +48,61 @@ protected InvalidationEventHubImpl(boolean productionMode) { callbacks = CollectionFactory.newThreadSafeList(); } + this.logger = logger; } /** * Notifies all listeners/callbacks. */ protected final void fireInvalidationEvent() + { + fireInvalidationEvent(Collections.emptyList()); + } + + /** + * Notifies all listeners/callbacks. + */ + public final void fireInvalidationEvent(List resources) { if (callbacks == null) { return; } - - for (Runnable callback : callbacks) + + final Set alreadyProcessed = new HashSet<>(); + + int level = 1; + do { - callback.run(); + final Set extraResources = new HashSet<>(); + Set 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> callback : callbacks) + { + final List newResources = callback.apply(resources); + if (newResources == null) { + throw new TapestryException("InvalidationEventHub callback functions cannot return null", null); + } + actuallyNewResources = newResources.stream() + .filter(r -> !alreadyProcessed.contains(r)) + .collect(Collectors.toSet()); + extraResources.addAll(actuallyNewResources); + alreadyProcessed.addAll(newResources); + } + level++; + 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 +110,10 @@ public final void addInvalidationCallback(Runnable callback) // ignore the callback. if (callbacks != null) { - callbacks.add(callback); + callbacks.add((r) -> { + callback.run(); + return Collections.emptyList(); + }); } } @@ -94,4 +143,13 @@ public void run() }); } + @Override + public void addInvalidationCallback(Function, List> callback) + { + if (callbacks != null) + { + callbacks.add(callback); + } + } + } diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/internal/pageload/PageLoaderImpl.java b/tapestry-core/src/main/java/org/apache/tapestry5/internal/pageload/PageLoaderImpl.java index 781ca73659..7a2233247c 100644 --- a/tapestry-core/src/main/java/org/apache/tapestry5/internal/pageload/PageLoaderImpl.java +++ b/tapestry-core/src/main/java/org/apache/tapestry5/internal/pageload/PageLoaderImpl.java @@ -12,12 +12,16 @@ package org.apache.tapestry5.internal.pageload; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + import org.apache.tapestry5.Binding; import org.apache.tapestry5.BindingConstants; import org.apache.tapestry5.ComponentResources; import org.apache.tapestry5.MarkupWriter; -import org.apache.tapestry5.beanmodel.internal.services.*; -import org.apache.tapestry5.beanmodel.services.*; import org.apache.tapestry5.commons.Location; import org.apache.tapestry5.commons.internal.services.StringInterner; import org.apache.tapestry5.commons.internal.util.TapestryException; @@ -26,18 +30,36 @@ import org.apache.tapestry5.commons.util.CollectionFactory; import org.apache.tapestry5.commons.util.Stack; import org.apache.tapestry5.commons.util.UnknownValueException; -import org.apache.tapestry5.http.services.RequestGlobals; import org.apache.tapestry5.internal.InternalComponentResources; import org.apache.tapestry5.internal.InternalConstants; import org.apache.tapestry5.internal.bindings.LiteralBinding; -import org.apache.tapestry5.internal.parser.*; +import org.apache.tapestry5.internal.parser.AttributeToken; +import org.apache.tapestry5.internal.parser.BlockToken; +import org.apache.tapestry5.internal.parser.CDATAToken; +import org.apache.tapestry5.internal.parser.CommentToken; +import org.apache.tapestry5.internal.parser.ComponentTemplate; +import org.apache.tapestry5.internal.parser.DTDToken; +import org.apache.tapestry5.internal.parser.DefineNamespacePrefixToken; +import org.apache.tapestry5.internal.parser.ExpansionToken; +import org.apache.tapestry5.internal.parser.ExtensionPointToken; +import org.apache.tapestry5.internal.parser.ParameterToken; +import org.apache.tapestry5.internal.parser.StartComponentToken; +import org.apache.tapestry5.internal.parser.StartElementToken; +import org.apache.tapestry5.internal.parser.TemplateToken; +import org.apache.tapestry5.internal.parser.TextToken; +import org.apache.tapestry5.internal.parser.TokenType; import org.apache.tapestry5.internal.services.ComponentInstantiatorSource; import org.apache.tapestry5.internal.services.ComponentTemplateSource; import org.apache.tapestry5.internal.services.Instantiator; import org.apache.tapestry5.internal.services.PageElementFactory; import org.apache.tapestry5.internal.services.PageLoader; import org.apache.tapestry5.internal.services.PersistentFieldManager; -import org.apache.tapestry5.internal.structure.*; +import org.apache.tapestry5.internal.structure.BlockImpl; +import org.apache.tapestry5.internal.structure.ComponentPageElement; +import org.apache.tapestry5.internal.structure.ComponentPageElementResources; +import org.apache.tapestry5.internal.structure.ComponentPageElementResourcesSource; +import org.apache.tapestry5.internal.structure.Page; +import org.apache.tapestry5.internal.structure.PageImpl; import org.apache.tapestry5.ioc.Invokable; import org.apache.tapestry5.ioc.OperationTracker; import org.apache.tapestry5.ioc.annotations.ComponentClasses; @@ -55,10 +77,6 @@ import org.apache.tapestry5.services.pageload.ComponentResourceSelector; import org.slf4j.Logger; -import java.util.Collections; -import java.util.List; -import java.util.Map; - /** * There's still a lot of room to beef up {@link org.apache.tapestry5.internal.pageload.ComponentAssembler} and * {@link org.apache.tapestry5.internal.pageload.EmbeddedComponentAssembler} to perform more static analysis, but @@ -157,13 +175,11 @@ public String toString() private final MetaDataLocator metaDataLocator; - private final RequestGlobals requestGlobals; - public PageLoaderImpl(ComponentInstantiatorSource instantiatorSource, ComponentTemplateSource templateSource, PageElementFactory elementFactory, ComponentPageElementResourcesSource resourcesSource, ComponentClassResolver componentClassResolver, PersistentFieldManager persistentFieldManager, StringInterner interner, OperationTracker tracker, PerthreadManager perThreadManager, - Logger logger, MetaDataLocator metaDataLocator, RequestGlobals requestGlobals) + Logger logger, MetaDataLocator metaDataLocator) { this.instantiatorSource = instantiatorSource; this.templateSource = templateSource; @@ -176,7 +192,6 @@ public PageLoaderImpl(ComponentInstantiatorSource instantiatorSource, ComponentT this.perThreadManager = perThreadManager; this.logger = logger; this.metaDataLocator = metaDataLocator; - this.requestGlobals = requestGlobals; } @PostInjection @@ -184,9 +199,38 @@ public void setupInvalidation(@ComponentClasses InvalidationEventHub classesHub, @ComponentTemplates InvalidationEventHub templatesHub, @ComponentMessages InvalidationEventHub messagesHub) { - classesHub.clearOnInvalidation(cache); - templatesHub.clearOnInvalidation(cache); - messagesHub.clearOnInvalidation(cache); + classesHub.addInvalidationCallback(this::listen); + templatesHub.addInvalidationCallback(this::listen); + messagesHub.addInvalidationCallback(this::listen); + } + + private List listen(List resources) + { + + if (resources.isEmpty()) + { + cache.clear(); + } + else + { + + final Iterator> iterator = cache.entrySet().iterator(); + + while (iterator.hasNext()) + { + final Entry entry = iterator.next(); + for (String resource : resources) + { + if (resource.equals(entry.getKey().className)) + { + iterator.remove(); + } + } + } + + } + + return Collections.emptyList(); } public void clearCache() diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/internal/renderers/RequestRenderer.java b/tapestry-core/src/main/java/org/apache/tapestry5/internal/renderers/RequestRenderer.java index b5ebd0c2df..289889c08c 100644 --- a/tapestry-core/src/main/java/org/apache/tapestry5/internal/renderers/RequestRenderer.java +++ b/tapestry-core/src/main/java/org/apache/tapestry5/internal/renderers/RequestRenderer.java @@ -15,16 +15,26 @@ package org.apache.tapestry5.internal.renderers; import org.apache.tapestry5.MarkupWriter; +import org.apache.tapestry5.SymbolConstants; import org.apache.tapestry5.commons.util.CollectionFactory; import org.apache.tapestry5.http.TapestryHttpSymbolConstants; import org.apache.tapestry5.http.services.Context; import org.apache.tapestry5.http.services.Request; +import org.apache.tapestry5.internal.services.PageSource; +import org.apache.tapestry5.internal.structure.Page; import org.apache.tapestry5.ioc.annotations.Primary; import org.apache.tapestry5.ioc.annotations.Symbol; import org.apache.tapestry5.ioc.internal.util.InternalUtils; +import org.apache.tapestry5.services.ComponentClassResolver; import org.apache.tapestry5.services.ObjectRenderer; +import org.apache.tapestry5.services.pageload.PageClassLoaderContext; +import org.apache.tapestry5.services.pageload.PageClassLoaderContextManager; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; import java.util.List; +import java.util.stream.Collectors; public class RequestRenderer implements ObjectRenderer { @@ -33,12 +43,31 @@ public class RequestRenderer implements ObjectRenderer private final String contextPath; private final ObjectRenderer masterObjectRenderer; - - public RequestRenderer(@Primary ObjectRenderer masterObjectRenderer, Context context, @Symbol(TapestryHttpSymbolConstants.CONTEXT_PATH) String contextPath) + + private final boolean productionMode; + + private final PageClassLoaderContextManager pageClassLoaderContextManager; + + private final PageSource pageSource; + + private final ComponentClassResolver componentClassResolver; + + public RequestRenderer( + @Primary ObjectRenderer masterObjectRenderer, + Context context, + @Symbol(TapestryHttpSymbolConstants.CONTEXT_PATH) String contextPath, + @Symbol(SymbolConstants.PRODUCTION_MODE) boolean productionMode, + PageClassLoaderContextManager pageClassLoaderContextManager, + PageSource pageSource, + ComponentClassResolver componentClassResolver) { this.masterObjectRenderer = masterObjectRenderer; this.context = context; this.contextPath = contextPath; + this.productionMode = productionMode; + this.pageClassLoaderContextManager = pageClassLoaderContextManager; + this.pageSource = pageSource; + this.componentClassResolver = componentClassResolver; } public void render(Request request, MarkupWriter writer) @@ -48,6 +77,9 @@ public void render(Request request, MarkupWriter writer) headers(request, writer); attributes(request, writer); context(writer); + +// pageClassloaderContext(writer); +// pages(writer); } private void coreProperties(Request request, MarkupWriter writer) @@ -243,4 +275,89 @@ private void attributes(Request request, MarkupWriter writer) writer.end(); // dl } +// private void pageClassloaderContext(MarkupWriter writer) +// { +// if (!productionMode) +// { +// section(writer, "Page Classloader Context"); +// writer.element("ul"); +// render(pageClassLoaderContextManager.getRoot(), writer); +// writer.end(); // ul +// } +// } +// +// private void render(PageClassloaderContext context, MarkupWriter writer) +// { +// if (context != null) +// { +// +// writer.element("li"); +// +// writer.element("p"); +// writer.element("em"); +// writer.write(context.getName()); +// writer.write(", "); +// writer.write(context.getClassLoader().toString()); +// writer.end(); // em +// writer.end(); // p +// +// writer.element("p"); +// writer.write(context.getClassNames().stream().collect(Collectors.joining(", "))); +// writer.end(); // p +// +// if (!context.getChildren().isEmpty()) +// { +// writer.element("ul"); +// for (PageClassloaderContext child : context.getChildren()) +// { +// render(child, writer); +// } +// writer.end(); // ul +// } +// writer.end(); // li +// +// } +// +// } +// +// private void pages(MarkupWriter writer) +// { +// if (!productionMode) +// { +// section(writer, "Pages"); +// writer.element("table", "class", "table table-condensed table-hover table-striped exception-report-threads"); +// writer.element("thead"); +// +// writer.element("td"); +// writer.write("Name"); +// writer.end(); //td Name +// +// writer.element("td"); +// writer.write("Context"); +// writer.end(); //td Context +// +// writer.end(); // thead +// +// writer.element("tbody"); +// +// List pages = new ArrayList<>(pageSource.getAllPages()); +// Collections.sort(pages, Comparator.comparing(Page::getName)); +// +// for (Page page : pages) { +// writer.element("tr"); +// writer.element("td"); +// writer.write(page.getName()); +// writer.end(); // td +// writer.element("td"); +// writer.write(pageClassLoaderContextManager.getRoot().findByClassName(componentClassResolver.getClassName(page.getName())).toString()); +// writer.end(); // td +// writer.end(); // tr +// } +// +// writer.end(); // tbody +// +// writer.end(); // table +// } +// } + } diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/AssetSourceImpl.java b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/AssetSourceImpl.java index d83cfb1899..db63dcd2d8 100644 --- a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/AssetSourceImpl.java +++ b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/AssetSourceImpl.java @@ -17,9 +17,12 @@ import java.net.MalformedURLException; import java.net.URL; import java.util.Arrays; +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.concurrent.atomic.AtomicBoolean; import org.apache.tapestry5.Asset; @@ -109,7 +112,23 @@ public AssetSourceImpl(ThreadLocale threadLocale, @PostInjection public void clearCacheWhenResourcesChange(ResourceChangeTracker tracker) { - tracker.clearOnInvalidation(cache); + tracker.addInvalidationCallback(this::invalidate); + } + + private List invalidate(List resources) + { + final Iterator>> iterator = cache.entrySet().iterator(); + for (String resource : resources) + { + while (iterator.hasNext()) + { + if (iterator.next().getKey().toString().equals(resource)) + { + iterator.remove(); + } + } + } + return Collections.emptyList(); } public Asset getClasspathAsset(String path) diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ClassNameHolder.java b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ClassNameHolder.java new file mode 100644 index 0000000000..832e50aaf7 --- /dev/null +++ b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ClassNameHolder.java @@ -0,0 +1,30 @@ +// 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.ioc.internal.util.URLChangeTracker; + +/** + * Interface that defines types who provide a class name for {@linkplain URLChangeTracker} purposes. + * + * @since 5.8.3 + */ +public interface ClassNameHolder +{ + /** + * Returns the class name associated with a given resource tracked by {@link URLChangeTracker}. + */ + String getClassName(); +} diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ComponentClassCacheImpl.java b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ComponentClassCacheImpl.java index 49655e5a36..504f612ee7 100644 --- a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ComponentClassCacheImpl.java +++ b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ComponentClassCacheImpl.java @@ -24,7 +24,11 @@ import org.apache.tapestry5.ioc.annotations.ComponentLayer; import org.apache.tapestry5.ioc.annotations.PostInjection; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; import java.util.Map; +import java.util.Map.Entry; public class ComponentClassCacheImpl implements ComponentClassCache { @@ -43,7 +47,33 @@ public ComponentClassCacheImpl(@ComponentLayer @PostInjection public void setupInvalidation(@ComponentClasses InvalidationEventHub hub) { - hub.clearOnInvalidation(cache); + hub.addInvalidationCallback(this::listen);; + } + + @SuppressWarnings("rawtypes") + private List listen(List resources) + { + + if (resources.isEmpty()) + { + cache.clear(); + } + else { + + final Iterator> iterator = cache.entrySet().iterator(); + + while (iterator.hasNext()) + { + final Entry entry = iterator.next(); + if (resources.contains(entry.getKey())) + { + iterator.remove(); + } + } + + } + + return Collections.emptyList(); } @SuppressWarnings("unchecked") @@ -75,7 +105,7 @@ public Class forName(String className) private Class lookupClassForType(String className) { - ClassLoader componentLoader = plasticFactory.getClassLoader(); + ClassLoader componentLoader = plasticFactory.getProxyFactory(className).getClassLoader(); try { return PlasticInternalUtils.toClass(componentLoader, className); diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ComponentClassResolverImpl.java b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ComponentClassResolverImpl.java index 8ba9c0a3df..77651e95cc 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.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,47 @@ public Collection getLibraryMappings() return libraryMappings; } + @Override + public String getLogicalName(String className) + { + final Data thisData = getData(); + String result = thisData.pageClassNameToLogicalName.get(className); + if (result == null) + { + result = getKeyByValue(thisData.componentToClassName, className); + } + if (result == null ) + { + result = getKeyByValue(thisData.mixinToClassName, className); + } + + return result; + } + + @Override + public String getClassName(String logicalName) + { + final Data thisData = getData(); + String result = getKeyByValue(thisData.pageClassNameToLogicalName, logicalName); + if (result == null) + { + result = thisData.componentToClassName.get(logicalName); + } + if (result == null ) + { + result = thisData.mixinToClassName.get(logicalName); + } + return result; + } + + + private String getKeyByValue(Map 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/ComponentDependencyGraphvizGenerator.java b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ComponentDependencyGraphvizGenerator.java new file mode 100644 index 0000000000..d0a83396f9 --- /dev/null +++ b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ComponentDependencyGraphvizGenerator.java @@ -0,0 +1,30 @@ +// Copyright 2023 The Apache Software Foundation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package org.apache.tapestry5.internal.services; + +/** + * Service that generates a Graphviz DOT description file + * for a given component's dependency graph or for the whole set of dependencies of all components. + * @since 5.8.3 + */ +public interface ComponentDependencyGraphvizGenerator { + + /** + * Generates the Graphviz DOT file and returns it as a Strting. + * @param classNames the component (including page) class names to generate the dependency graph. + * @return + */ + String generate(String ... classNames); + +} diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ComponentDependencyGraphvizGeneratorImpl.java b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ComponentDependencyGraphvizGeneratorImpl.java new file mode 100644 index 0000000000..908633b9d6 --- /dev/null +++ b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ComponentDependencyGraphvizGeneratorImpl.java @@ -0,0 +1,184 @@ +// Copyright 2023 The Apache Software Foundation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package org.apache.tapestry5.internal.services; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; + +import org.apache.tapestry5.internal.services.ComponentDependencyRegistry.DependencyType; +import org.apache.tapestry5.services.ComponentClassResolver; + +public class ComponentDependencyGraphvizGeneratorImpl implements ComponentDependencyGraphvizGenerator { + + final private ComponentClassResolver componentClassResolver; + + final private ComponentDependencyRegistry componentDependencyRegistry; + + + public ComponentDependencyGraphvizGeneratorImpl(ComponentDependencyRegistry componentDependencyRegistry, + ComponentClassResolver componentClassResolver) + { + super(); + this.componentDependencyRegistry = componentDependencyRegistry; + this.componentClassResolver = componentClassResolver; + } + + @Override + public String generate(String... classNames) + { + + final StringBuilder dotFile = new StringBuilder("digraph {\n\n"); + + dotFile.append("\trankdir=LR;\n"); + dotFile.append("\tfontname=\"Helvetica,Arial,sans-serif\";\n"); + dotFile.append("\tsplines=ortho;\n\n"); + dotFile.append("\tnode [fontname=\"Helvetica,Arial,sans-serif\",fontsize=\"10pt\"];\n"); + dotFile.append("\tnode [shape=rect];\n\n"); + + final Set allClasses = new HashSet<>(); + + for (String className : classNames) + { + final Node node = createNode(componentClassResolver.getLogicalName(className), className); + dotFile.append(getNodeDefinition(node)); + for (DependencyType dependencyType : DependencyType.values()) + { + addDependencies(className, allClasses, dependencyType); + } + + final StringBuilder dependencySection = new StringBuilder(); + + for (Dependency dependency : node.dependencies) + { + dependencySection.append(getNodeDependencyDefinition(node, dependency.className, dependency.type)); + } + + dotFile.append("\n"); + dotFile.append(dependencySection); + dotFile.append("\n"); + + } + + + dotFile.append("}"); + + return dotFile.toString(); + } + + private String getNodeDefinition(Node node) + { + return String.format("\t%s [label=\"%s\", tooltip=\"%s\"];\n", node.id, node.label, node.className); + } + + private String getNodeDependencyDefinition(Node node, String dependency, DependencyType dependencyType) + { + String extraDefinition; + switch (dependencyType) + { + case INJECT_PAGE: extraDefinition = " [style=dashed]"; break; + case SUPERCLASS: extraDefinition = " [arrowhead=empty]"; break; + default: extraDefinition = ""; + } + return String.format("\t%s -> %s%s\n", node.id, escapeNodeId(getNodeLabel(dependency)), extraDefinition); + } + + private String getNodeLabel(String className) + { + final String logicalName = componentClassResolver.getLogicalName(className); + return getNodeLabel(className, logicalName, false); + } + + private static String getNodeLabel(String className, final String logicalName, boolean beautify) { + return logicalName != null ? beautifyLogicalName(logicalName) : (beautify ? beautifyClassName(className) : className); + } + + private static String beautifyLogicalName(String logicalName) { + return logicalName.startsWith("core/") ? logicalName.replace("core/", "") : logicalName; + } + + private static String beautifyClassName(String className) + { + String name = className.substring(className.lastIndexOf('.') + 1); + if (className.contains(".base.")) + { + name += " (base class)"; + } + else if (className.contains(".mixins.")) + { + name += " (mixin)"; + } + return name; + } + + private static String escapeNodeId(String label) { + return label.replace('.', '_').replace('/', '_'); + } + + private void addDependencies(String className, Set allClasses, DependencyType type) + { + if (!allClasses.contains(className)) + { + allClasses.add(className); + for (String dependency : componentDependencyRegistry.getDependencies(className, type)) + { + addDependencies(dependency, allClasses, type); + } + } + } + + private Node createNode(String logicalName, String className) + { + Collection deps = new HashSet<>(); + for (DependencyType type : DependencyType.values()) + { + final Set dependencies = componentDependencyRegistry.getDependencies(className, type); + for (String dependency : dependencies) + { + deps.add(new Dependency(dependency, type)); + } + } + return new Node(logicalName, className, deps); + } + + private static final class Dependency + { + final private String className; + final private DependencyType type; + public Dependency(String className, DependencyType type) + { + super(); + this.className = className; + this.type = type; + } + } + + private static final class Node { + + final private String id; + final private String className; + final private String label; + final private Set dependencies = new HashSet<>(); + + public Node(String logicalName, String className, Collection dependencies) + { + super(); + this.label = getNodeLabel(className, logicalName, true); + this.id = escapeNodeId(getNodeLabel(className, logicalName, false)); + this.className = className; + this.dependencies.addAll(dependencies); + } + + } +} 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 0000000000..1613095c2d --- /dev/null +++ b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ComponentDependencyRegistry.java @@ -0,0 +1,149 @@ +// 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.annotations.Component; +import org.apache.tapestry5.annotations.InjectComponent; +import org.apache.tapestry5.annotations.InjectPage; +import org.apache.tapestry5.commons.services.InvalidationEventHub; +import org.apache.tapestry5.internal.structure.ComponentPageElement; +import org.apache.tapestry5.model.MutableComponentModel; +import org.apache.tapestry5.plastic.PlasticField; + + +/** + * 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 { + + /** + * Enum class defining the types of dependency components, pages and mixins can + * have among them. + */ + public static enum DependencyType + { + + /** + * Simple usage of components and mixins in components and pages + */ + USAGE, + + /** + * Superclass/subclass dependency. + */ + SUPERCLASS, + + /** + * Dependency by usage of the {@linkplain InjectPage} annotation. + */ + INJECT_PAGE; + } + + /** + * Name of the file where the dependency information is stored between webapp runs. + */ + String FILENAME = "tapestryComponentDependencies.json"; + + /** + * Register all the dependencies of a given class. + */ + void register(Class clasz); + + /** + * Register all the dependencies of a given component. + */ + void register(ComponentPageElement componentPageElement); + + /** + * Register a dependency of a component class with another through annotations + * such as {@link InjectPage}, {@link InjectComponent} and {@link Component}. + */ + void register(PlasticField plasticField, MutableComponentModel componentModel); + + /** + * Clears all dependency information for a given component. + */ + void clear(String className); + + /** + * Clears all dependency information for a given component. + */ + void clear(ComponentPageElement componentPageElement); + + /** + * Clears all dependency information. + */ + void clear(); + + /** + * Returns the fully qualified names of the direct dependencies of a given component. + */ + Set getDependents(String className); + + /** + * Returns the fully qualified names of the direct dependencies of a given component + * and a given dependency type. + * @see DependencyType + */ + Set getDependencies(String className, DependencyType type); + + /** + * Signs up this registry to invalidation events from a given hub. + */ + void listen(InvalidationEventHub invalidationEventHub); + + /** + * Writes the current component dependency data to a file so it can be reused in a new run later. + * @see #FILENAME + */ + void writeFile(); + + /** + * Tells whether this registry already contans a given class name. + */ + boolean contains(String className); + + /** + * Returns the set of all class names in the registry. + */ + Set getClassNames(); + + /** + * Returns the set of all root classes (i.e. ones with no dependencies). + */ + Set getRootClasses(); + + /** + * Returns whether stored dependency information is present. + */ + boolean isStoredDependencyInformationPresent(); + + /** + * Tells this service to ignore invalidations in this thread. + */ + void disableInvalidations(); + + /** + * Tells this service to stop ignoring invalidations in this thread. + */ + void enableInvalidations(); + +} diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ComponentDependencyRegistryImpl.java b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ComponentDependencyRegistryImpl.java new file mode 100644 index 0000000000..d6d5d99f32 --- /dev/null +++ b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ComponentDependencyRegistryImpl.java @@ -0,0 +1,912 @@ +// Copyright 2022, 2023 The Apache Software Foundation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package org.apache.tapestry5.internal.services; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.WeakHashMap; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +import org.apache.tapestry5.ComponentResources; +import org.apache.tapestry5.annotations.InjectComponent; +import org.apache.tapestry5.annotations.InjectPage; +import org.apache.tapestry5.annotations.Mixin; +import org.apache.tapestry5.annotations.MixinClasses; +import org.apache.tapestry5.annotations.Mixins; +import org.apache.tapestry5.commons.Resource; +import org.apache.tapestry5.commons.internal.util.TapestryException; +import org.apache.tapestry5.commons.services.InvalidationEventHub; +import org.apache.tapestry5.internal.TapestryInternalUtils; +import org.apache.tapestry5.internal.parser.ComponentTemplate; +import org.apache.tapestry5.internal.parser.StartComponentToken; +import org.apache.tapestry5.internal.parser.TemplateToken; +import org.apache.tapestry5.internal.structure.ComponentPageElement; +import org.apache.tapestry5.ioc.Orderable; +import org.apache.tapestry5.ioc.internal.util.ClasspathResource; +import org.apache.tapestry5.ioc.internal.util.InternalUtils; +import org.apache.tapestry5.ioc.services.PerthreadManager; +import org.apache.tapestry5.json.JSONArray; +import org.apache.tapestry5.json.JSONObject; +import org.apache.tapestry5.model.ComponentModel; +import org.apache.tapestry5.model.EmbeddedComponentModel; +import org.apache.tapestry5.model.MutableComponentModel; +import org.apache.tapestry5.model.ParameterModel; +import org.apache.tapestry5.plastic.PlasticField; +import org.apache.tapestry5.plastic.PlasticManager; +import org.apache.tapestry5.runtime.Component; +import org.apache.tapestry5.services.ComponentClassResolver; +import org.apache.tapestry5.services.pageload.PageClassLoaderContextManager; +import org.apache.tapestry5.services.templates.ComponentTemplateLocator; +import org.slf4j.Logger; + +public class ComponentDependencyRegistryImpl implements ComponentDependencyRegistry +{ + + private static final List EMPTY_LIST = Collections.emptyList(); + + final private PageClassLoaderContextManager pageClassLoaderContextManager; + + private static final String META_ATTRIBUTE = "injectedComponentDependencies"; + + private static final String META_ATTRIBUTE_SEPARATOR = ","; + + // Key is a component, values are the components that depend on it. + final private Map> map; + + // Cache to check which classes were already processed or not. + final private Set alreadyProcessed; + + final private File storedDependencies; + + final private static ThreadLocal INVALIDATIONS_DISABLED = ThreadLocal.withInitial(() -> 0); + + final private PlasticManager plasticManager; + + final private ComponentClassResolver resolver; + + final private TemplateParser templateParser; + + final private Map isPageCache = new WeakHashMap<>(); + + @SuppressWarnings("deprecation") + final private ComponentTemplateLocator componentTemplateLocator; + + final private boolean storedDependencyInformationPresent; + + public ComponentDependencyRegistryImpl( + final PageClassLoaderContextManager pageClassLoaderContextManager, + final PlasticManager plasticManager, + final ComponentClassResolver componentClassResolver, + final TemplateParser templateParser, + final ComponentTemplateLocator componentTemplateLocator) + { + this.pageClassLoaderContextManager = pageClassLoaderContextManager; + map = new HashMap<>(); + alreadyProcessed = new HashSet<>(); + this.plasticManager = plasticManager; + this.resolver = componentClassResolver; + this.templateParser = templateParser; + this.componentTemplateLocator = componentTemplateLocator; + + storedDependencies = new File(FILENAME); + if (storedDependencies.exists()) + { + try (FileReader fileReader = new FileReader(storedDependencies); + BufferedReader reader = new BufferedReader(fileReader)) + { + StringBuilder builder = new StringBuilder(); + String line = reader.readLine(); + while (line != null) + { + builder.append(line); + line = reader.readLine(); + } + JSONArray jsonArray = new JSONArray(builder.toString()); + for (int i = 0; i < jsonArray.size(); i++) + { + final JSONObject jsonObject = jsonArray.getJSONObject(i); + final String className = jsonObject.getString("class"); + final DependencyType dependencyType = DependencyType.valueOf(jsonObject.getString("type")); + final String dependency = jsonObject.getString("dependency"); + add(className, dependency, dependencyType); + alreadyProcessed.add(dependency); + alreadyProcessed.add(className); + } + } catch (IOException e) + { + throw new TapestryException("Exception trying to read " + FILENAME, e); + } + + } + + storedDependencyInformationPresent = !map.isEmpty(); + + } + + public void setupThreadCleanup(final PerthreadManager perthreadManager) + { + perthreadManager.addThreadCleanupCallback(() -> { + INVALIDATIONS_DISABLED.set(0); + }); + } + + @Override + public void register(Class component) + { + + final String className = component.getName(); + final Set> furtherDependencies = new HashSet<>(); + Consumer> processClass = furtherDependencies::add; + Consumer processClassName = s -> { + try { + furtherDependencies.add(component.getClassLoader().loadClass(s)); + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + }; + + // Components declared in the template + registerTemplate(component, processClassName); + + // Dependencies from injecting or component-declaring annotations: + // @InjectPage, @InjectComponent + for (Field field : component.getDeclaredFields()) + { + + // Component injection annotation + if (field.isAnnotationPresent(InjectComponent.class)) + { + final Class dependency = field.getType(); + add(component, dependency, DependencyType.USAGE); + processClass.accept(dependency); + } + + // Page injection annotation + if (field.isAnnotationPresent(InjectPage.class)) + { + final Class dependency = field.getType(); + add(component, dependency, DependencyType.INJECT_PAGE); + } + + // @Component + registerComponentInstance(field, processClassName); + + // Mixins, class level: @Mixin + registerMixin(field, processClassName); + + // Mixins applied to embedded component instances through @MixinClasses or @Mixins + registerComponentInstanceMixins(field, processClass, processClassName); + } + + // Superclass + Class superclass = component.getSuperclass(); + if (isTransformed(superclass)) + { + processClass.accept(superclass); + add(component, superclass, DependencyType.SUPERCLASS); + } + + alreadyProcessed.add(className); + + for (Class dependency : furtherDependencies) + { + // Avoid infinite recursion + final String dependencyClassName = dependency.getName(); + if (!alreadyProcessed.contains(dependencyClassName) + && plasticManager.shouldInterceptClassLoading(dependency.getName())) + { + register(dependency); + } + } + + } + + /** + * Notice only the main template (i.e. not the locale- or axis-specific ones) + * are checked here. They hopefully will be covered when the ComponentModel-based + * component dependency processing is done. + * @param component + * @param processClassName + */ + @SuppressWarnings("deprecation") + private void registerTemplate(Class component, Consumer processClassName) + { + // TODO: implement caching of template dependency information, probably + // by listening separaterly to ComponentTemplateSource to invalidate caches + // just when template changes. + + final String className = component.getName(); + ComponentModel mock = new ComponentModelMock(component, isPage(className)); + final Resource templateResource = componentTemplateLocator.locateTemplate(mock, Locale.getDefault()); + String dependency; + if (templateResource != null) + { + final ComponentTemplate template = templateParser.parseTemplate(templateResource); + for (TemplateToken token: template.getTokens()) + { + if (token instanceof StartComponentToken) + { + StartComponentToken componentToken = (StartComponentToken) token; + String logicalName = componentToken.getComponentType(); + if (logicalName != null) + { + dependency = resolver.resolveComponentTypeToClassName(logicalName); + add(className, dependency, DependencyType.USAGE); + processClassName.accept(dependency); + } + for (String mixin : TapestryInternalUtils.splitAtCommas(componentToken.getMixins())) + { + dependency = resolver.resolveMixinTypeToClassName(mixin); + add(className, dependency, DependencyType.USAGE); + processClassName.accept(dependency); + } + } + } + } + } + + private boolean isNotPage(final String className) + { + return !isPage(className); + } + + private boolean isPage(final String className) + { + Boolean result = isPageCache.get(className); + if (result == null) + { + result = resolver.isPage(className); + isPageCache.put(className, result); + } + return result; + } + + private void registerComponentInstance(Field field, Consumer processClassName) + { + if (field.isAnnotationPresent(org.apache.tapestry5.annotations.Component.class)) + { + org.apache.tapestry5.annotations.Component component = + field.getAnnotation(org.apache.tapestry5.annotations.Component.class); + + final String typeFromAnnotation = component.type().trim(); + String dependency; + if (typeFromAnnotation.isEmpty()) + { + dependency = field.getType().getName(); + } + else + { + dependency = resolver.resolveComponentTypeToClassName(typeFromAnnotation); + } + add(field.getDeclaringClass().getName(), dependency, DependencyType.USAGE); + processClassName.accept(dependency); + } + } + + private void registerMixin(Field field, Consumer processClassName) { + if (field.isAnnotationPresent(Mixin.class)) + { + // Logic adapted from MixinWorker + String mixinType = field.getAnnotation(Mixin.class).value(); + String mixinClassName = InternalUtils.isBlank(mixinType) ? + getFieldTypeClassName(field) : + resolver.resolveMixinTypeToClassName(mixinType); + + add(getDeclaringClassName(field), mixinClassName, DependencyType.USAGE); + processClassName.accept(mixinClassName); + } + } + + private String getDeclaringClassName(Field field) { + return field.getDeclaringClass().getName(); + } + + private String getFieldTypeClassName(Field field) { + return field.getType().getName(); + } + + private void registerComponentInstanceMixins(Field field, Consumer> processClass, Consumer processClassName) + { + + if (field.isAnnotationPresent(org.apache.tapestry5.annotations.Component.class)) + { + + MixinClasses mixinClasses = field.getAnnotation(MixinClasses.class); + if (mixinClasses != null) + { + for (Class dependency : mixinClasses.value()) + { + add(field.getDeclaringClass(), dependency, DependencyType.USAGE); + processClass.accept(dependency); + } + } + + Mixins mixins = field.getAnnotation(Mixins.class); + if (mixins != null) + { + for (String mixin : mixins.value()) + { + // Logic adapted from MixinsWorker + Orderable typeAndOrder = TapestryInternalUtils.mixinTypeAndOrder(mixin); + final String dependency = resolver.resolveMixinTypeToClassName(typeAndOrder.getTarget()); + add(getDeclaringClassName(field), dependency, DependencyType.USAGE); + processClassName.accept(dependency); + } + } + + } + + } + + @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, DependencyType.USAGE); + register(child); + } + + // Mixins, class level + final ComponentResources componentResources = componentPageElement.getComponentResources(); + final ComponentModel componentModel = componentResources.getComponentModel(); + for (String mixinClassName : componentModel.getMixinClassNames()) + { + add(componentClassName, mixinClassName, DependencyType.USAGE); + } + + // Mixins applied to embedded component instances + final List embeddedComponentIds = componentModel.getEmbeddedComponentIds(); + for (String id : embeddedComponentIds) + { + final EmbeddedComponentModel embeddedComponentModel = componentResources + .getComponentModel() + .getEmbeddedComponentModel(id); + final List mixinClassNames = embeddedComponentModel + .getMixinClassNames(); + for (String mixinClassName : mixinClassNames) { + add(componentClassName, mixinClassName, DependencyType.USAGE); + } + } + + // Superclass + final Component component = componentPageElement.getComponent(); + Class parent = component.getClass().getSuperclass(); + if (parent != null && !Object.class.equals(parent)) + { + add(componentClassName, parent.getName(), DependencyType.SUPERCLASS); + } + + // Dependencies from injecting annotations: + // @InjectPage, @InjectComponent, @InjectComponent + final String metaDependencies = component.getComponentResources().getComponentModel().getMeta(META_ATTRIBUTE); + if (metaDependencies != null) + { + for (String dependency : metaDependencies.split(META_ATTRIBUTE_SEPARATOR)) + { + add(componentClassName, dependency, + isPage(dependency) ? DependencyType.INJECT_PAGE : DependencyType.USAGE); + } + } + + alreadyProcessed.add(componentClassName); + + } + + } + + } + + @Override + public void register(PlasticField plasticField, MutableComponentModel componentModel) + { + if (plasticField.hasAnnotation(InjectPage.class) || + plasticField.hasAnnotation(InjectComponent.class) || + plasticField.hasAnnotation(org.apache.tapestry5.annotations.Component.class)) + { + String dependencies = componentModel.getMeta(META_ATTRIBUTE); + final String dependency = plasticField.getTypeName(); + if (dependencies == null) + { + dependencies = dependency; + } + else + { + if (!dependencies.contains(dependency)) + { + dependencies = dependencies + META_ATTRIBUTE_SEPARATOR + dependency; + } + } + componentModel.setMeta(META_ATTRIBUTE, dependencies); + } + } + + private String getClassName(ComponentPageElement component) + { + return component.getComponentResources().getComponentModel().getComponentClassName(); + } + + @Override + public void clear(String className) + { + synchronized (map) + { + alreadyProcessed.remove(className); + map.remove(className); + final Collection> allDependentSets = map.values(); + for (Set dependents : allDependentSets) + { + if (dependents != null) + { + final Iterator iterator = dependents.iterator(); + while (iterator.hasNext()) + { + if (className.equals(iterator.next().className)) + { + iterator.remove(); + } + } + } + } + } + } + + @Override + public void clear(ComponentPageElement component) + { + clear(getClassName(component)); + } + + @Override + public void clear() { + map.clear(); + alreadyProcessed.clear(); + } + + @Override + public Set getDependents(String className) + { + final Set dependents = map.get(className); + return dependents != null + ? dependents.stream().map(d -> d.className).collect(Collectors.toSet()) + : Collections.emptySet(); + } + + @Override + public Set getDependencies(String className, DependencyType type) + { + Set dependencies = Collections.emptySet(); + if (alreadyProcessed.contains(className)) + { + dependencies = map.entrySet().stream() + .filter(e -> contains(e.getValue(), className, type)) + .map(e -> e.getKey()) + .collect(Collectors.toSet()); + } + + return dependencies; + } + + + private boolean contains(Set dependencies, String className, DependencyType type) + { + boolean contains = false; + for (Dependency dependency : dependencies) + { + if (dependency.type.equals(type) && dependency.className.equals(className)) + { + contains = true; + break; + } + } + return contains; + } + + private void add(ComponentPageElement component, ComponentPageElement dependency, DependencyType type) + { + add(getClassName(component), getClassName(dependency), type); + } + + // Just for unit tests + void add(String component, String dependency, DependencyType type, boolean markAsAlreadyProcessed) + { + if (markAsAlreadyProcessed) + { + alreadyProcessed.add(component); + } + if (dependency != null) + { + add(component, dependency, type); + } + } + + private void add(Class component, Class dependency, DependencyType type) + { + if (plasticManager.shouldInterceptClassLoading(dependency.getName())) + { + add(component.getName(), dependency.getName(), type); + } + } + + private void add(String component, String dependency, DependencyType type) + { + Objects.requireNonNull(component, "Parameter component cannot be null"); + Objects.requireNonNull(dependency, "Parameter dependency cannot be null"); + Objects.requireNonNull(dependency, "Parameter type cannot be null"); + synchronized (map) + { + Set dependents = map.get(dependency); + if (dependents == null) + { + dependents = new HashSet<>(); + map.put(dependency, dependents); + } + dependents.add(new Dependency(component, type)); + } + } + + @Override + public void listen(InvalidationEventHub invalidationEventHub) + { + invalidationEventHub.addInvalidationCallback(this::listen); + } + + // Protected just for testing + List listen(List resources) + { + List furtherDependents = EMPTY_LIST; + if (resources.isEmpty()) + { + clear(); + furtherDependents = EMPTY_LIST; + } + else if (INVALIDATIONS_DISABLED.get() > 0) + { + furtherDependents = Collections.emptyList(); + } + // Don't invalidate component dependency information when + // PageClassloaderContextManager is merging contexts + // TODO: is this still needed since the inception of INVALIDATIONS_ENABLED? + else if (!pageClassLoaderContextManager.isMerging()) + { + furtherDependents = new ArrayList<>(); + for (String resource : resources) + { + + final Set dependents = getDependents(resource); + for (String furtherDependent : dependents) + { + if (!resources.contains(furtherDependent) && !furtherDependents.contains(furtherDependent)) + { + furtherDependents.add(furtherDependent); + } + } + + clear(resource); + + } + } + return furtherDependents; + } + + @Override + public void writeFile() + { + synchronized (this) + { + try (FileWriter fileWriter = new FileWriter(storedDependencies); + BufferedWriter bufferedWriter = new BufferedWriter(fileWriter)) + { + Set classNames = new HashSet<>(alreadyProcessed.size()); + classNames.addAll(map.keySet()); + classNames.addAll(alreadyProcessed); + JSONArray jsonArray = new JSONArray(); + for (String className : classNames) + { + for (DependencyType dependencyType : DependencyType.values()) + { + final Set dependencies = getDependencies(className, dependencyType); + for (String dependency : dependencies) + { + JSONObject object = new JSONObject(); + object.put("class", className); + object.put("type", dependencyType.name()); + object.put("dependency", dependency); + jsonArray.add(object); + } + } + } + bufferedWriter.write(jsonArray.toString()); + } + catch (IOException e) + { + throw new TapestryException("Exception trying to read " + FILENAME, e); + } + } + } + + @Override + public boolean contains(String className) + { + return alreadyProcessed.contains(className); + } + + @Override + public Set getClassNames() + { + return Collections.unmodifiableSet(new HashSet<>(alreadyProcessed)); + } + + @Override + public Set getRootClasses() { + return alreadyProcessed.stream() + .filter(c -> getDependencies(c, DependencyType.USAGE).isEmpty() && + getDependencies(c, DependencyType.INJECT_PAGE).isEmpty() && + getDependencies(c, DependencyType.SUPERCLASS).isEmpty()) + .collect(Collectors.toSet()); + } + + private boolean isTransformed(Class clasz) + { + return plasticManager.shouldInterceptClassLoading(clasz.getName()); + } + + @Override + public boolean isStoredDependencyInformationPresent() + { + return storedDependencyInformationPresent; + } + + @Override + public void disableInvalidations() + { + INVALIDATIONS_DISABLED.set(INVALIDATIONS_DISABLED.get() + 1); + } + + @Override + public void enableInvalidations() + { + INVALIDATIONS_DISABLED.set(INVALIDATIONS_DISABLED.get() - 1); + if (INVALIDATIONS_DISABLED.get() < 0) + { + INVALIDATIONS_DISABLED.set(0); + } + } + + /** + * Only really implemented method is {@link ComponentModel#getBaseResource()} + */ + private class ComponentModelMock implements ComponentModel + { + + final private Resource baseResource; + final private boolean isPage; + final private String componentClassName; + + public ComponentModelMock(Class component, boolean isPage) + { + componentClassName = component.getName(); + String templateLocation = componentClassName.replace('.', '/'); + baseResource = new ClasspathResource(templateLocation); + + this.isPage = isPage; + } + + @Override + public Resource getBaseResource() + { + return baseResource; + } + + @Override + public String getLibraryName() + { + return null; + } + + @Override + public boolean isPage() + { + return isPage; + } + + @Override + public String getComponentClassName() + { + return componentClassName; + } + + @Override + public List getEmbeddedComponentIds() + { + return null; + } + + @Override + public EmbeddedComponentModel getEmbeddedComponentModel(String componentId) + { + return null; + } + + @Override + public String getFieldPersistenceStrategy(String fieldName) + { + return null; + } + + @Override + public Logger getLogger() + { + return null; + } + + @Override + public List getMixinClassNames() + { + return null; + } + + @Override + public ParameterModel getParameterModel(String parameterName) + { + return null; + } + + @Override + public boolean isFormalParameter(String parameterName) + { + return false; + } + + @Override + public List getParameterNames() + { + return null; + } + + @Override + public List getDeclaredParameterNames() + { + return null; + } + + @Override + public List getPersistentFieldNames() + { + return null; + } + + @Override + public boolean isRootClass() + { + return false; + } + + @Override + public boolean getSupportsInformalParameters() + { + return false; + } + + @Override + public ComponentModel getParentModel() + { + return null; + } + + @Override + public boolean isMixinAfter() + { + return false; + } + + @Override + public String getMeta(String key) + { + return null; + } + + @Override + public Set getHandledRenderPhases() + { + return null; + } + + @Override + public boolean handlesEvent(String eventType) + { + return false; + } + + @Override + public String[] getOrderForMixin(String mixinClassName) + { + return null; + } + + @Override + public boolean handleActivationEventContext() + { + return false; + } + + } + + private static final class Dependency + { + private final String className; + private final DependencyType type; + + public Dependency(String className, DependencyType dependencyType) + { + super(); + this.className = className; + this.type = dependencyType; + } + + @Override + public int hashCode() { + return Objects.hash(className, type); + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) + { + return true; + } + if (!(obj instanceof Dependency)) + { + return false; + } + Dependency other = (Dependency) obj; + return Objects.equals(className, other.className) && type == other.type; + } + + @Override + public String toString() + { + return "Dependency [className=" + className + ", dependencyType=" + type + "]"; + } + + } + +} 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 09c568f413..a0ae6d2ea6 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,12 +12,23 @@ package org.apache.tapestry5.internal.services; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Map.Entry; +import java.util.Objects; import java.util.Set; +import java.util.stream.Collectors; import org.apache.tapestry5.ComponentResources; +import org.apache.tapestry5.SymbolConstants; import org.apache.tapestry5.beanmodel.services.PlasticProxyFactoryImpl; +import org.apache.tapestry5.commons.Location; +import org.apache.tapestry5.commons.ObjectCreator; import org.apache.tapestry5.commons.Resource; import org.apache.tapestry5.commons.services.PlasticProxyFactory; import org.apache.tapestry5.commons.util.CollectionFactory; @@ -27,6 +38,7 @@ import org.apache.tapestry5.internal.InternalConstants; import org.apache.tapestry5.internal.model.MutableComponentModelImpl; import org.apache.tapestry5.internal.plastic.PlasticInternalUtils; +import org.apache.tapestry5.internal.services.ComponentDependencyRegistry.DependencyType; import org.apache.tapestry5.ioc.Invokable; import org.apache.tapestry5.ioc.LoggerSource; import org.apache.tapestry5.ioc.OperationTracker; @@ -53,6 +65,8 @@ import org.apache.tapestry5.plastic.PlasticClass; import org.apache.tapestry5.plastic.PlasticClassEvent; import org.apache.tapestry5.plastic.PlasticClassListener; +import org.apache.tapestry5.plastic.PlasticClassTransformation; +import org.apache.tapestry5.plastic.PlasticClassTransformer; import org.apache.tapestry5.plastic.PlasticField; import org.apache.tapestry5.plastic.PlasticManager; import org.apache.tapestry5.plastic.PlasticManager.PlasticManagerBuilder; @@ -67,6 +81,8 @@ import org.apache.tapestry5.services.ComponentClassResolver; import org.apache.tapestry5.services.ComponentEventHandler; import org.apache.tapestry5.services.TransformConstants; +import org.apache.tapestry5.services.pageload.PageClassLoaderContext; +import org.apache.tapestry5.services.pageload.PageClassLoaderContextManager; import org.apache.tapestry5.services.transform.ComponentClassTransformWorker2; import org.apache.tapestry5.services.transform.ControlledPackageType; import org.apache.tapestry5.services.transform.TransformationSupport; @@ -80,7 +96,7 @@ public final class ComponentInstantiatorSourceImpl implements ComponentInstantia { private final Set controlledPackageNames = CollectionFactory.newSet(); - private final URLChangeTracker changeTracker; + private final URLChangeTracker changeTracker; private final ClassLoader parent; @@ -95,12 +111,20 @@ public final class ComponentInstantiatorSourceImpl implements ComponentInstantia private final InternalComponentInvalidationEventHub invalidationHub; private final boolean productionMode; + + private final boolean multipleClassLoaders; private final ComponentClassResolver resolver; - - private volatile PlasticProxyFactory proxyFactory; - - private volatile PlasticManager manager; + + private final PageClassLoaderContextManager pageClassLoaderContextManager; + + private PageClassLoaderContext rootPageClassloaderContext; + + private PlasticProxyFactoryProxy plasticProxyFactoryProxy; + + private ComponentDependencyRegistry componentDependencyRegistry; + + private static final ThreadLocal CURRENT_PAGE = ThreadLocal.withInitial(() -> null); /** * Map from class name to Instantiator. @@ -141,19 +165,30 @@ public ComponentInstantiatorSourceImpl(Logger logger, @Symbol(TapestryHttpSymbolConstants.PRODUCTION_MODE) boolean productionMode, + @Symbol(SymbolConstants.MULTIPLE_CLASSLOADERS) + boolean multipleClassLoaders, + ComponentClassResolver resolver, - InternalComponentInvalidationEventHub invalidationHub) + InternalComponentInvalidationEventHub invalidationHub, + + PageClassLoaderContextManager pageClassLoaderContextManager, + + ComponentDependencyRegistry componentDependencyRegistry + ) { this.parent = proxyFactory.getClassLoader(); this.transformerChain = transformerChain; this.logger = logger; this.loggerSource = loggerSource; - this.changeTracker = new URLChangeTracker(classpathURLConverter); + this.changeTracker = new URLChangeTracker(classpathURLConverter); this.tracker = tracker; this.invalidationHub = invalidationHub; this.productionMode = productionMode; + this.multipleClassLoaders = multipleClassLoaders; this.resolver = resolver; + this.pageClassLoaderContextManager = pageClassLoaderContextManager; + this.componentDependencyRegistry = componentDependencyRegistry; // For now, we just need the keys of the configuration. When there are more types of controlled // packages, we'll need to do more. @@ -161,69 +196,170 @@ public ComponentInstantiatorSourceImpl(Logger logger, controlledPackageNames.addAll(configuration.keySet()); initializeService(); + + pageClassLoaderContextManager.initialize( + rootPageClassloaderContext, + ComponentInstantiatorSourceImpl.this::createPlasticProxyFactory); + } @PostInjection public void listenForUpdates(UpdateListenerHub hub) { - invalidationHub.addInvalidationCallback(this); + invalidationHub.addInvalidationCallback(this::invalidate); hub.addUpdateListener(this); } public synchronized void checkForUpdates() { - if (changeTracker.containsChanges()) + final Set changedResources = changeTracker.getChangedResourcesInfo(); + if (!changedResources.isEmpty()) { - invalidationHub.classInControlledPackageHasChanged(); + + final List classNames = changedResources.stream().map(ClassName::getClassName).collect(Collectors.toList()); + + if (logger.isInfoEnabled()) + { + logger.info("Component class(es) changed: {}", String.join(", ", classNames)); + } + + if (multipleClassLoaders) + { + + final Set classesToInvalidate = new HashSet<>(); + + for (String className : classNames) + { + final PageClassLoaderContext context = rootPageClassloaderContext.findByClassName(className); + if (context != rootPageClassloaderContext && context != null) + { + classesToInvalidate.addAll(pageClassLoaderContextManager.invalidate(context)); + } + } + + classNames.clear(); + classNames.addAll(classesToInvalidate); + + invalidate(classNames); + + invalidationHub.fireInvalidationEvent(classNames); + } + else + { + invalidationHub.classInControlledPackageHasChanged(); + } + } } + private List invalidate(final List classNames) { + + if (classNames.isEmpty()) + { + clearCaches(); + } + else + { + + final String currentPage = CURRENT_PAGE.get(); + + final Iterator> classToInstantiatorIterator = classToInstantiator.entrySet().iterator(); + while (classToInstantiatorIterator.hasNext()) + { + final String className = classToInstantiatorIterator.next().getKey(); + if (!className.equals(currentPage) && classNames.contains(className)) + { + classToInstantiatorIterator.remove(); + } + } + + final Iterator> classToModelIterator = classToModel.entrySet().iterator(); + while (classToModelIterator.hasNext()) + { + final String className = classToModelIterator.next().getKey(); + if (!className.equals(currentPage) && classNames.contains(className)) + { + classToModelIterator.remove(); + } + } + + } + + return Collections.emptyList(); + } + public void forceComponentInvalidation() { - changeTracker.clear(); + clearCaches(); invalidationHub.classInControlledPackageHasChanged(); } + private void clearCaches() + { + classToInstantiator.clear(); + pageClassLoaderContextManager.clear(); + } + public void run() { changeTracker.clear(); classToInstantiator.clear(); - proxyFactory.clearCache(); - - // Release the existing class pool, loader and so forth. - // Create a new one. - + classToModel.clear(); + pageClassLoaderContextManager.clear(); initializeService(); } /** * Invoked at object creation, or when there are updates to class files (i.e., invalidation), to create a new set of * Javassist class pools and loaders. + * Since TAP5-2742, this method is only called once. */ private void initializeService() { - PlasticManagerBuilder builder = PlasticManager.withClassLoader(parent).delegate(this) - .packages(controlledPackageNames); - - if (!productionMode) + + pageClassLoaderContextManager.clear(); + + if (rootPageClassloaderContext == null) { - builder.enable(TransformationOption.FIELD_WRITEBEHIND); + logger.info("Initializing page pool"); + + pageClassLoaderContextManager.clear(); + + PlasticProxyFactory proxyFactory = createPlasticProxyFactory(parent); + rootPageClassloaderContext = new PageClassLoaderContext( + "root", null, Collections.emptySet(), proxyFactory, pageClassLoaderContextManager::get); + } + else + { + logger.info("Restarting page pool"); } - - manager = builder.create(); - - manager.addPlasticClassListener(this); - - proxyFactory = new PlasticProxyFactoryImpl(manager, logger); classToInstantiator.clear(); classToModel.clear(); } + private PlasticProxyFactory createPlasticProxyFactory(final ClassLoader parentClassloader) + { + PlasticManagerBuilder builder = PlasticManager.withClassLoader(parentClassloader) + .delegate(this) + .packages(controlledPackageNames); + if (!productionMode) + { + builder.enable(TransformationOption.FIELD_WRITEBEHIND); + } + PlasticManager plasticManager = builder.create(); + plasticManager.addPlasticClassListener(this); + PlasticProxyFactory proxyFactory = new PlasticProxyFactoryImpl(plasticManager, logger); + return proxyFactory; + } + public Instantiator getInstantiator(final String className) { return classToInstantiator.computeIfAbsent(className, this::createInstantiatorForClass); } + + private static final ThreadLocal> OPEN_INSTANTIATORS = + ThreadLocal.withInitial(HashSet::new); private Instantiator createInstantiatorForClass(final String className) { @@ -232,12 +368,40 @@ private Instantiator createInstantiatorForClass(final String className) { public Instantiator invoke() { + // Force the creation of the class (and the transformation of the class). This will first // trigger transformations of any base classes. + + OPEN_INSTANTIATORS.get().add(className); + + componentDependencyRegistry.disableInvalidations(); + PageClassLoaderContext context; + try + { + context = pageClassLoaderContextManager.get(className); + } + finally + { + componentDependencyRegistry.enableInvalidations(); + } + + // Make sure the dependencies have been processed in case + // there was some invalidation going on and they're not there. - final ClassInstantiator plasticInstantiator = manager.getClassInstantiator(className); - + // TODO: maybe we need superclasses here too? + final Set dependencies = componentDependencyRegistry.getDependencies(className, DependencyType.USAGE); + for (String dependency : dependencies) + { + if (!OPEN_INSTANTIATORS.get().contains(dependency)) + { + createInstantiatorForClass(dependency); + } + } + + ClassInstantiator plasticInstantiator = context.getPlasticManager().getClassInstantiator(className); final ComponentModel model = classToModel.get(className); + + OPEN_INSTANTIATORS.get().remove(className); return new Instantiator() { @@ -255,7 +419,7 @@ public ComponentModel getModel() @Override public String toString() { - return String.format("[Instantiator[%s]", className); + return String.format("[Instantiator[%s:%s]", className, context); } }; } @@ -269,9 +433,13 @@ public boolean exists(String className) public PlasticProxyFactory getProxyFactory() { - return proxyFactory; + if (plasticProxyFactoryProxy == null) + { + plasticProxyFactoryProxy = new PlasticProxyFactoryProxy(); + } + return plasticProxyFactoryProxy; } - + public void transform(final PlasticClass plasticClass) { tracker.run(String.format("Running component class transformations on %s", plasticClass.getClassName()), @@ -306,7 +474,7 @@ public void run() Resource baseResource = new ClasspathResource(parent, PlasticInternalUtils .toClassPath(className)); - changeTracker.add(baseResource.toURL()); + changeTracker.add(baseResource.toURL(), new ClassName(className)); if (isRoot) { @@ -423,7 +591,8 @@ public Class toClass(String typeName) { try { - return PlasticInternalUtils.toClass(manager.getClassLoader(), typeName); + final PageClassLoaderContext context = pageClassLoaderContextManager.get(typeName); + return PlasticInternalUtils.toClass(context.getPlasticManager().getClassLoader(), typeName); } catch (ClassNotFoundException ex) { throw new RuntimeException(String.format( @@ -500,4 +669,146 @@ public void run() } } } + + private static class ClassName implements ClassNameHolder + { + private String className; + + public ClassName(String className) + { + super(); + this.className = className; + } + + @Override + public String getClassName() + { + return className; + } + + @Override + public int hashCode() { + return Objects.hash(className); + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) { + return true; + } + if (!(obj instanceof ClassName)) { + return false; + } + ClassName other = (ClassName) obj; + return Objects.equals(className, other.className); + } + + @Override + public String toString() + { + return className; + } + + } + + private class PlasticProxyFactoryProxy implements PlasticProxyFactory + { + + @Override + public void addPlasticClassListener(PlasticClassListener listener) + { + throw new UnsupportedOperationException(); + } + + @Override + public void removePlasticClassListener(PlasticClassListener listener) + { + throw new UnsupportedOperationException(); + } + + @Override + public ClassLoader getClassLoader() { + return rootPageClassloaderContext.getProxyFactory().getClassLoader(); + } + + @Override + public ClassInstantiator createProxy(Class interfaceType, PlasticClassTransformer callback) + { + return getProxyFactory(interfaceType.getName()).createProxy(interfaceType, callback); + } + + @Override + public ClassInstantiator createProxy(Class interfaceType, + Class implementationType, + PlasticClassTransformer callback, + boolean introduceInterface) + { + throw new UnsupportedOperationException(); + } + + @Override + public ClassInstantiator createProxy(Class interfaceType, Class implementationType, PlasticClassTransformer callback) + { + throw new UnsupportedOperationException(); + } + + @Override + public PlasticClassTransformation createProxyTransformation(Class interfaceType) + { + throw new UnsupportedOperationException(); + } + + @Override + public PlasticClassTransformation createProxyTransformation(Class interfaceType, Class implementationType) + { + throw new UnsupportedOperationException(); + } + + @Override + public T createProxy(Class interfaceType, ObjectCreator creator, String description) + { + throw new UnsupportedOperationException(); + } + + @Override + public T createProxy(Class interfaceType, Class implementationType, ObjectCreator creator, String description) + { + throw new UnsupportedOperationException(); + } + + @Override + public Location getMethodLocation(Method method) { + return getProxyFactory(method.getDeclaringClass().getName()).getMethodLocation(method); + } + + @Override + public Location getConstructorLocation(Constructor constructor) + { + return getProxyFactory(constructor.getDeclaringClass().getName()).getConstructorLocation(constructor); + } + + @Override + public void clearCache() { + throw new UnsupportedOperationException(); + } + + @Override + public PlasticManager getPlasticManager() { + return rootPageClassloaderContext.getProxyFactory().getPlasticManager(); + } + + @Override + public PlasticProxyFactory getProxyFactory(String className) + { + PageClassLoaderContext context = rootPageClassloaderContext.findByClassName(className); + if (context == null) + { + context = pageClassLoaderContextManager.get(className); + } + return context.getProxyFactory(); + } + + } + } 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 1a97c9551c..fbd52e3580 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 @@ -19,6 +19,7 @@ import java.util.Locale; import java.util.concurrent.Callable; +import org.apache.tapestry5.SymbolConstants; import org.apache.tapestry5.commons.Messages; import org.apache.tapestry5.commons.Resource; import org.apache.tapestry5.commons.internal.util.TapestryException; @@ -31,11 +32,13 @@ import org.apache.tapestry5.ioc.services.ThreadLocale; import org.apache.tapestry5.ioc.services.UpdateListener; import org.apache.tapestry5.model.ComponentModel; +import org.apache.tapestry5.services.ComponentClassResolver; import org.apache.tapestry5.services.messages.ComponentMessagesSource; 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 { @@ -78,29 +81,36 @@ public MessagesBundle getParent() } public ComponentMessagesSourceImpl(@Symbol(TapestryHttpSymbolConstants.PRODUCTION_MODE) - boolean productionMode, List appCatalogResources, PropertiesFileParser parser, + boolean productionMode, + @Symbol(SymbolConstants.MULTIPLE_CLASSLOADERS) + boolean multipleClassLoaders, + List appCatalogResources, PropertiesFileParser parser, ComponentResourceLocator resourceLocator, ClasspathURLConverter classpathURLConverter, ComponentRequestSelectorAnalyzer componentRequestSelectorAnalyzer, - ThreadLocale threadLocale) + ThreadLocale threadLocale, ComponentClassResolver componentClassResolver, + Logger logger) { - this(productionMode, appCatalogResources, resourceLocator, parser, new URLChangeTracker(classpathURLConverter), componentRequestSelectorAnalyzer, threadLocale); + this(productionMode, multipleClassLoaders, appCatalogResources, resourceLocator, parser, new URLChangeTracker(classpathURLConverter), + componentRequestSelectorAnalyzer, threadLocale, componentClassResolver, logger); } - ComponentMessagesSourceImpl(boolean productionMode, Resource appCatalogResource, + ComponentMessagesSourceImpl(boolean productionMode, boolean multipleClassLoaders, Resource appCatalogResource, ComponentResourceLocator resourceLocator, PropertiesFileParser parser, URLChangeTracker tracker, ComponentRequestSelectorAnalyzer componentRequestSelectorAnalyzer, - ThreadLocale threadLocale) + ThreadLocale threadLocale, ComponentClassResolver componentClassResolver, + Logger logger) { - this(productionMode, Arrays.asList(appCatalogResource), resourceLocator, parser, tracker, componentRequestSelectorAnalyzer, threadLocale); + this(productionMode, multipleClassLoaders, Arrays.asList(appCatalogResource), resourceLocator, parser, tracker, componentRequestSelectorAnalyzer, threadLocale, componentClassResolver, logger); } - ComponentMessagesSourceImpl(boolean productionMode, List appCatalogResources, + ComponentMessagesSourceImpl(boolean productionMode, boolean multipleClassLoaders, List appCatalogResources, ComponentResourceLocator resourceLocator, PropertiesFileParser parser, URLChangeTracker tracker, ComponentRequestSelectorAnalyzer componentRequestSelectorAnalyzer, - ThreadLocale threadLocale) + ThreadLocale threadLocale, ComponentClassResolver componentClassResolver, + Logger logger) { - messagesSource = new MessagesSourceImpl(productionMode, productionMode ? null : tracker, resourceLocator, - parser); + messagesSource = new MessagesSourceImpl(productionMode, multipleClassLoaders, productionMode ? null : tracker, resourceLocator, + parser, componentClassResolver, 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 71bac204b8..b1ca0d9a21 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,17 @@ 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 java.util.stream.Collectors; + +import org.apache.tapestry5.SymbolConstants; import org.apache.tapestry5.TapestryConstants; import org.apache.tapestry5.commons.Location; import org.apache.tapestry5.commons.Resource; @@ -35,11 +46,7 @@ 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. @@ -49,13 +56,17 @@ public final class ComponentTemplateSourceImpl extends InvalidationEventHubImpl { private final TemplateParser parser; - private final URLChangeTracker tracker; + private final URLChangeTracker tracker; private final ComponentResourceLocator locator; private final ComponentRequestSelectorAnalyzer componentRequestSelectorAnalyzer; private final ThreadLocale threadLocale; + + private final Logger logger; + + private final boolean multipleClassLoaders; /** * Caches from a key (combining component name and locale) to a resource. Often, many different keys will point to @@ -109,25 +120,31 @@ public boolean usesStrictMixinParameters() public ComponentTemplateSourceImpl(@Inject @Symbol(TapestryHttpSymbolConstants.PRODUCTION_MODE) - boolean productionMode, TemplateParser parser, ComponentResourceLocator locator, + boolean productionMode, + @Inject + @Symbol(SymbolConstants.MULTIPLE_CLASSLOADERS) + boolean multipleClassLoaders, + 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, multipleClassLoaders, parser, locator, new URLChangeTracker(classpathURLConverter), componentRequestSelectorAnalyzer, threadLocale, logger); } - ComponentTemplateSourceImpl(boolean productionMode, TemplateParser parser, ComponentResourceLocator locator, - URLChangeTracker tracker, ComponentRequestSelectorAnalyzer componentRequestSelectorAnalyzer, - ThreadLocale threadLocale) + ComponentTemplateSourceImpl(boolean productionMode, boolean multipleClassLoaders, TemplateParser parser, ComponentResourceLocator locator, + URLChangeTracker tracker, ComponentRequestSelectorAnalyzer componentRequestSelectorAnalyzer, + 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; + this.multipleClassLoaders = multipleClassLoaders; } @PostInjection @@ -170,7 +187,7 @@ public ComponentTemplate getTemplate(ComponentModel componentModel, ComponentRes if (result == null) { - result = parseTemplate(resource); + result = parseTemplate(resource, componentModel.getComponentClassName()); templates.put(resource, result); } @@ -196,7 +213,7 @@ public ComponentTemplate getTemplate(ComponentModel componentModel, Locale local } } - 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 +221,7 @@ private ComponentTemplate parseTemplate(Resource r) if (!r.exists()) return missingTemplate; - tracker.add(r.toURL()); + tracker.add(r.toURL(), new TemplateTrackingInfo(r.getPath(), className)); return parser.parseTemplate(r); } @@ -235,12 +252,43 @@ private Resource locateTemplateResource(ComponentModel initialModel, ComponentRe * 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 changedResourcesInfo = tracker.getChangedResourcesInfo(); + if (!changedResourcesInfo.isEmpty()) { - invalidate(); + if (logger.isInfoEnabled()) + { + logger.info("Changed template(s) found: {}", String.join(", ", + changedResourcesInfo.stream().map(TemplateTrackingInfo::getTemplate).collect(Collectors.toList()))); + } + + if (multipleClassLoaders) + { + + final Iterator> templateResourcesIterator = templateResources.entrySet().iterator(); + for (TemplateTrackingInfo info : changedResourcesInfo) + { + while (templateResourcesIterator.hasNext()) + { + final MultiKey key = templateResourcesIterator.next().getKey(); + if (info.getClassName().equals((String) key.getValues()[0])) + { + templates.remove(templateResources.get(key)); + templateResourcesIterator.remove(); + } + } + } + + fireInvalidationEvent(changedResourcesInfo.stream().map(TemplateTrackingInfo::getClassName).collect(Collectors.toList())); + + } + else + { + invalidate(); + } } } 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 e536a7766e..fb1864edc2 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.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 adeddc1677..f96ad69796 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 @@ -12,6 +12,15 @@ package org.apache.tapestry5.internal.services; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + import org.apache.tapestry5.commons.Messages; import org.apache.tapestry5.commons.Resource; import org.apache.tapestry5.commons.util.CaseInsensitiveMap; @@ -20,13 +29,11 @@ import org.apache.tapestry5.func.F; import org.apache.tapestry5.internal.event.InvalidationEventHubImpl; import org.apache.tapestry5.ioc.internal.util.URLChangeTracker; +import org.apache.tapestry5.services.ComponentClassResolver; import org.apache.tapestry5.services.messages.PropertiesFileParser; import org.apache.tapestry5.services.pageload.ComponentResourceLocator; import org.apache.tapestry5.services.pageload.ComponentResourceSelector; - -import java.util.Collections; -import java.util.List; -import java.util.Map; +import org.slf4j.Logger; /** * A utility class that encapsulates all the logic for reading properties files and assembling {@link Messages} from @@ -43,12 +50,18 @@ */ public class MessagesSourceImpl extends InvalidationEventHubImpl implements MessagesSource { - private final URLChangeTracker tracker; + private final URLChangeTracker tracker; private final PropertiesFileParser propertiesFileParser; private final ComponentResourceLocator resourceLocator; - + + private final ComponentClassResolver componentClassResolver; + + private final boolean multipleClassLoaders; + + private final Logger logger; + /** * Keyed on bundle id and ComponentResourceSelector. */ @@ -67,21 +80,97 @@ public class MessagesSourceImpl extends InvalidationEventHubImpl implements Mess private final Map emptyMap = Collections.emptyMap(); - public MessagesSourceImpl(boolean productionMode, URLChangeTracker tracker, - ComponentResourceLocator resourceLocator, PropertiesFileParser propertiesFileParser) + public MessagesSourceImpl(boolean productionMode, boolean multipleClassLoaders, URLChangeTracker tracker, + ComponentResourceLocator resourceLocator, PropertiesFileParser propertiesFileParser, + ComponentClassResolver componentClassResolver, + Logger logger) { - super(productionMode); + super(productionMode, logger); this.tracker = tracker; this.propertiesFileParser = propertiesFileParser; this.resourceLocator = resourceLocator; + this.logger = logger; + this.componentClassResolver = componentClassResolver; + this.multipleClassLoaders = multipleClassLoaders; } public void checkForUpdates() { - if (tracker != null && tracker.containsChanges()) + if (tracker != null) { - invalidate(); + final Set changedResources = tracker.getChangedResourcesInfo(); + if (!changedResources.isEmpty() && logger.isInfoEnabled()) + { + logger.info("Changed message file(s): {}", changedResources.stream() + .map(MessagesTrackingInfo::getResource) + .map(Resource::toString) + .collect(Collectors.joining(", "))); + } + + boolean applicationLevelChange = false; + + for (MessagesTrackingInfo info : changedResources) + { + + final String className = info.getClassName(); + + // An application-level file was changed, so we need to invalidate everything. + if (className == null || !multipleClassLoaders) + { + invalidate(); + applicationLevelChange = true; + break; + } + else + { + + final Iterator> messagesByBundleIdAndSelectorIterator = + messagesByBundleIdAndSelector.entrySet().iterator(); + + while (messagesByBundleIdAndSelectorIterator.hasNext()) + { + final Entry entry = messagesByBundleIdAndSelectorIterator.next(); + if (className.equals(entry.getKey().getValues()[0])) + { + messagesByBundleIdAndSelectorIterator.remove(); + } + } + + final Iterator>> cookedPropertiesIterator = + cookedProperties.entrySet().iterator(); + + while (cookedPropertiesIterator.hasNext()) + { + final Entry> entry = cookedPropertiesIterator.next(); + if (className.equals(entry.getKey().getValues()[0])) + { + cookedPropertiesIterator.remove(); + } + } + + final String resourceFile = info.getResource().getFile(); + final Iterator>> rawPropertiesIterator = rawProperties.entrySet().iterator(); + while (rawPropertiesIterator.hasNext()) + { + final Entry> entry = rawPropertiesIterator.next(); + if (resourceFile.equals(entry.getKey().getFile())) + { + rawPropertiesIterator.remove(); + } + } + + } + } + + if (!changedResources.isEmpty() && !applicationLevelChange) + { + fireInvalidationEvent(changedResources.stream() + .filter(Objects::nonNull) + .map(ClassNameHolder::getClassName) + .filter(Objects::nonNull) + .collect(Collectors.toList())); + } } } @@ -149,9 +238,9 @@ private Map findBundleProperties(MessagesBundle bundle, Componen for (Resource localization : F.flow(localizations).reverse()) { - Map rawProperties = getRawProperties(localization); + Map rawProperties = getRawProperties(localization, bundle); - // Woould be nice to write into the cookedProperties cache here, + // Would be nice to write into the cookedProperties cache here, // but we can't because we don't know the selector part of the MultiKey. previous = extend(previous, rawProperties); @@ -182,13 +271,13 @@ private Map extend(Map base, Map return result; } - private Map getRawProperties(Resource localization) + private Map getRawProperties(Resource localization, MessagesBundle bundle) { Map result = rawProperties.get(localization); if (result == null) { - result = readProperties(localization); + result = readProperties(localization, bundle); rawProperties.put(localization, result); } @@ -198,15 +287,18 @@ private Map getRawProperties(Resource localization) /** * Creates and returns a new map that contains properties read from the properties file. + * @param bundle */ - private Map readProperties(Resource resource) + private Map readProperties(Resource resource, MessagesBundle bundle) { if (!resource.exists()) return emptyMap; if (tracker != null) { - tracker.add(resource.toURL()); + MessagesTrackingInfo info = new MessagesTrackingInfo( + resource, bundle != null ? bundle.getId() : bundle, getClassName(bundle)); + tracker.add(resource.toURL(), info); } try @@ -218,4 +310,22 @@ private Map readProperties(Resource resource) } } + private String getClassName(MessagesBundle bundle) + { + String className = null; + if (bundle != null && bundle.getBaseResource().getPath() != null) + { + final String path = bundle.getBaseResource().getPath(); + if (path.endsWith(".class")) + { + className = path.replace('/', '.').replace(".class", ""); + if (!componentClassResolver.isPage(className)) + { + className = null; + } + } + } + return className; + } + } diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/MessagesTrackingInfo.java b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/MessagesTrackingInfo.java new file mode 100644 index 0000000000..5eead54453 --- /dev/null +++ b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/MessagesTrackingInfo.java @@ -0,0 +1,81 @@ +// 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.Objects; + +import org.apache.tapestry5.commons.Resource; + +/** + * Class that holds information about a messages properties file for tracking. + */ +final public class MessagesTrackingInfo implements ClassNameHolder +{ + + private Object bundleId; + private Resource resource; + private String className; + + public MessagesTrackingInfo(Resource resource, Object bundleId, String className) + { + super(); + this.resource = resource; + this.className = className; + this.bundleId = bundleId; + } + + public Object getBundleId() + { + return bundleId; + } + + public Resource getResource() + { + return resource; + } + + public String getClassName() + { + return className; + } + + @Override + public int hashCode() + { + return Objects.hash(bundleId, className, resource); + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + MessagesTrackingInfo other = (MessagesTrackingInfo) obj; + return Objects.equals(bundleId, other.bundleId) + && Objects.equals(className, other.className) + && Objects.equals(resource, other.resource); + } + + @Override + public String toString() + { + return "MessagesTrackingInfo [resource=" + resource + ", className=" + className + + ", bundleId=" + bundleId + "]"; + } + +} \ No newline at end of file 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 cc15c1658a..fe92fe3df9 100644 --- a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/PageSourceImpl.java +++ b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/PageSourceImpl.java @@ -14,22 +14,35 @@ package org.apache.tapestry5.internal.services; +import java.lang.ref.SoftReference; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +import org.apache.tapestry5.SymbolConstants; import org.apache.tapestry5.commons.services.InvalidationEventHub; import org.apache.tapestry5.commons.util.CollectionFactory; import org.apache.tapestry5.func.F; import org.apache.tapestry5.func.Mapper; +import org.apache.tapestry5.internal.services.ComponentDependencyRegistry.DependencyType; import org.apache.tapestry5.internal.services.assets.ResourceChangeTracker; +import org.apache.tapestry5.internal.structure.ComponentPageElement; import org.apache.tapestry5.internal.structure.Page; import org.apache.tapestry5.ioc.annotations.ComponentClasses; import org.apache.tapestry5.ioc.annotations.PostInjection; +import org.apache.tapestry5.ioc.annotations.Symbol; +import org.apache.tapestry5.services.ComponentClassResolver; import org.apache.tapestry5.services.ComponentMessages; import org.apache.tapestry5.services.ComponentTemplates; import org.apache.tapestry5.services.pageload.ComponentRequestSelectorAnalyzer; import org.apache.tapestry5.services.pageload.ComponentResourceSelector; - -import java.lang.ref.SoftReference; -import java.util.Map; -import java.util.Set; +import org.apache.tapestry5.services.pageload.PageClassLoaderContext; +import org.apache.tapestry5.services.pageload.PageClassLoaderContextManager; +import org.slf4j.Logger; public class PageSourceImpl implements PageSource { @@ -37,6 +50,18 @@ public class PageSourceImpl implements PageSource private final PageLoader pageLoader; + private final ComponentDependencyRegistry componentDependencyRegistry; + + private final ComponentClassResolver componentClassResolver; + + private final PageClassLoaderContextManager pageClassLoaderContextManager; + + private final Logger logger; + + final private boolean productionMode; + + final private boolean multipleClassLoaders; + private static final class CachedPageKey { final String pageName; @@ -66,17 +91,55 @@ public boolean equals(Object obj) return pageName.equals(other.pageName) && selector.equals(other.selector); } + + @Override + public String toString() { + return "CachedPageKey [pageName=" + pageName + ", selector=" + selector + "]"; + } + + } private final Map> pageCache = CollectionFactory.newConcurrentMap(); - public PageSourceImpl(PageLoader pageLoader, ComponentRequestSelectorAnalyzer selectorAnalyzer) + public PageSourceImpl(PageLoader pageLoader, ComponentRequestSelectorAnalyzer selectorAnalyzer, + ComponentDependencyRegistry componentDependencyRegistry, + ComponentClassResolver componentClassResolver, + PageClassLoaderContextManager pageClassLoaderContextManager, + @Symbol(SymbolConstants.PRODUCTION_MODE) boolean productionMode, + @Symbol(SymbolConstants.MULTIPLE_CLASSLOADERS) boolean multipleClassLoaders, + Logger logger) { this.pageLoader = pageLoader; this.selectorAnalyzer = selectorAnalyzer; + this.componentDependencyRegistry = componentDependencyRegistry; + this.componentClassResolver = componentClassResolver; + this.productionMode = productionMode; + this.multipleClassLoaders = multipleClassLoaders; + this.pageClassLoaderContextManager = pageClassLoaderContextManager; + this.logger = logger; } - + public Page getPage(String canonicalPageName) + { + if (!productionMode) + { + componentDependencyRegistry.disableInvalidations(); + } + try + { + return getPage(canonicalPageName, true); + } + finally + { + if (!productionMode) + { + componentDependencyRegistry.enableInvalidations(); + } + } + } + + public Page getPage(String canonicalPageName, boolean invalidateUnknownContext) { ComponentResourceSelector selector = selectorAnalyzer.buildSelectorForRequest(); @@ -85,7 +148,7 @@ public Page getPage(String canonicalPageName) // The while loop looks superfluous, but it helps to ensure that the Page instance, // with all of its mutable construction-time state, is properly published to other // threads (at least, as I understand Brian Goetz's explanation, it should be). - + while (true) { SoftReference ref = pageCache.get(key); @@ -96,6 +159,21 @@ public Page getPage(String canonicalPageName) { return page; } + + final String className = componentClassResolver.resolvePageNameToClassName(canonicalPageName); + if (multipleClassLoaders) + { + + // Avoiding problems in PlasticClassPool.createTransformation() + // when the class being loaded has a page superclass + final List pageDependencies = preprocessPageDependencies(className); + + for (String pageClassName : pageDependencies) + { + page = getPage(componentClassResolver.resolvePageClassNameToPageName(pageClassName), false); + } + + } // In rare race conditions, we may see the same page loaded multiple times across // different threads. The last built one will "evict" the others from the page cache, @@ -106,7 +184,83 @@ public Page getPage(String canonicalPageName) ref = new SoftReference(page); pageCache.put(key, ref); + + if (!productionMode) + { + final ComponentPageElement rootElement = page.getRootElement(); + componentDependencyRegistry.clear(rootElement); + componentDependencyRegistry.register(rootElement); + PageClassLoaderContext context = pageClassLoaderContextManager.get(className); + + if (context.isUnknown() && multipleClassLoaders) + { + this.pageCache.remove(key); + if (invalidateUnknownContext) + { + pageClassLoaderContextManager.invalidateAndFireInvalidationEvents(context); + preprocessPageDependencies(className); + } + context.getClassNames().clear(); + // Avoiding bad invalidations + return getPage(canonicalPageName, false); + } + } + + } + + + } + + private List preprocessPageDependencies(final String className) { + final List pageDependencies = new ArrayList<>(); + pageDependencies.addAll( + new ArrayList(componentDependencyRegistry.getDependencies(className, DependencyType.INJECT_PAGE))); + pageDependencies.addAll( + new ArrayList(componentDependencyRegistry.getDependencies(className, DependencyType.SUPERCLASS))); + + final Iterator iterator = pageDependencies.iterator(); + while (iterator.hasNext()) + { + if (!iterator.next().contains(".pages.")) + { + iterator.remove(); + } } + + preprocessPageClassLoaderContexts(className, pageDependencies); + return pageDependencies; + } + + private void preprocessPageClassLoaderContexts(String className, final List pageDependencies) { + for (int i = 0; i < 5; i++) + { + pageClassLoaderContextManager.get(className); + for (String pageClassName : pageDependencies) + { + final PageClassLoaderContext context = pageClassLoaderContextManager.get(pageClassName); + if (i == 1) + { + try + { + context.getClassLoader().loadClass(pageClassName); + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + } + } + } + + // TODO: remove +// for (String pageClassName : pageDependencies) +// { +// try +// { +// pageClassLoaderContextManager.get(pageClassName).getClassLoader().loadClass(pageClassName); +// } catch (ClassNotFoundException e) +// { +// throw new RuntimeException(e); +// } +// } } @PostInjection @@ -115,19 +269,56 @@ public void setupInvalidation(@ComponentClasses InvalidationEventHub classesHub, @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 // checksum as part of the asset URL), then when we notice a change to // any Resource, it is necessary to discard all page instances. - resourceChangeTracker.clearOnInvalidation(pageCache); + // From 5.8.3 on, Tapestry tries to only invalidate the components and pages known as + // using the changed resources. If a given resource is changed but not associated with any + // component, then all of them are invalidated. + resourceChangeTracker.addInvalidationCallback(this::listen); + } + + private List listen(List resources) + { + + if (resources.isEmpty()) + { + clearCache(); + } + else + { + String pageName; + for (String className : resources) + { + if (componentClassResolver.isPage(className)) + { + pageName = componentClassResolver.resolvePageClassNameToPageName(className); + final Iterator>> iterator = pageCache.entrySet().iterator(); + while (iterator.hasNext()) + { + final Entry> entry = iterator.next(); + final String entryPageName = entry.getKey().pageName; + if (entryPageName.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 91a8fec2c5..d0266b0883 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/RequestPageCacheImpl.java b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/RequestPageCacheImpl.java index 457b409496..99563acb08 100644 --- a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/RequestPageCacheImpl.java +++ b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/RequestPageCacheImpl.java @@ -14,20 +14,24 @@ package org.apache.tapestry5.internal.services; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import org.apache.tapestry5.commons.services.InvalidationEventHub; import org.apache.tapestry5.commons.util.CollectionFactory; import org.apache.tapestry5.commons.util.ExceptionUtils; import org.apache.tapestry5.http.services.RequestGlobals; import org.apache.tapestry5.internal.InternalConstants; import org.apache.tapestry5.internal.structure.Page; import org.apache.tapestry5.ioc.ScopeConstants; +import org.apache.tapestry5.ioc.annotations.ComponentClasses; import org.apache.tapestry5.ioc.annotations.PostInjection; import org.apache.tapestry5.ioc.annotations.Scope; import org.apache.tapestry5.ioc.services.PerthreadManager; import org.apache.tapestry5.services.ComponentClassResolver; import org.slf4j.Logger; -import java.util.Map; - /** * In Tapestry 5.1, the implementation of this worked with the page pool (a pool of page instances, reserved * to individual requests/threads). Page pooling was deprecated in 5.2 and removed in 5.3. @@ -36,6 +40,8 @@ */ @Scope(ScopeConstants.PERTHREAD) public class RequestPageCacheImpl implements RequestPageCache, Runnable + +/// This should have a listener too! { private final Logger logger; @@ -46,8 +52,9 @@ public class RequestPageCacheImpl implements RequestPageCache, Runnable private final RequestGlobals requestGlobals; private final Map cache = CollectionFactory.newMap(); - - public RequestPageCacheImpl(Logger logger, ComponentClassResolver resolver, PageSource pageSource, RequestGlobals requestGlobals) + + public RequestPageCacheImpl(Logger logger, ComponentClassResolver resolver, + PageSource pageSource, RequestGlobals requestGlobals) { this.logger = logger; this.resolver = resolver; @@ -56,9 +63,11 @@ public RequestPageCacheImpl(Logger logger, ComponentClassResolver resolver, Page } @PostInjection - public void listenForThreadCleanup(PerthreadManager perthreadManager) + public void listenForThreadCleanup(PerthreadManager perthreadManager, + @ComponentClasses InvalidationEventHub classesHub) { perthreadManager.addThreadCleanupCallback(this); + classesHub.addInvalidationCallback(this::listen); } public void run() @@ -105,4 +114,20 @@ public Page get(String pageName) return page; } + + private List listen(List resources) + { + // TODO: we probably don't need this anymore + for (String resource : resources) + { + if (resolver.isPage(resource)) + { + final String canonicalName = resolver.canonicalizePageName( + resolver.getLogicalName(resource)); + cache.remove(canonicalName); + } + } + return Collections.emptyList(); + } + } 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 11aa54c3df..eb0e78939d 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 @@ 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,15 @@ public void addInvalidationCallback(Runnable callback) public void clearOnInvalidation(Map map) { } + + @Override + public void addInvalidationCallback(Function, List> function) + { + } + + @Override + public void fireInvalidationEvent(List resources) + { + } + } diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/TemplateTrackingInfo.java b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/TemplateTrackingInfo.java new file mode 100644 index 0000000000..d750af02a5 --- /dev/null +++ b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/TemplateTrackingInfo.java @@ -0,0 +1,70 @@ +// 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.Objects; + +/** + * Class that holds information about a template for tracking. + */ +final public class TemplateTrackingInfo implements ClassNameHolder +{ + + private String template; + private String className; + + public TemplateTrackingInfo(String template, String className) + { + super(); + this.template = template; + this.className = className; + } + + public String getTemplate() + { + return template; + } + + public String getClassName() + { + return className; + } + + @Override + public int hashCode() + { + return Objects.hash(className, template); + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) { + return true; + } + if (!(obj instanceof TemplateTrackingInfo)) + { + return false; + } + TemplateTrackingInfo other = (TemplateTrackingInfo) obj; + return Objects.equals(className, other.className) && Objects.equals(template, other.template); + } + + @Override + public String toString() + { + return "TemplateTrackingInfo [template=" + template + ", className=" + className + "]"; + } + +} \ No newline at end of file diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/assets/ResourceChangeTracker.java b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/assets/ResourceChangeTracker.java index 3c0ded1e8c..9f4eb4f499 100644 --- a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/assets/ResourceChangeTracker.java +++ b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/assets/ResourceChangeTracker.java @@ -1,4 +1,4 @@ -// Copyright 2011, 2012 The Apache Software Foundation +// Copyright 2011, 2012, 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. @@ -51,4 +51,21 @@ public interface ResourceChangeTracker extends InvalidationEventHub, ResourceDep * @since 5.4 */ void forceInvalidationEvent(); + + /** + * Informs this service that the resources being loaded are associated with a given Tapestry + * component (i.e. component, page, mixin and base) component class. + * + * @param className The fully classified class name of the component or page associated with + * the current resources being processed. + * @since 5.8.3 + */ + void setCurrentClassName(String className); + + /** + * Informs this service that no component class is associated with the resources being loaded. + * @since 5.8.3 + */ + void clearCurrentClassName(); + } 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 917af3a7ec..73d4d26440 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 @@ -1,4 +1,4 @@ -// Copyright 2011, 2012 The Apache Software Foundation +// Copyright 2011, 2012, 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. @@ -14,20 +14,35 @@ package org.apache.tapestry5.internal.services.assets; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +import org.apache.tapestry5.SymbolConstants; import org.apache.tapestry5.commons.Resource; import org.apache.tapestry5.http.TapestryHttpSymbolConstants; import org.apache.tapestry5.internal.event.InvalidationEventHubImpl; +import org.apache.tapestry5.internal.services.ClassNameHolder; import org.apache.tapestry5.ioc.annotations.PostInjection; import org.apache.tapestry5.ioc.annotations.Symbol; 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 { - private final URLChangeTracker tracker; + private final URLChangeTracker tracker; + + private final ThreadLocal currentClassName; + + private final Logger logger; + + private final boolean multipleClassLoaders; /** * Used in production mode as the last modified time of any resource exposed to the client. Remember that @@ -38,30 +53,36 @@ public class ResourceChangeTrackerImpl extends InvalidationEventHubImpl implemen public ResourceChangeTrackerImpl(ClasspathURLConverter classpathURLConverter, @Symbol(TapestryHttpSymbolConstants.PRODUCTION_MODE) - boolean productionMode) + boolean productionMode, + @Symbol(SymbolConstants.MULTIPLE_CLASSLOADERS) + boolean multipleClassLoaders, Logger logger) { - super(productionMode); + super(productionMode, logger); + this.logger = logger; + this.multipleClassLoaders = multipleClassLoaders; // Use granularity of seconds (not milliseconds) since that works properly // with response headers for identifying last modified. Don't track // folder changes, just changes to actual files. - tracker = productionMode ? null : new URLChangeTracker(classpathURLConverter, true, false); + tracker = productionMode ? null : new URLChangeTracker(classpathURLConverter, true, false); + currentClassName = productionMode ? null : new ThreadLocal<>(); } - + @PostInjection public void registerWithUpdateListenerHub(UpdateListenerHub hub) { hub.addUpdateListener(this); } + public long trackResource(Resource resource) { if (tracker == null) { return fixedLastModifiedTime; } - - return tracker.add(resource.toURL()); + + return tracker.add(resource.toURL(), new ResourceInfo(resource.toString(), currentClassName.get())); } public void addDependency(Resource dependency) @@ -81,10 +102,113 @@ public void forceInvalidationEvent() public void checkForUpdates() { - if (tracker.containsChanges()) + if (tracker != null) + { + final Set changedResources = tracker.getChangedResourcesInfo(); + if (!changedResources.isEmpty()) + { + logger.info("Changed resources: {}", changedResources.stream() + .map(ResourceInfo::getResource) + .collect(Collectors.joining(", "))); + } + + boolean applicationLevelChange = false; + + for (ResourceInfo info : changedResources) + { + + // An application-level file was changed, so we need to invalidate everything. + if (info.getClassName() == null || !multipleClassLoaders) + { + forceInvalidationEvent(); + applicationLevelChange = true; + break; + } + + } + + if (!changedResources.isEmpty() && !applicationLevelChange) + { + List resources = new ArrayList<>(4); + resources.addAll(changedResources.stream() + .filter(Objects::nonNull) + .map(ResourceInfo::getResource) + .filter(Objects::nonNull) + .collect(Collectors.toList())); + resources.addAll(changedResources.stream() + .filter(Objects::nonNull) + .map(ClassNameHolder::getClassName) + .filter(Objects::nonNull) + .collect(Collectors.toList())); + fireInvalidationEvent(resources); + } + } + } + + @Override + public void setCurrentClassName(String className) + { + if (currentClassName != null) + { + currentClassName.set(className); + } + } + + @Override + public void clearCurrentClassName() + { + if (currentClassName != null) { - forceInvalidationEvent(); + currentClassName.set(null); } } + + private static class ResourceInfo implements ClassNameHolder + { + private String resource; + private String className; + public ResourceInfo(String resource, String className) + { + super(); + this.className = className; + this.resource = resource; + } + + @Override + public int hashCode() + { + return Objects.hash(className, resource); + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + ResourceInfo other = (ResourceInfo) obj; + return Objects.equals(className, other.className) && Objects.equals(resource, other.resource); + } + + @Override + public String toString() { + return "ResourceInfo [path=" + resource + ", className=" + className + "]"; + } + + public String getResource() + { + return resource; + } + + public String getClassName() + { + return className; + } + + } + } diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/rest/DefaultOpenApiDescriptionGenerator.java b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/rest/DefaultOpenApiDescriptionGenerator.java index b3bdb539b4..d0bd12f66d 100644 --- a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/rest/DefaultOpenApiDescriptionGenerator.java +++ b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/rest/DefaultOpenApiDescriptionGenerator.java @@ -595,10 +595,10 @@ public Method findMethod(Class pageClass, final String name, List para } } } - if (method == null && pageClass.getName().equals("org.apache.tapestry5.integration.app1.pages.rest.RestTypeDescriptionsDemo")) - { - System.out.println("WTF!"); - } +// if (method == null && pageClass.getName().equals("org.apache.tapestry5.integration.app1.pages.rest.RestTypeDescriptionsDemo")) +// { +// System.out.println("WTF!"); +// } // In case of the same class being loaded from different classloaders, // let's try to find the method in a different way. // if (method == null) 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 0152e401ec..1e17fe90b8 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.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 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 151b5c8aff..36fd6bc6ec 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 ComponentPageElement getEmbeddedElement(String embeddedId) if (embeddedElement == null) { - Set ids = CollectionFactory.newSet(); - - if (children != null) - { - for (ComponentPageElement child : children) - { - ids.add(child.getId()); - } - } + Set 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 ComponentPageElement getEmbeddedElement(String embeddedId) return embeddedElement; } + @Override + public Set getEmbeddedElementIds() { + Set 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/internal/transform/ImportWorker.java b/tapestry-core/src/main/java/org/apache/tapestry5/internal/transform/ImportWorker.java index a3801a6ea6..e47553fbde 100644 --- a/tapestry-core/src/main/java/org/apache/tapestry5/internal/transform/ImportWorker.java +++ b/tapestry-core/src/main/java/org/apache/tapestry5/internal/transform/ImportWorker.java @@ -14,11 +14,13 @@ import org.apache.tapestry5.Asset; import org.apache.tapestry5.ComponentResources; +import org.apache.tapestry5.SymbolConstants; import org.apache.tapestry5.annotations.Import; import org.apache.tapestry5.annotations.SetupRender; import org.apache.tapestry5.func.F; import org.apache.tapestry5.func.Mapper; import org.apache.tapestry5.func.Worker; +import org.apache.tapestry5.internal.services.assets.ResourceChangeTracker; import org.apache.tapestry5.ioc.services.SymbolSource; import org.apache.tapestry5.model.MutableComponentModel; import org.apache.tapestry5.plastic.*; @@ -44,6 +46,10 @@ public class ImportWorker implements ComponentClassTransformWorker2 private final SymbolSource symbolSource; private final AssetSource assetSource; + + private final ResourceChangeTracker resourceChangeTracker; + + private final boolean multipleClassLoaders; private final Worker importLibrary = new Worker() { @@ -69,21 +75,29 @@ public String map(String element) } }; - public ImportWorker(JavaScriptSupport javascriptSupport, SymbolSource symbolSource, AssetSource assetSource) + public ImportWorker(JavaScriptSupport javascriptSupport, SymbolSource symbolSource, AssetSource assetSource, + ResourceChangeTracker resourceChangeTracker) { this.javascriptSupport = javascriptSupport; this.symbolSource = symbolSource; this.assetSource = assetSource; + this.resourceChangeTracker = resourceChangeTracker; + this.multipleClassLoaders = + !Boolean.valueOf(symbolSource.valueForSymbol(SymbolConstants.PRODUCTION_MODE)) && + Boolean.valueOf(symbolSource.valueForSymbol(SymbolConstants.MULTIPLE_CLASSLOADERS)); } public void transform(PlasticClass componentClass, TransformationSupport support, MutableComponentModel model) { + resourceChangeTracker.setCurrentClassName(model.getComponentClassName()); processClassAnnotationAtSetupRenderPhase(componentClass, model); for (PlasticMethod m : componentClass.getMethodsWithAnnotation(Import.class)) { decorateMethod(componentClass, model, m); } + + resourceChangeTracker.clearCurrentClassName(); } private void processClassAnnotationAtSetupRenderPhase(PlasticClass componentClass, MutableComponentModel model) @@ -112,8 +126,6 @@ private void decorateMethod(PlasticClass componentClass, MutableComponentModel m { importStacks(method, annotation.stack()); - String libraryName = model.getLibraryName(); - importLibraries(componentClass, model, method, annotation.library()); importStylesheets(componentClass, model, method, annotation.stylesheet()); @@ -264,6 +276,7 @@ public Asset map(String assetPath) private void addMethodAssetOperationAdvice(PlasticMethod method, final FieldHandle access, final Worker operation) { + final String className = method.getPlasticClass().getClassName(); method.addAdvice(new MethodAdvice() { public void advise(MethodInvocation invocation) @@ -272,7 +285,17 @@ public void advise(MethodInvocation invocation) Asset[] assets = (Asset[]) access.get(invocation.getInstance()); + if (multipleClassLoaders) + { + resourceChangeTracker.setCurrentClassName(className); + } + F.flow(assets).each(operation); + + if (multipleClassLoaders) + { + resourceChangeTracker.clearCurrentClassName(); + } } }); } diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/internal/transform/InjectComponentWorker.java b/tapestry-core/src/main/java/org/apache/tapestry5/internal/transform/InjectComponentWorker.java index 96525242fc..b18518ca9d 100644 --- a/tapestry-core/src/main/java/org/apache/tapestry5/internal/transform/InjectComponentWorker.java +++ b/tapestry-core/src/main/java/org/apache/tapestry5/internal/transform/InjectComponentWorker.java @@ -16,6 +16,7 @@ import org.apache.tapestry5.ComponentResources; import org.apache.tapestry5.annotations.InjectComponent; +import org.apache.tapestry5.commons.util.DifferentClassVersionsException; import org.apache.tapestry5.commons.util.UnknownValueException; import org.apache.tapestry5.internal.services.ComponentClassCache; import org.apache.tapestry5.ioc.internal.util.InternalUtils; @@ -24,6 +25,7 @@ import org.apache.tapestry5.runtime.Component; import org.apache.tapestry5.services.transform.ComponentClassTransformWorker2; import org.apache.tapestry5.services.transform.TransformationSupport; +import org.slf4j.Logger; /** * Recognizes the {@link org.apache.tapestry5.annotations.InjectComponent} annotation, and converts the field into a @@ -77,15 +79,29 @@ private void load() fieldName, getComponentClassName(), ex.getMessage()), ex); } + @SuppressWarnings("rawtypes") Class fieldType = classCache.forName(type); if (!fieldType.isInstance(embedded)) - throw new RuntimeException( - String - .format( - "Unable to inject component '%s' into field %s of %s. Class %s is not assignable to a field of type %s.", - componentId, fieldName, getComponentClassName(), - embedded.getClass().getName(), fieldType.getName())); + { + final String className = fieldType.getName(); + final String message = String + .format( + "Unable to inject component '%s' into field %s of %s. Class %s is not assignable to a field of type %s.", + componentId, fieldName, getComponentClassName(), + embedded.getClass().getName(), className); + if (embedded.getClass().getName().equals(className)) + { + logger.warn(message); + throw new DifferentClassVersionsException(message, className, + fieldType.getClassLoader(), embedded.getClass().getClassLoader()); + } + else + { + throw new RuntimeException(message); + } + } + } private String getComponentClassName() @@ -100,10 +116,13 @@ public Object get(Object instance, InstanceContext context) } private final ComponentClassCache classCache; - - public InjectComponentWorker(ComponentClassCache classCache) + + private final Logger logger; + + public InjectComponentWorker(ComponentClassCache classCache, final Logger logger) { this.classCache = classCache; + this.logger = logger; } public void transform(PlasticClass plasticClass, TransformationSupport support, MutableComponentModel model) diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/internal/transform/InjectPageWorker.java b/tapestry-core/src/main/java/org/apache/tapestry5/internal/transform/InjectPageWorker.java index dc36b58f40..e7f4ba399c 100644 --- a/tapestry-core/src/main/java/org/apache/tapestry5/internal/transform/InjectPageWorker.java +++ b/tapestry-core/src/main/java/org/apache/tapestry5/internal/transform/InjectPageWorker.java @@ -1,4 +1,4 @@ -// Copyright 2006, 2007, 2008, 2010, 2011 The Apache Software Foundation +// Copyright 2006, 2007, 2008, 2010, 2011, 2023 The Apache Software Foundation // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ import org.apache.tapestry5.annotations.InjectPage; import org.apache.tapestry5.commons.ObjectCreator; +import org.apache.tapestry5.internal.services.ComponentDependencyRegistry; import org.apache.tapestry5.ioc.internal.util.InternalUtils; import org.apache.tapestry5.ioc.services.PerthreadManager; import org.apache.tapestry5.model.MutableComponentModel; @@ -65,22 +66,26 @@ public Object get(Object instance, InstanceContext context) private final PerthreadManager perThreadManager; - public InjectPageWorker(ComponentSource componentSource, ComponentClassResolver resolver, PerthreadManager perThreadManager) + private final ComponentDependencyRegistry componentDependencyRegistry; + + public InjectPageWorker(ComponentSource componentSource, ComponentClassResolver resolver, PerthreadManager perThreadManager, + ComponentDependencyRegistry componentDependencyRegistry) { this.componentSource = componentSource; this.resolver = resolver; this.perThreadManager = perThreadManager; + this.componentDependencyRegistry = componentDependencyRegistry; } public void transform(PlasticClass plasticClass, TransformationSupport support, MutableComponentModel model) { for (PlasticField field : plasticClass.getFieldsWithAnnotation(InjectPage.class)) { - addInjectedPage(field); + addInjectedPage(field, model); } } - private void addInjectedPage(PlasticField field) + private void addInjectedPage(PlasticField field, MutableComponentModel model) { InjectPage annotation = field.getAnnotation(InjectPage.class); @@ -94,5 +99,7 @@ private void addInjectedPage(PlasticField field) .resolvePageClassNameToPageName(field.getTypeName()) : pageName; field.setConduit(new InjectedPageConduit(field.getPlasticClass().getClassName(), fieldName, injectedPageName)); + + componentDependencyRegistry.register(field, model); } } diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/internal/util/MessageCatalogResource.java b/tapestry-core/src/main/java/org/apache/tapestry5/internal/util/MessageCatalogResource.java index ef89f8cfe8..8992efe59f 100644 --- a/tapestry-core/src/main/java/org/apache/tapestry5/internal/util/MessageCatalogResource.java +++ b/tapestry-core/src/main/java/org/apache/tapestry5/internal/util/MessageCatalogResource.java @@ -54,7 +54,11 @@ public void run() // but that's the breaks). When that occurs, we tell the ResourceChangeTracker to fire its invalidation // event. That flushes out all the assets it has cached, including StreamableResources for JavaScript files, // including the one created here to represent the application message catalog. - changeTracker.forceInvalidationEvent(); + + // TAP-2742: now we're doing smarter page cache invalidation, + // so we don't need to clear everything anymore. + + // changeTracker.forceInvalidationEvent(); } }); } diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/modules/DashboardModule.java b/tapestry-core/src/main/java/org/apache/tapestry5/modules/DashboardModule.java index 002fea71d6..c4985b510c 100644 --- a/tapestry-core/src/main/java/org/apache/tapestry5/modules/DashboardModule.java +++ b/tapestry-core/src/main/java/org/apache/tapestry5/modules/DashboardModule.java @@ -1,4 +1,4 @@ -// Copyright 2013, 2014 The Apache Software Foundation +// Copyright 2013, 2014, 2023 The Apache Software Foundation // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -15,6 +15,8 @@ package org.apache.tapestry5.modules; import org.apache.tapestry5.commons.OrderedConfiguration; +import org.apache.tapestry5.internal.services.ComponentDependencyGraphvizGenerator; +import org.apache.tapestry5.internal.services.ComponentDependencyGraphvizGeneratorImpl; import org.apache.tapestry5.internal.services.dashboard.DashboardManagerImpl; import org.apache.tapestry5.ioc.ServiceBinder; import org.apache.tapestry5.ioc.annotations.Contribute; @@ -26,6 +28,7 @@ public class DashboardModule public static void bind(ServiceBinder binder) { binder.bind(DashboardManager.class, DashboardManagerImpl.class); + binder.bind(ComponentDependencyGraphvizGenerator.class, ComponentDependencyGraphvizGeneratorImpl.class); } @Contribute(DashboardManager.class) @@ -34,5 +37,6 @@ public static void defaultTabs(OrderedConfiguration configuration) configuration.add("Pages", new DashboardTab("Pages", "core/PageCatalog")); configuration.add("Services", new DashboardTab("Services", "core/ServiceStatus")); configuration.add("Libraries", new DashboardTab("ComponentLibraries", "core/ComponentLibraries")); + configuration.add("PageDependencyGraph", new DashboardTab("PageDependencyGraph", "core/PageDependencyGraph")); } } diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/modules/InternalModule.java b/tapestry-core/src/main/java/org/apache/tapestry5/modules/InternalModule.java index 0b9b738065..122068abac 100644 --- a/tapestry-core/src/main/java/org/apache/tapestry5/modules/InternalModule.java +++ b/tapestry-core/src/main/java/org/apache/tapestry5/modules/InternalModule.java @@ -116,7 +116,7 @@ public static void bind(ServiceBinder binder) binder.bind(AjaxFormUpdateController.class); binder.bind(ResourceDigestManager.class, ResourceDigestManagerImpl.class); // Remove in Tapestry 5.5 binder.bind(RequestPageCache.class, RequestPageCacheImpl.class); - binder.bind(ComponentInstantiatorSource.class); + binder.bind(ComponentInstantiatorSource.class).eagerLoad(); binder.bind(InternalComponentInvalidationEventHub.class); binder.bind(PageSource.class, PageSourceImpl.class); binder.bind(PageLoader.class, PageLoaderImpl.class).preventReloading(); diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/modules/PageLoadModule.java b/tapestry-core/src/main/java/org/apache/tapestry5/modules/PageLoadModule.java index 597a38afc2..1a993ab034 100644 --- a/tapestry-core/src/main/java/org/apache/tapestry5/modules/PageLoadModule.java +++ b/tapestry-core/src/main/java/org/apache/tapestry5/modules/PageLoadModule.java @@ -13,10 +13,12 @@ package org.apache.tapestry5.modules; import org.apache.tapestry5.SymbolConstants; +import org.apache.tapestry5.commons.MappedConfiguration; import org.apache.tapestry5.http.TapestryHttpSymbolConstants; import org.apache.tapestry5.internal.pageload.DefaultComponentRequestSelectorAnalyzer; import org.apache.tapestry5.internal.pageload.DefaultComponentResourceLocator; import org.apache.tapestry5.internal.pageload.PagePreloaderImpl; +import org.apache.tapestry5.internal.services.ComponentDependencyRegistry; import org.apache.tapestry5.internal.services.ComponentTemplateSource; import org.apache.tapestry5.internal.services.ComponentTemplateSourceImpl; import org.apache.tapestry5.ioc.ServiceBinder; @@ -26,6 +28,8 @@ import org.apache.tapestry5.services.Core; import org.apache.tapestry5.services.pageload.ComponentRequestSelectorAnalyzer; import org.apache.tapestry5.services.pageload.ComponentResourceLocator; +import org.apache.tapestry5.services.pageload.PageClassLoaderContextManager; +import org.apache.tapestry5.services.pageload.PageClassLoaderContextManagerImpl; import org.apache.tapestry5.services.pageload.PagePreloader; import org.apache.tapestry5.services.pageload.PreloaderMode; @@ -35,12 +39,22 @@ @Marker(Core.class) public class PageLoadModule { + + /** + * Contributes factory defaults that may be overridden. + */ + public static void contributeFactoryDefaults(MappedConfiguration configuration) + { + configuration.add(SymbolConstants.MULTIPLE_CLASSLOADERS, false); + } + public static void bind(ServiceBinder binder) { binder.bind(ComponentRequestSelectorAnalyzer.class, DefaultComponentRequestSelectorAnalyzer.class); binder.bind(ComponentResourceLocator.class, DefaultComponentResourceLocator.class); binder.bind(ComponentTemplateSource.class, ComponentTemplateSourceImpl.class); binder.bind(PagePreloader.class, PagePreloaderImpl.class); + binder.bind(PageClassLoaderContextManager.class, PageClassLoaderContextManagerImpl.class); } @Startup @@ -55,4 +69,25 @@ public static void preloadPages(PagePreloader preloader, preloader.preloadPages(); } } + + @Startup + public void preloadPageClassLoaderContexts( + PageClassLoaderContextManager pageClassLoaderContextManager, + ComponentDependencyRegistry componentDependencyRegistry, + @Symbol(SymbolConstants.PRODUCTION_MODE) boolean productionMode, + @Symbol(SymbolConstants.MULTIPLE_CLASSLOADERS) boolean multipleClassLoaders) + { + if (!productionMode && multipleClassLoaders) + { + // Preload the page activation context tree for the already known classes + for (int i = 0; i < 5; i++) + { + for (String className : componentDependencyRegistry.getClassNames()) + { + pageClassLoaderContextManager.get(className); + } + } + } + } + } 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 34b9bec4ce..467bcd2a33 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 @@ -20,15 +20,19 @@ import java.net.URL; import java.text.DateFormat; import java.text.SimpleDateFormat; +import java.util.Arrays; import java.util.Calendar; import java.util.Collection; +import java.util.Collections; import java.util.Date; +import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Properties; import java.util.Set; import java.util.regex.Pattern; +import java.util.stream.Collectors; import org.apache.tapestry5.Asset; import org.apache.tapestry5.BindingConstants; @@ -104,7 +108,12 @@ import org.apache.tapestry5.commons.util.AvailableValues; import org.apache.tapestry5.commons.util.CollectionFactory; import org.apache.tapestry5.commons.util.StrategyRegistry; +import org.apache.tapestry5.corelib.components.BeanEditor; +import org.apache.tapestry5.corelib.components.PropertyDisplay; +import org.apache.tapestry5.corelib.components.PropertyEditor; import org.apache.tapestry5.corelib.data.SecureOption; +import org.apache.tapestry5.corelib.pages.PropertyDisplayBlocks; +import org.apache.tapestry5.corelib.pages.PropertyEditBlocks; import org.apache.tapestry5.grid.GridConstants; import org.apache.tapestry5.grid.GridDataSource; import org.apache.tapestry5.http.Link; @@ -163,6 +172,7 @@ 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; @@ -362,10 +372,12 @@ import org.apache.tapestry5.services.meta.FixedExtractor; import org.apache.tapestry5.services.meta.MetaDataExtractor; import org.apache.tapestry5.services.meta.MetaWorker; +import org.apache.tapestry5.services.pageload.PageClassLoaderContextManager; +import org.apache.tapestry5.services.pageload.PageClassLoaderContextManagerImpl; import org.apache.tapestry5.services.pageload.PreloaderMode; +import org.apache.tapestry5.services.rest.MappedEntityManager; import org.apache.tapestry5.services.rest.OpenApiDescriptionGenerator; import org.apache.tapestry5.services.rest.OpenApiTypeDescriber; -import org.apache.tapestry5.services.rest.MappedEntityManager; import org.apache.tapestry5.services.security.ClientWhitelist; import org.apache.tapestry5.services.security.WhitelistAnalyzer; import org.apache.tapestry5.services.templates.ComponentTemplateLocator; @@ -516,7 +528,7 @@ public static void bind(ServiceBinder binder) binder.bind(AjaxResponseRenderer.class, AjaxResponseRendererImpl.class); binder.bind(AlertManager.class, AlertManagerImpl.class); binder.bind(ValidationDecoratorFactory.class, ValidationDecoratorFactoryImpl.class); - binder.bind(PropertyConduitSource.class, PropertyConduitSourceImpl.class); + binder.bind(PropertyConduitSource.class, PropertyConduitSourceImpl.class).eagerLoad(); binder.bind(ClientWhitelist.class, ClientWhitelistImpl.class); binder.bind(MetaDataLocator.class, MetaDataLocatorImpl.class); binder.bind(ComponentClassCache.class, ComponentClassCacheImpl.class); @@ -665,7 +677,8 @@ public static void provideCoreAndAppLibraries(Configuration conf public static void provideTransformWorkers( OrderedConfiguration configuration, MetaWorker metaWorker, - ComponentClassResolver resolver) + ComponentClassResolver resolver, + ComponentDependencyRegistry componentDependencyRegistry) { configuration.add("Property", new PropertyWorker()); @@ -954,7 +967,9 @@ public void contributeHttpServletRequestHandler(OrderedConfiguration configuration, Context context, @Symbol(TapestryHttpSymbolConstants.PRODUCTION_MODE) - boolean productionMode) + boolean productionMode, + + final PageClassLoaderContextManager pageClassLoaderContextManager) { RequestFilter staticFilesFilter = new StaticFilesFilter(context); @@ -997,6 +1012,7 @@ public boolean service(Request request, Response response, RequestHandler handle configuration.add("EndOfRequest", fireEndOfRequestEvent); configuration.addInstance("ErrorFilter", RequestErrorFilter.class); + } /** @@ -2766,7 +2782,33 @@ public static void contributeMappedEntityManager(Configuration configura { configuration.add(appRootPackage + ".rest.entities"); } - + + public static ComponentDependencyRegistry buildComponentDependencyRegistry( + InternalComponentInvalidationEventHub internalComponentInvalidationEventHub, + ResourceChangeTracker resourceChangeTracker, + ComponentTemplateSource componentTemplateSource, + PageClassLoaderContextManager pageClassLoaderContextManager, + ComponentInstantiatorSource componentInstantiatorSource, + ComponentClassResolver componentClassResolver, + TemplateParser templateParser, + ComponentTemplateLocator componentTemplateLocator, + PerthreadManager perthreadManager) + { + ComponentDependencyRegistryImpl componentDependencyRegistry = + new ComponentDependencyRegistryImpl( + pageClassLoaderContextManager, + componentInstantiatorSource.getProxyFactory().getPlasticManager(), + componentClassResolver, + templateParser, + componentTemplateLocator); + componentDependencyRegistry.listen(internalComponentInvalidationEventHub); + componentDependencyRegistry.listen(resourceChangeTracker); + componentDependencyRegistry.listen(componentTemplateSource.getInvalidationEventHub()); + // TODO: remove + componentDependencyRegistry.setupThreadCleanup(perthreadManager); + return componentDependencyRegistry; + } + private static final class TapestryCoreComponentLibraryInfoSource implements ComponentLibraryInfoSource { @@ -2787,8 +2829,8 @@ public ComponentLibraryInfo find(LibraryMapping libraryMapping) info.setDescription("Components provided out-of-the-box by Tapestry"); info.setDocumentationUrl("http://tapestry.apache.org/component-reference.html"); info.setJavadocUrl("http://tapestry.apache.org/current/apidocs/"); - info.setSourceBrowseUrl("https://git-wip-us.apache.org/repos/asf?p=tapestry-5.git;a=summary"); - info.setSourceRootUrl("https://git-wip-us.apache.org/repos/asf?p=tapestry-5.git;a=blob;f=tapestry-core/src/main/java/"); + info.setSourceBrowseUrl("https://gitbox.apache.org/repos/asf?p=tapestry-5.git;a=summary"); + info.setSourceRootUrl("https://gitbox.apache.org/repos/asf?p=tapestry-5.git;a=blob;f=tapestry-core/src/main/java/"); info.setIssueTrackerUrl("https://issues.apache.org/jira/browse/TAP5"); info.setHomepageUrl("http://tapestry.apache.org"); info.setLibraryMapping(libraryMapping); 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 e84d175e96..62e81f63d2 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,19 @@ public interface ComponentClassResolver /** * Returns the library mappings. - * @return */ @IncompatibleChange(release = "5.4", details = "Added method") Collection 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); + + /** + * Returns the class name for a page, component or class given its logical name. + */ + @IncompatibleChange(release = "5.8.3", details = "Added method") + String getClassName(String logicalName); } diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/services/pageload/PageClassLoaderContext.java b/tapestry-core/src/main/java/org/apache/tapestry5/services/pageload/PageClassLoaderContext.java new file mode 100644 index 0000000000..8ae71f5f49 --- /dev/null +++ b/tapestry-core/src/main/java/org/apache/tapestry5/services/pageload/PageClassLoaderContext.java @@ -0,0 +1,372 @@ +// Copyright 2023 The Apache Software Foundation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package org.apache.tapestry5.services.pageload; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; +import java.util.function.Function; + +import org.apache.tapestry5.commons.services.PlasticProxyFactory; +import org.apache.tapestry5.internal.plastic.PlasticClassLoader; +import org.apache.tapestry5.internal.plastic.PlasticClassPool; +import org.apache.tapestry5.plastic.PlasticManager; +import org.apache.tapestry5.plastic.PlasticUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Class that encapsulates a classloader context for Tapestry's live class reloading. + * Each instance contains basically a classloader, a set of classnames, a parent + * context (possibly null) and child contexts (possibly empty). + */ +public class PageClassLoaderContext +{ + + private static final Logger LOGGER = LoggerFactory.getLogger(PageClassLoaderContext.class); + + private final String name; + + private final PageClassLoaderContext parent; + + private final Set classNames = new HashSet<>(); + + private final Set children; + + private final PlasticManager plasticManager; + + private final PlasticProxyFactory proxyFactory; + + private PageClassLoaderContext root; + + private final Function provider; + + /** + * Name of the unknown context (i.e. the one for controlled classes + * without dependency information at the moment). + */ + public static final String UNKOWN_CONTEXT_NAME = "unknown"; + + public PageClassLoaderContext(String name, + PageClassLoaderContext parent, + Set classNames, + PlasticProxyFactory plasticProxyFactory, + Function provider) + { + super(); + this.name = name; + this.parent = parent; + this.classNames.addAll(classNames); + this.plasticManager = plasticProxyFactory.getPlasticManager(); + this.proxyFactory = plasticProxyFactory; + this.provider = provider; + children = new HashSet<>(); + if (plasticProxyFactory.getClassLoader() instanceof PlasticClassLoader) + { + final PlasticClassLoader plasticClassLoader = (PlasticClassLoader) plasticManager.getClassLoader(); + plasticClassLoader.setTag(name); + plasticClassLoader.setFilter(this::filter); + plasticClassLoader.setAlternativeClassloading(this::alternativeClassLoading); + // getPlasticManager().getPool().setName(name); + if (parent != null) + { + getPlasticManager().getPool().setParent(parent.getPlasticManager().getPool()); + } + } + } + + private Class alternativeClassLoading(String className) + { + Class clasz = null; + setRootFieldIfNeeded(); + PageClassLoaderContext context = root.findByClassName( + PlasticUtils.getEnclosingClassName(className)); + if (isRoot() && context == null) + { + context = this; + } + if (context != null) + { + try + { + final PlasticClassLoader classLoader = (PlasticClassLoader) context.getClassLoader(); + // Avoiding infinite recursion + synchronized (classLoader) + { + classLoader.setAlternativeClassloading(null); + clasz = classLoader.loadClass(className); + classLoader.setAlternativeClassloading(this::alternativeClassLoading); + } + } catch (ClassNotFoundException e) + { + throw new RuntimeException(e); + } + } + else if (root.getPlasticManager().shouldInterceptClassLoading(className)) + { + context = provider.apply(className); + } + return clasz; + } + + private void setRootFieldIfNeeded() + { + if (root == null) + { + if (isRoot()) + { + root = this; + } + else + { + root = this; + while (!root.isRoot()) + { + root = root.getParent(); + } + } + } + } + + /** + * Returns the name of this context. + */ + public String getName() + { + return name; + } + + /** + * Returns the parent of this context. + */ + public PageClassLoaderContext getParent() + { + return parent; + } + + /** + * Returns the set of classes that belong in this context. + */ + public Set getClassNames() + { + return classNames; + } + + /** + * Returns the children of this context. + */ + public Set getChildren() + { + return children; + } + + /** + * Returns this context's {@linkplain PlasticManager} instance. + */ + public PlasticManager getPlasticManager() + { + return plasticManager; + } + + /** + * Returns this context's {@linkplain PlasticProxyFactory} instance. + */ + public PlasticProxyFactory getProxyFactory() + { + return proxyFactory; + } + + /** + * Adds a class to this context. + */ + public void addClass(String className) + { + classNames.add(className); + } + + /** + * Adds a child context. + */ + public void addChild(PageClassLoaderContext context) + { + children.add(context); + } + + /** + * Removes a child context. + */ + public void removeChild(PageClassLoaderContext context) + { + children.remove(context); + } + + /** + * Searches for the context that contains the given class in itself and recursivel in its children. + */ + public PageClassLoaderContext findByClassName(String className) + { + PageClassLoaderContext context = null; + if (classNames.contains(className)) + { + context = this; + } + else + { + for (PageClassLoaderContext child : children) { + context = child.findByClassName(className); + if (context != null) + { + break; + } + } + } + return context; + } + + /** + * Returns the {@linkplain ClassLoader} associated with this context. + */ + public ClassLoader getClassLoader() + { + return proxyFactory.getClassLoader(); + } + + /** + * Invalidates this context and its children recursively. This shouldn't + * be called directly, just through {@link PageClassLoaderContextManager#invalidate(PageClassLoaderContext...)}. + */ + public void invalidate() + { + for (PageClassLoaderContext child : new ArrayList<>(children)) + { + child.invalidate(); + } + LOGGER.debug("Invalidating page classloader context '{}' (class loader {}, classes : {})", + name, proxyFactory.getClassLoader(), classNames); +// classNames.clear(); + parent.getChildren().remove(this); + proxyFactory.clearCache(); + } + + /** + * Returns whether this is the root context. + */ + public boolean isRoot() + { + return parent == null; + } + + /** + * Returns whether this is the unknwon context. + * @see #UNKOWN_CONTEXT_NAME + */ + public boolean isUnknown() + { + return name.equals(UNKOWN_CONTEXT_NAME); + } + + /** + * Returns the set of descendents (children and their children recursively + * of this context. + */ + public Set getDescendents() + { + Set descendents; + if (children.isEmpty()) + { + descendents = Collections.emptySet(); + } + else + { + descendents = new HashSet<>(children); + for (PageClassLoaderContext child : children) + { + descendents.addAll(child.getDescendents()); + } + } + return descendents; + } + + @Override + public int hashCode() { + return Objects.hash(name); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof PageClassLoaderContext)) { + return false; + } + PageClassLoaderContext other = (PageClassLoaderContext) obj; + return Objects.equals(name, other.name); + } + + @Override + public String toString() + { + return "PageClassloaderContext [name=" + name + + ", parent=" + (parent != null ? parent.getName() : "null" ) + + ", classLoader=" + afterAt(proxyFactory.getClassLoader().toString()) + + (isRoot() ? "" : ", classNames=" + classNames) + + "]"; + } + + public String toRecursiveString() + { + StringBuilder builder = new StringBuilder(); + toRecursiveString(builder, ""); + return builder.toString(); + } + + private void toRecursiveString(StringBuilder builder, String tabs) + { + builder.append(tabs); + builder.append(name); + builder.append(" : "); + builder.append(afterAt(proxyFactory.getClassLoader().toString())); + builder.append(" : "); + builder.append(classNames); + builder.append("\n"); + for (PageClassLoaderContext child : children) { + child.toRecursiveString(builder, tabs + "\t"); + } + } + + private static String afterAt(String string) + { + int index = string.indexOf('@'); + if (index > 0) + { + string = string.substring(index + 1); + } + return string; + } + + private boolean filter(String className) + { + final int index = className.indexOf("$"); + if (index > 0) + { + className = className.substring(0, index); + } + // TODO: do we really need the className.contains(".base.") part? + return classNames.contains(className) || className.contains(".base.") || isUnknown(); + } + +} diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/services/pageload/PageClassLoaderContextManager.java b/tapestry-core/src/main/java/org/apache/tapestry5/services/pageload/PageClassLoaderContextManager.java new file mode 100644 index 0000000000..b37f0a0fc9 --- /dev/null +++ b/tapestry-core/src/main/java/org/apache/tapestry5/services/pageload/PageClassLoaderContextManager.java @@ -0,0 +1,99 @@ +// Copyright 2023 The Apache Software Foundation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package org.apache.tapestry5.services.pageload; + +import java.util.Set; +import java.util.function.Function; + +import org.apache.tapestry5.commons.services.PlasticProxyFactory; +import org.apache.tapestry5.internal.services.ComponentInstantiatorSource; + +/** + * Service that creates {@linkplain PageClassLoaderContext} instances (except the root one) + * when a class in a controlled page is first used in the Tapestry page pool. Existing + * contexts may be reused for a given class, specially when in production mode. + * + * @see ComponentInstantiatorSource + * @since 5.8.3 + */ +public interface PageClassLoaderContextManager +{ + + /** + * Processes a class, given its class name and the root context. + * @param className the class fully qualified name. + * {@linkplain} ClassLoader} and returns a new {@linkplain PlasticProxyFactory}. + * @return the {@link PageClassLoaderContext} associated with that class. + */ + PageClassLoaderContext get(String className); + + /** + * Invalidates page classloader contexts and returns a set containing the names + * of all classes that should be invalidated. + */ + Set invalidate(PageClassLoaderContext... contexts); + + /** + * Invalidates page classloader contexts and invalidates the classes in the context as well. + */ + void invalidateAndFireInvalidationEvents(PageClassLoaderContext... contexts); + + /** + * Returns the root context. + */ + PageClassLoaderContext getRoot(); + + /** + * Clears any state held by this manager. + */ + void clear(); + + /** + * Returns whether contexts are being merged. + */ + boolean isMerging(); + + /** + * Removes one specific class from this manager, invalidating the context where + * it is. + */ + void clear(String className); + + /** + * Initializes this service with the root context and a Plastic proxy factory provider. + * Method can only be called once. None of the parameters may be null. + */ + void initialize(PageClassLoaderContext root, Function plasticProxyFactoryProvider); + + /** + * Returns the Class instance appropriate for a given component given a page name. + * @param clasz the class instance. + * @param pageName the page name. + * @return a Class instance. + */ + Class getClassInstance(Class clasz, String pageName); + + /** + * Invalidates the "unknown" page classloader context context. + */ + void invalidateUnknownContext(); + + /** + * Preloads all data, first by collecting dependency data for all existing pages + * and the components, mixins and superclasses they use, then creating the + * page classloader contexts. + */ + void preload(); + +} diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/services/pageload/PageClassLoaderContextManagerImpl.java b/tapestry-core/src/main/java/org/apache/tapestry5/services/pageload/PageClassLoaderContextManagerImpl.java new file mode 100644 index 0000000000..05df15c08b --- /dev/null +++ b/tapestry-core/src/main/java/org/apache/tapestry5/services/pageload/PageClassLoaderContextManagerImpl.java @@ -0,0 +1,683 @@ +// Copyright 2023 The Apache Software Foundation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package org.apache.tapestry5.services.pageload; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +import org.apache.tapestry5.SymbolConstants; +import org.apache.tapestry5.commons.internal.util.TapestryException; +import org.apache.tapestry5.commons.services.InvalidationEventHub; +import org.apache.tapestry5.commons.services.PlasticProxyFactory; +import org.apache.tapestry5.internal.services.ComponentDependencyRegistry; +import org.apache.tapestry5.internal.services.ComponentDependencyRegistry.DependencyType; +import org.apache.tapestry5.internal.services.InternalComponentInvalidationEventHub; +import org.apache.tapestry5.ioc.annotations.ComponentClasses; +import org.apache.tapestry5.ioc.annotations.Symbol; +import org.apache.tapestry5.plastic.PlasticUtils; +import org.apache.tapestry5.services.ComponentClassResolver; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Default {@linkplain PageClassLoaderContextManager} implementation. + * + * @since 5.8.3 + */ +public class PageClassLoaderContextManagerImpl implements PageClassLoaderContextManager +{ + + private static final Logger LOGGER = LoggerFactory.getLogger(PageClassLoaderContextManager.class); + + private final ComponentDependencyRegistry componentDependencyRegistry; + + private final ComponentClassResolver componentClassResolver; + + private final InternalComponentInvalidationEventHub invalidationHub; + + private final InvalidationEventHub componentClassesInvalidationEventHub; + + private final boolean multipleClassLoaders; + + private final static ThreadLocal NESTED_MERGE_COUNT = ThreadLocal.withInitial(() -> 0); + + private final static ThreadLocal INVALIDATING_CONTEXT = ThreadLocal.withInitial(() -> false); + + private static final AtomicInteger MERGED_COUNTER = new AtomicInteger(1); + + private Function plasticProxyFactoryProvider; + + private PageClassLoaderContext root; + + public PageClassLoaderContextManagerImpl( + final ComponentDependencyRegistry componentDependencyRegistry, + final ComponentClassResolver componentClassResolver, + final InternalComponentInvalidationEventHub invalidationHub, + final @ComponentClasses InvalidationEventHub componentClassesInvalidationEventHub, + final @Symbol(SymbolConstants.MULTIPLE_CLASSLOADERS) boolean multipleClassLoaders) + { + super(); + this.componentDependencyRegistry = componentDependencyRegistry; + this.componentClassResolver = componentClassResolver; + this.invalidationHub = invalidationHub; + this.componentClassesInvalidationEventHub = componentClassesInvalidationEventHub; + this.multipleClassLoaders = multipleClassLoaders; + invalidationHub.addInvalidationCallback(this::listen); + NESTED_MERGE_COUNT.set(0); + } + + @Override + public void invalidateUnknownContext() + { + synchronized (this) { + markAsNotInvalidatingContext(); + for (PageClassLoaderContext context : root.getChildren()) + { + if (context.isUnknown()) + { + invalidateAndFireInvalidationEvents(context); + break; + } + } + } + } + + @Override + public void initialize( + final PageClassLoaderContext root, + final Function plasticProxyFactoryProvider) + { + if (this.root != null) + { + throw new IllegalStateException("PageClassloaderContextManager.initialize() can only be called once"); + } + Objects.requireNonNull(root); + Objects.requireNonNull(plasticProxyFactoryProvider); + this.root = root; + this.plasticProxyFactoryProvider = plasticProxyFactoryProvider; + if (multipleClassLoaders) + { + LOGGER.debug("Root context: {}", root); + } + } + + @Override + public PageClassLoaderContext get(final String className) + { + PageClassLoaderContext context; + + final String enclosingClassName = PlasticUtils.getEnclosingClassName(className); + context = root.findByClassName(enclosingClassName); + + if (context == null) + { + Set classesToInvalidate = new HashSet<>(); + + context = processUsingDependencies( + enclosingClassName, + root, + () -> getUnknownContext(root, plasticProxyFactoryProvider), + plasticProxyFactoryProvider, + classesToInvalidate); + + if (!classesToInvalidate.isEmpty()) + { + invalidate(classesToInvalidate); + } + + if (!className.equals(enclosingClassName)) + { + loadClass(className, context); + } + + } + + return context; + + } + + private PageClassLoaderContext getUnknownContext(final PageClassLoaderContext root, + final Function plasticProxyFactoryProvider) + { + + PageClassLoaderContext unknownContext = null; + + for (PageClassLoaderContext child : root.getChildren()) + { + if (child.getName().equals(PageClassLoaderContext.UNKOWN_CONTEXT_NAME)) + { + unknownContext = child; + break; + } + } + + if (unknownContext == null) + { + unknownContext = new PageClassLoaderContext(PageClassLoaderContext.UNKOWN_CONTEXT_NAME, root, + Collections.emptySet(), + plasticProxyFactoryProvider.apply(root.getClassLoader()), + this::get); + root.addChild(unknownContext); + if (multipleClassLoaders) + { + LOGGER.debug("Unknown context: {}", unknownContext); + } + } + return unknownContext; + } + + private PageClassLoaderContext processUsingDependencies( + String className, + PageClassLoaderContext root, + Supplier unknownContextProvider, + Function plasticProxyFactoryProvider, Set classesToInvalidate) + { + return processUsingDependencies(className, root, unknownContextProvider, plasticProxyFactoryProvider, classesToInvalidate, new HashSet<>()); + } + + private PageClassLoaderContext processUsingDependencies( + String className, + PageClassLoaderContext root, + Supplier unknownContextProvider, + Function plasticProxyFactoryProvider, + Set classesToInvalidate, + Set alreadyProcessed) + { + return processUsingDependencies(className, root, unknownContextProvider, + plasticProxyFactoryProvider, classesToInvalidate, alreadyProcessed, true); + } + + + private PageClassLoaderContext processUsingDependencies( + String className, + PageClassLoaderContext root, + Supplier unknownContextProvider, + Function plasticProxyFactoryProvider, + Set classesToInvalidate, + Set alreadyProcessed, + boolean processCircularDependencies) + { + PageClassLoaderContext context = root.findByClassName(className); + if (context == null) + { + + // Class isn't in a controlled package, so it doesn't get transformed + // and should go for the root context, which is never thrown out. + if (!root.getPlasticManager().shouldInterceptClassLoading(className)) + { + context = root; + } else { + if ( + !componentDependencyRegistry.contains(className) || + !multipleClassLoaders + // TODO: review this +// && componentDependencyRegistry.getDependents(className).isEmpty() + ) + { + context = unknownContextProvider.get(); + } + else + { + + alreadyProcessed.add(className); + + // Sorting dependencies alphabetically so we have consistent results. + List dependencies = new ArrayList<>(getDependenciesWithoutPages(className)); + Collections.sort(dependencies); + + // Process dependencies depth-first + for (String dependency : dependencies) + { + // Avoid infinite recursion loops + if (!alreadyProcessed.contains(dependency)/* && + !circularDependencies.contains(dependency)*/) + { + processUsingDependencies(dependency, root, unknownContextProvider, + plasticProxyFactoryProvider, classesToInvalidate, alreadyProcessed, false); + } + } + + // Collect context dependencies + Set contextDependencies = new HashSet<>(); + for (String dependency : dependencies) + { + PageClassLoaderContext dependencyContext = root.findByClassName(dependency); + if (dependencyContext == null) + { + dependencyContext = processUsingDependencies(dependency, root, unknownContextProvider, + plasticProxyFactoryProvider, classesToInvalidate, alreadyProcessed); + + } + if (!dependencyContext.isRoot()) + { + contextDependencies.add(dependencyContext); + } + } + + if (contextDependencies.size() == 0) + { + context = new PageClassLoaderContext( + getContextName(className), + root, + Collections.singleton(className), + plasticProxyFactoryProvider.apply(root.getClassLoader()), + this::get); + } + else + { + PageClassLoaderContext parentContext; + if (contextDependencies.size() == 1) + { + parentContext = contextDependencies.iterator().next(); + } + else + { + parentContext = merge(contextDependencies, plasticProxyFactoryProvider, root, classesToInvalidate); + } + context = new PageClassLoaderContext( + getContextName(className), + parentContext, + Collections.singleton(className), + plasticProxyFactoryProvider.apply(parentContext.getClassLoader()), + this::get); + } + + context.getParent().addChild(context); + + // Ensure non-page class is initialized in the correct context and classloader. + // Pages get their own context and classloader, so this initialization + // is both non-needed and a cause for an NPE if it happens. + if (!componentClassResolver.isPage(className) + || componentDependencyRegistry.getDependencies(className, DependencyType.USAGE).isEmpty()) + { + loadClass(className, context); + } + + LOGGER.debug("New context: {}", context); + + } + } + + } + context.addClass(className); + + return context; + } + + private Set getDependenciesWithoutPages(String className) + { + return componentDependencyRegistry.getDependencies(className, DependencyType.USAGE); + } + + private Class loadClass(String className, PageClassLoaderContext context) + { + try + { + final ClassLoader classLoader = context.getPlasticManager().getClassLoader(); + return classLoader.loadClass(className); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private PageClassLoaderContext merge( + Set contextDependencies, + Function plasticProxyFactoryProvider, + PageClassLoaderContext root, Set classesToInvalidate) + { + + NESTED_MERGE_COUNT.set(NESTED_MERGE_COUNT.get() + 1); + + if (LOGGER.isDebugEnabled()) + { + + LOGGER.debug("Nested merge count going up to {}", NESTED_MERGE_COUNT.get()); + + String classes; + StringBuilder builder = new StringBuilder(); + builder.append("Merging the following page classloader contexts into one:\n"); + for (PageClassLoaderContext context : contextDependencies) + { + classes = context.getClassNames().stream() + .map(this::getContextName) + .sorted() + .collect(Collectors.joining(", ")); + builder.append(String.format("\t%s (parent %s) (%s)\n", context.getName(), context.getParent().getName(), classes)); + } + LOGGER.debug(builder.toString().trim()); + } + + Set allContextsIncludingDescendents = new HashSet<>(); + for (PageClassLoaderContext context : contextDependencies) + { + allContextsIncludingDescendents.add(context); + allContextsIncludingDescendents.addAll(context.getDescendents()); + } + + PageClassLoaderContext merged; + + // Collect the classes in these dependencies, then invalidate the contexts + + Set furtherDependencies = new HashSet<>(); + + Set classNames = new HashSet<>(); + + for (PageClassLoaderContext context : contextDependencies) + { + if (!context.isRoot()) + { + classNames.addAll(context.getClassNames()); + } + final PageClassLoaderContext parent = context.getParent(); + // We don't want the merged context to have a further dependency on + // the root context (it's not mergeable) nor on itself. + if (!parent.isRoot() && + !allContextsIncludingDescendents.contains(parent)) + { + furtherDependencies.add(parent); + } + } + + final List contextsToInvalidate = contextDependencies.stream() + .filter(c -> !c.isRoot()) + .collect(Collectors.toList()); + + if (!contextsToInvalidate.isEmpty()) + { + classesToInvalidate.addAll(invalidate(contextsToInvalidate.toArray(new PageClassLoaderContext[contextsToInvalidate.size()]))); + } + + PageClassLoaderContext parent; + + // No context dependencies, so parent is going to be the root one + if (furtherDependencies.size() == 0) + { + parent = root; + } + else + { + // Single shared context dependency, so it's our parent + if (furtherDependencies.size() == 1) + { + parent = furtherDependencies.iterator().next(); + } + // No single context dependency, so we'll need to recursively merge it + // so we can have a single parent. + else + { + parent = merge(furtherDependencies, plasticProxyFactoryProvider, root, classesToInvalidate); + LOGGER.debug("New context: {}", parent); + } + } + + merged = new PageClassLoaderContext( + "merged " + MERGED_COUNTER.getAndIncrement(), + parent, + classNames, + plasticProxyFactoryProvider.apply(parent.getClassLoader()), + this::get); + + parent.addChild(merged); + +// for (String className : classNames) +// { +// loadClass(className, merged); +// } + + NESTED_MERGE_COUNT.set(NESTED_MERGE_COUNT.get() - 1); + if (LOGGER.isDebugEnabled()) + { + LOGGER.debug("Nested merge count going down to {}", NESTED_MERGE_COUNT.get()); + } + + return merged; + } + + @Override + public void clear(String className) + { + final PageClassLoaderContext context = root.findByClassName(className); + if (context != null) + { +// invalidationHub.fireInvalidationEvent(new ArrayList<>(invalidate(context))); + invalidate(context); + } + } + + private String getContextName(String className) + { + String contextName = componentClassResolver.getLogicalName(className); + if (contextName == null) + { + contextName = className; + } + return contextName; + } + + @Override + public Set invalidate(PageClassLoaderContext ... contexts) + { + Set classNames = new HashSet<>(); + for (PageClassLoaderContext context : contexts) { + addClassNames(context, classNames); + context.invalidate(); + if (context.getParent() != null) + { + context.getParent().removeChild(context); + } + } + return classNames; + } + + private List listen(List resources) + { + + List returnValue; + + if (!multipleClassLoaders) + { + for (PageClassLoaderContext context : root.getChildren()) + { + context.invalidate(); + } + returnValue = Collections.emptyList(); + } + else if (INVALIDATING_CONTEXT.get()) + { + returnValue = Collections.emptyList(); + } + else + { + + Set contextsToInvalidate = new HashSet<>(); + for (String resource : resources) + { + PageClassLoaderContext context = root.findByClassName(resource); + if (context != null && !context.isRoot()) + { + contextsToInvalidate.add(context); + } + } + + Set furtherResources = invalidate(contextsToInvalidate.toArray( + new PageClassLoaderContext[contextsToInvalidate.size()])); + + // We don't want to invalidate resources more than once + furtherResources.removeAll(resources); + + returnValue = new ArrayList<>(furtherResources); + } + + return returnValue; + + } + + @SuppressWarnings("unchecked") + @Override + public void invalidateAndFireInvalidationEvents(PageClassLoaderContext... contexts) { + markAsInvalidatingContext(); + if (multipleClassLoaders) + { + final Set classNames = invalidate(contexts); + invalidate(classNames); + } + else + { + invalidate(Collections.EMPTY_SET); + } + markAsNotInvalidatingContext(); + } + + private void markAsNotInvalidatingContext() { + INVALIDATING_CONTEXT.set(false); + } + + private void markAsInvalidatingContext() { + INVALIDATING_CONTEXT.set(true); + } + + private void invalidate(Set classesToInvalidate) { + if (!classesToInvalidate.isEmpty()) + { + LOGGER.debug("Invalidating classes {}", classesToInvalidate); + markAsInvalidatingContext(); + final List classesToInvalidateAsList = new ArrayList<>(classesToInvalidate); + + componentDependencyRegistry.disableInvalidations(); + + try + { + // TODO: do we really need both invalidation hubs to be invoked here? + invalidationHub.fireInvalidationEvent(classesToInvalidateAsList); + componentClassesInvalidationEventHub.fireInvalidationEvent(classesToInvalidateAsList); + markAsNotInvalidatingContext(); + } + finally + { + componentDependencyRegistry.enableInvalidations(); + } + + } + } + + private void addClassNames(PageClassLoaderContext context, Set classNames) { + classNames.addAll(context.getClassNames()); + for (PageClassLoaderContext child : context.getChildren()) { + addClassNames(child, classNames); + } + } + + @Override + public PageClassLoaderContext getRoot() { + return root; + } + + @Override + public boolean isMerging() + { + return NESTED_MERGE_COUNT.get() > 0; + } + + @Override + public void clear() + { + } + + @Override + public Class getClassInstance(Class clasz, String pageName) + { + final String className = clasz.getName(); + PageClassLoaderContext context = root.findByClassName(className); + if (context == null) + { + context = get(className); + } + try + { + clasz = context.getProxyFactory().getClassLoader().loadClass(className); + } catch (ClassNotFoundException e) + { + throw new TapestryException(e.getMessage(), e); + } + return clasz; + } + + @Override + public void preload() + { + + final PageClassLoaderContext context = new PageClassLoaderContext(PageClassLoaderContext.UNKOWN_CONTEXT_NAME, root, + Collections.emptySet(), + plasticProxyFactoryProvider.apply(root.getClassLoader()), + this::get); + + final List pageNames = componentClassResolver.getPageNames(); + final List classNames = new ArrayList<>(pageNames.size()); + + long start = System.currentTimeMillis(); + + LOGGER.info("Preloading dependency information for {} pages", pageNames.size()); + + for (String page : pageNames) + { + try + { + final String className = componentClassResolver.resolvePageNameToClassName(page); + componentDependencyRegistry.register(context.getClassLoader().loadClass(className)); + classNames.add(className); + } catch (ClassNotFoundException e) + { + throw new RuntimeException(e); + } + } + + long finish = System.currentTimeMillis(); + + if (LOGGER.isInfoEnabled()) + { + LOGGER.info(String.format("Dependency information gathered in %.3f ms", (finish - start) / 1000.0)); + } + + context.invalidate(); + + LOGGER.info("Starting preloading page classloader contexts"); + + start = System.currentTimeMillis(); + + for (int i = 0; i < 10; i++) + { + for (String className : classNames) + { + get(className); + } + } + + finish = System.currentTimeMillis(); + + if (LOGGER.isInfoEnabled()) + { + LOGGER.info(String.format("Preloading of page classloadercontexts finished in %.3f ms", (finish - start) / 1000.0)); + } + + } + +} diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/services/pageload/package-info.java b/tapestry-core/src/main/java/org/apache/tapestry5/services/pageload/package-info.java index f80452ba05..5c462eb257 100644 --- a/tapestry-core/src/main/java/org/apache/tapestry5/services/pageload/package-info.java +++ b/tapestry-core/src/main/java/org/apache/tapestry5/services/pageload/package-info.java @@ -1,4 +1,4 @@ -// Copyright 2011 The Apache Software Foundation +// Copyright 2011-2023 The Apache Software Foundation // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/tapestry-core/src/main/resources/org/apache/tapestry5/core.properties b/tapestry-core/src/main/resources/org/apache/tapestry5/core.properties index 61cac585b7..bda821d348 100644 --- a/tapestry-core/src/main/resources/org/apache/tapestry5/core.properties +++ b/tapestry-core/src/main/resources/org/apache/tapestry5/core.properties @@ -140,6 +140,9 @@ private-default-localdate-format=lll # see ComponentLibraries page not-informed=Not informed +# see Graphviz +download-graphviz-image = Download graph as SVG + # OpenAPI generation openapi.viewer-title=OpenAPI definition viewer openapi-title=OpenAPI description 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 5e7c7a2f3a..0e82423310 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,16 +12,23 @@ + + ${page.stats.componentCount} + Structure info + ${formatElapsed(page.stats.assemblyTime)} ${page.selector.toShortString()} + + Clear cached instance +

- There are no pages in the page cache. This can only occur immediately after clearing the cache. - + There are no pages in the page cache. This can only occur immediately after clearing the cache.

@@ -36,8 +43,39 @@ Run the GC + Store dependency information + + Preload dependency information and page classloader contexts + - + + + +
+
Component dependency information for ${selectedPage.name} (just direct dependencies)
+
+
    +
  • + ${displayLogicalName} (${dependency}) +
  • +
+
+
+
+
${selectedPage.name}'s component tree
+
+
    + +
+
+
+
+
${selectedPage.name}'s dependency tree
+
+ +
+
+
Load single page
diff --git a/tapestry-core/src/main/resources/org/apache/tapestry5/corelib/pages/PageDependencyGraph.tml b/tapestry-core/src/main/resources/org/apache/tapestry5/corelib/pages/PageDependencyGraph.tml new file mode 100644 index 0000000000..21a6ec7c19 --- /dev/null +++ b/tapestry-core/src/main/resources/org/apache/tapestry5/corelib/pages/PageDependencyGraph.tml @@ -0,0 +1,13 @@ + + +

+ This page provides a graph, generated with Graphviz + and @hpcc-js/wasm, + showing the dependencies of all already loaded + pages and its components, mixins and base classes. +

+ + + +
\ No newline at end of file diff --git a/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/BeanEditorTests.java b/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/BeanEditorTests.java index e403ff3ef2..e4b99bf0c8 100644 --- a/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/BeanEditorTests.java +++ b/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/BeanEditorTests.java @@ -151,9 +151,11 @@ public void bean_editor_overrides() * TAPESTRY-1869 */ @Test - public void null_fields_and_bean_editor() + public void null_fields_and_bean_editor() throws InterruptedException { openLinks("Number BeanEditor Demo"); + + Thread.sleep(2000);// Another of that weird sitations ... clickAndWait(SUBMIT); diff --git a/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/FormTests.java b/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/FormTests.java index 699eee455d..d12b7f584a 100644 --- a/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/FormTests.java +++ b/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/FormTests.java @@ -907,13 +907,19 @@ public void link_submit_component_with_nested_element() } @Test - public void calendar_field_inside_bean_editor() + public void calendar_field_inside_bean_editor() throws InterruptedException { - openLinks("BeanEditor / Calendar Demo", "Reset Page State"); +// openLinks("BeanEditor / Calendar Demo", "Reset Page State"); + open("/beaneditcalendardemo"); + clickAndWait("link=Reset Page State"); type("calendar", "04/06/1978"); + + Thread.sleep(1000); // Test seems to go too fast clickAndWait(SUBMIT); + + Thread.sleep(1000); // Test seems to go too fast assertTextPresent("Apr 6, 1978"); diff --git a/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/SelectObj.java b/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/SelectObj.java new file mode 100644 index 0000000000..843f18e8b4 --- /dev/null +++ b/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/SelectObj.java @@ -0,0 +1,23 @@ +package org.apache.tapestry5.integration.app1; + +public class SelectObj +{ + final int id; + final String label; + + public SelectObj(int id, String label) + { + this.id = id; + this.label = label; + } + + public int getId() + { + return id; + } + + public String getLabel() + { + return label; + } +} \ No newline at end of file diff --git a/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/SelectObjModel.java b/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/SelectObjModel.java new file mode 100644 index 0000000000..80ede04c29 --- /dev/null +++ b/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/SelectObjModel.java @@ -0,0 +1,67 @@ +package org.apache.tapestry5.integration.app1; + +import java.util.List; + +import org.apache.tapestry5.AbstractOptionModel; +import org.apache.tapestry5.OptionGroupModel; +import org.apache.tapestry5.OptionModel; +import org.apache.tapestry5.ValueEncoder; +import org.apache.tapestry5.func.F; +import org.apache.tapestry5.func.Mapper; +import org.apache.tapestry5.util.AbstractSelectModel; + +public class SelectObjModel extends AbstractSelectModel implements ValueEncoder +{ + private final List options; + + public SelectObjModel(List options) + { + this.options = options; + } + + public List getOptionGroups() + { + return null; + } + + public List getOptions() + { + assert options != null; + return F.flow(options).map(new Mapper() + { + public OptionModel map(final SelectObj input) + { + return new AbstractOptionModel() + { + public Object getValue() + { + return input; + } + + public String getLabel() + { + return input.getLabel(); + } + }; + } + }).toList(); + } + + public String toClient(SelectObj value) + { + return String.valueOf(value.getId()); + } + + public SelectObj toValue(String clientValue) + { + int id = Integer.parseInt(clientValue); + + for (SelectObj so : options) + { + if (so.id == id) + return so; + } + + return null; + } +} \ No newline at end of file diff --git a/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/components/Border.java b/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/components/Border.java index 9214e2ddca..78430dd884 100644 --- a/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/components/Border.java +++ b/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/components/Border.java @@ -17,6 +17,7 @@ import org.apache.tapestry5.annotations.Property; import org.apache.tapestry5.http.services.Request; import org.apache.tapestry5.ioc.annotations.Inject; +import org.apache.tapestry5.services.pageload.PageClassLoaderContextManager; import java.util.Calendar; @@ -33,6 +34,9 @@ public class Border @Inject private ComponentResources resources; + + @Inject @Property + private PageClassLoaderContextManager pccm; public static final int year; diff --git a/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/InstanceMixinDependencies.java b/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/InstanceMixinDependencies.java new file mode 100644 index 0000000000..53146858d0 --- /dev/null +++ b/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/InstanceMixinDependencies.java @@ -0,0 +1,30 @@ +// Copyright 2023 The Apache Software Foundation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package org.apache.tapestry5.integration.app1.pages; + +import org.apache.tapestry5.annotations.Component; +import org.apache.tapestry5.annotations.MixinClasses; +import org.apache.tapestry5.annotations.Mixins; +import org.apache.tapestry5.corelib.components.TextField; +import org.apache.tapestry5.integration.app1.mixins.EchoValue; + +public class InstanceMixinDependencies +{ + @Component + @Mixins("echovalue2::before:echovalue3") + @MixinClasses(value={EchoValue.class},order={"after:echovalue2;after:echovalue3"}) + private TextField order3; + +} diff --git a/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/MultiZoneUpdateInsideForm.java b/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/MultiZoneUpdateInsideForm.java index 326c849269..137339e7ea 100644 --- a/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/MultiZoneUpdateInsideForm.java +++ b/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/MultiZoneUpdateInsideForm.java @@ -16,23 +16,18 @@ import java.util.ArrayList; import java.util.List; -import org.apache.tapestry5.AbstractOptionModel; import org.apache.tapestry5.EventContext; -import org.apache.tapestry5.OptionGroupModel; -import org.apache.tapestry5.OptionModel; import org.apache.tapestry5.SelectModel; -import org.apache.tapestry5.ValueEncoder; import org.apache.tapestry5.ajax.MultiZoneUpdate; import org.apache.tapestry5.annotations.Component; import org.apache.tapestry5.annotations.Log; import org.apache.tapestry5.annotations.Property; import org.apache.tapestry5.corelib.components.Select; import org.apache.tapestry5.corelib.components.Zone; -import org.apache.tapestry5.func.F; -import org.apache.tapestry5.func.Mapper; import org.apache.tapestry5.http.services.Request; +import org.apache.tapestry5.integration.app1.SelectObj; +import org.apache.tapestry5.integration.app1.SelectObjModel; import org.apache.tapestry5.ioc.annotations.Inject; -import org.apache.tapestry5.util.AbstractSelectModel; public class MultiZoneUpdateInsideForm { @@ -69,84 +64,6 @@ public Object[] getSelectContext() { return new Object[] {13, RetentionPolicy.RUNTIME}; } - public class SelectObj - { - final int id; - final String label; - - public SelectObj(int id, String label) - { - this.id = id; - this.label = label; - } - - public int getId() - { - return id; - } - - public String getLabel() - { - return label; - } - } - - public class SelectObjModel extends AbstractSelectModel implements ValueEncoder - { - private final List options; - - public SelectObjModel(List options) - { - this.options = options; - } - - public List getOptionGroups() - { - return null; - } - - public List getOptions() - { - assert options != null; - return F.flow(options).map(new Mapper() - { - public OptionModel map(final SelectObj input) - { - return new AbstractOptionModel() - { - public Object getValue() - { - return input; - } - - public String getLabel() - { - return input.getLabel(); - } - }; - } - }).toList(); - } - - public String toClient(SelectObj value) - { - return String.valueOf(value.getId()); - } - - public SelectObj toValue(String clientValue) - { - int id = Integer.parseInt(clientValue); - - for (SelectObj so : options) - { - if (so.id == id) - return so; - } - - return null; - } - } - void onActivate(EventContext ctx) { List select1List = new ArrayList(); diff --git a/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/NestedBeanDisplay.java b/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/NestedBeanDisplay.java index 157ef7df8c..02c434039a 100644 --- a/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/NestedBeanDisplay.java +++ b/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/NestedBeanDisplay.java @@ -24,7 +24,10 @@ public class NestedBeanDisplay @Property private Person parent; - Object initialize(Person person) + // Visibility changed from package-private to public due to smarter page invalidation + // (classes in different classloaders cannot access non-public method + // TODO: maybe find some way to avoid this? + public Object initialize(Person person) { parent = person; diff --git a/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/NumberBeanDisplayDemo.java b/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/NumberBeanDisplayDemo.java index ddb09bc456..44b994f7ea 100644 --- a/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/NumberBeanDisplayDemo.java +++ b/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/NumberBeanDisplayDemo.java @@ -22,7 +22,8 @@ public class NumberBeanDisplayDemo @Persist private IntegerHolder holder; - Object initialize(IntegerHolder holder) + // public needed for smarter page invalidation + public Object initialize(IntegerHolder holder) { this.holder = holder; 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 0000000000..9edb441c30 --- /dev/null +++ b/tapestry-core/src/test/java/org/apache/tapestry5/internal/event/InvalidationEventHubImplTest.java @@ -0,0 +1,74 @@ +// 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.slf4j.LoggerFactory; +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, LoggerFactory.getLogger(InvalidationEventHubImpl.class)); + final String firstInitialElement = "a"; + final String secondInitialElement = "b"; + final List initialResources = Arrays.asList(firstInitialElement, secondInitialElement); + final AtomicInteger callCount = new AtomicInteger(0); + Function, List> 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(), firstInitialElement); + } + else if (r.size() == 3 && r.contains(firstInitialElement.toUpperCase()) && + r.contains(secondInitialElement.toUpperCase()) && + r.contains(firstInitialElement)) { + 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, LoggerFactory.getLogger(InvalidationEventHubImpl.class)); + invalidationEventHub.addInvalidationCallback((s) -> null); + 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 new file mode 100644 index 0000000000..5ee5af2285 --- /dev/null +++ b/tapestry-core/src/test/java/org/apache/tapestry5/internal/services/ComponentDependencyRegistryImplTest.java @@ -0,0 +1,456 @@ +// 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 static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.io.File; +import java.net.URL; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import org.apache.tapestry5.commons.MappedConfiguration; +import org.apache.tapestry5.corelib.base.AbstractComponentEventLink; +import org.apache.tapestry5.corelib.base.AbstractField; +import org.apache.tapestry5.corelib.base.AbstractLink; +import org.apache.tapestry5.corelib.base.AbstractPropertyOutput; +import org.apache.tapestry5.corelib.base.AbstractTextField; +import org.apache.tapestry5.corelib.components.ActionLink; +import org.apache.tapestry5.corelib.components.Alerts; +import org.apache.tapestry5.corelib.components.Any; +import org.apache.tapestry5.corelib.components.BeanEditForm; +import org.apache.tapestry5.corelib.components.BeanEditor; +import org.apache.tapestry5.corelib.components.Delegate; +import org.apache.tapestry5.corelib.components.DevTool; +import org.apache.tapestry5.corelib.components.Errors; +import org.apache.tapestry5.corelib.components.EventLink; +import org.apache.tapestry5.corelib.components.Form; +import org.apache.tapestry5.corelib.components.Glyphicon; +import org.apache.tapestry5.corelib.components.If; +import org.apache.tapestry5.corelib.components.Label; +import org.apache.tapestry5.corelib.components.Loop; +import org.apache.tapestry5.corelib.components.Output; +import org.apache.tapestry5.corelib.components.PageLink; +import org.apache.tapestry5.corelib.components.PropertyDisplay; +import org.apache.tapestry5.corelib.components.PropertyEditor; +import org.apache.tapestry5.corelib.components.RenderObject; +import org.apache.tapestry5.corelib.components.Submit; +import org.apache.tapestry5.corelib.components.TextField; +import org.apache.tapestry5.corelib.components.TextOutput; +import org.apache.tapestry5.corelib.components.Zone; +import org.apache.tapestry5.corelib.mixins.FormGroup; +import org.apache.tapestry5.corelib.mixins.RenderDisabled; +import org.apache.tapestry5.corelib.pages.PropertyEditBlocks; +import org.apache.tapestry5.integration.app1.components.Border; +import org.apache.tapestry5.integration.app1.components.ErrorComponent; +import org.apache.tapestry5.integration.app1.components.OuterAny; +import org.apache.tapestry5.integration.app1.components.TextOnlyOnDisabledTextField; +import org.apache.tapestry5.integration.app1.mixins.AltTitleDefault; +import org.apache.tapestry5.integration.app1.mixins.EchoValue; +import org.apache.tapestry5.integration.app1.mixins.EchoValue2; +import org.apache.tapestry5.integration.app1.mixins.TextOnlyOnDisabled; +import org.apache.tapestry5.integration.app1.pages.AlertsDemo; +import org.apache.tapestry5.integration.app1.pages.BlockCaller; +import org.apache.tapestry5.integration.app1.pages.BlockHolder; +import org.apache.tapestry5.integration.app1.pages.EmbeddedComponentTypeConflict; +import org.apache.tapestry5.integration.app1.pages.InstanceMixinDependencies; +import org.apache.tapestry5.integration.app1.pages.MixinParameterDefault; +import org.apache.tapestry5.internal.services.ComponentDependencyRegistry.DependencyType; +import org.apache.tapestry5.internal.services.templates.DefaultTemplateLocator; +import org.apache.tapestry5.ioc.internal.QuietOperationTracker; +import org.apache.tapestry5.modules.TapestryModule; +import org.apache.tapestry5.plastic.PlasticManager; +import org.apache.tapestry5.services.ComponentClassResolver; +import org.apache.tapestry5.services.pageload.PageClassLoaderContextManager; +import org.apache.tapestry5.services.templates.ComponentTemplateLocator; +import org.easymock.EasyMock; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +/** + * Tests {@link ComponentDependencyRegistryImpl}. + */ +public class ComponentDependencyRegistryImplTest +{ + + private ComponentDependencyRegistryImpl componentDependencyRegistry; + + private PageClassLoaderContextManager pageClassLoaderContextManager; + + private ComponentClassResolver resolver; + + private PlasticManager plasticManager; + + private TemplateParser templateParser; + + @SuppressWarnings("deprecation") + private ComponentTemplateLocator componentTemplateLocator; + + @BeforeMethod + public void setup() + { + assertFalse( + String.format("During testing, %s shouldn't exist", ComponentDependencyRegistry.FILENAME), + new File(ComponentDependencyRegistry.FILENAME).exists()); + + MockMappedConfiguration templateConfiguration = new MockMappedConfiguration(); + TapestryModule.contributeTemplateParser(templateConfiguration); + templateParser = new TemplateParserImpl(templateConfiguration.map, false, new QuietOperationTracker()); + + componentTemplateLocator = new DefaultTemplateLocator(); + + resolver = EasyMock.createMock(ComponentClassResolver.class); + + expectResolveComponent(TextField.class); + expectResolveComponent(Border.class); + expectResolveComponent(BeanEditForm.class); + expectResolveComponent(Zone.class); + expectResolveComponent(ActionLink.class); + expectResolveComponent(If.class); + expectResolveComponent(ErrorComponent.class); + expectResolveComponent(EventLink.class); + expectResolveComponent(Output.class); + expectResolveComponent(Delegate.class); + expectResolveComponent(TextOutput.class); + expectResolveComponent(Label.class); + expectResolveComponent(BeanEditor.class); + expectResolveComponent(Loop.class); + expectResolveComponent(PropertyEditor.class); + expectResolveComponent(PropertyDisplay.class); + expectResolveComponent(Errors.class); + expectResolveComponent(Submit.class); + expectResolveComponent(PageLink.class); + expectResolveComponent(DevTool.class); + expectResolveComponent(Alerts.class); + expectResolveComponent(RenderObject.class); + expectResolveComponent(Form.class); + expectResolveComponent(Glyphicon.class); + + EasyMock.expect(resolver.resolveMixinTypeToClassName("textonlyondisabled")) + .andReturn(TextOnlyOnDisabled.class.getName()).anyTimes(); + EasyMock.expect(resolver.resolveMixinTypeToClassName("echovalue2")) + .andReturn(EchoValue2.class.getName()).anyTimes(); + EasyMock.expect(resolver.resolveMixinTypeToClassName("alttitledefault")) + .andReturn(AltTitleDefault.class.getName()).anyTimes(); + EasyMock.expect(resolver.resolveMixinTypeToClassName("formgroup")) + .andReturn(FormGroup.class.getName()).anyTimes(); + + // TODO: remove this +// EasyMock.expect(resolver.getLogicalName(EasyMock.anyString())).andAnswer(() -> (String) EasyMock.getCurrentArguments()[0]).anyTimes(); + EasyMock.expect(resolver.isPage(EasyMock.anyString())).andAnswer(() -> { + String string = (String) EasyMock.getCurrentArguments()[0]; + return string.contains(".pages."); + }).anyTimes(); + + pageClassLoaderContextManager = EasyMock.createMock(PageClassLoaderContextManager.class); + plasticManager = EasyMock.createMock(PlasticManager.class); + EasyMock.expect(plasticManager.shouldInterceptClassLoading(EasyMock.anyString())) + .andAnswer(() -> { + String className = (String) EasyMock.getCurrentArguments()[0]; + return className.contains(".pages.") || className.contains(".mixins.") || + className.contains(".components.") || className.contains(".base."); + }).anyTimes(); + + componentDependencyRegistry = new ComponentDependencyRegistryImpl( + pageClassLoaderContextManager, plasticManager, resolver, templateParser, + componentTemplateLocator); + EasyMock.replay(pageClassLoaderContextManager, plasticManager, resolver); + } + + private void expectResolveComponent(final Class clasz) { + final String className = clasz.getName(); + final java.lang.String simpleName = clasz.getSimpleName(); + EasyMock.expect(resolver.resolveComponentTypeToClassName(simpleName)) + .andReturn(className).anyTimes(); + EasyMock.expect(resolver.resolveComponentTypeToClassName(simpleName.toLowerCase())) + .andReturn(className).anyTimes(); + EasyMock.expect(resolver.resolveComponentTypeToClassName( + simpleName.substring(0, 1).toLowerCase() + simpleName.substring(1))) + .andReturn(className).anyTimes(); + } + + private void configurePCCM(boolean merging) + { + EasyMock.reset(pageClassLoaderContextManager); + EasyMock.expect(pageClassLoaderContextManager.isMerging()).andReturn(merging).anyTimes(); + EasyMock.replay(pageClassLoaderContextManager); + } + + @Test(timeOut = 5000) + public void listen() + { + add("foo", "bar"); + add("d", "a"); + add("dd", "aa"); + add("dd", "a"); + add("dd", "a"); + + final List resources = Arrays.asList("a", "aa", "none"); + + configurePCCM(true); + List result = componentDependencyRegistry.listen(resources); + assertEquals(0, result.size()); + + configurePCCM(false); + result = componentDependencyRegistry.listen(resources); + Collections.sort(result); + assertEquals(result, Arrays.asList("d", "dd")); + assertEquals("bar", getDependencies("foo").iterator().next()); + assertEquals("foo", componentDependencyRegistry.getDependents("bar").iterator().next()); + + configurePCCM(false); + result = componentDependencyRegistry.listen(Collections.emptyList()); + assertEquals(result, Collections.emptyList()); + assertEquals(componentDependencyRegistry.getDependents("bar").size(), 0); + assertEquals(getDependencies("foo").size(), 0); + + } + + private Set getDependencies(String className) + { + return componentDependencyRegistry.getDependencies(className, DependencyType.USAGE); + } + + @Test + public void dependency_methods() + { + + final String foo = "foo"; + final String bar = "bar"; + final String something = "something"; + final String other = "other"; + final String fulano = "fulano"; + final String beltrano = "beltrano"; + final String sicrano = "sicrano"; + + assertEquals( + "getDependents() should never return null", + Collections.emptySet(), + getDependencies(foo)); + + assertEquals( + "getDependents() should never return null", + Collections.emptySet(), + componentDependencyRegistry.getDependents(foo)); + + // In Brazil, Fulano, Beltrano and Sicrano are the most used people + // placeholder names, in that order. + + add(foo, bar); + add(something, fulano); + add(other, beltrano); + add(other, fulano); + add(other, fulano); + add(bar, null); + add(fulano, null); + add(beltrano, null); + add(beltrano, sicrano); + add(sicrano, null); + + assertEquals(new HashSet<>(Arrays.asList(fulano, beltrano)), getDependencies(other)); + assertEquals(new HashSet<>(Arrays.asList(fulano)), getDependencies(something)); + assertEquals(new HashSet<>(Arrays.asList()), getDependencies(fulano)); + assertEquals(new HashSet<>(Arrays.asList(bar)), getDependencies(foo)); + assertEquals(new HashSet<>(Arrays.asList()), getDependencies(bar)); + assertEquals(new HashSet<>(Arrays.asList(sicrano)), getDependencies(beltrano)); + + assertEquals(new HashSet<>(Arrays.asList(foo)), componentDependencyRegistry.getDependents(bar)); + assertEquals(new HashSet<>(Arrays.asList(other, something)), componentDependencyRegistry.getDependents(fulano)); + assertEquals(new HashSet<>(Arrays.asList()), componentDependencyRegistry.getDependents(foo)); + assertEquals(new HashSet<>(Arrays.asList(other)), componentDependencyRegistry.getDependents(beltrano)); + assertEquals(new HashSet<>(Arrays.asList(beltrano)), componentDependencyRegistry.getDependents(sicrano)); + + assertEquals(new HashSet<>(Arrays.asList(bar, fulano, sicrano)), + componentDependencyRegistry.getRootClasses()); + + assertTrue(componentDependencyRegistry.contains(foo)); + assertTrue(componentDependencyRegistry.contains(bar)); + assertTrue(componentDependencyRegistry.contains(other)); + assertTrue(componentDependencyRegistry.contains(something)); + assertTrue(componentDependencyRegistry.contains(fulano)); + assertTrue(componentDependencyRegistry.contains(beltrano)); + assertTrue(componentDependencyRegistry.contains(sicrano)); + assertFalse(componentDependencyRegistry.contains("blah")); + + assertTrue(componentDependencyRegistry.getClassNames().contains(foo)); + assertTrue(componentDependencyRegistry.getClassNames().contains(bar)); + assertTrue(componentDependencyRegistry.getClassNames().contains(other)); + assertTrue(componentDependencyRegistry.getClassNames().contains(something)); + assertTrue(componentDependencyRegistry.getClassNames().contains(fulano)); + assertTrue(componentDependencyRegistry.getClassNames().contains(beltrano)); + assertTrue(componentDependencyRegistry.getClassNames().contains(sicrano)); + assertFalse(componentDependencyRegistry.getClassNames().contains("blah")); + + // Testing the clear method + componentDependencyRegistry.clear(beltrano); + + assertEquals(new HashSet<>(Arrays.asList(fulano)), getDependencies(other)); + assertEquals(new HashSet<>(Arrays.asList(fulano)), getDependencies(something)); + assertEquals(new HashSet<>(Arrays.asList()), getDependencies(fulano)); + assertEquals(new HashSet<>(Arrays.asList(bar)), getDependencies(foo)); + assertEquals(new HashSet<>(Arrays.asList()), getDependencies(bar)); + assertEquals(new HashSet<>(Arrays.asList()), getDependencies(sicrano)); + + assertEquals(new HashSet<>(Arrays.asList(foo)), componentDependencyRegistry.getDependents(bar)); + assertEquals(new HashSet<>(Arrays.asList(other, something)), componentDependencyRegistry.getDependents(fulano)); + assertEquals(new HashSet<>(Arrays.asList()), componentDependencyRegistry.getDependents(foo)); + assertEquals(new HashSet<>(Arrays.asList()), componentDependencyRegistry.getDependents(beltrano)); + assertEquals(new HashSet<>(Arrays.asList()), componentDependencyRegistry.getDependents(sicrano)); + + assertEquals(new HashSet<>(Arrays.asList(bar, fulano, sicrano)), componentDependencyRegistry.getRootClasses()); + + assertTrue(componentDependencyRegistry.contains(foo)); + assertTrue(componentDependencyRegistry.contains(bar)); + assertTrue(componentDependencyRegistry.contains(other)); + assertTrue(componentDependencyRegistry.contains(something)); + assertTrue(componentDependencyRegistry.contains(fulano)); + assertTrue(componentDependencyRegistry.contains(sicrano)); + assertFalse(componentDependencyRegistry.contains(beltrano)); + assertFalse(componentDependencyRegistry.contains("blah")); + + assertTrue(componentDependencyRegistry.getClassNames().contains(foo)); + assertTrue(componentDependencyRegistry.getClassNames().contains(bar)); + assertTrue(componentDependencyRegistry.getClassNames().contains(other)); + assertTrue(componentDependencyRegistry.getClassNames().contains(something)); + assertTrue(componentDependencyRegistry.getClassNames().contains(fulano)); + assertTrue(componentDependencyRegistry.getClassNames().contains(sicrano)); + assertFalse(componentDependencyRegistry.getClassNames().contains(beltrano)); + assertFalse(componentDependencyRegistry.getClassNames().contains("blah")); + + } + + // Tested code isn't being used at the moment + @Test(enabled = false) + public void register() + { + + componentDependencyRegistry.clear(); + + // Dynamic dependency definitions + componentDependencyRegistry.register(PropertyDisplay.class); + assertDependencies(PropertyDisplay.class, AbstractPropertyOutput.class); + + componentDependencyRegistry.register(PropertyEditor.class); + assertDependencies(PropertyEditor.class); + + // Superclass + componentDependencyRegistry.register(EventLink.class); + + // Superclass, recursively + assertDependencies(AbstractComponentEventLink.class, AbstractLink.class); + assertDependencies(AbstractLink.class); + + // @InjectComponent + // Components declared in templates + componentDependencyRegistry.register(AlertsDemo.class); + assertDependencies(AlertsDemo.class, Zone.class, // @InjectComponent + // Components declared in template + Border.class, BeanEditForm.class, Zone.class, If.class, + ActionLink.class, ErrorComponent.class, EventLink.class); + + // Mixins defined in in templates (t:mixins="..."). + componentDependencyRegistry.register(MixinParameterDefault.class); + assertDependencies(MixinParameterDefault.class, AltTitleDefault.class, // Mixin + ActionLink.class, Border.class); // Components declared in template); + + // @Component, type() not defined + componentDependencyRegistry.register(OuterAny.class); + assertDependencies(OuterAny.class, Any.class); + + // @Component, type() defined + componentDependencyRegistry.register(EmbeddedComponentTypeConflict.class); + assertDependencies(EmbeddedComponentTypeConflict.class, TextField.class); + + // @Mixin, type() not defined + componentDependencyRegistry.register(AbstractTextField.class); + assertDependencies(AbstractTextField.class, + RenderDisabled.class, AbstractField.class); + + // @Mixin, type() defined + componentDependencyRegistry.register(TextOnlyOnDisabledTextField.class); + assertDependencies(TextOnlyOnDisabledTextField.class, + TextOnlyOnDisabled.class, TextField.class); + + // @MixinClasses and @Mixins + componentDependencyRegistry.register(InstanceMixinDependencies.class); + assertDependencies(InstanceMixinDependencies.class, + EchoValue.class, EchoValue2.class, TextField.class); + + + } + + private void assertDependencies(Class clasz, Class... dependencies) { + assertEquals( + setOf(dependencies), + getDependencies(clasz.getName())); + } + + private static Set setOf(Class ... classes) + { + return Arrays.asList(classes).stream() + .map(Class::getName) + .collect(Collectors.toSet()); + } + +// private static Set setOf(String ... strings) +// { +// return new HashSet<>(Arrays.asList(strings)); +// } + + private void add(String component, String dependency) + { + componentDependencyRegistry.add(component, dependency, DependencyType.USAGE, true); + } + + private static final class MockMappedConfiguration implements MappedConfiguration + { + + private final Map map = new HashMap<>(); + + @Override + public void add(String key, URL value) + { + map.put(key, value); + } + + @Override + public void override(String key, URL value) + { + throw new UnsupportedOperationException(); + } + + @Override + public void addInstance(String key, Class clazz) + { + throw new UnsupportedOperationException(); + } + + @Override + public void overrideInstance(String key, Class clazz) + { + throw new UnsupportedOperationException(); + } + + } + +} 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 7a0247805c..69e0d9566a 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 @@ -30,11 +30,16 @@ import org.apache.tapestry5.ioc.services.ClasspathURLConverter; import org.apache.tapestry5.ioc.services.ThreadLocale; import org.apache.tapestry5.model.ComponentModel; +import org.apache.tapestry5.services.ComponentClassResolver; import org.apache.tapestry5.services.messages.ComponentMessagesSource; import org.apache.tapestry5.services.pageload.ComponentRequestSelectorAnalyzer; import org.apache.tapestry5.services.pageload.ComponentResourceLocator; +import org.easymock.EasyMock; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.testng.annotations.AfterClass; import org.testng.annotations.BeforeClass; +import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; /** @@ -63,14 +68,27 @@ public class ComponentMessagesSourceImplTest extends InternalBaseTestCase private ComponentMessagesSourceImpl source; private ComponentResourceLocator resourceLocator; + + private Logger logger = LoggerFactory.getLogger(ComponentMessagesSourceImplTest.class); + + private final ComponentClassResolver componentClassResolver = EasyMock.createMock(ComponentClassResolver.class); + + @BeforeMethod + public void setupMethod() + { + EasyMock.reset(componentClassResolver); + EasyMock.expect(componentClassResolver.isPage(EasyMock.anyString())).andReturn(false).anyTimes(); + EasyMock.replay(componentClassResolver); + } + @BeforeClass public void setup() { resourceLocator = getService(ComponentResourceLocator.class); - source = new ComponentMessagesSourceImpl(false, simpleComponentResource.forFile("AppCatalog.properties"), - resourceLocator, new PropertiesFileParserImpl(), tracker, componentRequestSelectorAnalyzer, threadLocale); + source = new ComponentMessagesSourceImpl(false, false, simpleComponentResource.forFile("AppCatalog.properties"), + resourceLocator, new PropertiesFileParserImpl(), tracker, componentRequestSelectorAnalyzer, threadLocale, componentClassResolver, logger); } @AfterClass @@ -239,8 +257,8 @@ public void no_app_catalog() Resource resource = simpleComponentResource.forFile("NoSuchAppCatalog.properties"); List resources = Arrays.asList(resource); - ComponentMessagesSource source = new ComponentMessagesSourceImpl(true, resources, - new PropertiesFileParserImpl(), resourceLocator, converter, componentRequestSelectorAnalyzer, threadLocale); + ComponentMessagesSource source = new ComponentMessagesSourceImpl(true, false, resources, + new PropertiesFileParserImpl(), resourceLocator, converter, componentRequestSelectorAnalyzer, threadLocale, componentClassResolver, 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 63a89d5868..ee303a45e6 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.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 @@ -102,16 +106,18 @@ public void caching() Resource resource = mockResource(); ComponentResourceLocator locator = mockLocator(model, english, resource); - train_getComponentClassName(model, PACKAGE + ".Fred"); + final String className = PACKAGE + ".Fred"; + train_getComponentClassName(model, className); expect(resource.exists()).andReturn(true); + expect(resource.getPath()).andReturn(className.replace(".", "/") + ".tml"); expect(resource.toURL()).andReturn(null); train_parseTemplate(parser, resource, template); replay(); - ComponentTemplateSource source = new ComponentTemplateSourceImpl(true, parser, locator, converter, componentRequestSelectorAnalyzer, threadLocale); + ComponentTemplateSource source = new ComponentTemplateSourceImpl(true, false, parser, locator, converter, componentRequestSelectorAnalyzer, threadLocale, logger); assertSame(source.getTemplate(model, english), template); @@ -160,7 +166,7 @@ public void invalidation() throws Exception replay(); - ComponentTemplateSourceImpl source = new ComponentTemplateSourceImpl(false, parser, locator, converter, componentRequestSelectorAnalyzer, threadLocale); + ComponentTemplateSourceImpl source = new ComponentTemplateSourceImpl(false, false, parser, locator, converter, componentRequestSelectorAnalyzer, threadLocale, logger); source.addInvalidationListener(listener); assertSame(source.getTemplate(model, Locale.ENGLISH), template); @@ -215,11 +221,13 @@ public void localization_to_same() ComponentModel model = mockComponentModel(); ComponentResourceLocator locator = newMock(ComponentResourceLocator.class); - train_getComponentClassName(model, PACKAGE + ".Fred"); + final String className = PACKAGE + ".Fred"; + train_getComponentClassName(model, className); expect(locator.locateTemplate(model, english)).andReturn(resource).once(); expect(resource.exists()).andReturn(true).anyTimes(); + expect(resource.getPath()).andReturn(className.replace(".", "/") + ".tml").anyTimes(); expect(resource.toURL()).andReturn(null).anyTimes(); expect(locator.locateTemplate(model, french)).andReturn(resource).once(); @@ -228,7 +236,7 @@ public void localization_to_same() replay(); - ComponentTemplateSourceImpl source = new ComponentTemplateSourceImpl(true, parser, locator, converter, componentRequestSelectorAnalyzer, threadLocale); + ComponentTemplateSourceImpl source = new ComponentTemplateSourceImpl(true, false, parser, locator, converter, componentRequestSelectorAnalyzer, threadLocale, logger); assertSame(source.getTemplate(model, Locale.ENGLISH), template); @@ -266,7 +274,7 @@ public void no_template_found() replay(); - ComponentTemplateSourceImpl source = new ComponentTemplateSourceImpl(true, parser, locator, converter, componentRequestSelectorAnalyzer, threadLocale); + ComponentTemplateSourceImpl source = new ComponentTemplateSourceImpl(true, false, parser, locator, converter, componentRequestSelectorAnalyzer, threadLocale, logger); ComponentTemplate template = source.getTemplate(model, Locale.ENGLISH); @@ -285,20 +293,22 @@ public void child_component_inherits_parent_template() Resource resource = mockResource(); ComponentResourceLocator locator = mockLocator(model, english, null); - train_getComponentClassName(model, "foo.Bar"); + final String className = "foo.Bar"; + train_getComponentClassName(model, className); train_getParentModel(model, parentModel); expect(locator.locateTemplate(parentModel, english)).andReturn(resource).once(); expect(resource.exists()).andReturn(true); + expect(resource.getPath()).andReturn(className.replace(".", "/") + ".tml"); expect(resource.toURL()).andReturn(null); train_parseTemplate(parser, resource, template); replay(); - ComponentTemplateSource source = new ComponentTemplateSourceImpl(true, parser, locator, converter, componentRequestSelectorAnalyzer, threadLocale); + ComponentTemplateSource source = new ComponentTemplateSourceImpl(true, false, parser, locator, converter, componentRequestSelectorAnalyzer, threadLocale, logger); assertSame(source.getTemplate(model, english), template); diff --git a/tapestry-core/src/test/resources/log4j.properties b/tapestry-core/src/test/resources/log4j.properties index 7090fd495a..febecf129a 100644 --- a/tapestry-core/src/test/resources/log4j.properties +++ b/tapestry-core/src/test/resources/log4j.properties @@ -20,5 +20,7 @@ log4j.category.org.openqa.selenium.server=warn log4j.category.org.apache.tapestry5.integration.app1.base.InheritBase=debug log4j.category.org.apache.tapestry5.integration.app1.pages.inherit=debug +#log4j.category.org.apache.tapestry5.services.pageload=debug +log4j.category.org.apache.tapestry5.integration.app1.services.AppModule.TimingFilter=ERROR # log4j.category.tapestry.transformer.org.apache.tapestry5.integration.app1.base.InheritBase=debug # log4j.category.tapestry.transformer.org.apache.tapestry5.integration.app1.pages.inherit=debug diff --git a/tapestry-core/src/test/resources/org/apache/tapestry5/integration/app1/components/Border.tml b/tapestry-core/src/test/resources/org/apache/tapestry5/integration/app1/components/Border.tml index 007f7a0174..908c919614 100644 --- a/tapestry-core/src/test/resources/org/apache/tapestry5/integration/app1/components/Border.tml +++ b/tapestry-core/src/test/resources/org/apache/tapestry5/integration/app1/components/Border.tml @@ -44,6 +44,7 @@ alert-class is for the AlertsTest.check_informal_parameters test + diff --git a/tapestry-core/src/test/resources/org/apache/tapestry5/integration/app1/pages/nested/AssetDemo.tml b/tapestry-core/src/test/resources/org/apache/tapestry5/integration/app1/pages/nested/AssetDemo.tml index ab2b1d10dc..fafd9332cf 100644 --- a/tapestry-core/src/test/resources/org/apache/tapestry5/integration/app1/pages/nested/AssetDemo.tml +++ b/tapestry-core/src/test/resources/org/apache/tapestry5/integration/app1/pages/nested/AssetDemo.tml @@ -89,4 +89,6 @@

Asset with bad checksum: ${assetWithWrongChecksumUrl}

+

Message: ${message:note}

+ \ No newline at end of file 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 0efb4fd248..fa9d56b0ef 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 @@ private void trackClassFileChanges(String className) 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 aa2c85df82..4baafe32c6 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 @@ -14,15 +14,17 @@ package org.apache.tapestry5.ioc.internal.util; -import org.apache.tapestry5.commons.util.CollectionFactory; -import org.apache.tapestry5.ioc.internal.services.ClasspathURLConverterImpl; -import org.apache.tapestry5.ioc.services.ClasspathURLConverter; - 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; + +import org.apache.tapestry5.commons.util.CollectionFactory; +import org.apache.tapestry5.ioc.internal.services.ClasspathURLConverterImpl; +import org.apache.tapestry5.ioc.services.ClasspathURLConverter; /** * Given a (growing) set of URLs, can periodically check to see if any of the underlying resources has changed. This @@ -30,12 +32,15 @@ * granularity is used by default. Second-level granularity is provided for compatibility with browsers vis-a-vis * resource caching -- that's how granular they get with their "If-Modified-Since", "Last-Modified" and "Expires" * headers. + * + * @param The type of the optional information about the tracked resource. This type should + * implement equals() and hashCode(). */ -public class URLChangeTracker +public class URLChangeTracker { private static final long FILE_DOES_NOT_EXIST_TIMESTAMP = -1L; - private final Map fileToTimestamp = CollectionFactory.newConcurrentMap(); + private final Map fileToTimestamp = CollectionFactory.newConcurrentMap(); private final boolean granularitySeconds; @@ -135,6 +140,24 @@ public static File toFileFromFileProtocolURL(URL url) * 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 resourceInfo + * an optional object containing information about the tracked URL. It's + * returned in the {@link #getChangedResourcesInfo()} method. + * @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, T resourceInfo) { if (url == null) return 0; @@ -147,14 +170,14 @@ public long add(URL url) 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, resourceInfo)); if (trackFolderChanges) { @@ -163,7 +186,7 @@ public long add(URL url) if (!fileToTimestamp.containsKey(dir)) { long dirTimestamp = readTimestamp(dir); - fileToTimestamp.put(dir, dirTimestamp); + fileToTimestamp.put(dir, new TrackingInfo(dirTimestamp, null)); } } @@ -205,20 +228,48 @@ public boolean containsChanges() // concurrently, but CheckForUpdatesFilter ensures that it will be invoked // synchronously. - for (Map.Entry entry : fileToTimestamp.entrySet()) + for (Map.Entry 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 non-null resource information for all files with a changed timestamp. + */ + public Set getChangedResourcesInfo() + { + + Set changedResourcesInfo = new HashSet<>(); + + for (Map.Entry entry : fileToTimestamp.entrySet()) + { + long newTimestamp = readTimestamp(entry.getKey()); + final TrackingInfo value = entry.getValue(); + long current = value.timestamp; + + if (current != newTimestamp) + { + if (value.resourceInfo != null) + { + changedResourcesInfo.add(value.resourceInfo); + } + value.timestamp = newTimestamp; + } + } + + return changedResourcesInfo; + } + /** * Returns the time that the specified file was last modified, possibly rounded down to the nearest second. @@ -249,9 +300,9 @@ private long applyGranularity(long timestamp) */ public void forceChange() { - for (Map.Entry e : fileToTimestamp.entrySet()) + for (Map.Entry e : fileToTimestamp.entrySet()) { - e.setValue(0l); + e.getValue().timestamp = 0l; } } @@ -262,5 +313,25 @@ int trackedFileCount() { return fileToTimestamp.size(); } + + private final class TrackingInfo + { + + private long timestamp; + private T resourceInfo; + + public TrackingInfo(long timestamp, T resourceInfo) + { + this.timestamp = timestamp; + this.resourceInfo = resourceInfo; + } + + @Override + public String toString() + { + return "Info [timestamp=" + timestamp + ", resourceInfo=" + resourceInfo + "]"; + } + + } } diff --git a/tapestry-webresources/src/test/resources/GebConfig.groovy b/tapestry-webresources/src/test/resources/GebConfig.groovy index a45863a021..35515537cf 100644 --- a/tapestry-webresources/src/test/resources/GebConfig.groovy +++ b/tapestry-webresources/src/test/resources/GebConfig.groovy @@ -1,4 +1,4 @@ -import io.github.bonigarcia.wdm.FirefoxDriverManager +import io.github.bonigarcia.wdm.managers.FirefoxDriverManager driver = "firefox"