package de.cronn.reflection.util.immutable; import static org.assertj.core.api.Assertions.*; import java.io.File; import java.net.URI; import java.nio.file.Paths; import java.time.Duration; import java.time.Instant; import java.time.LocalDate; import java.time.Month; import java.time.ZonedDateTime; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.UUID; import java.util.concurrent.CompletionService; import java.util.concurrent.ExecutorCompletionService; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import org.apache.commons.lang3.SerializationUtils; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; import de.cronn.reflection.util.immutable.collection.DeepImmutableList; import de.cronn.reflection.util.immutable.collection.DeepImmutableMap; import de.cronn.reflection.util.immutable.collection.DeepImmutableSet; import de.cronn.reflection.util.testclasses.ClassWithDefaultMethods; import de.cronn.reflection.util.testclasses.ClassWithInheritedDefaultMethods; import de.cronn.reflection.util.testclasses.FinalClass; import de.cronn.reflection.util.testclasses.OtherTestEntity; import de.cronn.reflection.util.testclasses.SubclassOfClassWithDefaultMethods; import de.cronn.reflection.util.testclasses.TestEntity; import de.cronn.reflection.util.testclasses.TestEnum; public class ImmutableProxyTest { public static final String IMMUTABLE_EXCEPTION_MESSAGE = "This instance is immutable." + " Annotate the method with @ReadOnly if this is a false-positive."; private static final long TEST_TIMEOUT_SECONDS = 30; @Test void testImmutableProxy() throws Exception { TestEntity original = new TestEntity(123); TestEntity immutableProxy = ImmutableProxy.create(original); assertThat(immutableProxy).hasSameHashCodeAs(original); assertThat(immutableProxy).hasToString(original.toString()); assertThat(immutableProxy.getNumber()).isEqualTo(123); assertThatExceptionOfType(UnsupportedOperationException.class) .isThrownBy(() -> immutableProxy.setNumber(456)) .withMessage(IMMUTABLE_EXCEPTION_MESSAGE); } @Test void testUnwrap() throws Exception { TestEntity original = new TestEntity(123); TestEntity immutableProxy = ImmutableProxy.create(original); assertThat(ImmutableProxy.unwrap(immutableProxy)).isSameAs(original); assertThat(ImmutableProxy.unwrap(original)).isSameAs(original); } @Test void testImmutableProxyOnFinalClass() throws Exception { FinalClass finalClass = new FinalClass(); assertThatExceptionOfType(IllegalArgumentException.class) .isThrownBy(() -> ImmutableProxy.create(finalClass)) .withMessage("Cannot subclass primitive, array or final types: " + FinalClass.class); } @Test void testImmutableProxy_TestEntity() throws Exception { TestEntity original = new TestEntity(123); original.setSomeInstant(Instant.parse("2018-07-12T13:38:56Z")); original.setSomeUuid(UUID.fromString("28e93b24-7252-43d8-a223-ca0b3270bd7f")); original.setSomeFile(new File("some-file")); original.setSomePath(Paths.get("some path")); original.setSomeUri(new URI("file://some-path")); original.setSomeList(Arrays.asList(new OtherTestEntity("one"), new OtherTestEntity("other"))); original.setSomeSet(new LinkedHashSet<>(Arrays.asList("a", "b", "c"))); TestEntity immutableProxy = ImmutableProxy.create(original); assertThat(immutableProxy.getSomeList()).isNotSameAs(original.getSomeList()); assertThat(immutableProxy.getSomeSet()).isNotSameAs(original.getSomeSet()); assertThatExceptionOfType(UnsupportedOperationException.class) .isThrownBy(() -> immutableProxy.getSomeList().clear()) .withMessage("This list is immutable"); assertThatExceptionOfType(UnsupportedOperationException.class) .isThrownBy(() -> immutableProxy.getSomeSet().clear()) .withMessage("This set is immutable"); assertThatExceptionOfType(UnsupportedOperationException.class) .isThrownBy(() -> immutableProxy.getSomeCollection().clear()) .withMessage("This collection is immutable"); assertThatExceptionOfType(UnsupportedOperationException.class) .isThrownBy(() -> immutableProxy.getSomeIterable().iterator().remove()) .withMessage("This collection is immutable"); assertThatExceptionOfType(UnsupportedOperationException.class) .isThrownBy(immutableProxy::clear) .withMessage(IMMUTABLE_EXCEPTION_MESSAGE); assertThat(immutableProxy.countSomeSet()).isEqualTo(immutableProxy.getSomeSet().size()); assertThat(immutableProxy.countSomeList()).isEqualTo(immutableProxy.getSomeList().size()); assertThat(immutableProxy.countNothing()).isZero(); assertThat(immutableProxy.asMyself()).isInstanceOf(Immutable.class); assertThat(immutableProxy.asMyself().getSomeInstant()).isSameAs(original.getSomeInstant()); assertThat(immutableProxy.asMyself().getSomeUuid()).isSameAs(original.getSomeUuid()); assertThat(immutableProxy.asMyself().getSomeFile()).isSameAs(original.getSomeFile()); assertThat(immutableProxy.asMyself().getSomePath()).isSameAs(original.getSomePath()); assertThat(immutableProxy.asMyself().getSomeUri()).isSameAs(original.getSomeUri()); } @Test void testDate() throws Exception { Date original = new Date(12345678); Date immutableDate = ImmutableProxy.create(original); assertThat(immutableDate).isInstanceOf(Immutable.class); assertThat(immutableDate).hasTime(12345678L); assertThat(immutableDate.clone()).isEqualTo(original); assertThatExceptionOfType(UnsupportedOperationException.class) .isThrownBy(() -> immutableDate.setTime(12345L)) .withMessage(IMMUTABLE_EXCEPTION_MESSAGE); } @Test void testImmutableProxyOnObject() throws Exception { Object original = new Object(); Object immutableProxy = ImmutableProxy.create(original); assertThat(immutableProxy).hasSameHashCodeAs(original); assertThat(immutableProxy).isEqualTo(original); } @Test void testImmutableProxyIsAView() throws Exception { TestEntity original = new TestEntity(123); TestEntity immutableProxy = ImmutableProxy.create(original); original.setNumber(456); assertThat(immutableProxy.getNumber()).isEqualTo(456); } @Test void testImmutableProxy_ReferencedEntityIsAlsoImmutable() throws Exception { TestEntity original = new TestEntity(); original.setOtherTestEntity(new OtherTestEntity()); TestEntity immutableProxy = ImmutableProxy.create(original); assertThatExceptionOfType(UnsupportedOperationException.class) .isThrownBy(() -> immutableProxy.getOtherTestEntity().setName("other name")) .withMessage(IMMUTABLE_EXCEPTION_MESSAGE); } @Test void testImmutableProxy_Collection() throws Exception { TestEntity original = new TestEntity(); original.setSomeList(new ArrayList<>()); original.setSomeSet(new LinkedHashSet<>()); TestEntity immutableProxy = ImmutableProxy.create(original); List<OtherTestEntity> immutableList = immutableProxy.getSomeList(); assertThat(immutableList).isEqualTo(original.getSomeList()); assertThat(immutableList).hasSameSizeAs(original.getSomeList()); assertThat(immutableList).hasSameHashCodeAs(original.getSomeList()); assertThatExceptionOfType(UnsupportedOperationException.class) .isThrownBy(() -> immutableList.add(new OtherTestEntity())) .withMessage("This list is immutable"); assertThatExceptionOfType(UnsupportedOperationException.class) .isThrownBy(() -> immutableProxy.getSomeSet().add("new value")) .withMessage("This set is immutable"); assertThat(immutableList).isEmpty(); original.getSomeList().add(new OtherTestEntity()); assertThat(immutableList).hasSize(1); assertThat(immutableList).isNotEmpty(); original.getSomeList().add(new OtherTestEntity()); assertThat(immutableList).hasSize(2); assertThat(immutableList.get(0)).isSameAs(immutableList.get(0)); assertThatExceptionOfType(UnsupportedOperationException.class) .isThrownBy(() -> immutableList.get(0).setName("new name")) .withMessage(IMMUTABLE_EXCEPTION_MESSAGE); assertThatExceptionOfType(UnsupportedOperationException.class) .isThrownBy(() -> ((OtherTestEntity) immutableList.toArray()[0]).setName("new name")) .withMessage(IMMUTABLE_EXCEPTION_MESSAGE); assertThatExceptionOfType(UnsupportedOperationException.class) .isThrownBy(() -> immutableList.toArray(new OtherTestEntity[0])[0].setName("new name")) .withMessage(IMMUTABLE_EXCEPTION_MESSAGE); assertThatExceptionOfType(UnsupportedOperationException.class) .isThrownBy(() -> immutableList.stream().findFirst().get().setName("new name")) .withMessage(IMMUTABLE_EXCEPTION_MESSAGE); assertThatExceptionOfType(UnsupportedOperationException.class) .isThrownBy(() -> immutableList.forEach(entity -> entity.setName("new name"))) .withMessage(IMMUTABLE_EXCEPTION_MESSAGE); assertThatExceptionOfType(UnsupportedOperationException.class) .isThrownBy(() -> immutableList.removeIf(w -> true)) .withMessage("This list is immutable"); } @Test void testImmutableProxy_TooSpecificReturnType() throws Exception { TestEntity original = new TestEntity(); original.setSomeList(Collections.emptyList()); TestEntity immutableProxy = ImmutableProxy.create(original); assertThatExceptionOfType(UnsupportedOperationException.class) .isThrownBy(immutableProxy::getSomeArrayList) .withMessage("Cannot create immutable collection for TestEntity.getSomeArrayList." + " The return type is unknown or too specific: class java.util.ArrayList." + " Consider to define a more generic type: Set/List/Collection"); assertThatExceptionOfType(UnsupportedOperationException.class) .isThrownBy(immutableProxy::getSomeTreeMap) .withMessage("Cannot create immutable map for TestEntity.getSomeTreeMap." + " The return type is unknown or too specific: class java.util.TreeMap." + " Consider to define a more generic type: Map"); } @Test void testImmutableProxy_Collection_Stream() throws Exception { Collection<TestEntity> entities = Arrays.asList(new TestEntity(1), new TestEntity(2)); Collection<TestEntity> immutableCollection = ImmutableProxy.create(entities); assertThat(immutableCollection.stream().anyMatch(wheel -> wheel.getNumber() == 1)).isTrue(); List<TestEntity> immutableList = ImmutableProxy.create(new ArrayList<>(entities)); assertThat(immutableList.stream().anyMatch(wheel -> wheel.getNumber() == 1)).isTrue(); Set<TestEntity> immutableSet = ImmutableProxy.create(new LinkedHashSet<>(entities)); assertThat(immutableSet.stream().anyMatch(wheel -> wheel.getNumber() == 1)).isTrue(); } @Test void testImmutableProxy_Collection_Iterator() throws Exception { Collection<TestEntity> entities = Arrays.asList(new TestEntity(1), new TestEntity(2)); Collection<TestEntity> immutableEntityCollection = ImmutableProxy.create(entities); List<TestEntity> immutableEntityList = ImmutableProxy.create(new ArrayList<>(entities)); Set<TestEntity> immutableEntitySet = ImmutableProxy.create(new LinkedHashSet<>(entities)); // iterator next -> readOnly should work assertThat(immutableEntityCollection.iterator().next().getNumber()).isEqualTo(1); assertThat(immutableEntityList.iterator().next().getNumber()).isEqualTo(1); assertThat(immutableEntitySet.iterator().next().getNumber()).isEqualTo(1); // iterator next -> modification should be forbidden assertThatExceptionOfType(UnsupportedOperationException.class) .isThrownBy(() -> immutableEntityCollection.iterator().next().setNumber(123)) .withMessage(IMMUTABLE_EXCEPTION_MESSAGE); assertThatExceptionOfType(UnsupportedOperationException.class) .isThrownBy(() -> immutableEntityList.iterator().next().setNumber(123)) .withMessage(IMMUTABLE_EXCEPTION_MESSAGE); assertThatExceptionOfType(UnsupportedOperationException.class) .isThrownBy(() -> immutableEntitySet.iterator().next().setNumber(123)) .withMessage(IMMUTABLE_EXCEPTION_MESSAGE); // iterator remove should be forbidden assertThatExceptionOfType(UnsupportedOperationException.class) .isThrownBy(() -> immutableEntityCollection.iterator().remove()) .withMessage("This collection is immutable"); assertThatExceptionOfType(UnsupportedOperationException.class) .isThrownBy(() -> immutableEntityList.iterator().remove()) .withMessage("This list is immutable"); assertThatExceptionOfType(UnsupportedOperationException.class) .isThrownBy(() -> immutableEntitySet.iterator().remove()) .withMessage("This set is immutable"); } @Test void testImmutableProxy_Map() throws Exception { TestEntity original = new TestEntity(); original.setSomeMap(new LinkedHashMap<>()); original.getSomeMap().put("a", new OtherTestEntity("a")); original.getSomeMap().put("b", new OtherTestEntity("b")); TestEntity immutableProxy = ImmutableProxy.create(original); Map<String, OtherTestEntity> immutableMap = immutableProxy.getSomeMap(); assertThat(ImmutableProxy.isImmutable(immutableMap)).isTrue(); assertThat(immutableMap).hasSameSizeAs(original.getSomeMap()); assertThat(immutableMap.get("a").getImmutableValue()).isEqualTo("a"); assertThatExceptionOfType(UnsupportedOperationException.class) .isThrownBy(() -> immutableMap.get("a").setName("new name")) .withMessage(IMMUTABLE_EXCEPTION_MESSAGE); } @Test void testImmutableProxyOnImmutableValue() throws Exception { Object nullValue = null; assertThat(ImmutableProxy.create(nullValue)).isNull(); assertImmutableProxyReturnsSameInstance("abc"); assertImmutableProxyReturnsSameInstance(5L); assertImmutableProxyReturnsSameInstance(10); assertImmutableProxyReturnsSameInstance(1.5); assertImmutableProxyReturnsSameInstance(3.14F); assertImmutableProxyReturnsSameInstance(true); assertImmutableProxyReturnsSameInstance('a'); assertImmutableProxyReturnsSameInstance(LocalDate.of(2018, Month.JULY, 12)); assertImmutableProxyReturnsSameInstance(Instant.parse("2019-03-17T11:19:38.000Z")); assertImmutableProxyReturnsSameInstance(ZonedDateTime.parse("2019-03-17T11:19:38.000+02:00")); assertImmutableProxyReturnsSameInstance(Duration.ofSeconds(5)); assertImmutableProxyReturnsSameInstance(TestEnum.NORMAL); assertImmutableProxyReturnsSameInstance(TestEnum.SPECIAL); assertImmutableProxyReturnsSameInstance(TestEnum.SPECIAL); assertImmutableProxyReturnsSameInstance(DeepImmutableSet.of("foo")); assertImmutableProxyReturnsSameInstance(DeepImmutableList.of("bar")); } private static void assertImmutableProxyReturnsSameInstance(Object value) { assertThat(ImmutableProxy.create(value)).isSameAs(value); } @Test void testImmutableProxyIsEqualToOriginal() throws Exception { TestEntity original = new TestEntity(123); TestEntity immutableProxy = ImmutableProxy.create(original); assertThat(immutableProxy).isEqualTo(original); } @Test void testImmutableProxyOnImmutableProxy() throws Exception { TestEntity original = new TestEntity(123); TestEntity proxy1 = ImmutableProxy.create(original); TestEntity proxy2 = ImmutableProxy.create(proxy1); assertThat(proxy2).isSameAs(proxy1); } @Test @Timeout(TEST_TIMEOUT_SECONDS) void testConcurrentlyCreateProxy() throws Exception { ExecutorService executorService = Executors.newFixedThreadPool(5); try { CompletionService<TestEntity> completionService = new ExecutorCompletionService<>(executorService); for (int x = 0; x < 50; x++) { TestEntity entity = new TestEntity(100 + x); int numRepetitions = 20; for (int i = 0; i < numRepetitions; i++) { completionService.submit(() -> ImmutableProxy.create(entity)); } for (int i = 0; i < numRepetitions; i++) { TestEntity immutableProxy = completionService.take().get(); assertThat(immutableProxy).isNotSameAs(entity); assertThat(immutableProxy.getNumber()).isEqualTo(entity.getNumber()); } ImmutableProxy.clearCache(); } } finally { executorService.shutdown(); executorService.awaitTermination(TEST_TIMEOUT_SECONDS, TimeUnit.SECONDS); } } @Test void testIsImmutableProxy() throws Exception { assertThat(ImmutableProxy.isImmutableProxy(null)).isFalse(); assertThat(ImmutableProxy.isImmutableProxy(ImmutableProxy.create("foo"))).isFalse(); assertThat(ImmutableProxy.isImmutableProxy(ImmutableProxy.create(new TestEntity()))).isTrue(); } @Test void testReadOnlyAnnotationsInInterface() throws Exception { assertThat(ImmutableProxy.create(new ClassWithInheritedDefaultMethods()).size()).isEqualTo(0); assertThat(ImmutableProxy.create(new SubclassOfClassWithDefaultMethods()).size()).isEqualTo(0); assertThat(ImmutableProxy.create(new ClassWithDefaultMethods()).size()).isEqualTo(0); } @Test void testDoNotProxyReturnValueIfDisabledByReadOnlyAnnotation() throws Exception { TestEntity proxy = ImmutableProxy.create(new TestEntity(123)); TestEntity reference = proxy.asReference(); assertThat(reference.getNumber()).isEqualTo(123); assertThat(ImmutableProxy.isImmutableProxy(reference)).isFalse(); reference.setNumber(456); assertThat(reference.getNumber()).isEqualTo(456); TestEntity immutableReference = (TestEntity) proxy.asReferenceImmutableProxy(); assertThat(ImmutableProxy.isImmutableProxy(immutableReference)).isTrue(); assertThatExceptionOfType(UnsupportedOperationException.class) .isThrownBy(() -> immutableReference.setNumber(456)) .withMessage(IMMUTABLE_EXCEPTION_MESSAGE); } @Test void testDoNotProxyReturnValueInCloneMethod() throws Exception { TestEntity proxy = ImmutableProxy.create(new TestEntity(123)); TestEntity clone = proxy.clone(); assertThat(clone.getNumber()).isEqualTo(123); assertThat(ImmutableProxy.isImmutableProxy(clone)).isFalse(); clone.setNumber(456); assertThat(clone.getNumber()).isEqualTo(456); } @Test void testSerializeDeepImmutableSet() throws Exception { DeepImmutableSet<String> proxy = (DeepImmutableSet<String>) ImmutableProxy.create(Collections.singleton("foo")); DeepImmutableSet<String> clone = SerializationUtils.clone(proxy); assertThat(clone).isNotSameAs(proxy).containsExactly("foo"); } @Test void testSerializeDeepImmutableList() throws Exception { DeepImmutableList<String> proxy = (DeepImmutableList<String>) ImmutableProxy.create(Arrays.asList("a", "b", "c")); DeepImmutableList<String> clone = SerializationUtils.clone(proxy); assertThat(clone).isNotSameAs(proxy).containsExactly("a", "b", "c"); } @Test void testSerializeDeepImmutableMap() throws Exception { DeepImmutableMap<String, String> proxy = (DeepImmutableMap<String, String>) ImmutableProxy.create(Collections.singletonMap("k", "v")); DeepImmutableMap<String, String> clone = SerializationUtils.clone(proxy); assertThat(clone).isNotSameAs(proxy).containsExactly(entry("k", "v")); } @Test void testSerializeImmutableProxy() throws Exception { TestEntity original = new TestEntity(123); original.setSomeList(Arrays.asList( new OtherTestEntity("one"), new OtherTestEntity("other") )); TestEntity proxy = ImmutableProxy.create(original); OtherTestEntity firstElementBefore = proxy.getSomeList().get(0); TestEntity clone = SerializationUtils.clone(proxy); assertThat(clone).isInstanceOf(Immutable.class); OtherTestEntity firstElementAfter = clone.getSomeList().get(0); assertThat(firstElementAfter).isNotSameAs(firstElementBefore); } }