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; + } + } + +}