    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.SoftReference;
    036import java.net.MalformedURLException;
    037import java.net.URL;
    038import java.util.Arrays;
    039import java.util.List;
    040import java.util.Locale;
    041import java.util.Map;
    042import java.util.WeakHashMap;
    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        ),
    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                                }
    177                                else
    178                                {
    179                                    url = expanded;
    180                                }
    181
    182                                try
    183                                {
    184                                    UrlResource resource = new UrlResource(new URL(url));
    185                                    return new UrlAsset(url, resource);
    186                                }
    187                                catch (MalformedURLException e)
    188                                {
    189                                    throw new RuntimeException(e);
    190                                }
    191                            }
    192                            else
    193                            {
    194                                return getAssetInLocale(resources.getBaseResource(), expanded, resources.getLocale());
    195                            }
    196                        }
    197
    198                        // No prefix, so implicitly classpath:, or explicitly classpath:
    199
    200                        String restOfPath = expanded.substring(dotx + 1);
    201
    202                        // This is tricky, because a relative path (including "../") is ok in 5.3, since its just somewhere
    203                        // else on the classpath (though you can "stray" out of the "safe" zone).  In 5.4, under /META-INF/assets/
    204                        // it's possible to "stray" out beyond the safe zone more easily, into parts of the classpath that can't be
    205                        // represented in the URL.
    206
    207                        // Ends with trailing slash:
    208                        String metaRoot = "META-INF/assets/" + toPathPrefix(libraryName);
    209
    210                        String trimmedRestOfPath = restOfPath.startsWith("/") ? restOfPath.substring(1) : restOfPath;
    211
    212
    213                        // TAP5-2044: Some components specify a full path, starting with META-INF/assets/, and we should just trust them.
    214                        // The warning logic below is for compnents that specify a relative path. Our bad decisions come back to haunt us;
    215                        // Resource paths should always had a leading slash to differentiate relative from complete.
    216                        String metaPath = trimmedRestOfPath.startsWith("META-INF/assets/") ? trimmedRestOfPath : metaRoot + trimmedRestOfPath;
    217
    218                        // Based on the path, metaResource is where it should exist in a 5.4 and beyond world ... unless the expanded
    219                        // path was a bit too full of ../ sequences, in which case the expanded path is not valid and we adjust the
    220                        // error we write.
    221
    222                        Resource metaResource = findLocalizedResource(null, metaPath, resources.getLocale());
    223
    224                        Asset result = getComponentAsset(resources, expanded, metaResource);
    225
    226                        if (result == null)
    227                        {
    228                            throw new RuntimeException(String.format("Unable to locate asset '%s' for component %s. It should be located at %s.",
    229                                    path, resources.getCompleteId(),
    230                                    metaPath));
    231                        }
    232
    233                        // This is the best way to tell if the result is an asset for a Classpath resource.
    234
    235                        Resource resultResource = result.getResource();
    236
    237                        if (!resultResource.equals(metaResource))
    238                        {
    239                            if (firstWarning.getAndSet(false))
    240                            {
    241                                logger.error("Packaging of classpath assets has changed in release 5.4; " +
    242                                        "Assets should no longer be on the main classpath, " +
    243                                        "but should be moved to 'META-INF/assets/' or a sub-folder. Future releases of Tapestry may " +
    244                                        "no longer support assets on the main classpath.");
    245                            }
    246
    247                            if (metaResource.getFolder().startsWith(metaRoot))
    248                            {
    249                                logger.warn(String.format("Classpath asset '/%s' should be moved to folder '/%s/'.",
    250                                        resultResource.getPath(),
    251                                        metaResource.getFolder()));
    252                            } else
    253                            {
    254                                logger.warn(String.format("Classpath asset '/%s' should be moved under folder '/%s', and the relative path adjusted.",
    255                                        resultResource.getPath(),
    256                                        metaRoot));
    257                            }
    258                        }
    259
    260                        return result;
    261                    }
    262                }
    263
    264        );
    265    }
    266
    267    private Asset getComponentAsset(ComponentResources resources, String expandedPath, Resource metaResource)
    268    {
    269
    270        if (expandedPath.contains(":") || expandedPath.startsWith("/"))
    271        {
    272            return getAssetInLocale(resources.getBaseResource(), expandedPath, resources.getLocale());
    273        }
    274
    275        // So, it's relative to the component.  First, check if there's a match using the 5.4 rules.
    276
    277        if (metaResource.exists())
    278        {
    279            return getAssetForResource(metaResource);
    280        }
    281
    282        Resource oldStyle = findLocalizedResource(resources.getBaseResource(), expandedPath, resources.getLocale());
    283
    284        if (oldStyle == null || !oldStyle.exists())
    285        {
    286            return null;
    287        }
    288
    289        return getAssetForResource(oldStyle);
    290    }
    291
    292    /**
    293     * Figure out the relative path, under /META-INF/assets/ for resources for a given library.
    294     * The application library is the blank string and goes directly in /assets/; other libraries
    295     * are like virtual folders within /assets/.
    296     */
    297    private String toPathPrefix(String libraryName)
    298    {
    299        return libraryName.equals("") ? "" : libraryName + "/";
    300    }
    301
    302    public Asset getUnlocalizedAsset(String path)
    303    {
    304        return getAssetInLocale(null, path, null);
    305    }
    306
    307    private Asset getAssetInLocale(Resource baseResource, String path, Locale locale)
    308    {
    309        return getLocalizedAssetFromResource(findResource(baseResource, path), locale);
    310    }
    311
    312    /**
    313     * @param baseResource
    314     *         the base resource (or null for classpath root) that path will extend from
    315     * @param path
    316     *         extension path from the base resource
    317     * @return the resource, unlocalized, which may not exist (may be for a path with no actual resource)
    318     */
    319    private Resource findResource(Resource baseResource, String path)
    320    {
    321        assert path != null;
    322        int colonx = path.indexOf(':');
    323
    324        if (colonx < 0)
    325        {
    326            Resource root = baseResource != null ? baseResource : prefixToRootResource.get(AssetConstants.CLASSPATH);
    327
    328            return root.forFile(path);
    329        }
    330
    331        String prefix = path.substring(0, colonx);
    332
    333        Resource root = prefixToRootResource.get(prefix);
    334
    335        if (root == null)
    336            throw new IllegalArgumentException(String.format("Unknown prefix for asset path '%s'.", path));
    337
    338        return root.forFile(path.substring(colonx + 1));
    339    }
    340
    341    /**
    342     * Finds a localized resource.
    343     *
    344     * @param baseResource
    345     *         base resource, or null for classpath root
    346     * @param path
    347     *         path from baseResource to expected resource
    348     * @param locale
    349     *         locale to localize for, or null to not localize
    350     * @return resource, which may not exist
    351     */
    352    private Resource findLocalizedResource(Resource baseResource, String path, Locale locale)
    353    {
    354        Resource unlocalized = findResource(baseResource, path);
    355
    356        if (locale == null || !unlocalized.exists())
    357        {
    358            return unlocalized;
    359        }
    360
    361        return localize(unlocalized, locale);
    362    }
    363
    364    private Resource localize(Resource unlocalized, Locale locale)
    365    {
    366        Resource localized = unlocalized.forLocale(locale);
    367
    368        return localized != null ? localized : unlocalized;
    369    }
    370
    371    private Asset getLocalizedAssetFromResource(Resource unlocalized, Locale locale)
    372    {
    373        Resource localized = locale == null ? unlocalized : unlocalized.forLocale(locale);
    374
    375        if (localized == null || !localized.exists())
    376            throw new RuntimeException(String.format("Unable to locate asset '%s' (the file does not exist).", unlocalized));
    377
    378        return getAssetForResource(localized);
    379    }
    380
    381    private Asset getAssetForResource(Resource resource)
    382    {
    383        try
    384        {
    385            acquireReadLock();
    386
    387            Asset result = TapestryInternalUtils.getAndDeref(cache, resource);
    388
    389            if (result == null)
    390            {
    391                result = createAssetFromResource(resource);
    392                cache.put(resource, new SoftReference(result));
    393            }
    394
    395            return result;
    396        } finally
    397        {
    398            releaseReadLock();
    399        }
    400    }
    401
    402    private Locale defaulted(Locale locale)
    403    {
    404        return locale != null ? locale : threadLocale.getLocale();
    405    }
    406
    407    private Asset createAssetFromResource(Resource resource)
    408    {
    409        // The class of the resource is derived from the class of the base resource.
    410        // So we can then use the class of the resource as a key to locate the correct asset
    411        // factory.
    412
    413        try
    414        {
    415            upgradeReadLockToWriteLock();
    416
    417            // Check for competing thread beat us to it (not very likely!):
    418
    419            Asset result = TapestryInternalUtils.getAndDeref(cache, resource);
    420
    421            if (result != null)
    422            {
    423                return result;
    424            }
    425
    426            Class resourceClass = resource.getClass();
    427
    428            AssetFactory factory = registry.get(resourceClass);
    429
    430            return factory.createAsset(resource);
    431        } finally
    432        {
    433            downgradeWriteLockToReadLock();
    434        }
    435    }
    436}