/*
 * @(#)URI.java              0.3-3 06/05/2001
 *
 *  This file is part of the HTTPClient package
 *  Copyright (C) 1996-2001 Ronald Tschal�r
 *
 *  This library 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 library 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 library; if not, write to the Free
 *  Software Foundation, Inc., 59 Temple Place, Suite 330, Boston,
 *  MA 02111-1307, USA
 *
 *  For questions, suggestions, bug-reports, enhancement-requests etc.
 *  I may be contacted at:
 *
 *  ronald@innovation.ch
 *
 *  The HTTPClient's home page is located at:
 *
 *  http://www.innovation.ch/java/HTTPClient/ 
 *
 */

package org.exoplatform.common.http.client;

import java.net.MalformedURLException;
import java.net.URL;
import java.util.BitSet;
import java.util.Hashtable;

/**
 * This class represents a generic URI, as defined in RFC-2396. This is similar
 * to java.net.URL, with the following enhancements:
 * <UL>
 * <LI>it doesn't require a URLStreamhandler to exist for the scheme; this
 * allows this class to be used to hold any URI, construct absolute URIs from
 * relative ones, etc.
 * <LI>it handles escapes correctly
 * <LI>equals() works correctly
 * <LI>relative URIs are correctly constructed
 * <LI>it has methods for accessing various fields such as userinfo, fragment,
 * params, etc.
 * <LI>it handles less common forms of resources such as the "*" used in http
 * URLs.
 * </UL>
 * <P>
 * The elements are always stored in escaped form.
 * <P>
 * While RFC-2396 distinguishes between just two forms of URI's, those that
 * follow the generic syntax and those that don't, this class knows about a
 * third form, named semi-generic, used by quite a few popular schemes.
 * Semi-generic syntax treats the path part as opaque, i.e. has the form
 * &lt;scheme&gt;://&lt;authority&gt;/&lt;opaque&gt; . Relative URI's of this
 * type are only resolved as far as absolute paths - relative paths do not
 * exist.
 * <P>
 * Ideally, java.net.URL should subclass URI.
 * @see <A HREF="http://www.ics.uci.edu/pub/ietf/uri/rfc2396.txt">rfc-2396</A>
 * @version 0.3-3 06/05/2001
 * @author Ronald Tschal�r
 * @since V0.3-1
 */
public class URI
{
   /**
    * If true, then the parser will resolve certain URI's in backwards compatible
    * (but technically incorrect) manner. Example:
    * 
    * <PRE>
    * base   = http://a/b/c/d;p?q
    * rel    = http:g
    * result = http:g      (correct)
    * result = http://a/b/c/g (backwards compatible)
    * </PRE>
    * 
    * See rfc-2396, section 5.2, step 3, second paragraph.
    */
   public static final boolean ENABLE_BACKWARDS_COMPATIBILITY = true;

   protected static final Hashtable defaultPorts = new Hashtable();

   protected static final Hashtable usesGenericSyntax = new Hashtable();

   protected static final Hashtable usesSemiGenericSyntax = new Hashtable();

   /* various character classes as defined in the draft */
   protected static final BitSet alphanumChar;

   protected static final BitSet markChar;

   protected static final BitSet reservedChar;

   protected static final BitSet unreservedChar;

   protected static final BitSet uricChar;

   protected static final BitSet pcharChar;

   protected static final BitSet userinfoChar;

   protected static final BitSet schemeChar;

   protected static final BitSet hostChar;

   protected static final BitSet opaqueChar;

   protected static final BitSet reg_nameChar;

   /*
    * These are not directly in the spec, but used for escaping and unescaping
    * parts
    */

   /** list of characters which must not be unescaped when unescaping a scheme */
   public static final BitSet resvdSchemeChar;

   /** list of characters which must not be unescaped when unescaping a userinfo */
   public static final BitSet resvdUIChar;

   /** list of characters which must not be unescaped when unescaping a host */
   public static final BitSet resvdHostChar;

   /** list of characters which must not be unescaped when unescaping a path */
   public static final BitSet resvdPathChar;

   /**
    * list of characters which must not be unescaped when unescaping a query
    * string
    */
   public static final BitSet resvdQueryChar;

   /** list of characters which must not be escaped when escaping a path */
   public static final BitSet escpdPathChar;

   /** list of characters which must not be escaped when escaping a query string */
   public static final BitSet escpdQueryChar;

   /**
    * list of characters which must not be escaped when escaping a fragment
    * identifier
    */
   public static final BitSet escpdFragChar;

