/* 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.server;

import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;

import java.net.ServerSocket;
import java.net.Socket;
import java.net.UnknownHostException;

import java.sql.DriverManager;

import java.util.Enumeration;
import java.util.Properties;
import java.util.StringTokenizer;

import org.hsqldb.Database;
import org.hsqldb.DatabaseManager;
import org.hsqldb.DatabaseURL;
import org.hsqldb.HsqlException;
import org.hsqldb.error.Error;
import org.hsqldb.error.ErrorCode;
import org.hsqldb.lib.ArrayUtil;
import org.hsqldb.lib.FileUtil;
import org.hsqldb.lib.HashSet;
import org.hsqldb.lib.IntKeyHashMap;
import org.hsqldb.lib.Iterator;
import org.hsqldb.lib.Notified;
import org.hsqldb.lib.StopWatch;
import org.hsqldb.lib.StringUtil;
import org.hsqldb.persist.HsqlDatabaseProperties;
import org.hsqldb.persist.HsqlProperties;
import org.hsqldb.resources.ResourceBundleHandler;
import org.hsqldb.result.Result;
import org.hsqldb.result.ResultConstants;
import org.hsqldb.types.HsqlDateTime.SystemTimeString;

// fredt@users 20020215 - patch 1.7.0
// methods reorganised to use new HsqlProperties class
// fredt@users 20020424 - patch 1.7.0 - shutdown without exit
// see the comments in ServerConnection.java
// unsaved@users 20021113 - patch 1.7.2 - SSL support
// campbell-burnet@users 20030510-14 - 1.7.2 - SSL support moved to factory interface
// campbell-burnet@users 20030510-14 - 1.7.2 - service control, JavaBean API
// fredt@users 20030916 - 1.7.2 - review, simplification and multiple DB's
// fredt@users 20040320 - 1.7.2 - review and correction
// fredt@users 20050225 - 1.8.0 - minor corrections
// fredt@users 20051231 - 1.8.1 - support for remote opening of databases
// fredt@users 20080531 - 1.8.1 - removed synchronized from print methods
// unnecessary and could cause deadlock

/**
 * The HyperSQL hsql: and hsqls: protocol network database server. <p>
 *
 * A Server object acts as a network database server and is one way of using
 * the client-server mode of HSQLDB Database Engine. Instances of this
 * class handle native HSQL protocol connections exclusively, allowing database
 * queries to be performed efficiently across the network.  Server's direct
 * descendant, WebServer, handles HTTP protocol connections exclusively,
 * allowing HSQL protocol to be tunnelled over HTTP to avoid sandbox and
 * firewall issues, albeit less efficiently. <p>
 *
 * There are a number of ways to configure and start a Server instance. <p>
 *
 * When started from the command line or programatically via the main(String[])
 * method, configuration occurs in three phases, with later phases overriding
 * properties set by previous phases:
 *
 * <ol>
 *   <li>Upon construction, a Server object is assigned a set of default
 *       properties.
 *
 *   <li>If it exists, properties are loaded from a file named
 *       'server.properties' in the present working directory.
 *
 *   <li>The command line arguments (alternatively, the String[] passed to
 *       main()) are parsed and used to further configure the Server's
 *       properties.
 *
 * </ol> <p>
 *
 * From the command line, the options are as follows:
 * <pre>
 * +-----------------+-------------+----------+------------------------------+
 * |    OPTION       |    TYPE     | DEFAULT  |         DESCRIPTION          |
 * +-----------------+-------------+----------+------------------------------|
 * | --help          |             |          | prints this message          |
 * | --address       | name|number | any      | server inet address          |
 * | --port          | number      | 9001/544 | port at which server listens |
 * | --database.i    | [type]spec  | 0=test   | path of database i           |
 * | --dbname.i      | alias       |          | url alias for database i     |
 * | --silent        | true|false  | true     | false =&gt; display all queries |
 * | --trace         | true|false  | false    | display JDBC trace messages  |
 * | --tls           | true|false  | false    | TLS/SSL (secure) sockets     |
 * | --no_system_exit| true|false  | false    | do not issue System.exit()   |
 * | --remote_open   | true|false  | false    | can open databases remotely  |
 * | --props         | filepath    |          | file path of properties file |
 * +-----------------+-------------+----------+------------------------------+
 * </pre>
 *
 * The <em>database.i</em> and <em>dbname.i</em> options need further
 * explanation:
 *
 * <ul>
 *   <li>Multiple databases can be served by each instance of the Server.
 *       The value of <em>i</em> is currently limited to the range 0..9,
 *       allowing up to 10 different databases. Any number is this range
 *       can be used.
 *
 *   <li>The value assigned to <em>database.i</em> is interpreted using the
 *       format <b>'[type]spec'</b>, where the optional <em>type</em> component
 *       is one of <b>'file:'</b>, <b>'res:'</b> or <b>'mem:'</b> and the
 *       <em>spec</em> component is interpreted in the context of the
 *       <em>type</em> component.  <p>
 *
 *       If omitted, the <em>type</em> component is taken to be
 *       <b>'file:'</b>.  <p>
 *
 *        A full description of how
 *       <b>'[type]spec'</b> values are interpreted appears in the overview for
 *       {@link org.hsqldb.jdbc.JDBCConnection JDBCConnection}.
 *
 *   <li>The value assigned to <em>dbname.i</em> is taken to be the key used to
 *       look up the desired database instance and thus corresponds to the
 *       <b>&lt;alias&gt;</b> component of the HSQLDB HSQL protocol database
 *       connection url:
 *       'jdbc:hsqldb:hsql[s]://host[port][/<b>&lt;alias&gt;</b>]'.
 *
 *   <li>The value of <em>database.0</em> is special. If  <em>dbname.0</em>
 *       is not specified, then this defaults to an empty string and
 *       a connection is made to <em>database.0</em> path when
 *       the <b>&lt;alias&gt;</b> component of an HSQLDB HSQL protocol database
 *       connection url is omitted. If a <em>database</em> key/value pair is
 *       found in the properties when the main method is called, this
 *       pair is supersedes the <em>database.0</em> setting<p>
 *
 *       This behaviour allows the previous
 *       database connection url format to work with essentially unchanged
 *       semantics.
 *
 *   <li>When the  <em>remote_open</em> property is true, a connection attempt
 *       to an unopened database results in the database being opened. The URL
 *       for connection should include the property filepath to specify the path.
 *       'jdbc:hsqldb:hsql[s]://host[port]/<b>&lt;alias&gt;;filepath=hsqldb:file:&lt;database path&gt;</b>'.
 *       the given alias and filepath value will be associated together. The
 *       database user and password to start this connection must be valid.
 *       If this form of connection is used again, after the database has been
 *       opened, the filepath property is ignored.
 *
 *   <li>Once an alias such as "mydb" has been associated with a path, it cannot
 *       be  reassigned to a different path.
 *
 *   <li>If a database is closed with the SHUTDOWN command, its
 *       alias is removed. It is then possible to connect to this database again
 *       with a different (or the same) alias.
 *
 *   <li>If the same database is connected to via two different
 *       aliases, and then one of the is closed with the SHUTDOWN command, the
 *       other is also closed.
 * </ul>
 *
 * From the 'server.properties' file, options can be set similarly, using a
 * slightly different format. <p>
 *
 * Here is an example 'server.properties' file:
 *
 * <pre>
 * server.port=9001
 * server.database.0=test
 * server.dbname.0=...
 * ...
 * server.database.n=...
 * server.dbname.n=...
 * server.silent=true
 * </pre>
 *
 * Starting with 1.7.2, Server has been refactored to become a simple JavaBean
 * with non-blocking start() and stop() service methods.  It is possible to
 * configure a Server instance through the JavaBean API as well, but this
 * part of the public interface is still under review and will not be finalized
 * or documented fully until the final 1.7.2 release. <p>
 *
 * <b>Note:</b> <p>
 *
 * The 'no_system_exit' property is of particular interest. <p>
 *
 * If a Server instance is to run embedded in, say, an application server,
 * such as when the JDBCDataSource or HsqlServerFactory classes are used, it
 * is typically necessary to avoid calling System.exit() when the Server
 * instance shuts down. <p>
 *
 * By default, 'no_system_exit' is set:
 *
 * <ol>
 *    <li><b>true</b> when a Server is started directly from the start()
 *        method.
 *
 *    <li><b>false</b> when a Server is started from the main(String[])
 *         method.
 * </ol>
 *
 * These values are natural to their context because the first case allows
 * the JVM to exit by default on Server shutdown when a Server instance is
 * started from a command line environment, whereas the second case prevents
 * a typically unwanted JVM exit on Server shutdown when a Server instance
 * is started as part of a larger framework.
 *
 * @author Fred Toussi (fredt@users dot sourceforge.net)
 * @version 2.7.4
 * @since 1.7.2
 */
