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