diff --git a/zanata-war/src/main/java/org/zanata/database/ConnectionWrapper.java b/zanata-war/src/main/java/org/zanata/database/ConnectionWrapper.java
index 31aefd32a3..ec6b88d13e 100644
--- a/zanata-war/src/main/java/org/zanata/database/ConnectionWrapper.java
+++ b/zanata-war/src/main/java/org/zanata/database/ConnectionWrapper.java
@@ -24,10 +24,7 @@
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
-import java.lang.reflect.Proxy;
import java.sql.Connection;
-import java.sql.DatabaseMetaData;
-import java.sql.SQLException;
import java.sql.Statement;
import java.util.Set;
@@ -36,6 +33,9 @@
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
+import static java.lang.reflect.Proxy.getInvocationHandler;
+import static java.lang.reflect.Proxy.isProxyClass;
+
/**
* @author Sean Flanigan sflaniga@redhat.com
@@ -52,42 +52,25 @@ class ConnectionWrapper implements InvocationHandler {
// sets before attempting more queries.
public static final String CONCURRENT_RESULTSET =
"Streaming ResultSet is still open on this Connection";
- private final Connection connection;
+ private final Connection originalConnection;
private Set resultSetsOpened = Sets.newHashSet();
private Throwable streamingResultSetOpened;
- public static Connection wrap(Connection connection) {
- if (Proxy.isProxyClass(connection.getClass())
- && Proxy.getInvocationHandler(connection) instanceof ConnectionWrapper) {
- return connection;
+ public static Connection wrap(Connection conn) {
+ // avoid double-wrapping:
+ if (isProxyClass(conn.getClass())
+ && getInvocationHandler(conn) instanceof ConnectionWrapper) {
+ return conn;
}
return ProxyUtil
- .newProxy(connection, new ConnectionWrapper(connection));
- }
-
- public static Connection wrapUnlessMysql(Connection connection)
- throws SQLException {
- DatabaseMetaData metaData = connection.getMetaData();
- String databaseName = metaData.getDatabaseProductName();
- if ("MySQL".equals(databaseName)) {
- return connection;
- } else {
- return wrap(connection);
- }
- }
-
- /**
- * @return the connection
- */
- public Connection getConnection() {
- return connection;
+ .newProxy(conn, new ConnectionWrapper(conn));
}
@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
if (method.getName().equals("toString")) {
- return "ConnectionWrapper->" + connection.toString();
+ return "ConnectionWrapper->" + originalConnection.toString();
}
if (method.getName().equals("close")) {
if (streamingResultSetOpened != null) {
@@ -100,7 +83,7 @@ public Object invoke(Object proxy, Method method, Object[] args)
}
}
try {
- Object result = method.invoke(connection, args);
+ Object result = method.invoke(originalConnection, args);
if (result instanceof Statement) {
Statement statement = (Statement) result;
return StatementWrapper.wrap(statement, (Connection) proxy);
@@ -111,14 +94,23 @@ public Object invoke(Object proxy, Method method, Object[] args)
}
}
- public void executed() throws SQLException {
+ /**
+ * Notify ConnectionWrapper that Statement.execute() has been called.
+ * @throws StreamingResultSetSQLException
+ */
+ public void executed() throws StreamingResultSetSQLException {
if (streamingResultSetOpened != null) {
throw new StreamingResultSetSQLException(CONCURRENT_RESULTSET,
streamingResultSetOpened);
}
}
- public void resultSetOpened(Throwable throwable) throws SQLException {
+ /**
+ * Notify ConnectionWrapper that a Statement has opened a non-streaming
+ * ResultSet.
+ * @throws StreamingResultSetSQLException
+ */
+ public void resultSetOpened(Throwable throwable) throws StreamingResultSetSQLException {
if (streamingResultSetOpened != null) {
throw new StreamingResultSetSQLException(CONCURRENT_RESULTSET,
streamingResultSetOpened);
@@ -126,8 +118,13 @@ public void resultSetOpened(Throwable throwable) throws SQLException {
resultSetsOpened.add(throwable);
}
+ /**
+ * Notify ConnectionWrapper that a Statement has opened a streaming
+ * ResultSet.
+ * @throws StreamingResultSetSQLException
+ */
public void streamingResultSetOpened(Throwable throwable)
- throws SQLException {
+ throws StreamingResultSetSQLException {
if (streamingResultSetOpened != null) {
throw new StreamingResultSetSQLException(CONCURRENT_RESULTSET,
streamingResultSetOpened);
@@ -138,10 +135,18 @@ public void streamingResultSetOpened(Throwable throwable)
streamingResultSetOpened = throwable;
}
+ /**
+ * Notify ConnectionWrapper that a non-streaming ResultSet has been closed.
+ * @throws StreamingResultSetSQLException
+ */
public void resultSetClosed(Throwable throwable) {
resultSetsOpened.remove(throwable);
}
+ /**
+ * Notify ConnectionWrapper that a streaming ResultSet has been closed.
+ * @throws StreamingResultSetSQLException
+ */
public void streamingResultSetClosed() {
streamingResultSetOpened = null;
}
diff --git a/zanata-war/src/main/java/org/zanata/database/WrappedDatasourceConnectionProvider.java b/zanata-war/src/main/java/org/zanata/database/WrappedDatasourceConnectionProvider.java
index d41d1bb1ad..03fe0ad290 100644
--- a/zanata-war/src/main/java/org/zanata/database/WrappedDatasourceConnectionProvider.java
+++ b/zanata-war/src/main/java/org/zanata/database/WrappedDatasourceConnectionProvider.java
@@ -27,23 +27,17 @@
import org.hibernate.service.jdbc.connections.internal.DatasourceConnectionProviderImpl;
/**
- * This class wraps JDBC Connections/Statements/ResultSets to detect attempts to
- * use mysql's streaming ResultSet feature. It then watches for any usage which
- * would exceed the limitations of mysql's streaming ResultSets, and throws an
- * SQLException. This enables us to catch these problems without having to test
- * against mysql in our unit tests.
- *
* @author Sean Flanigan sflaniga@redhat.com
- *
*/
public class WrappedDatasourceConnectionProvider extends
DatasourceConnectionProviderImpl {
private static final long serialVersionUID = 1L;
+ private final WrapperManager wrapperManager = new WrapperManager();
@Override
public Connection getConnection() throws SQLException {
- return ConnectionWrapper.wrapUnlessMysql(super.getConnection());
+ return wrapperManager.wrapIfNeeded(super.getConnection());
}
}
diff --git a/zanata-war/src/main/java/org/zanata/database/WrappedDriverManagerConnectionProvider.java b/zanata-war/src/main/java/org/zanata/database/WrappedDriverManagerConnectionProvider.java
index a01b518103..51f45a9e3c 100644
--- a/zanata-war/src/main/java/org/zanata/database/WrappedDriverManagerConnectionProvider.java
+++ b/zanata-war/src/main/java/org/zanata/database/WrappedDriverManagerConnectionProvider.java
@@ -27,23 +27,17 @@
import org.hibernate.service.jdbc.connections.internal.DriverManagerConnectionProviderImpl;
/**
- * This class wraps JDBC Connections/Statements/ResultSets to detect attempts to
- * use mysql's streaming ResultSet feature. It then watches for any usage which
- * would exceed the limitations of mysql's streaming ResultSets, and throws an
- * SQLException. This enables us to catch these problems without having to test
- * against mysql in our unit tests.
- *
* @author Sean Flanigan sflaniga@redhat.com
- *
*/
public class WrappedDriverManagerConnectionProvider extends
DriverManagerConnectionProviderImpl {
private static final long serialVersionUID = 1L;
+ private final WrapperManager wrapperManager = new WrapperManager();
@Override
public Connection getConnection() throws SQLException {
- return ConnectionWrapper.wrapUnlessMysql(super.getConnection());
+ return wrapperManager.wrapIfNeeded(super.getConnection());
}
}
diff --git a/zanata-war/src/main/java/org/zanata/database/WrapperManager.java b/zanata-war/src/main/java/org/zanata/database/WrapperManager.java
new file mode 100644
index 0000000000..a2f63f73d3
--- /dev/null
+++ b/zanata-war/src/main/java/org/zanata/database/WrapperManager.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright 2013, Red Hat, Inc. and individual contributors
+ * as indicated by the @author tags. See the copyright.txt file in the
+ * distribution for a full listing of individual contributors.
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+
+package org.zanata.database;
+
+import java.sql.Connection;
+import java.sql.DatabaseMetaData;
+import java.sql.SQLException;
+
+import lombok.extern.slf4j.Slf4j;
+import org.hibernate.service.jdbc.connections.internal.DriverManagerConnectionProviderImpl;
+
+/**
+ * This class wraps JDBC Connections/Statements/ResultSets to detect attempts to
+ * use mysql's streaming ResultSet feature. It then watches for any usage which
+ * would exceed the limitations of mysql's streaming ResultSets, and throws an
+ * SQLException. This enables us to catch these problems without having to test
+ * against mysql in our unit tests.
+ *
+ * @author Sean Flanigan sflaniga@redhat.com
+ *
+ */
+@Slf4j
+public class WrapperManager {
+ public static final String PROPERTY_USE_WRAPPER =
+ "zanata.connection.use.wrapper";
+ private static final String USE_WRAPPER =
+ System.getProperty(PROPERTY_USE_WRAPPER);
+
+ private boolean checkedFirstConnection = false;
+ private boolean wrappingEnabled = false;
+
+ public Connection wrapIfNeeded(Connection conn) throws SQLException {
+ if (!checkedFirstConnection) {
+ DatabaseMetaData metaData = conn.getMetaData();
+ checkSupported(metaData);
+ wrappingEnabled = shouldWrap(metaData);
+ checkedFirstConnection = true;
+ }
+ if (wrappingEnabled) {
+ Connection wrapped = ConnectionWrapper.wrap(conn);
+ log.debug("Connection {} is wrapped by {}",
+ conn, wrapped);
+ return wrapped;
+ } else {
+ return conn;
+ }
+ }
+
+ /**
+ * Log warnings or errors if the database or driver is not supported.
+ * @param metaData
+ * @throws SQLException
+ */
+ private static void checkSupported(DatabaseMetaData metaData)
+ throws SQLException {
+ String dbName = metaData.getDatabaseProductName();
+ String dbVer = metaData.getDatabaseProductVersion();
+ int dbMaj = metaData.getDatabaseMajorVersion();
+ int dbMin = metaData.getDatabaseMinorVersion();
+ log.info("Database product: {} version: {} ({}.{})",
+ dbName, dbVer, dbMaj, dbMin);
+ String lcName = dbName.toLowerCase();
+ if (lcName.contains("mysql")) {
+ log.info("Using MySQL database");
+ if (dbMaj != 5) {
+ log.warn("Unsupported MySQL major version: {}", dbMaj);
+ }
+ if (dbMin > 5) {
+ log.warn("Unsupported MySQL minor version: {}", dbMin);
+ }
+ } else if (lcName.contains("h2")) {
+ log.info("Using H2 database (not for production)");
+ } else {
+ log.warn("Unsupported database");
+ }
+ String drvName = metaData.getDriverName();
+ String drvVer = metaData.getDriverVersion();
+ int drvMaj = metaData.getDriverMajorVersion();
+ int drvMin = metaData.getDriverMinorVersion();
+ log.info("JDBC driver: {} version: {} ({}.{})",
+ drvName, drvVer, drvMaj, drvMin);
+ }
+
+ private static boolean shouldWrap(DatabaseMetaData metaData)
+ throws SQLException {
+ if (USE_WRAPPER != null) {
+ if ("false".equals(USE_WRAPPER)) {
+ log.info("Not wrapping JDBC connection (disabled by system " +
+ "property {})", PROPERTY_USE_WRAPPER);
+ return false;
+ }
+ if ("true".equals(USE_WRAPPER)) {
+ log.info("Wrapping JDBC connection (forced by system " +
+ "property {})", PROPERTY_USE_WRAPPER);
+ return true;
+ }
+ if (!("auto".equals(USE_WRAPPER))) {
+ log.warn("Unknown value for system property {}: {}",
+ PROPERTY_USE_WRAPPER, USE_WRAPPER);
+ }
+ }
+ String driverName = metaData.getDriverName();
+ if (driverName.equals("MySQL Connector Java") ||
+ driverName.equals("MySQL-AB JDBC Driver") ||
+ driverName.equals("mariadb-jdbc")) {
+ // these drivers are known to use streaming result sets
+ // when fetchSize == Integer.MIN_VALUE
+ log.info("No need to wrap JDBC connection: driver: {}", driverName);
+ return false;
+ } else if (driverName.toLowerCase().contains("mysql") ||
+ driverName.toLowerCase().contains("mariadb")) {
+ // NB: if a future mysql/mariadb driver does away with the
+ // fetchSize trick for streaming, please add a special case and
+ // return true, or remove the Zanata code which calls
+ // setFetchSize(Integer.MIN_VALUE). See StreamingEntityIterator.
+ log.warn("Unrecognised mysql/mariadb driver: {}", driverName);
+ log.warn("Streaming results may not work");
+ return false;
+ } else {
+ log.info("Wrapping JDBC connection: found non-mysql/mariadb " +
+ "driver: {}", driverName);
+ return true;
+ }
+ }
+
+}