URL Class

package javaxt.utils;
import java.util.*;

//******************************************************************************
//**  URL Class
//******************************************************************************
/**
 *   Used to parse urls, extract querystring parameters, etc. Partial
 *   implementation of the java.net.URL class. Provides a querystring parser
 *   that is not part of the java.net.URL class. Can be used to parse non-http
 *   URLs, including JDBC connection strings.
 *
 ******************************************************************************/

public class URL {

    private LinkedHashMap<String, List<String>> parameters;
    private LinkedHashMap<String, List<String>> extendedParameters;
    private String protocol;
    private String host;
    private Integer port;
    private String path;


  //**************************************************************************
  //** Constructor
  //**************************************************************************
  /** Creates a new instance of URL using a java.net.URL */

    public URL(java.net.URL url){
        this(url.toString());
    }


  //**************************************************************************
  //** Constructor
  //**************************************************************************
  /** Creates a new instance of URL using string representing a url. */

    public URL(String url){

        url = url.trim();
        parameters = new LinkedHashMap<>();
        extendedParameters = new LinkedHashMap<>();


        if (url.contains("://")){
            protocol = url.substring(0,url.indexOf("://"));
            url = url.substring(url.indexOf("://") + 3);
        }
        else{
            if (url.startsWith("jdbc")){
                protocol = url.substring(0, url.indexOf(";"));
                url = url.substring(url.indexOf(";")+1);
            }
        }



        if (url.contains("?")){
            String query = url.substring(url.indexOf("?")+1);
            url = url.substring(0, url.indexOf("?"));
            parameters = parseQueryString(query);
        }
        else{ //no query string, check for jdbc params

            int idx = url.indexOf(";");
            if (idx>-1){ //found jdbc delimiter
                extendedParameters = parseJDBCParams(url.substring(idx+1));
                url = url.substring(0, idx);
            }
        }

        if (url.contains("/")){
            path = url.substring(url.indexOf("/"));
            url = url.substring(0, url.indexOf("/"));
        }

        if (url.contains(":")){
            try{
                port = Integer.valueOf(url.substring(url.indexOf(":")+1));
                url = url.substring(0, url.indexOf(":"));
            }
            catch(Exception e){}
        }

        host = url;
    }


  //**************************************************************************
  //** exists
  //**************************************************************************
  /** Used to test whether the url endpoint exists. Currently only supports
   *  HTTP URLs.
   */
    public boolean exists(){
        try{
            java.net.URLConnection conn = new java.net.URL(this.toString()).openConnection();
            conn.setConnectTimeout(5000);
            conn.getInputStream();
            return true;
        }
        catch(Exception e){
            //System.err.println(e.toString());
            //System.err.println("URL not found: " + this.toString());
        }
        return false;

    }


  //**************************************************************************
  //** parseQueryString
  //**************************************************************************
  /** Used to parse a url query string and create a list of name/value pairs.
   */
    public static LinkedHashMap<String, List<String>> parseQueryString(String query){


      //Create an empty hashmap
        LinkedHashMap<String, List<String>> parameters = new LinkedHashMap<>();
        if (query==null) return parameters;

        query = query.trim();
        if (query.length()==0) return parameters;


      //Parse the querystring, one character at a time
        if (query.startsWith("&")) query = query.substring(1);
        query += "&";


        StringBuffer word = new StringBuffer();
        for (int i=0; i<query.length(); i++){

            String c = query.substring(i,i+1);
            if (c.equals("&")){

                if (i+5<query.length() && query.substring(i,i+5).equals("&")){
                    word.append(c);
                }
                else{
                    int x = word.indexOf("=");
                    if (x>=0){
                        String key = word.substring(0,x);
                        String value = decode(word.substring(x+1));

                        List<String> values = getParameter(key, parameters);
                        if (values==null) values = new LinkedList<>();
                        values.add(value);
                        setParameter(key, values, parameters);
                    }
                    else{
                        setParameter(word.toString(), null, parameters);
                    }

                    word = new StringBuffer();
                }
            }
            else{
                word.append(c);
            }
        }

        return parameters;
    }


