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 }