    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}