Start line:  
End line:  

Snippet Preview

Snippet HTML Code

Stack Overflow Questions
   /*
    * Copyright 1&1 Internet AG, http://www.1and1.org
    *
    * This program is free software; you can redistribute it and/or modify
    * it under the terms of the GNU Lesser General Public License as published by
    * the Free Software Foundation; either version 2 of the License,
    * or (at your option) any later version.
    *
    * This program is distributed in the hope that it will be useful,
   * but WITHOUT ANY WARRANTY; without even the implied warranty of
   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
   * See the GNU Lesser General Public License for more details.
   *
   * You should have received a copy of the GNU Lesser General Public License
   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
   */
  
  package net.sf.beezle.sushi.fs;
  
  
  import java.io.Reader;
  import java.io.Writer;
  import java.net.URI;
  import java.util.Arrays;
  import java.util.List;

Abstraction from a file: something stored under a path that you can get an input stream from or output stream to. FileNode is the most prominent example of a node. The api is similar to java.world.File. It provides the same functionality, adds some methods useful for scripting, and removes some redundant methods to simplify api (in particular the constructors).

A node is identified by a URI, whose most important part is the path.

The path is a sequence of names separated by /, even for files on windows. It never starts or ends with a separator. It does not include the root, but it always includes the path of the base. A node with an empty path is called root node. That last part of the path is the name. Paths are always specified decoded, but URIs always contain encoded paths.

The base is a node this node is relative to. It's optional, a node without base is called absolute. It's use to simplify (shorten!) toString output.

Your application usually creates some "working-directory" nodes with world.node(URI). They will be used to create actual working nodes with node.join(path). The constructor of the respective node class is rarely used directly, it's used indirectly by the filesystem.

A node is immutable, except for its base.

Method names try to be short, but no abbreviations. Exceptions from this rule are mkfile, mkdir and mklink, because mkdir is a well-established name.

If an Implementation cannot (or does not want to) implement a method (e.g. move), it throws an UnsupportedOperationException.

