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.ioc.internal.util;
014
015import org.apache.tapestry5.ioc.Resource;
016import org.apache.tapestry5.ioc.util.LocalizedNameGenerator;
017
018import java.io.BufferedInputStream;
019import java.io.File;
020import java.io.IOException;
021import java.io.InputStream;
022import java.net.URISyntaxException;
023import java.net.URL;
024import java.util.List;
025import java.util.Locale;
026
027/**
028 * Abstract implementation of {@link Resource}. Subclasses must implement the abstract methods {@link Resource#toURL()}
029 * and {@link #newResource(String)} as well as toString(), hashCode() and equals().
030 */
031public abstract class AbstractResource extends LockSupport implements Resource
032{
033    private static class Localization
034    {
035        final Locale locale;
036
037        final Resource resource;
038
039        final Localization next;
040
041        private Localization(Locale locale, Resource resource, Localization next)
042        {
043            this.locale = locale;
044            this.resource = resource;
045            this.next = next;
046        }
047    }
048
049    private final String path;
050
051    // Guarded by Lock
052    private boolean exists, existsComputed;
053
054    // Guarded by lock
055    private Localization firstLocalization;
056
057    protected AbstractResource(String path)
058    {
059        assert path != null;
060
061        // Normalize paths to NOT start with a leading slash
062        this.path = path.startsWith("/") ? path.substring(1) : path;
063    }
064
065    @Override
066    public final String getPath()
067    {
068        return path;
069    }
070
071    @Override
072    public final String getFile()
073    {
074        return extractFile(path);
075    }
076
077    private static String extractFile(String path)
078    {
079        int slashx = path.lastIndexOf('/');
080
081        return path.substring(slashx + 1);
082    }
083
084    @Override
085    public final String getFolder()
086    {
087        int slashx = path.lastIndexOf('/');
088
089        return (slashx < 0) ? "" : path.substring(0, slashx);
090    }
091
092    @Override
093    public final Resource forFile(String relativePath)
094    {
095        assert relativePath != null;
096
097        List<String> terms = CollectionFactory.newList();
098
099        for (String term : getFolder().split("/"))
100        {
101            terms.add(term);
102        }
103
104        for (String term : relativePath.split("/"))
105        {
106            // This will occur if the relative path contains sequential slashes
107
108            if (term.equals("") || term.equals("."))
109            {
110                continue;
111            }
112
113            if (term.equals(".."))
114            {
115                if (terms.isEmpty())
116                {
117                    throw new IllegalStateException(String.format("Relative path '%s' for %s would go above root.", relativePath, this));
118                }
119
120                terms.remove(terms.size() - 1);
121
122                continue;
123            }
124
125            // TODO: term blank or otherwise invalid?
126            // TODO: final term should not be "." or "..", or for that matter, the
127            // name of a folder, since a Resource should be a file within
128            // a folder.
129
130            terms.add(term);
131        }
132
133        StringBuilder path = new StringBuilder(100);
134        String sep = "";
135
136        for (String term : terms)
137        {
138            path.append(sep).append(term);
139            sep = "/";
140        }
141
142        return createResource(path.toString());
143    }
144
145    @Override
146    public final Resource forLocale(Locale locale)
147    {
148        try
149        {
150            acquireReadLock();
151
152            for (Localization l = firstLocalization; l != null; l = l.next)
153            {
154                if (l.locale.equals(locale))
155                {
156                    return l.resource;
157                }
158            }
159
160            return populateLocalizationCache(locale);
161        } finally
162        {
163            releaseReadLock();
164        }
165    }
166
167    private Resource populateLocalizationCache(Locale locale)
168    {
169        try
170        {
171            upgradeReadLockToWriteLock();
172
173            // Race condition: another thread may have beaten us to it:
174
175            for (Localization l = firstLocalization; l != null; l = l.next)
176            {
177                if (l.locale.equals(locale))
178                {
179                    return l.resource;
180                }
181            }
182
183            Resource result = findLocalizedResource(locale);
184
185            firstLocalization = new Localization(locale, result, firstLocalization);
186
187            return result;
188
189        } finally
190        {
191            downgradeWriteLockToReadLock();
192        }
193    }
194
195    private Resource findLocalizedResource(Locale locale)
196    {
197        for (String path : new LocalizedNameGenerator(this.path, locale))
198        {
199            Resource potential = createResource(path);
200
201            if (potential.exists())
202                return potential;
203        }
204
205        return null;
206    }
207
208    @Override
209    public final Resource withExtension(String extension)
210    {
211        assert InternalUtils.isNonBlank(extension);
212        int dotx = path.lastIndexOf('.');
213
214        if (dotx < 0)
215            return createResource(path + "." + extension);
216
217        return createResource(path.substring(0, dotx + 1) + extension);
218    }
219
220    /**
221     * Creates a new resource, unless the path matches the current Resource's path (in which case, this resource is
222     * returned).
223     */
224    private Resource createResource(String path)
225    {
226        if (this.path.equals(path))
227            return this;
228
229        return newResource(path);
230    }
231
232    /**
233     * Simple check for whether {@link #toURL()} returns null or not.
234     */
235    @Override
236    public boolean exists()
237    {
238        try
239        {
240            acquireReadLock();
241
242            if (!existsComputed)
243            {
244                computeExists();
245            }
246
247            return exists;
248        } finally
249        {
250            releaseReadLock();
251        }
252    }
253
254    private void computeExists()
255    {
256        try
257        {
258            upgradeReadLockToWriteLock();
259
260            if (!existsComputed)
261            {
262                exists = toURL() != null;
263                existsComputed = true;
264            }
265        } finally
266        {
267            downgradeWriteLockToReadLock();
268        }
269    }
270
271    /**
272     * Obtains the URL for the Resource and opens the stream, wrapped by a BufferedInputStream.
273     */
274    @Override
275    public InputStream openStream() throws IOException
276    {
277        URL url = toURL();
278
279        if (url == null)
280        {
281            return null;
282        }
283        if ("jar".equals(url.getProtocol())){
284
285
286            // TAP5-2448: make sure that the URL does not reference a directory
287            String urlAsString = url.toString();
288
289            int indexOfExclamationMark = urlAsString.indexOf('!');
290
291            String resourceInJar = urlAsString.substring(indexOfExclamationMark + 2);
292
293            URL directoryResource = Thread.currentThread().getContextClassLoader().getResource(resourceInJar + "/");
294
295            boolean isDirectory = directoryResource != null && "jar".equals(directoryResource.getProtocol());
296
297            if (isDirectory)
298            {
299                throw new IOException("Cannot open a stream for a resource that references a directory inside a JAR file (" + url + ").");
300            }
301            
302        }
303
304        return new BufferedInputStream(url.openStream());
305    }
306
307    /**
308     * Factory method provided by subclasses.
309     */
310    protected abstract Resource newResource(String path);
311
312    /**
313     * Validates that the URL is correct; at this time, a correct URL is one of:
314     * <ul><li>null</li>
315     * <li>a non-file: URL</li>
316     * <li>a file: URL where the case of the file matches the corresponding path element</li>
317     * </ul>
318     * See <a href="https://issues.apache.org/jira/browse/TAP5-1007">TAP5-1007</a>
319     *
320     * @param url
321     *         to validate
322     * @since 5.4
323     */
324    protected void validateURL(URL url)
325    {
326        if (url == null)
327        {
328            return;
329        }
330
331        // Don't have to be concerned with the  ClasspathURLConverter since this is intended as a
332        // runtime check during development; it's about ensuring that what works in development on
333        // a case-insensitive file system will work in production on the classpath (or other case sensitive
334        // file system).
335
336        if (!url.getProtocol().equals("file"))
337        {
338            return;
339        }
340
341        File file = toFile(url);
342
343        String expectedFileName = null;
344
345        try
346        {
347            // On Windows, the canonical path uses backslash ('\') for the separator; an easy hack
348            // is to convert the platform file separator to match sane operating systems (which use a foward slash).
349            String sep = System.getProperty("file.separator");
350            expectedFileName = extractFile(file.getCanonicalPath().replace(sep, "/"));
351        } catch (IOException e)
352        {
353            return;
354        }
355
356        String actualFileName = getFile();
357
358        if (actualFileName.equals(expectedFileName))
359        {
360            return;
361        }
362
363        throw new IllegalStateException(String.format("Resource %s does not match the case of the actual file name, '%s'.",
364                this, expectedFileName));
365
366    }
367
368    private File toFile(URL url)
369    {
370        try
371        {
372            return new File(url.toURI());
373        } catch (URISyntaxException ex)
374        {
375            return new File(url.getPath());
376        }
377    }
378
379    @Override
380    public boolean isVirtual()
381    {
382        return false;
383    }
384}