1 /*
2 * Copyright (c) 2019, 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 * @summary Test serialization of value classes
27 * @enablePreview
28 * @modules java.base/jdk.internal java.base/jdk.internal.value
29 * @compile ValueSerializationTest.java
30 * @run testng/othervm ValueSerializationTest
31 */
32
33 import static java.io.ObjectStreamConstants.*;
34
35 import java.io.ByteArrayInputStream;
36 import java.io.ByteArrayOutputStream;
37 import java.io.DataOutputStream;
38 import java.io.Externalizable;
39 import java.io.IOException;
40 import java.io.InvalidClassException;
41 import java.io.InvalidObjectException;
42 import java.io.NotSerializableException;
43 import java.io.ObjectInput;
44 import java.io.ObjectInputStream;
45 import java.io.ObjectOutput;
46 import java.io.ObjectOutputStream;
47 import java.io.ObjectStreamClass;
48 import java.io.ObjectStreamException;
49 import java.io.Serial;
50 import java.io.Serializable;
51
52 import jdk.internal.MigratedValueClass;
53 import jdk.internal.value.DeserializeConstructor;
54
55 import org.testng.Assert;
56 import org.testng.annotations.DataProvider;
57 import org.testng.annotations.Test;
58 import static org.testng.Assert.assertEquals;
59 import static org.testng.Assert.assertThrows;
60
61 public class ValueSerializationTest {
62
63 static final Class<NotSerializableException> NSE = NotSerializableException.class;
64 private static final Class<InvalidClassException> ICE = InvalidClassException.class;
65
66 @DataProvider(name = "doesNotImplementSerializable")
67 public Object[][] doesNotImplementSerializable() {
68 return new Object[][] {
69 new Object[] { new NonSerializablePoint(10, 100), NSE},
70 new Object[] { new NonSerializablePointNoCons(10, 100), ICE},
71 // an array of Points
72 new Object[] { new NonSerializablePoint[] {new NonSerializablePoint(1, 5)}, NSE},
73 new Object[] { new Object[] {new NonSerializablePoint(3, 7)}, NSE},
74 new Object[] { new ExternalizablePoint(12, 102), ICE},
75 new Object[] { new ExternalizablePoint[] {
76 new ExternalizablePoint(3, 7),
77 new ExternalizablePoint(2, 8) }, ICE},
78 new Object[] { new Object[] {
79 new ExternalizablePoint(13, 17),
80 new ExternalizablePoint(14, 18) }, ICE},
81 };
82 }
83
84 // value class that DOES NOT implement Serializable should throw ICE
85 @Test(dataProvider = "doesNotImplementSerializable")
86 public void doesNotImplementSerializable(Object obj, Class expectedException) {
87 assertThrows(expectedException, () -> serialize(obj));
88 }
89
90 /* Non-Serializable point. */
91 public static value class NonSerializablePoint {
92 public int x;
93 public int y;
94
95 public NonSerializablePoint(int x, int y) {
96 this.x = x;
97 this.y = y;
98 }
99 @Override public String toString() {
100 return "[NonSerializablePoint x=" + x + " y=" + y + "]";
101 }
102 }
103
104 /* Non-Serializable point, because it does not have an @DeserializeConstructor constructor. */
105 public static value class NonSerializablePointNoCons implements Serializable {
106 public int x;
107 public int y;
108
109 // Note: Must NOT have @DeserializeConstructor annotation
110 public NonSerializablePointNoCons(int x, int y) {
111 this.x = x;
112 this.y = y;
113 }
114 @Override public String toString() {
115 return "[NonSerializablePointNoCons x=" + x + " y=" + y + "]";
116 }
117 }
118
119 /* An Externalizable Point is not Serializable, readExternal cannot modify fields */
120 static value class ExternalizablePoint implements Externalizable {
121 public int x;
122 public int y;
123 public ExternalizablePoint() {this.x = 0; this.y = 0;}
124 ExternalizablePoint(int x, int y) { this.x = x; this.y = y; }
125 @Override public void readExternal(ObjectInput in) { }
126 @Override public void writeExternal(ObjectOutput out) { }
127 @Override public String toString() {
128 return "[ExternalizablePoint x=" + x + " y=" + y + "]"; }
129 }
130
131 @DataProvider(name = "ImplementSerializable")
132 public Object[][] implementSerializable() {
133 return new Object[][]{
134 new Object[]{new SerializablePoint(11, 101)},
135 new Object[]{new SerializablePoint[]{
136 new SerializablePoint(1, 5),
137 new SerializablePoint(2, 6)}},
138 new Object[]{new Object[]{
139 new SerializablePoint(3, 7),
140 new SerializablePoint(4, 8)}},
141 new Object[]{new SerializableFoo(45)},
142 new Object[]{new SerializableFoo[]{new SerializableFoo(46)}},
143 new Object[]{new ExternalizableFoo("hello")},
144 new Object[]{new ExternalizableFoo[]{new ExternalizableFoo("there")}},
145 };
146 }
147
148 // value class that DOES implement Serializable is supported
149 @Test(dataProvider = "ImplementSerializable")
150 public void implementSerializable(Object obj) throws IOException, ClassNotFoundException {
151 byte[] bytes = serialize(obj);
152 Object actual = deserialize(bytes);
153 if (obj.getClass().isArray())
154 assertEquals((Object[])obj, (Object[])actual);
155 else
156 assertEquals(obj, actual);
157 }
158
159 /* A Serializable value class Point */
160 @MigratedValueClass
161 static value class SerializablePoint implements Serializable {
162 public int x;
163 public int y;
164 @DeserializeConstructor
165 private SerializablePoint(int x, int y) { this.x = x; this.y = y; }
166
167 @Override public String toString() {
168 return "[SerializablePoint x=" + x + " y=" + y + "]";
169 }
170 }
171
172 /* A Serializable Foo, with a serial proxy */
173 static value class SerializableFoo implements Serializable {
174 public int x;
175 @DeserializeConstructor
176 SerializableFoo(int x) { this.x = x; }
177
178 @Serial Object writeReplace() throws ObjectStreamException {
179 return new SerialFooProxy(x);
180 }
181 @Serial private void readObject(ObjectInputStream s) throws InvalidObjectException {
182 throw new InvalidObjectException("Proxy required");
183 }
184 private record SerialFooProxy(int x) implements Serializable {
185 @Serial Object readResolve() throws ObjectStreamException {
186 return new SerializableFoo(x);
187 }
188 }
189 }
190
191 /* An Externalizable Foo, with a serial proxy */
192 static value class ExternalizableFoo implements Externalizable {
193 public String s;
194 ExternalizableFoo(String s) { this.s = s; }
195 public boolean equals(Object other) {
196 if (other instanceof ExternalizableFoo foo) {
197 return s.equals(foo.s);
198 } else {
199 return false;
200 }
201 }
202 @Serial Object writeReplace() throws ObjectStreamException {
203 return new SerialFooProxy(s);
204 }
205 private record SerialFooProxy(String s) implements Serializable {
206 @Serial Object readResolve() throws ObjectStreamException {
207 return new ExternalizableFoo(s);
208 }
209 }
210 @Override public void readExternal(ObjectInput in) { }
211 @Override public void writeExternal(ObjectOutput out) { }
212 }
213
214 // Generate a byte stream containing a reference to the named class with the SVID and flags.
215 private static byte[] byteStreamFor(String className, long uid, byte flags)
216 throws Exception
217 {
218 ByteArrayOutputStream baos = new ByteArrayOutputStream();
219 DataOutputStream dos = new DataOutputStream(baos);
220 dos.writeShort(STREAM_MAGIC);
221 dos.writeShort(STREAM_VERSION);
222 dos.writeByte(TC_OBJECT);
223 dos.writeByte(TC_CLASSDESC);
224 dos.writeUTF(className);
225 dos.writeLong(uid);
226 dos.writeByte(flags);
227 dos.writeShort(0); // number of fields
228 dos.writeByte(TC_ENDBLOCKDATA); // no annotations
229 dos.writeByte(TC_NULL); // no superclasses
230 dos.close();
231 return baos.toByteArray();
232 }
233
234 @DataProvider(name = "classes")
235 public Object[][] classes() {
236 return new Object[][] {
237 new Object[] { ExternalizableFoo.class, SC_EXTERNALIZABLE, ICE },
238 new Object[] { ExternalizableFoo.class, SC_SERIALIZABLE, ICE },
239 new Object[] { SerializablePoint.class, SC_EXTERNALIZABLE, ICE },
240 new Object[] { SerializablePoint.class, SC_SERIALIZABLE, null },
241 };
242 }
243
244 // value class read directly from a byte stream
245 // a byte stream is generated containing a reference to the class with the flags and SVID.
246 // Reading the class from the stream verifies the exceptions thrown if there is a mismatch
247 // between the stream and the local class.
248 @Test(dataProvider = "classes")
249 public void deserialize(Class<?> cls, byte flags, Class<?> expected) throws Exception {
250 var clsDesc = ObjectStreamClass.lookup(cls);
251 long uid = clsDesc == null ? 0L : clsDesc.getSerialVersionUID();
252 byte[] serialBytes = byteStreamFor(cls.getName(), uid, flags);
253 try {
254 deserialize(serialBytes);
255 Assert.assertNull(expected, "Expected exception");
256 } catch (IOException ioe) {
257 Assert.assertEquals(ioe.getClass(), expected);
258 }
259 }
260
261 static <T> byte[] serialize(T obj) throws IOException {
262 ByteArrayOutputStream baos = new ByteArrayOutputStream();
263 ObjectOutputStream oos = new ObjectOutputStream(baos);
264 oos.writeObject(obj);
265 oos.close();
266 return baos.toByteArray();
267 }
268
269 @SuppressWarnings("unchecked")
270 static <T> T deserialize(byte[] streamBytes)
271 throws IOException, ClassNotFoundException
272 {
273 ByteArrayInputStream bais = new ByteArrayInputStream(streamBytes);
274 ObjectInputStream ois = new ObjectInputStream(bais);
275 return (T) ois.readObject();
276 }
277 }