| // |
| // ======================================================================== |
| // Copyright (c) 1995-2017 Mort Bay Consulting Pty. Ltd. |
| // ------------------------------------------------------------------------ |
| // All rights reserved. This program and the accompanying materials |
| // are made available under the terms of the Eclipse Public License v1.0 |
| // and Apache License v2.0 which accompanies this distribution. |
| // |
| // The Eclipse Public License is available at |
| // http://www.eclipse.org/legal/epl-v10.html |
| // |
| // The Apache License v2.0 is available at |
| // http://www.opensource.org/licenses/apache2.0.php |
| // |
| // You may elect to redistribute this code under either of these licenses. |
| // ======================================================================== |
| // |
| |
| package org.eclipse.jetty.util.resource; |
| |
| import java.io.File; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.net.MalformedURLException; |
| import java.net.URI; |
| import java.net.URISyntaxException; |
| import java.net.URL; |
| import java.nio.channels.FileChannel; |
| import java.nio.channels.ReadableByteChannel; |
| import java.nio.file.DirectoryIteratorException; |
| import java.nio.file.DirectoryStream; |
| import java.nio.file.Files; |
| import java.nio.file.InvalidPathException; |
| import java.nio.file.LinkOption; |
| import java.nio.file.Path; |
| import java.nio.file.Paths; |
| import java.nio.file.StandardOpenOption; |
| import java.nio.file.attribute.FileTime; |
| import java.util.ArrayList; |
| import java.util.List; |
| |
| import org.eclipse.jetty.util.IO; |
| import org.eclipse.jetty.util.StringUtil; |
| import org.eclipse.jetty.util.URIUtil; |
| import org.eclipse.jetty.util.log.Log; |
| import org.eclipse.jetty.util.log.Logger; |
| |
| /** |
| * Java NIO Path equivalent of FileResource. |
| */ |
| public class PathResource extends Resource |
| { |
| private static final Logger LOG = Log.getLogger(PathResource.class); |
| private final static LinkOption NO_FOLLOW_LINKS[] = new LinkOption[] { LinkOption.NOFOLLOW_LINKS }; |
| private final static LinkOption FOLLOW_LINKS[] = new LinkOption[] {}; |
| |
| private final Path path; |
| private final Path alias; |
| private final URI uri; |
| |
| private final Path checkAliasPath() |
| { |
| Path abs = path; |
| |
| /* Catch situation where the Path class has already normalized |
| * the URI eg. input path "aa./foo.txt" |
| * from an #addPath(String) is normalized away during |
| * the creation of a Path object reference. |
| * If the URI is different then the Path.toUri() then |
| * we will just use the original URI to construct the |
| * alias reference Path. |
| */ |
| |
| if(!URIUtil.equalsIgnoreEncodings(uri,path.toUri())) |
| { |
| try |
| { |
| return Paths.get(uri).toRealPath(FOLLOW_LINKS); |
| } |
| catch (IOException ignored) |
| { |
| // If the toRealPath() call fails, then let |
| // the alias checking routines continue on |
| // to other techniques. |
| LOG.ignore(ignored); |
| } |
| } |
| |
| if (!abs.isAbsolute()) |
| { |
| abs = path.toAbsolutePath(); |
| } |
| |
| try |
| { |
| if (Files.isSymbolicLink(path)) |
| return path.getParent().resolve(Files.readSymbolicLink(path)); |
| if (Files.exists(path)) |
| { |
| Path real = abs.toRealPath(FOLLOW_LINKS); |
| |
| /* |
| * If the real path is not the same as the absolute path |
| * then we know that the real path is the alias for the |
| * provided path. |
| * |
| * For OS's that are case insensitive, this should |
| * return the real (on-disk / case correct) version |
| * of the path. |
| * |
| * We have to be careful on Windows and OSX. |
| * |
| * Assume we have the following scenario |
| * Path a = new File("foo").toPath(); |
| * Files.createFile(a); |
| * Path b = new File("FOO").toPath(); |
| * |
| * There now exists a file called "foo" on disk. |
| * Using Windows or OSX, with a Path reference of |
| * "FOO", "Foo", "fOO", etc.. means the following |
| * |
| * | OSX | Windows | Linux |
| * -----------------------+---------+------------+--------- |
| * Files.exists(a) | True | True | True |
| * Files.exists(b) | True | True | False |
| * Files.isSameFile(a,b) | True | True | False |
| * a.equals(b) | False | True | False |
| * |
| * See the javadoc for Path.equals() for details about this FileSystem |
| * behavior difference |
| * |
| * We also cannot rely on a.compareTo(b) as this is roughly equivalent |
| * in implementation to a.equals(b) |
| */ |
| |
| int absCount = abs.getNameCount(); |
| int realCount = real.getNameCount(); |
| if (absCount != realCount) |
| { |
| // different number of segments |
| return real; |
| } |
| |
| // compare each segment of path, backwards |
| for (int i = realCount-1; i >= 0; i--) |
| { |
| if (!abs.getName(i).toString().equals(real.getName(i).toString())) |
| { |
| return real; |
| } |
| } |
| } |
| } |
| catch (IOException e) |
| { |
| LOG.ignore(e); |
| } |
| catch (Exception e) |
| { |
| LOG.warn("bad alias ({} {}) for {}", e.getClass().getName(), e.getMessage(),path); |
| } |
| return null; |
| } |
| |
| /** |
| * Construct a new PathResource from a File object. |
| * <p> |
| * An invocation of this convenience constructor of the form. |
| * </p> |
| * <pre> |
| * new PathResource(file); |
| * </pre> |
| * <p> |
| * behaves in exactly the same way as the expression |
| * </p> |
| * <pre> |
| * new PathResource(file.toPath()); |
| * </pre> |
| |
| * @param file the file to use |
| */ |
| public PathResource(File file) |
| { |
| this(file.toPath()); |
| } |
| |
| /** |
| * Construct a new PathResource from a Path object. |
| * |
| * @param path the path to use |
| */ |
| public PathResource(Path path) |
| { |
| this.path = path.toAbsolutePath(); |
| assertValidPath(path); |
| this.uri = this.path.toUri(); |
| this.alias = checkAliasPath(); |
| } |
| |
| /** |
| * Construct a new PathResource from a parent PathResource |
| * and child sub path |
| * |
| * @param parent the parent path resource |
| * @param childPath the child sub path |
| */ |
| private PathResource(PathResource parent, String childPath) throws MalformedURLException |
| { |
| // Calculate the URI and the path separately, so that any aliasing done by |
| // FileSystem.getPath(path,childPath) is visiable as a difference to the URI |
| // obtained via URIUtil.addDecodedPath(uri,childPath) |
| |
| this.path = parent.path.getFileSystem().getPath(parent.path.toString(), childPath); |
| if (isDirectory() &&!childPath.endsWith("/")) |
| childPath+="/"; |
| this.uri = URIUtil.addPath(parent.uri,childPath); |
| this.alias = checkAliasPath(); |
| } |
| |
| /** |
| * Construct a new PathResource from a URI object. |
| * <p> |
| * Must be an absolute URI using the <code>file</code> scheme. |
| * |
| * @param uri the URI to build this PathResource from. |
| * @throws IOException if unable to construct the PathResource from the URI. |
| */ |
| public PathResource(URI uri) throws IOException |
| { |
| if (!uri.isAbsolute()) |
| { |
| throw new IllegalArgumentException("not an absolute uri"); |
| } |
| |
| if (!uri.getScheme().equalsIgnoreCase("file")) |
| { |
| throw new IllegalArgumentException("not file: scheme"); |
| } |
| |
| Path path; |
| try |
| { |
| path = Paths.get(uri); |
| } |
| catch (InvalidPathException e) |
| { |
| throw e; |
| } |
| catch (IllegalArgumentException e) |
| { |
| throw e; |
| } |
| catch (Exception e) |
| { |
| LOG.ignore(e); |
| throw new IOException("Unable to build Path from: " + uri,e); |
| } |
| |
| this.path = path.toAbsolutePath(); |
| this.uri = path.toUri(); |
| this.alias = checkAliasPath(); |
| } |
| |
| /** |
| * Create a new PathResource from a provided URL object. |
| * <p> |
| * An invocation of this convenience constructor of the form. |
| * </p> |
| * <pre> |
| * new PathResource(url); |
| * </pre> |
| * <p> |
| * behaves in exactly the same way as the expression |
| * </p> |
| * <pre> |
| * new PathResource(url.toURI()); |
| * </pre> |
| * |
| * @param url the url to attempt to create PathResource from |
| * @throws IOException if URL doesn't point to a location that can be transformed to a PathResource |
| * @throws URISyntaxException if the provided URL was malformed |
| */ |
| public PathResource(URL url) throws IOException, URISyntaxException |
| { |
| this(url.toURI()); |
| } |
| |
| @Override |
| public Resource addPath(final String subpath) throws IOException, MalformedURLException |
| { |
| String cpath = URIUtil.canonicalPath(subpath); |
| |
| if ((cpath == null) || (cpath.length() == 0)) |
| throw new MalformedURLException(subpath); |
| |
| if ("/".equals(cpath)) |
| return this; |
| |
| // subpaths are always under PathResource |
| // compensate for input subpaths like "/subdir" |
| // where default resolve behavior would be |
| // to treat that like an absolute path |
| |
| return new PathResource(this, subpath); |
| } |
| |
| private void assertValidPath(Path path) |
| { |
| // TODO merged from 9.2, check if necessary |
| String str = path.toString(); |
| int idx = StringUtil.indexOfControlChars(str); |
| if(idx >= 0) |
| { |
| throw new InvalidPathException(str, "Invalid Character at index " + idx); |
| } |
| } |
| |
| @Override |
| public void close() |
| { |
| // not applicable for FileSytem / Path |
| } |
| |
| @Override |
| public boolean delete() throws SecurityException |
| { |
| try |
| { |
| return Files.deleteIfExists(path); |
| } |
| catch (IOException e) |
| { |
| LOG.ignore(e); |
| return false; |
| } |
| } |
| |
| @Override |
| public boolean equals(Object obj) |
| { |
| if (this == obj) |
| { |
| return true; |
| } |
| if (obj == null) |
| { |
| return false; |
| } |
| if (getClass() != obj.getClass()) |
| { |
| return false; |
| } |
| PathResource other = (PathResource)obj; |
| if (path == null) |
| { |
| if (other.path != null) |
| { |
| return false; |
| } |
| } |
| else if (!path.equals(other.path)) |
| { |
| return false; |
| } |
| return true; |
| } |
| |
| @Override |
| public boolean exists() |
| { |
| return Files.exists(path,NO_FOLLOW_LINKS); |
| } |
| |
| @Override |
| public File getFile() throws IOException |
| { |
| return path.toFile(); |
| } |
| |
| /** |
| * @return the {@link Path} of the resource |
| */ |
| public Path getPath() |
| { |
| return path; |
| } |
| |
| @Override |
| public InputStream getInputStream() throws IOException |
| { |
| /* Mimic behavior from old FileResource class and its |
| * usage of java.io.FileInputStream(File) which will trigger |
| * an IOException on construction if the path is a directory |
| */ |
| if (Files.isDirectory(path)) |
| throw new IOException(path + " is a directory"); |
| |
| return Files.newInputStream(path,StandardOpenOption.READ); |
| } |
| |
| @Override |
| public String getName() |
| { |
| return path.toAbsolutePath().toString(); |
| } |
| |
| @Override |
| public ReadableByteChannel getReadableByteChannel() throws IOException |
| { |
| return FileChannel.open(path,StandardOpenOption.READ); |
| } |
| |
| @Override |
| public URI getURI() |
| { |
| return this.uri; |
| } |
| |
| @Override |
| public URL getURL() |
| { |
| try |
| { |
| return path.toUri().toURL(); |
| } |
| catch (MalformedURLException e) |
| { |
| return null; |
| } |
| } |
| |
| @Override |
| public int hashCode() |
| { |
| final int prime = 31; |
| int result = 1; |
| result = (prime * result) + ((path == null)?0:path.hashCode()); |
| return result; |
| } |
| |
| @Override |
| public boolean isContainedIn(Resource r) throws MalformedURLException |
| { |
| // not applicable for FileSystem / path |
| return false; |
| } |
| |
| @Override |
| public boolean isDirectory() |
| { |
| return Files.isDirectory(path,FOLLOW_LINKS); |
| } |
| |
| @Override |
| public long lastModified() |
| { |
| try |
| { |
| FileTime ft = Files.getLastModifiedTime(path,FOLLOW_LINKS); |
| return ft.toMillis(); |
| } |
| catch (IOException e) |
| { |
| LOG.ignore(e); |
| return 0; |
| } |
| } |
| |
| @Override |
| public long length() |
| { |
| try |
| { |
| return Files.size(path); |
| } |
| catch (IOException e) |
| { |
| // in case of error, use File.length logic of 0L |
| return 0L; |
| } |
| } |
| |
| @Override |
| public boolean isAlias() |
| { |
| return this.alias!=null; |
| } |
| |
| /** |
| * The Alias as a Path. |
| * <p> |
| * Note: this cannot return the alias as a DIFFERENT path in 100% of situations, |
| * due to Java's internal Path/File normalization. |
| * </p> |
| * |
| * @return the alias as a path. |
| */ |
| public Path getAliasPath() |
| { |
| return this.alias; |
| } |
| |
| @Override |
| public URI getAlias() |
| { |
| return this.alias==null?null:this.alias.toUri(); |
| } |
| |
| @Override |
| public String[] list() |
| { |
| try (DirectoryStream<Path> dir = Files.newDirectoryStream(path)) |
| { |
| List<String> entries = new ArrayList<>(); |
| for (Path entry : dir) |
| { |
| String name = entry.getFileName().toString(); |
| |
| if (Files.isDirectory(entry)) |
| { |
| name += "/"; |
| } |
| |
| entries.add(name); |
| } |
| int size = entries.size(); |
| return entries.toArray(new String[size]); |
| } |
| catch (DirectoryIteratorException e) |
| { |
| LOG.debug(e); |
| } |
| catch (IOException e) |
| { |
| LOG.debug(e); |
| } |
| return null; |
| } |
| |
| @Override |
| public boolean renameTo(Resource dest) throws SecurityException |
| { |
| if (dest instanceof PathResource) |
| { |
| PathResource destRes = (PathResource)dest; |
| try |
| { |
| Path result = Files.move(path,destRes.path); |
| return Files.exists(result,NO_FOLLOW_LINKS); |
| } |
| catch (IOException e) |
| { |
| LOG.ignore(e); |
| return false; |
| } |
| } |
| else |
| { |
| return false; |
| } |
| } |
| |
| @Override |
| public void copyTo(File destination) throws IOException |
| { |
| if (isDirectory()) |
| { |
| IO.copyDir(this.path.toFile(),destination); |
| } |
| else |
| { |
| Files.copy(this.path,destination.toPath()); |
| } |
| } |
| |
| @Override |
| public String toString() |
| { |
| return this.uri.toASCIIString(); |
| } |
| } |