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 }