This is an automated email from the ASF dual-hosted git repository. thiagohp pushed a commit to branch TAP5-2779 in repository https://gitbox.apache.org/repos/asf/tapestry-5.git
commit a573e5d016624901495af25fddd8c3516ce30d38 Author: Thiago H. de Paula Figueiredo <thi...@arsmachina.com.br> AuthorDate: Mon Jun 24 22:29:11 2024 -0300 TAP5-2779: fixing attempted duplicate class definition exception (pass 1, still with a lot of debugging and testing code and needing a big cleanup) --- .../internal/plastic/PlasticClassLoader.java | 107 +++++++- .../internal/plastic/PlasticClassPool.java | 20 +- .../corelib/pages/ComponentLibraries.java | 163 +++++++++++- .../tapestry5/corelib/pages/ExceptionReport.java | 6 + .../tapestry5/corelib/pages/PageCatalog.java | 1 + .../corelib/pages/PageClassLoaderContexts.java | 94 +++++++ .../tapestry5/internal/ThrowawayClassLoader.java | 92 +++++++ .../ComponentDependencyGraphvizGeneratorImpl.java | 28 +- .../services/ComponentDependencyRegistry.java | 7 + .../services/ComponentDependencyRegistryImpl.java | 17 +- .../services/ComponentInstantiatorSourceImpl.java | 63 ++++- .../services/ComponentModelSourceImpl.java | 55 +++- .../apache/tapestry5/modules/DashboardModule.java | 10 +- .../apache/tapestry5/modules/PageLoadModule.java | 9 +- .../pageload/PageClassLoaderContextManager.java | 6 + .../PageClassLoaderContextManagerImpl.java | 117 +++++++-- .../tapestry5/corelib/pages/ComponentLibraries.tml | 289 +++++++++++++-------- .../apache/tapestry5/corelib/pages/PageCatalog.tml | 2 +- .../corelib/pages/PageClassLoaderContexts.tml | 17 ++ tapestry-core/src/test/app1/AtComponentType.tml | 6 + tapestry-core/src/test/app1/GridDemo.tml | 2 +- .../integration/app1/pages/AtComponentType.java | 24 ++ .../tapestry5/integration/app1/pages/GridDemo.java | 6 + .../ComponentDependencyRegistryImplTest.java | 101 ++++++- tapestry-core/src/test/resources/log4j.properties | 1 + 25 files changed, 1040 insertions(+), 203 deletions(-) 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 85a263b75..c0774c13a 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 @@ -1,4 +1,4 @@ -// Copyright 2011 The Apache Software Foundation +// Copyright 2011, 2023, 2024 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,6 +14,14 @@ package org.apache.tapestry5.internal.plastic; +import java.util.ArrayList; +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.function.Consumer; import java.util.function.Function; import java.util.function.Predicate; @@ -33,12 +41,21 @@ public class PlasticClassLoader extends ClassLoader private Function<String, Class<?>> alternativeClassloading; + private Consumer<String> beforeLoadClass; + + private Set<String> loadedClasses; + private String tag; + private Map<String, Class<?>> cache; + + private static List<String> log = new ArrayList<>(); + public PlasticClassLoader(ClassLoader parent, ClassLoaderDelegate delegate) { super(parent); this.delegate = delegate; + loadedClasses = new HashSet<>(); } @Override @@ -50,24 +67,55 @@ public class PlasticClassLoader extends ClassLoader if (loadedClass != null) return loadedClass; - + if (shouldInterceptClassLoading(name)) { - Class<?> c = null; - if ((filter != null && filter.test(name)) || (filter == null && delegate.shouldInterceptClassLoading(name))) - { - c = delegate.loadAndTransformClass(name); + + if (name.equals("org.apache.tapestry5.integration.app1.pages.GridDemo")) { + System.out.println(); } - else if (alternativeClassloading != null) + + Class<?> c = getFromCache(name); + + if (c == null) { - c = alternativeClassloading.apply(name); + + if ((filter != null && filter.test(name)) || (filter == null && delegate.shouldInterceptClassLoading(name))) + { + + try { + log.add(String.format("Loading %s in %s", name, this)); + c = delegate.loadAndTransformClass(name); + } + catch (LinkageError e) { + e.printStackTrace(); + for (String entry : log) { + System.out.println(entry); + } + System.out.println(); + throw e; + } + } + else if (alternativeClassloading != null) + { + c = alternativeClassloading.apply(name); + } + + if (cache != null && c != null) + { + cache.put(name, c); + } + } if (c == null) { return super.loadClass(name, resolve); } - + + +// loadedClasses.add(name + " : " + System.currentTimeMillis()); + if (resolve) resolveClass(c); @@ -88,6 +136,10 @@ public class PlasticClassLoader extends ClassLoader { synchronized(getClassLoadingLock(className)) { + if (className.equals("org.apache.tapestry5.integration.app1.pages.GridDemo")) + { + System.out.println(); + } return defineClass(className, bytecode, 0, bytecode.length); } } @@ -110,6 +162,10 @@ public class PlasticClassLoader extends ClassLoader public void setTag(String tag) { this.tag = tag; + if (cache == null) + { + cache = Collections.synchronizedMap(new HashMap<>()); + } } /** @@ -126,9 +182,38 @@ public class PlasticClassLoader extends ClassLoader @Override public String toString() { - final String superToString = super.toString(); - final String id = superToString.substring(superToString.indexOf('@')).trim(); + final String id = getClassLoaderId(); return String.format("PlasticClassLoader[%s, tag=%s, parent=%s]", id, tag, getParent()); } + public String getClassLoaderId() { + final String superToString = super.toString(); + return superToString.substring(superToString.indexOf('@')).trim(); + } + + private Class<?> getFromCache(String name) + { + Class<?> c = null; + if (cache != null && cache.containsKey(name)) + { + c = cache.get(name); + } + if (c == null && getParent() instanceof PlasticClassLoader) + { + c = ((PlasticClassLoader) getParent()).getFromCache(name); + } + return c; + } + +// /** +// * Sets a {@linkplain Consumer} that will be called before a transformed +// * class is loaded. +// * @param beforeLoadClass the <code>Consumer<String></code>. +// * @since 5.8.7 +// */ +// public void setBeforeLoadClass(Consumer<String> beforeLoadClass) +// { +// this.beforeLoadClass = beforeLoadClass; +// } + } 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 9daa47d51..0bc3bcb7c 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 @@ -753,7 +753,18 @@ public class PlasticClassPool implements ClassLoaderDelegate, Opcodes, PlasticCl private FieldInstrumentations getFieldInstrumentations(String classInternalName) { - FieldInstrumentations result = instrumentations.get(classInternalName); + + // Check whether parent pool already has instrumentations for + // that class to avoid a duplicated class definition attempt + // when running tapestry-core in multiple classloader mode. + PlasticClassPool current = this; + FieldInstrumentations result; + do + { + result = current.instrumentations.get(classInternalName); + current = current.parent; + } + while (current != null && result == null); if (result != null) { @@ -845,6 +856,13 @@ public class PlasticClassPool implements ClassLoaderDelegate, Opcodes, PlasticCl throw new RuntimeException(e); } } + + @Override + public String toString() + { + return "PlasticClassPool [loader=" + loader + "]"; + } + } diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/corelib/pages/ComponentLibraries.java b/tapestry-core/src/main/java/org/apache/tapestry5/corelib/pages/ComponentLibraries.java index 4ebaad2f1..83e4cfa27 100644 --- a/tapestry-core/src/main/java/org/apache/tapestry5/corelib/pages/ComponentLibraries.java +++ b/tapestry-core/src/main/java/org/apache/tapestry5/corelib/pages/ComponentLibraries.java @@ -21,11 +21,20 @@ import java.util.List; import org.apache.tapestry5.Block; import org.apache.tapestry5.annotations.Cached; +import org.apache.tapestry5.annotations.InjectComponent; import org.apache.tapestry5.annotations.OnEvent; +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.corelib.components.Zone; import org.apache.tapestry5.http.TapestryHttpSymbolConstants; +import org.apache.tapestry5.internal.ThrowawayClassLoader; +import org.apache.tapestry5.internal.plastic.ClassLoaderDelegate; +import org.apache.tapestry5.internal.plastic.PlasticClassLoader; +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.ioc.annotations.Description; import org.apache.tapestry5.ioc.annotations.Inject; import org.apache.tapestry5.ioc.annotations.Symbol; @@ -59,6 +68,9 @@ public class ComponentLibraries }; private static enum Type { PAGE, COMPONENT, MIXIN } + + @InjectComponent + private Zone zone; @Inject private ComponentClassResolver componentClassResolver; @@ -97,6 +109,39 @@ public class ComponentLibraries @Inject private ComponentLibraryInfoSource componentLibraryInfoSource; + @Inject + private ComponentDependencyRegistry componentDependencyRegistry; + + @Inject + private ComponentDependencyGraphvizGenerator componentDependencyGraphvizGenerator; + + @Property + private String selectedComponent; + + @Property + private String dependency; + + @Persist + @Property + private boolean showEverything; + +// void onActivate(List<String> context) +// { +// if (context.size() > 0) +// { +// selectedComponent = String.join("/", context); +// } +// else +// { +// selectedComponent = null; +// } +// } +// +// Object[] onPassivate() +// { +// return selectedComponent.split("/"); +// } + @Cached public List<LibraryMapping> getLibraryMappings() { @@ -105,7 +150,7 @@ public class ComponentLibraries // add all the library mappings, except the "" (empty string) one. for (LibraryMapping libraryMapping : componentClassResolver.getLibraryMappings()) { - if (!"".equals(libraryMapping.libraryName)) { + if (showEverything || !"".equals(libraryMapping.libraryName)) { mappings.add(libraryMapping); } } @@ -132,6 +177,7 @@ public class ComponentLibraries private List<String> filter(final List<String> allNames) { List<String> logicalNames = new ArrayList<String>(); + final List<LibraryMapping> libraryMappings = getLibraryMappings(); for (String name : allNames) { @@ -140,6 +186,25 @@ public class ComponentLibraries { logicalNames.add(name); } + else + { + if (libraryMapping.libraryName.equals("")) + { + boolean isWebappLibrary = true; + for (LibraryMapping otherLibraryMapping : libraryMappings) { + if (!libraryMapping.equals(otherLibraryMapping) && + name.startsWith(otherLibraryMapping.libraryName + "/")) + { + isWebappLibrary = false; + break; + } + } + if (isWebappLibrary) + { + logicalNames.add(name); + } + } + } } return logicalNames; @@ -211,7 +276,12 @@ public class ComponentLibraries @Cached(watch = "logicalName") public Description getDescription() throws ClassNotFoundException { - return Class.forName(getClassName()).getAnnotation(Description.class); + try { + return Class.forName(getClassName()).getAnnotation(Description.class); + } catch (Exception e) { + e.printStackTrace(); + return null; + } } public boolean isClassHasTags() throws ClassNotFoundException @@ -325,5 +395,94 @@ public class ComponentLibraries object.put(propertyName, value); } } + + public String getGraphvizValue() + { + return componentDependencyGraphvizGenerator.generate( + getClassName(selectedComponent)); + } + + public String getClassName(String logicalName) + { + return componentClassResolver.getClassName(logicalName); + } + + public String getComponentClassName() + { + return getClassName(selectedComponent); + } + + public List<String> getDependencies() + { + final String className = componentClassResolver.getClassName(selectedComponent); + final List<String> dependencies = new ArrayList<>(); + dependencies.addAll(componentDependencyRegistry.getDependencies(className, DependencyType.INJECT_PAGE)); + dependencies.addAll(componentDependencyRegistry.getDependencies(className, DependencyType.SUPERCLASS)); + dependencies.addAll(componentDependencyRegistry.getDependencies(className, DependencyType.USAGE)); + Collections.sort(dependencies); + return dependencies; + } + + public List<String> getDependents() + { + final String className = componentClassResolver.getClassName(selectedComponent); + List<String> dependents = new ArrayList<>( + componentDependencyRegistry.getDependents(className)); + Collections.sort(dependents); + return dependents; + } + + public String getDisplayLogicalName() + { + return componentClassResolver.getLogicalName(dependency); + } + + public Object onSelectComponent(String selectedComponent) + { + this.selectedComponent = selectedComponent; + final String className = componentClassResolver.getClassName(selectedComponent); + if (!componentDependencyRegistry.contains(className)) + { + + final ClassLoader classLoader = new ThrowawayClassLoader(getClass().getClassLoader()); + + try + { + componentDependencyRegistry.register(classLoader.loadClass(className)); + } catch (ClassNotFoundException e) + { + throw new RuntimeException(e); + } + } + return zone.getBody(); + } + + public Object getContext() + { + return logicalName; + } + + public Object onReset() + { + selectedComponent = null; + return zone.getBody(); + } + + public Object onShowEverything() + { + showEverything = true; + return zone.getBody(); + } + + public Object onShowRestricted() + { + showEverything = false; + return zone.getBody(); + } + + public String getLibraryName() + { + return !libraryMapping.libraryName.isEmpty() ? libraryMapping.libraryName : "Webapp's own component library"; + } } 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 f11d80548..985b38fa6 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 @@ -41,6 +41,7 @@ import org.apache.tapestry5.ioc.internal.util.InternalUtils; import org.apache.tapestry5.services.ExceptionReporter; import org.apache.tapestry5.services.PageRenderLinkSource; import org.apache.tapestry5.services.URLEncoder; +import org.apache.tapestry5.services.pageload.PageClassLoaderContextManager; import java.net.MalformedURLException; import java.net.URL; @@ -123,6 +124,10 @@ public class ExceptionReport extends AbstractInternalPage implements ExceptionRe @Inject @ComponentClasses private InvalidationEventHub classesInvalidationHub; + + @Inject + @Property + private PageClassLoaderContextManager pageClassLoaderContextManager; private String failurePage; @@ -187,6 +192,7 @@ public class ExceptionReport extends AbstractInternalPage implements ExceptionRe public void reportException(Throwable exception) { + System.out.print(pageClassLoaderContextManager.getRoot().toRecursiveString()); rootException = exception; rootURL = baseURLSource.getBaseURL(request.isSecure()); 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 974a4a9e4..ba28b0a98 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 @@ -227,6 +227,7 @@ public class PageCatalog void onActionFromPreloadPageClassLoaderContexts() { pageClassLoaderContextManager.preload(); + alertManager.warn("Component dependency information and page classloader contexts preloaded."); } Object onClearPage(String className) diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/corelib/pages/PageClassLoaderContexts.java b/tapestry-core/src/main/java/org/apache/tapestry5/corelib/pages/PageClassLoaderContexts.java new file mode 100644 index 000000000..d8bd3a024 --- /dev/null +++ b/tapestry-core/src/main/java/org/apache/tapestry5/corelib/pages/PageClassLoaderContexts.java @@ -0,0 +1,94 @@ +// Copyright 2024 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.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +import org.apache.tapestry5.MarkupWriter; +import org.apache.tapestry5.annotations.UnknownActivationContextCheck; +import org.apache.tapestry5.annotations.WhitelistAccessOnly; +import org.apache.tapestry5.internal.plastic.PlasticClassLoader; +import org.apache.tapestry5.ioc.annotations.Inject; +import org.apache.tapestry5.services.pageload.PageClassLoaderContext; +import org.apache.tapestry5.services.pageload.PageClassLoaderContextManager; + +/** + * Shows information about the page classloader contexts. + */ +@UnknownActivationContextCheck(false) +@WhitelistAccessOnly +public class PageClassLoaderContexts +{ + + @Inject + private PageClassLoaderContextManager pageClassLoaderContextManager; + + void onRender(MarkupWriter writer) + { + final PageClassLoaderContext root = pageClassLoaderContextManager.getRoot(); + render(root, writer); + } + + private void render(PageClassLoaderContext context, MarkupWriter writer) + { + + writer.element("li"); + writer.element("details"); + writer.element("summary"); + writer.element("span", "class", "glyphicon glyphicon-zoom-in"); + writer.end(); // span + writer.write(context.getName()); + writer.write(" ("); + writer.write(((PlasticClassLoader) context.getClassLoader()).getClassLoaderId()); + writer.write(")"); + writer.end(); // summary + + if (!context.isRoot() && !context.getClassNames().isEmpty()) + { + final List<String> classNames = new ArrayList<>(context.getClassNames()); + Collections.sort(classNames); + writer.element("ul"); + for (String className : classNames) + { + writer.element("li").text(className); + writer.end(); // li + } + writer.end(); // ul + } + + writer.end(); // details + + final List<PageClassLoaderContext> children = new ArrayList<>(context.getChildren()); + + if (!children.isEmpty()) + { + children.sort(Comparator.comparing(PageClassLoaderContext::getName)); + writer.element("ul"); + for (PageClassLoaderContext child : children) + { + writer.element("li"); + render(child, writer); + writer.end(); // li + } + writer.end(); // ul + } + + writer.end(); //li + } + +} diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/internal/ThrowawayClassLoader.java b/tapestry-core/src/main/java/org/apache/tapestry5/internal/ThrowawayClassLoader.java new file mode 100644 index 000000000..79851bfb0 --- /dev/null +++ b/tapestry-core/src/main/java/org/apache/tapestry5/internal/ThrowawayClassLoader.java @@ -0,0 +1,92 @@ +// Copyright 2024 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; + +import static org.junit.Assert.assertNotEquals; + +import org.apache.tapestry5.internal.plastic.PlasticInternalUtils; + +public class ThrowawayClassLoader extends ClassLoader +{ + + final private ClassLoader parent; + + public ThrowawayClassLoader(ClassLoader parent) + { + super(parent); + this.parent = parent; + } + + @Override + protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException + { + synchronized (getClassLoadingLock(name)) + + { + // First, check if the class has already been loaded + Class<?> c = findLoadedClass(name); + if (c == null) { + if (name.contains(".base.") || name.contains(".pages.") || + name.contains(".components.") || name.contains(".mixins.")) + { + final byte[] bytes = PlasticInternalUtils.readBytecodeForClass(parent, name, true); + c = defineClass(name, bytes, 0, bytes.length); + if (resolve) + { + resolveClass(c); + } + } + else + { + c = parent.loadClass(name); + } + } + return c; + } + } + + public static void main(String[] args) throws Exception + { + + final String className = "org.apache.tapestry5.corelib.components.BeanEditor"; + + final ClassLoader parentClassLoader = ThrowawayClassLoader.class.getClassLoader(); + ClassLoader classLoader1 = create(parentClassLoader); + ClassLoader classLoader2 = create(parentClassLoader); + + System.out.println("Parent class loader 1: " + parentClassLoader); + System.out.println("Class loader 1 : " + classLoader1); + System.out.println("Class loader 2 : " + classLoader2); + + Class class1 = classLoader1.loadClass(className); + Class class2 = classLoader2.loadClass(className); + Class class3 = parentClassLoader.loadClass(className); + + System.out.println("Class 1 : " + class1.getClassLoader()); + System.out.println("Class 2 : " + class2.getClassLoader()); + System.out.println("Class 3 : " + class3.getClassLoader()); + + assertNotEquals(class1, class2); + assertNotEquals(class1, class3); + assertNotEquals(class2, class3); + + } + + private static ClassLoader create(final ClassLoader parentClassLoader) { +// return TapestryInternalUtils.createThrowawayClassloader(parentClassLoader); + return new ThrowawayClassLoader(parentClassLoader); + } + +} 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 index 24671c003..ef9cf1017 100644 --- 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 @@ -59,12 +59,7 @@ public class ComponentDependencyGraphvizGeneratorImpl implements ComponentDepend for (String className : classNames) { createNode(className, nodeMap); - for (DependencyType dependencyType : DependencyType.values()) - { - addDependencies(className, allClasses, dependencyType, nodeMap); - } - - + addDependencies(className, allClasses, nodeMap); } final List<Node> nodes = new ArrayList<>(nodeMap.values()); @@ -141,16 +136,19 @@ public class ComponentDependencyGraphvizGeneratorImpl implements ComponentDepend return label.replace('.', '_').replace('/', '_'); } - private void addDependencies(String className, Set<String> allClasses, DependencyType type, Map<String, Node> nodeMap) + private void addDependencies(String className, Set<String> allClasses, Map<String, Node> nodeMap) { if (!allClasses.contains(className)) { createNode(className, nodeMap); - for (String dependency : componentDependencyRegistry.getDependencies(className, type)) + allClasses.add(className); + for (DependencyType type : DependencyType.values()) { - addDependencies(dependency, allClasses, type, nodeMap); + for (String dependency : componentDependencyRegistry.getDependencies(className, type)) + { + addDependencies(dependency, allClasses, nodeMap); + } } - allClasses.add(className); } } @@ -188,7 +186,8 @@ public class ComponentDependencyGraphvizGeneratorImpl implements ComponentDepend } } - private static final class Node { + private static final class Node + { final private String id; final private String className; @@ -204,5 +203,12 @@ public class ComponentDependencyGraphvizGeneratorImpl implements ComponentDepend this.dependencies.addAll(dependencies); } + @Override + public String toString() + { + return "Node [id=" + id + ", className=" + className + ", dependencies=" + dependencies + ", label=" + label + "]"; + } + } + } diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ComponentDependencyRegistry.java b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ComponentDependencyRegistry.java index ad79630f4..6c02ae75f 100644 --- a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ComponentDependencyRegistry.java +++ b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ComponentDependencyRegistry.java @@ -67,6 +67,13 @@ public interface ComponentDependencyRegistry { */ void register(Class<?> clasz); + /** + * Register all the dependencies of a given class and uses a given + * classloader to load other classes if needed. + * @since 5.8.7 + */ + void register(Class<?> clasz, ClassLoader classLoader); + /** * Register all the dependencies of a given component. */ diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ComponentDependencyRegistryImpl.java b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ComponentDependencyRegistryImpl.java index 090d71268..e8099b309 100644 --- a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ComponentDependencyRegistryImpl.java +++ b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ComponentDependencyRegistryImpl.java @@ -179,9 +179,15 @@ public class ComponentDependencyRegistryImpl implements ComponentDependencyRegis INVALIDATIONS_DISABLED.set(0); }); } - + @Override public void register(Class<?> component) + { + register(component, component.getClassLoader()); + } + + @Override + public void register(Class<?> component, ClassLoader classLoader) { final String className = component.getName(); @@ -189,7 +195,7 @@ public class ComponentDependencyRegistryImpl implements ComponentDependencyRegis Consumer<Class<?>> processClass = furtherDependencies::add; Consumer<String> processClassName = s -> { try { - furtherDependencies.add(component.getClassLoader().loadClass(s)); + furtherDependencies.add(classLoader.loadClass(s)); } catch (ClassNotFoundException e) { throw new RuntimeException(e); } @@ -216,6 +222,7 @@ public class ComponentDependencyRegistryImpl implements ComponentDependencyRegis { final Class<?> dependency = field.getType(); add(component, dependency, DependencyType.INJECT_PAGE); + processClass.accept(dependency); } // @Component @@ -245,7 +252,7 @@ public class ComponentDependencyRegistryImpl implements ComponentDependencyRegis if (!alreadyProcessed.contains(dependencyClassName) && plasticManager.shouldInterceptClassLoading(dependency.getName())) { - register(dependency); + register(dependency, classLoader); } } @@ -261,14 +268,14 @@ public class ComponentDependencyRegistryImpl implements ComponentDependencyRegis private void registerTemplate(Class<?> component, Consumer<String> processClassName) { // TODO: implement caching of template dependency information, probably - // by listening separaterly to ComponentTemplateSource to invalidate caches + // by listening separately 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) + if (templateResource != null && templateResource.exists()) { final ComponentTemplate template = templateParser.parseTemplate(templateResource); final List<TemplateToken> tokens = new LinkedList<>(); 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 8a5344c72..4a2b296b5 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 @@ -356,7 +356,23 @@ public final class ComponentInstantiatorSourceImpl implements ComponentInstantia public Instantiator getInstantiator(final String className) { - return classToInstantiator.computeIfAbsent(className, this::createInstantiatorForClass); + Instantiator instantiator; + if (multipleClassLoaders) + { + instantiator = classToInstantiator.get(className); + + if (instantiator == null) + { + instantiator = createInstantiatorForClass(className); + classToInstantiator.put(className, instantiator); + } + + } + else + { + instantiator = classToInstantiator.computeIfAbsent(className, this::createInstantiatorForClass); + } + return instantiator; } private static final ThreadLocal<Set<String>> OPEN_INSTANTIATORS = @@ -376,6 +392,30 @@ public final class ComponentInstantiatorSourceImpl implements ComponentInstantia OPEN_INSTANTIATORS.get().add(className); componentDependencyRegistry.disableInvalidations(); + + // Make sure the dependencies have been processed in case + // there was some invalidation going on and they're not there. + + final Set<String> dependencies = new HashSet<>(); + dependencies.addAll( + componentDependencyRegistry.getDependencies(className, DependencyType.USAGE)); +// dependencies.addAll( +// componentDependencyRegistry.getDependencies(className, DependencyType.SUPERCLASS)); + for (String dependency : dependencies) + { + if (!OPEN_INSTANTIATORS.get().contains(dependency)) + { + if (multipleClassLoaders) + { + getInstantiator(dependency); + } + else + { + createInstantiatorForClass(dependency); + } + } + } + PageClassLoaderContext context; try { @@ -386,20 +426,21 @@ public final class ComponentInstantiatorSourceImpl implements ComponentInstantia componentDependencyRegistry.enableInvalidations(); } - // Make sure the dependencies have been processed in case - // there was some invalidation going on and they're not there. - - // TODO: maybe we need superclasses here too? - final Set<String> dependencies = componentDependencyRegistry.getDependencies(className, DependencyType.USAGE); - for (String dependency : dependencies) + ClassInstantiator<Component> plasticInstantiator; + try { - if (!OPEN_INSTANTIATORS.get().contains(dependency)) + if (className.equals("org.apache.tapestry5.integration.app1.pages.GridInLoopDemo")) { + System.out.println(); + } + plasticInstantiator = context.getPlasticManager().getClassInstantiator(className); + if (multipleClassLoaders) { - createInstantiatorForClass(dependency); + context.getPlasticManager().getClassLoader().loadClass(className); } + } catch (Exception e) { + System.out.println(pageClassLoaderContextManager.getRoot().toRecursiveString()); + throw new RuntimeException(e); } - - ClassInstantiator<Component> plasticInstantiator = context.getPlasticManager().getClassInstantiator(className); final ComponentModel model = classToModel.get(className); OPEN_INSTANTIATORS.get().remove(className); diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ComponentModelSourceImpl.java b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ComponentModelSourceImpl.java index 9c01847b3..b167b39ba 100644 --- a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ComponentModelSourceImpl.java +++ b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ComponentModelSourceImpl.java @@ -14,6 +14,11 @@ package org.apache.tapestry5.internal.services; +import java.util.Set; + +import org.apache.tapestry5.SymbolConstants; +import org.apache.tapestry5.internal.services.ComponentDependencyRegistry.DependencyType; +import org.apache.tapestry5.ioc.annotations.Symbol; import org.apache.tapestry5.model.ComponentModel; import org.apache.tapestry5.services.ComponentClassResolver; @@ -22,15 +27,58 @@ public class ComponentModelSourceImpl implements ComponentModelSource private final ComponentClassResolver resolver; private final ComponentInstantiatorSource source; + + private final ComponentDependencyRegistry componentDependencyRegistry; + + private final PageSource pageSource; + + private final boolean multipleClassLoaders; - public ComponentModelSourceImpl(ComponentClassResolver resolver, ComponentInstantiatorSource source) + public ComponentModelSourceImpl(ComponentClassResolver resolver, ComponentInstantiatorSource source, + ComponentDependencyRegistry componentDependencyRegistry, + PageSource pageSource, + @Symbol(SymbolConstants.PRODUCTION_MODE) boolean productionMode, + @Symbol(SymbolConstants.MULTIPLE_CLASSLOADERS) boolean multipleClassLoaders) { this.resolver = resolver; this.source = source; + this.componentDependencyRegistry = componentDependencyRegistry; + this.pageSource = pageSource; + this.multipleClassLoaders = !productionMode && multipleClassLoaders; } public ComponentModel getModel(String componentClassName) { + if (multipleClassLoaders && isPage(componentClassName)) + { + + if (componentClassName.contains("GridDemo") || + componentClassName.contains("GridInLoopDemo")) + { + System.out.println(); + } + + final Set<String> superclasses = componentDependencyRegistry.getDependencies( + componentClassName, DependencyType.SUPERCLASS); + + if (!superclasses.isEmpty()) + { + final String superclass = superclasses.iterator().next(); + if (isPage(superclass)) + { + getModel(superclass); + try + { + pageSource.getPage(resolver.getLogicalName(componentClassName)); + } + catch (Exception e) + { + e.printStackTrace(); + //ignore + } + } + } + } return source.getInstantiator(componentClassName).getModel(); } @@ -38,4 +86,9 @@ public class ComponentModelSourceImpl implements ComponentModelSource { return getModel(resolver.resolvePageNameToClassName(pageName)); } + + private boolean isPage(String componentClassName) + { + return componentClassName.contains(".pages."); + } } 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 c4985b510..c2938bf1e 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 @@ -14,12 +14,14 @@ package org.apache.tapestry5.modules; +import org.apache.tapestry5.SymbolConstants; 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; +import org.apache.tapestry5.ioc.annotations.Symbol; import org.apache.tapestry5.services.dashboard.DashboardManager; import org.apache.tapestry5.services.dashboard.DashboardTab; @@ -32,11 +34,17 @@ public class DashboardModule } @Contribute(DashboardManager.class) - public static void defaultTabs(OrderedConfiguration<DashboardTab> configuration) + public static void defaultTabs(OrderedConfiguration<DashboardTab> configuration, + @Symbol(SymbolConstants.PRODUCTION_MODE) boolean productionMode, + @Symbol(SymbolConstants.MULTIPLE_CLASSLOADERS) boolean multipleClassLoaders) { 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")); + if (!productionMode && multipleClassLoaders) + { + configuration.add("PageClassLoaderContexts", new DashboardTab("PageClassLoaderContexts", "core/PageClassLoaderContexts")); + } } } 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 ae6e76a77..b658553bb 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 @@ -97,14 +97,7 @@ public class PageLoadModule { 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); - } - } + pageClassLoaderContextManager.preloadContexts(); } // Preload the dependency information for all pages // when in production mode. Without that, exceptions during 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 index b37f0a0fc..fa91b70f3 100644 --- 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 @@ -95,5 +95,11 @@ public interface PageClassLoaderContextManager * page classloader contexts. */ void preload(); + + /** + * Preloads the page classloader contexts. + * @since 5.8.7 + */ + void preloadContexts(); } 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 index afe8a6746..7a3df12f7 100644 --- 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 @@ -28,6 +28,10 @@ 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.TapestryInternalUtils; +import org.apache.tapestry5.internal.ThrowawayClassLoader; +import org.apache.tapestry5.internal.plastic.ClassLoaderDelegate; +import org.apache.tapestry5.internal.plastic.PlasticClassLoader; import org.apache.tapestry5.internal.services.ComponentDependencyRegistry; import org.apache.tapestry5.internal.services.ComponentDependencyRegistry.DependencyType; import org.apache.tapestry5.internal.services.InternalComponentInvalidationEventHub; @@ -66,6 +70,8 @@ public class PageClassLoaderContextManagerImpl implements PageClassLoaderContext private static final AtomicInteger MERGED_COUNTER = new AtomicInteger(1); + private final static ThreadLocal<Boolean> CONTEXTS_CHANGED = ThreadLocal.withInitial(() -> false); + private Function<ClassLoader, PlasticProxyFactory> plasticProxyFactoryProvider; private PageClassLoaderContext root; @@ -83,7 +89,7 @@ public class PageClassLoaderContextManagerImpl implements PageClassLoaderContext this.componentClassResolver = componentClassResolver; this.invalidationHub = invalidationHub; this.componentClassesInvalidationEventHub = componentClassesInvalidationEventHub; - this.multipleClassLoaders = multipleClassLoaders; + this.multipleClassLoaders = multipleClassLoaders && !productionMode; this.productionMode = productionMode; invalidationHub.addInvalidationCallback(this::listen); NESTED_MERGE_COUNT.set(0); @@ -147,7 +153,7 @@ public class PageClassLoaderContextManagerImpl implements PageClassLoaderContext if (!className.equals(enclosingClassName)) { - loadClass(className, context); + loadClass(enclosingClassName, context); } } @@ -227,7 +233,6 @@ public class PageClassLoaderContextManagerImpl implements PageClassLoaderContext if (context == null) { - LOGGER.debug("Processing class {}", className); // 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. @@ -235,6 +240,7 @@ public class PageClassLoaderContextManagerImpl implements PageClassLoaderContext { context = root; } else { + LOGGER.debug("Processing class {}", className); if (!productionMode && ( !componentDependencyRegistry.contains(className) || !multipleClassLoaders)) @@ -267,7 +273,7 @@ public class PageClassLoaderContextManagerImpl implements PageClassLoaderContext { PageClassLoaderContext dependencyContext = root.findByClassName(dependency); // Avoid infinite recursion loops - if (!alreadyProcessed.contains(dependency)) + if (multipleClassLoaders || !alreadyProcessed.contains(dependency)) { if (dependencyContext == null) { @@ -319,14 +325,31 @@ public class PageClassLoaderContextManagerImpl implements PageClassLoaderContext 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); - } +// // 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()) +// { +// // Avoiding "attempted duplicate class definition" due to +// // loading a class into a classloader which already loaded +// // that class before. +// if (!context.getClassNames().contains(className)) +// { +// list.get().add(className); +// try { +// loadClass(className, context); +// } +// catch (LinkageError e) { +// System.out.println("-------------------------"); +// for (String c : list.get()) { +// System.out.println(c); +// } +// System.out.println("-------------------------"); +// throw e; +// } +// } +// } if (multipleClassLoaders) { @@ -334,6 +357,7 @@ public class PageClassLoaderContextManagerImpl implements PageClassLoaderContext } } + CONTEXTS_CHANGED.set(true); } } @@ -347,6 +371,7 @@ public class PageClassLoaderContextManagerImpl implements PageClassLoaderContext Set<String> dependencies = new HashSet<>(); dependencies.addAll(componentDependencyRegistry.getDependencies(className, DependencyType.USAGE)); dependencies.addAll(componentDependencyRegistry.getDependencies(className, DependencyType.SUPERCLASS)); + dependencies.remove(className); // Just in case return Collections.unmodifiableSet(dependencies); } @@ -387,12 +412,19 @@ public class PageClassLoaderContextManagerImpl implements PageClassLoaderContext } LOGGER.debug(builder.toString().trim()); } - + + final Set<String> classesToReprocess = multipleClassLoaders ? new HashSet<>() : Collections.emptySet(); Set<PageClassLoaderContext> allContextsIncludingDescendents = new HashSet<>(); + for (PageClassLoaderContext context : contextDependencies) { + final Set<PageClassLoaderContext> descendents = context.getDescendents(); allContextsIncludingDescendents.add(context); - allContextsIncludingDescendents.addAll(context.getDescendents()); + allContextsIncludingDescendents.addAll(descendents); + for (PageClassLoaderContext descendent : descendents) + { + addClassNames(descendent, classesToReprocess); + } } PageClassLoaderContext merged; @@ -460,6 +492,18 @@ public class PageClassLoaderContextManagerImpl implements PageClassLoaderContext parent.addChild(merged); + // Recreating contexts for classes that got invalidated but + // aren't part of the new merged context (i.e. the classes + // in contexts are are descendent of the merged contexts). +// if (!classesToReprocess.isEmpty()) +// { +// final List<String> sorted = new ArrayList<>(classesToReprocess); +// for (String className : sorted) +// { +// get(className); +// } +// } + // for (String className : classNames) // { // loadClass(className, merged); @@ -647,10 +691,7 @@ public class PageClassLoaderContextManagerImpl implements PageClassLoaderContext public void preload() { - final PageClassLoaderContext context = new PageClassLoaderContext(PageClassLoaderContext.UNKOWN_CONTEXT_NAME, root, - Collections.emptySet(), - plasticProxyFactoryProvider.apply(root.getClassLoader()), - this::get); + final ClassLoader classLoader = new ThrowawayClassLoader(PageClassLoaderContext.class.getClassLoader()); final List<String> pageNames = componentClassResolver.getPageNames(); final List<String> classNames = new ArrayList<>(pageNames.size()); @@ -664,7 +705,7 @@ public class PageClassLoaderContextManagerImpl implements PageClassLoaderContext try { final String className = componentClassResolver.resolvePageNameToClassName(page); - componentDependencyRegistry.register(context.getClassLoader().loadClass(className)); + componentDependencyRegistry.register(classLoader.loadClass(className)); classNames.add(className); } catch (ClassNotFoundException e) { @@ -683,27 +724,57 @@ public class PageClassLoaderContextManagerImpl implements PageClassLoaderContext LOGGER.info(String.format("Dependency information gathered in %.3f ms", (finish - start) / 1000.0)); } - context.invalidate(); + preloadContexts(); + } + + @Override + public void preloadContexts() + { + long start; + long finish; LOGGER.info("Starting preloading page classloader contexts"); start = System.currentTimeMillis(); - for (int i = 0; i < 10; i++) + final List<String> classNames = new ArrayList<>(); + classNames.addAll(componentClassResolver.getMixinNames().stream() + .map(s -> componentClassResolver.resolveMixinTypeToClassName(s)) + .sorted() + .collect(Collectors.toList())); + + classNames.addAll(componentClassResolver.getComponentNames().stream() + .map(s -> componentClassResolver.resolveComponentTypeToClassName(s)) + .sorted() + .collect(Collectors.toList())); + + classNames.addAll(componentClassResolver.getPageNames().stream() + .map(s -> componentClassResolver.resolvePageNameToClassName(s)) + .sorted() + .collect(Collectors.toList())); + + int runs = 0; + + // The run counter check is to just avoid possible infinite loops, + // although that's very unlikely. + CONTEXTS_CHANGED.set(true); + while (runs < 5000 && CONTEXTS_CHANGED.get() == true) { + runs++; + CONTEXTS_CHANGED.set(false); for (String className : classNames) { get(className); } + System.out.println(CONTEXTS_CHANGED.get()); } finish = System.currentTimeMillis(); if (LOGGER.isInfoEnabled()) { - LOGGER.info(String.format("Preloading of page classloadercontexts finished in %.3f ms", (finish - start) / 1000.0)); + LOGGER.info(String.format("Preloading of page classloader contexts finished in %.3f ms (%d passes)", (finish - start) / 1000.0, runs)); } - } } diff --git a/tapestry-core/src/main/resources/org/apache/tapestry5/corelib/pages/ComponentLibraries.tml b/tapestry-core/src/main/resources/org/apache/tapestry5/corelib/pages/ComponentLibraries.tml index e5323cb5a..a43b79659 100644 --- a/tapestry-core/src/main/resources/org/apache/tapestry5/corelib/pages/ComponentLibraries.tml +++ b/tapestry-core/src/main/resources/org/apache/tapestry5/corelib/pages/ComponentLibraries.tml @@ -3,125 +3,180 @@ <h2 t:type="If" t:test="productionMode">This page is disabled in production mode</h2> - <t:if t:test="!productionMode"> - <h1><strong>${libraryMappings.size()}</strong> component libraries used</h1> - - - <ul id="libraryList" class="list-group"> - <li t:type="Loop" t:source="libraryMappings" t:value="libraryMapping" class="list-group-item"> - <a href="#${libraryClientId}"> - <code>${libraryMapping.libraryName}</code> <t:if test="info">: ${info.name}</t:if> - </a> - <p t:type="If" t:test="info?.description"> - ${info.description} - </p> - <p class="tags" t:type="If" t:test="!info?.tags.empty"> - Tags: - <span t:type="Loop" t:source="info.tags" t:value="var:tag" class="badge" - style="margin-right: 0.3em; font-size: 0.75em"> - ${var:tag} - </span> - </p> - </li> - </ul> - - - <div id="libraries"> - - <div class="libraryInfo" t:type="Loop" t:source="libraryMappings" t:value="libraryMapping" id="${libraryClientId}"> - - <h2><code>${libraryMapping.libraryName}</code> <t:if test="info">: ${info.name}</t:if></h2> - - <t:if test="info"> + <t:If t:test="!productionMode"> + <t:zone> - <dl class="dl-horizontal"> - - <dt>Homepage</dt> - <dd class="homepage"> - <t:if test="info.homepageUrl" else="message:not-informed"> - <a href="${info.homepageUrl}">${info.homepageUrl}</a> - </t:if> - </dd> - - <dt>Version</dt> - <dd class="version"> - <t:if test="info.version" else="message:not-informed"> - ${info.version} - </t:if> - </dd> - - <dt>Tapestry version</dt> - <dd class="tapestryVersion"> - <t:if test="info.tapestryVersion" else="message:not-informed"> - ${info.tapestryVersion} - </t:if> - </dd> + <p> + <t:if test="showEverything"> + <p:then> + <t:eventLink event="showRestricted" zone="^"> + Don't show webapp library + </t:eventLink> + </p:then> + <p:else> + <t:eventLink event="showEverything" zone="^"> + Show webapp library + </t:eventLink> + </p:else> + </t:if> + </p> + + <t:If test="!selectedComponent"> + + <h1><strong>${libraryMappings.size()}</strong> component libraries used</h1> + + <ul id="libraryList" class="list-group"> + <li t:type="Loop" t:source="libraryMappings" t:value="libraryMapping" class="list-group-item"> + <a href="#${libraryClientId}"> + <code>${libraryName}</code> <t:if test="info">: ${info.name}</t:if> + </a> + <p t:type="If" t:test="info?.description"> + ${info.description} + </p> + <p class="tags" t:type="If" t:test="!info?.tags.empty"> + Tags: + <span t:type="Loop" t:source="info.tags" t:value="var:tag" class="badge" + style="margin-right: 0.3em; font-size: 0.75em"> + ${var:tag} + </span> + </p> + </li> + </ul> + + + <div id="libraries"> - <dt>Documentation URL</dt> - <dd class="documentationUrl"> - <t:if test="info.documentationUrl" else="message:not-informed"> - <a href="${info.documentationUrl}">${info.documentationUrl}</a> - </t:if> - </dd> - - <dt>JavaDoc URL</dt> - <dd class="javadocUrl"> - <t:if test="info.javadocUrl" else="message:not-informed"> - <a href="${info.javadocUrl}">${info.javadocUrl}</a> - </t:if> - </dd> - - <t:if test="info.dependencyManagementInfoPresent"> - <dt>Dependency information</dt> - <dd class="dependencyInformation"> - Group id <code class="groupId">${info.groupId}</code>, - artifact id <code class="groupId">${info.artifactId}</code>, - version <code class="groupId">${info.version}</code> - <br/> - <a href="${info.dependencyManagementInfoUrl}" - target="_blank"> - <em>More information at Maven Central Respository</em> - </a> - </dd> - </t:if> - - <dt>Source browse URL</dt> - <dd class="sourceBrowseUrl"> - <t:if test="info.sourceBrowseUrl" else="message:not-informed"> - <a href="${info.sourceBrowseUrl}">${info.sourceBrowseUrl}</a> - </t:if> - </dd> - - <dt>Issue tracker URL</dt> - <dd class="issueTrackerUrl"> - <t:if test="info.issueTrackerUrl" else="message:not-informed"> - <a href="${info.issueTrackerUrl}">${info.issueTrackerUrl}</a> - </t:if> - </dd> - - <dt></dt> - <dd class="jsonDescription"> - <a t:type="EventLink" t:event="json" t:context="libraryMapping.libraryName">Generate JSON description</a> - </dd> + <div class="libraryInfo" t:type="Loop" t:source="libraryMappings" t:value="libraryMapping" id="${libraryClientId}"> - </dl> - - </t:if> + <h2><code>${libraryName}</code> <t:if test="info">: ${info.name}</t:if></h2> + + <t:if test="info"> + + <dl class="dl-horizontal"> + + <dt>Homepage</dt> + <dd class="homepage"> + <t:if test="info.homepageUrl" else="message:not-informed"> + <a href="${info.homepageUrl}">${info.homepageUrl}</a> + </t:if> + </dd> + + <dt>Version</dt> + <dd class="version"> + <t:if test="info.version" else="message:not-informed"> + ${info.version} + </t:if> + </dd> + + <dt>Tapestry version</dt> + <dd class="tapestryVersion"> + <t:if test="info.tapestryVersion" else="message:not-informed"> + ${info.tapestryVersion} + </t:if> + </dd> + + <dt>Documentation URL</dt> + <dd class="documentationUrl"> + <t:if test="info.documentationUrl" else="message:not-informed"> + <a href="${info.documentationUrl}">${info.documentationUrl}</a> + </t:if> + </dd> + + <dt>JavaDoc URL</dt> + <dd class="javadocUrl"> + <t:if test="info.javadocUrl" else="message:not-informed"> + <a href="${info.javadocUrl}">${info.javadocUrl}</a> + </t:if> + </dd> + + <t:if test="info.dependencyManagementInfoPresent"> + <dt>Dependency information</dt> + <dd class="dependencyInformation"> + Group id <code class="groupId">${info.groupId}</code>, + artifact id <code class="groupId">${info.artifactId}</code>, + version <code class="groupId">${info.version}</code> + <br/> + <a href="${info.dependencyManagementInfoUrl}" + target="_blank"> + <em>More information at Maven Central Respository</em> + </a> + </dd> + </t:if> + + <dt>Source browse URL</dt> + <dd class="sourceBrowseUrl"> + <t:if test="info.sourceBrowseUrl" else="message:not-informed"> + <a href="${info.sourceBrowseUrl}">${info.sourceBrowseUrl}</a> + </t:if> + </dd> + + <dt>Issue tracker URL</dt> + <dd class="issueTrackerUrl"> + <t:if test="info.issueTrackerUrl" else="message:not-informed"> + <a href="${info.issueTrackerUrl}">${info.issueTrackerUrl}</a> + </t:if> + </dd> + + <dt></dt> + <dd class="jsonDescription"> + <a t:type="EventLink" t:event="json" t:context="libraryMapping.libraryName">Generate JSON description</a> + </dd> + + </dl> + + </t:if> + + <p t:type="If" t:test="!info" class="noInformation">No additional information provided for <code>${libraryMapping.libraryName}</code>.</p> + + <!-- <div t:type="Zone" t:id="pages" id="prop:libraryClientZoneClientId"> --> + <!-- </div> --> + + <div t:type="Delegate" to="componentsTable"></div> + <div t:type="Delegate" to="pagesTable"></div> + <div t:type="Delegate" to="mixinsTable"></div> + + </div> + + </div> - <p t:type="If" t:test="!info" class="noInformation">No additional information provided for <code>${libraryMapping.libraryName}</code>.</p> + </t:If> - <!-- <div t:type="Zone" t:id="pages" id="prop:libraryClientZoneClientId"> --> - <!-- </div> --> - - <div t:type="Delegate" to="componentsTable"></div> - <div t:type="Delegate" to="pagesTable"></div> - <div t:type="Delegate" to="mixinsTable"></div> + <t:If test="selectedComponent"> + <h1><strong>${selectedComponent}</strong> (${componentClassName})</h1> - </div> - - </div> - - </t:if> + <p> + <t:eventLink event="reset" zone="^">Back to component listing</t:eventLink> + </p> + + <div class="panel panel-default vert-offset"> + <div class="panel-heading">Component dependency information for ${selectedComponent} (just direct dependencies)</div> + <div class="panel-body"> + <ul> + <li t:type="Loop" t:value="dependency" t:source="dependencies"> + ${displayLogicalName} (${dependency}) + </li> + </ul> + </div> + </div> + <div class="panel panel-default vert-offset"> + <div class="panel-heading">Components depending on ${selectedComponent} (just direct dependencies)</div> + <div class="panel-body"> + <ul> + <li t:type="Loop" t:value="dependency" t:source="dependents"> + ${displayLogicalName} (${dependency}) + </li> + </ul> + </div> + </div> + <div class="panel panel-default vert-offset"> + <div class="panel-heading">${selectedComponent}'s dependency tree</div> + <div class="panel-body"> + <t:graphviz value="graphvizValue" showSource="true"/> + </div> + </div> + </t:If> + </t:zone> + </t:If> <t:block id="classesTable"> <div t:type="If" t:test="!logicalNames.empty"> @@ -138,7 +193,11 @@ </thead> <tbody> <tr t:type="Loop" t:source="logicalNames" t:value="logicalName"> - <td><code>${simpleLogicalName}</code></td> + <td> + <t:eventLink event="selectComponent" t:context="context" t:zone="^"> + <code>${simpleLogicalName}</code> + </t:eventLink> + </td> <td>${description?.text()}</td> <td> <ul t:type="If" t:test="classHasTags" style="padding: 0; margin: 0;"> @@ -163,5 +222,5 @@ </table> </div> </t:block> - + </t:block> 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 bdac0f8b1..2eea91036 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 @@ -77,7 +77,7 @@ <div class="panel panel-default vert-offset" t:type="If" t:test="selectedPage"> <div class="panel-heading">${selectedPage.name}'s dependency tree</div> <div class="panel-body"> - <t:graphviz value="graphvizValue"/> + <t:graphviz value="graphvizValue" showSource="true"/> </div> </div> </t:if> diff --git a/tapestry-core/src/main/resources/org/apache/tapestry5/corelib/pages/PageClassLoaderContexts.tml b/tapestry-core/src/main/resources/org/apache/tapestry5/corelib/pages/PageClassLoaderContexts.tml new file mode 100644 index 000000000..8e39c0f0e --- /dev/null +++ b/tapestry-core/src/main/resources/org/apache/tapestry5/corelib/pages/PageClassLoaderContexts.tml @@ -0,0 +1,17 @@ +<t:block id="content" xmlns:t="http://tapestry.apache.org/schema/tapestry_5_3.xsd" + xmlns:p="tapestry:parameter"> + <h1>Page classloader contexts <t:glyphicon name="list" /></h1> + <style> + #tree li { + display: block; + margin-top: 0.25em; + line-height: 2m; + } + #tree .glyphicon { + margin-right: 0.25em; + } + </style> + <ul id="tree"> + <t:trigger event="render"/> + </ul> +</t:block> \ No newline at end of file diff --git a/tapestry-core/src/test/app1/AtComponentType.tml b/tapestry-core/src/test/app1/AtComponentType.tml new file mode 100644 index 000000000..50a2bf92e --- /dev/null +++ b/tapestry-core/src/test/app1/AtComponentType.tml @@ -0,0 +1,6 @@ +<t:border xmlns:t="http://tapestry.apache.org/schema/tapestry_5_0_0.xsd" + xmlns:p="tapestry:parameter"> + + <input t:id="input"/> + +</t:border> \ No newline at end of file diff --git a/tapestry-core/src/test/app1/GridDemo.tml b/tapestry-core/src/test/app1/GridDemo.tml index 5a4f12eb7..650ecf163 100644 --- a/tapestry-core/src/test/app1/GridDemo.tml +++ b/tapestry-core/src/test/app1/GridDemo.tml @@ -1,6 +1,6 @@ <html t:type="Border" xmlns:t="http://tapestry.apache.org/schema/tapestry_5_0_0.xsd"> - <h1>Grid Demo</h1> + <h1>Grid Demo!!!!</h1> <table t:type="grid" t:id="grid" source="tracks" row="track"> <t:parameter name="ratingheader"> diff --git a/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/AtComponentType.java b/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/AtComponentType.java new file mode 100644 index 000000000..de0627822 --- /dev/null +++ b/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/AtComponentType.java @@ -0,0 +1,24 @@ +// Copyright 2024 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.Field; +import org.apache.tapestry5.annotations.Component; + +public class AtComponentType +{ + @Component(type = "textfield") + private Field input; +} diff --git a/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/GridDemo.java b/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/GridDemo.java index 1d064920d..888794360 100644 --- a/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/GridDemo.java +++ b/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/GridDemo.java @@ -14,6 +14,7 @@ package org.apache.tapestry5.integration.app1.pages; +import org.apache.tapestry5.MarkupWriter; import org.apache.tapestry5.annotations.InjectComponent; import org.apache.tapestry5.annotations.Property; import org.apache.tapestry5.corelib.components.Grid; @@ -48,4 +49,9 @@ public class GridDemo { grid.getSortModel().updateSort("rating"); } + + public void afterRender(MarkupWriter writer) + { + writer.getDocument().find("html/body").elementAt(1, "p").text("@@@@@"); + } } diff --git a/tapestry-core/src/test/java/org/apache/tapestry5/internal/services/ComponentDependencyRegistryImplTest.java b/tapestry-core/src/test/java/org/apache/tapestry5/internal/services/ComponentDependencyRegistryImplTest.java index 8b89e7216..6cc761ac3 100644 --- a/tapestry-core/src/test/java/org/apache/tapestry5/internal/services/ComponentDependencyRegistryImplTest.java +++ b/tapestry-core/src/test/java/org/apache/tapestry5/internal/services/ComponentDependencyRegistryImplTest.java @@ -19,17 +19,20 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import java.io.File; +import java.net.MalformedURLException; 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.Locale; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; import org.apache.tapestry5.commons.MappedConfiguration; +import org.apache.tapestry5.commons.Resource; import org.apache.tapestry5.corelib.base.AbstractComponentEventLink; import org.apache.tapestry5.corelib.base.AbstractField; import org.apache.tapestry5.corelib.base.AbstractLink; @@ -52,6 +55,7 @@ import org.apache.tapestry5.corelib.components.Loop; import org.apache.tapestry5.corelib.components.Output; import org.apache.tapestry5.corelib.components.OutputRaw; import org.apache.tapestry5.corelib.components.PageLink; +import org.apache.tapestry5.corelib.components.PasswordField; import org.apache.tapestry5.corelib.components.PropertyDisplay; import org.apache.tapestry5.corelib.components.PropertyEditor; import org.apache.tapestry5.corelib.components.RenderObject; @@ -61,9 +65,7 @@ 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.base.BaseLayoutPage; -import org.apache.tapestry5.integration.app1.base.EmptyExtendTemplate; import org.apache.tapestry5.integration.app1.components.Border; import org.apache.tapestry5.integration.app1.components.ErrorComponent; import org.apache.tapestry5.integration.app1.components.OuterAny; @@ -75,15 +77,17 @@ 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.AtComponentType; 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.integration.app1.pages.TemplateOverrideDemo; import org.apache.tapestry5.internal.services.ComponentDependencyRegistry.DependencyType; import org.apache.tapestry5.internal.services.templates.DefaultTemplateLocator; +import org.apache.tapestry5.internal.services.templates.PageTemplateLocator; import org.apache.tapestry5.ioc.internal.QuietOperationTracker; +import org.apache.tapestry5.ioc.internal.util.AbstractResource; +import org.apache.tapestry5.model.ComponentModel; import org.apache.tapestry5.modules.TapestryModule; import org.apache.tapestry5.plastic.PlasticManager; import org.apache.tapestry5.services.ComponentClassResolver; @@ -96,6 +100,7 @@ import org.testng.annotations.Test; /** * Tests {@link ComponentDependencyRegistryImpl}. */ +@SuppressWarnings("deprecation") public class ComponentDependencyRegistryImplTest { @@ -123,11 +128,37 @@ public class ComponentDependencyRegistryImplTest TapestryModule.contributeTemplateParser(templateConfiguration); templateParser = new TemplateParserImpl(templateConfiguration.map, false, new QuietOperationTracker()); - componentTemplateLocator = new DefaultTemplateLocator(); + final String rootFolder = "src/test/app1/"; + final File folderFile = new File(rootFolder); + final Resource app1ContextFolderResource = new FileResource("/", folderFile); + + final PageTemplateLocator pageTemplateLocator = new PageTemplateLocator(app1ContextFolderResource, resolver, ""); + final DefaultTemplateLocator defaultTemplateLocator = new DefaultTemplateLocator(); + componentTemplateLocator = new ComponentTemplateLocator() + { + @Override + public Resource locateTemplate(ComponentModel model, Locale locale) + { + Resource resource = defaultTemplateLocator.locateTemplate(model, locale); + if (resource == null) + { + resource = pageTemplateLocator.locateTemplate(model, locale); + } + return resource != null && resource.exists() ? resource : null; + } + }; resolver = EasyMock.createMock(ComponentClassResolver.class); + EasyMock.expect(resolver.resolvePageClassNameToPageName(EasyMock.anyString())) + .andAnswer(() -> { + String s = ((String) EasyMock.getCurrentArguments()[0]); + final String pageName = s.substring(s.lastIndexOf('.') + 1); + return pageName; + }).anyTimes(); + expectResolveComponent(TextField.class); + expectResolveComponent(PasswordField.class); expectResolveComponent(Border.class); expectResolveComponent(BeanEditForm.class); expectResolveComponent(Zone.class); @@ -163,8 +194,6 @@ public class ComponentDependencyRegistryImplTest 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."); @@ -351,7 +380,6 @@ public class ComponentDependencyRegistryImplTest } - // Tested code isn't being used at the moment @Test public void register() { @@ -390,8 +418,8 @@ public class ComponentDependencyRegistryImplTest assertDependencies(OuterAny.class, Any.class); // @Component, type() defined - componentDependencyRegistry.register(EmbeddedComponentTypeConflict.class); - assertDependencies(EmbeddedComponentTypeConflict.class, TextField.class); + componentDependencyRegistry.register(AtComponentType.class); + assertDependencies(AtComponentType.class, TextField.class, Border.class); // @Mixin, type() not defined componentDependencyRegistry.register(AbstractTextField.class); @@ -416,8 +444,15 @@ public class ComponentDependencyRegistryImplTest // Templates with <t:replace> componentDependencyRegistry.register(SubclassWithImport.class); assertDependencies(SubclassWithImport.class, - OutputRaw.class, SuperclassWithImport.class); - + OutputRaw.class, SuperclassWithImport.class, Loop.class); + + // Circular dependency: BlockHolder <-> BlockCaller + componentDependencyRegistry.register(BlockHolder.class); + assertDependencies(BlockHolder.class, + BlockCaller.class, Loop.class, ActionLink.class); + assertDependencies(BlockCaller.class, + BlockHolder.class, Border.class, PageLink.class, Delegate.class); + } private void assertDependencies(Class clasz, Class... dependencies) { @@ -474,4 +509,46 @@ public class ComponentDependencyRegistryImplTest } + final private static class FileResource extends AbstractResource + { + + final private File file; + + final private String path; + + public FileResource(String path, File file) + { + super(path); + this.file = file; + this.path = path; + } + + @SuppressWarnings("deprecation") + @Override + public java.net.URL toURL() + { + try + { + final File actualFile = new File(file, path); + return actualFile.exists() ? actualFile.toURL() : null; + } catch (MalformedURLException e) + { + throw new RuntimeException(e); + } + } + + @Override + protected Resource newResource(String path) + { + return new FileResource(path, file); + } + + @Override + public String toString() + { + return "FileResource [file=" + file + "/" + path + "]"; + } + + } + } diff --git a/tapestry-core/src/test/resources/log4j.properties b/tapestry-core/src/test/resources/log4j.properties index febecf129..9dcdaacb2 100644 --- a/tapestry-core/src/test/resources/log4j.properties +++ b/tapestry-core/src/test/resources/log4j.properties @@ -21,6 +21,7 @@ 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.internal.services.InternalComponentInvalidationEventHub=off 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