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