public class Server implements HsqlSocketRequestHandler, Notified {

//
    protected static final int serverBundleHandle =
        ResourceBundleHandler.getBundleHandle(
            "org_hsqldb_server_Server_messages",
            null);

//
    ServerProperties serverProperties;

//
    HashSet<ServerConnection> serverConnSet;
    final Object              serverConnSetSync = new Object();

//  As of HSQLDB 1.9.0, the following arrays are used starting from 0.
//  The indexes do not correspond to the user-specified indexes.
    protected String[]         dbAlias;
    protected String[]         dbType;
    protected String[]         dbPath;
    protected HsqlProperties[] dbProps;
    protected int[]            dbID;
    protected long[]           dbActionSequence;

// set of aliases
    HashSet<String> aliasSet = new HashSet<String>();

//  Currently unused
    protected int maxConnections;
    protected int maxWaitForClientData = 1000;
    volatile long actionSequence;

//
    protected String                serverId;
    protected int                   serverProtocol;
    protected ThreadGroup           serverConnectionThreadGroup;
    protected HsqlSocketFactory     socketFactory;
    protected volatile ServerSocket socket;

//
    private Thread             serverThread;
    private Throwable          serverError;
    private volatile int       serverState;
    private volatile boolean   isSilent;
    protected volatile boolean isRemoteOpen;
    protected boolean          isDaemon;
    private PrintWriter        logWriter;
    private PrintWriter        errWriter;
    private ServerAcl          acl = null;    // null means no access tests
    private volatile boolean   isShuttingDown;
    private SystemTimeString   sysTime = new SystemTimeString();

    /**
     * A specialized Thread inner class in which the run() method of this
     * server executes.
     */
    private class ServerThread extends Thread {

        /**
         * Constructs a new thread in which to execute the run method
         * of this server.
         *
         * @param name The thread name
         */
        ServerThread(String name) {
            super(name);

            setName(name + '@' + Integer.toString(Server.this.hashCode(), 16));
        }

        /**
         * Executes the run() method of this server
         */
        public void run() {
            Server.this.run();
            printWithThread("ServerThread.run() exited");
        }
    }

    /**
     * Returns thread object for "HSQLDB Server" thread
     * @return thread
     */
    public Thread getServerThread() {
        return serverThread;
    }

    /**
     * Creates a new Server instance handling HSQL protocol connections.
     */
    public Server() {
        this(ServerConstants.SC_PROTOCOL_HSQL);
    }

    /**
     * Creates a new Server instance handling the specified connection
     * protocol. <p>
     *
     * For example, the no-args WebServer constructor invokes this constructor
     * with ServerConstants.SC_PROTOCOL_HTTP, while the Server() no args
     * constructor invokes this constructor with
     * ServerConstants.SC_PROTOCOL_HSQL. <p>
     *
     * @param protocol the ServerConstants code indicating which
     *      connection protocol to handle
     */
    protected Server(int protocol) {
        init(protocol);
    }

    /**
     * Checks if this Server object is or is not running and throws if the
     * current state does not match the specified value.
     *
     * @param running if true, ensure the server is running, else ensure the
     *      server is not running
     * @throws HsqlException if the supplied value does not match the
     *      current running status
     */
    public void checkRunning(boolean running) {

        int     state;
        boolean error;

        printWithThread("checkRunning(" + running + ") entered");

        state = getState();
        error = (running && state != ServerConstants.SERVER_STATE_ONLINE)
                || (!running && state != ServerConstants.SERVER_STATE_SHUTDOWN);

        if (error) {
            String msg = "server is " + (running
                                         ? "not "
                                         : "") + "running";

            throw Error.error(ErrorCode.GENERAL_ERROR, msg);
        }

        printWithThread("checkRunning(" + running + ") exited");
    }

    /**
     * Returns true if this server is not running
     *
     * @return boolean
     */
    public boolean isNotRunning() {
        int state = getState();

        return state == ServerConstants.SERVER_STATE_SHUTDOWN;
    }

    /**
     * Closes all connections to this Server.
     */
    public synchronized void signalCloseAllServerConnections() {

        ServerConnection[] array;

        printWithThread("signalCloseAllServerConnections() entered");

        synchronized (serverConnSetSync) {

            // snapshot
            array = new ServerConnection[serverConnSet.size()];

            serverConnSet.toArray(array);
        }

        for (int i = 0; i < array.length; i++) {
            ServerConnection sc = array[i];

            printWithThread("Closing " + sc);

            // also removes all but one connection from serverConnSet
            sc.signalClose();
        }

        printWithThread("signalCloseAllServerConnections() exited");
    }

    /**
     * Retrieves, in string form, this server's host address.
     *
     * @return this server's host address
     */
    public String getAddress() {
        return socket == null
               ? serverProperties.getProperty(ServerProperties.sc_key_address)
               : socket.getInetAddress().getHostAddress();
    }

    /**
     * Retrieves the url alias (network name) of the i'th database
     * that this Server hosts.
     *
     * @param index the index of the url alias upon which to report
     * @param asconfigured if true, report the configured value, else
     *      the live value
     * @return the url alias component of the i'th database
     *      that this Server hosts, or null if no such name exists.
     */
    public String getDatabaseName(int index, boolean asconfigured) {

        if (asconfigured) {
            return serverProperties.getProperty(
                ServerProperties.sc_key_dbname + "." + index);
        } else if (getState() == ServerConstants.SERVER_STATE_ONLINE) {
            return (dbAlias == null || index < 0 || index >= dbAlias.length)
                   ? null
                   : dbAlias[index];
        } else {
            return null;
        }
    }

    /**
     * Retrieves the HSQLDB path descriptor (uri) of the i'th
     * Database that this Server hosts.
     *
     * @param index the index of the uri upon which to report
     * @param asconfigured if true, report the configured value, else
     *      the live value
     * @return the HSQLDB database path descriptor of the i'th database
     *      that this Server hosts, or null if no such path descriptor
     *      exists
     */
    public String getDatabasePath(int index, boolean asconfigured) {

        if (asconfigured) {
            return serverProperties.getProperty(
                ServerProperties.sc_key_database + "." + index);
        } else if (getState() == ServerConstants.SERVER_STATE_ONLINE) {
            return (dbPath == null || index < 0 || index >= dbPath.length)
                   ? null
                   : dbPath[index];
        } else {
            return null;
        }
    }

    public String getDatabaseType(int index) {
        return (dbType == null || index < 0 || index >= dbType.length)
               ? null
               : dbType[index];
    }

    /**
     * Retrieves the name of the web page served when no page is specified.
     * This attribute is relevant only when server protocol is HTTP(S).
     *
     * @return the name of the web page served when no page is specified
     */
    public String getDefaultWebPage() {
        return "[IGNORED]";
    }

    /**
     * Retrieves a String object describing the command line and
     * properties options for this Server.
     *
     * @return the command line and properties options help for this Server
     */
    public String getHelpString() {
        return ResourceBundleHandler.getString(
            serverBundleHandle,
            "server.help");
    }

    /**
     * Retrieves the PrintWriter to which server errors are printed.
     *
     * @return the PrintWriter to which server errors are printed.
     */
    public PrintWriter getErrWriter() {
        return errWriter;
    }

    /**
     * Retrieves the PrintWriter to which server messages are printed.
     *
     * @return the PrintWriter to which server messages are printed.
     */
    public PrintWriter getLogWriter() {
        return logWriter;
    }

    /**
     * Retrieves this server's host port as configured.
     *
     * @return this server's host port as configured
     */
    public int getPort() {
        return serverProperties.getIntegerProperty(
            ServerProperties.sc_key_port,
            ServerConfiguration.getDefaultPort(serverProtocol, isTls()));
    }

