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