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