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