View Javadoc

1   //
2   //  ========================================================================
3   //  Copyright (c) 1995-2016 Mort Bay Consulting Pty. Ltd.
4   //  ------------------------------------------------------------------------
5   //  All rights reserved. This program and the accompanying materials
6   //  are made available under the terms of the Eclipse Public License v1.0
7   //  and Apache License v2.0 which accompanies this distribution.
8   //
9   //      The Eclipse Public License is available at
10  //      http://www.eclipse.org/legal/epl-v10.html
11  //
12  //      The Apache License v2.0 is available at
13  //      http://www.opensource.org/licenses/apache2.0.php
14  //
15  //  You may elect to redistribute this code under either of these licenses.
16  //  ========================================================================
17  //
18  
19  package org.eclipse.jetty.util.resource;
20  
21  import java.io.File;
22  import java.io.IOException;
23  import java.io.InputStream;
24  import java.net.MalformedURLException;
25  import java.net.URI;
26  import java.net.URISyntaxException;
27  import java.net.URL;
28  import java.nio.channels.FileChannel;
29  import java.nio.channels.ReadableByteChannel;
30  import java.nio.file.DirectoryIteratorException;
31  import java.nio.file.DirectoryStream;
32  import java.nio.file.Files;
33  import java.nio.file.InvalidPathException;
34  import java.nio.file.LinkOption;
35  import java.nio.file.Path;
36  import java.nio.file.StandardOpenOption;
37  import java.nio.file.attribute.FileTime;
38  import java.util.ArrayList;
39  import java.util.List;
40  
41  import org.eclipse.jetty.util.IO;
42  import org.eclipse.jetty.util.URIUtil;
43  import org.eclipse.jetty.util.log.Log;
44  import org.eclipse.jetty.util.log.Logger;
45  
46  /**
47   * Java NIO Path equivalent of FileResource.
48   */
49  public class PathResource extends Resource
50  {
51      private static final Logger LOG = Log.getLogger(PathResource.class);
52      private final static LinkOption NO_FOLLOW_LINKS[] = new LinkOption[] { LinkOption.NOFOLLOW_LINKS };
53      private final static LinkOption FOLLOW_LINKS[] = new LinkOption[] {};
54      
55      private final Path path;
56      private final Path alias;
57      private final URI uri;
58      
59      private static final Path checkAliasPath(final Path path)
60      {
61          Path abs = path;
62          if (!abs.isAbsolute())
63          {
64              abs = path.toAbsolutePath();
65          }
66  
67          try
68          {
69              if (Files.isSymbolicLink(path))
70                  return Files.readSymbolicLink(path);
71              if (Files.exists(path))
72              {
73                  Path real = abs.toRealPath(FOLLOW_LINKS);
74                  
75                  /*
76                   * If the real path is not the same as the absolute path
77                   * then we know that the real path is the alias for the
78                   * provided path.
79                   *
80                   * For OS's that are case insensitive, this should
81                   * return the real (on-disk / case correct) version
82                   * of the path.
83                   *
84                   * We have to be careful on Windows and OSX.
85                   * 
86                   * Assume we have the following scenario
87                   *   Path a = new File("foo").toPath();
88                   *   Files.createFile(a);
89                   *   Path b = new File("FOO").toPath();
90                   * 
91                   * There now exists a file called "foo" on disk.
92                   * Using Windows or OSX, with a Path reference of
93                   * "FOO", "Foo", "fOO", etc.. means the following
94                   * 
95                   *                        |  OSX    |  Windows   |  Linux
96                   * -----------------------+---------+------------+---------
97                   * Files.exists(a)        |  True   |  True      |  True
98                   * Files.exists(b)        |  True   |  True      |  False
99                   * Files.isSameFile(a,b)  |  True   |  True      |  False
100                  * a.equals(b)            |  False  |  True      |  False
101                  * 
102                  * See the javadoc for Path.equals() for details about this FileSystem
103                  * behavior difference
104                  * 
105                  * We also cannot rely on a.compareTo(b) as this is roughly equivalent
106                  * in implementation to a.equals(b)
107                  */
108                 
109                 int absCount = abs.getNameCount();
110                 int realCount = real.getNameCount();
111                 if (absCount != realCount)
112                 {
113                     // different number of segments
114                     return real;
115                 }
116                 
117                 // compare each segment of path, backwards
118                 for (int i = realCount-1; i >= 0; i--)
119                 {
120                     if (!abs.getName(i).toString().equals(real.getName(i).toString()))
121                     {
122                         return real;
123                     }
124                 }
125             }
126         }
127         catch (IOException e)
128         {
129             LOG.ignore(e);
130         }
131         catch (Exception e)
132         {
133             LOG.warn("bad alias ({} {}) for {}", e.getClass().getName(), e.getMessage(),path);
134         }
135         return null;
136     }
137 
138     /**
139      * Construct a new PathResource from a File object.
140      * <p>
141      * An invocation of this convenience constructor of the form.
142      * </p>
143      * <pre>
144      * new PathResource(file);
145      * </pre>
146      * <p>
147      * behaves in exactly the same way as the expression
148      * </p>
149      * <pre>
150      * new PathResource(file.toPath());
151      * </pre>
152 
153      * @param file the file to use
154      */
155     public PathResource(File file)
156     {
157         this(file.toPath());
158     }
159 
160     /**
161      * Construct a new PathResource from a Path object.
162      * 
163      * @param path the path to use
164      */
165     public PathResource(Path path)
166     {
167         this.path = path.toAbsolutePath();
168         this.uri = this.path.toUri();
169         this.alias = checkAliasPath(path);
170     }
171 
172     /**
173      * Construct a new PathResource from a URI object.
174      * <p>
175      * Must be an absolute URI using the <code>file</code> scheme.
176      * 
177      * @param uri the URI to build this PathResource from.
178      * @throws IOException if unable to construct the PathResource from the URI.
179      */
180     public PathResource(URI uri) throws IOException
181     {
182         if (!uri.isAbsolute())
183         {
184             throw new IllegalArgumentException("not an absolute uri");
185         }
186 
187         if (!uri.getScheme().equalsIgnoreCase("file"))
188         {
189             throw new IllegalArgumentException("not file: scheme");
190         }
191 
192         Path path;
193         try
194         {
195             path = new File(uri).toPath();
196         }
197         catch (InvalidPathException e)
198         {
199             throw e;
200         }
201         catch (IllegalArgumentException e)
202         {
203             throw e;
204         }
205         catch (Exception e)
206         {
207             LOG.ignore(e);
208             throw new IOException("Unable to build Path from: " + uri,e);
209         }
210 
211         this.path = path.toAbsolutePath();
212         this.uri = path.toUri();
213         this.alias = checkAliasPath(path);
214     }
215 
216     /**
217      * Create a new PathResource from a provided URL object.
218      * <p>
219      * An invocation of this convenience constructor of the form.
220      * </p>
221      * <pre>
222      * new PathResource(url);
223      * </pre>
224      * <p>
225      * behaves in exactly the same way as the expression
226      * </p>
227      * <pre>
228      * new PathResource(url.toURI());
229      * </pre>
230      * 
231      * @param url the url to attempt to create PathResource from
232      * @throws IOException if URL doesn't point to a location that can be transformed to a PathResource
233      * @throws URISyntaxException if the provided URL was malformed
234      */
235     public PathResource(URL url) throws IOException, URISyntaxException
236     {
237         this(url.toURI());
238     }
239 
240     @Override
241     public Resource addPath(final String subpath) throws IOException, MalformedURLException
242     {
243         String cpath = URIUtil.canonicalPath(subpath);
244 
245         if ((cpath == null) || (cpath.length() == 0))
246             throw new MalformedURLException();
247 
248         if ("/".equals(cpath))
249             return this;
250 
251         // subpaths are always under PathResource
252         // compensate for input subpaths like "/subdir"
253         // where default resolve behavior would be
254         // to treat that like an absolute path
255         return new PathResource(this.path.getFileSystem().getPath(path.toString(), subpath));
256     }
257 
258     @Override
259     public void close()
260     {
261         // not applicable for FileSytem / Path
262     }
263 
264     @Override
265     public boolean delete() throws SecurityException
266     {
267         try
268         {
269             return Files.deleteIfExists(path);
270         }
271         catch (IOException e)
272         {
273             LOG.ignore(e);
274             return false;
275         }
276     }
277 
278     @Override
279     public boolean equals(Object obj)
280     {
281         if (this == obj)
282         {
283             return true;
284         }
285         if (obj == null)
286         {
287             return false;
288         }
289         if (getClass() != obj.getClass())
290         {
291             return false;
292         }
293         PathResource other = (PathResource)obj;
294         if (path == null)
295         {
296             if (other.path != null)
297             {
298                 return false;
299             }
300         }
301         else if (!path.equals(other.path))
302         {
303             return false;
304         }
305         return true;
306     }
307 
308     @Override
309     public boolean exists()
310     {
311         return Files.exists(path,NO_FOLLOW_LINKS);
312     }
313 
314     @Override
315     public File getFile() throws IOException
316     {
317         return path.toFile();
318     }
319 
320     /**
321      * @return the {@link Path} of the resource
322      */
323     public Path getPath()
324     {
325         return path;
326     }
327 
328     @Override
329     public InputStream getInputStream() throws IOException
330     {
331         return Files.newInputStream(path,StandardOpenOption.READ);
332     }
333 
334     @Override
335     public String getName()
336     {
337         return path.toAbsolutePath().toString();
338     }
339 
340     @Override
341     public ReadableByteChannel getReadableByteChannel() throws IOException
342     {
343         return FileChannel.open(path,StandardOpenOption.READ);
344     }
345 
346     @Override
347     public URI getURI()
348     {
349         return this.uri;
350     }
351 
352     @Override
353     public URL getURL()
354     {
355         try
356         {
357             return path.toUri().toURL();
358         }
359         catch (MalformedURLException e)
360         {
361             return null;
362         }
363     }
364 
365     @Override
366     public int hashCode()
367     {
368         final int prime = 31;
369         int result = 1;
370         result = (prime * result) + ((path == null)?0:path.hashCode());
371         return result;
372     }
373 
374     @Override
375     public boolean isContainedIn(Resource r) throws MalformedURLException
376     {
377         // not applicable for FileSystem / path
378         return false;
379     }
380 
381     @Override
382     public boolean isDirectory()
383     {
384         return Files.isDirectory(path,FOLLOW_LINKS);
385     }
386 
387     @Override
388     public long lastModified()
389     {
390         try
391         {
392             FileTime ft = Files.getLastModifiedTime(path,FOLLOW_LINKS);
393             return ft.toMillis();
394         }
395         catch (IOException e)
396         {
397             LOG.ignore(e);
398             return 0;
399         }
400     }
401 
402     @Override
403     public long length()
404     {
405         try
406         {
407             return Files.size(path);
408         }
409         catch (IOException e)
410         {
411             // in case of error, use File.length logic of 0L
412             return 0L;
413         }
414     }
415 
416     @Override
417     public boolean isAlias()
418     {
419         return this.alias!=null;
420     }
421     
422     /**
423      * The Alias as a Path.
424      * 
425      * @return the alias as a path.
426      */
427     public Path getAliasPath()
428     {
429         return this.alias;
430     }
431     
432     @Override
433     public URI getAlias()
434     {
435         return this.alias==null?null:this.alias.toUri();
436     }
437 
438     @Override
439     public String[] list()
440     {
441         try (DirectoryStream<Path> dir = Files.newDirectoryStream(path))
442         {
443             List<String> entries = new ArrayList<>();
444             for (Path entry : dir)
445             {
446                 String name = entry.getFileName().toString();
447 
448                 if (Files.isDirectory(entry))
449                 {
450                     name += "/";
451                 }
452 
453                 entries.add(name);
454             }
455             int size = entries.size();
456             return entries.toArray(new String[size]);
457         }
458         catch (DirectoryIteratorException e)
459         {
460             LOG.debug(e);
461         }
462         catch (IOException e)
463         {
464             LOG.debug(e);
465         }
466         return null;
467     }
468 
469     @Override
470     public boolean renameTo(Resource dest) throws SecurityException
471     {
472         if (dest instanceof PathResource)
473         {
474             PathResource destRes = (PathResource)dest;
475             try
476             {
477                 Path result = Files.move(path,destRes.path);
478                 return Files.exists(result,NO_FOLLOW_LINKS);
479             }
480             catch (IOException e)
481             {
482                 LOG.ignore(e);
483                 return false;
484             }
485         }
486         else
487         {
488             return false;
489         }
490     }
491 
492     @Override
493     public void copyTo(File destination) throws IOException
494     {
495         if (isDirectory())
496         {
497             IO.copyDir(this.path.toFile(),destination);
498         }
499         else
500         {
501             Files.copy(this.path,destination.toPath());
502         }
503     }
504 
505     @Override
506     public String toString()
507     {
508         return this.uri.toASCIIString();
509     }
510 }