  //**************************************************************************
  //** parseJDBCParams
  //**************************************************************************
  /** Used to parse a JDBC parameter strings and return a list of name/value
   *  pairs.
   */
    public static LinkedHashMap<String, List<String>> parseJDBCParams(String params){
        LinkedHashMap<String, List<String>> parameters = new LinkedHashMap<>();
        final String[] pairs = params.split(";");
        for (String pair : pairs) {
            int idx = pair.indexOf("=");
            String key = idx > 0 ? decode(pair.substring(0, idx)) : pair;
            String value = idx > 0 && pair.length() > idx + 1 ? decode(pair.substring(idx + 1)) : null;

            if (!parameters.containsKey(key)) parameters.put(key, new LinkedList<>());
            parameters.get(key).add(value);
        }
        return parameters;
    }


  //**************************************************************************
  //** decode
  //**************************************************************************
  /** Used to decode a URL encoded string
   */
    public static String decode(String str){
        try{

          //Replace unencoded "%" characters with "%25". The regex finds a "%"
          //but only those that are NOT followed by 2 hex characters (0-F),
          //then replaces with the ENCODED version of the % character "%25".
          //Credit: https://stackoverflow.com/a/18368345
            str = str.replaceAll("%(?![0-9a-fA-F]{2})", "%25");


          //Decode the string with the URLDecoder
            if (str.contains("+")){
                StringBuilder out = new StringBuilder();
                while (str.contains("+")){
                    int idx = str.indexOf("+");
                    if (idx==0){
                        out.append("+");
                        str = str.substring(1);
                    }
                    else{
                        out.append(java.net.URLDecoder.decode(str.substring(0, idx), "UTF-8"));
                        str = str.substring(idx);
                    }
                }
                out.append(java.net.URLDecoder.decode(str, "UTF-8"));
                return out.toString();
            }
            else{
                return java.net.URLDecoder.decode(str, "UTF-8");
            }
        }
        catch(Exception e){
            return str;
        }
    }


  //**************************************************************************
  //** encode
  //**************************************************************************
  /** Used to URL encode a string
   */
    public static String encode(String str){
        try{
            if (str.contains(" ")){
                StringBuilder out = new StringBuilder();
                while (str.contains(" ")){
                    int idx = str.indexOf(" ");
                    if (idx==0){
                        out.append("%20");
                        str = str.substring(1);
                    }
                    else{
                        out.append(java.net.URLEncoder.encode(str.substring(0, idx), "UTF-8"));
                        str = str.substring(idx);
                    }
                }
                out.append(java.net.URLEncoder.encode(str, "UTF-8"));
                return out.toString();
            }
            else{
                return java.net.URLEncoder.encode(str, "UTF-8"); //.replaceAll("\\+", "%2B");
            }
        }
        catch(Exception e){
            return str;
        }
    }


  //**************************************************************************
  //** setParameter
  //**************************************************************************
  /** Used to set or update a value for a given parameter in the query string.
   *  If append is true, the value will be added to other values for this key.
   */
    public void setParameter(String key, String value, boolean append){

        if (value!=null) value = decode(value);

        key = key.toLowerCase();
        if (append){
            List<String> values = getParameter(key, parameters);
            Iterator<String> it = values.iterator();
            while(it.hasNext()){
                if (it.next().equalsIgnoreCase(value)){
                    append = false;
                    break;
                }
            }
            if (append) {
                values.add(value);
                setParameter(key, values, parameters);
            }

        }
        else{
            if (value!=null){
                List<String> values = new LinkedList<>();
                values.add(value);
                setParameter(key, values, parameters);
            }
        }

    }


