//
// ========================================================================
// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under
// the terms of the Eclipse Public License 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0
//
// This Source Code may also be made available under the following
// Secondary Licenses when the conditions for such availability set
// forth in the Eclipse Public License, v. 2.0 are satisfied:
// the Apache License v2.0 which is available at
// https://www.apache.org/licenses/LICENSE-2.0
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//

package org.eclipse.jetty.util.resource;

import java.io.File;
import java.io.IOError;
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.ReadableByteChannel;
import java.nio.channels.SeekableByteChannel;
import java.nio.file.DirectoryIteratorException;
import java.nio.file.DirectoryStream;
import java.nio.file.FileSystems;
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.slf4j.Logger;
import org.slf4j.LoggerFactory;

Java NIO Path Resource.
/** * Java NIO Path Resource. */
public class PathResource extends Resource { private static final Logger LOG = LoggerFactory.getLogger(PathResource.class); private static final LinkOption[] NO_FOLLOW_LINKS = new LinkOption[]{LinkOption.NOFOLLOW_LINKS}; private static final LinkOption[] FOLLOW_LINKS = new LinkOption[]{}; private final Path path; private final Path alias; private final URI uri; private final boolean belongsToDefaultFileSystem; 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.trace("IGNORED", 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 (!isSameName(abs, real)) { return real; } } } catch (IOException e) { LOG.trace("IGNORED", e); } catch (Exception e) { LOG.warn("bad alias ({} {}) for {}", e.getClass().getName(), e.getMessage(), path); } return null; }
Test if the paths are the same name.

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)

/** * Test if the paths are the same name. * * <p> * 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. * </p> * * <p> * For OS's that are case insensitive, this should * return the real (on-disk / case correct) version * of the path. * </p> * * <p> * We have to be careful on Windows and OSX. * </p> * * <p> * Assume we have the following scenario: * </p> * * <pre> * Path a = new File("foo").toPath(); * Files.createFile(a); * Path b = new File("FOO").toPath(); * </pre> * * <p> * There now exists a file called {@code foo} on disk. * Using Windows or OSX, with a Path reference of * {@code FOO}, {@code Foo}, {@code fOO}, etc.. means the following * </p> * * <pre> * | 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 * </pre> * * <p> * See the javadoc for Path.equals() for details about this FileSystem * behavior difference * </p> * * <p> * We also cannot rely on a.compareTo(b) as this is roughly equivalent * in implementation to a.equals(b) * </p> */
public static boolean isSameName(Path pathA, Path pathB) { int aCount = pathA.getNameCount(); int bCount = pathB.getNameCount(); if (aCount != bCount) { // different number of segments return false; } // compare each segment of path, backwards for (int i = bCount; i-- > 0; ) { if (!pathA.getName(i).toString().equals(pathB.getName(i).toString())) { return false; } } return true; }
Construct a new PathResource from a File object.

An invocation of this convenience constructor of the form.

new PathResource(file);

behaves in exactly the same way as the expression

new PathResource(file.toPath());
Params:
  • file – the file to use
/** * 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.
Params:
  • path – the path to use
/** * Construct a new PathResource from a Path object. * * @param path the path to use */
public PathResource(Path path) { Path absPath = path; try { absPath = path.toRealPath(NO_FOLLOW_LINKS); } catch (IOError | IOException e) { // Not able to resolve real/canonical path from provided path // This could be due to a glob reference, or a reference // to a path that doesn't exist (yet) if (LOG.isDebugEnabled()) LOG.debug("Unable to get real/canonical path for {}", path, e); } // cleanup any lingering relative path nonsense (like "/./" and "/../") this.path = absPath.normalize(); assertValidPath(path); this.uri = this.path.toUri(); this.alias = checkAliasPath(); this.belongsToDefaultFileSystem = this.path.getFileSystem() == FileSystems.getDefault(); }
Construct a new PathResource from a parent PathResource and child sub path
Params:
  • parent – the parent path resource
  • childPath – the child sub path
/** * 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) { // Calculate the URI and the path separately, so that any aliasing done by // FileSystem.getPath(path,childPath) is visible 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(); this.belongsToDefaultFileSystem = this.path.getFileSystem() == FileSystems.getDefault(); }
Construct a new PathResource from a URI object.

Must be an absolute URI using the file scheme.

Params:
  • uri – the URI to build this PathResource from.
Throws:
  • IOException – if unable to construct the PathResource from the URI.
/** * 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 (IllegalArgumentException e) { throw e; } catch (Exception e) { LOG.trace("IGNORED", e); throw new IOException("Unable to build Path from: " + uri, e); } this.path = path.toAbsolutePath(); this.uri = path.toUri(); this.alias = checkAliasPath(); this.belongsToDefaultFileSystem = this.path.getFileSystem() == FileSystems.getDefault(); }
Create a new PathResource from a provided URL object.

An invocation of this convenience constructor of the form.

new PathResource(url);

behaves in exactly the same way as the expression

new PathResource(url.toURI());
Params:
  • 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
  • URISyntaxException – if the provided URL was malformed
/** * 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 { 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.trace("IGNORED", 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 { if (!belongsToDefaultFileSystem) return null; return path.toFile(); }
Returns:the Path of the resource
/** * @return the {@link Path} of the resource */
public Path getPath() { return path; } @Override public InputStream getInputStream() throws IOException { return Files.newInputStream(path, StandardOpenOption.READ); } @Override public String getName() { return path.toAbsolutePath().toString(); } @Override public ReadableByteChannel getReadableByteChannel() throws IOException { return newSeekableByteChannel(); } public SeekableByteChannel newSeekableByteChannel() throws IOException { return Files.newByteChannel(path, StandardOpenOption.READ); } @Override public URI getURI() { return this.uri; } @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) { try { PathResource pr = PathResource.class.cast(r); return (path.startsWith(pr.getPath())); } catch (ClassCastException e) { 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.trace("IGNORED", 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.

Note: this cannot return the alias as a DIFFERENT path in 100% of situations, due to Java's internal Path/File normalization.

Returns:the alias as a path.
/** * 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("Directory list failure", e); } catch (IOException e) { LOG.debug("Directory list access failure", 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.trace("IGNORED", 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(); } }