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}