/* Copyright (c) 2001-2024, The HSQL Development Group
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * Redistributions of source code must retain the above copyright notice, this
 * list of conditions and the following disclaimer.
 *
 * Redistributions in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer in the documentation
 * and/or other materials provided with the distribution.
 *
 * Neither the name of the HSQL Development Group nor the names of its
 * contributors may be used to endorse or promote products derived from this
 * software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL HSQL DEVELOPMENT GROUP, HSQLDB.ORG,
 * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */


package org.hsqldb;

import java.util.Locale;

import org.hsqldb.persist.HsqlProperties;
import org.hsqldb.server.ServerConstants;

/*
 * Parses a connection URL into parts.
 *
 * @author Fred Toussi (fredt@users dot sourceforge.net)
 * @version 2.7.3
 * @since 1.8.0
 */

// patch 1.9.0 by Blaine Simpson - IPv6 support
public final class DatabaseURL {

    public static final String S_DOT               = ".";
    public static final String S_MEM               = "mem:";
    public static final String S_FILE              = "file:";
    public static final String S_RES               = "res:";
    public static final String S_ALIAS             = "alias:";
    public static final String S_HSQL              = "hsql://";
    public static final String S_HSQLS             = "hsqls://";
    public static final String S_HTTP              = "http://";
    public static final String S_HTTPS             = "https://";
    public static final String S_URL_PREFIX        = "jdbc:hsqldb:";
    public static final String S_URL_INTERNAL      = "jdbc:default:connection";
    public static final String url_connection_type = "connection_type";
    public static final String url_database        = "database";

    /**
     * Returns true if type represents an in-process connection to database.
     */
    public static boolean isInProcessDatabaseType(String type) {
        return S_FILE.equals(type) || S_RES.equals(type) || S_MEM.equals(type);
    }

