diff --git a/apps/jetty/java/src/org/mortbay/util/FileResource.java b/apps/jetty/java/src/org/mortbay/util/FileResource.java
new file mode 100644
index 0000000000000000000000000000000000000000..8788f14fdfb59228ee3c40c0fd351ebb90a2d9b6
--- /dev/null
+++ b/apps/jetty/java/src/org/mortbay/util/FileResource.java
@@ -0,0 +1,352 @@
+// ========================================================================
+// $Id: FileResource.java,v 1.31 2006/01/04 13:55:31 gregwilkins Exp $
+// Copyright 1996-2004 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at 
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ========================================================================
+package org.mortbay.util;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.MalformedURLException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.net.URLConnection;
+import java.security.Permission;
+
+import org.apache.commons.logging.Log;
+import org.mortbay.log.LogFactory;
+
+
+/* ------------------------------------------------------------ */
+/** File Resource.
+ *
+ * Handle resources of implied or explicit file type.
+ * This class can check for aliasing in the filesystem (eg case
+ * insensitivity).  By default this is turned on if the platform does
+ * not have the "/" path separator, or it can be controlled with the
+ * "org.mortbay.util.FileResource.checkAliases" system parameter.
+ *
+ * If alias checking is turned on, then aliased resources are
+ * treated as if they do not exist, nor can they be created.
+ *
+ * @version $Revision: 1.31 $
+ * @author Greg Wilkins (gregw)
+ */
+public class FileResource extends URLResource
+{
+	private static Log log = LogFactory.getLog(Credential.class);
+    private static boolean __checkAliases;
+    static
+    {
+        __checkAliases=
+            "true".equalsIgnoreCase
+            (System.getProperty("org.mortbay.util.FileResource.checkAliases","true"));
+ 
+       if (__checkAliases)
+            log.info("Checking Resource aliases");
+    }
+    
+    /* ------------------------------------------------------------ */
+    private File _file;
+    private transient URL _alias=null;
+    private transient boolean _aliasChecked=false;
+
+    /* ------------------------------------------------------------------------------- */
+    /** setCheckAliases.
+     * @param checkAliases True of resource aliases are to be checked for (eg case insensitivity or 8.3 short names) and treated as not found.
+     */
+    public static void setCheckAliases(boolean checkAliases)
+    {
+        __checkAliases=checkAliases;
+    }
+
+    /* ------------------------------------------------------------------------------- */
+    /** getCheckAliases.
+     * @return True of resource aliases are to be checked for (eg case insensitivity or 8.3 short names) and treated as not found.
+     */
+    public static boolean getCheckAliases()
+    {
+        return __checkAliases;
+    }
+    
+    /* -------------------------------------------------------- */
+    FileResource(URL url)
+        throws IOException, URISyntaxException
+    {
+        super(url,null);
+
+        try
+        {
+            // Try standard API to convert URL to file.
+            _file =new File(new URI(url.toString()));
+        }
+        catch (Exception e)
+        {
+            LogSupport.ignore(log,e);
+            try
+            {
+                // Assume that File.toURL produced unencoded chars. So try
+                // encoding them.
+                String urls=
+                    "file:"+org.mortbay.util.URI.encodePath(url.toString().substring(5));
+                _file =new File(new URI(urls));
+            }
+            catch (Exception e2)
+            {
+                LogSupport.ignore(log,e2);
+
+                // Still can't get the file.  Doh! try good old hack!
+                checkConnection();
+                Permission perm = _connection.getPermission();
+                _file = new File(perm==null?url.getFile():perm.getName());
+            }
+        }
+        
+        if (_file.isDirectory() && !_urlString.endsWith("/"))
+            _urlString=_urlString+"/";
+    }
+    
+    /* -------------------------------------------------------- */
+    FileResource(URL url, URLConnection connection, File file)
+    {
+        super(url,connection);
+        _file=file;
+        if (_file.isDirectory() && !_urlString.endsWith("/"))
+            _urlString=_urlString+"/";
+    }
+    
+    /* -------------------------------------------------------- */
+    public Resource addPath(String path)
+        throws IOException,MalformedURLException
+    {
+        FileResource r=null;
+
+        if (!isDirectory())
+        {
+            r=(FileResource)super.addPath(path);
+        }
+        else
+        {
+            path = org.mortbay.util.URI.canonicalPath(path);
+            
+            // treat all paths being added as relative
+            String rel=path;
+            if (path.startsWith("/"))
+                rel = path.substring(1);
+            
+            File newFile = new File(_file,rel.replace('/', File.separatorChar));
+            r=new FileResource(newFile.toURI().toURL(),null,newFile);
+        }
+        
+        String encoded=org.mortbay.util.URI.encodePath(path);
+        int expected=r._urlString.length()-encoded.length();
+        int index = r._urlString.lastIndexOf(encoded, expected);
+        
+        if (expected!=index && ((expected-1)!=index || path.endsWith("/") || !r.isDirectory()))
+        {
+            r._alias=r._url;
+            r._aliasChecked=true;
+        }                   
+        return r;
+    }
+   
+    
+    /* ------------------------------------------------------------ */
+    public URL getAlias()
+    {
+        if (__checkAliases) {
+          if (!_aliasChecked)
+          {
+            try
+            {    
+                String abs=_file.getAbsolutePath();
+                String can=_file.getCanonicalPath();
+                
+                if (abs.length()!=can.length() || !abs.equals(can))
+                    _alias=new File(can).toURI().toURL();
+                
+                _aliasChecked=true;
+
+                if (_alias!=null && log.isDebugEnabled())
+                {
+                    log.debug("ALIAS abs="+abs);
+                    log.debug("ALIAS can="+can);
+                }
+            }
+            catch(Exception e)
+            {
+                log.warn(LogSupport.EXCEPTION,e);
+                return getURL();
+            }
+          }                
+        } else return null;
+        return _alias;
+    }
+    
+    /* -------------------------------------------------------- */
+    /**
+     * Returns true if the resource exists.
+     */
+    public boolean exists()
+    {
+        return _file.exists();
+    }
+        
+    /* -------------------------------------------------------- */
+    /**
+     * Returns the last modified time
+     */
+    public long lastModified()
+    {
+        return _file.lastModified();
+    }
+
+    /* -------------------------------------------------------- */
+    /**
+     * Returns true if the respresenetd resource is a container/directory.
+     */
+    public boolean isDirectory()
+    {
+        return _file.isDirectory();
+    }
+
+    /* --------------------------------------------------------- */
+    /**
+     * Return the length of the resource
+     */
+    public long length()
+    {
+        return _file.length();
+    }
+        
+
+    /* --------------------------------------------------------- */
+    /**
+     * Returns the name of the resource
+     */
+    public String getName()
+    {
+        return _file.getAbsolutePath();
+    }
+        
+    /* ------------------------------------------------------------ */
+    /**
+     * Returns an File representing the given resource or NULL if this
+     * is not possible.
+     */
+    public File getFile()
+    {
+        return _file;
+    }
+        
+    /* --------------------------------------------------------- */
+    /**
+     * Returns an input stream to the resource
+     */
+    public InputStream getInputStream() throws IOException
+    {
+        return new FileInputStream(_file);
+    }
+        
+    /* --------------------------------------------------------- */
+    /**
+     * Returns an output stream to the resource
+     */
+    public OutputStream getOutputStream()
+        throws java.io.IOException, SecurityException
+    {
+        return new FileOutputStream(_file);
+    }
+        
+    /* --------------------------------------------------------- */
+    /**
+     * Deletes the given resource
+     */
+    public boolean delete()
+        throws SecurityException
+    {
+        return _file.delete();
+    }
+
+    /* --------------------------------------------------------- */
+    /**
+     * Rename the given resource
+     */
+    public boolean renameTo( Resource dest)
+        throws SecurityException
+    {
+        if( dest instanceof FileResource)
+            return _file.renameTo( ((FileResource)dest)._file);
+        else
+            return false;
+    }
+
+    /* --------------------------------------------------------- */
+    /**
+     * Returns a list of resources contained in the given resource
+     */
+    public String[] list()
+    {
+        String[] list =_file.list();
+        if (list==null)
+            return null;
+        for (int i=list.length;i-->0;)
+        {
+            if (new File(_file,list[i]).isDirectory() &&
+                !list[i].endsWith("/"))
+                list[i]+="/";
+        }
+        return list;
+    }
+         
+    /* ------------------------------------------------------------ */
+    /** Encode according to this resource type.
+     * File URIs are encoded.
+     * @param uri URI to encode.
+     * @return The uri unchanged.
+     */
+    public String encode(String uri)
+    {
+        return uri;
+    }
+    
+    /* ------------------------------------------------------------ */
+    /** 
+     * @param o
+     * @return 
+     */
+    public boolean equals( Object o)
+    {
+        if (this == o)
+            return true;
+
+        if (null == o || ! (o instanceof FileResource))
+            return false;
+
+        FileResource f=(FileResource)o;
+        return f._file == _file || (null != _file && _file.equals(f._file));
+    }
+
+    /* ------------------------------------------------------------ */
+    /**
+     * @return the hashcode.
+     */
+    public int hashCode()
+    {
+       return null == _file ? super.hashCode() : _file.hashCode();
+    }
+}
diff --git a/apps/jetty/java/src/org/mortbay/util/Resource.java b/apps/jetty/java/src/org/mortbay/util/Resource.java
index 4180b8b011ccf33c3e45b5a1da9cf702cc08cc86..ed992cfd55f482ab7d0fa8a4cd3918fcc6c1cba0 100644
--- a/apps/jetty/java/src/org/mortbay/util/Resource.java
+++ b/apps/jetty/java/src/org/mortbay/util/Resource.java
@@ -392,7 +392,7 @@ public abstract class Resource implements Serializable
             buf.append(path);
             buf.append("\">");
             buf.append(StringUtil.replace(StringUtil.replace(ls[i],"<","&lt;"),">","&gt;"));
