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}