ClientConnection.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.io.BufferedInputStream;
import java.io.DataInputStream;
import java.io.IOException;

import java.net.Socket;

import java.text.SimpleDateFormat;

import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.TimeZone;

import org.hsqldb.error.Error;
import org.hsqldb.error.ErrorCode;
import org.hsqldb.jdbc.JDBCConnection;
import org.hsqldb.lib.DataOutputStream;
import org.hsqldb.map.ValuePool;
import org.hsqldb.navigator.RowSetNavigatorClient;
import org.hsqldb.persist.HsqlProperties;
import org.hsqldb.result.Result;
import org.hsqldb.result.ResultConstants;
import org.hsqldb.result.ResultLob;
import org.hsqldb.rowio.RowInputBinary;
import org.hsqldb.rowio.RowOutputBinary;
import org.hsqldb.rowio.RowOutputInterface;
import org.hsqldb.server.HsqlSocketFactory;
import org.hsqldb.types.BlobDataID;
import org.hsqldb.types.ClobDataID;
import org.hsqldb.types.HsqlDateTime;

/**
 * Base remote session proxy implementation. Uses instances of Result to
 * transmit and receive data. This implementation utilises the updated HSQL
 * protocol.
 *
 * @author Fred Toussi (fredt@users dot sourceforge.net)
 * @version 2.7.3
 * @since 1.7.2
 */
public class ClientConnection implements SessionInterface, Cloneable {

    /**
     * Specifies the Compatibility version required for both Servers and
     * network JDBC Clients built with this baseline.  Must remain public
     * for Server to have visibility to it.
     *
     * Update this value only when the current version of HSQLDB does not
     * have inter-compatibility with Server and network JDBC Driver of
     * the previous HSQLDB version.
     *
     * Must specify all 4 version segments (any segment may be the value 0,
     * however). The string elements at (position p from right counted from 0)
     * are multiplied by 100 to power p and added up, then negated, to form the
     * integer representation of version string.
     */
    public static final String NETWORK_COMPATIBILITY_VERSION     = "2.3.4.0";
    public static final int    NETWORK_COMPATIBILITY_VERSION_INT = -2030400;

    //
    static final int             BUFFER_SIZE = 0x1000;
    final byte[]                 mainBuffer  = new byte[BUFFER_SIZE];
    private boolean              isClosed;
    private Socket               socket;
    protected DataOutputStream   dataOutput;
    protected DataInputStream    dataInput;
    protected RowOutputInterface rowOut;
    protected RowInputBinary     rowIn;
    private Result               resultOut;
    private long                 sessionID;
    private long                 lobIDSequence = -1;
    protected int                randomID;

    //
    private boolean  isReadOnlyDefault = false;
    private boolean  isAutoCommit      = true;
    private TimeZone timeZone;
    private Scanner  scanner;
    private Calendar calendar;
    private Calendar calendarGMT;
    SimpleDateFormat simpleDateFormatGMT;

    //
    JDBCConnection connection;
    String         host;
    int            port;
    String         path;
    String         database;
    boolean        isTLS;
    boolean        isTLSWrapper;
    int            databaseID;
    String         clientPropertiesString;
    HsqlProperties clientProperties;
    String         databaseUniqueName;

    /**
     * Establishes a connection to the server.
     */
    public ClientConnection(
            String host,
            int port,
            String path,
            String database,
            boolean isTLS,
            boolean isTLSWrapper,
            String user,
            String password,
            TimeZone timeZone) {

        this.host         = host;
        this.port         = port;
        this.path         = path;
        this.database     = database;
        this.isTLS        = isTLS;
        this.isTLSWrapper = isTLSWrapper;
        this.timeZone     = timeZone;

        initStructures();
        initConnection(host, port, isTLS);

        Result login = Result.newConnectionAttemptRequest(
            user,
            password,
            database,
            timeZone.getID(),
            timeZone.getOffset(System.currentTimeMillis()) / 1000);
        Result resultIn = execute(login);

        if (resultIn.isError()) {
            throw Error.error(resultIn);
        }

        sessionID              = resultIn.getSessionId();
        databaseID             = resultIn.getDatabaseId();
        databaseUniqueName     = resultIn.getDatabaseName();
        clientPropertiesString = resultIn.getMainString();
        randomID               = resultIn.getSessionRandomID();
    }

