1 /*
2 * Copyright (c) 2019, 2026, 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 junit/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 import java.util.stream.Stream;
52
53 import jdk.internal.MigratedValueClass;
54 import jdk.internal.value.DeserializeConstructor;
55
56 import org.junit.jupiter.api.Assertions;
57 import static org.junit.jupiter.api.Assertions.assertEquals;
58 import static org.junit.jupiter.api.Assertions.assertThrows;
59 import org.junit.jupiter.api.TestInstance;
60 import org.junit.jupiter.params.ParameterizedTest;
61 import org.junit.jupiter.params.provider.Arguments;
62 import org.junit.jupiter.params.provider.MethodSource;
63
64 public class ValueSerializationTest {
65
66 static final Class<NotSerializableException> NSE = NotSerializableException.class;
67 private static final Class<InvalidClassException> ICE = InvalidClassException.class;
68
69 public static Stream<Arguments> doesNotImplementSerializable() {
70 return Stream.of(
71 Arguments.of( new NonSerializablePoint(10, 100), NSE),
72 Arguments.of( new NonSerializablePointNoCons(10, 100), ICE),
73 // an array of Points
74 Arguments.of( new NonSerializablePoint[] {new NonSerializablePoint(1, 5)}, NSE),
75 Arguments.of( Arguments.of(new NonSerializablePoint(3, 7)), NSE),
76 Arguments.of( new ExternalizablePoint(12, 102), ICE),
77 Arguments.of( new ExternalizablePoint[] {
78 new ExternalizablePoint(3, 7),
79 new ExternalizablePoint(2, 8) }, ICE),
80 Arguments.of( new Object[] {
81 new ExternalizablePoint(13, 17),
82 new ExternalizablePoint(14, 18) }, ICE));
83 }
84
85 // value class that DOES NOT implement Serializable should throw ICE
86 @ParameterizedTest
87 @MethodSource("doesNotImplementSerializable")
88 public void doesNotImplementSerializable(Object obj, Class expectedException) {
89 assertThrows(expectedException, () -> serialize(obj));
90 }
91
92 /* Non-Serializable point. */
93 public static value class NonSerializablePoint {
94 public int x;
95 public int y;
96
97 public NonSerializablePoint(int x, int y) {
98 this.x = x;
99 this.y = y;
100 }
101 @Override public String toString() {
102 return "[NonSerializablePoint x=" + x + " y=" + y + "]";
103 }
104 }
105
106 /* Non-Serializable point, because it does not have an @DeserializeConstructor constructor. */
107 public static value class NonSerializablePointNoCons implements Serializable {
108 public int x;
109 public int y;
110
111 // Note: Must NOT have @DeserializeConstructor annotation
112 public NonSerializablePointNoCons(int x, int y) {
113 this.x = x;
114 this.y = y;
115 }
116 @Override public String toString() {
117 return "[NonSerializablePointNoCons x=" + x + " y=" + y + "]";
118 }
119 }
120
121 /* An Externalizable Point is not Serializable, readExternal cannot modify fields */
122 static value class ExternalizablePoint implements Externalizable {
123 public int x;
124 public int y;
125 public ExternalizablePoint() {this.x = 0; this.y = 0;}
126 ExternalizablePoint(int x, int y) { this.x = x; this.y = y; }
127 @Override public void readExternal(ObjectInput in) { }
128 @Override public void writeExternal(ObjectOutput out) { }
129 @Override public String toString() {
130 return "[ExternalizablePoint x=" + x + " y=" + y + "]"; }
131 }
132
133 public static Stream<Arguments> implementSerializable() {
134 return Stream.of(
135 Arguments.of(new SerializablePoint(11, 101)),
136 Arguments.of((Object)(new SerializablePoint[]{
137 new SerializablePoint(1, 5),
138 new SerializablePoint(2, 6)}),
139 Arguments.of(Arguments.of(
140 new SerializablePoint(3, 7),
141 new SerializablePoint(4, 8))),
142 Arguments.of(new SerializableFoo(45)),
143 Arguments.of((Object)(new SerializableFoo[]{new SerializableFoo(46)})),
144 Arguments.of(new ExternalizableFoo("hello")),
145 Arguments.of((Object)new ExternalizableFoo[]{new ExternalizableFoo("there")})));
146 }
147
148 // value class that DOES implement Serializable is supported
149 @ParameterizedTest
150 @MethodSource("implementSerializable")
151 public void implementSerializable(Object obj) throws IOException, ClassNotFoundException {
152 byte[] bytes = serialize(obj);
153 Object actual = deserialize(bytes);
154 if (obj.getClass().isArray())
155 Assertions.assertArrayEquals((Object[])actual, (Object[])obj);
156 else
157 assertEquals(actual, obj);
158 }
159
160 /* A Serializable value class Point */
161 @MigratedValueClass
162 static value class SerializablePoint implements Serializable {
163 public int x;
164 public int y;
165 @DeserializeConstructor
166 private SerializablePoint(int x, int y) { this.x = x; this.y = y; }
167
168 @Override public String toString() {
169 return "[SerializablePoint x=" + x + " y=" + y + "]";
170 }
171 }
172
173 /* A Serializable Foo, with a serial proxy */
174 static value class SerializableFoo implements Serializable {
175 public int x;
176 @DeserializeConstructor
177 SerializableFoo(int x) { this.x = x; }
178
179 @Serial Object writeReplace() throws ObjectStreamException {
180 return new SerialFooProxy(x);
181 }
182 @Serial private void readObject(ObjectInputStream s) throws InvalidObjectException {
183 throw new InvalidObjectException("Proxy required");
184 }
185 private record SerialFooProxy(int x) implements Serializable {
186 @Serial Object readResolve() throws ObjectStreamException {
187 return new SerializableFoo(x);
188 }
189 }
190 }
191
192 /* An Externalizable Foo, with a serial proxy */
193 static value class ExternalizableFoo implements Externalizable {
194 public String s;
195 ExternalizableFoo(String s) { this.s = s; }
196 public boolean equals(Object other) {
197 if (other instanceof ExternalizableFoo foo) {
198 return s.equals(foo.s);
199 } else {
200 return false;
201 }
202 }
203 @Serial Object writeReplace() throws ObjectStreamException {
204 return new SerialFooProxy(s);
205 }
206 private record SerialFooProxy(String s) implements Serializable {
207 @Serial Object readResolve() throws ObjectStreamException {
208 return new ExternalizableFoo(s);
209 }
210 }
211 @Override public void readExternal(ObjectInput in) { }
212 @Override public void writeExternal(ObjectOutput out) { }
213 }
214
215 // Generate a byte stream containing a reference to the named class with the SVID and flags.
216 private static byte[] byteStreamFor(String className, long uid, byte flags)
217 throws Exception
218 {
219 ByteArrayOutputStream baos = new ByteArrayOutputStream();
220 DataOutputStream dos = new DataOutputStream(baos);
221 dos.writeShort(STREAM_MAGIC);
222 dos.writeShort(STREAM_VERSION);
223 dos.writeByte(TC_OBJECT);
224 dos.writeByte(TC_CLASSDESC);
225 dos.writeUTF(className);
226 dos.writeLong(uid);
227 dos.writeByte(flags);
228 dos.writeShort(0); // number of fields
229 dos.writeByte(TC_ENDBLOCKDATA); // no annotations
230 dos.writeByte(TC_NULL); // no superclasses
231 dos.close();
232 return baos.toByteArray();
233 }
234
235 public static Stream<Arguments> classes() {
236 return Stream.of(
237 Arguments.of( ExternalizableFoo.class, SC_EXTERNALIZABLE, ICE ),
238 Arguments.of( ExternalizableFoo.class, SC_SERIALIZABLE, ICE ),
239 Arguments.of( SerializablePoint.class, SC_EXTERNALIZABLE, ICE ),
240 Arguments.of( 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 @ParameterizedTest
249 @MethodSource("classes")
250 public void deserialize(Class<?> cls, byte flags, Class<Exception> expected) throws Exception {
251 var clsDesc = ObjectStreamClass.lookup(cls);
252 long uid = clsDesc == null ? 0L : clsDesc.getSerialVersionUID();
253 byte[] serialBytes = byteStreamFor(cls.getName(), uid, flags);
254 if (expected == null) {
255 Assertions.assertDoesNotThrow(() -> deserialize(serialBytes));
256 } else {
257 Assertions.assertThrows(expected, () -> deserialize(serialBytes));
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 }