  //**************************************************************************
  //** setParameter
  //**************************************************************************
  /** Used to set or update a value for a given parameter.
   */
    public void setParameter(String key, String value){
        setParameter(key, value, false);
    }


  //**************************************************************************
  //** getParameter
  //**************************************************************************
  /** Returns the value of a specific variable supplied in the query string.
   *
   *  @param key Query string parameter name. Performs a case insensitive
   *   search for the keyword.
   *
   *  @return Returns a comma delimited list of values associated with the
   *  given key. Returns a zero length string if the key is not found or if
   *  the value is null.
   */
    public String getParameter(String key){
        StringBuilder str = new StringBuilder();
        List<String> values = getParameter(key, parameters);
        if (values!=null){
            for (int i=0; i<values.size(); i++){
                str.append(values.get(i));
                if (i<values.size()-1) str.append(",");
            }
            return str.toString();
        }
        else{
            return "";
        }
    }


  //**************************************************************************
  //** getParameter
  //**************************************************************************
  /** Returns the value of a specific variable supplied in the query string.
   *  @param keys An array containing multiple possible parameter names.
   *  Performs a case insensitive search for each parameter name and returns
   *  the value for the first match.
   *
   *  @return Returns a comma delimited list of values associated with the
   *  given keys. Returns a zero length string if the key is not found or if
   *  the value is null.
   */
    public String getParameter(String[] keys){

        StringBuilder str = new StringBuilder();
        for (String key : keys){
            List<String> values = getParameter(key.toLowerCase(), parameters);
            if (values!=null){
                for (int i=0; i<values.size(); i++){
                    str.append(values.get(i) + ",");
                }
            }
        }

        String value = str.toString();
        if (value.endsWith(",")) value = value.substring(0, value.length()-1);
        return value;
    }


  //**************************************************************************
  //** getParameters
  //**************************************************************************
  /** Returns a list of parameters found in query string.
   */
    public LinkedHashMap<String, List<String>> getParameters(){
        return parameters;
    }


  //**************************************************************************
  //** getExtendedParameters
  //**************************************************************************
  /** Returns a list of extended parameters (e.g. jdbc params) that are not
   *  part of a standard url
   */
    public LinkedHashMap<String, List<String>> getExtendedParameters(){
        return extendedParameters;
    }


  //**************************************************************************
  //** removeParameter
  //**************************************************************************
  /** Used to remove a parameter from the query string
   */
    public String removeParameter(String key){
        StringBuilder str = new StringBuilder();
        List<String> values = removeParameter(key, parameters);
        if (values!=null){
            for (int i=0; i<values.size(); i++){
                str.append(values.get(i));
                if (i<values.size()-1) str.append(",");
            }
            return str.toString();
        }
        else{
            return "";
        }
    }


    public static List<String> getParameter(String key, HashMap<String, List<String>> parameters){
        List<String> values = parameters.get(key);
        if (values==null){
            Iterator<String> it = parameters.keySet().iterator();
            while (it.hasNext()){
                String s = it.next();
                if (s.equalsIgnoreCase(key)) return parameters.get(s);
            }
        }
        return values;
    };

    public static void setParameter(String key, List<String> values, HashMap<String, List<String>> parameters){
        Iterator<String> it = parameters.keySet().iterator();
        while (it.hasNext()){
            String s = it.next();
            if (s.equalsIgnoreCase(key)){
                parameters.put(key, values);
                return;
            }
        }
        parameters.put(key, values);
    }

    public static List<String> removeParameter(String key, HashMap<String, List<String>> parameters){
        List<String> values = parameters.remove(key);
        if (values==null){
            Iterator<String> it = parameters.keySet().iterator();
            while (it.hasNext()){
                String s = it.next();
                if (s.equalsIgnoreCase(key)) return parameters.remove(s);
            }
        }
        return values;
    };


  //**************************************************************************
  //** getHost
  //**************************************************************************
  /** Returns the host name or IP address found in the URL.
   */
    public String getHost(){
        return host;
    }


