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