Modify error handling so dialects can translate exceptions

Application level code is particularly interested in duplicate
primary key types of errors being special from other types of
ORM exceptions.  Decoding these is driver specific, so we defer
the error handling to each dialect.

Signed-off-by: Shawn O. Pearce <sop@google.com>
diff --git a/src/main/java/com/google/gwtorm/client/OrmDuplicateKeyException.java b/src/main/java/com/google/gwtorm/client/OrmDuplicateKeyException.java
new file mode 100644
index 0000000..0874895
--- /dev/null
+++ b/src/main/java/com/google/gwtorm/client/OrmDuplicateKeyException.java
@@ -0,0 +1,26 @@
+// Copyright 2009 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gwtorm.client;
+
+/** Indicates one or more entities were concurrently inserted with the same key. */
+public class OrmDuplicateKeyException extends OrmException {
+  public OrmDuplicateKeyException(final String msg) {
+    super(msg);
+  }
+
+  public OrmDuplicateKeyException(final String msg, final Throwable why) {
+    super(msg, why);
+  }
+}
diff --git a/src/main/java/com/google/gwtorm/jdbc/JdbcAccess.java b/src/main/java/com/google/gwtorm/jdbc/JdbcAccess.java
index bd08a98..aa5775f 100644
--- a/src/main/java/com/google/gwtorm/jdbc/JdbcAccess.java
+++ b/src/main/java/com/google/gwtorm/jdbc/JdbcAccess.java
@@ -81,8 +81,7 @@
     try {
       return schema.getConnection().prepareStatement(sql);
     } catch (SQLException e) {
-      linkCause(e);
-      throw new OrmException("Prepare failure\n" + sql, e);
+      throw convertError("prepare SQL\n" + sql + "\n", e);
     }
   }
 
@@ -123,8 +122,7 @@
         ps.close();
       }
     } catch (SQLException e) {
-      linkCause(e);
-      throw new OrmException("Fetch failure: " + getRelationName(), e);
+      throw convertError("fetch", e);
     }
   }
 
@@ -148,8 +146,7 @@
         ps.close();
       }
     } catch (SQLException e) {
-      linkCause(e);
-      throw new OrmException("Fetch failure: " + getRelationName(), e);
+      throw convertError("fetch", e);
     }
   }
 
@@ -172,8 +169,7 @@
         ps.close();
       }
     } catch (SQLException e) {
-      linkCause(e);
-      throw new OrmException("Insert failure: " + getRelationName(), e);
+      throw convertError("insert", e);
     }
   }
 
@@ -196,8 +192,7 @@
         ps.close();
       }
     } catch (SQLException e) {
-      linkCause(e);
-      throw new OrmException("Update failure: " + getRelationName(), e);
+      throw convertError("update", e);
     }
   }
 
@@ -220,8 +215,7 @@
         ps.close();
       }
     } catch (SQLException e) {
-      linkCause(e);
-      throw new OrmException("Delete failure: " + getRelationName(), e);
+      throw convertError("delete", e);
     }
   }
 
@@ -242,10 +236,11 @@
     }
   }
 
-  private static void linkCause(final SQLException e) {
-    if (e.getCause() == null && e.getNextException() != null) {
-      e.initCause(e.getNextException());
+  private OrmException convertError(final String op, final SQLException err) {
+    if (err.getCause() == null && err.getNextException() != null) {
+      err.initCause(err.getNextException());
     }
+    return schema.getDialect().convertError(op, getRelationName(), err);
   }
 
   protected abstract T newEntityInstance();
diff --git a/src/main/java/com/google/gwtorm/jdbc/JdbcSchema.java b/src/main/java/com/google/gwtorm/jdbc/JdbcSchema.java
index 4327560..17b6cc0 100644
--- a/src/main/java/com/google/gwtorm/jdbc/JdbcSchema.java
+++ b/src/main/java/com/google/gwtorm/jdbc/JdbcSchema.java
@@ -41,6 +41,10 @@
     return conn;
   }
 
+  public final SqlDialect getDialect() {
+    return dbDef.getDialect();
+  }
+
   public void createSchema() throws OrmException {
     final SqlDialect dialect = dbDef.getDialect();
     final SchemaModel model = dbDef.getSchemaModel();
@@ -84,7 +88,7 @@
         st.close();
       }
     } catch (SQLException e) {
-      throw new OrmException("Sequence query failed", e);
+      throw convertError("sequence", query, e);
     }
   }
 
@@ -102,4 +106,12 @@
       conn = null;
     }
   }
+
+  private OrmException convertError(final String op, final String what,
+      final SQLException err) {
+    if (err.getCause() == null && err.getNextException() != null) {
+      err.initCause(err.getNextException());
+    }
+    return getDialect().convertError(op, what, err);
+  }
 }
