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();
}
}