Repository: pgjdbc/pgjdbc
Pull request #1019 (2017-11-20 16:30:59)

refactor: add MissingDeprecated into checkstyle
add MissingDeprecated into checkstyle + fix warnings


Patch:
diff --git a/.travis.yml b/.travis.yml
index 0b11bc35..5de16385 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -8,7 +8,8 @@ before_script:
   - test "x$XA" == 'x' || ./.travis/travis_configure_xa.sh
   - test "x$REPLICATION" == 'x' || ./.travis/travis_configure_replication.sh
   - ./.travis/travis_start_postgres.sh
-  - psql -U postgres -c "create user test with password 'test';"
+  - test "x$PG_VERSION" != 'xHEAD' || psql -U postgres -c "set password_encryption='scram-sha-256'; create user test with password 'test';"
+  - test "x$PG_VERSION" = 'xHEAD' || psql -U postgres -c "create user test with password 'test';"
   - test "x$REPLICATION" == 'x' || psql -U postgres -c "alter user test with replication;"
   - psql -c 'create database test owner test;' -U postgres
   - echo "MAVEN_OPTS='-Xmx1g -Dgpg.skip=true'" > ~/.mavenrc
diff --git a/docs/documentation/head/connect.md b/docs/documentation/head/connect.md
index 3aa78453..87f1e99d 100644
--- a/docs/documentation/head/connect.md
+++ b/docs/documentation/head/connect.md
@@ -116,7 +116,7 @@ Connection conn = DriverManager.getConnection(url);
 
 	Class name of hostname verifier. Defaults to using `org.postgresql.ssl.jdbc4.LibPQFactory.verify()`
 
-* **sslpaswordcallback** = String
+* **sslpasswordcallback** = String
 
 	Class name of the SSL password provider. Defaults to `org.postgresql.ssl.jdbc4.LibPQFactory.ConsoleCallbackHandler`
 
diff --git a/pgjdbc/pom.xml b/pgjdbc/pom.xml
index ac72cef0..9f74a373 100644
--- a/pgjdbc/pom.xml
+++ b/pgjdbc/pom.xml
@@ -36,6 +36,14 @@
     <checkstyle.version>7.8.2</checkstyle.version>
   </properties>
 
+  <dependencies>
+    <dependency>
+      <groupId>com.ongres.scram</groupId>
+      <artifactId>client</artifactId>
+      <version>1.0.0-beta.2</version>
+    </dependency>
+  </dependencies>
+
   <profiles>
     <profile>
       <id>translate</id>
@@ -249,6 +257,56 @@
           <outputEncoding>UTF-8</outputEncoding>
         </configuration>
       </plugin>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-shade-plugin</artifactId>
+        <version>3.1.0</version>
+        <configuration>
+          <minimizeJar>true</minimizeJar>
+          <filters>
+            <filter>
+              <artifact>com.ongres.scram:client</artifact>
+              <includes>
+                <include>*.*</include>
+              </includes>
+            </filter>
+            <filter>
+              <artifact>com.github.dblock.waffle:waffle-jna</artifact>
+              <excludes>
+                <exclude>**</exclude>
+              </excludes>
+            </filter>
+            <filter>
+              <artifact>org.slf4j:jcl-over-slf4j</artifact>
+              <excludes>
+                <exclude>**</exclude>
+              </excludes>
+            </filter>
+            <filter>
+              <artifact>*:*</artifact>
+              <excludes>
+                <exclude>com/sun/jna/**</exclude>
+              </excludes>
+            </filter>
+          </filters>
+        </configuration>
+        <executions>
+          <execution>
+            <phase>package</phase>
+            <goals>
+              <goal>shade</goal>
+            </goals>
+            <configuration>
+              <relocations>
+                <relocation>
+                  <pattern>com.ongres</pattern>
+                  <shadedPattern>org.postgresql.shaded.com.ongres</shadedPattern>
+                </relocation>
+              </relocations>
+            </configuration>
+          </execution>
+        </executions>
+      </plugin>
     </plugins>
 
     <pluginManagement>
@@ -266,7 +324,7 @@
           </dependencies>
           <configuration>
             <configLocation>src/main/checkstyle/checks.xml</configLocation>
-	    <suppressionsLocation>src/main/checkstyle/suppressions.xml</suppressionsLocation>
+            <suppressionsLocation>src/main/checkstyle/suppressions.xml</suppressionsLocation>
             <violationSeverity>error</violationSeverity>
             <failOnViolation>true</failOnViolation>
             <failsOnError>true</failsOnError>
diff --git a/pgjdbc/src/main/checkstyle/checks.xml b/pgjdbc/src/main/checkstyle/checks.xml
index 36a3e658..ff8e7ad6 100644
--- a/pgjdbc/src/main/checkstyle/checks.xml
+++ b/pgjdbc/src/main/checkstyle/checks.xml
@@ -91,6 +91,7 @@
                value="WhitespaceAround: ''{0}'' is not preceded with whitespace."/>
     </module>
     <module name="OneStatementPerLine"/>
+    <module name="MissingDeprecated"/>
     <module name="MultipleVariableDeclarations"/>
     <module name="ArrayTypeStyle"/>
     <!--<module name="MissingSwitchDefault"/>-->
diff --git a/pgjdbc/src/main/java/org/postgresql/PGConnection.java b/pgjdbc/src/main/java/org/postgresql/PGConnection.java
index 3182ebae..24560981 100644
--- a/pgjdbc/src/main/java/org/postgresql/PGConnection.java
+++ b/pgjdbc/src/main/java/org/postgresql/PGConnection.java
@@ -83,6 +83,7 @@ public interface PGConnection {
    *             does not work correctly for registering classes that cannot be directly loaded by
    *             the JDBC driver's classloader.
    */
+  @Deprecated
   void addDataType(String type, String className);
 
   /**
diff --git a/pgjdbc/src/main/java/org/postgresql/PGRefCursorResultSet.java b/pgjdbc/src/main/java/org/postgresql/PGRefCursorResultSet.java
index 293aa0c6..8fc678b9 100644
--- a/pgjdbc/src/main/java/org/postgresql/PGRefCursorResultSet.java
+++ b/pgjdbc/src/main/java/org/postgresql/PGRefCursorResultSet.java
@@ -12,6 +12,7 @@ package org.postgresql;
  *             code should call getString() on the ResultSet that contains the refcursor to obtain
  *             the underlying cursor name.
  */
+@Deprecated
 public interface PGRefCursorResultSet {
 
   /**
@@ -19,5 +20,6 @@ public interface PGRefCursorResultSet {
    * @deprecated As of 8.0, replaced with calling getString() on the ResultSet that this ResultSet
    *             was obtained from.
    */
+  @Deprecated
   String getRefCursor();
 }
