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 }