    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}