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