    /**
     * Retrieves this server socket's actual host port.
     *
     * @return this server socket's actual host port
     */
    public int getLocalPort() {
        return socket == null
               ? -1
               : socket.getLocalPort();
    }

    /**
     * Retrieves this server socket's max wait time for connection handshake
     * in milliseconds.
     *
     * @return this server socket's max wait time
     */
    public int getMaxWait() {
        return maxWaitForClientData;
    }

    /**
     * Retrieves this server's product name.  <p>
     *
     * Typically, this will be something like: "HSQLDB xxx server".
     *
     * @return the product name of this server
     */
    public String getProductName() {
        return "HSQLDB server";
    }

    /**
     * Retrieves the server's product version, as a String.  <p>
     *
     * Typically, this will be something like: "1.x.x" or "2.x.x" and so on.
     *
     * @return the product version of the server
     */
    public String getProductVersion() {
        return HsqlDatabaseProperties.THIS_VERSION;
    }

    /**
     * Retrieves a string resresentaion of the network protocol
     * this server offers, typically one of 'HTTP', HTTPS', 'HSQL' or 'HSQLS'.
     *
     * @return string representation of this server's protocol
     */
    public String getProtocol() {
        return isTls()
               ? "HSQLS"
               : "HSQL";
    }

    /**
     * Retrieves a Throwable indicating the last server error, if any. <p>
     *
     * @return a Throwable indicating the last server error
     */
    public Throwable getServerError() {
        return serverError;
    }

    /**
     * Retrieves a String identifying this Server object.
     *
     * @return a String identifying this Server object
     */
    public String getServerId() {
        return serverId;
    }

    /**
     * Retrieves current state of this server in numerically coded form. <p>
     *
     * Typically, this will be one of:
     *
     * <ol>
     * <li>ServerProperties.SERVER_STATE_ONLINE (1)
     * <li>ServerProperties.SERVER_STATE_OPENING (4)
     * <li>ServerProperties.SERVER_STATE_CLOSING (8)
     * <li>ServerProperties.SERVER_STATE_SHUTDOWN (16)
     * </ol>
     *
     * @return this server's state code.
     */
    public int getState() {
        return serverState;
    }

    /**
     * Retrieves a character sequence describing this server's current state,
     * including the message of the last exception, if there is one, and it
     * is still in context.
     *
     * @return this server's state represented as a character sequence.
     */
    public String getStateDescriptor() {

        String state;

        switch (serverState) {

            case ServerConstants.SERVER_STATE_SHUTDOWN :
                state = "SHUTDOWN";
                break;

            case ServerConstants.SERVER_STATE_OPENING :
                state = "OPENING";
                break;

            case ServerConstants.SERVER_STATE_CLOSING :
                state = "CLOSING";
                break;

            case ServerConstants.SERVER_STATE_ONLINE :
                state = "ONLINE";
                break;

            default :
                state = "UNKNOWN";
                break;
        }

        return state;
    }

    /**
     * Retrieves the root context (directory) from which web content
     * is served.  This property is relevant only when the server
     * protocol is HTTP(S).  Although unlikely, it may be that in the future
     * other contexts, such as jar urls may be supported, so that pages can
     * be served from the contents of a jar or from the JVM class path.
     *
     * @return the root context (directory) from which web content is served
     */
    public String getWebRoot() {
        return "[IGNORED]";
    }

    /**
     * Assigns the specified socket to a new connection handler and
     * starts the handler in a new Thread.
     *
     * @param s the socket to connect
     */
    public void handleConnection(Socket s) {

        Thread   t;
        Runnable r;
        String   ctn;

        printWithThread("handleConnection(" + s + ") entered");

        if (!allowConnection(s)) {
            try {
                s.close();
            } catch (Exception e) {}

            printWithThread("allowConnection(): connection refused");
            printWithThread("handleConnection() exited");

            return;
        }

        // Maybe set up socket options, SSL
        // Session tracing/callbacks, etc.
        if (socketFactory != null) {
            socketFactory.configureSocket(s);
        }

        if (serverProtocol == ServerConstants.SC_PROTOCOL_HSQL) {
            r   = new ServerConnection(s, this);
            ctn = ((ServerConnection) r).getConnectionThreadName();
        } else {
            r   = new WebServerConnection(s, (WebServer) this);
            ctn = ((WebServerConnection) r).getConnectionThreadName();
        }

        t = new Thread(serverConnectionThreadGroup, r, ctn);

        t.start();
        printWithThread("handleConnection() exited");
    }

    /**
     * Retrieves whether this server calls System.exit() when shutdown.
     *
     * @return true if this server does not call System.exit()
     */
    public boolean isNoSystemExit() {
        return serverProperties.isPropertyTrue(
            ServerProperties.sc_key_no_system_exit);
    }

    /**
     * Retrieves whether this server restarts on shutdown.
     *
     * @return true this server restarts on shutdown
     */
    public boolean isRestartOnShutdown() {
        return serverProperties.isPropertyTrue(
            ServerProperties.sc_key_autorestart_server);
    }

    /**
     * Retrieves whether silent mode operation was requested in
     * the server properties.
     *
     * @return if true, silent mode was requested, else trace messages
     *      are to be printed
     */
    public boolean isSilent() {
        return isSilent;
    }

    /**
     * Retrieves whether the use of secure sockets was requested in the
     * server properties.
     *
     * @return if true, secure sockets are requested, else not
     */
    public boolean isTls() {
        return serverProperties.isPropertyTrue(ServerProperties.sc_key_tls);
    }

    /**
     * Retrieves whether JDBC trace messages are to go to System.out or the
     * DriverManger PrintStream/PrintWriter, if any.
     *
     * @return true if tracing is on (JDBC trace messages to system out)
     */
    public boolean isTrace() {
        return serverProperties.isPropertyTrue(ServerProperties.sc_key_trace);
    }

    /**
     * Attempts to put properties from the file
     * with the specified path. The file
     * extension '.properties' is implicit and should not
     * be included in the path specification.
     *
     * @param path the path of the desired properties file, without the
     *      '.properties' file extension
     * @throws HsqlException if this server is running
     * @return true if the indicated file was read successfully, else false
     */
    public boolean putPropertiesFromFile(String path) {
        return putPropertiesFromFile(path, ".properties");
    }

    /**
     * Attempts to put properties from the file with given extension.
     *
     * @param path the path of the desired properties file.
     * @param extension extension to add to path
     * @throws HsqlException if this server is running
     * @return true if the indicated file was read successfully, else false
     *
     */
    public boolean putPropertiesFromFile(String path, String extension) {

        if (getState() != ServerConstants.SERVER_STATE_SHUTDOWN) {
            throw Error.error(ErrorCode.GENERAL_ERROR, "server properties");
        }

        path = FileUtil.getFileUtil().canonicalOrAbsolutePath(path);

        ServerProperties p = ServerConfiguration.getPropertiesFromFile(
            ServerConstants.SC_PROTOCOL_HSQL,
            path,
            extension);

        if (p == null || p.isEmpty()) {
            return false;
        }

        printWithThread("putPropertiesFromFile(): [" + path + ".properties]");

        try {
            setProperties(p);
        } catch (Exception e) {
            throw Error.error(
                e,
                ErrorCode.GENERAL_ERROR,
                ErrorCode.M_Message_Pair,
                new String[]{ "Failed to set properties" });
        }

        return true;
    }

    /**
     * Puts properties from the supplied string argument.  The relevant
     * key value pairs are the same as those for the (web)server.properties
     * file format, except that the 'server.' prefix should not be specified.
     *
     * @param s semicolon-delimited key=value pair string,
     *      e.g. silent=false;port=8080;...
     * @throws HsqlException if this server is running
     */
    public void putPropertiesFromString(String s) {

        if (getState() != ServerConstants.SERVER_STATE_SHUTDOWN) {
            throw Error.error(ErrorCode.GENERAL_ERROR);
        }

        if (StringUtil.isEmpty(s)) {
            return;
        }

        printWithThread("putPropertiesFromString(): [" + s + "]");

        HsqlProperties p = HsqlProperties.delimitedArgPairsToProps(
            s,
            "=",
            ";",
            ServerProperties.sc_key_prefix);

        try {
            setProperties(p);
        } catch (Exception e) {
            throw Error.error(
                e,
                ErrorCode.GENERAL_ERROR,
                ErrorCode.M_Message_Pair,
                new String[]{ "Failed to set properties" });
        }
    }

