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}