View Javadoc
1   /*
2    * Copyright (C) 2005-2015 Schlichtherle IT Services.
3    * All rights reserved. Use is subject to license terms.
4    */
5   package net.java.truevfs.kernel.spec;
6   
7   import java.beans.ConstructorProperties;
8   import java.io.*;
9   import java.net.URI;
10  import java.net.URISyntaxException;
11  import javax.annotation.CheckForNull;
12  import javax.annotation.Nullable;
13  import javax.annotation.concurrent.Immutable;
14  
15  import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
16  import net.java.truecommons.shed.QuotedUriSyntaxException;
17  import net.java.truecommons.shed.UriBuilder;
18  import static net.java.truevfs.kernel.spec.FsUriModifier.CANONICALIZE;
19  import static net.java.truevfs.kernel.spec.FsUriModifier.NULL;
20  import static net.java.truevfs.kernel.spec.FsUriModifier.PostFix.NODE_PATH;
21  
22  /**
23   * Addresses a file system node.
24   * The purpose of a file system path is to parse a {@link URI} and decompose it
25   * into a file system {@link #getMountPoint() mount point} and
26   * {@linkplain #getNodeName() node name}.
27   *
28   * <h3><a name="specification">Specification</a></h3>
29   * <p>
30   * A path adds the following syntax constraints to a
31   * {@link URI Uniform Resource Identifier}:
32   * <ol>
33   * <li>The URI must not define a fragment component.
34   * <li>If the URI is opaque, its scheme specific part must contain at least
35   *     one mount point separator {@code "!/"}.
36   *     The part <em>up to</em> the last mount point separator is parsed
37   *     according to the syntax constraints for an {@link FsMountPoint} and set
38   *     as the value of the {@link #getMountPoint() mountPoint} property.
39   *     The part <em>after</em> the last mount point separator is parsed
40   *     according to the syntax constraints for an {@link FsNodeName} and set
41   *     as the value of the {@linkplain #getNodeName() node name} property.
42   * <li>Otherwise, if the URI is absolute, it's resolved with {@code "."},
43   *     parsed according to the syntax constraints for an {@link FsMountPoint}
44   *     and set as the value of the {@link #getMountPoint() mountPoint} property.
45   *     The URI relativized to this mount point is parsed according to the
46   *     syntax constraints for an {@link FsNodeName} and set as the value of
47   *     the {@linkplain #getNodeName() node name} property.
48   * <li>Otherwise, the value of the {@link #getMountPoint() mountPoint} property
49   *     is set to {@code null} and the URI is parsed according to the syntax
50   *     constraints for an {@link FsNodeName} and set as the value of the
51   *     {@linkplain #getNodeName() node name} property.
52   * </ol>
53   * For opaque URIs of the form {@code jar:<url>!/<node>}, these constraints
54   * build a close subset of the syntax allowed by a
55   * {@link java.net.JarURLConnection}.
56   *
57   * <h3><a name="examples">Examples</a></h3>
58   * <p>
59   * Examples for <em>valid</em> node path URIs:
60   * <table border=1 cellpadding=5 summary="">
61   * <thead>
62   * <tr>
63   *   <th>{@link #getUri() uri} property</th>
64   *   <th>{@link #getMountPoint() mountPoint} URI</th>
65   *   <th>{@link #getNodeName() nodeName} URI</th>
66   * </tr>
67   * </thead>
68   * <tbody>
69   * <tr>
70   *   <td>{@code foo}</td>
71   *   <td>(null)</td>
72   *   <td>{@code foo}</td>
73   * </tr>
74   * <tr>
75   *   <td>{@code foo:/bar}</td>
76   *   <td>{@code foo:/}</td>
77   *   <td>{@code bar}</td>
78   * </tr>
79   * <tr>
80   *   <td>{@code foo:/bar/}</td>
81   *   <td>{@code foo:/bar}</td>
82   *   <td>(empty - not null)</td>
83   * </tr>
84   * <tr>
85   *   <td>{@code foo:bar:/baz!/bang}</td>
86   *   <td>{@code foo:bar:/baz!/}</td>
87   *   <td>{@code bang}</td>
88   * </tr>
89   * </tbody>
90   * </table>
91   * <p>
92   * Examples for <em>invalid</em> node path URIs:
93   * <table border=1 cellpadding=5 summary="">
94   * <thead>
95   * <tr>
96   *   <th>URI</th>
97   *   <th>Issue</th>
98   * </tr>
99   * </thead>
100  * <tbody>
101  * <tr>
102  *   <td>{@code /foo}</td>
103  *   <td>leading slash separator not allowed if URI is not absolute</td>
104  * </tr>
105  * <tr>
106  *   <td>{@code foo/}</td>
107  *   <td>trailing slash separator not allowed if URI is not absolute</td>
108  * </tr>
109  * <tr>
110  *   <td>{@code foo:bar}</td>
111  *   <td>missing mount point separator in opaque URI</td>
112  * </tr>
113  * <tr>
114  *   <td>{@code foo:bar:baz:/bang!/boom}</td>
115  *   <td>dito for {@code bar:baz:/bang}</td>
116  * </tr>
117  * </tbody>
118  * </table>
119  *
120  * <h3><a name="identities">Identities</a></h3>
121  * <p>
122  * For any path {@code p}, it's generally true that
123  * {@code new FsNodePath(p.getUri()).equals(p)}.
124  * <p>
125  * Furthermore, it's generally true that
126  * {@code new FsNodePath(p.getMountPoint(), p.getNodeName()).equals(p)}.
127  *
128  * <h3><a name="serialization">Serialization</a></h3>
129  * <p>
130  * This class supports serialization with both
131  * {@link java.io.ObjectOutputStream} and {@link java.beans.XMLEncoder}.
132  *
133  * @see    FsMountPoint
134  * @see    FsNodeName
135  * @see    FsScheme
136  * @author Christian Schlichtherle
137  */
138 @Immutable
139 public final class FsNodePath
140 implements Serializable, Comparable<FsNodePath> {
141 
142     private static final long serialVersionUID = 5798435461242930648L;
143 
144     private static final URI DOT = URI.create(".");
145 
146     @SuppressFBWarnings("JCIP_FIELD_ISNT_FINAL_IN_IMMUTABLE_CLASS")
147     private URI uri; // not final for serialization only!
148 
149     private transient @Nullable FsMountPoint mountPoint;
150 
151     private transient FsNodeName nodeName;
152 
153     private transient volatile @Nullable URI hierarchical;
154 
155     /**
156      * Equivalent to {@link #create(URI, FsUriModifier) create(uri, FsUriModifier.NULL)}.
157      */
158     public static FsNodePath
159     create(URI uri) {
160         return create(uri, NULL);
161     }
162 
163     /**
164      * Constructs a new path by parsing the given URI.
165      * This static factory method calls
166      * {@link #FsNodePath(URI, FsUriModifier) new FsNodePath(uri, modifier)}
167      * and wraps any thrown {@link URISyntaxException} in an
168      * {@link IllegalArgumentException}.
169      *
170      * @param  uri the {@link #getUri() URI}.
171      * @param  modifier the URI modifier.
172      * @throws IllegalArgumentException if {@code uri} does not conform to the
173      *         syntax constraints for paths.
174      * @return A new path.
175      */
176     public static FsNodePath
177     create(URI uri, FsUriModifier modifier) {
178         try {
179             return new FsNodePath(uri, modifier);
180         } catch (URISyntaxException ex) {
181             throw new IllegalArgumentException(ex);
182         }
183     }
184 
185     /**
186      * Equivalent to {@link #FsNodePath(URI, FsUriModifier) new FsNodePath(file.toURI(), FsUriModifier.CANONICALIZE)}.
187      * Note that this constructor is expected not to throw any exceptions.
188      */
189     public FsNodePath(File file) {
190         try {
191             parse(file.toURI(), CANONICALIZE);
192         } catch (URISyntaxException ex) {
193             throw new AssertionError(ex);
194         }
195     }
196 
197     /**
198      * Equivalent to {@link #FsNodePath(URI, FsUriModifier) new FsNodePath(uri, FsUriModifier.NULL)}.
199      */
200     @ConstructorProperties("uri")
201     public FsNodePath(URI uri) throws URISyntaxException {
202         parse(uri, NULL);
203     }
204 
205     /**
206      * Constructs a new path by parsing the given URI.
207      *
208      * @param  uri the non-{@code null} {@link #getUri() URI}.
209      * @param  modifier the URI modifier.
210      * @throws URISyntaxException if {@code uri} does not conform to the
211      *         syntax constraints for paths.
212      */
213     public FsNodePath(URI uri, FsUriModifier modifier)
214     throws URISyntaxException {
215         parse(uri, modifier);
216     }
217 
218     /**
219      * Constructs a new path by composing its URI from the given nullable mount
220      * point and node name.
221      *
222      * @param  mountPoint the nullable {@link #getMountPoint() mount point}.
223      * @param  nodeName the {@link #getNodeName() node name}.
224      */
225     public FsNodePath(
226             final @CheckForNull FsMountPoint mountPoint,
227             final FsNodeName nodeName) {
228         URI mpu;
229         if (null == mountPoint) {
230             this.uri = nodeName.getUri();
231         } else if (nodeName.isRoot()) {
232             this.uri = mountPoint.getUri();
233         } else if ((mpu = mountPoint.getUri()).isOpaque()) {
234             try {
235                 // Compute mountPoint + nodeName, but ensure that all URI
236                 // components are properly quoted.
237                 final String mpussp = mpu.getRawSchemeSpecificPart();
238                 final int mpusspl = mpussp.length();
239                 final URI enu = nodeName.getUri();
240                 final String enup = enu.getRawPath();
241                 final int enupl = enup.length();
242                 final String enuq = enu.getRawQuery();
243                 final int enuql = null == enuq ? 0 : enuq.length() + 1;
244                 final StringBuilder ssp =
245                         new StringBuilder(mpusspl + enupl + enuql)
246                         .append(mpussp)
247                         .append(enup);
248                 if (null != enuq)
249                     ssp.append('?').append(enuq);
250                 this.uri = new UriBuilder(true)
251                         .scheme(mpu.getScheme())
252                         .path(ssp.toString())
253                         .fragment(enu.getRawFragment())
254                         .getUri();
255             } catch (URISyntaxException ex) {
256                 throw new AssertionError(ex);
257             }
258         } else {
259             this.uri = mpu.resolve(nodeName.getUri());
260         }
261         this.mountPoint = mountPoint;
262         this.nodeName = nodeName;
263 
264         assert invariants();
265     }
266 
267     private void writeObject(ObjectOutputStream out)
268     throws IOException {
269         out.writeObject(uri.toString());
270     }
271 
272     private void readObject(ObjectInputStream in)
273     throws IOException, ClassNotFoundException {
274         try {
275             parse(new URI(in.readObject().toString()), NULL);
276         } catch (URISyntaxException ex) {
277             throw (InvalidObjectException) new InvalidObjectException(ex.toString())
278                     .initCause(ex);
279         }
280     }
281 
282     private void parse(URI uri, final FsUriModifier modifier)
283     throws URISyntaxException {
284         uri = modifier.modify(uri, NODE_PATH);
285         if (null != uri.getRawFragment())
286             throw new QuotedUriSyntaxException(uri, "Fragment component not allowed");
287         if (uri.isOpaque()) {
288             final String ssp = uri.getRawSchemeSpecificPart();
289             final int i = ssp.lastIndexOf(FsMountPoint.SEPARATOR);
290             if (0 > i)
291                 throw new QuotedUriSyntaxException(uri,
292                         "Missing mount point separator \"" + FsMountPoint.SEPARATOR + '"');
293             final UriBuilder b = new UriBuilder(true);
294             mountPoint = new FsMountPoint(
295                     b.scheme(uri.getScheme())
296                      .path(ssp.substring(0, i + 2))
297                      .toUri(),
298                     modifier);
299             nodeName = new FsNodeName(
300                     b.clear()
301                      .pathQuery(ssp.substring(i + 2))
302                      .fragment(uri.getRawFragment())
303                      .toUri(),
304                     modifier);
305             if (NULL != modifier) {
306                 URI mpu = mountPoint.getUri();
307                 URI nuri = new URI(mpu.getScheme() + ':' + mpu.getRawSchemeSpecificPart() + nodeName.getUri());
308                 if (!uri.equals(nuri))
309                     uri = nuri;
310             }
311         } else if (uri.isAbsolute()) {
312             mountPoint = new FsMountPoint(uri.resolve(DOT), modifier);
313             nodeName = new FsNodeName(mountPoint.getUri().relativize(uri), modifier);
314         } else {
315             mountPoint = null;
316             nodeName = new FsNodeName(uri, modifier);
317             if (NULL != modifier)
318                 uri = nodeName.getUri();
319         }
320         this.uri = uri;
321 
322         assert invariants();
323     }
324 
325     private boolean invariants() {
326         assert null != getUri();
327         assert null == getUri().getRawFragment();
328         assert (null != getMountPoint()) == getUri().isAbsolute();
329         assert null != getNodeName();
330         if (getUri().isOpaque()) {
331             assert getUri().getRawSchemeSpecificPart().contains(FsMountPoint.SEPARATOR);
332             /*try {
333                 assert getUri().equals(new URI(getMountPoint().getUri().getScheme(), getMountPoint().getUri().getSchemeSpecificPart() + toDecodedUri(getNodeName()), null));
334             } catch (URISyntaxException ex) {
335                 throw new AssertionError(ex);
336             }*/
337         } else if (getUri().isAbsolute()) {
338             assert getUri().normalize() == getUri();
339             assert getUri().equals(getMountPoint().getUri().resolve(getNodeName().getUri()));
340         } else {
341             assert getUri().normalize() == getUri();
342             assert getNodeName().getUri() == getUri();
343         }
344         return true;
345     }
346 
347     /**
348      * Returns the URI for this node path.
349      *
350      * @return The URI for this node path.
351      */
352     public URI getUri() { return uri; }
353 
354     /**
355      * Returns a URI which is recursively transformed from the URI of this
356      * path so that it's absolute and hierarchical.
357      * If this path is already in absolute and hierarchical form, its URI gets
358      * returned.
359      * <p>
360      * For example, the path URIs {@code zip:file:/archive!/node} and
361      * {@code tar:file:/archive!/node} would both produce the hierarchical URI
362      * {@code file:/archive/node}.
363      *
364      * @return A URI which is recursively transformed from the URI of this
365      *         path so that it's absolute and hierarchical.
366      */
367     public URI toHierarchicalUri() {
368         final URI hierarchical = this.hierarchical;
369         if (null != hierarchical) return hierarchical;
370         if (uri.isOpaque()) {
371             final URI mpu = mountPoint.toHierarchicalUri();
372             final URI enu = nodeName.getUri();
373             try {
374                 return this.hierarchical = enu.toString().isEmpty()
375                         ? mpu
376                         : new UriBuilder(mpu, true)
377                             .path(mpu.getRawPath() + FsNodeName.SEPARATOR)
378                             .getUri()
379                             .resolve(enu);
380             } catch (URISyntaxException ex) {
381                 throw new AssertionError(ex);
382             }
383         } else {
384             return this.hierarchical = uri;
385         }
386     }
387 
388     /**
389      * Returns the mount point component or {@code null} iff this path's
390      * {@link #getUri() URI} is not absolute.
391      *
392      * @return The nullable mount point.
393      */
394     public @Nullable FsMountPoint getMountPoint() { return mountPoint; }
395 
396     /**
397      * Returns the node name component.
398      * This may be empty, but is never {@code null}.
399      *
400      * @return The node name component.
401      */
402     public FsNodeName getNodeName() { return nodeName; }
403 
404     /**
405      * Resolves the given node name against this path.
406      *
407      * @param  nodeName a node name relative to this path.
408      * @return A new path with an absolute URI.
409      */
410     public FsNodePath
411     resolve(final FsNodeName nodeName) {
412         if (nodeName.isRoot() && null == this.uri.getQuery()) return this;
413         return new FsNodePath(
414                 this.mountPoint,
415                 new FsNodeName(this.nodeName, nodeName));
416     }
417 
418     /**
419      * Implements a natural ordering which is consistent with
420      * {@link #equals(Object)}.
421      */
422     @Override
423     public int compareTo(FsNodePath that) {
424         return this.uri.compareTo(that.uri);
425     }
426 
427     /**
428      * Returns {@code true} iff the given object is a path name and its URI
429      * {@link URI#equals(Object) equals} the URI of this path name.
430      * Note that this ignores the mount point and node name.
431      */
432     @Override
433     public boolean equals(@CheckForNull Object that) {
434         return this == that
435                 || that instanceof FsNodePath
436                     && this.uri.equals(((FsNodePath) that).uri);
437     }
438 
439     /**
440      * Returns a hash code which is consistent with {@link #equals(Object)}.
441      */
442     @Override
443     public int hashCode() { return uri.hashCode(); }
444 
445     /**
446      * Equivalent to calling {@link URI#toString()} on {@link #getUri()}.
447      */
448     @Override
449     public String toString() { return uri.toString(); }
450 }