1 /*
2 * Copyright (c) 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 * @modules java.base/jdk.internal java.base/jdk.internal.misc
27 * @run junit/othervm --enable-preview SerializeAllValueClasses
28 * @run junit/othervm SerializeAllValueClasses
29 */
30
31 import org.junit.jupiter.api.Test;
32 import org.junit.jupiter.params.ParameterizedTest;
33 import org.junit.jupiter.params.provider.Arguments;
34 import org.junit.jupiter.params.provider.MethodSource;
35
36 import java.io.ByteArrayInputStream;
37 import java.io.ByteArrayOutputStream;
38 import java.io.IOException;
39 import java.io.ObjectInputStream;
40 import java.io.ObjectOutputStream;
41 import java.io.Serializable;
42 import java.lang.reflect.Constructor;
43 import java.lang.reflect.InvocationTargetException;
44 import java.lang.reflect.Method;
45 import java.lang.reflect.Modifier;
46 import java.net.URI;
47 import java.net.URISyntaxException;
48 import java.nio.file.FileSystem;
49 import java.nio.file.FileSystems;
50 import java.nio.file.Files;
51 import java.nio.file.Path;
52 import java.time.Clock;
53 import java.time.Duration;
54 import java.time.Instant;
55 import java.time.LocalDate;
56 import java.time.LocalDateTime;
57 import java.time.LocalTime;
58 import java.time.Month;
59 import java.time.ZonedDateTime;
60 import java.time.chrono.HijrahDate;
61 import java.time.chrono.JapaneseDate;
62 import java.time.temporal.ChronoUnit;
63 import java.time.temporal.TemporalAccessor;
64 import java.time.temporal.TemporalUnit;
65 import java.util.Arrays;
66 import java.util.HashMap;
67 import java.util.List;
68 import java.util.Map;
69 import java.util.Optional;
70 import java.util.stream.Collectors;
71 import java.util.stream.Stream;
72
73 import jdk.internal.misc.PreviewFeatures;
74
75 import static org.junit.jupiter.api.Assertions.*;
76
77 /**
78 * Scans all classes in the JDK for those recognized as value classes
79 * or with the annotation jdk.internal.misc.ValueBasedClass.
80 *
81 * Scanning is done over the jrt: filesystem. Classes are matched using the
82 * following criteria:
83 *
84 * - serializable
85 * - is a public or protected class
86 * - has public or protected constructor
87 *
88 * This returns a list of class', which is convenient for the caller.
89 */
90
91 public class SerializeAllValueClasses {
92 // Cache of instances of known classes suitable as arguments to constructors
93 // or factory methods.
94 private static final Map<Class<?>, Object> argumentForType = initInstances();
95
96 private static Map<Class<?>, Object> initInstances() {
97 Map<Class<?>, Object> map = new HashMap<>();
98 map.put(Integer.class, 12); map.put(int.class, 12);
99 map.put(Short.class, (short)3); map.put(short.class, (short)3);
100 map.put(Byte.class, (byte)4); map.put(byte.class, (byte)4);
101 map.put(Long.class, 5L); map.put(long.class, 5L);
102 map.put(Character.class, 'C'); map.put(char.class, 'C');
103 map.put(Float.class, 1.0f); map.put(float.class, 1.0f);
104 map.put(Double.class, 2.0d); map.put(double.class, 2.0d);
105 map.put(Duration.class, Duration.ofHours(1));
106 map.put(TemporalUnit.class, ChronoUnit.SECONDS);
107 map.put(LocalTime.class, LocalTime.of(12, 1));
108 map.put(LocalDate.class, LocalDate.of(2024, 1, 1));
109 map.put(LocalDateTime.class, LocalDateTime.of(2024, 2, 1, 12, 2));
110 map.put(TemporalAccessor.class, ZonedDateTime.now());
111 map.put(ZonedDateTime.class, ZonedDateTime.now());
112 map.put(Clock.class, Clock.systemUTC());
113 map.put(Month.class, Month.JANUARY);
114 map.put(Instant.class, Instant.now());
115 map.put(JapaneseDate.class, JapaneseDate.now());
116 map.put(HijrahDate.class, HijrahDate.now());
117 return map;
118 }
119
120
121 // Stream the value classes to the test
122 private static Stream<Arguments> classProvider() throws IOException, URISyntaxException {
123 return findAll().stream().map(c -> Arguments.of(c));
124 }
125
126 @Test
127 void info() {
128 var info = (PreviewFeatures.isEnabled()) ? " Checking preview classes declared as `value class`" :
129 " Checking identity classes with annotation `jdk.internal.ValueBased.class`";
130 System.err.println(info);
131 }
132
133 @ParameterizedTest
134 @MethodSource("classProvider")
135 void testValueClass(Class<?> clazz) {
136 boolean atLeastOne = false;
137
138 Object expected = argumentForType.get(clazz);
139 if (expected != null) {
140 serializeDeserialize(expected);
141 atLeastOne = true;
142 }
143 var cons = clazz.getConstructors();
144 for (Constructor<?> c : cons) {
145 Object[] args = makeArgs(c.getParameterTypes(), clazz);
146 if (args != null) {
147 try {
148 expected = c.newInstance(args);
149 serializeDeserialize(expected);
150 atLeastOne = true;
151 break; // one is enough
152 } catch (InvocationTargetException | InstantiationException |
153 IllegalAccessException e) {
154 // Ignore
155 System.err.printf("""
156 Ignoring constructor: %s
157 Generated arguments are invalid: %s
158 %s
159 """,
160 c, Arrays.toString(args), e.getCause());
161 }
162 }
163 }
164
165 // Scan for suitable factory methods
166 for (Method m : clazz.getMethods()) {
167 if (Modifier.isStatic(m.getModifiers()) &&
168 m.getReturnType().equals(clazz)) {
169 // static method returning itself
170 Object[] args = makeArgs(m.getParameterTypes(), clazz);
171 if (args != null) {
172 try {
173 expected = m.invoke(null, args);
174 serializeDeserialize(expected);
175 atLeastOne = true;
176 break; // one is enough
177 } catch (IllegalAccessException | InvocationTargetException e) {
178 // Ignore
179 System.err.printf("""
180 Ignoring factory: %s
181 Generated arguments are invalid: %s
182 %s
183 """,
184 m, Arrays.toString(args), e.getCause());
185 }
186 }
187 }
188 }
189 assertTrue(atLeastOne, "No constructor or factory found for " + clazz);
190 }
191
192 /**
193 * {@return an array of instances matching the parameter types, or null}
194 *
195 * @param paramTypes an array of parameter types
196 * @param forClazz the owner class for which the parameters are being generated
197 */
198 private Object[] makeArgs(Class<?>[] paramTypes, Class<?> forClazz) {
199 Object[] args = Arrays.stream(paramTypes)
200 .map(t -> makeArg(t, forClazz))
201 .toArray();
202 for (Object arg : args) {
203 if (arg == null)
204 return null;
205 }
206 return args;
207 }
208
209 /**
210 * {@return an instance of the class, or null if not available}
211 * String values are customized by the requesting owner.
212 * For example, "true" is returned as a value when requested for "Boolean".
213 * @param paramType the parameter type
214 * @param forClass the owner class
215 */
216 private static Object makeArg(Class<?> paramType, Class<?> forClass) {
217 return (paramType == String.class || paramType == CharSequence.class)
218 ? makeStringArg(forClass)
219 : argumentForType.get(paramType);
220 }
221
222 /**
223 * {@return a string representation of an instance of class, or null}
224 * Mostly special cased for core value classes.
225 * @param forClass a Class
226 */
227 private static String makeStringArg(Class<?> forClass) {
228 if (forClass == Integer.class || forClass == int.class ||
229 forClass == Byte.class || forClass == byte.class ||
230 forClass == Short.class || forClass == short.class ||
231 forClass == Long.class || forClass == long.class) {
232 return "0";
233 } else if (forClass == Boolean.class || forClass == boolean.class) {
234 return "true";
235 } else if (forClass == Float.class || forClass == float.class ||
236 forClass == Double.class || forClass == double.class) {
237 return "1.0";
238 } else if (forClass == Duration.class) {
239 return "PT4H";
240 } else if (forClass == LocalDate.class) {
241 return LocalDate.of(2024, 1, 1).toString();
242 } else if (forClass == LocalDateTime.class) {
243 return LocalDateTime.of(2024, 1, 1, 12, 1).toString();
244 } else if (forClass == LocalTime.class) {
245 return LocalTime.of(12, 1).toString();
246 } else if (forClass == Instant.class) {
247 return Instant.ofEpochSecond(5_000_000, 1000).toString();
248 } else {
249 return null;
250 }
251 }
252
253 static final ClassLoader LOADER = SerializeAllValueClasses.class.getClassLoader();
254
255 private static Optional<Class<?>> findClass(String name) {
256 try {
257 Class<?> clazz = Class.forName(name, false, LOADER);
258 return Optional.of(clazz);
259 } catch (ClassNotFoundException | ExceptionInInitializerError |
260 NoClassDefFoundError | IllegalAccessError ex) {
261 return Optional.empty();
262 }
263 }
264
265 private static boolean isClass(Class<?> clazz) {
266 return !(clazz.isEnum() || clazz.isInterface());
267 }
268
269 private static boolean isNonAbstract(Class<?> clazz) {
270 return (clazz.getModifiers() & Modifier.ABSTRACT) == 0;
271 }
272
273 private static boolean isPublicOrProtected(Class<?> clazz) {
274 return (clazz.getModifiers() & (Modifier.PUBLIC | Modifier.PROTECTED)) != 0;
275 }
276
277 @SuppressWarnings("preview")
278 private static boolean isValueClass(Class<?> clazz) {
279 if (PreviewFeatures.isEnabled())
280 return clazz.isValue();
281 var a = clazz.getAnnotation(jdk.internal.ValueBased.class);
282 return a != null;
283 }
284
285 /**
286 * Scans classes in the JDK and returns matching classes.
287 *
288 * @return list of matching class
289 * @throws IOException if an unexpected exception occurs
290 * @throws URISyntaxException if an unexpected exception occurs
291 */
292 public static List<Class<?>> findAll() throws IOException, URISyntaxException {
293 FileSystem fs = FileSystems.getFileSystem(new URI("jrt:/"));
294 Path dir = fs.getPath("/modules");
295 try (final Stream<Path> paths = Files.walk(dir)) {
296 // each path is in the form: /modules/<modname>/<pkg>/<pkg>/.../name.class
297 return paths.filter((path) -> path.getNameCount() > 2)
298 .map((path) -> path.subpath(2, path.getNameCount()))
299 .map(Path::toString)
300 .filter((name) -> name.endsWith(".class"))
301 .map((name) -> name.replaceFirst("\\.class$", ""))
302 .filter((name) -> !name.equals("module-info"))
303 .map((name) -> name.replaceAll("/", "."))
304 .flatMap((java.lang.String name) -> findClass(name).stream())
305 .filter(Serializable.class::isAssignableFrom)
306 .filter(SerializeAllValueClasses::isClass)
307 .filter(SerializeAllValueClasses::isNonAbstract)
308 .filter((klass) -> !klass.isSealed())
309 .filter(SerializeAllValueClasses::isValueClass)
310 .filter(SerializeAllValueClasses::isPublicOrProtected)
311 .collect(Collectors.toList());
312 }
313 }
314
315 private void serializeDeserialize(Object expected) {
316 try {
317 Object actual = deserialize(serialize(expected));
318 assertEquals(expected, actual, "round trip compare fail");
319 } catch (IOException | ClassNotFoundException e) {
320 fail("serialize/Deserialize", e);
321 }
322 }
323
324 /**
325 * Serialize an object into byte array.
326 */
327 private static byte[] serialize(Object obj) throws IOException {
328 ByteArrayOutputStream bs = new ByteArrayOutputStream();
329 try (ObjectOutputStream out = new ObjectOutputStream(bs)) {
330 out.writeObject(obj);
331 }
332 return bs.toByteArray();
333 }
334
335 /**
336 * Deserialize an object from byte array using the requested classloader.
337 */
338 private static Object deserialize(byte[] ba) throws IOException, ClassNotFoundException {
339 try (ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(ba))) {
340 return in.readObject();
341 }
342 }
343
344 }