Mercurial > hg > monetdb-java
changeset 172:60063c67f9e7 embedded
Merged with default
author | Pedro Ferreira <pedro.ferreira@monetdbsolutions.com> |
---|---|
date | Tue, 19 Sep 2017 13:49:34 +0200 (2017-09-19) |
parents | 0f95fee3cf29 (diff) 296c4a16ef9f (current diff) |
children | 89c285fc0a49 |
files | build.properties pom.xml src/main/java/nl/cwi/monetdb/client/JdbcClient.java src/main/java/nl/cwi/monetdb/jdbc/MonetBlob.java src/main/java/nl/cwi/monetdb/jdbc/MonetClob.java src/main/java/nl/cwi/monetdb/jdbc/MonetConnection.java src/main/java/nl/cwi/monetdb/jdbc/MonetDataSource.java src/main/java/nl/cwi/monetdb/jdbc/MonetDatabaseMetaData.java src/main/java/nl/cwi/monetdb/jdbc/MonetDriver.java.in src/main/java/nl/cwi/monetdb/jdbc/MonetPreparedStatement.java src/main/java/nl/cwi/monetdb/jdbc/MonetResultSet.java src/main/java/nl/cwi/monetdb/jdbc/MonetStatement.java src/main/java/nl/cwi/monetdb/mcl/connection/mapi/MapiConnection.java tests/Bug_PrepStmtSetString_6382.java tests/Test_PSmetadata.java tests/build.xml |
diffstat | 87 files changed, 8023 insertions(+), 7319 deletions(-) [+] |
line wrap: on
line diff
--- a/build.properties +++ b/build.properties @@ -9,7 +9,7 @@ # major release number MCL_MAJOR=1 # minor release number -MCL_MINOR=15 +MCL_MINOR=14 ## @@ -19,7 +19,7 @@ MCL_MINOR=15 # major release number JDBC_MAJOR=2 # minor release number -JDBC_MINOR=26 +JDBC_MINOR=32 # an additional identifying string JDBC_VER_SUFFIX=Liberica # the default port to connect on, if no port given when using SQL @@ -30,8 +30,7 @@ JDBC_DEF_PORT=50000 ## General ## -# should we compile with debugging symbols? Not for public releases +# should we compile with debugging symbols enable_debug=false - -# should we optimize the build, preferably not for debugging. Yes for public releases +# should we optimize the build, preferably not for debugging enable_optimize=true
--- a/build.xml +++ b/build.xml @@ -18,7 +18,6 @@ Copyright 1997 - July 2008 CWI, August 2 <project name="MonetDB_Java_Drivers" default="default" basedir="."> - <!-- set global properties for this build --> <property name="srcdir" value="src/main/java" /> <property name="libdir" value="lib" /> @@ -31,20 +30,14 @@ Copyright 1997 - July 2008 CWI, August 2 <property name="util-package" value="nl/cwi/monetdb/util" /> <property name="mero-control-package" value="nl/cwi/monetdb/merovingian" /> - <property file="build.local.properties" /> <property file="build.properties" /> - <property name="jdbc-jar" - value="${jardir}/monetdb-jdbc-${JDBC_MAJOR}.${JDBC_MINOR}.jar" /> - <property name="jdbcclient-jar" - value="${jardir}/jdbcclient.jar" /> - <property name="jmonetdb-jar" - value="${jardir}/jmonetdb.jar" /> - <property name="mcl-jar" - value="${jardir}/monetdb-mcl-${MCL_MAJOR}.${MCL_MINOR}.jar" /> + <property name="jdbc-jar" value="${jardir}/monetdb-jdbc-new-${JDBC_MAJOR}.${JDBC_MINOR}.jar" /> + <property name="jdbcclient-jar" value="${jardir}/jdbcclient.jar" /> + <property name="jmonetdb-jar" value="${jardir}/jmonetdb.jar" /> + <property name="mcl-jar" value="${jardir}/monetdb-mcl-${MCL_MAJOR}.${MCL_MINOR}.jar" /> - <property name="mero-control-jar" - value="${jardir}/merocontrol.jar" /> + <property name="mero-control-jar" value="${jardir}/merocontrol.jar" /> <!-- @@ -258,7 +251,7 @@ Copyright 1997 - July 2008 CWI, August 2 </target> <!-- - This generates MonetDriver.java from its ".java.in" equivalents. It's + This generates MonetDriver.java.in from its ".java.in" equivalents. It's required for importing the driver version properties. --> <target name="driver" depends="prepare" unless="uptodate.drivers"> @@ -282,7 +275,7 @@ Copyright 1997 - July 2008 CWI, August 2 <!-- now copy and filter the file --> <copy file="${srcdir}/${jdbc-package}/MonetDriver.java.in" overwrite="true" - tofile="${srcdir}/${jdbc-package}/MonetDriver.java" + tofile="src/main/java/nl/cwi/monetdb/jdbc/MonetDriver.java.in" filtering="yes" /> <echo message="Configured build for the ${JDBC_MAJOR}.${JDBC_MINOR} (${JDBC_VER_SUFFIX}) edition driver" />
--- a/example/MJDBCTest.java +++ b/example/MJDBCTest.java @@ -17,8 +17,7 @@ import java.sql.*; */ public class MJDBCTest { public static void main(String[] args) throws Exception { - // make sure the driver is loaded - Class.forName("nl.cwi.monetdb.jdbc.MonetDriver"); + //Class.forName("nl.cwi.monetdb.jdbc.MonetDriver"); // turn on debugging (disabled) //nl.cwi.monetdb.jdbc.MonetConnection.setDebug(true); Connection con = DriverManager.getConnection("jdbc:monetdb://localhost/notused", "monetdb", "monetdb");
--- a/example/PreparedExample.java +++ b/example/PreparedExample.java @@ -15,8 +15,7 @@ import java.sql.*; */ public class PreparedExample { public static void main(String[] args) throws Exception { - // make sure the driver is loaded - Class.forName("nl.cwi.monetdb.jdbc.MonetDriver"); + //Class.forName("nl.cwi.monetdb.jdbc.MonetDriver"); Connection con = DriverManager.getConnection("jdbc:monetdb://localhost/notused", "monetdb", "monetdb"); PreparedStatement st = con.prepareStatement("SELECT ? AS a1, ? AS a2"); ResultSet rs;
--- a/example/SQLImport.java +++ b/example/SQLImport.java @@ -37,8 +37,7 @@ public class SQLImport { // open the file BufferedReader fr = new BufferedReader(new FileReader(args[0])); - // make sure the driver is loaded - Class.forName("nl.cwi.monetdb.jdbc.MonetDriver"); + // Class.forName("nl.cwi.monetdb.jdbc.MonetDriver"); // request a connection suitable for Monet from the driver manager // note that the database specifier is currently not implemented, for // Monet itself can't access multiple databases.
--- a/example/SQLcopyinto.java +++ b/example/SQLcopyinto.java @@ -9,8 +9,9 @@ import java.sql.*; import java.io.*; import java.util.*; -import nl.cwi.monetdb.mcl.net.*; -import nl.cwi.monetdb.mcl.io.*; + +import nl.cwi.monetdb.mcl.connection.mapi.MapiConnection; +import nl.cwi.monetdb.mcl.protocol.AbstractProtocol; /** * This example demonstrates how the MonetDB JDBC driver can facilitate @@ -22,12 +23,9 @@ import nl.cwi.monetdb.mcl.io.*; public class SQLcopyinto { public static void main(String[] args) throws Exception { - // make sure the driver is loaded - Class.forName("nl.cwi.monetdb.jdbc.MonetDriver"); - // request a connection suitable for Monet from the driver manager - // note that the database specifier is currently not implemented, for - // Monet itself can't access multiple databases. - // turn on debugging + // Class.forName("nl.cwi.monetdb.jdbc.MonetDriver"); + // request a connection suitable for Monet from the driver manager note that the database specifier is currently + // not implemented, for Monet itself can't access multiple databases turn on debugging Connection con = DriverManager.getConnection("jdbc:monetdb://localhost/database", "monetdb", "monetdb"); // get a statement to execute on @@ -45,24 +43,19 @@ public class SQLcopyinto { // of course also be done simultaneously with the JDBC // connection being kept connected - MapiSocket server = new MapiSocket(); - - server.setDatabase("database"); - server.setLanguage("sql"); + MapiConnection server = new MapiConnection(null, null, "sql", false, true,"localhost", 50000, "database"); try { - List warning = - server.connect("localhost", 50000, "monetdb", "monetdb"); + List warning = server.connect("monetdb", "monetdb"); if (warning != null) { - for (Iterator it = warning.iterator(); it.hasNext(); ) { - System.out.println(it.next().toString()); + for (Object aWarning : warning) { + System.out.println(aWarning.toString()); } } + AbstractProtocol oldmMapiProtocol = server.getProtocol(); - BufferedMCLReader in = server.getReader(); - BufferedMCLWriter out = server.getWriter(); - - String error = in.waitForPrompt(); + oldmMapiProtocol.waitUntilPrompt(); + String error = oldmMapiProtocol.getRemainingStringLine(0); if (error != null) throw new Exception(error); @@ -70,15 +63,13 @@ public class SQLcopyinto { // the leading 's' is essential, since it is a protocol // marker that should not be omitted, likewise the // trailing semicolon - out.write('s'); - out.write(query); - out.newLine(); + oldmMapiProtocol.writeNextQuery("s", query, "\n"); + for (int i = 0; i < 100; i++) { - out.write("" + i + ",val_" + i); - out.newLine(); + oldmMapiProtocol.writeNextQuery(null, "" + i + ",val_" + i, "\n"); } - out.writeLine(""); // need this one for synchronisation over flush() - error = in.waitForPrompt(); + oldmMapiProtocol.waitUntilPrompt(); + error = oldmMapiProtocol.getRemainingStringLine(0); if (error != null) throw new Exception(error); // disconnect from server
--- a/pom.xml +++ b/pom.xml @@ -1,152 +1,141 @@ <?xml version="1.0" encoding="UTF-8"?> <project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 - http://maven.apache.org/xsd/maven-4.0.0.xsd" - xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> - <modelVersion>4.0.0</modelVersion> + http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> + <modelVersion>4.0.0</modelVersion> - <groupId>monetdb</groupId> - <artifactId>monetdb-jdbc</artifactId> - <version>2.26</version> - <name>${project.groupId}:${project.artifactId}</name> - <description>MonetDB JDBC driver</description> - <url>https://www.monetdb.org</url> + <groupId>monetdb</groupId> + <artifactId>monetdb-jdbc-new</artifactId> + <version>2.32</version> + <name>MonetDB JDBC new</name> + <description>MonetDB Adapted JDBC driver for embedded connection</description> + <url>https://www.monetdb.org</url> - <licenses> - <license> - <name>Mozilla Public License, Version 2.0</name> - <url>https://www.mozilla.org/MPL/2.0/</url> - </license> - </licenses> + <licenses> + <license> + <name>Mozilla Public License, Version 2.0</name> + <url>https://www.mozilla.org/MPL/2.0/</url> + </license> + </licenses> - <developers> - <developer> - <name>Sjoerd Mullender</name> - <email>sjoerd@monetdb.org</email> - <organization>MonetDB</organization> - <organizationUrl>https://www.monetdb.org</organizationUrl> - </developer> - <developer> - <name>Dimitar Nedev</name> - <email>dimitar.nedev@monetdbsolutions.com</email> - <organization>MonetDB Solutions</organization> - <organizationUrl>https://www.monetdbsolutions.com</organizationUrl> - </developer> - </developers> + <developers> + <developer> + <name>Pedro Ferreira</name> + <email>pedro.ferreira@monetdbsolutions.com</email> + <organization>MonetDB</organization> + <organizationUrl>https://www.monetdb.org</organizationUrl> + </developer> + </developers> - <distributionManagement> - <snapshotRepository> - <id>ossrh</id> - <url>https://oss.sonatype.org/content/repositories/snapshots</url> - </snapshotRepository> - <repository> - <id>ossrh</id> - <url>https://oss.sonatype.org/service/local/staging/deploy/maven2/</url> - </repository> - </distributionManagement> + <distributionManagement> + <snapshotRepository> + <id>ossrh</id> + <url>https://oss.sonatype.org/content/repositories/snapshots</url> + </snapshotRepository> + <repository> + <id>ossrh</id> + <url>https://oss.sonatype.org/service/local/staging/deploy/maven2/</url> + </repository> + </distributionManagement> - <scm> - <connection>scm:hg:https://dev.monetdb.org/hg/MonetDB/</connection> - <developerConnection>scm:hg:ssh://hg@dev.monetdb.org/MonetDB/</developerConnection> - <url>https://dev.monetdb.org/hg/MonetDB/</url> - </scm> + <scm> + <connection>scm:hg:https://dev.monetdb.org/hg/monetdb-java/</connection> + <developerConnection>scm:hg:ssh://hg@dev.monetdb.org/monetdb-java/</developerConnection> + <url>https://dev.monetdb.org/hg/monetdb-java/</url> + </scm> - <properties> - <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> - <jdbc.sources>nl/cwi/monetdb/jdbc/**/*.java</jdbc.sources> - <mcl.sources>nl/cwi/monetdb/mcl/**/*.java</mcl.sources> - </properties> + <properties> + <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> + <jdbc.sources>nl/cwi/monetdb/jdbc/**/*.java</jdbc.sources> + </properties> - <build> - <plugins> - <plugin> - <groupId>org.apache.maven.plugins</groupId> - <artifactId>maven-compiler-plugin</artifactId> - <version>3.2</version> - <configuration> - <includes> - <include>${jdbc.sources}</include> - <include>${mcl.sources}</include> - </includes> - <source>1.7</source> - <target>1.7</target> - </configuration> - </plugin> - <plugin> - <groupId>org.apache.maven.plugins</groupId> - <artifactId>maven-source-plugin</artifactId> - <version>2.2.1</version> - <executions> - <execution> - <id>attach-sources</id> - <goals> - <goal>jar-no-fork</goal> - </goals> - </execution> - </executions> - <configuration> - <includes> - <include>${jdbc.sources}</include> - <include>${mcl.sources}</include> - </includes> - </configuration> - </plugin> - <plugin> - <groupId>org.apache.maven.plugins</groupId> - <artifactId>maven-javadoc-plugin</artifactId> - <version>2.9.1</version> - <executions> - <execution> - <id>attach-javadocs</id> - <goals> - <goal>jar</goal> - </goals> - </execution> - </executions> - <configuration> - <sourceFileIncludes> - <include>${jdbc.sources}</include> - <include>${mcl.sources}</include> - </sourceFileIncludes> - <additionalparam>-Xdoclint:none</additionalparam> - </configuration> - </plugin> - <plugin> - <groupId>org.sonatype.plugins</groupId> - <artifactId>nexus-staging-maven-plugin</artifactId> - <version>1.6.3</version> - <extensions>true</extensions> - <configuration> - <serverId>ossrh</serverId> - <nexusUrl>https://oss.sonatype.org/</nexusUrl> - <autoReleaseAfterClose>false</autoReleaseAfterClose> - </configuration> - </plugin> - <plugin> - <groupId>org.apache.maven.plugins</groupId> - <artifactId>maven-release-plugin</artifactId> - <version>2.5</version> - <configuration> - <autoVersionSubmodules>true</autoVersionSubmodules> - <useReleaseProfile>false</useReleaseProfile> - <releaseProfiles>release</releaseProfiles> - <goals>deploy</goals> - </configuration> - </plugin> - <plugin> - <groupId>org.apache.maven.plugins</groupId> - <artifactId>maven-gpg-plugin</artifactId> - <version>1.5</version> - <executions> - <execution> - <id>sign-artifacts</id> - <phase>verify</phase> - <goals> - <goal>sign</goal> - </goals> - </execution> - </executions> - </plugin> - </plugins> - </build> - + <build> + <plugins> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-compiler-plugin</artifactId> + <version>3.6.1</version> + <configuration> + <includes> + <include>${jdbc.sources}</include> + </includes> + <source>1.7</source> + <target>1.7</target> + </configuration> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-source-plugin</artifactId> + <version>3.0.1</version> + <executions> + <execution> + <id>attach-sources</id> + <goals> + <goal>jar-no-fork</goal> + </goals> + </execution> + </executions> + <configuration> + <includes> + <include>${jdbc.sources}</include> + </includes> + </configuration> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-javadoc-plugin</artifactId> + <version>2.10.4</version> + <executions> + <execution> + <id>attach-javadocs</id> + <goals> + <goal>jar</goal> + </goals> + </execution> + </executions> + <configuration> + <sourceFileIncludes> + <include>${jdbc.sources}</include> + </sourceFileIncludes> + <additionalparam>-Xdoclint:none</additionalparam> + </configuration> + </plugin> + <plugin> + <groupId>org.sonatype.plugins</groupId> + <artifactId>nexus-staging-maven-plugin</artifactId> + <version>1.6.8</version> + <extensions>true</extensions> + <configuration> + <serverId>ossrh</serverId> + <nexusUrl>https://oss.sonatype.org/</nexusUrl> + <autoReleaseAfterClose>true</autoReleaseAfterClose> + </configuration> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-release-plugin</artifactId> + <version>2.5.3</version> + <configuration> + <autoVersionSubmodules>true</autoVersionSubmodules> + <useReleaseProfile>false</useReleaseProfile> + <releaseProfiles>release</releaseProfiles> + <goals>deploy</goals> + </configuration> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-gpg-plugin</artifactId> + <version>1.6</version> + <executions> + <execution> + <id>sign-artifacts</id> + <phase>verify</phase> + <goals> + <goal>sign</goal> + </goals> + </execution> + </executions> + </plugin> + </plugins> + </build> </project>
--- a/src/main/java/nl/cwi/monetdb/client/JMonetDB.java +++ b/src/main/java/nl/cwi/monetdb/client/JMonetDB.java @@ -13,11 +13,7 @@ import nl.cwi.monetdb.merovingian.Sabaot import nl.cwi.monetdb.util.CmdLineOpts; import nl.cwi.monetdb.util.OptionsException; -import java.io.BufferedWriter; -import java.io.OutputStreamWriter; -import java.io.PrintWriter; import java.util.ArrayList; -import java.util.Iterator; import java.util.List; /** @@ -29,9 +25,8 @@ import java.util.List; */ public class JMonetDB { - private static PrintWriter out; - public final static void main(String[] args) throws Exception { + public static void main(String[] args) throws Exception { CmdLineOpts copts = new CmdLineOpts(); // arguments which take exactly one argument @@ -68,25 +63,21 @@ public class JMonetDB { if (copts.getOption("help").isPresent()) { System.out.print( -"Usage java -jar jmonetdb.jar\n" + -" -h host[:port] -p port -P passphrase [-X<opt>] -c cmd ...\n" + -"or using long option equivalents --host --port --passphrase.\n" + -"Arguments may be written directly after the option like -p50000.\n" + -"\n" + -"If no host and port are given, localhost and 50000 are assumed.\n" + -"\n" + -"OPTIONS\n" + -copts.produceHelpMessage() -); + "Usage java -jar jmonetdb.jar\n" + + " -h host[:port] -p port -P passphrase [-X<opt>] -c cmd ...\n" + + "or using long option equivalents --host --port --passphrase.\n" + + "Arguments may be written directly after the option like -p50000.\n" + + "\n" + + "If no host and port are given, localhost and 50000 are assumed.\n" + + "\n" + + "OPTIONS\n" + + copts.produceHelpMessage()); System.exit(0); } - out = new PrintWriter(new BufferedWriter(new OutputStreamWriter(System.out))); - String pass = copts.getOption("passphrase").getArgument(); - // we need the password from the user, fetch it with a pseudo - // password protector + // we need the password from the user, fetch it with a pseudo password protector if (pass == null) { char[] tmp = System.console().readPassword("passphrase: "); if (tmp == null) { @@ -124,11 +115,6 @@ copts.produceHelpMessage() } // FIXME: Control needs to respect Xhash - if (copts.getOption("Xdebug").isPresent()) { - String fname = copts.getOption("Xdebug").getArgument(); - ctl.setDebug(fname); - } - String[] commands = copts.getOption("command").getArguments(); if (commands[0].equals("status")) { List<SabaothDB> sdbs; @@ -139,9 +125,7 @@ copts.produceHelpMessage() for (int i = 1; i < commands.length; i++) sdbs.add(ctl.getStatus(commands[i])); } - Iterator<SabaothDB> it = sdbs.iterator(); - while (it.hasNext()) { - SabaothDB sdb = it.next(); + for (SabaothDB sdb : sdbs) { System.out.println(sdb.getName() + " " + sdb.getURI()); } }
--- a/src/main/java/nl/cwi/monetdb/client/JdbcClient.java +++ b/src/main/java/nl/cwi/monetdb/client/JdbcClient.java @@ -37,6 +37,7 @@ import java.sql.SQLWarning; import java.util.ArrayList; import java.util.LinkedList; import java.util.List; +import java.util.Objects; import java.util.zip.GZIPInputStream; /** @@ -59,7 +60,7 @@ public final class JdbcClient { private static PrintWriter out; private static Exporter exporter; - public final static void main(String[] args) throws Exception { + public static void main(String[] args) throws Exception { CmdLineOpts copts = new CmdLineOpts(); // arguments which take exactly one argument @@ -215,7 +216,7 @@ public final class JdbcClient { // build the hostname String host = copts.getOption("host").getArgument(); - if (host.indexOf(":") == -1) { + if (!host.contains(":")) { host = host + ":" + copts.getOption("port").getArgument(); } @@ -297,7 +298,7 @@ public final class JdbcClient { // request the tables available in the current schema in the database tbl = dbmd.getTables(null, con.getSchema(), null, types); - List<Table> tables = new LinkedList<Table>(); + List<Table> tables = new LinkedList<>(); while (tbl.next()) { tables.add(new Table( tbl.getString(2), // 2 = "TABLE_SCHEM" @@ -305,7 +306,6 @@ public final class JdbcClient { tbl.getString(4))); // 4 = "TABLE_TYPE" } tbl.close(); - tbl = null; if (xmlMode) { exporter = new XMLExporter(out); @@ -325,12 +325,10 @@ public final class JdbcClient { // dump specific table(s) or not? if (copts.getOption("dump").getArgumentCount() > 0) { // yes we do String[] dumpers = copts.getOption("dump").getArguments(); - for (int i = 0; i < tables.size(); i++) { - Table ttmp = tables.get(i); - for (int j = 0; j < dumpers.length; j++) { - if (ttmp.getName().equalsIgnoreCase(dumpers[j].toString()) || - ttmp.getFqname().equalsIgnoreCase(dumpers[j].toString())) - { + for (Table ttmp : tables) { + for (String dumper : dumpers) { + if (ttmp.getName().equalsIgnoreCase(dumper) || + ttmp.getFqname().equalsIgnoreCase(dumper)) { // dump the table doDump(out, ttmp); } @@ -354,7 +352,6 @@ public final class JdbcClient { fk.addDependency(pk); } tbl.close(); - tbl = null; // search for cycles of type a -> (x ->)+ b probably not // the most optimal way, but it works by just scanning @@ -538,18 +535,12 @@ public final class JdbcClient { * @throws IOException if an IO exception occurs * @throws SQLException if a database related error occurs */ - public static void processInteractive( - boolean hasFile, - boolean doEcho, - boolean scolonterm, - String user - ) - throws IOException, SQLException - { + public static void processInteractive(boolean hasFile, boolean doEcho, boolean scolonterm, String user) + throws IOException, SQLException { // an SQL stack keeps track of ( " and ' SQLStack stack = new SQLStack(); // a query part is a line of an SQL query - QueryPart qp = null; + QueryPart qp; String query = "", curLine; boolean wasComplete = true, doProcess, lastac = false; @@ -773,16 +764,11 @@ public final class JdbcClient { * @param showTiming flag to specify if timing information nees to be printed * @throws SQLException if a database related error occurs */ - private static void executeQuery(String query, - Statement stmt, - PrintWriter out, - boolean showTiming) - throws SQLException - { + private static void executeQuery(String query, Statement stmt, PrintWriter out, boolean showTiming) + throws SQLException { // warnings generated during querying SQLWarning warn; - long startTime = (showTiming ? System.currentTimeMillis() : 0); - long finishTime = 0; + long startTime = (showTiming ? System.currentTimeMillis() : 0), finishTime; // execute the query, let the driver decide what type it is int aff = -1; @@ -952,11 +938,7 @@ public final class JdbcClient { * @param scolonterm whether a ';' makes this query part complete * @return a QueryPart object containing the results of this parse */ - private static QueryPart scanQuery( - String query, - SQLStack stack, - boolean scolonterm) - { + private static QueryPart scanQuery(String query, SQLStack stack, boolean scolonterm) { // examine string, char for char boolean wasInString = (stack.peek() == '\''); boolean wasInIdentifier = (stack.peek() == '"');
--- a/src/main/java/nl/cwi/monetdb/jdbc/MonetBlob.java +++ b/src/main/java/nl/cwi/monetdb/jdbc/MonetBlob.java @@ -11,9 +11,11 @@ package nl.cwi.monetdb.jdbc; import java.io.ByteArrayInputStream; import java.io.InputStream; import java.io.OutputStream; +import java.io.Serializable; import java.sql.Blob; import java.sql.SQLException; import java.sql.SQLFeatureNotSupportedException; +import java.util.Arrays; /** * The MonetBlob class implements the {@link java.sql.Blob} interface. Because @@ -25,13 +27,18 @@ import java.sql.SQLFeatureNotSupportedEx * * @author Fabian Groffen */ -public class MonetBlob implements Blob { - private byte[] buf; +public class MonetBlob implements Blob, Serializable, Comparable<MonetBlob> { + + private byte[] buffer; - protected MonetBlob(byte[] data) { - buf = data; + public MonetBlob(byte[] buf) { + this.buffer = buf; } - + + public byte[] getBuffer() { + return buffer; + } + static MonetBlob create(String in) { // unpack the HEX (BLOB) notation to real bytes int len = in.length() / 2; @@ -46,127 +53,100 @@ public class MonetBlob implements Blob { /* internal utility method */ private void checkBufIsNotNull() throws SQLException { - if (buf == null) + if (buffer == null) throw new SQLException("This MonetBlob has been freed", "M1M20"); } //== begin interface Blob - + /** - * This method frees the Blob object and releases the resources that - * it holds. The object is invalid once the free method is called. + * This method frees the Blob object and releases the resources that it holds. The object is invalid once the + * free method is called. * - * After free has been called, any attempt to invoke a method other - * than free will result in a SQLException being thrown. If free is - * called multiple times, the subsequent calls to free are treated - * as a no-op. - * - * @throws SQLException if an error occurs releasing the Blob's - * resources - * @throws SQLFeatureNotSupportedException - if the JDBC driver does - * not support this method + * After free has been called, any attempt to invoke a method other than free will result in a SQLException being + * thrown. If free is called multiple times, the subsequent calls to free are treated as a no-op. */ @Override public void free() throws SQLException { - buf = null; + buffer = null; } - + /** - * Retrieves the BLOB value designated by this Blob instance as a - * stream. + * Retrieves the BLOB value designated by this Blob instance as a stream. * * @return a stream containing the BLOB data * @throws SQLException if there is an error accessing the BLOB value - * @throws SQLFeatureNotSupportedException if the JDBC driver does - * not support this method */ @Override public InputStream getBinaryStream() throws SQLException { checkBufIsNotNull(); - return new ByteArrayInputStream(buf); + return new ByteArrayInputStream(buffer); } /** - * Returns an InputStream object that contains a partial Blob value, - * starting with the byte specified by pos, which is length bytes in - * length. + * Returns an InputStream object that contains a partial Blob value, starting with the byte specified by pos, + * which is length bytes in length. * - * @param pos the offset to the first byte of the partial value to - * be retrieved. The first byte in the Blob is at position 1 - * @param length the length in bytes of the partial value to be - * retrieved - * @return InputStream through which the partial Blob value can be - * read. - * @throws SQLException if pos is less than 1 or if pos is - * greater than the number of bytes in the Blob or if pos + - * length is greater than the number of bytes in the Blob - * @throws SQLFeatureNotSupportedException if the JDBC driver does - * not support this method + * @param pos the offset to the first byte of the partial value to be retrieved. The first byte in the Blob is at + * position 1 + * @param length the length in bytes of the partial value to be retrieved + * @return InputStream through which the partial Blob value can be read. + * @throws SQLException if pos is less than 1 or if pos is greater than the number of bytes in the Blob or if pos + + * length is greater than the number of bytes in the Blob */ @Override - public InputStream getBinaryStream(long pos, long length) - throws SQLException - { + public InputStream getBinaryStream(long pos, long length) throws SQLException { checkBufIsNotNull(); if (pos < 1) throw new SQLException("pos is less than 1", "M1M05"); - if (pos - 1 > buf.length) + if (pos - 1 > buffer.length) throw new SQLException("pos is greater than the number of bytes in the Blob", "M1M05"); - if (pos - 1 + length > buf.length) + if (pos - 1 + length > buffer.length) throw new SQLException("pos + length is greater than the number of bytes in the Blob", "M1M05"); - return new ByteArrayInputStream(buf, (int)(pos - 1), (int)length); + return new ByteArrayInputStream(buffer, (int)(pos - 1), (int)length); } /** - * Retrieves all or part of the BLOB value that this Blob object - * represents, as an array of bytes. This byte array contains up to - * length consecutive bytes starting at position pos. + * Retrieves all or part of the BLOB value that this Blob object represents, as an array of bytes. This byte array + * contains up to length consecutive bytes starting at position pos. * - * @param pos the ordinal position of the first byte in the BLOB - * value to be extracted; the first byte is at position 1. + * @param pos the ordinal position of the first byte in the BLOB value to be extracted; the first byte is at + * position 1. * @param length the number of consecutive bytes to be copied - * @return a byte array containing up to length consecutive bytes - * from the BLOB value designated by this Blob object, - * starting with the byte at position pos. - * @throws SQLException if there is an error accessing the - * BLOB value + * @return a byte array containing up to length consecutive bytes from the BLOB value designated by this Blob + * object, starting with the byte at position pos. + * @throws SQLException if there is an error accessing the BLOB value */ @Override public byte[] getBytes(long pos, int length) throws SQLException { checkBufIsNotNull(); try { - return java.util.Arrays.copyOfRange(buf, (int) pos - 1, (int) pos - 1 + length); + return java.util.Arrays.copyOfRange(buffer, (int) pos - 1, (int) pos - 1 + length); } catch (IndexOutOfBoundsException e) { throw new SQLException(e.getMessage(), "M0M10"); } } /** - * Returns the number of bytes in the BLOB value designated by this - * Blob object. + * Returns the number of bytes in the BLOB value designated by this Blob object. * * @return length of the BLOB in bytes - * @throws SQLException if there is an error accessing the length - * of the BLOB value + * @throws SQLException if there is an error accessing the length of the BLOB value */ @Override public long length() throws SQLException { checkBufIsNotNull(); - return (long)buf.length; + return (long)buffer.length; } /** - * Retrieves the byte position in the BLOB value designated by this - * Blob object at which pattern begins. The search begins at - * position start. + * Retrieves the byte position in the BLOB value designated by this Blob object at which pattern begins. The search + * begins at position start. * - * @param pattern the Blob object designating the BLOB value for - * which to search - * @param start the position in the BLOB value at which to begin - * searching; the first position is 1 + * @param pattern the Blob object designating the BLOB value for which to search + * @param start the position in the BLOB value at which to begin searching; the first position is 1 * @return the position at which the pattern begins, else -1 - * @throws SQLException if there is an error accessing the - * BLOB value + * @throws SQLException if there is an error accessing the BLOB value */ @Override public long position(Blob pattern, long start) throws SQLException { @@ -174,25 +154,22 @@ public class MonetBlob implements Blob { } /** - * Retrieves the byte position at which the specified byte array - * pattern begins within the BLOB value that this Blob object - * represents. The search for pattern begins at position start. + * Retrieves the byte position at which the specified byte array pattern begins within the BLOB value that this + * Blob object represents. The search for pattern begins at position start. * * @param pattern the byte array for which to search - * @param start the position at which to begin searching; - * the first position is 1 + * @param start the position at which to begin searching; the first position is 1 * @return the position at which the pattern appears, else -1 - * @throws SQLException if there is an error accessing the - * BLOB value + * @throws SQLException if there is an error accessing the BLOB value */ @Override public long position(byte[] pattern, long start) throws SQLException { checkBufIsNotNull(); try { - for (int i = (int)(start - 1); i < buf.length - pattern.length; i++) { + for (int i = (int)(start - 1); i < buffer.length - pattern.length; i++) { int j; for (j = 0; j < pattern.length; j++) { - if (buf[i + j] != pattern[j]) + if (buffer[i + j] != pattern[j]) break; } if (j == pattern.length) @@ -213,14 +190,10 @@ public class MonetBlob implements Blob { * the length of the Blob value will be increased to accomodate the * extra bytes. * - * @param pos the position in the BLOB value at which to start - * writing; the first position is 1 - * @return a java.io.OutputStream object to which data can be - * written - * @throws SQLException if there is an error accessing the BLOB - * value or if pos is less than 1 - * @throws SQLFeatureNotSupportedException if the JDBC driver does - * not support this method + * @param pos the position in the BLOB value at which to start writing; the first position is 1 + * @return a java.io.OutputStream object to which data can be written + * @throws SQLException if there is an error accessing the BLOB value or if pos is less than 1 + * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method */ @Override public OutputStream setBinaryStream(long pos) throws SQLException { @@ -232,13 +205,10 @@ public class MonetBlob implements Blob { * object represents, starting at position pos, and returns the * number of bytes written. * - * @param pos the position in the BLOB object at which to start - * writing - * @param bytes the array of bytes to be written to the BLOB value - * that this Blob object represents + * @param pos the position in the BLOB object at which to start writing + * @param bytes the array of bytes to be written to the BLOB value that this Blob object represents * @return the number of bytes written - * @throws SQLException if there is an error accessing the - * BLOB value + * @throws SQLException if there is an error accessing the BLOB value */ @Override public int setBytes(long pos, byte[] bytes) throws SQLException { @@ -251,27 +221,19 @@ public class MonetBlob implements Blob { * written. Writing starts at position pos in the BLOB value; len * bytes from the given byte array are written. * - * @param pos the position in the BLOB object at which to start - * writing - * @param bytes the array of bytes to be written to this BLOB - * object - * @param offset the offset into the array bytes at which to start - * reading the bytes to be set - * @param len the number of bytes to be written to the BLOB value - * from the array of bytes bytes + * @param pos the position in the BLOB object at which to start writing + * @param bytes the array of bytes to be written to this BLOB object + * @param offset the offset into the array bytes at which to start reading the bytes to be set + * @param len the number of bytes to be written to the BLOB value from the array of bytes bytes * @return the number of bytes written - * @throws SQLException if there is an error accessing the - * BLOB value + * @throws SQLException if there is an error accessing the BLOB value */ @Override - public int setBytes(long pos, byte[] bytes, int offset, int len) - throws SQLException - { + public int setBytes(long pos, byte[] bytes, int offset, int len) throws SQLException { checkBufIsNotNull(); try { /* transactions? what are you talking about? */ - for (int i = (int)pos; i < len; i++) - buf[i] = bytes[offset - 1 + i]; + System.arraycopy(bytes, offset - 1 + (int) pos, buffer, (int) pos, len - (int) pos); } catch (IndexOutOfBoundsException e) { throw new SQLException(e.getMessage(), "M0M10"); } @@ -279,22 +241,48 @@ public class MonetBlob implements Blob { } /** - * Truncates the BLOB value that this Blob object represents to be - * len bytes in length. + * Truncates the BLOB value that this Blob object represents to be len bytes in length. * - * @param len the length, in bytes, to which the BLOB value - * should be truncated - * @throws SQLException if there is an error accessing the - * BLOB value + * @param len the length, in bytes, to which the BLOB value should be truncated + * @throws SQLException if there is an error accessing the BLOB value */ @Override public void truncate(long len) throws SQLException { checkBufIsNotNull(); - if (buf.length > len) { + if (buffer.length > len) { byte[] newbuf = new byte[(int)len]; - for (int i = 0; i < len; i++) - newbuf[i] = buf[i]; - buf = newbuf; + System.arraycopy(buffer, 0, newbuf, 0, (int) len); + buffer = newbuf; } } + + /** + * Overriding the equals method for the byte array. + */ + @Override + public boolean equals(Object obj) { + return obj instanceof MonetBlob && Arrays.equals(this.buffer, ((MonetBlob) obj).buffer); + } + + /** + * Overriding the hashCode method for the byte array. + */ + @Override + public int hashCode() { return Arrays.hashCode(this.buffer); } + + /** + * Overriding the toString method for the byte array. + */ + @Override + public String toString() { return Arrays.toString(this.buffer); } + + @Override + public int compareTo(MonetBlob o) { + byte[] first = this.buffer, second = o.buffer; + int len = Math.min(first.length, second.length), res = 0; + for(int i = 0; i < len ; i++) { + res = res + first[i] - second[i]; + } + return res; + } }
--- a/src/main/java/nl/cwi/monetdb/jdbc/MonetClob.java +++ b/src/main/java/nl/cwi/monetdb/jdbc/MonetClob.java @@ -8,11 +8,7 @@ package nl.cwi.monetdb.jdbc; -import java.io.InputStream; -import java.io.OutputStream; -import java.io.Reader; -import java.io.StringReader; -import java.io.Writer; +import java.io.*; import java.sql.Clob; import java.sql.SQLException; import java.sql.SQLFeatureNotSupportedException; @@ -28,77 +24,78 @@ import java.sql.SQLFeatureNotSupportedEx * * @author Fabian Groffen */ -public class MonetClob implements Clob { +public class MonetClob implements Clob, Serializable, Comparable<MonetClob> { + + private final StringBuilder buffer; - private StringBuilder buf; + public MonetClob(String in) { + if(in != null) + buffer = new StringBuilder(in); + else + buffer = null; + } - protected MonetClob(String in) { - buf = new StringBuilder(in); + public MonetClob(char[] toParse, int startPosition, int count) { + buffer = new StringBuilder(new String(toParse, startPosition, count)); } /* internal utility method */ private void checkBufIsNotNull() throws SQLException { - if (buf == null) + if (buffer == null) throw new SQLException("This MonetClob has been freed", "M1M20"); } //== begin interface Clob /** - * This method frees the Clob object and releases the resources the - * resources that it holds. The object is invalid once the free - * method is called. + * This method frees the Clob object and releases the resources the resources that it holds. The object is invalid + * once the free method is called. * - * After free has been called, any attempt to invoke a method other - * than free will result in a SQLException being thrown. If free is - * called multiple times, the subsequent calls to free are treated - * as a no-op. + * After free has been called, any attempt to invoke a method other than free will result in a SQLException being + * thrown. If free is called multiple times, the subsequent calls to free are treated as a no-op. */ @Override public void free() { - buf = null; + if (buffer != null) + buffer.setLength(0); } /** - * Retrieves the CLOB value designated by this Clob object as an - * ascii stream. + * Retrieves the CLOB value designated by this Clob object as an ascii stream. * * @return a java.io.InputStream object containing the CLOB data - * @throws SQLFeatureNotSupportedException this JDBC driver does - * not support this method + * @throws SQLException if there is an error accessing the CLOB value */ @Override public InputStream getAsciiStream() throws SQLException { - throw new SQLFeatureNotSupportedException("Method getAsciiStream() not supported", "0A000"); + checkBufIsNotNull(); + if (buffer.length() == 0) + throw new SQLException("This Clob has been freed", "M1M20"); + return new ByteArrayInputStream(buffer.toString().getBytes()); } /** - * Retrieves the CLOB value designated by this Clob object as a - * java.io.Reader object (or as a stream of characters). + * Retrieves the CLOB value designated by this Clob object as a java.io.Reader object + * (or as a stream of characters). * * @return a java.io.Reader object containing the CLOB data - * @throws SQLFeatureNotSupportedException this JDBC driver does - * not support this method + * @throws SQLException if there is an error accessing the CLOB value */ @Override public Reader getCharacterStream() throws SQLException { checkBufIsNotNull(); - return new StringReader(buf.toString()); + return new StringReader(buffer.toString()); } /** - * Returns a Reader object that contains a partial Clob value, - * starting with the character specified by pos, which is length - * characters in length. + * Returns a Reader object that contains a partial Clob value, starting with the character specified by pos, which + * is length characters in length. * - * @param pos the offset to the first character of the partial value - * to be retrieved. The first character in the Clob is at - * position 1. - * @param length the length in characters of the partial value to be - * retrieved. + * @param pos the offset to the first character of the partial value to be retrieved. The first character in the + * Clob is at position 1. + * @param length the length in characters of the partial value to be retrieved. * @return Reader through which the partial Clob value can be read. - * @throws SQLFeatureNotSupportedException this JDBC driver does - * not support this method + * @throws SQLException if there is an error accessing the CLOB value */ @Override public Reader getCharacterStream(long pos, long length) throws SQLException { @@ -107,77 +104,64 @@ public class MonetClob implements Clob { } /** - * Retrieves a copy of the specified substring in the CLOB value - * designated by this Clob object. The substring begins at - * position pos and has up to length consecutive characters. + * Retrieves a copy of the specified substring in the CLOB value designated by this Clob object. The substring + * begins at position pos and has up to length consecutive characters. * - * @param pos the first character of the substring to be - * extracted. The first character is at position 1. + * @param pos the first character of the substring to be extracted. The first character is at position 1. * @param length the number of consecutive characters to be copied - * @return a String that is the specified substring in the - * CLOB value designated by this Clob object - * @throws SQLException if there is an error accessing the - * CLOB value + * @return a String that is the specified substring in the CLOB value designated by this Clob object + * @throws SQLException if there is an error accessing the CLOB value */ @Override public String getSubString(long pos, int length) throws SQLException { checkBufIsNotNull(); try { - return buf.substring((int)(pos - 1), (int)(pos - 1 + length)); + return buffer.substring((int)(pos - 1), (int)(pos - 1 + length)); } catch (IndexOutOfBoundsException e) { throw new SQLException(e.getMessage()); } } /** - * Retrieves the number of characters in the CLOB value designated - * by this Clob object. + * Retrieves the number of characters in the CLOB value designated by this Clob object. * * @return length of the CLOB in characters - * @throws SQLException if there is an error accessing the length - * of the CLOB value + * @throws SQLException if there is an error accessing the length of the CLOB value */ @Override public long length() throws SQLException { checkBufIsNotNull(); - return (long)buf.length(); + return (long)buffer.length(); } /** - * Retrieves the character position at which the specified Clob - * object searchstr appears in this Clob object. The search - * begins at position start. + * Retrieves the character position at which the specified Clob object searchstr appears in this Clob object. + * The search begins at position start. * * @param searchstr the Clob object for which to search - * @param start the position at which to begin searching; - * the first position is 1 - * @return the position at which the Clob object appears or - * -1 if it is not present; the first position is 1 - * @throws SQLException if there is an error accessing the - * CLOB value + * @param start the position at which to begin searching; the first position is 1 + * @return the position at which the Clob object appears or -1 if it is not present; the first position is 1 + * @throws SQLException if there is an error accessing the CLOB value */ @Override public long position(Clob searchstr, long start) throws SQLException { + checkBufIsNotNull(); return position(searchstr.getSubString(1L, (int)(searchstr.length())), start); } /** - * Retrieves the character position at which the specified - * substring searchstr appears in the SQL CLOB value represented - * by this Clob object. The search begins at position start. + * Retrieves the character position at which the specified substring searchstr appears in the SQL CLOB value + * represented by this Clob object. The search begins at position start. * * @param searchstr the substring for which to search - * @param start the position at which to begin searching; - * the first position is 1 - * @return the position at which the substring appears or - * -1 if it is not present; the first position is 1 - * @throws SQLException if there is an error accessing the - * CLOB value + * @param start the position at which to begin searching; the first position is 1 + * @return the position at which the substring appears or -1 if it is not present; the first position is 1 + * @throws SQLException if there is an error accessing the CLOB value */ @Override public long position(String searchstr, long start) throws SQLException { checkBufIsNotNull(); - return (long)(buf.indexOf(searchstr, (int)(start - 1))); + return (long)(buffer.indexOf(searchstr, (int)(start - 1))); } @Override @@ -191,16 +175,12 @@ public class MonetClob implements Clob { } /** - * Writes the given Java String to the CLOB value that this - * Clob object designates at the position pos. + * Writes the given Java String to the CLOB value that this Clob object designates at the position pos. * - * @param pos the position at which to start writing to the - * CLOB value that this Clob object represents - * @param str the string to be written to the CLOB value that - * this Clob designates + * @param pos the position at which to start writing to the CLOB value that this Clob object represents + * @param str the string to be written to the CLOB value that this Clob designates * @return the number of characters written - * @throws SQLException if there is an error accessing the - * CLOB value + * @throws SQLException if there is an error accessing the CLOB value */ @Override public int setString(long pos, String str) throws SQLException { @@ -208,30 +188,22 @@ public class MonetClob implements Clob { } /** - * Writes len characters of str, starting at character offset, - * to the CLOB value that this Clob represents. + * Writes len characters of str, starting at character offset, to the CLOB value that this Clob represents. * - * @param pos the position at which to start writing to this - * CLOB object - * @param str the string to be written to the CLOB value that - * this Clob object represents - * @param offset the offset into str to start reading the - * characters to be written + * @param pos the position at which to start writing to this CLOB object + * @param str the string to be written to the CLOB value that this Clob object represents + * @param offset the offset into str to start reading the characters to be written * @param len the number of characters to be written * @return the number of characters written - * @throws SQLException if there is an error accessing the - * CLOB value + * @throws SQLException if there is an error accessing the CLOB value */ @Override - public int setString(long pos, String str, int offset, int len) - throws SQLException - { + public int setString(long pos, String str, int offset, int len) throws SQLException { checkBufIsNotNull(); - - int buflen = buf.length(); + int buflen = buffer.length(); int retlen = Math.min(buflen, (int)(pos - 1 + len)); if (retlen > 0) { - buf.replace((int)(pos - 1), (int)(pos + retlen), str.substring(offset - 1, (offset + len))); + buffer.replace((int)(pos - 1), (int)(pos + retlen), str.substring(offset - 1, (offset + len))); return retlen; } else { return 0; @@ -239,29 +211,53 @@ public class MonetClob implements Clob { } /** - * Truncates the CLOB value that this Clob designates to - * have a length of len characters. + * Truncates the CLOB value that this Clob designates to have a length of len characters. * - * @param len the length, in bytes, to which the CLOB value - * should be truncated - * @throws SQLException if there is an error accessing the - * CLOB value + * @param len the length, in bytes, to which the CLOB value should be truncated + * @throws SQLException if there is an error accessing the CLOB value */ @Override public void truncate(long len) throws SQLException { checkBufIsNotNull(); - // this command is a no-op + buffer.setLength((int) len); } /** - * Returns the String behind this Clob. This is a MonetClob - * extension that does not violate nor is described in the Clob - * interface. + * Returns the String behind this Clob. This is a MonetClob extension that does not violate nor is described in + * the Clob interface. * * @return the String this Clob wraps. */ @Override public String toString() { - return (buf == null) ? "null" : buf.toString(); + if (buffer == null || buffer.length() == 0) + return "null"; + return buffer.toString(); + } + + /** + * Overriding the equals method for the byte array. + */ + @Override + public boolean equals(Object obj) { + return obj instanceof MonetClob && this.toString().equals(obj.toString()); + } + + /** + * Overriding the hashCode method for the byte array. + */ + @Override + public int hashCode() { + if (buffer == null || buffer.length() == 0) + return 0; + return this.buffer.toString().hashCode(); + } + + /** + * Adding the compare to method. + */ + @Override + public int compareTo(MonetClob o) { + return this.toString().compareTo(o.toString()); } }
--- a/src/main/java/nl/cwi/monetdb/jdbc/MonetConnection.java +++ b/src/main/java/nl/cwi/monetdb/jdbc/MonetConnection.java @@ -1,50 +1,21 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. - * - * Copyright 1997 - July 2008 CWI, August 2008 - 2017 MonetDB B.V. - */ - package nl.cwi.monetdb.jdbc; -import java.io.File; +import nl.cwi.monetdb.mcl.connection.ControlCommands; +import nl.cwi.monetdb.mcl.connection.IMonetDBLanguage; +import nl.cwi.monetdb.mcl.connection.MCLException; +import nl.cwi.monetdb.mcl.connection.SenderThread; +import nl.cwi.monetdb.mcl.protocol.AbstractProtocol; +import nl.cwi.monetdb.mcl.protocol.ProtocolException; +import nl.cwi.monetdb.mcl.protocol.ServerResponses; +import nl.cwi.monetdb.mcl.protocol.StarterHeaders; +import nl.cwi.monetdb.mcl.responses.*; + import java.io.IOException; import java.net.SocketException; import java.net.SocketTimeoutException; -import java.sql.CallableStatement; -import java.sql.Connection; -import java.sql.DatabaseMetaData; -import java.sql.PreparedStatement; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.sql.SQLFeatureNotSupportedException; -import java.sql.SQLNonTransientConnectionException; -import java.sql.SQLWarning; -import java.sql.Savepoint; -import java.sql.Statement; -import java.util.ArrayList; -import java.util.Calendar; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.Properties; -import java.util.WeakHashMap; +import java.sql.*; +import java.util.*; import java.util.concurrent.Executor; -import java.util.concurrent.locks.Condition; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReentrantLock; - -import nl.cwi.monetdb.jdbc.types.INET; -import nl.cwi.monetdb.jdbc.types.URL; -import nl.cwi.monetdb.mcl.MCLException; -import nl.cwi.monetdb.mcl.io.BufferedMCLReader; -import nl.cwi.monetdb.mcl.io.BufferedMCLWriter; -import nl.cwi.monetdb.mcl.net.MapiSocket; -import nl.cwi.monetdb.mcl.parser.HeaderLineParser; -import nl.cwi.monetdb.mcl.parser.MCLParseException; -import nl.cwi.monetdb.mcl.parser.StartOfHeaderParser; /** * A {@link Connection} suitable for the MonetDB database. @@ -68,287 +39,218 @@ import nl.cwi.monetdb.mcl.parser.StartOf * The current state of this connection is that it nearly implements the * whole Connection interface. * - * @author Fabian Groffen - * @author Martin van Dinther + * @author Fabian Groffen, Martin van Dinther, Pedro Ferreira * @version 1.3 */ -public class MonetConnection - extends MonetWrapper - implements Connection, AutoCloseable -{ - /** the successful processed input properties */ - private final Properties conn_props = new Properties(); +public abstract class MonetConnection extends MonetWrapper implements Connection, AutoCloseable { + + /** The sequence counter */ + private static int SeqCounter = 0; - /** The hostname to connect to */ - private final String hostname; - /** The port to connect on the host to */ - private int port = 0; - /** The database to use (currently not used) */ - private final String database; - /** The username to use when authenticating */ - private final String username; - /** The password to use when authenticating */ - private final String password; + /** + * Gets the current sequence counter. + * + * @return The current sequence counter + */ + public static int getSeqCounter() { + return SeqCounter; + } - /** A connection to mserver5 using a TCP socket */ - private final MapiSocket server; - /** The Reader from the server */ - private final BufferedMCLReader in; - /** The Writer to the server */ - private final BufferedMCLWriter out; - - /** A StartOfHeaderParser declared for reuse. */ - private StartOfHeaderParser sohp = new StartOfHeaderParser(); - + /** The successful processed input properties */ + protected final Properties conn_props; + /** The language to connect with */ + protected IMonetDBLanguage language; + /** Authentication hash method */ + protected final String hash; + /** An optional thread that is used for sending large queries */ + private SenderThread senderThread; /** Whether this Connection is closed (and cannot be used anymore) */ private boolean closed; - /** Whether this Connection is in autocommit mode */ private boolean autoCommit = true; - /** The stack of warnings for this Connection object */ - private SQLWarning warnings = null; - /** The Connection specific mapping of user defined types to Java - * types */ + private SQLWarning warnings; + /** The Connection specific mapping of user defined types to Java types */ private Map<String,Class<?>> typeMap = new HashMap<String,Class<?>>() { - private static final long serialVersionUID = 1L; - { - put("inet", INET.class); - put("url", URL.class); + private static final long serialVersionUID = 1L; { + put("inet", MonetINET.class); + put("url", MonetURL.class); } }; - // See javadoc for documentation about WeakHashMap if you don't know what - // it does !!!NOW!!! (only when you deal with it of course) + // See javadoc for documentation about WeakHashMap if you don't know what it does !!!NOW!!! + // (only when you deal with it of course) /** A Map containing all (active) Statements created from this Connection */ - private Map<Statement,?> statements = new WeakHashMap<Statement, Object>(); + private Map<Statement,?> statements = new WeakHashMap<>(); + /** The number of results we receive from the server at once */ + private int curReplySize = -1; // the server by default uses -1 (all) + /** Whether or not BLOB is mapped to LONGVARBINARY within the driver */ + private final boolean blobIsBinary; + /** Whether or not CLOB is mapped to LONGVARCHAR within the driver */ + private final boolean clobIsLongChar; + /** The underlying proticol provided by the connection (MAPI or embedded) */ + protected AbstractProtocol protocol; + /** Tells if the connection is embedded or not */ + private final boolean isEmbedded; - /** The number of results we receive from the server at once */ - private int curReplySize = -1; // the server by default uses -1 (all) - - /** A template to apply to each query (like pre and post fixes), filled in constructor */ - final String[] queryTempl = new String[3]; // pre, post, sep + /** + * Constructor of a Connection for MonetDB. At this moment the current implementation limits itself to storing the + * given host, database, username and password for later use by the createStatement() call. This constructor is + * only accessible to classes from the jdbc package. + */ + public MonetConnection(Properties props, String hash, IMonetDBLanguage language, boolean blobIsBinary, + boolean clobIsLongChar) { + this.conn_props = props; + this.hash = hash; + this.language = language; + this.blobIsBinary = blobIsBinary; + this.clobIsLongChar = clobIsLongChar; + //"instance of" should be cleanner, but this is faster. + this.isEmbedded = props.getProperty("embedded", "false").equals("true"); + } - /** A template to apply to each command (like pre and post fixes), filled in constructor */ - final String[] commandTempl = new String[3]; // pre, post, sep + /** + * Checks if the conection is embedded or not + * + * @return If the connection is embedded + */ + public boolean isEmbedded() { + return isEmbedded; + } + + /** + * Gets the connection's language data. + * + * @return The connection's language data + */ + public IMonetDBLanguage getLanguage() { + return language; + } - /** the SQL language */ - final static int LANG_SQL = 0; - /** the MAL language (officially *NOT* supported) */ - final static int LANG_MAL = 3; - /** an unknown language */ - final static int LANG_UNKNOWN = -1; - /** The language which is used */ - final int lang; + /** + * Gets the connection's protocol. + * + * @return The connection's protocol + */ + public AbstractProtocol getProtocol() { + return this.protocol; + } - /** Whether or not BLOB is mapped to BINARY within the driver */ - private final boolean blobIsBinary; + /** + * Connects to the server, authenticating the user. + * + * @param user The user name to authenticate + * @param pass The user's password + * @return A List with informational (warning) messages. If this list is empty; then there are no warnings. + * @throws IOException if an I/O error occurs when creating the socket + * @throws ProtocolException if bogus data is received + * @throws MCLException if an MCL related error occurs + */ + public abstract List<String> connect(String user, String pass) throws IOException, ProtocolException, MCLException; + + /** + * Gets the underlying connection block size length. + * + * @return The block size length + */ + public abstract int getBlockSize(); /** - * Constructor of a Connection for MonetDB. At this moment the - * current implementation limits itself to storing the given host, - * database, username and password for later use by the - * createStatement() call. This constructor is only accessible to - * classes from the jdbc package. + * Gets the underlying connection default fetch size for DataBlock responses. * - * @param props a Property hashtable holding the properties needed for connecting - * @throws SQLException if a database error occurs - * @throws IllegalArgumentException is one of the arguments is null or empty + * @return The default fetch size */ - MonetConnection(Properties props) - throws SQLException, IllegalArgumentException - { - // get supported property values from the props argument. - // When a value is found add it to the internal conn_props list for use by getClientInfo(). - this.hostname = props.getProperty("host"); - if (this.hostname != null) - conn_props.setProperty("host", this.hostname); + public abstract int getDefFetchsize(); + + /** + * Gets the initial value for the StringBuilder size. + * + * @return The initial value for the StringBuilder size + */ + public abstract int initialStringBuilderSize(); - String port_prop = props.getProperty("port"); - if (port_prop != null) { - try { - this.port = Integer.parseInt(port_prop); - } catch (NumberFormatException e) { - addWarning("Unable to parse port number from: " + port_prop, "M1M05"); - } - conn_props.setProperty("port", Integer.toString(this.port)); - } - - this.database = props.getProperty("database"); - if (this.database != null) - conn_props.setProperty("database", this.database); - - this.username = props.getProperty("user"); - if (this.username != null) - conn_props.setProperty("user", this.username); + /** + * Gets the underlying connection socket timeout. + * + * @return The underlying connection socket timeout + */ + public abstract int getSoTimeout() throws SocketException; - this.password = props.getProperty("password"); - if (this.password != null) - conn_props.setProperty("password", this.password); - - String language = props.getProperty("language"); - if (language != null) - conn_props.setProperty("language", language); - - boolean debug = false; - String debug_prop = props.getProperty("debug"); - if (debug_prop != null) { - debug = Boolean.parseBoolean(debug_prop); - conn_props.setProperty("debug", Boolean.toString(debug)); - } - - String hash = props.getProperty("hash"); - if (hash != null) - conn_props.setProperty("hash", hash); + /** + * Sets the underlying connection socket timeout. + * + * @param timeout The specified timeout, in milliseconds. A timeout of zero is interpreted as an infinite timeout + */ + public abstract void setSoTimeout(int timeout) throws SocketException; - String blobIsBinary_prop = props.getProperty("treat_blob_as_binary"); - if (blobIsBinary_prop != null) { - blobIsBinary = Boolean.parseBoolean(blobIsBinary_prop); - conn_props.setProperty("treat_blob_as_binary", Boolean.toString(blobIsBinary)); - } else { - blobIsBinary = false; - } + /** + * Closes the underlying connection implementation. + * + * @throws IOException if an I/O error occurs while closing the connection + */ + public abstract void closeUnderlyingConnection() throws IOException; - int sockTimeout = 0; - String so_timeout_prop = props.getProperty("so_timeout"); - if (so_timeout_prop != null) { - try { - sockTimeout = Integer.parseInt(so_timeout_prop); - if (sockTimeout < 0) { - addWarning("Negative socket timeout not allowed. Value ignored", "M1M05"); - sockTimeout = 0; - } - } catch (NumberFormatException e) { - addWarning("Unable to parse socket timeout number from: " + so_timeout_prop, "M1M05"); - } - conn_props.setProperty("so_timeout", Integer.toString(sockTimeout)); - } + /** + * Gets the underlying connection JDBC String URL. + * + * @return The underlying connection JDBC String URL + */ + public abstract String getJDBCURL(); - // check mandatory input arguments - if (hostname == null || hostname.isEmpty()) - throw new IllegalArgumentException("Missing or empty host name"); - if (port <= 0) - throw new IllegalArgumentException("Invalid port number. It should not be " + (port < 0 ? "negative" : "0")); - if (username == null || username.isEmpty()) - throw new IllegalArgumentException("Missing or empty user name"); - if (password == null || password.isEmpty()) - throw new IllegalArgumentException("Missing or empty password"); - if (language == null || language.isEmpty()) { - // fallback to default language: sql - language = "sql"; - addWarning("No language specified, defaulting to 'sql'", "M1M05"); - } + /** + * Sends a control command to the server. + * + * @param commandID the command identifier according to {@link ControlCommands} listing + * @param data The integer to send according to the control command + * @throws SQLException if an IO exception or a database error occurs + */ + public abstract void sendControlCommand(int commandID, int data) throws SQLException; - server = new MapiSocket(); - if (hash != null) - server.setHash(hash); - if (database != null) - server.setDatabase(database); - server.setLanguage(language); - - // we're debugging here... uhm, should be off in real life - if (debug) { + /** + * Releases this Connection object's database and JDBC resources immediately instead of waiting for them to be + * automatically released. All Statements created from this Connection will be closed when this method is called. + * + * Calling the method close on a Connection object that is already closed is a no-op. + */ + @Override + public void close() { + for (Statement st : statements.keySet()) { try { - String fname = props.getProperty("logfile", "monet_" + - System.currentTimeMillis() + ".log"); - File f = new File(fname); - int ext = fname.lastIndexOf('.'); - if (ext < 0) - ext = fname.length(); - String pre = fname.substring(0, ext); - String suf = fname.substring(ext); - - for (int i = 1; f.exists(); i++) { - f = new File(pre + "-" + i + suf); - } - - server.debug(f.getAbsolutePath()); - } catch (IOException ex) { - throw new SQLNonTransientConnectionException("Opening logfile failed: " + ex.getMessage(), "08M01"); + st.close(); + } catch (SQLException e) { + // better luck next time! } } - + // close the socket or the embedded server try { - List<String> warnings = server.connect(hostname, port, username, password); - for (String warning : warnings) { - addWarning(warning, "01M02"); - } - - // apply NetworkTimeout value from legacy (pre 4.1) driver - // so_timeout calls - server.setSoTimeout(sockTimeout); - - in = server.getReader(); - out = server.getWriter(); - - String error = in.waitForPrompt(); - if (error != null) - throw new SQLNonTransientConnectionException((error.length() > 6) ? error.substring(6) : error, "08001"); + this.closeUnderlyingConnection(); } catch (IOException e) { - throw new SQLNonTransientConnectionException("Unable to connect (" + hostname + ":" + port + "): " + e.getMessage(), "08006"); - } catch (MCLParseException e) { - throw new SQLNonTransientConnectionException(e.getMessage(), "08001"); - } catch (MCLException e) { - String[] connex = e.getMessage().split("\n"); - SQLException sqle = new SQLNonTransientConnectionException(connex[0], "08001", e); - for (int i = 1; i < connex.length; i++) { - sqle.setNextException(new SQLNonTransientConnectionException(connex[1], "08001")); - } - throw sqle; + // ignore it } - - // we seem to have managed to log in, let's store the - // language used and language specific query templates - if ("sql".equals(language)) { - lang = LANG_SQL; + // close active SendThread if any + if (senderThread != null) { + senderThread.shutdown(); + senderThread = null; + } + // report ourselves as closed + closed = true; + } - queryTempl[0] = "s"; // pre - queryTempl[1] = "\n;"; // post - queryTempl[2] = "\n;\n"; // separator - - commandTempl[0] = "X"; // pre - commandTempl[1] = null; // post - commandTempl[2] = "\nX"; // separator - } else if ("mal".equals(language)) { - lang = LANG_MAL; - - queryTempl[0] = null; - queryTempl[1] = ";\n"; - queryTempl[2] = ";\n"; - - commandTempl[0] = null; // pre - commandTempl[1] = null; // post - commandTempl[2] = null; // separator - } else { - lang = LANG_UNKNOWN; - } - - // the following initialisers are only valid when the language is SQL... - if (lang == LANG_SQL) { - // enable auto commit - setAutoCommit(true); - - // set our time zone on the server - Calendar cal = Calendar.getInstance(); - int offset = cal.get(Calendar.ZONE_OFFSET) + cal.get(Calendar.DST_OFFSET); - offset /= (60 * 1000); // milliseconds to minutes - String tz = offset < 0 ? "-" : "+"; - tz += (Math.abs(offset) / 60 < 10 ? "0" : "") + (Math.abs(offset) / 60) + ":"; - offset -= (offset / 60) * 60; - tz += (offset < 10 ? "0" : "") + offset; - sendIndependentCommand("SET TIME ZONE INTERVAL '" + tz + "' HOUR TO MINUTE"); - } - - // we're absolutely not closed, since we're brand new - closed = false; + /** + * Destructor called by garbage collector before destroying this object tries to disconnect the MonetDB connection + * if it has not been disconnected already. + */ + @Override + protected void finalize() throws Throwable { + this.close(); + super.finalize(); } //== methods of interface Connection /** - * Clears all warnings reported for this Connection object. After a - * call to this method, the method getWarnings returns null until a - * new warning is reported for this Connection object. + * Clears all warnings reported for this Connection object. After a call to this method, the method getWarnings + * returns null until a new warning is reported for this Connection object. */ @Override public void clearWarnings() { @@ -356,51 +258,16 @@ public class MonetConnection } /** - * Releases this Connection object's database and JDBC resources - * immediately instead of waiting for them to be automatically - * released. All Statements created from this Connection will be - * closed when this method is called. + * Makes all changes made since the previous commit/rollback permanent and releases any database locks currently + * held by this Connection object. This method should be used only when auto-commit mode has been disabled. * - * Calling the method close on a Connection object that is already - * closed is a no-op. - */ - @Override - public void close() { - synchronized (server) { - for (Statement st : statements.keySet()) { - try { - st.close(); - } catch (SQLException e) { - // better luck next time! - } - } - // close the socket - server.close(); - // close active SendThread if any - if (sendThread != null) { - sendThread.shutdown(); - sendThread = null; - } - // report ourselves as closed - closed = true; - } - } - - /** - * Makes all changes made since the previous commit/rollback - * permanent and releases any database locks currently held by this - * Connection object. This method should be used only when - * auto-commit mode has been disabled. - * - * @throws SQLException if a database access error occurs or this - * Connection object is in auto-commit mode + * @throws SQLException if a database access error occurs or this Connection object is in auto-commit mode * @see #setAutoCommit(boolean) */ @Override public void commit() throws SQLException { - // note: can't use sendIndependentCommand here because we need - // to process the auto_commit state the server gives - sendTransactionCommand("COMMIT"); + // note: can't use sendIndependentCommand here because we need to process the auto_commit state the server gives + this.sendTransactionCommand("COMMIT"); } /** @@ -494,8 +361,7 @@ public class MonetConnection * Retrieves this Connection object's current catalog name. * * @return the current catalog name or null if there is none - * @throws SQLException if a database access error occurs or the - * current language is not SQL + * @throws SQLException if a database access error occurs or the current language is not SQL */ @Override public String getCatalog() throws SQLException { @@ -504,13 +370,9 @@ public class MonetConnection } /** - * Retrieves the current holdability of ResultSet objects created - * using this Connection object. + * Retrieves the current holdability of ResultSet objects created using this Connection object. * - * @return the holdability, one of - * ResultSet.HOLD_CURSORS_OVER_COMMIT or - * ResultSet.CLOSE_CURSORS_AT_COMMIT - * @see #setHoldability() + * @return the holdability, one of ResultSet.HOLD_CURSORS_OVER_COMMIT or ResultSet.CLOSE_CURSORS_AT_COMMIT */ @Override public int getHoldability() { @@ -531,18 +393,16 @@ public class MonetConnection */ @Override public DatabaseMetaData getMetaData() throws SQLException { - if (lang != LANG_SQL) + if (!this.language.getRepresentation().equals("sql")) { throw new SQLException("This method is only supported in SQL mode", "M0M04"); - + } return new MonetDatabaseMetaData(this); } /** - * Retrieves this Connection object's current transaction isolation - * level. + * Retrieves this Connection object's current transaction isolation level. * - * @return the current transaction isolation level, which will be - * Connection.TRANSACTION_SERIALIZABLE + * @return the current transaction isolation level, which will be Connection.TRANSACTION_SERIALIZABLE */ @Override public int getTransactionIsolation() { @@ -550,12 +410,10 @@ public class MonetConnection } /** - * Retrieves the Map object associated with this Connection object. - * Unless the application has added an entry, the type map returned - * will be empty. + * Retrieves the Map object associated with this Connection object. Unless the application has added an entry, + * the type map returned will be empty. * - * @return the java.util.Map object associated with this Connection - * object + * @return the java.util.Map object associated with this Connection object */ @Override public Map<String,Class<?>> getTypeMap() { @@ -569,22 +427,19 @@ public class MonetConnection * the method SQLWarning.getNextWarning on the warning that was * retrieved previously. * - * This method may not be called on a closed connection; doing so - * will cause an SQLException to be thrown. + * This method may not be called on a closed connection; doing so will cause an SQLException to be thrown. * * Note: Subsequent warnings will be chained to this SQLWarning. * * @return the first SQLWarning object or null if there are none - * @throws SQLException if a database access error occurs or this method is - * called on a closed connection + * @throws SQLException if a database access error occurs or this method is called on a closed connection */ @Override public SQLWarning getWarnings() throws SQLException { - if (closed) + if (closed) { throw new SQLException("Cannot call on closed Connection", "M1M20"); - - // if there are no warnings, this will be null, which fits with the - // specification. + } + // if there are no warnings, this will be null, which fits with the specification. return warnings; } @@ -600,8 +455,7 @@ public class MonetConnection * can determine that a connection is invalid by catching any * exceptions that might be thrown when an operation is attempted. * - * @return true if this Connection object is closed; false if it is - * still open + * @return true if this Connection object is closed; false if it is still open */ @Override public boolean isClosed() { @@ -731,16 +585,10 @@ public class MonetConnection */ @Override public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency, int resultSetHoldability) - throws SQLException - { + throws SQLException { try { - PreparedStatement ret = new MonetPreparedStatement( - this, - resultSetType, - resultSetConcurrency, - resultSetHoldability, - sql - ); + PreparedStatement ret = new MonetPreparedStatement(this, resultSetType, resultSetConcurrency, + resultSetHoldability, sql); // store it in the map for when we close... statements.put(ret, null); return ret; @@ -787,7 +635,7 @@ public class MonetConnection @Override public PreparedStatement prepareStatement(String sql, int autoGeneratedKeys) throws SQLException { if (autoGeneratedKeys != Statement.RETURN_GENERATED_KEYS && - autoGeneratedKeys != Statement.NO_GENERATED_KEYS) + autoGeneratedKeys != Statement.NO_GENERATED_KEYS) throw new SQLException("Invalid argument, expected RETURN_GENERATED_KEYS or NO_GENERATED_KEYS", "M1M05"); /* MonetDB has no way to disable this, so just do the normal @@ -871,20 +719,20 @@ public class MonetConnection */ @Override public void releaseSavepoint(Savepoint savepoint) throws SQLException { - if (!(savepoint instanceof MonetSavepoint)) + if (!(savepoint instanceof MonetSavepoint)) { throw new SQLException("This driver can only handle savepoints it created itself", "M0M06"); - - MonetSavepoint sp = (MonetSavepoint)savepoint; - + } + MonetSavepoint sp = (MonetSavepoint) savepoint; // note: can't use sendIndependentCommand here because we need // to process the auto_commit state the server gives - sendTransactionCommand("RELEASE SAVEPOINT " + sp.getName()); + this.sendTransactionCommand("RELEASE SAVEPOINT " + sp.getName()); } /** * Undoes all changes made in the current transaction and releases - * any database locks currently held by this Connection object. - * This method should be used only when auto-commit mode has been disabled. + * any database locks currently held by this Connection object. This + * method should be used only when auto-commit mode has been + * disabled. * * @throws SQLException if a database access error occurs or this * Connection object is in auto-commit mode @@ -894,13 +742,15 @@ public class MonetConnection public void rollback() throws SQLException { // note: can't use sendIndependentCommand here because we need // to process the auto_commit state the server gives - sendTransactionCommand("ROLLBACK"); + // create a container for the result + this.sendTransactionCommand("ROLLBACK"); } /** * Undoes all changes made after the given Savepoint object was set. * - * This method should be used only when auto-commit has been disabled. + * This method should be used only when auto-commit has been + * disabled. * * @param savepoint the Savepoint object to roll back to * @throws SQLException if a database access error occurs, the @@ -909,14 +759,15 @@ public class MonetConnection */ @Override public void rollback(Savepoint savepoint) throws SQLException { - if (!(savepoint instanceof MonetSavepoint)) + if (!(savepoint instanceof MonetSavepoint)) { throw new SQLException("This driver can only handle savepoints it created itself", "M0M06"); + } MonetSavepoint sp = (MonetSavepoint)savepoint; - // note: can't use sendIndependentCommand here because we need // to process the auto_commit state the server gives - sendTransactionCommand("ROLLBACK TO SAVEPOINT " + sp.getName()); + // create a container for the result + this.sendTransactionCommand("ROLLBACK TO SAVEPOINT " + sp.getName()); } /** @@ -925,7 +776,8 @@ public class MonetConnection * will be executed and committed as individual transactions. * Otherwise, its SQL statements are grouped into transactions that * are terminated by a call to either the method commit or the - * method rollback. By default, new connections are in auto-commit mode. + * method rollback. By default, new connections are in auto-commit + * mode. * * The commit occurs when the statement completes or the next * execute occurs, whichever comes first. In the case of statements @@ -946,7 +798,7 @@ public class MonetConnection @Override public void setAutoCommit(boolean autoCommit) throws SQLException { if (this.autoCommit != autoCommit) { - sendControlCommand("auto_commit " + (autoCommit ? "1" : "0")); + this.sendControlCommand(ControlCommands.AUTO_COMMIT, (autoCommit ? 1 : 0)); this.autoCommit = autoCommit; } } @@ -958,7 +810,7 @@ public class MonetConnection */ @Override public void setCatalog(String catalog) throws SQLException { - // silently ignore this request as MonetDB does not support catalogs + throw new SQLFeatureNotSupportedException("setCatalog(String catalog) not supported", "0A000"); } /** @@ -990,8 +842,9 @@ public class MonetConnection */ @Override public void setReadOnly(boolean readOnly) throws SQLException { - if (readOnly == true) + if (readOnly) { addWarning("cannot setReadOnly(true): read-only Connection mode not supported", "01M08"); + } } /** @@ -1006,16 +859,17 @@ public class MonetConnection public Savepoint setSavepoint() throws SQLException { // create a new Savepoint object MonetSavepoint sp = new MonetSavepoint(); - // note: can't use sendIndependentCommand here because we need // to process the auto_commit state the server gives - sendTransactionCommand("SAVEPOINT " + sp.getName()); + // create a container for the result + this.sendTransactionCommand("SAVEPOINT " + sp.getName()); return sp; } /** - * Creates a savepoint with the given name in the current transaction - * and returns the new Savepoint object that represents it. + * Creates a savepoint with the given name in the current + * transaction and returns the new Savepoint object that represents + * it. * * @param name a String containing the name of the savepoint * @return the new Savepoint object @@ -1031,30 +885,28 @@ public class MonetConnection } catch (IllegalArgumentException e) { throw new SQLException(e.getMessage(), "M0M03"); } - // note: can't use sendIndependentCommand here because we need // to process the auto_commit state the server gives - sendTransactionCommand("SAVEPOINT " + sp.getName()); + // create a container for the result + this.sendTransactionCommand("SAVEPOINT " + sp.getName()); return sp; } /** * Attempts to change the transaction isolation level for this * Connection object to the one given. The constants defined in the - * interface Connection are the possible transaction isolation levels. + * interface Connection are the possible transaction isolation + * levels. * - * @param level one of the following Connection constants: - * Connection.TRANSACTION_READ_UNCOMMITTED, - * Connection.TRANSACTION_READ_COMMITTED, - * Connection.TRANSACTION_REPEATABLE_READ, or + * @param level one of the following Connection constants: Connection.TRANSACTION_READ_UNCOMMITTED, + * Connection.TRANSACTION_READ_COMMITTED, Connection.TRANSACTION_REPEATABLE_READ, or * Connection.TRANSACTION_SERIALIZABLE. */ @Override public void setTransactionIsolation(int level) { if (level != TRANSACTION_SERIALIZABLE) { addWarning("MonetDB only supports fully serializable " + - "transactions, continuing with transaction level " + - "raised to TRANSACTION_SERIALIZABLE", "01M09"); + "transactions, continuing with transaction level raised to TRANSACTION_SERIALIZABLE", "01M09"); } } @@ -1078,12 +930,9 @@ public class MonetConnection */ @Override public String toString() { - return "MonetDB Connection (" + getJDBCURL() + ") " + - (closed ? "connected" : "disconnected"); + return "MonetDB Connection (" + this.getJDBCURL() + ") " + (closed ? "disconnected" : "connected"); } - //== Java 1.6 methods (JDBC 4.0) - /** * Factory method for creating Array objects. * @@ -1106,9 +955,12 @@ public class MonetConnection * type or a standard SQL type supported by this database. * This is the value returned by Array.getBaseTypeName * @return an Array object whose elements map to the specified SQL type - * @throws SQLException - if a database error occurs, the JDBC type is not appropriate for the typeName and - * the conversion is not supported, the typeName is null or this method is called on a closed connection - * @throws SQLFeatureNotSupportedException - if the JDBC driver does not support this data type + * @throws SQLException if a database error occurs, the JDBC type + * is not appropriate for the typeName and the conversion is + * not supported, the typeName is null or this method is + * called on a closed connection + * @throws SQLFeatureNotSupportedException the JDBC driver does + * not support this data type * @since 1.6 */ @Override @@ -1117,6 +969,8 @@ public class MonetConnection } + //== end methods of interface java.sql.Connection + /** * Constructs an object that implements the Clob interface. The * object returned initially contains no data. The setAsciiStream, @@ -1124,8 +978,8 @@ public class MonetConnection * may be used to add data to the Clob. * * @return a MonetClob instance - * @throws SQLException - if an object that implements the Clob interface can not be constructed, - * this method is called on a closed connection or a database access error occurs. + * @throws SQLFeatureNotSupportedException the JDBC driver does + * not support MonetClob objects that can be filled in * @since 1.6 */ @Override @@ -1140,14 +994,13 @@ public class MonetConnection * data to the Blob. * * @return a MonetBlob instance - * @throws SQLException - if an object that implements the Blob interface can not be constructed, - * this method is called on a closed connection or a database access error occurs. + * @throws SQLFeatureNotSupportedException the JDBC driver does + * not support MonetBlob objects that can be filled in * @since 1.6 */ @Override public java.sql.Blob createBlob() throws SQLException { - byte[] buf = new byte[1]; - return new MonetBlob(buf); + return new MonetBlob(new byte[1]); } /** @@ -1157,9 +1010,8 @@ public class MonetConnection * may be used to add data to the NClob. * * @return an NClob instance - * @throws SQLException - if an object that implements the NClob interface can not be constructed, - * this method is called on a closed connection or a database access error occurs. - * @throws SQLFeatureNotSupportedException - if the JDBC driver does not support this data type + * @throws SQLFeatureNotSupportedException the JDBC driver does + * not support MonetNClob objects that can be filled in * @since 1.6 */ @Override @@ -1177,9 +1029,10 @@ public class MonetConnection * @param attributes the attributes that populate the returned object * @return a Struct object that maps to the given SQL type and is * populated with the given attributes - * @throws SQLException - if an object that implements the Struct interface can not be constructed, - * this method is called on a closed connection or a database access error occurs. - * @throws SQLFeatureNotSupportedException - if the JDBC driver does not support this data type + * @throws SQLException if a database error occurs, the typeName + * is null or this method is called on a closed connection + * @throws SQLFeatureNotSupportedException the JDBC driver does + * not support this data type * @since 1.6 */ @Override @@ -1194,9 +1047,8 @@ public class MonetConnection * interface may be used to add data to the SQLXML object. * * @return An object that implements the SQLXML interface - * @throws SQLException - if an object that implements the SQLXML interface can not be constructed, - * this method is called on a closed connection or a database access error occurs. - * @throws SQLFeatureNotSupportedException - if the JDBC driver does not support this data type + * @throws SQLFeatureNotSupportedException the JDBC driver does + * not support this data type * @since 1.6 */ @Override @@ -1252,7 +1104,6 @@ public class MonetConnection } } catch (SQLException se) { String msg = se.getMessage(); - // System.out.println("Con.isValid(): " + msg); if (msg != null && msg.equals("Current transaction is aborted (please ROLLBACK)")) { isValid = true; } @@ -1362,21 +1213,14 @@ public class MonetConnection return; } // only set value for supported property names - if (name.equals("host") || - name.equals("port") || - name.equals("user") || - name.equals("password") || - name.equals("database") || - name.equals("language") || - name.equals("so_timeout") || - name.equals("debug") || - name.equals("hash") || - name.equals("treat_blob_as_binary")) { + if (name.equals("host") || name.equals("port") || name.equals("user") || name.equals("password") || + name.equals("database") || name.equals("language") || name.equals("so_timeout") || + name.equals("hash") || name.equals("treat_blob_as_binary") || name.equals("follow_redirects") || + name.equals("treat_clob_as_longvarchar") || name.equals("embedded") || name.equals("directory")) { conn_props.setProperty(name, value); } else { addWarning("setClientInfo: " + name + "is not a recognised property", "01M07"); } - return; } /** @@ -1405,22 +1249,17 @@ public class MonetConnection @Override public void setClientInfo(Properties props) throws java.sql.SQLClientInfoException { if (props != null) { - for (Entry<Object, Object> entry : props.entrySet()) { - setClientInfo(entry.getKey().toString(), - entry.getValue().toString()); + for (Map.Entry<Object, Object> entry : props.entrySet()) { + setClientInfo(entry.getKey().toString(), entry.getValue().toString()); } } } - - //== Java 1.7 methods (JDBC 4.1) - /** * Sets the given schema name to access. * * @param schema the name of a schema in which to work - * @throws SQLException if a database access error occurs or this - * method is called on a closed connection + * @throws SQLException if a database access error occurs or this method is called on a closed connection * @since 1.7 */ @Override @@ -1431,6 +1270,7 @@ public class MonetConnection throw new SQLException("Missing schema name", "M1M05"); Statement st = createStatement(); + schema = schema.replaceAll("\\\\", "\\\\\\\\").replaceAll("'", "\\\\'"); try { st.execute("SET SCHEMA \"" + schema + "\""); } finally { @@ -1442,15 +1282,13 @@ public class MonetConnection * Retrieves this Connection object's current schema name. * * @return the current schema name or null if there is none - * @throws SQLException if a database access error occurs or this - * method is called on a closed connection - * @since 1.7 + * @throws SQLException if a database access error occurs or this method is called on a closed connection */ @Override public String getSchema() throws SQLException { - if (closed) + if (closed) { throw new SQLException("Cannot call on closed Connection", "M1M20"); - + } String cur_schema; Statement st = createStatement(); ResultSet rs = null; @@ -1478,13 +1316,9 @@ public class MonetConnection * Calling abort marks the connection closed and releases any * resources. Calling abort on a closed connection is a no-op. * - * @param executor The Executor implementation which will be used by - * abort - * @throws SQLException if a database access error occurs or the - * executor is null - * @throws SecurityException if a security manager exists and - * its checkPermission method denies calling abort - * @since 1.7 + * @param executor The Executor implementation which will be used by abort + * @throws SQLException if a database access error occurs or the executor is null + * @throws SecurityException if a security manager exists and its checkPermission method denies calling abort */ @Override public void abort(Executor executor) throws SQLException { @@ -1507,14 +1341,10 @@ public class MonetConnection * isClosed or Connection.isValid methods, will result in a * SQLException. * - * @param executor The Executor implementation which will be used by - * setNetworkTimeout - * @param millis The time in milliseconds to wait for the - * database operation to complete - * @throws SQLException if a database access error occurs, this - * method is called on a closed connection, the executor is - * null, or the value specified for seconds is less than 0. - * @since 1.7 + * @param executor The Executor implementation which will be used by setNetworkTimeout + * @param millis The time in milliseconds to wait for the database operation to complete + * @throws SQLException if a database access error occurs, this method is called on a closed connection, the + * executor is null, or the value specified for seconds is less than 0. */ @Override public void setNetworkTimeout(Executor executor, int millis) throws SQLException { @@ -1526,7 +1356,7 @@ public class MonetConnection throw new SQLException("milliseconds is less than zero", "M1M05"); try { - server.setSoTimeout(millis); + this.setSoTimeout(millis); } catch (SocketException e) { throw new SQLNonTransientConnectionException(e.getMessage(), "08000"); } @@ -1549,34 +1379,27 @@ public class MonetConnection throw new SQLException("Cannot call on closed Connection", "M1M20"); try { - return server.getSoTimeout(); + return this.getSoTimeout(); } catch (SocketException e) { throw new SQLNonTransientConnectionException(e.getMessage(), "08000"); } } - - //== end methods of interface java.sql.Connection - - - /** - * Returns the MonetDB JDBC Connection URL (without user name and password). - * Defined as public because it is called from: MonetDatabaseMetaData.java getURL() - */ - public String getJDBCURL() { - String language = ""; - if (lang == LANG_MAL) - language = "?language=mal"; - return "jdbc:monetdb://" + hostname + ":" + port + "/" + database + language; - } + //== end methods of interface Connection /** * Returns whether the BLOB type should be mapped to BINARY type. */ - boolean getBlobAsBinary() { + public boolean getBlobAsBinary() { return blobIsBinary; } + /** + * Returns whether the CLOB type should be mapped to LONGVARCHAR type. + */ + public boolean getClobAsLongChar() { + return clobIsLongChar; + } /** * Sends the given string to MonetDB as special transaction command. @@ -1598,67 +1421,32 @@ public class MonetConnection } /** - * Sends the given string to MonetDB as regular statement, making - * sure there is a prompt after the command is sent. All possible - * returned information is discarded. Encountered errors are - * reported. + * Sends the given string to MonetDB as regular statement, making sure there is a prompt after the command is sent. + * All possible returned information is discarded. Encountered errors are reported. * * @param command the exact string to send to MonetDB * @throws SQLException if an IO exception or a database error occurs */ void sendIndependentCommand(String command) throws SQLException { - synchronized (server) { - try { - out.writeLine( - (queryTempl[0] == null ? "" : queryTempl[0]) + - command + - (queryTempl[1] == null ? "" : queryTempl[1])); - String error = in.waitForPrompt(); - if (error != null) - throw new SQLException(error.substring(6), error.substring(0, 5)); - } catch (SocketTimeoutException e) { - close(); // JDBC 4.1 semantics: abort() - throw new SQLNonTransientConnectionException("connection timed out", "08M33"); - } catch (IOException e) { - throw new SQLNonTransientConnectionException(e.getMessage(), "08000"); + try { + protocol.writeNextQuery(language.getQueryTemplateIndex(0), command, language.getQueryTemplateIndex(1)); + protocol.waitUntilPrompt(); + int csrh = protocol.getCurrentServerResponse(); + if (csrh == ServerResponses.ERROR) { + String error = protocol.getRemainingStringLine(0); + throw new SQLException(error.substring(6), error.substring(0, 5)); } + } catch (SocketTimeoutException e) { + close(); // JDBC 4.1 semantics: abort() + throw new SQLNonTransientConnectionException("connection timed out", "08M33"); + } catch (IOException e) { + throw new SQLNonTransientConnectionException(e.getMessage(), "08000"); } } /** - * Sends the given string to MonetDB as control statement, making - * sure there is a prompt after the command is sent. All possible - * returned information is discarded. Encountered errors are - * reported. - * - * @param command the exact string to send to MonetDB - * @throws SQLException if an IO exception or a database error occurs - */ - void sendControlCommand(String command) throws SQLException { - // send X command - synchronized (server) { - try { - out.writeLine( - (commandTempl[0] == null ? "" : commandTempl[0]) + - command + - (commandTempl[1] == null ? "" : commandTempl[1])); - String error = in.waitForPrompt(); - if (error != null) - throw new SQLException(error.substring(6), error.substring(0, 5)); - } catch (SocketTimeoutException e) { - close(); // JDBC 4.1 semantics, abort() - throw new SQLNonTransientConnectionException("connection timed out", "08M33"); - } catch (IOException e) { - throw new SQLNonTransientConnectionException(e.getMessage(), "08000"); - } - } - } - - /** - * Adds a warning to the pile of warnings this Connection object - * has. If there were no warnings (or clearWarnings was called) - * this warning will be the first, otherwise this warning will get - * appended to the current warning. + * Adds a warning to the pile of warnings this Connection object has. If there were no warnings (or clearWarnings + * was called) this warning will be the first, otherwise this warning will get appended to the current warning. * * @param reason the warning message */ @@ -1670,765 +1458,81 @@ public class MonetConnection } } - /** the default number of rows that are (attempted to) read at once */ - private final static int DEF_FETCHSIZE = 250; - /** The sequence counter */ - private static int seqCounter = 0; - - /** An optional thread that is used for sending large queries */ - private SendThread sendThread = null; - /** - * A Response is a message sent by the server to indicate some - * action has taken place, and possible results of that action. - */ - // {{{ interface Response - interface Response { - /** - * Adds a line to the underlying Response implementation. - * - * @param line the header line as String - * @param linetype the line type according to the MAPI protocol - * @return a non-null String if the line is invalid, - * or additional lines are not allowed. - */ - public abstract String addLine(String line, int linetype); - - /** - * Returns whether this Reponse expects more lines to be added - * to it. - * - * @return true if a next line should be added, false otherwise - */ - public abstract boolean wantsMore(); - - /** - * Indicates that no more header lines will be added to this - * Response implementation. - * - * @throws SQLException if the contents of the Response is not - * consistent or sufficient. - */ - public abstract void complete() throws SQLException; - - /** - * Instructs the Response implementation to close and do the - * necessary clean up procedures. - * - * @throws SQLException - */ - public abstract void close(); - } - // }}} - - /** - * The ResultSetResponse represents a tabular result sent by the - * server. This is typically an SQL table. The MAPI headers of the - * Response look like: - * <pre> - * &1 1 28 2 10 - * # name, value # name - * # varchar, varchar # type - * </pre> - * there the first line consists out of<br /> - * <tt>&"qt" "id" "tc" "cc" "rc"</tt>. + * A list of Response objects. Responses are added to this list. Methods of this class are not synchronized. This is + * left as responsibility to the caller to prevent concurrent access. */ - // {{{ ResultSetResponse class implementation - class ResultSetResponse implements Response { - /** The number of columns in this result */ - public final int columncount; - /** The total number of rows this result set has */ - public final int tuplecount; - /** The numbers of rows to retrieve per DataBlockResponse */ - private int cacheSize; - /** The table ID of this result */ - public final int id; - /** The names of the columns in this result */ - private String[] name; - /** The types of the columns in this result */ - private String[] type; - /** The max string length for each column in this result */ - private int[] columnLengths; - /** The table for each column in this result */ - private String[] tableNames; - /** The query sequence number */ - private final int seqnr; - /** A List of result blocks (chunks of size fetchSize/cacheSize) */ - private DataBlockResponse[] resultBlocks; - - /** A bitmap telling whether the headers are set or not */ - private boolean[] isSet; - /** Whether this Response is closed */ - private boolean closed; - - /** The Connection that we should use when requesting a new block */ - private MonetConnection.ResponseList parent; - /** Whether the fetchSize was explitly set by the user */ - private boolean cacheSizeSetExplicitly = false; - /** Whether we should send an Xclose command to the server - * if we close this Response */ - private boolean destroyOnClose; - /** the offset to be used on Xexport queries */ - private int blockOffset = 0; - - /** A parser for header lines */ - HeaderLineParser hlp; - - private final static int NAMES = 0; - private final static int TYPES = 1; - private final static int TABLES = 2; - private final static int LENS = 3; - - - /** - * Sole constructor, which requires a MonetConnection parent to - * be given. - * - * @param id the ID of the result set - * @param tuplecount the total number of tuples in the result set - * @param columncount the number of columns in the result set - * @param rowcount the number of rows in the current block - * @param parent the parent that created this Response and will - * supply new result blocks when necessary - * @param seq the query sequence number - */ - ResultSetResponse( - int id, - int tuplecount, - int columncount, - int rowcount, - MonetConnection.ResponseList parent, - int seq) - throws SQLException - { - isSet = new boolean[7]; - this.parent = parent; - if (parent.cachesize == 0) { - /* Below we have to calculate how many "chunks" we need - * to allocate to store the entire result. However, if - * the user didn't set a cache size, as in this case, we - * need to stick to our defaults. */ - cacheSize = MonetConnection.DEF_FETCHSIZE; - cacheSizeSetExplicitly = false; - } else { - cacheSize = parent.cachesize; - cacheSizeSetExplicitly = true; - } - /* So far, so good. Now the problem with EXPLAIN, DOT, etc - * queries is, that they don't support any block fetching, - * so we need to always fetch everything at once. For that - * reason, the cache size is here set to the rowcount if - * it's larger, such that we do a full fetch at once. - * (Because we always set a reply_size, we can only get a - * larger rowcount from the server if it doesn't paginate, - * because it's a pseudo SQL result.) */ - if (rowcount > cacheSize) - cacheSize = rowcount; - seqnr = seq; - closed = false; - destroyOnClose = id > 0 && tuplecount > rowcount; - - this.id = id; - this.tuplecount = tuplecount; - this.columncount = columncount; - this.resultBlocks = - new DataBlockResponse[(tuplecount / cacheSize) + 1]; - - hlp = new HeaderLineParser(columncount); - - resultBlocks[0] = new DataBlockResponse( - rowcount, - parent.rstype == ResultSet.TYPE_FORWARD_ONLY - ); - } + public class ResponseList { - /** - * Parses the given string and changes the value of the matching - * header appropriately, or passes it on to the underlying - * DataResponse. - * - * @param tmpLine the string that contains the header - * @return a non-null String if the header cannot be parsed or - * is unknown - */ - // {{{ addLine - @Override - public String addLine(String tmpLine, int linetype) { - if (isSet[LENS] && isSet[TYPES] && isSet[TABLES] && isSet[NAMES]) { - return resultBlocks[0].addLine(tmpLine, linetype); - } - - if (linetype != BufferedMCLReader.HEADER) - return "header expected, got: " + tmpLine; - - // depending on the name of the header, we continue - try { - switch (hlp.parse(tmpLine)) { - case HeaderLineParser.NAME: - name = hlp.values.clone(); - isSet[NAMES] = true; - break; - case HeaderLineParser.LENGTH: - columnLengths = hlp.intValues.clone(); - isSet[LENS] = true; - break; - case HeaderLineParser.TYPE: - type = hlp.values.clone(); - isSet[TYPES] = true; - break; - case HeaderLineParser.TABLE: - tableNames = hlp.values.clone(); - isSet[TABLES] = true; - break; - } - } catch (MCLParseException e) { - return e.getMessage(); - } - - // all is well - return null; - } - // }}} - - /** - * Returns whether this ResultSetResponse needs more lines. - * This method returns true if not all headers are set, or the - * first DataBlockResponse reports to want more. - */ - @Override - public boolean wantsMore() { - if (isSet[LENS] && isSet[TYPES] && isSet[TABLES] && isSet[NAMES]) { - return resultBlocks[0].wantsMore(); - } else { - return true; - } - } - - /** - * Returns an array of Strings containing the values between - * ',\t' separators. - * - * @param chrLine a character array holding the input data - * @param start where the relevant data starts - * @param stop where the relevant data stops - * @return an array of Strings - */ - final private String[] getValues(char[] chrLine, int start, int stop) { - int elem = 0; - String[] values = new String[columncount]; - - for (int i = start; i < stop; i++) { - if (chrLine[i] == '\t' && chrLine[i - 1] == ',') { - values[elem++] = - new String(chrLine, start, i - 1 - start); - start = i + 1; - } - } - // at the left over part - values[elem++] = new String(chrLine, start, stop - start); - - return values; - } - - /** - * Adds the given DataBlockResponse to this ResultSetResponse at - * the given block position. - * - * @param offset the offset number of rows for this block - * @param rr the DataBlockResponse to add - */ - void addDataBlockResponse(int offset, DataBlockResponse rr) { - int block = (offset - blockOffset) / cacheSize; - resultBlocks[block] = rr; - } - - /** - * Marks this Response as being completed. A complete Response - * needs to be consistent with regard to its internal data. - * - * @throws SQLException if the data currently in this Response is not - * sufficient to be consistant - */ - @Override - public void complete() throws SQLException { - String error = ""; - if (!isSet[NAMES]) error = "name header missing\n"; - if (!isSet[TYPES]) error += "type header missing\n"; - if (!isSet[TABLES]) error += "table name header missing\n"; - if (!isSet[LENS]) error += "column width header missing\n"; - if (!error.isEmpty()) - throw new SQLException(error, "M0M10"); - } - - /** - * Returns the names of the columns - * - * @return the names of the columns - */ - String[] getNames() { - return name; - } - - /** - * Returns the types of the columns - * - * @return the types of the columns - */ - String[] getTypes() { - return type; - } - - /** - * Returns the tables of the columns - * - * @return the tables of the columns - */ - String[] getTableNames() { - return tableNames; - } - - /** - * Returns the lengths of the columns - * - * @return the lengths of the columns - */ - int[] getColumnLengths() { - return columnLengths; - } - - /** - * Returns the cache size used within this Response - * - * @return the cache size - */ - int getCacheSize() { - return cacheSize; - } - - /** - * Returns the current block offset - * - * @return the current block offset - */ - int getBlockOffset() { - return blockOffset; - } - - /** - * Returns the ResultSet type, FORWARD_ONLY or not. - * - * @return the ResultSet type - */ - int getRSType() { - return parent.rstype; - } + /** The cache size (number of rows in a DataBlockResponse object) */ + private final int cachesize; + /** The maximum number of results for this query */ + private final int maxrows; + /** The ResultSet type to produce */ + private final int rstype; + /** The ResultSet concurrency to produce */ + private final int rsconcur; + /** The sequence number of this ResponseList */ + private final int seqnr; + /** A list of the Responses associated with the query, in the right order */ + private final List<IResponse> responses = new ArrayList<>(); + /** A map of ResultSetResponses, used for additional DataBlockResponse mapping */ + private Map<Integer, ResultSetResponse> rsresponses; + /** The current header returned by getNextResponse() */ + private int curResponse = -1; /** - * Returns the concurrency of the ResultSet. - * - * @return the ResultSet concurrency - */ - int getRSConcur() { - return parent.rsconcur; - } - - /** - * Returns a line from the cache. If the line is already present in the - * cache, it is returned, if not appropriate actions are taken to make - * sure the right block is being fetched and as soon as the requested - * line is fetched it is returned. - * - * @param row the row in the result set to return - * @return the exact row read as requested or null if the requested row - * is out of the scope of the result set - * @throws SQLException if an database error occurs - */ - String getLine(int row) throws SQLException { - if (row >= tuplecount || row < 0) - return null; - - int block = (row - blockOffset) / cacheSize; - int blockLine = (row - blockOffset) % cacheSize; - - // do we have the right block loaded? (optimistic try) - DataBlockResponse rawr; - // load block if appropriate - if ((rawr = resultBlocks[block]) == null) { - /// TODO: ponder about a maximum number of blocks to keep - /// in memory when dealing with random access to - /// reduce memory blow-up - - // if we're running forward only, we can discard the old - // block loaded - if (parent.rstype == ResultSet.TYPE_FORWARD_ONLY) { - for (int i = 0; i < block; i++) - resultBlocks[i] = null; - - if (MonetConnection.seqCounter - 1 == seqnr && - !cacheSizeSetExplicitly && - tuplecount - row > cacheSize && - cacheSize < MonetConnection.DEF_FETCHSIZE * 10) - { - // there has no query been issued after this - // one, so we can consider this an uninterrupted - // continuation request. Let's once increase - // the cacheSize as it was not explicitly set, - // since the chances are high that we won't - // bother anyone else by doing so, and just - // gaining some performance. - - // store the previous position in the - // blockOffset variable - blockOffset += cacheSize; - - // increase the cache size (a lot) - cacheSize *= 10; - - // by changing the cacheSize, we also - // change the block measures. Luckily - // we don't care about previous blocks - // because we have a forward running - // pointer only. However, we do have - // to recalculate the block number, to - // ensure the next call to find this - // new block. - block = (row - blockOffset) / cacheSize; - blockLine = (row - blockOffset) % cacheSize; - } - } - - // ok, need to fetch cache block first - parent.executeQuery(commandTempl, - "export " + id + " " + ((block * cacheSize) + blockOffset) + " " + cacheSize); - rawr = resultBlocks[block]; - if (rawr == null) - throw new SQLException("resultBlocks[" + block + "] should have been fetched by now", "M0M10"); - } - - return rawr.getRow(blockLine); - } - - /** - * Closes this Response by sending an Xclose to the server indicating - * that the result can be closed at the server side as well. - */ - @Override - public void close() { - if (closed) return; - - // send command to server indicating we're done with this - // result only if we had an ID in the header and this result - // was larger than the reply size - try { - if (destroyOnClose) - sendControlCommand("close " + id); - } catch (SQLException e) { - // probably a connection error... - } - - // close the data block associated with us - for (int i = 1; i < resultBlocks.length; i++) { - DataBlockResponse r = resultBlocks[i]; - if (r != null) - r.close(); - } - - closed = true; - } - - /** - * Returns whether this Response is closed - * - * @return whether this Response is closed - */ - boolean isClosed() { - return closed; - } - } - // }}} - - /** - * The DataBlockResponse is tabular data belonging to a - * ResultSetResponse. Tabular data from the server typically looks - * like: - * <pre> - * [ "value", 56 ] - * </pre> - * where each column is separated by ",\t" and each tuple surrounded - * by brackets ("[" and "]"). A DataBlockResponse object holds the - * raw data as read from the server, in a parsed manner, ready for - * easy retrieval. - * - * This object is not intended to be queried by multiple threads - * synchronously. It is designed to work for one thread retrieving - * rows from it. When multiple threads will retrieve rows from this - * object, it is possible for threads to get the same data. - */ - // {{{ DataBlockResponse class implementation - static class DataBlockResponse implements Response { - /** The String array to keep the data in */ - private final String[] data; - - /** The counter which keeps the current position in the data array */ - private int pos; - /** Whether we can discard lines as soon as we have read them */ - private boolean forwardOnly; - - /** - * Constructs a DataBlockResponse object - * @param size the size of the data array to create - * @param forward whether this is a forward only result - */ - DataBlockResponse(int size, boolean forward) { - pos = -1; - data = new String[size]; - forwardOnly = forward; - } - - /** - * addLine adds a String of data to this object's data array. - * Note that an IndexOutOfBoundsException can be thrown when an - * attempt is made to add more than the original construction size - * specified. - * - * @param line the header line as String - * @param linetype the line type according to the MAPI protocol - * @return a non-null String if the line is invalid, - * or additional lines are not allowed. - */ - @Override - public String addLine(String line, int linetype) { - if (linetype != BufferedMCLReader.RESULT) - return "protocol violation: unexpected line in data block: " + line; - // add to the backing array - data[++pos] = line; - - // all is well - return null; - } - - /** - * Returns whether this Reponse expects more lines to be added - * to it. - * - * @return true if a next line should be added, false otherwise - */ - @Override - public boolean wantsMore() { - // remember: pos is the value already stored - return pos + 1 < data.length; - } - - /** - * Indicates that no more header lines will be added to this - * Response implementation. In most cases this is a redundant - * operation because the data array is full. However... it can - * happen that this is NOT the case! - * - * @throws SQLException if not all rows are filled - */ - @Override - public void complete() throws SQLException { - if ((pos + 1) != data.length) - throw new SQLException("Inconsistent state detected! Current block capacity: " - + data.length + ", block usage: " + (pos + 1) + ". Did MonetDB send what it promised to?", "M0M10"); - } - - /** - * Instructs the Response implementation to close and do the - * necessary clean up procedures. - * - * @throws SQLException - */ - @Override - public void close() { - // feed all rows to the garbage collector - for (int i = 0; i < data.length; i++) data[i] = null; - } - - /** - * Retrieves the required row. Warning: if the requested rows - * is out of bounds, an IndexOutOfBoundsException will be - * thrown. - * - * @param line the row to retrieve - * @return the requested row as String - */ - String getRow(int line) { - if (forwardOnly) { - String ret = data[line]; - data[line] = null; - return ret; - } else { - return data[line]; - } - } - } - // }}} - - /** - * The UpdateResponse represents an update statement response. It - * is issued on an UPDATE, INSERT or DELETE SQL statement. This - * response keeps a count field that represents the affected rows - * and a field that contains the last inserted auto-generated ID, or - * -1 if not applicable.<br /> - * <tt>&2 0 -1</tt> - */ - // {{{ UpdateResponse class implementation - static class UpdateResponse implements Response { - public final int count; - public final String lastid; - - public UpdateResponse(int cnt, String id) { - // fill the blank finals - this.count = cnt; - this.lastid = id; - } - - @Override - public String addLine(String line, int linetype) { - return "Header lines are not supported for an UpdateResponse"; - } - - @Override - public boolean wantsMore() { - return false; - } - - @Override - public void complete() { - // empty, because there is nothing to check - } - - @Override - public void close() { - // nothing to do here... - } - } - // }}} - - /** - * The SchemaResponse represents an schema modification response. - * It is issued on statements like CREATE, DROP or ALTER TABLE. - * This response keeps a field that represents the success state, as - * defined by JDBC, which is currently in MonetDB's case alwats - * SUCCESS_NO_INFO. Note that this state is not sent by the - * server.<br /> - * <tt>&3</tt> - */ - // {{{ SchemaResponse class implementation - class SchemaResponse implements Response { - public final int state = Statement.SUCCESS_NO_INFO; - - @Override - public String addLine(String line, int linetype) { - return "Header lines are not supported for a SchemaResponse"; - } - - @Override - public boolean wantsMore() { - return false; - } - - @Override - public void complete() { - // empty, because there is nothing to check - } - - @Override - public void close() { - // nothing to do here... - } - } - // }}} - - /** - * The AutoCommitResponse represents a transaction message. It - * stores (a change in) the server side auto commit mode.<br /> - * <tt>&4 (t|f)</tt> - */ - // {{{ AutoCommitResponse class implementation - class AutoCommitResponse extends SchemaResponse { - public final boolean autocommit; - - public AutoCommitResponse(boolean ac) { - // fill the blank final - this.autocommit = ac; - } - } - // }}} - - /** - * A list of Response objects. Responses are added to this list. - * Methods of this class are not synchronized. This is left as - * responsibility to the caller to prevent concurrent access. - */ - // {{{ ResponseList class implementation - class ResponseList { - /** The cache size (number of rows in a DataBlockResponse object) */ - final int cachesize; - /** The maximum number of results for this query */ - final int maxrows; - /** The ResultSet type to produce */ - final int rstype; - /** The ResultSet concurrency to produce */ - final int rsconcur; - /** The sequence number of this ResponseList */ - final int seqnr; - /** A list of the Responses associated with the query, - * in the right order */ - private List<Response> responses; - /** A map of ResultSetResponses, used for additional - * DataBlockResponse mapping */ - private Map<Integer, ResultSetResponse> rsresponses; - - /** The current header returned by getNextResponse() */ - private int curResponse; - - /** - * Main constructor. The query argument can either be a String - * or List. An SQLException is thrown if another object - * instance is supplied. + * Main constructor. The query argument can either be a String or List. An SQLException is thrown if another + * object instance is supplied. * * @param cachesize overall cachesize to use * @param maxrows maximum number of rows to allow in the set * @param rstype the type of result sets to produce * @param rsconcur the concurrency of result sets to produce */ - ResponseList( - int cachesize, - int maxrows, - int rstype, - int rsconcur - ) throws SQLException { + ResponseList(int cachesize, int maxrows, int rstype, int rsconcur) { this.cachesize = cachesize; this.maxrows = maxrows; this.rstype = rstype; this.rsconcur = rsconcur; - responses = new ArrayList<Response>(); - curResponse = -1; - seqnr = MonetConnection.seqCounter++; + this.seqnr = SeqCounter++; + } + + public int getCachesize() { + return cachesize; + } + + public int getRstype() { + return rstype; + } + + public int getRsconcur() { + return rsconcur; + } + + public int getMaxrows() { + return maxrows; } /** - * Retrieves the next available response, or null if there are - * no more responses. + * Retrieves the next available response, or null if there are no more responses. * * @return the next Response available or null */ - Response getNextResponse() throws SQLException { + IResponse getNextResponse() throws SQLException { if (rstype == ResultSet.TYPE_FORWARD_ONLY) { // free resources if we're running forward only if (curResponse >= 0 && curResponse < responses.size()) { - Response tmp = responses.get(curResponse); - if (tmp != null) tmp.close(); + IResponse tmp = responses.get(curResponse); + if (tmp != null) { + tmp.close(); + } responses.set(curResponse, null); } } curResponse++; if (curResponse >= responses.size()) { - // ResponseList is obviously completed so, there are no - // more responses + // ResponseList is obviously completed so, there are no more responses return null; } else { // return this response @@ -2437,13 +1541,13 @@ public class MonetConnection } /** - * Closes the Reponse at index i, if not null. + * Closes the Response at index i, if not null. * * @param i the index position of the header to close */ void closeResponse(int i) { if (i < 0 || i >= responses.size()) return; - Response tmp = responses.set(i, null); + IResponse tmp = responses.set(i, null); if (tmp != null) tmp.close(); } @@ -2465,21 +1569,19 @@ public class MonetConnection } /** - * Closes this ResponseList by closing all the Responses in this - * ResponseList. + * Closes this ResponseList by closing all the Responses in this ResponseList. */ - void close() { + public void close() { for (int i = 0; i < responses.size(); i++) { closeResponse(i); } } /** - * Returns whether this ResponseList has still unclosed - * Responses. + * Returns whether this ResponseList has still unclosed Responses. */ boolean hasUnclosedResponses() { - for (Response r : responses) { + for (IResponse r : responses) { if (r != null) return true; } @@ -2487,252 +1589,187 @@ public class MonetConnection } /** - * Executes the query contained in this ResponseList, and - * stores the Responses resulting from this query in this + * Executes the query contained in this ResponseList, and stores the Responses resulting from this query in this * ResponseList. * * @throws SQLException if a database error occurs */ void processQuery(String query) throws SQLException { - executeQuery(queryTempl, query); + this.executeQuery(language.getQueryTemplates(), query); } /** * Internal executor of queries. * * @param templ the template to fill in - * @param the query to execute + * @param query the query to execute * @throws SQLException if a database error occurs */ @SuppressWarnings("fallthrough") - void executeQuery(String[] templ, String query) - throws SQLException - { - boolean sendThreadInUse = false; + public void executeQuery(String[] templ, String query) throws SQLException { String error = null; try { - synchronized (server) { - // make sure we're ready to send query; read data till we - // have the prompt it is possible (and most likely) that we - // already have the prompt and do not have to skip any - // lines. Ignore errors from previous result sets. - in.waitForPrompt(); + // make sure we're ready to send query; read data till we have the prompt it is possible (and most + // likely) that we already have the prompt and do not have to skip any lines. Ignore errors from + // previous result sets. + protocol.waitUntilPrompt(); - // {{{ set reply size - /** - * Change the reply size of the server. If the given - * value is the same as the current value known to use, - * then ignore this call. If it is set to 0 we get a - * prompt after the server sent it's header. - */ - int size = cachesize == 0 ? DEF_FETCHSIZE : cachesize; - size = maxrows != 0 ? Math.min(maxrows, size) : size; - // don't do work if it's not needed - if (lang == LANG_SQL && size != curReplySize && templ != commandTempl) { - sendControlCommand("reply_size " + size); + // {{{ set reply size + /* + * Change the reply size of the server. If the given value is the same as the current value known + * to use, then ignore this call. If it is set to 0 we get a prompt after the server sent it's + * header. + * + * 2017: For now, in the embedded connection, the value set cachesize will be always the default one. + */ + int size = (cachesize != 0 && !isEmbedded) ? cachesize : MonetConnection.this.getDefFetchsize(); + size = maxrows != 0 ? Math.min(maxrows, size) : size; + // don't do work if it's not needed + if (!isEmbedded && language.getRepresentation().equals("sql") && size != curReplySize && + !Arrays.deepEquals(templ, language.getCommandTemplates())) { + sendControlCommand(ControlCommands.REPLY_SIZE, size); + // store the reply size after a successful change + curReplySize = size; + } + // }}} set reply size - // store the reply size after a successful change - curReplySize = size; + // If the query is larger than the TCP buffer size, use a special send thread to avoid deadlock with + // the server due to blocking behaviour when the buffer is full. Because the server will be writing + // back results to us, it will eventually block as well when its TCP buffer gets full, as we are + // blocking an not consuming from it. The result is a state where both client and server want to + // write, but block. + if (query.length() > getBlockSize()) { + // get a reference to the send thread + if (senderThread == null) { + senderThread = new SenderThread(protocol); } - // }}} set reply size - - // If the query is larger than the TCP buffer size, use a - // special send thread to avoid deadlock with the server due - // to blocking behaviour when the buffer is full. Because - // the server will be writing back results to us, it will - // eventually block as well when its TCP buffer gets full, - // as we are blocking an not consuming from it. The result - // is a state where both client and server want to write, - // but block. - if (query.length() > MapiSocket.BLOCK) { - // get a reference to the send thread - if (sendThread == null) - sendThread = new SendThread(out); - // tell it to do some work! - sendThread.runQuery(templ, query); - sendThreadInUse = true; - } else { - // this is a simple call, which is a lot cheaper and will - // always succeed for small queries. - out.writeLine( - (templ[0] == null ? "" : templ[0]) + - query + - (templ[1] == null ? "" : templ[1])); - } + // tell it to do some work! + senderThread.runQuery(templ, query); + } else { + // this is a simple call, which is a lot cheaper and will always succeed for small queries. + protocol.writeNextQuery((templ[0] == null) ? "" : templ[0], query, + (templ[1] == null) ? "" : templ[1]); + } - // go for new results - String tmpLine = in.readLine(); - int linetype = in.getLineType(); - Response res = null; - while (linetype != BufferedMCLReader.PROMPT) { - // each response should start with a start of header - // (or error) - switch (linetype) { - case BufferedMCLReader.SOHEADER: - // make the response object, and fill it - try { - switch (sohp.parse(tmpLine)) { - case StartOfHeaderParser.Q_PARSE: - throw new MCLParseException("Q_PARSE header not allowed here", 1); - case StartOfHeaderParser.Q_TABLE: - case StartOfHeaderParser.Q_PREPARE: { - int id = sohp.getNextAsInt(); - int tuplecount = sohp.getNextAsInt(); - int columncount = sohp.getNextAsInt(); - int rowcount = sohp.getNextAsInt(); - // enforce the maxrows setting - if (maxrows != 0 && tuplecount > maxrows) - tuplecount = maxrows; - res = new ResultSetResponse( - id, - tuplecount, - columncount, - rowcount, - this, - seqnr - ); - // only add this resultset to - // the hashmap if it can possibly - // have an additional datablock - if (rowcount < tuplecount) { - if (rsresponses == null) - rsresponses = new HashMap<Integer, ResultSetResponse>(); - rsresponses.put( - Integer.valueOf(id), - (ResultSetResponse) res - ); - } - } break; - case StartOfHeaderParser.Q_UPDATE: - res = new UpdateResponse( - sohp.getNextAsInt(), // count - sohp.getNextAsString() // key-id - ); + // go for new results + protocol.fetchNextResponseData(); + int nextResponse = protocol.getCurrentServerResponse(); + IResponse res = null; + while (nextResponse != ServerResponses.PROMPT) { + // each response should start with a start of header (or error) + switch (nextResponse) { + case ServerResponses.SOHEADER: + // make the response object, and fill it + int nextStartHeader = protocol.getNextStarterHeader(); + try { + switch (nextStartHeader) { + case StarterHeaders.Q_PARSE: + throw new ProtocolException("Q_PARSE header not allowed here"); + case StarterHeaders.Q_TABLE: + case StarterHeaders.Q_PREPARE: { + res = protocol.getNextResultSetResponse(MonetConnection.this, + ResponseList.this, this.seqnr, this.maxrows); + ResultSetResponse rsreponse = (ResultSetResponse) res; + if (rsresponses == null) { + rsresponses = new HashMap<>(); + } + rsresponses.put(rsreponse.getId(), rsreponse); + } + break; + case StarterHeaders.Q_UPDATE: + res = protocol.getNextUpdateResponse(); break; - case StartOfHeaderParser.Q_SCHEMA: - res = new SchemaResponse(); - break; - case StartOfHeaderParser.Q_TRANS: - boolean ac = sohp.getNextAsString().equals("t") ? true : false; - if (autoCommit && ac) { - addWarning("Server enabled auto commit " + - "mode while local state " + - "already was auto commit.", "01M11" - ); - } - autoCommit = ac; - res = new AutoCommitResponse(ac); + case StarterHeaders.Q_SCHEMA: + res = protocol.getNextSchemaResponse(); break; - case StartOfHeaderParser.Q_BLOCK: { - // a new block of results for a - // response... - int id = sohp.getNextAsInt(); - sohp.getNextAsInt(); // columncount - int rowcount = sohp.getNextAsInt(); - int offset = sohp.getNextAsInt(); - ResultSetResponse t = - rsresponses.get(Integer.valueOf(id)); - if (t == null) { - error = "M0M12!no ResultSetResponse with id " + id + " found"; - break; - } + case StarterHeaders.Q_TRANS: + res = protocol.getNextAutoCommitResponse(); + boolean isAutoCommit = ((AutoCommitResponse) res).isAutocommit(); - DataBlockResponse r = - new DataBlockResponse( - rowcount, // rowcount - t.getRSType() == ResultSet.TYPE_FORWARD_ONLY - ); - - t.addDataBlockResponse(offset, r); - res = r; - } break; + if (MonetConnection.this.getAutoCommit() && isAutoCommit) { + MonetConnection.this.addWarning("Server enabled auto commit mode " + + "while local state already was auto commit.", "01M11"); + } + MonetConnection.this.autoCommit = isAutoCommit; + break; + case StarterHeaders.Q_BLOCK: { + AbstractDataBlockResponse next = protocol.getNextDatablockResponse(rsresponses); + if (next == null) { + error = "M0M12!No ResultSetResponse for a DataBlock found"; + break; + } + res = next; } - } catch (MCLParseException e) { - error = "M0M10!error while parsing start of header:\n" + - e.getMessage() + - " found: '" + tmpLine.charAt(e.getErrorOffset()) + "'" + - " in: \"" + tmpLine + "\"" + - " at pos: " + e.getErrorOffset(); - // flush all the rest - in.waitForPrompt(); - linetype = in.getLineType(); break; } - - // immediately handle errors after parsing - // the header (res may be null) - if (error != null) { - in.waitForPrompt(); - linetype = in.getLineType(); - break; - } - - // here we have a res object, which - // we can start filling - while (res.wantsMore()) { - error = res.addLine( - in.readLine(), - in.getLineType() - ); - if (error != null) { - // right, some protocol violation, - // skip the rest of the result - error = "M0M10!" + error; - in.waitForPrompt(); - linetype = in.getLineType(); + } catch (ProtocolException e) { + error = "M0M10!error while parsing start of header:\n" + e.getMessage() + " found: '" + + protocol.getRemainingStringLine(0).charAt(e.getErrorOffset()) + "'" + + " in: \"" + protocol.getRemainingStringLine(0) + "\"" + " at pos: " + + e.getErrorOffset(); + // flush all the rest + protocol.waitUntilPrompt(); + nextResponse = protocol.getCurrentServerResponse(); + break; + } + // immediately handle errors after parsing the header (res may be null) + if (error != null) { + protocol.waitUntilPrompt(); + nextResponse = protocol.getCurrentServerResponse(); + break; + } + // here we have a res object, which we can start filling + if (res instanceof IIncompleteResponse) { + IIncompleteResponse iter = (IIncompleteResponse) res; + while (iter.wantsMore()) { + try { + protocol.fetchNextResponseData(); + iter.addLines(protocol); + } catch (ProtocolException ex) { + // right, some protocol violation, skip the rest of the result + error = "M0M10!" + ex.getMessage(); + protocol.waitUntilPrompt(); + nextResponse = protocol.getCurrentServerResponse(); break; } } - if (error != null) - break; - // it is of no use to store - // DataBlockReponses, you never want to - // retrieve them directly anyway - if (!(res instanceof DataBlockResponse)) - responses.add(res); + } - // read the next line (can be prompt, new - // result, error, etc.) before we start the - // loop over - tmpLine = in.readLine(); - linetype = in.getLineType(); - break; - case BufferedMCLReader.INFO: - addWarning(tmpLine.substring(1), "01000"); + if (error != null) { + break; + } - // read the next line (can be prompt, new - // result, error, etc.) before we start the - // loop over - tmpLine = in.readLine(); - linetype = in.getLineType(); + // it is of no use to store DataBlockResponses, you never want to retrieve them directly + // anyway + if (!(res instanceof AbstractDataBlockResponse)) { + responses.add(res); + } + // read the next line (can be prompt, new result, error, etc.) before we start the loop over + protocol.fetchNextResponseData(); + nextResponse = protocol.getCurrentServerResponse(); break; - default: // Yeah... in Java this is correct! - // we have something we don't - // expect/understand, let's make it an error - // message - tmpLine = "!M0M10!protocol violation, unexpected line: " + tmpLine; - // don't break; fall through... - case BufferedMCLReader.ERROR: - // read everything till the prompt (should be - // error) we don't know if we ignore some - // garbage here... but the log should reveal - // that - error = in.waitForPrompt(); - linetype = in.getLineType(); - if (error != null) { - error = tmpLine.substring(1) + "\n" + error; - } else { - error = tmpLine.substring(1); - } + case ServerResponses.INFO: + addWarning(protocol.getRemainingStringLine(0), "01000"); + // read the next line (can be prompt, new result, error, etc.) before we start the loop over + protocol.fetchNextResponseData(); + nextResponse = protocol.getCurrentServerResponse(); break; - } + case ServerResponses.ERROR: + // read everything till the prompt (should be error) we don't know if we ignore some + // garbage here... but the log should reveal that + error = protocol.getRemainingStringLine(0); + protocol.waitUntilPrompt(); + nextResponse = protocol.getCurrentServerResponse(); + break; + default: + throw new SQLException("Protocol violation, unexpected line!", "M0M10"); } } - // if we used the sendThread, make sure it has finished - if (sendThreadInUse) { - String tmp = sendThread.getErrors(); + // if we used the senderThread, make sure it has finished + if (senderThread != null) { + String tmp = senderThread.getErrors(); if (tmp != null) { if (error == null) { error = "08000!" + tmp; @@ -2743,13 +1780,15 @@ public class MonetConnection } if (error != null) { SQLException ret = null; - String[] errors = error.split("\n"); - for (int i = 0; i < errors.length; i++) { + String[] errorsList = error.split("\n"); + for (String singleError : errorsList) { SQLException newErr; - if (errors[i].length() >= 6) { - newErr = new SQLException(errors[i].substring(6), errors[i].substring(0, 5)); + String reason = isEmbedded() ? singleError : singleError.substring(6); + String sqlState = isEmbedded() ? "M0M10" : singleError.substring(0, 5); + if (singleError.length() >= 6) { + newErr = new SQLException(reason, sqlState); } else { - newErr = new SQLNonTransientConnectionException(errors[i], "08000"); + newErr = new SQLNonTransientConnectionException(singleError, "08000"); } if (ret == null) { ret = newErr; @@ -2760,7 +1799,7 @@ public class MonetConnection throw ret; } } catch (SocketTimeoutException e) { - close(); // JDBC 4.1 semantics, abort() + this.close(); // JDBC 4.1 semantics, abort() throw new SQLNonTransientConnectionException("connection timed out", "08M33"); } catch (IOException e) { closed = true; @@ -2768,146 +1807,4 @@ public class MonetConnection } } } - // }}} - - /** - * A thread to send a query to the server. When sending large - * amounts of data to a server, the output buffer of the underlying - * communication socket may overflow. In such case the sending - * process blocks. In order to prevent deadlock, it might be - * desirable that the driver as a whole does not block. This thread - * facilitates the prevention of such 'full block', because this - * separate thread only will block.<br /> - * This thread is designed for reuse, as thread creation costs are - * high. - */ - // {{{ SendThread class implementation - static class SendThread extends Thread { - /** The state WAIT represents this thread to be waiting for - * something to do */ - private final static int WAIT = 0; - /** The state QUERY represents this thread to be executing a query */ - private final static int QUERY = 1; - /** The state SHUTDOWN is the final state that ends this thread */ - private final static int SHUTDOWN = -1; - - private String[] templ; - private String query; - private BufferedMCLWriter out; - private String error; - private int state = WAIT; - - final Lock sendLock = new ReentrantLock(); - final Condition queryAvailable = sendLock.newCondition(); - final Condition waiting = sendLock.newCondition(); - - /** - * Constructor which immediately starts this thread and sets it - * into daemon mode. - * - * @param monet the socket to write to - */ - public SendThread(BufferedMCLWriter out) { - super("SendThread"); - setDaemon(true); - this.out = out; - start(); - } - - @Override - public void run() { - sendLock.lock(); - try { - while (true) { - while (state == WAIT) { - try { - queryAvailable.await(); - } catch (InterruptedException e) { - // woken up, eh? - } - } - if (state == SHUTDOWN) - break; - - // state is QUERY here - try { - out.writeLine( - (templ[0] == null ? "" : templ[0]) + - query + - (templ[1] == null ? "" : templ[1])); - } catch (IOException e) { - error = e.getMessage(); - } - - // update our state, and notify, maybe someone is waiting - // for us in throwErrors - state = WAIT; - waiting.signal(); - } - } finally { - sendLock.unlock(); - } - } - - /** - * Starts sending the given query over the given socket. Beware - * that the thread should be finished (can be assured by calling - * throwErrors()) before this method is called! - * - * @param templ the query template - * @param query the query itself - * @throws SQLException if this SendThread is already in use - */ - public void runQuery(String[] templ, String query) throws SQLException { - sendLock.lock(); - try { - if (state != WAIT) - throw new SQLException("SendThread already in use or shutting down!", "M0M03"); - - this.templ = templ; - this.query = query; - - // let the thread know there is some work to do - state = QUERY; - queryAvailable.signal(); - } finally { - sendLock.unlock(); - } - } - - /** - * Returns errors encountered during the sending process. - * - * @return the errors or null if none - */ - public String getErrors() { - sendLock.lock(); - try { - // make sure the thread is in WAIT state, not QUERY - while (state == QUERY) { - try { - waiting.await(); - } catch (InterruptedException e) { - // just try again - } - } - if (state == SHUTDOWN) - error = "SendThread is shutting down"; - } finally { - sendLock.unlock(); - } - return error; - } - - /** - * Requests this SendThread to stop. - */ - public void shutdown() { - sendLock.lock(); - state = SHUTDOWN; - sendLock.unlock(); - this.interrupt(); // break any wait conditions - } - } - // }}} }
--- a/src/main/java/nl/cwi/monetdb/jdbc/MonetDataSource.java +++ b/src/main/java/nl/cwi/monetdb/jdbc/MonetDataSource.java @@ -27,16 +27,18 @@ import java.util.logging.Logger; * * Additionally, pooled connections can be used when using a DataSource. * - * @author Fabian Groffen + * @author Fabian Groffen, Pedro Ferreira * @version 0.1 */ public class MonetDataSource extends MonetWrapper implements DataSource { - private String description; - private int loginTimeout = 0; + private String user; - // insecure, but how to do it better? - private String password; - private String url; + private String password; // insecure, but how to do it better? + private String description = "MonetDB database"; + private String url = "jdbc:monetdb://localhost/"; + private int loginTimeout; + private String directory; + private final MonetDriver driver = new MonetDriver(); // the following properties are also standard: // private String dataSourceName; @@ -44,24 +46,10 @@ public class MonetDataSource extends Mon // private String serverName; // private String role; - - private final MonetDriver driver; + public MonetDataSource() {} /** - * Constructor of a MonetDataSource which uses default settings for a - * connection. You probably want to change this setting using the - * method setURL. - */ - public MonetDataSource() { - description = "MonetDB database"; - url = "jdbc:monetdb://localhost/"; - - driver = new MonetDriver(); - } - - /** - * Attempts to establish a connection with the data source that this - * DataSource object represents. + * Attempts to establish a connection with the data source that this DataSource object represents. * * @return a MonetConnection * @throws SQLException if connecting to the database fails @@ -72,8 +60,7 @@ public class MonetDataSource extends Mon } /** - * Attempts to establish a connection with the data source that this - * DataSource object represents. + * Attempts to establish a connection with the data source that this DataSource object represents. * * @param username the username to use * @param password the password to use @@ -81,23 +68,22 @@ public class MonetDataSource extends Mon * @throws SQLException if connecting to the database fails */ @Override - public Connection getConnection(String username, String password) - throws SQLException - { - if (loginTimeout > 0) { - /// could enable Socket.setSoTimeout(int timeout) here... - } + public Connection getConnection(String username, String password) throws SQLException { Properties props = new Properties(); props.put("user", username); props.put("password", password); - + if (loginTimeout > 0) { + props.put("so_timeout", Integer.toString(loginTimeout)); + } + if(directory != null) { + props.put("embedded", "true"); + props.put("directory", directory); + } return driver.connect(url, props); } - /** - * Gets the maximum time in seconds that this data source can wait while - * attempting to connect to a database. + * Gets the maximum time in seconds that this data source can wait while attempting to connect to a database. * * @return login timeout default is 0 (infinite) */ @@ -107,8 +93,7 @@ public class MonetDataSource extends Mon } /** - * Sets the maximum time in seconds that this data source will wait while - * attempting to connect to a database. + * Sets the maximum time in seconds that this data source will wait while attempting to connect to a database. * * @param seconds the number of seconds to wait before aborting the connect */ @@ -128,14 +113,12 @@ public class MonetDataSource extends Mon } /** - * Sets the log writer for this DataSource object to the given - * java.io.PrintWriter object. + * Sets the log writer for this DataSource object to the given java.io.PrintWriter object. * * @param out a PrintWriter - ignored */ @Override - public void setLogWriter(PrintWriter out) { - } + public void setLogWriter(PrintWriter out) {} /** * Sets the password to use when connecting. There is no getter @@ -179,7 +162,7 @@ public class MonetDataSource extends Mon * * @param url the connection URL */ - public void setDatabaseName(String url) { + public void setURL(String url) { this.url = url; } @@ -202,6 +185,33 @@ public class MonetDataSource extends Mon } /** + * Gets the directory value + * + * @return the directory value + */ + public String getDirectory() { + return directory; + } + + /** + * Sets the directory value, meaning it wil start an embedded connection + * + * @param directory The directory location + */ + public void setDirectory(String directory) { + this.directory = directory; + } + + /** + * Gets the embedded connection directory. If not, then a MAPI connection will be created instead. + * + * @return If the connection will be embedded. If not, then a MAPI connection will be created instead. + */ + public boolean isEmbedded() { + return directory != null; + } + + /** * Return the parent Logger of all the Loggers used by this data * source. This should be the Logger farthest from the root Logger * that is still an ancestor of all of the Loggers used by this data @@ -210,8 +220,7 @@ public class MonetDataSource extends Mon * may be the root Logger. * * @return the parent Logger for this data source - * @throws SQLFeatureNotSupportedException if the data source does - * not use java.util.logging + * @throws SQLFeatureNotSupportedException if the data source does not use java.util.logging */ @Override public Logger getParentLogger() throws SQLFeatureNotSupportedException {
--- a/src/main/java/nl/cwi/monetdb/jdbc/MonetDatabaseMetaData.java +++ b/src/main/java/nl/cwi/monetdb/jdbc/MonetDatabaseMetaData.java @@ -17,8 +17,6 @@ import java.sql.ResultSetMetaData; import java.sql.RowIdLifetime; import java.sql.Types; -import java.util.ArrayList; - /** * A DatabaseMetaData object suitable for the MonetDB database. * @@ -89,7 +87,7 @@ public class MonetDatabaseMetaData exten * we set it to close (and free server resources) when the ResultSet object is closed by the caller. */ private ResultSet executeMetaDataQuery(String query) throws SQLException { - Statement stmt = null; + Statement stmt; ResultSet rs = null; stmt = con.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY); if (stmt != null) { @@ -182,8 +180,6 @@ public class MonetDatabaseMetaData exten * By contrast, the method nullsAreSortedAtStart indicates whether NULL values are sorted at the beginning regardless of sort order. * * @return true because MonetDB shows NULL values at the beginning upon ORDER BY .. ASC - * - * @return negative of nullsAreSortedHigh() * @see #nullsAreSortedHigh() */ @Override @@ -1698,12 +1694,8 @@ public class MonetDatabaseMetaData exten * @throws SQLException if a database access error occurs */ @Override - public ResultSet getProcedures( - String catalog, - String schemaPattern, - String procedureNamePattern - ) throws SQLException - { + public ResultSet getProcedures(String catalog, String schemaPattern, String procedureNamePattern) + throws SQLException { StringBuilder query = new StringBuilder(980); query.append("SELECT cast(null as varchar(1)) AS \"PROCEDURE_CAT\", " + "\"schemas\".\"name\" AS \"PROCEDURE_SCHEM\", " + @@ -1804,12 +1796,8 @@ public class MonetDatabaseMetaData exten * @see #getSearchStringEscape */ @Override - public ResultSet getProcedureColumns( - String catalog, - String schemaPattern, - String procedureNamePattern, - String columnNamePattern - ) throws SQLException { + public ResultSet getProcedureColumns(String catalog, String schemaPattern, String procedureNamePattern, + String columnNamePattern) throws SQLException { StringBuilder query = new StringBuilder(2900); query.append("SELECT cast(null as varchar(1)) AS \"PROCEDURE_CAT\", " + "\"schemas\".\"name\" AS \"PROCEDURE_SCHEM\", " + @@ -1936,13 +1924,8 @@ public class MonetDatabaseMetaData exten * @throws SQLException if a database-access error occurs. */ @Override - public ResultSet getTables( - String catalog, - String schemaPattern, - String tableNamePattern, - String types[] - ) throws SQLException - { + public ResultSet getTables(String catalog, String schemaPattern, String tableNamePattern, String types[]) + throws SQLException { // as of Jul2015 release the sys.tables.type values (0 through 6) is extended with new values 10, 11, 20, and 30 (for system and temp tables/views). // as of Jul2015 release we also have a new table: sys.table_types with names for the new table types // for correct behavior we need to know if the server is using the old (pre Jul2015) or new sys.tables.type values @@ -2039,9 +2022,7 @@ public class MonetDatabaseMetaData exten * @throws SQLException if a database error occurs */ @Override - public ResultSet getSchemas(String catalog, String schemaPattern) - throws SQLException - { + public ResultSet getSchemas(String catalog, String schemaPattern) throws SQLException { StringBuilder query = new StringBuilder(170); query.append("SELECT \"name\" AS \"TABLE_SCHEM\", " + "cast(null as char(1)) AS \"TABLE_CATALOG\" " + @@ -2187,13 +2168,8 @@ public class MonetDatabaseMetaData exten * @see #getSearchStringEscape */ @Override - public ResultSet getColumns( - String catalog, - String schemaPattern, - String tableNamePattern, - String columnNamePattern - ) throws SQLException - { + public ResultSet getColumns(String catalog, String schemaPattern, String tableNamePattern, String columnNamePattern) + throws SQLException { StringBuilder query = new StringBuilder(2450); query.append("SELECT cast(null as char(1)) AS \"TABLE_CAT\", " + "\"schemas\".\"name\" AS \"TABLE_SCHEM\", " + @@ -2278,13 +2254,8 @@ public class MonetDatabaseMetaData exten * @throws SQLException if a database error occurs */ @Override - public ResultSet getColumnPrivileges( - String catalog, - String schemaPattern, - String tableNamePattern, - String columnNamePattern - ) throws SQLException - { + public ResultSet getColumnPrivileges(String catalog, String schemaPattern, String tableNamePattern, + String columnNamePattern) throws SQLException { StringBuilder query = new StringBuilder(1100); query.append("SELECT cast(null as char(1)) AS \"TABLE_CAT\", " + "\"schemas\".\"name\" AS \"TABLE_SCHEM\", " + @@ -2367,12 +2338,8 @@ public class MonetDatabaseMetaData exten * @throws SQLException if a database error occurs */ @Override - public ResultSet getTablePrivileges( - String catalog, - String schemaPattern, - String tableNamePattern - ) throws SQLException - { + public ResultSet getTablePrivileges(String catalog, String schemaPattern, String tableNamePattern) + throws SQLException { StringBuilder query = new StringBuilder(1000); query.append("SELECT cast(null as char(1)) AS \"TABLE_CAT\", " + "\"schemas\".\"name\" AS \"TABLE_SCHEM\", " + @@ -2456,14 +2423,8 @@ public class MonetDatabaseMetaData exten * @throws SQLException if a database error occurs */ @Override - public ResultSet getBestRowIdentifier( - String catalog, - String schema, - String table, - int scope, - boolean nullable - ) throws SQLException - { + public ResultSet getBestRowIdentifier(String catalog, String schema, String table, int scope, boolean nullable) + throws SQLException { StringBuilder query = new StringBuilder(1500); query.append("SELECT CAST(").append(DatabaseMetaData.bestRowSession).append(" AS smallint) AS \"SCOPE\", " + "\"columns\".\"name\" AS \"COLUMN_NAME\", " + @@ -2536,12 +2497,7 @@ public class MonetDatabaseMetaData exten * @throws SQLException if a database error occurs */ @Override - public ResultSet getVersionColumns( - String catalog, - String schema, - String table - ) throws SQLException - { + public ResultSet getVersionColumns(String catalog, String schema, String table) throws SQLException { // MonetDB currently does not have columns which update themselves, so return an empty ResultSet String query = "SELECT CAST(0 as smallint) AS \"SCOPE\", " + @@ -2579,12 +2535,7 @@ public class MonetDatabaseMetaData exten * @throws SQLException if a database error occurs */ @Override - public ResultSet getPrimaryKeys( - String catalog, - String schema, - String table - ) throws SQLException - { + public ResultSet getPrimaryKeys(String catalog, String schema, String table) throws SQLException { StringBuilder query = new StringBuilder(600); query.append("SELECT cast(null AS varchar(1)) AS \"TABLE_CAT\", " + "\"schemas\".\"name\" AS \"TABLE_SCHEM\", " + @@ -2618,7 +2569,6 @@ public class MonetDatabaseMetaData exten return executeMetaDataQuery(query.toString()); } - private final static String keyQuery = "SELECT cast(null AS varchar(1)) AS \"PKTABLE_CAT\", " + "\"pkschema\".\"name\" AS \"PKTABLE_SCHEM\", " + @@ -2713,9 +2663,7 @@ public class MonetDatabaseMetaData exten * @throws SQLException if a database error occurs */ @Override - public ResultSet getImportedKeys(String catalog, String schema, String table) - throws SQLException - { + public ResultSet getImportedKeys(String catalog, String schema, String table) throws SQLException { StringBuilder query = new StringBuilder(keyQuery.length() + 250); query.append(keyQuery); @@ -2796,9 +2744,7 @@ public class MonetDatabaseMetaData exten * @throws SQLException if a database error occurs */ @Override - public ResultSet getExportedKeys(String catalog, String schema, String table) - throws SQLException - { + public ResultSet getExportedKeys(String catalog, String schema, String table) throws SQLException { StringBuilder query = new StringBuilder(keyQuery.length() + 250); query.append(keyQuery); @@ -2885,15 +2831,8 @@ public class MonetDatabaseMetaData exten * @see #getImportedKeys */ @Override - public ResultSet getCrossReference( - String pcatalog, - String pschema, - String ptable, - String fcatalog, - String fschema, - String ftable - ) throws SQLException - { + public ResultSet getCrossReference(String pcatalog, String pschema, String ptable, String fcatalog, String fschema, + String ftable) throws SQLException { StringBuilder query = new StringBuilder(keyQuery.length() + 350); query.append(keyQuery); @@ -2975,7 +2914,7 @@ public class MonetDatabaseMetaData exten * </OL> * * @return ResultSet each row is a SQL type description - * @throws Exception if the developer made a Boo-Boo + * @throws SQLException if the developer made a Boo-Boo */ @Override public ResultSet getTypeInfo() throws SQLException { @@ -3066,14 +3005,8 @@ public class MonetDatabaseMetaData exten * @throws SQLException if a database occurs */ @Override - public ResultSet getIndexInfo( - String catalog, - String schema, - String table, - boolean unique, - boolean approximate - ) throws SQLException - { + public ResultSet getIndexInfo(String catalog, String schema, String table, boolean unique, boolean approximate) + throws SQLException { String table_row_count = "0"; if (!approximate && schema != null && table != null && schema.length() > 0 && table.length() > 0) { @@ -3160,8 +3093,7 @@ public class MonetDatabaseMetaData exten /** - * Does the database support the concurrency type in combination - * with the given result set type? + * Does the database support the concurrency type in combination with the given result set type? * * @param type - defined in java.sql.ResultSet * @param concurrency - type defined in java.sql.ResultSet @@ -3169,9 +3101,7 @@ public class MonetDatabaseMetaData exten * @throws SQLException - if a database access error occurs */ @Override - public boolean supportsResultSetConcurrency(int type, int concurrency) - throws SQLException - { + public boolean supportsResultSetConcurrency(int type, int concurrency) throws SQLException { // These combinations are not supported! if (type == ResultSet.TYPE_SCROLL_SENSITIVE) return false; @@ -3258,16 +3188,10 @@ public class MonetDatabaseMetaData exten * user-generated reference type of the SELF_REFERENCING_COLUMN of a structured type as defined * in java.sql.Types (null if DATA_TYPE is not DISTINCT or not STRUCT with REFERENCE_GENERATION = USER_DEFINED) * - * @throws SQLException */ @Override - public ResultSet getUDTs( - String catalog, - String schemaPattern, - String typeNamePattern, - int[] types - ) throws SQLException - { + public ResultSet getUDTs(String catalog, String schemaPattern, String typeNamePattern, int[] types) + throws SQLException { StringBuilder query = new StringBuilder(990); if (types != null && types.length > 0) { query.append("SELECT * FROM ("); @@ -3341,8 +3265,7 @@ public class MonetDatabaseMetaData exten /** * Retrieves whether this database supports savepoints. * - * @return <code>true</code> if savepoints are supported; - * <code>false</code> otherwise + * @return <code>true</code> if savepoints are supported; <code>false</code> otherwise */ @Override public boolean supportsSavepoints() { @@ -3350,11 +3273,9 @@ public class MonetDatabaseMetaData exten } /** - * Retrieves whether this database supports named parameters to callable - * statements. + * Retrieves whether this database supports named parameters to callable statements. * - * @return <code>true</code> if named parameters are supported; - * <code>false</code> otherwise + * @return <code>true</code> if named parameters are supported; <code>false</code> otherwise */ @Override public boolean supportsNamedParameters() { @@ -3363,12 +3284,10 @@ public class MonetDatabaseMetaData exten /** * Retrieves whether it is possible to have multiple <code>ResultSet</code> objects - * returned from a <code>CallableStatement</code> object - * simultaneously. + * returned from a <code>CallableStatement</code> object simultaneously. * * @return <code>true</code> if a <code>CallableStatement</code> object - * can return multiple <code>ResultSet</code> objects - * simultaneously; <code>false</code> otherwise + * can return multiple <code>ResultSet</code> objects simultaneously; <code>false</code> otherwise */ @Override public boolean supportsMultipleOpenResults() { @@ -3376,11 +3295,10 @@ public class MonetDatabaseMetaData exten } /** - * Retrieves whether auto-generated keys can be retrieved after - * a statement has been executed. + * Retrieves whether auto-generated keys can be retrieved after a statement has been executed. * - * @return <code>true</code> if auto-generated keys can be retrieved - * after a statement has executed; <code>false</code> otherwise + * @return <code>true</code> if auto-generated keys can be retrieved after a statement has executed; + * <code>false</code> otherwise */ @Override public boolean supportsGetGeneratedKeys() { @@ -3427,17 +3345,10 @@ public class MonetDatabaseMetaData exten * @throws SQLException if a database access error occurs */ @Override - public ResultSet getSuperTypes( - String catalog, - String schemaPattern, - String typeNamePattern - ) throws SQLException - { - String query = - "SELECT cast(null as char(1)) AS \"TYPE_CAT\", '' AS \"TYPE_SCHEM\", '' AS \"TYPE_NAME\", " + + public ResultSet getSuperTypes(String catalog, String schemaPattern, String typeNamePattern) throws SQLException { + String query = "SELECT cast(null as char(1)) AS \"TYPE_CAT\", '' AS \"TYPE_SCHEM\", '' AS \"TYPE_NAME\", " + "cast(null as char(1)) AS \"SUPERTYPE_CAT\", '' AS \"SUPERTYPE_SCHEM\", '' AS \"SUPERTYPE_NAME\" " + "WHERE 1 = 0"; - return executeMetaDataQuery(query); } @@ -3474,17 +3385,9 @@ public class MonetDatabaseMetaData exten * @throws SQLException if a database access error occurs */ @Override - public ResultSet getSuperTables( - String catalog, - String schemaPattern, - String tableNamePattern - ) throws SQLException - { - String query = - "SELECT cast(null as char(1)) AS \"TABLE_CAT\", " + - "'' AS \"TABLE_SCHEM\", '' AS \"TABLE_NAME\", '' AS \"SUPERTABLE_NAME\" " + - "WHERE 1 = 0"; - + public ResultSet getSuperTables(String catalog, String schemaPattern, String tableNamePattern) throws SQLException { + String query = "SELECT cast(null as char(1)) AS \"TABLE_CAT\", " + + "'' AS \"TABLE_SCHEM\", '' AS \"TABLE_NAME\", '' AS \"SUPERTABLE_NAME\" WHERE 1 = 0"; return executeMetaDataQuery(query); } @@ -3558,15 +3461,9 @@ public class MonetDatabaseMetaData exten * @throws SQLException if a database access error occurs */ @Override - public ResultSet getAttributes( - String catalog, - String schemaPattern, - String typeNamePattern, - String attributeNamePattern - ) throws SQLException - { - String query = - "SELECT cast(null as char(1)) AS \"TYPE_CAT\", '' AS \"TYPE_SCHEM\", '' AS \"TYPE_NAME\", " + + public ResultSet getAttributes(String catalog, String schemaPattern, String typeNamePattern, + String attributeNamePattern) throws SQLException { + String query = "SELECT cast(null as char(1)) AS \"TYPE_CAT\", '' AS \"TYPE_SCHEM\", '' AS \"TYPE_NAME\", " + "'' AS \"ATTR_NAME\", CAST(0 as int) AS \"DATA_TYPE\", '' AS \"ATTR_TYPE_NAME\", CAST(0 as int) AS \"ATTR_SIZE\", " + "CAST(0 as int) AS \"DECIMAL_DIGITS\", CAST(0 as int) AS \"NUM_PREC_RADIX\", CAST(0 as int) AS \"NULLABLE\", " + "'' AS \"REMARKS\", '' AS \"ATTR_DEF\", CAST(0 as int) AS \"SQL_DATA_TYPE\", " + @@ -3575,15 +3472,13 @@ public class MonetDatabaseMetaData exten "'' AS \"SCOPE_CATALOG\", '' AS \"SCOPE_SCHEMA\", '' AS \"SCOPE_TABLE\", " + "CAST(0 as smallint) AS \"SOURCE_DATA_TYPE\" " + "WHERE 1 = 0"; - return executeMetaDataQuery(query); } /** * Retrieves whether this database supports the given result set holdability. * - * @param holdability one of the following constants: - * <code>ResultSet.HOLD_CURSORS_OVER_COMMIT</code> or + * @param holdability one of the following constants: <code>ResultSet.HOLD_CURSORS_OVER_COMMIT</code> or * <code>ResultSet.CLOSE_CURSORS_AT_COMMIT</code> * @return <code>true</code> if so; <code>false</code> otherwise * @see Connection @@ -3596,12 +3491,10 @@ public class MonetDatabaseMetaData exten } /** - * Retrieves the default holdability of this <code>ResultSet</code> - * object. + * Retrieves the default holdability of this <code>ResultSet</code> object. * - * @return the default holdability; either - * <code>ResultSet.HOLD_CURSORS_OVER_COMMIT</code> or - * <code>ResultSet.CLOSE_CURSORS_AT_COMMIT</code> + * @return the default holdability; either <code>ResultSet.HOLD_CURSORS_OVER_COMMIT</code> or + * <code>ResultSet.CLOSE_CURSORS_AT_COMMIT</code> */ @Override public int getResultSetHoldability() { @@ -3679,9 +3572,7 @@ public class MonetDatabaseMetaData exten /** * Indicates whether the SQLSTATEs returned by <code>SQLException.getSQLState</code> * is X/Open (now known as Open Group) SQL CLI or SQL:2003. - * @return the type of SQLSTATEs, one of: - * sqlStateXOpen or - * sqlStateSQL + * @return the type of SQLSTATEs, one of: sqlStateXOpen or sqlStateSQL */ @Override public int getSQLStateType() { @@ -3690,8 +3581,7 @@ public class MonetDatabaseMetaData exten } /** - * Indicates whether updates made to a LOB are made on a copy or directly - * to the LOB. + * Indicates whether updates made to a LOB are made on a copy or directly to the LOB. * @return <code>true</code> if updates are made to a copy of the LOB; * <code>false</code> if updates are made directly to the LOB */ @@ -3716,9 +3606,8 @@ public class MonetDatabaseMetaData exten //== 1.6 methods (JDBC 4) /** - * Indicates whether or not this data source supports the SQL ROWID - * type, and if so the lifetime for which a RowId object remains - * valid. + * Indicates whether or not this data source supports the SQL ROWID type, and if so the lifetime for which a RowId + * object remains valid. * * @return ROWID_UNSUPPORTED for now */ @@ -3729,8 +3618,7 @@ public class MonetDatabaseMetaData exten } /** - * Get the schema names available in this database. The results - * are ordered by schema name. + * Get the schema names available in this database. The results are ordered by schema name. * * <P>The schema column is: * <OL> @@ -3738,8 +3626,7 @@ public class MonetDatabaseMetaData exten * <LI><B>TABLE_CATALOG</B> String => catalog name (may be null) * </OL> * - * @return ResultSet each row has a single String column that is a - * schema name + * @return ResultSet each row has a single String column that is a schema name * @throws SQLException if a database error occurs */ @Override @@ -3748,8 +3635,8 @@ public class MonetDatabaseMetaData exten } /** - * Retrieves whether this database supports invoking user-defined or - * vendor functions using the stored procedure escape syntax. + * Retrieves whether this database supports invoking user-defined or vendor functions using the stored procedure + * escape syntax. * * @return true if so; false otherwise */ @@ -3792,8 +3679,8 @@ public class MonetDatabaseMetaData exten * * The ResultSet is sorted by the NAME column * - * @return A ResultSet object; each row is a supported client info - * property, none in case of MonetDB's current JDBC driver + * @return A ResultSet object; each row is a supported client info property, none in case of MonetDB's current JDBC + * driver * @throws SQLException if a database access error occurs */ @Override @@ -3857,12 +3744,8 @@ public class MonetDatabaseMetaData exten * @throws SQLException if a database access error occurs */ @Override - public ResultSet getFunctions( - String catalog, - String schemaPattern, - String functionNamePattern) - throws SQLException - { + public ResultSet getFunctions(String catalog, String schemaPattern, String functionNamePattern) + throws SQLException { StringBuilder query = new StringBuilder(800); query.append("SELECT cast(null as varchar(1)) AS \"FUNCTION_CAT\", " + "\"schemas\".\"name\" AS \"FUNCTION_SCHEM\", " + @@ -3957,13 +3840,8 @@ public class MonetDatabaseMetaData exten * @throws SQLException - if a database access error occurs */ @Override - public ResultSet getFunctionColumns( - String catalog, - String schemaPattern, - String functionNamePattern, - String columnNamePattern) - throws SQLException - { + public ResultSet getFunctionColumns(String catalog, String schemaPattern, String functionNamePattern, + String columnNamePattern) throws SQLException { StringBuilder query = new StringBuilder(2600); query.append("SELECT DISTINCT CAST(null as char(1)) AS \"FUNCTION_CAT\", " + "\"schemas\".\"name\" AS \"FUNCTION_SCHEM\", " + @@ -4056,13 +3934,8 @@ public class MonetDatabaseMetaData exten * @throws SQLException if a database access error occurs */ @Override - public ResultSet getPseudoColumns( - String catalog, - String schemaPattern, - String tableNamePattern, - String columnNamePattern) - throws SQLException - { + public ResultSet getPseudoColumns(String catalog, String schemaPattern, String tableNamePattern, + String columnNamePattern) throws SQLException { // MonetDB currently does not support pseudo or hidden columns, so return an empty ResultSet String query = "SELECT CAST(null as char(1)) AS \"TABLE_CAT\", " +
--- a/src/main/java/nl/cwi/monetdb/jdbc/MonetDriver.java.in +++ b/src/main/java/nl/cwi/monetdb/jdbc/MonetDriver.java.in @@ -8,6 +8,14 @@ package nl.cwi.monetdb.jdbc; +import nl.cwi.monetdb.mcl.connection.MCLException; +import nl.cwi.monetdb.mcl.connection.mapi.MapiConnection; +import nl.cwi.monetdb.mcl.connection.mapi.MapiLanguage; +import nl.cwi.monetdb.mcl.protocol.ProtocolException; + +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.net.SocketException; import java.net.URI; import java.net.URISyntaxException; import java.sql.Connection; @@ -18,10 +26,8 @@ import java.sql.SQLException; import java.sql.SQLFeatureNotSupportedException; import java.sql.SQLNonTransientConnectionException; import java.sql.Types; -import java.util.ArrayList; -import java.util.List; +import java.util.*; import java.util.Map.Entry; -import java.util.Properties; import java.util.logging.Logger; /** @@ -41,21 +47,23 @@ import java.util.logging.Logger; * where [:<port>] denotes that a port is optional. If not * given the default (@JDBC_DEF_PORT@) will be used. * - * @author Fabian Groffen + * @author Fabian Groffen, Pedro Ferreira * @version @JDBC_MAJOR@.@JDBC_MINOR@ (@JDBC_VER_SUFFIX@) */ -final public class MonetDriver implements Driver { - // the url kind will be jdbc:monetdb://<host>[:<port>]/<database> +public final class MonetDriver implements Driver { + // the url kind will be jdbc:monetdb://<host>[:<port>]/<database> (in a MAPI connection) + // the url kind will be jdbc:monetdb:embedded:<directory> (in an Embedded connection) // Chapter 9.2.1 from Sun JDBC 3.0 specification /** The prefix of a MonetDB url */ - private static final String MONETURL = "jdbc:monetdb://"; + private static final String MONETURL = "jdbc:monetdb:"; /** Major version of this driver */ private static final int DRIVERMAJOR = @JDBC_MAJOR@; /** Minor version of this driver */ private static final int DRIVERMINOR = @JDBC_MINOR@; + /** Version suffix string */ private static final String DRIVERVERSIONSUFFIX = - "@JDBC_VER_SUFFIX@ based on MCL v@MCL_MAJOR@.@MCL_MINOR@"; + "@JDBC_VER_SUFFIX@ based on MCL v@MCL_MAJOR@.@MCL_MINOR@"; // We're not fully compliant, but what we support is compliant /** Whether this driver is JDBC compliant or not */ private static final boolean MONETJDBCCOMPLIANT = false; @@ -63,6 +71,7 @@ final public class MonetDriver implement /** MonetDB default port to connect to */ private static final String PORT = "@JDBC_DEF_PORT@"; + private static Class embeddedConnectionClass = null; // initialize this class: register it at the DriverManager // Chapter 9.2 from Sun JDBC 3.0 specification @@ -90,86 +99,6 @@ final public class MonetDriver implement } /** - * Attempts to make a database connection to the given URL. The driver - * should return "null" if it realizes it is the wrong kind of driver to - * connect to the given URL. This will be common, as when the JDBC driver - * manager is asked to connect to a given URL it passes the URL to each - * loaded driver in turn. - * - * The driver should throw an SQLException if it is the right driver to - * connect to the given URL but has trouble connecting to the database. - * - * The java.util.Properties argument can be used to pass arbitrary string - * tag/value pairs as connection arguments. Normally at least "user" and - * "password" properties should be included in the Properties object. - * - * @param url the URL of the database to which to connect - * @param info a list of arbitrary string tag/value pairs as connection - * arguments. Normally at least a "user" and "password" property - * should be included - * @return a Connection object that represents a connection to the URL - * @throws SQLException if a database access error occurs - */ - @Override - public Connection connect(String url, Properties info) - throws SQLException - { - int tmp; - Properties props = new Properties(); - // set the optional properties and their defaults here - props.put("port", PORT); - props.put("debug", "false"); - props.put("language", "sql"); // mal, sql, <future> - props.put("so_timeout", "0"); - - props.putAll(info); - info = props; - - // url should be of style jdbc:monetdb://<host>/<database> - if (!acceptsURL(url)) - throw new SQLNonTransientConnectionException("Invalid URL: it does not start with: " + MONETURL, "08M26"); - - // remove leading "jdbc:" so the rest is a valid hierarchical URI - URI uri; - try { - uri = new URI(url.substring(5)); - } catch (URISyntaxException e) { - throw new SQLNonTransientConnectionException(e.toString(), "08M26"); - } - - String uri_host = uri.getHost(); - if (uri_host == null) - throw new SQLNonTransientConnectionException("Invalid URL: no hostname given or unparsable in '" + url + "'", "08M26"); - info.put("host", uri_host); - - int uri_port = uri.getPort(); - if (uri_port > 0) - info.put("port", "" + uri_port); - - // check the database - String uri_path = uri.getPath(); - if (uri_path != null && uri_path.length() != 0) { - uri_path = uri_path.substring(1); - if (!uri_path.trim().isEmpty()) - info.put("database", uri_path); - } - - String uri_query = uri.getQuery(); - if (uri_query != null) { - // handle additional arguments - String args[] = uri_query.split("&"); - for (int i = 0; i < args.length; i++) { - tmp = args[i].indexOf('='); - if (tmp > 0) - info.put(args[i].substring(0, tmp), args[i].substring(tmp + 1)); - } - } - - // finally return the Connection as requested - return new MonetConnection(info); - } - - /** * Retrieves the driver's major version number. Initially this should be 1. * * @return this driver's major version number @@ -200,22 +129,20 @@ final public class MonetDriver implement * getPropertyInfo method. * * @param url the URL of the database to which to connect - * @param info a proposed list of tag/value pairs that will be sent on - * connect open - * @return an array of DriverPropertyInfo objects describing possible - * properties. This array may be an empty array if no properties - * are required. + * @param info a proposed list of tag/value pairs that will be sent on connect open + * @return an array of DriverPropertyInfo objects describing possible properties. This array may be an empty array + * if no properties are required. */ @Override public DriverPropertyInfo[] getPropertyInfo(String url, Properties info) { if (!acceptsURL(url)) return null; - List<DriverPropertyInfo> props = new ArrayList<DriverPropertyInfo>(); + List<DriverPropertyInfo> props = new ArrayList<>(); DriverPropertyInfo prop = new DriverPropertyInfo("user", info.getProperty("user")); prop.required = true; - prop.description = "The user loginname to use when authenticating on the database server"; + prop.description = "The user name to use when authenticating on the database server"; props.add(prop); prop = new DriverPropertyInfo("password", info.getProperty("password")); @@ -223,14 +150,29 @@ final public class MonetDriver implement prop.description = "The password to use when authenticating on the database server"; props.add(prop); - prop = new DriverPropertyInfo("debug", "false"); + prop = new DriverPropertyInfo("hash", ""); prop.required = false; - prop.description = "Whether or not to create a log file for debugging purposes"; + prop.description = "Force the use of the given hash algorithm during challenge response (one of SHA1, MD5, plain) (MAPI connection only)"; + props.add(prop); + + prop = new DriverPropertyInfo("so_timeout", "0"); + prop.required = false; + prop.description = "Defines the maximum time to wait in milliseconds on a blocking read socket call (MAPI connection only)"; // this corresponds to the Connection.setNetworkTimeout() method introduced in JDBC 4.1 props.add(prop); - prop = new DriverPropertyInfo("logfile", ""); + prop = new DriverPropertyInfo("follow_redirects", "true"); + prop.required = false; + prop.description = "Whether redirects issued by the server should be followed (MAPI connection only)"; + props.add(prop); + + prop = new DriverPropertyInfo("treat_blob_as_binary", "false"); prop.required = false; - prop.description = "The filename to write the debug log to. Only takes effect if debug is set to true. If the file exists, an incrementing number is added, till the filename is unique."; + prop.description = "Whether BLOBs on the server should be treated as LONGVARBINARY types, thus mapped to byte[] (MAPI connection only)"; + props.add(prop); + + prop = new DriverPropertyInfo("treat_clob_as_longvarchar", "false"); + prop.required = false; + prop.description = "Whether CLOBs on the server should be treated as LONGVARCHAR types, thus mapped to String (MAPI connection only)"; props.add(prop); prop = new DriverPropertyInfo("language", "sql"); @@ -238,26 +180,6 @@ final public class MonetDriver implement prop.description = "What language to use for MonetDB conversations (experts only)"; props.add(prop); - prop = new DriverPropertyInfo("hash", ""); - prop.required = false; - prop.description = "Force the use of the given hash algorithm during challenge response (one of SHA1, MD5, plain)"; - props.add(prop); - - prop = new DriverPropertyInfo("follow_redirects", "true"); - prop.required = false; - prop.description = "Whether redirects issued by the server should be followed"; - props.add(prop); - - prop = new DriverPropertyInfo("treat_blob_as_binary", "false"); - prop.required = false; - prop.description = "Whether BLOBs on the server should be treated as BINARY types, thus mapped to byte[]"; - props.add(prop); - - prop = new DriverPropertyInfo("so_timeout", "0"); - prop.required = false; - prop.description = "Defines the maximum time to wait in milliseconds on a blocking read socket call"; // this corresponds to the Connection.setNetworkTimeout() method introduced in JDBC 4.1 - props.add(prop); - DriverPropertyInfo[] dpi = new DriverPropertyInfo[props.size()]; return props.toArray(dpi); } @@ -285,100 +207,76 @@ final public class MonetDriver implement return MONETJDBCCOMPLIANT; } - /** - * Return the parent Logger of all the Loggers used by this data source. - * This should be the Logger farthest from the root Logger that is - * still an ancestor of all of the Loggers used by this data source. - * Configuring this Logger will affect all of the log messages - * generated by the data source. - * In the worst case, this may be the root Logger. - * - * @return the parent Logger for this data source - * @throws SQLFeatureNotSupportedException if the data source does - * not use java.util.logging - * @since 1.7 - */ - @Override - public Logger getParentLogger() throws SQLFeatureNotSupportedException { - throw new SQLFeatureNotSupportedException("java.util.logging not in use", "0A000"); - } - //== end methods of interface driver - /** A static Map containing the mapping between MonetDB types and Java SQL types */ /* use SELECT sqlname, * FROM sys.types order by 1, id; to view all MonetDB types */ /* see http://docs.oracle.com/javase/7/docs/api/java/sql/Types.html to view all supported java SQL types */ - private static java.util.Map<String, Integer> typeMap = new java.util.HashMap<String, Integer>(); + private static Map<String, Integer> typeMap = new HashMap<>(); static { // fill the typeMap once - // typeMap.put("any", Integer.valueOf(Types.???)); - typeMap.put("bigint", Integer.valueOf(Types.BIGINT)); - typeMap.put("blob", Integer.valueOf(Types.BLOB)); - typeMap.put("boolean", Integer.valueOf(Types.BOOLEAN)); - typeMap.put("char", Integer.valueOf(Types.CHAR)); - typeMap.put("clob", Integer.valueOf(Types.CLOB)); - typeMap.put("date", Integer.valueOf(Types.DATE)); - typeMap.put("decimal", Integer.valueOf(Types.DECIMAL)); - typeMap.put("double", Integer.valueOf(Types.DOUBLE)); - // typeMap.put("geometry", Integer.valueOf(Types.???)); - // typeMap.put("geometrya", Integer.valueOf(Types.???)); - typeMap.put("hugeint", Integer.valueOf(Types.NUMERIC)); - typeMap.put("inet", Integer.valueOf(Types.VARCHAR)); - typeMap.put("int", Integer.valueOf(Types.INTEGER)); - typeMap.put("json", Integer.valueOf(Types.VARCHAR)); - // typeMap.put("mbr", Integer.valueOf(Types.???)); - typeMap.put("month_interval", Integer.valueOf(Types.INTEGER)); - typeMap.put("oid", Integer.valueOf(Types.BIGINT)); - // typeMap.put("ptr", Integer.valueOf(Types.???)); - typeMap.put("real", Integer.valueOf(Types.REAL)); - typeMap.put("sec_interval", Integer.valueOf(Types.DECIMAL)); - typeMap.put("smallint", Integer.valueOf(Types.SMALLINT)); - // typeMap.put("table", Integer.valueOf(Types.???)); - typeMap.put("time", Integer.valueOf(Types.TIME)); - typeMap.put("timestamp", Integer.valueOf(Types.TIMESTAMP)); - typeMap.put("timestamptz", Integer.valueOf(Types.TIMESTAMP)); -// new in Java 8: Types.TIMESTAMP_WITH_TIMEZONE (value 2014). Can't use it yet as we compile for java 7 - typeMap.put("timetz", Integer.valueOf(Types.TIME)); -// new in Java 8: Types.TIME_WITH_TIMEZONE (value 2013). Can't use it yet as we compile for java 7 - typeMap.put("tinyint", Integer.valueOf(Types.TINYINT)); - typeMap.put("url", Integer.valueOf(Types.VARCHAR)); - typeMap.put("uuid", Integer.valueOf(Types.VARCHAR)); - typeMap.put("varchar", Integer.valueOf(Types.VARCHAR)); - typeMap.put("wrd", Integer.valueOf(Types.BIGINT)); // keep it in for older MonetDB servers + // typeMap.put("any", Types.???); + typeMap.put("bigint", Types.BIGINT); + typeMap.put("blob", Types.BLOB); + typeMap.put("boolean", Types.BOOLEAN); + typeMap.put("char", Types.CHAR); + typeMap.put("clob", Types.CLOB); + typeMap.put("date", Types.DATE); + typeMap.put("decimal", Types.DECIMAL); + typeMap.put("double", Types.DOUBLE); + typeMap.put("geometry", Types.OTHER); + typeMap.put("geometrya", Types.OTHER); + typeMap.put("hugeint", Types.NUMERIC); + typeMap.put("inet", Types.OTHER); + typeMap.put("int", Types.INTEGER); + typeMap.put("json", Types.OTHER); + // typeMap.put("mbr", Types.???); + typeMap.put("month_interval", Types.INTEGER); + // typeMap.put("oid", Types.BIGINT); + // typeMap.put("ptr", Types.???); + typeMap.put("real", Types.REAL); + typeMap.put("sec_interval", Types.BIGINT); + typeMap.put("smallint", Types.SMALLINT); + // typeMap.put("table", Types.???); + typeMap.put("time", Types.TIME); + typeMap.put("timestamp", Types.TIMESTAMP); + typeMap.put("timestamptz", 2014); //Types.TIMESTAMP_WITH_TIMEZONE make it compile on Java 7 + typeMap.put("timetz", 2013); //Types.TIME_WITH_TIMEZONE make it compile on Java 7 + typeMap.put("tinyint", Types.TINYINT); //but we will convert to java.lang.Byte + typeMap.put("url", Types.OTHER); + typeMap.put("uuid", Types.OTHER); + typeMap.put("varchar", Types.VARCHAR); + typeMap.put("wrd", Types.BIGINT); } /** * Returns the java.sql.Types equivalent of the given MonetDB type. * * @param type the type as used by MonetDB - * @return the mathing java.sql.Types constant or java.sql.Types.OTHER if - * nothing matched on the given string + * @return the matching java.sql.Types constant or java.sql.Types.OTHER if nothing matched on the given string */ - static int getJavaType(String type) { - // match the column type on a java.sql.Types constant + public static int getJavaType(String type) { + // match the currentColumns type on a java.sql.Types constant Integer tp = typeMap.get(type); if (tp != null) { - return tp.intValue(); + return tp; } else { - // this should not be able to happen - // do not assert, since maybe future versions introduce - // new types + // this should not be able to happen do not assert, since maybe future versions introduce new types return Types.OTHER; } } + private static String TypeMapppingSQL = null; // cache to optimise getSQLTypeMap() + /** * Returns a String usable in an SQL statement to map the server types * to values of java.sql.Types using the global static type map. * The returned string will be a SQL CASE x statement where the x is - * replaced with the given column name (or expression) string. + * replaced with the given currentColumns name (or expression) string. * - * @param column a String representing the value that should be evaluated - * in the SQL CASE statement + * @param column a String representing the value that should be evaluated in the SQL CASE statement * @return a SQL CASE statement */ - private static String TypeMapppingSQL = null; // cache to optimise getSQLTypeMap() static String getSQLTypeMap(String column) { if (TypeMapppingSQL == null) { // first time, compose TypeMappping SQL string @@ -409,4 +307,239 @@ final public class MonetDriver implement public static int getDriverMinorVersion() { return DRIVERMINOR; } + + /** + * Return the parent Logger of all the Loggers used by this data source. + * This should be the Logger farthest from the root Logger that is + * still an ancestor of all of the Loggers used by this data source. + * Configuring this Logger will affect all of the log messages + * generated by the data source. In the worst case, this may be the root Logger. + * + * @return the parent Logger for this data source + * @throws SQLFeatureNotSupportedException if the data source does not use java.util.logging + * @since 1.7 + */ + @Override + public Logger getParentLogger() throws SQLFeatureNotSupportedException { + throw new SQLFeatureNotSupportedException("java.util.logging not in use", "0A000"); + } + + /** + * Attempts to make a database connection to the given URL. The driver + * should return "null" if it realizes it is the wrong kind of driver to + * connect to the given URL. This will be common, as when the JDBC driver + * manager is asked to connect to a given URL it passes the URL to each + * loaded driver in turn. + * + * The driver should throw an SQLException if it is the right driver to + * connect to the given URL but has trouble connecting to the database. + * + * The java.util.Properties argument can be used to pass arbitrary string + * tag/value pairs as connection arguments. Normally at least "user" and + * "password" properties should be included in the Properties object. + * + * @param url the URL of the database to which to connect + * @param info a list of arbitrary string tag/value pairs as connection arguments. Normally at least a "user" and + * "password" property should be included + * @return a Connection object that represents a connection to the URL + * @throws SQLException if a database access error occurs + */ + public Connection connect(String url, Properties info) throws SQLException { + int tmp; + boolean isEmbedded; + Properties props = new Properties(); + props.putAll(info); + info = props; + + if (!acceptsURL(url)) + throw new SQLException("Invalid URL: it does not start with: " + MONETURL, "08M26"); + + if(!url.startsWith("jdbc:monetdb:embedded:")) { + // url should be of style jdbc:monetdb://<host>/<database> + isEmbedded = false; + URI uri; + try { + uri = new URI(url.substring(5)); + } catch (URISyntaxException e) { + throw new SQLException(e.toString(), "08M26"); + } + + String uri_host = uri.getHost(); + if (uri_host == null) + throw new SQLException("Invalid URL: no hostname given or unparsable in '" + url + "'", "08M26"); + info.put("host", uri_host); + + int uri_port = uri.getPort(); + if (uri_port > 0) + info.put("port", "" + uri_port); + + // check the database + String uri_path = uri.getPath(); + if (uri_path != null && uri_path.length() != 0) { + uri_path = uri_path.substring(1); + if (!uri_path.trim().isEmpty()) + info.put("database", uri_path); + } + + String uri_query = uri.getQuery(); + if (uri_query != null) { + // handle additional arguments + String args[] = uri_query.split("&"); + for (String arg : args) { + tmp = arg.indexOf('='); + if (tmp > 0) + info.put(arg.substring(0, tmp), arg.substring(tmp + 1)); + } + } + } else { + // url should be of style jdbc:monetdb:embedded:<directory> + isEmbedded = true; + info.put("directory", url.substring(22)); + } + + info.put("embedded", Boolean.toString(isEmbedded)); + // finally return the Connection as requested + return CreateMonetDBJDBCConnection(info); + } + + @SuppressWarnings("unchecked") + private static MonetConnection CreateMonetDBJDBCConnection(Properties props) throws SQLException, + IllegalArgumentException { + MonetConnection res; + + boolean isEmbedded = Boolean.parseBoolean(props.getProperty("embedded", "false")); + String language = props.getProperty("language", "sql"); + String username = props.getProperty("user"); + String password = props.getProperty("password"); + String hash = props.getProperty("hash"); + int sockTimeout = 0; + + if(isEmbedded) { //instantiate the connection + try { + String directory = props.getProperty("directory"); + if (directory != null && (directory.trim().isEmpty() || directory.equals(":memory:"))) + directory = null; + if(embeddedConnectionClass == null) { + embeddedConnectionClass = Class.forName("nl.cwi.monetdb.embedded.jdbc.EmbeddedConnection"); + if(embeddedConnectionClass == null) { //if it is still null then there is a problem! + throw new SQLNonTransientConnectionException("EmbeddedConnection Class not found! Please add monetdb-java-lite jar to the CLASSPATH"); + } + } + res = (MonetConnection) embeddedConnectionClass + .getDeclaredConstructor(Properties.class, String.class, String.class, String.class) + .newInstance(props, hash, language, directory); + } catch (InvocationTargetException | InstantiationException | IllegalAccessException | + NoSuchMethodException | ClassNotFoundException e) { + throw new SQLNonTransientConnectionException(e); + } + } else { + String hostname = props.getProperty("host"); + if (hostname == null || hostname.trim().isEmpty()) + throw new IllegalArgumentException("hostname should not be null or empty"); + if (username == null || username.trim().isEmpty()) + throw new IllegalArgumentException("user should not be null or empty"); + if (password == null || password.trim().isEmpty()) + throw new IllegalArgumentException("password should not be null or empty"); + String database = props.getProperty("database"); + if (database == null || database.trim().isEmpty()) + throw new IllegalArgumentException("database should not be null or empty"); + + boolean blobIsBinary = Boolean.valueOf(props.getProperty("treat_blob_as_binary", "false")); + boolean clobIsLongChar = Boolean.valueOf(props.getProperty("treat_clob_as_longvarchar", "false")); + + boolean negative1 = false, failedparse1 = false; + int port = 0; + try { + port = Integer.parseInt(props.getProperty("port", PORT)); + } catch (NumberFormatException e) { + failedparse1 = true; + props.setProperty("port", PORT); + } + if (port <= 0) { + negative1 = true; + port = Integer.parseInt(PORT); + props.setProperty("port", PORT); + } + + String timeout = props.getProperty("so_timeout", "0"); + boolean negative2 = false, failedparse2 = false; + try { + sockTimeout = Integer.parseInt(timeout); + } catch (NumberFormatException e) { + sockTimeout = 0; + failedparse2 = true; + props.setProperty("so_timeout", "0"); + } + if (sockTimeout < 0) { + negative2 = true; + sockTimeout = 0; + props.setProperty("so_timeout", "0"); + } + res = new MapiConnection(props, hash, language, blobIsBinary, clobIsLongChar, hostname, port, database); + if(failedparse1) { + res.addWarning("Unable to parse port number from: " + port, "M1M05"); + } + if(negative1) { + res.addWarning("Negative port not allowed. Value ignored", "M1M05"); + } + if(failedparse2) { + res.addWarning("Unable to parse socket timeout number from: " + timeout, "M1M05"); + } + if(negative2) { + res.addWarning("Negative socket timeout not allowed. Value ignored", "M1M05"); + } + try { + res.setSoTimeout(sockTimeout); + } catch(SocketException ex) { + res.addWarning("Failed to set socket timeout: " + ex.getMessage(), "M1M05"); + } + } + + try { //attempt to connect and authenticate the user + List<String> warnings = res.connect(username, password); + if(warnings != null) { + for (String warning : warnings) { + res.addWarning(warning, "01M02"); + } + } + // apply NetworkTimeout value from legacy (pre 4.1) driver so_timeout calls + if(!isEmbedded) { + res.setSoTimeout(sockTimeout); + } + } catch (IOException e) { + if(!isEmbedded) { + MapiConnection con = (MapiConnection) res; + throw new SQLException("Unable to connect (" + con.getHostname() + ":" + + con.getPort() + "): " + e.getMessage(), "08006"); + } else { + throw new SQLNonTransientConnectionException("Unable to connect: " + e.getMessage(), "08006"); + } + } catch (ProtocolException e) { + throw new SQLNonTransientConnectionException(e.getMessage(), "08001"); + } catch (MCLException e) { + String[] connex = e.getMessage().split("\n"); + SQLException sqle = new SQLNonTransientConnectionException(connex[0], "08001", e); + for (int i = 1; i < connex.length; i++) { + sqle.setNextException(new SQLNonTransientConnectionException(connex[1], "08001")); + } + throw sqle; + } + + if (!isEmbedded && res.getLanguage() == MapiLanguage.LANG_SQL) { //set the timezone only in the MAPI connection + // enable auto commit + res.setAutoCommit(true); + // set our time zone on the server + Calendar cal = Calendar.getInstance(); + int offset = cal.get(Calendar.ZONE_OFFSET) + cal.get(Calendar.DST_OFFSET); + offset /= (60 * 1000); // milliseconds to minutes + String tz = offset < 0 ? "-" : "+"; + tz += (Math.abs(offset) / 60 < 10 ? "0" : "") + (Math.abs(offset) / 60) + ":"; + offset -= (offset / 60) * 60; + tz += (offset < 10 ? "0" : "") + offset; + + res.sendIndependentCommand("SET TIME ZONE INTERVAL '" + tz + "' HOUR TO MINUTE"); + } + + return res; + } }
new file mode 100644 --- /dev/null +++ b/src/main/java/nl/cwi/monetdb/jdbc/MonetINET.java @@ -0,0 +1,171 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * Copyright 1997 - July 2008 CWI, August 2008 - 2017 MonetDB B.V. + */ + +package nl.cwi.monetdb.jdbc; + +import java.net.Inet4Address; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.sql.SQLData; +import java.sql.SQLException; +import java.sql.SQLInput; +import java.sql.SQLOutput; + +/** + * The INET class represents the INET datatype in MonetDB. It + * represents a IPv4 address with a certain mask applied. Currently, + * IPv6 is not supported. + * <br /> + * The input format for INET is x.x.x.x/y where x.x.x.x is an IP address + * and y is the number of bits in the netmask. If the /y part is left + * off, then the netmask is 32, and the value represents just a single + * host. On display, the /y portion is suppressed if the netmask is 32. + * <br /> + * This class allows to retrieve the value of this INET as InetAddress. + * This is probably meaningful only and only if the netmask is 32. The + * getNetmaskBits() method can be used to retrieve the subnet bits. + * + * @author Fabian Groffen + */ +public class MonetINET implements SQLData { + + private static String FromString(String newinet) throws Exception { + if (newinet == null) { + return null; + } + int slash = newinet.indexOf('/'); + String tinet = newinet; + if (slash != -1) { + int netmask; + // ok, see if it is a valid netmask + try { + netmask = Integer.parseInt(newinet.substring(slash + 1)); + } catch (NumberFormatException nfe) { + throw new Exception("cannot parse netmask bits: " + newinet.substring(slash + 1)); + } + if (netmask <= 0 || netmask > 32) + throw new Exception("netmask must be >0 and <32"); + tinet = newinet.substring(0, slash); + } + // check dotted quad + String quads[] = tinet.split("\\."); + if (quads.length != 4) + throw new Exception("expected dotted quad (xxx.xxx.xxx.xxx)"); + for (int i = 0; i < 4; i++) { + int quadv; + try { + quadv = Integer.parseInt(quads[i]); + } catch (NumberFormatException nfe) { + throw new Exception("cannot parse number: " + quads[i]); + } + if (quadv < 0 || quadv > 255) + throw new Exception("value must be between 0 and 255: " + quads[i]); + } + // everything is fine + return newinet; + } + + private String inet; + + public MonetINET(String inet) throws Exception { + this.inet = FromString(inet); + } + + @Override + public String getSQLTypeName() { + return "inet"; + } + + @Override + public void readSQL(SQLInput stream, String typeName) throws SQLException { + if (typeName.compareTo("inet") != 0) + throw new SQLException("can only use this class with 'inet' type", "M1M05"); + inet = stream.readString(); + } + + @Override + public void writeSQL(SQLOutput stream) throws SQLException { + stream.writeString(inet); + } + + @Override + public String toString() { + return inet; + } + + public void fromString(String newinet) throws Exception { + inet = FromString(newinet); + } + + public String getAddress() { + if (inet == null) + return null; + + // inet optionally has a /y part, if y < 32, chop it off + int slash = inet.indexOf('/'); + if (slash != -1) + return inet.substring(0, slash); + return inet; + } + + public void setAddress(String newinet) throws Exception { + if (newinet == null) { + inet = null; + return; + } + if (newinet.indexOf('/') != -1) + throw new Exception("IPv4 address cannot contain '/' " + "(use fromString() instead)"); + fromString(newinet); + } + + public int getNetmaskBits() throws SQLException { + if (inet == null) + return 0; + + // if netmask is 32, it is omitted in the output + int slash = inet.indexOf('/'); + if (slash == -1) + return 32; + try { + return Integer.parseInt(inet.substring(slash + 1)); + } catch (NumberFormatException nfe) { + throw new SQLException("cannot parse netmask bits: " + inet.substring(slash + 1), "M0M27"); + } + } + + public void setNetmaskBits(int bits) throws Exception { + String newinet = inet; + if (newinet == null) { + newinet = "0.0.0.0/" + bits; + } else { + int slash = newinet.indexOf('/'); + if (slash != -1) { + newinet = newinet.substring(0, slash + 1) + bits; + } else { + newinet = newinet + "/" + bits; + } + } + fromString(newinet); + } + + public InetAddress getInetAddress() throws SQLException { + if (inet == null) + return null; + try { + return InetAddress.getByName(getAddress()); + } catch (UnknownHostException uhe) { + throw new SQLException("could not resolve IP address", "M0M27"); + } + } + + public void setInetAddress(InetAddress iaddr) throws Exception { + if (!(iaddr instanceof Inet4Address)) + throw new Exception("only IPv4 are supported currently"); + fromString(iaddr.getHostAddress()); + } +}
--- a/src/main/java/nl/cwi/monetdb/jdbc/MonetPreparedStatement.java +++ b/src/main/java/nl/cwi/monetdb/jdbc/MonetPreparedStatement.java @@ -8,6 +8,10 @@ package nl.cwi.monetdb.jdbc; +import nl.cwi.monetdb.mcl.connection.ControlCommands; +import nl.cwi.monetdb.mcl.responses.ResultSetResponse; + +import java.io.ByteArrayOutputStream; import java.io.InputStream; import java.io.Reader; import java.io.IOException; @@ -16,26 +20,7 @@ import java.math.BigInteger; import java.math.RoundingMode; import java.net.URL; import java.nio.CharBuffer; -import java.sql.Array; -import java.sql.Blob; -import java.sql.Clob; -import java.sql.NClob; -import java.sql.ParameterMetaData; -import java.sql.PreparedStatement; -import java.sql.Ref; -import java.sql.ResultSet; -import java.sql.ResultSetMetaData; -import java.sql.RowId; -import java.sql.SQLData; -import java.sql.SQLDataException; -import java.sql.SQLException; -import java.sql.SQLFeatureNotSupportedException; -import java.sql.SQLOutput; -import java.sql.SQLXML; -import java.sql.Struct; -import java.sql.Time; -import java.sql.Timestamp; -import java.sql.Types; +import java.sql.*; import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.Map; @@ -60,14 +45,12 @@ import java.util.Map; * [ "int", 9, 0 ] * </pre> * - * @author Fabian Groffen - * @author Martin van Dinther - * @version 0.5 + * @author Fabian Groffen, Martin van Dinther, Pedro Ferreira + * @version 0.5v */ -public class MonetPreparedStatement - extends MonetStatement - implements PreparedStatement, AutoCloseable -{ +public class MonetPreparedStatement extends MonetStatement implements PreparedStatement, AutoCloseable { + + private final MonetConnection connection; private final String[] monetdbType; private final int[] javaType; private final int[] digits; @@ -78,28 +61,17 @@ public class MonetPreparedStatement private final int id; private final int size; private final int rscolcnt; - private final String[] values; - private final MonetConnection connection; - - /* placeholders for date/time pattern formats created once (only when needed), used multiple times */ - /** Format of a timestamp with RFC822 time zone */ - private SimpleDateFormat mTimestampZ; - /** Format of a timestamp */ - private SimpleDateFormat mTimestamp; - /** Format of a time with RFC822 time zone */ - private SimpleDateFormat mTimeZ; - /** Format of a time */ - private SimpleDateFormat mTime; - /** Format of a date used by mserver */ - private SimpleDateFormat mDate; + private final SimpleDateFormat mTimestampZ; + private final SimpleDateFormat mTimestamp; + private final SimpleDateFormat mTimeZ; + private final SimpleDateFormat mTime; + private final SimpleDateFormat mDate; /** - * MonetPreparedStatement constructor which checks the arguments for - * validity. A MonetPreparedStatement is backed by a - * {@link MonetStatement}, which deals with most of the required stuff of - * this class. + * MonetPreparedStatement constructor which checks the arguments for validity. A MonetPreparedStatement is backed + * by a {@link MonetStatement}, which deals with most of the required stuff of this class. * * @param connection the connection that created this Statement * @param resultSetType type of {@link ResultSet} to produce @@ -108,28 +80,17 @@ public class MonetPreparedStatement * @throws SQLException if an error occurs during login * @throws IllegalArgumentException is one of the arguments is null or empty */ - MonetPreparedStatement( - MonetConnection connection, - int resultSetType, - int resultSetConcurrency, - int resultSetHoldability, - String prepareQuery) - throws SQLException, IllegalArgumentException - { - super( - connection, - resultSetType, - resultSetConcurrency, - resultSetHoldability - ); + MonetPreparedStatement(MonetConnection connection, int resultSetType, int resultSetConcurrency, + int resultSetHoldability, String prepareQuery) throws SQLException, IllegalArgumentException { + super(connection, resultSetType, resultSetConcurrency, resultSetHoldability); if (!super.execute("PREPARE " + prepareQuery)) throw new SQLException("Unexpected server response", "M0M10"); // cheat a bit to get the ID and the number of columns - id = ((MonetConnection.ResultSetResponse)header).id; - size = ((MonetConnection.ResultSetResponse)header).tuplecount; - rscolcnt = ((MonetConnection.ResultSetResponse)header).columncount; + id = ((ResultSetResponse)header).getId(); + size = ((ResultSetResponse)header).getTuplecount(); + rscolcnt = ((ResultSetResponse)header).getColumncount(); // initialise blank finals monetdbType = new String[size]; @@ -172,54 +133,18 @@ public class MonetPreparedStatement // PreparedStatements are by default poolable poolable = true; + + mTimestampZ = connection.getProtocol().getMonetTimestampTz(); + mTimestamp = connection.getProtocol().getMonetTimestamp(); + mTimeZ = connection.getProtocol().getMonetTimeTz(); + mTime = connection.getProtocol().getMonetTime(); + mDate = connection.getProtocol().getMonetDate(); } - /** - * Constructs an empty MonetPreparedStatement. This constructor is - * in particular useful for extensions of this class. - * - * @param connection the connection that created this Statement - * @param resultSetType type of ResultSet to produce - * @param resultSetConcurrency concurrency of ResultSet to produce - * @throws SQLException if an error occurs during login - */ - /* Disabled this constructor code as it is not part of the JDBC interface - It may be enabled again when a subclass is constructed which needs it. - MonetPreparedStatement( - MonetConnection connection, - int resultSetType, - int resultSetConcurrency, - int resultSetHoldability) - throws SQLException - { - super( - connection, - resultSetType, - resultSetConcurrency, - resultSetHoldability - ); - // initialise blank finals - monetdbType = null; - javaType = null; - digits = null; - scale = null; - schema = null; - table = null; - column = null; - values = null; - id = -1; - size = -1; - rscolcnt = -1; - - this.connection = connection; - } - */ - //== methods interface PreparedStatement /** - * Adds a set of parameters to this PreparedStatement object's batch - * of commands. + * Adds a set of parameters to this PreparedStatement object's batch of commands. * * @throws SQLException if a database access error occurs */ @@ -262,10 +187,9 @@ public class MonetPreparedStatement * getUpdateCount to retrieve the result; you must call * getMoreResults to move to any subsequent result(s). * - * @return true if the first result is a ResultSet object; false if the - * first result is an update count or there is no result - * @throws SQLException if a database access error occurs or an argument - * is supplied to this method + * @return true if the first result is a ResultSet object; false if the first result is an update count or there is + * no result + * @throws SQLException if a database access error occurs or an argument is supplied to this method */ @Override public boolean execute() throws SQLException { @@ -279,55 +203,49 @@ public class MonetPreparedStatement } /** - * Executes the SQL query in this PreparedStatement object and returns the - * ResultSet object generated by the query. + * Executes the SQL query in this PreparedStatement object and returns the ResultSet object generated by the query. * - * @return a ResultSet object that contains the data produced by the query; - * never null - * @throws SQLException if a database access error occurs or the SQL - * statement does not return a ResultSet object + * @return a ResultSet object that contains the data produced by the query never null + * @throws SQLException if a database access error occurs or the SQL statement does not return a ResultSet object */ @Override public ResultSet executeQuery() throws SQLException { - if (execute() != true) + if (!execute()) throw new SQLException("Query did not produce a result set", "M1M19"); return getResultSet(); } - /** override the executeQuery from the Statement to throw an SQLException*/ + /** override the executeQuery from the Statement to throw an SQLException */ @Override public ResultSet executeQuery(String q) throws SQLException { throw new SQLException("This method is not available in a PreparedStatement!", "M1M05"); } /** - * Executes the SQL statement in this PreparedStatement object, which must - * be an SQL INSERT, UPDATE or DELETE statement; or an SQL statement that - * returns nothing, such as a DDL statement. + * Executes the SQL statement in this PreparedStatement object, which must be an SQL INSERT, UPDATE or DELETE + * statement; or an SQL statement that returns nothing, such as a DDL statement. * * @return either (1) the row count for INSERT, UPDATE, or DELETE * statements or (2) 0 for SQL statements that return nothing - * @throws SQLException if a database access error occurs or the SQL - * statement returns a ResultSet object + * @throws SQLException if a database access error occurs or the SQL statement returns a ResultSet object */ @Override public int executeUpdate() throws SQLException { - if (execute() != false) + if (execute()) throw new SQLException("Query produced a result set", "M1M17"); - return getUpdateCount(); } - /** override the executeUpdate from the Statement to throw an SQLException*/ + /** override the executeUpdate from the Statement to throw an SQLException */ @Override public int executeUpdate(String q) throws SQLException { throw new SQLException("This method is not available in a PreparedStatement!", "M1M05"); } /** - * Returns the index (0..size-1) in the backing arrays for the given - * resultset column number or an SQLException when not found + * Returns the index (0..size-1) in the backing arrays for the given resultset column number or an SQLException + * when not found */ private int getColumnIdx(int colnr) throws SQLException { int curcol = 0; @@ -341,6 +259,7 @@ public class MonetPreparedStatement } throw new SQLException("No such column with index: " + colnr, "M1M05"); } + /** * Returns the index (0..size-1) in the backing arrays for the given * parameter number or an SQLException when not found @@ -358,7 +277,6 @@ public class MonetPreparedStatement throw new SQLException("No such parameter with index: " + paramnr, "M1M05"); } - /* helper for the anonymous class inside getMetaData */ private abstract class rsmdw extends MonetWrapper implements ResultSetMetaData {} /** @@ -375,7 +293,6 @@ public class MonetPreparedStatement * * @return the description of a ResultSet object's columns or null if the * driver cannot return a ResultSetMetaData object - * @throws SQLException if a database access error occurs */ @Override public ResultSetMetaData getMetaData() { @@ -387,7 +304,7 @@ public class MonetPreparedStatement /** * Returns the number of columns in this ResultSet object. * - * @returns the number of columns + * @return the number of columns */ @Override public int getColumnCount() { @@ -414,7 +331,7 @@ public class MonetPreparedStatement * query call to pull the IS_AUTOINCREMENT value for this column. * See also ResultSetMetaData.isAutoIncrement() */ - // For now we simply allways return false. + // For now we simply always return false. return false; } @@ -422,37 +339,28 @@ public class MonetPreparedStatement * Indicates whether a column's case matters. * * @param column the first column is 1, the second is 2, ... - * @returns false + * @return if the column is case sensitive */ @Override public boolean isCaseSensitive(int column) throws SQLException { switch (getColumnType(column)) { + case Types.CLOB: case Types.CHAR: - case Types.LONGVARCHAR: // MonetDB doesn't use type LONGVARCHAR, it's here for completeness - case Types.CLOB: - return true; case Types.VARCHAR: - String monettype = getColumnTypeName(column); - if (monettype != null) { - // data of type inet or uuid is not case sensitive - if ("inet".equals(monettype) - || "uuid".equals(monettype)) - return false; - } + case Types.LONGVARCHAR: return true; + default: + return false; } - - return false; } /** - * Indicates whether the designated column can be used in a - * where clause. + * Indicates whether the designated column can be used in a where clause. * * Returning true for all here, even for CLOB, BLOB. * * @param column the first column is 1, the second is 2, ... - * @returns true + * @return true */ @Override public boolean isSearchable(int column) { @@ -460,14 +368,12 @@ public class MonetPreparedStatement } /** - * Indicates whether the designated column is a cash value. - * From the MonetDB database perspective it is by definition - * unknown whether the value is a currency, because there are - * no currency datatypes such as MONEY. With this knowledge - * we can always return false here. + * Indicates whether the designated column is a cash value. From the MonetDB database perspective it is by + * definition unknown whether the value is a currency, because there are no currency datatypes such as + * MONEY. With this knowledge we can always return false here. * * @param column the first column is 1, the second is 2, ... - * @returns false + * @return false */ @Override public boolean isCurrency(int column) { @@ -475,8 +381,7 @@ public class MonetPreparedStatement } /** - * Indicates whether values in the designated column are signed - * numbers. + * Indicates whether values in the designated column are signed numbers. * Within MonetDB all numeric types (except oid and ptr) are signed. * * @param column the first column is 1, the second is 2, ... @@ -486,40 +391,25 @@ public class MonetPreparedStatement public boolean isSigned(int column) throws SQLException { // we can hardcode this, based on the colum type switch (getColumnType(column)) { - case Types.NUMERIC: - case Types.DECIMAL: case Types.TINYINT: case Types.SMALLINT: case Types.INTEGER: case Types.REAL: - case Types.FLOAT: case Types.DOUBLE: - return true; case Types.BIGINT: - String monettype = getColumnTypeName(column); - if (monettype != null) { - if ("oid".equals(monettype) - || "ptr".equals(monettype)) - return false; - } + case Types.NUMERIC: + case Types.DECIMAL: return true; - case Types.BIT: // we don't use type BIT, it's here for completeness - case Types.BOOLEAN: - case Types.DATE: - case Types.TIME: - case Types.TIMESTAMP: default: return false; } } /** - * Indicates the designated column's normal maximum width in - * characters. + * Indicates the designated column's normal maximum width in characters. * * @param column the first column is 1, the second is 2, ... - * @return the normal maximum number of characters allowed as the - * width of the designated column + * @return the normal maximum number of characters allowed as the width of the designated column * @throws SQLException if there is no such column */ @Override @@ -563,10 +453,8 @@ public class MonetPreparedStatement } /** - * Get the designated column's number of decimal digits. - * This method is currently very expensive as it needs to - * retrieve the information from the database using an SQL - * query. + * Get the designated column's number of decimal digits. This method is currently very expensive as it + * needs to retrieve the information from the database using an SQL query. * * @param column the first column is 1, the second is 2, ... * @return precision @@ -582,10 +470,8 @@ public class MonetPreparedStatement } /** - * Gets the designated column's number of digits to right of - * the decimal point. This method is currently very - * expensive as it needs to retrieve the information from - * the database using an SQL query. + * Gets the designated column's number of digits to right of the decimal point. This method is currently + * very expensive as it needs to retrieve the information from the database using an SQL query. * * @param column the first column is 1, the second is 2, ... * @return scale @@ -601,10 +487,8 @@ public class MonetPreparedStatement } /** - * Indicates the nullability of values in the designated - * column. This method is currently very expensive as it - * needs to retrieve the information from the database using - * an SQL query. + * Indicates the nullability of values in the designated column. This method is currently very expensive as + * it needs to retrieve the information from the database using an SQL query. * * @param column the first column is 1, the second is 2, ... * @return nullability @@ -620,8 +504,7 @@ public class MonetPreparedStatement * MonetDB does not support the catalog naming concept as in: catalog.schema.table naming scheme * * @param column the first column is 1, the second is 2, ... - * @return the name of the catalog for the table in which the given - * column appears or "" if not applicable + * @return the name of the catalog for the table in which the given column appears or "" if not applicable */ @Override public String getCatalogName(int column) throws SQLException { @@ -629,9 +512,8 @@ public class MonetPreparedStatement } /** - * Indicates whether the designated column is definitely not - * writable. MonetDB does not support cursor updates, so - * nothing is writable. + * Indicates whether the designated column is definitely not writable. MonetDB does not support cursor + * updates, so nothing is writable. * * @param column the first column is 1, the second is 2, ... * @return true if so; false otherwise @@ -642,8 +524,7 @@ public class MonetPreparedStatement } /** - * Indicates whether it is possible for a write on the - * designated column to succeed. + * Indicates whether it is possible for a write on the designated column to succeed. * * @param column the first column is 1, the second is 2, ... * @return true if so; false otherwise @@ -654,8 +535,7 @@ public class MonetPreparedStatement } /** - * Indicates whether a write on the designated column will - * definitely succeed. + * Indicates whether a write on the designated column will definitely succeed. * * @param column the first column is 1, the second is 2, ... * @return true if so; false otherwise @@ -666,18 +546,14 @@ public class MonetPreparedStatement } /** - * Returns the fully-qualified name of the Java class whose - * instances are manufactured if the method - * ResultSet.getObject is called to retrieve a value from - * the column. ResultSet.getObject may return a subclass of - * the class returned by this method. + * Returns the fully-qualified name of the Java class whose instances are manufactured if the method + * ResultSet.getObject is called to retrieve a value from the column. ResultSet.getObject may return a + * subclass of the class returned by this method. * * @param column the first column is 1, the second is 2, ... - * @return the fully-qualified name of the class in the Java - * programming language that would be used by the method - * ResultSet.getObject to retrieve the value in the - * specified column. This is the class name used for custom - * mapping. + * @return the fully-qualified name of the class in the Java programming language that would be used by the + * method ResultSet.getObject to retrieve the value in the specified column. This is the class name used + * for custom mapping. * @throws SQLException if there is no such column */ @Override @@ -686,9 +562,8 @@ public class MonetPreparedStatement } /** - * Gets the designated column's suggested title for use in - * printouts and displays. This is currently equal to - * getColumnName(). + * Gets the designated column's suggested title for use in printouts and displays. This is currently equal + * to getColumnName(). * * @param column the first column is 1, the second is 2, ... * @return the suggested column title @@ -702,7 +577,7 @@ public class MonetPreparedStatement /** * Gets the designated column's name * - * @param column the first column is 1, the second is 2, ... + * @param colnr the first column is 1, the second is 2, ... * @return the column name * @throws SQLException if there is no such column */ @@ -735,9 +610,8 @@ public class MonetPreparedStatement * Retrieves the designated column's database-specific type name. * * @param column the first column is 1, the second is 2, ... - * @return type name used by the database. If the column type is a - * user-defined type, then a fully-qualified type name is - * returned. + * @return type name used by the database. If the column type is a user-defined type, then a + * fully-qualified type name is returned. * @throws SQLException if there is no such column */ @Override @@ -754,20 +628,17 @@ public class MonetPreparedStatement /* helper class for the anonymous class in getParameterMetaData */ private abstract class pmdw extends MonetWrapper implements ParameterMetaData {} /** - * Retrieves the number, types and properties of this - * PreparedStatement object's parameters. + * Retrieves the number, types and properties of this PreparedStatement object's parameters. * - * @return a ParameterMetaData object that contains information - * about the number, types and properties of this - * PreparedStatement object's parameters + * @return a ParameterMetaData object that contains information about the number, types and properties of this + * PreparedStatement object's parameters * @throws SQLException if a database access error occurs */ @Override public ParameterMetaData getParameterMetaData() throws SQLException { return new pmdw() { /** - * Retrieves the number of parameters in the - * PreparedStatement object for which this ParameterMetaData + * Retrieves the number of parameters in the PreparedStatement object for which this ParameterMetaData * object contains information. * * @return the number of parameters @@ -786,16 +657,13 @@ public class MonetPreparedStatement } /** - * Retrieves whether null values are allowed in the - * designated parameter. + * Retrieves whether null values are allowed in the designated parameter. * * This is currently always unknown for MonetDB/SQL. * * @param param the first parameter is 1, the second is 2, ... - * @return the nullability status of the given parameter; - * one of ParameterMetaData.parameterNoNulls, - * ParameterMetaData.parameterNullable, or - * ParameterMetaData.parameterNullableUnknown + * @return the nullability status of the given parameter; one of ParameterMetaData.parameterNoNulls, + * ParameterMetaData.parameterNullable, or ParameterMetaData.parameterNullableUnknown * @throws SQLException if a database access error occurs */ @Override @@ -804,8 +672,7 @@ public class MonetPreparedStatement } /** - * Retrieves whether values for the designated parameter can - * be signed numbers. + * Retrieves whether values for the designated parameter can be signed numbers. * * @param param the first parameter is 1, the second is 2, ... * @return true if so; false otherwise @@ -813,23 +680,17 @@ public class MonetPreparedStatement */ @Override public boolean isSigned(int param) throws SQLException { - // we can hardcode this, based on the colum type + // we can hardcode this, based on the column type switch (getParameterType(param)) { - case Types.NUMERIC: - case Types.DECIMAL: case Types.TINYINT: case Types.SMALLINT: case Types.INTEGER: + case Types.REAL: + case Types.DOUBLE: case Types.BIGINT: - case Types.REAL: - case Types.FLOAT: - case Types.DOUBLE: + case Types.NUMERIC: + case Types.DECIMAL: return true; - case Types.BIT: // we don't use type BIT, it's here for completeness - case Types.BOOLEAN: - case Types.DATE: - case Types.TIME: - case Types.TIMESTAMP: default: return false; } @@ -923,7 +784,7 @@ public class MonetPreparedStatement Map<String,Class<?>> map = getConnection().getTypeMap(); Class<?> c; if (map.containsKey(typeName)) { - c = (Class)map.get(typeName); + c = map.get(typeName); } else { c = MonetResultSet.getClassForType(getParameterType(param)); } @@ -981,9 +842,7 @@ public class MonetPreparedStatement * not support this method */ @Override - public void setAsciiStream(int parameterIndex, InputStream x) - throws SQLException - { + public void setAsciiStream(int parameterIndex, InputStream x) throws SQLException { throw newSQLFeatureNotSupportedException("setAsciiStream"); } @@ -1004,9 +863,7 @@ public class MonetPreparedStatement * @throws SQLException if a database access error occurs */ @Override - public void setAsciiStream(int parameterIndex, InputStream x, int length) - throws SQLException - { + public void setAsciiStream(int parameterIndex, InputStream x, int length) throws SQLException { throw newSQLFeatureNotSupportedException("setAsciiStream"); } @@ -1030,9 +887,7 @@ public class MonetPreparedStatement * not support this method */ @Override - public void setAsciiStream(int parameterIndex, InputStream x, long length) - throws SQLException - { + public void setAsciiStream(int parameterIndex, InputStream x, long length) throws SQLException { throw newSQLFeatureNotSupportedException("setAsciiStream"); } @@ -1055,7 +910,8 @@ public class MonetPreparedStatement // if precision is now greater than that of the db, throw an error: if (x.precision() > digits[i]) { - throw new SQLDataException("DECIMAL value exceeds allowed digits/scale: " + x.toPlainString() + " (" + digits[i] + "/" + scale[i] + ")", "22003"); + throw new SQLDataException("DECIMAL value exceeds allowed digits/scale: " + x.toPlainString() + + " (" + digits[i] + "/" + scale[i] + ")", "22003"); } // MonetDB doesn't like leading 0's, since it counts them as part of @@ -1088,9 +944,7 @@ public class MonetPreparedStatement * not support this method */ @Override - public void setBinaryStream(int parameterIndex, InputStream x) - throws SQLException - { + public void setBinaryStream(int parameterIndex, InputStream x) throws SQLException { throw newSQLFeatureNotSupportedException("setBinaryStream"); } @@ -1112,9 +966,7 @@ public class MonetPreparedStatement * not support this method */ @Override - public void setBinaryStream(int parameterIndex, InputStream x, int length) - throws SQLException - { + public void setBinaryStream(int parameterIndex, InputStream x, int length) throws SQLException { throw newSQLFeatureNotSupportedException("setBinaryStream"); } @@ -1136,9 +988,7 @@ public class MonetPreparedStatement * not support this method */ @Override - public void setBinaryStream(int parameterIndex, InputStream x, long length) - throws SQLException - { + public void setBinaryStream(int parameterIndex, InputStream x, long length) throws SQLException { throw newSQLFeatureNotSupportedException("setBinaryStream"); } @@ -1149,12 +999,25 @@ public class MonetPreparedStatement * @param parameterIndex the first parameter is 1, the second is 2, ... * @param x a Blob object that maps an SQL BLOB value * @throws SQLException if a database access error occurs - * @throws SQLFeatureNotSupportedException the JDBC driver does - * not support this method */ @Override public void setBlob(int parameterIndex, InputStream x) throws SQLException { - throw newSQLFeatureNotSupportedException("setBlob"); + if (x == null) { + setNull(parameterIndex, -1); + return; + } + // Some buffer. Size of 8192 is default for BufferedReader, so... + byte[] arr = new byte[8192]; + ByteArrayOutputStream buf = new ByteArrayOutputStream(); + int numChars; + try { + while ((numChars = x.read(arr, 0, arr.length)) > 0) { + buf.write(arr, 0, numChars); + } + setBytes(parameterIndex, buf.toByteArray()); + } catch (IOException e) { + throw new SQLException(e); + } } /** @@ -1164,12 +1027,14 @@ public class MonetPreparedStatement * @param parameterIndex the first parameter is 1, the second is 2, ... * @param x a Blob object that maps an SQL BLOB value * @throws SQLException if a database access error occurs - * @throws SQLFeatureNotSupportedException the JDBC driver does - * not support this method */ @Override public void setBlob(int parameterIndex, Blob x) throws SQLException { - throw newSQLFeatureNotSupportedException("setBlob"); + if (x == null) { + setNull(parameterIndex, -1); + return; + } + setBytes(parameterIndex, x.getBytes(0, (int) x.length())); } /** @@ -1184,16 +1049,26 @@ public class MonetPreparedStatement * should be sent to the server as a LONGVARBINARY or a BLOB. * * @param parameterIndex the first parameter is 1, the second is 2, ... - * @param is an object that contains the data to set the parameter - * value to + * @param is an object that contains the data to set the parameter value to * @param length the number of bytes in the parameter data * @throws SQLException if a database access error occurs - * @throws SQLFeatureNotSupportedException the JDBC driver does - * not support this method */ @Override public void setBlob(int parameterIndex, InputStream is, long length) throws SQLException { - throw newSQLFeatureNotSupportedException("setBlob"); + if (is == null) { + setNull(parameterIndex, -1); + return; + } + try { + byte[] arr = new byte[(int) length]; + ByteArrayOutputStream buf = new ByteArrayOutputStream((int) length); + + int numChars = is.read(arr, 0, (int) length); + buf.write(arr, 0, numChars); + setBytes(parameterIndex, buf.toByteArray()); + } catch (IOException e) { + throw new SQLException(e); + } } /** @@ -1223,7 +1098,7 @@ public class MonetPreparedStatement setValue(parameterIndex, Byte.toString(x)); } - static final String HEXES = "0123456789ABCDEF"; + private static final String HEXES = "0123456789ABCDEF"; /** * Sets the designated parameter to the given Java array of bytes. The * driver converts this to an SQL VARBINARY or LONGVARBINARY (depending @@ -1242,11 +1117,8 @@ public class MonetPreparedStatement } StringBuilder hex = new StringBuilder(x.length * 2); - byte b; - for (int i = 0; i < x.length; i++) { - b = x[i]; - hex.append(HEXES.charAt((b & 0xF0) >> 4)) - .append(HEXES.charAt((b & 0x0F))); + for (byte aX : x) { + hex.append(HEXES.charAt((aX & 0xF0) >> 4)).append(HEXES.charAt((aX & 0x0F))); } setValue(parameterIndex, "blob '" + hex.toString() + "'"); } @@ -1268,12 +1140,7 @@ public class MonetPreparedStatement * @throws SQLException if a database access error occurs */ @Override - public void setCharacterStream( - int parameterIndex, - Reader reader, - int length) - throws SQLException - { + public void setCharacterStream(int parameterIndex, Reader reader, int length) throws SQLException { if (reader == null) { setNull(parameterIndex, -1); return; @@ -1302,13 +1169,9 @@ public class MonetPreparedStatement * @param parameterIndex the first parameter is 1, the second is 2, ... * @param reader the java.io.Reader object that contains the Unicode data * @throws SQLException if a database access error occurs - * @throws SQLFeatureNotSupportedException the JDBC driver does - * not support this method */ @Override - public void setCharacterStream(int parameterIndex, Reader reader) - throws SQLException - { + public void setCharacterStream(int parameterIndex, Reader reader) throws SQLException { setCharacterStream(parameterIndex, reader, 0); } @@ -1329,12 +1192,7 @@ public class MonetPreparedStatement * @throws SQLException if a database access error occurs */ @Override - public void setCharacterStream( - int parameterIndex, - Reader reader, - long length) - throws SQLException - { + public void setCharacterStream(int parameterIndex, Reader reader, long length) throws SQLException { // given the implementation of the int-version, downcast is ok setCharacterStream(parameterIndex, reader, (int)length); } @@ -1368,8 +1226,6 @@ public class MonetPreparedStatement * @param reader an object that contains the data to set the parameter * value to * @throws SQLException if a database access error occurs - * @throws SQLFeatureNotSupportedException the JDBC driver does - * not support this method */ @Override public void setClob(int parameterIndex, Reader reader) throws SQLException { @@ -1416,7 +1272,7 @@ public class MonetPreparedStatement } // simply serialise the CLOB into a variable for now... far from // efficient, but might work for a few cases... - CharBuffer buf = CharBuffer.allocate((int)length); // have to down cast :( + CharBuffer buf = CharBuffer.allocate((int) length); // have to down cast :( try { reader.read(buf); } catch (IOException e) { @@ -1438,9 +1294,7 @@ public class MonetPreparedStatement * @throws SQLException if a database access error occurs */ @Override - public void setDate(int parameterIndex, java.sql.Date x) - throws SQLException - { + public void setDate(int parameterIndex, Date x) throws SQLException { setDate(parameterIndex, x, null); } @@ -1459,9 +1313,7 @@ public class MonetPreparedStatement * @throws SQLException if a database access error occurs */ @Override - public void setDate(int parameterIndex, java.sql.Date x, Calendar cal) - throws SQLException - { + public void setDate(int parameterIndex, Date x, Calendar cal) throws SQLException { if (x == null) { setNull(parameterIndex, -1); return; @@ -1470,10 +1322,6 @@ public class MonetPreparedStatement if (cal == null) { setValue(parameterIndex, "date '" + x.toString() + "'"); } else { - if (mDate == null) { - // first time usage, create and keep the mDate object for next usage - mDate = new SimpleDateFormat("yyyy-MM-dd"); - } mDate.setTimeZone(cal.getTimeZone()); setValue(parameterIndex, "date '" + mDate.format(x) + "'"); } @@ -1562,9 +1410,7 @@ public class MonetPreparedStatement * not support this method */ @Override - public void setNCharacterStream(int parameterIndex, Reader value, long length) - throws SQLException - { + public void setNCharacterStream(int parameterIndex, Reader value, long length) throws SQLException { setCharacterStream(parameterIndex, value, length); } @@ -1681,9 +1527,7 @@ public class MonetPreparedStatement * @throws SQLException if a database access error occurs */ @Override - public void setNull(int parameterIndex, int sqlType, String typeName) - throws SQLException - { + public void setNull(int parameterIndex, int sqlType, String typeName) throws SQLException { // MonetDB/SQL's NULL needs no type setNull(parameterIndex, sqlType); } @@ -1732,9 +1576,7 @@ public class MonetPreparedStatement * @throws SQLException if a database access error occurs */ @Override - public void setObject(int parameterIndex, Object x, int targetSqlType) - throws SQLException - { + public void setObject(int parameterIndex, Object x, int targetSqlType) throws SQLException { setObject(parameterIndex, x, targetSqlType, 0); } @@ -1772,18 +1614,11 @@ public class MonetPreparedStatement * @see Types */ @Override - public void setObject( - int parameterIndex, - Object x, - int targetSqlType, - int scale) - throws SQLException - { + public void setObject(int parameterIndex, Object x, int targetSqlType, int scale) throws SQLException { if (x == null) { setNull(parameterIndex, -1); return; } - // this is according to table B-5 if (x instanceof String) { setString(parameterIndex, (String)x); @@ -1799,13 +1634,13 @@ public class MonetPreparedStatement switch (targetSqlType) { case Types.TINYINT: setByte(parameterIndex, num.byteValue()); - break; + break; case Types.SMALLINT: setShort(parameterIndex, num.shortValue()); - break; + break; case Types.INTEGER: setInt(parameterIndex, num.intValue()); - break; + break; case Types.BIGINT: if (x instanceof BigDecimal) { BigDecimal bd = (BigDecimal)x; @@ -1813,23 +1648,23 @@ public class MonetPreparedStatement } else { setLong(parameterIndex, num.longValue()); } - break; + break; case Types.REAL: setFloat(parameterIndex, num.floatValue()); - break; + break; case Types.FLOAT: case Types.DOUBLE: setDouble(parameterIndex, num.doubleValue()); - break; + break; case Types.DECIMAL: case Types.NUMERIC: if (x instanceof BigDecimal) { setBigDecimal(parameterIndex, (BigDecimal)x); } else { setBigDecimal(parameterIndex, - new BigDecimal(num.doubleValue())); + new BigDecimal(num.doubleValue())); } - break; + break; case Types.BIT: case Types.BOOLEAN: if (num.doubleValue() != 0.0) { @@ -1837,13 +1672,13 @@ public class MonetPreparedStatement } else { setBoolean(parameterIndex, false); } - break; + break; case Types.CHAR: case Types.VARCHAR: case Types.LONGVARCHAR: case Types.CLOB: setString(parameterIndex, x.toString()); - break; + break; default: throw new SQLException("Conversion not allowed", "M1M05"); } @@ -1852,23 +1687,23 @@ public class MonetPreparedStatement switch (targetSqlType) { case Types.TINYINT: setByte(parameterIndex, (byte)(val ? 1 : 0)); - break; + break; case Types.SMALLINT: setShort(parameterIndex, (short)(val ? 1 : 0)); - break; + break; case Types.INTEGER: setInt(parameterIndex, (val ? 1 : 0)); // do not cast to (int) as it generates a compiler warning - break; + break; case Types.BIGINT: setLong(parameterIndex, (long)(val ? 1 : 0)); - break; + break; case Types.REAL: setFloat(parameterIndex, (float)(val ? 1.0 : 0.0)); - break; + break; case Types.FLOAT: case Types.DOUBLE: setDouble(parameterIndex, (val ? 1.0 : 0.0)); // do no cast to (double) as it generates a compiler warning - break; + break; case Types.DECIMAL: case Types.NUMERIC: { @@ -1883,13 +1718,13 @@ public class MonetPreparedStatement case Types.BIT: case Types.BOOLEAN: setBoolean(parameterIndex, val); - break; + break; case Types.CHAR: case Types.VARCHAR: case Types.LONGVARCHAR: case Types.CLOB: setString(parameterIndex, x.toString()); - break; + break; default: throw new SQLException("Conversion not allowed", "M1M05"); } @@ -1898,7 +1733,7 @@ public class MonetPreparedStatement switch (targetSqlType) { case Types.BIGINT: setLong(parameterIndex, num.longValue()); - break; + break; case Types.DECIMAL: case Types.NUMERIC: { @@ -1915,7 +1750,7 @@ public class MonetPreparedStatement case Types.LONGVARCHAR: case Types.CLOB: setString(parameterIndex, x.toString()); - break; + break; default: throw new SQLException("Conversion not allowed", "M1M05"); } @@ -1925,7 +1760,7 @@ public class MonetPreparedStatement case Types.VARBINARY: case Types.LONGVARBINARY: setBytes(parameterIndex, (byte[])x); - break; + break; default: throw new SQLException("Conversion not allowed", "M1M05"); } @@ -1943,50 +1778,49 @@ public class MonetPreparedStatement setDate(parameterIndex, new java.sql.Date(((Timestamp)x).getTime())); } else if (x instanceof java.util.Date) { setDate(parameterIndex, new java.sql.Date( - ((java.util.Date)x).getTime())); + ((java.util.Date)x).getTime())); } else if (x instanceof Calendar) { - setDate(parameterIndex, new java.sql.Date( - ((Calendar)x).getTimeInMillis())); + setDate(parameterIndex, new java.sql.Date(((Calendar)x).getTimeInMillis())); } else { throw new SQLException("Conversion not allowed", "M1M05"); } - break; + break; case Types.TIME: + case 2013: //Types.TIME_WITH_TIMEZONE: if (x instanceof Time) { setTime(parameterIndex, (Time)x); } else if (x instanceof Timestamp) { setTime(parameterIndex, new Time(((Timestamp)x).getTime())); } else if (x instanceof java.util.Date) { setTime(parameterIndex, new java.sql.Time( - ((java.util.Date)x).getTime())); + ((java.util.Date)x).getTime())); } else if (x instanceof Calendar) { - setTime(parameterIndex, new java.sql.Time( - ((Calendar)x).getTimeInMillis())); + setTime(parameterIndex, new java.sql.Time(((Calendar)x).getTimeInMillis())); } else { throw new SQLException("Conversion not allowed", "M1M05"); } - break; + break; case Types.TIMESTAMP: + case 2014: //Types.TIMESTAMP_WITH_TIMEZONE: if (x instanceof Timestamp) { setTimestamp(parameterIndex, (Timestamp)x); } else if (x instanceof java.sql.Date) { setTimestamp(parameterIndex, new Timestamp(((java.sql.Date)x).getTime())); } else if (x instanceof java.util.Date) { setTimestamp(parameterIndex, new java.sql.Timestamp( - ((java.util.Date)x).getTime())); + ((java.util.Date)x).getTime())); } else if (x instanceof Calendar) { - setTimestamp(parameterIndex, new java.sql.Timestamp( - ((Calendar)x).getTimeInMillis())); + setTimestamp(parameterIndex, new java.sql.Timestamp(((Calendar)x).getTimeInMillis())); } else { throw new SQLException("Conversion not allowed", "M1M05"); } - break; + break; case Types.CHAR: case Types.VARCHAR: case Types.LONGVARCHAR: case Types.CLOB: setString(parameterIndex, x.toString()); - break; + break; default: throw new SQLException("Conversion not allowed", "M1M05"); } @@ -2023,8 +1857,8 @@ public class MonetPreparedStatement // with the actual sqltype the server expects, or we // will get an error back setValue( - paramnr, - sqltype + " '" + x.replaceAll("\\\\", "\\\\\\\\").replaceAll("'", "\\\\'") + "'" + paramnr, + sqltype + " '" + x.replaceAll("\\\\", "\\\\\\\\").replaceAll("'", "\\\\'") + "'" ); } @@ -2225,7 +2059,6 @@ public class MonetPreparedStatement setNull(parameterIndex, -1); return; } - int paramIdx = getParamIdx(parameterIndex); // this will throw a SQLException if parameter can not be found /* depending on the parameter data type (as expected by MonetDB) we @@ -2249,9 +2082,8 @@ public class MonetPreparedStatement try { // check if x represents a valid inet string to prevent // failing exec #(..., ...) calls which destroy the prepared statement, see bug 6351 - nl.cwi.monetdb.jdbc.types.INET inet_obj = new nl.cwi.monetdb.jdbc.types.INET(); - inet_obj.fromString(x); - } catch (SQLException se) { + nl.cwi.monetdb.jdbc.MonetINET inet_obj = new nl.cwi.monetdb.jdbc.MonetINET(x); + } catch (Exception se) { throw new SQLDataException("Conversion of string: " + x + " to parameter data type " + paramMonetdbType + " failed. " + se.getMessage(), "22M29"); } castprefix = "inet "; @@ -2315,16 +2147,17 @@ public class MonetPreparedStatement try { // check (by calling parse) if the string represents a valid number to prevent // failing exec #(..., ...) calls which destroy the prepared statement, see bug 6351 - if (paramJdbcType == Types.INTEGER || paramJdbcType == Types.SMALLINT || paramJdbcType == Types.TINYINT) { + if (paramJdbcType == Types.TINYINT) { + int number = Byte.parseByte(x); + } else if (paramJdbcType == Types.SMALLINT ) { + int number = Short.parseShort(x); + } else if (paramJdbcType == Types.INTEGER) { int number = Integer.parseInt(x); - } else - if (paramJdbcType == Types.BIGINT) { + } else if (paramJdbcType == Types.BIGINT) { long number = Long.parseLong(x); - } else - if (paramJdbcType == Types.REAL || paramJdbcType == Types.DOUBLE || paramJdbcType == Types.FLOAT) { + } else if (paramJdbcType == Types.REAL || paramJdbcType == Types.DOUBLE || paramJdbcType == Types.FLOAT) { double number = Double.parseDouble(x); - } else - if (paramJdbcType == Types.DECIMAL || paramJdbcType == Types.NUMERIC) { + } else { BigDecimal number = new BigDecimal(x); } } catch (NumberFormatException nfe) { @@ -2342,17 +2175,17 @@ public class MonetPreparedStatement break; case Types.DATE: case Types.TIME: + case 2013: //Types.TIME_WITH_TIMEZONE: case Types.TIMESTAMP: + case 2014: //Types.TIMESTAMP_WITH_TIMEZONE: try { // check if the string represents a valid calendar date or time or timestamp to prevent // failing exec #(..., ...) calls which destroy the prepared statement, see bug 6351 if (paramJdbcType == Types.DATE) { java.sql.Date datum = java.sql.Date.valueOf(x); - } else - if (paramJdbcType == Types.TIME) { + } else if (paramJdbcType == Types.TIME || paramJdbcType == 2013) { Time tijdstip = Time.valueOf(x); - } else - if (paramJdbcType == Types.TIMESTAMP) { + } else { Timestamp tijdstip = Timestamp.valueOf(x); } } catch (IllegalArgumentException iae) { @@ -2432,9 +2265,7 @@ public class MonetPreparedStatement * @throws SQLException if a database access error occurs */ @Override - public void setTime(int parameterIndex, Time x, Calendar cal) - throws SQLException - { + public void setTime(int parameterIndex, Time x, Calendar cal) throws SQLException { if (x == null) { setNull(parameterIndex, -1); return; @@ -2445,13 +2276,8 @@ public class MonetPreparedStatement if (hasTimeZone) { // timezone shouldn't matter, since the server is timezone // aware in this case - if (mTimeZ == null) { - // first time usage, create and keep the mTimeZ object for next usage - mTimeZ = new SimpleDateFormat("HH:mm:ss.SSSZ"); - } String RFC822 = mTimeZ.format(x); - setValue(parameterIndex, "timetz '" + - RFC822.substring(0, 15) + ":" + RFC822.substring(15) + "'"); + setValue(parameterIndex, "timetz '" + RFC822.substring(0, 15) + ":" + RFC822.substring(15) + "'"); } else { // server is not timezone aware for this field, and no // calendar given, since we told the server our timezone at @@ -2460,10 +2286,6 @@ public class MonetPreparedStatement if (cal == null) { setValue(parameterIndex, "time '" + x.toString() + "'"); } else { - if (mTime == null) { - // first time usage, create and keep the mTime object for next usage - mTime = new SimpleDateFormat("HH:mm:ss.SSS"); - } mTime.setTimeZone(cal.getTimeZone()); setValue(parameterIndex, "time '" + mTime.format(x) + "'"); } @@ -2480,9 +2302,7 @@ public class MonetPreparedStatement * @throws SQLException if a database access error occurs */ @Override - public void setTimestamp(int parameterIndex, Timestamp x) - throws SQLException - { + public void setTimestamp(int parameterIndex, Timestamp x) throws SQLException { setTimestamp(parameterIndex, x, null); } @@ -2503,9 +2323,7 @@ public class MonetPreparedStatement * @throws SQLException if a database access error occurs */ @Override - public void setTimestamp(int parameterIndex, Timestamp x, Calendar cal) - throws SQLException - { + public void setTimestamp(int parameterIndex, Timestamp x, Calendar cal) throws SQLException { if (x == null) { setNull(parameterIndex, -1); return; @@ -2516,13 +2334,8 @@ public class MonetPreparedStatement if (hasTimeZone) { // timezone shouldn't matter, since the server is timezone // aware in this case - if (mTimestampZ == null) { - // first time usage, create and keep the mTimestampZ object for next usage - mTimestampZ = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSSZ"); - } String RFC822 = mTimestampZ.format(x); - setValue(parameterIndex, "timestamptz '" + - RFC822.substring(0, 26) + ":" + RFC822.substring(26) + "'"); + setValue(parameterIndex, "timestamptz '" + RFC822.substring(0, 26) + ":" + RFC822.substring(26) + "'"); } else { // server is not timezone aware for this field, and no // calendar given, since we told the server our timezone at @@ -2530,10 +2343,6 @@ public class MonetPreparedStatement if (cal == null) { setValue(parameterIndex, "timestamp '" + x.toString() + "'"); } else { - if (mTimestamp == null) { - // first time usage, create and keep the mTimestamp object for next usage - mTimestamp = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS"); - } mTimestamp.setTimeZone(cal.getTimeZone()); setValue(parameterIndex, "timestamp '" + mTimestamp.format(x) + "'"); } @@ -2562,9 +2371,7 @@ public class MonetPreparedStatement */ @Override @Deprecated - public void setUnicodeStream(int parameterIndex, InputStream x, int length) - throws SQLException - { + public void setUnicodeStream(int parameterIndex, InputStream x, int length) throws SQLException { throw newSQLFeatureNotSupportedException("setUnicodeStream"); } @@ -2585,9 +2392,8 @@ public class MonetPreparedStatement } String val = x.toString(); - setValue(parameterIndex, - "url '" + val.replaceAll("\\\\", "\\\\\\\\").replaceAll("'", "\\\\'") + "'" - ); + setValue(parameterIndex, "url '" + val.replaceAll("\\\\", "\\\\\\\\") + .replaceAll("'", "\\\\'") + "'"); } /** @@ -2608,7 +2414,7 @@ public class MonetPreparedStatement public void close() { try { if (!closed && id != -1) - connection.sendControlCommand("release " + id); + connection.sendControlCommand(ControlCommands.RELEASE, id); } catch (SQLException e) { // probably server closed connection } @@ -2616,8 +2422,7 @@ public class MonetPreparedStatement } /** - * Call close to release the server-sided handle for this - * PreparedStatement. + * Call close to release the server-sided handle for this PreparedStatement. */ @Override protected void finalize() { @@ -2627,9 +2432,8 @@ public class MonetPreparedStatement //== end methods interface PreparedStatement /** - * Sets the given index with the supplied value. If the given index is - * out of bounds, and SQLException is thrown. The given value should - * never be null. + * Sets the given index with the supplied value. If the given index is out of bounds, and SQLException is thrown. + * The given value should never be null. * * @param parameterIndex the parameter index * @param val the exact String representation to set @@ -2677,7 +2481,7 @@ public class MonetPreparedStatement * @param paramIdx the parameter index number * @return a new created SQLDataException object with SQLState 22010 */ - private final static SQLDataException newSQLInvalidParameterIndexException(int paramIdx) { + private static SQLDataException newSQLInvalidParameterIndexException(int paramIdx) { return new SQLDataException("Invalid Parameter Index number: " + paramIdx, "22010"); } @@ -2689,7 +2493,7 @@ public class MonetPreparedStatement * @param name the method name * @return a new created SQLFeatureNotSupportedException object with SQLState 0A000 */ - private final static SQLFeatureNotSupportedException newSQLFeatureNotSupportedException(String name) { + private static SQLFeatureNotSupportedException newSQLFeatureNotSupportedException(String name) { return new SQLFeatureNotSupportedException("Method " + name + " not implemented", "0A000"); } }
--- a/src/main/java/nl/cwi/monetdb/jdbc/MonetResultSet.java +++ b/src/main/java/nl/cwi/monetdb/jdbc/MonetResultSet.java @@ -8,42 +8,20 @@ package nl.cwi.monetdb.jdbc; -import nl.cwi.monetdb.mcl.parser.MCLParseException; -import nl.cwi.monetdb.mcl.parser.TupleLineParser; -import java.io.ByteArrayInputStream; -import java.io.InputStream; -import java.io.Reader; -import java.io.StringReader; +import nl.cwi.monetdb.mcl.protocol.ProtocolException; +import nl.cwi.monetdb.mcl.responses.AbstractDataBlockResponse; +import nl.cwi.monetdb.mcl.responses.ResultSetResponse; + +import java.io.*; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.math.BigDecimal; import java.net.MalformedURLException; import java.net.URL; -import java.sql.Array; -import java.sql.Blob; -import java.sql.Clob; -import java.sql.Connection; -import java.sql.DatabaseMetaData; -import java.sql.NClob; -import java.sql.Ref; -import java.sql.ResultSet; -import java.sql.ResultSetMetaData; -import java.sql.RowId; -import java.sql.SQLData; -import java.sql.SQLDataException; -import java.sql.SQLException; -import java.sql.SQLFeatureNotSupportedException; -import java.sql.SQLInput; -import java.sql.SQLWarning; -import java.sql.SQLXML; -import java.sql.Statement; -import java.sql.Time; -import java.sql.Timestamp; -import java.sql.Types; -import java.text.SimpleDateFormat; +import java.sql.*; import java.util.Calendar; import java.util.Map; -import java.util.TimeZone; +import java.util.UUID; /** * A ResultSet suitable for the MonetDB database. @@ -63,22 +41,16 @@ import java.util.TimeZone; * for FORWARD_ONLY result sets the memory usage will be likely lower for large * result sets. * - * @author Fabian Groffen, Martin van Dinther + * @author Fabian Groffen, Martin van Dinther, Pedro Ferreira * @version 0.8 */ -public class MonetResultSet - extends MonetWrapper - implements ResultSet, AutoCloseable -{ - // the following have default access modifier for the MonetVirtualResultSet subclass - /** The current line of the buffer split in columns */ - final TupleLineParser tlp; +public class MonetResultSet extends MonetWrapper implements ResultSet, AutoCloseable { + /** The current position of the cursor for this ResultSet object */ int curRow = 0; - // a blank final is immutable once assigned in the constructor /** A Header to retrieve lines from */ - private final MonetConnection.ResultSetResponse header; + private final ResultSetResponse header; /** The names of the columns in this ResultSet */ private final String[] columns; /** The MonetDB types of the columns in this ResultSet */ @@ -87,20 +59,18 @@ public class MonetResultSet private final int[] JdbcSQLTypes; /** The number of rows in this ResultSet */ final int tupleCount; - /** The parental Statement object */ private final Statement statement; - /** The type of this ResultSet (forward or scrollable) */ private int type = TYPE_FORWARD_ONLY; /** The concurrency of this ResultSet (currently only read-only) */ private int concurrency = CONCUR_READ_ONLY; /** The warnings for this ResultSet object */ private SQLWarning warnings; - /** whether the last read field (via some getXyz() method) was NULL */ - private boolean lastReadWasNull = true; /** Just a dummy variable to keep store the fetchsize set. */ private int fetchSize; + /** The current row's values */ + private AbstractDataBlockResponse currentBlock; /** * Main constructor backed by the given Header. @@ -108,13 +78,8 @@ public class MonetResultSet * @param statement the statement which created this ResultSet * @param header a header containing the query, resultset type, etc. * @throws IllegalArgumentException if called with null or invalid value for one of the arguments - * @throws SQLException is a protocol error occurs */ - MonetResultSet( - Statement statement, - MonetConnection.ResultSetResponse header) - throws SQLException - { + MonetResultSet(Statement statement, ResultSetResponse header) throws IllegalArgumentException { if (statement == null) { throw new IllegalArgumentException("Statement may not be null!"); } @@ -125,23 +90,14 @@ public class MonetResultSet this.header = header; this.type = header.getRSType(); this.concurrency = header.getRSConcur(); - /* if we have a header object, the fetchSize used for this result set - is the header's cacheSize */ + /* if we have a header object, the fetchSize used for this result set is the header's cacheSize */ this.fetchSize = header.getCacheSize(); - // well there is only one supported concurrency, so we don't have to - // bother about that - - // throws SQLException on getters of Header, so we find out immediately - // if an error occurred for this query - columns = header.getNames(); - types = header.getTypes(); - tupleCount = header.tuplecount; - - // create result array - tlp = new TupleLineParser(columns.length); - - JdbcSQLTypes = new int[types.length]; - populateJdbcSQLtypesArray(); + // well there is only one supported concurrency, so we don't have to bother about that + // throws SQLException on getters of Header, so we find out immediately if an error occurred for this query + this.tupleCount = header.getTuplecount(); + this.columns = header.getNames(); + this.types = header.getTypes(); + this.JdbcSQLTypes = header.getJdbcSQLTypes(); } /** @@ -153,16 +109,9 @@ public class MonetResultSet * @param types the column types * @param results the number of rows in the ResultSet * @throws IllegalArgumentException if called with null or invalid value for one of the arguments - * @throws IOException if communicating with monet failed - * @throws SQLException is a protocol error occurs */ - MonetResultSet( - Statement statement, - String[] columns, - String[] types, - int results - ) throws IllegalArgumentException - { + MonetResultSet(Statement statement, String[] columns, String[] types, int[] JdbcSQLTypes, int results) + throws IllegalArgumentException { if (statement == null) { throw new IllegalArgumentException("Statement may not be null!"); } @@ -175,73 +124,43 @@ public class MonetResultSet if (results < 0) { throw new IllegalArgumentException("Negative rowcount not allowed!"); } - this.statement = statement; this.header = null; this.fetchSize = 0; - + this.tupleCount = results; this.columns = columns; this.types = types; - this.tupleCount = results; - - this.tlp = new TupleLineParser(columns.length); - - JdbcSQLTypes = new int[types.length]; - populateJdbcSQLtypesArray(); + this.JdbcSQLTypes = JdbcSQLTypes; } - /** - * Internal utility method to fill the JdbcSQLTypes array with derivable values. - * By doing it once (in the constructor) we can avoid doing this in many getXyz() methods again and again - * thereby improving getXyz() method performance. - */ - private void populateJdbcSQLtypesArray() { - for (int i = 0; i < types.length; i++) { - int javaSQLtype = MonetDriver.getJavaType(types[i]); - JdbcSQLTypes[i] = javaSQLtype; - if (javaSQLtype == Types.BLOB) { - try { - if (((MonetConnection)statement.getConnection()).getBlobAsBinary()) - JdbcSQLTypes[i] = Types.BINARY; - } catch (SQLException se) { /* ignore it */ } - } - } - } - - //== methods of interface ResultSet // Chapter 14.2.2 Sun JDBC 3.0 Specification + /** * Moves the cursor to the given row number in this ResultSet object. * - * If the row number is positive, the cursor moves to the given row number - * with respect to the beginning of the result set. The first row is row 1, - * the second is row 2, and so on. + * If the row number is positive, the cursor moves to the given row number with respect to the beginning of the + * result set. The first row is row 1, the second is row 2, and so on. * - * If the given row number is negative, the cursor moves to an absolute row - * position with respect to the end of the result set. For example, calling - * the method absolute(-1) positions the cursor on the last row; calling the + * If the given row number is negative, the cursor moves to an absolute row position with respect to the end of the + * result set. For example, calling the method absolute(-1) positions the cursor on the last row; calling the * method absolute(-2) moves the cursor to the next-to-last row, and so on. * - * An attempt to position the cursor beyond the first/last row in the result - * set leaves the cursor before the first row or after the last row. - * Note: calling absolute(1) is the same as calling first(). Calling - * absolute(-1) is the same as calling last(). + * An attempt to position the cursor beyond the first/last row in the result set leaves the cursor before the first + * row or after the last row. + * Note: calling absolute(1) is the same as calling first(). Calling absolute(-1) is the same as calling last(). * - * @param row the number of the row to which the cursor should move. A - * positive number indicates the row number counting from the - * beginning of the result set; a negative number indicates the row - * number counting from the end of the result set + * @param row the number of the row to which the cursor should move. A positive number indicates the row number + * counting from the beginning of the result set; a negative number indicates the row number counting from + * the end of the result set * @return true if the cursor is on the result set; false otherwise - * @throws SQLException if a database access error occurs, or the result set - * type is TYPE_FORWARD_ONLY + * @throws SQLException if a database access error occurs, or the result set type is TYPE_FORWARD_ONLY */ @Override public boolean absolute(int row) throws SQLException { if (row != curRow + 1 && type == TYPE_FORWARD_ONLY) - throw new SQLException("(Absolute) positioning not allowed on forward " + - " only result sets!", "M1M05"); + throw new SQLException("(Absolute) positioning not allowed on forward only result sets!", "M1M05"); if (header.isClosed()) throw new SQLException("ResultSet is closed!", "M1M20"); @@ -257,28 +176,16 @@ public class MonetResultSet else if (row > tupleCount + 1) row = tupleCount + 1; // after last - String tmpLine = header.getLine(row - 1); - - // store it - curRow = row; - - if (tmpLine == null) - return false; - try { - tlp.parse(tmpLine); - } catch (MCLParseException e) { - throw new SQLException(e.getMessage(), "M0M10"); - } - - return true; + this.curRow = row; + this.currentBlock = header.getDataBlockCorrespondingToLine(row - 1); + return this.curRow <= this.tupleCount; } /** - * Moves the cursor to the end of this ResultSet object, just after the last - * row. This method has no effect if the result set contains no rows. + * Moves the cursor to the end of this ResultSet object, just after the last row. This method has no effect if the + * result set contains no rows. * - * @throws SQLException if a database access error occurs or the result set - * type is TYPE_FORWARD_ONLY + * @throws SQLException if a database access error occurs or the result set type is TYPE_FORWARD_ONLY */ @Override public void afterLast() throws SQLException { @@ -286,11 +193,10 @@ public class MonetResultSet } /** - * Moves the cursor to the front of this ResultSet object, just before the - * first row. This method has no effect if the result set contains no rows. + * Moves the cursor to the front of this ResultSet object, just before the first row. This method has no effect + * if the result set contains no rows. * - * @throws SQLException if a database access error occurs or the result set - * type is TYPE_FORWARD_ONLY + * @throws SQLException if a database access error occurs or the result set type is TYPE_FORWARD_ONLY */ @Override public void beforeFirst() throws SQLException { @@ -298,9 +204,8 @@ public class MonetResultSet } /** - * Clears all warnings reported for this ResultSet object. After a call to - * this method, the method getWarnings returns null until a new warning is - * reported for this ResultSet object. + * Clears all warnings reported for this ResultSet object. After a call to this method, the method getWarnings + * returns null until a new warning is reported for this ResultSet object. */ @Override public void clearWarnings() { @@ -308,24 +213,24 @@ public class MonetResultSet } /** - * Releases this ResultSet object's database (and JDBC) resources - * immediately instead of waiting for this to happen when it is - * automatically closed. + * Releases this ResultSet object's database (and JDBC) resources immediately instead of waiting for this to happen + * when it is automatically closed. */ @Override public void close() { if (header != null && !header.isClosed()) { header.close(); } - if (statement instanceof MonetStatement) + if (statement instanceof MonetStatement) { ((MonetStatement)statement).closeIfCompletion(); + } } // Chapter 14.2.3 from Sun JDBC 3.0 specification + /** - * Maps the given ResultSet column name to its ResultSet column index. - * Column names supplied to getter methods are case insensitive. If a select - * list contains the same column more than once, the first instance of the + * Maps the given ResultSet column name to its ResultSet column index. Column names supplied to getter methods are + * case insensitive. If a select list contains the same column more than once, the first instance of the * column will be returned. * * @param columnLabel the name of the column @@ -352,10 +257,8 @@ public class MonetResultSet /** * Moves the cursor to the first row in this ResultSet object. * - * @return true if the cursor is on a valid row; false if there are no rows - * in the result set - * @throws SQLException - if a database access error occurs or the result - * set type is TYPE_FORWARD_ONLY + * @return true if the cursor is on a valid row; false if there are no rows in the result set + * @throws SQLException - if a database access error occurs or the result set type is TYPE_FORWARD_ONLY */ @Override public boolean first() throws SQLException { @@ -366,16 +269,45 @@ public class MonetResultSet public Array getArray(int columnIndex) throws SQLException { throw newSQLFeatureNotSupportedException("getArray"); } + @Override public Array getArray(String columnLabel) throws SQLException { throw newSQLFeatureNotSupportedException("getArray"); } - /* Mapi doesn't allow something for streams at the moment, thus all not implemented for now */ @Override public InputStream getAsciiStream(int columnIndex) throws SQLException { - throw newSQLFeatureNotSupportedException("getAsciiStream"); + try { + InputStream res = null; + switch (JdbcSQLTypes[columnIndex - 1]) { + case Types.CLOB: + Clob cl = getClob(columnIndex); + if(cl != null) { + res = cl.getAsciiStream(); + } + break; + case Types.BLOB: + case Types.LONGVARBINARY: + case Types.CHAR: + case Types.VARCHAR: + case Types.LONGVARCHAR: + byte[] bytes = getBytes(columnIndex); + if(bytes != null) { + res = new ByteArrayInputStream(getBytes(columnIndex)); + } + break; + default: + throw new SQLException("Conversion from " + types[columnIndex - 1] + + " to ascii stream not supported", "M1M05"); + } + return res; + } catch (ClassCastException ex) { + throw new SQLException(ex.getMessage()); + } catch (IndexOutOfBoundsException e) { + throw newSQLInvalidColumnIndexException(columnIndex); + } } + @Override public InputStream getAsciiStream(String columnLabel) throws SQLException { throw newSQLFeatureNotSupportedException("getAsciiStream"); @@ -386,6 +318,7 @@ public class MonetResultSet public InputStream getUnicodeStream(int columnIndex) throws SQLException { throw newSQLFeatureNotSupportedException("getUnicodeStream"); } + @Override @Deprecated public InputStream getUnicodeStream(String columnLabel) throws SQLException { @@ -393,69 +326,64 @@ public class MonetResultSet } /** - * Retrieves the value of the designated column in the current row - * of this ResultSet object as a stream of uninterpreted bytes. The - * value can then be read in chunks from the stream. This method is - * particularly suitable for retrieving large LONGVARBINARY values. + * Retrieves the value of the designated column in the current row of this ResultSet object as a stream of + * uninterpreted bytes. The value can then be read in chunks from the stream. This method is particularly suitable + * for retrieving large LONGVARBINARY values. * <br/><br/> - * Note: All the data in the returned stream must be read prior to - * getting the value of any other column. The next call to a getter - * method implicitly closes the stream. Also, a stream may return 0 - * when the method InputStream.available is called whether there is - * data available or not. + * Note: All the data in the returned stream must be read prior to getting the value of any other column. The next + * call to a getter method implicitly closes the stream. Also, a stream may return 0 when the method + * InputStream.available is called whether there is data available or not. * * @param columnIndex the first column is 1, the second is 2, ... - * @return a Java input stream that delivers the database column - * value as a stream of uninterpreted bytes; if the value is SQL - * NULL, the value returned is null - * @throws SQLException if the columnIndex is not valid; if a - * database access error occurs or this method is called on a closed - * result set + * @return a Java input stream that delivers the database column value as a stream of uninterpreted bytes; if the + * value is SQL NULL, the value returned is null + * @throws SQLException if the columnIndex is not valid; if a database access error occurs or this method is called + * on a closed result set */ @Override public InputStream getBinaryStream(int columnIndex) throws SQLException { try { + InputStream res = null; switch (JdbcSQLTypes[columnIndex - 1]) { case Types.BLOB: - Blob blob = getBlob(columnIndex); - if (blob == null) - return null; - return blob.getBinaryStream(); - case Types.BINARY: - case Types.VARBINARY: + Blob cl = getBlob(columnIndex); + if(cl != null) { + res = cl.getBinaryStream(); + } + break; case Types.LONGVARBINARY: - byte[] bte = getBytes(columnIndex); - if (bte == null) - return null; - return new ByteArrayInputStream(bte); + byte[] bytes = getBytes(columnIndex); + if(bytes != null) { + res = new ByteArrayInputStream(getBytes(columnIndex)); + } + break; + default: + throw new SQLException("Conversion from " + types[columnIndex - 1] + + " to binary stream not supported", "M1M05"); } - throw new SQLException("Cannot operate on " + types[columnIndex - 1] + " type", "M1M05"); + return res; + } catch (ClassCastException ex) { + throw new SQLException(ex.getMessage()); } catch (IndexOutOfBoundsException e) { throw newSQLInvalidColumnIndexException(columnIndex); } } - + /** - * Retrieves the value of the designated column in the current row - * of this ResultSet object as a stream of uninterpreted bytes. The - * value can then be read in chunks from the stream. This method is - * particularly suitable for retrieving large LONGVARBINARY values. + * Retrieves the value of the designated column in the current row of this ResultSet object as a stream of + * uninterpreted bytes. The value can then be read in chunks from the stream. This method is particularly suitable + * for retrieving large LONGVARBINARY values. * <br/><br/> - * Note: All the data in the returned stream must be read prior to - * getting the value of any other column. The next call to a getter - * method implicitly closes the stream. Also, a stream may return 0 - * when the method available is called whether there is data - * available or not. + * Note: All the data in the returned stream must be read prior to getting the value of any other column. The next + * call to a getter method implicitly closes the stream. Also, a stream may return 0 when the method available is + * called whether there is data available or not. * - * @param columnLabel the label for the column specified with - * the SQL AS clause. If the SQL AS clause was not specified, then - * the label is the name of the column - * @return a Java input stream that delivers the database column - * value as a stream of uninterpreted bytes; if the value is SQL - * NULL, the result is null - * @throws SQLException if the columnLabel is not valid; if a - * database access error occurs or this method is called on a closed - * result set + * @param columnLabel the label for the column specified with he SQL AS clause. If the SQL AS clause was not + * specified, then the label is the name of the column + * @return a Java input stream that delivers the database column value as a stream of uninterpreted bytes; if the + * value is SQL NULL, the result is null + * @throws SQLException if the columnLabel is not valid; if a database access error occurs or this method is called + * on a closed result set */ @Override public InputStream getBinaryStream(String columnLabel) throws SQLException { @@ -463,38 +391,35 @@ public class MonetResultSet } /** - * Retrieves the value of the designated column in the current row - * of this ResultSet object as a java.io.Reader object. + * Retrieves the value of the designated column in the current row of this ResultSet object as a java.io.Reader + * object. * * @param columnIndex the first column is 1, the second is 2, ... - * @return a java.io.Reader object that contains the column value; - * if the value is SQL NULL, the value returned is null in - * the Java programming language. + * @return a java.io.Reader object that contains the column value; if the value is SQL NULL, the value returned + * is null in the Java programming language. * @throws SQLException if a database access error occurs */ @Override public Reader getCharacterStream(int columnIndex) throws SQLException { try { - String val = tlp.values[columnIndex - 1]; - if (val == null) { - lastReadWasNull = true; - return null; - } - lastReadWasNull = false; - return new StringReader(val); + String ss = currentBlock.getValueAsString(columnIndex - 1); + return (ss == null) ? null : new StringReader(ss); + } catch (ClassCastException ex) { + throw new SQLException(ex.getMessage()); + } catch (ProtocolException e) { + throw new SQLException(e); } catch (IndexOutOfBoundsException e) { throw newSQLInvalidColumnIndexException(columnIndex); } } /** - * Retrieves the value of the designated column in the current row - * of this ResultSet object as a java.io.Reader object. + * Retrieves the value of the designated column in the current row of this ResultSet object as a java.io.Reader + * object. * * @param columnLabel the name of the column - * @return a java.io.Reader object that contains the column value; - * if the value is SQL NULL, the value returned is null in - * the Java programming language. + * @return a java.io.Reader object that contains the column value; if the value is SQL NULL, the value returned is + * null in the Java programming language. * @throws SQLException if a database access error occurs */ @Override @@ -503,18 +428,14 @@ public class MonetResultSet } /** - * Retrieves the value of the designated column in the current row - * of this ResultSet object as a java.io.Reader object. It is - * intended for use when accessing NCHAR,NVARCHAR and LONGNVARCHAR - * columns. + * Retrieves the value of the designated column in the current row of this ResultSet object as a java.io.Reader + * object. It is intended for use when accessing NCHAR, NVARCHAR and LONGNVARCHAR columns. * * @param columnIndex the first column is 1, the second is 2, ... - * @return a java.io.Reader object that contains the column value; - * if the value is SQL NULL, the value returned is null in - * the Java programming language. + * @return a java.io.Reader object that contains the column value; if the value is SQL NULL, the value returned is + * null in the Java programming language. * @throws SQLException if a database access error occurs - * @throws SQLFeatureNotSupportedException the JDBC driver does - * not support this method + * @throws SQLFeatureNotSupportedException the JDBC driver does not support this method */ @Override public Reader getNCharacterStream(int columnIndex) throws SQLException { @@ -522,15 +443,12 @@ public class MonetResultSet } /** - * Retrieves the value of the designated column in the current row - * of this ResultSet object as a java.io.Reader object. It is - * intended for use when accessing NCHAR,NVARCHAR and LONGNVARCHAR - * columns. + * Retrieves the value of the designated column in the current row of this ResultSet object as a java.io.Reader + * object. It is intended for use when accessing NCHAR, NVARCHAR and LONGNVARCHAR columns. * * @param columnLabel the name of the column - * @return a java.io.Reader object that contains the column value; - * if the value is SQL NULL, the value returned is null in - * the Java programming language. + * @return a java.io.Reader object that contains the column value; if the value is SQL NULL, the value returned is + * null in the Java programming language. * @throws SQLException if a database access error occurs * @throws SQLFeatureNotSupportedException the JDBC driver does * not support this method @@ -541,39 +459,32 @@ public class MonetResultSet } /** - * Retrieves the value of the designated column in the current row - * of this ResultSet object as a Blob object in the Java programming - * language. + * Retrieves the value of the designated column in the current row of this ResultSet object as a Blob object in + * the Java programming language. * * @param columnIndex the first column is 1, the second is 2, ... - * @return a Blob object representing the SQL BLOB value in the - * specified column + * @return a Blob object representing the SQL BLOB value in the specified column * @throws SQLException if a database access error occurs */ @Override public Blob getBlob(int columnIndex) throws SQLException { try { - String val = tlp.values[columnIndex - 1]; - if (val == null) { - lastReadWasNull = true; - return null; - } - lastReadWasNull = false; - return MonetBlob.create(val); + return (MonetBlob) currentBlock.getObjectValue(columnIndex - 1); + } catch (ClassCastException ex) { + throw new SQLException(ex.getMessage()); + } catch (ProtocolException e) { + throw new SQLException(e); } catch (IndexOutOfBoundsException e) { throw newSQLInvalidColumnIndexException(columnIndex); } } /** - * Retrieves the value of the designated column in the current row - * of this ResultSet object as a Blob object in the Java programming - * language. + * Retrieves the value of the designated column in the current row of this ResultSet object as a Blob object in the + * Java programming language. * - * @param columnLabel the name of the column from which to retrieve - * the value - * @return a Blob object representing the SQL BLOB value in the - * specified column + * @param columnLabel the name of the column from which to retrieve the value + * @return a Blob object representing the SQL BLOB value in the specified column * @throws SQLException if a database access error occurs */ @Override @@ -582,39 +493,32 @@ public class MonetResultSet } /** - * Retrieves the value of the designated column in the current row - * of this ResultSet object as a Clob object in the + * Retrieves the value of the designated column in the current row of this ResultSet object as a Clob object in the * Java programming language. * * @param columnIndex the first column is 1, the second is 2, ... - * @return a Clob object representing the SQL CLOB value in the - * specified column + * @return a Clob object representing the SQL CLOB value in the specified column * @throws SQLException if a database access error occurs */ @Override public Clob getClob(int columnIndex) throws SQLException { try { - String val = tlp.values[columnIndex - 1]; - if (val == null) { - lastReadWasNull = true; - return null; - } - lastReadWasNull = false; - return new MonetClob(val); + return (MonetClob) currentBlock.getObjectValue(columnIndex - 1); + } catch (ClassCastException ex) { + throw new SQLException(ex.getMessage()); + } catch (ProtocolException e) { + throw new SQLException(e); } catch (IndexOutOfBoundsException e) { throw newSQLInvalidColumnIndexException(columnIndex); } } /** - * Retrieves the value of the designated column in the current row - * of this ResultSet object as a Clob object in the + * Retrieves the value of the designated column in the current row of this ResultSet object as a Clob object in the * Java programming language. * - * @param columnLabel the name of the column from which to retrieve - * the value - * @return a Clob object representing the SQL CLOB value in the - * specified column + * @param columnLabel the name of the column from which to retrieve the value + * @return a Clob object representing the SQL CLOB value in the specified column * @throws SQLException if a database access error occurs */ @Override @@ -623,16 +527,13 @@ public class MonetResultSet } /** - * Retrieves the value of the designated column in the current row - * of this ResultSet object as a NClob object in the + * Retrieves the value of the designated column in the current row of this ResultSet object as a NClob object in the * Java programming language. * * @param columnIndex the first column is 1, the second is 2, ... - * @return a NClob object representing the SQL NCLOB value in the - * specified column + * @return a NClob object representing the SQL NCLOB value in the specified column * @throws SQLException if a database access error occurs - * @throws SQLFeatureNotSupportedException the JDBC driver does - * not support this method + * @throws SQLFeatureNotSupportedException the JDBC driver does not support this method */ @Override public NClob getNClob(int columnIndex) throws SQLException { @@ -640,17 +541,13 @@ public class MonetResultSet } /** - * Retrieves the value of the designated column in the current row - * of this ResultSet object as a NClob object in the + * Retrieves the value of the designated column in the current row of this ResultSet object as a NClob object in the * Java programming language. * - * @param columnLabel the name of the column from which to retrieve - * the value - * @return a NClob object representing the SQL NCLOB value in the - * specified column + * @param columnLabel the name of the column from which to retrieve the value + * @return a NClob object representing the SQL NCLOB value in the specified column * @throws SQLException if a database access error occurs - * @throws SQLFeatureNotSupportedException the JDBC driver does - * not support this method + * @throws SQLFeatureNotSupportedException the JDBC driver does not support this method */ @Override public NClob getNClob(String columnLabel) throws SQLException { @@ -669,15 +566,47 @@ public class MonetResultSet @Override public BigDecimal getBigDecimal(int columnIndex) throws SQLException { try { - String val = tlp.values[columnIndex - 1]; - if (val == null) { - lastReadWasNull = true; - return null; + switch (JdbcSQLTypes[columnIndex - 1]) { + case Types.NUMERIC: + case Types.DECIMAL: + return (BigDecimal) currentBlock.getObjectValue(columnIndex - 1); + case Types.BOOLEAN: + boolean bol = currentBlock.getBooleanValue(columnIndex - 1); + return currentBlock.isLastReadWasNull() ? null : new BigDecimal(bol ? 1 : 0); + case Types.TINYINT: + byte bb = currentBlock.getByteValue(columnIndex - 1); + return currentBlock.isLastReadWasNull() ? null : new BigDecimal(bb); + case Types.SMALLINT: + short sh = currentBlock.getShortValue(columnIndex - 1); + return currentBlock.isLastReadWasNull() ? null : new BigDecimal(sh); + case Types.INTEGER: + int in = currentBlock.getIntValue(columnIndex - 1); + return currentBlock.isLastReadWasNull() ? null : new BigDecimal(in); + case Types.BIGINT: + long lon = currentBlock.getLongValue(columnIndex - 1); + return currentBlock.isLastReadWasNull() ? null : new BigDecimal(lon); + case Types.REAL: + float floa = currentBlock.getFloatValue(columnIndex - 1); + return currentBlock.isLastReadWasNull() ? null : new BigDecimal(floa); + case Types.DOUBLE: + double dou = currentBlock.getDoubleValue(columnIndex - 1); + return currentBlock.isLastReadWasNull() ? null : new BigDecimal(dou); + case Types.CHAR: + case Types.VARCHAR: + case Types.LONGVARCHAR: + case Types.CLOB: + case Types.BLOB: + case Types.LONGVARBINARY: + String ss = currentBlock.getValueAsString(columnIndex - 1); + return (ss == null) ? null : new BigDecimal(ss); + default: //OTHERS, BLOB, LONGVARBINARY, TIME... + throw new SQLException("Conversion from " + types[columnIndex - 1] + + " to boolean type not supported", "M1M05"); } - lastReadWasNull = false; - return new BigDecimal(val); - } catch (NumberFormatException e) { - throw newSQLNumberFormatException(e); + } catch (ClassCastException ex) { + throw new SQLException(ex.getMessage()); + } catch (ProtocolException e) { + throw new SQLException(e); } catch (IndexOutOfBoundsException e) { throw newSQLInvalidColumnIndexException(columnIndex); } @@ -695,22 +624,15 @@ public class MonetResultSet */ @Override @Deprecated - public BigDecimal getBigDecimal(int columnIndex, int scale) - throws SQLException - { + public BigDecimal getBigDecimal(int columnIndex, int scale) throws SQLException { try { - String val = tlp.values[columnIndex - 1]; - if (val == null) { - lastReadWasNull = true; - return null; + BigDecimal val = getBigDecimal(columnIndex); + if(val != null) { + val.setScale(scale); } - lastReadWasNull = false; - - BigDecimal bd = new BigDecimal(val); - bd.setScale(scale); - return bd; - } catch (NumberFormatException e) { - throw newSQLNumberFormatException(e); + return val; + } catch (ClassCastException ex) { + throw new SQLException(ex.getMessage()); } catch (IndexOutOfBoundsException e) { throw newSQLInvalidColumnIndexException(columnIndex); } @@ -742,88 +664,76 @@ public class MonetResultSet */ @Override @Deprecated - public BigDecimal getBigDecimal(String columnLabel, int scale) - throws SQLException - { + public BigDecimal getBigDecimal(String columnLabel, int scale) throws SQLException { return getBigDecimal(findColumn(columnLabel), scale); } // See Sun JDBC Specification 3.0 Table B-6 /** - * Retrieves the value of the designated column in the current row of this - * ResultSet object as a boolean in the Java programming language. + * Retrieves the value of the designated column in the current row of this ResultSet object as a boolean in the Java + * programming language. * * @param columnIndex the first column is 1, the second is 2, ... - * @return the column value; if the value is SQL NULL, the value returned - * is false + * @return the column value; if the value is SQL NULL, the value returned is false * @throws SQLException if there is no such column */ @Override public boolean getBoolean(int columnIndex) throws SQLException { try { - String val = tlp.values[columnIndex - 1]; - if (val == null) { - lastReadWasNull = true; - return false; // if the value is SQL NULL, the value returned is false - } - lastReadWasNull = false; - - // match common cases first - if ("false".equalsIgnoreCase(val) || "0".equals(val)) - return false; - if ("true".equalsIgnoreCase(val) || "1".equals(val)) - return true; - // match type specific values switch (JdbcSQLTypes[columnIndex - 1]) { case Types.BOOLEAN: + boolean bol = currentBlock.getBooleanValue(columnIndex - 1); + return !currentBlock.isLastReadWasNull() && bol; + case Types.TINYINT: + byte bb = currentBlock.getByteValue(columnIndex - 1); + return !currentBlock.isLastReadWasNull() && bb != 0; + case Types.SMALLINT: + short sh = currentBlock.getShortValue(columnIndex - 1); + return !currentBlock.isLastReadWasNull() && sh != 0; + case Types.INTEGER: + int in = currentBlock.getIntValue(columnIndex - 1); + return !currentBlock.isLastReadWasNull() && in != 0; + case Types.BIGINT: + long lon = currentBlock.getLongValue(columnIndex - 1); + return !currentBlock.isLastReadWasNull() && lon != 0; + case Types.REAL: + float floa = currentBlock.getFloatValue(columnIndex - 1); + return !currentBlock.isLastReadWasNull() && floa != 0.0f; + case Types.DOUBLE: + double dou = currentBlock.getDoubleValue(columnIndex - 1); + return !currentBlock.isLastReadWasNull() && dou != 0.0d; case Types.CHAR: case Types.VARCHAR: - case Types.LONGVARCHAR: // MonetDB doesn't use type LONGVARCHAR, it's here for completeness + case Types.LONGVARCHAR: case Types.CLOB: - // check if string value equals "true" (case insensitive) or not - return Boolean.parseBoolean(val); - case Types.BIT: // MonetDB doesn't use type BinaryDigit, it's here for completeness - case Types.TINYINT: - case Types.SMALLINT: - case Types.INTEGER: - if (getInt(columnIndex) == 0) { - return false; - } - return true; - case Types.BIGINT: - if (getLong(columnIndex) == 0L) { - return false; - } - return true; - case Types.DOUBLE: - case Types.FLOAT: - case Types.REAL: - if (getDouble(columnIndex) == 0.0) { - return false; - } - return true; + case Types.BLOB: + case Types.LONGVARBINARY: + String val = currentBlock.getValueAsString(columnIndex - 1); + return val != null && !"0".equals(val) && ("1".equals(val) || Boolean.parseBoolean(val)); + case Types.NUMERIC: case Types.DECIMAL: - case Types.NUMERIC: - if (getBigDecimal(columnIndex).compareTo(BigDecimal.ZERO) == 0) { - return false; - } - return true; - default: - throw new SQLException("Conversion from " + types[columnIndex - 1] + " to boolean type not supported", "M1M05"); + BigDecimal bigdec = (BigDecimal) currentBlock.getValueAsObject(columnIndex - 1); + return bigdec != null && bigdec.compareTo(BigDecimal.ZERO) != 0; + default: //OTHERS, BLOB, LONGVARBINARY, TIME... + throw new SQLException("Conversion from " + types[columnIndex - 1] + + " to boolean type not supported", "M1M05"); } + } catch (ClassCastException ex) { + throw new SQLException(ex.getMessage()); + } catch (ProtocolException e) { + throw new SQLException(e); } catch (IndexOutOfBoundsException e) { throw newSQLInvalidColumnIndexException(columnIndex); } } /** - * Retrieves the value of the designated column in the current row of this - * ResultSet object as a boolean in the Java programming language. + * Retrieves the value of the designated column in the current row of this ResultSet object as a boolean in the Java + * programming language. * * @param columnLabel the SQL name of the column - * @return the column value; if the value is SQL NULL, the value returned - * is false + * @return the column value; if the value is SQL NULL, the value returned is false * @throws SQLException if the ResultSet object does not contain columnLabel */ @Override @@ -832,26 +742,58 @@ public class MonetResultSet } /** - * Retrieves the value of the designated column in the current row of this - * ResultSet object as a byte in the Java programming language. + * Retrieves the value of the designated column in the current row of this ResultSet object as a byte in the Java + * programming language. * * @param columnIndex the first column is 1, the second is 2, ... - * @return the column value; if the value is SQL NULL, the value returned - * is 0 + * @return the column value; if the value is SQL NULL, the value returned is 0 * @throws SQLException if a database access error occurs */ @Override public byte getByte(int columnIndex) throws SQLException { try { - String val = tlp.values[columnIndex - 1]; - if (val == null) { - lastReadWasNull = true; - return (byte) 0; + switch (JdbcSQLTypes[columnIndex - 1]) { + case Types.TINYINT: + byte bb = currentBlock.getByteValue(columnIndex - 1); + return currentBlock.isLastReadWasNull() ? 0 : bb; + case Types.BOOLEAN: + boolean bol = currentBlock.getBooleanValue(columnIndex - 1); + return currentBlock.isLastReadWasNull() ? 0 : (bol ? (byte) 1 : (byte) 0); + case Types.SMALLINT: + short sh = currentBlock.getShortValue(columnIndex - 1); + return currentBlock.isLastReadWasNull() ? 0 : (byte) sh; + case Types.INTEGER: + int in = currentBlock.getIntValue(columnIndex - 1); + return currentBlock.isLastReadWasNull() ? 0 : (byte) in; + case Types.BIGINT: + long lon = currentBlock.getLongValue(columnIndex - 1); + return currentBlock.isLastReadWasNull() ? 0 : (byte) lon; + case Types.REAL: + float floa = currentBlock.getFloatValue(columnIndex - 1); + return currentBlock.isLastReadWasNull() ? 0 : (byte) Math.round(floa); + case Types.DOUBLE: + double dou = currentBlock.getDoubleValue(columnIndex - 1); + return currentBlock.isLastReadWasNull() ? 0 : (byte) Math.round(dou); + case Types.CHAR: + case Types.VARCHAR: + case Types.LONGVARCHAR: + case Types.CLOB: + case Types.BLOB: + case Types.LONGVARBINARY: + String ss = currentBlock.getValueAsString(columnIndex - 1); + return ss == null ? 0 : Byte.parseByte(ss); + case Types.NUMERIC: + case Types.DECIMAL: + BigDecimal bigdec = (BigDecimal) currentBlock.getValueAsObject(columnIndex - 1); + return bigdec == null ? 0 : bigdec.byteValue(); + default: //OTHERS, BLOB, LONGVARBINARY, TIME... + throw new SQLException("Conversion from " + types[columnIndex - 1] + + " to byte type not supported", "M1M05"); } - lastReadWasNull = false; - return Byte.parseByte(val); - } catch (NumberFormatException e) { - throw newSQLNumberFormatException(e); + } catch (ClassCastException ex) { + throw new SQLException(ex.getMessage()); + } catch (ProtocolException e) { + throw new SQLException(e); } catch (IndexOutOfBoundsException e) { throw newSQLInvalidColumnIndexException(columnIndex); } @@ -877,58 +819,44 @@ public class MonetResultSet * bytes represent the raw values returned by the driver. * * @param columnIndex the first column is 1, the second is 2, ... - * @return the column value; if the value is SQL NULL, the value returned - * is null + * @return the column value; if the value is SQL NULL, the value returned is null * @throws SQLException if a database access error occurs */ @Override public byte[] getBytes(int columnIndex) throws SQLException { try { - String val = tlp.values[columnIndex - 1]; - if (val == null) { - lastReadWasNull = true; - return null; - } - lastReadWasNull = false; - // According to Table B-6, getBytes() only operates on BINARY types switch (JdbcSQLTypes[columnIndex - 1]) { case Types.BLOB: - case Types.BINARY: - case Types.VARBINARY: + MonetBlob mb = (MonetBlob) currentBlock.getObjectValue(columnIndex - 1); + return mb == null ? null : mb.getBuffer(); case Types.LONGVARBINARY: // unpack the HEX (BLOB) notation to real bytes - int len = val.length() / 2; - byte[] buf = new byte[len]; - int offset; - for (int j = 0; j < len; j++) { - offset = j * 2; - buf[j] = (byte)Integer.parseInt(val.substring(offset, offset + 2), 16); - } - return buf; + return (byte[]) currentBlock.getObjectValue(columnIndex - 1); + case Types.CHAR: + case Types.VARCHAR: + case Types.LONGVARCHAR: + case Types.CLOB: + String ss = currentBlock.getValueAsString(columnIndex - 1); + return ss == null ? null : ss.getBytes(); default: throw new SQLException("Cannot operate on " + types[columnIndex - 1] + " type", "M1M05"); } - } catch (NumberFormatException e) { - throw newSQLNumberFormatException(e); + } catch (ClassCastException ex) { + throw new SQLException(ex.getMessage()); + } catch (ProtocolException e) { + throw new SQLException(e); } catch (IndexOutOfBoundsException e) { throw newSQLInvalidColumnIndexException(columnIndex); } } /** - * Retrieves the value of the designated column in the current row of this - * ResultSet object as a byte array in the Java programming language. The - * bytes represent the raw values returned by the driver. - * - * NOTE: Since the mapi protocol is ASCII-based, this method only returns - * Java byte representations of Strings, which is nothing more than - * an encoding into a sequence of bytes using the platform's default - * charset. + * Retrieves the value of the designated column in the current row of this ResultSet object as a byte array in the + * Java programming language. The bytes represent the raw values returned by the driver. * * @param columnLabel the SQL name of the column - * @return the column value; if the value is SQL NULL, the value returned - * is null + * @return the column value; if the value is SQL NULL, the value returned is null * @throws SQLException if a database access error occurs */ @Override @@ -937,14 +865,12 @@ public class MonetResultSet } /** - * Retrieves the concurrency mode of this ResultSet object. The concurrency - * used is determined by the Statement object that created the result set. + * Retrieves the concurrency mode of this ResultSet object. The concurrency used is determined by the Statement + * object that created the result set. * - * NOTE: MonetDB only supports read-only result sets, and will always return - * ResultSet.CONCUR_READ_ONLY + * NOTE: MonetDB only supports read-only result sets, and will always return ResultSet.CONCUR_READ_ONLY * - * @return the concurrency type, either ResultSet.CONCUR_READ_ONLY or - * ResultSet.CONCUR_UPDATABLE + * @return the concurrency type, either ResultSet.CONCUR_READ_ONLY or ResultSet.CONCUR_UPDATABLE */ @Override public int getConcurrency() { @@ -965,9 +891,8 @@ public class MonetResultSet * cursor used by a ResultSet object. The current row of a ResultSet object * is also the current row of this SQL cursor. * - * Note: If positioned update is not supported, a SQLException is thrown. - * MonetDB currently doesn't support updates, so the SQLException is - * thrown for now. + * Note: If positioned update is not supported, a SQLException is thrown. MonetDB currently doesn't support updates, + * so the SQLException is thrown for now. * * @return the SQL name for this ResultSet object's cursor * @throws SQLException if a database access error occurs @@ -975,7 +900,7 @@ public class MonetResultSet @Override public String getCursorName() throws SQLException { throw new SQLException("Positioned updates not supported for this " + - "cursor (" + (header != null ? header.id : "") + ")", "0AM21"); + "cursor (" + (header != null ? header.getId() : "") + ")", "0AM21"); } /** @@ -983,22 +908,54 @@ public class MonetResultSet * ResultSet object as a double in the Java programming language. * * @param columnIndex the first column is 1, the second is 2, ... - * @return the column value; if the value is SQL NULL, the value returned - * is 0 + * @return the column value; if the value is SQL NULL, the value returned is 0 * @throws SQLException if there is no such column */ @Override public double getDouble(int columnIndex) throws SQLException { try { - String val = tlp.values[columnIndex - 1]; - if (val == null) { - lastReadWasNull = true; - return 0; + switch (JdbcSQLTypes[columnIndex - 1]) { + case Types.DOUBLE: + double res = currentBlock.getDoubleValue(columnIndex - 1); + return currentBlock.isLastReadWasNull() ? 0.0d : res; + case Types.BOOLEAN: + boolean bol = currentBlock.getBooleanValue(columnIndex - 1); + return currentBlock.isLastReadWasNull() ? 0.0d : (bol ? 1.0d : 0.0d); + case Types.TINYINT: + byte bb = currentBlock.getByteValue(columnIndex - 1); + return currentBlock.isLastReadWasNull() ? 0.0d : (double) bb; + case Types.SMALLINT: + short sh = currentBlock.getShortValue(columnIndex - 1); + return currentBlock.isLastReadWasNull() ? 0.0d : (double) sh; + case Types.INTEGER: + int in = currentBlock.getIntValue(columnIndex - 1); + return currentBlock.isLastReadWasNull() ? 0.0d : (double) in; + case Types.BIGINT: + long lon = currentBlock.getLongValue(columnIndex - 1); + return currentBlock.isLastReadWasNull() ? 0.0d : (double) lon; + case Types.REAL: + float floa = currentBlock.getFloatValue(columnIndex - 1); + return currentBlock.isLastReadWasNull() ? 0.0d : floa; + case Types.CHAR: + case Types.VARCHAR: + case Types.LONGVARCHAR: + case Types.CLOB: + case Types.BLOB: + case Types.LONGVARBINARY: + String ss = currentBlock.getValueAsString(columnIndex - 1); + return ss == null ? 0.0d : Double.parseDouble(ss); + case Types.NUMERIC: + case Types.DECIMAL: + BigDecimal bigdec = (BigDecimal) currentBlock.getValueAsObject(columnIndex - 1); + return bigdec == null ? 0.0d : bigdec.doubleValue(); + default: //OTHERS, BLOB, LONGVARBINARY, TIME... + throw new SQLException("Conversion from " + types[columnIndex - 1] + + " to double type not supported", "M1M05"); } - lastReadWasNull = false; - return Double.parseDouble(val); - } catch (NumberFormatException e) { - throw newSQLNumberFormatException(e); + } catch (ClassCastException ex) { + throw new SQLException(ex.getMessage()); + } catch (ProtocolException e) { + throw new SQLException(e); } catch (IndexOutOfBoundsException e) { throw newSQLInvalidColumnIndexException(columnIndex); } @@ -1009,8 +966,7 @@ public class MonetResultSet * ResultSet object as a double in the Java programming language. * * @param columnLabel the SQL name of the column - * @return the column value; if the value is SQL NULL, the value returned - * is 0 + * @return the column value; if the value is SQL NULL, the value returned is 0 * @throws SQLException if the ResultSet object does not contain columnLabel */ @Override @@ -1021,8 +977,7 @@ public class MonetResultSet /** * Retrieves the holdability of this ResultSet object. * - * @return either ResultSet.HOLD_CURSORS_OVER_COMMIT or - * ResultSet.CLOSE_CURSORS_AT_COMMIT + * @return either ResultSet.HOLD_CURSORS_OVER_COMMIT or ResultSet.CLOSE_CURSORS_AT_COMMIT * @throws SQLException if a database access error occurs */ @Override @@ -1054,13 +1009,13 @@ public class MonetResultSet @Override public void setFetchDirection(int direction) throws SQLException { switch (direction) { - case ResultSet.FETCH_FORWARD: - break; - case ResultSet.FETCH_REVERSE: - case ResultSet.FETCH_UNKNOWN: - throw new SQLException("Not supported direction " + direction, "0A000"); - default: - throw new SQLException("Illegal direction: " + direction, "M1M05"); + case ResultSet.FETCH_FORWARD: + break; + case ResultSet.FETCH_REVERSE: + case ResultSet.FETCH_UNKNOWN: + throw new SQLException("Not supported direction " + direction, "0A000"); + default: + throw new SQLException("Illegal direction: " + direction, "M1M05"); } } @@ -1103,30 +1058,62 @@ public class MonetResultSet * ResultSet object as a float in the Java programming language. * * @param columnIndex the first column is 1, the second is 2, ... - * @return the column value; if the value is SQL NULL, the value returned - * is 0 + * @return the column value; if the value is SQL NULL, the value returned is 0 * @throws SQLException if there is no such column */ @Override public float getFloat(int columnIndex) throws SQLException { try { - String val = tlp.values[columnIndex - 1]; - if (val == null) { - lastReadWasNull = true; - return 0; + switch (JdbcSQLTypes[columnIndex - 1]) { + case Types.REAL: + float res = currentBlock.getFloatValue(columnIndex - 1); + return currentBlock.isLastReadWasNull() ? 0.0f : res; + case Types.BOOLEAN: + boolean bol = currentBlock.getBooleanValue(columnIndex - 1); + return currentBlock.isLastReadWasNull() ? 0.0f : (bol ? 1.0f : 0.0f); + case Types.TINYINT: + byte bb = currentBlock.getByteValue(columnIndex - 1); + return currentBlock.isLastReadWasNull() ? 0.0f : (float) bb; + case Types.SMALLINT: + short sh = currentBlock.getShortValue(columnIndex - 1); + return currentBlock.isLastReadWasNull() ? 0.0f : (float) sh; + case Types.INTEGER: + int in = currentBlock.getIntValue(columnIndex - 1); + return currentBlock.isLastReadWasNull() ? 0.0f : (float) in; + case Types.BIGINT: + long lon = currentBlock.getLongValue(columnIndex - 1); + return currentBlock.isLastReadWasNull() ? 0.0f : (float) lon; + case Types.DOUBLE: + double dou = currentBlock.getDoubleValue(columnIndex - 1); + return currentBlock.isLastReadWasNull() ? 0.0f : (float) dou; + case Types.CHAR: + case Types.VARCHAR: + case Types.LONGVARCHAR: + case Types.CLOB: + case Types.BLOB: + case Types.LONGVARBINARY: + String ss = currentBlock.getValueAsString(columnIndex - 1); + return ss == null ? 0.0f : Float.parseFloat(ss); + case Types.NUMERIC: + case Types.DECIMAL: + BigDecimal bigdec = (BigDecimal) currentBlock.getValueAsObject(columnIndex - 1); + return bigdec == null ? 0.0f : bigdec.floatValue(); + default: //OTHERS, BLOB, LONGVARBINARY, TIME... + throw new SQLException("Conversion from " + types[columnIndex - 1] + + " to float type not supported", "M1M05"); } - lastReadWasNull = false; - return Float.parseFloat(val); - } catch (NumberFormatException e) { - throw newSQLNumberFormatException(e); + } catch (ClassCastException ex) { + throw new SQLException(ex.getMessage()); + } catch (ProtocolException e) { + throw new SQLException(e); } catch (IndexOutOfBoundsException e) { throw newSQLInvalidColumnIndexException(columnIndex); } } /** - * Retrieves the value of the designated column in the current row of this - * ResultSet object as a float in the Java programming language. + * Retrieves the value of the designated column in the current row of this ResultSet object as a float in the Java + * programming language. * * @param columnLabel the SQL name of the column * @return the column value; if the value is SQL NULL, the value returned is 0 @@ -1138,8 +1125,8 @@ public class MonetResultSet } /** - * Retrieves the value of the designated column in the current row of this - * ResultSet object as an int in the Java programming language. + * Retrieves the value of the designated column in the current row of this ResultSet object as an int in the + * Java programming language. * * @param columnIndex the first column is 1, the second is 2, ... * @return the column value; if the value is SQL NULL, the value returned is 0 @@ -1147,37 +1134,57 @@ public class MonetResultSet */ @Override public int getInt(int columnIndex) throws SQLException { - String val = ""; try { - val = tlp.values[columnIndex - 1]; - if (val == null) { - lastReadWasNull = true; - return 0; + switch (JdbcSQLTypes[columnIndex - 1]) { + case Types.INTEGER: + int res = currentBlock.getIntValue(columnIndex - 1); + return currentBlock.isLastReadWasNull() ? 0 : res; + case Types.BOOLEAN: + boolean bol = currentBlock.getBooleanValue(columnIndex - 1); + return currentBlock.isLastReadWasNull() ? 0 : (bol ? 1 : 0); + case Types.TINYINT: + byte bb = currentBlock.getByteValue(columnIndex - 1); + return currentBlock.isLastReadWasNull() ? 0 : bb; + case Types.SMALLINT: + short sh = currentBlock.getShortValue(columnIndex - 1); + return currentBlock.isLastReadWasNull() ? 0 : sh; + case Types.BIGINT: + long lon = currentBlock.getLongValue(columnIndex - 1); + return currentBlock.isLastReadWasNull() ? 0 : (int) lon; + case Types.REAL: + float floa = currentBlock.getFloatValue(columnIndex - 1); + return currentBlock.isLastReadWasNull() ? 0 : Math.round(floa); + case Types.DOUBLE: + double dou = currentBlock.getDoubleValue(columnIndex - 1); + return currentBlock.isLastReadWasNull() ? 0 : (int) Math.round(dou); + case Types.CHAR: + case Types.VARCHAR: + case Types.LONGVARCHAR: + case Types.CLOB: + case Types.BLOB: + case Types.LONGVARBINARY: + String ss = currentBlock.getValueAsString(columnIndex - 1); + return ss == null ? 0 : Integer.parseInt(ss); + case Types.NUMERIC: + case Types.DECIMAL: + BigDecimal bigdec = (BigDecimal) currentBlock.getValueAsObject(columnIndex - 1); + return bigdec == null ? 0 : bigdec.intValue(); + default: //OTHERS, BLOB, LONGVARBINARY, TIME... + throw new SQLException("Conversion from " + types[columnIndex - 1] + + " to integer type not supported", "M1M05"); } - lastReadWasNull = false; - return Integer.parseInt(val); - } catch (NumberFormatException e) { - // The oid datatype values (as string) have a @0 suffix in the string value. - // To allow succesful parsing and conversion to int, we need to remove the suffix first - if ("oid".equals(types[columnIndex - 1])) { - int len = val.length(); - if (len > 2 && val.endsWith("@0")) { - try { - return Integer.parseInt(val.substring(0, len-2)); - } catch (NumberFormatException nfe) { - throw newSQLNumberFormatException(nfe); - } - } - } - throw newSQLNumberFormatException(e); + } catch (ClassCastException ex) { + throw new SQLException(ex.getMessage()); + } catch (ProtocolException e) { + throw new SQLException(e); } catch (IndexOutOfBoundsException e) { throw newSQLInvalidColumnIndexException(columnIndex); } } /** - * Retrieves the value of the designated column in the current row of this - * ResultSet object as an int in the Java programming language. + * Retrieves the value of the designated column in the current row of this ResultSet object as an int in the + * Java programming language. * * @param columnLabel the SQL name of the column * @return the column value; if the value is SQL NULL, the value returned is 0 @@ -1189,8 +1196,8 @@ public class MonetResultSet } /** - * Retrieves the value of the designated column in the current row of this - * ResultSet object as a long in the Java programming language. + * Retrieves the value of the designated column in the current row of this ResultSet object as a long in the Java + * programming language. * * @param columnIndex the first column is 1, the second is 2, ... * @return the column value; if the value is SQL NULL, the value returned is 0 @@ -1198,41 +1205,60 @@ public class MonetResultSet */ @Override public long getLong(int columnIndex) throws SQLException { - String val = ""; try { - val = tlp.values[columnIndex - 1]; - if (val == null) { - lastReadWasNull = true; - return 0; + switch (JdbcSQLTypes[columnIndex - 1]) { + case Types.BIGINT: + long res = currentBlock.getLongValue(columnIndex - 1); + return currentBlock.isLastReadWasNull() ? 0 : res; + case Types.BOOLEAN: + boolean bol = currentBlock.getBooleanValue(columnIndex - 1); + return currentBlock.isLastReadWasNull() ? 0 : (bol ? 1 : 0); + case Types.TINYINT: + byte bb = currentBlock.getByteValue(columnIndex - 1); + return currentBlock.isLastReadWasNull() ? 0 : bb; + case Types.SMALLINT: + short sh = currentBlock.getShortValue(columnIndex - 1); + return currentBlock.isLastReadWasNull() ? 0 : sh; + case Types.INTEGER: + int in = currentBlock.getIntValue(columnIndex - 1); + return currentBlock.isLastReadWasNull() ? 0 : in; + case Types.REAL: + float floa = currentBlock.getFloatValue(columnIndex - 1); + return currentBlock.isLastReadWasNull() ? 0 : Math.round(floa); + case Types.DOUBLE: + double dou = currentBlock.getDoubleValue(columnIndex - 1); + return currentBlock.isLastReadWasNull() ? 0 : Math.round(dou); + case Types.CHAR: + case Types.VARCHAR: + case Types.LONGVARCHAR: + case Types.CLOB: + case Types.BLOB: + case Types.LONGVARBINARY: + String ss = currentBlock.getValueAsString(columnIndex - 1); + return ss == null ? 0 : Long.parseLong(ss); + case Types.NUMERIC: + case Types.DECIMAL: + BigDecimal bigdec = (BigDecimal) currentBlock.getValueAsObject(columnIndex - 1); + return bigdec == null ? 0 : bigdec.longValue(); + default: //OTHERS, BLOB, LONGVARBINARY, TIME... + throw new SQLException("Conversion from " + types[columnIndex - 1] + + " to long type not supported", "M1M05"); } - lastReadWasNull = false; - return Long.parseLong(val); - } catch (NumberFormatException e) { - // The oid datatype values (as string) have a @0 suffix in the string value. - // To allow succesful parsing and conversion to long, we need to remove the suffix first - if ("oid".equals(types[columnIndex - 1])) { - int len = val.length(); - if (len > 2 && val.endsWith("@0")) { - try { - return Long.parseLong(val.substring(0, len-2)); - } catch (NumberFormatException nfe) { - throw newSQLNumberFormatException(nfe); - } - } - } - throw newSQLNumberFormatException(e); + } catch (ClassCastException ex) { + throw new SQLException(ex.getMessage()); + } catch (ProtocolException e) { + throw new SQLException(e); } catch (IndexOutOfBoundsException e) { throw newSQLInvalidColumnIndexException(columnIndex); } } /** - * Retrieves the value of the designated column in the current row of this - * ResultSet object as a long in the Java programming language. + * Retrieves the value of the designated column in the current row of this ResultSet object as a long in the Java + * programming language. * * @param columnLabel the SQL name of the column - * @return the column value; if the value is SQL NULL, the value returned - * is 0 + * @return the column value; if the value is SQL NULL, the value returned is 0 * @throws SQLException if the ResultSet object does not contain columnLabel */ @Override @@ -1242,9 +1268,9 @@ public class MonetResultSet /* helper for the anonymous class inside getMetaData */ private abstract class rsmdw extends MonetWrapper implements ResultSetMetaData {} + /** - * Retrieves the number, types and properties of this ResultSet object's - * columns. + * Retrieves the number, types and properties of this ResultSet object's columns. * * @return the description of this ResultSet object's columns */ @@ -1269,8 +1295,7 @@ public class MonetResultSet * and cache the precision, scale, isNullable and isAutoincrement values in the above array chaches. * Also we only call md.getColumns() when we have a non empty schema name and table name and column name. */ - private void fetchColumnInfo(int column) throws SQLException - { + private void fetchColumnInfo(int column) throws SQLException { if (column <= 0 || column > columns.length) throw newSQLInvalidColumnIndexException(column); @@ -1319,7 +1344,7 @@ public class MonetResultSet /** * Returns the number of columns in this ResultSet object. * - * @returns the number of columns + * @return the number of columns */ @Override public int getColumnCount() { @@ -1336,7 +1361,7 @@ public class MonetResultSet @Override public boolean isAutoIncrement(int column) throws SQLException { try { - if (_is_fetched[column] != true) { + if (!_is_fetched[column]) { fetchColumnInfo(column); } return _isAutoincrement[column]; @@ -1349,27 +1374,19 @@ public class MonetResultSet * Indicates whether a column's case matters. * * @param column the first column is 1, the second is 2, ... - * @returns true for all character string columns else false + * @return true for all character string columns else false */ @Override public boolean isCaseSensitive(int column) throws SQLException { switch (getColumnType(column)) { + case Types.CLOB: case Types.CHAR: - case Types.LONGVARCHAR: // MonetDB doesn't use type LONGVARCHAR, it's here for completeness - case Types.CLOB: - return true; case Types.VARCHAR: - String monettype = getColumnTypeName(column); - if (monettype != null && monettype.length() == 4) { - // data of type inet or uuid is not case sensitive - if ("inet".equals(monettype) - || "uuid".equals(monettype)) - return false; - } + case Types.LONGVARCHAR: return true; + default: + return false; } - - return false; } /** @@ -1382,7 +1399,7 @@ public class MonetResultSet * real column existing in a table or not... * * @param column the first column is 1, the second is 2, ... - * @returns true + * @return true */ @Override public boolean isSearchable(int column) { @@ -1397,7 +1414,7 @@ public class MonetResultSet * we can always return false here. * * @param column the first column is 1, the second is 2, ... - * @returns false + * @return false */ @Override public boolean isCurrency(int column) { @@ -1405,8 +1422,7 @@ public class MonetResultSet } /** - * Indicates whether values in the designated column are signed - * numbers. + * Indicates whether values in the designated column are signed numbers. * Within MonetDB all numeric types (except oid and ptr) are signed. * * @param column the first column is 1, the second is 2, ... @@ -1414,43 +1430,26 @@ public class MonetResultSet */ @Override public boolean isSigned(int column) throws SQLException { - // we can hardcode this, based on the colum type switch (getColumnType(column)) { - case Types.NUMERIC: - case Types.DECIMAL: case Types.TINYINT: case Types.SMALLINT: case Types.INTEGER: case Types.REAL: - case Types.FLOAT: case Types.DOUBLE: - return true; case Types.BIGINT: - String monettype = getColumnTypeName(column); - if (monettype != null && monettype.length() == 3) { - // data of type oid or ptr is not signed - if ("oid".equals(monettype) - || "ptr".equals(monettype)) - return false; - } + case Types.NUMERIC: + case Types.DECIMAL: return true; - case Types.BIT: // we don't use type BIT, it's here for completeness - case Types.BOOLEAN: - case Types.DATE: - case Types.TIME: - case Types.TIMESTAMP: default: return false; } } /** - * Indicates the designated column's normal maximum width in - * characters. + * Indicates the designated column's normal maximum width in characters. * * @param column the first column is 1, the second is 2, ... - * @return the normal maximum number of characters allowed as the - * width of the designated column + * @return the normal maximum number of characters allowed as the width of the designated column * @throws SQLException if there is no such column */ @Override @@ -1514,10 +1513,8 @@ public class MonetResultSet } /** - * Get the designated column's number of decimal digits. - * This method is currently very expensive as it needs to - * retrieve the information from the database using an SQL - * query. + * Get the designated column's number of decimal digits. This method is currently very expensive as it needs + * to retrieve the information from the database using an SQL query. * * @param column the first column is 1, the second is 2, ... * @return precision @@ -1526,17 +1523,18 @@ public class MonetResultSet @Override public int getPrecision(int column) throws SQLException { try { - if (_is_fetched[column] != true) { + if (!_is_fetched[column]) { fetchColumnInfo(column); } if (_precision[column] == 0) { - // apparently no precision could be fetched - // use columnDisplaySize() value for variable length data types + // apparently no precision could be fetched use columnDisplaySize() value for variable + // length data types switch (getColumnType(column)) { case Types.CHAR: case Types.VARCHAR: - case Types.LONGVARCHAR: // MonetDB doesn't use type LONGVARCHAR, it's here for completeness + case Types.LONGVARCHAR: case Types.CLOB: + case Types.LONGVARBINARY: case Types.BLOB: case Types.NUMERIC: case Types.DECIMAL: @@ -1557,13 +1555,9 @@ public class MonetResultSet case Types.REAL: _precision[column] = 7; break; - case Types.FLOAT: case Types.DOUBLE: _precision[column] = 15; break; - case Types.BIT: // MonetDB doesn't use type BIT, it's here for completeness - _precision[column] = 1; - break; case Types.BOOLEAN: _precision[column] = 5; break; @@ -1571,9 +1565,11 @@ public class MonetResultSet _precision[column] = 10; break; case Types.TIME: + case 2013: //Types.TIME_WITH_TIMEZONE: _precision[column] = 8; break; case Types.TIMESTAMP: + case 2014: //Types.TIMESTAMP_WITH_TIMEZONE: _precision[column] = 19; break; default: @@ -1600,7 +1596,7 @@ public class MonetResultSet @Override public int getScale(int column) throws SQLException { try { - if (_is_fetched[column] != true) { + if (!_is_fetched[column]) { fetchColumnInfo(column); } return _scale[column]; @@ -1616,13 +1612,14 @@ public class MonetResultSet * an SQL query. * * @param column the first column is 1, the second is 2, ... - * @return the nullability status of the given column; one of columnNoNulls, columnNullable or columnNullableUnknown + * @return the nullability status of the given column; one of columnNoNulls, columnNullable or + * columnNullableUnknown * @throws SQLException if a database access error occurs */ @Override public int isNullable(int column) throws SQLException { try { - if (_is_fetched[column] != true) { + if (!_is_fetched[column]) { fetchColumnInfo(column); } return _isNullable[column]; @@ -1641,14 +1638,12 @@ public class MonetResultSet */ @Override public String getCatalogName(int column) throws SQLException { - return null; // MonetDB does NOT support catalogs - + return null; // MonetDB does NOT support catalogs } /** - * Indicates whether the designated column is definitely not - * writable. MonetDB does not support cursor updates, so - * nothing is writable. + * Indicates whether the designated column is definitely not writable. MonetDB does not support + * cursor updates, so nothing is writable. * * @param column the first column is 1, the second is 2, ... * @return true if so; false otherwise @@ -1659,8 +1654,7 @@ public class MonetResultSet } /** - * Indicates whether it is possible for a write on the - * designated column to succeed. + * Indicates whether it is possible for a write on the designated column to succeed. * * @param column the first column is 1, the second is 2, ... * @return true if so; false otherwise @@ -1671,8 +1665,7 @@ public class MonetResultSet } /** - * Indicates whether a write on the designated column will - * definitely succeed. + * Indicates whether a write on the designated column will definitely succeed. * * @param column the first column is 1, the second is 2, ... * @return true if so; false otherwise @@ -1709,7 +1702,7 @@ public class MonetResultSet if (conn != null) { Map<String,Class<?>> map = conn.getTypeMap(); if (map != null && map.containsKey(MonetDBType)) { - type = (Class)map.get(MonetDBType); + type = map.get(MonetDBType); } } if (type == null) { @@ -1726,9 +1719,8 @@ public class MonetResultSet } /** - * Gets the designated column's suggested title for use in - * printouts and displays. This is currently equal to - * getColumnName(). + * Gets the designated column's suggested title for use in printouts and displays. This is currently equal + * to getColumnName(). * * @param column the first column is 1, the second is 2, ... * @return the suggested column title @@ -1740,7 +1732,7 @@ public class MonetResultSet } /** - * Gets the designated column's name + * Gets the designated column's name. * * @param column the first column is 1, the second is 2, ... * @return the column name @@ -1775,9 +1767,8 @@ public class MonetResultSet * Retrieves the designated column's database-specific type name. * * @param column the first column is 1, the second is 2, ... - * @return type name used by the database. If the column type is a - * user-defined type, then a fully-qualified type name is - * returned. + * @return type name used by the database. If the column type is a user-defined type, then a + * fully-qualified type name is returned. * @throws SQLException if there is no such column */ @Override @@ -1806,7 +1797,7 @@ public class MonetResultSet * types. In the JDBC 2.0 API, the behavior of method getObject is extended * to materialize data of SQL user-defined types. When a column contains a * structured or distinct value, the behavior of this method is as if it - * were a call to: getObject(columnIndex, this.getStatement().getConnection().getTypeMap()). + * were a call to: getObject(columnIndex, this.getStatement().getInternalConnection().getTypeMap()). * * @param columnIndex the first column is 1, the second is 2, ... * @return a java.lang.Object holding the column value or null @@ -1814,153 +1805,298 @@ public class MonetResultSet */ @Override public Object getObject(int columnIndex) throws SQLException { - // Many generic JDBC programs use getObject(colnr) to retrieve value objects from a resultset - // For speed the implementation should be as fast as possible, so avoid method calls (by inlining code) where possible + // Many generic JDBC programs use getObject(colnr) to retrieve value objects from a resultset. For speed the + // implementation should be as fast as possible, so avoid method calls (by inlining code) where possible final int JdbcType; - final String val; try { - val = tlp.values[columnIndex - 1]; - if (val == null) { - lastReadWasNull = true; - return null; - } - lastReadWasNull = false; JdbcType = JdbcSQLTypes[columnIndex - 1]; - } catch (IndexOutOfBoundsException e) { - throw newSQLInvalidColumnIndexException(columnIndex); - } - - switch(JdbcType) { - case Types.BIT: // MonetDB doesn't use type BInary digiT, it's here for completeness - case Types.TINYINT: - case Types.SMALLINT: - try { - return Short.valueOf(val); - } catch (NumberFormatException e) { - return val; - } - case Types.INTEGER: - try { - return Integer.valueOf(val); - } catch (NumberFormatException e) { - return val; - } - case Types.BIGINT: - try { - return Long.valueOf(val); - } catch (NumberFormatException e) { - return val; - } - case Types.DOUBLE: - case Types.FLOAT: - try { - return Double.valueOf(val); - } catch (NumberFormatException e) { - return val; - } - case Types.REAL: - try { - return Float.valueOf(val); - } catch (NumberFormatException e) { - return val; - } - case Types.DECIMAL: - case Types.NUMERIC: - try { - return new BigDecimal(val); - } catch (NumberFormatException e) { + switch(JdbcType) { + case Types.BOOLEAN: + case Types.TINYINT: + case Types.SMALLINT: + case Types.INTEGER: + case Types.BIGINT: + case Types.REAL: + case Types.DOUBLE: + case Types.DECIMAL: + case Types.NUMERIC: + case Types.CHAR: + case Types.VARCHAR: + case Types.LONGVARCHAR: + case Types.CLOB: + case Types.LONGVARBINARY: + case Types.BLOB: + return currentBlock.getValueAsObject(columnIndex - 1); + case Types.DATE: + return getDate(columnIndex); + case Types.TIME: + case 2013: //Types.TIME_WITH_TIMEZONE: + return getTime(columnIndex); + case Types.TIMESTAMP: + case 2014: //Types.TIMESTAMP_WITH_TIMEZONE: + return getTimestamp(columnIndex); + case Types.OTHER: { + // The MonetDB types: inet, json, url and uuid are all mapped to Types.OTHER in MonetDriver.typeMap + // For these MonetDB types (except json, see comments below) we try to create objects of the corresponding class. + String MonetDBType = types[columnIndex - 1]; + String val = currentBlock.getValueAsString(columnIndex - 1); + if (val == null) { + return null; + } + switch (MonetDBType.length()) { + case 3: + if ("url".equals(MonetDBType)) { + try { + return new MonetURL(val); + } catch (Exception exc) { + // ignore exception and just return the val String object + return val; + } + } + break; + case 4: + if ("inet".equals(MonetDBType)) { + try { + return new MonetINET(val); + } catch (Exception exc) { + // ignore exception and just return the val String object + return val; + } + } else if ("uuid".equals(MonetDBType)) { + try { + return UUID.fromString(val); + } catch (IllegalArgumentException exc) { + // ignore exception and just return the val String object + return val; + } +// } else if ("json".equals(MonetDBType)) { //the same happens for geometry + // There is no support for JSON in standard java class libraries. + // Possibly we could use org.json.simple.JSONObject or other/faster libs + // javax.json.Json is not released yet (see https://json-processing-spec.java.net/) + // see also https://github.com/fabienrenaud/java-json-benchmark + // Note that it would make our JDBC driver dependent of an external jar + // and we don't want that so simply return it as String object + } + break; + } return val; } - case Types.BOOLEAN: - return Boolean.valueOf(val); - case Types.VARCHAR: - { - - // The MonetDB types: inet, json, url and uuid are all mapped to Types.VARCHAR in MonetDriver.typeMap - // For these MonetDB types (except json, see comments below) we try to create objects of the corresponding class. - String MonetDBType = types[columnIndex - 1]; - switch (MonetDBType.length()) { - case 3: - if ("url".equals(MonetDBType)) { - try { - nl.cwi.monetdb.jdbc.types.URL url_obj = new nl.cwi.monetdb.jdbc.types.URL(); - url_obj.fromString(val); - return url_obj; - } catch (MalformedURLException exc) { - // ignore exception and just return the val String object - return val; - } catch (Exception exc) { - // ignore exception and just return the val String object - return val; - } - } - break; - case 4: - if ("inet".equals(MonetDBType)) { - try { - nl.cwi.monetdb.jdbc.types.INET inet_obj = new nl.cwi.monetdb.jdbc.types.INET(); - inet_obj.fromString(val); - return inet_obj; - } catch (Exception exc) { - // ignore exception and just return the val String object - return val; - } - } else - if ("uuid".equals(MonetDBType)) { - try { - return java.util.UUID.fromString(val); - } catch (IllegalArgumentException exc) { - // ignore exception and just return the val String object - return val; - } -// } else -// if ("json".equals(MonetDBType)) { - // There is no support for JSON in standard java class libraries. - // Possibly we could use org.json.simple.JSONObject or other/faster libs - // javax.json.Json is not released yet (see https://json-processing-spec.java.net/) - // see also https://github.com/fabienrenaud/java-json-benchmark - // Note that it would make our JDBC driver dependent of an external jar - // and we don't want that so simply return it as String object -// return val; - } - break; - } - return val; + default: + // When we get here the column type is a non-standard JDBC SQL type, possibly a User Defined Type. + // Just call getObject(int, Map) for those rare cases. + return getObject(columnIndex, this.getStatement().getConnection().getTypeMap()); } - case Types.CHAR: - case Types.LONGVARCHAR: // MonetDB doesn't use type LONGVARCHAR, it's here for completeness - return val; - case Types.CLOB: - return new MonetClob(val); - case Types.BLOB: - return MonetBlob.create(val); - case Types.DATE: - return getDate(columnIndex, null); - case Types.TIME: - return getTime(columnIndex, null); - case Types.TIMESTAMP: - return getTimestamp(columnIndex, null); - case Types.BINARY: - case Types.VARBINARY: - case Types.LONGVARBINARY: // MonetDB doesn't use type LONGVARBINARY, it's here for completeness - return getBytes(columnIndex); - case Types.OTHER: - default: - // When we get here the column type is a non-standard JDBC SQL type, possibly a User Defined Type. - // Just call getObject(int, Map) for those rare cases. - return getObject(columnIndex, this.getStatement().getConnection().getTypeMap()); + } catch (IndexOutOfBoundsException e) { + throw newSQLInvalidColumnIndexException(columnIndex); + } catch (ProtocolException e) { + throw new SQLException(e); } } private boolean classImplementsSQLData(Class<?> cl) { Class<?>[] cls = cl.getInterfaces(); - for (int i = 0; i < cls.length; i++) { - if (cls[i] == SQLData.class) + for (Class<?> cl1 : cls) { + if (cl1 == SQLData.class) return true; } return false; } + @SuppressWarnings("unchecked") + private Object getObjectFromClass(int columnIndex, Class<?> type) throws SQLException { + try { + + if (type == null) { + // fallback to the standard Class mappings + type = getClassForType(JdbcSQLTypes[columnIndex - 1]); + } + if (type == null || type == String.class) { + return currentBlock.getValueAsString(columnIndex - 1); + } else if (type == BigDecimal.class) { + return getBigDecimal(columnIndex); + } else if (type == Boolean.class) { + return getBoolean(columnIndex); + } else if (type == Byte.class) { + return getByte(columnIndex); + } else if (type == Short.class) { + return getShort(columnIndex); + } else if (type == Integer.class) { + return getInt(columnIndex); + } else if (type == Long.class) { + return getLong(columnIndex); + } else if (type == Float.class) { + return getFloat(columnIndex); + } else if (type == Double.class) { + return getDouble(columnIndex); + } else if (type == byte[].class) { + return getBytes(columnIndex); + } else if (type == Date.class) { + return getDate(columnIndex); + } else if (type == Time.class) { + return getTime(columnIndex); + } else if (type == Timestamp.class) { + return getTimestamp(columnIndex); + } else if (type == Clob.class) { + return getClob(columnIndex); + } else if (type == Blob.class) { + return getBlob(columnIndex); + } else if (classImplementsSQLData(type)) { + SQLData x; + try { + Constructor<? extends SQLData> ctor = ((Class) type).getConstructor(); + x = ctor.newInstance(); + } catch (NoSuchMethodException | InstantiationException | InvocationTargetException + | SecurityException | IllegalAccessException nsme) { + throw new SQLException(nsme.getMessage(), "M0M27"); + } + final int colnum = columnIndex; + final boolean valwasnull = wasNull(); + SQLInput input = new SQLInput() { + @Override + public String readString() throws SQLException { + return getString(colnum); + } + + @Override + public boolean readBoolean() throws SQLException { + return getBoolean(colnum); + } + + @Override + public byte readByte() throws SQLException { + return getByte(colnum); + } + + @Override + public short readShort() throws SQLException { + return getShort(colnum); + } + + @Override + public int readInt() throws SQLException { + return getInt(colnum); + } + + @Override + public long readLong() throws SQLException { + return getLong(colnum); + } + + @Override + public float readFloat() throws SQLException { + return getFloat(colnum); + } + + @Override + public double readDouble() throws SQLException { + return getDouble(colnum); + } + + @Override + public BigDecimal readBigDecimal() throws SQLException { + return getBigDecimal(colnum); + } + + @Override + public byte[] readBytes() throws SQLException { + return getBytes(colnum); + } + + @Override + public Date readDate() throws SQLException { + return getDate(colnum); + } + + @Override + public Time readTime() throws SQLException { + return getTime(colnum); + } + + @Override + public Timestamp readTimestamp() throws SQLException { + return getTimestamp(colnum); + } + + @Override + public Reader readCharacterStream() throws SQLException { + return getCharacterStream(colnum); + } + + @Override + public InputStream readAsciiStream() throws SQLException { + return getAsciiStream(colnum); + } + + @Override + public InputStream readBinaryStream() throws SQLException { + return getBinaryStream(colnum); + } + + @Override + public Object readObject() throws SQLException { + return getObject(colnum); + } + + @Override + public Ref readRef() throws SQLException { + return getRef(colnum); + } + + @Override + public Blob readBlob() throws SQLException { + return getBlob(colnum); + } + + @Override + public Clob readClob() throws SQLException { + return getClob(colnum); + } + + @Override + public Array readArray() throws SQLException { + return getArray(colnum); + } + + @Override + public boolean wasNull() throws SQLException { + return valwasnull; + } + + @Override + public URL readURL() throws SQLException { + return getURL(colnum); + } + + @Override + public NClob readNClob() throws SQLException { + return getNClob(colnum); + } + + @Override + public String readNString() throws SQLException { + return getNString(colnum); + } + + @Override + public SQLXML readSQLXML() throws SQLException { + return getSQLXML(colnum); + } + + @Override + public RowId readRowId() throws SQLException { + return getRowId(colnum); + } + }; + x.readSQL(input, types[columnIndex - 1]); + return x; + } else { + return currentBlock.getObjectValue(columnIndex - 1); + } + } catch (ProtocolException e) { + throw new SQLException(e); + } + } + /** * Gets the value of the designated column in the current row of this * ResultSet object as an Object in the Java programming language. @@ -1978,7 +2114,7 @@ public class MonetResultSet * If Connection.getTypeMap does not throw a SQLFeatureNotSupportedException, then * when a column contains a structured or distinct value, the behavior of this * method is as if it were a call to: getObject(columnIndex, - * this.getStatement().getConnection().getTypeMap()). + * this.getStatement().getInternalConnection().getTypeMap()). * If Connection.getTypeMap does throw a SQLFeatureNotSupportedException, then * structured values are not supported, and distinct values are mapped to the * default Java class as determined by the underlying SQL type of the DISTINCT type. @@ -1986,226 +2122,21 @@ public class MonetResultSet * @param columnIndex the first column is 1, the second is 2, ... * @param map a java.util.Map object that contains the mapping from SQL * type names to classes in the Java programming language - * @return an Object in the Java programming language representing the SQL - * value + * @return an Object in the Java programming language representing the SQL value * @throws SQLException if a database access error occurs */ @Override - @SuppressWarnings("unchecked") - public Object getObject(int columnIndex, Map<String,Class<?>> map) - throws SQLException - { - final String val; - final String MonetDBtype; + public Object getObject(int columnIndex, Map<String, Class<?>> map) throws SQLException { try { - val = tlp.values[columnIndex - 1]; - if (val == null) { - lastReadWasNull = true; - return null; + String MonetDBtype = types[columnIndex - 1]; + Class<?> type = null; + if (map != null && map.containsKey(MonetDBtype)) { + type = map.get(MonetDBtype); } - lastReadWasNull = false; - MonetDBtype = types[columnIndex - 1]; + return getObjectFromClass(columnIndex, type); } catch (IndexOutOfBoundsException e) { throw newSQLInvalidColumnIndexException(columnIndex); } - - Class<?> type = null; - if (map != null && map.containsKey(MonetDBtype)) { - type = map.get(MonetDBtype); - } - if (type == null) { - // fallback to the standard Class mappings - type = getClassForType(JdbcSQLTypes[columnIndex - 1]); - } - - if (type == null || type == String.class) { - return val; - } else if (type == BigDecimal.class) { - return getBigDecimal(columnIndex); - } else if (type == Boolean.class) { - return Boolean.valueOf(getBoolean(columnIndex)); - } else if (type == Short.class) { - return Short.valueOf(getShort(columnIndex)); - } else if (type == Integer.class) { - return Integer.valueOf(getInt(columnIndex)); - } else if (type == Long.class) { - return Long.valueOf(getLong(columnIndex)); - } else if (type == Float.class) { - return Float.valueOf(getFloat(columnIndex)); - } else if (type == Double.class) { - return Double.valueOf(getDouble(columnIndex)); - } else if (type == byte[].class) { - return getBytes(columnIndex); - } else if (type == java.sql.Date.class) { - return getDate(columnIndex, null); - } else if (type == Time.class) { - return getTime(columnIndex, null); - } else if (type == Timestamp.class) { - return getTimestamp(columnIndex, null); - } else if (type == Clob.class) { - return getClob(columnIndex); - } else if (type == Blob.class) { - return getBlob(columnIndex); - } else if (classImplementsSQLData(type)) { - SQLData x; - try { - Constructor<? extends SQLData> ctor = - ((Class)type).getConstructor(); - x = ctor.newInstance(); - } catch (NoSuchMethodException nsme) { - throw new SQLException(nsme.getMessage(), "M0M27"); - } catch (InstantiationException ie) { - throw new SQLException(ie.getMessage(), "M0M27"); - } catch (IllegalAccessException iae) { - throw new SQLException(iae.getMessage(), "M0M27"); - } catch (InvocationTargetException ite) { - throw new SQLException(ite.getMessage(), "M0M27"); - } catch (SecurityException se) { - throw new SQLException(se.getMessage(), "M0M27"); - } - final int colnum = columnIndex; - final boolean valwasnull = wasNull(); - SQLInput input = new SQLInput() { - @Override - public String readString() throws SQLException { - return getString(colnum); - } - - @Override - public boolean readBoolean() throws SQLException { - return getBoolean(colnum); - } - - @Override - public byte readByte() throws SQLException { - return getByte(colnum); - } - - @Override - public short readShort() throws SQLException { - return getShort(colnum); - } - - @Override - public int readInt() throws SQLException { - return getInt(colnum); - } - - @Override - public long readLong() throws SQLException { - return getLong(colnum); - } - - @Override - public float readFloat() throws SQLException { - return getFloat(colnum); - } - - @Override - public double readDouble() throws SQLException { - return getDouble(colnum); - } - - @Override - public BigDecimal readBigDecimal() throws SQLException { - return getBigDecimal(colnum); - } - - @Override - public byte[] readBytes() throws SQLException { - return getBytes(colnum); - } - - @Override - public java.sql.Date readDate() throws SQLException { - return getDate(colnum, null); - } - - @Override - public java.sql.Time readTime() throws SQLException { - return getTime(colnum, null); - } - - @Override - public Timestamp readTimestamp() throws SQLException { - return getTimestamp(colnum, null); - } - - @Override - public Reader readCharacterStream() throws SQLException { - return getCharacterStream(colnum); - } - - @Override - public InputStream readAsciiStream() throws SQLException { - return getAsciiStream(colnum); - } - - @Override - public InputStream readBinaryStream() throws SQLException { - return getBinaryStream(colnum); - } - - @Override - public Object readObject() throws SQLException { - return getObject(colnum); - } - - @Override - public Ref readRef() throws SQLException { - return getRef(colnum); - } - - @Override - public Blob readBlob() throws SQLException { - return getBlob(colnum); - } - - @Override - public Clob readClob() throws SQLException { - return getClob(colnum); - } - - @Override - public Array readArray() throws SQLException { - return getArray(colnum); - } - - @Override - public boolean wasNull() throws SQLException { - return valwasnull; - } - - @Override - public URL readURL() throws SQLException { - return getURL(colnum); - } - - @Override - public NClob readNClob() throws SQLException { - return getNClob(colnum); - } - - @Override - public String readNString() throws SQLException { - return getNString(colnum); - } - - @Override - public SQLXML readSQLXML() throws SQLException { - return getSQLXML(colnum); - } - - @Override - public RowId readRowId() throws SQLException { - return getRowId(colnum); - } - }; - x.readSQL(input, MonetDBtype); - return x; - } else { - return val; - } } /** @@ -2221,20 +2152,21 @@ public class MonetResultSet * Additional conversions may be supported and are vendor defined. * * @param columnIndex the first column is 1, the second is 2, ... - * @param type Class representing the Java data type to convert the - * designated column to + * @param type Class representing the Java data type to convert the designated column to * @return an instance of type holding the column value - * @throws SQLException if conversion is not supported, type is - * null or another error occurs. The getCause() method of - * the exception may provide a more detailed exception, for - * example, if a conversion error occurs + * @throws SQLException if conversion is not supported, type is null or another error occurs. The getCause() method + * of the exception may provide a more detailed exception, for example, if a conversion error occurs */ @Override + @SuppressWarnings("unchecked") public <T> T getObject(int columnIndex, Class<T> type) throws SQLException { if (type == null) throw new SQLException("type is null", "M1M05"); - - throw new SQLFeatureNotSupportedException("cannot return a Java generic type based on static types from getXXX methods", "0AM34"); + try { + return (T) getObjectFromClass(columnIndex, type); + } catch (IndexOutOfBoundsException e) { + throw newSQLInvalidColumnIndexException(columnIndex); + } } /** @@ -2244,38 +2176,29 @@ public class MonetResultSet * supported. If the conversion is not supported or null is * specified for the type, a SQLException is thrown. * - * @param columnLabel the label for the column specified with the - * SQL AS clause. If the SQL AS clause was not specified, - * then the label is the name of the column - * @param type Class representing the Java data type to convert the - * designated column to + * @param columnLabel the label for the column specified with the SQL AS clause. If the SQL AS clause was not + * specified, then the label is the name of the column + * @param type Class representing the Java data type to convert the designated column to * @return an instance of type holding the column value - * @throws SQLException if conversion is not supported, type is - * null or another error occurs. The getCause() method of - * the exception may provide a more detailed exception, for - * example, if a conversion error occurs + * @throws SQLException if conversion is not supported, type is null or another error occurs. The getCause() method + * of the exception may provide a more detailed exception, for example, if a conversion error occurs */ @Override - public <T> T getObject(String columnLabel, Class<T> type) - throws SQLException - { + public <T> T getObject(String columnLabel, Class<T> type) throws SQLException { return getObject(findColumn(columnLabel), type); } /** - * Helper method to support the getObject and - * ResultsetMetaData.getColumnClassName JDBC methods. + * Helper method to support the getObject and ResultsetMetaData.getColumnClassName JDBC methods. * * @param type a value from java.sql.Types * @return a Class object from which an instance would be returned */ static Class<?> getClassForType(int type) { /** - * This switch returns the types as objects according to table B-3 from - * Oracle's JDBC specification 4.1 + * This switch returns the types as objects according to table B-3 from Oracle's JDBC specification 4.1 */ - // keep this switch regarding the returned classes aligned with getObject(int, Map) ! - switch(type) { + switch(type) { // keep this switch regarding the returned classes aligned with getObject(int, Map) ! case Types.CHAR: case Types.VARCHAR: case Types.LONGVARCHAR: @@ -2285,8 +2208,8 @@ public class MonetResultSet return BigDecimal.class; case Types.BOOLEAN: return Boolean.class; - case Types.BIT: // MonetDB doesn't support type BIT, it's here for completeness case Types.TINYINT: + return Byte.class; case Types.SMALLINT: return Short.class; case Types.INTEGER: @@ -2295,26 +2218,23 @@ public class MonetResultSet return Long.class; case Types.REAL: return Float.class; - case Types.FLOAT: case Types.DOUBLE: return Double.class; - case Types.BINARY: // MonetDB currently does not support these - case Types.VARBINARY: // see treat_blob_as_binary property case Types.LONGVARBINARY: return byte[].class; case Types.DATE: - return java.sql.Date.class; + return Date.class; case Types.TIME: + case 2013: //Types.TIME_WITH_TIMEZONE: return Time.class; case Types.TIMESTAMP: + case 2014: //Types.TIMESTAMP_WITH_TIMEZONE: return Timestamp.class; case Types.CLOB: return Clob.class; case Types.BLOB: return Blob.class; - - // all the rest are currently not implemented and used - default: + default: // all the rest are currently not implemented and used return String.class; } } @@ -2329,8 +2249,7 @@ public class MonetResultSet * built-in types specified in the JDBC specification. If the value is an * SQL NULL, the driver returns a Java null. * - * This method may also be used to read database-specific abstract data - * types. + * This method may also be used to read database-specific abstract data types. * * @param columnLabel the SQL name of the column * @return a java.lang.Object holding the column value @@ -2369,8 +2288,7 @@ public class MonetResultSet } /** - * Retrieves the current row number. The first row is number 1, the second - * number 2, and so on. + * Retrieves the current row number. The first row is number 1, the second number 2, and so on. * * @return the current row number; 0 if there is no current row */ @@ -2385,11 +2303,9 @@ public class MonetResultSet * programming language. * * @param columnIndex the first column is 1, the second is 2, ... - * @return the column value; if the value is SQL NULL, the value returned - * is null + * @return the column value; if the value is SQL NULL, the value returned is null * @throws SQLException if there is no such column - * @throws SQLFeatureNotSupportedException the JDBC driver does - * not support this method + * @throws SQLFeatureNotSupportedException the JDBC driver does not support this method */ @Override public RowId getRowId(int columnIndex) throws SQLException { @@ -2397,16 +2313,13 @@ public class MonetResultSet } /** - * Retrieves the value of the designated column in the current row - * of this ResultSet object as a java.sql.RowId object in the Java - * programming language. + * Retrieves the value of the designated column in the current row of this ResultSet object as a java.sql.RowId + * object in the Java programming language. * * @param columnLabel the SQL name of the column - * @return the column value; if the value is SQL NULL, the value returned - * is null + * @return the column value; if the value is SQL NULL, the value returned is null * @throws SQLException if the ResultSet object does not contain columnLabel - * @throws SQLFeatureNotSupportedException the JDBC driver does - * not support this method + * @throws SQLFeatureNotSupportedException the JDBC driver does not support this method */ @Override public RowId getRowId(String columnLabel) throws SQLException { @@ -2414,38 +2327,69 @@ public class MonetResultSet } /** - * Retrieves the value of the designated column in the current row of this - * ResultSet object as a short in the Java programming language. + * Retrieves the value of the designated column in the current row of this ResultSet object as a short in the Java + * programming language. * * @param columnIndex the first column is 1, the second is 2, ... - * @return the column value; if the value is SQL NULL, the value returned - * is 0 + * @return the column value; if the value is SQL NULL, the value returned is 0 * @throws SQLException if there is no such column */ @Override public short getShort(int columnIndex) throws SQLException { try { - String val = tlp.values[columnIndex - 1]; - if (val == null) { - lastReadWasNull = true; - return 0; + switch (JdbcSQLTypes[columnIndex - 1]) { + case Types.SMALLINT: + short res = currentBlock.getShortValue(columnIndex - 1); + return currentBlock.isLastReadWasNull() ? 0 : res; + case Types.BOOLEAN: + boolean bol = currentBlock.getBooleanValue(columnIndex - 1); + return currentBlock.isLastReadWasNull() ? (short) 0 : (bol ? (short) 1 : (short) 0); + case Types.TINYINT: + byte bb = currentBlock.getByteValue(columnIndex - 1); + return currentBlock.isLastReadWasNull() ? 0 : bb; + case Types.INTEGER: + int in = currentBlock.getIntValue(columnIndex - 1); + return currentBlock.isLastReadWasNull() ? 0 : (short) in; + case Types.BIGINT: + long lon = currentBlock.getLongValue(columnIndex - 1); + return currentBlock.isLastReadWasNull() ? 0 : (short) lon; + case Types.REAL: + float floa = currentBlock.getFloatValue(columnIndex - 1); + return currentBlock.isLastReadWasNull() ? 0 : (short) Math.round(floa); + case Types.DOUBLE: + double dou = currentBlock.getDoubleValue(columnIndex - 1); + return currentBlock.isLastReadWasNull() ? 0 : (short) Math.round(dou); + case Types.CHAR: + case Types.VARCHAR: + case Types.LONGVARCHAR: + case Types.CLOB: + case Types.BLOB: + case Types.LONGVARBINARY: + String ss = currentBlock.getValueAsString(columnIndex - 1); + return ss == null ? 0 : Short.parseShort(ss); + case Types.NUMERIC: + case Types.DECIMAL: + BigDecimal bigdec = (BigDecimal) currentBlock.getValueAsObject(columnIndex - 1); + return bigdec == null ? 0 : bigdec.shortValue(); + default: //OTHERS, BLOB, LONGVARBINARY, TIME... + throw new SQLException("Conversion from " + types[columnIndex - 1] + + " to short type not supported", "M1M05"); } - lastReadWasNull = false; - return Short.parseShort(val); - } catch (NumberFormatException e) { - throw newSQLNumberFormatException(e); + } catch (ClassCastException ex) { + throw new SQLException(ex.getMessage()); + } catch (ProtocolException e) { + throw new SQLException(e); } catch (IndexOutOfBoundsException e) { throw newSQLInvalidColumnIndexException(columnIndex); } } /** - * Retrieves the value of the designated column in the current row of this - * ResultSet object as a short in the Java programming language. + * Retrieves the value of the designated column in the current row of this ResultSet object as a short in the Java + * programming language. * * @param columnLabel the SQL name of the column - * @return the column value; if the value is SQL NULL, the value returned - * is 0 + * @return the column value; if the value is SQL NULL, the value returned is 0 * @throws SQLException if the ResultSet object does not contain columnLabel */ @Override @@ -2454,12 +2398,11 @@ public class MonetResultSet } /** - * Retrieves the Statement object that produced this ResultSet object. If - * the result set was generated some other way, such as by a - * DatabaseMetaData method, this method returns null. + * Retrieves the Statement object that produced this ResultSet object. If the result set was generated some other + * way, such as by a DatabaseMetaData method, this method returns null. * - * @return the Statement object that produced this ResultSet object or null - * if the result set was produced some other way + * @return the Statement object that produced this ResultSet object or null if the result set was produced some + * other way */ @Override public Statement getStatement() { @@ -2467,24 +2410,21 @@ public class MonetResultSet } /** - * Retrieves the value of the designated column in the current row of this - * ResultSet object as a String in the Java programming language. + * Retrieves the value of the designated column in the current row of this ResultSet object as a String in the Java + * programming language. * * @param columnIndex the first column is 1, the second is 2, ... - * @return the column value; if the value is SQL NULL, the value returned - * is null + * @return the column value; if the value is SQL NULL, the value returned is null * @throws SQLException if there is no such column */ @Override public String getString(int columnIndex) throws SQLException { try { - String val = tlp.values[columnIndex - 1]; - if (val == null) { - lastReadWasNull = true; - return null; - } - lastReadWasNull = false; - return val; + return currentBlock.getValueAsString(columnIndex - 1); + } catch (ClassCastException ex) { + throw new SQLException(ex.getMessage()); + } catch (ProtocolException e) { + throw new SQLException(e); } catch (IndexOutOfBoundsException e) { throw newSQLInvalidColumnIndexException(columnIndex); } @@ -2495,8 +2435,7 @@ public class MonetResultSet * ResultSet object as a String in the Java programming language. * * @param columnLabel the SQL name of the column - * @return the column value; if the value is SQL NULL, the value returned - * is null + * @return the column value; if the value is SQL NULL, the value returned is null * @throws SQLException if the ResultSet object does not contain columnLabel */ @Override @@ -2511,11 +2450,9 @@ public class MonetResultSet * and LONGNVARCHAR columns. * * @param columnIndex the first column is 1, the second is 2, ... - * @return the column value; if the value is SQL NULL, the value returned - * is null + * @return the column value; if the value is SQL NULL, the value returned is null * @throws SQLException if there is no such column - * @throws SQLFeatureNotSupportedException the JDBC driver does - * not support this method + * @throws SQLFeatureNotSupportedException the JDBC driver does not support this method */ @Override public String getNString(int columnIndex) throws SQLException { @@ -2529,11 +2466,9 @@ public class MonetResultSet * and LONGNVARCHAR columns. * * @param columnLabel the SQL name of the column - * @return the column value; if the value is SQL NULL, the value returned - * is null + * @return the column value; if the value is SQL NULL, the value returned is null * @throws SQLException if the ResultSet object does not contain columnLabel - * @throws SQLFeatureNotSupportedException the JDBC driver does - * not support this method + * @throws SQLFeatureNotSupportedException the JDBC driver does not support this method */ @Override public String getNString(String columnLabel) throws SQLException { @@ -2541,15 +2476,13 @@ public class MonetResultSet } /** - * Retrieves the value of the designated column in the current row - * of this ResultSet as a java.sql.SQLXML object in the Java - * programming language. + * Retrieves the value of the designated column in the current row of this ResultSet as a java.sql.SQLXML object + * in the Java programming language. * * @param columnIndex the first column is 1, the second is 2, ... * @return a SQLXML object that maps an SQL XML value * @throws SQLException if a database access error occurs - * @throws SQLFeatureNotSupportedException the JDBC driver does - * not support this method + * @throws SQLFeatureNotSupportedException the JDBC driver does not support this method */ @Override public SQLXML getSQLXML(int columnIndex) throws SQLException { @@ -2557,216 +2490,33 @@ public class MonetResultSet } /** - * Retrieves the value of the designated column in the current row - * of this ResultSet as a java.sql.SQLXML object in the Java - * programming language. + * Retrieves the value of the designated column in the current row of this ResultSet as a java.sql.SQLXML object + * in the Java programming language. * - * @param columnLabel the label for the column specified with the SQL AS - * clause. If the SQL AS clause was not specified, then the - * label is the name of the column + * @param columnLabel the label for the column specified with the SQL AS clause. If the SQL AS clause was not + * specified, then the label is the name of the column * @return a SQLXML object that maps an SQL XML value * @throws SQLException if a database access error occurs - * @throws SQLFeatureNotSupportedException the JDBC driver does - * not support this method + * @throws SQLFeatureNotSupportedException the JDBC driver does not support this method */ @Override public SQLXML getSQLXML(String columnLabel) throws SQLException { throw newSQLFeatureNotSupportedException("getSQLXML"); } - // This behaviour is according table B-6 of Sun JDBC Specification 3.0 - private SimpleDateFormat dateFormat; - private SimpleDateFormat timeFormat; - private SimpleDateFormat timestampFormat; - /** - * Helper method which parses the date/time value for columns of type - * TIME, DATE and TIMESTAMP. For the types CHAR, VARCHAR and - * LONGVARCHAR an attempt is made to parse the date according to the - * given type. The given Calender object is filled with the parsed - * data. Optional fractional seconds (nanos) are returned by this - * method. If the underlying type of the column is none of the - * mentioned six, January 1st 1970 0:00:00 GMT is returned.<br /> - * The dates are parsed with the given Calendar. - * - * @param cal the Calendar to use/fill when parsing the date/time - * @param col the column to parse - * @param type the corresponding java.sql.Types type of the calling - * function - * @return the fractional seconds (nanos) or -1 if the value is NULL - * @throws SQLException if a database error occurs - */ - private int getJavaDate(Calendar cal, int columnIndex, int type) - throws SQLException - { - if (cal == null) - throw new IllegalArgumentException("No Calendar object given!"); - - final String monetDate; - final String MonetDBType; - int JdbcType; - try { - monetDate = tlp.values[columnIndex - 1]; - if (monetDate == null) { - lastReadWasNull = true; - return -1; - } - lastReadWasNull = false; - MonetDBType = types[columnIndex - 1]; - JdbcType = JdbcSQLTypes[columnIndex - 1]; - // If we got a string type, set the JdbcType to the given type - // so we attempt to parse it as the caller thinks it is. - if (JdbcType == Types.CHAR || - JdbcType == Types.VARCHAR || - JdbcType == Types.LONGVARCHAR || - JdbcType == Types.CLOB) - { - JdbcType = type; - } - } catch (IndexOutOfBoundsException e) { - throw newSQLInvalidColumnIndexException(columnIndex); - } - - TimeZone ptz = cal.getTimeZone(); - - // it is important to parse the time in the given timezone in - // order to get a correct (UTC) time value, hence we need to - // parse it first - if (MonetDBType != null && ("timetz".equals(MonetDBType) || "timestamptz".equals(MonetDBType))) { - int vallen = monetDate.length(); - if (vallen >= 6) { - // MonetDB/SQL99: Sign TwoDigitHours : Minutes - ptz = TimeZone.getTimeZone("GMT" + monetDate.substring(vallen - 6, vallen)); - } - } - - java.util.Date pdate = null; - final java.text.ParsePosition ppos = new java.text.ParsePosition(0); - switch(JdbcType) { - case Types.DATE: - if (dateFormat == null) { - // first time usage, create and keep the dateFormat object for next usage - dateFormat = new SimpleDateFormat("yyyy-MM-dd"); - } - dateFormat.setTimeZone(ptz); - pdate = dateFormat.parse(monetDate, ppos); - break; - case Types.TIME: - if (timeFormat == null) { - // first time usage, create and keep the timeFormat object for next usage - timeFormat = new SimpleDateFormat("HH:mm:ss"); - } - timeFormat.setTimeZone(ptz); - pdate = timeFormat.parse(monetDate, ppos); - break; - case Types.TIMESTAMP: - if (timestampFormat == null) { - // first time usage, create and keep the timestampFormat object for next usage - timestampFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); - } - timestampFormat.setTimeZone(ptz); - pdate = timestampFormat.parse(monetDate, ppos); - break; - default: - addWarning("unsupported data type", "01M03"); - cal.clear(); - return 0; - } - if (pdate == null) { - // parsing failed - int epos = ppos.getErrorIndex(); - if (epos == -1) { - addWarning("parsing '" + monetDate + "' failed", "01M10"); - } else if (epos < monetDate.length()) { - addWarning("parsing failed," + - " found: '" + monetDate.charAt(epos) + "'" + - " in: \"" + monetDate + "\"" + - " at pos: " + epos, "01M10"); - } else { - addWarning("parsing failed, expected more data after '" + - monetDate + "'", "01M10"); - } - // default value - cal.clear(); - return 0; - } - cal.setTime(pdate); - - int nanos = 0; - if (JdbcType == Types.TIME || JdbcType == Types.TIMESTAMP) { - // parse additional nanos (if any) - int pos = ppos.getIndex(); - char[] monDate = monetDate.toCharArray(); - if (pos < monDate.length && monDate[pos] == '.') { - pos++; - int ctr; - try { - nanos = getIntrinsicValue(monDate[pos], pos++); - for (ctr = 1; - pos < monDate.length && - monDate[pos] >= '0' && - monDate[pos] <= '9'; - ctr++) - { - if (ctr < 9) { - nanos *= 10; - nanos += (getIntrinsicValue(monDate[pos], pos)); - } - if (ctr == 2) // we have three at this point - cal.set(Calendar.MILLISECOND, nanos); - pos++; - } - while (ctr++ < 9) - nanos *= 10; - } catch(MCLParseException e) { - addWarning(e.getMessage() + - " found: '" + monDate[e.getErrorOffset()] + "'" + - " in: \"" + monetDate + "\"" + - " at pos: " + e.getErrorOffset(), "01M10"); - // default value - cal.clear(); - nanos = 0; - } - } - } - return nanos; - } - - /** - * Small helper method that returns the intrinsic value of a char if - * it represents a digit. If a non-digit character is encountered - * an MCLParseException is thrown. - * - * @param c the char - * @param pos the position - * @return the intrinsic value of the char - * @throws MCLParseException if c is not a digit - */ - private final static int getIntrinsicValue(char c, int pos) - throws MCLParseException - { - // note: don't use Character.isDigit() here, because - // we only want ISO-LATIN-1 digits - if (c >= '0' && c <= '9') { - return (int)c - (int)'0'; - } else { - throw new MCLParseException("Expected a digit", pos); - } - } - /** * Retrieves the value of the designated column in the current row of this * ResultSet object as a java.sql.Date object in the Java programming * language. * * @param columnIndex the first column is 1, the second is 2, ... - * @return the column value; if the value is SQL NULL, the value returned - * is null + * @return the column value; if the value is SQL NULL, the value returned is null * @throws SQLException if a database access error occurs * @see #getDate(int col, Calendar cal) */ @Override - public java.sql.Date getDate(int columnIndex) throws SQLException { - return getDate(columnIndex, null); + public Date getDate(int columnIndex) throws SQLException { + return getDate(columnIndex, Calendar.getInstance()); } /** @@ -2778,33 +2528,54 @@ public class MonetResultSet * * @param columnIndex the first column is 1, the second is 2, ... * @param cal the java.util.Calendar object to use in constructing the date - * @return the column value; if the value is SQL NULL, the value returned - * is null + * @return the column value; if the value is SQL NULL, the value returned is null * @throws SQLException if a database access error occurs */ @Override - public java.sql.Date getDate(int columnIndex, Calendar cal) - throws SQLException - { + public Date getDate(int columnIndex, Calendar cal) throws SQLException { try { - String val = tlp.values[columnIndex - 1]; - if (val == null) { - lastReadWasNull = true; - return null; + Calendar res; + long millis; + switch (JdbcSQLTypes[columnIndex - 1]) { + case Types.DATE: + case Types.TIME: + case Types.TIMESTAMP: + res = (Calendar) currentBlock.getValueAsObject(columnIndex - 1); + if(res == null) { + return null; + } + millis = res.getTimeInMillis() - res.getTimeZone().getRawOffset() + cal.getTimeZone().getRawOffset(); + break; + case 2013: //Types.TIME_WITH_TIMEZONE: + case 2014: //Types.TIMESTAMP_WITH_TIMEZONE: + res = (Calendar) currentBlock.getValueAsObject(columnIndex - 1); + if(res == null) { + return null; + } + millis = res.getTimeInMillis(); + break; + case Types.CHAR: + case Types.VARCHAR: + case Types.LONGVARCHAR: + case Types.CLOB: + case Types.BLOB: + case Types.LONGVARBINARY: + res = currentBlock.getDateValueFromString(this, columnIndex - 1, Types.DATE); + if(res == null) { + return null; + } + millis = res.getTimeInMillis() - res.getTimeZone().getRawOffset() + cal.getTimeZone().getRawOffset(); + break; + default: + this.addWarning("unsupported data type", "01M03"); + cal.clear(); + millis = 0; } - lastReadWasNull = false; - if (cal == null) { - if (val.contains("-") && !val.contains(":") && !val.contains("+")) { - // try to convert string in JDBC date escape format yyyy-[m]m-[d]d directly to a Date object - return java.sql.Date.valueOf(val); - } - cal = Calendar.getInstance(); - } - int ret = getJavaDate(cal, columnIndex, Types.DATE); - return ret == -1 ? null : new java.sql.Date(cal.getTimeInMillis()); - } catch (IllegalArgumentException iae) { - throw new SQLDataException("Could not convert value to a Date. Expected JDBC date escape format yyyy-[m]m-[d]d. " - + iae.getMessage(), "22007"); // 22007 = invalid datetime format + return new Date(millis); + } catch (ClassCastException ex) { + throw new SQLException(ex.getMessage()); + } catch (ProtocolException e) { + throw new SQLException(e); } catch (IndexOutOfBoundsException e) { throw newSQLInvalidColumnIndexException(columnIndex); } @@ -2815,15 +2586,13 @@ public class MonetResultSet * ResultSet object as a java.sql.Date object in the Java programming * language. * - * @param columnLabel the SQL name of the column from which to retrieve the - * value - * @return the column value; if the value is SQL NULL, the value returned - * is null + * @param columnLabel the SQL name of the column from which to retrieve the value + * @return the column value; if the value is SQL NULL, the value returned is null * @throws SQLException if a database access error occurs */ @Override - public java.sql.Date getDate(String columnLabel) throws SQLException { - return getDate(findColumn(columnLabel), null); + public Date getDate(String columnLabel) throws SQLException { + return getDate(findColumn(columnLabel), Calendar.getInstance()); } /** @@ -2833,28 +2602,22 @@ public class MonetResultSet * millisecond value for the date if the underlying database does not store * timezone information. * - * @param columnLabel the SQL name of the column from which to retrieve the - * value + * @param columnLabel the SQL name of the column from which to retrieve the value * @param cal the java.util.Calendar object to use in constructing the date - * @return the column value; if the value is SQL NULL, the value returned - * is null + * @return the column value; if the value is SQL NULL, the value returned is null * @throws SQLException if a database access error occurs */ @Override - public java.sql.Date getDate(String columnLabel, Calendar cal) - throws SQLException - { + public Date getDate(String columnLabel, Calendar cal) throws SQLException { return getDate(findColumn(columnLabel), cal); } /** - * Retrieves the value of the designated column in the current row of this - * ResultSet object as a java.sql.Time object in the Java programming - * language. + * Retrieves the value of the designated column in the current row of this ResultSet object as a java.sql.Time + * object in the Java programming language. * * @param columnIndex the first column is 1, the second is 2, ... - * @return the column value; if the value is SQL NULL, the value returned - * is null + * @return the column value; if the value is SQL NULL, the value returned is null * @throws SQLException if a database access error occurs */ @Override @@ -2870,49 +2633,67 @@ public class MonetResultSet * timezone information. * * @param columnIndex the first column is 1, the second is 2, ... - * @param cal the java.util.Calendar object to use in constructing the - * timestamp - * @return the column value as a java.sql.Time object; if the value is - * SQL NULL, the value returned is null in the Java programming - * language + * @param cal the java.util.Calendar object to use in constructing the timestamp + * @return the column value as a java.sql.Time object; if the value is SQL NULL, the value returned is null in the + * Java programming language * @throws SQLException if a database access error occurs */ @Override - public Time getTime(int columnIndex, Calendar cal) - throws SQLException - { + public Time getTime(int columnIndex, Calendar cal) throws SQLException { try { - String val = tlp.values[columnIndex - 1]; - if (val == null) { - lastReadWasNull = true; - return null; + Calendar res; + long millis; + switch (JdbcSQLTypes[columnIndex - 1]) { + case Types.DATE: + case Types.TIME: + case Types.TIMESTAMP: + res = (Calendar) currentBlock.getValueAsObject(columnIndex - 1); + if(res == null) { + return null; + } + millis = res.getTimeInMillis() - res.getTimeZone().getRawOffset() + cal.getTimeZone().getRawOffset(); + break; + case 2013: //Types.TIME_WITH_TIMEZONE: + case 2014: //Types.TIMESTAMP_WITH_TIMEZONE: + res = (Calendar) currentBlock.getValueAsObject(columnIndex - 1); + if(res == null) { + return null; + } + millis = res.getTimeInMillis(); + break; + case Types.CHAR: + case Types.VARCHAR: + case Types.LONGVARCHAR: + case Types.CLOB: + case Types.BLOB: + case Types.LONGVARBINARY: + res = currentBlock.getDateValueFromString(this, columnIndex - 1, Types.TIME); + if(res == null) { + return null; + } + millis = res.getTimeInMillis() - res.getTimeZone().getRawOffset() + cal.getTimeZone().getRawOffset(); + break; + default: + this.addWarning("unsupported data type", "01M03"); + cal.clear(); + millis = 0; } - lastReadWasNull = false; - if (cal == null) { - if (!val.contains("-") && val.contains(":") && !val.contains("+")) { - // try to convert string in JDBC time escape format hh:mm:ss directly to a Time object - return Time.valueOf(val); - } - cal = Calendar.getInstance(); - } - int ret = getJavaDate(cal, columnIndex, Types.TIME); - return ret == -1 ? null : new Time(cal.getTimeInMillis()); - } catch (IllegalArgumentException iae) { - throw new SQLDataException("Could not convert value to a Time. Expected JDBC time escape format hh:mm:ss. " - + iae.getMessage(), "22007"); // 22007 = invalid datetime format + return new Time(millis); + } catch (ClassCastException ex) { + throw new SQLException(ex.getMessage()); + } catch (ProtocolException e) { + throw new SQLException(e); } catch (IndexOutOfBoundsException e) { throw newSQLInvalidColumnIndexException(columnIndex); } } /** - * Retrieves the value of the designated column in the current row of this - * ResultSet object as a java.sql.Time object in the Java programming - * language. + * Retrieves the value of the designated column in the current row of this ResultSet object as a java.sql.Time + * object in the Java programming language. * * @param columnLabel the SQL name of the column - * @return the column value; if the value is SQL NULL, the value returned - * is null + * @return the column value; if the value is SQL NULL, the value returned is null * @throws SQLException if a database access error occurs */ @Override @@ -2928,28 +2709,22 @@ public class MonetResultSet * timezone information. * * @param columnLabel the SQL name of the column - * @param cal the java.util.Calendar object to use in constructing the - * timestamp - * @return the column value as a java.sql.Time object; if the value is - * SQL NULL, the value returned is null in the Java programming - * language + * @param cal the java.util.Calendar object to use in constructing the timestamp + * @return the column value as a java.sql.Time object; if the value is SQL NULL, the value returned is null in the + * Java programming language * @throws SQLException if a database access error occurs */ @Override - public Time getTime(String columnLabel, Calendar cal) - throws SQLException - { + public Time getTime(String columnLabel, Calendar cal) throws SQLException { return getTime(findColumn(columnLabel), cal); } /** - * Retrieves the value of the designated column in the current row of this - * ResultSet object as a java.sql.Timestamp object in the Java programming - * language. + * Retrieves the value of the designated column in the current row of this ResultSet object as a java.sql.Timestamp + * object in the Java programming language. * * @param columnIndex the first column is 1, the second is 2, ... - * @return the column value; if the value is SQL NULL, the value returned - * is null + * @return the column value; if the value is SQL NULL, the value returned is null * @throws SQLException if a database access error occurs */ @Override @@ -2965,54 +2740,84 @@ public class MonetResultSet * store timezone information. * * @param columnIndex the first column is 1, the second is 2, ... - * @param cal the java.util.Calendar object to use in constructing the - * timestamp - * @return the column value as a java.sql.Timestamp object; if the value is - * SQL NULL, the value returned is null in the Java programming - * language + * @param cal the java.util.Calendar object to use in constructing the timestamp + * @return the column value as a java.sql.Timestamp object; if the value is SQL NULL, the value returned is null + * in the Java programming language * @throws SQLException if a database access error occurs */ @Override - public Timestamp getTimestamp(int columnIndex, Calendar cal) - throws SQLException - { + public Timestamp getTimestamp(int columnIndex, Calendar cal) throws SQLException { try { - String val = tlp.values[columnIndex - 1]; - if (val == null) { - lastReadWasNull = true; - return null; + Calendar res; + long millis; + int nanos = 0; + switch (JdbcSQLTypes[columnIndex - 1]) { + case Types.DATE: + case Types.TIME: + res = (Calendar) currentBlock.getValueAsObject(columnIndex - 1); + if(res == null) { + return null; + } + millis = res.getTimeInMillis() - res.getTimeZone().getRawOffset() + cal.getTimeZone().getRawOffset(); + break; + case 2013: //Types.TIME_WITH_TIMEZONE: + res = (Calendar) currentBlock.getValueAsObject(columnIndex - 1); + if(res == null) { + return null; + } + millis = res.getTimeInMillis(); + break; + case Types.TIMESTAMP: + res = (Calendar) currentBlock.getValueAsObject(columnIndex - 1); + if(res == null) { + return null; + } + nanos = currentBlock.getLastNanos(); + millis = res.getTimeInMillis() - res.getTimeZone().getRawOffset() + cal.getTimeZone().getRawOffset(); + break; + case 2014: //Types.TIMESTAMP_WITH_TIMEZONE: + res = (Calendar) currentBlock.getValueAsObject(columnIndex - 1); + if(res == null) { + return null; + } + nanos = currentBlock.getLastNanos(); + millis = res.getTimeInMillis(); + break; + case Types.CHAR: + case Types.VARCHAR: + case Types.LONGVARCHAR: + case Types.CLOB: + case Types.BLOB: + case Types.LONGVARBINARY: + res = currentBlock.getDateValueFromString(this, columnIndex - 1, Types.TIMESTAMP); + if(res == null) { + return null; + } + millis = res.getTimeInMillis() - res.getTimeZone().getRawOffset() + cal.getTimeZone().getRawOffset(); + break; + default: + this.addWarning("unsupported data type", "01M03"); + cal.clear(); + millis = 0; } - lastReadWasNull = false; - if (cal == null) { - if (val.contains("-") && val.contains(":") && !val.contains("+")) { - // try to convert string in JDBC timestamp escape format yyyy-[m]m-[d]d hh:mm:ss[.f...] directly to a Timestamp object - return Timestamp.valueOf(val); - } - cal = Calendar.getInstance(); - } - int nanos = getJavaDate(cal, columnIndex, Types.TIMESTAMP); - if (nanos == -1) - return null; - - Timestamp ts = new Timestamp(cal.getTimeInMillis()); - ts.setNanos(nanos); - return ts; - } catch (IllegalArgumentException iae) { - throw new SQLDataException("Could not convert value to a Timestamp. Expected JDBC time escape format yyyy-[m]m-[d]d hh:mm:ss[.f...]. " - + iae.getMessage(), "22007"); // 22007 = invalid datetime format + Timestamp result = new Timestamp(millis); + result.setNanos(nanos); + return result; + } catch (ClassCastException ex) { + throw new SQLException(ex.getMessage()); + } catch (ProtocolException e) { + throw new SQLException(e); } catch (IndexOutOfBoundsException e) { throw newSQLInvalidColumnIndexException(columnIndex); } } /** - * Retrieves the value of the designated column in the current row of this - * ResultSet object as a java.sql.Timestamp object in the Java programming - * language. + * Retrieves the value of the designated column in the current row of this ResultSet object as a java.sql.Timestamp + * object in the Java programming language. * * @param columnLabel the SQL name of the column - * @return the column value; if the value is SQL NULL, the value returned - * is null + * @return the column value; if the value is SQL NULL, the value returned is null * @throws SQLException if a database access error occurs */ @Override @@ -3028,17 +2833,13 @@ public class MonetResultSet * store timezone information. * * @param columnLabel the SQL name of the column - * @param cal the java.util.Calendar object to use in constructing the - * timestamp + * @param cal the java.util.Calendar object to use in constructing the timestamp * @return the column value as a java.sql.Timestamp object; if the value is - * SQL NULL, the value returned is null in the Java programming - * language + * SQL NULL, the value returned is null in the Java programming language * @throws SQLException if a database access error occurs */ @Override - public Timestamp getTimestamp(String columnLabel, Calendar cal) - throws SQLException - { + public Timestamp getTimestamp(String columnLabel, Calendar cal) throws SQLException { return getTimestamp(findColumn(columnLabel), cal); } @@ -3046,8 +2847,7 @@ public class MonetResultSet * Retrieves the type of this ResultSet object. The type is determined by * the Statement object that created the result set. * - * @return ResultSet.TYPE_FORWARD_ONLY, ResultSet.TYPE_SCROLL_INSENSITIVE, - * or ResultSet.TYPE_SCROLL_SENSITIVE + * @return ResultSet.TYPE_FORWARD_ONLY, ResultSet.TYPE_SCROLL_INSENSITIVE or ResultSet.TYPE_SCROLL_SENSITIVE */ @Override public int getType() { @@ -3055,48 +2855,51 @@ public class MonetResultSet } /** - * Retrieves the value of the designated column in the current row - * of this ResultSet object as a java.net.URL object in the Java - * programming language. + * Retrieves the value of the designated column in the current row of this ResultSet object as a java.net.URL + * object in the Java programming language. * - * @param columnIndex the index of the column 1 is the first, - * 2 is the second,... - * @return the column value as a java.net.URL object; if the value - * is SQL NULL, the value returned is null in the Java - * programming language - * @throws SQLException if a database access error occurs, or if a - * URL is malformed + * @param columnIndex the index of the column 1 is the first, 2 is the second,... + * @return the column value as a java.net.URL object; if the value is SQL NULL, the value returned is null in the + * Java programming language + * @throws SQLException if a database access error occurs, or if a URL is malformed */ @Override public URL getURL(int columnIndex) throws SQLException { try { - String val = tlp.values[columnIndex - 1]; - if (val == null) { - lastReadWasNull = true; - return null; + switch(JdbcSQLTypes[columnIndex - 1]) { //if it's a string type, will attempt the conversion + case Types.OTHER: + if("url".equals(types[columnIndex - 1])) { + String ss = currentBlock.getValueAsString(columnIndex - 1); + return ss == null ? null : new URL(ss); + } + break; + case Types.CHAR: + case Types.VARCHAR: + case Types.LONGVARCHAR: + case Types.CLOB: + case Types.BLOB: + case Types.LONGVARBINARY: + String sss = currentBlock.getValueAsString(columnIndex - 1); + return sss == null ? null : new URL(sss); } - lastReadWasNull = false; - try { - return new URL(val); - } catch (MalformedURLException e) { - throw new SQLException(e.getMessage(), "M1M05"); - } + throw new SQLException("Cannot convert " + types[columnIndex - 1] + " to an url", "M1M05"); } catch (IndexOutOfBoundsException e) { throw newSQLInvalidColumnIndexException(columnIndex); + } catch (ProtocolException e) { + throw new SQLException(e); + } catch (MalformedURLException e) { + throw new SQLException(e.getMessage(), "M1M05"); } } /** - * Retrieves the value of the designated column in the current row - * of this ResultSet object as a java.net.URL object in the Java - * programming language. + * Retrieves the value of the designated column in the current row of this ResultSet object as a java.net.URL object + * in the Java programming language. * * @param columnLabel the SQL name of the column - * @return the column value as a java.net.URL object; if the value - * is SQL NULL, the value returned is null in the Java - * programming language - * @throws SQLException if a database access error occurs, or if a - * URL is malformed + * @return the column value as a java.net.URL object; if the value is SQL NULL, the value returned is null in the + * Java programming language + * @throws SQLException if a database access error occurs, or if a URL is malformed */ @Override public URL getURL(String columnLabel) throws SQLException { @@ -3123,17 +2926,15 @@ public class MonetResultSet if (header != null && header.isClosed()) throw new SQLException("Cannot call on closed ResultSet", "M1M20"); - // if there are no warnings, this will be null, which fits with the - // specification. + // if there are no warnings, this will be null, which fits with the specification. return warnings; } /** - * Retrieves whether the cursor is after the last row in this ResultSet - * object. + * Retrieves whether the cursor is after the last row in this ResultSet object. * - * @return true if the cursor is after the last row; false if the cursor is - * at any other position or the result set contains no rows + * @return true if the cursor is after the last row; false if the cursor is at any other position or the result set + * contains no rows */ @Override public boolean isAfterLast() { @@ -3141,11 +2942,10 @@ public class MonetResultSet } /** - * Retrieves whether the cursor is before the first row in this ResultSet - * object. + * Retrieves whether the cursor is before the first row in this ResultSet object. * - * @return true if the cursor is before the first row; false if the cursor - * is at any other position or the result set contains no rows + * @return true if the cursor is before the first row; false if the cursor is at any other position or the result + * set contains no rows */ @Override public boolean isBeforeFirst() { @@ -3153,12 +2953,10 @@ public class MonetResultSet } /** - * Retrieves whether this ResultSet object has been closed. A - * ResultSet is closed if the method close has been called on it, or - * if it is automatically closed. + * Retrieves whether this ResultSet object has been closed. A ResultSet is closed if the method close has been + * called on it, or if it is automatically closed. * - * @return true if this ResultSet object is closed; false if it is - * still open + * @return true if this ResultSet object is closed; false if it is still open */ @Override public boolean isClosed() { @@ -3166,8 +2964,7 @@ public class MonetResultSet } /** - * Retrieves whether the cursor is on the first row of this ResultSet - * object. + * Retrieves whether the cursor is on the first row of this ResultSet object. * * @return true if the cursor is on the first row; false otherwise */ @@ -3189,10 +2986,8 @@ public class MonetResultSet /** * Moves the cursor to the last row in this ResultSet object. * - * @return true if the cursor is on a valid row; false if there are no rows - * in the result set - * @throws SQLException if a database access error occurs or the result set - * type is TYPE_FORWARD_ONLY + * @return true if the cursor is on a valid row; false if there are no rows in the result set + * @throws SQLException if a database access error occurs or the result set type is TYPE_FORWARD_ONLY */ @Override public boolean last() throws SQLException { @@ -3209,10 +3004,8 @@ public class MonetResultSet * next will implicitly close it. A ResultSet object's warning chain is * cleared when a new row is read. * - * @return true if the new current row is valid; false if there are no - * more rows - * @throws SQLException if a database access error occurs or ResultSet is - * closed + * @return true if the new current row is valid; false if there are no more rows + * @throws SQLException if a database access error occurs or ResultSet is closed */ @Override public boolean next() throws SQLException { @@ -3222,10 +3015,9 @@ public class MonetResultSet /** * Moves the cursor to the previous row in this ResultSet object. * - * @return true if the cursor is on a valid row; false if it is off - * the result set - * @throws SQLException if a database access error occurs or ResultSet is - * closed or the result set type is TYPE_FORWARD_ONLY + * @return true if the cursor is on a valid row; false if it is off the result set + * @throws SQLException if a database access error occurs or ResultSet is closed or the result set type is + * TYPE_FORWARD_ONLY */ @Override public boolean previous() throws SQLException { @@ -3299,7 +3091,7 @@ public class MonetResultSet * * Note: Support for the rowUpdated method is optional with a result set concurrency of CONCUR_READ_ONLY * - * Returns: true if the current row is detected to have been visibly updated by the owner or another; false otherwise + * Returns: true if the current row is detected to have been visibly updated by the owner or another;false otherwise * * Throws: * SQLException - if a database access error occurs or this method is called on a closed result set @@ -3312,8 +3104,23 @@ public class MonetResultSet return false; } - /* the next methods are all related to updateable result sets, which we - currently do not support */ + /** + * Adds a warning to the pile of warnings this ResultSet object has. If + * there were no warnings (or clearWarnings was called) this warning will + * be the first, otherwise this warning will get appended to the current + * warning. + * + * @param reason the warning message + */ + public void addWarning(String reason, String sqlstate) { + if (warnings == null) { + warnings = new SQLWarning(reason, sqlstate); + } else { + warnings.setNextWarning(new SQLWarning(reason, sqlstate)); + } + } + + /* the next methods are all related to updateable result sets, which we currently do not support */ @Override public void cancelRowUpdates() throws SQLException { throw newSQLFeatureNotSupportedException("cancelRowUpdates"); @@ -3344,7 +3151,6 @@ public class MonetResultSet throw newSQLFeatureNotSupportedException("refreshRow"); } - @Override public void updateArray(int columnIndex, Array x) throws SQLException { throw newSQLFeatureNotSupportedException("updateArray"); @@ -3520,10 +3326,6 @@ public class MonetResultSet throw newSQLFeatureNotSupportedException("updateNCharacterStream"); } - public void updateNCharacterStream(int columnIndex, Reader x, int length) throws SQLException { - throw newSQLFeatureNotSupportedException("updateNCharacterStream"); - } - @Override public void updateNCharacterStream(int columnIndex, Reader x, long length) throws SQLException { throw newSQLFeatureNotSupportedException("updateNCharacterStream"); @@ -3534,10 +3336,6 @@ public class MonetResultSet throw newSQLFeatureNotSupportedException("updateNCharacterStream"); } - public void updateNCharacterStream(String columnLabel, Reader reader, int length) throws SQLException { - throw newSQLFeatureNotSupportedException("updateNCharacterStream"); - } - @Override public void updateNCharacterStream(String columnLabel, Reader reader, long length) throws SQLException { throw newSQLFeatureNotSupportedException("updateNCharacterStream"); @@ -3775,66 +3573,34 @@ public class MonetResultSet * its value and then call the method wasNull to see if the value read was * SQL NULL. * - * @return true if the last column value read was SQL NULL and false - * otherwise + * @return true if the last column value read was SQL NULL and false otherwise */ @Override public boolean wasNull() { - return lastReadWasNull; + return currentBlock.isLastReadWasNull(); } //== end methods of interface ResultSet /** - * Adds a warning to the pile of warnings this ResultSet object has. If - * there were no warnings (or clearWarnings was called) this warning will - * be the first, otherwise this warning will get appended to the current - * warning. - * - * @param reason the warning message - */ - private void addWarning(String reason, String sqlstate) { - if (warnings == null) { - warnings = new SQLWarning(reason, sqlstate); - } else { - warnings.setNextWarning(new SQLWarning(reason, sqlstate)); - } - } - - /** * Small helper method that formats the "Invalid Column Index number ..." message - * and creates a new SQLDataException object whose SQLState is set - * to "22010": invalid indicator parameter value. + * and creates a new SQLDataException object whose SQLState is set to "22010": invalid indicator parameter value. * * @param colIdx the column index number * @return a new created SQLDataException object with SQLState 22010 */ - public final static SQLDataException newSQLInvalidColumnIndexException(int colIdx) { + static SQLDataException newSQLInvalidColumnIndexException(int colIdx) { return new SQLDataException("Invalid Column Index number: " + colIdx, "22010"); } /** - * Small helper method that formats the "Could not convert value to a number" message - * and creates a new SQLDataException object whose SQLState is set - * to "22003": Numeric value out of range. - * - * @param error the NumberFormatException - * @return a new created SQLDataException object with SQLState 22003 - */ - private final static SQLDataException newSQLNumberFormatException(NumberFormatException error) { - return new SQLDataException("Could not convert value to a number. " + error.getMessage(), "22003"); - } - - /** * Small helper method that formats the "Method ... not implemented" message - * and creates a new SQLFeatureNotSupportedException object - * whose SQLState is set to "0A000": feature not supported. + * and creates a new SQLFeatureNotSupportedException object whose SQLState is set to "0A000": feature not supported. * * @param name the method name * @return a new created SQLFeatureNotSupportedException object with SQLState 0A000 */ - private final static SQLFeatureNotSupportedException newSQLFeatureNotSupportedException(String name) { + private static SQLFeatureNotSupportedException newSQLFeatureNotSupportedException(String name) { return new SQLFeatureNotSupportedException("Method " + name + " not implemented", "0A000"); } } -
--- a/src/main/java/nl/cwi/monetdb/jdbc/MonetSavepoint.java +++ b/src/main/java/nl/cwi/monetdb/jdbc/MonetSavepoint.java @@ -42,8 +42,8 @@ public class MonetSavepoint implements S private final int id; public MonetSavepoint(String name) throws IllegalArgumentException { - if (name == null) throw new IllegalArgumentException("null not allowed"); - + if (name == null) + throw new IllegalArgumentException("Null name not allowed"); this.id = getNextId(); this.name = name; } @@ -53,43 +53,37 @@ public class MonetSavepoint implements S this.name = null; } - /** - * Retrieves the generated ID for the savepoint that this Savepoint object - * represents. + * Retrieves the generated ID for the savepoint that this Savepoint object represents. * * @return the numeric ID of this savepoint * @throws SQLException if this is a named savepoint */ @Override public int getSavepointId() throws SQLException { - if (name != null) throw - new SQLException("Cannot getID for named savepoint", "3B000"); - + if (name != null) + throw new SQLException("Cannot getID for named savepoint", "3B000"); return getId(); } /** - * Retrieves the name of the savepoint that this Savepoint object - * represents. + * Retrieves the name of the savepoint that this Savepoint object represents. * * @return the name of this savepoint * @throws SQLException if this is an un-named savepoint */ @Override public String getSavepointName() throws SQLException { - if (name == null) throw - new SQLException("Unable to retrieve name of unnamed savepoint", "3B000"); - + if (name == null) + throw new SQLException("Unable to retrieve name of unnamed savepoint", "3B000"); return name; } //== end of methods from Savepoint interface /** - * Retrieves the savepoint id, like the getSavepointId method with the only - * difference that this method will always return the id, regardless of - * whether it is named or not. + * Retrieves the savepoint id, like the getSavepointId method with the only difference that this method will always + * return the id, regardless of whether it is named or not. * * @return the numeric ID of this savepoint */ @@ -98,9 +92,8 @@ public class MonetSavepoint implements S } /** - * Returns the name to use when referencing this save point to the MonetDB - * database. The returned value is guaranteed to be unique and consists of - * a prefix string and a sequence number. + * Returns the name to use when referencing this save point to the MonetDB database. The returned value is + * guaranteed to be unique and consists of a prefix string and a sequence number. * * @return the unique savepoint name */ @@ -116,8 +109,7 @@ public class MonetSavepoint implements S * Therefore two successive calls to this method need not to have a * difference of 1. * - * @return the next int which is guaranteed to be higher than the one - * at the last call to this method. + * @return the next int which is guaranteed to be higher than the one at the last call to this method. */ private static int getNextId() { return highestId.incrementAndGet();
--- a/src/main/java/nl/cwi/monetdb/jdbc/MonetStatement.java +++ b/src/main/java/nl/cwi/monetdb/jdbc/MonetStatement.java @@ -8,7 +8,10 @@ package nl.cwi.monetdb.jdbc; -import nl.cwi.monetdb.mcl.net.MapiSocket; +import nl.cwi.monetdb.mcl.responses.*; +import nl.cwi.monetdb.mcl.responses.ResultSetResponse; + +import java.io.IOException; import java.sql.BatchUpdateException; import java.sql.Connection; import java.sql.Statement; @@ -22,17 +25,17 @@ import java.util.concurrent.locks.Reentr /** * A Statement suitable for the MonetDB database. - * + * * The object used for executing a static SQL statement and returning * the results it produces.<br /> - * + * * By default, only one {@link ResultSet} object per Statement object can be * open at the same time. Therefore, if the reading of one ResultSet * object is interleaved with the reading of another, each must have * been generated by different {@link Statement} objects. All execution methods * in the Statement interface implicitly close a Statement's current * ResultSet object if an open one exists. - * + * * The current state of this Statement is that it only implements the * executeQuery() which returns a ResultSet where from results can be * read and executeUpdate() which doesn't return the affected rows. @@ -40,46 +43,36 @@ import java.util.concurrent.locks.Reentr * which relies on server side auto commit.<br /> * Multi-result queries are supported using the getMoreResults() method. * - * @author Fabian Groffen * @author Martin van Dinther * @version 0.7 */ -public class MonetStatement - extends MonetWrapper - implements Statement, AutoCloseable -{ - /** the default value of maxRows, 0 indicates unlimited */ - static final int DEF_MAXROWS = 0; - +public class MonetStatement extends MonetWrapper implements Statement, AutoCloseable { /** The parental Connection object */ private MonetConnection connection; /** The last ResponseList object this Statement produced */ private MonetConnection.ResponseList lastResponseList; /** The last Response that this object uses */ - MonetConnection.Response header; + IResponse header; /** The warnings this Statement object generated */ private SQLWarning warnings; /** Whether this Statement object is closed or not */ protected boolean closed; /** Whether the application wants this Statement object to be pooled */ - protected boolean poolable; - /** Whether this Statement should be closed if the last ResultSet - * closes */ + boolean poolable; + /** Whether this Statement should be closed if the last ResultSet closes */ private boolean closeOnCompletion = false; /** The size of the blocks of results to ask for at the server */ private int fetchSize = 0; - /** The maximum number of rows to return in a ResultSet */ - private int maxRows = DEF_MAXROWS; + /** The maximum number of rows to return in a ResultSet, 0 indicates unlimited */ + private int maxRows = 0; /** The suggested direction of fetching data (implemented but not used) */ private int fetchDirection = ResultSet.FETCH_FORWARD; /** The type of ResultSet to produce; i.e. forward only, random access */ private int resultSetType = ResultSet.TYPE_FORWARD_ONLY; /** The concurrency of the ResultSet to produce */ private int resultSetConcurrency = ResultSet.CONCUR_READ_ONLY; - /** A List to hold all queries of a batch */ - private List<String> batch = new ArrayList<String>(); - + private List<String> batch = new ArrayList<>(); /** * MonetStatement constructor which checks the arguments for validity, tries @@ -89,18 +82,12 @@ public class MonetStatement * @param connection the connection that created this Statement * @param resultSetType type of ResultSet to produce * @param resultSetConcurrency concurrency of ResultSet to produce - * @throws SQLException if an error occurs during login * @throws IllegalArgumentException is one of the arguments is null or empty */ - MonetStatement( - MonetConnection connection, - int resultSetType, - int resultSetConcurrency, - int resultSetHoldability) - throws SQLException, IllegalArgumentException - { - if (connection == null) - throw new IllegalArgumentException("No Connection given!"); + MonetStatement(MonetConnection connection, int resultSetType, int resultSetConcurrency, int resultSetHoldability) + throws IllegalArgumentException { + if (connection == null) throw + new IllegalArgumentException("No Connection given!"); this.connection = connection; this.resultSetType = resultSetType; @@ -109,13 +96,13 @@ public class MonetStatement // check our limits, and generate warnings as appropriate if (resultSetConcurrency != ResultSet.CONCUR_READ_ONLY) { addWarning("No concurrency mode other then read only is supported, continuing with concurrency level READ_ONLY", "01M13"); - resultSetConcurrency = ResultSet.CONCUR_READ_ONLY; + this.resultSetConcurrency = ResultSet.CONCUR_READ_ONLY; } // check type for supported mode if (resultSetType == ResultSet.TYPE_SCROLL_SENSITIVE) { addWarning("Change sensitive scrolling ResultSet objects are not supported, continuing with a change non-sensitive scrollable cursor.", "01M14"); - resultSetType = ResultSet.TYPE_SCROLL_INSENSITIVE; + this.resultSetType = ResultSet.TYPE_SCROLL_INSENSITIVE; } // check type for supported holdability @@ -130,9 +117,8 @@ public class MonetStatement //== methods of interface Statement /** - * Adds the given SQL command to the current list of commmands for this - * Statement object. The commands in this list can be executed as a - * batch by calling the method executeBatch. + * Adds the given SQL command to the current list of commands for this Statement object. The commands in this list + * can be executed as a batch by calling the method executeBatch. * * @param sql typically this is a static SQL INSERT or UPDATE statement * @throws SQLException so the PreparedStatement can throw this exception @@ -150,8 +136,8 @@ public class MonetStatement batch.clear(); } - Lock batchLock = new ReentrantLock(); - + private Lock batchLock = new ReentrantLock(); + /** * Submits a batch of commands to the database for execution and if * all commands execute successfully, returns an array of update @@ -193,9 +179,6 @@ public class MonetStatement */ @Override public int[] executeBatch() throws SQLException { - // this method is synchronized to make sure noone gets inbetween the - // operations we execute below - batchLock.lock(); try { // don't think long if there isn't much to do @@ -208,16 +191,17 @@ public class MonetStatement boolean error = false; BatchUpdateException e = new BatchUpdateException("Error(s) occurred while executing the batch, see next SQLExceptions for details", "22000", counts); - StringBuilder tmpBatch = new StringBuilder(MapiSocket.BLOCK); - String sep = connection.queryTempl[2]; + int builderSize = connection.initialStringBuilderSize(); + StringBuilder tmpBatch = new StringBuilder(builderSize); + String sep = connection.getLanguage().getQueryTemplateIndex(2); for (int i = 0; i < batch.size(); i++) { String tmp = batch.get(i); - if (sep.length() + tmp.length() > MapiSocket.BLOCK) { - // The thing is too big. Way too big. Since it won't - // be optimal anyway, just add it to whatever we have - // and continue. - if (!first) + if (sep.length() + tmp.length() > builderSize) { + // The thing is too big. Way too big. Since it won't be optimal anyway, just add it to whatever we + // have and continue. + if (!first) { tmpBatch.append(sep); + } tmpBatch.append(tmp); // send and receive error |= internalBatch(tmpBatch.toString(), counts, offset, i + 1, e); @@ -226,7 +210,7 @@ public class MonetStatement first = true; continue; } - if (tmpBatch.length() + sep.length() + tmp.length() >= MapiSocket.BLOCK) { + if (tmpBatch.length() + sep.length() + tmp.length() >= builderSize) { // send and receive error |= internalBatch(tmpBatch.toString(), counts, offset, i + 1, e); offset = i; @@ -250,14 +234,8 @@ public class MonetStatement } } - private boolean internalBatch( - String batch, - int[] counts, - int offset, - int max, - BatchUpdateException e) - throws BatchUpdateException - { + private boolean internalBatch(String batch, int[] counts, int offset, int max, BatchUpdateException e) + throws BatchUpdateException { try { boolean type = internalExecute(batch); int count = -1; @@ -266,17 +244,14 @@ public class MonetStatement if (offset >= max) throw new SQLException("Overflow: don't use multi statements when batching (" + max + ")", "M1M16"); if (type) { - e.setNextException( - new SQLException("Batch query produced a ResultSet! " + - "Ignoring and setting update count to " + - "value " + EXECUTE_FAILED, "M1M17")); + e.setNextException(new SQLException("Batch query produced a ResultSet! " + + "Ignoring and setting update count to value " + EXECUTE_FAILED, "M1M17")); counts[offset] = EXECUTE_FAILED; } else if (count >= 0) { counts[offset] = count; } offset++; - } while ((type = getMoreResults()) || - (count = getUpdateCount()) != -1); + } while ((type = getMoreResults()) || (count = getUpdateCount()) != -1); } catch (SQLException ex) { e.setNextException(ex); for (; offset < max; offset++) { @@ -288,7 +263,7 @@ public class MonetStatement } /** - * Cancels this Statement object if both the DBMS and driver support + * Cancels this Statement object if both the DBMS and driver support * aborting an SQL statement. This method can be used by one thread to * cancel a statement that is being executed by another thread. * @@ -326,8 +301,9 @@ public class MonetStatement @Override public void close() { // close previous ResultSet, if not closed already - if (lastResponseList != null) + if (lastResponseList != null) { lastResponseList.close(); + } closed = true; } @@ -386,13 +362,10 @@ public class MonetStatement * Statement.NO_GENERATED_KEYS. */ @Override - public boolean execute(String sql, int autoGeneratedKeys) - throws SQLException - { - if (autoGeneratedKeys != Statement.RETURN_GENERATED_KEYS && - autoGeneratedKeys != Statement.NO_GENERATED_KEYS) + public boolean execute(String sql, int autoGeneratedKeys) throws SQLException { + if (autoGeneratedKeys != Statement.RETURN_GENERATED_KEYS && autoGeneratedKeys != Statement.NO_GENERATED_KEYS) { throw new SQLException("Invalid argument, expected RETURN_GENERATED_KEYS or NO_GENERATED_KEYS", "M1M05"); - + } /* MonetDB has no way to disable this, so just do the normal thing ;) */ return internalExecute(sql); } @@ -434,10 +407,8 @@ public class MonetStatement * valid column indexes */ @Override - public boolean execute(String sql, int[] columnIndexes) - throws SQLException - { - addWarning("execute: generated keys for fixed set of columns not supported", "01M18"); + public boolean execute(String sql, int[] columnIndexes) throws SQLException { + this.addWarning("execute: generated keys for fixed set of columns not supported", "01M18"); return execute(sql, Statement.RETURN_GENERATED_KEYS); } @@ -478,9 +449,7 @@ public class MonetStatement * not valid column names */ @Override - public boolean execute(String sql, String[] columnNames) - throws SQLException - { + public boolean execute(String sql, String[] columnNames) throws SQLException { addWarning("execute: generated keys for fixed set of columns not supported", "01M18"); return execute(sql, Statement.RETURN_GENERATED_KEYS); } @@ -506,12 +475,7 @@ public class MonetStatement } // create a container for the result - lastResponseList = connection.new ResponseList( - fetchSize, - maxRows, - resultSetType, - resultSetConcurrency - ); + lastResponseList = connection.new ResponseList(fetchSize, maxRows, resultSetType, resultSetConcurrency); // fill the header list by processing the query lastResponseList.processQuery(sql); @@ -531,7 +495,7 @@ public class MonetStatement */ @Override public ResultSet executeQuery(String sql) throws SQLException { - if (execute(sql) != true) + if (!execute(sql)) throw new SQLException("Query did not produce a result set", "M1M19"); return getResultSet(); @@ -551,7 +515,7 @@ public class MonetStatement */ @Override public int executeUpdate(String sql) throws SQLException { - if (execute(sql) != false) + if (execute(sql)) throw new SQLException("Query produced a result set", "M1M17"); return getUpdateCount(); @@ -576,15 +540,12 @@ public class MonetStatement * given constant is not one of those allowed */ @Override - public int executeUpdate(String sql, int autoGeneratedKeys) - throws SQLException - { - if (autoGeneratedKeys != Statement.RETURN_GENERATED_KEYS && - autoGeneratedKeys != Statement.NO_GENERATED_KEYS) + public int executeUpdate(String sql, int autoGeneratedKeys) throws SQLException { + if (autoGeneratedKeys != Statement.RETURN_GENERATED_KEYS && autoGeneratedKeys != Statement.NO_GENERATED_KEYS) throw new SQLException("Invalid argument, expected RETURN_GENERATED_KEYS or NO_GENERATED_KEYS", "M1M05"); - + /* MonetDB has no way to disable this, so just do the normal thing ;) */ - if (execute(sql) != false) + if (execute(sql)) throw new SQLException("Query produced a result set", "M1M17"); return getUpdateCount(); @@ -614,9 +575,7 @@ public class MonetStatement * whose elements are valid column indexes */ @Override - public int executeUpdate(String sql, int[] columnIndexes) - throws SQLException - { + public int executeUpdate(String sql, int[] columnIndexes) throws SQLException { addWarning("executeUpdate: generated keys for fixed set of columns not supported", "01M18"); return executeUpdate(sql, Statement.RETURN_GENERATED_KEYS); } @@ -645,9 +604,7 @@ public class MonetStatement * whose elements are valid column names */ @Override - public int executeUpdate(String sql, String[] columnNames) - throws SQLException - { + public int executeUpdate(String sql, String[] columnNames) throws SQLException { addWarning("executeUpdate: generated keys for fixed set of columns not supported", "01M18"); return executeUpdate(sql, Statement.RETURN_GENERATED_KEYS); } @@ -683,8 +640,7 @@ public class MonetStatement * setFetchSize, or the method setFetchSize was called as such to let * the driver ignore the hint, 0 is returned. * - * @return the default fetch size for result sets generated from this - * Statement object + * @return the default fetch size for result sets generated from this Statement object */ @Override public int getFetchSize() { @@ -696,38 +652,34 @@ public class MonetStatement * executing this Statement object. If this Statement object did not * generate any keys, an empty ResultSet object is returned. * - * @return a ResultSet object containing the auto-generated key(s) - * generated by the execution of this Statement object + * @return a ResultSet object containing the auto-generated key(s) generated by the execution of this Statement + * object * @throws SQLException - if a database access error occurs */ @Override public ResultSet getGeneratedKeys() throws SQLException { - String[] columns, types; - String[][] results; - - columns = new String[1]; - types = new String[1]; + String[] columns = new String[1], types = new String[1]; + int[] jdbcTypes = new int[1]; + Object[] results; columns[0] = "GENERATED_KEY"; - /* the generated key should be an integer, because (wait for it) other + /* the generated key should be an integer, because (wait for it) other * frameworks such as spring expect this. */ types[0] = "BIGINT"; + jdbcTypes[0] = MonetDriver.getJavaType(types[0]); - if (header instanceof MonetConnection.UpdateResponse) { - String lastid = ((MonetConnection.UpdateResponse)header).lastid; - if (lastid.equals("-1")) { - results = new String[0][1]; - } else { - results = new String[1][1]; - results[0][0] = lastid; - } + results = new Object[1]; + results[0] = new long[1]; + + if (header instanceof UpdateResponse) { + ((long[]) results[0])[0] = ((UpdateResponse)header).getLastid(); } else { - results = new String[0][1]; + ((long[]) results[0])[0] = -1; } try { - return new MonetVirtualResultSet(this, columns, types, results); - } catch (IllegalArgumentException e) { + return new MonetVirtualResultSet(this, columns, types, jdbcTypes, results); + } catch (IllegalArgumentException | IOException e) { throw new SQLException("Internal driver error: " + e.getMessage(), "M0M03"); } } @@ -745,7 +697,6 @@ public class MonetStatement * * @return the current column size limit for columns storing * character and binary values; zero means there is no limit - * @throws SQLException if a database access error occurs */ @Override public int getMaxFieldSize() { @@ -815,16 +766,14 @@ public class MonetStatement // we default to keep current result, which requires no action header = lastResponseList.getNextResponse(); - return (header instanceof MonetConnection.ResultSetResponse); + return header instanceof ResultSetResponse; } /** - * Retrieves the number of seconds the driver will wait for a - * Statement object to execute. If the limit is exceeded, a - * SQLException is thrown. + * Retrieves the number of seconds the driver will wait for a Statement object to execute. If the limit is exceeded, + * a SQLException is thrown. * - * @return the current query timeout limit in seconds; zero means - * there is no limit + * @return the current query timeout limit in seconds; zero means there is no limit * @throws SQLException if a database access error occurs * @see #setQueryTimeout(int) */ @@ -853,24 +802,20 @@ public class MonetStatement } /** - * Retrieves the current result as a ResultSet object. This method - * should be called only once per result. + * Retrieves the current result as a ResultSet object. This method should be called only once per result. * - * @return the current result as a ResultSet object or null if the result - * is an update count or there are no more results + * @return the current result as a ResultSet object or null if the result is an update count or there are no more + * results * @throws SQLException if a database access error occurs */ @Override public ResultSet getResultSet() throws SQLException { - return (header instanceof MonetConnection.ResultSetResponse) - ? new MonetResultSet(this, - (MonetConnection.ResultSetResponse)header) - : null; + return (header instanceof ResultSetResponse) ? new MonetResultSet(this, (ResultSetResponse)header) + : null; } /** - * Retrieves the result set concurrency for ResultSet objects generated - * by this Statement object. + * Retrieves the result set concurrency for ResultSet objects generated by this Statement object. * * @return either ResultSet.CONCUR_READ_ONLY or ResultSet.CONCUR_UPDATABLE */ @@ -880,11 +825,9 @@ public class MonetStatement } /** - * Retrieves the result set holdability for ResultSet objects - * generated by this Statement object. + * Retrieves the result set holdability for ResultSet objects generated by this Statement object. * - * @return either ResultSet.HOLD_CURSORS_OVER_COMMIT or - * ResultSet.CLOSE_CURSORS_AT_COMMIT + * @return either ResultSet.HOLD_CURSORS_OVER_COMMIT or ResultSet.CLOSE_CURSORS_AT_COMMIT * @throws SQLException if a database access error occurs */ @Override @@ -893,12 +836,9 @@ public class MonetStatement } /** - * Retrieves the result set type for ResultSet objects generated by this - * Statement object. + * Retrieves the result set type for ResultSet objects generated by this Statement object. * - * @return one of ResultSet.TYPE_FORWARD_ONLY, - * ResultSet.TYPE_SCROLL_INSENSITIVE, or - * ResultSet.TYPE_SCROLL_SENSITIVE + * @return one of ResultSet.TYPE_FORWARD_ONLY, ResultSet.TYPE_SCROLL_INSENSITIVE, or ResultSet.TYPE_SCROLL_SENSITIVE */ @Override public int getResultSetType() { @@ -917,12 +857,11 @@ public class MonetStatement @Override public int getUpdateCount() throws SQLException { int ret = -1; - if (header instanceof MonetConnection.UpdateResponse) { - ret = ((MonetConnection.UpdateResponse)header).count; - } else if (header instanceof MonetConnection.SchemaResponse) { - ret = ((MonetConnection.SchemaResponse)header).state; + if (header instanceof UpdateResponse) { + ret = ((UpdateResponse)header).getCount(); + } else if (header instanceof SchemaResponse) { + ret = ((SchemaResponse)header).getState(); } - return ret; } @@ -943,9 +882,9 @@ public class MonetStatement */ @Override public SQLWarning getWarnings() throws SQLException { - if (closed) + if (closed) { throw new SQLException("Cannot call on closed Statement", "M1M20"); - + } // if there are no warnings, this will be null, which fits with the // specification. return warnings; @@ -999,8 +938,9 @@ public class MonetStatement */ @Override public void setEscapeProcessing(boolean enable) throws SQLException { - if (enable) + if (enable) { addWarning("setEscapeProcessing: JDBC escape syntax is not supported by this driver", "01M22"); + } } /** @@ -1019,10 +959,8 @@ public class MonetStatement */ @Override public void setFetchDirection(int direction) throws SQLException { - if (direction == ResultSet.FETCH_FORWARD || - direction == ResultSet.FETCH_REVERSE || - direction == ResultSet.FETCH_UNKNOWN) - { + if (direction == ResultSet.FETCH_FORWARD || direction == ResultSet.FETCH_REVERSE + || direction == ResultSet.FETCH_UNKNOWN) { fetchDirection = direction; } else { throw new SQLException("Illegal direction: " + direction, "M1M05"); @@ -1067,10 +1005,12 @@ public class MonetStatement */ @Override public void setMaxFieldSize(int max) throws SQLException { - if (max < 0) + if (max < 0) { throw new SQLException("Illegal max value: " + max, "M1M05"); - if (max > 0) + } + if (max > 0) { addWarning("setMaxFieldSize: field size limitation not supported", "01M23"); + } } /** @@ -1083,8 +1023,9 @@ public class MonetStatement */ @Override public void setMaxRows(int max) throws SQLException { - if (max < 0) + if (max < 0) { throw new SQLException("Illegal max value: " + max, "M1M05"); + } maxRows = max; } @@ -1100,9 +1041,9 @@ public class MonetStatement */ @Override public void setQueryTimeout(int seconds) throws SQLException { - if (seconds < 0) + if (seconds < 0) { throw new SQLException("Illegal timeout value: " + seconds, "M1M05"); - + } Statement st = null; try { st = connection.createStatement(); @@ -1134,7 +1075,7 @@ public class MonetStatement /** * Requests that a Statement be pooled or not pooled. The value * specified is a hint to the statement pool implementation - * indicating whether the applicaiton wants the statement to be + * indicating whether the application wants the statement to be * pooled. It is up to the statement pool manager as to whether the * hint is used. * @@ -1155,8 +1096,7 @@ public class MonetStatement } /** - * Returns a value indicating whether the Statement is poolable or - * not. + * Returns a value indicating whether the Statement is poolable or not. * * @return true if the Statement is poolable; false otherwise */ @@ -1166,7 +1106,7 @@ public class MonetStatement } //== 1.7 methods (JDBC 4.1) - + /** * Specifies that this Statement will be closed when all its * dependent result sets are closed. If execution of the Statement @@ -1176,8 +1116,9 @@ public class MonetStatement */ @Override public void closeOnCompletion() throws SQLException { - if (closed) + if (closed) { throw new SQLException("Cannot call on closed Statement", "M1M20"); + } closeOnCompletion = true; } @@ -1191,8 +1132,9 @@ public class MonetStatement */ @Override public boolean isCloseOnCompletion() throws SQLException { - if (closed) + if (closed) { throw new SQLException("Cannot call on closed Statement", "M1M20"); + } return closeOnCompletion; } @@ -1242,37 +1184,31 @@ public class MonetStatement * TODO: try to eliminate the need for this class completely. */ final class MonetVirtualResultSet extends MonetResultSet { - private String results[][]; + private Object[] results; private boolean closed; - MonetVirtualResultSet( - Statement statement, - String[] columns, - String[] types, - String[][] results - ) throws IllegalArgumentException { - super(statement, columns, types, results.length); - + MonetVirtualResultSet(Statement statement, String[] columns, String[] types, int[] jdbcTypes, Object[] results) + throws IllegalArgumentException, IOException, SQLException { + super(statement, columns, types, jdbcTypes, results.length); this.results = results; - closed = false; + this.closed = false; } /** - * This method is overridden in order to let it use the results array - * instead of the cache in the Statement object that created it. + * This method is overridden in order to let it use the results array instead of the cache in the Statement object + * that created it. * - * @param row the number of the row to which the cursor should move. A - * positive number indicates the row number counting from the - * beginning of the result set; a negative number indicates the row - * number counting from the end of the result set + * @param row the number of the row to which the cursor should move. A positive number indicates the row number + * counting from the beginning of the result set; a negative number indicates the row number counting from the end + * of the result set * @return true if the cursor is on the result set; false otherwise * @throws SQLException if a database error occurs */ @Override public boolean absolute(int row) throws SQLException { - if (closed) + if (closed) { throw new SQLException("ResultSet is closed!", "M1M20"); - + } // first calculate what the JDBC row is if (row < 0) { // calculate the negatives... @@ -1283,22 +1219,25 @@ final class MonetVirtualResultSet extend else if (row > tupleCount + 1) row = tupleCount + 1; // after last // store it - curRow = row; + this.curRow = row; // see if we have the row - if (row < 1 || row > tupleCount) return false; + return !(row < 1 || row > tupleCount); + } - for (int i = 0; i < results[row - 1].length; i++) { - tlp.values[i] = results[row - 1][i]; - } + @Override + public int getInt(int column) throws SQLException { + return (int) ((long[]) results[0])[0]; + } - return true; + @Override + public long getLong(int column) throws SQLException { + return ((long[]) results[0])[0]; } /** - * Mainly here to prevent errors when the close method is called. There - * is no real need for this object to close it. We simply remove our - * resultset data. + * Mainly here to prevent errors when the close method is called. There is no real need for this object to close it. + * We simply remove our resultset data. */ @Override public void close() {
new file mode 100644 --- /dev/null +++ b/src/main/java/nl/cwi/monetdb/jdbc/MonetURL.java @@ -0,0 +1,77 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * Copyright 1997 - July 2008 CWI, August 2008 - 2017 MonetDB B.V. + */ + +package nl.cwi.monetdb.jdbc; + +import java.net.MalformedURLException; +import java.net.URL; +import java.sql.SQLData; +import java.sql.SQLException; +import java.sql.SQLInput; +import java.sql.SQLOutput; + +/** + * The URL class represents the URL datatype in MonetDB. It represents an URL, that is, a well-formed string conforming + * to RFC2396. + * + * @author Fabian Groffen + */ +public class MonetURL implements SQLData { + + public static String FromString(String newurl) throws Exception { + if (newurl == null) { + return null; + } + // if above doesn't fail (throws an Exception), it is fine + new URL(newurl); + return newurl; + } + + private String url; + + public MonetURL(String inet) throws Exception { + this.url = FromString(inet); + } + + @Override + public String getSQLTypeName() { + return "url"; + } + + @Override + public void readSQL(SQLInput stream, String typeName) throws SQLException { + if (typeName.compareTo("url") != 0) + throw new SQLException("can only use this class with 'url' type", "M1M05"); + url = stream.readString(); + } + + @Override + public void writeSQL(SQLOutput stream) throws SQLException { + stream.writeString(url); + } + + @Override + public String toString() { + return url; + } + + public URL getURL() throws SQLException { + if (url == null) + return null; + + try { + return new URL(url); + } catch (MalformedURLException mue) { + throw new SQLException("data is not a valid URL", "M0M27"); + } + } + + public void setURL(URL nurl) throws Exception { + url = nurl.toString(); + } +}
deleted file mode 100644 --- a/src/main/java/nl/cwi/monetdb/jdbc/types/INET.java +++ /dev/null @@ -1,166 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. - * - * Copyright 1997 - July 2008 CWI, August 2008 - 2017 MonetDB B.V. - */ - -package nl.cwi.monetdb.jdbc.types; - -import java.net.Inet4Address; -import java.net.InetAddress; -import java.net.UnknownHostException; -import java.sql.SQLData; -import java.sql.SQLDataException; -import java.sql.SQLException; -import java.sql.SQLInput; -import java.sql.SQLOutput; - -/** - * The INET class represents the INET datatype in MonetDB. - * It represents a IPv4 address with a certain mask applied. - * Currently, IPv6 is not supported. - * <br /> - * The input format for INET is x.x.x.x/y where x.x.x.x is an IP address - * and y is the number of bits in the netmask. If the /y part is left - * off, then the netmask is 32, and the value represents just a single - * host. On display, the /y portion is suppressed if the netmask is 32. - * <br /> - * This class allows to retrieve the value of this INET as InetAddress. - * This is probably meaningful only and only if the netmask is 32. - * The getNetmaskBits() method can be used to retrieve the subnet bits. - */ -public class INET implements SQLData { - private String inet; - - @Override - public String getSQLTypeName() { - return "inet"; - } - - @Override - public void readSQL(SQLInput stream, String typeName) throws SQLException { - if (!"inet".equals(typeName)) - throw new SQLException("can only use this class with 'inet' type", "M1M05"); - inet = stream.readString(); - } - - @Override - public void writeSQL(SQLOutput stream) throws SQLException { - stream.writeString(inet); - } - - @Override - public String toString() { - return inet; - } - - public void fromString(String newinet) throws SQLException { - if (newinet == null) { - inet = newinet; - return; - } - String tinet = newinet; - int slash = newinet.indexOf('/'); - if (slash != -1) { - int netmask; - // ok, see if it is a valid netmask - try { - netmask = Integer.parseInt(newinet.substring(slash + 1)); - } catch (NumberFormatException nfe) { - throw new SQLDataException("cannot parse netmask bits: " + - newinet.substring(slash + 1), "22M29"); - } - if (netmask <= 0 || netmask > 32) - throw new SQLDataException("netmask must be >0 and <32", "22M29"); - tinet = newinet.substring(0, slash); - } - // check dotted quad - String quads[] = tinet.split("\\."); - if (quads.length != 4) - throw new SQLDataException("expected dotted quad (xxx.xxx.xxx.xxx)", "22M29"); - for (int i = 0; i < 4; i++) { - int quadv; - try { - quadv = Integer.parseInt(quads[i]); - } catch (NumberFormatException nfe) { - throw new SQLDataException("cannot parse number: " + quads[i], "22M29"); - } - if (quadv < 0 || quadv > 255) - throw new SQLDataException("value must be between 0 and 255: " + quads[i], "22M29"); - } - // everything is fine - inet = newinet; - } - - public String getAddress() { - if (inet == null) - return null; - - // inet optionally has a /y part, if y < 32, chop it off - int slash = inet.indexOf('/'); - if (slash != -1) - return inet.substring(0, slash); - return inet; - } - - public void setAddress(String newinet) throws Exception { - if (newinet == null) { - inet = newinet; - return; - } - if (newinet.indexOf('/') != -1) - throw new Exception("IPv4 address cannot contain '/' " + - "(use fromString() instead)"); - fromString(newinet); - } - - public int getNetmaskBits() throws SQLException { - if (inet == null) - return 0; - - // if netmask is 32, it is omitted in the output - int slash = inet.indexOf('/'); - if (slash == -1) - return 32; - try { - return Integer.parseInt(inet.substring(slash + 1)); - } catch (NumberFormatException nfe) { - throw new SQLDataException("cannot parse netmask bits: " + - inet.substring(slash + 1), "22M29"); - } - } - - public void setNetmaskBits(int bits) throws Exception { - String newinet = inet; - if (newinet == null) { - newinet = "0.0.0.0/" + bits; - } else { - int slash = newinet.indexOf('/'); - if (slash != -1) { - newinet = newinet.substring(0, slash + 1) + bits; - } else { - newinet = newinet + "/" + bits; - } - } - fromString(newinet); - } - - public InetAddress getInetAddress() throws SQLException { - if (inet == null) - return null; - - try { - return InetAddress.getByName(getAddress()); - } catch (UnknownHostException uhe) { - throw new SQLDataException("could not resolve IP address", "22M29"); - } - } - - public void setInetAddress(InetAddress iaddr) throws Exception { - if (!(iaddr instanceof Inet4Address)) - throw new Exception("only IPv4 are supported currently"); - fromString(iaddr.getHostAddress()); - } -}
deleted file mode 100644 --- a/src/main/java/nl/cwi/monetdb/jdbc/types/URL.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. - * - * Copyright 1997 - July 2008 CWI, August 2008 - 2017 MonetDB B.V. - */ - -package nl.cwi.monetdb.jdbc.types; - -import java.net.MalformedURLException; -import java.sql.SQLData; -import java.sql.SQLDataException; -import java.sql.SQLException; -import java.sql.SQLInput; -import java.sql.SQLOutput; - -/** - * The URL class represents the URL datatype in MonetDB. - * It represents an URL, that is, a well-formed string conforming to RFC2396. - */ -public class URL implements SQLData { - private String url; - - @Override - public String getSQLTypeName() { - return "url"; - } - - @Override - public void readSQL(SQLInput stream, String typeName) throws SQLException { - if (!"url".equals(typeName)) - throw new SQLException("can only use this class with 'url' type", "M1M05"); - url = stream.readString(); - } - - @Override - public void writeSQL(SQLOutput stream) throws SQLException { - stream.writeString(url); - } - - @Override - public String toString() { - return url; - } - - public void fromString(String newurl) throws Exception { - if (newurl == null) { - url = newurl; - return; - } - new java.net.URL(newurl); - // if above doesn't fail (throws an Exception), it is fine - url = newurl; - } - - public java.net.URL getURL() throws SQLException { - if (url == null) - return null; - - try { - return new java.net.URL(url); - } catch (MalformedURLException mue) { - throw new SQLDataException("data is not a valid URL: " + mue.getMessage(), "22M30"); - } - } - - public void setURL(java.net.URL nurl) throws Exception { - url = nurl.toString(); - } -}
new file mode 100644 --- /dev/null +++ b/src/main/java/nl/cwi/monetdb/mcl/connection/ControlCommands.java @@ -0,0 +1,28 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * Copyright 1997 - July 2008 CWI, August 2008 - 2017 MonetDB B.V. + */ + +package nl.cwi.monetdb.mcl.connection; + +/** + * The listening of the MonetDB's control commands sent by the client during a JDBC connection. + * + * @author Fabian Groffen, Pedro Ferreira + */ +public final class ControlCommands { + + /* Please don't change the order or the values */ + + /** Send autocommit statement */ + public static final int AUTO_COMMIT = 1; + /** Set reply size for the server (for the maxrows specification) */ + public static final int REPLY_SIZE = 2; + /** Release a prepared statement data */ + public static final int RELEASE = 3; + /** Close a query */ + public static final int CLOSE = 4; +}
new file mode 100644 --- /dev/null +++ b/src/main/java/nl/cwi/monetdb/mcl/connection/IMonetDBLanguage.java @@ -0,0 +1,55 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * Copyright 1997 - July 2008 CWI, August 2008 - 2017 MonetDB B.V. + */ + +package nl.cwi.monetdb.mcl.connection; + +/** + * An interface which represents the delimiters for user queries depending on the language (SQL and MAL) and connection + * (Socket and Embedded). + * + * @author Pedro Ferreira + */ +public interface IMonetDBLanguage { + + /** + * Gets the String representation of a query delimiter represented through the index parameter. + * + * @param index The delimiter index starting from 0 + * @return The String representation of the delimiter + */ + String getQueryTemplateIndex(int index); + + /** + * Gets the String representation of a command delimiter represented through the index parameter. + * + * @param index The delimiter index starting from 0 + * @return The String representation of the delimiter + */ + String getCommandTemplateIndex(int index); + + /** + * Gets all query delimiters. + * + * @return All query delimiters + */ + String[] getQueryTemplates(); + + /** + * Gets all command delimiters. + * + * @return All command delimiters + */ + String[] getCommandTemplates(); + + /** + * Gets the String representation of the language currently used. + * + * @return The String representation of the language currently used. + */ + String getRepresentation(); +}
rename from src/main/java/nl/cwi/monetdb/mcl/MCLException.java rename to src/main/java/nl/cwi/monetdb/mcl/connection/MCLException.java --- a/src/main/java/nl/cwi/monetdb/mcl/MCLException.java +++ b/src/main/java/nl/cwi/monetdb/mcl/connection/MCLException.java @@ -6,19 +6,23 @@ * Copyright 1997 - July 2008 CWI, August 2008 - 2017 MonetDB B.V. */ -package nl.cwi.monetdb.mcl; +package nl.cwi.monetdb.mcl.connection; /** - * A general purpose Exception class for MCL related problems. This - * class should be used if no more precise Exception class exists. + * A general purpose Exception class for MCL related problems. This class should be used if no more precise Exception + * class exists. + * + * @author Fabian Groffen */ public class MCLException extends Exception { - /** - * - */ + private static final long serialVersionUID = 1L; public MCLException(String e) { super(e); } + + public MCLException(Throwable t) { + super(t); + } }
new file mode 100644 --- /dev/null +++ b/src/main/java/nl/cwi/monetdb/mcl/connection/SenderThread.java @@ -0,0 +1,147 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * Copyright 1997 - July 2008 CWI, August 2008 - 2017 MonetDB B.V. + */ + +package nl.cwi.monetdb.mcl.connection; + +import nl.cwi.monetdb.mcl.protocol.AbstractProtocol; + +import java.io.IOException; +import java.sql.SQLException; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +/** + * A thread to send a query to the server. When sending large amounts of data to a server, the output buffer of the + * underlying communication socket may overflow. In such case the sending process blocks. In order to prevent deadlock, + * it might be desirable that the driver as a whole does not block. This thread facilitates the prevention of such + * 'full block', because this separate thread only will block.<br /> This thread is designed for reuse, as thread + * creation costs are high. + * + * @author Fabian Groffen + */ +public class SenderThread extends Thread { + + /** The state WAIT represents this thread to be waiting for something to do */ + private static final int WAIT = 1; + /** The state QUERY represents this thread to be executing a query */ + private static final int QUERY = 2; + /** The state SHUTDOWN is the final state that ends this thread */ + private static final int SHUTDOWN = 3; + + private String[] templ; + private String query; + private AbstractProtocol protocol; + private String error; + private int state = SenderThread.WAIT; + private final Lock sendLock = new ReentrantLock(); + private final Condition queryAvailable = sendLock.newCondition(); + private final Condition waiting = sendLock.newCondition(); + + /** + * Constructor which immediately starts this thread and sets it into daemon mode. + * + * @param out the socket to write to + */ + public SenderThread(AbstractProtocol out) { + super("SendThread"); + this.setDaemon(true); + this.protocol = out; + this.start(); + } + + @Override + public void run() { + this.sendLock.lock(); + try { + while (true) { + while (this.state == SenderThread.WAIT) { + try { + this.queryAvailable.await(); + } catch (InterruptedException e) { + // woken up, eh? + } + } + if (this.state == SenderThread.SHUTDOWN) + break; + + // state is QUERY here + try { + this.protocol.writeNextQuery((templ[0] == null ? "" : templ[0]), query, + (templ[1] == null ? "" : templ[1])); + } catch (IOException e) { + this.error = e.getMessage(); + } + + // update our state, and notify, maybe someone is waiting for us in throwErrors + this.state = SenderThread.WAIT; + this.waiting.signal(); + } + } finally { + this.sendLock.unlock(); + } + } + + /** + * Starts sending the given query over the given socket. Beware that the thread should be finished (can be assured + * by calling throwErrors()) before this method is called! + * + * @param templ the query template + * @param query the query itself + * @throws SQLException if this SendThread is already in use + */ + public void runQuery(String[] templ, String query) throws SQLException { + this.sendLock.lock(); + try { + if (this.state != SenderThread.WAIT) { + throw new SQLException("Sender Thread already in use or shutting down!", "M0M03"); + } + this.templ = templ; + this.query = query; + // let the thread know there is some work to do + this.state = SenderThread.QUERY; + this.queryAvailable.signal(); + } finally { + this.sendLock.unlock(); + } + } + + /** + * Returns errors encountered during the sending process. + * + * @return the errors or null if none + */ + public String getErrors() { + this.sendLock.lock(); + try { + // make sure the thread is in WAIT state, not QUERY + while (this.state == SenderThread.QUERY) { + try { + this.waiting.await(); + } catch (InterruptedException e) { + // just try again + } + } + if (this.state == SenderThread.SHUTDOWN) + this.error = "SendThread is shutting down"; + } finally { + this.sendLock.unlock(); + } + return error; + } + + /** + * Requests this SendThread to stop. + */ + public void shutdown() { + sendLock.lock(); + state = SenderThread.SHUTDOWN; + sendLock.unlock(); + this.interrupt(); // break any wait conditions + } +}
new file mode 100644 --- /dev/null +++ b/src/main/java/nl/cwi/monetdb/mcl/connection/helpers/BufferReallocator.java @@ -0,0 +1,78 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * Copyright 1997 - July 2008 CWI, August 2008 - 2017 MonetDB B.V. + */ + +package nl.cwi.monetdb.mcl.connection.helpers; + +import java.nio.CharBuffer; + +/** + * An helper class to reallocate CharBuffer instance in way that won't overflow their capacity. (Adapted from the + * {@link StringBuilder} reallocation implementation). + * + * @author Pedro Ferreira + */ +public final class BufferReallocator { + + /** + * The possible MAX_ARRAY_SIZE, according to {@link AbstractStringBuilder}. + */ + private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; + + /** + * Calculates the CharBuffer's new capacity, throwing a {@link OutOfMemoryError}, if the capacity causes overflow. + * The capacity will always try to duplicate. + * + * @param buffer The buffer whose capacity will be expanded + * @return The buffer's new capacity + */ + private static int getNewCapacity(CharBuffer buffer) { + int minCapacity = buffer.capacity() << 1; + int newCapacity = (buffer.capacity() << 1) + 2; + if (newCapacity - minCapacity < 0) { + newCapacity = minCapacity; + } + + if(newCapacity <= 0 || MAX_ARRAY_SIZE - newCapacity < 0) { + if (Integer.MAX_VALUE - minCapacity < 0) { // overflow + throw new OutOfMemoryError(); + } + return (minCapacity > MAX_ARRAY_SIZE) ? minCapacity : MAX_ARRAY_SIZE; + } else { + return newCapacity; + } + } + + /** + * Reallocates the buffer by creating a new one with the new capacity and the contents of the previous one. + * + * @param buffer The buffer whose capacity will be expanded + * @return The new buffer allocated + */ + public static CharBuffer reallocateBuffer(CharBuffer buffer) { + int newCapacity = getNewCapacity(buffer); + CharBuffer newBuffer = CharBuffer.wrap(new char[newCapacity]); + buffer.flip(); + newBuffer.put(buffer.array()); + return newBuffer; + } + + /** + * Ensures that a buffer has a certain amount of capacity, creating a new one if the new capacity is larger than the + * current one in the buffer + * + * @param buffer The buffer whose capacity will be checked + * @param capacityThreshold The capacity threshold to test + * @return The original buffer or the new one allocated + */ + public static CharBuffer ensureCapacity(CharBuffer buffer, int capacityThreshold) { + if(capacityThreshold > buffer.capacity()) { + buffer = CharBuffer.wrap(new char[capacityThreshold]); + } + return buffer; + } +}
new file mode 100644 --- /dev/null +++ b/src/main/java/nl/cwi/monetdb/mcl/connection/helpers/ChannelSecurity.java @@ -0,0 +1,57 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * Copyright 1997 - July 2008 CWI, August 2008 - 2017 MonetDB B.V. + */ + +package nl.cwi.monetdb.mcl.connection.helpers; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +/** + * A helper class to process Hash digests during the authentication process. + * + * @author Fabian Groffen, Pedro Ferreira + */ +public final class ChannelSecurity { + + private static char hexChar(int n) { return (n > 9) ? (char) ('a' + (n - 10)) : (char) ('0' + n); } + + /** + * Helper method to convert a byte string to a hexadecimal String representation. + * + * @param digest The byte array to convert + * @return The byte array as a hexadecimal string + */ + private static String toHex(byte[] digest) { + char[] result = new char[digest.length * 2]; + int pos = 0; + for (byte aDigest : digest) { + result[pos++] = hexChar((aDigest & 0xf0) >> 4); + result[pos++] = hexChar(aDigest & 0x0f); + } + return new String(result); + } + + /** + * Digests several byte[] into a String digest, using a specified hash algorithm. + * + * @param algorithm The hash algorithm to use + * @param toDigests The Strings to digest + * @return The Strings digest as a hexadecimal string + */ + public static String digestStrings(String algorithm, byte[]... toDigests) { + try { + MessageDigest md = MessageDigest.getInstance(algorithm); + for (byte[] str : toDigests) { + md.update(str); + } + return toHex(md.digest()); + } catch (NoSuchAlgorithmException e) { + throw new AssertionError("internal error: " + e.toString()); + } + } +}
new file mode 100644 --- /dev/null +++ b/src/main/java/nl/cwi/monetdb/mcl/connection/helpers/GregorianCalendarParser.java @@ -0,0 +1,217 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * Copyright 1997 - July 2008 CWI, August 2008 - 2017 MonetDB B.V. + */ + +package nl.cwi.monetdb.mcl.connection.helpers; + +import nl.cwi.monetdb.jdbc.MonetResultSet; +import nl.cwi.monetdb.mcl.protocol.ProtocolException; + +import java.sql.Types; +import java.text.ParsePosition; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.TimeZone; + +/** + * A helper class to process MAPI dates, times and timestamps Strings into their Java representation. + * + * @author Fabian Groffen, Pedro Ferreira + */ +public final class GregorianCalendarParser { + + /** + * Small helper method that returns the intrinsic value of a char if it represents a digit. If a non-digit character + * is encountered an MCLParseException is thrown. + * + * @param c the char + * @param pos the position + * @return the intrinsic value of the char + * @throws ProtocolException if c is not a digit + */ + private static int getIntrinsicValue(char c, int pos) throws ProtocolException { + // note: don't use Character.isDigit() here, because we only want ISO-LATIN-1 digits + if (c >= '0' && c <= '9') { + return (int)c - (int)'0'; + } else { + throw new ProtocolException("Expected a digit", pos); + } + } + + /** The time zone information, to be re-used to avoid memory allocations */ + private static final TimeZone defaultTimeZone = TimeZone.getDefault(); + + /** + * Parses a date or time or timestamp MAPI String into a Java {@link Calendar} instance. + * + * @param mrs A MonetResultSet instance where warning can be added + * @param toParse The date or time or timestamp String to parse + * @param pos The position of the String to start the parsing + * @param parser The parser to use + * @param jdbcType The JDBC type of the column + * @return A {@link Calendar} instance of the parsed date + */ + public static Calendar parseDateString(MonetResultSet mrs, String toParse, ParsePosition pos, + SimpleDateFormat parser, int jdbcType) { + pos.setIndex(0); + Calendar res = new GregorianCalendar(); + if(jdbcType == Types.TIME || jdbcType == Types.TIMESTAMP || jdbcType == Types.DATE) { + parser.setTimeZone(TimeZone.getTimeZone("GMT" + toParse.substring(toParse.length() - 6))); + } else { + parser.setTimeZone(defaultTimeZone); + } + + Date aux = parser.parse(toParse, pos); + if (aux == null) { + // parsing failed + int epos = pos.getErrorIndex(); + if (epos == -1) { + mrs.addWarning("parsing '" + toParse + "' failed", "01M10"); + } else if (epos < toParse.length()) { + mrs.addWarning("parsing failed, found: '" + toParse.charAt(epos) + "' in: \"" + toParse + + "\" at pos: " + pos.getErrorIndex(), "01M10"); + } else { + mrs.addWarning("parsing failed, expected more data after '" + toParse + "'", "01M10"); + } + res.clear(); + } else { + res.setTime(aux); + } + + if (jdbcType != Types.DATE) { + // parse additional nanos (if any) + int pos1 = pos.getIndex(), nanos; + char[] monDate = toParse.toCharArray(); + if (pos1 < monDate.length && monDate[pos1] == '.') { + pos1++; + int ctr; + try { + nanos = getIntrinsicValue(monDate[pos1], pos1++); + for (ctr = 1; pos1 < monDate.length && monDate[pos1] >= '0' && monDate[pos1] <= '9'; ctr++) { + if (ctr < 9) { + nanos *= 10; + nanos += (getIntrinsicValue(monDate[pos1], pos1)); + } + if (ctr == 2) // we have three at this point + res.set(Calendar.MILLISECOND, nanos); + pos1++; + } + while (ctr++ < 9) + nanos *= 10; + } catch(ProtocolException e) { + mrs.addWarning(e.getMessage() + " found: '" + monDate[e.getErrorOffset()] + "' in: \"" + + toParse + "\" at pos: " + e.getErrorOffset(), "01M10"); + res.clear(); + } + } + } + return res; + } + + /** + * Parses a date MAPI String into a Java {@link Calendar} instance. + * + * @param toParse The date String to parse + * @param pos The position of the String to start the parsing + * @param parser The parser to use (date) + * @return A {@link Calendar} instance of the parsed date + * @throws ProtocolException If the String could not be parsed + */ + public static Calendar parseDate(String toParse, ParsePosition pos, SimpleDateFormat parser) + throws ProtocolException { + pos.setIndex(0); + Calendar res = new GregorianCalendar(); + Date util = parser.parse(toParse, pos); + if(util == null) { + res.clear(); + } else { + res.setTime(util); + } + return res; + } + + /** + * Parses a time or a timestamp MAPI String into a Java {@link Calendar} instance. + * + * @param toParse The time String to parse + * @param pos The position of the String to start the parsing + * @param hasTimeZone If the time String has timezone information + * @param parser The parser to use (time) + * @return A {@link Calendar} instance of the parsed time + * @throws ProtocolException If the String could not be parsed + */ + public static Calendar parseTime(String toParse, ParsePosition pos, SimpleDateFormat parser, boolean hasTimeZone) + throws ProtocolException { + pos.setIndex(0); + if(hasTimeZone) { // MonetDB/SQL99: Sign TwoDigitHours : Minutes + parser.setTimeZone(TimeZone.getTimeZone("GMT" + toParse.substring(toParse.length() - 6))); + } else { + parser.setTimeZone(defaultTimeZone); + } + + Calendar res = new GregorianCalendar(); + Date util = parser.parse(toParse, pos); + if(util == null) { + res.clear(); + } else { + res.setTime(util); + } + return res; + } + + /** + * Parses a timestamp MAPI String into a {@link TimestampHelper} instance. + * + * @param toParse The timestamp String to parse + * @param pos The position of the String to start the parsing + * @param hasTimeZone If the timestamp String has timezone information + * @param parser The parser to use (timestamp) + * @return A {@link TimestampHelper} instance of the parsed timestamp with nanos information + * @throws ProtocolException If the String could not be parsed + */ + public static TimestampHelper parseTimestamp(String toParse, ParsePosition pos, SimpleDateFormat parser, + boolean hasTimeZone) throws ProtocolException { + pos.setIndex(0); + if(hasTimeZone) { // MonetDB/SQL99: Sign TwoDigitHours : Minutes + parser.setTimeZone(TimeZone.getTimeZone("GMT" + toParse.substring(toParse.length() - 6))); + } else { + parser.setTimeZone(defaultTimeZone); + } + + GregorianCalendar res = new GregorianCalendar(); + Date util = parser.parse(toParse, pos); + if(util != null) { + res.setTime(util); + } else { + res.clear(); + } + + // parse additional nanos (if any) + int pos1 = pos.getIndex(), nanos = 0; + if (pos1 < toParse.length() && toParse.charAt(pos1) == '.') { + pos1++; + int ctr; + + nanos = getIntrinsicValue(toParse.charAt(pos1), pos1++); + for (ctr = 1; pos1 < toParse.length() && toParse.charAt(pos1) >= '0' && toParse.charAt(pos1) <= '9'; ctr++) { + if (ctr < 9) { + nanos *= 10; + nanos += (getIntrinsicValue(toParse.charAt(pos1), pos1)); + } + if (ctr == 2) { // we have three at this point + res.set(Calendar.MILLISECOND, nanos); + } + pos1++; + } + while (ctr++ < 9) + nanos *= 10; + } + return new TimestampHelper(res, nanos); + } +}
new file mode 100644 --- /dev/null +++ b/src/main/java/nl/cwi/monetdb/mcl/connection/helpers/TimestampHelper.java @@ -0,0 +1,72 @@ +package nl.cwi.monetdb.mcl.connection.helpers; + +import java.sql.Timestamp; +import java.util.Calendar; + +/** + * Due to the poor design of the old Java date and time API, when retrieving timestamps from the MAPI connection, this + * class is used to store the calendar with timezone information and the nanoseconds information to generate a + * {@link Timestamp} instance, as there is no mapping in Java Classes to store both information. + * + * @author Pedro Ferreira + */ +public class TimestampHelper { + + /** The calendar instance */ + private Calendar calendar; + + /** The nanoseconds information */ + private int nanoseconds; + + TimestampHelper(Calendar calendar, int nanoseconds) { + this.calendar = calendar; + this.nanoseconds = nanoseconds; + } + + /** + * Gets the Calendar instance. + * + * @return The Calendar instance + */ + public Calendar getCalendar() { + return calendar; + } + + /** + * Sets the Calendar instance. + * + * @param calendar The Calendar instance + */ + public void setCalendar(Calendar calendar) { + this.calendar = calendar; + } + + /** + * Gets the nanoseconds information. + * + * @return The nanoseconds information + */ + public int getNanoseconds() { + return nanoseconds; + } + + /** + * Sets the nanoseconds information. + * + * @param nanoseconds The nanoseconds information + */ + public void setNanoseconds(int nanoseconds) { + this.nanoseconds = nanoseconds; + } + + /** + * Generates a {@link Timestamp} instance from the provided information. + * + * @return The generated {@link Timestamp} instance + */ + public Timestamp getTimestamp() { + Timestamp res = new Timestamp(calendar.getTimeInMillis()); + res.setNanos(nanoseconds); + return res; + } +}
new file mode 100644 --- /dev/null +++ b/src/main/java/nl/cwi/monetdb/mcl/connection/mapi/AbstractSocket.java @@ -0,0 +1,273 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * Copyright 1997 - July 2008 CWI, August 2008 - 2017 MonetDB B.V. + */ + +package nl.cwi.monetdb.mcl.connection.mapi; + +import nl.cwi.monetdb.mcl.connection.helpers.BufferReallocator; + +import java.io.Closeable; +import java.io.IOException; +import java.net.Socket; +import java.net.SocketException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.CharBuffer; +import java.nio.charset.CharsetDecoder; +import java.nio.charset.CharsetEncoder; +import java.nio.charset.CoderResult; +import java.nio.charset.StandardCharsets; + +/** + * An abstract class to be extended by a JDBC socket connection. The base idea of this class is to allow easy + * integrations with future versions of the MAPI protocol. With new versions of the protocol, the way the data is + * fetched will be different hence this class should be sub-classed according to the protocol itself. + * <br/> + * Meanwhile the implementation of this class uses Java ByteBuffers which allows memory re-usage for more performance. + * Also MonetDB uses UTF-8 as its character encoding, hence it is required to convert into UTF-16 (JVM encoding). + * + * @author Pedro Ferreira + */ +public abstract class AbstractSocket implements Closeable { + /** The TCP Socket to mserver */ + protected final Socket socket; + /** The MAPI connection this socket belong to */ + protected final MapiConnection connection; + /** ByteBuffer to read from the underlying socket InputStream */ + private final ByteBuffer bufferIn; + /** ByteBuffer to write into the underlying socket OutputStream */ + private final ByteBuffer bufferOut; + /** The bytes read from the bufferIn decoded into UTF-16 */ + private final CharBuffer stringsDecoded; + /** The bytes to write into the bufferOut encoded into UTF-8 */ + private final CharBuffer stringsEncoded; + /** UTF-8 encoder */ + private final CharsetEncoder utf8Encoder = StandardCharsets.UTF_8.newEncoder(); + /** UTF-8 decoder */ + private final CharsetDecoder utf8Decoder = StandardCharsets.UTF_8.newDecoder(); + + AbstractSocket(String hostname, int port, MapiConnection connection) throws IOException { + this.socket = new Socket(hostname, port); + this.connection = connection; + this.bufferIn = ByteBuffer.wrap(new byte[getFullBlockSize()]); + this.bufferOut = ByteBuffer.wrap(new byte[getFullBlockSize()]); + this.stringsDecoded = CharBuffer.allocate(getFullBlockSize()); + this.stringsDecoded.flip(); + this.stringsEncoded = CharBuffer.allocate(getFullBlockSize()); + } + + /** + * Get the socket timeout in milliseconds. + * + * @return The currently in use socket timeout in milliseconds + * @throws SocketException If an error in the underlying connection happened + */ + int getSoTimeout() throws SocketException { + return socket.getSoTimeout(); + } + + /** + * Sets the socket timeout in milliseconds. + * + * @param s The socket timeout in milliseconds + * @throws SocketException If an error in the underlying connection happened + */ + void setSoTimeout(int s) throws SocketException { + socket.setSoTimeout(s); + } + + /** + * Sets the TCP no delay feature in the underlying socket. + * + * @param on A true or false value + * @throws SocketException If an error in the underlying connection happened + */ + void setTcpNoDelay(boolean on) throws SocketException { + socket.setTcpNoDelay(on); + } + + /** + * Sets the underlying socket Endianness. + * + * @param bo A ByteOrder order value either Little-endian or Big-endian + */ + void setSocketChannelEndianness(ByteOrder bo) { + this.bufferIn.order(bo); + this.bufferOut.order(bo); + } + + /** + * Gets the underlying socket full block size. + * + * @return The underlying socket full block size + */ + public abstract int getFullBlockSize(); + + /** + * Gets the underlying socket block size. + * + * @return The underlying socket block size + */ + public abstract int getBlockSize(); + + /** + * Reads from the underlying socket into the bufferIn. + * + * @return The number off bytes read + * @throws IOException If an error in the underlying connection happened + */ + abstract int readToBufferIn(ByteBuffer bufferIn) throws IOException; + + /** + * Writes from bufferOut into the underlying socket. + * + * @return The number off bytes written + * @throws IOException If an error in the underlying connection happened + */ + abstract int writeFromBufferOut(ByteBuffer bufferOut) throws IOException; + + /** + * Flushes the output. + * + * @throws IOException If an error in the underlying connection happened + */ + abstract void flush() throws IOException; + + /** + * Helper method to read and decode UTF-8 data. + * + * @throws IOException If an error in the underlying connection happened + */ + private void readToInputBuffer() throws IOException { + int read = this.readToBufferIn(this.bufferIn); + if(read == 0) { + throw new IOException("The server has reached EOF!"); + } + this.stringsDecoded.clear(); + this.utf8Decoder.reset(); + this.utf8Decoder.decode(this.bufferIn, this.stringsDecoded,true); + this.utf8Decoder.flush(this.stringsDecoded); + this.stringsDecoded.flip(); + } + + /** + * Reads a line into the input lineBuffer, reallocating it if necessary. + * + * @param lineBuffer The buffer the data will be read into + * @return The input lineBuffer + * @throws IOException If an error in the underlying connection happened + */ + public CharBuffer readLine(CharBuffer lineBuffer) throws IOException { + lineBuffer.clear(); + boolean found = false; + char[] sourceArray = this.stringsDecoded.array(); + int sourcePosition = this.stringsDecoded.position(); + int sourceLimit = this.stringsDecoded.limit(); + char[] destinationArray = lineBuffer.array(); + int destinationPosition = 0; + int destinationLimit = lineBuffer.limit(); + + while(!found) { + if(sourcePosition >= sourceLimit) { + this.stringsDecoded.position(sourcePosition); + this.readToInputBuffer(); + sourceArray = this.stringsDecoded.array(); + sourcePosition = 0; + sourceLimit = this.stringsDecoded.limit(); + } + char c = sourceArray[sourcePosition++]; + if(c == '\n') { + found = true; + } else { + if(destinationPosition + 1 >= destinationLimit) { + lineBuffer = BufferReallocator.reallocateBuffer(lineBuffer); + destinationArray = lineBuffer.array(); + destinationLimit = lineBuffer.limit(); + } + destinationArray[destinationPosition++] = c; + } + } + this.stringsDecoded.position(sourcePosition); + lineBuffer.position(destinationPosition); + lineBuffer.flip(); + return lineBuffer; + } + + /** + * Helper method to write, encode into UTF-8 and flush. + * + * @param toFlush A boolean indicating to flush the underlying stream or not + * @throws IOException If an error in the underlying connection happened + */ + private void writeToOutputBuffer(boolean toFlush) throws IOException { + this.stringsEncoded.flip(); + this.utf8Encoder.reset(); + CoderResult res; + int written = 0; + do { //to avoid overflow in the UTF-16 to UTF-8 conversion, has to do this cycle + res = this.utf8Encoder.encode(this.stringsEncoded, this.bufferOut, false); + written += this.writeFromBufferOut(this.bufferOut); + } while (res == CoderResult.OVERFLOW); + + this.utf8Encoder.encode(this.stringsEncoded, this.bufferOut, true); + this.utf8Encoder.flush(this.bufferOut); + written += this.writeFromBufferOut(this.bufferOut); + + this.stringsEncoded.clear(); + this.bufferOut.clear(); + if(written == 0) { + throw new IOException("The query could not be sent to the server!"); + } else { + if(toFlush) { + this.flush(); + } + } + } + + /** + * Writes a String line into the underlying socket. + * + * @param line The line to write in the socket + * @throws IOException If an error in the underlying connection happened + */ + private void writeNextBlock(String line) throws IOException { + int limit = line.length(); + int destinationPosition = this.stringsEncoded.position(); + int destinationCapacity = this.stringsEncoded.capacity(); + char[] destinationArray = this.stringsEncoded.array(); + + for (int i = 0; i < limit; i++) { + if (destinationPosition >= destinationCapacity) { + this.stringsEncoded.position(destinationPosition); + this.writeToOutputBuffer(false); + destinationArray = this.stringsEncoded.array(); + destinationPosition = 0; + } + destinationArray[destinationPosition++] = line.charAt(i); + } + this.stringsEncoded.position(destinationPosition); + } + + /** + * Writes a String line as well a String prefix and suffix if supplied. + * + * @param prefix The prefix to write before the line if provided + * @param line The line to write into the socket + * @param suffix The suffix to write after the line if provided + * @throws IOException If an error in the underlying connection happened + */ + public void writeNextLine(String prefix, String line, String suffix) throws IOException { + if(prefix != null) { + this.writeNextBlock(prefix); + } + this.writeNextBlock(line); + if(suffix != null) { + this.writeNextBlock(suffix); + } + this.writeToOutputBuffer(true); + } +}
new file mode 100644 --- /dev/null +++ b/src/main/java/nl/cwi/monetdb/mcl/connection/mapi/MapiConnection.java @@ -0,0 +1,529 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * Copyright 1997 - July 2008 CWI, August 2008 - 2017 MonetDB B.V. + */ + +package nl.cwi.monetdb.mcl.connection.mapi; + +import nl.cwi.monetdb.jdbc.MonetConnection; +import nl.cwi.monetdb.mcl.connection.ControlCommands; +import nl.cwi.monetdb.mcl.connection.MCLException; +import nl.cwi.monetdb.mcl.connection.helpers.ChannelSecurity; +import nl.cwi.monetdb.mcl.protocol.ProtocolException; +import nl.cwi.monetdb.mcl.protocol.ServerResponses; +import nl.cwi.monetdb.mcl.protocol.oldmapi.OldMapiProtocol; + +import java.io.IOException; +import java.net.SocketException; +import java.net.SocketTimeoutException; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.ByteOrder; +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.SQLNonTransientConnectionException; +import java.util.*; + +/** + * A {@link Connection} suitable for the MonetDB database using a MAPI connection. + * + * @author Fabian Groffen, Martin van Dinther, Pedro Ferreira + */ +public class MapiConnection extends MonetConnection { + + /** the PROMPT ASCII char sent by the server */ + static final char PROMPT_CHAR = '.'; + /** the default number of rows that are (attempted to) read at once */ + private static final int DEF_FETCHSIZE = 250; + + /** The hostname to connect to */ + private final String hostname; + /** The port to connect on the host to */ + private final int port; + /** The database to connect to */ + private String database; + /** The TCP Socket timeout in milliseconds. Default is 0 meaning the timeout is disabled (i.e., timeout of infinity) */ + private int soTimeout = 0; + /** Whether we should follow redirects */ + private boolean followRedirects = true; + /** How many redirections do we follow until we're fed up with it? */ + private int ttl = 10; + /** Protocol version of the connection */ + private int version; + /** Endianness of the server */ + private ByteOrder serverEndianness; + + public MapiConnection(Properties props, String hash, String language, boolean blobIsBinary, boolean clobIsLongChar, + String hostname, int port, String database) { + super(props, hash, MapiLanguage.getLanguageFromString(language), blobIsBinary, clobIsLongChar); + this.hostname = hostname; + this.port = port; + this.database = database; + } + + /** + * Gets the hostname of the server used on this connection. + * + * @return The hostname of the server used on this connection + */ + public String getHostname() { + return hostname; + } + + /** + * Gets the port of the server used on this connection. + * + * @return The port of the server used on this connection + */ + public int getPort() { + return port; + } + + /** + * Gets the database to connect to. If database is null, a connection is made to the default database of the server. + * This is also the default. + * + * @return The database name + */ + public String getDatabase() { + return database; + } + + /** + * Gets the SO_TIMEOUT from the underlying Socket. + * + * @return The currently in use timeout in milliseconds + */ + @Override + public int getSoTimeout() throws SocketException { + if(protocol != null) { + this.soTimeout = ((OldMapiProtocol)protocol).getSocket().getSoTimeout(); + } + return this.soTimeout; + } + + /** + * Set the SO_TIMEOUT on the underlying Socket. When for some reason the connection to the database hangs, this + * setting can be useful to break out of this indefinite wait. This option must be enabled prior to entering the + * blocking operation to have effect. + * + * @param timeout The specified timeout, in milliseconds. A timeout of zero is interpreted as an infinite timeout + */ + @Override + public void setSoTimeout(int timeout) throws SocketException { + if (timeout < 0) { + throw new IllegalArgumentException("Timeout can't be negative"); + } + if(protocol != null) { + ((OldMapiProtocol)protocol).getSocket().setSoTimeout(timeout); + } + this.soTimeout = timeout; + } + + /** + * Gets whether MCL redirections should be followed or not. If set to false, an MCLException will be thrown when a + * redirect is encountered during connect. The default behaviour is to automatically follow redirects. + * + * @return Whether to follow redirects (true) or not (false) + */ + public boolean isFollowRedirects() { + return followRedirects; + } + + /** + * Gets the number of redirects that are followed when followRedirects is true. In order to avoid going into an + * endless loop due to some evil server, or another error, a maximum number of redirects that may be followed can be + * set here. Note that to disable the following of redirects you should use setFollowRedirects. + * + * @see #isFollowRedirects() + * @return The number of redirects before an exception is thrown + */ + public int getTtl() { + return ttl; + } + + /** + * Gets the mapi protocol version used by this socket. The protocol version depends on the server being used. + * + * @return The mapi protocol version used by this socket + */ + public int getVersion() { + return version; + } + + /** + * Gets the connection server endianness. + * + * @return The connection server endianness + */ + public ByteOrder getServerEndianness() { + return serverEndianness; + } + + /** + * On a MAPI connection, the block size will be the block size of the connection. + * + * @return The block size length + */ + @Override + public int getBlockSize() { + return ((OldMapiProtocol)protocol).getSocket().getBlockSize(); + } + + /** + * On a MAPI connection the default fetch size per DataBlock is 250 rows. + * + * @return The default fetch size + */ + @Override + public int getDefFetchsize() { + return DEF_FETCHSIZE; + } + + @Override + public int initialStringBuilderSize() { + return this.getBlockSize(); + } + + /** + * Closes the underlying connection implementation. On a MAPI connection, the underlying socket is closed. + * + * @throws IOException if an I/O error occurs while closing the connection + */ + @Override + public synchronized void closeUnderlyingConnection() throws IOException { + ((OldMapiProtocol)protocol).getSocket().close(); + } + + /** + * Gets the underlying connection JDBC String URL. + * + * @return The underlying connection JDBC String URL + */ + @Override + public String getJDBCURL() { + String res = "jdbc:monetdb://" + this.hostname + ":" + this.port + "/" + this.database; + if (this.getLanguage() == MapiLanguage.LANG_MAL) + res += "?language=mal"; + return res; + } + + /** + * Sends a control command to the server. On a MAPI connection, regular MonetDB commands are sent to the server. + * + * @param commandID the command identifier according to {@link ControlCommands} listing + * @param data The integer to send according to the control command + * @throws SQLException if an IO exception or a database error occurs + */ + @Override + public void sendControlCommand(int commandID, int data) throws SQLException { + String command = null; + switch (commandID) { + case ControlCommands.AUTO_COMMIT: + command = "auto_commit " + ((data == 1) ? "1" : "0"); + break; + case ControlCommands.REPLY_SIZE: + command = "reply_size " + data; + break; + case ControlCommands.RELEASE: + command = "release " + data; + break; + case ControlCommands.CLOSE: + command = "close " + data; + } + try { + protocol.writeNextQuery(language.getCommandTemplateIndex(0), command, + language.getCommandTemplateIndex(1)); + protocol.waitUntilPrompt(); + int csrh = protocol.getCurrentServerResponse(); + if (csrh == ServerResponses.ERROR) { + String error = protocol.getRemainingStringLine(0); + throw new SQLException(error.substring(6), error.substring(0, 5)); + } + } catch (SocketTimeoutException e) { + close(); // JDBC 4.1 semantics, abort() + throw new SQLNonTransientConnectionException("connection timed out", "08M33"); + } catch (IOException e) { + throw new SQLNonTransientConnectionException(e.getMessage(), "08000"); + } + } + + /** + * Connects to the given host and port, logging in as the given user. If followRedirect is false, a + * RedirectionException is thrown when a redirect is encountered. + * + * @param user The user name to authenticate + * @param pass The user's password + * @return A List with informational (warning) messages. If this list is empty; then there are no warnings. + * @throws IOException if an I/O error occurs when creating the socket + * @throws ProtocolException if bogus data is received + * @throws MCLException if an MCL related error occurs + */ + @Override + public List<String> connect(String user, String pass) throws IOException, ProtocolException, MCLException { + // Wrap around the internal connect that needs to know if it should really make a TCP connection or not. + List<String> res = connect(this.hostname, this.port, user, pass, true); + // apply NetworkTimeout value from legacy (pre 4.1) driver so_timeout calls + this.setSoTimeout(this.getSoTimeout()); + return res; + } + + private List<String> connect(String host, int port, String user, String pass, boolean makeConnection) + throws IOException, ProtocolException, MCLException { + if (ttl-- <= 0) + throw new MCLException("Maximum number of redirects reached, aborting connection attempt. Sorry."); + + if (makeConnection) { + this.protocol = new OldMapiProtocol(new OldMapiSocket(this.hostname, this.port, this)); + //set nodelay, as it greatly speeds up small messages (like we often do) + ((OldMapiProtocol)this.protocol).getSocket().setTcpNoDelay(true); + ((OldMapiProtocol)this.protocol).getSocket().setSoTimeout(this.soTimeout); + } + + this.protocol.fetchNextResponseData(); + String nextLine = this.protocol.getRemainingStringLine(0); + this.protocol.waitUntilPrompt(); + String test = this.getChallengeResponse(nextLine, user, pass, this.language.getRepresentation(), + this.database, this.hash); + this.protocol.writeNextQuery("", test, ""); + + List<String> redirects = new ArrayList<>(); + List<String> warns = new ArrayList<>(); + String err = ""; + int next; + + do { + this.protocol.fetchNextResponseData(); + next = this.protocol.getCurrentServerResponse(); + switch (next) { + case ServerResponses.ERROR: + err += "\n" + this.protocol.getRemainingStringLine(7); + break; + case ServerResponses.INFO: + warns.add(this.protocol.getRemainingStringLine(1)); + break; + case ServerResponses.REDIRECT: + redirects.add(this.protocol.getRemainingStringLine(1)); + } + } while (next != ServerResponses.PROMPT); + + if (!err.equals("")) { + this.close(); + throw new MCLException(err.trim()); + } + if (!redirects.isEmpty()) { + if (followRedirects) { + // Ok, server wants us to go somewhere else. The list might have multiple clues on where to go. For now + // we don't support anything intelligent but trying the first one. URI should be in form of: + // "mapi:monetdb://host:port/database?arg=value&..." or "mapi:merovingian://proxy?arg=value&..." note + // that the extra arguments must be obeyed in both cases + String suri = redirects.get(0); + if (!suri.startsWith("mapi:")) + throw new MCLException("unsupported redirect: " + suri); + + URI u; + try { + u = new URI(suri.substring(5)); + } catch (URISyntaxException e) { + throw new ProtocolException(e.toString()); + } + String tmp = u.getQuery(); + if (tmp != null) { + String args[] = tmp.split("&"); + for (String arg : args) { + int pos = arg.indexOf("="); + if (pos > 0) { + tmp = arg.substring(0, pos); + switch (tmp) { + case "database": + tmp = arg.substring(pos + 1); + if (!tmp.equals(database)) { + warns.add("redirect points to different " + "database: " + tmp); + this.database = tmp; + } + break; + case "language": + tmp = arg.substring(pos + 1); + warns.add("redirect specifies use of different language: " + tmp); + this.language = MapiLanguage.getLanguageFromString(tmp); + break; + case "user": + tmp = arg.substring(pos + 1); + if (!tmp.equals(user)) + warns.add("ignoring different username '" + tmp + "' set by " + + "redirect, what are the security implications?"); + break; + case "password": + warns.add("ignoring different password set by redirect, " + + "what are the security implications?"); + break; + default: + warns.add("ignoring unknown argument '" + tmp + "' from redirect"); + break; + } + } else { + warns.add("ignoring illegal argument from redirect: " + arg); + } + } + } + + switch (u.getScheme()) { + case "monetdb": + tmp = u.getPath(); + if (tmp != null && tmp.length() != 0) { + tmp = tmp.substring(1).trim(); + if (!tmp.isEmpty() && !tmp.equals(database)) { + warns.add("redirect points to different " + "database: " + tmp); + this.database = tmp; + } + } + int p = u.getPort(); + warns.addAll(connect(u.getHost(), p == -1 ? port : p, user, pass, true)); + warns.add("Redirect by " + host + ":" + port + " to " + suri); + break; + case "merovingian": + // reuse this connection to inline connect to the right database that Merovingian proxies for us + warns.addAll(connect(host, port, user, pass, false)); + break; + default: + throw new MCLException("unsupported scheme in redirect: " + suri); + } + } else { + StringBuilder msg = new StringBuilder("The server sent a redirect for this connection:"); + for (String it : redirects) { + msg.append(" [").append(it).append("]"); + } + throw new MCLException(msg.toString()); + } + } + return warns; + } + + /** + * A little helper function that processes a challenge string, and returns a response string for the server. + * If the challenge string is null, a challengeless response is returned. + * + * @param chalstr the challenge string + * @param username the username to use + * @param password the password to use + * @param language the language to use + * @param database the database to connect to + * @param hash the hash method(s) to use, or NULL for all supported hashes + */ + private String getChallengeResponse(String chalstr, String username, String password, String language, + String database, String hash) throws ProtocolException, MCLException, + IOException { + String response; + String algo; + + // parse the challenge string, split it on ':' + String[] chaltok = chalstr.split(":"); + if (chaltok.length <= 4) + throw new ProtocolException("Server challenge string unusable! Challenge contains too few tokens: " + + chalstr); + + // challenge string to use as salt/key + String challenge = chaltok[0]; + String servert = chaltok[1]; + try { + this.version = Integer.parseInt(chaltok[2].trim()); // protocol version + } catch (NumberFormatException e) { + throw new ProtocolException("Protocol version unparseable: " + chaltok[2]); + } + + switch (chaltok[4]) { + case "BIG": + this.serverEndianness = ByteOrder.BIG_ENDIAN; + break; + case "LIT": + this.serverEndianness = ByteOrder.LITTLE_ENDIAN; + break; + default: + throw new ProtocolException("Invalid byte-order: " + chaltok[4]); + } + ((OldMapiProtocol)protocol).getSocket().setSocketChannelEndianness(this.serverEndianness); + + // handle the challenge according to the version it is + switch (this.version) { + case 9: + // proto 9 is like 8, but uses a hash instead of the plain password, the server tells us which hash in + // the challenge after the byte-order + /* NOTE: Java doesn't support RIPEMD160 :( */ + switch (chaltok[5]) { + case "SHA512": + algo = "SHA-512"; + break; + case "SHA384": + algo = "SHA-384"; + break; + case "SHA256": + algo = "SHA-256"; + /* NOTE: Java supports SHA-224 only on 8 */ + break; + case "SHA1": + algo = "SHA-1"; + break; + case "MD5": + algo = "MD5"; + break; + default: + throw new MCLException("Unsupported password hash: " + chaltok[5]); + } + + password = ChannelSecurity.digestStrings(algo, password.getBytes("UTF-8")); + + // proto 7 (finally) used the challenge and works with a password hash. The supported implementations + // come from the server challenge. We chose the best hash we can find, in the order SHA1, MD5, plain. + // Also, the byte-order is reported in the challenge string. proto 8 made this obsolete, but retained + // the byte-order report for future "binary" transports. In proto 8, the byte-order of the blocks is + // always little endian because most machines today are. + String hashes = (hash == null ? chaltok[3] : hash); + Set<String> hashesSet = new HashSet<>(Arrays.asList(hashes.toUpperCase().split("[, ]"))); + + // if we deal with merovingian, mask our credentials + if (servert.equals("merovingian") && !language.equals("control")) { + username = "merovingian"; + password = "merovingian"; + } + String pwhash; + + if (hashesSet.contains("SHA512")) { + algo = "SHA-512"; + pwhash = "{SHA512}"; + } else if (hashesSet.contains("SHA384")) { + algo = "SHA-384"; + pwhash = "{SHA384}"; + } else if (hashesSet.contains("SHA256")) { + algo = "SHA-256"; + pwhash = "{SHA256}"; + } else if (hashesSet.contains("SHA1")) { + algo = "SHA-1"; + pwhash = "{SHA1}"; + } else if (hashesSet.contains("MD5")) { + algo = "MD5"; + pwhash = "{MD5}"; + } else { + throw new MCLException("No supported password hashes in " + hashes); + } + + pwhash += ChannelSecurity.digestStrings(algo, password.getBytes("UTF-8"), + challenge.getBytes("UTF-8")); + + // generate response + response = "BIG:"; // JVM byte-order is big-endian + response += username + ":" + pwhash + ":" + language; + response += ":" + (database == null ? "" : database) + ":"; + + this.conn_props.setProperty("hash", hashes); + this.conn_props.setProperty("language", language); + this.conn_props.setProperty("database", database); + + return response; + default: + throw new MCLException("Unsupported protocol version: " + version); + } + } +}
new file mode 100644 --- /dev/null +++ b/src/main/java/nl/cwi/monetdb/mcl/connection/mapi/MapiLanguage.java @@ -0,0 +1,74 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * Copyright 1997 - July 2008 CWI, August 2008 - 2017 MonetDB B.V. + */ + +package nl.cwi.monetdb.mcl.connection.mapi; + +import nl.cwi.monetdb.mcl.connection.IMonetDBLanguage; + +/** + * The MAPI implementation of the available languages on a JDBC connection: SQL and MAL. + * + * @author Fabian Groffen, Pedro Ferreira + */ +public enum MapiLanguage implements IMonetDBLanguage { + + /** the SQL language */ + LANG_SQL(new String[]{"s", "\n;", "\n;\n"}, new String[]{"X", null, "\nX"}, "sql"), + /** the MAL language (officially *NOT* supported) */ + LANG_MAL(new String[]{null, ";\n", ";\n"}, new String[]{null, null, null}, "mal"), + /** an unknown language */ + LANG_UNKNOWN(null, null, "unknown"); + + MapiLanguage(String[] queryTemplates, String[] commandTemplates, String representation) { + this.queryTemplates = queryTemplates; + this.commandTemplates = commandTemplates; + this.representation = representation; + } + + private final String[] queryTemplates; + + private final String[] commandTemplates; + + private final String representation; + + @Override + public String getQueryTemplateIndex(int index) { + return queryTemplates[index]; + } + + @Override + public String getCommandTemplateIndex(int index) { + return commandTemplates[index]; + } + + @Override + public String[] getQueryTemplates() { + return queryTemplates; + } + + @Override + public String[] getCommandTemplates() { + return commandTemplates; + } + + @Override + public String getRepresentation() { + return representation; + } + + public static MapiLanguage getLanguageFromString(String language) { + switch (language) { + case "sql": + return LANG_SQL; + case "mal": + return LANG_MAL; + default: + return LANG_UNKNOWN; + } + } +}
new file mode 100644 --- /dev/null +++ b/src/main/java/nl/cwi/monetdb/mcl/connection/mapi/OldMapiSocket.java @@ -0,0 +1,362 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * Copyright 1997 - July 2008 CWI, August 2008 - 2017 MonetDB B.V. + */ + +package nl.cwi.monetdb.mcl.connection.mapi; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.ByteBuffer; + +/** + * A Socket for communicating with the MonetDB database in MAPI block mode. The OldMapiSocket implements the protocol + * specifics of the MAPI block mode protocol, + * + * For each line read, it is determined what type of line it is according to the MonetDB MAPI protocol version 9. This + * results in a line to be PROMPT, HEADER, RESULT, ERROR or UNKNOWN. + * + * The general use of this Socket must be seen only in the full context of a MAPI connection to a server. It has the + * same ingredients as a normal Socket, allowing for seamless plugging. + * <pre> + * Socket \ / InputStream ----> + * > o < + * MapiSocket / \ OutputStream ----> + * </pre> + * The OldMapiSocket allows to retrieve Streams for communicating. They are interfaced, so they can be chained in any + * way. While the Socket transparently deals with how data is sent over the wire, the actual data read needs to be + * interpreted, for which a Reader/Writer interface is most sufficient. + * + * @author Fabian Groffen + * @version 4.1 + */ +public class OldMapiSocket extends AbstractSocket { + + /** The full blocksize to use in the upper layer buffers */ + public final static int FULL_BLOCK = 8 * 1024; + + /** The blocksize (hardcoded in compliance with stream.mx) */ + private static final int BLOCK = FULL_BLOCK - 2; + + /** + * A short in two bytes for holding the block size in bytes. + */ + private final byte[] blklen = new byte[2]; + + /** + * The socket input stream read by blocks. + */ + private final OldMapiBlockInputStream inStream; + + /** + * The socket output stream written by blocks. + */ + private final OldMapiBlockOutputStream outStream; + + OldMapiSocket(String hostname, int port, MapiConnection connection) throws IOException { + super(hostname, port, connection); + this.inStream = new OldMapiBlockInputStream(socket.getInputStream()); + this.outStream = new OldMapiBlockOutputStream(socket.getOutputStream()); + } + + /** + * The block size to be used in the upper layer buffers + */ + @Override + public int getFullBlockSize() { + return FULL_BLOCK; + } + + /** + * The block size will be the one hardcoded on the connection. + */ + @Override + public int getBlockSize() { + return BLOCK; + } + + @Override + int readToBufferIn(ByteBuffer bufferIn) throws IOException { + return this.inStream.read(bufferIn); + } + + @Override + int writeFromBufferOut(ByteBuffer bufferOut) throws IOException { + return this.outStream.write(bufferOut); + } + + @Override + void flush() throws IOException { + this.outStream.flush(); + } + + @Override + public void close() throws IOException { + this.socket.close(); + } + + /** + * Inner class that is used to make the data on the blocked stream available as a normal stream. + */ + private class OldMapiBlockInputStream { + + private final InputStream inStream; + + private int readPos = 0; + + private int blockLen = 0; + + private final byte[] block = new byte[BLOCK + 3]; //\n.\n + + /** + * Constructs this BlockInputStream, backed by the given InputStream. A BufferedInputStream is internally used. + */ + OldMapiBlockInputStream(InputStream in) { + this.inStream = in; + } + + public int available() { + return blockLen - readPos; + } + + /** + * Small wrapper to get a blocking variant of the read() method on the BufferedInputStream. We want to benefit + * from the Buffered pre-fetching, but not dealing with half blocks. Changing this class to be able to use the + * partially received data will greatly complicate matters, while a performance improvement is debatable given + * the relatively small size of our blocks. Maybe it does speed up on slower links, then consider this method a + * quick bug fix/workaround. + * + * @return false if reading the block failed due to EOF + */ + private boolean _read(byte[] b, int len) throws IOException { + int s; + int off = 0; + + while (len > 0) { + s = inStream.read(b, off, len); + if (s == -1) { + // if we have read something before, we should have been able to read the whole, so make this fatal + if (off > 0) { + throw new IOException("Read from " + connection.getHostname() + ":" + + connection.getPort() + ": Incomplete block read from stream"); + } + return false; + } + len -= s; + off += s; + } + return true; + } + + /** + * Reads the next block on the stream into the internal buffer, or writes the prompt in the buffer. + * <p> + * The blocked stream protocol consists of first a two byte integer indicating the length of the block, then the + * block, followed by another length + block. The end of such sequence is put in the last bit of the length, and + * hence this length should be shifted to the right to obtain the real length value first. We simply fetch + * blocks here as soon as they are needed for the stream's read methods. + * <p> + * The user-flush, which is an implicit effect of the end of a block sequence, is communicated beyond the stream + * by inserting a prompt sequence on the stream after the last block. This method makes sure that a final block + * ends with a newline, if it doesn't already, in order to facilitate a Reader that is possibly chained to this + * InputStream. + * <p> + * If the stream is not positioned correctly, hell will break loose. + */ + private int readBlock() throws IOException { + // read next two bytes (short) + if (!_read(blklen, 2)) + return -1; + + // Get the short-value and store its value in blockLen. + blockLen = (short) ((blklen[0] & 0xFF) >> 1 | (blklen[1] & 0xFF) << 7); + readPos = 0; + + // sanity check to avoid bad servers make us do an ugly stack trace + if (blockLen > block.length) + throw new AssertionError("Server sent a block larger than BLOCKsize: " + + blockLen + " > " + block.length); + if (!_read(block, blockLen)) + return -1; + + // if this is the last block, make it end with a newline and prompt + if ((blklen[0] & 0x1) == 1) { + if (blockLen > 0 && block[blockLen - 1] != '\n') { + // to terminate the block in a Reader + block[blockLen++] = '\n'; + } + // insert 'fake' flush + block[blockLen++] = MapiConnection.PROMPT_CHAR; + block[blockLen++] = '\n'; + } + return blockLen; + } + + public int read() throws IOException { + if (available() == 0) { + if (readBlock() == -1) + return -1; + } + return (int) block[readPos++]; + } + + public int read(ByteBuffer b) throws IOException { + return read(b, 0, b.capacity()); + } + + public int read(ByteBuffer b, int off, int len) throws IOException { + b.clear(); + int t; + int size = 0; + while (size < len) { + t = available(); + if (t == 0) { + if (size != 0) + break; + if (readBlock() == -1) { + size = -1; + break; + } + t = available(); + } + if (len > t) { + System.arraycopy(block, readPos, b.array(), off, t); + off += t; + len -= t; + readPos += t; + size += t; + } else { + System.arraycopy(block, readPos, b.array(), off, len); + readPos += len; + size += len; + break; + } + } + if(size == -1) { //When nothing could be read, throw the exception + throw new IOException("Read from " + connection.getHostname() + ":" + + connection.getPort() + ": Incomplete block read from stream"); + } + b.position(size); + b.flip(); + return size; + } + + public long skip(long n) throws IOException { + long skip = n; + int t; + while (skip > 0) { + t = available(); + if (skip > t) { + skip -= t; + readPos += t; + readBlock(); + } else { + readPos += skip; + break; + } + } + return n; + } + } + + /** + * Inner class that is used to write data on a normal stream as a blocked stream. A call to the flush() method will + * write a "final" block to the underlying stream. Non-final blocks are written as soon as one or more bytes would + * not fit in the current block any more. This allows to write to a block to it's full size, and then flush it + * explicitly to have a final block being written to the stream. + */ + class OldMapiBlockOutputStream { + + private final OutputStream outStream; + + private int writePos = 0; + + private byte[] block = new byte[BLOCK]; + + private int blocksize = 0; + + /** + * Constructs this BlockOutputStream, backed by the given OutputStream. A BufferedOutputStream is internally + * used. + */ + OldMapiBlockOutputStream(OutputStream out) { + this.outStream = out; + } + + void flush() throws IOException { + // write the block (as final) then flush. + writeBlock(true); + outStream.flush(); + } + + /** + * writeBlock puts the data in the block on the stream. The boolean last controls whether the block is sent with + * an indicator to note it is the last block of a sequence or not. + * + * @param last whether this is the last block + * @throws IOException if writing to the stream failed + */ + void writeBlock(boolean last) throws IOException { + if (last) { + // always fits, because of BLOCK's size + blocksize = (short) writePos; + // this is the last block, so encode least significant bit in the first byte (little-endian) + blklen[0] = (byte) (blocksize << 1 & 0xFF | 1); + blklen[1] = (byte) (blocksize >> 7); + } else { + // always fits, because of BLOCK's size + blocksize = (short) BLOCK; + // another block will follow, encode least significant bit in the first byte (little-endian) + blklen[0] = (byte) (blocksize << 1 & 0xFF); + blklen[1] = (byte) (blocksize >> 7); + } + outStream.write(blklen); + // write the actual block + outStream.write(block, 0, writePos); + writePos = 0; + } + + void write(int b) throws IOException { + if (writePos == BLOCK) { + writeBlock(false); + } + block[writePos++] = (byte) b; + } + + int write(ByteBuffer b) throws IOException { + return write(b, 0, b.position()); + } + + int write(ByteBuffer b, int off, int len) throws IOException { + int t, written = 0; + while (len > 0) { + t = BLOCK - writePos; + if (len > t) { + System.arraycopy(b.array(), off, block, writePos, t); + off += t; + len -= t; + writePos += t; + written += t; + writeBlock(false); + } else { + System.arraycopy(b.array(), off, block, writePos, len); + writePos += len; + written += len; + break; + } + } + b.clear(); + return written; + } + + public void close() throws IOException { + // we don't want the flush() method to be called (default of the FilterOutputStream), so we close manually + // here + outStream.close(); + } + } +}
deleted file mode 100644 --- a/src/main/java/nl/cwi/monetdb/mcl/io/BufferedMCLReader.java +++ /dev/null @@ -1,195 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. - * - * Copyright 1997 - July 2008 CWI, August 2008 - 2017 MonetDB B.V. - */ - -package nl.cwi.monetdb.mcl.io; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.Reader; -import java.io.UnsupportedEncodingException; - -/** - * Read text from a character-input stream, buffering characters so as - * to provide a means for efficient reading of characters, arrays and - * lines. This class is based on the BufferedReader class, and provides - * extra functionality useful for MCL. - * - * The BufferedMCLReader is typically used as layer inbetween an - * InputStream and a specific interpreter of the data. - * <pre> - * / Response - * BufferedMCLReader ---o <- Tuple - * \ DataBlock - * </pre> - * Because the BufferedMCLReader provides an efficient way to access the - * data from the stream in a linewise fashion, whereby each line is - * identified as a certain type, consumers can easily decide how to - * parse each retrieved line. The line parsers from - * nl.cwi.monetdb.mcl.parser are well suited to work with the lines - * outputted by the BufferedMCLReader. - * This class is client-oriented, as it doesn't take into account the - * messages as the server receives them. - * - * @author Fabian Groffen <Fabian.Groffen> - * @see nl.cwi.monetdb.mcl.net.MapiSocket - * @see nl.cwi.monetdb.mcl.io.BufferedMCLWriter - */ -public class BufferedMCLReader extends BufferedReader { - /** The type of the last line read */ - private int lineType; - - /** "there is currently no line", or the the type is unknown is - represented by UNKNOWN */ - public final static int UNKNOWN = 0; - /** a line starting with ! indicates ERROR */ - public final static int ERROR = '!'; - /** a line starting with % indicates HEADER */ - public final static int HEADER = '%'; - /** a line starting with [ indicates RESULT */ - public final static int RESULT = '['; - /** a line which matches the pattern of prompt1 is a PROMPT */ - public final static int PROMPT = '.'; - /** a line which matches the pattern of prompt2 is a MORE */ - public final static int MORE = ','; - /** a line starting with & indicates the start of a header block */ - public final static int SOHEADER = '&'; - /** a line starting with ^ indicates REDIRECT */ - public final static int REDIRECT = '^'; - /** a line starting with # indicates INFO */ - public final static int INFO = '#'; - - /** - * Create a buffering character-input stream that uses a - * default-sized input buffer. - * - * @param in A Reader - */ - public BufferedMCLReader(Reader in) { - super(in); - } - - /** - * Create a buffering character-input stream that uses a - * default-sized input buffer, from an InputStream. - * - * @param in An InputStream - * @param enc Encoding - */ - public BufferedMCLReader(InputStream in, String enc) - throws UnsupportedEncodingException - { - super(new InputStreamReader(in, enc)); - } - - /** - * Read a line of text. A line is considered to be terminated by - * any one of a line feed ('\n'), a carriage return ('\r'), or a - * carriage return followed immediately by a linefeed. Before this - * method returns, it sets the linetype to any of the in MCL - * recognised line types. - * - * Warning: until the server properly prefixes all of its error - * messages with SQLSTATE codes, this method prefixes all errors it - * sees without sqlstate with the generic data exception code - * (22000). - * - * @return A String containing the contents of the line, not - * including any line-termination characters, or null if the - * end of the stream has been reached - * @throws IOException If an I/O error occurs - */ - @Override - public String readLine() throws IOException { - String r = super.readLine(); - setLineType(r); - if (lineType == ERROR && !r.matches("^![0-9A-Z]{5}!.+")) - r = "!22000!" + r.substring(1); - return r; - } - - /** - * Sets the linetype to the type of the string given. If the string - * is null, lineType is set to UNKNOWN. - * - * @param line the string to examine - */ - void setLineType(String line) { - lineType = UNKNOWN; - if (line == null || line.length() == 0) - return; - switch (line.charAt(0)) { - case '!': - lineType = ERROR; - break; - case '&': - lineType = SOHEADER; - break; - case '%': - lineType = HEADER; - break; - case '[': - lineType = RESULT; - break; - case '=': - lineType = RESULT; - break; - case '^': - lineType = REDIRECT; - break; - case '#': - lineType = INFO; - break; - case '.': - lineType = PROMPT; - break; - case ',': - lineType = MORE; - break; - } - } - - /** - * getLineType returns the type of the last line read. - * - * @return an integer representing the kind of line this is, one of the - * following constants: UNKNOWN, HEADER, ERROR, PROMPT, - * RESULT, REDIRECT, INFO - */ - public int getLineType() { - return lineType; - } - - /** - * Reads up till the MonetDB prompt, indicating the server is ready - * for a new command. All read data is discarded. If the last line - * read by readLine() was a prompt, this method will immediately - * return. - * - * If there are errors present in the lines that are read, then they - * are put in one string and returned <b>after</b> the prompt has - * been found. If no errors are present, null will be returned. - * - * @return a string containing error messages, or null if there aren't any - * @throws IOException if an IO exception occurs while talking to the server - * - * TODO(Wouter): should probably not have to be synchronized. + StringBuilder... - */ - final public synchronized String waitForPrompt() throws IOException { - String ret = "", tmp; - while (lineType != PROMPT) { - if ((tmp = readLine()) == null) - throw new IOException("Connection to server lost!"); - if (lineType == ERROR) - ret += "\n" + tmp.substring(1); - } - return ret == "" ? null : ret.trim(); - } - -}
deleted file mode 100644 --- a/src/main/java/nl/cwi/monetdb/mcl/io/BufferedMCLWriter.java +++ /dev/null @@ -1,101 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. - * - * Copyright 1997 - July 2008 CWI, August 2008 - 2017 MonetDB B.V. - */ - -package nl.cwi.monetdb.mcl.io; - -import java.io.BufferedWriter; -import java.io.IOException; -import java.io.OutputStream; -import java.io.OutputStreamWriter; -import java.io.UnsupportedEncodingException; -import java.io.Writer; - -/** - * Write text to a character-output stream, buffering characters so as - * to provide a means for efficient writing of single characters, - * arrays, and strings. - * - * In contrast to the BufferedWriter class, this class' newLine() - * method always writes the newline character '\n', regardless the - * platform's own notion of line separator. Apart from that there are - * no differences in the behaviour of this class, compared to its parent - * class, the BufferedWriter. A small convenience is built into this - * class for cooperation with the BufferedMCLReader, via the - * registerReader() method. It causes the reader to be reset upon each - * write performed though this class. This effectuates the MCL protocol - * flow where a write invalidates the state of the read buffers, since - * each write must be answered by the server. That also makes this - * class client-oriented when a reader is registered. - * - * @author Fabian Groffen <Fabian.Groffen> - * @see nl.cwi.monetdb.mcl.net.MapiSocket - * @see nl.cwi.monetdb.mcl.io.BufferedMCLWriter - */ -public class BufferedMCLWriter extends BufferedWriter { - private BufferedMCLReader reader; - - /** - * Create a buffered character-output stream that uses a - * default-sized output buffer. - * - * @param in A Writer - */ - public BufferedMCLWriter(Writer in) { - super(in); - } - - /** - * Create a buffered character-output stream that uses a - * default-sized output buffer, from an OutputStream. - * - * @param in An OutputStream - * @param enc Encoding - */ - public BufferedMCLWriter(OutputStream in, String enc) - throws UnsupportedEncodingException - { - super(new OutputStreamWriter(in, enc)); - } - - /** - * Registers the given reader in this writer. A registered reader - * receives a linetype reset when a line is written from this - * writer. - * - * @param r an BufferedMCLReader - */ - public void registerReader(BufferedMCLReader r) { - reader = r; - } - - /** - * Write a line separator. The line separator string is in this - * class always the single newline character '\n'. - * - * @throws IOException If an I/O error occurs - */ - @Override - public void newLine() throws IOException { - write('\n'); - } - - /** - * Write a single line, terminated with a line separator, and flush - * the stream. This is a shorthand method for a call to write() - * and flush(). - * - * @param line The line to write - * @throws IOException If an I/O error occurs - */ - public void writeLine(String line) throws IOException { - write(line); - flush(); - // reset reader state, last line isn't valid any more now - if (reader != null) reader.setLineType(null); - } -}
deleted file mode 100644 --- a/src/main/java/nl/cwi/monetdb/mcl/net/MapiSocket.java +++ /dev/null @@ -1,1130 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. - * - * Copyright 1997 - July 2008 CWI, August 2008 - 2017 MonetDB B.V. - */ - -package nl.cwi.monetdb.mcl.net; - -import java.io.BufferedInputStream; -import java.io.BufferedOutputStream; -import java.io.EOFException; -import java.io.FileWriter; -import java.io.FilterInputStream; -import java.io.FilterOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.io.PrintStream; -import java.io.PrintWriter; -import java.io.UnsupportedEncodingException; -import java.io.Writer; -import java.net.Socket; -import java.net.SocketException; -import java.net.URI; -import java.net.URISyntaxException; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -import nl.cwi.monetdb.mcl.MCLException; -import nl.cwi.monetdb.mcl.io.BufferedMCLReader; -import nl.cwi.monetdb.mcl.io.BufferedMCLWriter; -import nl.cwi.monetdb.mcl.parser.MCLParseException; - -/** - * A Socket for communicating with the MonetDB database in MAPI block - * mode. - * - * The MapiSocket implements the protocol specifics of the MAPI block - * mode protocol, and interfaces it as a socket that delivers a - * BufferedReader and a BufferedWriter. Because logging in is an - * integral part of the MAPI protocol, the MapiSocket performs the login - * procedure. Like the Socket class, various options can be set before - * calling the connect() method to influence the login process. Only - * after a successful call to connect() the BufferedReader and - * BufferedWriter can be retrieved. - * <br /> - * For each line read, it is determined what type of line it is - * according to the MonetDB MAPI protocol. This results in a line to be - * PROMPT, HEADER, RESULT, ERROR or UNKNOWN. Use the getLineType() - * method on the BufferedMCLReader to retrieve the type of the last - * line read. - * - * For debugging purposes a socket level debugging is implemented where - * each and every interaction to and from the MonetDB server is logged - * to a file on disk.<br /> - * Incoming messages are prefixed by "RX" (received by the driver), - * outgoing messages by "TX" (transmitted by the driver). Special - * decoded non-human readable messages are prefixed with "RD" and "TD" - * instead. Following this two char prefix, a timestamp follows as the - * number of milliseconds since the UNIX epoch. The rest of the line is - * a String representation of the data sent or received. - * - * The general use of this Socket must be seen only in the full context - * of a MAPI connection to a server. It has the same ingredients as a - * normal Socket, allowing for seamless plugging. - * <pre> - * Socket \ / InputStream ----> (BufferedMCL)Reader - * > o < - * MapiSocket / \ OutputStream ----> (BufferedMCL)Writer - * </pre> - * The MapiSocket allows to retrieve Streams for communicating. They - * are interfaced, so they can be chained in any way. While the Socket - * transparently deals with how data is sent over the wire, the actual - * data read needs to be interpreted, for which a Reader/Writer - * interface is most sufficient. In particular the BufferedMCL* - * implementations of those interfaces supply some extra functionality - * geared towards the format of the data. - * - * @author Fabian Groffen - * @version 4.1 - * @see nl.cwi.monetdb.mcl.io.BufferedMCLReader - * @see nl.cwi.monetdb.mcl.io.BufferedMCLWriter - */ -public final class MapiSocket { - /** The TCP Socket to mserver */ - private Socket con; - /** The TCP Socket timeout in milliseconds. Default is 0 meaning the timeout is disabled (i.e., timeout of infinity) */ - private int soTimeout = 0; - /** Stream from the Socket for reading */ - private InputStream fromMonet; - /** Stream from the Socket for writing */ - private OutputStream toMonet; - /** MCLReader on the InputStream */ - private BufferedMCLReader reader; - /** MCLWriter on the OutputStream */ - private BufferedMCLWriter writer; - /** protocol version of the connection */ - private int version; - - /** The database to connect to */ - private String database = null; - /** The language to connect with */ - private String language = "sql"; - /** The hash methods to use (null = default) */ - private String hash = null; - /** Whether we should follow redirects */ - private boolean followRedirects = true; - /** How many redirections do we follow until we're fed up with it? */ - private int ttl = 10; - /** Whether we are debugging or not */ - private boolean debug = false; - /** The Writer for the debug log-file */ - private Writer log; - - /** The blocksize (hardcoded in compliance with stream.mx) */ - public final static int BLOCK = 8 * 1024 - 2; - - /** A short in two bytes for holding the block size in bytes */ - private byte[] blklen = new byte[2]; - - /** - * Constructs a new MapiSocket. - */ - public MapiSocket() { - con = null; - } - - /** - * Sets the database to connect to. If database is null, a - * connection is made to the default database of the server. This - * is also the default. - * - * @param db the database - */ - public void setDatabase(String db) { - this.database = db; - } - - /** - * Sets the language to use for this connection. - * - * @param lang the language - */ - public void setLanguage(String lang) { - this.language = lang; - } - - /** - * Sets the hash method to use. Note that this method is intended - * for debugging purposes. Setting a hash method can yield in - * connection failures. Multiple hash methods can be given by - * separating the hashes by commas. - * DON'T USE THIS METHOD if you don't know what you're doing. - * - * @param hash the hash method to use - */ - public void setHash(String hash) { - this.hash = hash; - } - - /** - * Sets whether MCL redirections should be followed or not. If set - * to false, an MCLException will be thrown when a redirect is - * encountered during connect. The default bahaviour is to - * automatically follow redirects. - * - * @param r whether to follow redirects (true) or not (false) - */ - public void setFollowRedirects(boolean r) { - this.followRedirects = r; - } - - /** - * Sets the number of redirects that are followed when - * followRedirects is true. In order to avoid going into an endless - * loop due to some evil server, or another error, a maximum number - * of redirects that may be followed can be set here. Note that to - * disable the following of redirects you should use - * setFollowRedirects. - * - * @see #setFollowRedirects(boolean r) - * @param t the number of redirects before an exception is thrown - */ - public void setTTL(int t) { - this.ttl = t; - } - - /** - * Set the SO_TIMEOUT on the underlying Socket. When for some - * reason the connection to the database hangs, this setting can be - * useful to break out of this indefinite wait. - * This option must be enabled prior to entering the blocking - * operation to have effect. - * - * @param s The specified timeout, in milliseconds. A timeout - * of zero is interpreted as an infinite timeout. - * @throws SocketException Issue with the socket - */ - public void setSoTimeout(int s) throws SocketException { - if (s < 0) { - throw new IllegalArgumentException("timeout can't be negative"); - } - this.soTimeout = s; - // limit time to wait on blocking operations (0 = indefinite) - if (con != null) { - con.setSoTimeout(s); - } - } - - /** - * Gets the SO_TIMEOUT from the underlying Socket. - * - * @return the currently in use timeout in milliseconds - * @throws SocketException Issue with the socket - */ - public int getSoTimeout() throws SocketException { - if (con != null) { - this.soTimeout = con.getSoTimeout(); - } - return this.soTimeout; - } - - /** - * Enables/disables debug - * - * @param debug Value to set - */ - public void setDebug(boolean debug) { - this.debug = debug; - } - - /** - * Connects to the given host and port, logging in as the given - * user. If followRedirect is false, a RedirectionException is - * thrown when a redirect is encountered. - * - * @param host the hostname, or null for the loopback address - * @param port the port number - * @param user the username - * @param pass the password - * @return A List with informational (warning) messages. If this - * list is empty; then there are no warnings. - * @throws IOException if an I/O error occurs when creating the socket - * @throws MCLParseException if bogus data is received - * @throws MCLException if an MCL related error occurs - */ - public List<String> connect(String host, int port, String user, String pass) - throws IOException, MCLParseException, MCLException { - // Wrap around the internal connect that needs to know if it - // should really make a TCP connection or not. - return connect(host, port, user, pass, true); - } - - private List<String> connect(String host, int port, String user, String pass, - boolean makeConnection) - throws IOException, MCLParseException, MCLException { - if (ttl-- <= 0) - throw new MCLException("Maximum number of redirects reached, aborting connection attempt. Sorry."); - - if (makeConnection) { - con = new Socket(host, port); - con.setSoTimeout(this.soTimeout); - // set nodelay, as it greatly speeds up small messages (like we often do) - con.setTcpNoDelay(true); - - fromMonet = new BlockInputStream(con.getInputStream()); - toMonet = new BlockOutputStream(con.getOutputStream()); - try { - reader = new BufferedMCLReader(fromMonet, "UTF-8"); - writer = new BufferedMCLWriter(toMonet, "UTF-8"); - writer.registerReader(reader); - } catch (UnsupportedEncodingException e) { - throw new AssertionError(e.toString()); - } - } - - String c = reader.readLine(); - reader.waitForPrompt(); - writer.writeLine( - getChallengeResponse( - c, - user, - pass, - language, - database, - hash - ) - ); - - // read monet response till prompt - List<String> redirects = new ArrayList<String>(); - List<String> warns = new ArrayList<String>(); - String err = "", tmp; - int lineType; - do { - if ((tmp = reader.readLine()) == null) - throw new IOException("Read from " + - con.getInetAddress().getHostName() + ":" + - con.getPort() + ": End of stream reached"); - if ((lineType = reader.getLineType()) == BufferedMCLReader.ERROR) { - err += "\n" + tmp.substring(7); - } else if (lineType == BufferedMCLReader.INFO) { - warns.add(tmp.substring(1)); - } else if (lineType == BufferedMCLReader.REDIRECT) { - redirects.add(tmp.substring(1)); - } - } while (lineType != BufferedMCLReader.PROMPT); - if (err.length() > 0) { - close(); - throw new MCLException(err.trim()); - } - if (!redirects.isEmpty()) { - if (followRedirects) { - // Ok, server wants us to go somewhere else. The list - // might have multiple clues on where to go. For now we - // don't support anything intelligent but trying the - // first one. URI should be in form of: - // "mapi:monetdb://host:port/database?arg=value&..." - // or - // "mapi:merovingian://proxy?arg=value&..." - // note that the extra arguments must be obeyed in both - // cases - String suri = redirects.get(0).toString(); - if (!suri.startsWith("mapi:")) - throw new MCLException("unsupported redirect: " + suri); - - URI u; - try { - u = new URI(suri.substring(5)); - } catch (URISyntaxException e) { - throw new MCLParseException(e.toString()); - } - - tmp = u.getQuery(); - if (tmp != null) { - String args[] = tmp.split("&"); - for (int i = 0; i < args.length; i++) { - int pos = args[i].indexOf("="); - if (pos > 0) { - tmp = args[i].substring(0, pos); - if (tmp.equals("database")) { - tmp = args[i].substring(pos + 1); - if (!tmp.equals(database)) { - warns.add("redirect points to different " + - "database: " + tmp); - setDatabase(tmp); - } - } else if (tmp.equals("language")) { - tmp = args[i].substring(pos + 1); - warns.add("redirect specifies use of different language: " + tmp); - setLanguage(tmp); - } else if (tmp.equals("user")) { - tmp = args[i].substring(pos + 1); - if (!tmp.equals(user)) - warns.add("ignoring different username '" + tmp + "' set by " + - "redirect, what are the security implications?"); - } else if (tmp.equals("password")) { - warns.add("ignoring different password set by redirect, " + - "what are the security implications?"); - } else { - warns.add("ignoring unknown argument '" + tmp + "' from redirect"); - } - } else { - warns.add("ignoring illegal argument from redirect: " + args[i]); - } - } - } - - if (u.getScheme().equals("monetdb")) { - // this is a redirect to another (monetdb) server, - // which means a full reconnect - // avoid the debug log being closed - if (debug) { - debug = false; - close(); - debug = true; - } else { - close(); - } - tmp = u.getPath(); - if (tmp != null && tmp.length() != 0) { - tmp = tmp.substring(1).trim(); - if (!tmp.isEmpty() && !tmp.equals(database)) { - warns.add("redirect points to different " + "database: " + tmp); - setDatabase(tmp); - } - } - int p = u.getPort(); - warns.addAll(connect(u.getHost(), p == -1 ? port : p, user, pass, true)); - warns.add("Redirect by " + host + ":" + port + " to " + suri); - } else if (u.getScheme().equals("merovingian")) { - // reuse this connection to inline connect to the - // right database that Merovingian proxies for us - warns.addAll(connect(host, port, user, pass, false)); - } else { - throw new MCLException("unsupported scheme in redirect: " + suri); - } - } else { - StringBuilder msg = new StringBuilder("The server sent a redirect for this connection:"); - for (String it : redirects) { - msg.append(" [" + it + "]"); - } - throw new MCLException(msg.toString()); - } - } - return warns; - } - - /** - * A little helper function that processes a challenge string, and - * returns a response string for the server. If the challenge - * string is null, a challengeless response is returned. - * - * @param chalstr the challenge string - * @param username the username to use - * @param password the password to use - * @param language the language to use - * @param database the database to connect to - * @param hash the hash method(s) to use, or NULL for all supported hashes - */ - private String getChallengeResponse( - String chalstr, - String username, - String password, - String language, - String database, - String hash - ) throws MCLParseException, MCLException, IOException { - String response; - String algo; - - // parse the challenge string, split it on ':' - String[] chaltok = chalstr.split(":"); - if (chaltok.length <= 4) throw - new MCLParseException("Server challenge string unusable! Challenge contains too few tokens: " + chalstr); - - // challenge string to use as salt/key - String challenge = chaltok[0]; - String servert = chaltok[1]; - try { - version = Integer.parseInt(chaltok[2].trim()); // protocol version - } catch (NumberFormatException e) { - throw new MCLParseException("Protocol version unparseable: " + chaltok[3]); - } - - // handle the challenge according to the version it is - switch (version) { - default: - throw new MCLException("Unsupported protocol version: " + version); - case 9: - // proto 9 is like 8, but uses a hash instead of the - // plain password, the server tells us which hash in the - // challenge after the byte-order - - /* NOTE: Java doesn't support RIPEMD160 :( */ - if (chaltok[5].equals("SHA512")) { - algo = "SHA-512"; - } else if (chaltok[5].equals("SHA384")) { - algo = "SHA-384"; - } else if (chaltok[5].equals("SHA256")) { - algo = "SHA-256"; - /* NOTE: Java doesn't support SHA-224 */ - } else if (chaltok[5].equals("SHA1")) { - algo = "SHA-1"; - } else if (chaltok[5].equals("MD5")) { - algo = "MD5"; - } else { - throw new MCLException("Unsupported password hash: " + chaltok[5]); - } - - try { - MessageDigest md = MessageDigest.getInstance(algo); - md.update(password.getBytes("UTF-8")); - byte[] digest = md.digest(); - password = toHex(digest); - } catch (NoSuchAlgorithmException e) { - throw new AssertionError("internal error: " + e.toString()); - } catch (UnsupportedEncodingException e) { - throw new AssertionError("internal error: " + e.toString()); - } - - // proto 7 (finally) used the challenge and works with a - // password hash. The supported implementations come - // from the server challenge. We chose the best hash - // we can find, in the order SHA1, MD5, plain. Also, - // the byte-order is reported in the challenge string, - // which makes sense, since only blockmode is supported. - // proto 8 made this obsolete, but retained the - // byte-order report for future "binary" transports. In - // proto 8, the byte-order of the blocks is always little - // endian because most machines today are. - String hashes = (hash == null ? chaltok[3] : hash); - Set<String> hashesSet = new HashSet<String>(Arrays.asList(hashes.toUpperCase().split("[, ]"))); - - // if we deal with merovingian, mask our credentials - if (servert.equals("merovingian") && !language.equals("control")) { - username = "merovingian"; - password = "merovingian"; - } - String pwhash; - algo = null; - - if (hashesSet.contains("SHA512")) { - algo = "SHA-512"; - pwhash = "{SHA512}"; - } else if (hashesSet.contains("SHA384")) { - algo = "SHA-384"; - pwhash = "{SHA384}"; - } else if (hashesSet.contains("SHA256")) { - algo = "SHA-256"; - pwhash = "{SHA256}"; - } else if (hashesSet.contains("SHA1")) { - algo = "SHA-1"; - pwhash = "{SHA1}"; - } else if (hashesSet.contains("MD5")) { - algo = "MD5"; - pwhash = "{MD5}"; - } else { - throw new MCLException("no supported password hashes in " + hashes); - } - if (algo != null) { - try { - MessageDigest md = MessageDigest.getInstance(algo); - md.update(password.getBytes("UTF-8")); - md.update(challenge.getBytes("UTF-8")); - byte[] digest = md.digest(); - pwhash += toHex(digest); - } catch (NoSuchAlgorithmException e) { - throw new AssertionError("internal error: " + e.toString()); - } catch (UnsupportedEncodingException e) { - throw new AssertionError("internal error: " + e.toString()); - } - } - // TODO: some day when we need this, we should store - // this - if (chaltok[4].equals("BIG")) { - // byte-order of server is big-endian - } else if (chaltok[4].equals("LIT")) { - // byte-order of server is little-endian - } else { - throw new MCLParseException("Invalid byte-order: " + chaltok[5]); - } - - // generate response - response = "BIG:"; // JVM byte-order is big-endian - response += username + ":" + pwhash + ":" + language; - response += ":" + (database == null ? "" : database) + ":"; - - return response; - } - } - - private static char hexChar(int n) { - return (n > 9) - ? (char) ('a' + (n - 10)) - : (char) ('0' + n); - } - - /** - * Small helper method to convert a byte string to a hexadecimal - * string representation. - * - * @param digest the byte array to convert - * @return the byte array as hexadecimal string - */ - private static String toHex(byte[] digest) { - char[] result = new char[digest.length * 2]; - int pos = 0; - for (int i = 0; i < digest.length; i++) { - result[pos++] = hexChar((digest[i] & 0xf0) >> 4); - result[pos++] = hexChar(digest[i] & 0x0f); - } - return new String(result); - } - - /** - * Returns an InputStream that reads from this open connection on - * the MapiSocket. - * - * @return an input stream that reads from this open connection - */ - public InputStream getInputStream() { - return fromMonet; - } - - /** - * Returns an output stream for this MapiSocket. - * - * @return an output stream for writing bytes to this MapiSocket - */ - public OutputStream getOutputStream() { - return toMonet; - } - - /** - * Returns a Reader for this MapiSocket. The Reader is a - * BufferedMCLReader which does protocol interpretation of the - * BlockInputStream produced by this MapiSocket. - * - * @return a BufferedMCLReader connected to this MapiSocket - */ - public BufferedMCLReader getReader() { - return reader; - } - - /** - * Returns a Writer for this MapiSocket. The Writer is a - * BufferedMCLWriter which produces protocol compatible data blocks - * that the BlockOutputStream can properly translate into blocks. - * - * @return a BufferedMCLWriter connected to this MapiSocket - */ - public BufferedMCLWriter getWriter() { - return writer; - } - - /** - * Returns the mapi protocol version used by this socket. The - * protocol version depends on the server being used. Users of the - * MapiSocket should check this version to act appropriately. - * - * @return the mapi protocol version - */ - public int getProtocolVersion() { - return version; - } - - /** - * Enables logging to a file what is read and written from and to - * the server. Logging can be enabled at any time. However, it is - * encouraged to start debugging before actually connecting the - * socket. - * - * @param filename the name of the file to write to - * @throws IOException if the file could not be opened for writing - */ - public void debug(String filename) throws IOException { - debug(new FileWriter(filename)); - } - - /** - * Enables logging to a stream what is read and written from and to - * the server. Logging can be enabled at any time. However, it is - * encouraged to start debugging before actually connecting the - * socket. - * - * @param out to write the log to - * @throws IOException if the file could not be opened for writing - */ - public void debug(PrintStream out) throws IOException { - debug(new PrintWriter(out)); - } - - /** - * Enables logging to a stream what is read and written from and to - * the server. Logging can be enabled at any time. However, it is - * encouraged to start debugging before actually connecting the - * socket. - * - * @param out to write the log to - * @throws IOException if the file could not be opened for writing - */ - public void debug(Writer out) throws IOException { - log = out; - debug = true; - } - - /** - * Inner class that is used to write data on a normal stream as a - * blocked stream. A call to the flush() method will write a - * "final" block to the underlying stream. Non-final blocks are - * written as soon as one or more bytes would not fit in the - * current block any more. This allows to write to a block to it's - * full size, and then flush it explicitly to have a final block - * being written to the stream. - */ - class BlockOutputStream extends FilterOutputStream { - private int writePos = 0; - private byte[] block = new byte[BLOCK]; - private int blocksize = 0; - - /** - * Constructs this BlockOutputStream, backed by the given - * OutputStream. A BufferedOutputStream is internally used. - */ - public BlockOutputStream(OutputStream out) { - // always use a buffered stream, even though we know how - // much bytes to write/read, since this is just faster for - // some reason - super(new BufferedOutputStream(out)); - } - - @Override - public void flush() throws IOException { - // write the block (as final) then flush. - writeBlock(true); - out.flush(); - - // it's a bit nasty if an exception is thrown from the log, - // but ignoring it can be nasty as well, so it is decided to - // let it go so there is feedback about something going wrong - // it's a bit nasty if an exception is thrown from the log, - // but ignoring it can be nasty as well, so it is decided to - // let it go so there is feedback about something going wrong - if (debug) { - log.flush(); - } - } - - /** - * writeBlock puts the data in the block on the stream. The - * boolean last controls whether the block is sent with an - * indicator to note it is the last block of a sequence or not. - * - * @param last whether this is the last block - * @throws IOException if writing to the stream failed - */ - public void writeBlock(boolean last) throws IOException { - if (last) { - // always fits, because of BLOCK's size - blocksize = (short)writePos; - // this is the last block, so encode least - // significant bit in the first byte (little-endian) - blklen[0] = (byte)(blocksize << 1 & 0xFF | 1); - blklen[1] = (byte)(blocksize >> 7); - } else { - // always fits, because of BLOCK's size - blocksize = (short)BLOCK; - // another block will follow, encode least - // significant bit in the first byte (little-endian) - blklen[0] = (byte)(blocksize << 1 & 0xFF); - blklen[1] = (byte)(blocksize >> 7); - } - - out.write(blklen); - - // write the actual block - out.write(block, 0, writePos); - - if (debug) { - if (last) { - logTd("write final block: " + writePos + " bytes"); - } else { - logTd("write block: " + writePos + " bytes"); - } - logTx(new String(block, 0, writePos, "UTF-8")); - } - - writePos = 0; - } - - @Override - public void write(int b) throws IOException { - if (writePos == BLOCK) { - writeBlock(false); - } - block[writePos++] = (byte)b; - } - - @Override - public void write(byte[] b) throws IOException { - write(b, 0, b.length); - } - - @Override - public void write(byte[] b, int off, int len) throws IOException { - int t = 0; - while (len > 0) { - t = BLOCK - writePos; - if (len > t) { - System.arraycopy(b, off, block, writePos, t); - off += t; - len -= t; - writePos += t; - writeBlock(false); - } else { - System.arraycopy(b, off, block, writePos, len); - writePos += len; - break; - } - } - } - - @Override - public void close() throws IOException { - // we don't want the flush() method to be called (default of - // the FilterOutputStream), so we close manually here - out.close(); - } - } - - - /** - * Inner class that is used to make the data on the blocked stream - * available as a normal stream. - */ - class BlockInputStream extends FilterInputStream { - private int readPos = 0; - private int blockLen = 0; - private byte[] block = new byte[BLOCK + 3]; // \n.\n - - /** - * Constructs this BlockInputStream, backed by the given - * InputStream. A BufferedInputStream is internally used. - */ - public BlockInputStream(InputStream in) { - // always use a buffered stream, even though we know how - // much bytes to write/read, since this is just faster for - // some reason - super(new BufferedInputStream(in)); - } - - @Override - public int available() { - return blockLen - readPos; - } - - @Override - public boolean markSupported() { - return false; - } - - @Override - public void mark(int readlimit) { - throw new AssertionError("Not implemented!"); - } - - @Override - public void reset() { - throw new AssertionError("Not implemented!"); - } - - /** - * Small wrapper to get a blocking variant of the read() method - * on the BufferedInputStream. We want to benefit from the - * Buffered pre-fetching, but not dealing with half blocks. - * Changing this class to be able to use the partially received - * data will greatly complicate matters, while a performance - * improvement is debatable given the relatively small size of - * our blocks. Maybe it does speed up on slower links, then - * consider this method a quick bug fix/workaround. - * - * @return false if reading the block failed due to EOF - */ - private boolean _read(byte[] b, int len) throws IOException { - int s; - int off = 0; - - while (len > 0) { - s = in.read(b, off, len); - if (s == -1) { - // if we have read something before, we should have been - // able to read the whole, so make this fatal - if (off > 0) { - if (debug) { - logRd("the following incomplete block was received:"); - logRx(new String(b, 0, off, "UTF-8")); - } - throw new IOException("Read from " + - con.getInetAddress().getHostName() + ":" + - con.getPort() + ": Incomplete block read from stream"); - } - if (debug) - logRd("server closed the connection (EOF)"); - return false; - } - len -= s; - off += s; - } - - return true; - } - - /** - * Reads the next block on the stream into the internal buffer, - * or writes the prompt in the buffer. - * - * The blocked stream protocol consists of first a two byte - * integer indicating the length of the block, then the - * block, followed by another length + block. The end of - * such sequence is put in the last bit of the length, and - * hence this length should be shifted to the right to - * obtain the real length value first. We simply fetch - * blocks here as soon as they are needed for the stream's - * read methods. - * - * The user-flush, which is an implicit effect of the end of - * a block sequence, is communicated beyond the stream by - * inserting a prompt sequence on the stream after the last - * block. This method makes sure that a final block ends with a - * newline, if it doesn't already, in order to facilitate a - * Reader that is possibly chained to this InputStream. - * - * If the stream is not positioned correctly, hell will break - * loose. - */ - private int readBlock() throws IOException { - // read next two bytes (short) - if (!_read(blklen, 2)) - return(-1); - - // Get the short-value and store its value in blockLen. - blockLen = (short)( - (blklen[0] & 0xFF) >> 1 | - (blklen[1] & 0xFF) << 7 - ); - readPos = 0; - - if (debug) { - if ((blklen[0] & 0x1) == 1) { - logRd("read final block: " + blockLen + " bytes"); - } else { - logRd("read new block: " + blockLen + " bytes"); - } - } - - // sanity check to avoid bad servers make us do an ugly - // stack trace - if (blockLen > block.length) - throw new AssertionError("Server sent a block " + - "larger than BLOCKsize: " + - blockLen + " > " + block.length); - if (!_read(block, blockLen)) - return(-1); - - if (debug) - logRx(new String(block, 0, blockLen, "UTF-8")); - - // if this is the last block, make it end with a newline and - // prompt - if ((blklen[0] & 0x1) == 1) { - if (blockLen > 0 && block[blockLen - 1] != '\n') { - // to terminate the block in a Reader - block[blockLen++] = '\n'; - } - // insert 'fake' flush - block[blockLen++] = BufferedMCLReader.PROMPT; - block[blockLen++] = '\n'; - if (debug) - logRd("inserting prompt"); - } - - return(blockLen); - } - - @Override - public int read() throws IOException { - if (available() == 0) { - if (readBlock() == -1) - return(-1); - } - - if (debug) - logRx(new String(block, readPos, 1, "UTF-8")); - return (int)block[readPos++]; - } - - @Override - public int read(byte[] b) throws IOException { - return read(b, 0, b.length); - } - - @Override - public int read(byte[] b, int off, int len) throws IOException { - int t; - int size = 0; - while (size < len) { - t = available(); - if (t == 0) { - if (size != 0) - break; - if (readBlock() == -1) { - if (size == 0) - size = -1; - break; - } - t = available(); - } - if (len > t) { - System.arraycopy(block, readPos, b, off, t); - off += t; - len -= t; - readPos += t; - size += t; - } else { - System.arraycopy(block, readPos, b, off, len); - readPos += len; - size += len; - break; - } - } - return size; - } - - @Override - public long skip(long n) throws IOException { - long skip = n; - int t = 0; - while (skip > 0) { - t = available(); - if (skip > t) { - skip -= t; - readPos += t; - readBlock(); - } else { - readPos += skip; - break; - } - } - return n; - } - } - - /** - * Closes the streams and socket connected to the server if - * possible. If an error occurs during disconnecting it is ignored. - */ - public synchronized void close() { - if (writer != null) { - try { - writer.close(); - writer = null; - } catch (IOException e) { /* ignore it */ } - } - if (reader != null) { - try { - reader.close(); - reader = null; - } catch (IOException e) { /* ignore it */ } - } - if (toMonet != null) { - try { - toMonet.close(); - toMonet = null; - } catch (IOException e) { /* ignore it */ } - } - if (fromMonet != null) { - try { - fromMonet.close(); - fromMonet = null; - } catch (IOException e) { /* ignore it */ } - } - if (con != null) { - try { - con.close(); - con = null; - } catch (IOException e) { /* ignore it */ } - } - if (debug && log != null && log instanceof FileWriter) { - try { - log.close(); - log = null; - } catch (IOException e) { /* ignore it */ } - } - } - - /** - * Destructor called by garbage collector before destroying this - * object tries to disconnect the MonetDB connection if it has not - * been disconnected already. - */ - @Override - protected void finalize() throws Throwable { - close(); - super.finalize(); - } - - - /** - * Writes a logline tagged with a timestamp using the given string. - * Used for debugging purposes only and represents a message that is - * connected to writing to the socket. A logline might look like: - * TX 152545124: Hello MonetDB! - * - * @param message the message to log - * @throws IOException if an IO error occurs while writing to the logfile - */ - private void logTx(String message) throws IOException { - log.write("TX " + System.currentTimeMillis() + ": " + message + "\n"); - } - - /** - * Writes a logline tagged with a timestamp using the given string. - * Lines written using this log method are tagged as "added - * metadata" which is not strictly part of the data sent. - * - * @param message the message to log - * @throws IOException if an IO error occurs while writing to the logfile - */ - private void logTd(String message) throws IOException { - log.write("TD " + System.currentTimeMillis() + ": " + message + "\n"); - } - - /** - * Writes a logline tagged with a timestamp using the given string, - * and flushes afterwards. Used for debugging purposes only and - * represents a message that is connected to reading from the - * socket. The log is flushed after writing the line. A logline - * might look like: - * RX 152545124: Hi JDBC! - * - * @param message the message to log - * @throws IOException if an IO error occurs while writing to the logfile - */ - private void logRx(String message) throws IOException { - log.write("RX " + System.currentTimeMillis() + ": " + message + "\n"); - log.flush(); - } - - /** - * Writes a logline tagged with a timestamp using the given string, - * and flushes afterwards. Lines written using this log method are - * tagged as "added metadata" which is not strictly part of the data - * received. - * - * @param message the message to log - * @throws IOException if an IO error occurs while writing to the logfile - */ - private void logRd(String message) throws IOException { - log.write("RD " + System.currentTimeMillis() + ": " + message + "\n"); - log.flush(); - } -}
deleted file mode 100644 --- a/src/main/java/nl/cwi/monetdb/mcl/parser/HeaderLineParser.java +++ /dev/null @@ -1,226 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. - * - * Copyright 1997 - July 2008 CWI, August 2008 - 2017 MonetDB B.V. - */ - -package nl.cwi.monetdb.mcl.parser; - - -/** - * The HeaderLineParser is a generic MCLParser that extracts values from - * a metadata header in the MCL protocol either as string or integer - * values. - * - * @author Fabian Groffen <Fabian.Groffen> - */ -public class HeaderLineParser extends MCLParser { - private int type; - - public final static int NAME = 1; - public final static int LENGTH = 2; - public final static int TABLE = 3; - public final static int TYPE = 4; - - /** - * Constructs a HeaderLineParser which expects columncount columns. - * - * @param columncount the number of columns in the to be parsed string - */ - public HeaderLineParser(int columncount) { - super(columncount); - } - - /** - * Parses the given String source as header line. If source cannot - * be parsed, an MCLParseException is thrown. The columncount argument - * given during construction is used for allocation of the backing - * array. Parsing a header line with has more fields will therefore - * result in a crash. While this seems illogical, the caller should - * know this size, since the StartOfHeader contains this - * information. - * - * @param source a String which should be parsed - * @return the type of then parsed header line - * @throws MCLParseException if an error occurs during parsing - */ - @Override - public int parse(String source) throws MCLParseException { - char[] chrLine = source.toCharArray(); - int len = chrLine.length; - int pos = 0; - boolean foundChar = false; - boolean nameFound = false; - // find header name - for (int i = len - 1; i >= 0; i--) { - switch (chrLine[i]) { - case ' ': - case '\n': - case '\t': - case '\r': - if (!foundChar) { - len = i - 1; - } else { - pos = i + 1; - } - break; - case '#': - // found! - nameFound = true; - if (pos == 0) - pos = i + 1; - i = 0; // force the loop to terminate - break; - default: - foundChar = true; - pos = 0; - break; - } - } - if (!nameFound) - throw new MCLParseException("invalid header, no header name found", pos); - - // depending on the name of the header, we continue - switch (chrLine[pos]) { - case 'n': - if (len - pos == 4 && source.regionMatches(pos + 1, "name", 1, 3)) { - getValues(chrLine, 2, pos - 3); - type = NAME; - } - break; - case 'l': - if (len - pos == 6 && source.regionMatches(pos + 1, "length", 1, 5)) { - getIntValues(chrLine, 2, pos - 3); - type = LENGTH; - } - break; - case 't': - if (len - pos == 4 && source.regionMatches(pos + 1, "type", 1, 3)) { - getValues(chrLine, 2, pos - 3); - type = TYPE; - } else - if (len - pos == 10 && source.regionMatches(pos + 1, "table_name", 1, 9)) { - getValues(chrLine, 2, pos - 3); - type = TABLE; - } - break; - default: - throw new MCLParseException("unknown header: " + - (new String(chrLine, pos, len - pos))); - } - - // adjust colno - reset(); - - return type; - } - - /** - * Returns an array of Strings containing the values between - * ',\t' separators. - * - * As of Oct2014-SP1 release MAPI adds double quotes around names when - * the name contains a comma or a tab or a space or a # or " or \ escape character. - * See issue: https://www.monetdb.org/bugzilla/show_bug.cgi?id=3616 - * If the parsed name string part has a " as first and last character, - * we remove those added double quotes here. - * - * @param chrLine a character array holding the input data - * @param start where the relevant data starts - * @param stop where the relevant data stops - */ - final private void getValues(char[] chrLine, int start, int stop) { - int elem = 0; - boolean inString = false, escaped = false; - - for (int i = start; i < stop; i++) { - switch(chrLine[i]) { - case '\\': - escaped = !escaped; - break; - case '"': - /** - * If all strings are wrapped between two quotes, a \" can - * never exist outside a string. Thus if we believe that we - * are not within a string, we can safely assume we're about - * to enter a string if we find a quote. - * If we are in a string we should stop being in a string if - * we find a quote which is not prefixed by a \, for that - * would be an escaped quote. However, a nasty situation can - * occur where the string is like "test \\" as obvious, a - * test for a \ in front of a " doesn't hold here for all - * cases. Because "test \\\"" can exist as well, we need to - * know if a quote is prefixed by an escaping slash or not. - */ - if (!inString) { - inString = true; - } else if (!escaped) { - inString = false; - } - // reset escaped flag - escaped = false; - break; - case ',': - if (!inString && chrLine[i + 1] == '\t') { - // we found the field separator - if (chrLine[start] == '"') - start++; // skip leading double quote - if (elem < values.length) { - values[elem++] = new String(chrLine, start, i - (chrLine[i - 1] == '"' ? 1 : 0) - start); - } - i++; - start = i + 1; // reset start for the next name, skipping the field separator (a comma and tab) - } - // reset escaped flag - escaped = false; - break; - default: - escaped = false; - break; - } - } - // add the left over part (last column) - if (chrLine[start] == '"') - start++; // skip leading double quote - if (elem < values.length) - values[elem] = new String(chrLine, start, stop - (chrLine[stop - 1] == '"' ? 1 : 0) - start); - } - - /** - * Returns an array of ints containing the values between - * ',\t' separators. - * - * Feb2017 note - This integer parser doesn't have to parse negative - * numbers, because it is only used to parse column lengths - * which are always greater than 0. - * - * @param chrLine a character array holding the input data - * @param start where the relevant data starts - * @param stop where the relevant data stops - */ - final private void getIntValues(char[] chrLine, int start, int stop) throws MCLParseException { - int elem = 0; - int tmp = 0; - - for (int i = start; i < stop; i++) { - if (chrLine[i] == ',' && chrLine[i + 1] == '\t') { - intValues[elem++] = tmp; - tmp = 0; - start = i++; - } else { - // note: don't use Character.isDigit() here, because - // we only want ISO-LATIN-1 digits - if (chrLine[i] >= '0' && chrLine[i] <= '9') { - tmp *= 10; - tmp += (int)chrLine[i] - (int)'0'; - } else { - throw new MCLParseException("expected a digit in " + new String(chrLine) + " at " + i); - } - } - } - // add the left over part (last column) - intValues[elem] = tmp; - } -}
deleted file mode 100644 --- a/src/main/java/nl/cwi/monetdb/mcl/parser/MCLParser.java +++ /dev/null @@ -1,105 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. - * - * Copyright 1997 - July 2008 CWI, August 2008 - 2017 MonetDB B.V. - */ - -package nl.cwi.monetdb.mcl.parser; - - -/** - * Interface for parsers in MCL. The parser family in MCL is set up as - * a reusable object. This allows the same parser to be used again for - * the same type of work. While this is a very unnatural solution in - * the Java language, it prevents many object creations on a low level - * of the protocol. This favours performance. - * - * A typical parser has a method parse() which takes a String, and the - * methods hasNext() and next() to retrieve the values that were - * extracted by the parser. Parser specific methods may be available to - * perform common tasks. - * - * @author Fabian Groffen - */ -public abstract class MCLParser { - /** The String values found while parsing. Public, you may touch it. */ - public final String values[]; - /** The int values found while parsing. Public, you may touch it. */ - public final int intValues[]; - private int colnr; - - /** - * Creates an MCLParser targetted at a given number of field values. - * The lines parsed by an instance of this MCLParser should have - * exactly capacity field values. - * - * @param capacity the number of field values to expect - */ - protected MCLParser(int capacity) { - values = new String[capacity]; - intValues = new int[capacity]; - } - - /** - * Parse the given string, and populate the internal field array - * to allow for next() and hasNext() calls. - * - * @param source the String containing the line to parse - * @return value - * @throws MCLParseException if source cannot be (fully) parsed by - * this parser - * @see #next() - * @see #nextInt() - * @see #hasNext() - */ - abstract public int parse(String source) throws MCLParseException; - - /** - * Repositions the internal field offset to the start, such that the - * next call to next() will return the first field again. - */ - final public void reset() { - colnr = 0; - } - - /** - * Returns whether the next call to next() or nextInt() succeeds. - * - * @return true if the next call to next() or nextInt() is bound to - * succeed - * @see #next() - * @see #nextInt() - */ - final public boolean hasNext() { - return colnr < values.length; - } - - /** - * Returns the current field value, and advances the field counter - * to the next value. This method may fail with a RuntimeError if - * the current field counter is out of bounds. Call hasNext() to - * determine if the call to next() will succeed. - * - * @return the current field value - * @see #nextInt() - * @see #hasNext() - */ - final public String next() { - return values[colnr++]; - } - - /** - * Returns the current field value as integer, and advances the - * field counter to the next value. This method has the same - * characteristics as the next() method, apart from returning the - * field value as an integer. - * - * @return the current field value as integer - * @see #next() - */ - final public int nextInt() { - return intValues[colnr++]; - } -}
deleted file mode 100644 --- a/src/main/java/nl/cwi/monetdb/mcl/parser/StartOfHeaderParser.java +++ /dev/null @@ -1,145 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. - * - * Copyright 1997 - July 2008 CWI, August 2008 - 2017 MonetDB B.V. - */ - -package nl.cwi.monetdb.mcl.parser; - -import java.nio.CharBuffer; - -/** - * The StartOfHeaderParser allows easy examination of a start of header - * line. It does not fit into the general MCLParser framework because - * it uses a different interface. While the parser is very shallow, it - * requires the caller to know about the header lines that are parsed. - * All this parser does is detect the (valid) type of a soheader, and - * allow to return the fields in it as integer or string. An extra - * bonus is that it can return if another field should be present in the - * soheader. - * - * @author Fabian Groffen <Fabian.Groffen> - */ -public class StartOfHeaderParser { - private CharBuffer soh = null; - private int len; - private int pos; - - /* Query types (copied from sql_query.mx) */ - - /** A parse response (not handled) */ - public final static int Q_PARSE = '0'; - /** A tabular response (typical ResultSet) */ - public final static int Q_TABLE = '1'; - /** A response to an update statement, contains number of affected - * rows and generated key-id */ - public final static int Q_UPDATE = '2'; - /** A response to a schema update */ - public final static int Q_SCHEMA = '3'; - /** A response to a transation statement (start, rollback, abort, - * commit) */ - public final static int Q_TRANS = '4'; - /** A tabular response in response to a PREPARE statement containing - * information about the wildcard values that need to be supplied */ - public final static int Q_PREPARE = '5'; - /** A tabular continuation response (for a ResultSet) */ - public final static int Q_BLOCK = '6'; - /** An unknown and unsupported response */ - public final static int Q_UNKNOWN = 0 ; - - public final int parse(String in) throws MCLParseException { - soh = CharBuffer.wrap(in); - soh.get(); // skip the & - int type = soh.get(); - switch (type) { - default: - throw new MCLParseException("invalid or unknown header", 1); - case Q_PARSE: - case Q_SCHEMA: - len = 0; - break; - case Q_TABLE: - case Q_PREPARE: - len = 4; - soh.get(); - break; - case Q_UPDATE: - len = 2; - soh.get(); - break; - case Q_TRANS: - len = 1; - soh.get(); - break; - case Q_BLOCK: - len = 3; - soh.get(); - break; - } - pos = 0; - return type; - } - - public final boolean hasNext() { - return pos < len; - } - - /** - * Returns the next token in the CharBuffer as integer. The value is - * considered to end at the end of the CharBuffer or at a space. If - * a non-numeric character is encountered an MCLParseException is - * thrown. - * - * @return The next token in the CharBuffer as integer - * @throws MCLParseException if no numeric value could be read - */ - public final int getNextAsInt() throws MCLParseException { - boolean positive = true; - pos++; - if (!soh.hasRemaining()) throw - new MCLParseException("unexpected end of string", soh.position() - 1); - int tmp = 0; - char chr = soh.get(); - // note: don't use Character.isDigit() here, because - // we only want ISO-LATIN-1 digits - if(chr == '-') { - positive = false; - if (!soh.hasRemaining()) - throw new MCLParseException("unexpected end of string", soh.position() - 1); - chr = soh.get(); - } - if (chr >= '0' && chr <= '9') { - tmp = (int)chr - (int)'0'; - } else { - throw new MCLParseException("expected a digit", soh.position() - 1); - } - - while (soh.hasRemaining() && (chr = soh.get()) != ' ') { - tmp *= 10; - if (chr >= '0' && chr <= '9') { - tmp += (int)chr - (int)'0'; - } else { - throw new MCLParseException("expected a digit", soh.position() - 1); - } - } - - return positive ? tmp : -tmp; - } - - public final String getNextAsString() throws MCLParseException { - pos++; - if (!soh.hasRemaining()) throw - new MCLParseException("unexpected end of string", soh.position() - 1); - int cnt = 0; - soh.mark(); - while (soh.hasRemaining() && soh.get() != ' ') { - cnt++; - } - - soh.reset(); - - return soh.subSequence(0, cnt).toString(); - } -}
deleted file mode 100644 --- a/src/main/java/nl/cwi/monetdb/mcl/parser/TupleLineParser.java +++ /dev/null @@ -1,183 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. - * - * Copyright 1997 - July 2008 CWI, August 2008 - 2017 MonetDB B.V. - */ - -package nl.cwi.monetdb.mcl.parser; - -/** - * The TupleLineParser extracts the values from a given tuple. The - * number of values that are expected are known upfront to speed up - * allocation and validation. - * - * @author Fabian Groffen <Fabian.Groffen> - */ -public class TupleLineParser extends MCLParser { - /** - * Constructs a TupleLineParser which expects columncount columns. - * - * @param columncount the number of columns in the to be parsed string - */ - public TupleLineParser(int columncount) { - super(columncount); - } - - /** - * Parses the given String source as tuple line. If source cannot - * be parsed, a ParseException is thrown. The columncount argument - * is used for allocation of the returned array. While this seems - * illogical, the caller should know this size, since the - * StartOfHeader contains this information. - * - * @param source a String which should be parsed - * @return 0, as there is no 'type' of TupleLine - * @throws ParseException if an error occurs during parsing - */ - @Override - public int parse(String source) throws MCLParseException { - int len = source.length(); - char[] chrLine = new char[len]; - source.getChars(0, len, chrLine, 0); - - // first detect whether this is a single value line (=) or a - // real tuple ([) - if (chrLine[0] == '=') { - if (values.length != 1) - throw new MCLParseException(values.length + - " columns expected, but only single value found"); - - // return the whole string but the leading = - values[0] = source.substring(1); - - // reset colnr - reset(); - - return 0; - } - - // extract separate fields by examining string, char for char - boolean inString = false, escaped = false; - int cursor = 2, column = 0, i = 2; - StringBuilder uesc = new StringBuilder(); - for (; i < len; i++) { - switch(chrLine[i]) { - default: - escaped = false; - break; - case '\\': - escaped = !escaped; - break; - case '"': - /** - * If all strings are wrapped between two quotes, a \" can - * never exist outside a string. Thus if we believe that we - * are not within a string, we can safely assume we're about - * to enter a string if we find a quote. - * If we are in a string we should stop being in a string if - * we find a quote which is not prefixed by a \, for that - * would be an escaped quote. However, a nasty situation can - * occur where the string is like "test \\" as obvious, a - * test for a \ in front of a " doesn't hold here for all - * cases. Because "test \\\"" can exist as well, we need to - * know if a quote is prefixed by an escaping slash or not. - */ - if (!inString) { - inString = true; - } else if (!escaped) { - inString = false; - } - - // reset escaped flag - escaped = false; - break; - case '\t': - if (!inString && - (i > 0 && chrLine[i - 1] == ',') || - (i + 1 == len - 1 && chrLine[++i] == ']')) // dirty - { - // split! - if (chrLine[cursor] == '"' && - chrLine[i - 2] == '"') - { - // reuse the StringBuilder by cleaning it - uesc.delete(0, uesc.length()); - // prevent capacity increasements - uesc.ensureCapacity((i - 2) - (cursor + 1)); - for (int pos = cursor + 1; pos < i - 2; pos++) { - if (chrLine[pos] == '\\' && pos + 1 < i - 2) { - pos++; - // strToStr and strFromStr in gdk_atoms.mx only - // support \t \n \\ \" and \377 - switch (chrLine[pos]) { - case '\\': - uesc.append('\\'); - break; - case 'n': - uesc.append('\n'); - break; - case 't': - uesc.append('\t'); - break; - case '"': - uesc.append('"'); - break; - case '0': case '1': case '2': case '3': - // this could be an octal number, let's check it out - if (pos + 2 < i - 2 && - chrLine[pos + 1] >= '0' && chrLine[pos + 1] <= '7' && - chrLine[pos + 2] >= '0' && chrLine[pos + 2] <= '7' - ) { - // we got the number! - try { - uesc.append((char)(Integer.parseInt("" + chrLine[pos] + chrLine[pos + 1] + chrLine[pos + 2], 8))); - pos += 2; - } catch (NumberFormatException e) { - // hmmm, this point should never be reached actually... - throw new AssertionError("Flow error, should never try to parse non-number"); - } - } else { - // do default action if number seems not to be correct - uesc.append(chrLine[pos]); - } - break; - default: - // this is wrong, just ignore the escape, and print the char - uesc.append(chrLine[pos]); - break; - } - } else { - uesc.append(chrLine[pos]); - } - } - - // put the unescaped string in the right place - values[column++] = uesc.toString(); - } else if ((i - 1) - cursor == 4 && - source.indexOf("NULL", cursor) == cursor) - { - values[column++] = null; - } else { - values[column++] = - source.substring(cursor, i - 1); - } - cursor = i + 1; - } - - // reset escaped flag - escaped = false; - break; - } - } - // check if this result is of the size we expected it to be - if (column != values.length) - throw new MCLParseException("illegal result length: " + column + "\nlast read: " + (column > 0 ? values[column - 1] : "<none>")); - - // reset colnr - reset(); - - return 0; - } -}
new file mode 100644 --- /dev/null +++ b/src/main/java/nl/cwi/monetdb/mcl/protocol/AbstractProtocol.java @@ -0,0 +1,264 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * Copyright 1997 - July 2008 CWI, August 2008 - 2017 MonetDB B.V. + */ + +package nl.cwi.monetdb.mcl.protocol; + +import nl.cwi.monetdb.jdbc.MonetConnection; +import nl.cwi.monetdb.mcl.responses.*; + +import java.io.IOException; +import java.text.ParsePosition; +import java.text.SimpleDateFormat; +import java.util.Map; + +/** + * A generic protocol for the underlying connection, either as a socket or an embedded connection. All the server + * responses are retrieved from this class. At the same time, the user queries are submitted through this class. + * + * @author Pedro Ferreira + */ +public abstract class AbstractProtocol { + + /* only parse the date patterns once, use multiple times */ + /** Format of a date used by mserver */ + private final SimpleDateFormat monetDate = new SimpleDateFormat("yyyy-MM-dd"); + + /** Format of a time */ + private final SimpleDateFormat monetTime = new SimpleDateFormat("HH:mm:ss.SSS"); + /** Format of a time with RFC822 time zone */ + private final SimpleDateFormat monetTimeTz = new SimpleDateFormat("HH:mm:ss.SSSZ"); + /** Format to print a Time String */ + private final SimpleDateFormat monetTimePrinter = new SimpleDateFormat("HH:mm:ss"); + /** Format to print a TimeTz String */ + private final SimpleDateFormat monetTimeTzPrinter = new SimpleDateFormat("HH:mm:ssXXX"); + + /** Format of a timestamp */ + private final SimpleDateFormat monetTimestamp = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS"); + /** Format of a timestamp with RFC822 time zone */ + private final SimpleDateFormat monetTimestampTz = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSSZ"); + /** Format to print a TimeStamp String */ + private final SimpleDateFormat monetTimestampPrinter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSSSSS"); + /** Format to print a TimeStampTz String */ + private final SimpleDateFormat monetTimestampTzPrinter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSSSSSXXX"); + + /** A helper to parse Dates, Times and Timestamps, to be reused during the connection to save memory allocations. */ + private final ParsePosition monetParserPosition = new ParsePosition(0); + + /** + * Gets the MonetDB Date formatter. + * + * @return The MonetDB Date formatter + */ + public SimpleDateFormat getMonetDate() { + return monetDate; + } + + /** + * Gets the MonetDB Time formatter. + * + * @return The MonetDB Time formatter + */ + public SimpleDateFormat getMonetTime() { + return monetTime; + } + + /** + * Gets the MonetDB Time with RFC822 time zone formatter. + * + * @return The MonetDB Time with RFC822 time zone formatter + */ + public SimpleDateFormat getMonetTimeTz() { + return monetTimeTz; + } + + /** + * Gets the MonetDB Time printer. + * + * @return The MonetDB Time printer + */ + public SimpleDateFormat getMonetTimePrinter() { + return monetTimePrinter; + } + + /** + * Gets the MonetDB Time with timezone printer. + * + * @return The MonetDB Time with timezone printer. + */ + public SimpleDateFormat getMonetTimeTzPrinter() { + return monetTimeTzPrinter; + } + + /** + * Gets the MonetDB Timestamp formatter. + * + * @return The MonetDB Timestamp formatter + */ + public SimpleDateFormat getMonetTimestamp() { + return monetTimestamp; + } + + /** + * Gets the MonetDB Timestamp with RFC822 time zone formatter. + * + * @return The MonetDB Timestamp with RFC822 time zone formatter + */ + public SimpleDateFormat getMonetTimestampTz() { + return monetTimestampTz; + } + + /** + * Gets the MonetDB Timestamp printer. + * + * @return The MonetDB Timestamp printer + */ + public SimpleDateFormat getMonetTimestampPrinter() { + return monetTimestampPrinter; + } + + /** + * Gets the MonetDB Timestamp with timezone printer. + * + * @return The MonetDB Timestamp with timezone printer. + */ + public SimpleDateFormat getMonetTimestampTzPrinter() { + return monetTimestampTzPrinter; + } + + /** + * Gets the Protocol parser position. + * + * @return The Protocol parser position + */ + public ParsePosition getMonetParserPosition() { + return monetParserPosition; + } + + /** + * Waits until the server sends the PROMPT message, meaning that the next response is ready for retrieval. + * + * @throws IOException If an error in the underlying connection happened. + */ + public abstract void waitUntilPrompt() throws IOException; + + /** + * Fetches the server's next response data. + * + * @throws IOException If an error in the underlying connection happened. + */ + public abstract void fetchNextResponseData() throws IOException; + + /** + * Gets the current server response. + * + * @return The integer representation of the server response + */ + public abstract int getCurrentServerResponse(); + + /** + * Gets the next starter header of a server response. + * + * @return The integer representation of {@link StarterHeaders} + */ + public abstract int getNextStarterHeader(); + + /** + * Gets the next ResultSet response from the server, belonging to a ResponseList. + * + * @param con The current MonetDB's JDBC connection + * @param list The Response List this result set will belong to + * @param seqnr The sequence number of this result set on the Response List + * @param maxrows A maxrows to set if so + * @return The ResultSet instance + * @throws ProtocolException If an error in the underlying connection happened. + */ + public abstract ResultSetResponse getNextResultSetResponse(MonetConnection con, MonetConnection.ResponseList list, + int seqnr, int maxrows) throws ProtocolException; + + /** + * Gets the next UpdateResponse response from the server. + * + * @return An UpdateResponse instance + * @throws ProtocolException If an error in the underlying connection happened. + */ + public abstract UpdateResponse getNextUpdateResponse() throws ProtocolException; + + /** + * Gets the next SchemaResponse response from the server. + * + * @return A SchemaResponse instance + */ + public SchemaResponse getNextSchemaResponse() { + return new SchemaResponse(); + } + + /** + * Gets the next AutoCommitResponse response from the server. + * + * @return An AutoCommitResponse instance + * @throws ProtocolException If an error in the underlying connection happened. + */ + public abstract AutoCommitResponse getNextAutoCommitResponse() throws ProtocolException; + + /** + * Get an empty DataBlockResponse from the server. + * + * @param rowcount - Number of tuples + * @param columncount - Number of tuples + * @param protocol - This protocol + * @param JdbcSQLTypes - the types array + * @return An AbstractDataBlockResponse instance + */ + public abstract AbstractDataBlockResponse getAnEmptyDataBlockResponse(int rowcount, int columncount, + AbstractProtocol protocol, + int[] JdbcSQLTypes); + + /** + * Gets the next DataBlockResponse response from the server, belonging to a ResultSetResponse + * + * @param rsresponses A map of ResultSetResponse, in which this Block will belong to one of them, by checking its id + * against the keys of the Map. + * @return The DataBlockResponse instance + * @throws ProtocolException If an error in the underlying connection happened. + */ + public abstract AbstractDataBlockResponse getNextDatablockResponse(Map<Integer, ResultSetResponse> rsresponses) + throws ProtocolException; + + /** + * Gets the next Table Header for a ResultSetResponse. More than one of the parameter arrays can be filled at once. + * + * @param columnNames The column names array + * @param columnLengths The column lengths array + * @param types The columns SQL names array + * @param tableNames The columns schemas and names in format schema.table + * @return A TableResultHeaders integer representation, representing which of the fields was filled + * @throws ProtocolException If an error in the underlying connection happened. + */ + public abstract int getNextTableHeader(String[] columnNames, int[] columnLengths, String[] types, + String[] tableNames) throws ProtocolException; + + /** + * Gets the remaining response line from the underlying connection as a Java String. This method is mostly used to + * retrieve error Strings, when they are detected while parsing a response line. + * + * @param startIndex The first index in the response line to retrieve the String + * @return The String representation of the line starting at the provided index + */ + public abstract String getRemainingStringLine(int startIndex); + + /** + * Writes a user query to the server, while providing the respective prefixes and suffixes depending on the current + * language and connection used. + * + * @param prefix The prefix to append at the beginning of the query string + * @param query The user query to submit to the server + * @param suffix The suffix to append at the end of the query string + * @throws IOException If an error in the underlying connection happened. + */ + public abstract void writeNextQuery(String prefix, String query, String suffix) throws IOException; +}
rename from src/main/java/nl/cwi/monetdb/mcl/parser/MCLParseException.java rename to src/main/java/nl/cwi/monetdb/mcl/protocol/ProtocolException.java --- a/src/main/java/nl/cwi/monetdb/mcl/parser/MCLParseException.java +++ b/src/main/java/nl/cwi/monetdb/mcl/protocol/ProtocolException.java @@ -6,30 +6,27 @@ * Copyright 1997 - July 2008 CWI, August 2008 - 2017 MonetDB B.V. */ -package nl.cwi.monetdb.mcl.parser; +package nl.cwi.monetdb.mcl.protocol; import java.text.ParseException; /** - * When an MCLParseException is thrown, the MCL protocol is violated by - * the sender. In general a stream reader throws an - * MCLParseException as soon as something that is read cannot be - * understood or does not conform to the specifications (e.g. a - * missing field). The instance that throws the exception will try to - * give an error offset whenever possible. Alternatively it makes sure - * that the error message includes the offending data read. + * When an ProtocolException is thrown, the underlying protocol is violated by the sender. In general a stream reader + * throws an ProtocolException as soon as something that is read cannot be understood or does not conform to the + * specifications (e.g. a missing field). The instance that throws the exception will try to give an error offset + * whenever possible. Alternatively it makes sure that the error message includes the offending data read. + * + * @author Fabian Groffen */ -public class MCLParseException extends ParseException { - /** - * - */ +public class ProtocolException extends ParseException { + private static final long serialVersionUID = 1L; - public MCLParseException(String e) { + public ProtocolException(String e) { super(e, -1); } - public MCLParseException(String e, int offset) { + public ProtocolException(String e, int offset) { super(e, offset); } }
new file mode 100644 --- /dev/null +++ b/src/main/java/nl/cwi/monetdb/mcl/protocol/ServerResponses.java @@ -0,0 +1,40 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * Copyright 1997 - July 2008 CWI, August 2008 - 2017 MonetDB B.V. + */ + +package nl.cwi.monetdb.mcl.protocol; + +/** + * This class represents the possible stages of a query response by the server. + * + * @author Fabian Groffen, Pedro Ferreira + */ +public final class ServerResponses { + + private ServerResponses() {} + + /* Please don't change the order or the values */ + + /** "there is currently no line", or the the type is unknown is represented by UNKNOWN */ + public static final int UNKNOWN = 0; + /** a line starting with ! indicates ERROR */ + public static final int ERROR = 1; + /** a line starting with % indicates HEADER */ + public static final int HEADER = 2; + /** a line starting with [ indicates RESULT */ + public static final int RESULT = 3; + /** a line which matches the pattern of prompt1 is a PROMPT */ + public static final int PROMPT = 4; + /** a line which matches the pattern of prompt2 is a MORE */ + public static final int MORE = 5; + /** a line starting with & indicates the start of a header block */ + public static final int SOHEADER = 6; + /** a line starting with ^ indicates REDIRECT */ + public static final int REDIRECT = 7; + /** a line starting with # indicates INFO */ + public static final int INFO = 8; +}
new file mode 100644 --- /dev/null +++ b/src/main/java/nl/cwi/monetdb/mcl/protocol/StarterHeaders.java @@ -0,0 +1,40 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * Copyright 1997 - July 2008 CWI, August 2008 - 2017 MonetDB B.V. + */ + +package nl.cwi.monetdb.mcl.protocol; + +/** + * This class lists the possible responses of a query by the server. Notice that Q_PARSE is not used by neither a MAPI + * or an embedded connection, so it's here for completeness. + * + * @author Fabian Groffen, Pedro Ferreira + */ +public final class StarterHeaders { + + private StarterHeaders() {} + + /* Please don't change the order or the values */ + + /** A parse response (not handled) */ + public static final int Q_PARSE = 0; + /** A tabular response (typical ResultSet) */ + public static final int Q_TABLE = 1; + /** A response to an update statement, contains number of affected rows and generated key-id */ + public static final int Q_UPDATE = 2; + /** A response to a schema update */ + public static final int Q_SCHEMA = 3; + /** A response to a transaction statement (start, rollback, abort, commit) */ + public static final int Q_TRANS = 4; + /** A tabular response in response to a PREPARE statement containing information about the wildcard values that + * need to be supplied */ + public static final int Q_PREPARE = 5; + /** A tabular continuation response (for a ResultSet) */ + public static final int Q_BLOCK = 6; + /** An unknown and unsupported response */ + public static final int Q_UNKNOWN = 7; +}
new file mode 100644 --- /dev/null +++ b/src/main/java/nl/cwi/monetdb/mcl/protocol/TableResultHeaders.java @@ -0,0 +1,35 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * Copyright 1997 - July 2008 CWI, August 2008 - 2017 MonetDB B.V. + */ + +package nl.cwi.monetdb.mcl.protocol; + +/** + * This class lists the result table headers returned by the server. The integer values are used for the bitmap on the + * ResultSetResponse Class. + * + * @author Fabian Groffen, Pedro Ferreira + */ +public final class TableResultHeaders { + + private TableResultHeaders() {} + + /* Please don't change the order or the values */ + + /** When an unknown table header is returned on a MAPI connection */ + public static final int UNKNOWN = 0; + /** The column names */ + public static final int NAME = 1; + /** The column lengths */ + public static final int LENGTH = 2; + /** The column table and schemas names in format of schema.table */ + public static final int TABLE = 4; + /** The SQL name of the MonetDB data type of the column */ + public static final int TYPE = 8; + /** This header is returned by the JDBC embedded telling that it fetches all the previous headers at once */ + public static final int ALL = 15; +}
new file mode 100644 --- /dev/null +++ b/src/main/java/nl/cwi/monetdb/mcl/protocol/oldmapi/OldMapiDataBlockResponse.java @@ -0,0 +1,298 @@ +package nl.cwi.monetdb.mcl.protocol.oldmapi; + +import nl.cwi.monetdb.jdbc.MonetBlob; +import nl.cwi.monetdb.jdbc.MonetClob; +import nl.cwi.monetdb.mcl.connection.helpers.TimestampHelper; +import nl.cwi.monetdb.mcl.protocol.AbstractProtocol; +import nl.cwi.monetdb.mcl.protocol.ProtocolException; +import nl.cwi.monetdb.mcl.protocol.ServerResponses; +import nl.cwi.monetdb.mcl.responses.AbstractDataBlockResponse; + +import java.math.BigDecimal; +import java.sql.Date; +import java.sql.Time; +import java.sql.Timestamp; +import java.sql.Types; +import java.util.Arrays; +import java.util.Calendar; + +/** + * DataBlockResponse for an Old MAPI connection. + * + * @author Fabin Groffen, Pedro Ferreira + */ +public class OldMapiDataBlockResponse extends AbstractDataBlockResponse { + + /** The array to keep the data in */ + private Object[] data; + /** The counter which keeps the current position in the lines array */ + private int pos; + /** The last parsed nanos values for timestamps */ + private int lastNanos; + + OldMapiDataBlockResponse(int rowcount, int columncount, AbstractProtocol protocol, int[] JdbcSQLTypes) { + super(rowcount, protocol, JdbcSQLTypes); + this.pos = -1; + this.data = new Object[columncount]; + } + + @Override + public void addLines(AbstractProtocol protocol) throws ProtocolException { + int csrh = protocol.getCurrentServerResponse(); + if (csrh != ServerResponses.RESULT) { + throw new ProtocolException("protocol violation: unexpected line in data block: " + + protocol.getRemainingStringLine(0)); + } + if(this.pos == -1) { //if it's the first line, initialize the matrix + int numberOfColumns = this.data.length; + for (int i = 0 ; i < numberOfColumns ; i++) { + switch (this.jdbcSQLTypes[i]) { + case Types.INTEGER: + this.data[i] = new int[this.rowcount]; + break; + case Types.BOOLEAN: + case Types.TINYINT: + this.data[i] = new byte[this.rowcount]; + break; + case Types.SMALLINT: + this.data[i] = new short[this.rowcount]; + break; + case Types.REAL: + this.data[i] = new float[this.rowcount]; + break; + case Types.DOUBLE: + this.data[i] = new double[this.rowcount]; + break; + case Types.BIGINT: + this.data[i] = new long[this.rowcount]; + break; + case Types.DATE: + case Types.TIME: + case 2013: //Types.TIME_WITH_TIMEZONE: + this.data[i] = new Calendar[this.rowcount]; + break; + case Types.TIMESTAMP: + case 2014: //Types.TIMESTAMP_WITH_TIMEZONE: + this.data[i] = new TimestampHelper[this.rowcount]; + break; + case Types.NUMERIC: + case Types.DECIMAL: + this.data[i] = new BigDecimal[this.rowcount]; + break; + case Types.BLOB: + this.data[i] = new MonetBlob[this.rowcount]; + break; + case Types.CLOB: + this.data[i] = new MonetClob[this.rowcount]; + break; + case Types.LONGVARBINARY: + this.data[i] = new byte[this.rowcount][]; + break; + default: //CHAR, VARCHAR, OTHER + this.data[i] = new String[this.rowcount]; + } + } + } + // add to the backing array + int nextPos = this.pos + 1; + this.pos = ((OldMapiProtocol)this.protocol).parseTupleLines(nextPos, this.jdbcSQLTypes, this.data); + } + + /** + * Returns whether this Response expects more lines to be added to it. + * + * @return true if a next line should be added, false otherwise + */ + @Override + public boolean wantsMore() { + // remember: pos is the value already stored + return (this.pos + 1) < this.rowcount; + } + + /** + * Instructs the Response implementation to close and do the necessary clean up procedures. + */ + @Override + public void close() { + // feed all rows to the garbage collector + int numberOfColumns = this.data.length; + for (int i = 0; i < numberOfColumns; i++) { + data[i] = null; + } + data = null; + } + + /** + * Checks if a value in the current row is null. + * + * @param column The column index starting from 0 + * @return If the value is null or not. + */ + private boolean checkValueIsNull(int column) { + switch (this.jdbcSQLTypes[column]) { + case Types.BOOLEAN: + case Types.TINYINT: + this.lastReadWasNull = ((byte[]) this.data[column])[this.blockLine] == Byte.MIN_VALUE; + break; + case Types.SMALLINT: + this.lastReadWasNull = ((short[]) this.data[column])[this.blockLine] == Short.MIN_VALUE; + break; + case Types.INTEGER: + this.lastReadWasNull = ((int[]) this.data[column])[this.blockLine] == Integer.MIN_VALUE; + break; + case Types.BIGINT: + this.lastReadWasNull = ((long[]) this.data[column])[this.blockLine] == Long.MIN_VALUE; + break; + case Types.REAL: + this.lastReadWasNull = ((float[]) this.data[column])[this.blockLine] == Float.MIN_VALUE; + break; + case Types.DOUBLE: + this.lastReadWasNull = ((double[]) this.data[column])[this.blockLine] == Double.MIN_VALUE; + break; + default: + this.lastReadWasNull = ((Object[]) this.data[column])[this.blockLine] == null; + } + return this.lastReadWasNull; + } + + @Override + public boolean getBooleanValue(int column) { + return !this.checkValueIsNull(column) && ((byte[]) this.data[column])[this.blockLine] == 1; + } + + @Override + public byte getByteValue(int column) { + if(this.checkValueIsNull(column)) { + return 0; + } + return ((byte[]) this.data[column])[this.blockLine]; + } + + @Override + public short getShortValue(int column) { + if(this.checkValueIsNull(column)) { + return 0; + } + return ((short[]) this.data[column])[this.blockLine]; + } + + @Override + public int getIntValue(int column) { + if(this.checkValueIsNull(column)) { + return 0; + } + return ((int[]) this.data[column])[this.blockLine]; + } + + @Override + public long getLongValue(int column) { + if(this.checkValueIsNull(column)) { + return 0; + } + return ((long[]) this.data[column])[this.blockLine]; + } + + @Override + public float getFloatValue(int column) { + if(this.checkValueIsNull(column)) { + return 0.0f; + } + return ((float[]) this.data[column])[this.blockLine]; + } + + @Override + public double getDoubleValue(int column) { + if(this.checkValueIsNull(column)) { + return 0.0f; + } + return ((double[]) this.data[column])[this.blockLine]; + } + + @Override + public Object getObjectValue(int column) { + if(this.checkValueIsNull(column)) { + return null; + } + return ((Object[]) this.data[column])[this.blockLine]; + } + + @Override + public String getValueAsString(int column) { + switch (this.jdbcSQLTypes[column]) { + case Types.CHAR: + case Types.VARCHAR: + case Types.LONGVARCHAR: + case Types.OTHER: + return ((String[]) this.data[column])[this.blockLine]; + case Types.LONGVARBINARY: + return Arrays.toString(((byte[][]) this.data[column])[this.blockLine]); + case Types.BOOLEAN: + return ((byte[]) this.data[column])[this.blockLine] == 1 ? "true" : "false"; + case Types.TINYINT: + return Byte.toString(((byte[]) this.data[column])[this.blockLine]); + case Types.SMALLINT: + return Short.toString(((short[]) this.data[column])[this.blockLine]); + case Types.INTEGER: + return Integer.toString(((int[]) this.data[column])[this.blockLine]); + case Types.BIGINT: + return Long.toString(((long[]) this.data[column])[this.blockLine]); + case Types.REAL: + return Float.toString(((float[]) this.data[column])[this.blockLine]); + case Types.DOUBLE: + return Double.toString(((double[]) this.data[column])[this.blockLine]); + case Types.DATE: + Date aux1 = new Date(((Calendar[]) this.data[column])[this.blockLine].getTimeInMillis()); + return protocol.getMonetDate().format(aux1); + case Types.TIME: + Time aux2 = new Time(((Calendar[]) this.data[column])[this.blockLine].getTimeInMillis()); + return protocol.getMonetTimePrinter().format(aux2); + case 2013: //Types.TIME_WITH_TIMEZONE: + Time aux3 = new Time(((Calendar[]) this.data[column])[this.blockLine].getTimeInMillis()); + return protocol.getMonetTimeTzPrinter().format(aux3); + case Types.TIMESTAMP: + TimestampHelper thel = ((TimestampHelper[]) this.data[column])[this.blockLine]; + Timestamp aux4 = thel.getTimestamp(); + this.lastNanos = thel.getNanoseconds(); + return protocol.getMonetTimestampPrinter().format(aux4); + case 2014: //Types.TIMESTAMP_WITH_TIMEZONE: + TimestampHelper thelper = ((TimestampHelper[]) this.data[column])[this.blockLine]; + Timestamp aux5 = thelper.getTimestamp(); + this.lastNanos = thelper.getNanoseconds(); + return protocol.getMonetTimestampTzPrinter().format(aux5); + default: //BLOB, CLOB, BigDecimal + return ((Object[]) this.data[column])[this.blockLine].toString(); + } + } + + @Override + public Object getValueAsObject(int column) { + switch (this.jdbcSQLTypes[column]) { + case Types.BOOLEAN: + return ((byte[]) this.data[column])[this.blockLine] == 1; + case Types.TINYINT: + return ((byte[]) this.data[column])[this.blockLine]; + case Types.SMALLINT: + return ((short[]) this.data[column])[this.blockLine]; + case Types.INTEGER: + return ((int[]) this.data[column])[this.blockLine]; + case Types.BIGINT: + return ((long[]) this.data[column])[this.blockLine]; + case Types.REAL: + return ((float[]) this.data[column])[this.blockLine]; + case Types.DOUBLE: + return ((double[]) this.data[column])[this.blockLine]; + case Types.TIMESTAMP: + case 2014: //Types.TIMESTAMP_WITH_TIMEZONE: + TimestampHelper thelper = ((TimestampHelper[]) this.data[column])[this.blockLine]; + this.lastNanos = thelper.getNanoseconds(); + return thelper.getCalendar(); + default: + return ((Object[]) this.data[column])[this.blockLine]; + } + } + + @Override + public int getLastNanos() { + return this.lastNanos; + } +}
new file mode 100644 --- /dev/null +++ b/src/main/java/nl/cwi/monetdb/mcl/protocol/oldmapi/OldMapiProtocol.java @@ -0,0 +1,288 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * Copyright 1997 - July 2008 CWI, August 2008 - 2017 MonetDB B.V. + */ + +package nl.cwi.monetdb.mcl.protocol.oldmapi; + +import nl.cwi.monetdb.jdbc.MonetConnection; +import nl.cwi.monetdb.mcl.connection.mapi.OldMapiSocket; +import nl.cwi.monetdb.mcl.protocol.AbstractProtocol; +import nl.cwi.monetdb.mcl.protocol.ProtocolException; +import nl.cwi.monetdb.mcl.protocol.ServerResponses; +import nl.cwi.monetdb.mcl.protocol.StarterHeaders; +import nl.cwi.monetdb.mcl.responses.AbstractDataBlockResponse; +import nl.cwi.monetdb.mcl.responses.AutoCommitResponse; +import nl.cwi.monetdb.mcl.responses.ResultSetResponse; +import nl.cwi.monetdb.mcl.responses.UpdateResponse; + +import java.io.IOException; +import java.nio.CharBuffer; +import java.text.SimpleDateFormat; +import java.util.Map; + +/** + * The JDBC abstract protocol implementation on a MAPI connection using the protocol version 9. The connection holds a + * lineBuffer which will be reused during the whole connection for memory saving purposes. An additional tupleLineBuffer + * is used to help parsing tuple lines from a BLOCK response. + * + * @author Pedro Ferreira + */ +public class OldMapiProtocol extends AbstractProtocol { + + /** + * The default size for the tuple lines' CharBuffer (it should be less than the OldMapiSocket BLOCK size). + */ + private static final int TUPLE_LINE_BUFFER_DEFAULT_SIZE = 1024; + + /** Format of a time string from the old MAPI connection */ + final SimpleDateFormat timeParser = new SimpleDateFormat("HH:mm:ss"); + + /** Format of a timestamp string from the old MAPI connection */ + final SimpleDateFormat timestampParser = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + + /**The current server response */ + private int currentServerResponseHeader = ServerResponses.UNKNOWN; + + /** The underlying MAPI socket connection */ + private final OldMapiSocket socket; + + /** The buffer used to parse server's responses */ + CharBuffer lineBuffer; + + /** A helper buffer used to parse tuple line responses */ + CharBuffer tupleLineBuffer; + + public OldMapiProtocol(OldMapiSocket socket) { + this.socket = socket; + this.lineBuffer = CharBuffer.wrap(new char[OldMapiSocket.FULL_BLOCK]); + this.tupleLineBuffer = CharBuffer.wrap(new char[TUPLE_LINE_BUFFER_DEFAULT_SIZE]); + } + + /** + * Gets the underlying socket. + * + * @return The underlying socket + */ + public OldMapiSocket getSocket() { + return socket; + } + + /** + * Gets the current server response, obtained through the fetchNextResponseData method. + * + * @return The integer representation of {@link ServerResponses} + */ + @Override + public int getCurrentServerResponse() { + return currentServerResponseHeader; + } + + /** + * Reads up till the MonetDB prompt, indicating the server is ready for a new command. + * + * If there are errors present in the lines that are read, then they are put in one string and returned <b>after</b> + * the prompt has been found. + * + * @throws IOException if an IO exception occurs while talking to the server + */ + @Override + public void waitUntilPrompt() throws IOException { + while(this.currentServerResponseHeader != ServerResponses.PROMPT) { + this.lineBuffer = this.socket.readLine(this.lineBuffer); + if(this.lineBuffer.limit() == 0) { + throw new IOException("Connection to server lost!"); + } + this.currentServerResponseHeader = OldMapiServerResponseParser.parseOldMapiServerResponse(this); + this.lineBuffer.position(0); + if (this.currentServerResponseHeader == ServerResponses.ERROR) { + this.lineBuffer.position(1); + } + } + } + + /** + * Reads a line of text from the socket. A line is considered to be terminated by any one of a line feed ('\n'). + * + * Warning: until the server properly prefixes all of its error messages with SQLSTATE codes, this method prefixes + * all errors it sees without sqlstate with the generic data exception code (22000). + * + * @throws IOException If an I/O error occurs + */ + @Override + public void fetchNextResponseData() throws IOException { + this.lineBuffer = this.socket.readLine(this.lineBuffer); + if(this.lineBuffer.limit() == 0) { + throw new IOException("Connection to server lost!"); + } + this.currentServerResponseHeader = OldMapiServerResponseParser.parseOldMapiServerResponse(this); + if (this.currentServerResponseHeader == ServerResponses.ERROR && !this.lineBuffer.toString() + .matches("^[0-9A-Z]{5}!.+")) { + int limit = this.lineBuffer.limit(); + CharBuffer newbuffer = CharBuffer.wrap(new char[this.lineBuffer.capacity() + 7]); + newbuffer.put("!22000"); + newbuffer.put(this.lineBuffer.array(), 0, limit); + newbuffer.limit(limit + 6); + newbuffer.flip(); + this.lineBuffer = newbuffer; + } + this.lineBuffer.position(1); + } + + /** + * Gets the next starter header of a server response. + * + * @return The integer representation of {@link StarterHeaders} + */ + @Override + public int getNextStarterHeader() { + return OldMapiStartOfHeaderParser.getNextStartHeaderOnOldMapi(this); + } + + /** + * Gets the next ResultSet response from the server, belonging to a ResponseList. + * + * @param con The current MonetDB's JDBC connection + * @param list The Response List this result set will belong to + * @param seqnr The sequence number of this result set on the Response List + * @param maxrows A maxrows to set if so + * @return The ResultSet instance + * @throws ProtocolException If an error in the underlying connection happened. + */ + @Override + public ResultSetResponse getNextResultSetResponse(MonetConnection con, MonetConnection.ResponseList list, int seqnr, + int maxrows) throws ProtocolException { + int id = OldMapiStartOfHeaderParser.getNextResponseDataAsInt(this); //The order cannot be switched!! + int tuplecount = OldMapiStartOfHeaderParser.getNextResponseDataAsInt(this); + int columncount = OldMapiStartOfHeaderParser.getNextResponseDataAsInt(this); + int rowcount = OldMapiStartOfHeaderParser.getNextResponseDataAsInt(this); + if (maxrows != 0 && tuplecount > maxrows) { + tuplecount = maxrows; + } + return new ResultSetResponse(con, list, id, seqnr, rowcount, tuplecount, columncount); + } + + /** + * Gets the next UpdateResponse response from the server. + * + * @return An UpdateResponse instance + * @throws ProtocolException If an error in the underlying connection happened. + */ + @Override + public UpdateResponse getNextUpdateResponse() throws ProtocolException { + int count = OldMapiStartOfHeaderParser.getNextResponseDataAsInt(this); //The order cannot be switched!! + int lastId = OldMapiStartOfHeaderParser.getNextResponseDataAsInt(this); + return new UpdateResponse(lastId, count); + } + + /** + * Gets the next AutoCommitResponse response from the server. + * + * @return An AutoCommitResponse instance + * @throws ProtocolException If an error in the underlying connection happened. + */ + @Override + public AutoCommitResponse getNextAutoCommitResponse() throws ProtocolException { + boolean ac = this.lineBuffer.get() == 't'; + return new AutoCommitResponse(ac); + } + + @Override + public AbstractDataBlockResponse getAnEmptyDataBlockResponse(int rowcount, int columncount, + AbstractProtocol protocol, int[] JdbcSQLTypes) { + return new OldMapiDataBlockResponse(rowcount, columncount, protocol, JdbcSQLTypes); + } + + /** + * Gets the next DataBlockResponse response from the server, belonging to a ResultSetResponse + * + * @param rsresponses A map of ResultSetResponse, in which this Block will belong to one of them, by checking its id + * against the keys of the Map. + * @return The DataBlockResponse instance + * @throws ProtocolException If an error in the underlying connection happened. + */ + @Override + public AbstractDataBlockResponse getNextDatablockResponse(Map<Integer, ResultSetResponse> rsresponses) + throws ProtocolException { + int id = OldMapiStartOfHeaderParser.getNextResponseDataAsInt(this); //The order cannot be switched!! + OldMapiStartOfHeaderParser.getNextResponseDataAsInt(this); //column count + int rowcount = OldMapiStartOfHeaderParser.getNextResponseDataAsInt(this); + int offset = OldMapiStartOfHeaderParser.getNextResponseDataAsInt(this); + + ResultSetResponse rs = rsresponses.get(id); + if (rs == null) { + return null; + } + return rs.addDataBlockResponse(offset, rowcount); + } + + /** + * Gets the next Table Header for a ResultSetResponse. More than one of the parameter arrays can be filled at once. + * + * @param columnNames The column names array + * @param columnLengths The column lengths array + * @param types The columns SQL names array + * @param tableNames The columns schemas and names in format schema.table + * @return A TableResultHeaders integer representation, representing which of the fields was filled + * @throws ProtocolException If an error in the underlying connection happened. + */ + @Override + public int getNextTableHeader(String[] columnNames, int[] columnLengths, String[] types, String[] tableNames) + throws ProtocolException { + return OldMapiTableHeaderParser.getNextTableHeader(this.lineBuffer, columnNames, columnLengths, types, + tableNames); + } + + /** + * Retrieves the next values in a DataBlockResponse from the underlying connection, starting at a specific line + * number. + * + * @param firstLineNumber The first line number in the response to retrieve + * @param typesMap The JDBC types mapping array for every column in the ResultSetResponse of the DataBlock + * @param values An array of columns to fill the values + * @return The number of lines parsed from the underlying connection + * @throws ProtocolException If an error in the underlying connection happened. + */ + int parseTupleLines(int firstLineNumber, int[] typesMap, Object[] values) throws ProtocolException { + OldMapiTupleLineParser.oldMapiParseTupleLine(this, firstLineNumber, typesMap, values); + return firstLineNumber; + } + + /** + * Gets the remaining response line from the underlying connection as a Java String. This method is mostly used to + * retrieve error Strings, when they are detected while parsing a response line. + * + * @param startIndex The first index in the response line to retrieve the String + * @return The String representation of the line starting at the provided index + */ + @Override + public String getRemainingStringLine(int startIndex) { + if(this.lineBuffer.limit() > startIndex) { + if(this.lineBuffer.array()[startIndex] == '!') { + startIndex++; + } + return new String(this.lineBuffer.array(), startIndex, this.lineBuffer.limit() - startIndex); + } else { + return null; + } + } + + /** + * Writes a user query to the server, while providing the respective prefixes and suffixes depending on the current + * language and connection used. + * + * @param prefix The prefix to append at the beginning of the query string + * @param query The user query to submit to the server + * @param suffix The suffix to append at the end of the query string + * @throws IOException If an error in the underlying connection happened. + */ + @Override + public void writeNextQuery(String prefix, String query, String suffix) throws IOException { + this.socket.writeNextLine(prefix, query, suffix); + // reset reader state, last line isn't valid any more now + this.currentServerResponseHeader = ServerResponses.UNKNOWN; + } +}
new file mode 100644 --- /dev/null +++ b/src/main/java/nl/cwi/monetdb/mcl/protocol/oldmapi/OldMapiServerResponseParser.java @@ -0,0 +1,63 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * Copyright 1997 - July 2008 CWI, August 2008 - 2017 MonetDB B.V. + */ + +package nl.cwi.monetdb.mcl.protocol.oldmapi; + +import nl.cwi.monetdb.mcl.protocol.ServerResponses; + +/** + * This class parses the next server response on a MAPI connection using the next ASCII character on the stream. + * + * @author Fabian Groffen, Pedro Ferreira + */ +final class OldMapiServerResponseParser { + + private OldMapiServerResponseParser() {} + + /** + * Retrieves the next server response from an old MAPI protocol instance. + * + * @param protocol An Old MAPI protocol instance from which the next server response will be retrieved + * @return The integer representation of the next server response + */ + static int parseOldMapiServerResponse(OldMapiProtocol protocol) { + int res; + switch (protocol.lineBuffer.get()) { + case '!': + res = ServerResponses.ERROR; + break; + case '&': + res = ServerResponses.SOHEADER; + break; + case '%': + res = ServerResponses.HEADER; + break; + case '[': + res = ServerResponses.RESULT; + break; + case '=': + res = ServerResponses.RESULT; + break; + case '^': + res = ServerResponses.REDIRECT; + break; + case '#': + res = ServerResponses.INFO; + break; + case '.': + res = ServerResponses.PROMPT; + break; + case ',': + res = ServerResponses.MORE; + break; + default: + res = ServerResponses.UNKNOWN; + } + return res; + } +}
new file mode 100644 --- /dev/null +++ b/src/main/java/nl/cwi/monetdb/mcl/protocol/oldmapi/OldMapiStartOfHeaderParser.java @@ -0,0 +1,133 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * Copyright 1997 - July 2008 CWI, August 2008 - 2017 MonetDB B.V. + */ + +package nl.cwi.monetdb.mcl.protocol.oldmapi; + +import nl.cwi.monetdb.mcl.protocol.ProtocolException; +import nl.cwi.monetdb.mcl.protocol.StarterHeaders; + +/** + * The OldMapiStartOfHeaderParser is responsible to retrieve the server's headers on a SOHEADER response. Depending on + * the type of the header, the next tokens should be retrieved as integers or Strings. + * + * @author Fabian Groffen, Pedro Ferreira + */ +final class OldMapiStartOfHeaderParser { + + private OldMapiStartOfHeaderParser() {} + + static int getNextStartHeaderOnOldMapi(OldMapiProtocol protocol) { + int res; + switch (protocol.lineBuffer.get()) { + case '0': + res = StarterHeaders.Q_PARSE; + break; + case '1': + res = StarterHeaders.Q_TABLE; + protocol.lineBuffer.get(); + break; + case '2': + res = StarterHeaders.Q_UPDATE; + protocol.lineBuffer.get(); + break; + case '3': + res = StarterHeaders.Q_SCHEMA; + break; + case '4': + res = StarterHeaders.Q_TRANS; + protocol.lineBuffer.get(); + break; + case '5': + res = StarterHeaders.Q_PREPARE; + protocol.lineBuffer.get(); + break; + case '6': + res = StarterHeaders.Q_BLOCK; + protocol.lineBuffer.get(); + break; + default: + res = StarterHeaders.Q_UNKNOWN; + } + return res; + } + + /** + * Returns the next token in the Protocol's lineBuffer as an integer. The value is considered to end at the end of + * the lineBuffer or at a space. If a non-numeric character is encountered a ProtocolException is thrown. + * + * @param protocol An Old Mapi Protocol instance where the next token will be retrieved + * @return The next token in the Protocol as an integer + * @throws ProtocolException if no numeric value could be read + */ + static int getNextResponseDataAsInt(OldMapiProtocol protocol) throws ProtocolException { + int currentPointer = protocol.lineBuffer.position(); + int limit = protocol.lineBuffer.limit(); + char[] array = protocol.lineBuffer.array(); + + if (currentPointer >= limit) { + throw new ProtocolException("unexpected end of string", currentPointer - 1); + } + int tmp = 0; + boolean positive = true; + char chr = array[currentPointer++]; + // note: don't use Character.isDigit() here, because we only want ISO-LATIN-1 digits + if (chr >= '0' && chr <= '9') { + tmp = (int)chr - (int)'0'; + } else if(chr == '-') { + positive = false; + } else { + throw new ProtocolException("expected a digit", currentPointer - 1); + } + + while (currentPointer < limit) { + chr = array[currentPointer++]; + if(chr == ' ') { + break; + } + tmp *= 10; + if (chr >= '0' && chr <= '9') { + tmp += (int)chr - (int)'0'; + } else { + throw new ProtocolException("expected a digit", currentPointer - 1); + } + } + protocol.lineBuffer.position(currentPointer); + return positive ? tmp : -tmp; + } + + /** + * Returns the next token in the Protocol's lineBuffer as a String. The value is considered to end at the end of the + * lineBuffer or at a space. If no character is found, a ProtocolException is thrown. + * + * @param protocol An Old Mapi Protocol instance where the next token will be retrieved + * @return The next token in the Protocol as a String + * @throws ProtocolException if no character could be read + */ + static String getNextResponseDataAsString(OldMapiProtocol protocol) throws ProtocolException { + int currentPointer = protocol.lineBuffer.position(); + int limit = protocol.lineBuffer.limit(); + char[] array = protocol.lineBuffer.array(); + + if (currentPointer >= limit) { + throw new ProtocolException("unexpected end of string", currentPointer - 1); + } + int cnt = 0, mark = currentPointer; + char chr; + + while (currentPointer < limit) { + chr = array[currentPointer++]; + if(chr == ' ') { + break; + } + cnt++; + } + + protocol.lineBuffer.position(mark); + return new String(array, 0, cnt); + } +}
new file mode 100644 --- /dev/null +++ b/src/main/java/nl/cwi/monetdb/mcl/protocol/oldmapi/OldMapiTableHeaderParser.java @@ -0,0 +1,163 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * Copyright 1997 - July 2008 CWI, August 2008 - 2017 MonetDB B.V. + */ + +package nl.cwi.monetdb.mcl.protocol.oldmapi; + +import nl.cwi.monetdb.mcl.protocol.ProtocolException; +import nl.cwi.monetdb.mcl.protocol.TableResultHeaders; + +import java.nio.CharBuffer; + +/** + * The OldMapiTableHeaderParser is a generic parser that retrieves Q_TABLE, Q_PREPARE and Q_BLOCK responses data as + * integers and Strings to fill the Tables' metadata. + * + * @author Fabian Groffen, Pedro Ferreira + */ +final class OldMapiTableHeaderParser { + + private OldMapiTableHeaderParser() {} + + /** + * Retrieves the next table result set header and fills the respective array of values. + * + * @param lineBuffer An Old Mapi Protocol's lineBuffer to retrieve data + * @param columnNames The result set column names + * @param columnLengths The result set column lengths + * @param types The result set column SQL types + * @param tableNames The result set columns schema and table names in format schema.table + * @return The integer representation of the Table Result Header retrieved + * @throws ProtocolException If an error while parsing occurred + */ + static int getNextTableHeader(CharBuffer lineBuffer, String[] columnNames, int[] columnLengths, String[] types, + String[] tableNames) throws ProtocolException { + int res = TableResultHeaders.UNKNOWN; + int currentLength = lineBuffer.limit(); + char[] array = lineBuffer.array(); + + int pos = 0; + boolean foundChar = false, nameFound = false; + + // find header name + for (int i = currentLength - 1; i >= 0; i--) { + switch (array[i]) { + case ' ': + case '\n': + case '\t': + case '\r': + if (!foundChar) { + currentLength = i - 1; + } else { + pos = i + 1; + } + break; + case '#': + // found! + nameFound = true; + if (pos == 0) pos = i + 1; + i = 0; // force the loop to terminate + break; + default: + foundChar = true; + pos = 0; + break; + } + } + if (!nameFound) + throw new ProtocolException("Invalid header, no header name found", pos); + + // depending on the name of the header, we continue + switch (array[pos]) { + case 'n': //name + if (currentLength - pos == 4) { + getStringValues(array, pos - 3, columnNames); + res = TableResultHeaders.NAME; + } + break; + case 'l': //length + if (currentLength - pos == 6) { + getIntValues(array, pos - 3, columnLengths); + res = TableResultHeaders.LENGTH; + } + break; + case 't': + if (currentLength - pos == 4) { //type + getStringValues(array, pos - 3, types); + res = TableResultHeaders.TYPE; + } else if (currentLength - pos == 10) { //table_name + getStringValues(array, pos - 3, tableNames); + res = TableResultHeaders.TABLE; + } + break; + default: + throw new ProtocolException("Unknown header: " + new String(array, pos, currentLength - pos)); + } + return res; + } + + /** + * Fills a String array header with values. + * + * As of Oct2014-SP1 release MAPI adds double quotes around names when the name contains a comma or a tab or a space + * or a # or " character. + * See issue: https://www.monetdb.org/bugzilla/show_bug.cgi?id=3616 If the parsed name string part has a " as first + * and last character, we remove those added double quotes here. + * + * @param array The lineBuffer's backing array + * @param stop The position to stop parsing + * @param stringValues The String array to fill + */ + private static void getStringValues(char[] array, int stop, String[] stringValues) { + int elem = 0, start = 2; + + for (int i = start + 1; i < stop; i++) { + if (array[i] == '\t' && array[i - 1] == ',') { + if (array[start] == '"') { + start++; // skip leading double quote + } + stringValues[elem++] = new String(array, start, i - (array[i - 2] == '"' ? 2 : 1) - start); + start = i + 1; + } + } + // add the left over part (last column) + if (array[start] == '"') { + start++; // skip leading double quote + } + stringValues[elem] = new String(array, start, stop - (array[stop - 1] == '"' ? 1 : 0) - start); + } + + /** + * Fills an integer array header with values. + * + * @param array The lineBuffer's backing array + * @param stop The position to stop parsing + * @param intValues The integer array to fill + * @throws ProtocolException If an error while parsing occurred + */ + private static void getIntValues(char[] array, int stop, int[] intValues) throws ProtocolException { + int elem = 0, tmp = 0, start = 2; + + for (int i = start; i < stop; i++) { + if (array[i] == ',' && array[i + 1] == '\t') { + intValues[elem++] = tmp; + tmp = 0; + i++; + } else { + tmp *= 10; + // note: don't use Character.isDigit() here, because we only want ISO-LATIN-1 digits + if (array[i] >= '0' && array[i] <= '9') { + tmp += (int) array[i] - (int)'0'; + } else { + throw new ProtocolException("Expected a digit in " + new String(array) + " at " + i); + } + } + } + // add the left over part + intValues[elem] = tmp; + } +}
new file mode 100644 --- /dev/null +++ b/src/main/java/nl/cwi/monetdb/mcl/protocol/oldmapi/OldMapiTupleLineParser.java @@ -0,0 +1,322 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * Copyright 1997 - July 2008 CWI, August 2008 - 2017 MonetDB B.V. + */ + +package nl.cwi.monetdb.mcl.protocol.oldmapi; + +import nl.cwi.monetdb.jdbc.MonetBlob; +import nl.cwi.monetdb.jdbc.MonetClob; +import nl.cwi.monetdb.mcl.connection.helpers.BufferReallocator; +import nl.cwi.monetdb.mcl.connection.helpers.GregorianCalendarParser; +import nl.cwi.monetdb.mcl.connection.helpers.TimestampHelper; +import nl.cwi.monetdb.mcl.protocol.ProtocolException; + +import java.math.BigDecimal; +import java.nio.CharBuffer; +import java.sql.Types; +import java.util.Calendar; + +/** + * The OldMapiTupleLineParser extracts the values from a given tuple. The number of values that are expected are known + * upfront to speed up allocation and validation. For null values we will map into Java minimum values for primitives + * and null pointers for objects. + * + * @author Fabian Groffen, Pedro Ferreira + */ +final class OldMapiTupleLineParser { + + private OldMapiTupleLineParser() {} + + /** + * The character array representation of a NULL value found in a tuple. + */ + private static final char[] NULL_STRING = new char[]{'N','U','L','L'}; + + /** + * Parses the given OldMapiProtocol's lineBuffer source as tuple line for a DataBlock. If source cannot be parsed, a + * ProtocolException is thrown. The OldMapiProtocol's tupleLineBuffer is used to help parsing a column value. + * + * @param protocol The OldMapiProtocol instance to parse its lineBuffer + * @param lineNumber The row line number on the DataBlock to insert the tuple + * @param typesMap The JDBC types mapping array + * @param values An array of columns to fill the parsed values + * @return The number of columns parsed + * @throws ProtocolException If an error occurs during parsing + */ + static int oldMapiParseTupleLine(OldMapiProtocol protocol, int lineNumber, int[] typesMap, Object[] values) + throws ProtocolException { + CharBuffer lineBuffer = protocol.lineBuffer; + CharBuffer tupleLineBuffer = protocol.tupleLineBuffer; + + int len = lineBuffer.limit(); + char[] array = lineBuffer.array(); + + // first detect whether this is a single value line (=) or a real tuple ([) + if (array[0] == '=') { + if (typesMap.length != 1) { + throw new ProtocolException(typesMap.length + " columns expected, but only single value found"); + } + // return the whole string but the leading = + oldMapiStringToJavaDataConversion(protocol, array, 1, len - 1, lineNumber, values[0], + typesMap[0]); + return 1; + } + + // extract separate fields by examining string, char for char + boolean inString = false, escaped = false; + int cursor = 2, column = 0, i = 2; + for (; i < len; i++) { + switch(array[i]) { + default: + escaped = false; + break; + case '\\': + escaped = !escaped; + break; + case '"': + /** + * If all strings are wrapped between two quotes, a \" can never exist outside a string. Thus if we + * believe that we are not within a string, we can safely assume we're about to enter a string if we + * find a quote. If we are in a string we should stop being in a string if we find a quote which is + * not prefixed by a \, for that would be an escaped quote. However, a nasty situation can occur + * where the string is like "test \\" as obvious, a test for a \ in front of a " doesn't hold here + * for all cases. Because "test \\\"" can exist as well, we need to know if a quote is prefixed by + * an escaping slash or not. + */ + if (!inString) { + inString = true; + } else if (!escaped) { + inString = false; + } + // reset escaped flag + escaped = false; + break; + case '\t': + if (!inString && (i > 0 && array[i - 1] == ',') || (i + 1 == len - 1 && array[++i] == ']')) { // dirty + // split! + if (array[cursor] == '"' && array[i - 2] == '"') { + // reuse the tupleLineBuffer by cleaning it and ensure the capacity + tupleLineBuffer.clear(); + tupleLineBuffer = BufferReallocator.ensureCapacity(tupleLineBuffer, (i - 2) - (cursor + 1)); + + for (int pos = cursor + 1; pos < i - 2; pos++) { + if (array[cursor] == '\\' && pos + 1 < i - 2) { + pos++; + // strToStr and strFromStr in gdk_atoms.mx only support \t \n \\ \" and \377 + switch (array[pos]) { + case '\\': + tupleLineBuffer.put('\\'); + break; + case 'n': + tupleLineBuffer.put('\n'); + break; + case 't': + tupleLineBuffer.put('\t'); + break; + case '"': + tupleLineBuffer.put('"'); + break; + case '0': case '1': case '2': case '3': + // this could be an octal number, let's check it out + if (pos + 2 < i - 2 && array[pos + 1] >= '0' && array[pos + 1] <= '7' && + array[pos + 2] >= '0' && array[pos + 2] <= '7') { + // we got the number! + try { + tupleLineBuffer.put((char)(Integer.parseInt("" + array[pos] + array[pos + 1] + array[pos + 2], 8))); + pos += 2; + } catch (NumberFormatException e) { + // hmmm, this point should never be reached actually... + throw new AssertionError("Flow error, should never try to parse non-number"); + } + } else { + // do default action if number seems not to be correct + tupleLineBuffer.put(array[pos]); + } + break; + default: + // this is wrong, just ignore the escape, and print the char + tupleLineBuffer.put(array[pos]); + break; + } + } else if(array[pos] == '\\') { + pos++; + switch (array[pos]) { + case '\\': + tupleLineBuffer.put('\\'); + break; + case 'n': + tupleLineBuffer.put('\n'); + break; + case 't': + tupleLineBuffer.put('\t'); + break; + case '"': + tupleLineBuffer.put('"'); + break; + } + } else { + tupleLineBuffer.put(array[pos]); + } + } + // put the unescaped string in the right place + tupleLineBuffer.flip(); + oldMapiStringToJavaDataConversion(protocol, tupleLineBuffer.array(), 0, + tupleLineBuffer.limit(), lineNumber, values[column], typesMap[column]); + } else if ((i - 1) - cursor == 4 && OldMapiTupleLineParserHelper.charIndexOf(array, + 0,array.length, NULL_STRING, 0,4, cursor) == cursor) { + setNullValue(lineNumber, values[column], typesMap[column]); + } else { + oldMapiStringToJavaDataConversion(protocol, array, cursor, i - 1 - cursor, lineNumber, + values[column], typesMap[column]); + } + column++; + cursor = i + 1; + } + // reset escaped flag + escaped = false; + break; + } + } + protocol.tupleLineBuffer = tupleLineBuffer; + // check if this result is of the size we expected it to be + if (column != typesMap.length) + throw new ProtocolException("illegal result length: " + column + "\nlast read: " + + (column > 0 ? values[column - 1] : "<none>")); + return column; + } + + /** + * Parses a BLOB String from a tuple column, converting it into a Java byte[] representation. + * + * @param toParse The CharBuffer's backing array + * @param startPosition The first position in the array to parse + * @param count The number of characters to read from the starter position + * @return A Java byte[] instance with the parsed BLOB + */ + private static byte[] binaryBlobConverter(char[] toParse, int startPosition, int count) { + int len = (startPosition + count) / 2; + byte[] res = new byte[len]; + for (int i = 0; i < len; i++) { + res[i] = (byte) Integer.parseInt(new String(toParse, 2 * i, (2 * i) + 2), 16); + } + return res; + } + + /** + * Converts a segment of a CharBuffer's backing array into a Java primitive or object depending on the JDBC Mapping. + * + * @param toParse The CharBuffer's backing array + * @param startPosition The first position in the array to parse + * @param count The number of characters to read from the starter position + * @param lineNumber The row line number on the DataBlock to insert the tuple + * @param columnArray The column array where the value will be appended + * @param jDBCMapping The JDBC mapping of the value + * @throws ProtocolException If the JDBC Mapping is unknown + */ + private static void oldMapiStringToJavaDataConversion(OldMapiProtocol protocol, char[] toParse, int startPosition, + int count, int lineNumber, Object columnArray, + int jDBCMapping) throws ProtocolException { + switch (jDBCMapping) { + case Types.BOOLEAN: + ((byte[]) columnArray)[lineNumber] = OldMapiTupleLineParserHelper.charArrayToBoolean(toParse, startPosition); + break; + case Types.TINYINT: + ((byte[]) columnArray)[lineNumber] = OldMapiTupleLineParserHelper.charArrayToByte(toParse, startPosition, count); + break; + case Types.SMALLINT: + ((short[]) columnArray)[lineNumber] = OldMapiTupleLineParserHelper.charArrayToShort(toParse, startPosition, count); + break; + case Types.INTEGER: + ((int[]) columnArray)[lineNumber] = OldMapiTupleLineParserHelper.charArrayToInt(toParse, startPosition, count); + break; + case Types.BIGINT: + ((long[]) columnArray)[lineNumber] = OldMapiTupleLineParserHelper.charArrayToLong(toParse, startPosition, count); + break; + case Types.REAL: + ((float[]) columnArray)[lineNumber] = Float.parseFloat(new String(toParse, startPosition, count)); + break; + case Types.DOUBLE: + ((double[]) columnArray)[lineNumber] = Double.parseDouble(new String(toParse, startPosition, count)); + break; + case Types.NUMERIC: + case Types.DECIMAL: + ((BigDecimal[]) columnArray)[lineNumber] = new BigDecimal(toParse, startPosition, count); + break; + case Types.CHAR: + case Types.VARCHAR: + case Types.LONGVARCHAR: + case Types.OTHER: + ((String[]) columnArray)[lineNumber] = new String(toParse, startPosition, count); + break; + case Types.DATE: + ((Calendar[]) columnArray)[lineNumber] = GregorianCalendarParser.parseDate(new String(toParse, startPosition, count), + protocol.getMonetParserPosition(), protocol.getMonetDate()); + break; + case Types.TIME: + ((Calendar[]) columnArray)[lineNumber] = GregorianCalendarParser.parseTime(new String(toParse, startPosition, count), + protocol.getMonetParserPosition(), protocol.timeParser, false); + break; + case 2013: //Types.TIME_WITH_TIMEZONE: + ((Calendar[]) columnArray)[lineNumber] = GregorianCalendarParser.parseTime(new String(toParse, startPosition, count), + protocol.getMonetParserPosition(), protocol.timeParser, true); + break; + case Types.TIMESTAMP: + ((TimestampHelper[]) columnArray)[lineNumber] = GregorianCalendarParser.parseTimestamp(new String(toParse, startPosition, count), + protocol.getMonetParserPosition(), protocol.timestampParser, false); + break; + case 2014: //Types.TIMESTAMP_WITH_TIMEZONE: + ((TimestampHelper[]) columnArray)[lineNumber] = GregorianCalendarParser.parseTimestamp(new String(toParse, startPosition, count), + protocol.getMonetParserPosition(), protocol.timestampParser, true); + break; + case Types.CLOB: + ((MonetClob[]) columnArray)[lineNumber] = new MonetClob(toParse, startPosition, count); + break; + case Types.BLOB: + ((MonetBlob[]) columnArray)[lineNumber] = new MonetBlob(binaryBlobConverter(toParse, startPosition, count)); + break; + case Types.LONGVARBINARY: + ((byte[][]) columnArray)[lineNumber] = binaryBlobConverter(toParse, startPosition, count); + break; + default: + throw new ProtocolException("Unknown JDBC mapping!"); + } + } + + /** + * Maps MonetDB's null values with their respective Java representation. For the primitive types, we will map them + * to their minimum values, while for objects we just map into null pointers. + * + * @param lineNumber The row line number on the DataBlock to insert the tuple + * @param columnArray The column array where the value will be appended + * @param jDBCMapping The JDBC mapping of the value + */ + private static void setNullValue(int lineNumber, Object columnArray, int jDBCMapping) { + switch (jDBCMapping) { + case Types.BOOLEAN: + case Types.TINYINT: + ((byte[]) columnArray)[lineNumber] = Byte.MIN_VALUE; + break; + case Types.SMALLINT: + ((short[]) columnArray)[lineNumber] = Short.MIN_VALUE; + break; + case Types.INTEGER: + ((int[]) columnArray)[lineNumber] = Integer.MIN_VALUE; + break; + case Types.BIGINT: + ((long[]) columnArray)[lineNumber] = Long.MIN_VALUE; + break; + case Types.REAL: + ((float[]) columnArray)[lineNumber] = Float.MIN_VALUE; + break; + case Types.DOUBLE: + ((double[]) columnArray)[lineNumber] = Double.MIN_VALUE; + break; + default: + ((Object[]) columnArray)[lineNumber] = null; + } + } +}
new file mode 100644 --- /dev/null +++ b/src/main/java/nl/cwi/monetdb/mcl/protocol/oldmapi/OldMapiTupleLineParserHelper.java @@ -0,0 +1,232 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * Copyright 1997 - July 2008 CWI, August 2008 - 2017 MonetDB B.V. + */ + +package nl.cwi.monetdb.mcl.protocol.oldmapi; + +import nl.cwi.monetdb.mcl.protocol.ProtocolException; + +/** + * This is a helper Class for the OldMapiTupleLineParser. The main objective of this class is to parse primitive types + * without any memory allocation for performance reasons. The code may seem to be boilerplate, but it has to be done + * this way to due to poor typing of the Java programming language. + * + * @author Pedro Ferreira + */ +final class OldMapiTupleLineParserHelper { + + private OldMapiTupleLineParserHelper() {} + + /** + * Checks if a char[] (target) is inside on another (source), retrieving the first index on the source, where the + * target is, if found. In other words this a Java implementation of the strstr function from the C standard. + * As we search always from the beginning of the source, the start parameter is not used. + * + * @param source The source char[] to search + * @param sourceCount The number of characters in the source array to search + * @param target The target char[] to be found + * @param targetCount The result set column SQL types + * @return The integer representation of the Table Result Header retrieved + */ + static int charIndexOf(char[] source, int sourceOffset, int sourceCount, char[] target, int targetOffset, + int targetCount, int fromIndex) { + if (fromIndex >= sourceCount) { + return (targetCount == 0 ? sourceCount : -1); + } + if (fromIndex < 0) { + fromIndex = 0; + } + if (targetCount == 0) { + return fromIndex; + } + + char first = target[targetOffset]; + int max = sourceOffset + (sourceCount - targetCount); + + for (int i = sourceOffset + fromIndex; i <= max; i++) { + /* Look for first character. */ + if (source[i] != first) { + while (++i <= max && source[i] != first); + } + if (i <= max) { + int j = i + 1; + int end = j + targetCount - 1; + for (int k = targetOffset + 1; j < end && source[j] == target[k]; j++, k++); + + if (j == end) { + /* Found whole string. */ + return i - sourceOffset; + } + } + } + return -1; + } + + /** + * The character array representation of a TRUE value. + */ + private static final char[] TRUE_CONSTANT = new char[]{'t','r','u','e'}; + + /** + * Converts a segment of a CharBuffer's backing array into a Java boolean. + * + * @param start The first position in the array to parse + * @param data The CharBuffer's backing array to parse + * @return 1 it's a true value, 0 if false + */ + static byte charArrayToBoolean(char[] data, int start) { + return charIndexOf(data, 0, data.length, TRUE_CONSTANT, 0, 4, start) + == start ? (byte)1 : (byte)0; + } + + /** + * Converts a segment of a CharBuffer's backing array into a Java byte. + * + * @param data The CharBuffer's backing array to parse + * @param start The first position in the array to parse + * @param count The number of characters to read from the starter position + * @return The parsed byte value + */ + static byte charArrayToByte(char[] data, int start, int count) throws ProtocolException { + byte tmp = 0; + int limit = start + count; + boolean positive = true; + char chr = data[start++]; + + if (chr >= '0' && chr <= '9') { + tmp = (byte)(chr - '0'); + } else if(chr == '-') { + positive = false; + } else { + throw new ProtocolException("Expected a digit at the position " + (start - 1)); + } + while (start < limit) { + chr = data[start++]; + if(chr == ' ') { + break; + } + tmp *= 10; + if (chr >= '0' && chr <= '9') { + tmp += chr - '0'; + } else { + throw new ProtocolException("Expected a digit at the position " + (start - 1)); + } + } + return positive ? tmp : (byte) -tmp; + } + + /** + * Converts a segment of a CharBuffer's backing array into a Java short. + * + * @param data The CharBuffer's backing array to parse + * @param start The first position in the array to parse + * @param count The number of characters to read from the starter position + * @return The parsed short value + */ + static short charArrayToShort(char[] data, int start, int count) throws ProtocolException { + short tmp = 0; + int limit = start + count; + boolean positive = true; + char chr = data[start++]; + + if (chr >= '0' && chr <= '9') { + tmp = (short)(chr - '0'); + } else if(chr == '-') { + positive = false; + } else { + throw new ProtocolException("Expected a digit at the position " + (start - 1)); + } + while (start < limit) { + chr = data[start++]; + if(chr == ' ') { + break; + } + tmp *= 10; + if (chr >= '0' && chr <= '9') { + tmp += chr - '0'; + } else { + throw new ProtocolException("Expected a digit at the position " + (start - 1)); + } + } + return positive ? tmp : (short) -tmp; + } + + /** + * Converts a segment of a CharBuffer's backing array into a Java int. + * + * @param data The CharBuffer's backing array to parse + * @param start The first position in the array to parse + * @param count The number of characters to read from the starter position + * @return The parsed int value + */ + static int charArrayToInt(char[] data, int start, int count) throws ProtocolException { + int tmp = 0, limit = start + count; + boolean positive = true; + char chr = data[start++]; + + if (chr >= '0' && chr <= '9') { + tmp = chr - '0'; + } else if(chr == '-') { + positive = false; + } else { + throw new ProtocolException("Expected a digit at the position " + (start - 1)); + } + while (start < limit) { + chr = data[start++]; + if(chr == ' ') { + break; + } else if(chr == '.') { //for intervals + continue; + } + tmp *= 10; + if (chr >= '0' && chr <= '9') { + tmp += (int)chr - (int)'0'; + } else { + throw new ProtocolException("Expected a digit at the position " + (start - 1)); + } + } + return positive ? tmp : -tmp; + } + + /** + * Converts a segment of a CharBuffer's backing array into a Java long. + * + * @param data The CharBuffer's backing array to parse + * @param start The first position in the array to parse + * @param count The number of characters to read from the starter position + * @return The parsed long value + */ + static long charArrayToLong(char[] data, int start, int count) throws ProtocolException { + long tmp = 0; + int limit = start + count; + boolean positive = true; + char chr = data[start++]; + + if (chr >= '0' && chr <= '9') { + tmp = chr - '0'; + } else if(chr == '-') { + positive = false; + } else { + throw new ProtocolException("Expected a digit at the position " + (start - 1)); + } + while (start < limit) { + chr = data[start++]; + if(chr == ' ') { + break; + } else if(chr == '.') { //for intervals + continue; + } + tmp *= 10; + if (chr >= '0' && chr <= '9') { + tmp += chr - '0'; + } else { + throw new ProtocolException("Expected a digit at the position " + (start - 1)); + } + } + return positive ? tmp : -tmp; + } +}
new file mode 100644 --- /dev/null +++ b/src/main/java/nl/cwi/monetdb/mcl/responses/AbstractDataBlockResponse.java @@ -0,0 +1,212 @@ +package nl.cwi.monetdb.mcl.responses; + +import nl.cwi.monetdb.jdbc.MonetResultSet; +import nl.cwi.monetdb.mcl.connection.helpers.GregorianCalendarParser; +import nl.cwi.monetdb.mcl.protocol.AbstractProtocol; +import nl.cwi.monetdb.mcl.protocol.ProtocolException; + +import java.sql.SQLException; +import java.sql.Types; +import java.text.SimpleDateFormat; +import java.util.Calendar; + +/** + * The DataBlockResponse is tabular data belonging to a ResultSetResponse. On a MAPI connection, tabular data from the + * server typically looks like: + * <pre> + * [ "value", 56 ] + * </pre> + * where each column is separated by ",\t" and each tuple surrounded by brackets ("[" and "]"). A DataBlockResponse + * object holds the raw data as read from the server, in a parsed manner, ready for easy retrieval. Meanwhile on an + * Embedded connection, the data is automatically parsed. + * + * This object is not intended to be queried by multiple threads synchronously. It is designed to work for one thread + * retrieving rows from it. When multiple threads will retrieve rows from this object, it is possible for threads to + * get the same data. + * + * @author Fabian Groffen, Pedro Ferreira + */ +public abstract class AbstractDataBlockResponse implements IIncompleteResponse { + + /** The connection protocol to parse the tuple lines */ + public final AbstractProtocol protocol; + /** A 'pointer' to the current line */ + public int blockLine; + /** The number of rows in the block */ + public final int rowcount; + /** The JdbcSQLTypes mapping */ + public final int[] jdbcSQLTypes; + /** whether the last read field (via some getXyz() method) was NULL */ + public boolean lastReadWasNull = true; + + /** + * Constructs an AbstractDataBlockResponse object. + * + * @param rowcount the number of rows + * @param protocol the underlying protocol + * @param JdbcSQLTypes an array of the JDBC mappings of the columns + */ + public AbstractDataBlockResponse(int rowcount, AbstractProtocol protocol, int[] JdbcSQLTypes) { + this.rowcount = rowcount; + this.protocol = protocol; + this.jdbcSQLTypes = JdbcSQLTypes; + } + + /** + * Returns whether this Response expects more lines to be added to it. + * + * @return true if a next line should be added, false otherwise + */ + @Override + public abstract boolean wantsMore(); + + /** + * addLines adds a batch of rows to the block. Before adding the first line, the column arrays are allocated. + * + * @param protocol The connection's protocol to fetch data from + * @throws ProtocolException If the result line is not expected + */ + @Override + public abstract void addLines(AbstractProtocol protocol) throws ProtocolException; + + /* Methods to be called after the block construction has been completed */ + + /** + * Sets the current line number on the block. + * + * @param blockLine the block line number + */ + void setBlockLine(int blockLine) { + this.blockLine = blockLine; + } + + /** + * Returns if the last value read was null or not. + * + * @return If the last value read was null or not + */ + public boolean isLastReadWasNull() { + return lastReadWasNull; + } + + /** + * Gets the current row value as a Java Boolean. + * + * @param column The column index starting from 0 + * @return A Java Boolean if the column is a boolean, otherwise a ClassCastException is thrown + */ + public abstract boolean getBooleanValue(int column); + + /** + * Gets the current row value as a Java Byte. + * + * @param column The column index starting from 0 + * @return A Java Byte if the column is a tinyint, otherwise a ClassCastException is thrown + */ + public abstract byte getByteValue(int column); + + /** + * Gets the current row value as a Java Short. + * + * @param column The column index starting from 0 + * @return A Java Short if the column is a smallint, otherwise a ClassCastException is thrown + */ + public abstract short getShortValue(int column); + + /** + * Gets the current row value as a Java Integer. + * + * @param column The column index starting from 0 + * @return A Java Integer if the column is an integer or month_interval, otherwise a ClassCastException is thrown + */ + public abstract int getIntValue(int column); + + /** + * Gets the current row value as a Java Long. + * + * @param column The column index starting from 0 + * @return A Java Long if the column is a bigint or second_interval, otherwise a ClassCastException is thrown + */ + public abstract long getLongValue(int column); + + /** + * Gets the current row value as a Java Float. + * + * @param column The column index starting from 0 + * @return A Java Float if the column is a real, otherwise a ClassCastException is thrown + */ + public abstract float getFloatValue(int column); + + /** + * Gets the current row value as a Java Double. + * + * @param column The column index starting from 0 + * @return A Java Double if the column is a double, otherwise a ClassCastException is thrown + */ + public abstract double getDoubleValue(int column); + + /** + * Gets the current row value as a Java Object. + * + * @param column The column index starting from 0 + * @return A Java Object if the column is not a primitive type, otherwise a ClassCastException is thrown + */ + public abstract Object getObjectValue(int column) throws ProtocolException; + + /** + * Gets the current row value as a Java String. + * + * @param column The column index starting from 0 + * @return The String representation of the data type + */ + public abstract String getValueAsString(int column) throws ProtocolException; + + /** + * Gets the current row value as a Java Object. + * + * @param column The column index starting from 0 + * @return The Object representation of the data type + */ + public abstract Object getValueAsObject(int column) throws ProtocolException; + + /** + * To parse a String column in a date, time or timestamp instance, this method is used. + * + * @param mrs A MonetResultSet instance where warning can be added + * @param column The column index starting from 0 + * @param jdbcType The JDBC type of the column desired to convert + * @return A {@link Calendar} instance of the parsed date + * @throws SQLException If the conversation cannot be performed + */ + public Calendar getDateValueFromString(MonetResultSet mrs, int column, int jdbcType) throws SQLException { + try { + String value = this.getObjectValue(column).toString(); + SimpleDateFormat aux; + switch (jdbcType) { + case Types.DATE: + aux = protocol.getMonetDate(); + break; + case Types.TIME: + case 2013: //Types.TIME_WITH_TIMEZONE: + aux = protocol.getMonetTimePrinter(); + break; + case Types.TIMESTAMP: + case 2014: //Types.TIMESTAMP_WITH_TIMEZONE: + aux = protocol.getMonetTimestampPrinter(); + break; + default: + throw new SQLException("Internal error!", "M1M05"); + } + return GregorianCalendarParser.parseDateString(mrs, value, protocol.getMonetParserPosition(), aux, jdbcType); + } catch (ProtocolException e) { + throw new SQLException(e); + } + } + + /** + * Get the last parsed nanoseconds value. + * + * @return The last parsed nanoseconds value + */ + public abstract int getLastNanos(); +}
new file mode 100644 --- /dev/null +++ b/src/main/java/nl/cwi/monetdb/mcl/responses/AutoCommitResponse.java @@ -0,0 +1,38 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * Copyright 1997 - July 2008 CWI, August 2008 - 2017 MonetDB B.V. + */ + +package nl.cwi.monetdb.mcl.responses; + +/** + * The AutoCommitResponse represents a transaction message. It stores (a change in) the server side auto commit mode. + * <br /> + * <tt>&4 (t|f)</tt> + * + * @author Fabian Groffen + */ +public class AutoCommitResponse extends SchemaResponse { + + /** + * If the auto commit mode is set. + */ + private final boolean autocommit; + + public AutoCommitResponse(boolean ac) { + // fill the blank final + this.autocommit = ac; + } + + /** + * The auto commit value + * + * @return The auto commit value + */ + public boolean isAutocommit() { + return autocommit; + } +}
new file mode 100644 --- /dev/null +++ b/src/main/java/nl/cwi/monetdb/mcl/responses/IIncompleteResponse.java @@ -0,0 +1,36 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * Copyright 1997 - July 2008 CWI, August 2008 - 2017 MonetDB B.V. + */ + +package nl.cwi.monetdb.mcl.responses; + +import nl.cwi.monetdb.mcl.protocol.AbstractProtocol; +import nl.cwi.monetdb.mcl.protocol.ProtocolException; + +/** + * The ResultSetResponse and DatablockResponse Classes might require more than one Block response if the response is + * larger than the BlockSize. + * + * @author Fabian Groffen, Pedro Ferreira + */ +public interface IIncompleteResponse extends IResponse { + + /** + * Returns whether this Response expects more lines to be added to it. + * + * @return true if a next line should be added, false otherwise + */ + boolean wantsMore(); + + /** + * Adds a batch of data to the Response instance. + * + * @param protocol The connection's protocol to fetch data from + * @throws ProtocolException If the result line is not expected + */ + void addLines(AbstractProtocol protocol) throws ProtocolException; +}
new file mode 100644 --- /dev/null +++ b/src/main/java/nl/cwi/monetdb/mcl/responses/IResponse.java @@ -0,0 +1,23 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * Copyright 1997 - July 2008 CWI, August 2008 - 2017 MonetDB B.V. + */ + +package nl.cwi.monetdb.mcl.responses; + +/** + * A Response is a message sent by the server to indicate some action has taken place, and possible results of that + * action. + * + * @author Fabian Groffen, Pedro Ferreira + */ +public interface IResponse { + + /** + * Instructs the Response implementation to close and do the necessary clean up procedures. + */ + void close(); +}
new file mode 100644 --- /dev/null +++ b/src/main/java/nl/cwi/monetdb/mcl/responses/ResultSetResponse.java @@ -0,0 +1,406 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * Copyright 1997 - July 2008 CWI, August 2008 - 2017 MonetDB B.V. + */ + +package nl.cwi.monetdb.mcl.responses; + +import nl.cwi.monetdb.jdbc.MonetConnection; +import nl.cwi.monetdb.jdbc.MonetDriver; +import nl.cwi.monetdb.mcl.connection.ControlCommands; +import nl.cwi.monetdb.mcl.protocol.AbstractProtocol; +import nl.cwi.monetdb.mcl.protocol.ProtocolException; +import nl.cwi.monetdb.mcl.protocol.ServerResponses; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Types; + +/** + * The ResultSetResponse represents a tabular result sent by the server. This is typically an SQL table. The MAPI + * headers of the Response look like: + * <pre> + * &1 1 28 2 10 + * # name, value # name + * # varchar, varchar # type + * </pre> + * there the first line consists out of<br /> + * <tt>&"qt" "id" "tc" "cc" "rc"</tt>. + * Meanwhile on an Embedded connection the data is fetched with no parsing. + * + * @author Fabian Groffen, Pedro Ferreira + */ +public class ResultSetResponse implements IIncompleteResponse { + + /** The expected final value after the table headers are set. */ + private static final byte IS_SET_FINAL_VALUE = 15; + + /** The number of rows in the current block */ + private final int rowcount; + /** The total number of rows this result set has */ + private final int tuplecount; + /** The numbers of rows to retrieve per DataBlockResponse */ + private int cacheSize; + /** The table ID of this result */ + private final int id; + /** The names of the columns in this result */ + private final String[] name; + /** The types of the columns in this result */ + private final String[] type; + /** The JDBC SQL types of the columns in this ResultSet. The content will be derived from the MonetDB types[] */ + private final int[] JdbcSQLTypes; + /** The max string length for each column in this result */ + private final int[] columnLengths; + /** The table for each column in this result */ + private final String[] tableNames; + /** The query sequence number */ + private final int seqnr; + /** A bitmap telling whether the headers are set or not */ + private byte isSet; + /** Whether this Response is closed */ + private boolean closed; + /** The connection belonging for this ResultSetResponse */ + private MonetConnection con; + /** The Connection that we should use when requesting a new block */ + private MonetConnection.ResponseList parent; + /** Whether the fetchSize was explicitly set by the user */ + private boolean cacheSizeSetExplicitly = false; + /** Whether we should send an Xclose command to the server if we close this Response */ + private boolean destroyOnClose; + /** the offset to be used on Xexport queries */ + private int blockOffset = 0; + /** A List of result blocks (chunks of size fetchSize/cacheSize) */ + private final AbstractDataBlockResponse[] resultBlocks; + + /** + * Sole constructor, which requires a MonetConnection parent to be given. + * + * @param con The connection of this ResultSet + * @param parent the parent that created this Response and will supply new result blocks when necessary + * @param id the ID of the result set + * @param seq the query sequence number + * @param rowcount the number of rows in the current block + * @param tuplecount the total number of tuples in the result set + * @param columncount the number of columns in the result set + */ + public ResultSetResponse(MonetConnection con, MonetConnection.ResponseList parent, int id, int seq, int rowcount, + int tuplecount, int columncount) { + this.con = con; + this.parent = parent; + if (parent.getCachesize() == 0) { + /* Below we have to calculate how many "chunks" we need to allocate to store the entire result. However, if + the user didn't set a cache size, as in this case, we need to stick to our defaults. */ + cacheSize = con.getDefFetchsize(); + cacheSizeSetExplicitly = false; + } else { + cacheSize = parent.getCachesize(); + cacheSizeSetExplicitly = true; + } + + /* So far, so good. Now the problem with EXPLAIN, DOT, etc queries is, that they don't support any block + fetching, so we need to always fetch everything at once. For that reason, the cache size is here set to the + rowcount if it's larger, such that we do a full fetch at once. (Because we always set a reply_size, we can + only get a larger rowcount from the server if it doesn't paginate, because it's a pseudo SQL result.) */ + if (rowcount > cacheSize) { + cacheSize = rowcount; + } + this.seqnr = seq; + this.destroyOnClose = id > 0 && tuplecount > rowcount; + this.id = id; + this.rowcount = rowcount; + + int maxrows = parent.getMaxrows(); + this.tuplecount = (maxrows != 0 && tuplecount > maxrows) ? maxrows : tuplecount; + + this.name = new String[columncount]; + this.type = new String[columncount]; + this.tableNames = new String[columncount]; + this.columnLengths = new int[columncount]; + this.JdbcSQLTypes = new int[columncount]; + + this.resultBlocks = new AbstractDataBlockResponse[(tuplecount / cacheSize) + 1]; + this.resultBlocks[0] = con.getProtocol().getAnEmptyDataBlockResponse(rowcount, columncount, + con.getProtocol(), this.JdbcSQLTypes); + } + + /** + * Internal utility method to fill the JdbcSQLTypes array with derivable values. By doing it once (in the + * constructor) we can avoid doing this in many getXyz() methods again and again thereby improving getXyz() method + * performance. + */ + private void populateJdbcSQLTypesArray() { + for (int i = 0; i < this.type.length; i++) { + int javaSQLtype = MonetDriver.getJavaType(this.type[i]); + if (javaSQLtype == Types.BLOB && con.getBlobAsBinary()) { + javaSQLtype = Types.LONGVARBINARY; + } + if (javaSQLtype == Types.CLOB && con.getClobAsLongChar()) { + javaSQLtype = Types.LONGVARCHAR; + } + this.JdbcSQLTypes[i] = javaSQLtype; + } + } + + /** + * Returns whether this ResultSetResponse needs more lines. This method returns true if not all headers are set, + * or the first DataBlockResponse reports to want more. + */ + @Override + public boolean wantsMore() { + return this.isSet < IS_SET_FINAL_VALUE || resultBlocks[0].wantsMore(); + } + + /** + * Adds the given DataBlockResponse to this ResultSetResponse at the given block position. + * + * @param offset the offset number of rows for this block + * @param rowcount the number of rows for this block + */ + public AbstractDataBlockResponse addDataBlockResponse(int offset, int rowcount) { + int block = (offset - blockOffset) / cacheSize; + AbstractDataBlockResponse res = con.getProtocol().getAnEmptyDataBlockResponse(rowcount, + this.getColumncount(), this.con.getProtocol(), JdbcSQLTypes); + resultBlocks[block] = res; + return res; + } + + /** + * Returns this ResultSet id + * + * @return The resultSet id + */ + public int getId() { + return id; + } + + /** + * Returns the number of columns on this ResultSet + * + * @return The number of columns on this ResultSet + */ + public int getColumncount() { + return name.length; + } + + /** + * Returns the number of rows on this ResultSet + * + * @return The number of rows on this ResultSet + */ + public int getTuplecount() { + return tuplecount; + } + + /** + * Returns the number of rows on the current block + * + * @return The number of rows on the current block + */ + public int getRowcount() { + return rowcount; + } + + /** + * Returns the names of the columns + * + * @return The names of the columns + */ + public String[] getNames() { + return name; + } + + /** + * Returns the types of the columns + * + * @return The types of the columns + */ + public String[] getTypes() { + return type; + } + + /** + * Returns the JDBC types of the columns + * + * @return The JDBC types of the columns + */ + public int[] getJdbcSQLTypes() { + return JdbcSQLTypes; + } + + /** + * Returns the tables of the columns + * + * @return The tables of the columns + */ + public String[] getTableNames() { + return tableNames; + } + + /** + * Returns the lengths of the columns + * + * @return The lengths of the columns + */ + public int[] getColumnLengths() { + return columnLengths; + } + + /** + * Returns the cache size used within this Response + * + * @return The cache size + */ + public int getCacheSize() { + return cacheSize; + } + + /** + * Returns the current block offset + * + * @return The current block offset + */ + public int getBlockOffset() { + return blockOffset; + } + + /** + * Returns the ResultSet type, FORWARD_ONLY or not. + * + * @return The ResultSet type + */ + public int getRSType() { + return parent.getRstype(); + } + + /** + * Returns the concurrency of the ResultSet. + * + * @return The ResultSet concurrency + */ + public int getRSConcur() { + return parent.getRsconcur(); + } + + /** + * Gets the next table headers from the underlying protocol, or gets the next rows on to the underlying + * DataResponse if the headers are already retrieved. + * + * @param protocol the connection's protocol + * @throws ProtocolException if has a wrong header + */ + @Override + public void addLines(AbstractProtocol protocol) throws ProtocolException { + if (this.isSet >= IS_SET_FINAL_VALUE) { + this.resultBlocks[0].addLines(protocol); + } else { + int csrh = protocol.getCurrentServerResponse(); + if (csrh != ServerResponses.HEADER) { + throw new ProtocolException("header expected, got: " + protocol.getRemainingStringLine(0)); + } else { + int next = con.getProtocol().getNextTableHeader(this.name, this.columnLengths, this.type, this.tableNames); + this.isSet |= next; + if (this.isSet >= IS_SET_FINAL_VALUE) { + this.populateJdbcSQLTypesArray(); //VERY IMPORTANT to populate the JDBC types array + } + } + } + } + + /** + * Returns a line from the cache. If the line is already present in the cache, it is returned, if not appropriate + * actions are taken to make sure the right block is being fetched and as soon as the requested line is fetched it + * is returned. + * + * @param row the row in the result set to return + * @return the exact row read as requested or null if the requested row is out of the scope of the result set + * @throws SQLException if an database error occurs + */ + public AbstractDataBlockResponse getDataBlockCorrespondingToLine(int row) throws SQLException { + if (row >= tuplecount || row < 0) + return null; + + int block = (row - blockOffset) / cacheSize; + int blockLine = (row - blockOffset) % cacheSize; + + // do we have the right block loaded? (optimistic try) + AbstractDataBlockResponse rawr; + // load block if appropriate + if ((rawr = resultBlocks[block]) == null) { + // TODO: ponder about a maximum number of blocks to keep in memory when dealing with random access to + // reduce memory blow-up + + // if we're running forward only, we can discard the resultset block loaded + if (parent.getRstype() == ResultSet.TYPE_FORWARD_ONLY) { + for (int i = 0; i < block; i++) + resultBlocks[i] = null; + + if (MonetConnection.getSeqCounter() - 1 == seqnr && !cacheSizeSetExplicitly && + tuplecount - row > cacheSize && cacheSize < con.getDefFetchsize() * 10) { + // there has no query been issued after this one, so we can consider this an uninterrupted + // continuation request. Let's once increase the cacheSize as it was not explicitly set, + // since the chances are high that we won't bother anyone else by doing so, and just gaining + // some performance. + + // store the previous position in the blockOffset variable + blockOffset += cacheSize; + + // increase the cache size (a lot) + cacheSize *= 10; + + // by changing the cacheSize, we also change the block measures. Luckily we don't care about + // previous blocks because we have a forward running pointer only. However, we do have to + // recalculate the block number, to ensure the next call to find this new block. + block = (row - blockOffset) / cacheSize; + blockLine = (row - blockOffset) % cacheSize; + } + } + + // ok, need to fetch cache block first + parent.executeQuery(con.getLanguage().getCommandTemplates(), "export " + id + " " + + ((block * cacheSize) + blockOffset) + " " + cacheSize); + rawr = resultBlocks[block]; + if (rawr == null) { + throw new AssertionError("block " + block + " should have been fetched by now :("); + } + } + rawr.setBlockLine(blockLine); + return rawr; + } + + /** + * Closes this Response by sending an Xclose to the server indicating that the result can be closed at the server + * side as well. + */ + @Override + public void close() { + if (closed) return; + // send command to server indicating we're done with this result only if we had an ID in the header and this + // result was larger than the reply size + try { + if (destroyOnClose) { + con.sendControlCommand(ControlCommands.CLOSE, id); + } + } catch (SQLException e) { + // probably a connection error... + } + + // close the data block associated with us + for (AbstractDataBlockResponse r : resultBlocks) { + if (r != null) r.close(); + } + + closed = true; + } + + /** + * Returns whether this Response is closed + * + * @return whether this Response is closed + */ + public boolean isClosed() { + return closed; + } +}
new file mode 100644 --- /dev/null +++ b/src/main/java/nl/cwi/monetdb/mcl/responses/SchemaResponse.java @@ -0,0 +1,32 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * Copyright 1997 - July 2008 CWI, August 2008 - 2017 MonetDB B.V. + */ + +package nl.cwi.monetdb.mcl.responses; + +import java.sql.Statement; + +/** + * The SchemaResponse represents an schema modification response. It is issued on statements like CREATE, DROP or + * ALTER TABLE. This response keeps a field that represents the success state, as defined by JDBC, which is currently in + * MonetDB's case always SUCCESS_NO_INFO. Note that this state is not sent by the server. + * <br /> + * <tt>&3</tt> + * + * @author Fabian Groffen + */ +public class SchemaResponse implements IResponse { + + public int getState() { + return Statement.SUCCESS_NO_INFO; + } + + @Override + public void close() { + // nothing to do here... + } +}
new file mode 100644 --- /dev/null +++ b/src/main/java/nl/cwi/monetdb/mcl/responses/UpdateResponse.java @@ -0,0 +1,60 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * Copyright 1997 - July 2008 CWI, August 2008 - 2017 MonetDB B.V. + */ + +package nl.cwi.monetdb.mcl.responses; + +/** + * The UpdateResponse represents an update statement response. It is issued on an UPDATE, INSERT or DELETE SQL + * statement. This response keeps a count field that represents the affected rows and a field that contains the last + * inserted auto-generated ID, or -1 if not applicable. + * <br /> + * <tt>&2 0 -1</tt> + * + * @author Fabian Groffen, Pedro Ferreira + */ +public class UpdateResponse implements IResponse { + + /** + * The id of the response before this one. + */ + private final int lastid; + + /** + * The number of rows affected by the update statement. + */ + private final int count; + + public UpdateResponse(int lastid, int count) { + // fill the blank finals + this.lastid = lastid; + this.count = count; + } + + /** + * The id of the response before this one. + * + * @return The id of the response before this one + */ + public int getLastid() { + return lastid; + } + + /** + * The number of rows affected by the update statement. + * + * @return The number of rows affected by the update statement + */ + public int getCount() { + return count; + } + + @Override + public void close() { + // nothing to do here... + } +}
--- a/src/main/java/nl/cwi/monetdb/merovingian/Control.java +++ b/src/main/java/nl/cwi/monetdb/merovingian/Control.java @@ -8,10 +8,12 @@ package nl.cwi.monetdb.merovingian; -import nl.cwi.monetdb.mcl.net.MapiSocket; -import nl.cwi.monetdb.mcl.io.*; -import nl.cwi.monetdb.mcl.MCLException; -import nl.cwi.monetdb.mcl.parser.MCLParseException; +import nl.cwi.monetdb.mcl.connection.MCLException; +import nl.cwi.monetdb.mcl.connection.mapi.MapiConnection; +import nl.cwi.monetdb.mcl.protocol.AbstractProtocol; +import nl.cwi.monetdb.mcl.protocol.ProtocolException; +import nl.cwi.monetdb.mcl.protocol.ServerResponses; + import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; @@ -50,8 +52,6 @@ public class Control { /** The passphrase to use when connecting */ private final String passphrase; /** The file we should write MapiSocket debuglog to */ - private String debug; - /** * Constructs a new Control object. @@ -59,26 +59,12 @@ public class Control { * @throws IllegalArgumentException if host, port or passphrase are * null or <= 0 */ - public Control(String host, int port, String passphrase) - throws IllegalArgumentException - { + public Control(String host, int port, String passphrase) throws IllegalArgumentException { this.host = host; this.port = port; this.passphrase = passphrase; } - /** - * Instructs to write a MCL protocol debug log to the given file. - * This affects any newly performed command, and can be changed - * inbetween commands. Passing null to this method disables the - * debug log. - * - * @param filename the filename to write debug information to, or null - */ - public void setDebug(String filename) { - this.debug = filename; - } - private String controlHash(String pass, String salt) { long ho; long h = 0; @@ -109,27 +95,17 @@ public class Control { final static private String RESPONSE_OK = "OK"; - private List<String> sendCommand( - String database, String command, boolean hasOutput) - throws MerovingianException, IOException - { - BufferedMCLReader min; - BufferedMCLWriter mout; - MapiSocket ms = new MapiSocket(); - ms.setDatabase("merovingian"); - ms.setLanguage("control"); - if (debug != null) - ms.debug(debug); + private List<String> sendCommand(String database, String command, boolean hasOutput) + throws MerovingianException, IOException { + + MapiConnection server = new MapiConnection(null, null, "control", false, true, host, port, "merovingian"); + AbstractProtocol protocol = server.getProtocol(); try { - ms.connect(host, port, "monetdb", passphrase); - min = ms.getReader(); - mout = ms.getWriter(); - } catch (MCLParseException e) { - throw new MerovingianException(e.getMessage()); - } catch (MCLException e) { + server.connect("monetdb", passphrase); + } catch (ProtocolException | MCLException e) { throw new MerovingianException(e.getMessage()); } catch (AssertionError e) { // mcl panics - ms.close(); + server.close(); // Try old protocol instead Socket s; @@ -137,8 +113,7 @@ public class Control { BufferedReader in; s = new Socket(host, port); out = new PrintStream(s.getOutputStream()); - in = new BufferedReader( - new InputStreamReader(s.getInputStream())); + in = new BufferedReader(new InputStreamReader(s.getInputStream())); try { /* login ritual, step 1: get challenge from server */ String response = in.readLine(); @@ -189,7 +164,7 @@ public class Control { if (!hasOutput) return null; - ArrayList<String> l = new ArrayList<String>(); + ArrayList<String> l = new ArrayList<>(); while ((response = in.readLine()) != null) { l.add(response); } @@ -201,78 +176,64 @@ public class Control { } } - mout.writeLine(database + " " + command + "\n"); - ArrayList<String> l = new ArrayList<String>(); - String tmpLine = min.readLine(); - int linetype = min.getLineType(); - if (linetype == BufferedMCLReader.ERROR) - throw new MerovingianException(tmpLine.substring(6)); - if (linetype != BufferedMCLReader.RESULT) - throw new MerovingianException("unexpected line: " + tmpLine); - if (!tmpLine.substring(1).equals(RESPONSE_OK)) - throw new MerovingianException(tmpLine.substring(1)); - tmpLine = min.readLine(); - linetype = min.getLineType(); - while (linetype != BufferedMCLReader.PROMPT) { - if (linetype != BufferedMCLReader.RESULT) - throw new MerovingianException("unexpected line: " + - tmpLine); + protocol.writeNextQuery(null,database + " " + command, "\n"); + ArrayList<String> l = new ArrayList<>(); + protocol.waitUntilPrompt(); + int next = protocol.getCurrentServerResponse(); + String line = protocol.getRemainingStringLine(0); - l.add(tmpLine.substring(1)); + if (next == ServerResponses.ERROR) + throw new MerovingianException(line.substring(6)); + if (next != ServerResponses.RESULT) + throw new MerovingianException("unexpected line: " + line); + if (!line.substring(1).equals(RESPONSE_OK)) + throw new MerovingianException(line.substring(1)); - tmpLine = min.readLine(); - linetype = min.getLineType(); + next = protocol.getCurrentServerResponse(); + line = protocol.getRemainingStringLine(0); + while (next != ServerResponses.PROMPT) { + if (next != ServerResponses.RESULT) + throw new MerovingianException("unexpected line: " + line); + + l.add(line.substring(1)); + + next = protocol.getCurrentServerResponse(); + line = protocol.getRemainingStringLine(0); } - ms.close(); + server.close(); return l; } - public void start(String database) - throws MerovingianException, IOException - { + public void start(String database) throws MerovingianException, IOException { sendCommand(database, "start", false); } - public void stop(String database) - throws MerovingianException, IOException - { + public void stop(String database) throws MerovingianException, IOException { sendCommand(database, "stop", false); } - public void kill(String database) - throws MerovingianException, IOException - { + public void kill(String database) throws MerovingianException, IOException { sendCommand(database, "kill", false); } - public void create(String database) - throws MerovingianException, IOException - { + public void create(String database) throws MerovingianException, IOException { sendCommand(database, "create", false); } - public void destroy(String database) - throws MerovingianException, IOException - { + public void destroy(String database) throws MerovingianException, IOException { sendCommand(database, "destroy", false); } - public void lock(String database) - throws MerovingianException, IOException - { + public void lock(String database) throws MerovingianException, IOException { sendCommand(database, "lock", false); } - public void release(String database) - throws MerovingianException, IOException - { + public void release(String database) throws MerovingianException, IOException { sendCommand(database, "release", false); } - public void rename(String database, String newname) - throws MerovingianException, IOException - { + public void rename(String database, String newname) throws MerovingianException, IOException { if (newname == null) newname = ""; /* force error from merovingian */ @@ -291,9 +252,7 @@ public class Control { * @throws IOException if connecting to or communicating with * merovingian failed */ - public void setProperty(String database, String property, String value) - throws MerovingianException, IOException - { + public void setProperty(String database, String property, String value) throws MerovingianException, IOException { /* inherit: set to empty string */ if (value == null) value = ""; @@ -301,15 +260,11 @@ public class Control { sendCommand(database, property + "=" + value, false); } - public void inheritProperty(String database, String property) - throws MerovingianException, IOException - { + public void inheritProperty(String database, String property) throws MerovingianException, IOException { setProperty(database, property, null); } - public Properties getProperties(String database) - throws MerovingianException, IOException - { + public Properties getProperties(String database) throws MerovingianException, IOException { Properties ret = new Properties(); List<String> response = sendCommand(database, "get", true); for (String responseLine : response) { @@ -325,15 +280,11 @@ public class Control { return ret; } - public Properties getDefaultProperties() - throws MerovingianException, IOException - { + public Properties getDefaultProperties() throws MerovingianException, IOException { return(getProperties("#defaults")); } - public SabaothDB getStatus(String database) - throws MerovingianException, IOException - { + public SabaothDB getStatus(String database) throws MerovingianException, IOException { List<String> response = sendCommand(database, "status", true); if (response.isEmpty()) throw new MerovingianException("communication error"); @@ -348,9 +299,7 @@ public class Control { * @throws MerovingianException * @throws IOException */ - public boolean exists(String database) - throws MerovingianException, IOException - { + public boolean exists(String database) throws MerovingianException, IOException { List<SabaothDB> all = getAllStatuses(); for (SabaothDB db : all) { if (db.getName().equals(database)) { @@ -360,10 +309,8 @@ public class Control { return false; } - public List<SabaothDB> getAllStatuses() - throws MerovingianException, IOException - { - List<SabaothDB> l = new ArrayList<SabaothDB>(); + public List<SabaothDB> getAllStatuses() throws MerovingianException, IOException { + List<SabaothDB> l = new ArrayList<>(); List<String> response = sendCommand("#all", "status", true); try { for (String responseLine : response) { @@ -375,10 +322,8 @@ public class Control { return Collections.unmodifiableList(l); } - public List<URI> getAllNeighbours() - throws MerovingianException, IOException - { - List<URI> l = new ArrayList<URI>(); + public List<URI> getAllNeighbours() throws MerovingianException, IOException { + List<URI> l = new ArrayList<>(); List<String> response = sendCommand("anelosimus", "eximius", true); try { for (String responseLine : response) {
--- a/src/main/java/nl/cwi/monetdb/merovingian/SabaothDB.java +++ b/src/main/java/nl/cwi/monetdb/merovingian/SabaothDB.java @@ -24,15 +24,13 @@ import java.util.Date; public class SabaothDB { /** The name of the database */ private String dbname; - /** The URI how to connect to this database, or null if not - * shared */ + /** The URI how to connect to this database, or null if not shared */ private String uri; /** Whether or not the database is under maintenance */ private boolean locked; /** The state of this database, one of SABdbState */ private SABdbState state; - /** A list of Strings representing the available scenarios of this - * database */ + /** A list of Strings representing the available scenarios of this database */ private String[] scenarios; /** The number of times this database was started */ private int startCounter; @@ -64,11 +62,7 @@ public class SabaothDB { /** Sabaoth state enumeration */ public enum SABdbState { - SABdbIllegal (0), - SABdbRunning (1), - SABdbCrashed (2), - SABdbInactive(3), - SABdbStarting(4); + SABdbIllegal (0), SABdbRunning (1), SABdbCrashed (2), SABdbInactive(3), SABdbStarting(4); private final int cValue; @@ -88,8 +82,7 @@ public class SabaothDB { return(s); } } - throw new IllegalArgumentException("No such state with value: " - + val); + throw new IllegalArgumentException("No such state with value: " + val); } } @@ -98,9 +91,7 @@ public class SabaothDB { * * @param sabdb the serialised sabdb C-struct */ - public SabaothDB(String sabdb) - throws IllegalArgumentException - { + public SabaothDB(String sabdb) throws IllegalArgumentException { if (sabdb == null) throw new IllegalArgumentException("String is null"); if (!sabdb.startsWith(sabdbhdr)) @@ -136,7 +127,7 @@ public class SabaothDB { } else { this.uri = parts[pc++]; } - this.locked = parts[pc++].equals("1") ? true : false; + this.locked = parts[pc++].equals("1"); this.state = SABdbState.getInstance(Integer.parseInt(parts[pc++])); this.scenarios = parts[pc++].split("'"); if (protover == 1) /* skip connections */ @@ -170,7 +161,7 @@ public class SabaothDB { } else { this.lastStop = null; } - this.crashAvg1 = parts[pc++].equals("1") ? true : false; + this.crashAvg1 = parts[pc++].equals("1"); this.crashAvg10 = Double.parseDouble(parts[pc++]); this.crashAvg30 = Double.parseDouble(parts[pc++]); }
--- a/src/main/java/nl/cwi/monetdb/util/CmdLineOpts.java +++ b/src/main/java/nl/cwi/monetdb/util/CmdLineOpts.java @@ -20,9 +20,9 @@ import java.util.Properties; public class CmdLineOpts { /** the arguments we handle */ - private Map<String, OptionContainer> opts = new HashMap<String, OptionContainer>(); + private Map<String, OptionContainer> opts = new HashMap<>(); /** the options themself */ - private List<OptionContainer> options = new ArrayList<OptionContainer>(); + private List<OptionContainer> options = new ArrayList<>(); /** no arguments */ public static int CAR_ZERO = 0; @@ -35,24 +35,11 @@ public class CmdLineOpts { /** one or many arguments */ public static int CAR_ONE_MANY = 4; - public CmdLineOpts() { - } + public CmdLineOpts() {} - public void addOption( - String shorta, - String longa, - int type, - String defaulta, - String descriptiona) - throws OptionsException { - OptionContainer oc = - new OptionContainer( - shorta, - longa, - type, - defaulta, - descriptiona - ); + public void addOption(String shorta, String longa, int type, String defaulta, String descriptiona) + throws OptionsException { + OptionContainer oc = new OptionContainer(shorta, longa, type, defaulta, descriptiona); if (shorta != null) opts.put(shorta, oc); if (longa != null) opts.put(longa, oc); } @@ -102,7 +89,7 @@ public class CmdLineOpts { OptionContainer option = null; int quant = -1; int qcount = 0; - boolean moreData = false; + boolean moreData; for (int i = 0; i < args.length; i++) { if (option == null) { if (args[i].charAt(0) != '-') throw @@ -146,7 +133,7 @@ public class CmdLineOpts { // single char argument option = opts.get("" + args[i].charAt(1)); // is there more data left in the argument? - moreData = args[i].length() > 2 ? true : false; + moreData = args[i].length() > 2; } if (option != null) { @@ -288,20 +275,14 @@ public class CmdLineOpts { int cardinality; String shorta; String longa; - List<String> values = new ArrayList<String>(); + List<String> values = new ArrayList<>(); String name; String defaulta; String descriptiona; boolean present; - public OptionContainer( - String shorta, - String longa, - int cardinality, - String defaulta, - String descriptiona) - throws IllegalArgumentException - { + public OptionContainer(String shorta, String longa, int cardinality, String defaulta, String descriptiona) + throws IllegalArgumentException { this.cardinality = cardinality; this.shorta = shorta; this.longa = longa; @@ -309,11 +290,8 @@ public class CmdLineOpts { this.descriptiona = descriptiona; this.present = false; - if (cardinality != CAR_ZERO && - cardinality != CAR_ONE && - cardinality != CAR_ZERO_ONE && - cardinality != CAR_ZERO_MANY && - cardinality != CAR_ONE_MANY) + if (cardinality != CAR_ZERO && cardinality != CAR_ONE && cardinality != CAR_ZERO_ONE && + cardinality != CAR_ZERO_MANY && cardinality != CAR_ONE_MANY) throw new IllegalArgumentException("unknown cardinality"); if (shorta != null && shorta.length() != 1) throw new IllegalArgumentException("short option should consist of exactly one character");
--- a/src/main/java/nl/cwi/monetdb/util/Exporter.java +++ b/src/main/java/nl/cwi/monetdb/util/Exporter.java @@ -22,12 +22,8 @@ public abstract class Exporter { this.out = out; } - public abstract void dumpSchema( - DatabaseMetaData dbmd, - String type, - String catalog, - String schema, - String name) throws SQLException; + public abstract void dumpSchema(DatabaseMetaData dbmd, String type, String catalog, String schema, String name) + throws SQLException; public abstract void dumpResultSet(ResultSet rs) throws SQLException;
--- a/src/main/java/nl/cwi/monetdb/util/Extract.java +++ b/src/main/java/nl/cwi/monetdb/util/Extract.java @@ -17,8 +17,7 @@ import java.io.InputStreamReader; /** - * This file contains a function to extract files from its including Jar - * package. + * This file contains a function to extract files from its including Jar package. * * @author Ying Zhang <Y.Zhang@cwi.nl> * @version 0.1 @@ -27,30 +26,22 @@ import java.io.InputStreamReader; public class Extract { private static final int DEFAULT_BUFSIZE = 16386; - public Extract() {} - /** - * Extracts a file from the Jar package which includes this class to - * the given destination - * @param fromFile The file to extract, including it absolute path - * inside its including Jar package. + * Extracts a file from the Jar package which includes this class to the given destination + * @param fromFile The file to extract, including it absolute path inside its including Jar package. * @param toFile Destination for the extracted file * @throws FileNotFoundException If the file to extract can not be * found in its including Jar package. - * @throws IOException If any error happens during - * creating/reading/writing files. + * @throws IOException If any error happens during creating/reading/writing files. */ - public static void extractFile(String fromFile, String toFile) - throws FileNotFoundException, IOException - { + public static void extractFile(String fromFile, String toFile) throws FileNotFoundException, IOException { char[] cbuf = new char[DEFAULT_BUFSIZE]; - int ret = 0; + int ret; - InputStream is = new Extract().getClass().getResourceAsStream(fromFile); + InputStream is = Extract.class.getResourceAsStream(fromFile); if(is == null) { - throw new FileNotFoundException("File " + fromFile + - " does not exist in the JAR package."); + throw new FileNotFoundException("File " + fromFile + " does not exist in the JAR package."); } BufferedReader reader = new BufferedReader(new InputStreamReader(is));
--- a/src/main/java/nl/cwi/monetdb/util/SQLExporter.java +++ b/src/main/java/nl/cwi/monetdb/util/SQLExporter.java @@ -47,14 +47,8 @@ public class SQLExporter extends Exporte * @param name the table to describe in SQL CREATE format (not null) * @throws SQLException if a database related error occurs */ - public void dumpSchema( - DatabaseMetaData dbmd, - String type, - String catalog, - String schema, - String name) - throws SQLException - { + public void dumpSchema(DatabaseMetaData dbmd, String type, String catalog, String schema, String name) + throws SQLException { assert dbmd != null; assert type != null; assert schema != null; @@ -66,7 +60,7 @@ public class SQLExporter extends Exporte changeSchema(schema); // handle views directly - if (type.indexOf("VIEW") != -1) { + if (type.contains("VIEW")) { String[] types = new String[1]; types[0] = type; ResultSet tbl = dbmd.getTables(catalog, schema, name, types); @@ -116,22 +110,28 @@ public class SQLExporter extends Exporte s = cols.getString(colTypeNmIndex).toUpperCase(); // do some data type substitutions to match SQL standard - if (s.equals("INT")) { - s = "INTEGER"; - } else if (s.equals("SEC_INTERVAL")) { - s = "INTERVAL SECOND"; - } else if (s.equals("MONTH_INTERVAL")) { - s = "INTERVAL MONTH"; - } else if (s.equals("TIMETZ")) { - s = "TIME"; - // small hack to get desired behaviour: set digits when we have - // a time with time zone and at the same time masking the internal types - digits = 1; - } else if (s.equals("TIMESTAMPTZ")) { - s = "TIMESTAMP"; - // small hack to get desired behaviour: set digits when we have - // a timestamp with time zone and at the same time masking the internal types - digits = 1; + switch (s) { + case "INT": + s = "INTEGER"; + break; + case "SEC_INTERVAL": + s = "INTERVAL SECOND"; + break; + case "MONTH_INTERVAL": + s = "INTERVAL MONTH"; + break; + case "TIMETZ": + s = "TIME"; + // small hack to get desired behaviour: set digits when we have + // a time with time zone and at the same time masking the internal types + digits = 1; + break; + case "TIMESTAMPTZ": + s = "TIMESTAMP"; + // small hack to get desired behaviour: set digits when we have + // a timestamp with time zone and at the same time masking the internal types + digits = 1; + break; } sb.append(s); // add the data type for this column @@ -185,9 +185,9 @@ public class SQLExporter extends Exporte // key sequence order. So we have to sort ourself :( cols = dbmd.getPrimaryKeys(catalog, schema, name); // first make an 'index' of the KEY_SEQ column - SortedMap<Integer, Integer> seqIndex = new TreeMap<Integer, Integer>(); + SortedMap<Integer, Integer> seqIndex = new TreeMap<>(); for (i = 1; cols.next(); i++) { - seqIndex.put(Integer.valueOf(cols.getInt("KEY_SEQ")), Integer.valueOf(i)); + seqIndex.put(cols.getInt("KEY_SEQ"), i); } if (seqIndex.size() > 0) { // terminate the previous line @@ -200,7 +200,7 @@ public class SQLExporter extends Exporte it.hasNext(); i++) { Map.Entry<Integer, Integer> e = it.next(); - cols.absolute(e.getValue().intValue()); + cols.absolute(e.getValue()); if (i > 0) out.print(", "); out.print(dq(cols.getString("COLUMN_NAME"))); @@ -241,9 +241,9 @@ public class SQLExporter extends Exporte out.print("\tCONSTRAINT " + dq(cols.getString("FK_NAME")) + " FOREIGN KEY ("); boolean next; - Set<String> fk = new LinkedHashSet<String>(); + Set<String> fk = new LinkedHashSet<>(); fk.add(cols.getString("FKCOLUMN_NAME").intern()); - Set<String> pk = new LinkedHashSet<String>(); + Set<String> pk = new LinkedHashSet<>(); pk.add(cols.getString("PKCOLUMN_NAME").intern()); while ((next = cols.next()) && @@ -363,12 +363,9 @@ public class SQLExporter extends Exporte * format. * * @param rs the ResultSet to convert into INSERT INTO statements - * @param absolute if true, dumps table name prepended with schema name * @throws SQLException if a database related error occurs */ - private void resultSetToSQL(ResultSet rs) - throws SQLException - { + private void resultSetToSQL(ResultSet rs) throws SQLException { ResultSetMetaData rsmd = rs.getMetaData(); String statement = "INSERT INTO "; if (!useSchema) { @@ -520,7 +517,7 @@ public class SQLExporter extends Exporte private void changeSchema(String schema) { if (lastSchema == null) { - lastSchema = new Stack<String>(); + lastSchema = new Stack<>(); lastSchema.push(null); }
--- a/src/main/java/nl/cwi/monetdb/util/SQLRestore.java +++ b/src/main/java/nl/cwi/monetdb/util/SQLRestore.java @@ -14,11 +14,11 @@ import java.io.FileReader; import java.io.IOException; import java.util.concurrent.atomic.AtomicBoolean; -import nl.cwi.monetdb.mcl.MCLException; -import nl.cwi.monetdb.mcl.io.BufferedMCLReader; -import nl.cwi.monetdb.mcl.io.BufferedMCLWriter; -import nl.cwi.monetdb.mcl.net.MapiSocket; -import nl.cwi.monetdb.mcl.parser.MCLParseException; +import nl.cwi.monetdb.mcl.connection.MCLException; +import nl.cwi.monetdb.mcl.connection.mapi.MapiConnection; +import nl.cwi.monetdb.mcl.protocol.AbstractProtocol; +import nl.cwi.monetdb.mcl.protocol.ProtocolException; +import nl.cwi.monetdb.mcl.protocol.ServerResponses; /** * Use this class to restore an SQL dump file. @@ -41,40 +41,40 @@ public class SQLRestore { _dbName = dbName; } - private static class ServerResponseReader implements Runnable { - private final BufferedMCLReader _is; + private class ServerResponseReader implements Runnable { + private final MapiConnection _is; private final AtomicBoolean _errorState = new AtomicBoolean(false); private String _errorMessage = null; - ServerResponseReader(BufferedMCLReader is) { + ServerResponseReader(MapiConnection is) { _is = is; } public void run() { + AbstractProtocol protocol = _is.getProtocol(); + int next; + String line; try { while (true) { - String line = _is.readLine(); + protocol.waitUntilPrompt(); + next = protocol.getCurrentServerResponse(); + line = protocol.getRemainingStringLine(0); if (line == null) break; - int result = _is.getLineType(); - switch (result) { - case BufferedMCLReader.ERROR: - _errorMessage = line; - _errorState.set(true); - return; - default: - // do nothing... + switch (next) { + case ServerResponses.ERROR: + _errorMessage = line; + _errorState.set(true); + return; + default: + // do nothing... } } } catch (IOException e) { _errorMessage = e.getMessage(); _errorState.set(true); } finally { - try { - _is.close(); - } catch (IOException e) { - // ignore errors - } + _is.close(); } } @@ -102,34 +102,26 @@ public class SQLRestore { * @throws IOException */ public void restore(File source) throws IOException { - MapiSocket ms = new MapiSocket(); + MapiConnection server = new MapiConnection(null,null, "sql", false, true, _host, _port, _dbName); try { - ms.setLanguage("sql"); - ms.setDatabase(_dbName); - ms.connect(_host, _port, _user, _password); + server.connect(_user, _password); - BufferedMCLWriter os = ms.getWriter(); - BufferedMCLReader reader = ms.getReader(); - - ServerResponseReader srr = new ServerResponseReader(reader); - + ServerResponseReader srr = new ServerResponseReader(server); Thread responseReaderThread = new Thread(srr); responseReaderThread.start(); try { // FIXME: we assume here that the dump is in system's default encoding BufferedReader sourceData = new BufferedReader(new FileReader(source)); try { - os.write('s'); // signal that a new statement (or series of) is coming + AbstractProtocol protocol = server.getProtocol(); + protocol.writeNextQuery(null, "s", null); // signal that a new statement (or series of) is coming while(!srr.inErrorState()) { char[] buf = new char[4096]; int result = sourceData.read(buf); if (result < 0) break; - os.write(buf, 0, result); + protocol.writeNextQuery(null, new String(buf, 0, result), null); } - - os.flush(); // mark the end of the statement (or series of) - os.close(); } finally { sourceData.close(); } @@ -145,12 +137,10 @@ public class SQLRestore { throw new IOException(srr.getErrorMessage()); } } - } catch (MCLException e) { - throw new IOException(e.getMessage()); - } catch (MCLParseException e) { + } catch (MCLException | ProtocolException e) { throw new IOException(e.getMessage()); } finally { - ms.close(); + server.close(); } } @@ -158,7 +148,6 @@ public class SQLRestore { // do nothing at the moment... } - public static void main(String[] args) throws IOException { if (args.length != 6) { System.err.println("USAGE: java " + SQLRestore.class.getName() +
--- a/src/main/java/nl/cwi/monetdb/util/XMLExporter.java +++ b/src/main/java/nl/cwi/monetdb/util/XMLExporter.java @@ -30,15 +30,9 @@ public class XMLExporter extends Exporte super(out); } - public void dumpSchema( - DatabaseMetaData dbmd, - String type, - String catalog, - String schema, - String name) - throws SQLException - { - if (type.indexOf("VIEW") != -1) { + public void dumpSchema(DatabaseMetaData dbmd, String type, String catalog, String schema, String name) + throws SQLException { + if (type.contains("VIEW")) { String[] types = new String[1]; types[0] = type; ResultSet tbl = dbmd.getTables(catalog, schema, name, types); @@ -57,7 +51,7 @@ public class XMLExporter extends Exporte ResultSet cols = dbmd.getColumns(catalog, schema, name, null); String ident; - Set<String> types = new HashSet<String>(); + Set<String> types = new HashSet<>(); // walk through the ResultSet and create the types // for a bit of a clue on the types, see this url: // http://books.xmlschemata.org/relaxng/relax-CHP-19.html @@ -327,10 +321,8 @@ public class XMLExporter extends Exporte out.println("</xsd:schema>"); } - private final static SimpleDateFormat xsd_ts = - new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"); - private final static SimpleDateFormat xsd_tstz = - new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ"); + private final static SimpleDateFormat xsd_ts = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"); + private final static SimpleDateFormat xsd_tstz = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ"); /** * Generates an XML representation of the given ResultSet. @@ -351,9 +343,9 @@ public class XMLExporter extends Exporte case Types.TIMESTAMP: Timestamp ts = rs.getTimestamp(i); if ("timestamptz".equals(rsmd.getColumnTypeName(i))) { - data = xsd_tstz.format(ts).toString(); + data = xsd_tstz.format(ts); } else { - data = xsd_ts.format(ts).toString(); + data = xsd_ts.format(ts); } break; default:
--- a/tests/Bug_PrepStmtSetString_6382.java +++ b/tests/Bug_PrepStmtSetString_6382.java @@ -7,8 +7,9 @@ */ import java.sql.*; -import nl.cwi.monetdb.jdbc.types.INET; -import nl.cwi.monetdb.jdbc.types.URL; + +import nl.cwi.monetdb.jdbc.MonetINET; +import nl.cwi.monetdb.jdbc.MonetURL; public class Bug_PrepStmtSetString_6382 { public static void main(String[] args) throws Exception { @@ -98,11 +99,9 @@ public class Bug_PrepStmtSetString_6382 pstmt.setObject(2, "row " + row); pstmt.setObject(3, null); pstmt.setObject(4, java.util.UUID.fromString("ff125769-b63c-4c3c-859f-5b84a9349e24")); - URL myURL = new URL(); - myURL.fromString("https://en.wikipedia.org/wiki/IP_address"); + MonetURL myURL = new MonetURL("https://en.wikipedia.org/wiki/IP_address"); pstmt.setObject(5, myURL); - INET myINET = new INET(); - myINET.fromString("223.234.245.255"); + MonetINET myINET = new MonetINET("223.234.245.255"); pstmt.setObject(6, myINET); System.out.println("Inserting row " + row); inserted = pstmt.executeUpdate();
--- a/tests/Test_Clargequery.java +++ b/tests/Test_Clargequery.java @@ -44,7 +44,7 @@ public class Test_Clargequery { System.out.print("1. sending"); stmt1.execute(bigq.toString()); int i = 1; // we skip the first "getResultSet()" - while (stmt1.getMoreResults() != false) { + while (stmt1.getMoreResults()) { i++; } if (stmt1.getUpdateCount() != -1) {
--- a/tests/Test_Cmanycon.java +++ b/tests/Test_Cmanycon.java @@ -12,7 +12,7 @@ import java.util.*; public class Test_Cmanycon { public static void main(String[] args) throws Exception { // Class.forName("nl.cwi.monetdb.jdbc.MonetDriver"); // not needed anymore for self registering JDBC drivers - List cons = new ArrayList(100); // Connections go in here + List<Connection> cons = new ArrayList<>(100); // Connections go in here try { // spawn a lot of Connections, just for fun...
--- a/tests/Test_Int128.java +++ b/tests/Test_Int128.java @@ -20,10 +20,8 @@ public class Test_Int128 { public static void main(String[] args) throws Exception { // Class.forName("nl.cwi.monetdb.jdbc.MonetDriver"); // not needed anymore for self registering JDBC drivers Connection con = DriverManager.getConnection(args[0]); - BigInteger bi = new BigInteger( - "123456789012345678909876543210987654321"); - BigDecimal bd = new BigDecimal( - "1234567890123456789.9876543210987654321"); + BigInteger bi = new BigInteger("123456789012345678909876543210987654321"); + BigDecimal bd = new BigDecimal("1234567890123456789.9876543210987654321"); try { con.setAutoCommit(false); Statement s = con.createStatement();
--- a/tests/Test_PSmanycon.java +++ b/tests/Test_PSmanycon.java @@ -12,7 +12,7 @@ import java.util.*; public class Test_PSmanycon { public static void main(String[] args) throws Exception { // Class.forName("nl.cwi.monetdb.jdbc.MonetDriver"); // not needed anymore for self registering JDBC drivers - List pss = new ArrayList(100); // Connections go in here + List<PreparedStatement> pss = new ArrayList<>(100); // Connections go in here try { // spawn a lot of Connections, just for fun... @@ -51,7 +51,7 @@ public class Test_PSmanycon { Connection con = DriverManager.getConnection(args[0]); Statement stmt = con.createStatement(); try { - int affrows = stmt.executeUpdate("update foo where bar is wrong"); + stmt.executeUpdate("update foo where bar is wrong"); System.out.println("oops, faulty statement just got through :("); } catch (SQLException e) { System.out.println("Forced transaction failure");
--- a/tests/Test_PSmetadata.java +++ b/tests/Test_PSmetadata.java @@ -14,9 +14,8 @@ public class Test_PSmetadata { Connection con = DriverManager.getConnection(args[0]); Statement stmt = con.createStatement(); PreparedStatement pstmt; - ResultSet rs = null; - ResultSetMetaData rsmd = null; - ParameterMetaData pmd = null; + ResultSetMetaData rsmd; + ParameterMetaData pmd; con.setAutoCommit(false); // >> false: auto commit should be off now @@ -24,7 +23,6 @@ public class Test_PSmetadata { try { stmt.executeUpdate("CREATE TABLE table_Test_PSmetadata ( myint int, mydouble double, mybool boolean, myvarchar varchar(15), myclob clob )"); - // all NULLs stmt.executeUpdate("INSERT INTO table_Test_PSmetadata VALUES (NULL, NULL, NULL, NULL, NULL)"); // all filled in
--- a/tests/Test_PSsqldata.java +++ b/tests/Test_PSsqldata.java @@ -6,8 +6,10 @@ * Copyright 1997 - July 2008 CWI, August 2008 - 2017 MonetDB B.V. */ +import java.net.URL; import java.sql.*; -import nl.cwi.monetdb.jdbc.types.*; + +import nl.cwi.monetdb.jdbc.MonetINET; public class Test_PSsqldata { public static void main(String[] args) throws Exception { @@ -15,9 +17,9 @@ public class Test_PSsqldata { Connection con = DriverManager.getConnection(args[0]); Statement stmt = con.createStatement(); PreparedStatement pstmt; - ResultSet rs = null; - ResultSetMetaData rsmd = null; - ParameterMetaData pmd = null; + ResultSet rs; + ResultSetMetaData rsmd ; + ParameterMetaData pmd; con.setAutoCommit(false); // >> false: auto commit should be off now @@ -37,11 +39,8 @@ public class Test_PSsqldata { System.out.println("\tclassname " + pmd.getParameterClassName(col)); } - INET tinet = new INET(); - URL turl = new URL(); - - tinet.fromString("172.5.5.5/24"); - turl.fromString("http://www.monetdb.org/"); + MonetINET tinet = new MonetINET("172.5.5.5/24"); + URL turl = new URL("http://www.monetdb.org/"); pstmt.setObject(1, tinet); pstmt.setObject(2, turl); pstmt.execute(); @@ -59,13 +58,13 @@ public class Test_PSsqldata { System.out.println("" + i + ".\t<null>"); } else { System.out.println("" + i + ".\t" + x.toString()); - if (x instanceof INET) { - INET inet = (INET)x; + if (x instanceof MonetINET) { + MonetINET inet = (MonetINET)x; System.out.println("\t" + inet.getAddress() + "/" + inet.getNetmaskBits()); System.out.println("\t" + inet.getInetAddress().toString()); } else if (x instanceof URL) { URL url = (URL)x; - System.out.println("\t" + url.getURL().toString()); + System.out.println("\t" + url.toString()); } } }
--- a/tests/Test_PStimedate.java +++ b/tests/Test_PStimedate.java @@ -14,7 +14,7 @@ public class Test_PStimedate { Connection con = DriverManager.getConnection(args[0]); Statement stmt = con.createStatement(); PreparedStatement pstmt; - ResultSet rs = null; + ResultSet rs; //DatabaseMetaData dbmd = con.getMetaData(); con.setAutoCommit(false);
--- a/tests/Test_PStimezone.java +++ b/tests/Test_PStimezone.java @@ -22,7 +22,7 @@ public class Test_PStimezone { Connection con = DriverManager.getConnection(args[0]); Statement stmt = con.createStatement(); PreparedStatement pstmt; - ResultSet rs = null; + ResultSet rs; //DatabaseMetaData dbmd = con.getMetaData(); con.setAutoCommit(false);
--- a/tests/Test_PStypes.java +++ b/tests/Test_PStypes.java @@ -23,22 +23,21 @@ public class Test_PStypes { try { stmt.executeUpdate( -"CREATE TABLE htmtest (" + -" htmid bigint NOT NULL," + -" ra double ," + -" decl double ," + -" dra double ," + -" ddecl double ," + -" flux double ," + -" dflux double ," + -" freq double ," + -" bw double ," + -" type decimal(1,0)," + -" imageurl varchar(100)," + -" comment varchar(100)," + -" CONSTRAINT htmtest_htmid_pkey PRIMARY KEY (htmid)" + -")" -); + "CREATE TABLE htmtest (" + + " htmid bigint NOT NULL," + + " ra double ," + + " decl double ," + + " dra double ," + + " ddecl double ," + + " flux double ," + + " dflux double ," + + " freq double ," + + " bw double ," + + " type decimal(1,0)," + + " imageurl varchar(100)," + + " comment varchar(100)," + + " CONSTRAINT htmtest_htmid_pkey PRIMARY KEY (htmid)" + + ")"); // index is not used, but the original bug had it too stmt.executeUpdate("CREATE INDEX htmid ON htmtest (htmid)"); } catch (SQLException e) { @@ -49,9 +48,7 @@ public class Test_PStypes { } try { - pstmt = con.prepareStatement( -"INSERT INTO HTMTEST (HTMID,RA,DECL,FLUX,COMMENT) VALUES (?,?,?,?,?)" -); + pstmt = con.prepareStatement("INSERT INTO HTMTEST (HTMID,RA,DECL,FLUX,COMMENT) VALUES (?,?,?,?,?)"); System.out.print("1. inserting a record..."); pstmt.setLong(1, 1L); @@ -64,9 +61,7 @@ public class Test_PStypes { System.out.println("success :)"); // try an update like bug #1757923 - pstmt = con.prepareStatement( -"UPDATE HTMTEST set COMMENT=?, TYPE=? WHERE HTMID=?" -); + pstmt = con.prepareStatement("UPDATE HTMTEST set COMMENT=?, TYPE=? WHERE HTMID=?"); System.out.print("2. updating record..."); pstmt.setString(1, "some update");
--- a/tests/Test_Rbooleans.java +++ b/tests/Test_Rbooleans.java @@ -13,7 +13,7 @@ public class Test_Rbooleans { // Class.forName("nl.cwi.monetdb.jdbc.MonetDriver"); // not needed anymore for self registering JDBC drivers Connection con = DriverManager.getConnection(args[0]); Statement stmt = con.createStatement(); - ResultSet rs = null; + ResultSet rs; //DatabaseMetaData dbmd = con.getMetaData(); con.setAutoCommit(false);
--- a/tests/Test_Rmetadata.java +++ b/tests/Test_Rmetadata.java @@ -13,8 +13,8 @@ public class Test_Rmetadata { // Class.forName("nl.cwi.monetdb.jdbc.MonetDriver"); // not needed anymore for self registering JDBC drivers Connection con = DriverManager.getConnection(args[0]); Statement stmt = con.createStatement(); - ResultSet rs = null; - ResultSetMetaData rsmd = null; + ResultSet rs; + ResultSetMetaData rsmd; //DatabaseMetaData dbmd = con.getMetaData(); con.setAutoCommit(false); @@ -23,7 +23,6 @@ public class Test_Rmetadata { try { stmt.executeUpdate("CREATE TABLE table_Test_Rmetadata ( myint int, mydouble double, mybool boolean, myvarchar varchar(15), myclob clob )"); - // all NULLs stmt.executeUpdate("INSERT INTO table_Test_Rmetadata VALUES (NULL, NULL, NULL, NULL, NULL)"); // all filled in
--- a/tests/Test_Rsqldata.java +++ b/tests/Test_Rsqldata.java @@ -6,16 +6,18 @@ * Copyright 1997 - July 2008 CWI, August 2008 - 2017 MonetDB B.V. */ +import java.net.URL; import java.sql.*; -import nl.cwi.monetdb.jdbc.types.*; + +import nl.cwi.monetdb.jdbc.MonetINET; public class Test_Rsqldata { public static void main(String[] args) throws Exception { // Class.forName("nl.cwi.monetdb.jdbc.MonetDriver"); // not needed anymore for self registering JDBC drivers Connection con = DriverManager.getConnection(args[0]); Statement stmt = con.createStatement(); - ResultSet rs = null; - ResultSetMetaData rsmd = null; + ResultSet rs; + ResultSetMetaData rsmd; con.setAutoCommit(false); // >> false: auto commit should be off now @@ -51,13 +53,13 @@ public class Test_Rsqldata { System.out.println("" + i + ".\t<null>"); } else { System.out.println("" + i + ".\t" + x.toString()); - if (x instanceof INET) { - INET inet = (INET)x; + if (x instanceof MonetINET) { + MonetINET inet = (MonetINET)x; System.out.println("\t" + inet.getAddress() + "/" + inet.getNetmaskBits()); System.out.println("\t" + inet.getInetAddress().toString()); } else if (x instanceof URL) { URL url = (URL)x; - System.out.println("\t" + url.getURL().toString()); + System.out.println("\t" + url.toString()); } } }
--- a/tests/Test_Rtimedate.java +++ b/tests/Test_Rtimedate.java @@ -13,7 +13,7 @@ public class Test_Rtimedate { // Class.forName("nl.cwi.monetdb.jdbc.MonetDriver"); // not needed anymore for self registering JDBC drivers Connection con = DriverManager.getConnection(args[0]); Statement stmt = con.createStatement(); - ResultSet rs = null; + ResultSet rs; //DatabaseMetaData dbmd = con.getMetaData(); con.setAutoCommit(false);
--- a/tests/Test_Smoreresults.java +++ b/tests/Test_Smoreresults.java @@ -21,22 +21,22 @@ public class Test_Smoreresults { try { System.out.print("1. more results?..."); - if (stmt.getMoreResults() != false || stmt.getUpdateCount() != -1) + if (stmt.getMoreResults() || stmt.getUpdateCount() != -1) throw new SQLException("more results on an unitialised Statement, how can that be?"); System.out.println(" nope :)"); System.out.print("2. SELECT 1..."); - if (stmt.execute("SELECT 1;") == false) + if (!stmt.execute("SELECT 1;")) throw new SQLException("SELECT 1 returns update or no results"); System.out.println(" ResultSet :)"); System.out.print("3. more results?..."); - if (stmt.getMoreResults() != false || stmt.getUpdateCount() != -1) + if (stmt.getMoreResults() || stmt.getUpdateCount() != -1) throw new SQLException("more results after SELECT 1 query, how can that be?"); System.out.println(" nope :)"); System.out.print("4. even more results?..."); - if (stmt.getMoreResults() != false || stmt.getUpdateCount() != -1) + if (stmt.getMoreResults() || stmt.getUpdateCount() != -1) throw new SQLException("more results after SELECT 1 query, how can that be?"); System.out.println(" nope :)");
--- a/tests/build.xml +++ b/tests/build.xml @@ -17,7 +17,6 @@ Copyright 1997 - July 2008 CWI, August 2 <project name="JDBCTests" default="compile" basedir="."> - <property file="build.local.properties" /> <property file="build.properties" /> <property file="../build.properties" /> <!-- included for version --> @@ -26,7 +25,7 @@ Copyright 1997 - July 2008 CWI, August 2 <property name="builddir" value="build" /> <property name="jardir" value="../jars" /> <property name="jdbc_jar" - value="${jardir}/monetdb-jdbc-${JDBC_MAJOR}.${JDBC_MINOR}.jar" /> + value="${jardir}/monetdb-jdbc-new-${JDBC_MAJOR}.${JDBC_MINOR}.jar" /> <property name="jdbc_url" value="jdbc:monetdb://localhost/?user=monetdb&password=monetdb${debug}" /> <property name="jdbctests-jar"