You can read nodes using traditional InputStream or Writers. In addition to this pull-logic, the writoTo method provides push logic. Some Node implementations are more efficient when using writeTo(). (I'd appriciate if all underlying libraries provides pull logic because, push logic can be efficiently implemented on top ...)

As long as you stick to read operations, nodes are thread-save.

  
  public abstract class Node {
      protected UnsupportedOperationException unsupported(String op) {
          return new UnsupportedOperationException(getURI() + ":" + op);
      }
  
      public abstract Root<?> getRoot();
  
      public Node getRootNode() {
          return getRoot().node(""null);
      }
  
     public World getWorld() {
         return getRoot().getFilesystem().getWorld();
     }

    
Creates a stream to read this node. Closing the stream more than once is ok, but reading from a closed stream is rejected by an exception
 
     public abstract InputStream createInputStream() throws IOException;

    
Concenience method for writeTo(dest, 0).

Returns:
bytes actually written
Throws:
java.io.FileNotFoundException when this node is not a file
WriteToException for other errors
 
     public long writeTo(OutputStream destthrows WriteToExceptionFileNotFoundException {
         return writeTo(dest, 0);
     }

    
Writes all bytes except "skip" initial bytes of this node to out. Without closing out afterwards. Writes nothing if this node has less than skip bytes.

Returns:
bytes actually written
Throws:
java.io.FileNotFoundException when this node is not a file
WriteToException for other errors
 
     public abstract long writeTo(OutputStream destlong skipthrows WriteToExceptionFileNotFoundException;
 
     public long writeToImpl(OutputStream destlong skipthrows WriteToExceptionFileNotFoundException {
         InputStream src;
         long result;
 
         try {
             src = createInputStream();
             if (skip(srcskip)) {
                 return 0;
             }
             result = getWorld().getBuffer().copy(srcdest);
             src.close();
         } catch (FileNotFoundException e) {
             throw e;
         } catch (IOException e) {
             throw new WriteToException(thise);
         }
         return result;
     }

    

True:
when EOF was seen
 
     public static boolean skip(InputStream srclong countthrows IOException {
         long step;
         int c;
 
         while (count > 0) {
             step = src.skip(count);
             if (step == 0) {
                 // ByteArrayInputStream just return 0 when at end of file
                 c = src.read();
                 if (c < 0) {
                     // EOF
                     src.close();
                     return true;
                 } else {
                     count--;
                 }
             } else {
                 count -= step;
             }
         }
         return false;
     }
 
     public OutputStream createOutputStream() throws IOException {
         return createOutputStream(false);
     }
     public OutputStream createAppendStream() throws IOException {
         return createOutputStream(true);
     }

    
Create a stream to write this node. Closing the stream more than once is ok, but writing to a closed stream is rejected by an exception.
 
     public abstract OutputStream createOutputStream(boolean appendthrows IOException;

    
Lists child nodes of this node.

Returns:
List of child nodes or null if this node is a file. Note that returning null allows for optimizations because list() may be called on any existing node; otherwise, you'd have to inspect the resulting exception whether you called list on a file.
Throws:
ListException if this does not exist (in this case, cause is set to a FileNotFoundException), permission is denied, or another IO problem occurs.
 
     public abstract List<? extends Nodelist() throws ListException;

    
Fails if the directory already exists. Features define whether is operation is atomic.

Returns:
this
 
     public abstract Node mkdir() throws MkdirException;

    
Fails if the directory already exists. Features define whether this operation is atomic. This default implementation is not atomic.

Returns:
this
 
     public Node mkfile() throws MkfileException {
     	try {
 			if (exists()) {
 				throw new MkfileException(this);
 			}
 		} catch (IOException e) {
 			throw new MkfileException(thise);
 		}
 		return this;
     }


    
Deletes this node, no matter if it's a file or a directory. If this is a link, the link is deleted, not the link target.

Returns:
this
 
     public abstract Node deleteTree() throws DeleteException;

    

Throws:
DeleteException if this is not file
 
     public abstract Node deleteFile() throws DeleteException;

    

Throws:
DeleteException if this is not a directory or the directory is not empty
 
     public abstract Node deleteDirectory() throws DeleteException;

    
Moves this file or directory to dest. Throws an exception if this does not exist or if dest already exists. This method is a default implementation with copy and delete, derived classes should override it with a native implementation when available.

Returns:
dest
 
     public Node move(Node destthrows MoveException {
         try {
             dest.checkNotExists();
             copy(dest);
             deleteTree();
         } catch (IOException e) {
             throw new MoveException(thisdest"move failed"e);
         }
         return dest;
     }
 
     //-- status methods
 
    
Throws a LengthException if this node is not a file.
 
     public abstract long length() throws LengthException;

    

Returns:
true if the file exists, even if it's a dangling link
 
     public abstract boolean exists() throws ExistsException;
 
     public abstract boolean isFile() throws ExistsException;
     public abstract boolean isDirectory() throws ExistsException;
     public abstract boolean isLink() throws ExistsException;

    
Throws an exception is the file does not exist
 
     public abstract long getLastModified() throws GetLastModifiedException;
     public abstract void setLastModified(long millisthrows SetLastModifiedException;
 
     public abstract int getMode() throws IOException;
     public abstract void setMode(int modethrows IOException;
 
     public abstract int getUid() throws IOException;
     public abstract void setUid(int idthrows IOException;
     public abstract int getGid() throws IOException;
     public abstract void setGid(int idthrows IOException;
 
     //-- path functionality
 
    
Never starts or end with a slash or a drive; an empty string is the root path. The path is decoded, you have to encoded if you want to build an URI.
 
     public abstract String getPath();

    

Returns:
a normalized URI, not necessarily the URI this node was created from
 
     public URI getURI() {
         return URI.create(getRoot().getFilesystem().getScheme() + ":" + getRoot().getId() + encodePath(getPath()));
     }


    

Returns:
the last path segment (or an empty string for the root node
 
     public String getName() {
         String path;
 
         path = getPath();
         // ok for -1:
         return path.substring(path.lastIndexOf(.) + 1);
     }
 
     public String getExtension() {
         String name;
         int idx;
 
         name = getName();
         idx = name.lastIndexOf('.');
         if (idx <= 0 || idx == name.length() - 1) {
             return "";
         }
         return name.substring(idx + 1);
     }
 
 
     public abstract Node getParent();
     protected Node doGetParent() {
         String path;
         int idx;
 
         path = getPath();
         if ("".equals(path)) {
             return null;
         }
         idx = path.lastIndexOf(.);
         if (idx == -1) {
             return getRoot().node(""null);
         } else {
             return getRoot().node(path.substring(0, idx), null);
         }
     }
 
     public boolean hasDifferentAnchestor(Node anchestor) {
         Node parent;
 
         parent = getParent();
         if (parent == null) {
             return false;
         } else {
             return parent.hasAnchestor(anchestor);
         }
     }
 
     public boolean hasAnchestor(Node anchestor) {
         Node current;
 
         current = this;
         while (true) {
             if (current.equals(anchestor)) {
                 return true;
             }
             current = current.getParent();
             if (current == null) {
                 return false;
             }
         }
     }

    

Returns:
kind of a path, with . and .. where appropriate.
 
     public String getRelative(Node base) {
         String startfilepath;
         String destpath;
         String common;
         StringBuilder result;
         int len;
         int ups;
         int i;
 
         if (base.equals(this)) {
             return ".";
         }
         startfilepath = base.join("foo").getPath();
         destpath = getPath();
         common = Strings.getCommon(startfilepathdestpath);
         common = common.substring(0, common.lastIndexOf(.) + 1);  // ok for idx == -1
         len = common.length();
         startfilepath = startfilepath.substring(len);
         destpath = destpath.substring(len);
         result = new StringBuilder();
         ups = Strings.count(startfilepath.);
         for (i = 0; i < upsi++) {
             result.append("..").append(.);
         }
         result.append(destpath);
         return result.toString();
     }
 
     public abstract Node join(List<Stringpaths);
 
     protected Node doJoin(List<Stringpaths) {
         Root<?> root;
         Node result;
 
         root = getRoot();
         result = root.node(root.getFilesystem().join(getPath(), paths), null);
         return result;
     }
 
     public abstract Node join(String... names);
 
     public Node doJoin(String... names) {
         return join(Arrays.asList(names));
     }
 
     //-- input stream functionality
 
     public NodeReader createReader() throws IOException {
         return NodeReader.create(this);
     }
 
         return new ObjectInputStream(createInputStream());
     }

    
Reads all bytes of the node. Default implementation that works for all nodes: reads the file in chunks and builds the result in memory. Derived classes should override it if they can provide a more efficient implementation, e.g. by determining the length first if getting the length is cheap.

Returns:
Throws:
java.io.IOException
 
     public byte[] readBytes() throws IOException {
         InputStream src;
         byte[] result;
         Buffer buffer;
 
         src = createInputStream();
         buffer = getWorld().getBuffer();
         synchronized (buffer) {
             result = buffer.readBytes(src);
         }
         src.close();
         return result;
     }

    
Reads all chars of the node. Do not use this method on large files because it's memory consuming: the string is created from the byte array returned by readBytes.
 
     public String readString() throws IOException {
         return getWorld().getSettings().string(readBytes());
     }

    

Returns:
lines without tailing line separator
 
     public List<StringreadLines() throws IOException {
         return readLines(getWorld().getSettings().);
     }

    

Returns:
lines without tailing line separator
 
     public List<StringreadLines(LineFormat formatthrows IOException {
         return new LineReader(createReader(), format).collect();
     }

    
Reads properties with the encoding for this node
 
     public Properties readProperties() throws IOException {
         Properties p;
         Reader src;
 
         src = createReader();
         p = new Properties();
         p.load(src);
         src.close();
         return p;
     }
 
     public Object readObject() throws IOException {
         ObjectInputStream src;
         Object result;
 
         src = createObjectInputStream();
         try {
             result = src.readObject();
         } catch (ClassNotFoundException e) {
             throw new RuntimeException(e);
         }
         src.close();
         return result;
     }
 
     public Document readXml() throws IOExceptionSAXException {
         Builder builder;
 
         builder = getWorld().getXml().getBuilder();
         synchronized (builder) {
             return builder.parse(this);
         }
     }
 
         InputStream in;
         Templates templates;
 
         in = createInputStream();
         templates = Serializer.templates(new SAXSource(new InputSource(in)));
         in.close();
         return templates.newTransformer();
     }
 
     public void xslt(Transformer transformerNode destthrows IOExceptionTransformerException {
         InputStream in;
         OutputStream out;
 
         in = createInputStream();
         out = dest.createOutputStream();
         transformer.transform(new StreamSource(in), new StreamResult(out));
         out.close();
         in.close();
     }
 
     //--
 
     public Node checkExists() throws IOException {
         if (!exists()) {
             throw new IOException("no such file or directory: " + this);
         }
         return this;
     }
 
     public Node checkNotExists() throws IOException {
         if (exists()) {
             throw new IOException("file or directory already exists: " + this);
         }
         return this;
     }
 
     public Node checkDirectory() throws ExistsExceptionFileNotFoundException {
         if (isDirectory()) {
             return this;
         }
         if (exists()) {
             throw new FileNotFoundException("directory expected: " + this);
         } else {
             throw new FileNotFoundException("no such directory: " + this);
         }
     }
 
     public Node checkFile() throws ExistsExceptionFileNotFoundException {
         if (isFile()) {
             return this;
         }
         if (exists()) {
             throw new FileNotFoundException("file expected: " + this);
         } else {
             throw new FileNotFoundException("no such file: " + this);
         }
     }
 
     //--
 
    
Creates an absolute link. The signature of this method resembles the copy method.

Parameters:
dest symlink to be created
Returns:
dest;
 
     public Node link(Node destthrows LinkException {
         if (!getClass().equals(dest.getClass())) {
             throw new IllegalArgumentException(this.getClass() + " vs " + dest.getClass());
         }
         try {
             checkExists();
         } catch (IOException e) {
             throw new LinkException(thise);
         }
         // TODO: getRoot() for ssh root ...
         dest.mklink(. + this.getPath());
         return dest;
     }

    
Creates this link, pointing to the specified path. Throws an exception if this already exists or if the parent does not exist; the target is not checked, it may be absolute or relative
 
     public abstract void mklink(String paththrows LinkException;

    
Returns the link target of this file or throws an exception.
 
     public abstract String readLink() throws ReadLinkException;

    
Throws an exception if this is not a link.
 
     public Node resolveLink() throws ReadLinkException {
         String path;
 
         path = readLink();
         if (path.startsWith(.)) {
             return getRoot().node(path.substring(1), null);
         } else {
             return getParent().join(path);
         }
     }
 
     public void copy(Node destthrows CopyException {
         try {
             if (isDirectory()) {
                 dest.mkdirOpt();
                 copyDirectory(dest);
             } else {
                 copyFile(dest);
             }
         } catch (CopyException e) {
             throw e;
         } catch (IOException e) {
             throw new CopyException(thisdeste);
         }
     }

    
Overwrites dest if it already exists.

Returns:
dest
 
     public Node copyFile(Node destthrows CopyException {
         InputStream in;
 
         try {
             in = createInputStream();
             getWorld().getBuffer().copy(indest);
             in.close();
             return dest;
         } catch (IOException e) {
             throw new CopyException(thisdeste);
         }
     }

    
Convenience method for copy with filters below.

Returns:
list of files and directories created
 
     public List<NodecopyDirectory(Node destthrows CopyException {
         return copyDirectory(destgetWorld().filter().includeAll());
     }

    
Throws an exception is this or dest is not a directory. Overwrites existing files in dest.

Returns:
list of files and directories created
 
     public List<NodecopyDirectory(Node destdirFilter filterthrows CopyException {
         return new Copy(thisfilter).directory(destdir);
     }
 
     //-- diff
 
     public String diffDirectory(Node rightdirthrows IOException {
         return diffDirectory(rightdirfalse);
     }
 
     public String diffDirectory(Node rightdirboolean briefthrows IOException {
         return new Diff(brief).directory(thisrightdirgetWorld().filter().includeAll());
     }

    
cheap diff if you only need a yes/no answer
 
     public boolean diff(Node rightthrows IOException {
         return diff(rightnew Buffer(getWorld().getBuffer()));
     }

    
cheap diff if you only need a yes/no answer
 
     public boolean diff(Node rightBuffer rightBufferthrows IOException {
         InputStream leftSrc;
         InputStream rightSrc;
         Buffer leftBuffer;
         int leftChunk;
         int rightChunk;
         boolean[] leftEof;
         boolean[] rightEof;
         boolean result;
 
         leftBuffer = getWorld().getBuffer();
         leftSrc = createInputStream();
         leftEof = new boolean[] { false };
         rightSrc = right.createInputStream();
         rightEof = new boolean[] { false };
         result = false;
         do {
             leftChunk = leftEof[0] ? 0 : leftBuffer.fill(leftSrcleftEof);
             rightChunk = rightEof[0] ? 0 : rightBuffer.fill(rightSrcrightEof);
             if (leftChunk != rightChunk || leftBuffer.diff(rightBufferleftChunk)) {
                 result = true;
                 break;
             }
         } while (leftChunk > 0);
     	leftSrc.close();
     	rightSrc.close();
         return result;
     }
 
     //-- search for child nodes
 
    
uses default excludes
 
     public List<Nodefind(String... includesthrows IOException {
         return find(getWorld().filter().include(includes));
     }
 
     public Node findOne(String includethrows IOException {
         Node found;
 
         found = findOpt(include);
         if (found == null) {
             throw new FileNotFoundException(toString() + ": not found: " + include);
         }
         return found;
     }
 
     public Node findOpt(String includethrows IOException {
         List<Nodefound;
 
         found = find(include);
         switch (found.size()) {
         case 0:
             return null;
         case 1:
             return found.get(0);
         default:
             throw new IOException(toString() + ": ambiguous: " + include);
         }
     }
 
     public List<Nodefind(Filter filterthrows IOException {
         return filter.collect(this);
     }
 
     //--
 
     public Node deleteFileOpt() throws IOException {
         if (exists()) {
             deleteFile();
         }
         return this;
     }
 
     public Node deleteDirectoryOpt() throws IOException {
         if (exists()) {
             deleteDirectory();
         }
         return this;
     }
 
     public Node deleteTreeOpt() throws IOException {
         if (exists()) {
             deleteTree();
         }
         return this;
     }
 
     public Node mkdirOpt() throws MkdirException {
         try {
 			if (!isDirectory()) {
 			    mkdir(); // fail here if it's a file!
 			}
 		} catch (ExistsException e) {
 			throw new MkdirException(thise);
 		}
         return this;
     }
 
     public Node mkdirsOpt() throws MkdirException {
         Node parent;
 
         try {
 			if (!isDirectory()) {
 			    parent = getParent();
 			    if (parent != null) {
 			        parent.mkdirsOpt();
 			    }
 			    mkdir(); // fail here if it's a file!
 			}
 		} catch (ExistsException e) {
 			throw new MkdirException(thise);
 		}
         return this;
     }
 
     public Node mkdirs() throws MkdirException {
     	try {
     		if (exists()) {
     			throw new MkdirException(this);
     		}
     	    return mkdirsOpt();
     	} catch (IOException e) {
     		throw new MkdirException(thise);
     	}
     }
 
     //-- output create functionality
 
     public NodeWriter createWriter() throws IOException {
         return createWriter(false);
     }
 
     public NodeWriter createAppender() throws IOException {
         return createWriter(true);
     }
 
     public NodeWriter createWriter(boolean appendthrows IOException {
         return NodeWriter.create(thisappend);
     }
 
         return new ObjectOutputStream(createOutputStream());
     }
 
     public Node writeBytes(byte ... bytesthrows IOException {
         return writeBytes(bytes, 0, bytes.lengthfalse);
     }
 
     public Node appendBytes(byte ... bytesthrows IOException {
         return writeBytes(bytes, 0, bytes.lengthtrue);
     }
 
     public Node writeBytes(byte[] bytesint ofsint lenboolean appendthrows IOException {
         OutputStream out;
 
         out = createOutputStream(append);
         out.write(bytesofslen);
         out.close();
         return this;
     }
 
     public Node writeChars(char ... charsthrows IOException {
         return writeChars(chars, 0, chars.lengthfalse);
     }
 
     public Node appendChars(char ... charsthrows IOException {
         return writeChars(chars, 0, chars.lengthtrue);
     }
 
     public Node writeChars(char[] charsint ofsint lenboolean appendthrows IOException {
         Writer out;
 
         out = createWriter(append);
         out.write(charsofslen);
         out.close();
         return this;
     }
 
     public Node writeString(String txtthrows IOException {
         Writer w;
 
         w = createWriter();
         w.write(txt);
         w.close();
         return this;
     }
 
     public Node appendString(String txtthrows IOException {
         Writer w;
 
         w = createAppender();
         w.write(txt);
         w.close();
         return this;
     }
 
     public Node writeStrings(String ... strthrows IOException {
         return writeStrings(Arrays.asList(str));
     }
 
     public Node writeStrings(List<Stringstringsthrows IOException {
         return strings(createWriter(), strings);
     }
 
     public Node appendStrings(String ... strthrows IOException {
         return appendStrings(Arrays.asList(str));
     }
 
     public Node appendStrings(List<Stringstringsthrows IOException {
         return strings(createAppender(), strings);
     }
 
     private Node strings(Writer destList<Stringstringsthrows IOException {
         for (String str : strings) {
             dest.write(str);
         }
         dest.close();
         return this;
     }

    

Parameters:
line without tailing line separator
 
     public Node writeLines(String ... linethrows IOException {
         return writeLines(Arrays.asList(line));
     }

    

Parameters:
lines without tailing line separator
 
     public Node writeLines(List<Stringlinesthrows IOException {
         return lines(createWriter(), lines);
     }

    

Parameters:
line without tailing line separator
 
     public Node appendLines(String ... linethrows IOException {
         return appendLines(Arrays.asList(line));
     }

    

Parameters:
lines without tailing line separator
 
     public Node appendLines(List<Stringlinesthrows IOException {
         return lines(createAppender(), lines);
     }

    

Parameters:
lines without tailing line separator
 
     private Node lines(Writer destList<Stringlinesthrows IOException {
         String separator;
 
         separator = getWorld().getSettings()..getSeparator();
         for (String line : lines) {
             dest.write(line);
             dest.write(separator);
         }
         dest.close();
         return this;
     }
 
     public Node writeProperties(Properties pthrows IOException {
         return writeProperties(pnull);
     }
 
     public Node writeProperties(Properties pString commentthrows IOException {
         Writer dest;
 
         dest = createWriter();
         p.store(destcomment);
         dest.close();
         return this;
     }
 
     public Node writeObject(Serializable objthrows IOException {
         ObjectOutputStream out;
 
         out = createObjectOutputStream();
         out.writeObject(obj);
         out.close();
         return this;
     }

    
Convenience method for writeXml(node, true);
 
     public Node writeXml(org.w3c.dom.Node nodethrows IOException {
         return writeXml(nodetrue);
     }

    
Write the specified node into this file. Adds indentation/newlines when format is true. Otherwise, writes the document "as is" (but always prefixes the document with an xml declaration and encloses attributes in double quotes).

Returns:
this node
 
     public Node writeXml(org.w3c.dom.Node nodeboolean formatthrows IOException {
         getWorld().getXml().getSerializer().serialize(nodethisformat);
         return this;
     }
 
     //-- other
 
     public void gzip(Node destthrows IOException {
         InputStream in;
         OutputStream out;
 
         in = createInputStream();
         out = new GZIPOutputStream(dest.createOutputStream());
         getWorld().getBuffer().copy(inout);
         in.close();
         out.close();
     }
 
     public void gunzip(Node destthrows IOException {
         InputStream in;
         OutputStream out;
 
         in = new GZIPInputStream(createInputStream());
         out = dest.createOutputStream();
         getWorld().getBuffer().copy(inout);
         in.close();
         out.close();
     }
 
     public String sha() throws IOException {
         try {
             return digest("SHA");
         } catch (NoSuchAlgorithmException e) {
             throw new RuntimeException(e);
         }
     }
 
     public String md5() throws IOException {
         try {
             return digest("MD5");
         } catch (NoSuchAlgorithmException e) {
             throw new RuntimeException(e);
         }
     }
 
     public byte[] digestBytes(String namethrows IOExceptionNoSuchAlgorithmException {
         InputStream src;
         MessageDigest digest;
         Buffer buffer;
 
         src =  createInputStream();
         digest = MessageDigest.getInstance(name);
         synchronized (digest) {
             buffer = getWorld().getBuffer();
             synchronized (buffer) {
                buffer.digest(srcdigest);
            }
            src.close();
            return digest.digest();
        }
    }
    public String digest(String namethrows IOExceptionNoSuchAlgorithmException {
        return Strings.toHex(digestBytes(name));
    }
    //-- Object functionality
    @Override
    public boolean equals(Object obj) {
        Node node;
        if (obj == null || !getClass().equals(obj.getClass())) {
            return false;
        }
        node = (Nodeobj;
        if (!getPath().equals(node.getPath())) {
            return false;
        }
        return getRoot().equals(node.getRoot());
    }
    @Override
    public int hashCode() {
        return getPath().hashCode();
    }

    
Returns a String representation suitable for messages. CAUTION: don't use to convert to a string, use instead.
    @Override
    public String toString() {
        Node working;
        working = getWorld().getWorking();
        if (working == null || !getRoot().equals(working.getRoot())) {
            return getURI().toString();
        } else {
            if (hasAnchestor(working)) {
                return getRelative(working);
            } else {
                return . + getPath();
            }
        }
    }
    //--

    
TODO: is there a better way ... ?
    public static String encodePath(String path) {
        URI tmp;
        try {
            tmp = new URI("foo""host""/" + pathnull);
        } catch (URISyntaxException e) {
            throw new IllegalStateException(e);
        }
        return tmp.getRawPath().substring(1);
    }

    
TODO: is there a better way ... ?
    public static String decodePath(String path) {
        URI tmp;
        try {
            tmp = new URI("scheme://host/" + path);
        } catch (URISyntaxException e) {
            throw new IllegalStateException(e);
        }
        return tmp.getPath().substring(1);
    }