    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}