    001// Copyright 2023 The Apache Software Foundation
    002//
    003// Licensed under the Apache License, Version 2.0 (the "License");
    004// you may not use this file except in compliance with the License.
    005// You may obtain a copy of the License at
    006//
    007//     http://www.apache.org/licenses/LICENSE-2.0
    008//
    009// Unless required by applicable law or agreed to in writing, software
    010// distributed under the License is distributed on an "AS IS" BASIS,
    011// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    012// See the License for the specific language governing permissions and
    013// limitations under the License.
    014package org.apache.tapestry5.services.pageload;
    015
    016import java.util.ArrayList;
    017import java.util.Collections;
    018import java.util.HashSet;
    019import java.util.List;
    020import java.util.Objects;
    021import java.util.Set;
    022import java.util.concurrent.atomic.AtomicInteger;
    023import java.util.function.Function;
    024import java.util.function.Supplier;
    025import java.util.stream.Collectors;
    026
    027import org.apache.tapestry5.SymbolConstants;
    028import org.apache.tapestry5.commons.internal.util.TapestryException;
    029import org.apache.tapestry5.commons.services.InvalidationEventHub;
    030import org.apache.tapestry5.commons.services.PlasticProxyFactory;
    031import org.apache.tapestry5.internal.services.ComponentDependencyRegistry;
    032import org.apache.tapestry5.internal.services.ComponentDependencyRegistry.DependencyType;
    033import org.apache.tapestry5.internal.services.InternalComponentInvalidationEventHub;
    034import org.apache.tapestry5.ioc.annotations.ComponentClasses;
    035import org.apache.tapestry5.ioc.annotations.Symbol;
    036import org.apache.tapestry5.plastic.PlasticUtils;
    037import org.apache.tapestry5.services.ComponentClassResolver;
    038import org.slf4j.Logger;
    039import org.slf4j.LoggerFactory;
    040
    041/**
    042 * Default {@linkplain PageClassLoaderContextManager} implementation.
    043 *
    044 * @since 5.8.3
    045 */
    046public class PageClassLoaderContextManagerImpl implements PageClassLoaderContextManager
    047{
    048
    049    private static final Logger LOGGER = LoggerFactory.getLogger(PageClassLoaderContextManager.class);
    050
    051    private final ComponentDependencyRegistry componentDependencyRegistry;
    052
    053    private final ComponentClassResolver componentClassResolver;
    054
    055    private final InternalComponentInvalidationEventHub invalidationHub;
    056
    057    private final InvalidationEventHub componentClassesInvalidationEventHub;
    058
    059    private final boolean multipleClassLoaders;
    060
    061    private final static ThreadLocal<Integer> NESTED_MERGE_COUNT = ThreadLocal.withInitial(() -> 0);
    062
    063    private final static ThreadLocal<Boolean> INVALIDATING_CONTEXT = ThreadLocal.withInitial(() -> false);
    064
    065    private static final AtomicInteger MERGED_COUNTER = new AtomicInteger(1);
    066
    067    private Function<ClassLoader, PlasticProxyFactory> plasticProxyFactoryProvider;
    068
    069    private PageClassLoaderContext root;
    070
    071    public PageClassLoaderContextManagerImpl(
    072            final ComponentDependencyRegistry componentDependencyRegistry,
    073            final ComponentClassResolver componentClassResolver,
    074            final InternalComponentInvalidationEventHub invalidationHub,
    075            final @ComponentClasses InvalidationEventHub componentClassesInvalidationEventHub,
    076            final @Symbol(SymbolConstants.MULTIPLE_CLASSLOADERS) boolean multipleClassLoaders)
    077    {
    078        super();
    079        this.componentDependencyRegistry = componentDependencyRegistry;
    080        this.componentClassResolver = componentClassResolver;
    081        this.invalidationHub = invalidationHub;
    082        this.componentClassesInvalidationEventHub = componentClassesInvalidationEventHub;
    083        this.multipleClassLoaders = multipleClassLoaders;
    084        invalidationHub.addInvalidationCallback(this::listen);
    085        NESTED_MERGE_COUNT.set(0);
    086    }
    087
    088    @Override
    089    public void invalidateUnknownContext()
    090    {
    091        synchronized (this) {
    092            markAsNotInvalidatingContext();
    093            for (PageClassLoaderContext context : root.getChildren())
    094            {
    095                if (context.isUnknown())
    096                {
    097                    invalidateAndFireInvalidationEvents(context);
    098                    break;
    099                }
    100            }
    101        }
    102    }
    103
    104    @Override
    105    public void initialize(
    106            final PageClassLoaderContext root,
    107            final Function<ClassLoader, PlasticProxyFactory> plasticProxyFactoryProvider)
    108    {
    109        if (this.root != null)
    110        {
    111            throw new IllegalStateException("PageClassloaderContextManager.initialize() can only be called once");
    112        }
    113        Objects.requireNonNull(root);
    114        Objects.requireNonNull(plasticProxyFactoryProvider);
    115        this.root = root;
    116        this.plasticProxyFactoryProvider = plasticProxyFactoryProvider;
    117        if (multipleClassLoaders)
    118        {
    119            LOGGER.debug("Root context: {}", root);
    120        }
    121    }
    122
    123    @Override
    124    public PageClassLoaderContext get(final String className)
    125    {
    126        PageClassLoaderContext context;
    127
    128        final String enclosingClassName = PlasticUtils.getEnclosingClassName(className);
    129        context = root.findByClassName(enclosingClassName);
    130
    131        if (context == null)
    132        {
    133            Set<String> classesToInvalidate = new HashSet<>();
    134
    135            context = processUsingDependencies(
    136                    enclosingClassName,
    137                    root,
    138                    () -> getUnknownContext(root, plasticProxyFactoryProvider),
    139                    plasticProxyFactoryProvider,
    140                    classesToInvalidate);
    141
    142            if (!classesToInvalidate.isEmpty())
    143            {
    144                invalidate(classesToInvalidate);
    145            }
    146
    147            if (!className.equals(enclosingClassName))
    148            {
    149                loadClass(className, context);
    150            }
    151
    152        }
    153
    154        return context;
    155
    156    }
    157
    158    private PageClassLoaderContext getUnknownContext(final PageClassLoaderContext root,
    159            final Function<ClassLoader, PlasticProxyFactory> plasticProxyFactoryProvider)
    160    {
    161
    162        PageClassLoaderContext unknownContext = null;
    163
    164        for (PageClassLoaderContext child : root.getChildren())
    165        {
    166            if (child.getName().equals(PageClassLoaderContext.UNKOWN_CONTEXT_NAME))
    167            {
    168                unknownContext = child;
    169                break;
    170            }
    171        }
    172
    173        if (unknownContext == null)
    174        {
    175            unknownContext = new PageClassLoaderContext(PageClassLoaderContext.UNKOWN_CONTEXT_NAME, root,
    176                    Collections.emptySet(),
    177                    plasticProxyFactoryProvider.apply(root.getClassLoader()),
    178                    this::get);
    179            root.addChild(unknownContext);
    180            if (multipleClassLoaders)
    181            {
    182                LOGGER.debug("Unknown context: {}", unknownContext);
    183            }
    184        }
    185        return unknownContext;
    186    }
    187
    188    private PageClassLoaderContext processUsingDependencies(
    189            String className,
    190            PageClassLoaderContext root,
    191            Supplier<PageClassLoaderContext> unknownContextProvider,
    192            Function<ClassLoader, PlasticProxyFactory> plasticProxyFactoryProvider, Set<String> classesToInvalidate)
    193    {
    194        return processUsingDependencies(className, root, unknownContextProvider, plasticProxyFactoryProvider, classesToInvalidate, new HashSet<>());
    195    }
    196
    197    private PageClassLoaderContext processUsingDependencies(
    198            String className,
    199            PageClassLoaderContext root,
    200            Supplier<PageClassLoaderContext> unknownContextProvider,
    201            Function<ClassLoader, PlasticProxyFactory> plasticProxyFactoryProvider,
    202            Set<String> classesToInvalidate,
    203            Set<String> alreadyProcessed)
    204    {
    205        return processUsingDependencies(className, root, unknownContextProvider,
    206                plasticProxyFactoryProvider, classesToInvalidate, alreadyProcessed, true);
    207    }
    208
    209
    210    private PageClassLoaderContext processUsingDependencies(
    211            String className,
    212            PageClassLoaderContext root,
    213            Supplier<PageClassLoaderContext> unknownContextProvider,
    214            Function<ClassLoader, PlasticProxyFactory> plasticProxyFactoryProvider,
    215            Set<String> classesToInvalidate,
    216            Set<String> alreadyProcessed,
    217            boolean processCircularDependencies)
    218    {
    219        PageClassLoaderContext context = root.findByClassName(className);
    220        if (context == null)
    221        {
    222
    223            // Class isn't in a controlled package, so it doesn't get transformed
    224            // and should go for the root context, which is never thrown out.
    225            if (!root.getPlasticManager().shouldInterceptClassLoading(className))
    226            {
    227                context = root;
    228            } else {
    229                if (
    230                        !componentDependencyRegistry.contains(className) ||
    231                        !multipleClassLoaders
    232                        // TODO: review this
    233//                        && componentDependencyRegistry.getDependents(className).isEmpty()
    234                        )
    235                {
    236                    context = unknownContextProvider.get();
    237                }
    238                else
    239                {
    240
    241                    alreadyProcessed.add(className);
    242
    243                    // Sorting dependencies alphabetically so we have consistent results.
    244                    List<String> dependencies = new ArrayList<>(getDependenciesWithoutPages(className));
    245                    Collections.sort(dependencies);
    246
    247                    // Process dependencies depth-first
    248                    for (String dependency : dependencies)
    249                    {
    250                        // Avoid infinite recursion loops
    251                        if (!alreadyProcessed.contains(dependency)/* &&
    252                                !circularDependencies.contains(dependency)*/)
    253                        {
    254                            processUsingDependencies(dependency, root, unknownContextProvider,
    255                                    plasticProxyFactoryProvider, classesToInvalidate, alreadyProcessed, false);
    256                        }
    257                    }
    258
    259                    // Collect context dependencies
    260                    Set<PageClassLoaderContext> contextDependencies = new HashSet<>();
    261                    for (String dependency : dependencies)
    262                    {
    263                        PageClassLoaderContext dependencyContext = root.findByClassName(dependency);
    264                        if (dependencyContext == null)
    265                        {
    266                            dependencyContext = processUsingDependencies(dependency, root, unknownContextProvider,
    267                                    plasticProxyFactoryProvider, classesToInvalidate, alreadyProcessed);
    268
    269                        }
    270                        if (!dependencyContext.isRoot())
    271                        {
    272                            contextDependencies.add(dependencyContext);
    273                        }
    274                    }
    275
    276                    if (contextDependencies.size() == 0)
    277                    {
    278                        context = new PageClassLoaderContext(
    279                                getContextName(className),
    280                                root,
    281                                Collections.singleton(className),
    282                                plasticProxyFactoryProvider.apply(root.getClassLoader()),
    283                                this::get);
    284                    }
    285                    else
    286                    {
    287                        PageClassLoaderContext parentContext;
    288                        if (contextDependencies.size() == 1)
    289                        {
    290                            parentContext = contextDependencies.iterator().next();
    291                        }
    292                        else
    293                        {
    294                            parentContext = merge(contextDependencies, plasticProxyFactoryProvider, root, classesToInvalidate);
    295                        }
    296                        context = new PageClassLoaderContext(
    297                                getContextName(className),
    298                                parentContext,
    299                                Collections.singleton(className),
    300                                plasticProxyFactoryProvider.apply(parentContext.getClassLoader()),
    301                                this::get);
    302                    }
    303
    304                    context.getParent().addChild(context);
    305
    306                    // Ensure non-page class is initialized in the correct context and classloader.
    307                    // Pages get their own context and classloader, so this initialization
    308                    // is both non-needed and a cause for an NPE if it happens.
    309                    if (!componentClassResolver.isPage(className)
    310                            || componentDependencyRegistry.getDependencies(className, DependencyType.USAGE).isEmpty())
    311                    {
    312                        loadClass(className, context);
    313                    }
    314
    315                    LOGGER.debug("New context: {}", context);
    316
    317                }
    318            }
    319
    320        }
    321        context.addClass(className);
    322
    323        return context;
    324    }
    325
    326    private Set<String> getDependenciesWithoutPages(String className)
    327    {
    328        Set<String> dependencies = new HashSet<>();
    329        dependencies.addAll(componentDependencyRegistry.getDependencies(className, DependencyType.USAGE));
    330        dependencies.addAll(componentDependencyRegistry.getDependencies(className, DependencyType.SUPERCLASS));
    331        return Collections.unmodifiableSet(dependencies);
    332    }
    333
    334    private Class<?> loadClass(String className, PageClassLoaderContext context)
    335    {
    336        try
    337        {
    338            final ClassLoader classLoader = context.getPlasticManager().getClassLoader();
    339            return classLoader.loadClass(className);
    340        } catch (Exception e) {
    341            throw new RuntimeException(e);
    342        }
    343    }
    344
    345    private PageClassLoaderContext merge(
    346            Set<PageClassLoaderContext> contextDependencies,
    347            Function<ClassLoader, PlasticProxyFactory> plasticProxyFactoryProvider,
    348            PageClassLoaderContext root, Set<String> classesToInvalidate)
    349    {
    350
    351        NESTED_MERGE_COUNT.set(NESTED_MERGE_COUNT.get() + 1);
    352
    353        if (LOGGER.isDebugEnabled())
    354        {
    355
    356            LOGGER.debug("Nested merge count going up to {}", NESTED_MERGE_COUNT.get());
    357
    358            String classes;
    359            StringBuilder builder = new StringBuilder();
    360            builder.append("Merging the following page classloader contexts into one:\n");
    361            for (PageClassLoaderContext context : contextDependencies)
    362            {
    363                classes = context.getClassNames().stream()
    364                        .map(this::getContextName)
    365                        .sorted()
    366                        .collect(Collectors.joining(", "));
    367                builder.append(String.format("\t%s (parent %s) (%s)\n", context.getName(), context.getParent().getName(), classes));
    368            }
    369            LOGGER.debug(builder.toString().trim());
    370        }
    371
    372        Set<PageClassLoaderContext> allContextsIncludingDescendents = new HashSet<>();
    373        for (PageClassLoaderContext context : contextDependencies)
    374        {
    375            allContextsIncludingDescendents.add(context);
    376            allContextsIncludingDescendents.addAll(context.getDescendents());
    377        }
    378
    379        PageClassLoaderContext merged;
    380
    381        // Collect the classes in these dependencies, then invalidate the contexts
    382
    383        Set<PageClassLoaderContext> furtherDependencies = new HashSet<>();
    384
    385        Set<String> classNames = new HashSet<>();
    386
    387        for (PageClassLoaderContext context : contextDependencies)
    388        {
    389            if (!context.isRoot())
    390            {
    391                classNames.addAll(context.getClassNames());
    392            }
    393            final PageClassLoaderContext parent = context.getParent();
    394            // We don't want the merged context to have a further dependency on
    395            // the root context (it's not mergeable) nor on itself.
    396            if (!parent.isRoot() &&
    397                    !allContextsIncludingDescendents.contains(parent))
    398            {
    399                furtherDependencies.add(parent);
    400            }
    401        }
    402
    403        final List<PageClassLoaderContext> contextsToInvalidate = contextDependencies.stream()
    404            .filter(c -> !c.isRoot())
    405            .collect(Collectors.toList());
    406
    407        if (!contextsToInvalidate.isEmpty())
    408        {
    409            classesToInvalidate.addAll(invalidate(contextsToInvalidate.toArray(new PageClassLoaderContext[contextsToInvalidate.size()])));
    410        }
    411
    412        PageClassLoaderContext parent;
    413
    414        // No context dependencies, so parent is going to be the root one
    415        if (furtherDependencies.size() == 0)
    416        {
    417            parent = root;
    418        }
    419        else
    420        {
    421            // Single shared context dependency, so it's our parent
    422            if (furtherDependencies.size() == 1)
    423            {
    424                parent = furtherDependencies.iterator().next();
    425            }
    426            // No single context dependency, so we'll need to recursively merge it
    427            // so we can have a single parent.
    428            else
    429            {
    430                parent = merge(furtherDependencies, plasticProxyFactoryProvider, root, classesToInvalidate);
    431                LOGGER.debug("New context: {}", parent);
    432            }
    433        }
    434
    435        merged = new PageClassLoaderContext(
    436            "merged " + MERGED_COUNTER.getAndIncrement(),
    437            parent,
    438            classNames,
    439            plasticProxyFactoryProvider.apply(parent.getClassLoader()),
    440            this::get);
    441
    442        parent.addChild(merged);
    443
    444//        for (String className : classNames)
    445//        {
    446//            loadClass(className, merged);
    447//        }
    448
    449        NESTED_MERGE_COUNT.set(NESTED_MERGE_COUNT.get() - 1);
    450        if (LOGGER.isDebugEnabled())
    451        {
    452            LOGGER.debug("Nested merge count going down to {}", NESTED_MERGE_COUNT.get());
    453        }
    454
    455        return merged;
    456    }
    457
    458    @Override
    459    public void clear(String className)
    460    {
    461        final PageClassLoaderContext context = root.findByClassName(className);
    462        if (context != null)
    463        {
    464//            invalidationHub.fireInvalidationEvent(new ArrayList<>(invalidate(context)));
    465            invalidate(context);
    466        }
    467    }
    468
    469    private String getContextName(String className)
    470    {
    471        String contextName = componentClassResolver.getLogicalName(className);
    472        if (contextName == null)
    473        {
    474            contextName = className;
    475        }
    476        return contextName;
    477    }
    478
    479    @Override
    480    public Set<String> invalidate(PageClassLoaderContext ... contexts)
    481    {
    482        Set<String> classNames = new HashSet<>();
    483        for (PageClassLoaderContext context : contexts) {
    484            addClassNames(context, classNames);
    485            context.invalidate();
    486            if (context.getParent() != null)
    487            {
    488                context.getParent().removeChild(context);
    489            }
    490        }
    491        return classNames;
    492    }
    493
    494    private List<String> listen(List<String> resources)
    495    {
    496
    497        List<String> returnValue;
    498
    499        if (!multipleClassLoaders)
    500        {
    501            for (PageClassLoaderContext context : root.getChildren())
    502            {
    503                context.invalidate();
    504            }
    505            returnValue = Collections.emptyList();
    506        }
    507        else if (INVALIDATING_CONTEXT.get())
    508        {
    509            returnValue = Collections.emptyList();
    510        }
    511        else
    512        {
    513
    514            Set<PageClassLoaderContext> contextsToInvalidate = new HashSet<>();
    515            for (String resource : resources)
    516            {
    517                PageClassLoaderContext context = root.findByClassName(resource);
    518                if (context != null && !context.isRoot())
    519                {
    520                    contextsToInvalidate.add(context);
    521                }
    522            }
    523
    524            Set<String> furtherResources = invalidate(contextsToInvalidate.toArray(
    525                    new PageClassLoaderContext[contextsToInvalidate.size()]));
    526
    527            // We don't want to invalidate resources more than once
    528            furtherResources.removeAll(resources);
    529
    530            returnValue = new ArrayList<>(furtherResources);
    531        }
    532
    533        return returnValue;
    534
    535    }
    536
    537    @SuppressWarnings("unchecked")
    538    @Override
    539    public void invalidateAndFireInvalidationEvents(PageClassLoaderContext... contexts) {
    540        markAsInvalidatingContext();
    541        if (multipleClassLoaders)
    542        {
    543            final Set<String> classNames = invalidate(contexts);
    544            invalidate(classNames);
    545        }
    546        else
    547        {
    548            invalidate(Collections.EMPTY_SET);
    549        }
    550        markAsNotInvalidatingContext();
    551    }
    552
    553    private void markAsNotInvalidatingContext() {
    554        INVALIDATING_CONTEXT.set(false);
    555    }
    556
    557    private void markAsInvalidatingContext() {
    558        INVALIDATING_CONTEXT.set(true);
    559    }
    560
    561    private void invalidate(Set<String> classesToInvalidate) {
    562        if (!classesToInvalidate.isEmpty())
    563        {
    564            LOGGER.debug("Invalidating classes {}", classesToInvalidate);
    565            markAsInvalidatingContext();
    566            final List<String> classesToInvalidateAsList = new ArrayList<>(classesToInvalidate);
    567
    568            componentDependencyRegistry.disableInvalidations();
    569
    570            try
    571            {
    572                // TODO: do we really need both invalidation hubs to be invoked here?
    573                invalidationHub.fireInvalidationEvent(classesToInvalidateAsList);
    574                componentClassesInvalidationEventHub.fireInvalidationEvent(classesToInvalidateAsList);
    575                markAsNotInvalidatingContext();
    576            }
    577            finally
    578            {
    579                componentDependencyRegistry.enableInvalidations();
    580            }
    581
    582        }
    583    }
    584
    585    private void addClassNames(PageClassLoaderContext context, Set<String> classNames) {
    586        classNames.addAll(context.getClassNames());
    587        for (PageClassLoaderContext child : context.getChildren()) {
    588            addClassNames(child, classNames);
    589        }
    590    }
    591
    592    @Override
    593    public PageClassLoaderContext getRoot() {
    594        return root;
    595    }
    596
    597    @Override
    598    public boolean isMerging()
    599    {
    600        return NESTED_MERGE_COUNT.get() > 0;
    601    }
    602
    603    @Override
    604    public void clear()
    605    {
    606    }
    607
    608    @Override
    609    public Class<?> getClassInstance(Class<?> clasz, String pageName)
    610    {
    611        final String className = clasz.getName();
    612        PageClassLoaderContext context = root.findByClassName(className);
    613        if (context == null)
    614        {
    615            context = get(className);
    616        }
    617        try
    618        {
    619            clasz = context.getProxyFactory().getClassLoader().loadClass(className);
    620        } catch (ClassNotFoundException e)
    621        {
    622            throw new TapestryException(e.getMessage(), e);
    623        }
    624        return clasz;
    625    }
    626
    627    @Override
    628    public void preload()
    629    {
    630
    631        final PageClassLoaderContext context = new PageClassLoaderContext(PageClassLoaderContext.UNKOWN_CONTEXT_NAME, root,
    632                Collections.emptySet(),
    633                plasticProxyFactoryProvider.apply(root.getClassLoader()),
    634                this::get);
    635
    636        final List<String> pageNames = componentClassResolver.getPageNames();
    637        final List<String> classNames = new ArrayList<>(pageNames.size());
    638
    639        long start = System.currentTimeMillis();
    640
    641        LOGGER.info("Preloading dependency information for {} pages", pageNames.size());
    642
    643        for (String page : pageNames)
    644        {
    645            try
    646            {
    647                final String className = componentClassResolver.resolvePageNameToClassName(page);
    648                componentDependencyRegistry.register(context.getClassLoader().loadClass(className));
    649                classNames.add(className);
    650            } catch (ClassNotFoundException e)
    651            {
    652                throw new RuntimeException(e);
    653            }
    654        }
    655
    656        long finish = System.currentTimeMillis();
    657
    658        if (LOGGER.isInfoEnabled())
    659        {
    660            LOGGER.info(String.format("Dependency information gathered in %.3f ms", (finish - start) / 1000.0));
    661        }
    662
    663        context.invalidate();
    664
    665        LOGGER.info("Starting preloading page classloader contexts");
    666
    667        start = System.currentTimeMillis();
    668
    669        for (int i = 0; i < 10; i++)
    670        {
    671            for (String className : classNames)
    672            {
    673                get(className);
    674            }
    675        }
    676
    677        finish = System.currentTimeMillis();
    678
    679        if (LOGGER.isInfoEnabled())
    680        {
    681            LOGGER.info(String.format("Preloading of page classloadercontexts finished in %.3f ms", (finish - start) / 1000.0));
    682        }
    683
    684    }
    685
    686}