   static
   {
      defaultPorts.put("http", new Integer(80));
      defaultPorts.put("shttp", new Integer(80));
      defaultPorts.put("http-ng", new Integer(80));
      defaultPorts.put("coffee", new Integer(80));
      defaultPorts.put("https", new Integer(443));
      defaultPorts.put("ftp", new Integer(21));
      defaultPorts.put("telnet", new Integer(23));
      defaultPorts.put("nntp", new Integer(119));
      defaultPorts.put("news", new Integer(119));
      defaultPorts.put("snews", new Integer(563));
      defaultPorts.put("hnews", new Integer(80));
      defaultPorts.put("smtp", new Integer(25));
      defaultPorts.put("gopher", new Integer(70));
      defaultPorts.put("wais", new Integer(210));
      defaultPorts.put("whois", new Integer(43));
      defaultPorts.put("whois++", new Integer(63));
      defaultPorts.put("rwhois", new Integer(4321));
      defaultPorts.put("imap", new Integer(143));
      defaultPorts.put("pop", new Integer(110));
      defaultPorts.put("prospero", new Integer(1525));
      defaultPorts.put("irc", new Integer(194));
      defaultPorts.put("ldap", new Integer(389));
      defaultPorts.put("nfs", new Integer(2049));
      defaultPorts.put("z39.50r", new Integer(210));
      defaultPorts.put("z39.50s", new Integer(210));
      defaultPorts.put("vemmi", new Integer(575));
      defaultPorts.put("videotex", new Integer(516));
      defaultPorts.put("cmp", new Integer(829));

      usesGenericSyntax.put("http", Boolean.TRUE);
      usesGenericSyntax.put("https", Boolean.TRUE);
      usesGenericSyntax.put("shttp", Boolean.TRUE);
      usesGenericSyntax.put("coffee", Boolean.TRUE);
      usesGenericSyntax.put("ftp", Boolean.TRUE);
      usesGenericSyntax.put("file", Boolean.TRUE);
      usesGenericSyntax.put("nntp", Boolean.TRUE);
      usesGenericSyntax.put("news", Boolean.TRUE);
      usesGenericSyntax.put("snews", Boolean.TRUE);
      usesGenericSyntax.put("hnews", Boolean.TRUE);
      usesGenericSyntax.put("imap", Boolean.TRUE);
      usesGenericSyntax.put("wais", Boolean.TRUE);
      usesGenericSyntax.put("nfs", Boolean.TRUE);
      usesGenericSyntax.put("sip", Boolean.TRUE);
      usesGenericSyntax.put("sips", Boolean.TRUE);
      usesGenericSyntax.put("sipt", Boolean.TRUE);
      usesGenericSyntax.put("sipu", Boolean.TRUE);
      /*
       * Note: schemes which definitely don't use the generic-URI syntax and must
       * therefore never appear in the above list: "urn", "mailto", "sdp",
       * "service", "tv", "gsm-sms", "tel", "fax", "modem", "eid", "cid", "mid",
       * "data", "ldap"
       */

      usesSemiGenericSyntax.put("ldap", Boolean.TRUE);
      usesSemiGenericSyntax.put("irc", Boolean.TRUE);
      usesSemiGenericSyntax.put("gopher", Boolean.TRUE);
      usesSemiGenericSyntax.put("videotex", Boolean.TRUE);
      usesSemiGenericSyntax.put("rwhois", Boolean.TRUE);
      usesSemiGenericSyntax.put("whois++", Boolean.TRUE);
      usesSemiGenericSyntax.put("smtp", Boolean.TRUE);
      usesSemiGenericSyntax.put("telnet", Boolean.TRUE);
      usesSemiGenericSyntax.put("prospero", Boolean.TRUE);
      usesSemiGenericSyntax.put("pop", Boolean.TRUE);
      usesSemiGenericSyntax.put("vemmi", Boolean.TRUE);
      usesSemiGenericSyntax.put("z39.50r", Boolean.TRUE);
      usesSemiGenericSyntax.put("z39.50s", Boolean.TRUE);
      usesSemiGenericSyntax.put("stream", Boolean.TRUE);
      usesSemiGenericSyntax.put("cmp", Boolean.TRUE);

      alphanumChar = new BitSet(128);
      for (int ch = '0'; ch <= '9'; ch++)
         alphanumChar.set(ch);
      for (int ch = 'A'; ch <= 'Z'; ch++)
         alphanumChar.set(ch);
      for (int ch = 'a'; ch <= 'z'; ch++)
         alphanumChar.set(ch);

      markChar = new BitSet(128);
      markChar.set('-');
      markChar.set('_');
      markChar.set('.');
      markChar.set('!');
      markChar.set('~');
      markChar.set('*');
      markChar.set('\'');
      markChar.set('(');
      markChar.set(')');

      reservedChar = new BitSet(128);
      reservedChar.set(';');
      reservedChar.set('/');
      reservedChar.set('?');
      reservedChar.set(':');
      reservedChar.set('@');
      reservedChar.set('&');
      reservedChar.set('=');
      reservedChar.set('+');
      reservedChar.set('$');
      reservedChar.set(',');

      unreservedChar = new BitSet(128);
      unreservedChar.or(alphanumChar);
      unreservedChar.or(markChar);

      uricChar = new BitSet(128);
      uricChar.or(unreservedChar);
      uricChar.or(reservedChar);
      uricChar.set('%');

      pcharChar = new BitSet(128);
      pcharChar.or(unreservedChar);
      pcharChar.set('%');
      pcharChar.set(':');
      pcharChar.set('@');
      pcharChar.set('&');
      pcharChar.set('=');
      pcharChar.set('+');
      pcharChar.set('$');
      pcharChar.set(',');

      userinfoChar = new BitSet(128);
      userinfoChar.or(unreservedChar);
      userinfoChar.set('%');
      userinfoChar.set(';');
      userinfoChar.set(':');
      userinfoChar.set('&');
      userinfoChar.set('=');
      userinfoChar.set('+');
      userinfoChar.set('$');
      userinfoChar.set(',');

      // this actually shouldn't contain uppercase letters...
      schemeChar = new BitSet(128);
      schemeChar.or(alphanumChar);
      schemeChar.set('+');
      schemeChar.set('-');
      schemeChar.set('.');

      opaqueChar = new BitSet(128);
      opaqueChar.or(uricChar);

      hostChar = new BitSet(128);
      hostChar.or(alphanumChar);
      hostChar.set('-');
      hostChar.set('.');

      reg_nameChar = new BitSet(128);
      reg_nameChar.or(unreservedChar);
      reg_nameChar.set('$');
      reg_nameChar.set(',');
      reg_nameChar.set(';');
      reg_nameChar.set(':');
      reg_nameChar.set('@');
      reg_nameChar.set('&');
      reg_nameChar.set('=');
      reg_nameChar.set('+');

      resvdSchemeChar = new BitSet(128);
      resvdSchemeChar.set(':');

      resvdUIChar = new BitSet(128);
      resvdUIChar.set('@');

      resvdHostChar = new BitSet(128);
      resvdHostChar.set(':');
      resvdHostChar.set('/');
      resvdHostChar.set('?');
      resvdHostChar.set('#');

      resvdPathChar = new BitSet(128);
      resvdPathChar.set('/');
      resvdPathChar.set(';');
      resvdPathChar.set('?');
      resvdPathChar.set('#');

      resvdQueryChar = new BitSet(128);
      resvdQueryChar.set('#');

      escpdPathChar = new BitSet(128);
      escpdPathChar.or(pcharChar);
      escpdPathChar.set('%');
      escpdPathChar.set('/');
      escpdPathChar.set(';');

      escpdQueryChar = new BitSet(128);
      escpdQueryChar.or(uricChar);
      escpdQueryChar.clear('#');

      escpdFragChar = new BitSet(128);
      escpdFragChar.or(uricChar);
   }

   /* our uri in pieces */

   protected static final int OPAQUE = 0;

   protected static final int SEMI_GENERIC = 1;

   protected static final int GENERIC = 2;

   protected int type;

   protected String scheme;

   protected String opaque;

   protected String userinfo;

   protected String host;

   protected int port = -1;

   protected String path;

   protected String query;

   protected String fragment;

   /* cache the java.net.URL */

   protected URL url = null;

   // Constructors

   /**
    * Constructs a URI from the given string representation. The string must be
    * an absolute URI.
    * @param uri a String containing an absolute URI
    * @exception ParseException if no scheme can be found or a specified port
    *              cannot be parsed as a number
    */
   public URI(String uri) throws ParseException
   {
      this((URI)null, uri);
   }

   /**
    * Constructs a URI from the given string representation, relative to the
    * given base URI.
    * @param base the base URI, relative to which <var>rel_uri</var> is to be
    *          parsed
    * @param rel_uri a String containing a relative or absolute URI
    * @exception ParseException if <var>base</var> is null and <var>rel_uri</var>
    *              is not an absolute URI, or if <var>base</var> is not null and
    *              the scheme is not known to use the generic syntax, or if a
    *              given port cannot be parsed as a number
    */
   public URI(URI base, String rel_uri) throws ParseException
   {
      /*
       * Parsing is done according to the following RE:
       * ^(([^:/?#]+):)?(//([^/?#]))?([^?#])(\?([^#]))?(#(.))? 12 3 4 5 6 7 8 9 2:
       * scheme 4: authority 5: path 7: query 9: fragment
       */

      char[] uri = rel_uri.toCharArray();
      int pos = 0, idx, len = uri.length;

      // trim()

      while (pos < len && Character.isWhitespace(uri[pos]))
         pos++;
      while (len > 0 && Character.isWhitespace(uri[len - 1]))
         len--;

      // strip the special "url" or "uri" scheme

      if (pos < len - 3 && uri[pos + 3] == ':' && (uri[pos + 0] == 'u' || uri[pos + 0] == 'U')
         && (uri[pos + 1] == 'r' || uri[pos + 1] == 'R')
         && (uri[pos + 2] == 'i' || uri[pos + 2] == 'I' || uri[pos + 2] == 'l' || uri[pos + 2] == 'L'))
         pos += 4;

      // get scheme: (([^:/?#]+):)?

      idx = pos;
      while (idx < len && uri[idx] != ':' && uri[idx] != '/' && uri[idx] != '?' && uri[idx] != '#')
         idx++;
      if (idx < len && uri[idx] == ':')
      {
         scheme = rel_uri.substring(pos, idx).trim().toLowerCase();
         pos = idx + 1;
      }

      // check and resolve scheme

      String final_scheme = scheme;
      if (scheme == null)
      {
         if (base == null)
            throw new ParseException("No scheme found");
         final_scheme = base.scheme;
      }

      // check for generic vs. opaque

      type = usesGenericSyntax(final_scheme) ? GENERIC : usesSemiGenericSyntax(final_scheme) ? SEMI_GENERIC : OPAQUE;
      if (type == OPAQUE)
      {
         if (base != null && scheme == null)
            throw new ParseException("Can't resolve relative URI for " + "scheme " + final_scheme);

         opaque = escape(rel_uri.substring(pos), opaqueChar, true);
         if (opaque.length() > 0 && opaque.charAt(0) == '/')
            opaque = "%2F" + opaque.substring(1);
         return;
      }

      // get authority: (//([^/?#]*))?

      if (pos + 1 < len && uri[pos] == '/' && uri[pos + 1] == '/')
      {
         pos += 2;
         idx = pos;
         while (idx < len && uri[idx] != '/' && uri[idx] != '?' && uri[idx] != '#')
            idx++;

         parse_authority(rel_uri.substring(pos, idx), final_scheme);
         pos = idx;
      }

      // handle semi-generic and generic uri's

      if (type == SEMI_GENERIC)
      {
         path = escape(rel_uri.substring(pos), uricChar, true);
         if (path.length() > 0 && path.charAt(0) != '/')
            path = '/' + path;
      }
      else
      {
         // get path: ([^?#]*)

         idx = pos;
         while (idx < len && uri[idx] != '?' && uri[idx] != '#')
            idx++;
         path = escape(rel_uri.substring(pos, idx), escpdPathChar, true);
         pos = idx;

         // get query: (\?([^#]*))?

         if (pos < len && uri[pos] == '?')
         {
            pos += 1;
            idx = pos;
            while (idx < len && uri[idx] != '#')
               idx++;
            this.query = escape(rel_uri.substring(pos, idx), escpdQueryChar, true);
            pos = idx;
         }

         // get fragment: (#(.*))?

         if (pos < len && uri[pos] == '#')
            this.fragment = escape(rel_uri.substring(pos + 1, len), escpdFragChar, true);
      }

      // now resolve the parts relative to the base

      if (base != null)
      {
         if (scheme != null && // resolve scheme
            !(scheme.equals(base.scheme) && ENABLE_BACKWARDS_COMPATIBILITY))
            return;
         scheme = base.scheme;

         if (host != null) // resolve authority
            return;
         userinfo = base.userinfo;
         host = base.host;
         port = base.port;

         if (type == SEMI_GENERIC) // can't resolve relative paths
            return;

         if (path.length() == 0 && query == null) // current doc
         {
            path = base.path;
            query = base.query;
            return;
         }

         if (path.length() == 0 || path.charAt(0) != '/') // relative path
         {
            idx = (base.path != null) ? base.path.lastIndexOf('/') : -1;
            if (idx < 0)
               path = '/' + path;
            else
               path = base.path.substring(0, idx + 1) + path;

            path = canonicalizePath(path);
         }
      }
   }

