    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}