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