    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}