    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        return componentDependencyRegistry.getDependencies(className, DependencyType.USAGE);
    329    }
    330
    331    private Class<?> loadClass(String className, PageClassLoaderContext context)
    332    {
    333        try
    334        {
    335            final ClassLoader classLoader = context.getPlasticManager().getClassLoader();
    336            return classLoader.loadClass(className);
    337        } catch (Exception e) {
    338            throw new RuntimeException(e);
    339        }
    340    }
    341
    342    private PageClassLoaderContext merge(
    343            Set<PageClassLoaderContext> contextDependencies,
    344            Function<ClassLoader, PlasticProxyFactory> plasticProxyFactoryProvider,
    345            PageClassLoaderContext root, Set<String> classesToInvalidate)
    346    {
    347
    348        NESTED_MERGE_COUNT.set(NESTED_MERGE_COUNT.get() + 1);
    349
    350        if (LOGGER.isDebugEnabled())
    351        {
    352
    353            LOGGER.debug("Nested merge count going up to {}", NESTED_MERGE_COUNT.get());
    354
    355            String classes;
    356            StringBuilder builder = new StringBuilder();
    357            builder.append("Merging the following page classloader contexts into one:\n");
    358            for (PageClassLoaderContext context : contextDependencies)
    359            {
    360                classes = context.getClassNames().stream()
    361                        .map(this::getContextName)
    362                        .sorted()
    363                        .collect(Collectors.joining(", "));
    364                builder.append(String.format("\t%s (parent %s) (%s)\n", context.getName(), context.getParent().getName(), classes));
    365            }
    366            LOGGER.debug(builder.toString().trim());
    367        }
    368
    369        Set<PageClassLoaderContext> allContextsIncludingDescendents = new HashSet<>();
    370        for (PageClassLoaderContext context : contextDependencies)
    371        {
    372            allContextsIncludingDescendents.add(context);
    373            allContextsIncludingDescendents.addAll(context.getDescendents());
    374        }
    375
    376        PageClassLoaderContext merged;
    377
    378        // Collect the classes in these dependencies, then invalidate the contexts
    379
    380        Set<PageClassLoaderContext> furtherDependencies = new HashSet<>();
    381
    382        Set<String> classNames = new HashSet<>();
    383
    384        for (PageClassLoaderContext context : contextDependencies)
    385        {
    386            if (!context.isRoot())
    387            {
    388                classNames.addAll(context.getClassNames());
    389            }
    390            final PageClassLoaderContext parent = context.getParent();
    391            // We don't want the merged context to have a further dependency on
    392            // the root context (it's not mergeable) nor on itself.
    393            if (!parent.isRoot() &&
    394                    !allContextsIncludingDescendents.contains(parent))
    395            {
    396                furtherDependencies.add(parent);
    397            }
    398        }
    399
    400        final List<PageClassLoaderContext> contextsToInvalidate = contextDependencies.stream()
    401            .filter(c -> !c.isRoot())
    402            .collect(Collectors.toList());
    403
    404        if (!contextsToInvalidate.isEmpty())
    405        {
    406            classesToInvalidate.addAll(invalidate(contextsToInvalidate.toArray(new PageClassLoaderContext[contextsToInvalidate.size()])));
    407        }
    408
    409        PageClassLoaderContext parent;
    410
    411        // No context dependencies, so parent is going to be the root one
    412        if (furtherDependencies.size() == 0)
    413        {
    414            parent = root;
    415        }
    416        else
    417        {
    418            // Single shared context dependency, so it's our parent
    419            if (furtherDependencies.size() == 1)
    420            {
    421                parent = furtherDependencies.iterator().next();
    422            }
    423            // No single context dependency, so we'll need to recursively merge it
    424            // so we can have a single parent.
    425            else
    426            {
    427                parent = merge(furtherDependencies, plasticProxyFactoryProvider, root, classesToInvalidate);
    428                LOGGER.debug("New context: {}", parent);
    429            }
    430        }
    431
    432        merged = new PageClassLoaderContext(
    433            "merged " + MERGED_COUNTER.getAndIncrement(),
    434            parent,
    435            classNames,
    436            plasticProxyFactoryProvider.apply(parent.getClassLoader()),
    437            this::get);
    438
    439        parent.addChild(merged);
    440
    441//        for (String className : classNames)
    442//        {
    443//            loadClass(className, merged);
    444//        }
    445
    446        NESTED_MERGE_COUNT.set(NESTED_MERGE_COUNT.get() - 1);
    447        if (LOGGER.isDebugEnabled())
    448        {
    449            LOGGER.debug("Nested merge count going down to {}", NESTED_MERGE_COUNT.get());
    450        }
    451
    452        return merged;
    453    }
    454
    455    @Override
    456    public void clear(String className)
    457    {
    458        final PageClassLoaderContext context = root.findByClassName(className);
    459        if (context != null)
    460        {
    461//            invalidationHub.fireInvalidationEvent(new ArrayList<>(invalidate(context)));
    462            invalidate(context);
    463        }
    464    }
    465
    466    private String getContextName(String className)
    467    {
    468        String contextName = componentClassResolver.getLogicalName(className);
    469        if (contextName == null)
    470        {
    471            contextName = className;
    472        }
    473        return contextName;
    474    }
    475
    476    @Override
    477    public Set<String> invalidate(PageClassLoaderContext ... contexts)
    478    {
    479        Set<String> classNames = new HashSet<>();
    480        for (PageClassLoaderContext context : contexts) {
    481            addClassNames(context, classNames);
    482            context.invalidate();
    483            if (context.getParent() != null)
    484            {
    485                context.getParent().removeChild(context);
    486            }
    487        }
    488        return classNames;
    489    }
    490
    491    private List<String> listen(List<String> resources)
    492    {
    493
    494        List<String> returnValue;
    495
    496        if (!multipleClassLoaders)
    497        {
    498            for (PageClassLoaderContext context : root.getChildren())
    499            {
    500                context.invalidate();
    501            }
    502            returnValue = Collections.emptyList();
    503        }
    504        else if (INVALIDATING_CONTEXT.get())
    505        {
    506            returnValue = Collections.emptyList();
    507        }
    508        else
    509        {
    510
    511            Set<PageClassLoaderContext> contextsToInvalidate = new HashSet<>();
    512            for (String resource : resources)
    513            {
    514                PageClassLoaderContext context = root.findByClassName(resource);
    515                if (context != null && !context.isRoot())
    516                {
    517                    contextsToInvalidate.add(context);
    518                }
    519            }
    520
    521            Set<String> furtherResources = invalidate(contextsToInvalidate.toArray(
    522                    new PageClassLoaderContext[contextsToInvalidate.size()]));
    523
    524            // We don't want to invalidate resources more than once
    525            furtherResources.removeAll(resources);
    526
    527            returnValue = new ArrayList<>(furtherResources);
    528        }
    529
    530        return returnValue;
    531
    532    }
    533
    534    @SuppressWarnings("unchecked")
    535    @Override
    536    public void invalidateAndFireInvalidationEvents(PageClassLoaderContext... contexts) {
    537        markAsInvalidatingContext();
    538        if (multipleClassLoaders)
    539        {
    540            final Set<String> classNames = invalidate(contexts);
    541            invalidate(classNames);
    542        }
    543        else
    544        {
    545            invalidate(Collections.EMPTY_SET);
    546        }
    547        markAsNotInvalidatingContext();
    548    }
    549
    550    private void markAsNotInvalidatingContext() {
    551        INVALIDATING_CONTEXT.set(false);
    552    }
    553
    554    private void markAsInvalidatingContext() {
    555        INVALIDATING_CONTEXT.set(true);
    556    }
    557
    558    private void invalidate(Set<String> classesToInvalidate) {
    559        if (!classesToInvalidate.isEmpty())
    560        {
    561            LOGGER.debug("Invalidating classes {}", classesToInvalidate);
    562            markAsInvalidatingContext();
    563            final List<String> classesToInvalidateAsList = new ArrayList<>(classesToInvalidate);
    564
    565            componentDependencyRegistry.disableInvalidations();
    566
    567            try
    568            {
    569                // TODO: do we really need both invalidation hubs to be invoked here?
    570                invalidationHub.fireInvalidationEvent(classesToInvalidateAsList);
    571                componentClassesInvalidationEventHub.fireInvalidationEvent(classesToInvalidateAsList);
    572                markAsNotInvalidatingContext();
    573            }
    574            finally
    575            {
    576                componentDependencyRegistry.enableInvalidations();
    577            }
    578
    579        }
    580    }
    581
    582    private void addClassNames(PageClassLoaderContext context, Set<String> classNames) {
    583        classNames.addAll(context.getClassNames());
    584        for (PageClassLoaderContext child : context.getChildren()) {
    585            addClassNames(child, classNames);
    586        }
    587    }
    588
    589    @Override
    590    public PageClassLoaderContext getRoot() {
    591        return root;
    592    }
    593
    594    @Override
    595    public boolean isMerging()
    596    {
    597        return NESTED_MERGE_COUNT.get() > 0;
    598    }
    599
    600    @Override
    601    public void clear()
    602    {
    603    }
    604
    605    @Override
    606    public Class<?> getClassInstance(Class<?> clasz, String pageName)
    607    {
    608        final String className = clasz.getName();
    609        PageClassLoaderContext context = root.findByClassName(className);
    610        if (context == null)
    611        {
    612            context = get(className);
    613        }
    614        try
    615        {
    616            clasz = context.getProxyFactory().getClassLoader().loadClass(className);
    617        } catch (ClassNotFoundException e)
    618        {
    619            throw new TapestryException(e.getMessage(), e);
    620        }
    621        return clasz;
    622    }
    623
    624    @Override
    625    public void preload()
    626    {
    627
    628        final PageClassLoaderContext context = new PageClassLoaderContext(PageClassLoaderContext.UNKOWN_CONTEXT_NAME, root,
    629                Collections.emptySet(),
    630                plasticProxyFactoryProvider.apply(root.getClassLoader()),
    631                this::get);
    632
    633        final List<String> pageNames = componentClassResolver.getPageNames();
    634        final List<String> classNames = new ArrayList<>(pageNames.size());
    635
    636        long start = System.currentTimeMillis();
    637
    638        LOGGER.info("Preloading dependency information for {} pages", pageNames.size());
    639
    640        for (String page : pageNames)
    641        {
    642            try
    643            {
    644                final String className = componentClassResolver.resolvePageNameToClassName(page);
    645                componentDependencyRegistry.register(context.getClassLoader().loadClass(className));
    646                classNames.add(className);
    647            } catch (ClassNotFoundException e)
    648            {
    649                throw new RuntimeException(e);
    650            }
    651        }
    652
    653        long finish = System.currentTimeMillis();
    654
    655        if (LOGGER.isInfoEnabled())
    656        {
    657            LOGGER.info(String.format("Dependency information gathered in %.3f ms", (finish - start) / 1000.0));
    658        }
    659
    660        context.invalidate();
    661
    662        LOGGER.info("Starting preloading page classloader contexts");
    663
    664        start = System.currentTimeMillis();
    665
    666        for (int i = 0; i < 10; i++)
    667        {
    668            for (String className : classNames)
    669            {
    670                get(className);
    671            }
    672        }
    673
    674        finish = System.currentTimeMillis();
    675
    676        if (LOGGER.isInfoEnabled())
    677        {
    678            LOGGER.info(String.format("Preloading of page classloadercontexts finished in %.3f ms", (finish - start) / 1000.0));
    679        }
    680
    681    }
    682
    683}