    protected ClientConnection(ClientConnection other) {

        this.host         = other.host;
        this.port         = other.port;
        this.path         = other.path;
        this.database     = other.database;
        this.isTLS        = other.isTLS;
        this.isTLSWrapper = other.isTLSWrapper;
        this.timeZone     = other.timeZone;

        //
        this.sessionID              = other.sessionID;
        this.databaseID             = other.databaseID;
        this.databaseUniqueName     = other.databaseUniqueName;
        this.clientPropertiesString = other.clientPropertiesString;
        this.randomID               = other.randomID;

        initStructures();
        initConnection(host, port, isTLS);
    }

    /**
     * resultOut is reused to transmit all remote calls for session management.
     * Here the structure is preset for sending attributes.
     */
    private void initStructures() {

        RowOutputBinary rowOutTemp = new RowOutputBinary(mainBuffer);

        rowOut    = rowOutTemp;
        rowIn     = new RowInputBinary(rowOutTemp);
        resultOut = Result.newSessionAttributesResult();
    }

    protected void initConnection(String host, int port, boolean isTLS) {
        openConnection(host, port, isTLS);
    }

    protected void openConnection(String host, int port, boolean isTLS) {

        try {
            if (isTLSWrapper) {
                socket = HsqlSocketFactory.getInstance(false)
                                          .createSocket(host, port);
            }

            socket = HsqlSocketFactory.getInstance(isTLS)
                                      .createSocket(socket, host, port);

            socket.setTcpNoDelay(true);

            dataOutput = new DataOutputStream(socket.getOutputStream());
            dataInput = new DataInputStream(
                new BufferedInputStream(socket.getInputStream()));

            handshake();
        } catch (Exception e) {

            // The details from "e" should not be thrown away here.  This is
            // very useful info for end users to diagnose the runtime problem.
            throw new HsqlException(
                e,
                Error.getStateString(ErrorCode.X_08001),
                -ErrorCode.X_08001);
        }
    }

    protected void closeConnection() {

        try {
            if (socket != null) {
                socket.close();
            }
        } catch (Exception e) {}

        socket = null;
    }

    public synchronized Result execute(Result r) {

        if (isClosed) {
            return Result.newErrorResult(Error.error(ErrorCode.X_08503));
        }

        try {
            r.setSessionId(sessionID);
            r.setDatabaseId(databaseID);
            write(r);

            return read();
        } catch (Throwable e) {
            throw Error.error(e, ErrorCode.X_08006, e.toString());
        }
    }

    public synchronized RowSetNavigatorClient getRows(
            long navigatorId,
            int offset,
            int size) {

        try {
            resultOut.setResultType(ResultConstants.REQUESTDATA);
            resultOut.setResultId(navigatorId);
            resultOut.setUpdateCount(offset);
            resultOut.setFetchSize(size);

            Result result = execute(resultOut);

            return (RowSetNavigatorClient) result.getNavigator();
        } catch (Throwable e) {
            throw Error.error(e, ErrorCode.X_08006, e.toString());
        }
    }

    public synchronized void closeNavigator(long navigatorId) {

        try {
            resultOut.setResultType(ResultConstants.CLOSE_RESULT);
            resultOut.setResultId(navigatorId);
            execute(resultOut);
        } catch (Throwable e) {}
    }

    public synchronized void close() {

        if (isClosed) {
            return;
        }

        try {
            resultOut.setResultType(ResultConstants.DISCONNECT);
            execute(resultOut);
        } catch (Exception e) {}

        try {
            closeConnection();
        } catch (Exception e) {}

        isClosed = true;
    }

    public void setAttributeFromResult(Result result) {

        Object[] data = result.getSingleRowData();
        int      id   = (Integer) data[AttributePos.INFO_ID];

        switch (id) {

            case Attributes.INFO_AUTOCOMMIT :
                isAutoCommit = (Boolean) data[AttributePos.INFO_BOOLEAN];
                break;

            case Attributes.INFO_TIMEZONE :
                String zoneID = (String) data[AttributePos.INFO_VARCHAR];

                timeZone = TimeZone.getTimeZone(zoneID);
                break;
        }
    }

    public synchronized Object getAttribute(int id) {

        resultOut.setResultType(ResultConstants.GETSESSIONATTR);
        resultOut.setStatementType(id);

        Result in = execute(resultOut);

        if (in.isError()) {
            throw Error.error(in);
        }

        return getAttributeFromData(in, id);
    }

    public static Object getAttributeFromData(Result result, int id) {

        Object[] data = result.getSingleRowData();

        switch (id) {

            case Attributes.INFO_AUTOCOMMIT :
            case Attributes.INFO_CONNECTION_READONLY :
                return data[AttributePos.INFO_BOOLEAN];

            case Attributes.INFO_ISOLATION :
                return data[AttributePos.INFO_INTEGER];

            case Attributes.INFO_CATALOG :
                return data[AttributePos.INFO_VARCHAR];

            case Attributes.INFO_TIMEZONE :
                return data[AttributePos.INFO_VARCHAR];
        }

        return null;
    }

