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.Collections;
021import java.util.Iterator;
022import java.util.List;
023import java.util.Locale;
024import java.util.Map;
025import java.util.Map.Entry;
026import java.util.concurrent.atomic.AtomicBoolean;
027
028import org.apache.tapestry5.Asset;
029import org.apache.tapestry5.ComponentResources;
030import org.apache.tapestry5.commons.Resource;
031import org.apache.tapestry5.commons.internal.util.LockSupport;
032import org.apache.tapestry5.commons.util.CollectionFactory;
033import org.apache.tapestry5.commons.util.StrategyRegistry;
034import org.apache.tapestry5.http.services.Request;
035import org.apache.tapestry5.internal.AssetConstants;
036import org.apache.tapestry5.internal.TapestryInternalUtils;
037import org.apache.tapestry5.internal.services.assets.ResourceChangeTracker;
038import org.apache.tapestry5.ioc.Invokable;
039import org.apache.tapestry5.ioc.OperationTracker;
040import org.apache.tapestry5.ioc.annotations.PostInjection;
041import org.apache.tapestry5.ioc.internal.util.InternalUtils;
042import org.apache.tapestry5.ioc.services.SymbolSource;
043import org.apache.tapestry5.ioc.services.ThreadLocale;
044import org.apache.tapestry5.services.AssetFactory;
045import org.apache.tapestry5.services.AssetNotFoundException;
046import org.apache.tapestry5.services.AssetSource;
047import org.slf4j.Logger;
048
049@SuppressWarnings("all")
050public class AssetSourceImpl extends LockSupport implements AssetSource
051{
052
053    private final List<String> EXTERNAL_URL_PREFIXES = Arrays.asList(
054            AssetConstants.HTTP, AssetConstants.HTTPS, AssetConstants.PROTOCOL_RELATIVE, AssetConstants.FTP);
055
056    private final StrategyRegistry<AssetFactory> registry;
057
058    private final ThreadLocale threadLocale;
059
060    private final Map<String, Resource> prefixToRootResource = CollectionFactory.newMap();
061
062    private final Map<Resource, SoftReference<Asset>> cache = CollectionFactory.newConcurrentMap();
063
064    private final SymbolSource symbolSource;
065
066    private final Logger logger;
067
068    private final AtomicBoolean firstWarning = new AtomicBoolean(true);
069
070    private final OperationTracker tracker;
071
072    private final Request request;
073
074    private final Map<String, AssetFactory> configuration;
075
076    public AssetSourceImpl(ThreadLocale threadLocale,
077
078                           Map<String, AssetFactory> configuration, SymbolSource symbolSource, Logger logger, OperationTracker tracker)
079    {
080        this(threadLocale, configuration, symbolSource, logger, tracker, null);
081    }
082
083
084    public AssetSourceImpl(ThreadLocale threadLocale,
085
086                           Map<String, AssetFactory> configuration, SymbolSource symbolSource, Logger logger, OperationTracker tracker, Request request)
087    {
088        this.configuration = configuration;
089        this.threadLocale = threadLocale;
090        this.symbolSource = symbolSource;
091        this.logger = logger;
092        this.tracker = tracker;
093        this.request = request;
094
095        Map<Class, AssetFactory> byResourceClass = CollectionFactory.newMap();
096
097        for (Map.Entry<String, AssetFactory> e : configuration.entrySet())
098        {
099            String prefix = e.getKey();
100            AssetFactory factory = e.getValue();
101
102            Resource rootResource = factory.getRootResource();
103
104            byResourceClass.put(rootResource.getClass(), factory);
105
106            prefixToRootResource.put(prefix, rootResource);
107        }
108
109        registry = StrategyRegistry.newInstance(AssetFactory.class, byResourceClass);
110    }
111
112    @PostInjection
113    public void clearCacheWhenResourcesChange(ResourceChangeTracker tracker)
114    {
115        tracker.addInvalidationCallback(this::invalidate);
116    }
117    
118    private List<String> invalidate(List<String> resources)
119    {
120        final Iterator<Entry<Resource, SoftReference<Asset>>> iterator = cache.entrySet().iterator();
121        for (String resource : resources) 
122        {
123            while (iterator.hasNext())
124            {
125                if (iterator.next().getKey().toString().equals(resource))
126                {
127                    iterator.remove();
128                }
129            }
130        }
131        return Collections.emptyList();
132    }
133
134    public Asset getClasspathAsset(String path)
135    {
136        return getClasspathAsset(path, null);
137    }
138
139    public Asset getClasspathAsset(String path, Locale locale)
140    {
141        return getAsset(null, path, locale);
142    }
143
144    public Asset getContextAsset(String path, Locale locale)
145    {
146        return getAsset(prefixToRootResource.get(AssetConstants.CONTEXT), path, locale);
147    }
148
149    public Asset getAsset(Resource baseResource, String path, Locale locale)
150    {
151        return getAssetInLocale(baseResource, path, defaulted(locale));
152    }
153
154    public Resource resourceForPath(String path)
155    {
156        return findResource(null, path);
157    }
158
159    public Asset getExpandedAsset(String path)
160    {
161        return getUnlocalizedAsset(symbolSource.expandSymbols(path));
162    }
163
164    public Asset getComponentAsset(final ComponentResources resources, final String path, final String libraryName)
165    {
166        assert resources != null;
167
168        assert InternalUtils.isNonBlank(path);
169
170        return tracker.invoke(String.format("Resolving '%s' for component %s", path, resources.getCompleteId()),
171                new Invokable<Asset>()
172                {
173                    public Asset invoke()
174                    {
175                        // First, expand symbols:
176
177                        String expanded = symbolSource.expandSymbols(path);
178
179                        int dotx = expanded.indexOf(':');
180
181                        // We special case the hell out of 'classpath:' so that we can provide warnings today (5.4) and
182                        // blow up in a useful fashion tomorrow (5.5).
183
184                        if (expanded.startsWith("//") || (dotx > 0 && !expanded.substring(0, dotx).equalsIgnoreCase(AssetConstants.CLASSPATH)))
185                        {
186                            final String prefix = dotx >= 0 ? expanded.substring(0, dotx) : AssetConstants.PROTOCOL_RELATIVE;
187                            if (EXTERNAL_URL_PREFIXES.contains(prefix))
188                            {
189
190                                String url;
191                                if (prefix.equals(AssetConstants.PROTOCOL_RELATIVE))
192                                {
193                                    url = (request != null && request.isSecure() ? "https:" : "http:") + expanded;
194                                    url = url.replace("//:", "//");
195                                } else
196                                {
197                                    url = expanded;
198                                }
199
200                                try
201                                {
202                                    UrlResource resource = new UrlResource(new URL(url));
203                                    return new UrlAsset(url, resource);
204                                } catch (MalformedURLException e)
205                                {
206                                    throw new RuntimeException(e);
207                                }
208                            } else
209                            {
210                                return getAssetInLocale(resources.getBaseResource(), expanded, resources.getLocale());
211                            }
212                        }
213
214                        // No prefix, so implicitly classpath:, or explicitly classpath:
215
216                        String restOfPath = expanded.substring(dotx + 1);
217
218                        // This is tricky, because a relative path (including "../") is ok in 5.3, since its just somewhere
219                        // else on the classpath (though you can "stray" out of the "safe" zone).  In 5.4, under /META-INF/assets/
220                        // it's possible to "stray" out beyond the safe zone more easily, into parts of the classpath that can't be
221                        // represented in the URL.
222
223                        // Ends with trailing slash:
224                        String metaRoot = "META-INF/assets/" + toPathPrefix(libraryName);
225
226                        String trimmedRestOfPath = restOfPath.startsWith("/") ? restOfPath.substring(1) : restOfPath;
227
228
229                        // TAP5-2044: Some components specify a full path, starting with META-INF/assets/, and we should just trust them.
230                        // The warning logic below is for compnents that specify a relative path. Our bad decisions come back to haunt us;
231                        // Resource paths should always had a leading slash to differentiate relative from complete.
232                        String metaPath = trimmedRestOfPath.startsWith("META-INF/assets/") ? trimmedRestOfPath : metaRoot + trimmedRestOfPath;
233
234                        // Based on the path, metaResource is where it should exist in a 5.4 and beyond world ... unless the expanded
235                        // path was a bit too full of ../ sequences, in which case the expanded path is not valid and we adjust the
236                        // error we write.
237
238                        Resource metaResource = findLocalizedResource(null, metaPath, resources.getLocale());
239
240                        Asset result = getComponentAsset(resources, expanded, metaResource);
241
242                        if (result == null)
243                        {
244                            throw new RuntimeException(String.format("Unable to locate asset '%s' for component %s. It should be located at %s.",
245                                    path, resources.getCompleteId(),
246                                    metaPath));
247                        }
248
249                        // This is the best way to tell if the result is an asset for a Classpath resource.
250
251                        Resource resultResource = result.getResource();
252
253                        if (!resultResource.equals(metaResource))
254                        {
255                            if (firstWarning.getAndSet(false))
256                            {
257                                logger.error("Packaging of classpath assets has changed in release 5.4; " +
258                                        "Assets should no longer be on the main classpath, " +
259                                        "but should be moved to 'META-INF/assets/' or a sub-folder. Future releases of Tapestry may " +
260                                        "no longer support assets on the main classpath.");
261                            }
262
263                            if (metaResource.getFolder().startsWith(metaRoot))
264                            {
265                                logger.warn(String.format("Classpath asset '/%s' should be moved to folder '/%s/'.",
266                                        resultResource.getPath(),
267                                        metaResource.getFolder()));
268                            } else
269                            {
270                                logger.warn(String.format("Classpath asset '/%s' should be moved under folder '/%s', and the relative path adjusted.",
271                                        resultResource.getPath(),
272                                        metaRoot));
273                            }
274                        }
275
276                        return result;
277                    }
278                }
279
280        );
281    }
282
283    private Asset getComponentAsset(ComponentResources resources, String expandedPath, Resource metaResource)
284    {
285
286        if (expandedPath.contains(":") || expandedPath.startsWith("/"))
287        {
288            return getAssetInLocale(resources.getBaseResource(), expandedPath, resources.getLocale());
289        }
290
291        // So, it's relative to the component.  First, check if there's a match using the 5.4 rules.
292
293        if (metaResource.exists())
294        {
295            return getAssetForResource(metaResource);
296        }
297
298        Resource oldStyle = findLocalizedResource(resources.getBaseResource(), expandedPath, resources.getLocale());
299
300        if (oldStyle == null || !oldStyle.exists())
301        {
302            return null;
303        }
304
305        return getAssetForResource(oldStyle);
306    }
307
308    /**
309     * Figure out the relative path, under /META-INF/assets/ for resources for a given library.
310     * The application library is the blank string and goes directly in /assets/; other libraries
311     * are like virtual folders within /assets/.
312     */
313    private String toPathPrefix(String libraryName)
314    {
315        return libraryName.equals("") ? "" : libraryName + "/";
316    }
317
318    public Asset getUnlocalizedAsset(String path)
319    {
320        return getAssetInLocale(null, path, null);
321    }
322
323    private Asset getAssetInLocale(Resource baseResource, String path, Locale locale)
324    {
325        return getLocalizedAssetFromResource(findResource(baseResource, path), locale);
326    }
327
328    /**
329     * @param baseResource
330     *         the base resource (or null for classpath root) that path will extend from
331     * @param path
332     *         extension path from the base resource
333     * @return the resource, unlocalized, which may not exist (may be for a path with no actual resource)
334     */
335    private Resource findResource(Resource baseResource, String path)
336    {
337        assert path != null;
338        int colonx = path.indexOf(':');
339
340        if (colonx < 0)
341        {
342            Resource root = baseResource != null ? baseResource : prefixToRootResource.get(AssetConstants.CLASSPATH);
343
344            return root.forFile(path);
345        }
346
347        String prefix = path.substring(0, colonx);
348
349        Resource root = prefixToRootResource.get(prefix);
350
351        if (root == null)
352            throw new IllegalArgumentException(String.format("Unknown prefix for asset path '%s'.", path));
353
354        return root.forFile(path.substring(colonx + 1));
355    }
356
357    /**
358     * Finds a localized resource.
359     *
360     * @param baseResource
361     *         base resource, or null for classpath root
362     * @param path
363     *         path from baseResource to expected resource
364     * @param locale
365     *         locale to localize for, or null to not localize
366     * @return resource, which may not exist
367     */
368    private Resource findLocalizedResource(Resource baseResource, String path, Locale locale)
369    {
370        Resource unlocalized = findResource(baseResource, path);
371
372        if (locale == null || !unlocalized.exists())
373        {
374            return unlocalized;
375        }
376
377        return localize(unlocalized, locale);
378    }
379
380    private Resource localize(Resource unlocalized, Locale locale)
381    {
382        Resource localized = unlocalized.forLocale(locale);
383
384        return localized != null ? localized : unlocalized;
385    }
386
387    private Asset getLocalizedAssetFromResource(Resource unlocalized, Locale locale)
388    {
389        final Resource localized;
390        if (locale == null)
391        {
392            localized = unlocalized;
393        } else
394        {
395            Reference<Asset> reference = cache.get(unlocalized);
396            if (reference != null)
397            {
398                Asset asset = reference.get();
399                if (asset != null)
400                {
401                    unlocalized = asset.getResource(); // Prefer resource from cache to use its cache
402                }
403            }
404
405            localized = unlocalized.forLocale(locale);
406        }
407
408        if (localized == null || !localized.exists())
409        {
410            throw new AssetNotFoundException(String.format("Unable to locate asset '%s' (the file does not exist).", unlocalized), unlocalized);
411        }
412
413        return getAssetForResource(localized);
414    }
415
416    private Asset getAssetForResource(Resource resource)
417    {
418        try
419        {
420            acquireReadLock();
421
422            Asset result = TapestryInternalUtils.getAndDeref(cache, resource);
423
424            if (result == null)
425            {
426                result = createAssetFromResource(resource);
427                cache.put(resource, new SoftReference(result));
428            }
429
430            return result;
431        } finally
432        {
433            releaseReadLock();
434        }
435    }
436
437    private Locale defaulted(Locale locale)
438    {
439        return locale != null ? locale : threadLocale.getLocale();
440    }
441
442    private Asset createAssetFromResource(Resource resource)
443    {
444        // The class of the resource is derived from the class of the base resource.
445        // So we can then use the class of the resource as a key to locate the correct asset
446        // factory.
447
448        try
449        {
450            upgradeReadLockToWriteLock();
451
452            // Check for competing thread beat us to it (not very likely!):
453
454            Asset result = TapestryInternalUtils.getAndDeref(cache, resource);
455
456            if (result != null)
457            {
458                return result;
459            }
460
461            Class resourceClass = resource.getClass();
462
463            AssetFactory factory = registry.get(resourceClass);
464
465            return factory.createAsset(resource);
466        } finally
467        {
468            downgradeWriteLockToReadLock();
469        }
470    }
471}