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.Deserializer;
54
55 import jdk.test.lib.hexdump.HexPrinter;
56 import jdk.test.lib.hexdump.ObjectStreamPrinter;
57 import org.junit.jupiter.api.Assertions;
58 import org.junit.jupiter.api.Test;
59 import org.junit.jupiter.params.ParameterizedTest;
60 import org.junit.jupiter.params.provider.Arguments;
61 import org.junit.jupiter.params.provider.MethodSource;
62
63 public class SimpleValueGraphs implements Serializable {
64
65 private static final boolean DEBUG = true;
66
67 private static final SimpleValue foo1 = new SimpleValue("One", 1);
68 private static final SimpleValue foo2 = new SimpleValue("Two", 2);
69
70 public static Stream<Arguments> valueObjects() {
71 return Stream.of(
72 Arguments.of(new SimpleValue(new SimpleValue(1))),
73 Arguments.of(new SimpleValue(new SimpleValue(2), 3)),
74 Arguments.of((Object)(new SimpleValue[] {foo1, foo1, foo2, foo1, foo2 })));
75 }
76
77 @ParameterizedTest
78 @MethodSource("valueObjects")
79 public void roundTrip(Object expected) throws Exception {
80 byte[] bytes = serialize(expected);
81
82 if (DEBUG)
83 HexPrinter.simple().dest(System.out).formatter(ObjectStreamPrinter.formatter()).format(bytes);
84
85 Object actual = deserialize(bytes);
86 System.out.println("actual: " + actual.toString());
87 if (actual.getClass().isArray()) {
88 Assertions.assertArrayEquals((Object[]) expected, (Object[]) actual, "Mismatch " + expected.getClass());
89 } else {
90 Assertions.assertEquals(expected, actual, "Mismatch " + expected.getClass());
91 }
92 }
93
94 private static final Tree treeI = Tree.makeTree(3, (l, r) -> new TreeI((TreeI)l, (TreeI)l));
95 private static final Tree treeV = Tree.makeTree(3, (l, r) -> new TreeV((TreeV)l, (TreeV)l));
96
97 // Create a tree of identity objects with a cycle; it will serialize ok, but when deserialized as
98 // a value class the cycle is broken by replacing the back ref with null.
99 private static Tree treeCycle(boolean cycle) {
100 TreeI tree = (TreeI)Tree.makeTree(3, (l, r) -> new TreeI((TreeI)l, (TreeI)l));
101 tree.setLeft(cycle ? tree : null); // force a cycle or null
102 return tree;
103 }
104
105 public static Stream<Arguments> migrationObjects() {
106 return Stream.of(
107 Arguments.of(treeI, "TreeI", "TreeV", treeV), // Serialize as an identity class, deserialize as Value class
108 Arguments.of(treeCycle(true), "TreeI", "TreeV", treeCycle(false))
109 );
110 }
111
112 /**
113 * Test serializing an object graph, and deserialize with a modification of the serialized form.
114 * The modifications to the stream change the class name being deserialized.
115 * The cases include serializing an identity class and deserialize the corresponding
116 * value class.
117 *
118 * @param origObj an object to serialize
119 * @param origName a string in the serialized stream to replace
120 * @param replName a string to replace the original string
121 * @param expectedObject the expected object (graph) or an exception if it should fail
122 * @throws Exception some unexpected exception may be thrown and cause the test to fail
123 */
124 @ParameterizedTest
125 @MethodSource("migrationObjects")
126 public void treeVTest(Object origObj, String origName, String replName, Object expectedObject) throws Exception {
127 byte[] bytes = serialize(origObj);
128 if (DEBUG) {
129 System.out.println("Original serialized " + origObj.getClass().getName());
130 HexPrinter.simple().dest(System.out).formatter(ObjectStreamPrinter.formatter()).format(bytes);
131 }
132
133 // Modify the serialized bytes to change a class name from the serialized name
134 // to a different class. The replacement name must be the same length as the original name.
135 byte[] replBytes = patchBytes(bytes, origName, replName);
136 if (DEBUG) {
137 System.out.println("Modified serialized " + origObj.getClass().getName());
138 HexPrinter.simple().dest(System.out).formatter(ObjectStreamPrinter.formatter()).format(replBytes);
139 }
140 try {
141 Object actual = Assertions.assertDoesNotThrow(() ->deserialize(replBytes));
142
143 // Compare the shape of the actual and expected trees
144 Assertions.assertEquals(expectedObject.toString(), actual.toString(),
145 "Resulting object not equals: " + actual.getClass().getName());
146
147 } catch (Exception ex) {
148 ex.printStackTrace();
149 Assertions.assertEquals(expectedObject.getClass(), ex.getClass(), ex.toString());
150 Assertions.assertEquals(((Exception)expectedObject).getMessage(), ex.getMessage(), ex.toString());
151 }
152 }
153
154 /**
155 * Serialize an object and return the serialized bytes.
156 *
157 * @param expected an object to serialize
158 * @return a byte array containing the serialized object
159 */
160 private static byte[] serialize(Object expected) throws IOException {
161 try (ByteArrayOutputStream bout = new ByteArrayOutputStream();
162 ObjectOutputStream oout = new ObjectOutputStream(bout)) {
163 oout.writeObject(expected);
164 oout.flush();
165 return bout.toByteArray();
166 }
167 }
168
169 /**
170 * Deserialize an object from the byte array.
171 * @param bytes a byte array
172 * @return an Object read from the byte array
173 */
174 private static Object deserialize(byte[] bytes) throws IOException, ClassNotFoundException {
175 try (ByteArrayInputStream bin = new ByteArrayInputStream(bytes);
176 ObjectInputStream oin = new ObjectInputStream(bin)) {
177 return oin.readObject();
178 }
179 }
180
181 /**
182 * Replace every occurrence of the string in the byte array with the replacement.
183 * The strings are US_ASCII only.
184 * @param bytes a byte array
185 * @param orig a string, converted to bytes using US_ASCII, originally exists in the bytes
186 * @param repl a string, converted to bytes using US_ASCII, to replace the original bytes
187 * @return a new byte array that has been patched
188 */
189 private byte[] patchBytes(byte[] bytes, String orig, String repl) {
190 return patchBytes(bytes,
191 orig.getBytes(StandardCharsets.US_ASCII),
192 repl.getBytes(StandardCharsets.US_ASCII));
193 }
194
195 /**
196 * Replace every occurrence of the original bytes in the byte array with the replacement bytes.
197 * @param bytes a byte array
198 * @param orig a byte array containing existing bytes in the byte array
199 * @param repl a byte array to replace the original bytes
200 * @return a copy of the bytes array with each occurrence of the orig bytes with the replacement bytes
201 */
202 static byte[] patchBytes(byte[] bytes, byte[] orig, byte[] repl) {
203 if (orig.length != repl.length && orig.length > 0)
204 throw new IllegalArgumentException("orig bytes and replacement must be same length");
205 byte[] result = Arrays.copyOf(bytes, bytes.length);
206 for (int i = 0; i < result.length - orig.length; i++) {
207 if (Arrays.equals(result, i, i + orig.length, orig, 0, orig.length)) {
208 System.arraycopy(repl, 0, result, i, orig.length);
209 i = i + orig.length - 1; // continue replacing after this occurrence
210 }
211 }
212 return result;
213 }
214
215 public static class SimpleValue implements Serializable {
216 @Serial
217 private static final long serialVersionUID = 1L;
218
219 int i;
220 Serializable obj;
221
222 public SimpleValue(Serializable o) {
223 this.obj = o;
224 this.i = 0;
225 }
226 SimpleValue(int i) {
227 this.i = i;
228 }
229
230 public SimpleValue(Serializable o, int i) {
231 this.obj = o;
232 this.i = i;
233 }
234
235 public boolean equals(Object other) {
236 if (other instanceof SimpleValue simpleValue) {
237 return (i == simpleValue.i && Objects.equals(obj, simpleValue.obj));
238 }
239 return false;
240 }
241
242 public int hashCode() {
243 return i;
244 }
245
246 public String toString() {
247 return "SimpleValue{" + "i=" + i + ", obj=" + obj + '}';
248 }
249 }
250
251 interface Tree {
252 static Tree makeTree(int depth, BiFunction<Tree, Tree, Tree> genNode) {
253 if (depth <= 0) return null;
254 Tree left = makeTree(depth - 1, genNode);
255 Tree right = makeTree(depth - 1, genNode);
256 return genNode.apply(left, right);
257 }
258
259 Tree left();
260 Tree right();
261 }
262 static class TreeI implements Tree, Serializable {
263
264 @Serial
265 private static final long serialVersionUID = 2L;
266 private TreeI left;
267 private TreeI right;
268
269 TreeI(TreeI left, TreeI right) {
270 this.left = left;
271 this.right = right;
272 }
273
274 public TreeI left() {
275 return left;
276 }
277 public TreeI right() {
278 return right;
279 }
280
281 public void setLeft(TreeI left) {
282 this.left = left;
283 }
284 public void setRight(TreeI right) {
285 this.right = right;
286 }
287
288 public boolean equals(Object other) {
289 if (other instanceof TreeV tree) {
290 boolean leftEq = (this.left == null && tree.left == null) ||
291 left.equals(tree.left);
292 boolean rightEq = (this.right == null && tree.right == null) ||
293 right.equals(tree.right);
294 return leftEq == rightEq;
295 }
296 return false;
297 }
298 public String toString() {
299 return toString(5);
300 }
301 public String toString(int depth) {
302 if (depth <= 0)
303 return "!";
304 String l = (left != null) ? left.toString(depth - 1) : Character.toString(126);
305 String r = (right != null) ? right.toString(depth - 1) : Character.toString(126);
306 return "(" + l + r + ")";
307 }
308 }
309
310 static value class TreeV implements Tree, Serializable {
311
312 @Serial
313 private static final long serialVersionUID = 2L;
314 private TreeV left;
315 private TreeV right;
316
317 @Deserializer({"left", "right"})
318 TreeV(TreeV left, TreeV right) {
319 this.left = left;
320 this.right = right;
321 }
322
323 public TreeV left() {
324 return left;
325 }
326 public TreeV right() {
327 return right;
328 }
329
330 public boolean equals(Object other) {
331 // avoid ==, is substitutable check causes stack overflow.
332 if (other instanceof TreeV tree) {
333 return compRef(this.left, tree.left) && compRef(this.right, tree.right);
334 }
335 return false;
336 }
337
338 // Compare references but don't use ==; isSubstitutable may recurse
339 private static boolean compRef(Object o1, Object o2) {
340 if (o1 == null && o2 == null)
341 return true;
342 if (o1 != null && o2 != null)
343 return o1.equals(o2);
344 return false;
345
346 }
347 public String toString() {
348 return toString(10);
349 }
350 public String toString(int depth) {
351 if (depth <= 0)
352 return "!";
353 String l = (left != null) ? left.toString(depth - 1) : Character.toString(126);
354 String r = (right != null) ? right.toString(depth - 1) : Character.toString(126);
355 return "(" + l + r + ")";
356 }
357 }
358
359 @Test
360 void testExternalizableNotSer() {
361 var obj = new ValueExt();
362 var ex = Assertions.assertThrows(InvalidClassException.class, () -> serialize(obj));
363 Assertions.assertEquals("SimpleValueGraphs$ValueExt; cannot serialize value class", ex.getMessage());
364 }
365
366 @Test
367 void testExternalizableNotDeser() throws IOException {
368 var obj = new IdentExt();
369 byte[] bytes = serialize(obj);
370 byte[] newBytes = patchBytes(bytes, "IdentExt", "ValueExt");
371 var ex = Assertions.assertThrows(InvalidClassException.class, () -> deserialize(newBytes));
372 Assertions.assertTrue(ex.getMessage().contains("cannot serialize value class"));
373 }
374
375 // Exception trying to serialize
376 // Exception trying to deserialize
377
378 static class IdentExt implements Externalizable {
379 public void writeExternal(ObjectOutput is) {
380
381 }
382 public void readExternal(ObjectInput is) {
383
384 }
385 @Serial
386 private static final long serialVersionUID = 3L;
387 }
388
389 // Not Deserializable or Deserializable, no writeable fields
390 static value class ValueExt implements Externalizable {
391 public void writeExternal(ObjectOutput is) {
392
393 }
394 public void readExternal(ObjectInput is) {
395
396 }
397 @Serial
398 private static final long serialVersionUID = 3L;
399 }
400 }