org.hsqldb.server.ServerConnection Maven / Gradle / Ivy
Show all versions of sqltool Show documentation
/*
* For work developed by the HSQL Development Group:
*
* Copyright (c) 2001-2021, 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.
*
*
*
* For work originally developed by the Hypersonic SQL Group:
*
* Copyright (c) 1995-2000, The Hypersonic SQL 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 Hypersonic SQL 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 THE HYPERSONIC SQL GROUP,
* 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.
*
* This software consists of voluntary contributions made by many individuals
* on behalf of the Hypersonic SQL Group.
*/
package org.hsqldb.server;
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.io.EOFException;
import java.io.IOException;
import java.net.Socket;
import java.net.SocketException;
import java.util.concurrent.atomic.AtomicLong;
import org.hsqldb.ClientConnection;
import org.hsqldb.ColumnBase;
import org.hsqldb.DatabaseManager;
import org.hsqldb.HsqlException;
import org.hsqldb.Session;
import org.hsqldb.StatementTypes;
import org.hsqldb.error.Error;
import org.hsqldb.error.ErrorCode;
import org.hsqldb.lib.DataOutputStream;
import org.hsqldb.navigator.RowSetNavigator;
import org.hsqldb.persist.HsqlDatabaseProperties;
import org.hsqldb.resources.ResourceBundleHandler;
import org.hsqldb.result.Result;
import org.hsqldb.result.ResultConstants;
import org.hsqldb.result.ResultMetaData;
import org.hsqldb.result.ResultProperties;
import org.hsqldb.rowio.RowInputBinary;
import org.hsqldb.rowio.RowOutputBinary;
import org.hsqldb.rowio.RowOutputInterface;
import org.hsqldb.types.Type;
// fredt@users 20020215 - patch 461556 by paul-h@users - server factory
// fredt@users 20020424 - patch 1.7.0 by fredt - shutdown without exit
// fredt@users 20021002 - patch 1.7.1 by fredt - changed notification method
// fredt@users 20030618 - patch 1.7.2 by fredt - changed read/write methods
// fredt@users 20091013 - move set session to null suggested by Otto Joyner
/**
* All ServerConnection objects are listed in a Set in server
* and removed by this class when closed.
*
* When the database or server is shutdown, the signalClose() method is called
* for all current ServerConnection instances. This will call the private
* close() method. If the ServerConnection thread itself has caused the
* shutdown it returns the result of the operation to the client.
* (fredt@users)
*
* ODBC support added for version 2.0.0 by Blaine Simpson.
*
* ODBC support updated for version 2.5.x. (fredt@users)
*
* @author Blaine Simpson (unsaved@users dot sourceforge.net
* @author Fred Toussi (fredt@users dot sourceforge.net)
* @version 2.6.0
* @since 1.6.2
*/
class ServerConnection implements Runnable {
boolean keepAlive;
private String user;
int dbID;
int dbIndex;
private volatile Session session;
private Socket socket;
private Server server;
private DataInputStream dataInput;
private DataOutputStream dataOutput;
private long mThread;
static final int BUFFER_SIZE = 0x1000;
final byte[] mainBuffer = new byte[BUFFER_SIZE];
RowOutputInterface rowOut;
RowInputBinary rowIn;
Thread runnerThread;
InResultProcessor processor;
//
private static AtomicLong mCurrentThread = new AtomicLong(0);
//
protected static String TEXTBANNER_PART1 = null;
protected static String TEXTBANNER_PART2 = null;
static {
int serverBundleHandle = ResourceBundleHandler.getBundleHandle(
"org_hsqldb_server_Server_messages", null);
if (serverBundleHandle < 0) {
throw new RuntimeException(
"MISSING Resource Bundle. See source code");
// This will be caught before prod release.
// Not necessary to localize message.
}
TEXTBANNER_PART1 = ResourceBundleHandler.getString(serverBundleHandle,
"textbanner.part1");
TEXTBANNER_PART2 = ResourceBundleHandler.getString(serverBundleHandle,
"textbanner.part2");
if (TEXTBANNER_PART1 == null || TEXTBANNER_PART2 == null) {
throw new RuntimeException(
"MISSING Resource Bundle msg definition. See source code");
// This will be caught before prod release.
// Not necessary to localize message.
}
}
/**
* Creates a new ServerConnection to the specified Server on the
* specified socket.
*
* @param socket the network socket on which Server communication
* takes place
* @param server the Server instance to which the object
* represents a connection
*/
ServerConnection(Socket socket, Server server) {
RowOutputBinary rowOutTemp = new RowOutputBinary(mainBuffer);
rowIn = new RowInputBinary(rowOutTemp);
rowOut = rowOutTemp;
//
this.socket = socket;
this.server = server;
this.mThread = mCurrentThread.getAndIncrement();
synchronized (server.serverConnSetSync) {
server.serverConnSet.add(this);
}
}
/**
* Signals this object to close, including exiting the thread running
* the request handling loop
*/
void signalClose() {
keepAlive = false;
if (Thread.currentThread().equals(runnerThread)) {
Result resultOut = Result.updateZeroResult;
try {
resultOut.write(session, dataOutput, rowOut);
} catch (Throwable t) {}
}
close();
}
/**
* Closes this connection.
*/
private void close() {
if (session != null) {
session.close();
session = null;
}
// fredt@user - closing the socket is to stop this thread
synchronized (this) {
try {
if (socket != null) {
socket.close();
socket = null;
}
} catch (IOException e) {}
socket = null;
}
synchronized (server.serverConnSetSync) {
server.serverConnSet.remove(this);
}
try {
runnerThread.setContextClassLoader(null);
} catch (Throwable t) {}
}
/**
* Initializes this connection.
*
* Will return (not throw) if fail to initialize the connection.
*
*/
private void init() {
runnerThread = Thread.currentThread();
keepAlive = true;
try {
socket.setTcpNoDelay(true);
dataInput = new DataInputStream(
new BufferedInputStream(socket.getInputStream(), 16 * 1024));
dataOutput = new DataOutputStream(socket.getOutputStream());
int firstInt = handshake();
switch (streamProtocol) {
case HSQL_STREAM_PROTOCOL :
int msgType = dataInput.readByte();
processor.receiveConnection(msgType);
break;
case ODBC_STREAM_PROTOCOL :
processor.receiveConnection(firstInt);
break;
default :
// Protocol detection failures should already have been
// handled.
keepAlive = false;
}
} catch (Exception e) {
// Only "unexpected" failures are caught here.
// Expected failures will have been handled (by sending feedback
// to user-- with an output Result for normal protocols), then
// continuing.
StringBuilder sb = new StringBuilder(String.valueOf(mThread));
sb.append(":Failed to connect client.");
if (user != null) {
sb.append(" User '").append(user).append("'.");
}
sb.append(" Stack trace follows.");
server.printWithThread(sb.toString());
server.printStackTrace(e);
}
}
private static class ClientFailure extends Exception {
private String clientMessage = null;
public ClientFailure(String ourMessage, String clientMessage) {
super(ourMessage);
this.clientMessage = clientMessage;
}
public String getClientMessage() {
return clientMessage;
}
}
private CleanExit cleanExit = new CleanExit();
private void receiveResult(int resultMode) throws CleanExit, IOException {
boolean terminate = false;
Result resultIn = Result.newResult(session, resultMode, dataInput,
rowIn);
resultIn.readLobResults(session, dataInput);
server.printRequest(mThread, resultIn);
Result resultOut = null;
switch (resultIn.getType()) {
case ResultConstants.CONNECT : {
resultOut = setDatabase(resultIn);
break;
}
case ResultConstants.SQLCANCEL : {
resultOut = cancelStatement(resultIn);
terminate = true;
break;
}
case ResultConstants.DISCONNECT : {
resultOut = Result.updateZeroResult;
terminate = true;
break;
}
case ResultConstants.RESETSESSION : {
session.resetSession();
resultOut = Result.updateZeroResult;
break;
}
case ResultConstants.EXECUTE_INVALID : {
resultOut =
Result.newErrorResult(Error.error(ErrorCode.X_07502));
break;
}
default : {
resultOut = session.execute(resultIn);
break;
}
}
resultOut.write(session, dataOutput, rowOut);
rowOut.reset(mainBuffer);
rowIn.resetRow(mainBuffer.length);
if (terminate) {
throw cleanExit;
}
}
private OdbcPacketOutputStream outPacket = null;
private void receiveOdbcPacket(char inC) throws IOException, CleanExit {
/*
* The driver's notion of the transaction state, I (no) or T (yes),
* corresponds precisely inversely to our server-side Session
* autoCommit setting.
* If the user/app runs in non-autocommit mode and says to run a
* COMMIT followed by an INSERT, the driver will handle the user/app's
* facade of autocommittedness, and will send the server
* - COMMIT (which will cause us to set session.setAutoCommit(true)
*
- BEGIN (which will cause us to set session.setAutoCommit(false)
*
- INSERT...
*
*/
char c;
boolean sendReadyForQuery = false;
String psHandle, portalHandle, handle, dataString, tmpStr;
// Statement which must be executed after the primary statement, but
// before sending the ReadyForQuery Z packet.
String interposedStatement = null;
Result r, rOut;
int paramCount, lastSemi;
OdbcPreparedStatement odbcPs;
StatementPortal portal;
ResultMetaData pmd;
OdbcPacketInputStream inPacket = null;
Type[] colTypes;
PgType[] pgTypes;
try {
inPacket = OdbcPacketInputStream.newOdbcPacketInputStream(inC,
dataInput);
server.printWithThread("Got op (" + inPacket.packetType + ')');
server.printWithThread("Got packet length of "
+ inPacket.available()
+ " + type byte + 4 size header");
if (inPacket.available() >= 1000000000) {
throw new IOException("Insane packet length: "
+ inPacket.available()
+ " + type byte + 4 size header");
}
} catch (SocketException se) {
server.printWithThread("Ungraceful client exit: " + se);
throw cleanExit; // not "clean", but handled
} catch (IOException ioe) {
server.printWithThread("Fatal ODBC protocol failure: " + ioe);
try {
OdbcUtil.alertClient(OdbcUtil.ODBC_SEVERITY_FATAL,
ioe.toString(), "08P01", dataOutput);
// Code here means Protocol Violation
} catch (Exception e) {
// We just make an honest effort to notify the client
}
throw cleanExit; // not "clean", but handled
}
/*
* ODBC Service State Machine (the remainder of this method)
*/
switch (odbcCommMode) {
case OdbcUtil.ODBC_EXT_RECOVER_MODE :
if (inPacket.packetType != 'S') {
if (server.isTrace()) {
server.printWithThread("Ignoring a '"
+ inPacket.packetType + "'");
}
return;
}
odbcCommMode = OdbcUtil.ODBC_EXTENDED_MODE;
server.printWithThread(
"EXTENDED comm session being recovered");
// Now the main switch will handle the Sync packet carefully
// the same as if there were no recovery.
break;
case OdbcUtil.ODBC_SIMPLE_MODE :
switch (inPacket.packetType) {
case 'P' :
// This is the only way to make this switch, according
// to docs, but that does not allow for intermixing of
// static and prepared statement (selects or other).
// Therefore we allow all of the following, which works
// great.
case 'H' :
case 'S' :
case 'D' :
case 'B' :
case 'E' :
case 'C' :
odbcCommMode = OdbcUtil.ODBC_EXTENDED_MODE;
server.printWithThread(
"Switching mode from SIMPLE to EXTENDED");
// Do not detect unexpected ops here.
// In that case, leave the mode as it is, and the main
// switch below will handle appropriately.
}
break;
case OdbcUtil.ODBC_EXTENDED_MODE :
switch (inPacket.packetType) {
case 'Q' :
odbcCommMode = OdbcUtil.ODBC_SIMPLE_MODE;
server.printWithThread(
"Switching mode from EXTENDED to SIMPLE");
// Do not detect unexpected ops here.
// In that case, leave the mode as it is, and the main
// switch below will handle appropriately.
}
break;
default :
throw new RuntimeException("Unexpected ODBC comm mode value: "
+ odbcCommMode);
}
outPacket.reset();
try {
// Every switch case must either throw or break.
// For cases which break
// The packet will always be checked to make sure all bytes have
// been consumed.
// Set boolean sendReadyForQuery to send a Z/ReadyForQuery packet
// to client.
// DO NOT return early. If you need to abort, that is exceptional
// behavior and you should throw an Exception.
MAIN_ODBC_COMM_SWITCH:
switch (inPacket.packetType) {
case 'Q' : // Query packet
String sql = inPacket.readString();
// We don't ask for the null terminator
/* **********************************************
* These first few cases handle the driver's implicit handling
* of transactions. */
if (sql.startsWith("BEGIN;") || sql.equals("BEGIN")) {
/*
* We may get here because of Driver client trying to
* manage transactions implicitly; or because user/app.
* has really issued a "BEGIN" command.
* In the first case, we don't need to run the
* corresponding START TRANSACTION command, since the
* HyperSQL engine does this automatically, and can tell
* when it is needed far better than the client; however
* we do use this fact to update our Session autocommit
* state to match the client's notion.
* We ignore the latter case, because real HyperSQL
* user/apps will use "START TRANSACTION", not "BEGIN".
* Therefore, we just update autocommit state and run no
* other command against the engine.
*/
sql = sql.equals("BEGIN") ? null
: sql.substring(
"BEGIN;".length());
server.printWithThread(
"ODBC Trans started. Session AutoCommit -> F");
try {
session.setAutoCommit(false);
} catch (HsqlException he) {
throw new RecoverableOdbcFailure(
"Failed to change transaction state: "
+ he.getMessage(), he.getSQLState());
}
// Now just placate the driver
outPacket.write("BEGIN");
outPacket.xmit('C', dataOutput);
if (sql == null) {
sendReadyForQuery = true;
break;
}
}
if (sql.startsWith("SAVEPOINT ") && sql.indexOf(';') > 0) {
int firstSemi = sql.indexOf(';');
server.printWithThread(
"Interposing BEFORE primary statement: "
+ sql.substring(0, firstSemi));
odbcExecDirect(sql.substring(0, firstSemi));
sql = sql.substring(firstSemi + 1);
}
lastSemi = sql.lastIndexOf(';');
if (lastSemi > 0) {
String suffix = sql.substring(lastSemi + 1);
if (suffix.startsWith("RELEASE ")) {
interposedStatement = suffix;
sql = sql.substring(0, lastSemi);
}
if (suffix.startsWith("show transaction_isolation")) {
interposedStatement = null;
sql = "values session_isolation_level()";
}
}
/* ******************************************* */
String normalized = sql.trim().toLowerCase();
if (server.isTrace()) {
server.printWithThread("Received query (" + sql + ')');
}
/*
* BEWARE: We aren't supporting multiple result-sets from a
* compound statement. Plus, a general requirement is, the
* entire compound statement may return just one result set.
* I don't have time to check how it works elsewhere, but here,
* and for now, the Rowset-generating statement (SELECT, etc.)
* must be first in order for us to detect that we need to
* return a result set.
* If we do parse out the component statement here, the states
* set above apply to all executions, and only one Z packet
* should be sent at the very end.
*
* I find that the Driver can't handle compound statements
* which mix resultset + non-resultset statements (even in
* SIMPLE mode), so we are more capable than our client is.
*/
if (normalized.startsWith("select current_schema()")) {
//ok in PGS compatibility mode
// but transalate for other modes
sql = "values current_schema()";
normalized = sql;
}
if (normalized.startsWith("show ")) {
sql = "values " + sql.substring("show ".length());
normalized = sql;
}
if (normalized.startsWith("values n.nspname,")) {
// Executed by psqlodbc after every user-specified query.
server.printWithThread(
"Swallowing 'select n.nspname,...'");
outPacket.writeShort(1); // Num cols.
outPacket.write("oid");
outPacket.writeInt(201);
outPacket.writeShort(1);
outPacket.writeInt(23);
outPacket.writeShort(4);
outPacket.writeInt(-1);
outPacket.writeShort(0);
outPacket.xmit('T', dataOutput); // Xmit Row Definition
// This query returns no rows. typenam "lo"??
outPacket.write("SELECT");
outPacket.xmit('C', dataOutput);
sendReadyForQuery = true;
break;
}
if (normalized.startsWith(
"select oid, typbasetype from")) {
// Executed by psqlodbc immediately after connecting.
server.printWithThread(
"Simulating 'select oid, typbasetype...'");
/*
* This query is run as "a hack to get the oid of our
* large object oid type.
*/
outPacket.writeShort(2); // Num cols.
outPacket.write("oid"); // Col. name
outPacket.writeInt(101); // table ID
outPacket.writeShort(102); // column id
outPacket.writeInt(26); // Datatype ID [adtid]
outPacket.writeShort(4); // Datatype size [adtsize]
outPacket.writeInt(-1); // Var size [atttypmod]
outPacket.writeShort(0); // text "format code"
outPacket.write("typbasetype"); // Col. name
outPacket.writeInt(101); // table ID
outPacket.writeShort(103); // column id
outPacket.writeInt(26); // Datatype ID [adtid]
outPacket.writeShort(4); // Datatype size [adtsize]
outPacket.writeInt(-1); // Var size [atttypmod]
outPacket.writeShort(0); // text "format code"
outPacket.xmit('T', dataOutput); // sending a Tuple (row)
// This query returns no rows. typenam "lo"??
outPacket.write("SELECT");
outPacket.xmit('C', dataOutput);
sendReadyForQuery = true;
break;
}
if (normalized.startsWith("select ")
|| normalized.startsWith("values ")) {
server.printWithThread(
"Performing a real non-prepared SELECT...");
r = Result.newExecuteDirectRequest();
r.setPrepareOrExecuteProperties(
sql, 0, 0, StatementTypes.RETURN_RESULT, 0,
ResultProperties.defaultPropsValue,
java.sql.Statement.NO_GENERATED_KEYS, null, null);
server.printWithThread(sql);
rOut = session.execute(r);
switch (rOut.getType()) {
case ResultConstants.DATA :
break;
case ResultConstants.ERROR :
throw new RecoverableOdbcFailure(rOut);
default :
throw new RecoverableOdbcFailure(
"Output Result from Query execution is of "
+ "unexpected type: " + rOut.getType());
}
// See Result.newDataHeadResult() for what we have here
// .metaData, .navigator
RowSetNavigator navigator = rOut.getNavigator();
ResultMetaData md = rOut.metaData;
if (md == null) {
throw new RecoverableOdbcFailure(
"Failed to get metadata for query results");
}
int columnCount = md.getColumnCount();
String[] colLabels = md.getGeneratedColumnNames();
colTypes = md.columnTypes;
pgTypes = new PgType[columnCount];
for (int i = 0; i < pgTypes.length; i++) {
pgTypes[i] = PgType.getPgType(colTypes[i]);
}
// fredt : colLabels may not contain some column names
// colDefs is used when no label is present:
// SELECT TABLECOL AS COLLABLE has both name and label
// SELECT TABLECOL has name 'TABLECOL'
// SELECT 2 AS CONST has label 'CONST'
ColumnBase[] colDefs = md.columns;
// Num cols.
outPacket.writeShort(columnCount);
for (int i = 0; i < columnCount; i++) {
// col name
if (colLabels[i] != null) {
outPacket.write(colLabels[i]);
} else {
outPacket.write(colDefs[i].getNameString());
}
// table ID [relid]:
outPacket.writeInt(OdbcUtil.getTableOidForColumn(i,
md));
// column id [attid]
outPacket.writeShort(OdbcUtil.getIdForColumn(i,
md));
outPacket.writeInt(pgTypes[i].getOid());
// Datatype size [adtsize]
outPacket.writeShort(pgTypes[i].getTypeWidth());
outPacket.writeInt(pgTypes[i].getLPConstraint());
// Var size [atttypmod]
// This is the size constraint integer
// like VARCHAR(12) or DECIMAL(4).
// -1 if none specified for this column.
outPacket.writeShort(0);
// format code, 0 = text column, 1 = binary column,
// but entirely ignored by our driver.
// Would only be non-0 if a 'B' command requested it.
}
outPacket.xmit('T', dataOutput); // Xmit Row Definition
int rowNum = 0;
while (navigator.next()) {
rowNum++;
Object[] rowData = navigator.getCurrent();
// Row.getData(). Don't know why *Data.getCurrent()
// method returns Object instead of O[].
// TODO: Remove the assertion here:
if (rowData == null) {
throw new RecoverableOdbcFailure("Null row?");
}
if (rowData.length < columnCount) {
throw new RecoverableOdbcFailure(
"Data element mismatch. " + columnCount
+ " metadata cols, yet " + rowData.length
+ " data elements for row " + rowNum);
}
//server.printWithThread("Row " + rowNum + " has "
//+ rowData.length + " elements");
outPacket.writeShort(columnCount);
// This field is just swallowed by PG ODBC
// client, but OdbcUtil.validated by psql.
for (int i = 0; i < columnCount; i++) {
if (rowData[i] == null) {
/*
server.printWithThread("R" + rowNum + "C"
+ (i+1) + " => [null]");
*/
outPacket.writeInt(-1);
} else {
dataString =
pgTypes[i].valueString(rowData[i]);
outPacket.writeSized(dataString);
if (server.isTrace()) {
server.printWithThread(
"R" + rowNum + "C" + (i + 1)
+ " => ("
+ rowData[i].getClass().getName()
+ ") [" + dataString + ']');
}
}
}
outPacket.xmit('D', dataOutput);
}
outPacket.write("SELECT");
outPacket.xmit('C', dataOutput);
sendReadyForQuery = true;
break;
}
if (normalized.startsWith("deallocate \"")
&& normalized.charAt(normalized.length() - 1)
== '"') {
tmpStr = sql.trim().substring(
"deallocate \"".length()).trim();
// Must use "sql" directly since name is case-sensitive
handle = tmpStr.substring(0, tmpStr.length() - 1);
odbcPs = (OdbcPreparedStatement) sessionOdbcPsMap.get(
handle);
if (odbcPs != null) {
odbcPs.close();
}
portal =
(StatementPortal) sessionOdbcPortalMap.get(handle);
if (portal != null) {
portal.close();
}
if (odbcPs == null && portal == null) {
/*
throw new RecoverableOdbcFailure(null,
"No object present for handle: " + handle, "08P01");
Driver does not handle state change correctly, so
for now we just issue a warning:
OdbcUtil.alertClient(OdbcUtil.ODBC_SEVERITY_ERROR,
"No object present for handle: " + handle,
dataOutput);
TODO: Retest this. May have been side-effect of
other problems.
*/
server.printWithThread(
"Ignoring bad 'DEALLOCATE' cmd");
}
if (server.isTrace()) {
server.printWithThread("Deallocated PS/Portal '"
+ handle + "'");
}
outPacket.write("DEALLOCATE");
outPacket.xmit('C', dataOutput);
sendReadyForQuery = true;
break;
}
if (normalized.startsWith("set client_encoding to ")) {
server.printWithThread("Stubbing EXECDIR for: " + sql);
outPacket.write("SET");
outPacket.xmit('C', dataOutput);
sendReadyForQuery = true;
break;
}
// Case below is non-String-matched Qs:
server.printWithThread("Performing a real EXECDIRECT...");
odbcExecDirect(sql);
sendReadyForQuery = true;
break;
case 'X' : // Terminate packet
if (sessionOdbcPsMap.size()
> (sessionOdbcPsMap.containsKey("") ? 1
: 0)) {
server.printWithThread("Client left "
+ sessionOdbcPsMap.size()
+ " PS objects open");
}
if (sessionOdbcPortalMap.size()
> (sessionOdbcPortalMap.containsKey("") ? 1
: 0)) {
server.printWithThread("Client left "
+ sessionOdbcPortalMap.size()
+ " Portal objects open");
}
OdbcUtil.validateInputPacketSize(inPacket);
throw cleanExit;
case 'H' : // Flush packet
// No-op. It is impossible to cache while supporting multiple
// ps and portal objects, so there is nothing for a Flush to
// do. There isn't even a reply to a Flush packet.
break;
case 'S' : // Sync packet
// Special case for Sync packets.
// To facilitate recovery, we do not abort in case of problems.
if (session.isAutoCommit()) {
try {
// I don't see how this can be useful. If we ran DML, it
// will have autocommitted. If we have just switched to
// autoCommit mode, then according to spec we must have
// executed an implicit commit then.
server.printWithThread(
"Silly implicit commit by Sync");
session.commit(true);
// TODO: Find out if chain param should be T or F.
} catch (HsqlException he) {
server.printWithThread("Implicit commit failed: "
+ he);
OdbcUtil.alertClient(OdbcUtil.ODBC_SEVERITY_ERROR,
"Implicit commit failed",
he.getSQLState(), dataOutput);
}
}
sendReadyForQuery = true;
break;
case 'P' : // Parse packet
psHandle = inPacket.readString();
String query = OdbcUtil.revertMungledPreparedQuery(
inPacket.readString());
paramCount = inPacket.readUnsignedShort();
for (int i = 0; i < paramCount; i++) {
if (inPacket.readInt() != 0) {
throw new RecoverableOdbcFailure(
null,
"Parameter-type OID specifiers not supported yet",
"0A000");
}
}
if (server.isTrace()) {
server.printWithThread(
"Received Prepare request for query (" + query
+ ") with handle '" + psHandle + "'");
}
if (psHandle.length() > 0
&& sessionOdbcPsMap.containsKey(psHandle)) {
throw new RecoverableOdbcFailure(
null,
"PS handle '" + psHandle + "' already in use. "
+ "You must close it before recreating", "08P01");
}
new OdbcPreparedStatement(psHandle, query,
sessionOdbcPsMap, session);
outPacket.xmit('1', dataOutput);
break;
case 'D' : // Describe packet
c = inPacket.readByteChar();
handle = inPacket.readString();
odbcPs = null;
portal = null;
if (c == 'S') {
odbcPs = (OdbcPreparedStatement) sessionOdbcPsMap.get(
handle);
} else if (c == 'P') {
portal =
(StatementPortal) sessionOdbcPortalMap.get(handle);
} else {
throw new RecoverableOdbcFailure(
null,
"Description packet request type invalid: " + c,
"08P01");
}
if (server.isTrace()) {
server.printWithThread("Received Describe request for "
+ c + " of handle '" + handle
+ "'");
}
if (odbcPs == null && portal == null) {
throw new RecoverableOdbcFailure(
null,
"No object present for " + c + " handle: "
+ handle, "08P01");
}
Result ackResult = (odbcPs == null) ? portal.ackResult
: odbcPs.ackResult;
pmd = ackResult.parameterMetaData;
paramCount = pmd.getColumnCount();
Type[] paramTypes = pmd.getParameterTypes();
if (paramCount != paramTypes.length) {
throw new RecoverableOdbcFailure(
"Parameter count mismatch. Count of "
+ paramCount + " reported, but there are "
+ paramTypes.length + " param md objects");
}
if (c == 'S') {
outPacket.writeShort(paramCount);
for (int i = 0; i < paramTypes.length; i++) {
outPacket.writeInt(
PgType.getPgType(paramTypes[i]).getOid());
// TODO: Determine whether parameter typing works
// better for Strings when try to match table column
// or not. 2nd param to getPgType().
}
outPacket.xmit('t', dataOutput);
// ParameterDescription packet
}
ResultMetaData md = ackResult.metaData;
if (md.getColumnCount() < 1) {
if (server.isTrace()) {
server.printWithThread(
"Non-rowset query so returning NoData packet");
}
// Send NoData packet because no columnar output from
// this statement.
outPacket.xmit('n', dataOutput);
break;
}
// TODO:
// May need to pass the extra BIGINT pseudo-column for
// updatable-row or other purposes. In that case, it may
// make sense to use getExtendedColumnCount(), etc.
String[] colNames = md.getGeneratedColumnNames();
if (md.getColumnCount() != colNames.length) {
throw new RecoverableOdbcFailure(
"Couldn't get all column names: "
+ md.getColumnCount() + " cols. but only got "
+ colNames.length + " col. names");
}
colTypes = md.columnTypes;
pgTypes = new PgType[colNames.length];
ColumnBase[] colDefs = md.columns;
for (int i = 0; i < pgTypes.length; i++) {
pgTypes[i] = PgType.getPgType(colTypes[i]);
}
if (colNames.length != colDefs.length) {
throw new RecoverableOdbcFailure(
"Col data mismatch. " + colDefs.length
+ " col instances but " + colNames.length
+ " col names");
}
outPacket.writeShort(colNames.length); // Num cols.
for (int i = 0; i < colNames.length; i++) {
outPacket.write(colNames[i]); // Col. name
// table ID [relid]:
outPacket.writeInt(OdbcUtil.getTableOidForColumn(i,
md));
// column id [attid]
outPacket.writeShort(OdbcUtil.getIdForColumn(i, md));
outPacket.writeInt(pgTypes[i].getOid());
// Datatype size [adtsize]
outPacket.writeShort(pgTypes[i].getTypeWidth());
outPacket.writeInt(pgTypes[i].getLPConstraint());
// Var size [atttypmod]
// This is the size constraint integer
// like VARCHAR(12) or DECIMAL(4).
// -1 if none specified for this column.
outPacket.writeShort(0);
// format code, 0 = text column, 1 = binary column,
// but entirely ignored by our driver.
// Would only be non-0 if a 'B' command requested it.
}
outPacket.xmit('T', dataOutput); // Xmit Row Definition
break;
case 'B' : // Bind packet
portalHandle = inPacket.readString();
psHandle = inPacket.readString();
int paramFormatCount = inPacket.readUnsignedShort();
boolean[] paramBinary = new boolean[paramFormatCount];
for (int i = 0; i < paramFormatCount; i++) {
paramBinary[i] = inPacket.readUnsignedShort() != 0;
if (server.isTrace() && paramBinary[i]) {
server.printWithThread("Binary param #" + i);
}
}
paramCount = inPacket.readUnsignedShort();
Object[] paramVals = new Object[paramCount];
for (int i = 0; i < paramVals.length; i++) {
if (i < paramBinary.length && paramBinary[i]) {
paramVals[i] = inPacket.readSizedBinaryData();
} else {
paramVals[i] = inPacket.readSizedString();
}
}
int outFormatCount = inPacket.readUnsignedShort();
for (int i = 0; i < outFormatCount; i++) {
if (inPacket.readUnsignedShort() != 0) {
throw new RecoverableOdbcFailure(
null, "Binary output values not supported",
"0A000");
}
}
if (server.isTrace()) {
server.printWithThread(
"Received Bind request to make Portal from ("
+ psHandle + ")' with handle '" + portalHandle
+ "'");
}
odbcPs =
(OdbcPreparedStatement) sessionOdbcPsMap.get(psHandle);
if (odbcPs == null) {
throw new RecoverableOdbcFailure(
null,
"No object present for PS handle: " + psHandle,
"08P01");
}
if (portalHandle.length() > 0
&& sessionOdbcPortalMap.containsKey(
portalHandle)) {
throw new RecoverableOdbcFailure(
null,
"Portal handle '" + portalHandle
+ "' already in use. "
+ "You must close it before recreating", "08P01");
}
pmd = odbcPs.ackResult.parameterMetaData;
if (paramCount != pmd.getColumnCount()) {
throw new RecoverableOdbcFailure(
null,
"Client didn't specify all "
+ pmd.getColumnCount() + " parameters ("
+ paramCount + ')', "08P01");
}
new StatementPortal(portalHandle, odbcPs, paramVals,
sessionOdbcPortalMap);
outPacket.xmit('2', dataOutput);
break;
case 'E' : // Execute packet
portalHandle = inPacket.readString();
int fetchRows = inPacket.readInt();
if (server.isTrace()) {
server.printWithThread("Received Exec request for "
+ fetchRows
+ " rows from portal handle '"
+ portalHandle + "'");
}
portal = (StatementPortal) sessionOdbcPortalMap.get(
portalHandle);
if (portal == null) {
throw new RecoverableOdbcFailure(
null,
"No object present for Portal handle: "
+ portalHandle, "08P01");
}
// result properties means readonly, not holdable
portal.bindResult.setPreparedExecuteProperties(
portal.parameters, fetchRows, 0, 0, 0);
// 0 for maxRows means unlimited. Same for fetchRows.
rOut = session.execute(portal.bindResult);
switch (rOut.getType()) {
case ResultConstants.UPDATECOUNT :
outPacket.write(
OdbcUtil.echoBackReplyString(
portal.lcQuery, rOut.getUpdateCount()));
outPacket.xmit('C', dataOutput);
// end of rows (B or D packets)
// This keeps session.autoUpdate in sync with client's
// notion of transaction state.
if (portal.lcQuery.equals("commit")
|| portal.lcQuery.startsWith("commit ")
|| portal.lcQuery.equals("rollback")
|| portal.lcQuery.startsWith(
"rollback ")) {
try {
session.setAutoCommit(true);
} catch (HsqlException he) {
throw new RecoverableOdbcFailure(
"Failed to change transaction state: "
+ he.getMessage(), he.getSQLState());
}
}
break MAIN_ODBC_COMM_SWITCH;
case ResultConstants.DATA :
break;
case ResultConstants.ERROR :
throw new RecoverableOdbcFailure(rOut);
default :
throw new RecoverableOdbcFailure(
"Output Result from Portal execution is of "
+ "unexpected type: " + rOut.getType());
}
// See Result.newDataHeadResult() for what we have here
// .metaData, .navigator
RowSetNavigator navigator = rOut.getNavigator();
int rowNum = 0;
int colCount = portal.ackResult.metaData.getColumnCount();
while (navigator.next()) {
rowNum++;
Object[] rowData = navigator.getCurrent();
if (rowData == null) {
throw new RecoverableOdbcFailure("Null row?");
}
if (rowData.length < colCount) {
throw new RecoverableOdbcFailure(
"Data element mismatch. " + colCount
+ " metadata cols, yet " + rowData.length
+ " data elements for row " + rowNum);
}
//server.printWithThread("Row " + rowNum + " has "
//+ rowData.length + " elements");
outPacket.writeShort(colCount);
// This field is just swallowed by PG ODBC
// client, but validated by psql.
colTypes = portal.ackResult.metaData.columnTypes;
pgTypes = new PgType[colCount];
for (int i = 0; i < pgTypes.length; i++) {
pgTypes[i] = PgType.getPgType(colTypes[i]);
}
for (int i = 0; i < colCount; i++) {
if (rowData[i] == null) {
/*
server.printWithThread("R" + rowNum + "C"
+ (i+1) + " => [null]");
*/
outPacket.writeInt(-1);
} else {
dataString =
pgTypes[i].valueString(rowData[i]);
outPacket.writeSized(dataString);
if (server.isTrace()) {
server.printWithThread(
"R" + rowNum + "C" + (i + 1) + " => ("
+ rowData[i].getClass().getName()
+ ") [" + dataString + ']');
}
}
}
outPacket.xmit('D', dataOutput);
}
if (navigator.getSize() == 0 || navigator.afterLast()) {
outPacket.write("SELECT");
outPacket.xmit('C', dataOutput);
// end of rows (B or D packets)
} else {
outPacket.xmit('s', dataOutput);
}
// N.b., we return.
// You might think that this completion of an EXTENDED sequence
// would end in ReadyForQuery/Z, but no.
break;
case 'C' : // Close packet
c = inPacket.readByteChar();
handle = inPacket.readString();
odbcPs = null;
portal = null;
if (c == 'S') {
odbcPs = (OdbcPreparedStatement) sessionOdbcPsMap.get(
handle);
if (odbcPs != null) {
odbcPs.close();
}
} else if (c == 'P') {
portal =
(StatementPortal) sessionOdbcPortalMap.get(handle);
if (portal != null) {
portal.close();
}
} else {
throw new RecoverableOdbcFailure(
null,
"Description packet request type invalid: " + c,
"08P01");
}
// TODO: Try sending a warning to client for both == null.
// Broke things earlier, but that may have been due to
// other problems.
if (server.isTrace()) {
server.printWithThread("Closed " + c + " '" + handle
+ "'? "
+ (odbcPs != null
|| portal != null));
}
outPacket.xmit('3', dataOutput);
break;
default :
throw new RecoverableOdbcFailure(
null,
"Unsupported operation type (" + inPacket.packetType
+ ')', "0A000");
}
OdbcUtil.validateInputPacketSize(inPacket);
if (interposedStatement != null) {
server.printWithThread("Interposing AFTER primary statement: "
+ interposedStatement);
odbcExecDirect(interposedStatement);
}
if (sendReadyForQuery) {
outPacket.reset();
// The reset is unnecessary now. For safety in case somebody
// codes something above which may abort processing of a
// packet before xmit.
outPacket.writeByte(session.isAutoCommit() ? 'I'
: 'T');
outPacket.xmit('Z', dataOutput);
}
} catch (RecoverableOdbcFailure rf) {
Result errorResult = rf.getErrorResult();
if (errorResult == null) {
String stateCode = rf.getSqlStateCode();
String svrMsg = rf.toString();
String cliMsg = rf.getClientMessage();
if (server.isTrace()) {
server.printWithThread(svrMsg);
}
if (cliMsg != null) {
OdbcUtil.alertClient(OdbcUtil.ODBC_SEVERITY_ERROR, cliMsg,
stateCode, dataOutput);
}
} else {
if (server.isTrace()) {
server.printWithThread("Result object error: "
+ errorResult.getMainString());
}
// This class of error is not considered a Server problem, so
// we don't log on the server side.
OdbcUtil.alertClient(OdbcUtil.ODBC_SEVERITY_ERROR,
errorResult.getMainString(),
errorResult.getSubString(), dataOutput);
}
switch (odbcCommMode) {
case OdbcUtil.ODBC_SIMPLE_MODE :
outPacket.reset(); /// transaction status = Error
outPacket.writeByte('E'); /// transaction status = Error
// TODO: Consider keeping this state until the session
// is either committed or rolled back.
// (Right now we just return 'E' here, then revert to
// I or T).
outPacket.xmit('Z', dataOutput);
break;
case OdbcUtil.ODBC_EXTENDED_MODE :
odbcCommMode = OdbcUtil.ODBC_EXT_RECOVER_MODE;
server.printWithThread("Reverting to EXT_RECOVER mode");
break;
}
}
}
/**
* Initializes this connection and runs the request handling
* loop until closed.
*/
public void run() {
int msgType;
init();
if (session != null) {
try {
while (keepAlive) {
msgType = dataInput.readByte();
processor.receiveResult((char) msgType);
}
} catch (CleanExit ce) {
keepAlive = false;
} catch (IOException e) {
// fredt - is thrown when connection drops
server.printWithThread(mThread + ":disconnected " + user);
} catch (HsqlException e) {
// fredt - is thrown in unforeseen circumstances
if (keepAlive) {
server.printStackTrace(e);
}
} catch (Throwable e) {
// fredt - is thrown in unforeseen circumstances
if (keepAlive) {
server.printStackTrace(e);
}
}
}
close();
}
private Result setDatabase(Result resultIn) {
try {
String databaseName = resultIn.getDatabaseName();
dbIndex = server.getDBIndex(databaseName);
dbID = server.dbID[dbIndex];
user = resultIn.getMainString();
if (!server.isSilent()) {
server.printWithThread(mThread + ":Trying to connect user '"
+ user + "' to DB (" + databaseName
+ ')');
}
session = DatabaseManager.newSession(dbID, user,
resultIn.getSubString(),
resultIn.getZoneString(),
resultIn.getUpdateCount());
if (!server.isSilent()) {
server.printWithThread(mThread + ":Connected user '" + user
+ "'");
}
return Result.newConnectionAcknowledgeResponse(session);
} catch (HsqlException e) {
session = null;
return Result.newErrorResult(e);
} catch (Throwable e) {
session = null;
return Result.newErrorResult(e);
}
}
private Result cancelStatement(Result resultIn) {
try {
dbID = resultIn.getDatabaseId();
long sessionId = resultIn.getSessionId();
session = DatabaseManager.getSession(dbID, sessionId);
if (!server.isSilent()) {
server.printWithThread(mThread
+ ":Trying to cancel statement "
+ " to DB (" + dbID + ')');
}
return session.cancel(resultIn);
} catch (HsqlException e) {
session = null;
return Result.updateZeroResult;
} catch (Throwable t) {
session = null;
return Result.updateZeroResult;
}
}
/**
* Retrieves the thread name to be used when
* this object is the Runnable object of a Thread.
*
* @return the thread name to be used when this object is the Runnable
* object of a Thread.
*/
String getConnectionThreadName() {
return "HSQLDB Connection @" + Integer.toString(hashCode(), 16);
}
/**
* Don't want this too high, or users may give up before seeing the
* banner. Can't be too low or we could close a valid but slow
* client connection.
*/
public static long MAX_WAIT_FOR_CLIENT_DATA = 1000; // ms.
public static long CLIENT_DATA_POLLING_PERIOD = 100; // ms.
/**
* The only known case where a connection attempt will get stuck is
* if client connects with hsqls to a https server; or
* hsql to a http server.
* All other client X server combinations are handled gracefully.
*
* If returns (a.o.t. throws), then state variable streamProtocol will
* be set.
*
* @return int read as first thing off of stream
*/
public int handshake() throws IOException {
long clientDataDeadline = new java.util.Date().getTime()
+ MAX_WAIT_FOR_CLIENT_DATA;
if (!(socket instanceof javax.net.ssl.SSLSocket)) {
// available() does not work for SSL socket input stream
do {
try {
Thread.sleep(CLIENT_DATA_POLLING_PERIOD);
} catch (InterruptedException ie) {}
} while (dataInput.available() < 5
&& new java.util.Date().getTime() < clientDataDeadline);
// Old HSQLDB clients will send resultType byte + 4 length bytes
// New HSQLDB clients will send NCV int + above = 9 bytes
// ODBC clients will send a much larger StartupPacket
if (dataInput.available() < 1) {
dataOutput.write(
(TEXTBANNER_PART1
+ ClientConnection.NETWORK_COMPATIBILITY_VERSION
+ TEXTBANNER_PART2 + '\n').getBytes());
dataOutput.flush();
throw Error.error(ErrorCode.SERVER_UNKNOWN_CLIENT);
}
}
int firstInt = dataInput.readInt();
switch (firstInt >> 24) {
case 80 : // Empirically
server.print(
"Rejected attempt from client using hsql HTTP protocol");
return 0;
case 0 :
// For ODBC protocol, this is the first byte of a 4-byte int
// size. The size can never be large enough that the first
// byte will be non-zero.
streamProtocol = ODBC_STREAM_PROTOCOL;
processor = new OdbcInResultProcessor();
break;
default :
streamProtocol = HSQL_STREAM_PROTOCOL;
processor = new HsqlInResultProcessor();
if (firstInt
!= ClientConnection
.NETWORK_COMPATIBILITY_VERSION_INT) {
if (firstInt == -1900000) {
firstInt = -2000000;
}
String verString =
ClientConnection.toNetCompVersionString(firstInt);
throw Error.error(null,
ErrorCode.SERVER_VERSIONS_INCOMPATIBLE,
0, new String[] {
HsqlDatabaseProperties.THIS_VERSION, verString
});
}
// HSQL protocol client
}
return firstInt;
}
private void odbcConnect(int firstInt) throws IOException {
/* Until client receives teh ReadyForQuery packet at the end of this
* method, we (the server) initiate all packet exchanges. */
int major = dataInput.readUnsignedShort();
int minor = dataInput.readUnsignedShort();
// Can just return to fail, until the value of "session" is set below.
if (major == 1 && minor == 7) {
// This is what old HyperSQL versions always send
// TODO: Consider sending client a 1.8-compatible SQLException
server.print("A pre-version 2.0 client attempted to connect. "
+ "We rejected them.");
return;
}
if (major == 1234 && minor == 5679) {
// No reason to pay any attention to the size header in this case.
dataOutput.writeByte('N'); // SSL not supported yet
// TODO: Implement SSL here (and reply with 'S')
odbcConnect(dataInput.readInt());
return;
}
if (major == 1234 && minor == 5678) {
// No reason to pay any attention to the size header in this case.
if (firstInt != 16) {
server.print(
"ODBC cancellation request sent wrong packet length: "
+ firstInt);
}
server.print(
"Got an ODBC cancellation request for thread ID "
+ dataInput.readInt() + ", but we don't support "
+ "OOB cancellation yet. "
+ "Ignoring this request and closing the connection.");
// N.b., Spec says to NOT reply to client in this case.
return;
}
server.printWithThread("ODBC client connected. "
+ "ODBC Protocol Compatibility Version "
+ major + '.' + minor);
OdbcPacketInputStream inPacket =
OdbcPacketInputStream.newOdbcPacketInputStream('\0', dataInput,
firstInt - 8);
// - 4 for size of firstInt - 2 for major - 2 for minor
java.util.Map stringPairs = inPacket.readStringPairs();
if (server.isTrace()) {
server.print("String Pairs from ODBC client: " + stringPairs);
}
try {
try {
OdbcUtil.validateInputPacketSize(inPacket);
} catch (RecoverableOdbcFailure rf) {
// In this case, we do not treat it as recoverable
throw new ClientFailure(rf.toString(), rf.getClientMessage());
}
inPacket.close();
if (!stringPairs.containsKey("database")) {
throw new ClientFailure("Client did not identify database",
"Target database not identified");
}
if (!stringPairs.containsKey("user")) {
throw new ClientFailure("Client did not identify user",
"Target account not identified");
}
String databaseName = (String) stringPairs.get("database");
user = (String) stringPairs.get("user");
if (databaseName.equals("/")) {
// Work-around because ODBC doesn't allow "" for Database name
databaseName = "";
}
/* Unencoded/unsalted authentication */
dataOutput.writeByte('R');
dataOutput.writeInt(8); //size
dataOutput.writeInt(OdbcUtil.ODBC_AUTH_REQ_PASSWORD);
dataOutput.flush();
// areq of auth. mode.
char c = '\0';
try {
c = (char) dataInput.readByte();
} catch (EOFException eofe) {
server.printWithThread(
"Looks like we got a goofy psql no-auth attempt. "
+ "Will probably retry properly very shortly");
return;
}
if (c != 'p') {
throw new ClientFailure(
"Expected password prefix 'p', " + "but got '" + c + "'",
"Password value not prefixed with 'p'");
}
int len = dataInput.readInt() - 5;
// Is password len after -4 for count int -1 for null term
if (len < 0) {
throw new ClientFailure(
"Client submitted invalid password length " + len,
"Invalid password length " + len);
}
String password = ServerConnection.readNullTermdUTF(len,
dataInput);
dbIndex = server.getDBIndex(databaseName);
dbID = server.dbID[dbIndex];
if (!server.isSilent()) {
server.printWithThread(mThread + ":Trying to connect user '"
+ user + "' to DB (" + databaseName
+ ')');
}
try {
session = DatabaseManager.newSession(dbID, user, password,
null, 0);
// TODO: Find out what updateCount, the last para, is for:
// resultIn.getUpdateCount());
} catch (Exception e) {
throw new ClientFailure("User name or password denied: " + e,
"Login attempt rejected");
}
} catch (ClientFailure cf) {
server.print(cf.toString());
// Code below means CONNECTION FAILURE
OdbcUtil.alertClient(OdbcUtil.ODBC_SEVERITY_FATAL,
cf.getClientMessage(), "08006", dataOutput);
return;
}
outPacket = OdbcPacketOutputStream.newOdbcPacketOutputStream();
outPacket.writeInt(OdbcUtil.ODBC_AUTH_REQ_OK); //success
outPacket.xmit('R', dataOutput); // Notify client of success
for (int i = 0; i < OdbcUtil.hardcodedParams.length; i++) {
OdbcUtil.writeParam(OdbcUtil.hardcodedParams[i][0],
OdbcUtil.hardcodedParams[i][1], dataOutput);
}
// If/when we implement OOB cancellation, we would send the
// Session identifier and key here, with a 'K' packet.
outPacket.writeByte('I'); // Trans. status = Not in transaction
outPacket.xmit('Z', dataOutput); // Notify client of success
// This ReadyForQuery turns over responsibility to initiate packet
// exchanges to the client.
OdbcUtil.alertClient(
OdbcUtil.ODBC_SEVERITY_INFO,
"MHello\nYou have connected to HyperSQL ODBC Server", dataOutput);
dataOutput.flush();
}
private java.util.Map sessionOdbcPsMap = new java.util.HashMap();
private java.util.Map sessionOdbcPortalMap = new java.util.HashMap();
/**
* Read String directly from dataInput.
*
* @param reqLength Required length
*/
private static String readNullTermdUTF(int reqLength,
java.io.InputStream istream)
throws IOException {
/* Would be MUCH easier to do this with Java6's String
* encoding/decoding operations */
int bytesRead = 0;
byte[] ba = new byte[reqLength + 3];
ba[0] = (byte) (reqLength >>> 8);
ba[1] = (byte) reqLength;
while (bytesRead < reqLength + 1) {
bytesRead += istream.read(ba, 2 + bytesRead,
reqLength + 1 - bytesRead);
}
if (ba[ba.length - 1] != 0) {
throw new IOException("String not null-terminated");
}
for (int i = 2; i < ba.length - 1; i++) {
if (ba[i] == 0) {
throw new RuntimeException("Null internal to String at offset "
+ (i - 2));
}
}
java.io.DataInputStream dis =
new java.io.DataInputStream(new ByteArrayInputStream(ba));
String s = dis.readUTF();
//String s = java.io.DataInputStream.readUTF(dis);
// TODO: Test the previous two to see if one works better for
// high-order characters.
dis.close();
return s;
}
// Tentative state variable
private int streamProtocol = UNDEFINED_STREAM_PROTOCOL;
static final int UNDEFINED_STREAM_PROTOCOL = 0;
static final int HSQL_STREAM_PROTOCOL = 1;
static final int ODBC_STREAM_PROTOCOL = 2;
int odbcCommMode = OdbcUtil.ODBC_SIMPLE_MODE;
private void odbcExecDirect(String inStatement)
throws RecoverableOdbcFailure, IOException {
String statement = inStatement;
String norm = statement.trim().toLowerCase();
if (norm.startsWith("release ")
&& !norm.startsWith("release savepoint")) {
server.printWithThread(
"Transmogrifying 'RELEASE ...' to 'RELEASE SAVEPOINT...");
statement = statement.trim().substring(0, "release ".length())
+ "SAVEPOINT "
+ statement.trim().substring("release ".length());
}
Result r = Result.newExecuteDirectRequest();
r.setPrepareOrExecuteProperties(
statement, 0, 0, StatementTypes.RETURN_COUNT, 0,
ResultProperties.defaultPropsValue,
ResultConstants.RETURN_NO_GENERATED_KEYS, null, null);
server.printWithThread(statement);
Result rOut = session.execute(r);
switch (rOut.getType()) {
case ResultConstants.UPDATECOUNT :
break;
case ResultConstants.ERROR :
throw new RecoverableOdbcFailure(rOut);
default :
throw new RecoverableOdbcFailure(
"Output Result from execution is of "
+ "unexpected type: " + rOut.getType());
}
outPacket.reset();
outPacket.write(OdbcUtil.echoBackReplyString(norm,
rOut.getUpdateCount()));
// This keeps session.autoUpdate in sync with client's notion
// of transaction state.
outPacket.xmit('C', dataOutput);
if (norm.equals("commit") || norm.startsWith("commit ")
|| norm.equals("rollback") || norm.startsWith("rollback ")) {
try {
session.setAutoCommit(true);
} catch (HsqlException he) {
throw new RecoverableOdbcFailure(
"Failed to change transaction state: " + he.getMessage(),
he.getSQLState());
}
}
}
class OdbcInResultProcessor implements InResultProcessor {
public void receiveConnection(int type) throws IOException {
ServerConnection.this.odbcConnect(type);
}
public void receiveResult(int type) throws CleanExit, IOException {
ServerConnection.this.receiveOdbcPacket((char) type);
}
}
class HsqlInResultProcessor implements InResultProcessor {
public void receiveConnection(int type) throws CleanExit, IOException {
receiveResult(type);
}
public void receiveResult(int type) throws CleanExit, IOException {
ServerConnection.this.receiveResult(type);
}
}
}
interface InResultProcessor {
void receiveConnection(int type) throws CleanExit, IOException;
void receiveResult(int type) throws CleanExit, IOException;
}
class CleanExit extends Exception {}