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.SoftReference; 036import java.net.MalformedURLException; 037import java.net.URL; 038import java.util.Arrays; 039import java.util.List; 040import java.util.Locale; 041import java.util.Map; 042import java.util.WeakHashMap; 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 ), 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 } 177 else 178 { 179 url = expanded; 180 } 181 182 try 183 { 184 UrlResource resource = new UrlResource(new URL(url)); 185 return new UrlAsset(url, resource); 186 } 187 catch (MalformedURLException e) 188 { 189 throw new RuntimeException(e); 190 } 191 } 192 else 193 { 194 return getAssetInLocale(resources.getBaseResource(), expanded, resources.getLocale()); 195 } 196 } 197 198 // No prefix, so implicitly classpath:, or explicitly classpath: 199 200 String restOfPath = expanded.substring(dotx + 1); 201 202 // This is tricky, because a relative path (including "../") is ok in 5.3, since its just somewhere 203 // else on the classpath (though you can "stray" out of the "safe" zone). In 5.4, under /META-INF/assets/ 204 // it's possible to "stray" out beyond the safe zone more easily, into parts of the classpath that can't be 205 // represented in the URL. 206 207 // Ends with trailing slash: 208 String metaRoot = "META-INF/assets/" + toPathPrefix(libraryName); 209 210 String trimmedRestOfPath = restOfPath.startsWith("/") ? restOfPath.substring(1) : restOfPath; 211 212 213 // TAP5-2044: Some components specify a full path, starting with META-INF/assets/, and we should just trust them. 214 // The warning logic below is for compnents that specify a relative path. Our bad decisions come back to haunt us; 215 // Resource paths should always had a leading slash to differentiate relative from complete. 216 String metaPath = trimmedRestOfPath.startsWith("META-INF/assets/") ? trimmedRestOfPath : metaRoot + trimmedRestOfPath; 217 218 // Based on the path, metaResource is where it should exist in a 5.4 and beyond world ... unless the expanded 219 // path was a bit too full of ../ sequences, in which case the expanded path is not valid and we adjust the 220 // error we write. 221 222 Resource metaResource = findLocalizedResource(null, metaPath, resources.getLocale()); 223 224 Asset result = getComponentAsset(resources, expanded, metaResource); 225 226 if (result == null) 227 { 228 throw new RuntimeException(String.format("Unable to locate asset '%s' for component %s. It should be located at %s.", 229 path, resources.getCompleteId(), 230 metaPath)); 231 } 232 233 // This is the best way to tell if the result is an asset for a Classpath resource. 234 235 Resource resultResource = result.getResource(); 236 237 if (!resultResource.equals(metaResource)) 238 { 239 if (firstWarning.getAndSet(false)) 240 { 241 logger.error("Packaging of classpath assets has changed in release 5.4; " + 242 "Assets should no longer be on the main classpath, " + 243 "but should be moved to 'META-INF/assets/' or a sub-folder. Future releases of Tapestry may " + 244 "no longer support assets on the main classpath."); 245 } 246 247 if (metaResource.getFolder().startsWith(metaRoot)) 248 { 249 logger.warn(String.format("Classpath asset '/%s' should be moved to folder '/%s/'.", 250 resultResource.getPath(), 251 metaResource.getFolder())); 252 } else 253 { 254 logger.warn(String.format("Classpath asset '/%s' should be moved under folder '/%s', and the relative path adjusted.", 255 resultResource.getPath(), 256 metaRoot)); 257 } 258 } 259 260 return result; 261 } 262 } 263 264 ); 265 } 266 267 private Asset getComponentAsset(ComponentResources resources, String expandedPath, Resource metaResource) 268 { 269 270 if (expandedPath.contains(":") || expandedPath.startsWith("/")) 271 { 272 return getAssetInLocale(resources.getBaseResource(), expandedPath, resources.getLocale()); 273 } 274 275 // So, it's relative to the component. First, check if there's a match using the 5.4 rules. 276 277 if (metaResource.exists()) 278 { 279 return getAssetForResource(metaResource); 280 } 281 282 Resource oldStyle = findLocalizedResource(resources.getBaseResource(), expandedPath, resources.getLocale()); 283 284 if (oldStyle == null || !oldStyle.exists()) 285 { 286 return null; 287 } 288 289 return getAssetForResource(oldStyle); 290 } 291 292 /** 293 * Figure out the relative path, under /META-INF/assets/ for resources for a given library. 294 * The application library is the blank string and goes directly in /assets/; other libraries 295 * are like virtual folders within /assets/. 296 */ 297 private String toPathPrefix(String libraryName) 298 { 299 return libraryName.equals("") ? "" : libraryName + "/"; 300 } 301 302 public Asset getUnlocalizedAsset(String path) 303 { 304 return getAssetInLocale(null, path, null); 305 } 306 307 private Asset getAssetInLocale(Resource baseResource, String path, Locale locale) 308 { 309 return getLocalizedAssetFromResource(findResource(baseResource, path), locale); 310 } 311 312 /** 313 * @param baseResource 314 * the base resource (or null for classpath root) that path will extend from 315 * @param path 316 * extension path from the base resource 317 * @return the resource, unlocalized, which may not exist (may be for a path with no actual resource) 318 */ 319 private Resource findResource(Resource baseResource, String path) 320 { 321 assert path != null; 322 int colonx = path.indexOf(':'); 323 324 if (colonx < 0) 325 { 326 Resource root = baseResource != null ? baseResource : prefixToRootResource.get(AssetConstants.CLASSPATH); 327 328 return root.forFile(path); 329 } 330 331 String prefix = path.substring(0, colonx); 332 333 Resource root = prefixToRootResource.get(prefix); 334 335 if (root == null) 336 throw new IllegalArgumentException(String.format("Unknown prefix for asset path '%s'.", path)); 337 338 return root.forFile(path.substring(colonx + 1)); 339 } 340 341 /** 342 * Finds a localized resource. 343 * 344 * @param baseResource 345 * base resource, or null for classpath root 346 * @param path 347 * path from baseResource to expected resource 348 * @param locale 349 * locale to localize for, or null to not localize 350 * @return resource, which may not exist 351 */ 352 private Resource findLocalizedResource(Resource baseResource, String path, Locale locale) 353 { 354 Resource unlocalized = findResource(baseResource, path); 355 356 if (locale == null || !unlocalized.exists()) 357 { 358 return unlocalized; 359 } 360 361 return localize(unlocalized, locale); 362 } 363 364 private Resource localize(Resource unlocalized, Locale locale) 365 { 366 Resource localized = unlocalized.forLocale(locale); 367 368 return localized != null ? localized : unlocalized; 369 } 370 371 private Asset getLocalizedAssetFromResource(Resource unlocalized, Locale locale) 372 { 373 Resource localized = locale == null ? unlocalized : unlocalized.forLocale(locale); 374 375 if (localized == null || !localized.exists()) 376 throw new RuntimeException(String.format("Unable to locate asset '%s' (the file does not exist).", unlocalized)); 377 378 return getAssetForResource(localized); 379 } 380 381 private Asset getAssetForResource(Resource resource) 382 { 383 try 384 { 385 acquireReadLock(); 386 387 Asset result = TapestryInternalUtils.getAndDeref(cache, resource); 388 389 if (result == null) 390 { 391 result = createAssetFromResource(resource); 392 cache.put(resource, new SoftReference(result)); 393 } 394 395 return result; 396 } finally 397 { 398 releaseReadLock(); 399 } 400 } 401 402 private Locale defaulted(Locale locale) 403 { 404 return locale != null ? locale : threadLocale.getLocale(); 405 } 406 407 private Asset createAssetFromResource(Resource resource) 408 { 409 // The class of the resource is derived from the class of the base resource. 410 // So we can then use the class of the resource as a key to locate the correct asset 411 // factory. 412 413 try 414 { 415 upgradeReadLockToWriteLock(); 416 417 // Check for competing thread beat us to it (not very likely!): 418 419 Asset result = TapestryInternalUtils.getAndDeref(cache, resource); 420 421 if (result != null) 422 { 423 return result; 424 } 425 426 Class resourceClass = resource.getClass(); 427 428 AssetFactory factory = registry.get(resourceClass); 429 430 return factory.createAsset(resource); 431 } finally 432 { 433 downgradeWriteLockToReadLock(); 434 } 435 } 436}