    /**
     * Parses the url into components that are returned in a properties object.
     *
     * The following components are isolated:<p>
     *
     * url: the original url
     * <ul>
     *
     * <li> connection_type: a static string that indicate the protocol. If the
     * url does not begin with a valid protocol, null is returned by this method
     * instead of the properties object.
     *
     * <li> host: name of host in networked modes in lowercase
     *
     * <li> port: port number in networked mode, or 0 if not present
     *
     * <li> path: path of the resource on server in networked modes, minimum
     * (slash) with path elements appended apart from servlet path which is
     * (slash) plus the name of the servlet
     *
     * <li> database: database name. For memory, networked modes,
     * this is returned in lowercase, for file: and res: databases the original case of
     * characters is preserved. Returns empty string if name is not present in
     * the url.
     *
     * <li> Additional connection properties specified as key/value pairs.
     * </ul>
     *
     * @return null returned if the part that should represent the port is not
     *   an integer or the part for database name is empty. Empty
     *   HsqlProperties returned if url does not begin with valid protocol
     *   and could refer to another JDBC driver.
     * @param url String
     * @param hasPrefix indicates URL prefix is present
     * @param noPath indicates empty path and verbatim use of path elements as
     * database
     */
    public static HsqlProperties parseURL(
            String url,
            boolean hasPrefix,
            boolean noPath) {

        String         urlImage   = url.toLowerCase(Locale.ENGLISH);
        HsqlProperties props      = new HsqlProperties();
        HsqlProperties extraProps = null;
        String         arguments  = null;
        int            pos        = 0;
        String         type       = null;
        int            port       = 0;
        String         database;
        String         path;
        boolean        isNetwork = false;

        if (hasPrefix) {
            if (urlImage.startsWith(S_URL_PREFIX)) {
                pos = S_URL_PREFIX.length();
            } else {
                return props;
            }
        }

        while (true) {
            int replacePos = url.indexOf("${");

            if (replacePos == -1) {
                break;
            }

            int endPos = url.indexOf("}", replacePos);

            if (endPos == -1) {
                break;
            }

            String varName  = url.substring(replacePos + 2, endPos);
            String varValue = null;

            try {
                varValue = System.getProperty(varName);
            } catch (SecurityException e) {}

            if (varValue == null) {
                break;
            }

            url = url.substring(
                0,
                replacePos) + varValue + url.substring(endPos + 1);
            urlImage = url.toLowerCase(Locale.ENGLISH);
        }

        props.setProperty("url", url);

        int postUrlPos = url.length();

        // postUrlPos is the END position in url String,
        // wrt what remains to be processed.
        // I.e., if postUrlPos is 100, url no longer needs to examined at
        // index 100 or later.
        int semiPos = url.indexOf(';', pos);

        if (semiPos > -1) {
            arguments  = url.substring(semiPos + 1, urlImage.length());
            postUrlPos = semiPos;
            extraProps = HsqlProperties.delimitedArgPairsToProps(
                arguments,
                "=",
                ";",
                null);

            // validity checks are performed by engine
            props.addProperties(extraProps);
        }

        if (postUrlPos == pos + 1 && urlImage.startsWith(S_DOT, pos)) {
            type = S_DOT;
        } else if (urlImage.startsWith(S_MEM, pos)) {
            type = S_MEM;
        } else if (urlImage.startsWith(S_FILE, pos)) {
            type = S_FILE;
        } else if (urlImage.startsWith(S_RES, pos)) {
            type = S_RES;
        } else if (urlImage.startsWith(S_ALIAS, pos)) {
            type = S_ALIAS;
        } else if (urlImage.startsWith(S_HSQL, pos)) {
            type      = S_HSQL;
            port      = ServerConstants.SC_DEFAULT_HSQL_SERVER_PORT;
            isNetwork = true;
        } else if (urlImage.startsWith(S_HSQLS, pos)) {
            type      = S_HSQLS;
            port      = ServerConstants.SC_DEFAULT_HSQLS_SERVER_PORT;
            isNetwork = true;
        } else if (urlImage.startsWith(S_HTTP, pos)) {
            type      = S_HTTP;
            port      = ServerConstants.SC_DEFAULT_HTTP_SERVER_PORT;
            isNetwork = true;
        } else if (urlImage.startsWith(S_HTTPS, pos)) {
            type      = S_HTTPS;
            port      = ServerConstants.SC_DEFAULT_HTTPS_SERVER_PORT;
            isNetwork = true;
        }

        if (type == null) {
            type = S_FILE;
        } else if (type == S_DOT) {
            type = S_MEM;

            // keep pos
        } else {
            pos += type.length();
        }

        props.setProperty("connection_type", type);

        if (isNetwork) {

            // First capture 3 segments:  host + port + path
            String pathSeg = null;
            String hostSeg = null;
            String portSeg = null;
            int    endPos  = url.indexOf('/', pos);

            if (endPos > 0 && endPos < postUrlPos) {
                pathSeg = url.substring(endPos, postUrlPos);

                // N.b. pathSeg necessarily begins with /.
            } else {
                endPos = postUrlPos;
            }

            // Processing different for ipv6 host address and all others:
            if (url.charAt(pos) == '[') {

                // ipv6
                int endIpv6 = url.indexOf(']', pos + 2);

                // Notice 2 instead of 1 to require non-empty addr segment
                if (endIpv6 < 0 || endIpv6 >= endPos) {
                    return null;

                    // Wish could throw something with a useful message for user
                    // here.
                }

                hostSeg = urlImage.substring(pos + 1, endIpv6);

                if (endPos > endIpv6 + 1) {
                    portSeg = url.substring(endIpv6 + 1, endPos);
                }
            } else {

                // non-ipv6
                int colPos = url.indexOf(':', pos + 1);

                if (colPos > -1 && colPos < endPos) {

                    // portSeg will be non-empty, but could contain just ":"
                    portSeg = url.substring(colPos, endPos);
                } else {
                    colPos = -1;
                }

                hostSeg = urlImage.substring(
                    pos,
                    (colPos > 0)
                    ? colPos
                    : endPos);
            }

            // At this point, the entire url has been parsed into
            // hostSeg + portSeg + pathSeg.
            if (portSeg != null) {
                if (portSeg.length() < 2 || portSeg.charAt(0) != ':') {

                    // Wish could throw something with a useful message for user
                    // here.
                    return null;
                }

                try {
                    port = Integer.parseInt(portSeg.substring(1));
                } catch (NumberFormatException e) {

                    // System.err.println("NFE for (" + portSeg + ')'); debug
                    return null;
                }
            }

            if (noPath) {
                path     = "";
                database = pathSeg;
            } else if (pathSeg == null) {
                path     = "/";
                database = "";
            } else {
                int lastSlashPos = pathSeg.lastIndexOf('/');

                if (lastSlashPos < 1) {
                    path     = "/";
                    database = pathSeg.substring(1).toLowerCase(Locale.ENGLISH);
                } else {
                    path     = pathSeg.substring(0, lastSlashPos);
                    database = pathSeg.substring(lastSlashPos + 1);
                }
            }

            /* Just for debug.  Remove once stable:
            System.err.println("Host seg (" + hostSeg + "), Port val (" + port
                    + "), Path val (" + pathSeg + "), path (" + path
                    + "), db (" + database + ')');
             */
            props.setProperty("port", port);
            props.setProperty("host", hostSeg);
            props.setProperty("path", path);

            if (!noPath && extraProps != null) {
                String filePath = extraProps.getProperty("filepath");

                if (filePath != null && database.length() > 0) {
                    database += ";" + filePath;
                } else {
                    if (url.indexOf(S_MEM) == postUrlPos + 1
                            || url.indexOf(S_FILE) == postUrlPos + 1) {
                        database += url.substring(postUrlPos);
                    }
                }
            }
        } else {
            if (type == S_MEM) {
                database = urlImage.substring(pos, postUrlPos);
            } else if (type == S_RES) {
                database = url.substring(pos, postUrlPos);

                if (database.indexOf('/') != 0) {
                    database = '/' + database;
                }
            } else {
                database = url.substring(pos, postUrlPos);

                if (database.startsWith("~")) {
                    String userHome = "~";

                    try {
                        userHome = System.getProperty("user.home");
                    } catch (SecurityException e) {}

                    database = userHome + database.substring(1);
                }
            }

            if (database.isEmpty()) {
                return null;
            }
        }

        pos = database.indexOf("&password=");

        if (pos != -1) {
            String password = database.substring(pos + "&password=".length());

            props.setProperty("password", password);

            database = database.substring(0, pos);
        }

        pos = database.indexOf("?user=");

        if (pos != -1) {
            String user = database.substring(pos + "?user=".length());

            props.setProperty("user", user);

            database = database.substring(0, pos);
        }

        props.setProperty("database", database);

        return props;
    }
}