    /**
     * Sets the InetAddress with which this server's ServerSocket will be
     * constructed.  A null or empty string or the special value "0.0.0.0"
     * can be used to bypass explicit selection, causing the ServerSocket
     * to be constructed without specifying an InetAddress.
     *
     * @param address A string representing the desired InetAddress as would
     *    be retrieved by InetAddres.getByName(), or a null or empty string
     *    or "0.0.0.0" to signify that the server socket should be constructed
     *    using the signature that does not specify the InetAddress.
     * @throws HsqlException if this server is running
     */
    public void setAddress(String address) {

        checkRunning(false);

        if (StringUtil.isEmpty(address)) {
            address = ServerConstants.SC_DEFAULT_ADDRESS;
        }

        printWithThread("setAddress(" + address + ")");
        serverProperties.setProperty(ServerProperties.sc_key_address, address);
    }

    /**
     * Sets the external name (url alias) of the i'th hosted database.
     *
     * @param index int
     * @param name external name (url alias) of the i'th HSQLDB database
     *   instance this server is to host.
     */
    public void setDatabaseName(int index, String name) {

        checkRunning(false);
        printWithThread("setDatabaseName(" + index + "," + name + ")");
        serverProperties.setProperty(
            ServerProperties.sc_key_dbname + "." + index,
            name);
    }

    /**
     * Sets the path of the hosted database. The path always starts with the
     * catalog type. Examples of the path include: "file:mydir/mydb",
     * "mem:mymemdb", "res:org/mydomain/mydbs/settingsdb".
     *
     * @param index int
     * @param path The path of the i'th HSQLDB database instance this server is
     *   to host.
     */
    public void setDatabasePath(int index, String path) {

        checkRunning(false);
        printWithThread("setDatabasePath(" + index + "," + path + ")");
        serverProperties.setProperty(
            ServerProperties.sc_key_database + "." + index,
            path);
    }

    /**
     * Sets the name of the web page served when no page is specified.
     *
     * @param file the name of the web page served when no page is specified
     */
    public void setDefaultWebPage(String file) {

        checkRunning(false);
        printWithThread("setDefaultWebPage(" + file + ")");

        if (serverProtocol != ServerConstants.SC_PROTOCOL_HTTP) {
            return;
        }

        serverProperties.setProperty(
            ServerProperties.sc_key_web_default_page,
            file);
    }

    /**
     * Sets the server listen port.
     *
     * @param port the port at which this server listens
     */
    public void setPort(int port) {
        checkRunning(false);
        printWithThread("setPort(" + port + ")");
        serverProperties.setProperty(ServerProperties.sc_key_port, port);
    }

    /**
     * Sets the PrintWriter to which server errors are logged. <p>
     *
     * Setting this attribute to null disables server error logging
     *
     * @param pw the PrintWriter to which server messages are logged
     */
    public void setErrWriter(PrintWriter pw) {
        errWriter = pw;
    }

    /**
     * Sets the PrintWriter to which server messages are logged. <p>
     *
     * Setting this attribute to null disables server message logging
     *
     * @param pw the PrintWriter to which server messages are logged
     */
    public void setLogWriter(PrintWriter pw) {
        logWriter = pw;
    }

    /**
     * Sets whether this server calls System.exit() when shutdown.
     *
     * @param noExit if true, System.exit() will not be called.
     */
    public void setNoSystemExit(boolean noExit) {

        printWithThread("setNoSystemExit(" + noExit + ")");
        serverProperties.setProperty(
            ServerProperties.sc_key_no_system_exit,
            noExit);
    }

    /**
     * Sets whether this server restarts on shutdown.
     *
     * @param restart if true, this server restarts on shutdown
     */
    public void setRestartOnShutdown(boolean restart) {

        printWithThread("setRestartOnShutdown(" + restart + ")");
        serverProperties.setProperty(
            ServerProperties.sc_key_autorestart_server,
            restart);
    }

    /**
     * Sets silent mode operation
     *
     * @param silent if true, then silent mode, else trace messages
     *  are to be printed
     */
    public void setSilent(boolean silent) {

        serverProperties.setProperty(ServerProperties.sc_key_silent, silent);

        isSilent = silent;

        printWithThread("setSilent(" + silent + ")");
    }

    /**
     * Sets the max wait time for connection handshake in milliseconds.
     * The default is 1000 milliseconds.
     *
     * @param maxWait an integer between 0 and 30000
     */
    public void setMaxWait(int maxWait) {

        serverProperties.setProperty(ServerProperties.sc_key_max_wait, maxWait);

        maxWaitForClientData = maxWait;

        printWithThread("setMaxWait(" + maxWait + ")");
    }

    /**
     * Sets whether to use secure sockets
     *
     * @param tls true for secure sockets, else false
     * @throws HsqlException if this server is running
     */
    public void setTls(boolean tls) {
        checkRunning(false);
        printWithThread("setTls(" + tls + ")");
        serverProperties.setProperty(ServerProperties.sc_key_tls, tls);
    }

    /**
     * Sets whether trace messages go to System.out or the
     * DriverManger PrintStream/PrintWriter, if any.
     *
     * @param trace if true, route JDBC trace messages to System.out
     */
    public void setTrace(boolean trace) {
        printWithThread("setTrace(" + trace + ")");
        serverProperties.setProperty(ServerProperties.sc_key_trace, trace);
        setLogToSystem(trace);
    }

    /**
     * No-op as deprecated.
     *
     * @param daemon ignored
     */
    public void setDaemon(boolean daemon) {
        checkRunning(false);
        printWithThread("setDaemon(" + daemon + ")");
        serverProperties.setProperty(ServerProperties.sc_key_daemon, daemon);
    }

    /**
     * Sets the path of the root directory from which web content is served.
     *
     * @param root the root (context) directory from which web content
     *      is served
     */
    public void setWebRoot(String root) {

        checkRunning(false);

        root = (new File(root)).getAbsolutePath();

        printWithThread("setWebRoot(" + root + ")");

        if (serverProtocol != ServerConstants.SC_PROTOCOL_HTTP) {
            return;
        }

        serverProperties.setProperty(ServerProperties.sc_key_web_root, root);
    }

    /**
     * Sets server properties using the specified properties object
     *
     * @param props The object containing properties to set
     * @throws ServerAcl.AclFormatException
     *          ACL list was requested but problem loading ACL.
     * @throws IOException
     *          ACL list was requested but I/O problem loading ACL.
     */
    public void setProperties(
            Properties props)
            throws IOException,
                   ServerAcl.AclFormatException {
        HsqlProperties serverProps = new HsqlProperties(props);

        setProperties(serverProps);
    }

    /**
     * Sets server properties using the specified HsqlProperties object
     *
     * @param props The HsqlProperties object containing properties to set
     * @throws ServerAcl.AclFormatException
     *          ACL list was requested but problem loading ACL.
     * @throws IOException
     *          ACL list was requested but I/O problem loading ACL.
     */
    public void setProperties(
            HsqlProperties props)
            throws IOException,
                   ServerAcl.AclFormatException {

        if (!isNotRunning()) {
            checkRunning(false);
        }

        if (props != null) {
            props.validate();

            String[] errors = props.getErrorKeys();

            if (errors.length > 0) {
                throw Error.error(ErrorCode.SERVER_NO_DATABASE, errors[0]);
            }

            serverProperties.addProperties(props);
        }

        maxConnections = serverProperties.getIntegerProperty(
            ServerProperties.sc_key_max_connections,
            16);
        maxWaitForClientData = serverProperties.getIntegerProperty(
            ServerProperties.sc_key_max_wait,
            1000);

        setLogToSystem(isTrace());

        isSilent = serverProperties.isPropertyTrue(
            ServerProperties.sc_key_silent);
        isRemoteOpen = serverProperties.isPropertyTrue(
            ServerProperties.sc_key_remote_open_db);
        isDaemon = serverProperties.isPropertyTrue(
            ServerProperties.sc_key_daemon);

        String aclFilepath = serverProperties.getProperty(
            ServerProperties.sc_key_acl);

        if (aclFilepath != null) {
            acl = new ServerAcl(new File(aclFilepath));

            if (logWriter != null && !isSilent) {
                acl.setPrintWriter(logWriter);
            }
        }
    }