   /**
    * Remove all "/../" and "/./" from path, where possible. Leading "/../"'s are
    * not removed.
    * @param path the path to canonicalize
    * @return the canonicalized path
    */
   public static String canonicalizePath(String path)
   {
      int idx, len = path.length();
      if (!((idx = path.indexOf("/.")) != -1 && (idx == len - 2 || path.charAt(idx + 2) == '/' 
         || (path.charAt(idx + 2) == '.' && (idx == len - 3 || path.charAt(idx + 3) == '/')))))
         return path;

      char[] p = new char[path.length()]; // clean path
      path.getChars(0, p.length, p, 0);

      int beg = 0;
      for (idx = 1; idx < len; idx++)
      {
         if (p[idx] == '.' && p[idx - 1] == '/')
         {
            int end;
            if (idx == len - 1) // trailing "/."
            {
               end = idx;
               idx += 1;
            }
            else if (p[idx + 1] == '/') // "/./"
            {
               end = idx - 1;
               idx += 1;
            }
            else if (p[idx + 1] == '.' && (idx == len - 2 || p[idx + 2] == '/')) // "/../"
            {
               if (idx < beg + 2) // keep from backing up too much
               {
                  beg = idx + 2;
                  continue;
               }

               end = idx - 2;
               while (end > beg && p[end] != '/')
                  end--;
               if (p[end] != '/')
                  continue;
               if (idx == len - 2)
                  end++;
               idx += 2;
            }
            else
               continue;
            System.arraycopy(p, idx, p, end, len - idx);
            len -= idx - end;
            idx = end;
         }
      }

      return new String(p, 0, len);
   }

   /**
    * Parse the authority specific part
    */
   private void parse_authority(String authority, String scheme) throws ParseException
   {
      /*
       * The authority is further parsed according to:
       * ^(([^@])@?)(\[[^]]\]|[^:])?(:(.))? 12 3 4 5 2: userinfo 3: host 5: port
       */

      char[] uri = authority.toCharArray();
      int pos = 0, idx, len = uri.length;

      // get userinfo: (([^@]*)@?)

      idx = pos;
      while (idx < len && uri[idx] != '@')
         idx++;
      if (idx < len && uri[idx] == '@')
      {
         this.userinfo = escape(authority.substring(pos, idx), userinfoChar, true);
         pos = idx + 1;
      }

      // get host: (\[[^]]*\]|[^:]*)?

      idx = pos;
      if (idx < len && uri[idx] == '[') // IPv6
      {
         while (idx < len && uri[idx] != ']')
            idx++;
         if (idx == len)
            throw new ParseException("No closing ']' found for opening '['" + " at position " + pos + " in authority `"
               + authority + "'");
         this.host = authority.substring(pos + 1, idx);
         idx++;
      }
      else
      {
         while (idx < len && uri[idx] != ':')
            idx++;
         this.host = escape(authority.substring(pos, idx), uricChar, true);
      }
      pos = idx;

      // get port: (:(.*))?

      if (pos < (len - 1) && uri[pos] == ':')
      {
         int p;
         try
         {
            p = Integer.parseInt(unescape(authority.substring(pos + 1, len), null));
            if (p < 0)
               throw new NumberFormatException();
         }
         catch (NumberFormatException e)
         {
            throw new ParseException(authority.substring(pos + 1, len) + " is an invalid port number");
         }
         if (p == defaultPort(scheme))
            this.port = -1;
         else
            this.port = p;
      }
   }

   /**
    * Construct a URI from the given URL.
    * @param url the URL
    * @exception ParseException if <code>url.toExternalForm()</code> generates an
    *              invalid string representation
    */
   public URI(URL url) throws ParseException
   {
      this((URI)null, url.toExternalForm());
   }

   /**
    * Constructs a URI from the given parts, using the default port for this
    * scheme (if known). The parts must be in unescaped form.
    * @param scheme the scheme (sometimes known as protocol)
    * @param host the host
    * @param path the path part
    * @exception ParseException if <var>scheme</var> is null
    */
   public URI(String scheme, String host, String path) throws ParseException
   {
      this(scheme, null, host, -1, path, null, null);
   }

   /**
    * Constructs a URI from the given parts. The parts must be in unescaped form.
    * @param scheme the scheme (sometimes known as protocol)
    * @param host the host
    * @param port the port
    * @param path the path part
    * @exception ParseException if <var>scheme</var> is null
    */
   public URI(String scheme, String host, int port, String path) throws ParseException
   {
      this(scheme, null, host, port, path, null, null);
   }

   /**
    * Constructs a URI from the given parts. Any part except for the the scheme
    * may be null. The parts must be in unescaped form.
    * @param scheme the scheme (sometimes known as protocol)
    * @param userinfo the userinfo
    * @param host the host
    * @param port the port
    * @param path the path part
    * @param query the query string
    * @param fragment the fragment identifier
    * @exception ParseException if <var>scheme</var> is null
    */
   public URI(String scheme, String userinfo, String host, int port, String path, String query, String fragment)
      throws ParseException
   {
      if (scheme == null)
         throw new ParseException("missing scheme");
      this.scheme = escape(scheme.trim().toLowerCase(), schemeChar, true);
      if (userinfo != null)
         this.userinfo = escape(userinfo.trim(), userinfoChar, true);
      if (host != null)
      {
         host = host.trim();
         this.host = isIPV6Addr(host) ? host : escape(host, hostChar, true);
      }
      if (port != defaultPort(scheme))
         this.port = port;
      if (path != null)
         this.path = escape(path.trim(), escpdPathChar, true); // ???
      if (query != null)
         this.query = escape(query.trim(), escpdQueryChar, true);
      if (fragment != null)
         this.fragment = escape(fragment.trim(), escpdFragChar, true);

      type = usesGenericSyntax(scheme) ? GENERIC : SEMI_GENERIC;
   }

   private static final boolean isIPV6Addr(String host)
   {
      if (host.indexOf(':') < 0)
         return false;

      for (int idx = 0; idx < host.length(); idx++)
      {
         char ch = host.charAt(idx);
         if ((ch < '0' || ch > '9') && ch != ':')
            return false;
      }

      return true;
   }

