This is an automated email from the ASF dual-hosted git repository.

garydgregory pushed a commit to branch 1.X
in repository https://gitbox.apache.org/repos/asf/commons-beanutils.git


The following commit(s) were added to refs/heads/1.X by this push:
     new a1000029 Add DynaPropertySerializationTest
a1000029 is described below

commit a100002901ba6f06fe2b7da932c539d7f1c5adec
Author: Gary Gregory <[email protected]>
AuthorDate: Mon Jul 27 14:55:13 2026 +0000

    Add DynaPropertySerializationTest
---
 .../beanutils/DynaPropertySerializationTest.java   | 343 +++++++++++++++++++++
 1 file changed, 343 insertions(+)

diff --git 
a/src/test/java/org/apache/commons/beanutils/DynaPropertySerializationTest.java 
b/src/test/java/org/apache/commons/beanutils/DynaPropertySerializationTest.java
new file mode 100644
index 00000000..65387bed
--- /dev/null
+++ 
b/src/test/java/org/apache/commons/beanutils/DynaPropertySerializationTest.java
@@ -0,0 +1,343 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.beanutils;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.io.StreamCorruptedException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.junit.jupiter.api.Test;
+
+/**
+ * Comprehensive serialization tests for {@link 
DynaProperty#readObject(ObjectInputStream)}.
+ * <p>
+ * The {@code DynaProperty} class uses a custom serialization protocol to work 
around JVM issues with serializing primitive {@link Class} types. These tests
+ * verify that {@code readObject} correctly reconstructs {@code DynaProperty} 
instances for every supported primitive type, object types, indexed (array/List)
+ * properties, and mapped (Map) properties. A test is also included to verify 
that a {@link StreamCorruptedException} is thrown when the stream contains an
+ * unrecognized primitive-type constant.
+ * </p>
+ */
+class DynaPropertySerializationTest {
+
+    /**
+     * Serializes {@code prop} to a byte array.
+     */
+    private static byte[] serialize(final DynaProperty prop) throws 
IOException {
+        try (ByteArrayOutputStream bos = new ByteArrayOutputStream(); 
ObjectOutputStream oos = new ObjectOutputStream(bos)) {
+            oos.writeObject(prop);
+            return bos.toByteArray();
+        }
+    }
+
+    /**
+     * Deserializes a {@link DynaProperty} from {@code bytes}.
+     */
+    private static DynaProperty deserialize(final byte[] bytes) throws 
IOException, ClassNotFoundException {
+        try (ByteArrayInputStream bis = new ByteArrayInputStream(bytes); 
ObjectInputStream ois = new ObjectInputStream(bis)) {
+            return (DynaProperty) ois.readObject();
+        }
+    }
+
+    /**
+     * Full round-trip: serialize then deserialize and return the 
reconstructed instance.
+     */
+    private static DynaProperty roundTrip(final DynaProperty prop) throws 
IOException, ClassNotFoundException {
+        return deserialize(serialize(prop));
+    }
+
+    /**
+     * Assert that a round-tripped {@link DynaProperty} is equal to the 
original and is a distinct object.
+     */
+    private static void assertRoundTrip(final DynaProperty original) throws 
IOException, ClassNotFoundException {
+        final DynaProperty copy = roundTrip(original);
+        assertNotNull(copy);
+        assertNotSame(original, copy, "Deserialized instance must be a new 
object");
+        assertEquals(original.getName(), copy.getName(), "name must be 
preserved");
+        assertEquals(original.getType(), copy.getType(), "type must be 
preserved");
+        assertEquals(original.getContentType(), copy.getContentType(), 
"contentType must be preserved");
+        assertEquals(original, copy, "equals() must hold after round-trip");
+        assertEquals(original.hashCode(), copy.hashCode(), "hashCode() must 
hold after round-trip");
+    }
+
+    @Test
+    void testRoundTripNameOnly() throws Exception {
+        // DynaProperty(String) uses Object.class as the type
+        assertRoundTrip(new DynaProperty("nameOnly"));
+    }
+
+    @Test
+    void testRoundTripStringType() throws Exception {
+        assertRoundTrip(new DynaProperty("strProp", String.class));
+    }
+
+    @Test
+    void testRoundTripIntegerWrapperType() throws Exception {
+        assertRoundTrip(new DynaProperty("integerProp", Integer.class));
+    }
+
+    @Test
+    void testRoundTripObjectType() throws Exception {
+        assertRoundTrip(new DynaProperty("objProp", Object.class));
+    }
+
+    @Test
+    void testRoundTripBooleanPrimitiveType() throws Exception {
+        assertRoundTrip(new DynaProperty("boolProp", Boolean.TYPE));
+    }
+
+    @Test
+    void testRoundTripBytePrimitiveType() throws Exception {
+        assertRoundTrip(new DynaProperty("byteProp", Byte.TYPE));
+    }
+
+    @Test
+    void testRoundTripCharPrimitiveType() throws Exception {
+        assertRoundTrip(new DynaProperty("charProp", Character.TYPE));
+    }
+
+    @Test
+    void testRoundTripDoublePrimitiveType() throws Exception {
+        assertRoundTrip(new DynaProperty("doubleProp", Double.TYPE));
+    }
+
+    @Test
+    void testRoundTripFloatPrimitiveType() throws Exception {
+        assertRoundTrip(new DynaProperty("floatProp", Float.TYPE));
+    }
+
+    @Test
+    void testRoundTripIntPrimitiveType() throws Exception {
+        assertRoundTrip(new DynaProperty("intProp", Integer.TYPE));
+    }
+
+    @Test
+    void testRoundTripLongPrimitiveType() throws Exception {
+        assertRoundTrip(new DynaProperty("longProp", Long.TYPE));
+    }
+
+    @Test
+    void testRoundTripShortPrimitiveType() throws Exception {
+        assertRoundTrip(new DynaProperty("shortProp", Short.TYPE));
+    }
+
+    @Test
+    void testRoundTripObjectArrayType() throws Exception {
+        // String[] – contentType is String.class (non-primitive)
+        assertRoundTrip(new DynaProperty("strArrayProp", String[].class));
+    }
+
+    @Test
+    void testRoundTripPrimitiveArrayType() throws Exception {
+        // int[] – constructor sets contentType = Integer.TYPE (primitive)
+        final DynaProperty prop = new DynaProperty("intArrayProp", 
int[].class);
+        assertRoundTrip(prop);
+        // Verify contentType is correctly restored as the primitive type
+        final DynaProperty copy = roundTrip(prop);
+        assertEquals(Integer.TYPE, copy.getContentType());
+    }
+
+    @Test
+    void testRoundTripListTypeWithNoContentType() throws Exception {
+        // Raw List – isIndexed() returns true, contentType may be null
+        assertRoundTrip(new DynaProperty("listProp", List.class));
+    }
+
+    @Test
+    void testRoundTripListTypeWithExplicitContentType() throws Exception {
+        // List<String> – explicit contentType is an object class
+        assertRoundTrip(new DynaProperty("typedListProp", List.class, 
String.class));
+    }
+
+    @Test
+    void testRoundTripListTypeWithPrimitiveContentType() throws Exception {
+        // List<int> – explicit contentType is a primitive type
+        assertRoundTrip(new DynaProperty("intListProp", List.class, 
Integer.TYPE));
+    }
+
+    @Test
+    void testRoundTripConcreteListSubtype() throws Exception {
+        // ArrayList also satisfies isIndexed()
+        assertRoundTrip(new DynaProperty("arrayListProp", ArrayList.class, 
String.class));
+    }
+
+    @Test
+    void testRoundTripMapTypeWithNoContentType() throws Exception {
+        // Raw Map – isMapped() returns true, contentType may be null
+        assertRoundTrip(new DynaProperty("mapProp", Map.class));
+    }
+
+    @Test
+    void testRoundTripMapTypeWithObjectContentType() throws Exception {
+        // Map with String value type
+        assertRoundTrip(new DynaProperty("typedMapProp", Map.class, 
String.class));
+    }
+
+    @Test
+    void testRoundTripMapTypeWithPrimitiveContentType() throws Exception {
+        // Map with int value type
+        assertRoundTrip(new DynaProperty("intMapProp", Map.class, 
Integer.TYPE));
+    }
+
+    @Test
+    void testRoundTripConcreteMapSubtype() throws Exception {
+        // HashMap also satisfies isMapped()
+        assertRoundTrip(new DynaProperty("hashMapProp", HashMap.class, 
Double.TYPE));
+    }
+
+    @Test
+    void testNameIsPreservedAfterRoundTrip() throws Exception {
+        final String uniqueName = "uniquePropertyName_42";
+        final DynaProperty copy = roundTrip(new DynaProperty(uniqueName, 
String.class));
+        assertEquals(uniqueName, copy.getName());
+    }
+
+    @Test
+    void testTypeIsPreservedAfterRoundTripForEveryPrimitive() throws Exception 
{
+        final Class<?>[] primitives = { Boolean.TYPE, Byte.TYPE, 
Character.TYPE, Double.TYPE, Float.TYPE, Integer.TYPE, Long.TYPE, Short.TYPE };
+        for (final Class<?> primitive : primitives) {
+            final DynaProperty copy = roundTrip(new DynaProperty("p", 
primitive));
+            assertEquals(primitive, copy.getType(), () -> "type must be 
preserved for primitive " + primitive.getName());
+        }
+    }
+
+    @Test
+    void testContentTypeIsNullForSimpleProperty() throws Exception {
+        final DynaProperty copy = roundTrip(new DynaProperty("simple", 
String.class));
+        assertNull(copy.getContentType(), "Simple (non-indexed, non-mapped) 
property must have null contentType");
+    }
+
+    @Test
+    void testIsIndexedPreservedAfterRoundTrip() throws Exception {
+        final DynaProperty indexed = new DynaProperty("idx", List.class, 
String.class);
+        final DynaProperty copy = roundTrip(indexed);
+        assertEquals(indexed.isIndexed(), copy.isIndexed());
+    }
+
+    @Test
+    void testIsMappedPreservedAfterRoundTrip() throws Exception {
+        final DynaProperty mapped = new DynaProperty("map", Map.class, 
Integer.class);
+        final DynaProperty copy = roundTrip(mapped);
+        assertEquals(mapped.isMapped(), copy.isMapped());
+    }
+
+    @Test
+    void testToStringAfterRoundTrip() throws Exception {
+        final DynaProperty original = new DynaProperty("myProp", List.class, 
String.class);
+        final DynaProperty copy = roundTrip(original);
+        assertEquals(original.toString(), copy.toString());
+    }
+
+    @Test
+    void testMultipleSerializationsProduceSameResult() throws Exception {
+        final DynaProperty prop = new DynaProperty("multi", Integer.TYPE);
+        final DynaProperty copy1 = roundTrip(prop);
+        final DynaProperty copy2 = roundTrip(prop);
+        assertEquals(copy1, copy2);
+        assertEquals(copy1.hashCode(), copy2.hashCode());
+    }
+
+    @Test
+    void testSerializationIsIdempotent() throws Exception {
+        // serialize the result of the first round-trip and verify it 
round-trips again
+        final DynaProperty original = new DynaProperty("idempotent", 
Map.class, Long.TYPE);
+        final DynaProperty copy1 = roundTrip(original);
+        final DynaProperty copy2 = roundTrip(copy1);
+        assertEquals(original, copy2);
+    }
+
+    /**
+     * Verifies that {@link DynaProperty#readObject(ObjectInputStream)} throws 
a {@link StreamCorruptedException} when the stream contains an unrecognised
+     * primitive-type constant (i.e. an integer that is not in the range 1–8).
+     * <p>
+     * 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.
+     * </p>
+     */
+    @Test
+    void 
testReadObjectThrowsStreamCorruptedExceptionForInvalidPrimitiveTypeConstant() 
throws Exception {
+        // serialize a DynaProperty whose type is stored as BOOLEAN_TYPE (int 
1)
+        final byte[] booleanBytes = serialize(new DynaProperty("corrupt", 
Boolean.TYPE));
+        // serialize the same name/class but with BYTE_TYPE (int 2) to locate 
the int
+        final byte[] byteBytes = serialize(new DynaProperty("corrupt", 
Byte.TYPE));
+        // The two streams should be identical except for the last byte of the 
4-byte
+        // primitive-type int (0x01 vs 0x02). Find the first (and only) 
differing byte.
+        int diffIndex = -1;
+        for (int i = 0; i < booleanBytes.length; i++) {
+            if (booleanBytes[i] != byteBytes[i]) {
+                diffIndex = i;
+                break;
+            }
+        }
+        assertNotNull(Integer.valueOf(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();
+        corrupted[diffIndex - 3] = 0x00;
+        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),
+                "Expected StreamCorruptedException for unrecognised 
primitive-type constant");
+        assertInstanceOf(StreamCorruptedException.class, thrown, "Root cause 
must be StreamCorruptedException");
+    }
+
+    /**
+     * Variant of the corruption test using a <em>mapped</em> property so that 
the second {@code readAnyClass} call (for contentType) is the one that receives
+     * the bad constant.
+     */
+    @Test
+    void 
testReadObjectThrowsStreamCorruptedExceptionForInvalidContentTypePrimitiveConstant()
 throws Exception {
+        // serialize Map<String, Boolean> vs Map<String, Byte> – the type 
(Map.class)
+        // is written as a non-primitive class object; the contentType is 
written as a
+        // primitive-type constant.
+        final byte[] boolContentBytes = serialize(new 
DynaProperty("corruptContent", Map.class, Boolean.TYPE));
+        final byte[] byteContentBytes = serialize(new 
DynaProperty("corruptContent", Map.class, Byte.TYPE));
+        int diffIndex = -1;
+        for (int i = 0; i < boolContentBytes.length; i++) {
+            if (boolContentBytes[i] != byteContentBytes[i]) {
+                diffIndex = i;
+                break;
+            }
+        }
+        assertNotNull(Integer.valueOf(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),
+                "Expected StreamCorruptedException for unrecognised primitive 
contentType constant");
+        assertInstanceOf(StreamCorruptedException.class, thrown, "Root cause 
must be StreamCorruptedException");
+    }
+}

Reply via email to