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