    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            // Class isn't in a controlled package, so it doesn't get transformed
    225            // and should go for the root context, which is never thrown out.
    226            if (!root.getPlasticManager().shouldInterceptClassLoading(className))
    227            {
    228                context = root;
    229            } else {
    230                if (!productionMode && (
    231                        !componentDependencyRegistry.contains(className) ||
    232                        !multipleClassLoaders))
    233                {
    234                    context = unknownContextProvider.get();
    235                }
    236                else
    237                {
    238
    239                    alreadyProcessed.add(className);
    240
    241                    // Sorting dependencies alphabetically so we have consistent results.
    242                    List<String> dependencies = new ArrayList<>(getDependenciesWithoutPages(className));
    243                    Collections.sort(dependencies);
    244
    245                    // Process dependencies depth-first
    246                    for (String dependency : dependencies)
    247                    {
    248                        // Avoid infinite recursion loops
    249                        if (!alreadyProcessed.contains(dependency)/* &&
    250                                !circularDependencies.contains(dependency)*/)
    251                        {
    252                            processUsingDependencies(dependency, root, unknownContextProvider,
    253                                    plasticProxyFactoryProvider, classesToInvalidate, alreadyProcessed, false);
    254                        }
    255                    }
    256
    257                    // Collect context dependencies
    258                    Set<PageClassLoaderContext> contextDependencies = new HashSet<>();
    259                    for (String dependency : dependencies)
    260                    {
    261                        PageClassLoaderContext dependencyContext = root.findByClassName(dependency);
    262                        if (dependencyContext == null)
    263                        {
    264                            dependencyContext = processUsingDependencies(dependency, root, unknownContextProvider,
    265                                    plasticProxyFactoryProvider, classesToInvalidate, alreadyProcessed);
    266
    267                        }
    268                        if (!dependencyContext.isRoot())
    269                        {
    270                            contextDependencies.add(dependencyContext);
    271                        }
    272                    }
    273
    274                    if (!multipleClassLoaders)
    275                    {
    276                        context = root;
    277                    }
    278                    else if (contextDependencies.size() == 0)
    279                    {
    280                        context = new PageClassLoaderContext(
    281                                getContextName(className),
    282                                root,
    283                                Collections.singleton(className),
    284                                plasticProxyFactoryProvider.apply(root.getClassLoader()),
    285                                this::get);
    286                    }
    287                    else
    288                    {
    289                        PageClassLoaderContext parentContext;
    290                        if (contextDependencies.size() == 1)
    291                        {
    292                            parentContext = contextDependencies.iterator().next();
    293                        }
    294                        else
    295                        {
    296                            parentContext = merge(contextDependencies, plasticProxyFactoryProvider, root, classesToInvalidate);
    297                        }
    298                        context = new PageClassLoaderContext(
    299                                getContextName(className),
    300                                parentContext,
    301                                Collections.singleton(className),
    302                                plasticProxyFactoryProvider.apply(parentContext.getClassLoader()),
    303                                this::get);
    304                    }
    305
    306                    if (multipleClassLoaders)
    307                    {
    308                        context.getParent().addChild(context);
    309                    }
    310
    311                    // Ensure non-page class is initialized in the correct context and classloader.
    312                    // Pages get their own context and classloader, so this initialization
    313                    // is both non-needed and a cause for an NPE if it happens.
    314                    if (!componentClassResolver.isPage(className)
    315                            || componentDependencyRegistry.getDependencies(className, DependencyType.USAGE).isEmpty())
    316                    {
    317                        loadClass(className, context);
    318                    }
    319
    320                    if (multipleClassLoaders)
    321                    {
    322                        LOGGER.debug("New context: {}", context);
    323                    }
    324
    325                }
    326            }
    327
    328        }
    329        context.addClass(className);
    330
    331        return context;
    332    }
    333
    334    private Set<String> getDependenciesWithoutPages(String className)
    335    {
    336        Set<String> dependencies = new HashSet<>();
    337        dependencies.addAll(componentDependencyRegistry.getDependencies(className, DependencyType.USAGE));
    338        dependencies.addAll(componentDependencyRegistry.getDependencies(className, DependencyType.SUPERCLASS));
    339        return Collections.unmodifiableSet(dependencies);
    340    }
    341
    342    private Class<?> loadClass(String className, PageClassLoaderContext context)
    343    {
    344        try
    345        {
    346            final ClassLoader classLoader = context.getPlasticManager().getClassLoader();
    347            return classLoader.loadClass(className);
    348        } catch (Exception e) {
    349            throw new RuntimeException(e);
    350        }
    351    }
    352
    353    private PageClassLoaderContext merge(
    354            Set<PageClassLoaderContext> contextDependencies,
    355            Function<ClassLoader, PlasticProxyFactory> plasticProxyFactoryProvider,
    356            PageClassLoaderContext root, Set<String> classesToInvalidate)
    357    {
    358
    359        NESTED_MERGE_COUNT.set(NESTED_MERGE_COUNT.get() + 1);
    360
    361        if (LOGGER.isDebugEnabled())
    362        {
    363
    364            LOGGER.debug("Nested merge count going up to {}", NESTED_MERGE_COUNT.get());
    365
    366            String classes;
    367            StringBuilder builder = new StringBuilder();
    368            builder.append("Merging the following page classloader contexts into one:\n");
    369            for (PageClassLoaderContext context : contextDependencies)
    370            {
    371                classes = context.getClassNames().stream()
    372                        .map(this::getContextName)
    373                        .sorted()
    374                        .collect(Collectors.joining(", "));
    375                builder.append(String.format("\t%s (parent %s) (%s)\n", context.getName(), context.getParent().getName(), classes));
    376            }
    377            LOGGER.debug(builder.toString().trim());
    378        }
    379
    380        Set<PageClassLoaderContext> allContextsIncludingDescendents = new HashSet<>();
    381        for (PageClassLoaderContext context : contextDependencies)
    382        {
    383            allContextsIncludingDescendents.add(context);
    384            allContextsIncludingDescendents.addAll(context.getDescendents());
    385        }
    386
    387        PageClassLoaderContext merged;
    388
    389        // Collect the classes in these dependencies, then invalidate the contexts
    390
    391        Set<PageClassLoaderContext> furtherDependencies = new HashSet<>();
    392
    393        Set<String> classNames = new HashSet<>();
    394
    395        for (PageClassLoaderContext context : contextDependencies)
    396        {
    397            if (!context.isRoot())
    398            {
    399                classNames.addAll(context.getClassNames());
    400            }
    401            final PageClassLoaderContext parent = context.getParent();
    402            // We don't want the merged context to have a further dependency on
    403            // the root context (it's not mergeable) nor on itself.
    404            if (!parent.isRoot() &&
    405                    !allContextsIncludingDescendents.contains(parent))
    406            {
    407                furtherDependencies.add(parent);
    408            }
    409        }
    410
    411        final List<PageClassLoaderContext> contextsToInvalidate = contextDependencies.stream()
    412            .filter(c -> !c.isRoot())
    413            .collect(Collectors.toList());
    414
    415        if (!contextsToInvalidate.isEmpty())
    416        {
    417            classesToInvalidate.addAll(invalidate(contextsToInvalidate.toArray(new PageClassLoaderContext[contextsToInvalidate.size()])));
    418        }
    419
    420        PageClassLoaderContext parent;
    421
    422        // No context dependencies, so parent is going to be the root one
    423        if (furtherDependencies.size() == 0)
    424        {
    425            parent = root;
    426        }
    427        else
    428        {
    429            // Single shared context dependency, so it's our parent
    430            if (furtherDependencies.size() == 1)
    431            {
    432                parent = furtherDependencies.iterator().next();
    433            }
    434            // No single context dependency, so we'll need to recursively merge it
    435            // so we can have a single parent.
    436            else
    437            {
    438                parent = merge(furtherDependencies, plasticProxyFactoryProvider, root, classesToInvalidate);
    439                LOGGER.debug("New context: {}", parent);
    440            }
    441        }
    442
    443        merged = new PageClassLoaderContext(
    444            "merged " + MERGED_COUNTER.getAndIncrement(),
    445            parent,
    446            classNames,
    447            plasticProxyFactoryProvider.apply(parent.getClassLoader()),
    448            this::get);
    449
    450        parent.addChild(merged);
    451
    452//        for (String className : classNames)
    453//        {
    454//            loadClass(className, merged);
    455//        }
    456
    457        NESTED_MERGE_COUNT.set(NESTED_MERGE_COUNT.get() - 1);
    458        if (LOGGER.isDebugEnabled())
    459        {
    460            LOGGER.debug("Nested merge count going down to {}", NESTED_MERGE_COUNT.get());
    461        }
    462
    463        return merged;
    464    }
    465
    466    @Override
    467    public void clear(String className)
    468    {
    469        final PageClassLoaderContext context = root.findByClassName(className);
    470        if (context != null)
    471        {
    472//            invalidationHub.fireInvalidationEvent(new ArrayList<>(invalidate(context)));
    473            invalidate(context);
    474        }
    475    }
    476
    477    private String getContextName(String className)
    478    {
    479        String contextName = componentClassResolver.getLogicalName(className);
    480        if (contextName == null)
    481        {
    482            contextName = className;
    483        }
    484        return contextName;
    485    }
    486
    487    @Override
    488    public Set<String> invalidate(PageClassLoaderContext ... contexts)
    489    {
    490        Set<String> classNames = new HashSet<>();
    491        for (PageClassLoaderContext context : contexts) {
    492            addClassNames(context, classNames);
    493            context.invalidate();
    494            if (context.getParent() != null)
    495            {
    496                context.getParent().removeChild(context);
    497            }
    498        }
    499        return classNames;
    500    }
    501
    502    private List<String> listen(List<String> resources)
    503    {
    504
    505        List<String> returnValue;
    506
    507        if (!multipleClassLoaders)
    508        {
    509            for (PageClassLoaderContext context : root.getChildren())
    510            {
    511                context.invalidate();
    512            }
    513            returnValue = Collections.emptyList();
    514        }
    515        else if (INVALIDATING_CONTEXT.get())
    516        {
    517            returnValue = Collections.emptyList();
    518        }
    519        else
    520        {
    521
    522            Set<PageClassLoaderContext> contextsToInvalidate = new HashSet<>();
    523            for (String resource : resources)
    524            {
    525                PageClassLoaderContext context = root.findByClassName(resource);
    526                if (context != null && !context.isRoot())
    527                {
    528                    contextsToInvalidate.add(context);
    529                }
    530            }
    531
    532            Set<String> furtherResources = invalidate(contextsToInvalidate.toArray(
    533                    new PageClassLoaderContext[contextsToInvalidate.size()]));
    534
    535            // We don't want to invalidate resources more than once
    536            furtherResources.removeAll(resources);
    537
    538            returnValue = new ArrayList<>(furtherResources);
    539        }
    540
    541        return returnValue;
    542
    543    }
    544
    545    @SuppressWarnings("unchecked")
    546    @Override
    547    public void invalidateAndFireInvalidationEvents(PageClassLoaderContext... contexts) {
    548        markAsInvalidatingContext();
    549        if (multipleClassLoaders)
    550        {
    551            final Set<String> classNames = invalidate(contexts);
    552            invalidate(classNames);
    553        }
    554        else
    555        {
    556            invalidate(Collections.EMPTY_SET);
    557        }
    558        markAsNotInvalidatingContext();
    559    }
    560
    561    private void markAsNotInvalidatingContext() {
    562        INVALIDATING_CONTEXT.set(false);
    563    }
    564
    565    private void markAsInvalidatingContext() {
    566        INVALIDATING_CONTEXT.set(true);
    567    }
    568
    569    private void invalidate(Set<String> classesToInvalidate) {
    570        if (!classesToInvalidate.isEmpty())
    571        {
    572            LOGGER.debug("Invalidating classes {}", classesToInvalidate);
    573            markAsInvalidatingContext();
    574            final List<String> classesToInvalidateAsList = new ArrayList<>(classesToInvalidate);
    575
    576            componentDependencyRegistry.disableInvalidations();
    577
    578            try
    579            {
    580                // TODO: do we really need both invalidation hubs to be invoked here?
    581                invalidationHub.fireInvalidationEvent(classesToInvalidateAsList);
    582                componentClassesInvalidationEventHub.fireInvalidationEvent(classesToInvalidateAsList);
    583                markAsNotInvalidatingContext();
    584            }
    585            finally
    586            {
    587                componentDependencyRegistry.enableInvalidations();
    588            }
    589
    590        }
    591    }
    592
    593    private void addClassNames(PageClassLoaderContext context, Set<String> classNames) {
    594        classNames.addAll(context.getClassNames());
    595        for (PageClassLoaderContext child : context.getChildren()) {
    596            addClassNames(child, classNames);
    597        }
    598    }
    599
    600    @Override
    601    public PageClassLoaderContext getRoot() {
    602        return root;
    603    }
    604
    605    @Override
    606    public boolean isMerging()
    607    {
    608        return NESTED_MERGE_COUNT.get() > 0;
    609    }
    610
    611    @Override
    612    public void clear()
    613    {
    614    }
    615
    616    @Override
    617    public Class<?> getClassInstance(Class<?> clasz, String pageName)
    618    {
    619        final String className = clasz.getName();
    620        PageClassLoaderContext context = root.findByClassName(className);
    621        if (context == null)
    622        {
    623            context = get(className);
    624        }
    625        try
    626        {
    627            clasz = context.getProxyFactory().getClassLoader().loadClass(className);
    628        } catch (ClassNotFoundException e)
    629        {
    630            throw new TapestryException(e.getMessage(), e);
    631        }
    632        return clasz;
    633    }
    634
    635    @Override
    636    public void preload()
    637    {
    638
    639        final PageClassLoaderContext context = new PageClassLoaderContext(PageClassLoaderContext.UNKOWN_CONTEXT_NAME, root,
    640                Collections.emptySet(),
    641                plasticProxyFactoryProvider.apply(root.getClassLoader()),
    642                this::get);
    643
    644        final List<String> pageNames = componentClassResolver.getPageNames();
    645        final List<String> classNames = new ArrayList<>(pageNames.size());
    646
    647        long start = System.currentTimeMillis();
    648
    649        LOGGER.info("Preloading dependency information for {} pages", pageNames.size());
    650
    651        for (String page : pageNames)
    652        {
    653            try
    654            {
    655                final String className = componentClassResolver.resolvePageNameToClassName(page);
    656                componentDependencyRegistry.register(context.getClassLoader().loadClass(className));
    657                classNames.add(className);
    658            } catch (ClassNotFoundException e)
    659            {
    660                throw new RuntimeException(e);
    661            }
    662            catch (Exception e)
    663            {
    664                LOGGER.warn("Exception while preloading page " + page, e);
    665            }
    666        }
    667
    668        long finish = System.currentTimeMillis();
    669
    670        if (LOGGER.isInfoEnabled())
    671        {
    672            LOGGER.info(String.format("Dependency information gathered in %.3f ms", (finish - start) / 1000.0));
    673        }
    674
    675        context.invalidate();
    676
    677        LOGGER.info("Starting preloading page classloader contexts");
    678
    679        start = System.currentTimeMillis();
    680
    681        for (int i = 0; i < 10; i++)
    682        {
    683            for (String className : classNames)
    684            {
    685                get(className);
    686            }
    687        }
    688
    689        finish = System.currentTimeMillis();
    690
    691        if (LOGGER.isInfoEnabled())
    692        {
    693            LOGGER.info(String.format("Preloading of page classloadercontexts finished in %.3f ms", (finish - start) / 1000.0));
    694        }
    695
    696    }
    697
    698}