001// Copyright 2010, 2011, 2012 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.util.ArrayList; 019import java.util.Collections; 020import java.util.Iterator; 021import java.util.List; 022import java.util.Map; 023import java.util.Map.Entry; 024import java.util.Set; 025 026import org.apache.tapestry5.SymbolConstants; 027import org.apache.tapestry5.commons.services.InvalidationEventHub; 028import org.apache.tapestry5.commons.util.CollectionFactory; 029import org.apache.tapestry5.func.F; 030import org.apache.tapestry5.func.Mapper; 031import org.apache.tapestry5.internal.services.ComponentDependencyRegistry.DependencyType; 032import org.apache.tapestry5.internal.services.assets.ResourceChangeTracker; 033import org.apache.tapestry5.internal.structure.ComponentPageElement; 034import org.apache.tapestry5.internal.structure.Page; 035import org.apache.tapestry5.ioc.annotations.ComponentClasses; 036import org.apache.tapestry5.ioc.annotations.PostInjection; 037import org.apache.tapestry5.ioc.annotations.Symbol; 038import org.apache.tapestry5.services.ComponentClassResolver; 039import org.apache.tapestry5.services.ComponentMessages; 040import org.apache.tapestry5.services.ComponentTemplates; 041import org.apache.tapestry5.services.pageload.ComponentRequestSelectorAnalyzer; 042import org.apache.tapestry5.services.pageload.ComponentResourceSelector; 043import org.apache.tapestry5.services.pageload.PageCachingReferenceTypeService; 044import org.apache.tapestry5.services.pageload.PageClassLoaderContext; 045import org.apache.tapestry5.services.pageload.PageClassLoaderContextManager; 046import org.apache.tapestry5.services.pageload.ReferenceType; 047import org.slf4j.Logger; 048 049public class PageSourceImpl implements PageSource 050{ 051 private final ComponentRequestSelectorAnalyzer selectorAnalyzer; 052 053 private final PageLoader pageLoader; 054 055 private final ComponentDependencyRegistry componentDependencyRegistry; 056 057 private final ComponentClassResolver componentClassResolver; 058 059 private final PageClassLoaderContextManager pageClassLoaderContextManager; 060 061 private final PageCachingReferenceTypeService pageCachingReferenceTypeService; 062 063 private final Logger logger; 064 065 final private boolean productionMode; 066 067 final private boolean multipleClassLoaders; 068 069 private static final class CachedPageKey 070 { 071 final String pageName; 072 073 final ComponentResourceSelector selector; 074 075 public CachedPageKey(String pageName, ComponentResourceSelector selector) 076 { 077 this.pageName = pageName; 078 this.selector = selector; 079 } 080 081 public int hashCode() 082 { 083 return 37 * pageName.hashCode() + selector.hashCode(); 084 } 085 086 public boolean equals(Object obj) 087 { 088 if (this == obj) 089 return true; 090 091 if (!(obj instanceof CachedPageKey)) 092 return false; 093 094 CachedPageKey other = (CachedPageKey) obj; 095 096 return pageName.equals(other.pageName) && selector.equals(other.selector); 097 } 098 099 @Override 100 public String toString() { 101 return "CachedPageKey [pageName=" + pageName + ", selector=" + selector + "]"; 102 } 103 104 105 } 106 107 private final Map<CachedPageKey, Object> pageCache = CollectionFactory.newConcurrentMap(); 108 109 public PageSourceImpl(PageLoader pageLoader, ComponentRequestSelectorAnalyzer selectorAnalyzer, 110 ComponentDependencyRegistry componentDependencyRegistry, 111 ComponentClassResolver componentClassResolver, 112 PageClassLoaderContextManager pageClassLoaderContextManager, 113 PageCachingReferenceTypeService pageCachingReferenceTypeService, 114 @Symbol(SymbolConstants.PRODUCTION_MODE) boolean productionMode, 115 @Symbol(SymbolConstants.MULTIPLE_CLASSLOADERS) boolean multipleClassLoaders, 116 Logger logger) 117 { 118 this.pageLoader = pageLoader; 119 this.selectorAnalyzer = selectorAnalyzer; 120 this.componentDependencyRegistry = componentDependencyRegistry; 121 this.componentClassResolver = componentClassResolver; 122 this.productionMode = productionMode; 123 this.pageCachingReferenceTypeService = pageCachingReferenceTypeService; 124 this.multipleClassLoaders = multipleClassLoaders; 125 this.pageClassLoaderContextManager = pageClassLoaderContextManager; 126 this.logger = logger; 127 } 128 129 public Page getPage(String canonicalPageName) 130 { 131 if (!productionMode) 132 { 133 componentDependencyRegistry.disableInvalidations(); 134 } 135 try 136 { 137 return getPage(canonicalPageName, true); 138 } 139 finally 140 { 141 if (!productionMode) 142 { 143 componentDependencyRegistry.enableInvalidations(); 144 } 145 } 146 } 147 148 public Page getPage(String canonicalPageName, boolean invalidateUnknownContext) 149 { 150 ComponentResourceSelector selector = selectorAnalyzer.buildSelectorForRequest(); 151 152 CachedPageKey key = new CachedPageKey(canonicalPageName, selector); 153 154 // The while loop looks superfluous, but it helps to ensure that the Page instance, 155 // with all of its mutable construction-time state, is properly published to other 156 // threads (at least, as I understand Brian Goetz's explanation, it should be). 157 158 while (true) 159 { 160 161 Page page; 162 Object object = pageCache.get(key); 163 164 page = toPage(object); 165 166 if (page != null) 167 { 168 return page; 169 } 170 171 final String className = componentClassResolver.resolvePageNameToClassName(canonicalPageName); 172 if (multipleClassLoaders) 173 { 174 175 // Avoiding problems in PlasticClassPool.createTransformation() 176 // when the class being loaded has a page superclass 177 final List<String> pageDependencies = preprocessPageDependencies(className); 178 179 for (String pageClassName : pageDependencies) 180 { 181 page = getPage(componentClassResolver.resolvePageClassNameToPageName(pageClassName), false); 182 } 183 184 } 185 186 // In rare race conditions, we may see the same page loaded multiple times across 187 // different threads. The last built one will "evict" the others from the page cache, 188 // and the earlier ones will be GCed. 189 190 page = pageLoader.loadPage(canonicalPageName, selector); 191 192 final ReferenceType referenceType = pageCachingReferenceTypeService.get(canonicalPageName); 193 if (referenceType.equals(ReferenceType.SOFT)) 194 { 195 pageCache.put(key, new SoftReference<Page>(page)); 196 } 197 else 198 { 199 pageCache.put(key, page); 200 } 201 202 if (!productionMode) 203 { 204 final ComponentPageElement rootElement = page.getRootElement(); 205 componentDependencyRegistry.clear(rootElement); 206 componentDependencyRegistry.register(rootElement); 207 PageClassLoaderContext context = pageClassLoaderContextManager.get(className); 208 209 if (context.isUnknown() && multipleClassLoaders) 210 { 211 this.pageCache.remove(key); 212 if (invalidateUnknownContext) 213 { 214 pageClassLoaderContextManager.invalidateAndFireInvalidationEvents(context); 215 preprocessPageDependencies(className); 216 } 217 context.getClassNames().clear(); 218 // Avoiding bad invalidations 219 return getPage(canonicalPageName, false); 220 } 221 } 222 223 } 224 225 226 } 227 228 private List<String> preprocessPageDependencies(final String className) { 229 final List<String> pageDependencies = new ArrayList<>(); 230 pageDependencies.addAll( 231 new ArrayList<String>(componentDependencyRegistry.getDependencies(className, DependencyType.INJECT_PAGE))); 232 pageDependencies.addAll( 233 new ArrayList<String>(componentDependencyRegistry.getDependencies(className, DependencyType.SUPERCLASS))); 234 235 final Iterator<String> iterator = pageDependencies.iterator(); 236 while (iterator.hasNext()) 237 { 238 if (!iterator.next().contains(".pages.")) 239 { 240 iterator.remove(); 241 } 242 } 243 244 preprocessPageClassLoaderContexts(className, pageDependencies); 245 return pageDependencies; 246 } 247 248 private void preprocessPageClassLoaderContexts(String className, final List<String> pageDependencies) { 249 for (int i = 0; i < 5; i++) 250 { 251 pageClassLoaderContextManager.get(className); 252 for (String pageClassName : pageDependencies) 253 { 254 final PageClassLoaderContext context = pageClassLoaderContextManager.get(pageClassName); 255 if (i == 1) 256 { 257 try 258 { 259 context.getClassLoader().loadClass(pageClassName); 260 } catch (ClassNotFoundException e) { 261 throw new RuntimeException(e); 262 } 263 } 264 } 265 } 266 267 // TODO: remove 268// for (String pageClassName : pageDependencies) 269// { 270// try 271// { 272// pageClassLoaderContextManager.get(pageClassName).getClassLoader().loadClass(pageClassName); 273// } catch (ClassNotFoundException e) 274// { 275// throw new RuntimeException(e); 276// } 277// } 278 } 279 280 @PostInjection 281 public void setupInvalidation(@ComponentClasses InvalidationEventHub classesHub, 282 @ComponentTemplates InvalidationEventHub templatesHub, 283 @ComponentMessages InvalidationEventHub messagesHub, 284 ResourceChangeTracker resourceChangeTracker) 285 { 286 classesHub.addInvalidationCallback(this::listen); 287 templatesHub.addInvalidationCallback(this::listen); 288 messagesHub.addInvalidationCallback(this::listen); 289 290 // Because Assets can be injected into pages, and Assets are invalidated when 291 // an Asset's value is changed (partly due to the change, in 5.4, to include the asset's 292 // checksum as part of the asset URL), then when we notice a change to 293 // any Resource, it is necessary to discard all page instances. 294 // From 5.8.3 on, Tapestry tries to only invalidate the components and pages known as 295 // using the changed resources. If a given resource is changed but not associated with any 296 // component, then all of them are invalidated. 297 resourceChangeTracker.addInvalidationCallback(this::listen); 298 } 299 300 private List<String> listen(List<String> resources) 301 { 302 303 if (resources.isEmpty()) 304 { 305 clearCache(); 306 } 307 else 308 { 309 String pageName; 310 for (String className : resources) 311 { 312 if (componentClassResolver.isPage(className)) 313 { 314 pageName = componentClassResolver.resolvePageClassNameToPageName(className); 315 final Iterator<Entry<CachedPageKey, Object>> iterator = pageCache.entrySet().iterator(); 316 while (iterator.hasNext()) 317 { 318 final Entry<CachedPageKey, Object> entry = iterator.next(); 319 final String entryPageName = entry.getKey().pageName; 320 if (entryPageName.equalsIgnoreCase(pageName)) 321 { 322 logger.info("Clearing cached page '{}'", pageName); 323 iterator.remove(); 324 } 325 } 326 } 327 } 328 } 329 330 return Collections.emptyList(); 331 } 332 333 public void clearCache() 334 { 335 logger.info("Clearing page cache"); 336 pageCache.clear(); 337 } 338 339 public Set<Page> getAllPages() 340 { 341 return F.flow(pageCache.values()).map(new Mapper<Object, Page>() 342 { 343 public Page map(Object object) 344 { 345 return toPage(object); 346 } 347 }).removeNulls().toSet(); 348 } 349 350 private Page toPage(Object object) 351 { 352 Page page; 353 if (object instanceof SoftReference) 354 { 355 @SuppressWarnings("unchecked") 356 SoftReference<Page> ref = (SoftReference<Page>) object; 357 page = ref == null ? null : ref.get(); 358 } 359 else 360 { 361 page = (Page) object; 362 } 363 return page; 364 } 365 366}