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}