   /**
    * Constructs an opaque URI from the given parts.
    * @param scheme the scheme (sometimes known as protocol)
    * @param opaque the opaque part
    * @exception ParseException if <var>scheme</var> is null
    */
   public URI(String scheme, String opaque) throws ParseException
   {
      if (scheme == null)
         throw new ParseException("missing scheme");
      this.scheme = escape(scheme.trim().toLowerCase(), schemeChar, true);
      this.opaque = escape(opaque, opaqueChar, true);

      type = OPAQUE;
   }

   // Class Methods

   /**
    * @return true if the scheme should be parsed according to the generic-URI
    *         syntax
    */
   public static boolean usesGenericSyntax(String scheme)
   {
      return usesGenericSyntax.containsKey(scheme.trim().toLowerCase());
   }

   /**
    * @return true if the scheme should be parsed according to a semi-generic-URI
    *         syntax &lt;scheme&gt;://&lt;hostport&gt;/&lt;opaque&gt;
    */
   public static boolean usesSemiGenericSyntax(String scheme)
   {
      return usesSemiGenericSyntax.containsKey(scheme.trim().toLowerCase());
   }

   /**
    * Return the default port used by a given protocol.
    * @param protocol the protocol
    * @return the port number, or 0 if unknown
    */
   public final static int defaultPort(String protocol)
   {
      Integer port = (Integer)defaultPorts.get(protocol.trim().toLowerCase());
      return (port != null) ? port.intValue() : 0;
   }

   // Instance Methods

   /**
    * @return the scheme (often also referred to as protocol)
    */
   public String getScheme()
   {
      return scheme;
   }

   /**
    * @return the opaque part, or null if this URI is generic
    */
   public String getOpaque()
   {
      return opaque;
   }

   /**
    * @return the host
    */
   public String getHost()
   {
      return host;
   }

   /**
    * @return the port, or -1 if it's the default port, or 0 if unknown
    */
   public int getPort()
   {
      return port;
   }

   /**
    * @return the user info
    */
   public String getUserinfo()
   {
      return userinfo;
   }

   /**
    * @return the path
    */
   public String getPath()
   {
      return path;
   }

   /**
    * @return the query string
    */
   public String getQueryString()
   {
      return query;
   }

   /**
    * @return the path and query
    */
   public String getPathAndQuery()
   {
      if (query == null)
         return path;
      if (path == null)
         return "?" + query;
      return path + "?" + query;
   }

   /**
    * @return the fragment
    */
   public String getFragment()
   {
      return fragment;
   }

   /**
    * Does the scheme specific part of this URI use the generic-URI syntax?
    * <P>
    * In general URI are split into two categories: opaque-URI and generic-URI.
    * The generic-URI syntax is the syntax most are familiar with from URLs such
    * as ftp- and http-URLs, which is roughly:
    * 
    * <PRE>
    * generic-URI = scheme &quot;:&quot; [ &quot;//&quot; server ] [ &quot;/&quot; ] [ path_segments ] [ &quot;?&quot; query ]
    * </PRE>
    * 
    * (see RFC-2396 for exact syntax). Only URLs using the generic-URI syntax can
    * be used to create and resolve relative URIs.
    * <P>
    * Whether a given scheme is parsed according to the generic-URI syntax or
    * wether it is treated as opaque is determined by an internal table of URI
    * schemes.
    * @see <A HREF="http://www.ics.uci.edu/pub/ietf/uri/rfc2396.txt">rfc-2396</A>
    */
   public boolean isGenericURI()
   {
      return (type == GENERIC);
   }

   /**
    * Does the scheme specific part of this URI use the semi-generic-URI syntax?
    * <P>
    * Many schemes which don't follow the full generic syntax actually follow a
    * reduced form where the path part is treated is opaque. This is used for
    * example by ldap, smtp, pop, etc, and is roughly
    * 
    * <PRE>
    * generic-URI = scheme &quot;:&quot; [ &quot;//&quot; server ] [ &quot;/&quot; [ opaque_path ] ]
    * </PRE>
    * 
    * I.e. parsing is identical to the generic-syntax, except that the path part
    * is not further parsed. URLs using the semi-generic-URI syntax can be used
    * to create and resolve relative URIs with the restriction that all paths are
    * treated as absolute.
    * <P>
    * Whether a given scheme is parsed according to the semi-generic-URI syntax
    * is determined by an internal table of URI schemes.
    * @see #isGenericURI()
    */
   public boolean isSemiGenericURI()
   {
      return (type == SEMI_GENERIC);
   }

   /**
    * Will try to create a java.net.URL object from this URI.
    * @return the URL
    * @exception MalformedURLException if no handler is available for the scheme
    */
   public URL toURL() throws MalformedURLException
   {
      if (url != null)
         return url;

      if (opaque != null)
         return (url = new URL(scheme + ":" + opaque));

      String hostinfo;
      if (userinfo != null && host != null)
         hostinfo = userinfo + "@" + host;
      else if (userinfo != null)
         hostinfo = userinfo + "@";
      else
         hostinfo = host;

      StringBuffer file = new StringBuffer(100);
      assemblePath(file, true, true, false);

      url = new URL(scheme, hostinfo, port, file.toString());
      return url;
   }

   private final void assemblePath(StringBuffer buf, boolean printEmpty, boolean incFragment, boolean unescape)
   {
      if ((path == null || path.length() == 0) && printEmpty)
         buf.append('/');

      if (path != null)
         buf.append(unescape ? unescapeNoPE(path, resvdPathChar) : path);

      if (query != null)
      {
         buf.append('?');
         buf.append(unescape ? unescapeNoPE(query, resvdQueryChar) : query);
      }

      if (fragment != null && incFragment)
      {
         buf.append('#');
         buf.append(unescape ? unescapeNoPE(fragment, null) : fragment);
      }
   }

   private final String stringify(boolean unescape)
   {
      StringBuffer uri = new StringBuffer(100);

      if (scheme != null)
      {
         uri.append(unescape ? unescapeNoPE(scheme, resvdSchemeChar) : scheme);
         uri.append(':');
      }

      if (opaque != null) // it's an opaque-uri
      {
         uri.append(unescape ? unescapeNoPE(opaque, null) : opaque);
         return uri.toString();
      }

      if (userinfo != null || host != null || port != -1)
         uri.append("//");

      if (userinfo != null)
      {
         uri.append(unescape ? unescapeNoPE(userinfo, resvdUIChar) : userinfo);
         uri.append('@');
      }

      if (host != null)
      {
         if (host.indexOf(':') < 0)
            uri.append(unescape ? unescapeNoPE(host, resvdHostChar) : host);
         else
            uri.append('[').append(host).append(']');
      }

      if (port != -1)
      {
         uri.append(':');
         uri.append(port);
      }

      assemblePath(uri, false, true, unescape);

      return uri.toString();
   }

   /**
    * @return a string representation of this URI suitable for use in links,
    *         headers, etc.
    */
   public String toExternalForm()
   {
      return stringify(false);
   }

   /**
    * Return the URI as string. This differs from toExternalForm() in that all
    * elements are unescaped before assembly. This is <em>not suitable</em> for
    * passing to other apps or in header fields and such, and is usually not what
    * you want.
    * @return the URI as a string
    * @see #toExternalForm()
    */
   @Override
   public String toString()
   {
      return stringify(true);
   }

