    001// Copyright 2010, 2011, 2012, 2023, 2024 The Apache Software Foundation
    002//
    003// Licensed under the Apache License, Version 2.0 (the "License");
    004// you may not use this file except in compliance with the License.
    005// You may obtain a copy of the License at
    006//
    007// http://www.apache.org/licenses/LICENSE-2.0
    008//
    009// Unless required by applicable law or agreed to in writing, software
    010// distributed under the License is distributed on an "AS IS" BASIS,
    011// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    012// See the License for the specific language governing permissions and
    013// limitations under the License.
    014
    015package org.apache.tapestry5.internal.services;
    016
    017import java.lang.ref.SoftReference;
    018import java.lang.reflect.Modifier;
    019import java.util.ArrayList;
    020import java.util.Collections;
    021import java.util.HashSet;
    022import java.util.Iterator;
    023import java.util.List;
    024import java.util.Map;
    025import java.util.Map.Entry;
    026import java.util.Set;
    027
    028import org.apache.tapestry5.SymbolConstants;
    029import org.apache.tapestry5.commons.services.InvalidationEventHub;
    030import org.apache.tapestry5.commons.util.CollectionFactory;
    031import org.apache.tapestry5.func.F;
    032import org.apache.tapestry5.func.Mapper;
    033import org.apache.tapestry5.internal.ThrowawayClassLoader;
    034import org.apache.tapestry5.internal.services.ComponentDependencyRegistry.DependencyType;
    035import org.apache.tapestry5.internal.services.assets.ResourceChangeTracker;
    036import org.apache.tapestry5.internal.structure.ComponentPageElement;
    037import org.apache.tapestry5.internal.structure.Page;
    038import org.apache.tapestry5.ioc.annotations.ComponentClasses;
    039import org.apache.tapestry5.ioc.annotations.PostInjection;
    040import org.apache.tapestry5.ioc.annotations.Symbol;
    041import org.apache.tapestry5.services.ComponentClassResolver;
    042import org.apache.tapestry5.services.ComponentMessages;
    043import org.apache.tapestry5.services.ComponentTemplates;
    044import org.apache.tapestry5.services.pageload.ComponentRequestSelectorAnalyzer;
    045import org.apache.tapestry5.services.pageload.ComponentResourceSelector;
    046import org.apache.tapestry5.services.pageload.PageCachingReferenceTypeService;
    047import org.apache.tapestry5.services.pageload.PageClassLoaderContext;
    048import org.apache.tapestry5.services.pageload.PageClassLoaderContextManager;
    049import org.apache.tapestry5.services.pageload.ReferenceType;
    050import org.slf4j.Logger;
    051
    052public class PageSourceImpl implements PageSource
    053{
    054    private final ComponentRequestSelectorAnalyzer selectorAnalyzer;
    055
    056    private final PageLoader pageLoader;
    057
    058    private final ComponentDependencyRegistry componentDependencyRegistry;
    059
    060    private final ComponentClassResolver componentClassResolver;
    061
    062    private final PageClassLoaderContextManager pageClassLoaderContextManager;
    063
    064    private final PageCachingReferenceTypeService pageCachingReferenceTypeService;
    065
    066    private final Logger logger;
    067
    068    final private boolean productionMode;
    069
    070    final private boolean multipleClassLoaders;
    071
    072    private static final class CachedPageKey
    073    {
    074        final String pageName;
    075
    076        final ComponentResourceSelector selector;
    077
    078        public CachedPageKey(String pageName, ComponentResourceSelector selector)
    079        {
    080            this.pageName = pageName;
    081            this.selector = selector;
    082        }
    083
    084        public int hashCode()
    085        {
    086            return 37 * pageName.hashCode() + selector.hashCode();
    087        }
    088
    089        public boolean equals(Object obj)
    090        {
    091            if (this == obj)
    092                return true;
    093
    094            if (!(obj instanceof CachedPageKey))
    095                return false;
    096
    097            CachedPageKey other = (CachedPageKey) obj;
    098
    099            return pageName.equals(other.pageName) && selector.equals(other.selector);
    100        }
    101
    102        @Override
    103        public String toString() {
    104            return "CachedPageKey [pageName=" + pageName + ", selector=" + selector + "]";
    105        }
    106
    107
    108    }
    109
    110    private final Map<CachedPageKey, Object> pageCache = CollectionFactory.newConcurrentMap();
    111
    112    private final Map<String, Boolean> abstractClassInfoCache = CollectionFactory.newConcurrentMap();
    113
    114    private final static ThreadLocal<Set<String>> CALL_STACK =
    115            ThreadLocal.withInitial(HashSet::new);
    116
    117    public PageSourceImpl(PageLoader pageLoader, ComponentRequestSelectorAnalyzer selectorAnalyzer,
    118            ComponentDependencyRegistry componentDependencyRegistry,
    119            ComponentClassResolver componentClassResolver,
    120            PageClassLoaderContextManager pageClassLoaderContextManager,
    121            PageCachingReferenceTypeService pageCachingReferenceTypeService,
    122            @Symbol(SymbolConstants.PRODUCTION_MODE) boolean productionMode,
    123            @Symbol(SymbolConstants.MULTIPLE_CLASSLOADERS) boolean multipleClassLoaders,
    124            Logger logger)
    125    {
    126        this.pageLoader = pageLoader;
    127        this.selectorAnalyzer = selectorAnalyzer;
    128        this.componentDependencyRegistry = componentDependencyRegistry;
    129        this.componentClassResolver = componentClassResolver;
    130        this.productionMode = productionMode;
    131        this.pageCachingReferenceTypeService = pageCachingReferenceTypeService;
    132        this.multipleClassLoaders = multipleClassLoaders && !productionMode;
    133        this.pageClassLoaderContextManager = pageClassLoaderContextManager;
    134        this.logger = logger;
    135    }
    136
    137    public Page getPage(String canonicalPageName)
    138    {
    139        if (!productionMode)
    140        {
    141            componentDependencyRegistry.disableInvalidations();
    142        }
    143        try
    144        {
    145            @SuppressWarnings("unchecked")
    146            Set<String> alreadyProcessed = multipleClassLoaders ? new HashSet<>() : Collections.EMPTY_SET;
    147            return getPage(canonicalPageName, true, alreadyProcessed);
    148        }
    149        finally
    150        {
    151            if (!productionMode)
    152            {
    153                componentDependencyRegistry.enableInvalidations();
    154            }
    155        }
    156    }
    157
    158    public Page getPage(String canonicalPageName, boolean invalidateUnknownContext, Set<String> alreadyProcessed)
    159    {
    160        ComponentResourceSelector selector = selectorAnalyzer.buildSelectorForRequest();
    161
    162        CachedPageKey key = new CachedPageKey(canonicalPageName, selector);
    163
    164        // The while loop looks superfluous, but it helps to ensure that the Page instance,
    165        // with all of its mutable construction-time state, is properly published to other
    166        // threads (at least, as I understand Brian Goetz's explanation, it should be).
    167
    168        while (true)
    169        {
    170
    171            Page page;
    172            Object object = pageCache.get(key);
    173
    174            page = toPage(object);
    175
    176            if (page != null)
    177            {
    178                return page;
    179            }
    180
    181            final String className = componentClassResolver.resolvePageNameToClassName(canonicalPageName);
    182            if (multipleClassLoaders)
    183            {
    184
    185                // Avoiding problems in PlasticClassPool.createTransformation()
    186                // when the class being loaded has a page superclass
    187                final List<String> pageDependencies = getPageDependencies(className);
    188                CALL_STACK.get().add(className);
    189
    190                for (String dependencyClassName : pageDependencies)
    191                {
    192                    // Avoiding infinite recursion caused by circular dependencies
    193                    if (!alreadyProcessed.contains(dependencyClassName) &&
    194                            !CALL_STACK.get().contains(className))
    195                    {
    196                        alreadyProcessed.add(dependencyClassName);
    197
    198                        // Avoiding infinite recursion when, through component overriding,
    199                        // a dependency resolves to the same canonical page name as the
    200                        // one already requested in this call.
    201                        final String dependencyPageName = componentClassResolver.resolvePageClassNameToPageName(dependencyClassName);
    202                        final String resolvedDependencyPageClass = componentClassResolver.resolvePageNameToClassName(dependencyPageName);
    203                        if (!canonicalPageName.equals(dependencyPageName)
    204                                && !className.equals(resolvedDependencyPageClass)
    205                                && !isAbstract(dependencyClassName))
    206                        {
    207                            page = getPage(dependencyPageName,
    208                                    invalidateUnknownContext, alreadyProcessed);
    209                        }
    210                    }
    211                }
    212
    213            }
    214
    215            // In rare race conditions, we may see the same page loaded multiple times across
    216            // different threads. The last built one will "evict" the others from the page cache,
    217            // and the earlier ones will be GCed.
    218
    219            page = pageLoader.loadPage(canonicalPageName, selector);
    220
    221            final ReferenceType referenceType = pageCachingReferenceTypeService.get(canonicalPageName);
    222            if (referenceType.equals(ReferenceType.SOFT))
    223            {
    224                pageCache.put(key, new SoftReference<Page>(page));
    225            }
    226            else
    227            {
    228                pageCache.put(key, page);
    229            }
    230
    231            if (!productionMode)
    232            {
    233                final ComponentPageElement rootElement = page.getRootElement();
    234                componentDependencyRegistry.clear(rootElement);
    235                componentDependencyRegistry.register(rootElement.getComponent().getClass());
    236                PageClassLoaderContext context = pageClassLoaderContextManager.get(className);
    237
    238                if (context.isUnknown() && multipleClassLoaders)
    239                {
    240                    this.pageCache.remove(key);
    241                    if (invalidateUnknownContext)
    242                    {
    243                        pageClassLoaderContextManager.invalidateAndFireInvalidationEvents(context);
    244                        getPageDependencies(className);
    245                    }
    246                    context.getClassNames().clear();
    247                    // Avoiding bad invalidations
    248                    return getPage(canonicalPageName, false, alreadyProcessed);
    249                }
    250            }
    251
    252        }
    253
    254
    255    }
    256
    257    private List<String> getPageDependencies(final String className) {
    258        final List<String> pageDependencies = new ArrayList<>();
    259        pageDependencies.addAll(
    260                new ArrayList<String>(componentDependencyRegistry.getDependencies(className, DependencyType.INJECT_PAGE)));
    261        pageDependencies.addAll(
    262                new ArrayList<String>(componentDependencyRegistry.getDependencies(className, DependencyType.SUPERCLASS)));
    263
    264        final Iterator<String> iterator = pageDependencies.iterator();
    265        while (iterator.hasNext())
    266        {
    267            final String dependency = iterator.next();
    268            if (!dependency.contains(".pages.") && !dependency.equals(className))
    269            {
    270                iterator.remove();
    271            }
    272        }
    273
    274        return pageDependencies;
    275    }
    276
    277    @PostInjection
    278    public void setupInvalidation(@ComponentClasses InvalidationEventHub classesHub,
    279                                  @ComponentTemplates InvalidationEventHub templatesHub,
    280                                  @ComponentMessages InvalidationEventHub messagesHub,
    281                                  ResourceChangeTracker resourceChangeTracker)
    282    {
    283        classesHub.addInvalidationCallback(this::listen);
    284        templatesHub.addInvalidationCallback(this::listen);
    285        messagesHub.addInvalidationCallback(this::listen);
    286
    287        // Because Assets can be injected into pages, and Assets are invalidated when
    288        // an Asset's value is changed (partly due to the change, in 5.4, to include the asset's
    289        // checksum as part of the asset URL), then when we notice a change to
    290        // any Resource, it is necessary to discard all page instances.
    291        // From 5.8.3 on, Tapestry tries to only invalidate the components and pages known as
    292        // using the changed resources. If a given resource is changed but not associated with any
    293        // component, then all of them are invalidated.
    294        resourceChangeTracker.addInvalidationCallback(this::listen);
    295    }
    296
    297    private List<String> listen(List<String> resources)
    298    {
    299
    300        if (resources.isEmpty())
    301        {
    302            clearCache();
    303        }
    304        else
    305        {
    306            String pageName;
    307            for (String className : resources)
    308            {
    309                if (componentClassResolver.isPage(className))
    310                {
    311                    pageName = componentClassResolver.resolvePageClassNameToPageName(className);
    312                    final Iterator<Entry<CachedPageKey, Object>> iterator = pageCache.entrySet().iterator();
    313                    while (iterator.hasNext())
    314                    {
    315                        final Entry<CachedPageKey, Object> entry = iterator.next();
    316                        final String entryPageName = entry.getKey().pageName;
    317                        if (entryPageName.equalsIgnoreCase(pageName))
    318                        {
    319                            logger.info("Clearing cached page '{}'", pageName);
    320                            iterator.remove();
    321                        }
    322                    }
    323                    abstractClassInfoCache.remove(className);
    324                }
    325            }
    326        }
    327
    328        return Collections.emptyList();
    329    }
    330
    331    public void clearCache()
    332    {
    333        logger.info("Clearing page cache");
    334        pageCache.clear();
    335    }
    336
    337    public Set<Page> getAllPages()
    338    {
    339        return F.flow(pageCache.values()).map(new Mapper<Object, Page>()
    340        {
    341            public Page map(Object object)
    342            {
    343                return toPage(object);
    344            }
    345        }).removeNulls().toSet();
    346    }
    347
    348    private Page toPage(Object object)
    349    {
    350        Page page;
    351        if (object instanceof SoftReference)
    352        {
    353            @SuppressWarnings("unchecked")
    354            SoftReference<Page> ref = (SoftReference<Page>) object;
    355            page = ref == null ? null : ref.get();
    356        }
    357        else
    358        {
    359            page = (Page) object;
    360        }
    361        return page;
    362    }
    363
    364    private boolean isAbstract(final String className)
    365    {
    366        final Boolean computeIfAbsent = abstractClassInfoCache.computeIfAbsent(className,
    367                (s) -> Modifier.isAbstract(ThrowawayClassLoader.load(className).getModifiers()));
    368        return computeIfAbsent;
    369    }
    370
    371}