DatabaseURL.java
/* 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;
    }
}