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        return componentDependencyRegistry.getDependencies(className, DependencyType.USAGE);
329    }
330
331    private Class<?> loadClass(String className, PageClassLoaderContext context) 
332    {
333        try 
334        {
335            final ClassLoader classLoader = context.getPlasticManager().getClassLoader();
336            return classLoader.loadClass(className);
337        } catch (Exception e) {
338            throw new RuntimeException(e);
339        }
340    }
341    
342    private PageClassLoaderContext merge(
343            Set<PageClassLoaderContext> contextDependencies,
344            Function<ClassLoader, PlasticProxyFactory> plasticProxyFactoryProvider,
345            PageClassLoaderContext root, Set<String> classesToInvalidate) 
346    {
347        
348        NESTED_MERGE_COUNT.set(NESTED_MERGE_COUNT.get() + 1);
349        
350        if (LOGGER.isDebugEnabled())
351        {
352            
353            LOGGER.debug("Nested merge count going up to {}", NESTED_MERGE_COUNT.get());
354
355            String classes;
356            StringBuilder builder = new StringBuilder();
357            builder.append("Merging the following page classloader contexts into one:\n");
358            for (PageClassLoaderContext context : contextDependencies) 
359            {
360                classes = context.getClassNames().stream()
361                        .map(this::getContextName)
362                        .sorted()
363                        .collect(Collectors.joining(", "));
364                builder.append(String.format("\t%s (parent %s) (%s)\n", context.getName(), context.getParent().getName(), classes));
365            }
366            LOGGER.debug(builder.toString().trim());
367        }
368        
369        Set<PageClassLoaderContext> allContextsIncludingDescendents = new HashSet<>();
370        for (PageClassLoaderContext context : contextDependencies) 
371        {
372            allContextsIncludingDescendents.add(context);
373            allContextsIncludingDescendents.addAll(context.getDescendents());
374        }
375
376        PageClassLoaderContext merged;
377        
378        // Collect the classes in these dependencies, then invalidate the contexts
379        
380        Set<PageClassLoaderContext> furtherDependencies = new HashSet<>();
381        
382        Set<String> classNames = new HashSet<>();
383        
384        for (PageClassLoaderContext context : contextDependencies) 
385        {
386            if (!context.isRoot())
387            {
388                classNames.addAll(context.getClassNames());
389            }
390            final PageClassLoaderContext parent = context.getParent();
391            // We don't want the merged context to have a further dependency on 
392            // the root context (it's not mergeable) nor on itself.
393            if (!parent.isRoot() && 
394                    !allContextsIncludingDescendents.contains(parent))
395            {
396                furtherDependencies.add(parent);
397            }
398        }
399        
400        final List<PageClassLoaderContext> contextsToInvalidate = contextDependencies.stream()
401            .filter(c -> !c.isRoot())
402            .collect(Collectors.toList());
403        
404        if (!contextsToInvalidate.isEmpty())
405        {
406            classesToInvalidate.addAll(invalidate(contextsToInvalidate.toArray(new PageClassLoaderContext[contextsToInvalidate.size()])));
407        }
408        
409        PageClassLoaderContext parent;
410        
411        // No context dependencies, so parent is going to be the root one
412        if (furtherDependencies.size() == 0)
413        {
414            parent = root;
415        }
416        else 
417        {
418            // Single shared context dependency, so it's our parent
419            if (furtherDependencies.size() == 1)
420            {
421                parent = furtherDependencies.iterator().next();
422            }
423            // No single context dependency, so we'll need to recursively merge it
424            // so we can have a single parent.
425            else
426            {
427                parent = merge(furtherDependencies, plasticProxyFactoryProvider, root, classesToInvalidate);
428                LOGGER.debug("New context: {}", parent);
429            }
430        }
431        
432        merged = new PageClassLoaderContext(
433            "merged " + MERGED_COUNTER.getAndIncrement(),
434            parent, 
435            classNames, 
436            plasticProxyFactoryProvider.apply(parent.getClassLoader()),
437            this::get);
438        
439        parent.addChild(merged);
440        
441//        for (String className : classNames) 
442//        {
443//            loadClass(className, merged);
444//        }
445        
446        NESTED_MERGE_COUNT.set(NESTED_MERGE_COUNT.get() - 1);
447        if (LOGGER.isDebugEnabled())
448        {
449            LOGGER.debug("Nested merge count going down to {}", NESTED_MERGE_COUNT.get());
450        }
451        
452        return merged;
453    }
454
455    @Override
456    public void clear(String className) 
457    {
458        final PageClassLoaderContext context = root.findByClassName(className);
459        if (context != null)
460        {
461//            invalidationHub.fireInvalidationEvent(new ArrayList<>(invalidate(context)));
462            invalidate(context);
463        }
464    }
465
466    private String getContextName(String className)
467    {
468        String contextName = componentClassResolver.getLogicalName(className);
469        if (contextName == null)
470        {
471            contextName = className;
472        }
473        return contextName;
474    }
475
476    @Override
477    public Set<String> invalidate(PageClassLoaderContext ... contexts) 
478    {
479        Set<String> classNames = new HashSet<>();
480        for (PageClassLoaderContext context : contexts) {
481            addClassNames(context, classNames);
482            context.invalidate();
483            if (context.getParent() != null)
484            {
485                context.getParent().removeChild(context);
486            }
487        }
488        return classNames;
489    }
490    
491    private List<String> listen(List<String> resources)
492    {
493
494        List<String> returnValue;
495        
496        if (!multipleClassLoaders)
497        {
498            for (PageClassLoaderContext context : root.getChildren()) 
499            {
500                context.invalidate();
501            }
502            returnValue = Collections.emptyList();
503        }
504        else if (INVALIDATING_CONTEXT.get())
505        {
506            returnValue = Collections.emptyList();
507        }
508        else
509        {
510        
511            Set<PageClassLoaderContext> contextsToInvalidate = new HashSet<>();
512            for (String resource : resources) 
513            {
514                PageClassLoaderContext context = root.findByClassName(resource);
515                if (context != null && !context.isRoot())
516                {
517                    contextsToInvalidate.add(context);
518                }
519            }
520            
521            Set<String> furtherResources = invalidate(contextsToInvalidate.toArray(
522                    new PageClassLoaderContext[contextsToInvalidate.size()]));
523            
524            // We don't want to invalidate resources more than once
525            furtherResources.removeAll(resources);
526            
527            returnValue = new ArrayList<>(furtherResources);
528        }
529        
530        return returnValue;
531            
532    }
533
534    @SuppressWarnings("unchecked")
535    @Override
536    public void invalidateAndFireInvalidationEvents(PageClassLoaderContext... contexts) {
537        markAsInvalidatingContext();
538        if (multipleClassLoaders)
539        {
540            final Set<String> classNames = invalidate(contexts);
541            invalidate(classNames);
542        }
543        else
544        {
545            invalidate(Collections.EMPTY_SET);            
546        }
547        markAsNotInvalidatingContext();
548    }
549
550    private void markAsNotInvalidatingContext() {
551        INVALIDATING_CONTEXT.set(false);
552    }
553
554    private void markAsInvalidatingContext() {
555        INVALIDATING_CONTEXT.set(true);
556    }
557    
558    private void invalidate(Set<String> classesToInvalidate) {
559        if (!classesToInvalidate.isEmpty())
560        {
561            LOGGER.debug("Invalidating classes {}", classesToInvalidate);
562            markAsInvalidatingContext();
563            final List<String> classesToInvalidateAsList = new ArrayList<>(classesToInvalidate);
564            
565            componentDependencyRegistry.disableInvalidations();
566            
567            try 
568            {
569                // TODO: do we really need both invalidation hubs to be invoked here?
570                invalidationHub.fireInvalidationEvent(classesToInvalidateAsList);
571                componentClassesInvalidationEventHub.fireInvalidationEvent(classesToInvalidateAsList);
572                markAsNotInvalidatingContext();
573            }
574            finally
575            {
576                componentDependencyRegistry.enableInvalidations();
577            }
578            
579        }
580    }
581
582    private void addClassNames(PageClassLoaderContext context, Set<String> classNames) {
583        classNames.addAll(context.getClassNames());
584        for (PageClassLoaderContext child : context.getChildren()) {
585            addClassNames(child, classNames);
586        }
587    }
588
589    @Override
590    public PageClassLoaderContext getRoot() {
591        return root;
592    }
593
594    @Override
595    public boolean isMerging() 
596    {
597        return NESTED_MERGE_COUNT.get() > 0;
598    }
599
600    @Override
601    public void clear() 
602    {
603    }
604
605    @Override
606    public Class<?> getClassInstance(Class<?> clasz, String pageName) 
607    {
608        final String className = clasz.getName();
609        PageClassLoaderContext context = root.findByClassName(className);
610        if (context == null)
611        {
612            context = get(className);
613        }
614        try 
615        {
616            clasz = context.getProxyFactory().getClassLoader().loadClass(className);
617        } catch (ClassNotFoundException e) 
618        {
619            throw new TapestryException(e.getMessage(), e);
620        }
621        return clasz;
622    }
623    
624    @Override
625    public void preload() 
626    {
627        
628        final PageClassLoaderContext context = new PageClassLoaderContext(PageClassLoaderContext.UNKOWN_CONTEXT_NAME, root, 
629                Collections.emptySet(), 
630                plasticProxyFactoryProvider.apply(root.getClassLoader()),
631                this::get);
632        
633        final List<String> pageNames = componentClassResolver.getPageNames();
634        final List<String> classNames = new ArrayList<>(pageNames.size());
635        
636        long start = System.currentTimeMillis();
637        
638        LOGGER.info("Preloading dependency information for {} pages", pageNames.size());
639        
640        for (String page : pageNames)
641        {
642            try 
643            {
644                final String className = componentClassResolver.resolvePageNameToClassName(page);
645                componentDependencyRegistry.register(context.getClassLoader().loadClass(className));
646                classNames.add(className);
647            } catch (ClassNotFoundException e) 
648            {
649                throw new RuntimeException(e);
650            }
651        }
652        
653        long finish = System.currentTimeMillis();
654        
655        if (LOGGER.isInfoEnabled())
656        {
657            LOGGER.info(String.format("Dependency information gathered in %.3f ms", (finish - start) / 1000.0));
658        }
659        
660        context.invalidate();
661        
662        LOGGER.info("Starting preloading page classloader contexts");
663        
664        start = System.currentTimeMillis();
665        
666        for (int i = 0; i < 10; i++)
667        {
668            for (String className : classNames) 
669            {
670                get(className);
671            }
672        }
673        
674        finish = System.currentTimeMillis();
675
676        if (LOGGER.isInfoEnabled())
677        {
678            LOGGER.info(String.format("Preloading of page classloadercontexts finished in %.3f ms", (finish - start) / 1000.0));
679        }
680
681    }
682    
683}