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 * @run testng/othervm SimpleValueGraphs 30 */ 31 32 import java.lang.StackOverflowError; 33 import java.io.ByteArrayInputStream; 34 import java.io.ByteArrayOutputStream; 35 import java.io.Externalizable; 36 import java.io.IOException; 37 import java.io.ObjectInput; 38 import java.io.ObjectInputStream; 39 import java.io.ObjectOutput; 40 import java.io.ObjectOutputStream; 41 import java.io.Serializable; 42 import java.io.InvalidClassException; 43 import java.io.InvalidObjectException; 44 import java.io.NotSerializableException; 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 52 import org.testng.Assert; 53 import org.testng.annotations.DataProvider; 54 import org.testng.annotations.Test; 55 56 import jdk.test.lib.hexdump.HexPrinter; 57 import jdk.test.lib.hexdump.ObjectStreamPrinter; 58 59 @Test 60 public class SimpleValueGraphs implements Serializable { 61 62 private static boolean DEBUG = false; 63 64 private static SimpleValue foo1 = new SimpleValue("One", 1); 65 private static SimpleValue foo2 = new SimpleValue("Two", 2); 66 67 @DataProvider(name = "ValueObjects") 68 public Object[][] valueObjects() { 69 return new Object[][] { 70 {new SimpleValue(new SimpleValue(1))}, 71 {new SimpleValue(new SimpleValue(2), 3)}, 72 {new SimpleValue[] {foo1, foo1, foo2, foo1, foo2 }}, 73 }; 74 } 75 76 @Test(enabled = true, dataProvider = "ValueObjects") 77 public void roundTrip(Object expected) throws Exception { 78 byte[] bytes = serialize(expected); 79 80 if (DEBUG) 81 HexPrinter.simple().dest(System.out).formatter(ObjectStreamPrinter.formatter()).format(bytes); 82 83 Object actual = deserialize(bytes); 84 System.out.println("actual: " + actual.toString()); 85 Assert.assertEquals(actual, expected, "Mismatch" + expected.getClass()); 86 } 87 88 private static Tree treeI = Tree.makeTree(3, (l, r) -> new TreeI((TreeI)l, (TreeI)l)); 89 private static Tree treeV = Tree.makeTree(3, (l, r) -> new TreeV((TreeV)l, (TreeV)l)); 90 91 // Create a tree of identity objects with a cycle; it will serialize ok, but when deserialized as 92 // a value class the cycle is broken by replacing the back ref with null. 93 private static Tree treeCycle(boolean cycle) { 94 TreeI tree = (TreeI)Tree.makeTree(3, (l, r) -> new TreeI((TreeI)l, (TreeI)l)); 95 tree.setLeft(cycle ? tree : null); // force a cycle or null 96 return tree; 97 } 98 99 @DataProvider(name = "CompatibleChanges") 100 public Object[][] migrationObjects() { 101 return new Object[][] { 102 {treeI, "TreeI", "TreeV", treeV}, // Serialize as an identity class, deserialize as Value class 103 {treeCycle(true), "TreeI", "TreeV", treeCycle(false)}, 104 // TBD: add cases for serializing TreeV and converting to TreeI: 105 // Waiting for: 106 // JDK-8293321: [lworld] PrimitiveObjectMethods.substitutableInvoker StackOverFlowError 107 }; 108 } 109 110 /** 111 * Test serializing a object graph, and deserialize with a modification of the serialized form. 112 * The modifications to the stream change the class name being deserialized. 113 * The cases include serializing an identity class and deserialize the corresponding 114 * value class. 115 * 116 * @param origObj an object to serialize 117 * @param origName a string in the serialized stream to replace 118 * @param replName a string to replace the original string 119 * @param expectedObject the expected object (graph) or an exception if it should fail 120 * @throws Exception some unexpected exception may be thrown and cause the test to fail 121 */ 122 @Test(enabled = true, dataProvider = "CompatibleChanges") 123 public void treeVTest(Object origObj, String origName, String replName, Object expectedObject) throws Exception { 124 byte[] bytes = serialize(origObj); 125 if (DEBUG) { 126 System.out.println("Original serialized " + origObj.getClass().getName()); 127 HexPrinter.simple().dest(System.out).formatter(ObjectStreamPrinter.formatter()).format(bytes); 128 } 129 130 // Modify the serialized bytes to change a class name from the serialized name 131 // to a different class. The replacement name must be the same length as thr original name. 132 byte[] replBytes = patchBytes(bytes, origName, replName); 133 if (DEBUG) { 134 System.out.println("Modified serialized " + origObj.getClass().getName()); 135 HexPrinter.simple().dest(System.out).formatter(ObjectStreamPrinter.formatter()).format(replBytes); 136 } 137 try { 138 Object actual = deserialize(replBytes); 139 if (expectedObject instanceof Exception ex) { 140 Assert.fail("Unexpected: " + expectedObject); 141 } 142 // Compare the shape of the actual and expected trees 143 Assert.assertEquals(actual.toString(), expectedObject.toString(), 144 "Resulting object not equals: " + actual.getClass().getName()); 145 146 } catch (Exception ex) { 147 Assert.assertEquals(ex.getClass(), expectedObject.getClass(), "exception type"); 148 Assert.assertEquals(ex.getMessage(), ((Exception)expectedObject).getMessage(), "exception message"); 149 } 150 } 151 152 /** 153 * Serialize an object and return the serialized bytes. 154 * 155 * @param expected an object to serialize 156 * @return a byte array containing the serialized object 157 */ 158 private static byte[] serialize(Object expected) throws IOException { 159 try (ByteArrayOutputStream bout = new ByteArrayOutputStream(); 160 ObjectOutputStream oout = new ObjectOutputStream(bout)) { 161 oout.writeObject(expected); 162 oout.flush(); 163 return bout.toByteArray(); 164 } 165 } 166 167 /** 168 * Deserialize an object from the byte array. 169 * @param bytes a byte array 170 * @return an Object read from the byte array 171 */ 172 private static Object deserialize(byte[] bytes) throws IOException, ClassNotFoundException { 173 try (ByteArrayInputStream bin = new ByteArrayInputStream(bytes); 174 ObjectInputStream oin = new ObjectInputStream(bin)) { 175 return oin.readObject(); 176 } 177 } 178 179 /** 180 * Replace every occurence of the string in the byte array with the replacement. 181 * The strings are US_ASCII only. 182 * @param bytes a byte array 183 * @param orig a string, converted to bytes using US_ASCII, originally exists in the bytes 184 * @param repl a string, converted to byted using US_ASCII, to replace the original bytes 185 * @return a new byte array that has been patched 186 */ 187 private byte[] patchBytes(byte[] bytes, String orig, String repl) { 188 byte[] alt = patchBytes(bytes, 189 orig.getBytes(StandardCharsets.US_ASCII), 190 repl.getBytes(StandardCharsets.US_ASCII)); 191 return alt; 192 } 193 194 /** 195 * Replace every occurence of the original bytes in the byte array with the replacement bytes. 196 * @param bytes a byte array 197 * @param orig a byte array containing existing bytes in the byte array 198 * @param repl a byte array to replace the original bytes 199 * @return a copy of the bytes array with each occurence of the orig bytes with the replacement bytes 200 */ 201 static byte[] patchBytes(byte[] bytes, byte[] orig, byte[] repl) { 202 if (orig.length != repl.length && orig.length > 0) 203 throw new IllegalArgumentException("orig bytes and replacement must be same length"); 204 byte[] result = Arrays.copyOf(bytes, bytes.length); 205 for (int i = 0; i < result.length - orig.length; i++) { 206 if (Arrays.equals(result, i, i + orig.length, orig, 0, orig.length)) { 207 for (int j = 0; j < orig.length; j++) { 208 result[i + j] = repl[j]; 209 } 210 i = i + orig.length - 1; // continue replacing after this occurrence 211 } 212 } 213 return result; 214 } 215 216 public static class SimpleValue implements Serializable { 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 obj) { 236 if (obj instanceof SimpleValue simpleValue) { 237 return (i == simpleValue.i && Objects.equals(obj, simpleValue)); 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 Tree t = genNode.apply(left, right); 257 return t; 258 } 259 260 Tree left(); 261 Tree right(); 262 } 263 static class TreeI implements Tree, Serializable { 264 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 // avoid ==, is substutible check causes stack overflow. 290 if (other instanceof TreeV tree) { 291 boolean leftEq = (this.left == null && tree.left == null) || 292 left.equals(tree.left); 293 boolean rightEq = (this.right == null && tree.right == null) || 294 right.equals(tree.right); 295 return leftEq == rightEq; 296 } 297 return false; 298 } 299 public String toString() { 300 return toString(5); 301 } 302 public String toString(int depth) { 303 if (depth <= 0) 304 return "!"; 305 String l = (left != null) ? left.toString(depth - 1) : Character.toString(126); 306 String r = (right != null) ? right.toString(depth - 1) : Character.toString(126); 307 return "(" + l + r + ")"; 308 } 309 } 310 311 static value class TreeV implements Tree, Serializable { 312 313 private static final long serialVersionUID = 2L; 314 private TreeV left; 315 private TreeV right; 316 317 TreeV(TreeV left, TreeV right) { 318 this.left = left; 319 this.right = right; 320 } 321 322 public TreeV left() { 323 return left; 324 } 325 public TreeV right() { 326 return right; 327 } 328 329 public boolean equals(Object other) { 330 // avoid ==, is substutible check causes stack overflow. 331 if (other instanceof TreeV tree) { 332 return compRef(this.left, tree.left) && compRef(this.right, tree.right); 333 } 334 return false; 335 } 336 337 // Compare references but don't use ==; isSubstutitable may recurse 338 private static boolean compRef(Object o1, Object o2) { 339 if (o1 == null && o2 == null) 340 return true; 341 if (o1 != null && o2 != null) 342 return o1.equals(o2); 343 return false; 344 345 } 346 public String toString() { 347 return toString(10); 348 } 349 public String toString(int depth) { 350 if (depth <= 0) 351 return "!"; 352 String l = (left != null) ? left.toString(depth - 1) : Character.toString(126); 353 String r = (right != null) ? right.toString(depth - 1) : Character.toString(126); 354 return "(" + l + r + ")"; 355 } 356 } 357 358 @Test 359 void testExternalizableNotSer() { 360 var obj = new ValueExt(); 361 var ex = Assert.expectThrows(NotSerializableException.class, () -> serialize(obj)); 362 Assert.assertTrue(ex.getMessage().contains("Externalizable not valid for value class")); 363 } 364 365 @Test 366 void testExternalizableNotDeser() throws IOException { 367 var obj = new IdentExt(); 368 byte[] bytes = serialize(obj); 369 byte[] newBytes = patchBytes(bytes, "IdentExt", "ValueExt"); 370 var ex = Assert.expectThrows(NotSerializableException.class, () -> deserialize(newBytes)); 371 Assert.assertTrue(ex.getMessage().contains("Externalizable not valid for value class")); 372 } 373 374 // Exception trying to serialize 375 // Exception trying to deserialize 376 377 static class IdentExt implements Externalizable { 378 public void writeExternal(ObjectOutput is) { 379 380 } 381 public void readExternal(ObjectInput is) { 382 383 } 384 private static final long serialVersionUID = 3L; 385 } 386 387 static value class ValueExt implements Externalizable { 388 public void writeExternal(ObjectOutput is) { 389 390 } 391 public void readExternal(ObjectInput is) { 392 393 } 394 private static final long serialVersionUID = 3L; 395 396 } 397 }