/**
* 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.portlet;
import com.liferay.portal.kernel.bean.PortalBeanLocatorUtil;
import com.liferay.portal.kernel.dao.orm.EntityCacheUtil;
import com.liferay.portal.kernel.dao.orm.FinderCacheUtil;
import com.liferay.portal.kernel.exception.NoSuchPreferencesException;
import com.liferay.portal.kernel.portlet.PortalPreferences;
import com.liferay.portal.kernel.portlet.PortletPreferencesFactoryUtil;
import com.liferay.portal.kernel.service.PortalPreferencesLocalService;
import com.liferay.portal.kernel.service.PortalPreferencesLocalServiceUtil;
import com.liferay.portal.kernel.service.persistence.PortalPreferencesUtil;
import com.liferay.portal.kernel.test.ReflectionTestUtil;
import com.liferay.portal.kernel.test.rule.AggregateTestRule;
import com.liferay.portal.kernel.test.util.RandomTestUtil;
import com.liferay.portal.kernel.transaction.TransactionConfig;
import com.liferay.portal.kernel.transaction.TransactionInvokerUtil;
import com.liferay.portal.kernel.util.PortletKeys;
import com.liferay.portal.kernel.util.ProxyUtil;
import com.liferay.portal.kernel.util.ReflectionUtil;
import com.liferay.portal.spring.transaction.DefaultTransactionExecutor;
import com.liferay.portal.spring.transaction.TransactionAttributeAdapter;
import com.liferay.portal.spring.transaction.TransactionInterceptor;
import com.liferay.portal.spring.transaction.TransactionStatusAdapter;
import com.liferay.portal.test.rule.LiferayIntegrationTestRule;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.ConcurrentModificationException;
import java.util.concurrent.Callable;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.FutureTask;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.springframework.transaction.PlatformTransactionManager;
/**
* @author Matthew Tambara
* @author Shuyang Zhou
*/
public class PortalPreferencesImplTest {
@ClassRule
@Rule
public static final AggregateTestRule aggregateTestRule =
new LiferayIntegrationTestRule();
@BeforeClass
public static void setUpClass() throws NoSuchMethodException {
_transactionInterceptor =
(TransactionInterceptor)PortalBeanLocatorUtil.locate(
"transactionAdvice");
_originalTransactionExecutor = ReflectionTestUtil.getFieldValue(
_transactionInterceptor, "transactionExecutor");
_originalPortalPreferencesLocalService =
PortalPreferencesLocalServiceUtil.getService();
_updatePreferencesMethod =
PortalPreferencesLocalService.class.getMethod(
"updatePortalPreferences",
com.liferay.portal.kernel.model.PortalPreferences.class);
}
@Before
public void setUp() throws Exception {
_testOwnerId = RandomTestUtil.nextLong();
PortalPreferences portalPreferences =
PortletPreferencesFactoryUtil.getPortalPreferences(
_testOwnerId, true);
portalPreferences.setValue(_NAMESPACE, "testKey", "testValue");
_synchronizeThreadLocal.set(true);
}
@After
public void tearDown() throws Throwable {
_synchronizeThreadLocal.set(false);
TransactionConfig.Builder builder = new TransactionConfig.Builder();
TransactionInvokerUtil.invoke(
builder.build(),
new Callable<Void>() {
@Override
public Void call() throws NoSuchPreferencesException {
PortalPreferencesUtil.removeByO_O(
_testOwnerId, PortletKeys.PREFS_OWNER_TYPE_USER);
return null;
}
});
PortalPreferencesWrapperCacheUtil.remove(
_testOwnerId, PortletKeys.PREFS_OWNER_TYPE_USER);
}
@Test
public void testReset() throws Exception {
Callable<Void> callable = new Callable<Void>() {
@Override
public Void call() {
PortalPreferences portalPreferences =
PortletPreferencesFactoryUtil.getPortalPreferences(
_testOwnerId, true);
portalPreferences.resetValues(_NAMESPACE);
return null;
}
};
try {
updateSynchronously(
new FutureTask<>(callable), new FutureTask<>(callable));
Assert.fail();
}
catch (Exception e) {
Throwable cause = e.getCause();
Assert.assertSame(
ConcurrentModificationException.class, cause.getClass());
}
}
@Test
public void testSetSameKeyDifferentValues() throws Exception {
FutureTask<Void> futureTask1 = new FutureTask<>(
new Callable<Void>() {
@Override
public Void call() {
PortalPreferences portalPreferences =
PortletPreferencesFactoryUtil.getPortalPreferences(
_testOwnerId, true);
portalPreferences.setValues(
_NAMESPACE, _KEY_1, new String[] {null, _VALUE_2});
return null;
}
});
FutureTask<Void> futureTask2 = new FutureTask<>(
new Callable<Void>() {
@Override
public Void call() {
PortalPreferences portalPreferences =
PortletPreferencesFactoryUtil.getPortalPreferences(
_testOwnerId, true);
portalPreferences.setValue(_NAMESPACE, _KEY_1, _VALUE_1);
return null;
}
});
try {
updateSynchronously(futureTask1, futureTask2);
Assert.fail();
}
catch (Exception e) {
Throwable cause = e.getCause();
Assert.assertSame(
ConcurrentModificationException.class, cause.getClass());
}
}
@Test
public void testSetValueDifferentKeys() throws Exception {
FutureTask<Void> futureTask1 = new FutureTask<>(
new Callable<Void>() {
@Override
public Void call() {
PortalPreferences portalPreferences =
PortletPreferencesFactoryUtil.getPortalPreferences(
_testOwnerId, true);
portalPreferences.setValue(_NAMESPACE, _KEY_1, _VALUE_1);
return null;
}
});
FutureTask<Void> futureTask2 = new FutureTask<>(
new Callable<Void>() {
@Override
public Void call() {
PortalPreferences portalPreferences =
PortletPreferencesFactoryUtil.getPortalPreferences(
_testOwnerId, true);
portalPreferences.setValue(_NAMESPACE, _KEY_2, _VALUE_1);
return null;
}
});
updateSynchronously(futureTask1, futureTask2);
PortalPreferences portalPreferences =
PortletPreferencesFactoryUtil.getPortalPreferences(
_testOwnerId, true);
Assert.assertEquals(
_VALUE_1, portalPreferences.getValue(_NAMESPACE, _KEY_1));
Assert.assertEquals(
_VALUE_1, portalPreferences.getValue(_NAMESPACE, _KEY_2));
}
@Test
public void testSetValueSameKey() throws Exception {
FutureTask<Void> futureTask1 = new FutureTask<>(
new Callable<Void>() {
@Override
public Void call() {
PortalPreferences portalPreferences =
PortletPreferencesFactoryUtil.getPortalPreferences(
_testOwnerId, true);
portalPreferences.setValue(_NAMESPACE, _KEY_1, _VALUE_1);
return null;
}
});
FutureTask<Void> futureTask2 = new FutureTask<>(
new Callable<Void>() {
@Override
public Void call() {
PortalPreferences portalPreferences =
PortletPreferencesFactoryUtil.getPortalPreferences(
_testOwnerId, true);
portalPreferences.setValue(_NAMESPACE, _KEY_1, _VALUE_2);
return null;
}
});
try {
updateSynchronously(futureTask1, futureTask2);
Assert.fail();
}
catch (Exception e) {
Throwable cause = e.getCause();
Assert.assertSame(
ConcurrentModificationException.class, cause.getClass());
}
}
@Test
public void testSetValuesDifferentKeys() throws Exception {
FutureTask<Void> futureTask1 = new FutureTask<>(
new Callable<Void>() {
@Override
public Void call() {
PortalPreferences portalPreferences =
PortletPreferencesFactoryUtil.getPortalPreferences(
_testOwnerId, true);
portalPreferences.setValues(_NAMESPACE, _KEY_1, _VALUES_1);
return null;
}
});
FutureTask<Void> futureTask2 = new FutureTask<>(
new Callable<Void>() {
@Override
public Void call() {
PortalPreferences portalPreferences =
PortletPreferencesFactoryUtil.getPortalPreferences(
_testOwnerId, true);
portalPreferences.setValues(_NAMESPACE, _KEY_2, _VALUES_1);
return null;
}
});
updateSynchronously(futureTask1, futureTask2);
PortalPreferences portalPreferences =
PortletPreferencesFactoryUtil.getPortalPreferences(
_testOwnerId, true);
Assert.assertArrayEquals(
_VALUES_1, portalPreferences.getValues(_NAMESPACE, _KEY_1));
Assert.assertArrayEquals(
_VALUES_1, portalPreferences.getValues(_NAMESPACE, _KEY_2));
}
@Test
public void testSetValuesSameKey() throws Exception {
FutureTask<Void> futureTask1 = new FutureTask<>(
new Callable<Void>() {
@Override
public Void call() {
PortalPreferences portalPreferences =
PortletPreferencesFactoryUtil.getPortalPreferences(
_testOwnerId, true);
portalPreferences.setValues(_NAMESPACE, _KEY_1, _VALUES_1);
return null;
}
});
FutureTask<Void> futureTask2 = new FutureTask<>(
new Callable<Void>() {
@Override
public Void call() {
PortalPreferences portalPreferences =
PortletPreferencesFactoryUtil.getPortalPreferences(
_testOwnerId, true);
portalPreferences.setValues(_NAMESPACE, _KEY_1, _VALUES_2);
return null;
}
});
try {
updateSynchronously(futureTask1, futureTask2);
Assert.fail();
}
catch (Exception e) {
Throwable cause = e.getCause();
Assert.assertSame(
ConcurrentModificationException.class, cause.getClass());
}
}
protected void updateSynchronously(
FutureTask<Void> futureTask1, FutureTask<Void> futureTask2)
throws Exception {
ReflectionTestUtil.setFieldValue(
PortalPreferencesLocalServiceUtil.class, "_service",
ProxyUtil.newProxyInstance(
PortalPreferencesLocalService.class.getClassLoader(),
new Class<?>[] {PortalPreferencesLocalService.class},
new SynchronousInvocationHandler(_testOwnerId)));
Thread thread1 = new Thread(futureTask1, "Update Thread 1");
thread1.start();
Thread thread2 = new Thread(futureTask2, "Update Thread 2");
thread2.start();
futureTask1.get();
futureTask2.get();
EntityCacheUtil.clearLocalCache();
FinderCacheUtil.clearLocalCache();
}
protected static class SynchronizedTransactionExecutor
extends DefaultTransactionExecutor {
@Override
public void commit(
PlatformTransactionManager platformTransactionManager,
TransactionAttributeAdapter transactionAttributeAdapter,
TransactionStatusAdapter transactionStatusAdapter) {
if (!_synchronizeThreadLocal.get()) {
_originalTransactionExecutor.commit(
platformTransactionManager, transactionAttributeAdapter,
transactionStatusAdapter);
return;
}
try {
_cyclicBarrier.await();
_originalTransactionExecutor.commit(
platformTransactionManager, transactionAttributeAdapter,
transactionStatusAdapter);
}
catch (Throwable t) {
ReflectionUtil.throwException(t);
}
finally {
PortalPreferencesWrapperCacheUtil.remove(
_testOwnerId, PortletKeys.PREFS_OWNER_TYPE_USER);
}
}
private SynchronizedTransactionExecutor(long testOwnerId) {
_testOwnerId = testOwnerId;
}
private final CyclicBarrier _cyclicBarrier = new CyclicBarrier(
2,
new Runnable() {
@Override
public void run() {
_transactionInterceptor.setTransactionExecutor(
_originalTransactionExecutor);
}
});
private final long _testOwnerId;
}
protected static class SynchronousInvocationHandler
implements InvocationHandler {
@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
if (_synchronizeThreadLocal.get() &&
_updatePreferencesMethod.equals(method)) {
_cyclicBarrier.await();
}
return method.invoke(_originalPortalPreferencesLocalService, args);
}
private SynchronousInvocationHandler(long testOwnerId) {
_cyclicBarrier = new CyclicBarrier(
2,
new Runnable() {
@Override
public void run() {
_transactionInterceptor.setTransactionExecutor(
new SynchronizedTransactionExecutor(testOwnerId));
ReflectionTestUtil.setFieldValue(
PortalPreferencesLocalServiceUtil.class, "_service",
_originalPortalPreferencesLocalService);
}
});
}
private final CyclicBarrier _cyclicBarrier;
}
private static final String _KEY_1 = "key1";
private static final String _KEY_2 = "key2";
private static final String _NAMESPACE = "test";
private static final String _VALUE_1 = "value1";
private static final String _VALUE_2 = "value2";
private static final String[] _VALUES_1 = new String[] {"values1"};
private static final String[] _VALUES_2 = new String[] {"values2"};
private static PortalPreferencesLocalService
_originalPortalPreferencesLocalService;
private static DefaultTransactionExecutor _originalTransactionExecutor;
private static final ThreadLocal<Boolean> _synchronizeThreadLocal =
new InheritableThreadLocal<>();
private static TransactionInterceptor _transactionInterceptor;
private static Method _updatePreferencesMethod;
private long _testOwnerId;
}