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}