  //**************************************************************************
  //** setHost
  //**************************************************************************
  /** Used to update the host name or IP address found in the URL.
   */
    public void setHost(String host){
        if (host.contains(":")){
            port = Integer.valueOf(host.substring(host.indexOf(":")+1));
            host = host.substring(0, host.indexOf(":"));
        }
        this.host = host;
    }


  //**************************************************************************
  //** getPort
  //**************************************************************************
  /** Returns the server port found in the URL.
   */
    public Integer getPort(){
        return port;
    }


  //**************************************************************************
  //** setPort
  //**************************************************************************
  /** Used to update the port found in the URL.
   */
    public void setPort(int port){
        this.port = port;
    }


  //**************************************************************************
  //** setProtocol
  //**************************************************************************
  /** Used to update the protocol found in the URL.
   */
    public void setProtocol(String protocol){
        this.protocol = protocol;
    }


  //**************************************************************************
  //** getProtocol
  //**************************************************************************
  /** Returns the protocol found in the URL.
   */
    public String getProtocol(){
        return protocol;
    }


  //**************************************************************************
  //** getQueryString
  //**************************************************************************
  /** Returns the query string in the URL, or an empty string if none exists.
   */
    public String getQueryString(){

        StringBuilder str = new StringBuilder();
        HashSet<String> keys = getKeys();
        Iterator<String> it = keys.iterator();
        while (it.hasNext()){
            String key = it.next();
            List<String> values = getParameter(key, parameters);


            if (values==null || values.isEmpty()){
                str.append(encode(key));
            }
            else{
                int x = 0;
                for (String value : values){
                    if (x>0) str.append("&");
                    str.append(encode(key));
                    if (value!=null){
                        if (value.trim().length()>0){
                            str.append("=");
                            boolean isEncoded = !decode(value).equals(value);
                            str.append(isEncoded ? value : encode(value));
                        }
                    }
                    x++;
                }
            }


            if (it.hasNext()) str.append("&");
        }
        return str.toString();
    }


  //**************************************************************************
  //** setQueryString
  //**************************************************************************
  /** Used to update the query string in the URL.
   */
    public void setQueryString(String query){
        if (query==null){
            parameters = new LinkedHashMap<>();
        }
        else{
            query = query.trim();
            if (query.startsWith("?")) query = query.substring(1).trim();
            if (query.length()>0){
                parameters = parseQueryString(query);
            }
        }
    }


  //**************************************************************************
  //** getKeys
  //**************************************************************************
  /** Returns a list of parameter names found in the query string.
   */
    public HashSet<String> getKeys(){
        HashSet<String> keys = new HashSet<>();
        Iterator<String> it = parameters.keySet().iterator();
        while(it.hasNext()){
            keys.add(it.next());
        }
        return keys;
    }


  //**************************************************************************
  //** getPath
  //**************************************************************************
  /** Return the path portion of the URL, starting with a "/" character. The
   *  path does not include the query string. If no path is found, returns a
   *  null.
   */
    public String getPath(){
        return path;
    }