    public synchronized void setAttribute(int id, Object value) {

        setAttributeResult(resultOut, id, value);

        Result resultIn = execute(resultOut);

        if (resultIn.isError()) {
            throw Error.error(resultIn);
        }
    }

    public static Result setAttributeResult(
            Result result,
            int id,
            Object value) {

        if (result == null) {
            result = Result.newSessionAttributesResult();
        }

        result.setResultType(ResultConstants.SETSESSIONATTR);

        Object[] data = result.getSingleRowData();

        data[AttributePos.INFO_ID] = ValuePool.getInt(id);

        switch (id) {

            case Attributes.INFO_AUTOCOMMIT :
            case Attributes.INFO_CONNECTION_READONLY :
                data[AttributePos.INFO_BOOLEAN] = value;
                break;

            case Attributes.INFO_ISOLATION :
                data[AttributePos.INFO_INTEGER] = value;
                break;

            case Attributes.INFO_CATALOG :
            case Attributes.INFO_TIMEZONE :
                data[AttributePos.INFO_VARCHAR] = value;
                break;

            default :
        }

        return result;
    }

    public synchronized boolean isReadOnlyDefault() {

        Object info = getAttribute(Attributes.INFO_CONNECTION_READONLY);

        isReadOnlyDefault = ((Boolean) info).booleanValue();

        return isReadOnlyDefault;
    }

    public synchronized void setReadOnlyDefault(boolean mode) {

        if (mode != isReadOnlyDefault) {
            setAttribute(
                Attributes.INFO_CONNECTION_READONLY,
                mode
                ? Boolean.TRUE
                : Boolean.FALSE);

            isReadOnlyDefault = mode;
        }
    }

    public synchronized boolean isAutoCommit() {

        Object info = getAttribute(Attributes.INFO_AUTOCOMMIT);

        isAutoCommit = ((Boolean) info).booleanValue();

        return isAutoCommit;
    }

    public synchronized void setAutoCommit(boolean mode) {

        if (mode != isAutoCommit) {
            setAttribute(
                Attributes.INFO_AUTOCOMMIT,
                mode
                ? Boolean.TRUE
                : Boolean.FALSE);

            isAutoCommit = mode;
        }
    }

    public synchronized void setIsolationDefault(int level) {
        setAttribute(Attributes.INFO_ISOLATION, ValuePool.getInt(level));
    }

    public synchronized int getIsolation() {
        Object info = getAttribute(Attributes.INFO_ISOLATION);

        return ((Integer) info).intValue();
    }

    public synchronized boolean isClosed() {
        return isClosed;
    }

    public Session getSession() {
        return null;
    }

    public synchronized void startPhasedTransaction() {}

    public synchronized void prepareCommit() {

        resultOut.setAsTransactionEndRequest(
            ResultConstants.PREPARECOMMIT,
            null);

        Result in = execute(resultOut);

        if (in.isError()) {
            throw Error.error(in);
        }
    }

    public synchronized void commit(boolean chain) {

        resultOut.setAsTransactionEndRequest(ResultConstants.TX_COMMIT, null);

        Result in = execute(resultOut);

        if (in.isError()) {
            throw Error.error(in);
        }
    }

    public synchronized void rollback(boolean chain) {

        resultOut.setAsTransactionEndRequest(ResultConstants.TX_ROLLBACK, null);

        Result in = execute(resultOut);

        if (in.isError()) {
            throw Error.error(in);
        }
    }

    public synchronized void rollbackToSavepoint(String name) {

        resultOut.setAsTransactionEndRequest(
            ResultConstants.TX_SAVEPOINT_NAME_ROLLBACK,
            name);

        Result in = execute(resultOut);

        if (in.isError()) {
            throw Error.error(in);
        }
    }

    public synchronized void savepoint(String name) {

        Result result = Result.newSetSavepointRequest(name);
        Result in     = execute(result);

        if (in.isError()) {
            throw Error.error(in);
        }
    }

    public synchronized void releaseSavepoint(String name) {

        resultOut.setAsTransactionEndRequest(
            ResultConstants.TX_SAVEPOINT_NAME_RELEASE,
            name);

        Result in = execute(resultOut);

        if (in.isError()) {
            throw Error.error(in);
        }
    }

    public void addWarning(HsqlException warning) {}

    public synchronized long getId() {
        return sessionID;
    }

    public int getRandomId() {
        return randomID;
    }

