blob: d47b7a3e07af519232043af54f5f0d0f3b029494 [file] [log] [blame]
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package org.apache.tapestry5.ioc.internal.util;
import org.apache.tapestry5.ioc.Resource;
import org.apache.tapestry5.ioc.util.LocalizedNameGenerator;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.List;
import java.util.Locale;
/**
* Abstract implementation of {@link Resource}. Subclasses must implement the abstract methods {@link Resource#toURL()}
* and {@link #newResource(String)} as well as toString(), hashCode() and equals().
*/
public abstract class AbstractResource extends LockSupport implements Resource
{
private static class Localization
{
final Locale locale;
final Resource resource;
final Localization next;
private Localization(Locale locale, Resource resource, Localization next)
{
this.locale = locale;
this.resource = resource;
this.next = next;
}
}
private final String path;
// Guarded by Lock
private boolean exists, existsComputed;
// Guarded by lock
private Localization firstLocalization;
protected AbstractResource(String path)
{
assert path != null;
// Normalize paths to NOT start with a leading slash
this.path = path.startsWith("/") ? path.substring(1) : path;
}
@Override
public final String getPath()
{
return path;
}
@Override
public final String getFile()
{
return extractFile(path);
}
private static String extractFile(String path)
{
int slashx = path.lastIndexOf('/');
return path.substring(slashx + 1);
}
@Override
public final String getFolder()
{
int slashx = path.lastIndexOf('/');
return (slashx < 0) ? "" : path.substring(0, slashx);
}
@Override
public final Resource forFile(String relativePath)
{
assert relativePath != null;
List<String> terms = CollectionFactory.newList();
for (String term : getFolder().split("/"))
{
terms.add(term);
}
// Handling systems using backslash as the path separator, such as Windows
relativePath = relativePath.replace('\\', '/');
for (String term : relativePath.split("/"))
{
// This will occur if the relative path contains sequential slashes
if (term.equals("") || term.equals("."))
{
continue;
}
if (term.equals(".."))
{
if (terms.isEmpty())
{
throw new IllegalStateException(String.format("Relative path '%s' for %s would go above root.", relativePath, this));
}
terms.remove(terms.size() - 1);
continue;
}
// TODO: term blank or otherwise invalid?
// TODO: final term should not be "." or "..", or for that matter, the
// name of a folder, since a Resource should be a file within
// a folder.
terms.add(term);
}
StringBuilder path = new StringBuilder(100);
String sep = "";
for (String term : terms)
{
path.append(sep).append(term);
sep = "/";
}
return createResource(path.toString());
}
@Override
public final Resource forLocale(Locale locale)
{
try
{
acquireReadLock();
for (Localization l = firstLocalization; l != null; l = l.next)
{
if (l.locale.equals(locale))
{
return l.resource;
}
}
return populateLocalizationCache(locale);
} finally
{
releaseReadLock();
}
}
private Resource populateLocalizationCache(Locale locale)
{
try
{
upgradeReadLockToWriteLock();
// Race condition: another thread may have beaten us to it:
for (Localization l = firstLocalization; l != null; l = l.next)
{
if (l.locale.equals(locale))
{
return l.resource;
}
}
Resource result = findLocalizedResource(locale);
firstLocalization = new Localization(locale, result, firstLocalization);
return result;
} finally
{
downgradeWriteLockToReadLock();
}
}
private Resource findLocalizedResource(Locale locale)
{
for (String path : new LocalizedNameGenerator(this.path, locale))
{
Resource potential = createResource(path);
if (potential.exists())
return potential;
}
return null;
}
@Override
public final Resource withExtension(String extension)
{
assert InternalUtils.isNonBlank(extension);
int dotx = path.lastIndexOf('.');
if (dotx < 0)
return createResource(path + "." + extension);
return createResource(path.substring(0, dotx + 1) + extension);
}
/**
* Creates a new resource, unless the path matches the current Resource's path (in which case, this resource is
* returned).
*/
private Resource createResource(String path)
{
if (this.path.equals(path))
return this;
return newResource(path);
}
/**
* Simple check for whether {@link #toURL()} returns null or not.
*/
@Override
public boolean exists()
{
try
{
acquireReadLock();
if (!existsComputed)
{
computeExists();
}
return exists;
} finally
{
releaseReadLock();
}
}
private void computeExists()
{
try
{
upgradeReadLockToWriteLock();
if (!existsComputed)
{
exists = toURL() != null;
existsComputed = true;
}
} finally
{
downgradeWriteLockToReadLock();
}
}
/**
* Obtains the URL for the Resource and opens the stream, wrapped by a BufferedInputStream.
*/
@Override
public InputStream openStream() throws IOException
{
URL url = toURL();
if (url == null)
{
return null;
}
if ("jar".equals(url.getProtocol())){
// TAP5-2448: make sure that the URL does not reference a directory
String urlAsString = url.toString();
int indexOfExclamationMark = urlAsString.indexOf('!');
String resourceInJar = urlAsString.substring(indexOfExclamationMark + 2);
URL directoryResource = Thread.currentThread().getContextClassLoader().getResource(resourceInJar + "/");
boolean isDirectory = directoryResource != null && "jar".equals(directoryResource.getProtocol());
if (isDirectory)
{
throw new IOException("Cannot open a stream for a resource that references a directory inside a JAR file (" + url + ").");
}
}
return new BufferedInputStream(url.openStream());
}
/**
* Factory method provided by subclasses.
*/
protected abstract Resource newResource(String path);
/**
* Validates that the URL is correct; at this time, a correct URL is one of:
* <ul><li>null</li>
* <li>a non-file: URL</li>
* <li>a file: URL where the case of the file matches the corresponding path element</li>
* </ul>
* See <a href="https://issues.apache.org/jira/browse/TAP5-1007">TAP5-1007</a>
*
* @param url
* to validate
* @since 5.4
*/
protected void validateURL(URL url)
{
if (url == null)
{
return;
}
// Don't have to be concerned with the ClasspathURLConverter since this is intended as a
// runtime check during development; it's about ensuring that what works in development on
// a case-insensitive file system will work in production on the classpath (or other case sensitive
// file system).
if (!url.getProtocol().equals("file"))
{
return;
}
File file = toFile(url);
String expectedFileName = null;
try
{
// On Windows, the canonical path uses backslash ('\') for the separator; an easy hack
// is to convert the platform file separator to match sane operating systems (which use a foward slash).
String sep = System.getProperty("file.separator");
expectedFileName = extractFile(file.getCanonicalPath().replace(sep, "/"));
} catch (IOException e)
{
return;
}
String actualFileName = getFile();
if (actualFileName.equals(expectedFileName))
{
return;
}
throw new IllegalStateException(String.format("Resource %s does not match the case of the actual file name, '%s'.",
this, expectedFileName));
}
private File toFile(URL url)
{
try
{
return new File(url.toURI());
} catch (URISyntaxException ex)
{
return new File(url.getPath());
}
}
@Override
public boolean isVirtual()
{
return false;
}
}