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