-            buf.append("&nbsp;");
+            buf.append("</A>&nbsp;");
             buf.append("</TD><TD ALIGN=right>");
             buf.append(item.length());
             buf.append(" bytes&nbsp;</TD><TD>");
diff --git a/apps/jetty/java/src/org/mortbay/util/URI.java b/apps/jetty/java/src/org/mortbay/util/URI.java
new file mode 100644
index 0000000000000000000000000000000000000000..d5543832b77d67ff3e8c73a351e05b6f061e656f
--- /dev/null
+++ b/apps/jetty/java/src/org/mortbay/util/URI.java
@@ -0,0 +1,995 @@
+// ========================================================================
+// $Id: URI.java,v 1.40 2009/05/16 02:02:00 gregwilkins Exp $
+// Copyright 199-2004 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at 
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ========================================================================
+package org.mortbay.util;
+
+import java.io.UnsupportedEncodingException;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.commons.logging.Log;
+import org.mortbay.log.LogFactory;
+
+/* ------------------------------------------------------------ */
+/** URI Holder.
+ * This class assists with the decoding and encoding or HTTP URI's.
+ * It differs from the java.net.URL class as it does not provide
+ * communications ability, but it does assist with query string
+ * formatting.
+ * <P>ISO_8859_1 encoding is used by default for % encoded characters. This
+ * may be overridden with the org.mortbay.util.URI.charset system property.
+ * @see UrlEncoded
+ * @version $Id: URI.java,v 1.40 2009/05/16 02:02:00 gregwilkins Exp $
+ * @author Greg Wilkins (gregw)
+ */
+public class URI
+    implements Cloneable
+{
+    private static Log log = LogFactory.getLog(URI.class);
+
+    public static final String __CHARSET=System.getProperty("org.mortbay.util.URI.charset",StringUtil.__UTF_8);
+    public static final boolean __CHARSET_IS_DEFAULT=__CHARSET.equals(StringUtil.__UTF_8);
+    
+    /* ------------------------------------------------------------ */
+    private String _uri;
+    private String _scheme;
+    private String _host;
+    private int _port;
+    private String _path;
+    private String _encodedPath;
+    private String _query;
+    private UrlEncoded _parameters;
+    private boolean _dirty;
+    private static String unreserved = "/0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_.~";
+    private static String reserved = "!*'();:@&=+$,?%#[]";
+    private static String hexchars = "0123456789ABCDEF";
+    
+    /* ------------------------------------------------------------ */
+    /** Copy Constructor .
+     * @param uri
+     */
+    public URI(URI uri)
+        throws IllegalArgumentException
+    {
+        _uri=uri.toString();
+        _scheme=uri._scheme;
+        _host=uri._host;
+        _port=uri._port;
+        _path=uri._path;
+        _encodedPath=uri._encodedPath;
+        _query=uri._query;
+        if (uri._parameters!=null) 
+            _parameters=(UrlEncoded)uri._parameters.clone();
+        _dirty=false;
+    }
+    
+    /* ------------------------------------------------------------ */
+    /** Construct from a String.
+     * The string must contain a URI path, but optionaly may contain a
+     * scheme, host, port and query string.
+     * 
+     * @param uri [scheme://host[:port]]/path[?query]
+     */
+    public URI(String uri)
+        throws IllegalArgumentException
+    {
+        setURI(uri);
+    }
+    
+    /* ------------------------------------------------------------ */
+    public void setURI(String uri)
+        throws IllegalArgumentException
+    {
+        try
+        {    
+            _uri=uri;
+            _scheme=null;
+            _host=null;
+            _port=0;
+            _path=null;
+            _encodedPath=null;
+            _query=null;
+            if (_parameters!=null)
+                _parameters.clear();
+            
+            // Scan _uri for host, port, path & query
+            int maxi=uri.length()-1;
+            int mark=0;
+            int state=0;
+            int i=0;
+
+            if (maxi==0 || uri.charAt(0)=='/' && uri.charAt(1)!='/')
+            {
+                state=3;
+                _scheme=null;
+                _host=null;
+                _port=0;
+            }
+            else
+            {
+                for (i=0;state<3 && i<=maxi;i++)
+                {
+                    char c=uri.charAt(i);
+                    switch(state)
+                    {
+                      case 0: // looking for scheme or path
+                          if (c==':' &&
+                              uri.charAt(i+1)=='/' &&
+                              uri.charAt(i+2)=='/')
+                          {
+                              // found end of scheme & start of host
+                              _scheme=uri.substring(mark,i);
+                              i+=2;
+                              mark=i+1;
+                              state=1;
+                          }
+                          else if (i==0 && c=='/')
+                          {
+                              // Found path
+                              state=3;
+                          }
+                          else if (i==0 && c=='*')
+                          {
+                              state=5;
+                              _path="*";
+                              _encodedPath="*";
+                          }
+                          continue;
+                          
+                      case 1: // Get host & look for port or path
+                          if (c==':')
+                          {
+                              // found port
+                              _host=uri.substring(mark,i);
+                              mark=i+1;
+                              state=2;
+                          }
+                          else if (c=='/')
+                          {
+                              // found path
+                              _host=uri.substring(mark,i);
+                              mark=i;
+                              state=3;
+                          }
+                          continue;
+                          
+                      case 2: // Get port & look for path
+                          if (c=='/')
+                          {
+                              _port=TypeUtil.parseInt(uri,mark,i-mark,10);
+                              mark=i;
+                              state=3;
+                          }
+                          continue;
+                    }
+                }
+            }
+            
+            // State 3 - Get path & look for query
+            _query=null;
+            for (i++;i<=maxi;i++)
+            {
+                char c=uri.charAt(i); 
+                if (c=='?')
+                {
+                    // Found query
+                    _encodedPath=uri.substring(mark,i);
+                    _path=decodePath(_encodedPath);
+
+                    mark=i+1;
+                    state=4;
+                    break;
+                }
+            }
+
+            // complete last state
+            switch(state)
+            {
+              case 0:
+                  _dirty=false;
+                  _encodedPath=_uri;
+                  _path=decodePath(_encodedPath);
+                  break;
+                  
+              case 1:
+                  _dirty=true;
+                  _encodedPath="/";
+                  _path=_encodedPath;
+                  _host=uri.substring(mark);
+                  break;
+                  
+              case 2:
+                  _dirty=true;
+                  _encodedPath="/";
+                  _path=_encodedPath;
+                  _port=TypeUtil.parseInt(uri,mark,-1,10);
+                  break;
+              case 3:
+                  _dirty=(mark==maxi);
+                  _encodedPath=uri.substring(mark);
+                  _path=decodePath(_encodedPath);
+                  break;
+                  
+              case 4:
+                  _dirty=false; 
+                  if (mark<=maxi)
+                      _query=uri.substring(mark);
+                  break;
+                  
+              case 5:
+                  _dirty=false; 
+            }
+        
+            if (_query!=null && _query.length()>0)
+            {
+                if (_parameters==null)
+                    _parameters= new UrlEncoded();
+                else
+                    _parameters.clear();
+                _parameters.decode(_query,__CHARSET);
+                
+            }
+            else
+                _query=null;           
+        }
+        catch (Exception e)
+        {
+            LogSupport.ignore(log,e);
+            throw new IllegalArgumentException("Malformed URI '"+uri+
+                                               "' : "+e.toString());
+        }        
+    }
+
+    /* ------------------------------------------------------------ */
+    /** Is the URI an absolute URL? 
+     * @return True if the URI has a scheme or host
+     */
+    public boolean isAbsolute()
+    {
+        return _scheme!=null || _host!=null;
+    }
+    
+    /* ------------------------------------------------------------ */
+    /** Get the uri scheme.
+     * @return the URI scheme
+     */
+    public String getScheme()
+    {
+        return _scheme;
+    }
+    
+    /* ------------------------------------------------------------ */
+    /** Set the uri scheme.
+     * @param scheme the uri scheme
+     */
+    public void setScheme(String scheme)
+    {
+        _scheme=scheme;
+        _dirty=true;
+    }
+    
+    /* ------------------------------------------------------------ */
+    /** Get the uri host.
+     * @return the URI host
+     */
+    public String getHost()
+    {
+        return _host;
+    }
+    
+    /* ------------------------------------------------------------ */
+    /** Set the uri host.
+     * @param host the uri host
+     */
+    public void setHost(String host)
+    {
+        _host=host;
+        _dirty=true;
+    }
+    
+    /* ------------------------------------------------------------ */
+    /** Get the uri port.
+     * @return the URI port
+     */
+    public int getPort()
+    {
+        return _port;
+    }
+    
+    /* ------------------------------------------------------------ */
+    /** Set the uri port.
+     * A port of 0 implies use the default port.
+     * @param port the uri port
+     */
+    public void setPort(int port)
+    {
+        _port=port;
+        _dirty=true;
+    }
+    
+    /* ------------------------------------------------------------ */
+    /** Get the uri path.
+     * @return the URI path
+     */
+    public String getPath()
+    {
+        return _path;
+    }
+    
+    /* ------------------------------------------------------------ */
+    /** Get the encoded uri path.
+     * @return the URI path
+     */
+    public String getEncodedPath()
+    {
+        return _encodedPath;
+    }
+    
+    /* ------------------------------------------------------------ */
+    /** Set the uri path.
+     * @param path the URI path
+     */
+    public void setPath(String path)
+    {
+        _path=path;
+        _encodedPath=encodePath(_path);
+        _dirty=true;
+    }
+    
+    
+    /* ------------------------------------------------------------ */
+    /** Get the uri query String.
+     * @return the URI query string
+     */
+    public String getQuery()
+    {
+        if (_dirty && _parameters!=null)
+        {
+            _query = _parameters.encode(__CHARSET);
+            if (_query!=null && _query.length()==0)
+                _query=null;
+        }
+        return _query;
+    }
+    
+    /* ------------------------------------------------------------ */
+    /** Set the uri query String.
+     * @param query the URI query string
+     */
+    public void setQuery(String query)
+    {
+        _query=query;
+        
+        if (_parameters!=null)
+            _parameters.clear();
+        else if (query!=null)
+            _parameters=new UrlEncoded();
+        
+        if (query!=null)
+            _parameters.decode(query,__CHARSET);
+        
+        cleanURI();
+    }
+    
+    /* ------------------------------------------------------------ */
+    /** Get the uri query _parameters names.
+     * @return  Unmodifiable set of URI query _parameters names
+     */
+    public Set getParameterNames()
+    {
+        if (_parameters==null)
+            return Collections.EMPTY_SET;
+        return _parameters.keySet();
+    }
+    
+    /* ------------------------------------------------------------ */
+    /** Get the uri query _parameters.
+     * @return the URI query _parameters
+     */
+    public MultiMap getParameters()
+    {
+        if (_parameters==null)
+            _parameters=new UrlEncoded();
+        _dirty=true;
+        return _parameters;
+    }
+    
+    /* ------------------------------------------------------------ */
+    /** Get the uri query _parameters.
+     * @return the URI query _parameters in an unmodifiable map.
+     */
+    public Map getUnmodifiableParameters()
+    {
+        if (_parameters==null)
+            return Collections.EMPTY_MAP;
+        return Collections.unmodifiableMap(_parameters);
+    }
+    
+    /* ------------------------------------------------------------ */
+    /** Add the uri query _parameters to a MultiMap
+     */
+    public void putParametersTo(MultiMap map)
+    {
+        if (_parameters!=null && _parameters.size()>0)
+            map.putAll(_parameters);
+    }
+    
+    /* ------------------------------------------------------------ */
+    /** Clear the URI _parameters.
+     */
+    public void clearParameters()
+    {
+        if (_parameters!=null)
+        {
+            _dirty=true;
+            _parameters.clear();
+        }
+    }
+    
+    /* ------------------------------------------------------------ */
+    /** Add encoded _parameters.
+     * @param encoded A HTTP encoded string of _parameters: e.g.. "a=1&b=2"
+     */
+    public void put(String encoded)
+    {
+        UrlEncoded params = new UrlEncoded(encoded);
+        put(params);
+    }
+    
+    /* ------------------------------------------------------------ */
+    /** Add name value pair to the uri query _parameters.
+     * @param name name of value
+     * @param value The value, which may be a multi valued list or
+     * String array.
+     */
+    public Object put(Object name, Object value)
+    {
+        return getParameters().put(name,value);
+    }
+    
+    /* ------------------------------------------------------------ */
+    /** Add dictionary to the uri query _parameters.
+     */
+    public void put(Map values)
+    {
+        getParameters().putAll(values);
+    }
+
+    /* ------------------------------------------------------------ */
+    /** Get named value 
+     */
+    public String get(String name)
+    {
+        if (_parameters==null)
+            return null;
+        return (String)_parameters.get(name);
+    }
+    
+    /* ------------------------------------------------------------ */
+    /** Get named multiple values.
+     * @param name The parameter name
+     * @return Umodifiable list of values or null
+     */
+    public List getValues(String name)
+    {
+        if (_parameters==null)
+            return null;
+        return _parameters.getValues(name);
+    }
+    
+    /* ------------------------------------------------------------ */
+    /** Remove named value 
+     */
+    public void remove(String name)
+    {
+        if (_parameters!=null)
+        {
+            _dirty=
+                _parameters.remove(name)!=null;
+        }
+    }
+    
+    /* ------------------------------------------------------------ */
+    /** @return the URI string encoded.
+     */
+    public String toString()
+    {
+        if (_dirty)
+        {
+            getQuery();
+            cleanURI();
+        }
+        return _uri;
+    }
+
+    /* ------------------------------------------------------------ */
+    private void cleanURI()
+    {
+        StringBuffer buf = new StringBuffer(_uri.length()*2);
+        synchronized(buf)
+        {
+            if (_scheme!=null)
+            {
+                buf.append(_scheme);
+                buf.append("://");
+                buf.append(_host);
+                if (_port>0)
+                {
+                    buf.append(':');
+                    buf.append(_port);
+                }
+            }
+
+            buf.append(_encodedPath);
+            
+            if (_query!=null && _query.length()>0)
+            {
+                buf.append('?');
+                buf.append(_query);
+            }
+            _uri=buf.toString();
+            _dirty=false;
+        }
+    }
+    
+                
+    /* ------------------------------------------------------------ */
+    /** Encode a URI path.
+     * This is the same encoding offered by URLEncoder, except that
+     * the '/' character is not encoded.
+     * @param path The path the encode
+     * @return The encoded path
+     */
+    public static String encodePath(String path)
+    {
+        if (path==null || path.length()==0)
+            return path;
+        
+        StringBuffer buf = encodePath(null,path);
+        return buf==null?path:buf.toString();
+    }
+        
+    /* ------------------------------------------------------------ */
+    /** Encode a URI path.
+     * @param path The path the encode
+     * @param buf StringBuffer to encode path into (or null)
+     * @return The StringBuffer or null if no substitutions required.
+     */
+    public static StringBuffer encodePath(StringBuffer buf, String path)
+    {
+        // Convert path to native first.
+        byte[] b = null;
+        /*
+        try {
+            b = path.getBytes(__CHARSET);
+        } catch(UnsupportedEncodingException ex) {
+            return null; // Shouldn't be possible.
+        }
+        */
+        b = path.getBytes();
+        StringBuffer x = new StringBuffer(b.length);
+        for(int i=0; i<b.length; i++) {
+            x.append((char)(b[i]&0xff));
+        }
+        String _path = new String(x);
+        if(buf == null) {
+            loop:
+            for(int i = 0; i < _path.length(); i++) {
+                char c = _path.charAt(i);
+                String cs = "" + c;
+                if(reserved.contains(cs) || !unreserved.contains(cs)) {
+                    buf = new StringBuffer(_path.length() << 1);
+                    break loop;
+                }
+            }
+            if(buf == null) {
+                return null;
+            }
+        }
+        synchronized(buf) {
+            for(int i = 0; i < _path.length(); i++) {
+                char c = _path.charAt(i);
+                String cs = "" + c;
+                if(reserved.contains(cs) || !unreserved.contains(cs)) {
+                    if((c & 0xff) == c) {
+                        buf.append(gethex(c & 0xff));
+                    } else {
+                        buf.append(gethex((c >> 8) & 0xff));
+                        buf.append(gethex(c & 0xff));
+                    }
+                } else {
+                    buf.append(c);
+                }
+            }
+        }
+
+        return buf;
+    }
+    
+    /**
+     *
+     * @param decimal value not greater than 255.
+     * @return a percent sign followed by two hexadecimal digits.
+     */
+    private static String gethex(int decimal) {
+        return new String("%" + hexchars.charAt(decimal >> 4) + hexchars.charAt(decimal & 0xF));
+    }
+    /* ------------------------------------------------------------ */
+    /** Encode a URI path.
+     * @param path The path the encode
+     * @param buf StringBuffer to encode path into (or null)
+     * @param encode String of characters to encode. % is always encoded.
+     * @return The StringBuffer or null if no substitutions required.
+     */
+    public static StringBuffer encodeString(StringBuffer buf,
+                                            String path,
+                                            String encode)
+    {
+        if (buf==null)
+        {
+        loop:
+            for (int i=0;i<path.length();i++)
+            {
+                char c=path.charAt(i);
+                if (c=='%' || encode.indexOf(c)>=0)
+                {    
+                    buf=new StringBuffer(path.length()<<1);
+                    break loop;
+                }
+            }
+            if (buf==null)
+                return null;
+        }
+        
+        synchronized(buf)
+        {
+            for (int i=0;i<path.length();i++)
+            {
+                char c=path.charAt(i);
+                if (c=='%' || encode.indexOf(c)>=0)
+                {
+                    buf.append('%');
+                    StringUtil.append(buf,(byte)(0xff&c),16);
+                }
+                else
+                    buf.append(c);
+            }
+        }
+
+        return buf;
+    }
+    
+    /* ------------------------------------------------------------ */
+    /* Decode a URI path.
+     * @param path The path the encode
+     * @param buf StringBuffer to encode path into
+     */
+    public static String decodePath(String path)
+    {
+        int len=path.length();
+        byte[] bytes=null;
+        int n=0;
+        boolean noDecode=true;
+        
+        for (int i=0;i<len;i++)
+        {
+            char c = path.charAt(i);
+            
+            byte b = (byte)(0xff & c);
+
+            if (c=='%' && (i+2)<len)
+            {
+                noDecode=false;
+                b=(byte)(0xff&TypeUtil.parseInt(path,i+1,2,16));
+                i+=2;
+            }
+            else if (bytes==null)
+            {
+                n++;
+                continue;
+            }
+            
+            if (bytes==null)
+            {
+                noDecode=false;
+                bytes=new byte[len];
+                for (int j=0;j<n;j++)
+                    bytes[j]=(byte)(0xff & path.charAt(j));                
+            }
+            
+            bytes[n++]=b;
+        }
+
+        if (noDecode)
+            return path;
+
+        /*
+        try
+        {    
+            return new String(bytes,0,n,__CHARSET);
+        }
+        catch(UnsupportedEncodingException e)
+        {
+            log.warn(LogSupport.EXCEPTION,e);
+            return new String(bytes,0,n);
+        }
+        */
+        
+        return new String(bytes,0,n);
+        
+    }
+
+    /* ------------------------------------------------------------ */
+    /** Clone URI.
+     * @return cloned URI
+     */
+    public Object clone()
+	throws CloneNotSupportedException
+    {
+         URI u = (URI)super.clone();
+         if (_parameters!=null)
+             u._parameters=(UrlEncoded)_parameters.clone();
+         _dirty=false;
+         
+         return u;
+    }
+
+
+    /* ------------------------------------------------------------ */
+    /** Add two URI path segments.
+     * Handles null and empty paths, path and query params (eg ?a=b or
+     * ;JSESSIONID=xxx) and avoids duplicate '/'
+     * @param p1 URI path segment 
+     * @param p2 URI path segment
+     * @return Legally combined path segments.
+     */
+    public static String addPaths(String p1, String p2)
+    {
+        if (p1==null || p1.length()==0)
+        {
+            if (p2==null || p2.length()==0)
+                return p1;
+            return p2;
+        }
+        if (p2==null || p2.length()==0)
+            return p1;
+        
+        int split=p1.indexOf(';');
+        if (split<0)
+            split=p1.indexOf('?');
+        if (split==0)
+            return p2+p1;
+        if (split<0)
+            split=p1.length();
+
+        StringBuffer buf = new StringBuffer(p1.length()+p2.length()+2);
+        buf.append(p1);
+        
+        if (buf.charAt(split-1)=='/')
+        {
+            if (p2.startsWith("/"))
+            {
+                buf.deleteCharAt(split-1);
+                buf.insert(split-1,p2);
+            }
+            else
+                buf.insert(split,p2);
+        }
+        else
+        {
+            if (p2.startsWith("/"))
+                buf.insert(split,p2);
+            else
+            {
+                buf.insert(split,'/');
+                buf.insert(split+1,p2);
+            }
+        }
+
+        return buf.toString();
+    }
+    
+    /* ------------------------------------------------------------ */
+    /** Return the parent Path.
+     * Treat a URI like a directory path and return the parent directory.
+     */
+    public static String parentPath(String p)
+    {
+        if (p==null || "/".equals(p))
+            return null;
+        int slash=p.lastIndexOf('/',p.length()-2);
+        if (slash>=0)
+            return p.substring(0,slash+1);
+        return null;
+    }
+    
+    /* ------------------------------------------------------------ */
+    /** Strip parameters from a path.
+     * Return path upto any semicolon parameters.
+     */
+    public static String stripPath(String path)
+    {
+        if (path==null)
+            return null;
+        int semi=path.indexOf(';');
+        if (semi<0)
+            return path;
+        return path.substring(0,semi);
+    }
+    
+    /* ------------------------------------------------------------ */
+    /** Convert a path to a cananonical form.
+     * All instances of "." and ".." are factored out.  Null is returned
+     * if the path tries to .. above it's root.
+     * @param path 
+     * @return path or null.
+     */
+    public static String canonicalPath(String path)
+    {
+        if (path==null || path.length()==0)
+            return path;
+
+        int end=path.length();
+        int start = path.lastIndexOf('/', end);
+
+    search:
+        while (end>0)
+        {
+            switch(end-start)
+            {
+              case 2: // possible single dot
+                  if (path.charAt(start+1)!='.')
+                      break;
+                  break search;
+              case 3: // possible double dot
+                  if (path.charAt(start+1)!='.' || path.charAt(start+2)!='.')
+                      break;
+                  break search;
+            }
+            
+            end=start;
+            start=path.lastIndexOf('/',end-1);
+        }
+
+        // If we have checked the entire string
+        if (start>=end)
+            return path;
+        
+        StringBuffer buf = new StringBuffer(path);
+        int delStart=-1;
+        int delEnd=-1;
+        int skip=0;
+        
+        while (end>0)
+        {
+            switch(end-start)
+            {       
+              case 2: // possible single dot
+                  if (buf.charAt(start+1)!='.')
+                  {
+                      if (skip>0 && --skip==0)
+                      {   
+                          delStart=start>=0?start:0;
+                          if(delStart>0 && delEnd==buf.length() && buf.charAt(delEnd-1)=='.')
+                              delStart++;
+                      }
+                      break;
+                  }
+                  
+                  if(start<0 && buf.length()>2 && buf.charAt(1)=='/' && buf.charAt(2)=='/')
+                      break;
+                  
+                  if(delEnd<0)
+                      delEnd=end;
+                  delStart=start;
+                  if (delStart<0 || delStart==0&&buf.charAt(delStart)=='/')
+                  {
+                      delStart++;
+                      if (delEnd<buf.length() && buf.charAt(delEnd)=='/')
+                          delEnd++;
+                      break;
+                  }
+                  if (end==buf.length())
+                      delStart++;
+                  
+                  end=start--;
+                  while (start>=0 && buf.charAt(start)!='/')
+                      start--;
+                  continue;
+                  
+              case 3: // possible double dot
+                  if (buf.charAt(start+1)!='.' || buf.charAt(start+2)!='.')
+                  {
+                      if (skip>0 && --skip==0)
+                      {   delStart=start>=0?start:0;
+                      	  if(delStart>0 && delEnd==buf.length() && buf.charAt(delEnd-1)=='.')
+                      	      delStart++;
+                      }
+                      break;
+                  }
+                  
+                  delStart=start;
+                  if (delEnd<0)
+                      delEnd=end;
+
+                  skip++;
+                  end=start--;
+                  while (start>=0 && buf.charAt(start)!='/')
+                      start--;
+                  continue;
+
+              default:
+                  if (skip>0 && --skip==0)
+                  {
+                      delStart=start>=0?start:0;
+                      if(delEnd==buf.length() && buf.charAt(delEnd-1)=='.')
+                          delStart++;
+                  }
+            }     
+            
+            // Do the delete
+            if (skip<=0 && delStart>=0 && delStart>=0)
+            {  
+                buf.delete(delStart,delEnd);
+                delStart=delEnd=-1;
+                if (skip>0)
+                    delEnd=end;
+            }
+            
+            end=start--;
+            while (start>=0 && buf.charAt(start)!='/')
+                start--;
+        }      
+
+        // Too many ..
+        if (skip>0)
+            return null;
+        
+        // Do the delete
+        if (delEnd>=0)
+            buf.delete(delStart,delEnd);
+
+        return buf.toString();
+    }
+
+    /* ------------------------------------------------------------ */
+    /** 
+     * @param uri URI
+     * @return True if the uri has a scheme
+     */
+    public static boolean hasScheme(String uri)
+    {
+        for (int i=0;i<uri.length();i++)
+        {
+            char c=uri.charAt(i);
+            if (c==':')
+                return true;
+            if (!(c>='a'&&c<='z' ||
+                  c>='A'&&c<='Z' ||
+                  (i>0 &&(c>='0'&&c<='9' ||
+                          c=='.' ||
+                          c=='+' ||
+                          c=='-'))
+                  ))
+                break;
+        }
+        return false;
+    }
+    
+}
+
+
+