    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<String> CURRENT_PAGE =
    115            ThreadLocal.withInitial(() -> null);
    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                if (canonicalPageName.equals(CURRENT_PAGE.get()))
    186                {
    187                    throw new IllegalStateException("Infinite method loop detected. Bailing out.");
    188                }
    189                else
    190                {
    191                    CURRENT_PAGE.set(canonicalPageName);
    192                }
    193
    194                // Avoiding problems in PlasticClassPool.createTransformation()
    195                // when the class being loaded has a page superclass
    196                final List<String> pageDependencies = getPageDependencies(className);
    197
    198                for (String dependencyClassName : pageDependencies)
    199                {
    200                    // Avoiding infinite recursion caused by circular dependencies
    201                    if (!alreadyProcessed.contains(dependencyClassName))
    202                    {
    203                        alreadyProcessed.add(dependencyClassName);
    204
    205                        // Avoiding infinite recursion when, through component overriding,
    206                        // a dependency resolves to the same canonical page name as the
    207                        // one already requested in this call.
    208                        final String dependencyPageName = componentClassResolver.resolvePageClassNameToPageName(dependencyClassName);
    209                        final String resolvedDependencyPageClass = componentClassResolver.resolvePageNameToClassName(dependencyPageName);
    210                        if (!canonicalPageName.equals(dependencyPageName)
    211                                && !className.equals(resolvedDependencyPageClass)
    212                                && !isAbstract(dependencyClassName))
    213                        {
    214                            page = getPage(dependencyPageName,
    215                                    invalidateUnknownContext, alreadyProcessed);
    216                        }
    217                    }
    218                }
    219
    220            }
    221
    222            // In rare race conditions, we may see the same page loaded multiple times across
    223            // different threads. The last built one will "evict" the others from the page cache,
    224            // and the earlier ones will be GCed.
    225
    226            page = pageLoader.loadPage(canonicalPageName, selector);
    227
    228            final ReferenceType referenceType = pageCachingReferenceTypeService.get(canonicalPageName);
    229            if (referenceType.equals(ReferenceType.SOFT))
    230            {
    231                pageCache.put(key, new SoftReference<Page>(page));
    232            }
    233            else
    234            {
    235                pageCache.put(key, page);
    236            }
    237
    238            if (!productionMode)
    239            {
    240                final ComponentPageElement rootElement = page.getRootElement();
    241                componentDependencyRegistry.clear(rootElement);
    242                componentDependencyRegistry.register(rootElement.getComponent().getClass());
    243                PageClassLoaderContext context = pageClassLoaderContextManager.get(className);
    244
    245                if (context.isUnknown() && multipleClassLoaders)
    246                {
    247                    this.pageCache.remove(key);
    248                    if (invalidateUnknownContext)
    249                    {
    250                        pageClassLoaderContextManager.invalidateAndFireInvalidationEvents(context);
    251                        getPageDependencies(className);
    252                    }
    253                    context.getClassNames().clear();
    254                    // Avoiding bad invalidations
    255                    return getPage(canonicalPageName, false, alreadyProcessed);
    256                }
    257            }
    258
    259        }
    260
    261
    262    }
    263
    264    private List<String> getPageDependencies(final String className) {
    265        final List<String> pageDependencies = new ArrayList<>();
    266        pageDependencies.addAll(
    267                new ArrayList<String>(componentDependencyRegistry.getDependencies(className, DependencyType.INJECT_PAGE)));
    268        pageDependencies.addAll(
    269                new ArrayList<String>(componentDependencyRegistry.getDependencies(className, DependencyType.SUPERCLASS)));
    270
    271        final Iterator<String> iterator = pageDependencies.iterator();
    272        while (iterator.hasNext())
    273        {
    274            final String dependency = iterator.next();
    275            if (!dependency.contains(".pages.") && !dependency.equals(className))
    276            {
    277                iterator.remove();
    278            }
    279        }
    280
    281        return pageDependencies;
    282    }
    283
    284    @PostInjection
    285    public void setupInvalidation(@ComponentClasses InvalidationEventHub classesHub,
    286                                  @ComponentTemplates InvalidationEventHub templatesHub,
    287                                  @ComponentMessages InvalidationEventHub messagesHub,
    288                                  ResourceChangeTracker resourceChangeTracker)
    289    {
    290        classesHub.addInvalidationCallback(this::listen);
    291        templatesHub.addInvalidationCallback(this::listen);
    292        messagesHub.addInvalidationCallback(this::listen);
    293
    294        // Because Assets can be injected into pages, and Assets are invalidated when
    295        // an Asset's value is changed (partly due to the change, in 5.4, to include the asset's
    296        // checksum as part of the asset URL), then when we notice a change to
    297        // any Resource, it is necessary to discard all page instances.
    298        // From 5.8.3 on, Tapestry tries to only invalidate the components and pages known as
    299        // using the changed resources. If a given resource is changed but not associated with any
    300        // component, then all of them are invalidated.
    301        resourceChangeTracker.addInvalidationCallback(this::listen);
    302    }
    303
    304    private List<String> listen(List<String> resources)
    305    {
    306
    307        if (resources.isEmpty())
    308        {
    309            clearCache();
    310        }
    311        else
    312        {
    313            String pageName;
    314            for (String className : resources)
    315            {
    316                if (componentClassResolver.isPage(className))
    317                {
    318                    pageName = componentClassResolver.resolvePageClassNameToPageName(className);
    319                    final Iterator<Entry<CachedPageKey, Object>> iterator = pageCache.entrySet().iterator();
    320                    while (iterator.hasNext())
    321                    {
    322                        final Entry<CachedPageKey, Object> entry = iterator.next();
    323                        final String entryPageName = entry.getKey().pageName;
    324                        if (entryPageName.equalsIgnoreCase(pageName))
    325                        {
    326                            logger.info("Clearing cached page '{}'", pageName);
    327                            iterator.remove();
    328                        }
    329                    }
    330                    abstractClassInfoCache.remove(className);
    331                }
    332            }
    333        }
    334
    335        return Collections.emptyList();
    336    }
    337
    338    public void clearCache()
    339    {
    340        logger.info("Clearing page cache");
    341        pageCache.clear();
    342    }
    343
    344    public Set<Page> getAllPages()
    345    {
    346        return F.flow(pageCache.values()).map(new Mapper<Object, Page>()
    347        {
    348            public Page map(Object object)
    349            {
    350                return toPage(object);
    351            }
    352        }).removeNulls().toSet();
    353    }
    354
    355    private Page toPage(Object object)
    356    {
    357        Page page;
    358        if (object instanceof SoftReference)
    359        {
    360            @SuppressWarnings("unchecked")
    361            SoftReference<Page> ref = (SoftReference<Page>) object;
    362            page = ref == null ? null : ref.get();
    363        }
    364        else
    365        {
    366            page = (Page) object;
    367        }
    368        return page;
    369    }
    370
    371    private boolean isAbstract(final String className)
    372    {
    373        final Boolean computeIfAbsent = abstractClassInfoCache.computeIfAbsent(className,
    374                (s) -> Modifier.isAbstract(ThrowawayClassLoader.load(className).getModifiers()));
    375        return computeIfAbsent;
    376    }
    377
    378}