1 /* 2 * Copyright (c) 1999, 2024, 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(obj, simpleValue)); 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 }