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