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 }