diff --git a/src/main/java/com/google/gwtorm/schema/sql/DialectH2.java b/src/main/java/com/google/gwtorm/schema/sql/DialectH2.java
index adf6be2..3793d86 100644
--- a/src/main/java/com/google/gwtorm/schema/sql/DialectH2.java
+++ b/src/main/java/com/google/gwtorm/schema/sql/DialectH2.java
@@ -1,8 +1,26 @@
 package com.google.gwtorm.schema.sql;
 
+import com.google.gwtorm.client.OrmDuplicateKeyException;
+import com.google.gwtorm.client.OrmException;
+
+import java.sql.SQLException;
+
 /** Dialect for <a href="http://www.h2database.com/">H2</a> */
 public class DialectH2 extends SqlDialect {
   @Override
+  public OrmException convertError(final String op, final String entity,
+      final SQLException err) {
+    switch (getSQLStateInt(err)) {
+      case 23001: // UNIQUE CONSTRAINT VIOLATION
+        return new OrmDuplicateKeyException(entity, err);
+
+      case 23000: // CHECK CONSTRAINT VIOLATION
+      default:
+        return super.convertError(op, entity, err);
+    }
+  }
+
+  @Override
   public String getNextSequenceValueSql(final String seqname) {
     return "SELECT NEXT VALUE FOR " + seqname;
   }
diff --git a/src/main/java/com/google/gwtorm/schema/sql/DialectPostgreSQL.java b/src/main/java/com/google/gwtorm/schema/sql/DialectPostgreSQL.java
index 5d2391c..83fe3fc 100644
--- a/src/main/java/com/google/gwtorm/schema/sql/DialectPostgreSQL.java
+++ b/src/main/java/com/google/gwtorm/schema/sql/DialectPostgreSQL.java
@@ -1,5 +1,9 @@
 package com.google.gwtorm.schema.sql;
 
+import com.google.gwtorm.client.OrmDuplicateKeyException;
+import com.google.gwtorm.client.OrmException;
+
+import java.sql.SQLException;
 import java.sql.Types;
 
 /** Dialect for <a href="http://www.postgresql.org/>PostgreSQL</a> */
@@ -10,6 +14,22 @@
   }
 
   @Override
+  public OrmException convertError(final String op, final String entity,
+      final SQLException err) {
+    switch (getSQLStateInt(err)) {
+      case 23505: // UNIQUE CONSTRAINT VIOLATION
+        return new OrmDuplicateKeyException(entity, err);
+
+      case 23514: // CHECK CONSTRAINT VIOLATION
+      case 23503: // FOREIGN KEY CONSTRAINT VIOLATION
+      case 23502: // NOT NULL CONSTRAINT VIOLATION
+      case 23001: // RESTRICT VIOLATION
+      default:
+        return super.convertError(op, entity, err);
+    }
+  }
+
+  @Override
   public String getNextSequenceValueSql(final String seqname) {
     return "SELECT nextval('" + seqname + "')";
   }
diff --git a/src/main/java/com/google/gwtorm/schema/sql/SqlDialect.java b/src/main/java/com/google/gwtorm/schema/sql/SqlDialect.java
index 82aa4b2..2565513 100644
--- a/src/main/java/com/google/gwtorm/schema/sql/SqlDialect.java
+++ b/src/main/java/com/google/gwtorm/schema/sql/SqlDialect.java
@@ -14,10 +14,12 @@
 
 package com.google.gwtorm.schema.sql;
 
+import com.google.gwtorm.client.OrmException;
 import com.google.gwtorm.client.Sequence;
 import com.google.gwtorm.schema.ColumnModel;
 import com.google.gwtorm.schema.SequenceModel;
 
+import java.sql.SQLException;
 import java.sql.Types;
 import java.util.HashMap;
 import java.util.Map;
@@ -69,6 +71,41 @@
     return true;
   }
 
+  protected static String getSQLState(SQLException err) {
+    String ec;
+    SQLException next = err;
+    do {
+      ec = next.getSQLState();
+      next = next.getNextException();
+    } while (ec == null && next != null);
+    return ec;
+  }
+
+  protected static int getSQLStateInt(SQLException err) {
+    final String s = getSQLState(err);
+    if (s != null) {
+      try {
+        return Integer.parseInt(s);
+      } catch (NumberFormatException e) {
+        return -1;
+      }
+    }
+    return 0;
+  }
+
+  /**
+   * Convert a driver specific exception into an {@link OrmException}.
+   * 
+   * @param op short description of the operation, e.g. "update" or "fetch".
+   * @param entity name of the entity being accessed by the operation.
+   * @param err the driver specific exception.
+   * @return an OrmException the caller can throw.
+   */
+  public OrmException convertError(final String op, final String entity,
+      final SQLException err) {
+    return new OrmException(op + " failure on " + entity, err);
+  }
+
   public String getCreateSequenceSql(final SequenceModel seq) {
     final Sequence s = seq.getSequence();
     final StringBuilder r = new StringBuilder();