    001// Copyright 2023, 2024 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.Comparator;
    019import java.util.HashSet;
    020import java.util.List;
    021import java.util.Objects;
    022import java.util.Set;
    023import java.util.concurrent.atomic.AtomicInteger;
    024import java.util.function.Function;
    025import java.util.function.Supplier;
    026import java.util.stream.Collectors;
    027
    028import org.apache.tapestry5.SymbolConstants;
    029import org.apache.tapestry5.commons.internal.util.TapestryException;
    030import org.apache.tapestry5.commons.services.InvalidationEventHub;
    031import org.apache.tapestry5.commons.services.PlasticProxyFactory;
    032import org.apache.tapestry5.internal.ThrowawayClassLoader;
    033import org.apache.tapestry5.internal.services.ComponentDependencyRegistry;
    034import org.apache.tapestry5.internal.services.ComponentDependencyRegistry.DependencyType;
    035import org.apache.tapestry5.internal.services.InternalComponentInvalidationEventHub;
    036import org.apache.tapestry5.ioc.annotations.ComponentClasses;
    037import org.apache.tapestry5.ioc.annotations.Symbol;
    038import org.apache.tapestry5.plastic.PlasticUtils;
    039import org.apache.tapestry5.services.ComponentClassResolver;
    040import org.slf4j.Logger;
    041import org.slf4j.LoggerFactory;
    042
    043/**
    044 * Default {@linkplain PageClassLoaderContextManager} implementation.
    045 *
    046 * @since 5.8.3
    047 */
    048public class PageClassLoaderContextManagerImpl implements PageClassLoaderContextManager
    049{
    050
    051    private static final Logger LOGGER = LoggerFactory.getLogger(PageClassLoaderContextManager.class);
    052
    053    private final ComponentDependencyRegistry componentDependencyRegistry;
    054
    055    private final ComponentClassResolver componentClassResolver;
    056
    057    private final InternalComponentInvalidationEventHub invalidationHub;
    058
    059    private final InvalidationEventHub componentClassesInvalidationEventHub;
    060
    061    private final boolean multipleClassLoaders;
    062
    063    private final boolean productionMode;
    064
    065    private final static ThreadLocal<Integer> NESTED_MERGE_COUNT = ThreadLocal.withInitial(() -> 0);
    066
    067    private final static ThreadLocal<Boolean> INVALIDATING_CONTEXT = ThreadLocal.withInitial(() -> false);
    068
    069    private static final AtomicInteger MERGED_COUNTER = new AtomicInteger(1);
    070
    071    private final static ThreadLocal<AtomicInteger> CONTEXTS_CREATED = ThreadLocal.withInitial(AtomicInteger::new);
    072
    073    private Function<ClassLoader, PlasticProxyFactory> plasticProxyFactoryProvider;
    074
    075    private PageClassLoaderContext root;
    076
    077    private boolean preloadingContexts;
    078
    079    public PageClassLoaderContextManagerImpl(
    080            final ComponentDependencyRegistry componentDependencyRegistry,
    081            final ComponentClassResolver componentClassResolver,
    082            final InternalComponentInvalidationEventHub invalidationHub,
    083            final @ComponentClasses InvalidationEventHub componentClassesInvalidationEventHub,
    084            final @Symbol(SymbolConstants.PRODUCTION_MODE) boolean productionMode,
    085            final @Symbol(SymbolConstants.MULTIPLE_CLASSLOADERS) boolean multipleClassLoaders)
    086    {
    087        super();
    088        this.componentDependencyRegistry = componentDependencyRegistry;
    089        this.componentClassResolver = componentClassResolver;
    090        this.invalidationHub = invalidationHub;
    091        this.componentClassesInvalidationEventHub = componentClassesInvalidationEventHub;
    092        this.multipleClassLoaders = multipleClassLoaders && !productionMode;
    093        this.productionMode = productionMode;
    094        invalidationHub.addInvalidationCallback(this::listen);
    095        NESTED_MERGE_COUNT.set(0);
    096    }
    097
    098    @Override
    099    public void invalidateUnknownContext()
    100    {
    101        synchronized (this) {
    102            markAsNotInvalidatingContext();
    103            for (PageClassLoaderContext context : root.getChildren())
    104            {
    105                if (context.isUnknown())
    106                {
    107                    invalidateAndFireInvalidationEvents(context);
    108                    break;
    109                }
    110            }
    111        }
    112    }
    113
    114    @Override
    115    public void initialize(
    116            final PageClassLoaderContext root,
    117            final Function<ClassLoader, PlasticProxyFactory> plasticProxyFactoryProvider)
    118    {
    119        if (this.root != null)
    120        {
    121            throw new IllegalStateException("PageClassloaderContextManager.initialize() can only be called once");
    122        }
    123        Objects.requireNonNull(root);
    124        Objects.requireNonNull(plasticProxyFactoryProvider);
    125        this.root = root;
    126        this.plasticProxyFactoryProvider = plasticProxyFactoryProvider;
    127        LOGGER.info("Root context: {}", root);
    128    }
    129
    130    @Override
    131    public synchronized PageClassLoaderContext get(final String className)
    132    {
    133        PageClassLoaderContext context;
    134
    135        // Class isn't in a controlled package, so it doesn't get transformed
    136        // and should go for the root context, which is never thrown out.
    137        if (!root.getPlasticManager().shouldInterceptClassLoading(className))
    138        {
    139            context = root;
    140        }
    141        else if (productionMode || !multipleClassLoaders)
    142        {
    143            context = getUnknownContext(root, plasticProxyFactoryProvider);
    144        }
    145        else
    146        {
    147
    148            // Multiple classloader mode.
    149            final String enclosingClassName = getAdjustedClassName(className);
    150            context = root.findByClassName(enclosingClassName);
    151
    152            if (context == null)
    153            {
    154                Set<String> classesToInvalidate = new HashSet<>();
    155
    156                context = processUsingDependencies(
    157                        enclosingClassName,
    158                        root,
    159                        () -> getUnknownContext(root, plasticProxyFactoryProvider),
    160                        plasticProxyFactoryProvider,
    161                        classesToInvalidate);
    162
    163                if (!classesToInvalidate.isEmpty())
    164                {
    165                    invalidate(classesToInvalidate);
    166                }
    167
    168                if (!className.equals(enclosingClassName))
    169                {
    170                    loadClass(enclosingClassName, context);
    171                }
    172
    173            }
    174
    175        }
    176
    177        return context;
    178
    179    }
    180
    181    private String getAdjustedClassName(final String className)
    182    {
    183        return PlasticUtils.getEnclosingClassName(className)
    184                .replaceAll("\\[\\]", "");
    185    }
    186
    187    private PageClassLoaderContext getUnknownContext(final PageClassLoaderContext root,
    188            final Function<ClassLoader, PlasticProxyFactory> plasticProxyFactoryProvider)
    189    {
    190
    191        PageClassLoaderContext unknownContext = null;
    192
    193        for (PageClassLoaderContext child : root.getChildren())
    194        {
    195            if (child.getName().equals(PageClassLoaderContext.UNKOWN_CONTEXT_NAME))
    196            {
    197                unknownContext = child;
    198                break;
    199            }
    200        }
    201
    202        if (unknownContext == null)
    203        {
    204            unknownContext = new PageClassLoaderContext(PageClassLoaderContext.UNKOWN_CONTEXT_NAME, root,
    205                    Collections.emptySet(),
    206                    plasticProxyFactoryProvider.apply(root.getClassLoader()),
    207                    this::get);
    208            root.addChild(unknownContext);
    209            if (multipleClassLoaders)
    210            {
    211                LOGGER.debug("Unknown context: {}", unknownContext);
    212            }
    213        }
    214        return unknownContext;
    215    }
    216
    217    private PageClassLoaderContext processUsingDependencies(
    218            String className,
    219            PageClassLoaderContext root,
    220            Supplier<PageClassLoaderContext> unknownContextProvider,
    221            Function<ClassLoader, PlasticProxyFactory> plasticProxyFactoryProvider, Set<String> classesToInvalidate)
    222    {
    223        return processUsingDependencies(className, root, unknownContextProvider, plasticProxyFactoryProvider, classesToInvalidate, new HashSet<>());
    224    }
    225
    226    private PageClassLoaderContext processUsingDependencies(
    227            String className,
    228            PageClassLoaderContext root,
    229            Supplier<PageClassLoaderContext> unknownContextProvider,
    230            Function<ClassLoader, PlasticProxyFactory> plasticProxyFactoryProvider,
    231            Set<String> classesToInvalidate,
    232            Set<String> alreadyProcessed)
    233    {
    234        return processUsingDependencies(className, root, unknownContextProvider,
    235                plasticProxyFactoryProvider, classesToInvalidate, alreadyProcessed, true);
    236    }
    237
    238
    239    private PageClassLoaderContext processUsingDependencies(
    240            String className,
    241            PageClassLoaderContext root,
    242            Supplier<PageClassLoaderContext> unknownContextProvider,
    243            Function<ClassLoader, PlasticProxyFactory> plasticProxyFactoryProvider,
    244            Set<String> classesToInvalidate,
    245            Set<String> alreadyProcessed,
    246            boolean processCircularDependencies)
    247    {
    248        PageClassLoaderContext context = root.findByClassName(className);
    249        if (context == null)
    250        {
    251
    252            LOGGER.debug("Processing class {}", className);
    253            alreadyProcessed.add(className);
    254
    255            // Sorting dependencies by type/alphabetically so we have consistent
    256            // context trees between runs of the same webapp
    257            List<String> allNonPageDependencies = new ArrayList<>(
    258                    componentDependencyRegistry.getAllNonPageDependencies(className));
    259            Collections.sort(allNonPageDependencies, ClassNameComparator.INSTANCE);
    260
    261            List<String> dependencies = new ArrayList<>(getDependenciesWithoutPages(className));
    262            Collections.sort(dependencies, ClassNameComparator.INSTANCE);
    263
    264            // Process dependencies depth-first
    265            do
    266            {
    267
    268                // Very unlikely to have infinite loops, but lets
    269                // avoid them anyway.
    270                int passes = 0;
    271
    272                int contextsCreatedInThisPass = -1;
    273
    274                while (contextsCreatedInThisPass < CONTEXTS_CREATED.get().get() && passes < 1000)
    275                {
    276
    277                    contextsCreatedInThisPass = CONTEXTS_CREATED.get().get();
    278
    279                    for (String dependency : allNonPageDependencies)
    280                    {
    281                        // Avoid infinite recursion loops
    282                        if (!alreadyProcessed.contains(dependency)
    283                                || root.findByClassName(dependency) == null)
    284                        {
    285                            processUsingDependencies(dependency, root, unknownContextProvider,
    286                                    plasticProxyFactoryProvider, classesToInvalidate, alreadyProcessed, false);
    287                        }
    288                    }
    289
    290                }
    291
    292            }
    293            while (!allNeededContextsAvailable(allNonPageDependencies));
    294
    295            // Collect context dependencies
    296            Set<PageClassLoaderContext> contextDependencies = new HashSet<>();
    297            for (String dependency : allNonPageDependencies)
    298            {
    299                PageClassLoaderContext dependencyContext = root.findByClassName(dependency);
    300                // Avoid infinite recursion loops
    301                if (multipleClassLoaders || !alreadyProcessed.contains(dependency))
    302                {
    303                    if (dependencyContext == null)
    304                    {
    305                        dependencyContext = processUsingDependencies(dependency, root, unknownContextProvider,
    306                                plasticProxyFactoryProvider, classesToInvalidate, alreadyProcessed);
    307
    308                    }
    309                    if (!dependencyContext.isRoot())
    310                    {
    311                        contextDependencies.add(dependencyContext);
    312                    }
    313                }
    314            }
    315
    316            if (contextDependencies.size() == 0)
    317            {
    318                context = new PageClassLoaderContext(
    319                        getContextName(className),
    320                        root,
    321                        Collections.singleton(className),
    322                        plasticProxyFactoryProvider.apply(root.getClassLoader()),
    323                        this::get);
    324                CONTEXTS_CREATED.get().incrementAndGet();
    325            }
    326            else
    327            {
    328                PageClassLoaderContext parentContext;
    329                if (contextDependencies.size() == 1)
    330                {
    331                    parentContext = contextDependencies.iterator().next();
    332                }
    333                else
    334                {
    335                    parentContext = merge(contextDependencies, plasticProxyFactoryProvider, root, classesToInvalidate);
    336                }
    337                context = new PageClassLoaderContext(
    338                        getContextName(className),
    339                        parentContext,
    340                        Collections.singleton(className),
    341                        plasticProxyFactoryProvider.apply(parentContext.getClassLoader()),
    342                        this::get);
    343                CONTEXTS_CREATED.get().incrementAndGet();
    344            }
    345
    346            LOGGER.debug("New context: {}", context);
    347
    348        }
    349        context.addClass(className);
    350        context.getParent().addChild(context);
    351
    352        return context;
    353    }
    354
    355    private boolean allNeededContextsAvailable(List<String> dependencies)
    356    {
    357        boolean available = true;
    358        for (String dependency : dependencies)
    359        {
    360            if (root.findByClassName(dependency) == null)
    361            {
    362                available = false;
    363                break;
    364            }
    365        }
    366        return available;
    367    }
    368
    369    private Set<String> getDependenciesWithoutPages(String className)
    370    {
    371        Set<String> dependencies = new HashSet<>();
    372        dependencies.addAll(componentDependencyRegistry.getDependencies(className, DependencyType.USAGE));
    373        dependencies.addAll(componentDependencyRegistry.getDependencies(className, DependencyType.SUPERCLASS));
    374        dependencies.remove(className); // Just in case
    375        return Collections.unmodifiableSet(dependencies);
    376    }
    377
    378    private Class<?> loadClass(String className, PageClassLoaderContext context)
    379    {
    380        try
    381        {
    382            final ClassLoader classLoader = context.getPlasticManager().getClassLoader();
    383            return classLoader.loadClass(className);
    384        } catch (Exception e) {
    385            throw new RuntimeException(e);
    386        }
    387    }
    388
    389    private PageClassLoaderContext merge(
    390            Set<PageClassLoaderContext> contextDependencies,
    391            Function<ClassLoader, PlasticProxyFactory> plasticProxyFactoryProvider,
    392            PageClassLoaderContext root, Set<String> classesToInvalidate)
    393    {
    394
    395        NESTED_MERGE_COUNT.set(NESTED_MERGE_COUNT.get() + 1);
    396
    397        if (LOGGER.isDebugEnabled())
    398        {
    399
    400            LOGGER.debug("Nested merge count going up to {}", NESTED_MERGE_COUNT.get());
    401
    402            String classes;
    403            StringBuilder builder = new StringBuilder();
    404            builder.append("Merging the following page classloader contexts into one:\n");
    405            for (PageClassLoaderContext context : contextDependencies)
    406            {
    407                classes = context.getClassNames().stream()
    408                        .map(this::getContextName)
    409                        .sorted()
    410                        .collect(Collectors.joining(", "));
    411                builder.append(String.format("\t%s (parent %s) (%s)\n", context.getName(), context.getParent().getName(), classes));
    412            }
    413            LOGGER.debug(builder.toString().trim());
    414        }
    415
    416        final Set<String> classesToReprocess = multipleClassLoaders ? new HashSet<>() : Collections.emptySet();
    417        Set<PageClassLoaderContext> allContextsIncludingDescendents = new HashSet<>();
    418
    419        for (PageClassLoaderContext context : contextDependencies)
    420        {
    421            final Set<PageClassLoaderContext> descendents = context.getDescendents();
    422            allContextsIncludingDescendents.add(context);
    423            allContextsIncludingDescendents.addAll(descendents);
    424            for (PageClassLoaderContext descendent : descendents)
    425            {
    426                addClassNames(descendent, classesToReprocess);
    427            }
    428        }
    429
    430        PageClassLoaderContext merged;
    431
    432        // Collect the classes in these dependencies, then invalidate the contexts
    433
    434        Set<PageClassLoaderContext> furtherDependencies = new HashSet<>();
    435
    436        Set<String> classNames = new HashSet<>();
    437
    438        for (PageClassLoaderContext context : contextDependencies)
    439        {
    440            if (!context.isRoot())
    441            {
    442                classNames.addAll(context.getClassNames());
    443            }
    444            final PageClassLoaderContext parent = context.getParent();
    445            // We don't want the merged context to have a further dependency on
    446            // the root context (it's not mergeable) nor on itself.
    447            if (!parent.isRoot() &&
    448                    !allContextsIncludingDescendents.contains(parent))
    449            {
    450                furtherDependencies.add(parent);
    451            }
    452        }
    453
    454        final List<PageClassLoaderContext> contextsToInvalidate = contextDependencies.stream()
    455            .filter(c -> !c.isRoot())
    456            .collect(Collectors.toList());
    457
    458        if (!contextsToInvalidate.isEmpty())
    459        {
    460            classesToInvalidate.addAll(invalidate(contextsToInvalidate.toArray(new PageClassLoaderContext[contextsToInvalidate.size()])));
    461        }
    462
    463        PageClassLoaderContext parent;
    464
    465        // No context dependencies, so parent is going to be the root one
    466        if (furtherDependencies.size() == 0)
    467        {
    468            parent = root;
    469        }
    470        else
    471        {
    472            // Single shared context dependency, so it's our parent
    473            if (furtherDependencies.size() == 1)
    474            {
    475                parent = furtherDependencies.iterator().next();
    476            }
    477            // No single context dependency, so we'll need to recursively merge it
    478            // so we can have a single parent.
    479            else
    480            {
    481                parent = merge(furtherDependencies, plasticProxyFactoryProvider, root, classesToInvalidate);
    482                LOGGER.debug("New context: {}", parent);
    483            }
    484        }
    485
    486        merged = new PageClassLoaderContext(
    487            "merged " + MERGED_COUNTER.getAndIncrement(),
    488            parent,
    489            classNames,
    490            plasticProxyFactoryProvider.apply(parent.getClassLoader()),
    491            this::get);
    492
    493        CONTEXTS_CREATED.get().incrementAndGet();
    494
    495        parent.addChild(merged);
    496
    497        // Recreating contexts for classes that got invalidated but
    498        // aren't part of the new merged context (i.e. the classes
    499        // in contexts are are descendent of the merged contexts).
    500//        if (!classesToReprocess.isEmpty())
    501//        {
    502//            final List<String> sorted = new ArrayList<>(classesToReprocess);
    503//            for (String className : sorted)
    504//            {
    505//                get(className);
    506//            }
    507//        }
    508
    509//        for (String className : classNames)
    510//        {
    511//            loadClass(className, merged);
    512//        }
    513
    514        NESTED_MERGE_COUNT.set(NESTED_MERGE_COUNT.get() - 1);
    515        if (LOGGER.isDebugEnabled())
    516        {
    517            LOGGER.debug("Nested merge count going down to {}", NESTED_MERGE_COUNT.get());
    518        }
    519
    520        return merged;
    521    }
    522
    523    @Override
    524    public void clear(String className)
    525    {
    526        final PageClassLoaderContext context = root.findByClassName(className);
    527        if (context != null)
    528        {
    529//            invalidationHub.fireInvalidationEvent(new ArrayList<>(invalidate(context)));
    530            invalidate(context);
    531        }
    532    }
    533
    534    private String getContextName(String className)
    535    {
    536        String contextName = componentClassResolver.getLogicalName(className);
    537        if (contextName == null)
    538        {
    539            contextName = className;
    540        }
    541        return contextName;
    542    }
    543
    544    @Override
    545    public Set<String> invalidate(PageClassLoaderContext ... contexts)
    546    {
    547        Set<String> classNames = new HashSet<>();
    548        for (PageClassLoaderContext context : contexts) {
    549            addClassNames(context, classNames);
    550            context.invalidate();
    551            if (context.getParent() != null)
    552            {
    553                context.getParent().removeChild(context);
    554            }
    555        }
    556        return classNames;
    557    }
    558
    559    private List<String> listen(List<String> resources)
    560    {
    561
    562        List<String> returnValue;
    563
    564        if (!multipleClassLoaders)
    565        {
    566            for (PageClassLoaderContext context : root.getChildren())
    567            {
    568                context.invalidate();
    569            }
    570            returnValue = Collections.emptyList();
    571        }
    572        else if (INVALIDATING_CONTEXT.get())
    573        {
    574            returnValue = Collections.emptyList();
    575        }
    576        else
    577        {
    578
    579            Set<PageClassLoaderContext> contextsToInvalidate = new HashSet<>();
    580            for (String resource : resources)
    581            {
    582                PageClassLoaderContext context = root.findByClassName(resource);
    583                if (context != null && !context.isRoot())
    584                {
    585                    contextsToInvalidate.add(context);
    586                }
    587            }
    588
    589            Set<String> furtherResources = invalidate(contextsToInvalidate.toArray(
    590                    new PageClassLoaderContext[contextsToInvalidate.size()]));
    591
    592            // We don't want to invalidate resources more than once
    593            furtherResources.removeAll(resources);
    594
    595            returnValue = new ArrayList<>(furtherResources);
    596        }
    597
    598        return returnValue;
    599
    600    }
    601
    602    @SuppressWarnings("unchecked")
    603    @Override
    604    public void invalidateAndFireInvalidationEvents(PageClassLoaderContext... contexts) {
    605        markAsInvalidatingContext();
    606        if (multipleClassLoaders)
    607        {
    608            final Set<String> classNames = invalidate(contexts);
    609            invalidate(classNames);
    610        }
    611        else
    612        {
    613            invalidate(Collections.EMPTY_SET);
    614        }
    615        markAsNotInvalidatingContext();
    616    }
    617
    618    private void markAsNotInvalidatingContext() {
    619        INVALIDATING_CONTEXT.set(false);
    620    }
    621
    622    private void markAsInvalidatingContext() {
    623        INVALIDATING_CONTEXT.set(true);
    624    }
    625
    626    private void invalidate(Set<String> classesToInvalidate) {
    627        if (!classesToInvalidate.isEmpty() && !preloadingContexts)
    628        {
    629            LOGGER.debug("Invalidating classes {}", classesToInvalidate);
    630            markAsInvalidatingContext();
    631            final List<String> classesToInvalidateAsList = new ArrayList<>(classesToInvalidate);
    632
    633            componentDependencyRegistry.disableInvalidations();
    634
    635            try
    636            {
    637                // TODO: do we really need both invalidation hubs to be invoked here?
    638                invalidationHub.fireInvalidationEvent(classesToInvalidateAsList);
    639                componentClassesInvalidationEventHub.fireInvalidationEvent(classesToInvalidateAsList);
    640                markAsNotInvalidatingContext();
    641            }
    642            finally
    643            {
    644                componentDependencyRegistry.enableInvalidations();
    645            }
    646
    647        }
    648    }
    649
    650    private void addClassNames(PageClassLoaderContext context, Set<String> classNames) {
    651        classNames.addAll(context.getClassNames());
    652        for (PageClassLoaderContext child : context.getChildren()) {
    653            addClassNames(child, classNames);
    654        }
    655    }
    656
    657    @Override
    658    public PageClassLoaderContext getRoot() {
    659        return root;
    660    }
    661
    662    @Override
    663    public boolean isMerging()
    664    {
    665        return NESTED_MERGE_COUNT.get() > 0;
    666    }
    667
    668    @Override
    669    public void clear()
    670    {
    671    }
    672
    673    @Override
    674    public Class<?> getClassInstance(Class<?> clasz, String pageName)
    675    {
    676        final String className = clasz.getName();
    677        PageClassLoaderContext context = root.findByClassName(className);
    678        if (context == null)
    679        {
    680            context = get(className);
    681        }
    682        try
    683        {
    684            clasz = context.getProxyFactory().getClassLoader().loadClass(className);
    685        } catch (ClassNotFoundException e)
    686        {
    687            throw new TapestryException(e.getMessage(), e);
    688        }
    689        return clasz;
    690    }
    691
    692    @Override
    693    public void preload()
    694    {
    695
    696        final ClassLoader classLoader = new ThrowawayClassLoader(PageClassLoaderContext.class.getClassLoader());
    697
    698        final List<String> pageNames = componentClassResolver.getPageNames();
    699        final List<String> classNames = new ArrayList<>(pageNames.size());
    700
    701        long start = System.currentTimeMillis();
    702
    703        LOGGER.info("Preloading dependency information for {} pages", pageNames.size());
    704
    705        for (String page : pageNames)
    706        {
    707            try
    708            {
    709                final String className = componentClassResolver.resolvePageNameToClassName(page);
    710                componentDependencyRegistry.register(classLoader.loadClass(className));
    711                classNames.add(className);
    712            } catch (ClassNotFoundException e)
    713            {
    714                throw new RuntimeException(e);
    715            }
    716            catch (Exception e)
    717            {
    718                LOGGER.warn("Exception while preloading page " + page, e);
    719            }
    720        }
    721
    722        long finish = System.currentTimeMillis();
    723
    724        if (LOGGER.isInfoEnabled())
    725        {
    726            LOGGER.info(String.format("Dependency information for %d pages gathered in %.3f s",
    727                    pageNames.size(),  (finish - start) / 1000.0));
    728        }
    729
    730        preloadContexts();
    731
    732    }
    733
    734    @Override
    735    public void preloadContexts()
    736    {
    737        long start;
    738        long finish;
    739        LOGGER.info("Starting preloading page classloader contexts");
    740
    741        start = System.currentTimeMillis();
    742
    743        final List<String> classNames = new ArrayList<>(componentDependencyRegistry.getClassNames());
    744        classNames.sort(ClassNameComparator.INSTANCE);
    745
    746        int runs = 0;
    747        preloadingContexts = true;
    748
    749        try
    750        {
    751            // The run counter check is to just avoid possible infinite loops,
    752            // although that's very unlikely.
    753            int contexts = -1;
    754            while (runs < 5000 && contexts < CONTEXTS_CREATED.get().get())
    755            {
    756                runs++;
    757                contexts = CONTEXTS_CREATED.get().get();
    758                for (String className : classNames)
    759                {
    760                    get(className);
    761                }
    762            }
    763        }
    764        finally
    765        {
    766            preloadingContexts = false;
    767        }
    768
    769        finish = System.currentTimeMillis();
    770
    771        if (LOGGER.isInfoEnabled())
    772        {
    773            LOGGER.info(String.format("Preloading of page classloader contexts finished in %.3f s (%d passes)", (finish - start) / 1000.0, runs));
    774        }
    775    }
    776
    777    /**
    778     * Sorts base classes before mixins, mixins before components and components
    779     * before pages. If both classes belong to the same type, order alphabetically.
    780     */
    781    private static final class ClassNameComparator implements Comparator<String>
    782    {
    783
    784        private static final Comparator<String> INSTANCE = new ClassNameComparator();
    785
    786        @Override
    787        public int compare(String o1, String o2)
    788        {
    789            int value1 = getValue(o1);
    790            int value2 = getValue(o2);
    791            int comparison = value1 - value2;
    792            if (comparison == 0)
    793            {
    794                comparison = o1.compareTo(o2);
    795            }
    796            return comparison;
    797        }
    798
    799        private int getValue(String className)
    800        {
    801            int value;
    802            if (className.contains(".base."))
    803            {
    804                value = 0;
    805            }
    806            else if (className.contains(".mixins."))
    807            {
    808                value = 1;
    809            }
    810            else if (className.contains(".components."))
    811            {
    812                value = 2;
    813            }
    814            else
    815            {
    816                value = 3;
    817            }
    818            return value;
    819        }
    820
    821    }
    822
    823}