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.lang.ref.Reference; 016import java.lang.ref.SoftReference; 017import java.net.MalformedURLException; 018import java.net.URL; 019import java.util.Arrays; 020import java.util.Collections; 021import java.util.Iterator; 022import java.util.List; 023import java.util.Locale; 024import java.util.Map; 025import java.util.Map.Entry; 026import java.util.concurrent.atomic.AtomicBoolean; 027 028import org.apache.tapestry5.Asset; 029import org.apache.tapestry5.ComponentResources; 030import org.apache.tapestry5.commons.Resource; 031import org.apache.tapestry5.commons.internal.util.LockSupport; 032import org.apache.tapestry5.commons.util.CollectionFactory; 033import org.apache.tapestry5.commons.util.StrategyRegistry; 034import org.apache.tapestry5.http.services.Request; 035import org.apache.tapestry5.internal.AssetConstants; 036import org.apache.tapestry5.internal.TapestryInternalUtils; 037import org.apache.tapestry5.internal.services.assets.ResourceChangeTracker; 038import org.apache.tapestry5.ioc.Invokable; 039import org.apache.tapestry5.ioc.OperationTracker; 040import org.apache.tapestry5.ioc.annotations.PostInjection; 041import org.apache.tapestry5.ioc.internal.util.InternalUtils; 042import org.apache.tapestry5.ioc.services.SymbolSource; 043import org.apache.tapestry5.ioc.services.ThreadLocale; 044import org.apache.tapestry5.services.AssetFactory; 045import org.apache.tapestry5.services.AssetNotFoundException; 046import org.apache.tapestry5.services.AssetSource; 047import org.slf4j.Logger; 048 049@SuppressWarnings("all") 050public class AssetSourceImpl extends LockSupport implements AssetSource 051{ 052 053 private final List<String> EXTERNAL_URL_PREFIXES = Arrays.asList( 054 AssetConstants.HTTP, AssetConstants.HTTPS, AssetConstants.PROTOCOL_RELATIVE, AssetConstants.FTP); 055 056 private final StrategyRegistry<AssetFactory> registry; 057 058 private final ThreadLocale threadLocale; 059 060 private final Map<String, Resource> prefixToRootResource = CollectionFactory.newMap(); 061 062 private final Map<Resource, SoftReference<Asset>> cache = CollectionFactory.newConcurrentMap(); 063 064 private final SymbolSource symbolSource; 065 066 private final Logger logger; 067 068 private final AtomicBoolean firstWarning = new AtomicBoolean(true); 069 070 private final OperationTracker tracker; 071 072 private final Request request; 073 074 private final Map<String, AssetFactory> configuration; 075 076 public AssetSourceImpl(ThreadLocale threadLocale, 077 078 Map<String, AssetFactory> configuration, SymbolSource symbolSource, Logger logger, OperationTracker tracker) 079 { 080 this(threadLocale, configuration, symbolSource, logger, tracker, null); 081 } 082 083 084 public AssetSourceImpl(ThreadLocale threadLocale, 085 086 Map<String, AssetFactory> configuration, SymbolSource symbolSource, Logger logger, OperationTracker tracker, Request request) 087 { 088 this.configuration = configuration; 089 this.threadLocale = threadLocale; 090 this.symbolSource = symbolSource; 091 this.logger = logger; 092 this.tracker = tracker; 093 this.request = request; 094 095 Map<Class, AssetFactory> byResourceClass = CollectionFactory.newMap(); 096 097 for (Map.Entry<String, AssetFactory> e : configuration.entrySet()) 098 { 099 String prefix = e.getKey(); 100 AssetFactory factory = e.getValue(); 101 102 Resource rootResource = factory.getRootResource(); 103 104 byResourceClass.put(rootResource.getClass(), factory); 105 106 prefixToRootResource.put(prefix, rootResource); 107 } 108 109 registry = StrategyRegistry.newInstance(AssetFactory.class, byResourceClass); 110 } 111 112 @PostInjection 113 public void clearCacheWhenResourcesChange(ResourceChangeTracker tracker) 114 { 115 tracker.addInvalidationCallback(this::invalidate); 116 } 117 118 private List<String> invalidate(List<String> resources) 119 { 120 final Iterator<Entry<Resource, SoftReference<Asset>>> iterator = cache.entrySet().iterator(); 121 for (String resource : resources) 122 { 123 while (iterator.hasNext()) 124 { 125 if (iterator.next().getKey().toString().equals(resource)) 126 { 127 iterator.remove(); 128 } 129 } 130 } 131 return Collections.emptyList(); 132 } 133 134 public Asset getClasspathAsset(String path) 135 { 136 return getClasspathAsset(path, null); 137 } 138 139 public Asset getClasspathAsset(String path, Locale locale) 140 { 141 return getAsset(null, path, locale); 142 } 143 144 public Asset getContextAsset(String path, Locale locale) 145 { 146 return getAsset(prefixToRootResource.get(AssetConstants.CONTEXT), path, locale); 147 } 148 149 public Asset getAsset(Resource baseResource, String path, Locale locale) 150 { 151 return getAssetInLocale(baseResource, path, defaulted(locale)); 152 } 153 154 public Resource resourceForPath(String path) 155 { 156 return findResource(null, path); 157 } 158 159 public Asset getExpandedAsset(String path) 160 { 161 return getUnlocalizedAsset(symbolSource.expandSymbols(path)); 162 } 163 164 public Asset getComponentAsset(final ComponentResources resources, final String path, final String libraryName) 165 { 166 assert resources != null; 167 168 assert InternalUtils.isNonBlank(path); 169 170 return tracker.invoke(String.format("Resolving '%s' for component %s", path, resources.getCompleteId()), 171 new Invokable<Asset>() 172 { 173 public Asset invoke() 174 { 175 // First, expand symbols: 176 177 String expanded = symbolSource.expandSymbols(path); 178 179 int dotx = expanded.indexOf(':'); 180 181 // We special case the hell out of 'classpath:' so that we can provide warnings today (5.4) and 182 // blow up in a useful fashion tomorrow (5.5). 183 184 if (expanded.startsWith("//") || (dotx > 0 && !expanded.substring(0, dotx).equalsIgnoreCase(AssetConstants.CLASSPATH))) 185 { 186 final String prefix = dotx >= 0 ? expanded.substring(0, dotx) : AssetConstants.PROTOCOL_RELATIVE; 187 if (EXTERNAL_URL_PREFIXES.contains(prefix)) 188 { 189 190 String url; 191 if (prefix.equals(AssetConstants.PROTOCOL_RELATIVE)) 192 { 193 url = (request != null && request.isSecure() ? "https:" : "http:") + expanded; 194 url = url.replace("//:", "//"); 195 } else 196 { 197 url = expanded; 198 } 199 200 try 201 { 202 UrlResource resource = new UrlResource(new URL(url)); 203 return new UrlAsset(url, resource); 204 } catch (MalformedURLException e) 205 { 206 throw new RuntimeException(e); 207 } 208 } else 209 { 210 return getAssetInLocale(resources.getBaseResource(), expanded, resources.getLocale()); 211 } 212 } 213 214 // No prefix, so implicitly classpath:, or explicitly classpath: 215 216 String restOfPath = expanded.substring(dotx + 1); 217 218 // This is tricky, because a relative path (including "../") is ok in 5.3, since its just somewhere 219 // else on the classpath (though you can "stray" out of the "safe" zone). In 5.4, under /META-INF/assets/ 220 // it's possible to "stray" out beyond the safe zone more easily, into parts of the classpath that can't be 221 // represented in the URL. 222 223 // Ends with trailing slash: 224 String metaRoot = "META-INF/assets/" + toPathPrefix(libraryName); 225 226 String trimmedRestOfPath = restOfPath.startsWith("/") ? restOfPath.substring(1) : restOfPath; 227 228 229 // TAP5-2044: Some components specify a full path, starting with META-INF/assets/, and we should just trust them. 230 // The warning logic below is for compnents that specify a relative path. Our bad decisions come back to haunt us; 231 // Resource paths should always had a leading slash to differentiate relative from complete. 232 String metaPath = trimmedRestOfPath.startsWith("META-INF/assets/") ? trimmedRestOfPath : metaRoot + trimmedRestOfPath; 233 234 // Based on the path, metaResource is where it should exist in a 5.4 and beyond world ... unless the expanded 235 // path was a bit too full of ../ sequences, in which case the expanded path is not valid and we adjust the 236 // error we write. 237 238 Resource metaResource = findLocalizedResource(null, metaPath, resources.getLocale()); 239 240 Asset result = getComponentAsset(resources, expanded, metaResource); 241 242 if (result == null) 243 { 244 throw new RuntimeException(String.format("Unable to locate asset '%s' for component %s. It should be located at %s.", 245 path, resources.getCompleteId(), 246 metaPath)); 247 } 248 249 // This is the best way to tell if the result is an asset for a Classpath resource. 250 251 Resource resultResource = result.getResource(); 252 253 if (!resultResource.equals(metaResource)) 254 { 255 if (firstWarning.getAndSet(false)) 256 { 257 logger.error("Packaging of classpath assets has changed in release 5.4; " + 258 "Assets should no longer be on the main classpath, " + 259 "but should be moved to 'META-INF/assets/' or a sub-folder. Future releases of Tapestry may " + 260 "no longer support assets on the main classpath."); 261 } 262 263 if (metaResource.getFolder().startsWith(metaRoot)) 264 { 265 logger.warn(String.format("Classpath asset '/%s' should be moved to folder '/%s/'.", 266 resultResource.getPath(), 267 metaResource.getFolder())); 268 } else 269 { 270 logger.warn(String.format("Classpath asset '/%s' should be moved under folder '/%s', and the relative path adjusted.", 271 resultResource.getPath(), 272 metaRoot)); 273 } 274 } 275 276 return result; 277 } 278 } 279 280 ); 281 } 282 283 private Asset getComponentAsset(ComponentResources resources, String expandedPath, Resource metaResource) 284 { 285 286 if (expandedPath.contains(":") || expandedPath.startsWith("/")) 287 { 288 return getAssetInLocale(resources.getBaseResource(), expandedPath, resources.getLocale()); 289 } 290 291 // So, it's relative to the component. First, check if there's a match using the 5.4 rules. 292 293 if (metaResource.exists()) 294 { 295 return getAssetForResource(metaResource); 296 } 297 298 Resource oldStyle = findLocalizedResource(resources.getBaseResource(), expandedPath, resources.getLocale()); 299 300 if (oldStyle == null || !oldStyle.exists()) 301 { 302 return null; 303 } 304 305 return getAssetForResource(oldStyle); 306 } 307 308 /** 309 * Figure out the relative path, under /META-INF/assets/ for resources for a given library. 310 * The application library is the blank string and goes directly in /assets/; other libraries 311 * are like virtual folders within /assets/. 312 */ 313 private String toPathPrefix(String libraryName) 314 { 315 return libraryName.equals("") ? "" : libraryName + "/"; 316 } 317 318 public Asset getUnlocalizedAsset(String path) 319 { 320 return getAssetInLocale(null, path, null); 321 } 322 323 private Asset getAssetInLocale(Resource baseResource, String path, Locale locale) 324 { 325 return getLocalizedAssetFromResource(findResource(baseResource, path), locale); 326 } 327 328 /** 329 * @param baseResource 330 * the base resource (or null for classpath root) that path will extend from 331 * @param path 332 * extension path from the base resource 333 * @return the resource, unlocalized, which may not exist (may be for a path with no actual resource) 334 */ 335 private Resource findResource(Resource baseResource, String path) 336 { 337 assert path != null; 338 int colonx = path.indexOf(':'); 339 340 if (colonx < 0) 341 { 342 Resource root = baseResource != null ? baseResource : prefixToRootResource.get(AssetConstants.CLASSPATH); 343 344 return root.forFile(path); 345 } 346 347 String prefix = path.substring(0, colonx); 348 349 Resource root = prefixToRootResource.get(prefix); 350 351 if (root == null) 352 throw new IllegalArgumentException(String.format("Unknown prefix for asset path '%s'.", path)); 353 354 return root.forFile(path.substring(colonx + 1)); 355 } 356 357 /** 358 * Finds a localized resource. 359 * 360 * @param baseResource 361 * base resource, or null for classpath root 362 * @param path 363 * path from baseResource to expected resource 364 * @param locale 365 * locale to localize for, or null to not localize 366 * @return resource, which may not exist 367 */ 368 private Resource findLocalizedResource(Resource baseResource, String path, Locale locale) 369 { 370 Resource unlocalized = findResource(baseResource, path); 371 372 if (locale == null || !unlocalized.exists()) 373 { 374 return unlocalized; 375 } 376 377 return localize(unlocalized, locale); 378 } 379 380 private Resource localize(Resource unlocalized, Locale locale) 381 { 382 Resource localized = unlocalized.forLocale(locale); 383 384 return localized != null ? localized : unlocalized; 385 } 386 387 private Asset getLocalizedAssetFromResource(Resource unlocalized, Locale locale) 388 { 389 final Resource localized; 390 if (locale == null) 391 { 392 localized = unlocalized; 393 } else 394 { 395 Reference<Asset> reference = cache.get(unlocalized); 396 if (reference != null) 397 { 398 Asset asset = reference.get(); 399 if (asset != null) 400 { 401 unlocalized = asset.getResource(); // Prefer resource from cache to use its cache 402 } 403 } 404 405 localized = unlocalized.forLocale(locale); 406 } 407 408 if (localized == null || !localized.exists()) 409 { 410 throw new AssetNotFoundException(String.format("Unable to locate asset '%s' (the file does not exist).", unlocalized), unlocalized); 411 } 412 413 return getAssetForResource(localized); 414 } 415 416 private Asset getAssetForResource(Resource resource) 417 { 418 try 419 { 420 acquireReadLock(); 421 422 Asset result = TapestryInternalUtils.getAndDeref(cache, resource); 423 424 if (result == null) 425 { 426 result = createAssetFromResource(resource); 427 cache.put(resource, new SoftReference(result)); 428 } 429 430 return result; 431 } finally 432 { 433 releaseReadLock(); 434 } 435 } 436 437 private Locale defaulted(Locale locale) 438 { 439 return locale != null ? locale : threadLocale.getLocale(); 440 } 441 442 private Asset createAssetFromResource(Resource resource) 443 { 444 // The class of the resource is derived from the class of the base resource. 445 // So we can then use the class of the resource as a key to locate the correct asset 446 // factory. 447 448 try 449 { 450 upgradeReadLockToWriteLock(); 451 452 // Check for competing thread beat us to it (not very likely!): 453 454 Asset result = TapestryInternalUtils.getAndDeref(cache, resource); 455 456 if (result != null) 457 { 458 return result; 459 } 460 461 Class resourceClass = resource.getClass(); 462 463 AssetFactory factory = registry.get(resourceClass); 464 465 return factory.createAsset(resource); 466 } finally 467 { 468 downgradeWriteLockToReadLock(); 469 } 470 } 471}