1 /*
2 * Copyright (c) 1999, 2025, 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 testng/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.Serializable;
43 import java.io.InvalidClassException;
44
45 import java.util.Arrays;
46 import java.util.function.BiFunction;
47 import java.util.Objects;
48
49 import java.nio.charset.StandardCharsets;
50
51 import jdk.internal.value.DeserializeConstructor;
52 import jdk.internal.MigratedValueClass;
53
54 import org.testng.Assert;
55 import org.testng.annotations.DataProvider;
56 import org.testng.annotations.Test;
57
58 import jdk.test.lib.hexdump.HexPrinter;
59 import jdk.test.lib.hexdump.ObjectStreamPrinter;
60
61 @Test
62 public class SimpleValueGraphs implements Serializable {
63
64 private static boolean DEBUG = true;
65
66 private static SimpleValue foo1 = new SimpleValue("One", 1);
67 private static SimpleValue foo2 = new SimpleValue("Two", 2);
68
69 @DataProvider(name = "ValueObjects")
70 public Object[][] valueObjects() {
71 return new Object[][] {
72 {new SimpleValue(new SimpleValue(1))},
73 {new SimpleValue(new SimpleValue(2), 3)},
74 {new SimpleValue[] {foo1, foo1, foo2, foo1, foo2 }},
75 };
76 }
77
78 @Test(enabled = true, dataProvider = "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 Assert.assertEquals(actual, expected, "Mismatch" + expected.getClass());
88 }
89
90 private static Tree treeI = Tree.makeTree(3, (l, r) -> new TreeI((TreeI)l, (TreeI)l));
91 private static Tree treeV = Tree.makeTree(3, (l, r) -> new TreeV((TreeV)l, (TreeV)l));
92
93 // Create a tree of identity objects with a cycle; it will serialize ok, but when deserialized as
94 // a value class the cycle is broken by replacing the back ref with null.
95 private static Tree treeCycle(boolean cycle) {
96 TreeI tree = (TreeI)Tree.makeTree(3, (l, r) -> new TreeI((TreeI)l, (TreeI)l));
97 tree.setLeft(cycle ? tree : null); // force a cycle or null
98 return tree;
99 }
100
101 @DataProvider(name = "CompatibleChanges")
102 public Object[][] migrationObjects() {
103 return new Object[][] {
104 {treeI, "TreeI", "TreeV", treeV}, // Serialize as an identity class, deserialize as Value class
105 {treeCycle(true), "TreeI", "TreeV", treeCycle(false)},
106 // TBD: add cases for serializing TreeV and converting to TreeI:
107 // Waiting for:
108 // JDK-8293321: [lworld] PrimitiveObjectMethods.substitutableInvoker StackOverFlowError
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 @Test(enabled = true, dataProvider = "CompatibleChanges")
125 public void treeVTest(Object origObj, String origName, String replName, Object expectedObject) throws Exception {
126 byte[] bytes = serialize(origObj);
127 if (DEBUG) {
128 System.out.println("Original serialized " + origObj.getClass().getName());
129 HexPrinter.simple().dest(System.out).formatter(ObjectStreamPrinter.formatter()).format(bytes);
130 }
131
132 // Modify the serialized bytes to change a class name from the serialized name
133 // to a different class. The replacement name must be the same length as the original name.
134 byte[] replBytes = patchBytes(bytes, origName, replName);
135 if (DEBUG) {
136 System.out.println("Modified serialized " + origObj.getClass().getName());
137 HexPrinter.simple().dest(System.out).formatter(ObjectStreamPrinter.formatter()).format(replBytes);
138 }
139 try {
140 Object actual = deserialize(replBytes);
141 if (expectedObject instanceof Exception ex) {
142 Assert.fail("Unexpected: " + expectedObject);
143 }
144 // Compare the shape of the actual and expected trees
145 Assert.assertEquals(actual.toString(), expectedObject.toString(),
146 "Resulting object not equals: " + actual.getClass().getName());
147
148 } catch (Exception ex) {
149 ex.printStackTrace();
150 Assert.assertEquals(ex.getClass(), expectedObject.getClass(), ex.toString());
151 Assert.assertEquals(ex.getMessage(), ((Exception)expectedObject).getMessage(), ex.toString());
152 }
153 }
154
155 /**
156 * Serialize an object and return the serialized bytes.
157 *
158 * @param expected an object to serialize
159 * @return a byte array containing the serialized object
160 */
161 private static byte[] serialize(Object expected) throws IOException {
162 try (ByteArrayOutputStream bout = new ByteArrayOutputStream();
163 ObjectOutputStream oout = new ObjectOutputStream(bout)) {
164 oout.writeObject(expected);
165 oout.flush();
166 return bout.toByteArray();
167 }
168 }
169
170 /**
171 * Deserialize an object from the byte array.
172 * @param bytes a byte array
173 * @return an Object read from the byte array
174 */
175 private static Object deserialize(byte[] bytes) throws IOException, ClassNotFoundException {
176 try (ByteArrayInputStream bin = new ByteArrayInputStream(bytes);
177 ObjectInputStream oin = new ObjectInputStream(bin)) {
178 return oin.readObject();
179 }
180 }
181
182 /**
183 * Replace every occurrence of the string in the byte array with the replacement.
184 * The strings are US_ASCII only.
185 * @param bytes a byte array
186 * @param orig a string, converted to bytes using US_ASCII, originally exists in the bytes
187 * @param repl a string, converted to byted using US_ASCII, to replace the original bytes
188 * @return a new byte array that has been patched
189 */
190 private byte[] patchBytes(byte[] bytes, String orig, String repl) {
191 byte[] alt = patchBytes(bytes,
192 orig.getBytes(StandardCharsets.US_ASCII),
193 repl.getBytes(StandardCharsets.US_ASCII));
194 return alt;
195 }
196
197 /**
198 * Replace every occurrence of the original bytes in the byte array with the replacement bytes.
199 * @param bytes a byte array
200 * @param orig a byte array containing existing bytes in the byte array
201 * @param repl a byte array to replace the original bytes
202 * @return a copy of the bytes array with each occurence of the orig bytes with the replacement bytes
203 */
204 static byte[] patchBytes(byte[] bytes, byte[] orig, byte[] repl) {
205 if (orig.length != repl.length && orig.length > 0)
206 throw new IllegalArgumentException("orig bytes and replacement must be same length");
207 byte[] result = Arrays.copyOf(bytes, bytes.length);
208 for (int i = 0; i < result.length - orig.length; i++) {
209 if (Arrays.equals(result, i, i + orig.length, orig, 0, orig.length)) {
210 for (int j = 0; j < orig.length; j++) {
211 result[i + j] = repl[j];
212 }
213 i = i + orig.length - 1; // continue replacing after this occurrence
214 }
215 }
216 return result;
217 }
218
219 public static class SimpleValue implements Serializable {
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 obj) {
239 if (obj instanceof SimpleValue simpleValue) {
240 return (i == simpleValue.i && Objects.equals(this.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 Tree t = genNode.apply(left, right);
260 return t;
261 }
262
263 Tree left();
264 Tree right();
265 }
266 static class TreeI implements Tree, Serializable {
267
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 private static final long serialVersionUID = 2L;
317 private TreeV left;
318 private TreeV right;
319
320 @DeserializeConstructor
321 TreeV(TreeV left, TreeV right) {
322 this.left = left;
323 this.right = right;
324 }
325
326 public TreeV left() {
327 return left;
328 }
329 public TreeV right() {
330 return right;
331 }
332
333 public boolean equals(Object other) {
334 // avoid ==, is substutible check causes stack overflow.
335 if (other instanceof TreeV tree) {
336 return compRef(this.left, tree.left) && compRef(this.right, tree.right);
337 }
338 return false;
339 }
340
341 // Compare references but don't use ==; isSubstitutable may recurse
342 private static boolean compRef(Object o1, Object o2) {
343 if (o1 == null && o2 == null)
344 return true;
345 if (o1 != null && o2 != null)
346 return o1.equals(o2);
347 return false;
348
349 }
350 public String toString() {
351 return toString(10);
352 }
353 public String toString(int depth) {
354 if (depth <= 0)
355 return "!";
356 String l = (left != null) ? left.toString(depth - 1) : Character.toString(126);
357 String r = (right != null) ? right.toString(depth - 1) : Character.toString(126);
358 return "(" + l + r + ")";
359 }
360 }
361
362 @Test
363 void testExternalizableNotSer() {
364 var obj = new ValueExt();
365 var ex = Assert.expectThrows(InvalidClassException.class, () -> serialize(obj));
366 Assert.assertEquals(ex.getMessage(),
367 "SimpleValueGraphs$ValueExt; Externalizable not valid for value class");
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 = Assert.expectThrows(InvalidClassException.class, () -> deserialize(newBytes));
376 Assert.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 private static final long serialVersionUID = 3L;
390 }
391
392 // Not Desrializable or Deserializable, no writeable fields
393 static value class ValueExt implements Externalizable {
394 public void writeExternal(ObjectOutput is) {
395
396 }
397 public void readExternal(ObjectInput is) {
398
399 }
400 private static final long serialVersionUID = 3L;
401
402 }
403 }