   /**
    * @return true if <var>other</var> is either a URI or URL and it matches the
    *         current URI
    */
   @Override
   public boolean equals(Object other)
   {
      if (other instanceof URI)
      {
         URI o = (URI)other;
         return (scheme.equals(o.scheme) && (type == OPAQUE && areEqual(opaque, o.opaque) ||

         type == SEMI_GENERIC && areEqual(userinfo, o.userinfo) && areEqualIC(host, o.host) && port == o.port
            && areEqual(path, o.path) ||

         type == GENERIC && areEqual(userinfo, o.userinfo) && areEqualIC(host, o.host) && port == o.port
            && pathsEqual(path, o.path) && areEqual(query, o.query) && areEqual(fragment, o.fragment)));
      }

      if (other instanceof URL)
      {
         URL o = (URL)other;
         String h, f;

         if (userinfo != null)
            h = userinfo + "@" + host;
         else
            h = host;

         f = getPathAndQuery();

         return (scheme.equalsIgnoreCase(o.getProtocol()) && (type == OPAQUE && opaque.equals(o.getFile()) ||

         type == SEMI_GENERIC && areEqualIC(h, o.getHost())
            && (port == o.getPort() || o.getPort() == defaultPort(scheme)) && areEqual(f, o.getFile()) ||

         type == GENERIC && areEqualIC(h, o.getHost()) && (port == o.getPort() || o.getPort() == defaultPort(scheme))
            && pathsEqual(f, o.getFile()) && areEqual(fragment, o.getRef())));
      }

      return false;
   }

   private static final boolean areEqual(String s1, String s2)
   {
      return (s1 == null && s2 == null || s1 != null && s2 != null
         && (s1.equals(s2) || unescapeNoPE(s1, null).equals(unescapeNoPE(s2, null))));
   }

   private static final boolean areEqualIC(String s1, String s2)
   {
      return (s1 == null && s2 == null || s1 != null && s2 != null
         && (s1.equalsIgnoreCase(s2) || unescapeNoPE(s1, null).equalsIgnoreCase(unescapeNoPE(s2, null))));
   }

   private static final boolean pathsEqual(String p1, String p2)
   {
      if (p1 == null && p2 == null)
         return true;
      if (p1 == null || p2 == null)
         return false;
      if (p1.equals(p2))
         return true;

      // ok, so it wasn't that simple. Let's split into parts and compare
      // unescaped.
      int pos1 = 0, end1 = p1.length(), pos2 = 0, end2 = p2.length();
      while (pos1 < end1 && pos2 < end2)
      {
         int start1 = pos1, start2 = pos2;

         char ch;
         while (pos1 < end1 && (ch = p1.charAt(pos1)) != '/' && ch != ';')
            pos1++;
         while (pos2 < end2 && (ch = p2.charAt(pos2)) != '/' && ch != ';')
            pos2++;

         if (pos1 == end1 && pos2 < end2 || pos2 == end2 && pos1 < end1 || pos1 < end1 && pos2 < end2
            && p1.charAt(pos1) != p2.charAt(pos2))
            return false;

         if ((!p1.regionMatches(start1, p2, start2, pos1 - start1) || (pos1 - start1) != (pos2 - start2))
            && !unescapeNoPE(p1.substring(start1, pos1), null).equals(unescapeNoPE(p2.substring(start2, pos2), null)))
            return false;

         pos1++;
         pos2++;
      }

      return (pos1 == end1 && pos2 == end2);
   }

   private int hashCode = -1;

   /**
    * The hash code is calculated over scheme, host, path, and query.
    * @return the hash code
    */
   @Override
   public int hashCode()
   {
      if (hashCode == -1)
         hashCode =
            (scheme != null ? unescapeNoPE(scheme, null).hashCode() : 0)
               + (type == OPAQUE ? (opaque != null ? unescapeNoPE(opaque, null).hashCode() : 0) * 7 : (host != null
                  ? unescapeNoPE(host, null).toLowerCase().hashCode() : 0)
                  * 7
                  + (path != null ? unescapeNoPE(path, null).hashCode() : 0)
                  * 13
                  + (query != null ? unescapeNoPE(query, null).hashCode() : 0) * 17);

      return hashCode;
   }

   /**
    * Escape any character not in the given character class. Characters greater
    * 255 are always escaped according to ??? .
    * @param elem the string to escape
    * @param allowed_char the BitSet of all allowed characters
    * @param utf8 if true, will first UTF-8 encode unallowed characters
    * @return the string with all characters not in allowed_char escaped
    */
   public static String escape(String elem, BitSet allowed_char, boolean utf8)
   {
      return new String(escape(elem.toCharArray(), allowed_char, utf8));
   }

   /**
    * Escape any character not in the given character class. Characters greater
    * 255 are always escaped according to ??? .
    * @param elem the array of characters to escape
    * @param allowed_char the BitSet of all allowed characters
    * @param utf8 if true, will first UTF-8 encode unallowed characters
    * @return the elem array with all characters not in allowed_char escaped
    */
   public static char[] escape(char[] elem, BitSet allowed_char, boolean utf8)
   {
      int cnt = 0;
      for (int idx = 0; idx < elem.length; idx++)
      {
         if (!allowed_char.get(elem[idx]))
         {
            cnt += 2;
            if (utf8)
            {
               if (elem[idx] >= 0x0080)
                  cnt += 3;
               if (elem[idx] >= 0x00800)
                  cnt += 3;
               if ((elem[idx] & 0xFC00) == 0xD800 && idx + 1 < elem.length && (elem[idx + 1] & 0xFC00) == 0xDC00)
                  cnt -= 6;
            }
         }
      }

      if (cnt == 0)
         return elem;

      char[] tmp = new char[elem.length + cnt];
      for (int idx = 0, pos = 0; idx < elem.length; idx++)
      {
         char c = elem[idx];
         if (allowed_char.get(c))
            tmp[pos++] = c;
         else if (utf8)
         {
            /*
             * We're UTF-8 encoding the chars first, as recommended in the HTML 4.0
             * specification:
             * http://www.w3.org/TR/REC-html40/appendix/notes.html#h-B.2.1 Note that
             * this doesn't change things for ASCII chars
             */
            if (c <= 0x007F)
            {
               pos = enc(tmp, pos, c);
            }
            else if (c <= 0x07FF)
            {
               pos = enc(tmp, pos, 0xC0 | ((c >> 6) & 0x1F));
               pos = enc(tmp, pos, 0x80 | ((c >> 0) & 0x3F));
            }
            else if (!((c & 0xFC00) == 0xD800 && idx + 1 < elem.length && (elem[idx + 1] & 0xFC00) == 0xDC00))
            {
               pos = enc(tmp, pos, 0xE0 | ((c >> 12) & 0x0F));
               pos = enc(tmp, pos, 0x80 | ((c >> 6) & 0x3F));
               pos = enc(tmp, pos, 0x80 | ((c >> 0) & 0x3F));
            }
            else
            {
               int ch = ((c & 0x03FF) << 10) | (elem[++idx] & 0x03FF);
               ch += 0x10000;
               pos = enc(tmp, pos, 0xF0 | ((ch >> 18) & 0x07));
               pos = enc(tmp, pos, 0x80 | ((ch >> 12) & 0x3F));
               pos = enc(tmp, pos, 0x80 | ((ch >> 6) & 0x3F));
               pos = enc(tmp, pos, 0x80 | ((ch >> 0) & 0x3F));
            }
         }
         else
            pos = enc(tmp, pos, c);
      }

      return tmp;
   }

