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