diff --git a/.gitignore b/.gitignore
index 442e29d67..121b75c3c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,6 +4,7 @@
.settings/
/site-content/
/.idea/
+*.iml
# NetBeans files
nb-configuration.xml
diff --git a/pom.xml b/pom.xml
index e5fd99784..324d72c65 100644
--- a/pom.xml
+++ b/pom.xml
@@ -103,6 +103,12 @@
* Strategy: serialize two {@code DynaProperty} instances whose types differ only in the primitive-type constant written by {@code writeAnyClass} * (BOOLEAN_TYPE=1 vs BYTE_TYPE=2). Find the first differing byte (the last byte of the 4-byte int), then replace those 4 bytes with the invalid constant 99 - * before attempting deserialisation. + * after attempting deserialization. *
*/ @Test @@ -300,7 +306,7 @@ void testReadObjectThrowsStreamCorruptedExceptionForInvalidPrimitiveTypeConstant break; } } - assertNotNull(Integer.valueOf(diffIndex), "Streams must differ at the primitive-type int"); + assertNotEquals(-1, diffIndex, "Streams must differ at the primitive-type int"); // The 4-byte int starts 3 bytes before the differing byte (big-endian). // Overwrite all 4 bytes with the invalid constant 99 (0x00_00_00_63). final byte[] corrupted = booleanBytes.clone(); @@ -308,10 +314,11 @@ void testReadObjectThrowsStreamCorruptedExceptionForInvalidPrimitiveTypeConstant corrupted[diffIndex - 2] = 0x00; corrupted[diffIndex - 1] = 0x00; corrupted[diffIndex] = (byte) 99; - // Deserialising the corrupted stream must throw StreamCorruptedException. - final IOException thrown = assertThrows(IOException.class, () -> deserialize(corrupted), + // Deserializing the corrupted stream must throw StreamCorruptedException. + final StreamCorruptedException e = assertThrows(StreamCorruptedException.class, + () -> deserialize(corrupted), "Expected StreamCorruptedException for unrecognised primitive-type constant"); - assertInstanceOf(StreamCorruptedException.class, thrown, "Root cause must be StreamCorruptedException"); + assertTrue(e.getMessage().contains("Invalid primitive type.")); } /** @@ -332,15 +339,15 @@ void testReadObjectThrowsStreamCorruptedExceptionForInvalidContentTypePrimitiveC break; } } - assertNotNull(Integer.valueOf(diffIndex), "Streams must differ at the contentType primitive-type int"); + assertNotEquals(-1, diffIndex, "Streams must differ at the contentType primitive-type int"); final byte[] corrupted = boolContentBytes.clone(); corrupted[diffIndex - 3] = 0x00; corrupted[diffIndex - 2] = 0x00; corrupted[diffIndex - 1] = 0x00; corrupted[diffIndex] = (byte) 99; - final IOException thrown = assertThrows(IOException.class, () -> deserialize(corrupted), + final StreamCorruptedException e = assertThrows(StreamCorruptedException.class, () -> deserialize(corrupted), "Expected StreamCorruptedException for unrecognised primitive contentType constant"); - assertInstanceOf(StreamCorruptedException.class, thrown, "Root cause must be StreamCorruptedException"); + assertTrue(e.getMessage().contains("Invalid primitive type.")); } /** @@ -359,17 +366,17 @@ private static int findSequence(final byte[] haystack, final byte[] needle) { } /** - * Proves that {@link DynaProperty#writeObject(ObjectOutputStream)} calls {@code writeAnyClass} (which encodes the {@code type} field) before + * Proves that {@link DynaProperty#writeObject(ObjectOutputStream)} calls {@code writeAnyClass} (which encodes the {@code type} field) after * {@code defaultWriteObject} (which encodes the {@code name} field) for primitive types. *- * Strategy: serialise two {@link DynaProperty} instances that have an identical {@code name} but different primitive types. Because the name is identical, + * Strategy: serialize two {@link DynaProperty} instances that have an identical {@code name} but different primitive types. Because the name is identical, * both byte streams are the same from the class-descriptor through to the end of the default-field data. The streams diverge only at the - * {@code writeAnyClass} output (the primitive-type integer constant). If that divergence point is located before the name bytes in the stream, it + * {@code writeAnyClass} output (the primitive-type integer constant). If that divergence point is located after the name bytes in the stream, it * proves that {@code writeAnyClass} is called first. *
*/ @Test - void testWireFormatPrimitiveTypeDataPrecedesNameField() throws Exception { + void testWireFormatPrimitiveTypeDataFollowsNameField() throws Exception { final String sharedName = "sharedPrimitiveName"; // Boolean.TYPE encodes as writeBoolean(true) + writeInt(BOOLEAN_TYPE=1). // Byte.TYPE encodes as writeBoolean(true) + writeInt(BYTE_TYPE=2). @@ -381,7 +388,8 @@ void testWireFormatPrimitiveTypeDataPrecedesNameField() throws Exception { final int namePosition = findSequence(boolBytes, nameBytes); assertTrue(namePosition > 0, "Property name must be present in the serialized stream"); // The name must occupy the same position in both streams (identical name, identical class). - assertEquals(namePosition, findSequence(byteBytes, nameBytes), "Name must be at the same byte position in both streams"); + assertEquals(namePosition, findSequence(byteBytes, nameBytes), + "Name must be at the same byte position in both streams"); // Find the first byte where the two streams diverge: this is inside the // writeAnyClass output (the primitive-type integer constant differs: 1 vs 2). int firstDiff = -1; @@ -393,16 +401,17 @@ void testWireFormatPrimitiveTypeDataPrecedesNameField() throws Exception { } assertTrue(firstDiff >= 0, "Streams must diverge at the primitive-type constant"); // CRITICAL assertion: the divergence point (type data from writeAnyClass) must - // come BEFORE the name field (from defaultWriteObject). - assertTrue(firstDiff < namePosition, "writeAnyClass must be invoked before defaultWriteObject: " + "type-data divergence at byte " + firstDiff - + " must precede name field at byte " + namePosition); + // come AFTER the name field (from defaultWriteObject). + assertTrue(firstDiff > namePosition, + "writeAnyClass must be invoked after defaultWriteObject: type-data divergence at byte " + firstDiff + + " must follow name field at byte " + namePosition); // Sanity-check: both properties still round-trip correctly. assertRoundTrip(new DynaProperty(sharedName, Boolean.TYPE)); assertRoundTrip(new DynaProperty(sharedName, Byte.TYPE)); } /** - * Same proof as {@link #testWireFormatPrimitiveTypeDataPrecedesNameField()} but for object (non-primitive) types. + * Same proof as {@link #testWireFormatPrimitiveTypeDataFollowsNameField()} but for object (non-primitive) types. ** For object types {@code writeAnyClass} emits {@code writeBoolean(false)} followed by {@code writeObject(clazz)}. Two properties with the same name but * different object types ({@code String.class} vs {@code Integer.class}) differ in the class object written by {@code writeAnyClass}; the shared name is @@ -410,10 +419,10 @@ void testWireFormatPrimitiveTypeDataPrecedesNameField() throws Exception { *
*/ @Test - void testWireFormatObjectTypeDataPrecedesNameField() throws Exception { + void testWireFormatObjectTypeDataFollowsNameField() throws Exception { final String sharedName = "sharedObjectName"; // writeAnyClass for object type: writeBoolean(false) + writeObject(clazz). - // String.class and Integer.class are serialised differently, so streams diverge + // String.class and Integer.class are serialized differently, so streams diverge // at the class-object position (inside writeAnyClass output). final byte[] stringTypeBytes = serialize(new DynaProperty(sharedName, String.class)); final byte[] integerTypeBytes = serialize(new DynaProperty(sharedName, Integer.class)); @@ -424,7 +433,7 @@ void testWireFormatObjectTypeDataPrecedesNameField() throws Exception { assertTrue(namePositionInString > 0, "Name must be present in the String-type stream"); assertTrue(namePositionInInteger > 0, "Name must be present in the Integer-type stream"); // Find the first byte where the streams diverge (inside writeAnyClass output, - // where the serialised form of String.class differs from Integer.class). + // where the serialized form of String.class differs from Integer.class). int firstDiff = -1; for (int i = 0; i < stringTypeBytes.length; i++) { if (stringTypeBytes[i] != integerTypeBytes[i]) { @@ -433,27 +442,30 @@ void testWireFormatObjectTypeDataPrecedesNameField() throws Exception { } } assertTrue(firstDiff >= 0, "Streams must diverge where the class objects differ"); - // The divergence (type class data from writeAnyClass) must precede the name + // The divergence (type class data from writeAnyClass) must follow the name // (from defaultWriteObject) in BOTH streams. - assertTrue(firstDiff < namePositionInString, "writeAnyClass must be invoked before defaultWriteObject (String-type stream): " - + "type divergence at byte " + firstDiff + " must precede name at byte " + namePositionInString); - assertTrue(firstDiff < namePositionInInteger, "writeAnyClass must be invoked before defaultWriteObject (Integer-type stream): " - + "type divergence at byte " + firstDiff + " must precede name at byte " + namePositionInInteger); + assertTrue(firstDiff > namePositionInString, + "writeAnyClass must be invoked after defaultWriteObject (String-type stream): " + + "type divergence at byte " + firstDiff + " must follow name at byte " + namePositionInString); + assertTrue(firstDiff > namePositionInInteger, + "writeAnyClass must be invoked after defaultWriteObject (Integer-type stream): " + + "type divergence at byte " + firstDiff + " must follow name at byte " + + namePositionInInteger); assertRoundTrip(new DynaProperty(sharedName, String.class)); assertRoundTrip(new DynaProperty(sharedName, Integer.class)); } /** * Proves that for indexed and mapped properties the second {@code writeAnyClass} call (for {@code contentType}) also appears in the byte - * stream before the {@code name} field written by {@code defaultWriteObject}. + * stream after the {@code name} field written by {@code defaultWriteObject}. ** Two mapped properties share the same name and the same {@code Map.class} type but differ in their {@code contentType} ({@code Boolean.TYPE} vs * {@code Byte.TYPE}). Because the type ({@code Map.class}) is identical, the streams are the same through the first {@code writeAnyClass} call. They - * diverge at the second {@code writeAnyClass} call (contentType constant), which must still precede the name. + * diverge at the second {@code writeAnyClass} call (contentType constant), which must still follow the name. *
*/ @Test - void testWireFormatContentTypeDataPrecedesNameFieldForMappedProperty() throws Exception { + void testWireFormatContentTypeDataFollowsNameFieldForMappedProperty() throws Exception { final String sharedName = "sharedMappedName"; // Both use Map.class as the type (identical first writeAnyClass output). // They differ only in contentType: Boolean.TYPE (constant 1) vs Byte.TYPE (constant 2). @@ -462,7 +474,8 @@ void testWireFormatContentTypeDataPrecedesNameFieldForMappedProperty() throws Ex final byte[] nameBytes = sharedName.getBytes(StandardCharsets.UTF_8); final int namePosition = findSequence(boolContentBytes, nameBytes); assertTrue(namePosition > 0, "Name must be present in the serialized stream"); - assertEquals(namePosition, findSequence(byteContentBytes, nameBytes), "Name must be at the same byte position in both streams"); + assertEquals(namePosition, findSequence(byteContentBytes, nameBytes), + "Name must be at the same byte position in both streams"); // The streams are identical up to (and including) the type encoding of Map.class. // They diverge at the contentType constant written by the second writeAnyClass call. int firstDiff = -1; @@ -473,12 +486,52 @@ void testWireFormatContentTypeDataPrecedesNameFieldForMappedProperty() throws Ex } } assertTrue(firstDiff >= 0, "Streams must diverge at the contentType primitive-type constant"); - // The second writeAnyClass output (contentType) must still precede the name - // (defaultWriteObject), confirming that both writeAnyClass calls happen before + // The second writeAnyClass output (contentType) must still follow the name + // (defaultWriteObject), confirming that both writeAnyClass calls happen after // defaultWriteObject is invoked. - assertTrue(firstDiff < namePosition, "The second writeAnyClass call (contentType) must precede defaultWriteObject: " - + "content-type divergence at byte " + firstDiff + " must precede name at byte " + namePosition); + assertTrue(firstDiff > namePosition, + "The second writeAnyClass call (contentType) must follow defaultWriteObject: " + + "content-type divergence at byte " + firstDiff + " must follow name at byte " + namePosition); assertRoundTrip(new DynaProperty(sharedName, Map.class, Boolean.TYPE)); assertRoundTrip(new DynaProperty(sharedName, Map.class, Byte.TYPE)); } + + /** + * Tests cloning mechanism via Wildfly Object Cloner. + */ + @Test + public void testCloneViaWildflyObjectCloner() throws Exception { + final ClonerConfiguration paramConfig = new ClonerConfiguration(); + paramConfig.setClassCloner(new ClassLoaderClassCloner(getClass().getClassLoader())); + final ObjectCloner objectCloner = ObjectCloners.getSerializingObjectClonerFactory().createCloner(paramConfig); + + final DynaProperty testPropertyWithNameTypeContentType = new DynaProperty("test", List.class, Short.class); + final Object cloned = objectCloner.clone(testPropertyWithNameTypeContentType); + + assertEquals(testPropertyWithNameTypeContentType, cloned); + } + + /** + * Tests for deserializing from the old serialization mechanism of a DynaProperty. + */ + @Test + public void testDeserializeFromOldSerialization() throws Exception { + final String oldPropertyWithNameTypeContentType = + "rO0ABXNyAClvcmcuYXBhY2hlLmNvbW1vbnMuYmVhbnV0aWxzLkR5bmFQcm9wZXJ0eQAAAAAAAAABAwABTAAEbmFtZXQAEkxqYXZhL2xhbmcvU3RyaW5nO3hwdwEAdnIADmphdmEudXRpbC5MaXN0AAAAAAAAAAAAAAB4cHcBAHZyAA9qYXZhLmxhbmcuU2hvcnRoTTcTNGDaUgIAAVMABXZhbHVleHIAEGphdmEubGFuZy5OdW1iZXKGrJUdC5TgiwIAAHhwdAAFdGVzdDN4"; + + final byte[] decoded = Base64.getDecoder().decode(oldPropertyWithNameTypeContentType); + try (ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(decoded))) { + final InvalidClassException e = assertThrows(InvalidClassException.class, ois::readObject); + assertTrue(e.getMessage().contains("serialVersionUID")); + } + + // try new serialization and make sure it is *not* equal to the old representation + final DynaProperty newPropertyWithNameTypeContentType = new DynaProperty("test", List.class, Byte.class); + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + try (ObjectOutputStream oos = new ObjectOutputStream(buffer)) { + oos.writeObject(newPropertyWithNameTypeContentType); + } + + assertNotEquals(oldPropertyWithNameTypeContentType, Base64.getEncoder().encodeToString(buffer.toByteArray())); + } } diff --git a/src/test/java/org/apache/commons/beanutils/DynaPropertyTest.java b/src/test/java/org/apache/commons/beanutils/DynaPropertyTest.java index 3dee48e8a..b136b4a8e 100644 --- a/src/test/java/org/apache/commons/beanutils/DynaPropertyTest.java +++ b/src/test/java/org/apache/commons/beanutils/DynaPropertyTest.java @@ -17,16 +17,20 @@ package org.apache.commons.beanutils; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; -import java.util.Collection; +import java.util.List; -import junit.framework.TestCase; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; /** * Test case for {@link DynaProperty}. */ -public class DynaPropertyTest extends TestCase { +public class DynaPropertyTest { private DynaProperty testPropertyWithName; @@ -40,43 +44,33 @@ public class DynaPropertyTest extends TestCase { private DynaProperty testProperty3Duplicate; - /** - * Construct a new instance of this test case. - * - * @param name Name of the test case - */ - public DynaPropertyTest(final String name) { - super(name); - } - /** * Set up instance variables required by this test case. */ - @Override - protected void setUp() throws Exception { - super.setUp(); + @BeforeEach + public void setUp() { testPropertyWithName = new DynaProperty("test1"); testProperty1Duplicate = new DynaProperty("test1"); testPropertyWithNameAndType = new DynaProperty("test2", Integer.class); testProperty2Duplicate = new DynaProperty("test2", Integer.class); - testPropertyWithNameAndTypeAndContentType = new DynaProperty("test3", Collection.class, Short.class); - testProperty3Duplicate = new DynaProperty("test3", Collection.class, Short.class); + testPropertyWithNameAndTypeAndContentType = new DynaProperty("test3", List.class, Short.class); + testProperty3Duplicate = new DynaProperty("test3", List.class, Short.class); } /** * Tear down instance variables required by this test case. */ - @Override - protected void tearDown() throws Exception { + @AfterEach + public void tearDown() { testPropertyWithName = testProperty1Duplicate = null; testPropertyWithNameAndType = testProperty2Duplicate = null; testPropertyWithNameAndTypeAndContentType = testProperty3Duplicate = null; - super.tearDown(); } /** * Class under test for boolean equals(Object) */ + @Test public void testEqualsObject() { assertEquals(testPropertyWithName, testProperty1Duplicate); assertEquals(testPropertyWithNameAndType, testProperty2Duplicate); @@ -89,6 +83,7 @@ public void testEqualsObject() { /** * Class under test for int hashCode(Object) */ + @Test public void testHashCode() { final int initialHashCode = testPropertyWithNameAndTypeAndContentType.hashCode(); assertEquals(testPropertyWithName.hashCode(), testProperty1Duplicate.hashCode());