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