blob: afe8a674617139c8feac033aae74b9e85f8cddef [file] [log] [blame]
// 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 boolean productionMode;
private final static ThreadLocal<Integer> NESTED_MERGE_COUNT = ThreadLocal.withInitial(() -> 0);
private final static ThreadLocal<Boolean> INVALIDATING_CONTEXT = ThreadLocal.withInitial(() -> false);
private static final AtomicInteger MERGED_COUNTER = new AtomicInteger(1);
private Function<ClassLoader, PlasticProxyFactory> plasticProxyFactoryProvider;
private PageClassLoaderContext root;
public PageClassLoaderContextManagerImpl(
final ComponentDependencyRegistry componentDependencyRegistry,
final ComponentClassResolver componentClassResolver,
final InternalComponentInvalidationEventHub invalidationHub,
final @ComponentClasses InvalidationEventHub componentClassesInvalidationEventHub,
final @Symbol(SymbolConstants.PRODUCTION_MODE) boolean productionMode,
final @Symbol(SymbolConstants.MULTIPLE_CLASSLOADERS) boolean multipleClassLoaders)
{
super();
this.componentDependencyRegistry = componentDependencyRegistry;
this.componentClassResolver = componentClassResolver;
this.invalidationHub = invalidationHub;
this.componentClassesInvalidationEventHub = componentClassesInvalidationEventHub;
this.multipleClassLoaders = multipleClassLoaders;
this.productionMode = productionMode;
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<ClassLoader, PlasticProxyFactory> 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;
LOGGER.info("Root context: {}", root);
}
@Override
public synchronized PageClassLoaderContext get(final String className)
{
PageClassLoaderContext context;
final String enclosingClassName = getAdjustedClassName(className);
context = root.findByClassName(enclosingClassName);
if (context == null)
{
Set<String> 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 String getAdjustedClassName(final String className)
{
return PlasticUtils.getEnclosingClassName(className)
.replaceAll("\\[\\]", "");
}
private PageClassLoaderContext getUnknownContext(final PageClassLoaderContext root,
final Function<ClassLoader, PlasticProxyFactory> 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<PageClassLoaderContext> unknownContextProvider,
Function<ClassLoader, PlasticProxyFactory> plasticProxyFactoryProvider, Set<String> classesToInvalidate)
{
return processUsingDependencies(className, root, unknownContextProvider, plasticProxyFactoryProvider, classesToInvalidate, new HashSet<>());
}
private PageClassLoaderContext processUsingDependencies(
String className,
PageClassLoaderContext root,
Supplier<PageClassLoaderContext> unknownContextProvider,
Function<ClassLoader, PlasticProxyFactory> plasticProxyFactoryProvider,
Set<String> classesToInvalidate,
Set<String> alreadyProcessed)
{
return processUsingDependencies(className, root, unknownContextProvider,
plasticProxyFactoryProvider, classesToInvalidate, alreadyProcessed, true);
}
private PageClassLoaderContext processUsingDependencies(
String className,
PageClassLoaderContext root,
Supplier<PageClassLoaderContext> unknownContextProvider,
Function<ClassLoader, PlasticProxyFactory> plasticProxyFactoryProvider,
Set<String> classesToInvalidate,
Set<String> alreadyProcessed,
boolean processCircularDependencies)
{
PageClassLoaderContext context = root.findByClassName(className);
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.
if (!root.getPlasticManager().shouldInterceptClassLoading(className))
{
context = root;
} else {
if (!productionMode && (
!componentDependencyRegistry.contains(className) ||
!multipleClassLoaders))
{
context = unknownContextProvider.get();
}
else
{
alreadyProcessed.add(className);
// Sorting dependencies alphabetically so we have consistent results.
List<String> dependencies = new ArrayList<>(getDependenciesWithoutPages(className));
Collections.sort(dependencies);
// Process dependencies depth-first
for (String dependency : dependencies)
{
// Avoid infinite recursion loops
if (!alreadyProcessed.contains(dependency))
{
processUsingDependencies(dependency, root, unknownContextProvider,
plasticProxyFactoryProvider, classesToInvalidate, alreadyProcessed, false);
}
}
// Collect context dependencies
Set<PageClassLoaderContext> contextDependencies = new HashSet<>();
for (String dependency : dependencies)
{
PageClassLoaderContext dependencyContext = root.findByClassName(dependency);
// Avoid infinite recursion loops
if (!alreadyProcessed.contains(dependency))
{
if (dependencyContext == null)
{
dependencyContext = processUsingDependencies(dependency, root, unknownContextProvider,
plasticProxyFactoryProvider, classesToInvalidate, alreadyProcessed);
}
if (!dependencyContext.isRoot())
{
contextDependencies.add(dependencyContext);
}
}
}
if (!multipleClassLoaders)
{
context = root;
}
else 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);
}
if (multipleClassLoaders)
{
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);
}
if (multipleClassLoaders)
{
LOGGER.debug("New context: {}", context);
}
}
}
}
context.addClass(className);
return context;
}
private Set<String> getDependenciesWithoutPages(String className)
{
Set<String> dependencies = new HashSet<>();
dependencies.addAll(componentDependencyRegistry.getDependencies(className, DependencyType.USAGE));
dependencies.addAll(componentDependencyRegistry.getDependencies(className, DependencyType.SUPERCLASS));
return Collections.unmodifiableSet(dependencies);
}
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<PageClassLoaderContext> contextDependencies,
Function<ClassLoader, PlasticProxyFactory> plasticProxyFactoryProvider,
PageClassLoaderContext root, Set<String> 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<PageClassLoaderContext> 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<PageClassLoaderContext> furtherDependencies = new HashSet<>();
Set<String> 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<PageClassLoaderContext> 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<String> invalidate(PageClassLoaderContext ... contexts)
{
Set<String> classNames = new HashSet<>();
for (PageClassLoaderContext context : contexts) {
addClassNames(context, classNames);
context.invalidate();
if (context.getParent() != null)
{
context.getParent().removeChild(context);
}
}
return classNames;
}
private List<String> listen(List<String> resources)
{
List<String> returnValue;
if (!multipleClassLoaders)
{
for (PageClassLoaderContext context : root.getChildren())
{
context.invalidate();
}
returnValue = Collections.emptyList();
}
else if (INVALIDATING_CONTEXT.get())
{
returnValue = Collections.emptyList();
}
else
{
Set<PageClassLoaderContext> contextsToInvalidate = new HashSet<>();
for (String resource : resources)
{
PageClassLoaderContext context = root.findByClassName(resource);
if (context != null && !context.isRoot())
{
contextsToInvalidate.add(context);
}
}
Set<String> 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<String> 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<String> classesToInvalidate) {
if (!classesToInvalidate.isEmpty())
{
LOGGER.debug("Invalidating classes {}", classesToInvalidate);
markAsInvalidatingContext();
final List<String> 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<String> 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<String> pageNames = componentClassResolver.getPageNames();
final List<String> 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);
}
catch (Exception e)
{
LOGGER.warn("Exception while preloading page " + page, 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));
}
}
}