001// Copyright 2006-2014 The Apache Software Foundation
002//
003// Licensed under the Apache License, Version 2.0 (the "License");
004// you may not use this file except in compliance with the License.
005// You may obtain a copy of the License at
006//
007// http://www.apache.org/licenses/LICENSE-2.0
008//
009// Unless required by applicable law or agreed to in writing, software
010// distributed under the License is distributed on an "AS IS" BASIS,
011// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
012// See the License for the specific language governing permissions and
013// limitations under the License.
014
015package org.apache.tapestry5.internal.services;
016
017import org.apache.tapestry5.Asset;
018import org.apache.tapestry5.ComponentResources;
019import org.apache.tapestry5.internal.AssetConstants;
020import org.apache.tapestry5.internal.TapestryInternalUtils;
021import org.apache.tapestry5.internal.services.assets.ResourceChangeTracker;
022import org.apache.tapestry5.ioc.Invokable;
023import org.apache.tapestry5.ioc.OperationTracker;
024import org.apache.tapestry5.ioc.Resource;
025import org.apache.tapestry5.ioc.annotations.PostInjection;
026import org.apache.tapestry5.ioc.internal.util.CollectionFactory;
027import org.apache.tapestry5.ioc.internal.util.InternalUtils;
028import org.apache.tapestry5.ioc.internal.util.LockSupport;
029import org.apache.tapestry5.ioc.services.SymbolSource;
030import org.apache.tapestry5.ioc.services.ThreadLocale;
031import org.apache.tapestry5.ioc.util.StrategyRegistry;
032import org.apache.tapestry5.services.AssetFactory;
033import org.apache.tapestry5.services.AssetSource;
034import org.slf4j.Logger;
035
036import java.lang.ref.SoftReference;
037import java.util.Locale;
038import java.util.Map;
039import java.util.WeakHashMap;
040import java.util.concurrent.atomic.AtomicBoolean;
041
042@SuppressWarnings("all")
043public class AssetSourceImpl extends LockSupport implements AssetSource
044{
045    private final StrategyRegistry<AssetFactory> registry;
046
047    private final ThreadLocale threadLocale;
048
049    private final Map<String, Resource> prefixToRootResource = CollectionFactory.newMap();
050
051    private final Map<Resource, SoftReference<Asset>> cache = new WeakHashMap<Resource, SoftReference<Asset>>();
052
053    private final SymbolSource symbolSource;
054
055    private final Logger logger;
056
057    private final AtomicBoolean firstWarning = new AtomicBoolean(true);
058
059    private final OperationTracker tracker;
060
061    public AssetSourceImpl(ThreadLocale threadLocale,
062
063                           Map<String, AssetFactory> configuration, SymbolSource symbolSource, Logger logger, OperationTracker tracker)
064    {
065        this.threadLocale = threadLocale;
066        this.symbolSource = symbolSource;
067        this.logger = logger;
068        this.tracker = tracker;
069
070        Map<Class, AssetFactory> byResourceClass = CollectionFactory.newMap();
071
072        for (Map.Entry<String, AssetFactory> e : configuration.entrySet())
073        {
074            String prefix = e.getKey();
075            AssetFactory factory = e.getValue();
076
077            Resource rootResource = factory.getRootResource();
078
079            byResourceClass.put(rootResource.getClass(), factory);
080
081            prefixToRootResource.put(prefix, rootResource);
082        }
083
084        registry = StrategyRegistry.newInstance(AssetFactory.class, byResourceClass);
085    }
086
087    @PostInjection
088    public void clearCacheWhenResourcesChange(ResourceChangeTracker tracker)
089    {
090        tracker.clearOnInvalidation(cache);
091    }
092
093    public Asset getClasspathAsset(String path)
094    {
095        return getClasspathAsset(path, null);
096    }
097
098    public Asset getClasspathAsset(String path, Locale locale)
099    {
100        return getAsset(null, path, locale);
101    }
102
103    public Asset getContextAsset(String path, Locale locale)
104    {
105        return getAsset(prefixToRootResource.get(AssetConstants.CONTEXT), path, locale);
106    }
107
108    public Asset getAsset(Resource baseResource, String path, Locale locale)
109    {
110        return getAssetInLocale(baseResource, path, defaulted(locale));
111    }
112
113    public Resource resourceForPath(String path)
114    {
115        return findResource(null, path);
116    }
117
118    public Asset getExpandedAsset(String path)
119    {
120        return getUnlocalizedAsset(symbolSource.expandSymbols(path));
121    }
122
123    public Asset getComponentAsset(final ComponentResources resources, final String path)
124    {
125        assert resources != null;
126
127        assert InternalUtils.isNonBlank(path);
128
129        return tracker.invoke(String.format("Resolving '%s' for component %s", path, resources.getCompleteId()
130        ),
131                new Invokable<Asset>()
132                {
133                    public Asset invoke()
134                    {
135                        // First, expand symbols:
136
137                        String expanded = symbolSource.expandSymbols(path);
138
139                        int dotx = expanded.indexOf(':');
140
141                        // We special case the hell out of 'classpath:' so that we can provide warnings today (5.4) and
142                        // blow up in a useful fashion tomorrow (5.5).
143
144                        if (dotx > 0 && !expanded.substring(0, dotx).equalsIgnoreCase(AssetConstants.CLASSPATH))
145                        {
146                            return getAssetInLocale(resources.getBaseResource(), expanded, resources.getLocale());
147                        }
148
149                        // No prefix, so implicitly classpath:, or explicitly classpath:
150
151                        String restOfPath = expanded.substring(dotx + 1);
152
153                        // This is tricky, because a relative path (including "../") is ok in 5.3, since its just somewhere
154                        // else on the classpath (though you can "stray" out of the "safe" zone).  In 5.4, under /META-INF/assets/
155                        // it's possible to "stray" out beyond the safe zone more easily, into parts of the classpath that can't be
156                        // represented in the URL.
157
158                        // Ends with trailing slash:
159                        String metaRoot = "META-INF/assets/" + toPathPrefix(resources.getComponentModel().getLibraryName());
160
161                        String trimmedRestOfPath = restOfPath.startsWith("/") ? restOfPath.substring(1) : restOfPath;
162
163
164                        // TAP5-2044: Some components specify a full path, starting with META-INF/assets/, and we should just trust them.
165                        // The warning logic below is for compnents that specify a relative path. Our bad decisions come back to haunt us;
166                        // Resource paths should always had a leading slash to differentiate relative from complete.
167                        String metaPath = trimmedRestOfPath.startsWith("META-INF/assets/") ? trimmedRestOfPath : metaRoot + trimmedRestOfPath;
168
169                        // Based on the path, metaResource is where it should exist in a 5.4 and beyond world ... unless the expanded
170                        // path was a bit too full of ../ sequences, in which case the expanded path is not valid and we adjust the
171                        // error we write.
172
173                        Resource metaResource = findLocalizedResource(null, metaPath, resources.getLocale());
174
175                        Asset result = getComponentAsset(resources, expanded, metaResource);
176
177                        if (result == null)
178                        {
179                            throw new RuntimeException(String.format("Unable to locate asset '%s' for component %s. It should be located at %s.",
180                                    path, resources.getCompleteId(),
181                                    metaPath));
182                        }
183
184                        // This is the best way to tell if the result is an asset for a Classpath resource.
185
186                        Resource resultResource = result.getResource();
187
188                        if (!resultResource.equals(metaResource))
189                        {
190                            if (firstWarning.getAndSet(false))
191                            {
192                                logger.error("Packaging of classpath assets has changed in release 5.4; " +
193                                        "Assets should no longer be on the main classpath, " +
194                                        "but should be moved to 'META-INF/assets/' or a sub-folder. Future releases of Tapestry may " +
195                                        "no longer support assets on the main classpath.");
196                            }
197
198                            if (metaResource.getFolder().startsWith(metaRoot))
199                            {
200                                logger.warn(String.format("Classpath asset '/%s' should be moved to folder '/%s/'.",
201                                        resultResource.getPath(),
202                                        metaResource.getFolder()));
203                            } else
204                            {
205                                logger.warn(String.format("Classpath asset '/%s' should be moved under folder '/%s', and the relative path adjusted.",
206                                        resultResource.getPath(),
207                                        metaRoot));
208                            }
209                        }
210
211                        return result;
212                    }
213                }
214
215        );
216    }
217
218    private Asset getComponentAsset(ComponentResources resources, String expandedPath, Resource metaResource)
219    {
220
221        if (expandedPath.contains(":") || expandedPath.startsWith("/"))
222        {
223            return getAssetInLocale(resources.getBaseResource(), expandedPath, resources.getLocale());
224        }
225
226        // So, it's relative to the component.  First, check if there's a match using the 5.4 rules.
227
228        if (metaResource.exists())
229        {
230            return getAssetForResource(metaResource);
231        }
232
233        Resource oldStyle = findLocalizedResource(resources.getBaseResource(), expandedPath, resources.getLocale());
234
235        if (oldStyle == null || !oldStyle.exists())
236        {
237            return null;
238        }
239
240        return getAssetForResource(oldStyle);
241    }
242
243    /**
244     * Figure out the relative path, under /META-INF/assets/ for resources for a given library.
245     * The application library is the blank string and goes directly in /assets/; other libraries
246     * are like virtual folders within /assets/.
247     */
248    private String toPathPrefix(String libraryName)
249    {
250        return libraryName.equals("") ? "" : libraryName + "/";
251    }
252
253    public Asset getUnlocalizedAsset(String path)
254    {
255        return getAssetInLocale(null, path, null);
256    }
257
258    private Asset getAssetInLocale(Resource baseResource, String path, Locale locale)
259    {
260        return getLocalizedAssetFromResource(findResource(baseResource, path), locale);
261    }
262
263    /**
264     * @param baseResource
265     *         the base resource (or null for classpath root) that path will extend from
266     * @param path
267     *         extension path from the base resource
268     * @return the resource, unlocalized, which may not exist (may be for a path with no actual resource)
269     */
270    private Resource findResource(Resource baseResource, String path)
271    {
272        assert path != null;
273        int colonx = path.indexOf(':');
274
275        if (colonx < 0)
276        {
277            Resource root = baseResource != null ? baseResource : prefixToRootResource.get(AssetConstants.CLASSPATH);
278
279            return root.forFile(path);
280        }
281
282        String prefix = path.substring(0, colonx);
283
284        Resource root = prefixToRootResource.get(prefix);
285
286        if (root == null)
287            throw new IllegalArgumentException(String.format("Unknown prefix for asset path '%s'.", path));
288
289        return root.forFile(path.substring(colonx + 1));
290    }
291
292    /**
293     * Finds a localized resource.
294     *
295     * @param baseResource
296     *         base resource, or null for classpath root
297     * @param path
298     *         path from baseResource to expected resource
299     * @param locale
300     *         locale to localize for, or null to not localize
301     * @return resource, which may not exist
302     */
303    private Resource findLocalizedResource(Resource baseResource, String path, Locale locale)
304    {
305        Resource unlocalized = findResource(baseResource, path);
306
307        if (locale == null || !unlocalized.exists())
308        {
309            return unlocalized;
310        }
311
312        return localize(unlocalized, locale);
313    }
314
315    private Resource localize(Resource unlocalized, Locale locale)
316    {
317        Resource localized = unlocalized.forLocale(locale);
318
319        return localized != null ? localized : unlocalized;
320    }
321
322    private Asset getLocalizedAssetFromResource(Resource unlocalized, Locale locale)
323    {
324        Resource localized = locale == null ? unlocalized : unlocalized.forLocale(locale);
325
326        if (localized == null || !localized.exists())
327            throw new RuntimeException(String.format("Unable to locate asset '%s' (the file does not exist).", unlocalized));
328
329        return getAssetForResource(localized);
330    }
331
332    private Asset getAssetForResource(Resource resource)
333    {
334        try
335        {
336            acquireReadLock();
337
338            Asset result = TapestryInternalUtils.getAndDeref(cache, resource);
339
340            if (result == null)
341            {
342                result = createAssetFromResource(resource);
343                cache.put(resource, new SoftReference(result));
344            }
345
346            return result;
347        } finally
348        {
349            releaseReadLock();
350        }
351    }
352
353    private Locale defaulted(Locale locale)
354    {
355        return locale != null ? locale : threadLocale.getLocale();
356    }
357
358    private Asset createAssetFromResource(Resource resource)
359    {
360        // The class of the resource is derived from the class of the base resource.
361        // So we can then use the class of the resource as a key to locate the correct asset
362        // factory.
363
364        try
365        {
366            upgradeReadLockToWriteLock();
367
368            // Check for competing thread beat us to it (not very likely!):
369
370            Asset result = TapestryInternalUtils.getAndDeref(cache, resource);
371
372            if (result != null)
373            {
374                return result;
375            }
376
377            Class resourceClass = resource.getClass();
378
379            AssetFactory factory = registry.get(resourceClass);
380
381            return factory.createAsset(resource);
382        } finally
383        {
384            downgradeWriteLockToReadLock();
385        }
386    }
387}