/** * Copyright (c) 2000-present Liferay, Inc. All rights reserved. * * This library 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 library 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. */ package com.liferay.portal.kernel.dao.jdbc; import com.liferay.portal.kernel.concurrent.DefaultNoticeableFuture; import com.liferay.portal.kernel.nio.intraband.CancelingPortalExecutorManagerUtilAdvice; import com.liferay.portal.kernel.nio.intraband.PortalExecutorManagerUtilAdvice; import com.liferay.portal.kernel.test.ReflectionTestUtil; import com.liferay.portal.kernel.test.SwappableSecurityManager; import com.liferay.portal.kernel.test.rule.AggregateTestRule; import com.liferay.portal.kernel.test.rule.CodeCoverageAssertor; import com.liferay.portal.kernel.test.rule.NewEnv; import com.liferay.portal.kernel.util.PropsKeys; import com.liferay.portal.kernel.util.PropsUtilAdvice; import com.liferay.portal.kernel.util.ProxyUtil; import com.liferay.portal.kernel.util.ReflectionUtil; import com.liferay.portal.kernel.util.StringPool; import com.liferay.portal.test.rule.AdviseWith; import com.liferay.portal.test.rule.AspectJNewEnvTestRule; import com.liferay.registry.BasicRegistryImpl; import com.liferay.registry.RegistryUtil; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.sql.Connection; import java.sql.DatabaseMetaData; import java.sql.PreparedStatement; import java.sql.SQLException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.IdentityHashMap; import java.util.List; import java.util.Set; import java.util.concurrent.CancellationException; import java.util.concurrent.Future; import java.util.concurrent.atomic.AtomicInteger; import org.junit.Assert; import org.junit.Before; import org.junit.ClassRule; import org.junit.Rule; import org.junit.Test; /** * @author Shuyang Zhou * @author Preston Crary */ @NewEnv(type = NewEnv.Type.CLASSLOADER) public class AutoBatchPreparedStatementUtilTest { @ClassRule @Rule public static final AggregateTestRule aggregateTestRule = new AggregateTestRule( AspectJNewEnvTestRule.INSTANCE, CodeCoverageAssertor.INSTANCE); @Before public void setUp() { RegistryUtil.setRegistry(new BasicRegistryImpl()); } @AdviseWith(adviceClasses = {PropsUtilAdvice.class}) @Test public void testCINITFailure() throws ClassNotFoundException { PropsUtilAdvice.setProps(PropsKeys.HIBERNATE_JDBC_BATCH_SIZE, "0"); final NoSuchMethodException nsme = new NoSuchMethodException(); final AtomicInteger counter = new AtomicInteger(); try (SwappableSecurityManager swappableSecurityManager = new SwappableSecurityManager() { @Override public void checkPackageAccess(String pkg) { if (pkg.equals("java.sql") && (counter.getAndIncrement() == 1)) { ReflectionUtil.throwException(nsme); } } }) { swappableSecurityManager.install(); Class.forName(AutoBatchPreparedStatementUtil.class.getName()); } catch (ExceptionInInitializerError eiie) { Assert.assertSame(nsme, eiie.getCause()); } } @AdviseWith( adviceClasses = { CancelingPortalExecutorManagerUtilAdvice.class, PropsUtilAdvice.class } ) @Test public void testConcurrentCancellationException() { PropsUtilAdvice.setProps(PropsKeys.HIBERNATE_JDBC_BATCH_SIZE, "0"); doTestConcurrentCancellationException(true); doTestConcurrentCancellationException(false); } @AdviseWith( adviceClasses = { PortalExecutorManagerUtilAdvice.class, PropsUtilAdvice.class } ) @Test public void testConcurrentExecutionException() { PropsUtilAdvice.setProps(PropsKeys.HIBERNATE_JDBC_BATCH_SIZE, "0"); doTestConcurrentExecutionExceptions(true); doTestConcurrentExecutionExceptions(false); } @AdviseWith( adviceClasses = { PortalExecutorManagerUtilAdvice.class, PropsUtilAdvice.class } ) @Test public void testConcurrentWaitingForFutures() throws SQLException { PropsUtilAdvice.setProps(PropsKeys.HIBERNATE_JDBC_BATCH_SIZE, "0"); doTestConcurrentWaitingForFutures(true); doTestConcurrentWaitingForFutures(false); } @AdviseWith(adviceClasses = {PropsUtilAdvice.class} ) @Test public void testConstructor() throws ReflectiveOperationException { PropsUtilAdvice.setProps(PropsKeys.HIBERNATE_JDBC_BATCH_SIZE, "0"); Constructor<AutoBatchPreparedStatementUtil> constructor = AutoBatchPreparedStatementUtil.class.getDeclaredConstructor(); Assert.assertTrue(Modifier.isPublic(constructor.getModifiers())); constructor.setAccessible(true); constructor.newInstance(); } @AdviseWith( adviceClasses = { PortalExecutorManagerUtilAdvice.class, PropsUtilAdvice.class } ) @Test public void testNotSupportBatchUpdates() throws Exception { PropsUtilAdvice.setProps(PropsKeys.HIBERNATE_JDBC_BATCH_SIZE, "0"); doTestNotSupportBatchUpdates(); doTestNotSupportBatchUpdatesConcurrent(); } @AdviseWith( adviceClasses = { PortalExecutorManagerUtilAdvice.class, PropsUtilAdvice.class } ) @Test public void testSupportBatchUpdates() throws Exception { PropsUtilAdvice.setProps(PropsKeys.HIBERNATE_JDBC_BATCH_SIZE, "2"); doTestSupportBaseUpdates(); doTestSupportBaseUpdatesConcurrent(); } protected void doTestConcurrentCancellationException( boolean supportBatchUpdates) { try (PreparedStatement preparedStatement = AutoBatchPreparedStatementUtil.concurrentAutoBatch( (Connection)ProxyUtil.newProxyInstance( ClassLoader.getSystemClassLoader(), new Class<?>[] {Connection.class}, new ConnectionInvocationHandler( new PreparedStatementInvocationHandler( supportBatchUpdates))), StringPool.BLANK)) { preparedStatement.addBatch(); preparedStatement.executeBatch(); preparedStatement.addBatch(); preparedStatement.executeBatch(); } catch (Throwable t) { Assert.assertSame(CancellationException.class, t.getClass()); Throwable[] throwables = t.getSuppressed(); Assert.assertEquals( Arrays.toString(throwables), 1, throwables.length); Throwable throwable = throwables[0]; Assert.assertSame( CancellationException.class, throwable.getClass()); return; } Assert.fail(); } protected void doTestConcurrentExecutionExceptions( boolean supportBatchUpdates) { PreparedStatementInvocationHandler preparedStatementInvocationHandler = new PreparedStatementInvocationHandler(supportBatchUpdates); Set<Throwable> throwables = Collections.newSetFromMap( new IdentityHashMap<Throwable, Boolean>()); try (PreparedStatement preparedStatement = AutoBatchPreparedStatementUtil.concurrentAutoBatch( (Connection)ProxyUtil.newProxyInstance( ClassLoader.getSystemClassLoader(), new Class<?>[] {Connection.class}, new ConnectionInvocationHandler( preparedStatementInvocationHandler)), StringPool.BLANK)) { RuntimeException runtimeException1 = new RuntimeException(); throwables.add(runtimeException1); preparedStatementInvocationHandler.setRuntimeException( runtimeException1); preparedStatement.addBatch(); preparedStatement.executeBatch(); RuntimeException runtimeException2 = new RuntimeException(); throwables.add(runtimeException2); preparedStatementInvocationHandler.setRuntimeException( runtimeException2); preparedStatement.addBatch(); preparedStatement.executeBatch(); } catch (Throwable t) { Assert.assertTrue(throwables.contains(t)); Throwable[] suppressedThrowables = t.getSuppressed(); Assert.assertEquals( Arrays.toString(suppressedThrowables), 1, suppressedThrowables.length); Assert.assertTrue(throwables.contains(suppressedThrowables[0])); return; } Assert.fail(); } protected void doTestConcurrentWaitingForFutures( boolean supportBatchUpdates) throws SQLException { TestNoticeableFuture<Void> testNoticeableFuture = new TestNoticeableFuture<>(); try (PreparedStatement preparedStatement = AutoBatchPreparedStatementUtil.concurrentAutoBatch( (Connection)ProxyUtil.newProxyInstance( ClassLoader.getSystemClassLoader(), new Class<?>[] {Connection.class}, new ConnectionInvocationHandler( new PreparedStatementInvocationHandler( supportBatchUpdates))), StringPool.BLANK)) { InvocationHandler invocationHandler = ProxyUtil.getInvocationHandler(preparedStatement); Set<Future<Void>> futures = ReflectionTestUtil.getFieldValue( invocationHandler, "_futures"); futures.add(testNoticeableFuture); } Assert.assertTrue(testNoticeableFuture.hasCalledGet()); } protected void doTestNotSupportBatchUpdates() throws Exception { PreparedStatementInvocationHandler preparedStatementInvocationHandler = new PreparedStatementInvocationHandler(false); List<Method> methods = preparedStatementInvocationHandler.getMethods(); try (PreparedStatement preparedStatement = AutoBatchPreparedStatementUtil.autoBatch( (PreparedStatement)ProxyUtil.newProxyInstance( ClassLoader.getSystemClassLoader(), new Class<?>[] {PreparedStatement.class}, preparedStatementInvocationHandler))) { Assert.assertTrue(methods.toString(), methods.isEmpty()); // Calling addBatch fallbacks to executeUpdate preparedStatement.addBatch(); Assert.assertEquals(methods.toString(), 1, methods.size()); Assert.assertEquals( PreparedStatement.class.getMethod("executeUpdate"), methods.remove(0)); // Calling executeBatch does nothing Assert.assertArrayEquals( new int[0], preparedStatement.executeBatch()); Assert.assertTrue(methods.toString(), methods.isEmpty()); // Other methods like execute pass through preparedStatement.execute(); Assert.assertEquals(methods.toString(), 1, methods.size()); Assert.assertEquals( PreparedStatement.class.getMethod("execute"), methods.remove(0)); } Assert.assertEquals(methods.toString(), 1, methods.size()); Assert.assertEquals( PreparedStatement.class.getMethod("close"), methods.remove(0)); } protected void doTestNotSupportBatchUpdatesConcurrent() throws Exception { PreparedStatementInvocationHandler preparedStatementInvocationHandler = new PreparedStatementInvocationHandler(false); List<Method> methods = preparedStatementInvocationHandler.getMethods(); try (PreparedStatement preparedStatement = AutoBatchPreparedStatementUtil.concurrentAutoBatch( (Connection)ProxyUtil.newProxyInstance( ClassLoader.getSystemClassLoader(), new Class<?>[] {Connection.class}, new ConnectionInvocationHandler( preparedStatementInvocationHandler)), StringPool.BLANK)) { Assert.assertTrue(methods.toString(), methods.isEmpty()); // Calling addBatch fallbacks to executeUpdate preparedStatement.addBatch(); Assert.assertEquals(methods.toString(), 2, methods.size()); Assert.assertEquals( PreparedStatement.class.getMethod("executeUpdate"), methods.remove(0)); Assert.assertEquals( PreparedStatement.class.getMethod("close"), methods.remove(0)); // Calling executeBatch does nothing Assert.assertArrayEquals( new int[0], preparedStatement.executeBatch()); Assert.assertTrue(methods.toString(), methods.isEmpty()); // Other methods like execute pass through preparedStatement.execute(); Assert.assertEquals(methods.toString(), 1, methods.size()); Assert.assertEquals( PreparedStatement.class.getMethod("execute"), methods.remove(0)); } Assert.assertEquals(methods.toString(), 1, methods.size()); Assert.assertEquals( PreparedStatement.class.getMethod("close"), methods.remove(0)); } protected void doTestSupportBaseUpdates() throws Exception { PreparedStatementInvocationHandler preparedStatementInvocationHandler = new PreparedStatementInvocationHandler(true); List<Method> methods = preparedStatementInvocationHandler.getMethods(); try (PreparedStatement preparedStatement = AutoBatchPreparedStatementUtil.autoBatch( (PreparedStatement)ProxyUtil.newProxyInstance( ClassLoader.getSystemClassLoader(), new Class<?>[] {PreparedStatement.class}, preparedStatementInvocationHandler))) { InvocationHandler invocationHandler = ProxyUtil.getInvocationHandler(preparedStatement); Assert.assertEquals( Integer.valueOf(0), ReflectionTestUtil.getFieldValue(invocationHandler, "_count")); Assert.assertTrue(methods.toString(), methods.isEmpty()); // Protection for executing empty batch Assert.assertArrayEquals( new int[0], preparedStatement.executeBatch()); Assert.assertTrue(methods.toString(), methods.isEmpty()); Assert.assertEquals( Integer.valueOf(0), ReflectionTestUtil.getFieldValue(invocationHandler, "_count")); // Calling addBatch passes through when within the Hibernate JDBC // batch size preparedStatement.addBatch(); Assert.assertEquals(methods.toString(), 1, methods.size()); Assert.assertEquals( PreparedStatement.class.getMethod("addBatch"), methods.remove(0)); Assert.assertEquals( Integer.valueOf(1), ReflectionTestUtil.getFieldValue(invocationHandler, "_count")); // Calling addBatch passes through and triggers executeBatch when // exceeding the Hibernate JDBC batch size preparedStatement.addBatch(); Assert.assertEquals(methods.toString(), 2, methods.size()); Assert.assertEquals( PreparedStatement.class.getMethod("addBatch"), methods.remove(0)); Assert.assertEquals( PreparedStatement.class.getMethod("executeBatch"), methods.remove(0)); Assert.assertEquals( Integer.valueOf(0), ReflectionTestUtil.getFieldValue(invocationHandler, "_count")); // Calling addBatch passes through when within the Hibernate JDBC // batch size preparedStatement.addBatch(); Assert.assertEquals(methods.toString(), 1, methods.size()); Assert.assertEquals( PreparedStatement.class.getMethod("addBatch"), methods.remove(0)); Assert.assertEquals( Integer.valueOf(1), ReflectionTestUtil.getFieldValue(invocationHandler, "_count")); // Calling executeBatch passes through when batch is not empty preparedStatement.executeBatch(); Assert.assertEquals(methods.toString(), 1, methods.size()); Assert.assertEquals( PreparedStatement.class.getMethod("executeBatch"), methods.remove(0)); Assert.assertEquals( Integer.valueOf(0), ReflectionTestUtil.getFieldValue(invocationHandler, "_count")); // Other methods like execute pass through preparedStatement.execute(); Assert.assertEquals(methods.toString(), 1, methods.size()); Assert.assertEquals( PreparedStatement.class.getMethod("execute"), methods.remove(0)); Assert.assertEquals( Integer.valueOf(0), ReflectionTestUtil.getFieldValue(invocationHandler, "_count")); } Assert.assertEquals(methods.toString(), 1, methods.size()); Assert.assertEquals( PreparedStatement.class.getMethod("close"), methods.remove(0)); } protected void doTestSupportBaseUpdatesConcurrent() throws Exception { PreparedStatementInvocationHandler preparedStatementInvocationHandler = new PreparedStatementInvocationHandler(true); List<Method> methods = preparedStatementInvocationHandler.getMethods(); try (PreparedStatement preparedStatement = AutoBatchPreparedStatementUtil.concurrentAutoBatch( (Connection)ProxyUtil.newProxyInstance( ClassLoader.getSystemClassLoader(), new Class<?>[] {Connection.class}, new ConnectionInvocationHandler( preparedStatementInvocationHandler)), StringPool.BLANK)) { InvocationHandler invocationHandler = ProxyUtil.getInvocationHandler(preparedStatement); Assert.assertEquals( Integer.valueOf(0), ReflectionTestUtil.getFieldValue(invocationHandler, "_count")); Assert.assertTrue(methods.toString(), methods.isEmpty()); // Protection for executing empty batch Assert.assertArrayEquals( new int[0], preparedStatement.executeBatch()); Assert.assertTrue(methods.toString(), methods.isEmpty()); Assert.assertEquals( Integer.valueOf(0), ReflectionTestUtil.getFieldValue(invocationHandler, "_count")); // Calling addBatch passes through when within the Hibernate JDBC // batch size preparedStatement.addBatch(); Assert.assertEquals(methods.toString(), 1, methods.size()); Assert.assertEquals( PreparedStatement.class.getMethod("addBatch"), methods.remove(0)); Assert.assertEquals( Integer.valueOf(1), ReflectionTestUtil.getFieldValue(invocationHandler, "_count")); // Calling addBatch passes through and triggers executeBatch when // exceeding the Hibernate JDBC batch size preparedStatement.addBatch(); Assert.assertEquals(methods.toString(), 3, methods.size()); Assert.assertEquals( PreparedStatement.class.getMethod("addBatch"), methods.remove(0)); Assert.assertEquals( PreparedStatement.class.getMethod("executeBatch"), methods.remove(0)); Assert.assertEquals( Integer.valueOf(0), ReflectionTestUtil.getFieldValue(invocationHandler, "_count")); Assert.assertEquals( PreparedStatement.class.getMethod("close"), methods.remove(0)); // Calling addBatch passes through when within the Hibernate JDBC // batch size preparedStatement.addBatch(); Assert.assertEquals(methods.toString(), 1, methods.size()); Assert.assertEquals( PreparedStatement.class.getMethod("addBatch"), methods.remove(0)); Assert.assertEquals( Integer.valueOf(1), ReflectionTestUtil.getFieldValue(invocationHandler, "_count")); // Calling executeBatch passes through when batch is not empty preparedStatement.executeBatch(); Assert.assertEquals(methods.toString(), 2, methods.size()); Assert.assertEquals( PreparedStatement.class.getMethod("executeBatch"), methods.remove(0)); Assert.assertEquals( Integer.valueOf(0), ReflectionTestUtil.getFieldValue(invocationHandler, "_count")); Assert.assertEquals( PreparedStatement.class.getMethod("close"), methods.remove(0)); // Other methods like execute pass through preparedStatement.execute(); Assert.assertEquals(methods.toString(), 1, methods.size()); Assert.assertEquals( PreparedStatement.class.getMethod("execute"), methods.remove(0)); Assert.assertEquals( Integer.valueOf(0), ReflectionTestUtil.getFieldValue(invocationHandler, "_count")); } Assert.assertEquals(methods.toString(), 1, methods.size()); Assert.assertEquals( PreparedStatement.class.getMethod("close"), methods.remove(0)); } private static class ConnectionInvocationHandler implements InvocationHandler { @Override public Object invoke(Object proxy, Method method, Object[] args) throws NoSuchMethodException { if (method.equals(Connection.class.getMethod("getMetaData"))) { return ProxyUtil.newProxyInstance( ClassLoader.getSystemClassLoader(), new Class<?>[] {DatabaseMetaData.class}, new DatabaseMetaDataInvocationHandler( _preparedStatementInvocationHandler. _supportBatchUpdates)); } if (method.equals( Connection.class.getMethod( "prepareStatement", String.class))) { return ProxyUtil.newProxyInstance( ClassLoader.getSystemClassLoader(), new Class<?>[] {PreparedStatement.class}, _preparedStatementInvocationHandler); } throw new UnsupportedOperationException(); } private ConnectionInvocationHandler( PreparedStatementInvocationHandler preparedStatementInvocationHandler) { _preparedStatementInvocationHandler = preparedStatementInvocationHandler; } private final PreparedStatementInvocationHandler _preparedStatementInvocationHandler; } private static class DatabaseMetaDataInvocationHandler implements InvocationHandler { @Override public Object invoke(Object proxy, Method method, Object[] args) throws NoSuchMethodException { if (method.equals( DatabaseMetaData.class.getMethod("supportsBatchUpdates"))) { return _supportBatchUpdates; } throw new UnsupportedOperationException(); } private DatabaseMetaDataInvocationHandler(boolean supportBatchUpdates) { _supportBatchUpdates = supportBatchUpdates; } private final boolean _supportBatchUpdates; } private static class PreparedStatementInvocationHandler implements InvocationHandler { public List<Method> getMethods() { return _methods; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws NoSuchMethodException { if (method.equals( PreparedStatement.class.getMethod("getConnection"))) { return ProxyUtil.newProxyInstance( ClassLoader.getSystemClassLoader(), new Class<?>[] {Connection.class}, new ConnectionInvocationHandler(this)); } _methods.add(method); if (method.equals(PreparedStatement.class.getMethod("addBatch"))) { return null; } if (method.equals(PreparedStatement.class.getMethod("close"))) { return null; } if (method.equals(PreparedStatement.class.getMethod("execute"))) { return false; } if (method.equals( PreparedStatement.class.getMethod("executeBatch"))) { if (_runtimeException != null) { throw _runtimeException; } return new int[0]; } if (method.equals( PreparedStatement.class.getMethod("executeUpdate"))) { if (_runtimeException != null) { throw _runtimeException; } return 0; } throw new UnsupportedOperationException(); } public void setRuntimeException(RuntimeException runtimeException) { _runtimeException = runtimeException; } private PreparedStatementInvocationHandler( boolean supportBatchUpdates) { _supportBatchUpdates = supportBatchUpdates; } private final List<Method> _methods = new ArrayList<>(); private RuntimeException _runtimeException; private final boolean _supportBatchUpdates; } private static final class TestNoticeableFuture<T> extends DefaultNoticeableFuture<T> { @Override public T get() { _calledGet = true; return null; } public boolean hasCalledGet() { return _calledGet; } private boolean _calledGet; } }