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