    /**
     * Used by pooled connections to reset the server-side session to a new
     * one. In case of failure, the connection is closed.
     *
     * When the Connection.close() method is called, a pooled connection calls
     * this method instead of HSQLClientConnection.close(). It can then
     * reuse the HSQLClientConnection object with no further initialisation.
     *
     */
    public synchronized void resetSession() {

        Result login    = Result.newResetSessionRequest();
        Result resultIn = execute(login);

        if (resultIn.isError()) {
            isClosed = true;

            closeConnection();

            throw Error.error(resultIn);
        }

        sessionID  = resultIn.getSessionId();
        databaseID = resultIn.getDatabaseId();
    }

    protected void write(Result r) throws IOException,
            HsqlException {
        r.write(this, dataOutput, rowOut);
    }

    protected Result read() throws IOException,
                                   HsqlException {

        Result result = Result.newResult(dataInput, rowIn);

        result.readAdditionalResults(this, dataInput, rowIn);
        rowOut.reset(mainBuffer);
        rowIn.resetRow(mainBuffer.length);

        return result;
    }

    /**
     * Never called on this class
     */
    public synchronized String getInternalConnectionURL() {
        return null;
    }

    public Result cancel(Result result) {

        ClientConnection connection = new ClientConnection(this);

        try {
            return connection.execute(result);
        } finally {
            connection.closeConnection();
        }
    }

    public synchronized long getLobId() {
        return lobIDSequence--;
    }

    public BlobDataID createBlob(long length) {
        BlobDataID blob = new BlobDataID(getLobId());

        return blob;
    }

    public ClobDataID createClob(long length) {
        ClobDataID clob = new ClobDataID(getLobId());

        return clob;
    }

    /**
     * Does nothing here
     */
    public Result allocateResultLob(ResultLob resultLob) {
        return Result.updateZeroResult;
    }

    public Scanner getScanner() {

        if (scanner == null) {
            scanner = new Scanner();
        }

        return scanner;
    }

    public Calendar getCalendar() {

        if (calendar == null) {
            calendar = new GregorianCalendar(timeZone);
        }

        return calendar;
    }

    public Calendar getCalendarGMT() {

        if (calendarGMT == null) {
            calendarGMT = new GregorianCalendar(
                TimeZone.getTimeZone("GMT"),
                HsqlDateTime.defaultLocale);

            calendarGMT.setLenient(false);
        }

        return calendarGMT;
    }

    public SimpleDateFormat getSimpleDateFormatGMT() {

        if (simpleDateFormatGMT == null) {
            simpleDateFormatGMT = new SimpleDateFormat(
                "MMMM",
                HsqlDateTime.defaultLocale);

            Calendar cal = new GregorianCalendar(
                TimeZone.getTimeZone("GMT"),
                HsqlDateTime.defaultLocale);

            cal.setLenient(false);
            simpleDateFormatGMT.setCalendar(cal);
        }

        return simpleDateFormatGMT;
    }

    public TimeZone getTimeZone() {
        return timeZone;
    }

    public int getZoneSeconds() {
        return timeZone.getOffset(System.currentTimeMillis()) / 1000;
    }

    public int getStreamBlockSize() {
        return lobStreamBlockSize;
    }

    public HsqlProperties getClientProperties() {

        if (clientProperties == null) {
            if (clientPropertiesString.length() > 0) {
                clientProperties = HsqlProperties.delimitedArgPairsToProps(
                    clientPropertiesString,
                    "=",
                    ";",
                    null);
            } else {
                clientProperties = new HsqlProperties();
            }
        }

        return clientProperties;
    }

    public JDBCConnection getJDBCConnection() {
        return connection;
    }

    public void setJDBCConnection(JDBCConnection connection) {
        this.connection = connection;
    }

    public String getDatabaseUniqueName() {
        return databaseUniqueName;
    }

    /**
     * Converts specified encoded integer to a Network Compatibility Version
     * String. The transmitted integer is negative to distinguish it from
     * 7 bit ASCII characters.
     */
    public static String toNetCompVersionString(int i) {

        StringBuilder sb = new StringBuilder();

        i *= -1;

        sb.append(i / 1000000);

        i %= 1000000;

        sb.append('.');
        sb.append(i / 10000);

        i %= 10000;

        sb.append('.');
        sb.append(i / 100);

        i %= 100;

        sb.append('.');
        sb.append(i);

        return sb.toString();
    }

    protected void handshake() throws IOException {
        dataOutput.writeInt(NETWORK_COMPATIBILITY_VERSION_INT);
        dataOutput.flush();
    }

    public Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}