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