--- jdbc/Makefile Mon May 15 21:32:51 2000 +++ jdbc/Makefile Mon Sep 4 00:21:39 2000 @@ -4,7 +4,7 @@ # Makefile for Java JDBC interface # # IDENTIFICATION -# $Id: Makefile,v 1.22 2000/05/15 21:32:51 peter Exp $ +# $Id: Makefile,v 1.1 2000/09/04 00:06:02 william Exp william $ # #------------------------------------------------------------------------- @@ -226,7 +226,8 @@ # These are really test classes not true examples TESTS= example/metadata.class \ - example/threadsafe.class + example/threadsafe.class \ + example/Unicode.class # Non functional/obsolete examples # example/datestyle.class \ @@ -266,6 +267,7 @@ @echo The following tests have been built: @echo " example.metadata Tests various metadata methods" @echo " example.threadsafe Tests the driver's thread safety" + @echo " example.Unicode Tests unicode charset support" @echo ------------------------------------------------------------ @echo @@ -276,6 +278,7 @@ example/ImageViewer.class: example/ImageViewer.java example/threadsafe.class: example/threadsafe.java example/metadata.class: example/metadata.java +example/Unicode.class: example/Unicode.java ####################################################################### # --- jdbc/example/Unicode.java Thu Jan 1 00:00:00 1970 +++ jdbc/example/Unicode.java Mon Sep 4 01:07:36 2000 @@ -0,0 +1,240 @@ +package example; + +import java.io.*; +import java.sql.*; +import java.util.*; + +/** + * Test inserting and extracting Unicode-encoded strings. + * + * Synopsis: + * example.Unicode + * where must specify an existing database to which and + * give access and which has UNICODE as its encoding. + * (To create a database with UNICODE encoding, you need to compile + * postgres with "--enable-multibyte" and run createdb with the + * flag "-E UNICODE".) + * + * This test only produces output on error. + * + * @author William Webber + */ +public class Unicode { + + /** + * The url for the database to connect to. + */ + private String url; + + /** + * The user to connect as. + */ + private String user; + + /** + * The password to connect with. + */ + private String password; + + private static void usage() { + log("usage: example.Unicode "); + } + + private static void log(String message) { + System.err.println(message); + } + + private static void log(String message, Exception e) { + System.err.println(message); + e.printStackTrace(); + } + + + public Unicode(String url, String user, String password) { + this.url = url; + this.user = user; + this.password = password; + } + + /** + * Establish and return a connection to the database. + */ + private Connection getConnection() throws SQLException, + ClassNotFoundException { + Class.forName("org.postgresql.Driver"); + Properties info = new Properties(); + info.put("user", user); + info.put("password", password); + info.put("charSet", "utf-8"); + return DriverManager.getConnection(url, info); + } + + /** + * Get string representing a block of 256 consecutive unicode characters. + * We exclude the null character, "'", and "\". + */ + private String getSqlSafeUnicodeBlock(int blockNum) { + if (blockNum < 0 || blockNum > 255) + throw new IllegalArgumentException("blockNum must be from 0 to " + + "255: " + blockNum); + StringBuffer sb = new StringBuffer(256); + int blockFirst = blockNum * 256; + int blockLast = blockFirst + 256; + for (int i = blockFirst; i < blockLast; i++) { + char c = (char) i; + if (c == '\0' || c == '\'' || c == '\\') + continue; + sb.append(c); + } + return sb.toString(); + } + + /** + * Is the block a block of valid unicode values. + * d800 to db7f is the "unassigned high surrogate" range. + * db80 to dbff is the "private use" range. + * These should not be used in actual Unicode strings; + * at least, jdk1.2 will not convert them to utf-8. + */ + private boolean isValidUnicodeBlock(int blockNum) { + if (blockNum >= 0xd8 && blockNum <= 0xdb) + return false; + else + return true; + } + + /** + * Report incorrect block retrieval. + */ + private void reportRetrievalError(int blockNum, String block, + String retrieved) { + String message = "Block " + blockNum + " returned incorrectly: "; + int i = 0; + for (i = 0; i < block.length(); i++) { + if (i >= retrieved.length()) { + message += "too short"; + break; + } else if (retrieved.charAt(i) != block.charAt(i)) { + message += + "first changed character at position " + i + ", sent as 0x" + + Integer.toHexString((int) block.charAt(i)) + + ", retrieved as 0x" + + Integer.toHexString ((int) retrieved.charAt(i)); + break; + } + } + if (i >= block.length()) + message += "too long"; + log(message); + } + + /** + * Do the testing. + */ + public void runTest() { + Connection connection = null; + Statement statement = null; + int blockNum = 0; + final int CREATE = 0; + final int INSERT = 1; + final int SELECT = 2; + final int LIKE = 3; + int mode = CREATE; + try { + connection = getConnection(); + statement = connection.createStatement(); + statement.executeUpdate("CREATE TABLE test_unicode " + + "( blockNum INT PRIMARY KEY, " + + "block TEXT );"); + mode = INSERT; + for (blockNum = 0; blockNum < 256; blockNum++) { + if (isValidUnicodeBlock(blockNum)) { + String block = getSqlSafeUnicodeBlock(blockNum); + statement.executeUpdate + ("INSERT INTO test_unicode VALUES ( " + blockNum + + ", '" + block + "');"); + } + } + mode = SELECT; + for (blockNum = 0; blockNum < 256; blockNum++) { + if (isValidUnicodeBlock(blockNum)) { + String block = getSqlSafeUnicodeBlock(blockNum); + ResultSet rs = statement.executeQuery + ("SELECT block FROM test_unicode WHERE blockNum = " + + blockNum + ";"); + if (!rs.next()) + log("Could not retrieve block " + blockNum); + else { + String retrieved = rs.getString(1); + if (!retrieved.equals(block)) { + reportRetrievalError(blockNum, block, retrieved); + } + } + } + } + mode = LIKE; + for (blockNum = 0; blockNum < 256; blockNum++) { + if (isValidUnicodeBlock(blockNum)) { + String block = getSqlSafeUnicodeBlock(blockNum); + String likeString = "%" + + block.substring(2, block.length() - 3) + "%" ; + ResultSet rs = statement.executeQuery + ("SELECT blockNum FROM test_unicode WHERE block LIKE '" + + likeString + "';"); + if (!rs.next()) + log("Could get block " + blockNum + " using LIKE"); + } + } + } catch (SQLException sqle) { + switch (mode) { + case CREATE: + log("Exception creating database", sqle); + break; + case INSERT: + log("Exception inserting block " + blockNum, sqle); + break; + case SELECT: + log("Exception selecting block " + blockNum, sqle); + break; + case LIKE: + log("Exception doing LIKE on block " + blockNum, sqle); + break; + default: + log("Exception", sqle); + break; + } + } catch (ClassNotFoundException cnfe) { + log("Unable to load driver", cnfe); + return; + } + try { + if (statement != null) + statement.close(); + if (connection != null) + connection.close(); + } catch (SQLException sqle) { + log("Exception closing connections", sqle); + } + if (mode > CREATE) { + // If the backend gets what it regards as garbage on a connection, + // that connection may become unusable. To be safe, we create + // a fresh connection to delete the table. + try { + connection = getConnection(); + statement = connection.createStatement(); + statement.executeUpdate("DROP TABLE test_unicode;"); + } catch (Exception sqle) { + log("*** ERROR: unable to delete test table " + + "test_unicode; must be deleted manually", sqle); + } + } + } + + public static void main(String [] args) { + if (args.length != 3) { + usage(); + System.exit(1); + } + new Unicode(args[0], args[1], args[2]).runTest(); + } +} --- jdbc/org/postgresql/Connection.java Wed Apr 26 05:39:32 2000 +++ jdbc/org/postgresql/Connection.java Sun Sep 3 20:28:15 2000 @@ -10,7 +10,7 @@ import org.postgresql.util.*; /** - * $Id: Connection.java,v 1.1 2000/04/26 05:39:32 peter Exp $ + * $Id: Connection.java,v 1.1 2000/09/03 08:54:06 william Exp william $ * * This abstract class is used by org.postgresql.Driver to open either the JDBC1 or * JDBC2 versions of the Connection class. @@ -30,6 +30,14 @@ private String PG_PASSWORD; private String PG_DATABASE; private boolean PG_STATUS; + + /** + * The encoding to use for this connection. + * If null, the encoding has not been specified by the + * user, and the default encoding for the platform should be + * used. + */ + private String encoding; public boolean CONNECTION_OK = true; public boolean CONNECTION_BAD = false; @@ -111,6 +119,8 @@ PG_PORT = port; PG_HOST = new String(host); PG_STATUS = CONNECTION_BAD; + + encoding = info.getProperty("charSet"); // could be null // Now make the initial connection try @@ -154,7 +164,8 @@ // The most common one to be thrown here is: // "User authentication failed" // - throw new SQLException(pg_stream.ReceiveString(4096)); + throw new SQLException(pg_stream.ReceiveString + (4096, getEncoding())); case 'R': // Get the type of request @@ -224,7 +235,8 @@ break; case 'E': case 'N': - throw new SQLException(pg_stream.ReceiveString(4096)); + throw new SQLException(pg_stream.ReceiveString + (4096, getEncoding())); default: throw new PSQLException("postgresql.con.setup"); } @@ -313,19 +325,28 @@ Field[] fields = null; Vector tuples = new Vector(); - byte[] buf = new byte[sql.length()]; + byte[] buf = null; int fqp = 0; boolean hfr = false; String recv_status = null, msg; int update_count = 1; SQLException final_error = null; + if (getEncoding() == null) + buf = sql.getBytes(); + else { + try { + buf = sql.getBytes(getEncoding()); + } catch (UnsupportedEncodingException unse) { + throw new PSQLException("postgresql.con.encoding", + unse); + } + } - if (sql.length() > 8192) + if (buf.length > 8192) throw new PSQLException("postgresql.con.toolong",sql); try { pg_stream.SendChar('Q'); - buf = sql.getBytes(); pg_stream.Send(buf); pg_stream.SendChar(0); pg_stream.flush(); @@ -502,6 +523,15 @@ public String getUserName() throws SQLException { return PG_USER; + } + + /** + * Get the character encoding to use for this connection. + * @return the encoding to use, or null for the + * default encoding. + */ + public String getEncoding() throws SQLException { + return encoding; } /** --- jdbc/org/postgresql/Driver.java Wed Apr 26 05:39:32 2000 +++ jdbc/org/postgresql/Driver.java Mon Sep 4 01:50:23 2000 @@ -90,7 +90,15 @@ *

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. + * properties. In addition, the "charSet" property can be used to + * set a character set encoding (e.g. "utf-8") other than the platform + * default (typically Latin1). This is necessary in particular if storing + * multibyte characters in the database. For a list of supported + * character encoding , see + * http://java.sun.com/products/jdk/1.2/docs/guide/internat/encoding.doc.html + * Note that you will probably want to have set up the Postgres database + * itself to use the same encoding, with the "-E " argument + * to createdb. * * Our protocol takes the forms: *

