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 }