| // 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; |
| } |
| } |