--- jdbc/org/postgresql/PG_Stream.java	Wed Apr 26 05:39:32 2000
+++ jdbc/org/postgresql/PG_Stream.java	Sun Sep  3 20:29:09 2000
@@ -235,17 +235,22 @@
       }
       return n;
   }
+
+    public String ReceiveString(int maxsize) throws SQLException {
+        return ReceiveString(maxsize, null);
+    }
   
   /**
    * Receives a null-terminated string from the backend.  Maximum of
    * maxsiz bytes - if we don't see a null, then we assume something
    * has gone wrong.
    *
-   * @param maxsiz maximum length of string
+   * @param encoding the charset encoding to use.
+   * @param maxsiz maximum length of string in bytes
    * @return string from back end
    * @exception SQLException if an I/O error occurs
    */
-  public String ReceiveString(int maxsiz) throws SQLException
+  public String ReceiveString(int maxsiz, String encoding) throws SQLException
   {
     byte[] rst = new byte[maxsiz];
     int s = 0;
@@ -267,7 +272,16 @@
       } catch (IOException e) {
 	throw new PSQLException("postgresql.stream.ioerror",e);
       }
-      String v = new String(rst, 0, s);
+      String v = null;
+      if (encoding == null)
+          v = new String(rst, 0, s);
+      else {
+          try {
+              v = new String(rst, 0, s, encoding);
+          } catch (UnsupportedEncodingException unse) {
+              throw new PSQLException("postgresql.stream.encoding", unse);
+          }
+      }
       return v;
   }
   