diff --git a/pgjdbc/src/main/java/org/postgresql/PGStatement.java b/pgjdbc/src/main/java/org/postgresql/PGStatement.java
index c11adb9f..b1d53872 100644
--- a/pgjdbc/src/main/java/org/postgresql/PGStatement.java
+++ b/pgjdbc/src/main/java/org/postgresql/PGStatement.java
@@ -43,6 +43,7 @@ public interface PGStatement {
    * @since 7.3
    * @deprecated As of build 302, replaced by {@link #setPrepareThreshold(int)}
    */
+  @Deprecated
   void setUseServerPrepare(boolean flag) throws SQLException;
 
   /**
diff --git a/pgjdbc/src/main/java/org/postgresql/core/PGStream.java b/pgjdbc/src/main/java/org/postgresql/core/PGStream.java
index 99fe247d..61ca2521 100644
--- a/pgjdbc/src/main/java/org/postgresql/core/PGStream.java
+++ b/pgjdbc/src/main/java/org/postgresql/core/PGStream.java
@@ -541,4 +541,12 @@ public class PGStream implements Closeable, Flushable {
     pg_input.close();
     connection.close();
   }
+
+  public void setNetworkTimeout(int milliseconds) throws IOException {
+    connection.setSoTimeout(milliseconds);
+  }
+
+  public int getNetworkTimeout() throws IOException {
+    return connection.getSoTimeout();
+  }
 }
diff --git a/pgjdbc/src/main/java/org/postgresql/core/QueryExecutor.java b/pgjdbc/src/main/java/org/postgresql/core/QueryExecutor.java
index c76e8f31..ee83b244 100644
--- a/pgjdbc/src/main/java/org/postgresql/core/QueryExecutor.java
+++ b/pgjdbc/src/main/java/org/postgresql/core/QueryExecutor.java
@@ -14,6 +14,7 @@ import org.postgresql.jdbc.BatchResultHandler;
 import org.postgresql.jdbc.PreferQueryMode;
 import org.postgresql.util.HostSpec;
 
+import java.io.IOException;
 import java.sql.SQLException;
 import java.sql.SQLWarning;
 import java.util.List;
@@ -437,4 +438,8 @@ public interface QueryExecutor extends TypeTransferModeRegistry {
    * @return the ReplicationProtocol instance for this connection.
    */
   ReplicationProtocol getReplicationProtocol();
+
+  void setNetworkTimeout(int milliseconds) throws IOException;
+
+  int getNetworkTimeout() throws IOException;
 }
diff --git a/pgjdbc/src/main/java/org/postgresql/core/QueryExecutorBase.java b/pgjdbc/src/main/java/org/postgresql/core/QueryExecutorBase.java
index 732d3a29..6825eae0 100644
--- a/pgjdbc/src/main/java/org/postgresql/core/QueryExecutorBase.java
+++ b/pgjdbc/src/main/java/org/postgresql/core/QueryExecutorBase.java
@@ -79,6 +79,16 @@ public abstract class QueryExecutorBase implements QueryExecutor {
 
   protected abstract void sendCloseMessage() throws IOException;
 
+  @Override
+  public void setNetworkTimeout(int milliseconds) throws IOException {
+    pgStream.setNetworkTimeout(milliseconds);
+  }
+
+  @Override
+  public int getNetworkTimeout() throws IOException {
+    return pgStream.getNetworkTimeout();
+  }
+
   @Override
   public HostSpec getHostSpec() {
     return pgStream.getHostSpec();
diff --git a/pgjdbc/src/main/java/org/postgresql/core/v3/CompositeQuery.java b/pgjdbc/src/main/java/org/postgresql/core/v3/CompositeQuery.java
index 345e39fb..789979b6 100644
--- a/pgjdbc/src/main/java/org/postgresql/core/v3/CompositeQuery.java
+++ b/pgjdbc/src/main/java/org/postgresql/core/v3/CompositeQuery.java
@@ -44,7 +44,7 @@ class CompositeQuery implements Query {
 
   @Override
   public String getNativeSql() {
-    StringBuilder sbuf = new StringBuilder(subqueries[0].toString());
+    StringBuilder sbuf = new StringBuilder(subqueries[0].getNativeSql());
     for (int i = 1; i < subqueries.length; ++i) {
       sbuf.append(';');
       sbuf.append(subqueries[i].getNativeSql());
diff --git a/pgjdbc/src/main/java/org/postgresql/core/v3/ConnectionFactoryImpl.java b/pgjdbc/src/main/java/org/postgresql/core/v3/ConnectionFactoryImpl.java
index 3ccfc913..92da28d6 100644
--- a/pgjdbc/src/main/java/org/postgresql/core/v3/ConnectionFactoryImpl.java
+++ b/pgjdbc/src/main/java/org/postgresql/core/v3/ConnectionFactoryImpl.java
@@ -39,6 +39,7 @@ import java.util.Properties;
 import java.util.TimeZone;
 import java.util.logging.Level;
 import java.util.logging.Logger;
+
 import javax.net.SocketFactory;
 
 /**
@@ -59,6 +60,9 @@ public class ConnectionFactoryImpl extends ConnectionFactory {
   private static final int AUTH_REQ_GSS = 7;
   private static final int AUTH_REQ_GSS_CONTINUE = 8;
   private static final int AUTH_REQ_SSPI = 9;
+  private static final int AUTH_REQ_SASL = 10;
+  private static final int AUTH_REQ_SASL_CONTINUE = 11;
+  private static final int AUTH_REQ_SASL_FINAL = 12;
 
   /**
    * Marker exception; thrown when we want to fall back to using V2.
@@ -413,6 +417,11 @@ public class ConnectionFactoryImpl extends ConnectionFactory {
     /* SSPI negotiation state, if used */
     ISSPIClient sspiClient = null;
 
+    //#if mvn.project.property.postgresql.jdbc.spec >= "JDBC4.2"
+    /* SCRAM authentication state, if used */
+    org.postgresql.jre8.sasl.ScramAuthenticator scramAuthenticator = null;
+    //#endif
+
     try {
       authloop: while (true) {
         int beresp = pgStream.receiveChar();
@@ -604,6 +613,32 @@ public class ConnectionFactoryImpl extends ConnectionFactory {
                 sspiClient.continueSSPI(l_msgLen - 8);
                 break;
 
+              case AUTH_REQ_SASL:
+                LOGGER.log(Level.FINEST, " <=BE AuthenticationSASL");
+
+                //#if mvn.project.property.postgresql.jdbc.spec >= "JDBC4.2"
+                scramAuthenticator = new org.postgresql.jre8.sasl.ScramAuthenticator(user, password, pgStream);
+                scramAuthenticator.processServerMechanismsAndInit();
+                scramAuthenticator.sendScramClientFirstMessage();
+                //#else
+                if (true) {
+                  throw new PSQLException(GT.tr(
+                          "SCRAM authentication is not supported by this driver. You need JDK >= 8 and pgjdbc >= 42.2.0 (not \".jre\" vesions)",
+                          areq), PSQLState.CONNECTION_REJECTED);
+                }
+                //#endif
+                break;
+
+              //#if mvn.project.property.postgresql.jdbc.spec >= "JDBC4.2"
+              case AUTH_REQ_SASL_CONTINUE:
+                scramAuthenticator.processServerFirstMessage(l_msgLen - 4 - 4);
+                break;
+
+              case AUTH_REQ_SASL_FINAL:
+                scramAuthenticator.verifyServerSignature(l_msgLen - 4 - 4);
+                break;
+              //#endif
+
               case AUTH_REQ_OK:
                 /* Cleanup after successful authentication */
                 LOGGER.log(Level.FINEST, " <=BE AuthenticationOk");
diff --git a/pgjdbc/src/main/java/org/postgresql/jdbc/PgCallableStatement.java b/pgjdbc/src/main/java/org/postgresql/jdbc/PgCallableStatement.java
index 127b5b88..f1a16602 100644
--- a/pgjdbc/src/main/java/org/postgresql/jdbc/PgCallableStatement.java
+++ b/pgjdbc/src/main/java/org/postgresql/jdbc/PgCallableStatement.java
@@ -87,7 +87,11 @@ class PgCallableStatement extends PgPreparedStatement implements CallableStateme
           PSQLState.NO_DATA);
     }
 
-    ResultSet rs = result.getResultSet();
+    ResultSet rs;
+    synchronized (this) {
+      checkClosed();
+      rs = result.getResultSet();
+    }
     if (!rs.next()) {
       throw new PSQLException(GT.tr("A CallableStatement was executed with nothing returned."),
           PSQLState.NO_DATA);
@@ -146,7 +150,9 @@ class PgCallableStatement extends PgPreparedStatement implements CallableStateme
 
     }
     rs.close();
-    result = null;
+    synchronized (this) {
+      result = null;
+    }
     return false;
   }
 
diff --git a/pgjdbc/src/main/java/org/postgresql/jdbc/PgConnection.java b/pgjdbc/src/main/java/org/postgresql/jdbc/PgConnection.java
index 823bc9a3..a0911908 100644
--- a/pgjdbc/src/main/java/org/postgresql/jdbc/PgConnection.java
+++ b/pgjdbc/src/main/java/org/postgresql/jdbc/PgConnection.java
@@ -79,6 +79,7 @@ public class PgConnection implements BaseConnection {
   private static final Logger LOGGER = Logger.getLogger(PgConnection.class.getName());
 
   private static final SQLPermission SQL_PERMISSION_ABORT = new SQLPermission("callAbort");
+  private static final SQLPermission SQL_PERMISSION_NETWORK_TIMEOUT = new SQLPermission("setNetworkTimeout");
 
   //
   // Data initialized on construction:
@@ -1480,12 +1481,36 @@ public class PgConnection implements BaseConnection {
     }
   }
 
-  public void setNetworkTimeout(Executor executor, int milliseconds) throws SQLException {
-    throw org.postgresql.Driver.notImplemented(this.getClass(), "setNetworkTimeout(Executor, int)");
+  public void setNetworkTimeout(Executor executor /*not used*/, int milliseconds) throws SQLException {
+    checkClosed();
+
+    if (milliseconds < 0) {
+      throw new PSQLException(GT.tr("Network timeout must be a value greater than or equal to 0."),
+              PSQLState.INVALID_PARAMETER_VALUE);
+    }
+
+    SecurityManager securityManager = System.getSecurityManager();
+    if (securityManager != null) {
+      securityManager.checkPermission(SQL_PERMISSION_NETWORK_TIMEOUT);
+    }
+
+    try {
+      queryExecutor.setNetworkTimeout(milliseconds);
+    } catch (IOException ioe) {
+      throw new PSQLException(GT.tr("Unable to set network timeout."),
+              PSQLState.COMMUNICATION_ERROR, ioe);
+    }
   }
 
   public int getNetworkTimeout() throws SQLException {
-    throw org.postgresql.Driver.notImplemented(this.getClass(), "getNetworkTimeout()");
+    checkClosed();
+
+    try {
+      return queryExecutor.getNetworkTimeout();
+    } catch (IOException ioe) {
+      throw new PSQLException(GT.tr("Unable to get network timeout."),
+              PSQLState.COMMUNICATION_ERROR, ioe);
+    }
   }
 
   @Override
diff --git a/pgjdbc/src/main/java/org/postgresql/jdbc/PgPreparedStatement.java b/pgjdbc/src/main/java/org/postgresql/jdbc/PgPreparedStatement.java
index 517e96b6..28319be3 100644
--- a/pgjdbc/src/main/java/org/postgresql/jdbc/PgPreparedStatement.java
+++ b/pgjdbc/src/main/java/org/postgresql/jdbc/PgPreparedStatement.java
@@ -118,12 +118,7 @@ class PgPreparedStatement extends PgStatement implements PreparedStatement {
       throw new PSQLException(GT.tr("No results were returned by the query."), PSQLState.NO_DATA);
     }
 
-    if (result.getNext() != null) {
-      throw new PSQLException(GT.tr("Multiple ResultSets were returned by the query."),
-          PSQLState.TOO_MANY_RESULTS);
-    }
-
-    return result.getResultSet();
+    return getSingleResultSet();
   }
 
   public int executeUpdate(String p_sql) throws SQLException {
@@ -135,17 +130,7 @@ class PgPreparedStatement extends PgStatement implements PreparedStatement {
   public int executeUpdate() throws SQLException {
     executeWithFlags(QueryExecutor.QUERY_NO_RESULTS);
 
-    ResultWrapper iter = result;
-    while (iter != null) {
-      if (iter.getResultSet() != null) {
-        throw new PSQLException(GT.tr("A result was returned when none was expected."),
-            PSQLState.TOO_MANY_RESULTS);
-
-      }
-      iter = iter.getNext();
-    }
-
-    return getUpdateCount();
+    return getNoResultUpdateCount();
   }
 
   public boolean execute(String p_sql) throws SQLException {
@@ -168,7 +153,10 @@ class PgPreparedStatement extends PgStatement implements PreparedStatement {
 
       execute(preparedQuery, preparedParameters, flags);
 
-      return (result != null && result.getResultSet() != null);
+      synchronized (this) {
+        checkClosed();
+        return (result != null && result.getResultSet() != null);
+      }
     } finally {
       defaultTimeZone = null;
     }
@@ -182,24 +170,10 @@ class PgPreparedStatement extends PgStatement implements PreparedStatement {
   }
 
   @Override
-  public void close() throws SQLException {
-    if (isClosed) {
-      return;
-    }
-
+  public void closeImpl() throws SQLException {
     if (preparedQuery != null) {
-      // See #368. We need to prevent closing the same statement twice
-      // Otherwise we might "release" a query that someone else is already using
-      // In other words, client does .close() as usual, however cleanup thread might fail to observe
-      // isClosed=true
-      synchronized (preparedQuery) {
-        if (!isClosed) {
-          ((PgConnection) connection).releaseQuery(preparedQuery);
-        }
-      }
+      ((PgConnection) connection).releaseQuery(preparedQuery);
     }
-
-    super.close();
   }
 
   public void setNull(int parameterIndex, int sqlType) throws SQLException {
diff --git a/pgjdbc/src/main/java/org/postgresql/jdbc/PgStatement.java b/pgjdbc/src/main/java/org/postgresql/jdbc/PgStatement.java
index 7a09f688..b70d54c4 100644
--- a/pgjdbc/src/main/java/org/postgresql/jdbc/PgStatement.java
+++ b/pgjdbc/src/main/java/org/postgresql/jdbc/PgStatement.java
@@ -225,28 +225,41 @@ public class PgStatement implements Statement, BaseStatement {
       throw new PSQLException(GT.tr("No results were returned by the query."), PSQLState.NO_DATA);
     }
 
-    if (result.getNext() != null) {
-      throw new PSQLException(GT.tr("Multiple ResultSets were returned by the query."),
-          PSQLState.TOO_MANY_RESULTS);
-    }
+    return getSingleResultSet();
+  }
+
+  protected java.sql.ResultSet getSingleResultSet() throws SQLException {
+    synchronized (this) {
+      checkClosed();
+      if (result.getNext() != null) {
+        throw new PSQLException(GT.tr("Multiple ResultSets were returned by the query."),
+            PSQLState.TOO_MANY_RESULTS);
+      }
 
-    return result.getResultSet();
+      return result.getResultSet();
+    }
   }
 
   public int executeUpdate(String p_sql) throws SQLException {
     executeWithFlags(p_sql, QueryExecutor.QUERY_NO_RESULTS);
+    return getNoResultUpdateCount();
+  }
 
-    ResultWrapper iter = result;
-    while (iter != null) {
-      if (iter.getResultSet() != null) {
-        throw new PSQLException(GT.tr("A result was returned when none was expected."),
-            PSQLState.TOO_MANY_RESULTS);
+  protected int getNoResultUpdateCount() throws SQLException {
+    synchronized (this) {
+      checkClosed();
+      ResultWrapper iter = result;
+      while (iter != null) {
+        if (iter.getResultSet() != null) {
+          throw new PSQLException(GT.tr("A result was returned when none was expected."),
+              PSQLState.TOO_MANY_RESULTS);
 
+        }
+        iter = iter.getNext();
       }
-      iter = iter.getNext();
-    }
 
-    return getUpdateCount();
+      return getUpdateCount();
+    }
   }
 
   public boolean execute(String p_sql) throws SQLException {
@@ -292,7 +305,10 @@ public class PgStatement implements Statement, BaseStatement {
       flags |= QueryExecutor.QUERY_EXECUTE_AS_SIMPLE;
     }
     execute(simpleQuery, null, flags);
-    return (result != null && result.getResultSet() != null);
+    synchronized (this) {
+      checkClosed();
+      return (result != null && result.getResultSet() != null);
+    }
   }
 
   public boolean executeWithFlags(int flags) throws SQLException {
@@ -306,20 +322,22 @@ public class PgStatement implements Statement, BaseStatement {
     clearWarnings();
 
     // Close any existing resultsets associated with this statement.
-    while (firstUnclosedResult != null) {
-      ResultSet rs = firstUnclosedResult.getResultSet();
-      if (rs != null) {
-        rs.close();
+    synchronized (this) {
+      while (firstUnclosedResult != null) {
+        ResultSet rs = firstUnclosedResult.getResultSet();
+        if (rs != null) {
+          rs.close();
+        }
+        firstUnclosedResult = firstUnclosedResult.getNext();
       }
-      firstUnclosedResult = firstUnclosedResult.getNext();
-    }
-    result = null;
+      result = null;
 
-    if (generatedKeys != null) {
-      if (generatedKeys.getResultSet() != null) {
-        generatedKeys.getResultSet().close();
+      if (generatedKeys != null) {
+        if (generatedKeys.getResultSet() != null) {
+          generatedKeys.getResultSet().close();
+        }
+        generatedKeys = null;
       }
-      generatedKeys = null;
     }
   }
 
@@ -415,7 +433,9 @@ public class PgStatement implements Statement, BaseStatement {
     }
 
     StatementResultHandler handler = new StatementResultHandler();
-    result = null;
+    synchronized (this) {
+      result = null;
+    }
     try {
       startTimer();
       connection.getQueryExecutor().execute(queryToExecute, queryParameters, handler, maxrows,
@@ -423,17 +443,19 @@ public class PgStatement implements Statement, BaseStatement {
     } finally {
       killTimerTask();
     }
-    result = firstUnclosedResult = handler.getResults();
+    synchronized (this) {
+      checkClosed();
+      result = firstUnclosedResult = handler.getResults();
 
-    if (wantsGeneratedKeysOnce || wantsGeneratedKeysAlways) {
-      generatedKeys = result;
-      result = result.getNext();
+      if (wantsGeneratedKeysOnce || wantsGeneratedKeysAlways) {
+        generatedKeys = result;
+        result = result.getNext();
 
-      if (wantsGeneratedKeysOnce) {
-        wantsGeneratedKeysOnce = false;
+        if (wantsGeneratedKeysOnce) {
+          wantsGeneratedKeysOnce = false;
+        }
       }
     }
-
   }
 
   public void setCursorName(String name) throws SQLException {
@@ -441,35 +463,38 @@ public class PgStatement implements Statement, BaseStatement {
     // No-op.
   }
 
-  // This is intentionally non-volatile to avoid performance hit in isClosed checks
-  // see #close()
-  protected boolean isClosed = false;
+  private volatile boolean isClosed = false;
 
   public int getUpdateCount() throws SQLException {
-    checkClosed();
-    if (result == null || result.getResultSet() != null) {
-      return -1;
-    }
+    synchronized (this) {
+      checkClosed();
+      if (result == null || result.getResultSet() != null) {
+        return -1;
+      }
 
-    return result.getUpdateCount();
+      return result.getUpdateCount();
+    }
   }
 
   public boolean getMoreResults() throws SQLException {
-    if (result == null) {
-      return false;
-    }
+    synchronized (this) {
+      checkClosed();
+      if (result == null) {
+        return false;
+      }
 
-    result = result.getNext();
+      result = result.getNext();
 
-    // Close preceding resultsets.
-    while (firstUnclosedResult != result) {
-      if (firstUnclosedResult.getResultSet() != null) {
-        firstUnclosedResult.getResultSet().close();
+      // Close preceding resultsets.
+      while (firstUnclosedResult != result) {
+        if (firstUnclosedResult.getResultSet() != null) {
+          firstUnclosedResult.getResultSet().close();
+        }
+        firstUnclosedResult = firstUnclosedResult.getNext();
       }
-      firstUnclosedResult = firstUnclosedResult.getNext();
-    }
 
-    return (result != null && result.getResultSet() != null);
+      return (result != null && result.getResultSet() != null);
+    }
   }
 
   public int getMaxRows() throws SQLException {
@@ -580,13 +605,15 @@ public class PgStatement implements Statement, BaseStatement {
   }
 
   public java.sql.ResultSet getResultSet() throws SQLException {
-    checkClosed();
+    synchronized (this) {
+      checkClosed();
 
-    if (result == null) {
-      return null;
-    }
+      if (result == null) {
+        return null;
+      }
 
-    return result.getResultSet();
+      return result.getResultSet();
+    }
   }
 
   /**
@@ -595,17 +622,27 @@ public class PgStatement implements Statement, BaseStatement {
    *
    * {@inheritDoc}
    */
-  public void close() throws SQLException {
+  public final void close() throws SQLException {
     // closing an already closed Statement is a no-op.
-    if (isClosed) {
-      return;
+    synchronized (this) {
+      if (isClosed) {
+        return;
+      }
+      isClosed = true;
     }
 
-    cleanupTimer();
+    cancel();
 
     closeForNextExecution();
 
-    isClosed = true;
+    closeImpl();
+  }
+
+  /**
+   * This is guaranteed to be called exactly once even in case of concurrent {@link #close()} calls.
+   * @throws SQLException in case of error
+   */
+  protected void closeImpl() throws SQLException {
   }
 
   /*
@@ -615,11 +652,13 @@ public class PgStatement implements Statement, BaseStatement {
    */
 
   public long getLastOID() throws SQLException {
-    checkClosed();
-    if (result == null) {
-      return 0;
+    synchronized (this) {
+      checkClosed();
+      if (result == null) {
+        return 0;
+      }
+      return result.getInsertOID();
     }
-    return result.getInsertOID();
   }
 
   public void setPrepareThreshold(int newThreshold) throws SQLException {
@@ -646,7 +685,7 @@ public class PgStatement implements Statement, BaseStatement {
   }
 
   protected void checkClosed() throws SQLException {
-    if (isClosed) {
+    if (isClosed()) {
       throw new PSQLException(GT.tr("This statement has been closed."),
           PSQLState.OBJECT_NOT_IN_STATE);
     }
@@ -787,7 +826,9 @@ public class PgStatement implements Statement, BaseStatement {
       }
     }
 
-    result = null;
+    synchronized (this) {
+      result = null;
+    }
 
     try {
       startTimer();
@@ -796,8 +837,11 @@ public class PgStatement implements Statement, BaseStatement {
     } finally {
       killTimerTask();
       // There might be some rows generated even in case of failures
-      if (wantsGeneratedKeysAlways) {
-        generatedKeys = new ResultWrapper(handler.getGeneratedKeys());
+      synchronized (this) {
+        checkClosed();
+        if (wantsGeneratedKeysAlways) {
+          generatedKeys = new ResultWrapper(handler.getGeneratedKeys());
+        }
       }
     }
 
@@ -805,18 +849,20 @@ public class PgStatement implements Statement, BaseStatement {
   }
 
   public void cancel() throws SQLException {
-    if (!STATE_UPDATER.compareAndSet(this, StatementCancelState.IN_QUERY, StatementCancelState.CANCELING)) {
+    if (statementState == StatementCancelState.IDLE) {
+      return;
+    }
+    if (!STATE_UPDATER.compareAndSet(this, StatementCancelState.IN_QUERY,
+        StatementCancelState.CANCELING)) {
       // Not in query, there's nothing to cancel
       return;
     }
-    try {
-      // Synchronize on connection to avoid spinning in killTimerTask
-      synchronized (connection) {
+    // Synchronize on connection to avoid spinning in killTimerTask
+    synchronized (connection) {
+      try {
         connection.cancelQuery();
-      }
-    } finally {
-      STATE_UPDATER.set(this, StatementCancelState.CANCELLED);
-      synchronized (connection) {
+      } finally {
+        STATE_UPDATER.set(this, StatementCancelState.CANCELLED);
         connection.notifyAll(); // wake-up killTimerTask
       }
     }
@@ -925,8 +971,10 @@ public class PgStatement implements Statement, BaseStatement {
     // "timeout error"
     // We wait till state becomes "cancelled"
     boolean interrupted = false;
-    while (!STATE_UPDATER.compareAndSet(this, StatementCancelState.CANCELLED, StatementCancelState.IDLE)) {
-      synchronized (connection) {
+    synchronized (connection) {
+      // state check is performed under synchronized so it detects "cancelled" state faster
+      // In other words, it prevents unnecessary ".wait()" call
+      while (!STATE_UPDATER.compareAndSet(this, StatementCancelState.CANCELLED, StatementCancelState.IDLE)) {
         try {
           // Note: wait timeout here is irrelevant since synchronized(connection) would block until
           // .cancel finishes
@@ -1020,12 +1068,14 @@ public class PgStatement implements Statement, BaseStatement {
       return;
     }
 
-    ResultWrapper result = firstUnclosedResult;
-    while (result != null) {
-      if (result.getResultSet() != null && !result.getResultSet().isClosed()) {
-        return;
+    synchronized (this) {
+      ResultWrapper result = firstUnclosedResult;
+      while (result != null) {
+        if (result.getResultSet() != null && !result.getResultSet().isClosed()) {
+          return;
+        }
+        result = result.getNext();
       }
-      result = result.getNext();
     }
 
     // prevent all ResultSet.close arising from Statement.close to loop here
@@ -1039,39 +1089,44 @@ public class PgStatement implements Statement, BaseStatement {
   }
 
   public boolean getMoreResults(int current) throws SQLException {
-    // CLOSE_CURRENT_RESULT
-    if (current == Statement.CLOSE_CURRENT_RESULT && result != null
-        && result.getResultSet() != null) {
-      result.getResultSet().close();
-    }
+    synchronized (this) {
+      checkClosed();
+      // CLOSE_CURRENT_RESULT
+      if (current == Statement.CLOSE_CURRENT_RESULT && result != null
+          && result.getResultSet() != null) {
+        result.getResultSet().close();
+      }
 
-    // Advance resultset.
-    if (result != null) {
-      result = result.getNext();
-    }
+      // Advance resultset.
+      if (result != null) {
+        result = result.getNext();
+      }
 
-    // CLOSE_ALL_RESULTS
-    if (current == Statement.CLOSE_ALL_RESULTS) {
-      // Close preceding resultsets.
-      while (firstUnclosedResult != result) {
-        if (firstUnclosedResult.getResultSet() != null) {
-          firstUnclosedResult.getResultSet().close();
+      // CLOSE_ALL_RESULTS
+      if (current == Statement.CLOSE_ALL_RESULTS) {
+        // Close preceding resultsets.
+        while (firstUnclosedResult != result) {
+          if (firstUnclosedResult.getResultSet() != null) {
+            firstUnclosedResult.getResultSet().close();
+          }
+          firstUnclosedResult = firstUnclosedResult.getNext();
         }
-        firstUnclosedResult = firstUnclosedResult.getNext();
       }
-    }
 
-    // Done.
-    return (result != null && result.getResultSet() != null);
+      // Done.
+      return (result != null && result.getResultSet() != null);
+    }
   }
 
   public ResultSet getGeneratedKeys() throws SQLException {
-    checkClosed();
-    if (generatedKeys == null || generatedKeys.getResultSet() == null) {
-      return createDriverResultSet(new Field[0], new ArrayList<byte[][]>());
-    }
+    synchronized (this) {
+      checkClosed();
+      if (generatedKeys == null || generatedKeys.getResultSet() == null) {
+        return createDriverResultSet(new Field[0], new ArrayList<byte[][]>());
+      }
 
-    return generatedKeys.getResultSet();
+      return generatedKeys.getResultSet();
+    }
   }
 
   public int executeUpdate(String sql, int autoGeneratedKeys) throws SQLException {
diff --git a/pgjdbc/src/main/java/org/postgresql/jre8/sasl/ScramAuthenticator.java b/pgjdbc/src/main/java/org/postgresql/jre8/sasl/ScramAuthenticator.java
new file mode 100644
index 00000000..87daa05a
--- /dev/null
+++ b/pgjdbc/src/main/java/org/postgresql/jre8/sasl/ScramAuthenticator.java
@@ -0,0 +1,166 @@
+/*
+ * Copyright (c) 2017, PostgreSQL Global Development Group
+ * See the LICENSE file in the project root for more information.
+ */
+
+
+package org.postgresql.jre8.sasl;
+
+import org.postgresql.core.PGStream;
+import org.postgresql.util.GT;
+import org.postgresql.util.PSQLException;
+import org.postgresql.util.PSQLState;
+
+import com.ongres.scram.client.ScramClient;
+import com.ongres.scram.client.ScramSession;
+import com.ongres.scram.common.exception.ScramException;
+import com.ongres.scram.common.exception.ScramInvalidServerSignatureException;
+import com.ongres.scram.common.exception.ScramParseException;
+import com.ongres.scram.common.exception.ScramServerErrorException;
+import com.ongres.scram.common.stringprep.StringPreparations;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+public class ScramAuthenticator {
+  private static final Logger LOGGER = Logger.getLogger(ScramAuthenticator.class.getName());
+
+  private final String user;
+  private final String password;
+  private final PGStream pgStream;
+  private ScramClient scramClient;
+  private ScramSession scramSession;
+  private ScramSession.ServerFirstProcessor serverFirstProcessor;
+  private ScramSession.ClientFinalProcessor clientFinalProcessor;
+
+  @FunctionalInterface
+  private interface BodySender {
+    void sendBody(PGStream pgStream) throws IOException;
+  }
+
+  private void sendAuthenticationMessage(int bodyLength, BodySender bodySender)
+      throws IOException {
+    pgStream.sendChar('p');
+    pgStream.sendInteger4(Integer.BYTES + bodyLength);
+    bodySender.sendBody(pgStream);
+    pgStream.flush();
+  }
+
+  public ScramAuthenticator(String user, String password, PGStream pgStream) {
+    this.user = user;
+    this.password = password;
+    this.pgStream = pgStream;
+  }
+
+  public void processServerMechanismsAndInit() throws IOException, PSQLException {
+    List<String> mechanisms = new ArrayList<>();
+    do {
+      mechanisms.add(pgStream.receiveString());
+    } while (pgStream.peekChar() != 0);
+    int c = pgStream.receiveChar();
+    assert c == 0;
+    if (mechanisms.size() < 1) {
+      throw new PSQLException(
+          GT.tr("No SCRAM mechanism(s) advertised by the server"),
+          PSQLState.CONNECTION_REJECTED
+      );
+    }
+
+    try {
+      scramClient = ScramClient
+          .channelBinding(ScramClient.ChannelBinding.NO)
+          .stringPreparation(StringPreparations.NO_PREPARATION)
+          .selectMechanismBasedOnServerAdvertised(mechanisms.toArray(new String[]{}))
+          .setup();
+    } catch (IllegalArgumentException e) {
+      throw new PSQLException(
+          GT.tr("Invalid or unsupported by client SCRAM mechanisms", e),
+          PSQLState.CONNECTION_REJECTED
+      );
+    }
+    LOGGER.log(Level.FINEST, " Using SCRAM mechanism {0}", scramClient.getScramMechanism().getName());
+
+    scramSession =
+        scramClient.scramSession("*");   // Real username is ignored by server, uses startup one
+  }
+
+  public void sendScramClientFirstMessage() throws IOException {
+    String clientFirstMessage = scramSession.clientFirstMessage();
+    LOGGER.log(Level.FINEST, " FE=> SASLInitialResponse( {0} )", clientFirstMessage);
+
+    String scramMechanismName = scramClient.getScramMechanism().getName();
+    byte[] scramMechanismNameBytes = scramMechanismName.getBytes(StandardCharsets.UTF_8);
+    byte[] clientFirstMessageBytes = clientFirstMessage.getBytes(StandardCharsets.UTF_8);
+    sendAuthenticationMessage(
+        (scramMechanismNameBytes.length + 1) + 4 + clientFirstMessageBytes.length,
+        s -> {
+          s.send(scramMechanismNameBytes);
+          s.sendChar(0); // List terminated in '\0'
+          s.sendInteger4(clientFirstMessageBytes.length);
+          s.send(clientFirstMessageBytes);
+        }
+    );
+  }
+
+  public void processServerFirstMessage(int length) throws IOException, PSQLException {
+    String serverFirstMessage = pgStream.receiveString(length);
+    LOGGER.log(Level.FINEST, " <=BE AuthenticationSASLContinue( {0} )", serverFirstMessage);
+
+    try {
+      serverFirstProcessor = scramSession.receiveServerFirstMessage(serverFirstMessage);
+    } catch (ScramException e) {
+      throw new PSQLException(
+          GT.tr("Invalid server-first-message: {0}", serverFirstMessage),
+          PSQLState.CONNECTION_REJECTED,
+          e
+      );
+    }
+    LOGGER.log(Level.FINEST,
+            " <=BE AuthenticationSASLContinue(salt={0}, iterations={1})",
+        new Object[] { serverFirstProcessor.getSalt(), serverFirstProcessor.getIteration() }
+    );
+
+    clientFinalProcessor = serverFirstProcessor.clientFinalProcessor(password);
+
+    String clientFinalMessage = clientFinalProcessor.clientFinalMessage();
+    LOGGER.log(Level.FINEST, " FE=> SASLResponse( {0} )", clientFinalMessage);
+
+    byte[] clientFinalMessageBytes = clientFinalMessage.getBytes(StandardCharsets.UTF_8);
+    sendAuthenticationMessage(
+        clientFinalMessageBytes.length,
+        s -> s.send(clientFinalMessageBytes)
+    );
+  }
+
+  public void verifyServerSignature(int length) throws IOException, PSQLException {
+    String serverFinalMessage = pgStream.receiveString(length);
+    LOGGER.log(Level.FINEST, " <=BE AuthenticationSASLFinal( {0} )", serverFinalMessage);
+
+    try {
+      clientFinalProcessor.receiveServerFinalMessage(serverFinalMessage);
+    } catch (ScramParseException e) {
+      throw new PSQLException(
+          GT.tr("Invalid server-final-message: {0}", serverFinalMessage),
+          PSQLState.CONNECTION_REJECTED,
+          e
+      );
+    } catch (ScramServerErrorException e) {
+      throw new PSQLException(
+          GT.tr("SCRAM authentication failed, server returned error: {0}",
+              e.getError().getErrorMessage()),
+          PSQLState.CONNECTION_REJECTED,
+          e
+      );
+    } catch (ScramInvalidServerSignatureException e) {
+      throw new PSQLException(
+          GT.tr("Invalid server SCRAM signature"),
+          PSQLState.CONNECTION_REJECTED,
+          e
+      );
+    }
+  }
+}
diff --git a/pgjdbc/src/main/java/org/postgresql/largeobject/LargeObject.java b/pgjdbc/src/main/java/org/postgresql/largeobject/LargeObject.java
index b486ea40..38469a57 100644
--- a/pgjdbc/src/main/java/org/postgresql/largeobject/LargeObject.java
+++ b/pgjdbc/src/main/java/org/postgresql/largeobject/LargeObject.java
@@ -141,6 +141,7 @@ public class LargeObject
    * @return the OID of this LargeObject
    * @deprecated As of 8.3, replaced by {@link #getLongOID()}
    */
+  @Deprecated
   public int getOID() {
     return (int) oid;
   }
diff --git a/pgjdbc/src/main/java/org/postgresql/util/PSQLState.java b/pgjdbc/src/main/java/org/postgresql/util/PSQLState.java
index 2197f330..a77b4b42 100644
--- a/pgjdbc/src/main/java/org/postgresql/util/PSQLState.java
+++ b/pgjdbc/src/main/java/org/postgresql/util/PSQLState.java
@@ -87,6 +87,7 @@ public enum PSQLState {
   OBJECT_NOT_IN_STATE("55000"),
   OBJECT_IN_USE("55006"),
 
+  QUERY_CANCELED("57014"),
 
   SYSTEM_ERROR("60000"),
   IO_ERROR("58030"),


Test patch:
diff --git a/pgjdbc/src/test/java/org/postgresql/test/jdbc2/StatementTest.java b/pgjdbc/src/test/java/org/postgresql/test/jdbc2/StatementTest.java
index 445a15dc..5c9063da 100644
--- a/pgjdbc/src/test/java/org/postgresql/test/jdbc2/StatementTest.java
+++ b/pgjdbc/src/test/java/org/postgresql/test/jdbc2/StatementTest.java
@@ -18,6 +18,7 @@ import org.postgresql.test.TestUtil;
 import org.postgresql.util.PSQLState;
 
 import org.junit.After;
+import org.junit.Assert;
 import org.junit.Before;
 import org.junit.Test;
 
@@ -27,7 +28,9 @@ import java.sql.ResultSet;
 import java.sql.SQLException;
 import java.sql.SQLWarning;
 import java.sql.Statement;
-
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Random;
 import java.util.concurrent.Callable;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
@@ -792,6 +795,118 @@ public class StatementTest {
     assertEquals(0, sharedTimer.getRefCount());
   }
 
+  @Test(timeout = 10000)
+  public void testCloseInProgressStatement() throws Exception {
+    ExecutorService executor = Executors.newSingleThreadExecutor();
+    final Connection outerLockCon = TestUtil.openDB();
+    outerLockCon.setAutoCommit(false);
+    //Acquire an exclusive lock so we can block the notice generating statement
+    outerLockCon.createStatement().execute("LOCK TABLE test_lock IN ACCESS EXCLUSIVE MODE;");
+
+    try {
+      con.createStatement().execute("SET SESSION client_min_messages = 'NOTICE'");
+      con.createStatement()
+          .execute("CREATE OR REPLACE FUNCTION notify_then_sleep() RETURNS VOID AS "
+              + "$BODY$ "
+              + "BEGIN "
+              + "RAISE NOTICE 'start';"
+              + "LOCK TABLE test_lock IN ACCESS EXCLUSIVE MODE;"
+              + "END "
+              + "$BODY$ "
+              + "LANGUAGE plpgsql;");
+      int cancels = 0;
+      for (int i = 0; i < 100; i++) {
+        final Statement st = con.createStatement();
+        executor.submit(new Callable<Void>() {
+          @Override
+          public Void call() throws Exception {
+            long start = System.currentTimeMillis();
+            while (st.getWarnings() == null) {
+              long dt = System.currentTimeMillis() - start;
+              if (dt > 10000) {
+                throw new IllegalStateException("Expected to receive a notice within 10 seconds");
+              }
+            }
+            st.close();
+            return null;
+          }
+        });
+        st.setQueryTimeout(120);
+        try {
+          st.execute("select notify_then_sleep()");
+        } catch (SQLException e) {
+          Assert.assertEquals(
+              "Query is expected to be cancelled via st.close(), got " + e.getMessage(),
+              PSQLState.QUERY_CANCELED.getState(),
+              e.getSQLState()
+          );
+          cancels++;
+        } finally {
+          TestUtil.closeQuietly(st);
+        }
+      }
+      Assert.assertNotEquals("At least one QUERY_CANCELED state is expected", 0, cancels);
+    } finally {
+      executor.shutdown();
+      TestUtil.closeQuietly(outerLockCon);
+    }
+  }
+
+  @Test(timeout = 20000)
+  public void testFastCloses() throws SQLException {
+    ExecutorService executor = Executors.newSingleThreadExecutor();
+    con.createStatement().execute("SET SESSION client_min_messages = 'NOTICE'");
+    con.createStatement()
+        .execute("CREATE OR REPLACE FUNCTION notify_then_sleep() RETURNS VOID AS "
+            + "$BODY$ "
+            + "BEGIN "
+            + "RAISE NOTICE 'start';"
+            + "EXECUTE pg_sleep(1);" // Note: timeout value does not matter here, we just test if test crashes or locks somehow
+            + "END "
+            + "$BODY$ "
+            + "LANGUAGE plpgsql;");
+    Map<String, Integer> cnt = new HashMap<String, Integer>();
+    final Random rnd = new Random();
+    for (int i = 0; i < 1000; i++) {
+      final Statement st = con.createStatement();
+      executor.submit(new Callable<Void>() {
+        @Override
+        public Void call() throws Exception {
+          int s = rnd.nextInt(10);
+          if (s > 8) {
+            Thread.sleep(s - 9);
+          }
+          st.close();
+          return null;
+        }
+      });
+      ResultSet rs = null;
+      String sqlState = "0";
+      try {
+        rs = st.executeQuery("select 1");
+        // Acceptable
+      } catch (SQLException e) {
+        sqlState = e.getSQLState();
+        if (!PSQLState.OBJECT_NOT_IN_STATE.getState().equals(sqlState)
+            && !PSQLState.QUERY_CANCELED.getState().equals(sqlState)) {
+          Assert.assertEquals(
+              "Query is expected to be cancelled via st.close(), got " + e.getMessage(),
+              PSQLState.QUERY_CANCELED.getState(),
+              e.getSQLState()
+          );
+        }
+      } finally {
+        TestUtil.closeQuietly(rs);
+        TestUtil.closeQuietly(st);
+      }
+      Integer val = cnt.get(sqlState);
+      val = (val == null ? 0 : val) + 1;
+      cnt.put(sqlState, val);
+    }
+    System.out.println("[testFastCloses] total counts for each sql state: " + cnt);
+    executor.shutdown();
+  }
+
   /**
    * Tests that calling {@code java.sql.Statement#close()} from a concurrent thread does not result
    * in {@link java.util.ConcurrentModificationException}
diff --git a/pgjdbc/src/test/java/org/postgresql/test/jdbc4/jdbc41/Jdbc41TestSuite.java b/pgjdbc/src/test/java/org/postgresql/test/jdbc4/jdbc41/Jdbc41TestSuite.java
index 065be558..046696a0 100644
--- a/pgjdbc/src/test/java/org/postgresql/test/jdbc4/jdbc41/Jdbc41TestSuite.java
+++ b/pgjdbc/src/test/java/org/postgresql/test/jdbc4/jdbc41/Jdbc41TestSuite.java
@@ -17,6 +17,7 @@ import org.junit.runners.Suite;
         AbortTest.class,
         CloseOnCompletionTest.class,
         SharedTimerClassLoaderLeakTest.class,
+        NetworkTimeoutTest.class,
         GetObjectTest.class}
     )
 public class Jdbc41TestSuite {
diff --git a/pgjdbc/src/test/java/org/postgresql/test/jdbc4/jdbc41/NetworkTimeoutTest.java b/pgjdbc/src/test/java/org/postgresql/test/jdbc4/jdbc41/NetworkTimeoutTest.java
new file mode 100644
index 00000000..8adfd3ee
--- /dev/null
+++ b/pgjdbc/src/test/java/org/postgresql/test/jdbc4/jdbc41/NetworkTimeoutTest.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (c) 2007, PostgreSQL Global Development Group
+ * See the LICENSE file in the project root for more information.
+ */
+
+package org.postgresql.test.jdbc4.jdbc41;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import org.postgresql.test.TestUtil;
+
+import org.junit.Test;
+
+import java.sql.Connection;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.concurrent.TimeUnit;
+
+public class NetworkTimeoutTest {
+  @Test
+  public void testSetNetworkTimeout() throws Exception {
+    Connection _conn = TestUtil.openDB();
+    try {
+      _conn.setNetworkTimeout(null, 0);
+    } catch (SQLException e) {
+      fail("Connection.setNetworkTimeout() throw exception");
+    } finally {
+      TestUtil.closeDB(_conn);
+    }
+  }
+
+  @Test
+  public void testSetNetworkTimeoutInvalid() throws Exception {
+    Connection _conn = TestUtil.openDB();
+    try {
+      _conn.setNetworkTimeout(null, -1);
+      fail("Connection.setNetworkTimeout() did not throw expected exception");
+    } catch (SQLException e) {
+      // Passed
+    } finally {
+      TestUtil.closeDB(_conn);
+    }
+  }
+
+  @Test
+  public void testSetNetworkTimeoutValid() throws Exception {
+    Connection _conn = TestUtil.openDB();
+    try {
+      _conn.setNetworkTimeout(null, (int) TimeUnit.SECONDS.toMillis(5));
+      assertEquals(TimeUnit.SECONDS.toMillis(5), _conn.getNetworkTimeout());
+    } catch (SQLException e) {
+      fail("Connection.setNetworkTimeout() throw exception");
+    } finally {
+      TestUtil.closeDB(_conn);
+    }
+  }
+
+  @Test
+  public void testSetNetworkTimeoutEnforcement() throws Exception {
+    Connection _conn = TestUtil.openDB();
+    Statement stmt = null;
+    try {
+      _conn.setNetworkTimeout(null, (int) TimeUnit.SECONDS.toMillis(1));
+      stmt = _conn.createStatement();
+      stmt.execute("SELECT pg_sleep(2)");
+      fail("Connection.setNetworkTimeout() did not throw expected exception");
+    } catch (SQLException e) {
+      // assertTrue(stmt.isClosed());
+      assertTrue(_conn.isClosed());
+    } finally {
+      TestUtil.closeQuietly(stmt);
+      TestUtil.closeDB(_conn);
+    }
+  }
+}