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}