--- jdbc/org/postgresql/jdbc1/ResultSet.java	Mon Apr 17 20:07:48 2000
+++ jdbc/org/postgresql/jdbc1/ResultSet.java	Mon Sep  4 02:05:00 2000
@@ -147,7 +147,16 @@
     wasNullFlag = (this_row[columnIndex - 1] == null);
     if(wasNullFlag)
       return null;
-    return new String(this_row[columnIndex - 1]);
+    String encoding = connection.getEncoding();
+    if (encoding == null)
+        return new String(this_row[columnIndex - 1]);
+    else {
+        try {
+            return new String(this_row[columnIndex - 1], encoding);
+        } catch (UnsupportedEncodingException unse) {
+            throw new PSQLException("postgresql.res.encoding", unse);
+        }
+    }
   }
   
   /**
--- jdbc/org/postgresql/jdbc2/ResultSet.java	Fri May 12 20:54:22 2000
+++ jdbc/org/postgresql/jdbc2/ResultSet.java	Sun Sep  3 20:31:14 2000
@@ -148,7 +148,16 @@
     wasNullFlag = (this_row[columnIndex - 1] == null);
     if(wasNullFlag)
       return null;
-    return new String(this_row[columnIndex - 1]);
+    String encoding = connection.getEncoding();
+    if (encoding == null)
+        return new String(this_row[columnIndex - 1]);
+    else {
+        try {
+            return new String(this_row[columnIndex - 1], encoding);
+        } catch (UnsupportedEncodingException unse) {
+            throw new PSQLException("postgresql.res.encoding", unse);
+        }
+    }
   }
   
   /**