1 /*
  2  * Copyright (c) 1999, 2026, Oracle and/or its affiliates. All rights reserved.
  3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  4  *
  5  * This code is free software; you can redistribute it and/or modify it
  6  * under the terms of the GNU General Public License version 2 only, as
  7  * published by the Free Software Foundation.
  8  *
  9  * This code is distributed in the hope that it will be useful, but WITHOUT
 10  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 11  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 12  * version 2 for more details (a copy is included in the LICENSE file that
 13  * accompanied this code).
 14  *
 15  * You should have received a copy of the GNU General Public License version
 16  * 2 along with this work; if not, write to the Free Software Foundation,
 17  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 18  *
 19  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 20  * or visit www.oracle.com if you need additional information or have any
 21  * questions.
 22  */
 23 
 24 /*
 25  * @test
 26  * @library /test/lib
 27  * @summary Serialize and deserialize value objects
 28  * @enablePreview
 29  * @modules java.base/jdk.internal
 30  * @modules java.base/jdk.internal.value
 31  * @run junit/othervm SimpleValueGraphs
 32  */
 33 
 34 import java.io.ByteArrayInputStream;
 35 import java.io.ByteArrayOutputStream;
 36 import java.io.Externalizable;
 37 import java.io.IOException;
 38 import java.io.ObjectInput;
 39 import java.io.ObjectInputStream;
 40 import java.io.ObjectOutput;
 41 import java.io.ObjectOutputStream;
 42 import java.io.Serial;
 43 import java.io.Serializable;
 44 import java.io.InvalidClassException;
 45 
 46 import java.util.Arrays;
 47 import java.util.function.BiFunction;
 48 import java.util.Objects;
 49 
 50 import java.nio.charset.StandardCharsets;
 51 import java.util.stream.Stream;
 52 
 53 import jdk.internal.value.DeserializeConstructor;
 54 import jdk.internal.MigratedValueClass;
 55 
 56 
 57 import jdk.test.lib.hexdump.HexPrinter;
 58 import jdk.test.lib.hexdump.ObjectStreamPrinter;
 59 import org.junit.jupiter.api.Assertions;
 60 import org.junit.jupiter.api.Test;
 61 import org.junit.jupiter.api.TestInstance;
 62 import org.junit.jupiter.params.ParameterizedTest;
 63 import org.junit.jupiter.params.provider.Arguments;
 64 import org.junit.jupiter.params.provider.MethodSource;
 65 
 66 public class SimpleValueGraphs implements Serializable {
 67 
 68     private static final boolean DEBUG = true;
 69 
 70     private static final SimpleValue foo1 = new SimpleValue("One", 1);
 71     private static final SimpleValue foo2 = new SimpleValue("Two", 2);
 72 
 73     public static Stream<Arguments> valueObjects() {
 74         return Stream.of(
 75                 Arguments.of(new SimpleValue(new SimpleValue(1))),
 76                 Arguments.of(new SimpleValue(new SimpleValue(2), 3)),
 77                 Arguments.of((Object)(new SimpleValue[] {foo1, foo1, foo2, foo1, foo2 })));
 78     }
 79 
 80     @ParameterizedTest
 81     @MethodSource("valueObjects")
 82     public void roundTrip(Object expected) throws Exception {
 83         byte[] bytes = serialize(expected);
 84 
 85         if (DEBUG)
 86             HexPrinter.simple().dest(System.out).formatter(ObjectStreamPrinter.formatter()).format(bytes);
 87 
 88         Object actual = deserialize(bytes);
 89         System.out.println("actual: " + actual.toString());
 90         if (actual.getClass().isArray()) {
 91             Assertions.assertArrayEquals((Object[]) expected, (Object[]) actual, "Mismatch " + expected.getClass());
 92         } else {
 93             Assertions.assertEquals(expected, actual, "Mismatch " + expected.getClass());
 94         }
 95     }
 96 
 97     private static final Tree treeI = Tree.makeTree(3, (l, r) -> new TreeI((TreeI)l, (TreeI)l));
 98     private static final Tree treeV = Tree.makeTree(3, (l, r) -> new TreeV((TreeV)l, (TreeV)l));
 99 
100     // Create a tree of identity objects with a cycle; it will serialize ok, but when deserialized as
101     // a value class the cycle is broken by replacing the back ref with null.
102     private static Tree treeCycle(boolean cycle) {
103         TreeI tree = (TreeI)Tree.makeTree(3, (l, r) -> new TreeI((TreeI)l, (TreeI)l));
104         tree.setLeft(cycle ? tree : null);     // force a cycle or null
105         return tree;
106     }
107 
108     public static Stream<Arguments> migrationObjects() {
109         return Stream.of(
110                 Arguments.of(treeI, "TreeI", "TreeV", treeV), // Serialize as an identity class, deserialize as Value class
111                 Arguments.of(treeCycle(true), "TreeI", "TreeV", treeCycle(false))
112         );
113     }
114 
115     /**
116      * Test serializing an object graph, and deserialize with a modification of the serialized form.
117      * The modifications to the stream change the class name being deserialized.
118      * The cases include serializing an identity class and deserialize the corresponding
119      * value class.
120      *
121      * @param origObj an object to serialize
122      * @param origName a string in the serialized stream to replace
123      * @param replName a string to replace the original string
124      * @param expectedObject the expected object (graph) or an exception if it should fail
125      * @throws Exception some unexpected exception may be thrown and cause the test to fail
126      */
127     @ParameterizedTest
128     @MethodSource("migrationObjects")
129     public void treeVTest(Object origObj, String origName, String replName, Object expectedObject) throws Exception {
130         byte[] bytes = serialize(origObj);
131         if (DEBUG) {
132             System.out.println("Original serialized " + origObj.getClass().getName());
133             HexPrinter.simple().dest(System.out).formatter(ObjectStreamPrinter.formatter()).format(bytes);
134         }
135 
136         // Modify the serialized bytes to change a class name from the serialized name
137         // to a different class. The replacement name must be the same length as the original name.
138         byte[] replBytes = patchBytes(bytes, origName, replName);
139         if (DEBUG) {
140             System.out.println("Modified serialized " + origObj.getClass().getName());
141             HexPrinter.simple().dest(System.out).formatter(ObjectStreamPrinter.formatter()).format(replBytes);
142         }
143         try {
144             Object actual = Assertions.assertDoesNotThrow(() ->deserialize(replBytes));
145 
146             // Compare the shape of the actual and expected trees
147             Assertions.assertEquals(expectedObject.toString(), actual.toString(),
148                     "Resulting object not equals: " + actual.getClass().getName());
149 
150         } catch (Exception ex) {
151             ex.printStackTrace();
152             Assertions.assertEquals(expectedObject.getClass(), ex.getClass(), ex.toString());
153             Assertions.assertEquals(((Exception)expectedObject).getMessage(), ex.getMessage(), ex.toString());
154         }
155     }
156 
157     /**
158      * Serialize an object and return the serialized bytes.
159      *
160      * @param expected an object to serialize
161      * @return a byte array containing the serialized object
162      */
163     private static byte[] serialize(Object expected) throws IOException {
164         try (ByteArrayOutputStream bout = new ByteArrayOutputStream();
165              ObjectOutputStream oout = new ObjectOutputStream(bout)) {
166             oout.writeObject(expected);
167             oout.flush();
168             return bout.toByteArray();
169         }
170     }
171 
172     /**
173      * Deserialize an object from the byte array.
174      * @param bytes a byte array
175      * @return an Object read from the byte array
176      */
177     private static Object deserialize(byte[] bytes) throws IOException, ClassNotFoundException {
178         try (ByteArrayInputStream bin = new ByteArrayInputStream(bytes);
179              ObjectInputStream oin = new ObjectInputStream(bin)) {
180             return oin.readObject();
181         }
182     }
183 
184     /**
185      * Replace every occurrence of the string in the byte array with the replacement.
186      * The strings are US_ASCII only.
187      * @param bytes a byte array
188      * @param orig a string, converted to bytes using US_ASCII, originally exists in the bytes
189      * @param repl a string, converted to bytes using US_ASCII, to replace the original bytes
190      * @return a new byte array that has been patched
191      */
192     private byte[] patchBytes(byte[] bytes, String orig, String repl) {
193         return patchBytes(bytes,
194                 orig.getBytes(StandardCharsets.US_ASCII),
195                 repl.getBytes(StandardCharsets.US_ASCII));
196     }
197 
198     /**
199      * Replace every occurrence of the original bytes in the byte array with the replacement bytes.
200      * @param bytes a byte array
201      * @param orig a byte array containing existing bytes in the byte array
202      * @param repl a byte array to replace the original bytes
203      * @return a copy of the bytes array with each occurrence of the orig bytes with the replacement bytes
204      */
205     static byte[] patchBytes(byte[] bytes, byte[] orig, byte[] repl) {
206         if (orig.length != repl.length && orig.length > 0)
207             throw new IllegalArgumentException("orig bytes and replacement must be same length");
208         byte[] result = Arrays.copyOf(bytes, bytes.length);
209         for (int i = 0; i < result.length - orig.length; i++) {
210             if (Arrays.equals(result, i, i + orig.length, orig, 0, orig.length)) {
211                 System.arraycopy(repl, 0, result, i, orig.length);
212                 i = i + orig.length - 1;    // continue replacing after this occurrence
213             }
214         }
215         return result;
216     }
217 
218     public static class SimpleValue implements Serializable {
219         @Serial
220         private static final long serialVersionUID = 1L;
221 
222         int i;
223         Serializable obj;
224 
225         public SimpleValue(Serializable o) {
226             this.obj = o;
227             this.i = 0;
228         }
229         SimpleValue(int i) {
230             this.i = i;
231         }
232 
233         public SimpleValue(Serializable o, int i) {
234             this.obj = o;
235             this.i = i;
236         }
237 
238         public boolean equals(Object other) {
239             if (other instanceof SimpleValue simpleValue) {
240                 return (i == simpleValue.i && Objects.equals(obj, simpleValue.obj));
241             }
242             return false;
243         }
244 
245         public int hashCode() {
246             return i;
247         }
248 
249         public String toString() {
250             return "SimpleValue{" + "i=" + i + ", obj=" + obj + '}';
251         }
252     }
253 
254     interface Tree {
255         static Tree makeTree(int depth, BiFunction<Tree, Tree, Tree> genNode) {
256             if (depth <= 0) return null;
257             Tree left = makeTree(depth - 1, genNode);
258             Tree right = makeTree(depth - 1, genNode);
259             return genNode.apply(left, right);
260         }
261 
262         Tree left();
263         Tree right();
264     }
265     static class TreeI implements Tree, Serializable {
266 
267         @Serial
268         private static final long serialVersionUID = 2L;
269         private TreeI left;
270         private TreeI right;
271 
272         TreeI(TreeI left, TreeI right) {
273             this.left = left;
274             this.right = right;
275         }
276 
277         public TreeI left() {
278             return left;
279         }
280         public TreeI right() {
281             return right;
282         }
283 
284         public void setLeft(TreeI left) {
285             this.left = left;
286         }
287         public void setRight(TreeI right) {
288             this.right = right;
289         }
290 
291         public boolean equals(Object other) {
292             if (other instanceof TreeV tree) {
293                 boolean leftEq = (this.left == null && tree.left == null) ||
294                         left.equals(tree.left);
295                 boolean rightEq = (this.right == null && tree.right == null) ||
296                         right.equals(tree.right);
297                 return leftEq == rightEq;
298             }
299             return false;
300         }
301         public String toString() {
302             return toString(5);
303         }
304         public String toString(int depth) {
305             if (depth <= 0)
306                 return "!";
307             String l = (left != null) ? left.toString(depth - 1) : Character.toString(126);
308             String r = (right != null) ? right.toString(depth - 1) : Character.toString(126);
309             return "(" + l + r + ")";
310         }
311     }
312 
313     @MigratedValueClass
314     static value class TreeV implements Tree, Serializable {
315 
316         @Serial
317         private static final long serialVersionUID = 2L;
318         private TreeV left;
319         private TreeV right;
320 
321         @DeserializeConstructor
322         TreeV(TreeV left, TreeV right) {
323             this.left = left;
324             this.right = right;
325         }
326 
327         public TreeV left() {
328             return left;
329         }
330         public TreeV right() {
331             return right;
332         }
333 
334         public boolean equals(Object other) {
335             // avoid ==, is substitutable check causes stack overflow.
336             if (other instanceof TreeV tree) {
337                 return compRef(this.left, tree.left) && compRef(this.right, tree.right);
338             }
339             return false;
340         }
341 
342         // Compare references but don't use ==; isSubstitutable may recurse
343         private static boolean compRef(Object o1, Object o2) {
344             if (o1 == null && o2 == null)
345                 return true;
346             if (o1 != null && o2 != null)
347                 return o1.equals(o2);
348             return false;
349 
350         }
351         public String toString() {
352             return toString(10);
353         }
354         public String toString(int depth) {
355             if (depth <= 0)
356                 return "!";
357             String l = (left != null) ? left.toString(depth - 1) : Character.toString(126);
358             String r = (right != null) ? right.toString(depth - 1) : Character.toString(126);
359             return "(" + l + r + ")";
360         }
361     }
362 
363     @Test
364     void testExternalizableNotSer() {
365         var obj = new ValueExt();
366         var ex = Assertions.assertThrows(InvalidClassException.class, () -> serialize(obj));
367         Assertions.assertEquals("SimpleValueGraphs$ValueExt; Externalizable not valid for value class", ex.getMessage());
368     }
369 
370     @Test
371     void testExternalizableNotDeser() throws IOException {
372         var obj = new IdentExt();
373         byte[] bytes = serialize(obj);
374         byte[] newBytes = patchBytes(bytes, "IdentExt", "ValueExt");
375         var ex = Assertions.assertThrows(InvalidClassException.class, () -> deserialize(newBytes));
376         Assertions.assertTrue(ex.getMessage().contains("Externalizable not valid for value class"));
377     }
378 
379     // Exception trying to serialize
380     // Exception trying to deserialize
381 
382     static class IdentExt implements Externalizable {
383         public void writeExternal(ObjectOutput is) {
384 
385         }
386         public void readExternal(ObjectInput is) {
387 
388         }
389         @Serial
390         private static final long serialVersionUID = 3L;
391     }
392 
393     // Not Deserializable or Deserializable, no writeable fields
394     static value class ValueExt implements Externalizable {
395         public void writeExternal(ObjectOutput is) {
396 
397         }
398         public void readExternal(ObjectInput is) {
399 
400         }
401         @Serial
402         private static final long serialVersionUID = 3L;
403     }
404 }