001/*
002 * Copyright 2002-2019 the original author or authors.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      https://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package org.springframework.core.io;
018
019import java.io.File;
020import java.io.FileNotFoundException;
021import java.io.IOException;
022import java.io.InputStream;
023import java.io.OutputStream;
024import java.net.URI;
025import java.net.URL;
026import java.nio.channels.ReadableByteChannel;
027import java.nio.channels.WritableByteChannel;
028import java.nio.file.Files;
029import java.nio.file.NoSuchFileException;
030import java.nio.file.OpenOption;
031import java.nio.file.Path;
032import java.nio.file.Paths;
033import java.nio.file.StandardOpenOption;
034
035import org.springframework.lang.Nullable;
036import org.springframework.util.Assert;
037
038/**
039 * {@link Resource} implementation for {@link java.nio.file.Path} handles,
040 * performing all operations and transformations via the {@code Path} API.
041 * Supports resolution as a {@link File} and also as a {@link URL}.
042 * Implements the extended {@link WritableResource} interface.
043 *
044 * <p>Note: As of 5.1, {@link java.nio.file.Path} support is also available
045 * in {@link FileSystemResource#FileSystemResource(Path) FileSystemResource},
046 * applying Spring's standard String-based path transformations but
047 * performing all operations via the {@link java.nio.file.Files} API.
048 * This {@code PathResource} is effectively a pure {@code java.nio.path.Path}
049 * based alternative with different {@code createRelative} behavior.
050 *
051 * @author Philippe Marschall
052 * @author Juergen Hoeller
053 * @since 4.0
054 * @see java.nio.file.Path
055 * @see java.nio.file.Files
056 * @see FileSystemResource
057 */
058public class PathResource extends AbstractResource implements WritableResource {
059
060        private final Path path;
061
062
063        /**
064         * Create a new PathResource from a Path handle.
065         * <p>Note: Unlike {@link FileSystemResource}, when building relative resources
066         * via {@link #createRelative}, the relative path will be built <i>underneath</i>
067         * the given root: e.g. Paths.get("C:/dir1/"), relative path "dir2" -> "C:/dir1/dir2"!
068         * @param path a Path handle
069         */
070        public PathResource(Path path) {
071                Assert.notNull(path, "Path must not be null");
072                this.path = path.normalize();
073        }
074
075        /**
076         * Create a new PathResource from a Path handle.
077         * <p>Note: Unlike {@link FileSystemResource}, when building relative resources
078         * via {@link #createRelative}, the relative path will be built <i>underneath</i>
079         * the given root: e.g. Paths.get("C:/dir1/"), relative path "dir2" -> "C:/dir1/dir2"!
080         * @param path a path
081         * @see java.nio.file.Paths#get(String, String...)
082         */
083        public PathResource(String path) {
084                Assert.notNull(path, "Path must not be null");
085                this.path = Paths.get(path).normalize();
086        }
087
088        /**
089         * Create a new PathResource from a Path handle.
090         * <p>Note: Unlike {@link FileSystemResource}, when building relative resources
091         * via {@link #createRelative}, the relative path will be built <i>underneath</i>
092         * the given root: e.g. Paths.get("C:/dir1/"), relative path "dir2" -> "C:/dir1/dir2"!
093         * @param uri a path URI
094         * @see java.nio.file.Paths#get(URI)
095         */
096        public PathResource(URI uri) {
097                Assert.notNull(uri, "URI must not be null");
098                this.path = Paths.get(uri).normalize();
099        }
100
101
102        /**
103         * Return the file path for this resource.
104         */
105        public final String getPath() {
106                return this.path.toString();
107        }
108
109        /**
110         * This implementation returns whether the underlying file exists.
111         * @see java.nio.file.Files#exists(Path, java.nio.file.LinkOption...)
112         */
113        @Override
114        public boolean exists() {
115                return Files.exists(this.path);
116        }
117
118        /**
119         * This implementation checks whether the underlying file is marked as readable
120         * (and corresponds to an actual file with content, not to a directory).
121         * @see java.nio.file.Files#isReadable(Path)
122         * @see java.nio.file.Files#isDirectory(Path, java.nio.file.LinkOption...)
123         */
124        @Override
125        public boolean isReadable() {
126                return (Files.isReadable(this.path) && !Files.isDirectory(this.path));
127        }
128
129        /**
130         * This implementation opens a InputStream for the underlying file.
131         * @see java.nio.file.spi.FileSystemProvider#newInputStream(Path, OpenOption...)
132         */
133        @Override
134        public InputStream getInputStream() throws IOException {
135                if (!exists()) {
136                        throw new FileNotFoundException(getPath() + " (no such file or directory)");
137                }
138                if (Files.isDirectory(this.path)) {
139                        throw new FileNotFoundException(getPath() + " (is a directory)");
140                }
141                return Files.newInputStream(this.path);
142        }
143
144        /**
145         * This implementation checks whether the underlying file is marked as writable
146         * (and corresponds to an actual file with content, not to a directory).
147         * @see java.nio.file.Files#isWritable(Path)
148         * @see java.nio.file.Files#isDirectory(Path, java.nio.file.LinkOption...)
149         */
150        @Override
151        public boolean isWritable() {
152                return (Files.isWritable(this.path) && !Files.isDirectory(this.path));
153        }
154
155        /**
156         * This implementation opens a OutputStream for the underlying file.
157         * @see java.nio.file.spi.FileSystemProvider#newOutputStream(Path, OpenOption...)
158         */
159        @Override
160        public OutputStream getOutputStream() throws IOException {
161                if (Files.isDirectory(this.path)) {
162                        throw new FileNotFoundException(getPath() + " (is a directory)");
163                }
164                return Files.newOutputStream(this.path);
165        }
166
167        /**
168         * This implementation returns a URL for the underlying file.
169         * @see java.nio.file.Path#toUri()
170         * @see java.net.URI#toURL()
171         */
172        @Override
173        public URL getURL() throws IOException {
174                return this.path.toUri().toURL();
175        }
176
177        /**
178         * This implementation returns a URI for the underlying file.
179         * @see java.nio.file.Path#toUri()
180         */
181        @Override
182        public URI getURI() throws IOException {
183                return this.path.toUri();
184        }
185
186        /**
187         * This implementation always indicates a file.
188         */
189        @Override
190        public boolean isFile() {
191                return true;
192        }
193
194        /**
195         * This implementation returns the underlying File reference.
196         */
197        @Override
198        public File getFile() throws IOException {
199                try {
200                        return this.path.toFile();
201                }
202                catch (UnsupportedOperationException ex) {
203                        // Only paths on the default file system can be converted to a File:
204                        // Do exception translation for cases where conversion is not possible.
205                        throw new FileNotFoundException(this.path + " cannot be resolved to absolute file path");
206                }
207        }
208
209        /**
210         * This implementation opens a Channel for the underlying file.
211         * @see Files#newByteChannel(Path, OpenOption...)
212         */
213        @Override
214        public ReadableByteChannel readableChannel() throws IOException {
215                try {
216                        return Files.newByteChannel(this.path, StandardOpenOption.READ);
217                }
218                catch (NoSuchFileException ex) {
219                        throw new FileNotFoundException(ex.getMessage());
220                }
221        }
222
223        /**
224         * This implementation opens a Channel for the underlying file.
225         * @see Files#newByteChannel(Path, OpenOption...)
226         */
227        @Override
228        public WritableByteChannel writableChannel() throws IOException {
229                return Files.newByteChannel(this.path, StandardOpenOption.WRITE);
230        }
231
232        /**
233         * This implementation returns the underlying file's length.
234         */
235        @Override
236        public long contentLength() throws IOException {
237                return Files.size(this.path);
238        }
239
240        /**
241         * This implementation returns the underlying File's timestamp.
242         * @see java.nio.file.Files#getLastModifiedTime(Path, java.nio.file.LinkOption...)
243         */
244        @Override
245        public long lastModified() throws IOException {
246                // We can not use the superclass method since it uses conversion to a File and
247                // only a Path on the default file system can be converted to a File...
248                return Files.getLastModifiedTime(this.path).toMillis();
249        }
250
251        /**
252         * This implementation creates a PathResource, applying the given path
253         * relative to the path of the underlying file of this resource descriptor.
254         * @see java.nio.file.Path#resolve(String)
255         */
256        @Override
257        public Resource createRelative(String relativePath) {
258                return new PathResource(this.path.resolve(relativePath));
259        }
260
261        /**
262         * This implementation returns the name of the file.
263         * @see java.nio.file.Path#getFileName()
264         */
265        @Override
266        public String getFilename() {
267                return this.path.getFileName().toString();
268        }
269
270        @Override
271        public String getDescription() {
272                return "path [" + this.path.toAbsolutePath() + "]";
273        }
274
275
276        /**
277         * This implementation compares the underlying Path references.
278         */
279        @Override
280        public boolean equals(@Nullable Object other) {
281                return (this == other || (other instanceof PathResource &&
282                                this.path.equals(((PathResource) other).path)));
283        }
284
285        /**
286         * This implementation returns the hash code of the underlying Path reference.
287         */
288        @Override
289        public int hashCode() {
290                return this.path.hashCode();
291        }
292
293}