  //**************************************************************************
  //** setPath
  //**************************************************************************
  /** Used to update the path portion of the URL. If the supplied path starts
   *  with "./" or "../", only part of the path will be replaced. Otherwise,
   *  the entire path will be replaced.
   *  <p/>
   *  When supplying a relative path (path starting with "./" or "../"), the
   *  url parser assumes that directories in the original path are terminated
   *  with a "/". For example:
   *  <pre>http://www.example.com/path/</pre>
   *  If a path is not terminated with a "/", the parser assumes that the last
   *  "/" separates a path from a file. Example:
   *  <pre>http://www.example.com/path/file.html</pre>
   *  For example, if the original url looks like this:
   *  <pre>http://www.example.com/path/</pre>
   *  If you provide a relative path like "../index.html", will yield this:
   *  <pre>http://www.example.com/index.html</pre>
   *  <p/>
   *  Note that if the supplied path contains a query string,
   *  the original query string will be replaced with the new one.
   */
    public void setPath(String path){
        if (path==null){
            path = "";
        }
        else {
            path = path.trim();

            if (path.contains("?")){
                String query = path.substring(path.indexOf("?")+1);
                path = path.substring(0, path.indexOf("?"));
                parameters = parseQueryString(query);
            }

            if (path.contains(";")){ //found jdbc delimiter
                path = path.substring(0, path.indexOf(";"));
            }


            if (!path.startsWith("/")){

                if (path.startsWith("./") || path.startsWith("../")){

                    String RelPath = path;


                  //Remove "./" prefix in the RelPath
                    if (RelPath.length()>2){
                        if (RelPath.substring(0,2).equals("./")){
                            RelPath = RelPath.substring(2,RelPath.length());
                        }
                    }



                  //Build Path
                    String urlPath = "";
                    String newPath = "";
                    if (RelPath.substring(0,1).equals("/")){
                        newPath = RelPath;
                    }
                    else{

                        urlPath = "/";
                        String dir = "";
                        String orgPath = getPath();
                        if (orgPath==null) orgPath = "";
                        if (orgPath.length()>1 && !orgPath.endsWith("/")){
                            orgPath = orgPath.substring(0, orgPath.lastIndexOf("/"));
                        }
                        String[] arr = orgPath.split("/");
                        String[] arrRelPath = RelPath.split("/");
                        for (int i=0; i<=(arr.length-arrRelPath.length); i++){
                             dir = arr[i];
                             if (dir.length()>0){
                                 urlPath += dir + "/";
                             }
                        }



                      //This can be cleaned-up a bit...
                        if (RelPath.substring(0,1).equals("/")){
                            newPath = RelPath.substring(1,RelPath.length());
                        }
                        else if (RelPath.substring(0,2).equals("./")){
                            newPath = RelPath.substring(2,RelPath.length());
                        }
                        else if (RelPath.substring(0,3).equals("../")){
                            newPath = RelPath.replace("../", "");
                        }
                        else{
                            newPath = RelPath;
                        }
                    }

                    //System.out.println("urlPath: " + urlPath);
                    //System.out.println("newPath: " + newPath);

                    path = urlPath + newPath;


                }
                else{
                    path = "/" + path;
                }
            }

        }
        this.path = path;
    }


  //**************************************************************************
  //** toString
  //**************************************************************************
  /**  Returns the URL as a string.
   */
    public String toString(){

      //Update Host
        String host = this.host;

        if (port!=null && port>0) host += ":" + port;

      //Update Path
        String path = "";
        if (getPath()!=null) path = getPath();

      //Update Query String
        String query = getQueryString();
        if (query.length()>0) query = "?" + query;

      //Assemble URL
        return protocol + "://" + host + path + query;
    }


  //**************************************************************************
  //** toURL
  //**************************************************************************
  /** Returns a properly encoded URL for HTTP requests
   */
    public java.net.URL toURL(){
        java.net.URL url = null;
        try{

            Integer port = this.port;
            if (port==null){
                if (protocol.equalsIgnoreCase("http")) port = 80;
                else if (protocol.equalsIgnoreCase("https")) port = 443;
                else if (protocol.equalsIgnoreCase("ftp")) port = 23;
                else{
                    try{
                        port = new java.net.URL(protocol + "://" + host).getPort();
                    }
                    catch(Exception e){}
                }
            }
            url = new java.net.URI(protocol, null, host, port, path, null, null).toURL();


          //Encode and append QueryString as needed
            String query = getQueryString();
            if (query.length()>0){
                url = new java.net.URL(url.toString() + "?" + query);
            }

        }
        catch(Exception e){
            //e.printStackTrace();
        }
        return url;

    }
}