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}