   private static final char[] hex = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};

   private static final int enc(char[] out, int pos, int c)
   {
      out[pos++] = '%';
      out[pos++] = hex[(c >> 4) & 0xf];
      out[pos++] = hex[c & 0xf];
      return pos;
   }

   /**
    * Unescape escaped characters (i.e. %xx) except reserved ones.
    * @param str the string to unescape
    * @param reserved the characters which may not be unescaped, or null
    * @return the unescaped string
    * @exception ParseException if the two digits following a `%' are not a valid
    *              hex number
    */
   public static final String unescape(String str, BitSet reserved) throws ParseException
   {
      if (str == null || str.indexOf('%') == -1)
         return str; // an optimization

      char[] buf = str.toCharArray();
      char[] res = new char[buf.length];

      char[] utf = new char[4];
      int utf_idx = 0, utf_len = -1;
      int didx = 0;
      for (int sidx = 0; sidx < buf.length; sidx++)
      {
         if (buf[sidx] == '%')
         {
            int ch;
            try
            {
               if (sidx + 3 > buf.length)
                  throw new NumberFormatException();
               ch = Integer.parseInt(str.substring(sidx + 1, sidx + 3), 16);
               if (ch < 0)
                  throw new NumberFormatException();
               sidx += 2;
            }
            catch (NumberFormatException e)
            {
               /*
                * Hmm, people not reading specs again, so we just ignore it... throw
                * new ParseException(str.substring(sidx,sidx+3) + " is an invalid
                * code");
                */
               ch = buf[sidx];
            }

            // check if we're working on a utf-char
            if (utf_len > 0)
            {
               if ((ch & 0xC0) != 0x80) // oops, we misinterpreted
               {
                  didx = copyBuf(utf, utf_idx, ch, res, didx, reserved, false);
                  utf_len = -1;
               }
               else if (utf_idx == utf_len - 1) // end-of-char
               {
                  if ((utf[0] & 0xE0) == 0xC0)
                     ch = (utf[0] & 0x1F) << 6 | (ch & 0x3F);
                  else if ((utf[0] & 0xF0) == 0xE0)
                     ch = (utf[0] & 0x0F) << 12 | (utf[1] & 0x3F) << 6 | (ch & 0x3F);
                  else
                     ch = (utf[0] & 0x07) << 18 | (utf[1] & 0x3F) << 12 | (utf[2] & 0x3F) << 6 | (ch & 0x3F);
                  if (reserved != null && reserved.get(ch))
                     didx = copyBuf(utf, utf_idx, ch, res, didx, null, true);
                  else if (utf_len < 4)
                     res[didx++] = (char)ch;
                  else
                  {
                     ch -= 0x10000;
                     res[didx++] = (char)((ch >> 10) | 0xD800);
                     res[didx++] = (char)((ch & 0x03FF) | 0xDC00);
                  }
                  utf_len = -1;
               }
               else
                  // continue
                  utf[utf_idx++] = (char)ch;
            }
            // check if this is the start of a utf-char
            else if ((ch & 0xE0) == 0xC0 || (ch & 0xF0) == 0xE0 || (ch & 0xF8) == 0xF0)
            {
               if ((ch & 0xE0) == 0xC0)
                  utf_len = 2;
               else if ((ch & 0xF0) == 0xE0)
                  utf_len = 3;
               else
                  utf_len = 4;
               utf[0] = (char)ch;
               utf_idx = 1;
            }
            // leave reserved alone
            else if (reserved != null && reserved.get(ch))
            {
               res[didx++] = buf[sidx];
               sidx -= 2;
            }
            // just use the decoded version
            else
               res[didx++] = (char)ch;
         }
         else if (utf_len > 0) // oops, we misinterpreted
         {
            didx = copyBuf(utf, utf_idx, buf[sidx], res, didx, reserved, false);
            utf_len = -1;
         }
         else
            res[didx++] = buf[sidx];
      }
      if (utf_len > 0) // oops, we misinterpreted
         didx = copyBuf(utf, utf_idx, -1, res, didx, reserved, false);

      return new String(res, 0, didx);
   }

   private static final int copyBuf(char[] utf, int utf_idx, int ch, char[] res, int didx, BitSet reserved,
      boolean escapeAll)
   {
      if (ch >= 0)
         utf[utf_idx++] = (char)ch;

      for (int idx = 0; idx < utf_idx; idx++)
      {
         if (reserved != null && reserved.get(utf[idx]) || escapeAll)
            didx = enc(res, didx, utf[idx]);
         else
            res[didx++] = utf[idx];
      }

      return didx;
   }

   /**
    * Unescape escaped characters (i.e. %xx). If a ParseException would be thrown
    * then just return the original string.
    * @param str the string to unescape
    * @param reserved the characters which may not be unescaped, or null
    * @return the unescaped string, or the original string if unescaping would
    *         throw a ParseException
    * @see #unescape(java.lang.String, java.util.BitSet)
    */
   private static final String unescapeNoPE(String str, BitSet reserved)
   {
      try
      {
         return unescape(str, reserved);
      }
      catch (ParseException pe)
      {
         return str;
      }
   }

   /**
    * Run test set.
    * @exception Exception if any test fails
    */
   public static void main(String args[]) throws Exception
   {
      System.err.println(); //NOSONAR
      System.err.println("*** URI Tests ..."); //NOSONAR

      /*
       * Relative URI test set, taken from Section C of rfc-2396 and Roy's test1.
       * All Roy's URI parser tests can be found at
       * http://www.ics.uci.edu/~fielding/url/ The tests have been augmented by a
       * few for the IPv6 syntax
       */

      URI base = new URI("http://a/b/c/d;p?q");

      // normal examples
      testParser(base, "g:h", "g:h");
      testParser(base, "g", "http://a/b/c/g");
      testParser(base, "./g", "http://a/b/c/g");
      testParser(base, "g/", "http://a/b/c/g/");
      testParser(base, "/g", "http://a/g");
      testParser(base, "//g", "http://g");
      testParser(base, "//[23:54]", "http://[23:54]");
      testParser(base, "?y", "http://a/b/c/?y");
      testParser(base, "g?y", "http://a/b/c/g?y");
      testParser(base, "#s", "http://a/b/c/d;p?q#s");
      testParser(base, "g#s", "http://a/b/c/g#s");
      testParser(base, "g?y#s", "http://a/b/c/g?y#s");
      testParser(base, ";x", "http://a/b/c/;x");
      testParser(base, "g;x", "http://a/b/c/g;x");
      testParser(base, "g;x?y#s", "http://a/b/c/g;x?y#s");
      testParser(base, ".", "http://a/b/c/");
      testParser(base, "./", "http://a/b/c/");
      testParser(base, "..", "http://a/b/");
      testParser(base, "../", "http://a/b/");
      testParser(base, "../g", "http://a/b/g");
      testParser(base, "../..", "http://a/");
      testParser(base, "../../", "http://a/");
      testParser(base, "../../g", "http://a/g");

      // abnormal examples
      testParser(base, "", "http://a/b/c/d;p?q");
      testParser(base, "/./g", "http://a/./g");
      testParser(base, "/../g", "http://a/../g");
      testParser(base, "../../../g", "http://a/../g");
      testParser(base, "../../../../g", "http://a/../../g");
      testParser(base, "g.", "http://a/b/c/g.");
      testParser(base, ".g", "http://a/b/c/.g");
      testParser(base, "g..", "http://a/b/c/g..");
      testParser(base, "..g", "http://a/b/c/..g");
      testParser(base, "./../g", "http://a/b/g");
      testParser(base, "./g/.", "http://a/b/c/g/");
      testParser(base, "g/./h", "http://a/b/c/g/h");
      testParser(base, "g/../h", "http://a/b/c/h");
      testParser(base, "g;x=1/./y", "http://a/b/c/g;x=1/y");
      testParser(base, "g;x=1/../y", "http://a/b/c/y");
      testParser(base, "g?y/./x", "http://a/b/c/g?y/./x");
      testParser(base, "g?y/../x", "http://a/b/c/g?y/../x");
      testParser(base, "g#s/./x", "http://a/b/c/g#s/./x");
      testParser(base, "g#s/../x", "http://a/b/c/g#s/../x");
      if (ENABLE_BACKWARDS_COMPATIBILITY)
         testParser(base, "http:g", "http://a/b/c/g");
      else
         testParser(base, "http:g", "http:g");
      if (ENABLE_BACKWARDS_COMPATIBILITY)
         testParser(base, "http:", "http://a/b/c/d;p?q");
      else
         testParser(base, "http:", "http:");
      testParser(base, "./g:h", "http://a/b/c/g:h");

      /*
       * Roy's test2
       */
      base = new URI("http://a/b/c/d;p?q=1/2");

      testParser(base, "g", "http://a/b/c/g");
      testParser(base, "./g", "http://a/b/c/g");
      testParser(base, "g/", "http://a/b/c/g/");
      testParser(base, "/g", "http://a/g");
      testParser(base, "//g", "http://g");
      testParser(base, "//[23:54]", "http://[23:54]");
      testParser(base, "?y", "http://a/b/c/?y");
      testParser(base, "g?y", "http://a/b/c/g?y");
      testParser(base, "g?y/./x", "http://a/b/c/g?y/./x");
      testParser(base, "g?y/../x", "http://a/b/c/g?y/../x");
      testParser(base, "g#s", "http://a/b/c/g#s");
      testParser(base, "g#s/./x", "http://a/b/c/g#s/./x");
      testParser(base, "g#s/../x", "http://a/b/c/g#s/../x");
      testParser(base, "./", "http://a/b/c/");
      testParser(base, "../", "http://a/b/");
      testParser(base, "../g", "http://a/b/g");
      testParser(base, "../../", "http://a/");
      testParser(base, "../../g", "http://a/g");

      /*
       * Roy's test3
       */
      base = new URI("http://a/b/c/d;p=1/2?q");

      testParser(base, "g", "http://a/b/c/d;p=1/g");
      testParser(base, "./g", "http://a/b/c/d;p=1/g");
      testParser(base, "g/", "http://a/b/c/d;p=1/g/");
      testParser(base, "g?y", "http://a/b/c/d;p=1/g?y");
      testParser(base, ";x", "http://a/b/c/d;p=1/;x");
      testParser(base, "g;x", "http://a/b/c/d;p=1/g;x");
      testParser(base, "g;x=1/./y", "http://a/b/c/d;p=1/g;x=1/y");
      testParser(base, "g;x=1/../y", "http://a/b/c/d;p=1/y");
      testParser(base, "./", "http://a/b/c/d;p=1/");
      testParser(base, "../", "http://a/b/c/");
      testParser(base, "../g", "http://a/b/c/g");
      testParser(base, "../../", "http://a/b/");
      testParser(base, "../../g", "http://a/b/g");

      /*
       * Roy's test4
       */
      base = new URI("fred:///s//a/b/c");

      testParser(base, "g:h", "g:h");
      /*
       * we have to skip these, as usesGeneraicSyntax("fred") returns false and we
       * therefore don't parse relative URI's here. But test5 is the same except
       * that the http scheme is used. testParser(base, "g", "fred:///s//a/b/g");
       * testParser(base, "./g", "fred:///s//a/b/g"); testParser(base, "g/",
       * "fred:///s//a/b/g/"); testParser(base, "/g", "fred:///g");
       * testParser(base, "//g", "fred://g"); testParser(base, "//g/x",
       * "fred://g/x"); testParser(base, "///g", "fred:///g"); testParser(base,
       * "./", "fred:///s//a/b/"); testParser(base, "../", "fred:///s//a/");
       * testParser(base, "../g", "fred:///s//a/g"); testParser(base, "../../",
       * "fred:///s//"); testParser(base, "../../g", "fred:///s//g");
       * testParser(base, "../../../g", "fred:///s/g"); testParser(base,
       * "../../../../g", "fred:///g");
       */
      testPE(base, "g");

      /*
       * Roy's test5
       */
      base = new URI("http:///s//a/b/c");

      testParser(base, "g:h", "g:h");
      testParser(base, "g", "http:///s//a/b/g");
      testParser(base, "./g", "http:///s//a/b/g");
      testParser(base, "g/", "http:///s//a/b/g/");
      testParser(base, "/g", "http:///g");
      testParser(base, "//g", "http://g");
      testParser(base, "//[23:54]", "http://[23:54]");
      testParser(base, "//g/x", "http://g/x");
      testParser(base, "///g", "http:///g");
      testParser(base, "./", "http:///s//a/b/");
      testParser(base, "../", "http:///s//a/");
      testParser(base, "../g", "http:///s//a/g");
      testParser(base, "../../", "http:///s//");
      testParser(base, "../../g", "http:///s//g");
      testParser(base, "../../../g", "http:///s/g");
      testParser(base, "../../../../g", "http:///g");

      /*
       * Some additional parser tests
       */
      base = new URI("http://s");

      testParser(base, "ftp:h", "ftp:h");
      testParser(base, "ftp://h", "ftp://h");
      testParser(base, "//g", "http://g");
      testParser(base, "//g?h", "http://g?h");
      testParser(base, "g", "http://s/g");
      testParser(base, "./g", "http://s/g");
      testParser(base, "?g", "http://s/?g");
      testParser(base, "#g", "http://s#g");

      base = new URI("http:");

      testParser(base, "ftp:h", "ftp:h");
      testParser(base, "ftp://h", "ftp://h");
      testParser(base, "//g", "http://g");
      testParser(base, "g", "http:/g");
      testParser(base, "?g", "http:/?g");
      testParser(base, "#g", "http:#g");

      base = new URI("http://s/t");

      testParser(base, "ftp:/h", "ftp:/h");
      if (ENABLE_BACKWARDS_COMPATIBILITY)
         testParser(base, "http:/h", "http://s/h");
      else
         testParser(base, "http:/h", "http:/h");

      base = new URI("http://s/g?h/j");
      testParser(base, "k", "http://s/k");
      testParser(base, "k?l", "http://s/k?l");

      /*
       * Parser tests for semi-generic syntax
       */
      base = new URI("ldap:");

      testParser(base, "ldap:", "ldap:");
      testParser(base, "ldap://a", "ldap://a");
      testParser(base, "ldap://a/b", "ldap://a/b");
      testParser(base, "ldap:/b", "ldap:/b");

      testParser(base, "ftp:h", "ftp:h");
      testParser(base, "ftp://h", "ftp://h");
      testParser(base, "//g", "ldap://g");
      testParser(base, "//g?h", "ldap://g/?h");
      testParser(base, "g", "ldap:/g");
      testParser(base, "./g", "ldap:/./g");
      testParser(base, "?g", "ldap:/?g");
      testParser(base, "#g", "ldap:/%23g");

      base = new URI("ldap://s");

      if (ENABLE_BACKWARDS_COMPATIBILITY)
         testParser(base, "ldap:", "ldap://s");
      else
         testParser(base, "ldap:", "ldap:");
      testParser(base, "ldap://a", "ldap://a");
      testParser(base, "ldap://a/b", "ldap://a/b");
      if (ENABLE_BACKWARDS_COMPATIBILITY)
         testParser(base, "ldap:/b", "ldap://s/b");
      else
         testParser(base, "ldap:/b", "ldap:/b");

      testParser(base, "ftp:h", "ftp:h");
      testParser(base, "ftp://h", "ftp://h");
      testParser(base, "//g", "ldap://g");
      testParser(base, "//g?h", "ldap://g/?h");
      testParser(base, "g", "ldap://s/g");
      testParser(base, "./g", "ldap://s/./g");
      testParser(base, "?g", "ldap://s/?g");
      testParser(base, "#g", "ldap://s/%23g");

      base = new URI("ldap://s/t");

      testParser(base, "ftp:/h", "ftp:/h");
      if (ENABLE_BACKWARDS_COMPATIBILITY)
         testParser(base, "ldap:/h", "ldap://s/h");
      else
         testParser(base, "ldap:/h", "ldap:/h");

      if (ENABLE_BACKWARDS_COMPATIBILITY)
         testParser(base, "ldap:", "ldap://s");
      else
         testParser(base, "ldap:", "ldap:");
      testParser(base, "ldap://a", "ldap://a");
      testParser(base, "ldap://a/b", "ldap://a/b");

      testParser(base, "ftp:h", "ftp:h");
      testParser(base, "ftp://h", "ftp://h");
      testParser(base, "//g", "ldap://g");
      testParser(base, "//g?h", "ldap://g/?h");
      testParser(base, "g", "ldap://s/g");
      testParser(base, "./g", "ldap://s/./g");
      testParser(base, "?g", "ldap://s/?g");
      testParser(base, "#g", "ldap://s/%23g");

      /* equality tests */

      // protocol
      testNotEqual("http://a/", "nntp://a/");
      testNotEqual("http://a/", "https://a/");
      testNotEqual("http://a/", "shttp://a/");
      testEqual("http://a/", "Http://a/");
      testEqual("http://a/", "hTTP://a/");
      testEqual("url:http://a/", "hTTP://a/");
      testEqual("urI:http://a/", "hTTP://a/");

      // host
      testEqual("http://a/", "Http://A/");
      testEqual("http://a.b.c/", "Http://A.b.C/");
      testEqual("http:///", "Http:///");
      testEqual("http://[]/", "Http:///");
      testNotEqual("http:///", "Http://a/");
      testNotEqual("http://[]/", "Http://a/");
      testPE(null, "ftp://[23::43:1/");
      testPE(null, "ftp://[/");

      // port
      testEqual("http://a.b.c/", "Http://A.b.C:80/");
      testEqual("http://a.b.c:/", "Http://A.b.C:80/");
      testEqual("http://[23::45:::5:]/", "Http://[23::45:::5:]:80/");
      testEqual("http://[23::45:::5:]:/", "Http://[23::45:::5:]:80/");
      testEqual("nntp://a", "nntp://a:119");
      testEqual("nntp://a:", "nntp://a:119");
      testEqual("nntp://a/", "nntp://a:119/");
      testNotEqual("nntp://a", "nntp://a:118");
      testNotEqual("nntp://a", "nntp://a:0");
      testNotEqual("nntp://a:", "nntp://a:0");
      testEqual("telnet://:23/", "telnet:///");
      testPE(null, "ftp://:a/");
      testPE(null, "ftp://:-1/");
      testPE(null, "ftp://::1/");

      // userinfo
      testNotEqual("ftp://me@a", "ftp://a");
      testNotEqual("ftp://me@a", "ftp://Me@a");
      testEqual("ftp://Me@a", "ftp://Me@a");
      testEqual("ftp://Me:My@a:21", "ftp://Me:My@a");
      testEqual("ftp://Me:My@a:", "ftp://Me:My@a");
      testNotEqual("ftp://Me:My@a:21", "ftp://Me:my@a");
      testNotEqual("ftp://Me:My@a:", "ftp://Me:my@a");

      // path
      testEqual("ftp://a/b%2b/", "ftp://a/b+/");
      testEqual("ftp://a/b%2b/", "ftp://a/b+/");
      testEqual("ftp://a/b%5E/", "ftp://a/b^/");
      testEqual("ftp://a/b%4C/", "ftp://a/bL/");
      testNotEqual("ftp://a/b/", "ftp://a//b/");
      testNotEqual("ftp://a/b/", "ftp://a/b//");
      testNotEqual("ftp://a/b%4C/", "ftp://a/bl/");
      testNotEqual("ftp://a/b%3f/", "ftp://a/b?/");
      testNotEqual("ftp://a/b%2f/", "ftp://a/b//");
      testNotEqual("ftp://a/b%2fc/", "ftp://a/b/c/");
      testNotEqual("ftp://a/bc/", "ftp://a/b//");
      testNotEqual("ftp://a/bc/", "ftp://a/b/");
      testNotEqual("ftp://a/bc//", "ftp://a/b/");
      testNotEqual("ftp://a/b/", "ftp://a/bc//");
      testNotEqual("ftp://a/b/", "ftp://a/bc/");
      testNotEqual("ftp://a/b//", "ftp://a/bc/");

      testNotEqual("ftp://a/b;fc/", "ftp://a/bf;c/");
      testNotEqual("ftp://a/b%3bfc/", "ftp://a/b;fc/");
      testEqual("ftp://a/b;/;/", "ftp://a/b;/;/");
      testNotEqual("ftp://a/b;/", "ftp://a/b//");
      testNotEqual("ftp://a/b//", "ftp://a/b;/");
      testNotEqual("ftp://a/b/;", "ftp://a/b//");
      testNotEqual("ftp://a/b//", "ftp://a/b/;");
      testNotEqual("ftp://a/b;/", "ftp://a/b;//");
      testNotEqual("ftp://a/b;//", "ftp://a/b;/");

      // escaping/unescaping
      testEscape("hello\u1212there", "hello%E1%88%92there");
      testEscape("hello\u0232there", "hello%C8%B2there");
      testEscape("hello\uDA42\uDD42there", "hello%F2%A0%A5%82there");
      testEscape("hello\uDA42", "hello%ED%A9%82");
      testEscape("hello\uDA42there", "hello%ED%A9%82there");
      testUnescape("hello%F2%A0%A5%82there", "hello\uDA42\uDD42there");
      testUnescape("hello%F2%A0%A5there", "hello\u00F2\u00A0\u00A5there");
      testUnescape("hello%F2%A0there", "hello\u00F2\u00A0there");
      testUnescape("hello%F2there", "hello\u00F2there");
      testUnescape("hello%F2%A0%A5%82", "hello\uDA42\uDD42");
      testUnescape("hello%F2%A0%A5", "hello\u00F2\u00A0\u00A5");
      testUnescape("hello%F2%A0", "hello\u00F2\u00A0");
      testUnescape("hello%F2", "hello\u00F2");
      testUnescape("hello%E1%88%92there", "hello\u1212there");
      testUnescape("hello%E1%88there", "hello\u00E1\u0088there");
      testUnescape("hello%E1there", "hello\u00E1there");
      testUnescape("hello%E1%71there", "hello\u00E1qthere");
      testUnescape("hello%E1%88", "hello\u00E1\u0088");
      testUnescape("hello%E1%71", "hello\u00E1q");
      testUnescape("hello%E1", "hello\u00E1");
      testUnescape("hello%C8%B2there", "hello\u0232there");
      testUnescape("hello%C8there", "hello\u00C8there");
      testUnescape("hello%C8%71there", "hello\u00C8qthere");
      testUnescape("hello%C8%71", "hello\u00C8q");
      testUnescape("hello%C8", "hello\u00C8");
      testUnescape("%71there", "qthere");
      testUnescape("%B1there", "\u00B1there");

      System.err.println("*** Tests finished successfuly"); //NOSONAR
   }

   private static final String nl = System.getProperty("line.separator");

   private static void testParser(URI base, String relURI, String result) throws Exception
   {
      if (!(new URI(base, relURI).toExternalForm().equals(result)))
      {
         throw new Exception("Test failed: " + nl + "  base-URI = <" + base + ">" + nl + "  rel-URI  = <" + relURI
            + ">" + nl + "  expected   <" + result + ">" + nl + "  but got    <" + new URI(base, relURI) + ">");
      }
   }

   private static void testEqual(String one, String two) throws Exception
   {
      URI u1 = new URI(one);
      URI u2 = new URI(two);

      if (!u1.equals(u2))
      {
         throw new Exception("Test failed: " + nl + "  <" + one + "> != <" + two + ">");
      }
      if (u1.hashCode() != u2.hashCode())
      {
         throw new Exception("Test failed: " + nl + "  hashCode <" + one + "> != hashCode <" + two + ">");
      }
   }

   private static void testNotEqual(String one, String two) throws Exception
   {
      URI u1 = new URI(one);
      URI u2 = new URI(two);

      if (u1.equals(u2))
      {
         throw new Exception("Test failed: " + nl + "  <" + one + "> == <" + two + ">");
      }
   }

   private static void testPE(URI base, String uri) throws Exception
   {
      boolean got_pe = false;
      try
      {
         new URI(base, uri);
      }
      catch (ParseException pe)
      {
         got_pe = true;
      }
      if (!got_pe)
      {
         throw new Exception("Test failed: " + nl + "  <" + uri + "> should be invalid");
      }
   }

   private static void testEscape(String raw, String escaped) throws Exception
   {
      String test = new String(escape(raw.toCharArray(), uricChar, true));
      if (!test.equals(escaped))
         throw new Exception("Test failed: " + nl + "  raw-string: " + raw + nl + "  escaped:    " + test + nl
            + "  expected:   " + escaped);
   }

   private static void testUnescape(String escaped, String raw) throws Exception
   {
      if (!unescape(escaped, null).equals(raw))
         throw new Exception("Test failed: " + nl + "  escaped-string: " + escaped + nl + "  unescaped:      "
            + unescape(escaped, null) + nl + "  expected:       " + raw);
   }
}
