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}