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 }