001// Licensed under the Apache License, Version 2.0 (the "License"); 002// you may not use this file except in compliance with the License. 003// You may obtain a copy of the License at 004// 005// http://www.apache.org/licenses/LICENSE-2.0 006// 007// Unless required by applicable law or agreed to in writing, software 008// distributed under the License is distributed on an "AS IS" BASIS, 009// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 010// See the License for the specific language governing permissions and 011// limitations under the License. 012 013package org.apache.tapestry5.internal.services; 014 015import java.util.Collections; 016import java.util.Iterator; 017import java.util.List; 018import java.util.Locale; 019import java.util.Map; 020import java.util.Map.Entry; 021import java.util.Set; 022import java.util.stream.Collectors; 023 024import org.apache.tapestry5.SymbolConstants; 025import org.apache.tapestry5.TapestryConstants; 026import org.apache.tapestry5.commons.Location; 027import org.apache.tapestry5.commons.Resource; 028import org.apache.tapestry5.commons.services.InvalidationEventHub; 029import org.apache.tapestry5.commons.util.CollectionFactory; 030import org.apache.tapestry5.commons.util.MultiKey; 031import org.apache.tapestry5.http.TapestryHttpSymbolConstants; 032import org.apache.tapestry5.internal.event.InvalidationEventHubImpl; 033import org.apache.tapestry5.internal.parser.ComponentTemplate; 034import org.apache.tapestry5.internal.parser.TemplateToken; 035import org.apache.tapestry5.ioc.annotations.Inject; 036import org.apache.tapestry5.ioc.annotations.PostInjection; 037import org.apache.tapestry5.ioc.annotations.Symbol; 038import org.apache.tapestry5.ioc.internal.util.URLChangeTracker; 039import org.apache.tapestry5.ioc.services.ClasspathURLConverter; 040import org.apache.tapestry5.ioc.services.ThreadLocale; 041import org.apache.tapestry5.ioc.services.UpdateListener; 042import org.apache.tapestry5.ioc.services.UpdateListenerHub; 043import org.apache.tapestry5.model.ComponentModel; 044import org.apache.tapestry5.services.pageload.ComponentRequestSelectorAnalyzer; 045import org.apache.tapestry5.services.pageload.ComponentResourceLocator; 046import org.apache.tapestry5.services.pageload.ComponentResourceSelector; 047import org.apache.tapestry5.services.templates.ComponentTemplateLocator; 048import org.slf4j.Logger; 049 050/** 051 * Service implementation that manages a cache of parsed component templates. 052 */ 053public final class ComponentTemplateSourceImpl extends InvalidationEventHubImpl implements ComponentTemplateSource, 054 UpdateListener 055{ 056 private final TemplateParser parser; 057 058 private final URLChangeTracker<TemplateTrackingInfo> tracker; 059 060 private final ComponentResourceLocator locator; 061 062 private final ComponentRequestSelectorAnalyzer componentRequestSelectorAnalyzer; 063 064 private final ThreadLocale threadLocale; 065 066 private final Logger logger; 067 068 private final boolean multipleClassLoaders; 069 070 /** 071 * Caches from a key (combining component name and locale) to a resource. Often, many different keys will point to 072 * the same resource (i.e., "foo:en_US", "foo:en_UK", and "foo:en" may all be parsed from the same "foo.tml" 073 * resource). The resource may end up being null, meaning the template does not exist in any locale. 074 */ 075 private final Map<MultiKey, Resource> templateResources = CollectionFactory.newConcurrentMap(); 076 077 /** 078 * Cache of parsed templates, keyed on resource. 079 */ 080 private final Map<Resource, ComponentTemplate> templates = CollectionFactory.newConcurrentMap(); 081 082 private final ComponentTemplate missingTemplate = new ComponentTemplate() 083 { 084 public Map<String, Location> getComponentIds() 085 { 086 return Collections.emptyMap(); 087 } 088 089 public Resource getResource() 090 { 091 return null; 092 } 093 094 public List<TemplateToken> getTokens() 095 { 096 return Collections.emptyList(); 097 } 098 099 public boolean isMissing() 100 { 101 return true; 102 } 103 104 public List<TemplateToken> getExtensionPointTokens(String extensionPointId) 105 { 106 return null; 107 } 108 109 public boolean isExtension() 110 { 111 return false; 112 } 113 114 public boolean usesStrictMixinParameters() 115 { 116 return false; 117 } 118 119 @Override 120 public Set<String> getExtensionPointIds() 121 { 122 return Collections.emptySet(); 123 } 124 125 }; 126 127 public ComponentTemplateSourceImpl(@Inject 128 @Symbol(TapestryHttpSymbolConstants.PRODUCTION_MODE) 129 boolean productionMode, 130 @Inject 131 @Symbol(SymbolConstants.MULTIPLE_CLASSLOADERS) 132 boolean multipleClassLoaders, 133 TemplateParser parser, ComponentResourceLocator locator, 134 ClasspathURLConverter classpathURLConverter, 135 ComponentRequestSelectorAnalyzer componentRequestSelectorAnalyzer, 136 ThreadLocale threadLocale, Logger logger) 137 { 138 this(productionMode, multipleClassLoaders, parser, locator, new URLChangeTracker<TemplateTrackingInfo>(classpathURLConverter), componentRequestSelectorAnalyzer, threadLocale, logger); 139 } 140 141 ComponentTemplateSourceImpl(boolean productionMode, boolean multipleClassLoaders, TemplateParser parser, ComponentResourceLocator locator, 142 URLChangeTracker<TemplateTrackingInfo> tracker, ComponentRequestSelectorAnalyzer componentRequestSelectorAnalyzer, 143 ThreadLocale threadLocale, Logger logger) 144 { 145 super(productionMode, logger); 146 147 this.parser = parser; 148 this.locator = locator; 149 this.tracker = tracker; 150 this.componentRequestSelectorAnalyzer = componentRequestSelectorAnalyzer; 151 this.threadLocale = threadLocale; 152 this.logger = logger; 153 this.multipleClassLoaders = multipleClassLoaders; 154 } 155 156 @PostInjection 157 public void registerAsUpdateListener(UpdateListenerHub hub) 158 { 159 hub.addUpdateListener(this); 160 } 161 162 @PostInjection 163 public void setupReload(ReloadHelper helper) 164 { 165 helper.addReloadCallback(new Runnable() 166 { 167 public void run() 168 { 169 invalidate(); 170 } 171 }); 172 } 173 174 public ComponentTemplate getTemplate(ComponentModel componentModel, ComponentResourceSelector selector) 175 { 176 String componentName = componentModel.getComponentClassName(); 177 178 MultiKey key = new MultiKey(componentName, selector); 179 180 // First cache is key to resource. 181 182 Resource resource = templateResources.get(key); 183 184 if (resource == null) 185 { 186 resource = locateTemplateResource(componentModel, selector); 187 templateResources.put(key, resource); 188 } 189 190 // If we haven't yet parsed the template into the cache, do so now. 191 192 ComponentTemplate result = templates.get(resource); 193 194 if (result == null) 195 { 196 result = parseTemplate(resource, componentModel.getComponentClassName()); 197 templates.put(resource, result); 198 } 199 200 return result; 201 } 202 203 /** 204 * Resolves the component name to a localized {@link Resource} (using the {@link ComponentTemplateLocator} chain of 205 * command service). The localized resource is used as the key to a cache of {@link ComponentTemplate}s. 206 * 207 * If a template doesn't exist, then the missing ComponentTemplate is returned. 208 */ 209 public ComponentTemplate getTemplate(ComponentModel componentModel, Locale locale) 210 { 211 final Locale original = threadLocale.getLocale(); 212 try 213 { 214 threadLocale.setLocale(locale); 215 return getTemplate(componentModel, componentRequestSelectorAnalyzer.buildSelectorForRequest()); 216 } 217 finally { 218 threadLocale.setLocale(original); 219 } 220 } 221 222 private ComponentTemplate parseTemplate(Resource r, String className) 223 { 224 // In a race condition, we may parse the same template more than once. This will likely add 225 // the resource to the tracker multiple times. Not likely this will cause a big issue. 226 227 if (!r.exists()) 228 return missingTemplate; 229 230 tracker.add(r.toURL(), new TemplateTrackingInfo(r.getPath(), className)); 231 232 return parser.parseTemplate(r); 233 } 234 235 private Resource locateTemplateResource(ComponentModel initialModel, ComponentResourceSelector selector) 236 { 237 ComponentModel model = initialModel; 238 while (model != null) 239 { 240 Resource localized = locator.locateTemplate(model, selector); 241 242 if (localized != null) 243 return localized; 244 245 // Otherwise, this component doesn't have its own template ... lets work up to its 246 // base class and check there. 247 248 model = model.getParentModel(); 249 } 250 251 // This will be a Resource whose URL is null, which will be picked up later and force the 252 // return of the empty template. 253 254 return initialModel.getBaseResource().withExtension(TapestryConstants.TEMPLATE_EXTENSION); 255 } 256 257 /** 258 * Checks to see if any parsed resource has changed. If so, then all internal caches are cleared, and an 259 * invalidation event is fired. This is brute force ... a more targeted dependency management strategy may come 260 * later. 261 * Actually, TAP5-2742 did exactly that! :D 262 */ 263 public void checkForUpdates() 264 { 265 final Set<TemplateTrackingInfo> changedResourcesInfo = tracker.getChangedResourcesInfo(); 266 if (!changedResourcesInfo.isEmpty()) 267 { 268 if (logger.isInfoEnabled()) 269 { 270 logger.info("Changed template(s) found: {}", String.join(", ", 271 changedResourcesInfo.stream().map(TemplateTrackingInfo::getTemplate).collect(Collectors.toList()))); 272 } 273 274 if (multipleClassLoaders) 275 { 276 277 final Iterator<Entry<MultiKey, Resource>> templateResourcesIterator = templateResources.entrySet().iterator(); 278 for (TemplateTrackingInfo info : changedResourcesInfo) 279 { 280 while (templateResourcesIterator.hasNext()) 281 { 282 final MultiKey key = templateResourcesIterator.next().getKey(); 283 if (info.getClassName().equals((String) key.getValues()[0])) 284 { 285 templates.remove(templateResources.get(key)); 286 templateResourcesIterator.remove(); 287 } 288 } 289 } 290 291 fireInvalidationEvent(changedResourcesInfo.stream().map(TemplateTrackingInfo::getClassName).collect(Collectors.toList())); 292 293 } 294 else 295 { 296 invalidate(); 297 } 298 } 299 } 300 301 private void invalidate() 302 { 303 tracker.clear(); 304 templateResources.clear(); 305 templates.clear(); 306 fireInvalidationEvent(); 307 } 308 309 public InvalidationEventHub getInvalidationEventHub() 310 { 311 return this; 312 } 313}