    /**
     * Starts this server synchronously. <p>
     *
     * This method waits for current state to change from
     * SERVER_STATE_OPENNING. In order to discover the success or failure
     * of this operation, server state must be polled or a subclass of Server
     * must be used that overrides the setState method to provide state
     * change notification.
     *
     * @return the server state noted at entry to this method
     */
    public int start() {

        printWithThread("start() entered");

        int previousState = getState();

        if (serverThread != null) {
            printWithThread("start(): serverThread != null; no action taken");

            return previousState;
        }

        setState(ServerConstants.SERVER_STATE_OPENING);

        serverThread = new ServerThread("HSQLDB Server ");

        if (isDaemon) {
            serverThread.setDaemon(true);
        }

        serverThread.start();

        // call synchronized getState() to become owner of the Server Object's monitor
        while (getState() == ServerConstants.SERVER_STATE_OPENING) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {}
        }

        printWithThread("start() exiting");

        return previousState;
    }

    /**
     * Stops this server asynchronously. <p>
     *
     * This method returns immediately, regardless of current state.  In order
     * to discover the success or failure of this operation, server state must
     * be polled or a subclass of Server must be used that overrides the
     * setState method to provide state change notification.
     *
     * @return the server state noted at entry to this method
     */
    public int stop() {

        printWithThread("stop() entered");

        int previousState = getState();

        if (serverThread == null) {
            printWithThread("stop() serverThread is null; no action taken");

            return previousState;
        }

        releaseServerSocket();
        printWithThread("stop() exiting");

        return previousState;
    }

    /**
     * Retrieves whether the specified socket should be allowed to make a
     * connection.
     *
     * @param socket the socket to test.
     * @return boolean
     */
    protected boolean allowConnection(Socket socket) {

        if (isShuttingDown) {
            return false;
        }

        return acl == null
               || acl.permitAccess(socket.getInetAddress().getAddress());
    }

    /**
     * Initializes this server, setting the accepted connection protocol.
     *
     * @param protocol typically either SC_PROTOCOL_HTTP or SC_PROTOCOL_HSQL
     */
    protected void init(int protocol) {

        // PRE:  This method is only called from the constructor
        serverState      = ServerConstants.SERVER_STATE_SHUTDOWN;
        serverConnSet    = new HashSet<ServerConnection>();
        serverId         = toString();
        serverId         = serverId.substring(serverId.lastIndexOf('.') + 1);
        serverProtocol   = protocol;
        serverProperties = ServerConfiguration.newDefaultProperties(protocol);
        logWriter        = new PrintWriter(System.out);
        errWriter        = new PrintWriter(System.err);

        setLogToSystem(isTrace());
    }

    /**
     * Sets the server state value.
     *
     * @param state the new value
     */
    protected synchronized void setState(int state) {
        serverState = state;
    }

    /**
     * This is called from org.hsqldb.DatabaseManager when a database is
     * shutdown. This shuts the server down if it is the last database
     *
     * @param id database id
     */
    public final void notify(int id) {

        printWithThread("notify( database shutdown," + id + ") entered");
        releaseDatabase(id);

        boolean shutdown = true;

        for (int i = 0; i < dbID.length; i++) {
            if (dbAlias[i] != null) {
                shutdown = false;
                break;
            }
        }

        if (!isRemoteOpen && shutdown) {
            stop();
        }
    }

    /**
     * This releases the resources used for a database. Is called with id 0
     * multiple times for non-existent databases
     *
     * @param id int
     */
    final synchronized void releaseDatabase(int id) {

        ServerConnection[] array;

        printWithThread("releaseDatabase(" + id + ") entered");

        // check all slots as a database may be opened by multiple aliases
        for (int i = 0; i < dbID.length; i++) {
            if (dbID[i] == id && dbAlias[i] != null) {
                dbID[i]             = 0;
                dbActionSequence[i] = 0;
                dbAlias[i]          = null;
                dbPath[i]           = null;
                dbType[i]           = null;
                dbProps[i]          = null;
            }
        }

        synchronized (serverConnSetSync) {
            array = new ServerConnection[serverConnSet.size()];

            serverConnSet.toArray(array);
        }

        for (int i = 0; i < array.length; i++) {
            ServerConnection sc = array[i];

            if (sc.dbID == id) {
                sc.signalClose();
            }
        }

        printWithThread("releaseDatabase(" + id + ") exiting");
    }

    /**
     * Prints the specified message, s, formatted to identify that the print
     * operation is against this server instance.
     *
     * @param msg The message to print
     */
    protected void print(String msg) {

        PrintWriter writer = logWriter;

        if (writer != null) {
            writer.println("[" + serverId + "]: " + msg);
            writer.flush();
        }
    }

    /**
     * Prints value from server's resource bundle, formatted to
     * identify that the print operation is against this server instance.
     * Value may be localized according to the default JVM locale
     *
     * @param key the resource key
     */
    final void printResource(String key) {

        String          resource;
        StringTokenizer st;

        if (serverBundleHandle < 0) {
            return;
        }

        resource = ResourceBundleHandler.getString(serverBundleHandle, key);

        if (resource == null) {
            return;
        }

        st = new StringTokenizer(resource, "\n\r");

        while (st.hasMoreTokens()) {
            print(st.nextToken());
        }
    }

    /**
     * Prints the stack trace of the Throwable, t, to this Server object's
     * errWriter. <p>
     *
     * @param t the Throwable whose stack trace is to be printed
     */
    protected void printStackTrace(Throwable t) {

        if (errWriter != null) {
            t.printStackTrace(errWriter);
            errWriter.flush();
        }
    }

    /**
     * Prints the specified message, s, prepended with a timestamp representing
     * the current date and time, formatted to identify that the print
     * operation is against this server instance.
     *
     * @param msg the message to print
     */
    final void printWithTimestamp(String msg) {
        print(sysTime.getTimestampString() + " " + msg);
    }

    /**
     * Prints a message formatted similarly to print(String), additionally
     * identifying the current (calling) thread. Replaces old method
     * trace(String msg).
     *
     * @param msg the message to print
     */
    protected void printWithThread(String msg) {
        if (!isSilent()) {
            print("[" + Thread.currentThread() + "]: " + msg);
        }
    }

    /**
     * Prints an error message to this Server object's errWriter.
     * The message is formatted similarly to print(String),
     * additionally identifying the current (calling) thread.
     *
     * @param msg the message to print
     */
    protected void printError(String msg) {

        PrintWriter writer = errWriter;

        if (writer != null) {
            writer.print("[" + serverId + "]: ");
            writer.print("[" + Thread.currentThread() + "]: ");
            writer.println(msg);
            writer.flush();
        }
    }

    /**
     * Prints a description of the request encapsulated by the
     * Result argument, r.
     *
     * Printing occurs iff isSilent() is false. <p>
     *
     * The message is formatted similarly to print(String), additionally
     * indicating the connection identifier.  <p>
     *
     * For Server instances, cid is typically the value assigned to each
     * ServerConnection object that is unique amongst all such identifiers
     * in each distinct JVM session / class loader
     * context. <p>
     *
     * For WebServer instances, a single logical connection actually spawns
     * a new physical WebServerConnection object for each request, so the
     * cid is typically the underlying session id, since that does not
     * change for the duration of the logical connection.
     *
     * @param cid the connection identifier
     * @param r the request whose description is to be printed
     */
    final void printRequest(long cid, Result r) {

        if (isSilent()) {
            return;
        }

        StringBuilder sb = new StringBuilder();

        sb.append(cid);
        sb.append(':');

        switch (r.getType()) {

            case ResultConstants.PREPARE : {
                sb.append("SQLCLI:SQLPREPARE ");
                sb.append(r.getMainString());
                break;
            }

            case ResultConstants.EXECDIRECT : {
                sb.append(r.getMainString());
                break;
            }

            case ResultConstants.EXECUTE_INVALID :
            case ResultConstants.EXECUTE : {
                sb.append("SQLCLI:SQLEXECUTE:");
                sb.append(r.getStatementID());
                break;
            }

            case ResultConstants.BATCHEXECUTE :
                sb.append("SQLCLI:SQLEXECUTE:");
                sb.append("BATCHMODE:");
                sb.append(r.getStatementID());
                break;

            case ResultConstants.UPDATE_RESULT : {
                sb.append("SQLCLI:RESULTUPDATE:");
                sb.append(r.getStatementID());
                break;
            }

            case ResultConstants.FREESTMT : {
                sb.append("SQLCLI:SQLFREESTMT:");
                sb.append(r.getStatementID());
                break;
            }

            case ResultConstants.GETSESSIONATTR : {
                sb.append("HSQLCLI:GETSESSIONATTR");
                break;
            }

            case ResultConstants.SETSESSIONATTR : {
                sb.append("HSQLCLI:SETSESSIONATTR:");
                break;
            }

            case ResultConstants.ENDTRAN : {
                sb.append("SQLCLI:SQLENDTRAN:");

                switch (r.getActionType()) {

                    case ResultConstants.TX_COMMIT :
                        sb.append("COMMIT");
                        break;

                    case ResultConstants.TX_ROLLBACK :
                        sb.append("ROLLBACK");
                        break;

                    case ResultConstants.TX_SAVEPOINT_NAME_RELEASE :
                        sb.append("SAVEPOINT_NAME_RELEASE ");
                        sb.append(r.getMainString());
                        break;

                    case ResultConstants.TX_SAVEPOINT_NAME_ROLLBACK :
                        sb.append("SAVEPOINT_NAME_ROLLBACK ");
                        sb.append(r.getMainString());
                        break;

                    default :
                        sb.append(r.getActionType());
                }

                break;
            }

            case ResultConstants.STARTTRAN : {
                sb.append("SQLCLI:SQLSTARTTRAN");
                break;
            }

            case ResultConstants.DISCONNECT : {
                sb.append("SQLCLI:SQLDISCONNECT");
                break;
            }

            case ResultConstants.SETCONNECTATTR : {
                sb.append("SQLCLI:SQLSETCONNECTATTR:");

                switch (r.getConnectionAttrType()) {

                    case ResultConstants.SQL_ATTR_SAVEPOINT_NAME : {
                        sb.append("SQL_ATTR_SAVEPOINT_NAME ");
                        sb.append(r.getMainString());
                        break;
                    }

                    default : {
                        sb.append(r.getConnectionAttrType());
                    }
                }

                break;
            }

            case ResultConstants.CLOSE_RESULT : {
                sb.append("HQLCLI:CLOSE_RESULT:RESULT_ID ");
                sb.append(r.getResultId());
                break;
            }

            case ResultConstants.REQUESTDATA : {
                sb.append("HQLCLI:REQUESTDATA:RESULT_ID ");
                sb.append(r.getResultId());
                sb.append(" ROWOFFSET ");
                sb.append(r.getUpdateCount());
                sb.append(" ROWCOUNT ");
                sb.append(r.getFetchSize());
                break;
            }

            default : {
                sb.append("SQLCLI:MODE:");
                sb.append(r.getType());
                break;
            }
        }

        print(sb.toString());
    }

    /**
     * return database ID
     *
     * @param aliasPath String
     * @return int
     */
    synchronized final int getDBIndex(String aliasPath) {

        int    semipos  = aliasPath.indexOf(';');
        String alias    = aliasPath;
        String filepath = null;

        if (semipos != -1) {
            alias    = aliasPath.substring(0, semipos);
            filepath = aliasPath.substring(semipos + 1);
        }

        int dbIndex = ArrayUtil.find(dbAlias, alias);

        if (dbIndex == -1) {
            if (filepath == null) {
                HsqlException e = Error.error(
                    ErrorCode.GENERAL_ERROR,
                    "database alias does not exist");

                printError("database alias=" + alias + " does not exist");
                setServerError(e);

                throw e;
            } else {
                return openDatabase(alias, filepath);
            }
        } else {
            return dbIndex;
        }
    }

    /**
     * Open and return database index
     *
     * @param alias String
     * @param datapath String
     * @return int
     */
    final int openDatabase(String alias, String datapath) {

        if (!isRemoteOpen) {
            HsqlException e = Error.error(
                ErrorCode.GENERAL_ERROR,
                "remote open not allowed");

            printError("Remote database open not allowed");
            setServerError(e);

            throw e;
        }

        int i = getFirstEmptyDatabaseIndex();

        if (i < -1) {
            i = closeOldestDatabase();

            if (i < -1) {
                HsqlException e = Error.error(
                    ErrorCode.GENERAL_ERROR,
                    "limit of open databases reached");

                printError("limit of open databases reached");
                setServerError(e);

                throw e;
            }
        }

        HsqlProperties newprops = DatabaseURL.parseURL(datapath, false, false);

        if (newprops == null) {
            HsqlException e = Error.error(
                ErrorCode.GENERAL_ERROR,
                "invalid database path");

            printError("invalid database path");
            setServerError(e);

            throw e;
        }

        String path = newprops.getProperty(DatabaseURL.url_database);
        String type = newprops.getProperty(DatabaseURL.url_connection_type);

        try {
            int dbid = DatabaseManager.getDatabase(type, path, this, newprops);

            dbID[i]             = dbid;
            dbActionSequence[i] = actionSequence;
            dbAlias[i]          = alias;
            dbPath[i]           = path;
            dbType[i]           = type;
            dbProps[i]          = newprops;

            return i;
        } catch (HsqlException e) {
            printError(
                "Database [index=" + i + ", db=" + dbType[i] + dbPath[i]
                + ", alias=" + dbAlias[i] + "] did not open: " + e.toString());
            setServerError(e);

            throw e;
        }
    }

    final int getFirstEmptyDatabaseIndex() {

        for (int i = 0; i < dbAlias.length; i++) {
            if (dbAlias[i] == null) {
                return i;
            }
        }

        return -1;
    }

    /**
     * Opens this server's database instances. This method returns true If at
     * least one database goes online, otherwise it returns false. If opening
     * any of the databases is attempted and an exception is thrown, the server
     * error is set to this exception.
     *
     * @return boolean
     */
    final boolean openDatabases() {

        printWithThread("openDatabases() entered");

        boolean success = false;

        setDBInfoArrays();

        for (int i = 0; i < dbAlias.length; i++) {
            if (dbAlias[i] == null) {
                continue;
            }

            printWithThread(
                "Opening database: [" + dbType[i] + dbPath[i] + "]");

            StopWatch sw = new StopWatch();
            int       id;

            try {
                id = DatabaseManager.getDatabase(
                    dbType[i],
                    dbPath[i],
                    this,
                    dbProps[i]);
                dbID[i] = id;
                success = true;
            } catch (HsqlException e) {
                printError(
                    "Database [index=" + i + ", db=" + dbType[i] + dbPath[i]
                    + ", alias=" + dbAlias[i] + "] did not open: "
                    + e.toString());
                setServerError(e);

                dbAlias[i] = null;
                dbPath[i]  = null;
                dbType[i]  = null;
                dbProps[i] = null;
                continue;
            }

            sw.stop();

            String msg = "Database [index=" + i + ", id=" + id + ", db="
                         + dbType[i] + dbPath[i] + ", alias=" + dbAlias[i]
                         + "] opened successfully";

            print(sw.elapsedTimeToMessage(msg));
        }

        printWithThread("openDatabases() exiting");

        if (isRemoteOpen) {
            success = true;
        }

        if (!success && getServerError() == null) {

            // database alias / path list is empty or without full info for any DB
            setServerError(Error.error(ErrorCode.SERVER_NO_DATABASE));
        }

        return success;
    }

    /**
     * Initialises the database attributes lists from the server properties object.
     */
    private void setDBInfoArrays() {

        IntKeyHashMap<String> dbNumberMap  = getDBNameArray();
        int                   maxDatabases = dbNumberMap.size();

        if (serverProperties.isPropertyTrue(
                ServerProperties.sc_key_remote_open_db)) {
            int max = serverProperties.getIntegerProperty(
                ServerProperties.sc_key_max_databases,
                ServerConstants.SC_DEFAULT_MAX_DATABASES);

            if (maxDatabases < max) {
                maxDatabases = max;
            }
        }

        dbAlias          = new String[maxDatabases];
        dbPath           = new String[dbAlias.length];
        dbType           = new String[dbAlias.length];
        dbID             = new int[dbAlias.length];
        dbActionSequence = new long[dbAlias.length];
        dbProps          = new HsqlProperties[dbAlias.length];

        Iterator<Integer> it = dbNumberMap.keySet().iterator();

        for (int i = 0; it.hasNext(); ) {
            int    dbNumber = it.nextInt();
            String path     = getDatabasePath(dbNumber, true);

            if (path == null) {
                printWithThread(
                    "missing database path: " + dbNumberMap.get(dbNumber));
                continue;
            }

            HsqlProperties dbURL = DatabaseURL.parseURL(path, false, false);

            if (dbURL == null) {
                printWithThread("malformed database path: " + path);
                continue;
            }

            dbAlias[i] = dbNumberMap.get(dbNumber);
            dbPath[i]  = dbURL.getProperty("database");
            dbType[i]  = dbURL.getProperty("connection_type");
            dbProps[i] = dbURL;

            i++;
        }
    }

    /**
     * Returns a map of n values from server.dbname.n values to database names
     * from the properties object.
     *
     * @return IntKeyHashMap
     */
    private IntKeyHashMap<String> getDBNameArray() {

        final String          prefix = ServerProperties.sc_key_dbname + ".";
        final int             prefixLen    = prefix.length();
        IntKeyHashMap<String> idToAliasMap = new IntKeyHashMap<>();
        Enumeration<String>   en           = serverProperties.propertyNames();

        while (en.hasMoreElements()) {
            String key = en.nextElement();

            if (!key.startsWith(prefix)) {
                continue;
            }

            int dbNumber;

            try {
                dbNumber = Integer.parseInt(key.substring(prefixLen));
            } catch (NumberFormatException e1) {
                printWithThread("malformed database enumerator: " + key);
                continue;
            }

            String alias = serverProperties.getProperty(key).toLowerCase();

            if (!aliasSet.add(alias)) {
                printWithThread("duplicate alias: " + alias);
            }

            String existing = idToAliasMap.put(dbNumber, alias);

            if (existing != null) {
                printWithThread("duplicate database enumerator: " + key);
            }
        }

        return idToAliasMap;
    }

    /**
     * Constructs and installs a new ServerSocket instance for this server.
     *
     * @throws Exception if it is not possible to construct and install
     *      a new ServerSocket
     */
    private void openServerSocket() throws Exception {

        String    address;
        int       port;
        String[]  candidateAddrs;
        String    emsg;
        StopWatch sw;

        printWithThread("openServerSocket() entered");

        if (isTls()) {
            printWithThread("Requesting TLS/SSL-encrypted JDBC");
        }

        sw            = new StopWatch();
        socketFactory = HsqlSocketFactory.getInstance(isTls());
        address       = getAddress();
        port          = getPort();

        if (StringUtil.isEmpty(address)
                || ServerConstants.SC_DEFAULT_ADDRESS.equalsIgnoreCase(
                    address.trim())) {
            socket = socketFactory.createServerSocket(port);
        } else {
            try {
                socket = socketFactory.createServerSocket(port, address);
            } catch (UnknownHostException e) {
                candidateAddrs =
                    ServerConfiguration.listLocalInetAddressNames();

                int      messageID;
                String[] messageParameters;

                if (candidateAddrs.length > 0) {
                    messageID = ErrorCode.M_SERVER_OPEN_SERVER_SOCKET_1;

                    StringBuilder sb = new StringBuilder();

                    for (int i = 0; i < candidateAddrs.length; i++) {
                        if (sb.length() > 0) {
                            sb.append(", ");
                        }

                        sb.append(candidateAddrs[i]);
                    }

                    messageParameters = new String[]{ address, sb.toString() };
                } else {
                    messageID         = ErrorCode.M_SERVER_OPEN_SERVER_SOCKET_2;
                    messageParameters = new String[]{ address };
                }

                throw new UnknownHostException(
                    Error.getMessage(messageID, 0, messageParameters));
            }
        }

        /*
         * Following line necessary for Java 1.3 on UNIX.  See accept()
         * comment elsewhere in this file.
         */
        socket.setSoTimeout(1000);
        printWithThread("Got server socket: " + socket);
        print(sw.elapsedTimeToMessage("Server socket opened successfully"));

        if (socketFactory.isSecure()) {
            print("Using TLS/SSL-encrypted JDBC");
        }

        printWithThread("openServerSocket() exiting");
    }

    /** Prints a timestamped message indicating that this server is online */
    private void printServerOnlineMessage() {

        String s = getProductName() + " " + getProductVersion()
                   + " is online on port " + this.getPort();

        printWithTimestamp(s);
        printResource("online.help");
    }

    /**
     * Prints a description of the server properties iff !isSilent().
     */
    protected void printProperties() {

        Enumeration<String> e;
        String              key;
        String              value;

        // Avoid the waste of generating each description,
        // only for trace() to silently discard it
        if (isSilent()) {
            return;
        }

        e = serverProperties.propertyNames();

        while (e.hasMoreElements()) {
            key   = e.nextElement();
            value = serverProperties.getProperty(key);

            printWithThread(key + "=" + value);
        }
    }

    /**
     * Puts this server into the SERVER_CLOSING state, closes the ServerSocket
     * and nullifies the reference to it. If the ServerSocket is already null,
     * this method exists immediately, otherwise, the result is to fully
     * shut down the server.
     */
    private synchronized void releaseServerSocket() {

        printWithThread("releaseServerSocket() entered");

        if (socket != null) {
            printWithThread("Releasing server socket: [" + socket + "]");
            setState(ServerConstants.SERVER_STATE_CLOSING);

            try {
                socket.close();
            } catch (IOException e) {
                printError("Exception closing server socket");
                printError("releaseServerSocket(): " + e);
            }

            socket = null;
        }

        printWithThread("releaseServerSocket() exited");
    }

    /**
     * Attempts to bring this server fully online by opening
     * a new ServerSocket, obtaining the hosted databases,
     * notifying the status waiter thread (if any) and
     * finally entering the listen loop if all else succeeds.
     * If any part of the process fails, then this server enters
     * its shutdown sequence.
     */
    private void run() {

        StopWatch sw;
        String    tgName;

        printWithThread("run() entered");
        print("Initiating startup sequence...");
        printProperties();

        sw = new StopWatch();

        setServerError(null);

        try {

            // Faster init first:
            // It is huge waste to fully open the databases, only
            // to find that the socket address is already in use
            openServerSocket();
        } catch (Exception e) {
            setServerError(e);
            printError("run()/openServerSocket(): ");
            printStackTrace(e);
            shutdown(true);

            return;
        }

        tgName = "HSQLDB Connections @" + Integer.toString(this.hashCode(), 16);
        serverConnectionThreadGroup = new ThreadGroup(tgName);

        // Mount the databases this server is supposed to host.
        // This may take some time if the databases are not all
        // already open.
        if (!openDatabases()) {
            setServerError(null);
            printError("Shutting down because there are no open databases");
            shutdown(true);

            return;
        }

        // At this point, we have a valid server socket and
        // a valid hosted database set, so it's OK to start
        // listening for connections.
        setState(ServerConstants.SERVER_STATE_ONLINE);
        print(sw.elapsedTimeToMessage("Startup sequence completed"));
        printServerOnlineMessage();

        // isShuttingDown is only read after socket connections are 'accept'ed.
        isShuttingDown = false;    // In case shutdown was aborted previously.

        try {

            /*
             * This loop is necessary for UNIX w/ Sun Java 1.3 because
             * in that case the socket.close() elsewhere will not
             * interrupt this accept().
             */
            while (socket != null) {
                try {
                    handleConnection(socket.accept());
                } catch (java.io.InterruptedIOException iioe) {}
            }
        } catch (IOException ioe) {
            if (getState() == ServerConstants.SERVER_STATE_ONLINE) {
                setServerError(ioe);
                printError(this + ".run()/handleConnection(): ");
                printStackTrace(ioe);
            }
        } catch (Throwable t) {
            printWithThread(t.toString());
        } finally {
            shutdown(false);    // or maybe getServerError() != null?
        }
    }

    /**
     * Sets this Server's last encountered error state.
     *
     * @param t The new value for the server error
     */
    protected void setServerError(Throwable t) {
        serverError = t;
    }

    /**
     * Shuts down all the database served by this server. As a consequence,
     * this server and any other server that is serving a subset of the
     * databases will be shutdown, unless the server was started with
     * server.remote_open property.
     *
     * The shutdownMode must be one of:
     *
     * <ul>
     * <li>org.hsqldb.Database.CLOSEMODE_IMMEDIATELY = 1
     * <li>org.hsqldb.Database.CLOSEMODE_NORMAL = 2
     * <li>org.hsqldb.Database.CLOSEMODE_COMPACT = 3
     * <li>org.hsqldb.Database.CLOSEMODE_SCRIPT = 4
     * </ul>
     * @param shutdownMode a value between 1-4, usually 2 for NORMAL.
     */
    public void shutdownCatalogs(int shutdownMode) {

        if (shutdownMode < Database.CLOSEMODE_IMMEDIATELY
                || shutdownMode > Database.CLOSEMODE_SCRIPT) {
            throw new IllegalArgumentException("must be between 1 and 4");
        }

        DatabaseManager.shutdownDatabases(this, shutdownMode);
    }

    /**
     * Shuts down this server and all the database served by this server. As a
     * consequence, any other server that is serving a subset of the databases
     * will be shutdown, unless the server was started with server.remote_open
     * property.
     *
     * The shutdownMode must be one of:
     *
     * <ul>
     * <li>org.hsqldb.Database.CLOSEMODE_IMMEDIATELY = 1
     * <li>org.hsqldb.Database.CLOSEMODE_NORMAL = 2
     * <li>org.hsqldb.Database.CLOSEMODE_COMPACT = 3
     * <li>org.hsqldb.Database.CLOSEMODE_SCRIPT = 4
     * </ul>
     * @param shutdownMode a value between 1-4, usually 2 for NORMAL.
     */
    public void shutdownWithCatalogs(int shutdownMode) {

        if (shutdownMode < Database.CLOSEMODE_IMMEDIATELY
                || shutdownMode > Database.CLOSEMODE_SCRIPT) {
            throw new IllegalArgumentException("must be between 1 and 4");
        }

        // If an unchecked exception is thrown, isShuttingDown will be left true,
        // which is good from a security standpoint.
        isShuttingDown = true;

        // make handleConnection() reject new connection attempts
        DatabaseManager.shutdownDatabases(this, shutdownMode);
        shutdown(false);

        isShuttingDown = false;
    }

    /**
     * External method to shut down this server.
     */
    public void shutdown() {
        shutdown(false);
    }

    /**
     * Shuts down this server.
     *
     * @param error true if shutdown is in response to an error
     *      state, else false
     */
    protected synchronized void shutdown(boolean error) {

        if (serverState == ServerConstants.SERVER_STATE_SHUTDOWN) {
            return;
        }

        printWithThread("shutdown() entered");
        print("Initiating shutdown sequence...");

        StopWatch sw = new StopWatch();

        releaseServerSocket();
        DatabaseManager.deRegisterServer(this);

        if (dbPath != null) {
            for (int i = 0; i < dbPath.length; i++) {
                releaseDatabase(dbID[i]);
            }
        }

        // Be nice and let applications exit if there are no
        // running connection threads - wait at most 100 ms per active thread
        if (serverConnectionThreadGroup != null) {
            int count = serverConnectionThreadGroup.activeCount();

            for (; count > 0
                    && serverConnectionThreadGroup.activeCount() > 0; count--) {
                try {
                    Thread.sleep(100);
                } catch (Exception e) {

                    // e.getMessage();
                }
            }

            serverConnectionThreadGroup = null;
        }

        serverThread = null;

        setState(ServerConstants.SERVER_STATE_SHUTDOWN);
        print(sw.elapsedTimeToMessage("Shutdown sequence completed"));

        if (isNoSystemExit()) {
            printWithTimestamp("SHUTDOWN : System.exit() was not called");
            printWithThread("shutdown() exited");
        } else {
            printWithTimestamp("SHUTDOWN : System.exit() is called next");
            printWithThread("shutdown() exiting...");

            try {
                System.exit(0);
            } catch (Throwable t) {
                printWithThread(t.toString());
            }
        }
    }

    /**
     * Used by Connection object
     *
     * @param dbIndex int
     */
    synchronized void setActionSequence(int dbIndex) {
        dbActionSequence[dbIndex] = actionSequence++;
    }

    /**
     * Feature is turned off by, pending a property to allow it.
     *
     * @return int
     */
    protected int closeOldestDatabase() {

        return -1;

/*
        int      index = -1;
        Database db;

        synchronized (this) {
            long min = Long.MAX_VALUE;

            for (int i = 0; i < dbActionSequence.length; i++) {
                if (min > dbActionSequence[i]) {
                    min   = dbActionSequence[i];
                    index = i;
                }
            }

            if (index == -1) {
                return -1;
            }

            db = DatabaseManager.lookupDatabaseObject(dbType[index],
                    dbPath[index]);

            if (db == null) {
                return -1;
            }
        }

        db.close(Database.CLOSEMODE_IMMEDIATELY);

        return index;
 */
    }

    /**
     * Prints message for the specified key, without any special
     * formatting. The message content comes from the server
     * resource bundle and thus may  be localized according to the default
     * JVM locale.<p>
     *
     * Uses System.out directly instead of Trace.printSystemOut() so it
     * always prints, regardless of Trace settings.
     *
     * @param key for message
     */
    protected static void printHelp(String key) {
        System.out.println(
            ResourceBundleHandler.getString(serverBundleHandle, key));
    }

    /**
     * Creates and starts a new Server.<p>
     *
     * Allows starting a Server via the command line interface.<p>
     *
     * @param args the command line arguments for the Server instance
     */
    public static void main(String[] args) {

        HsqlProperties argProps;

        argProps = HsqlProperties.argArrayToProps(
            args,
            ServerProperties.sc_key_prefix);

        String[] errors = argProps.getErrorKeys();

        if (errors.length != 0) {
            System.out.println("no value for argument:" + errors[0]);
            printHelp("server.help");

            return;
        }

        String propsPath = argProps.getProperty(ServerProperties.sc_key_props);
        String propsExtension = "";

        if (propsPath == null) {
            propsPath      = "server";
            propsExtension = ".properties";
        } else {
            argProps.removeProperty(ServerProperties.sc_key_props);
        }

        propsPath = FileUtil.getFileUtil().canonicalOrAbsolutePath(propsPath);

        ServerProperties fileProps = ServerConfiguration.getPropertiesFromFile(
            ServerConstants.SC_PROTOCOL_HSQL,
            propsPath,
            propsExtension);
        ServerProperties props = fileProps == null
                                 ? new ServerProperties(
                                     ServerConstants.SC_PROTOCOL_HSQL)
                                 : fileProps;

        props.addProperties(argProps);
        ServerConfiguration.translateDefaultDatabaseProperty(props);

        // Standard behaviour when started from the command line
        // is to halt the VM when the server shuts down.  This may, of
        // course, be overridden by whatever, if any, security policy
        // is in place.
        ServerConfiguration.translateDefaultNoSystemExitProperty(props);
        ServerConfiguration.translateAddressProperty(props);

        // finished setting up properties;
        Server server = new Server();

        try {
            server.setProperties(props);
        } catch (Exception e) {
            server.printError("Failed to set properties");
            server.printStackTrace(e);

            return;
        }

        // now messages go to the channel specified in properties
        server.print("Startup sequence initiated from main() method");

        if (fileProps != null) {
            server.print(
                "Loaded properties from [" + propsPath + propsExtension + "]");
        } else {
            server.print("Could not load properties from file");
            server.print("Using cli/default properties only");
        }

        server.start();
    }

    private static void setLogToSystem(boolean value) {

        try {
            PrintWriter newPrintWriter = (value)
                                         ? new PrintWriter(System.out)
                                         : null;

            DriverManager.setLogWriter(newPrintWriter);
        } catch (Exception e) {}
    }
}
