1 /*
   2  * Copyright (c) 2024, 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 import combo.ComboInstance;
  25 import combo.ComboParameter;
  26 import combo.ComboTask;
  27 import combo.ComboTestHelper;
  28 
  29 import java.io.ByteArrayInputStream;
  30 import java.io.ByteArrayOutputStream;
  31 import java.io.EOFException;
  32 import java.io.IOException;
  33 import java.io.InputStream;
  34 import java.io.InvalidClassException;
  35 import java.io.NotSerializableException;
  36 import java.io.ObjectInputStream;
  37 import java.io.ObjectOutputStream;
  38 import java.io.ObjectStreamClass;
  39 import java.io.OptionalDataException;
  40 import java.io.Reader;
  41 import java.io.UncheckedIOException;
  42 import java.net.URL;
  43 import java.net.URLClassLoader;
  44 import java.nio.file.Files;
  45 import java.nio.file.Path;
  46 import java.util.Arrays;
  47 import java.util.HashMap;
  48 import java.util.HashSet;
  49 import java.util.List;
  50 import java.util.Locale;
  51 import java.util.Map;
  52 import java.util.Optional;
  53 import java.util.Set;
  54 import java.util.concurrent.ConcurrentHashMap;
  55 import java.util.function.BiFunction;
  56 import java.util.function.Predicate;
  57 import java.util.stream.Collectors;
  58 import java.util.stream.Stream;
  59 
  60 import javax.tools.Diagnostic;
  61 import javax.tools.JavaFileObject;
  62 
  63 import static jdk.test.lib.Asserts.assertEquals;
  64 import static jdk.test.lib.Asserts.assertTrue;
  65 import static jdk.test.lib.Asserts.assertFalse;
  66 
  67 import jdk.test.lib.hexdump.HexPrinter;
  68 import jdk.test.lib.hexdump.ObjectStreamPrinter;
  69 
  70 /*
  71  * @test
  72  * @summary Deserialization Combo tests
  73  * @library /test/langtools/tools/javac/lib /test/lib .
  74  * @modules jdk.compiler/com.sun.tools.javac.api
  75  *          jdk.compiler/com.sun.tools.javac.code
  76  *          jdk.compiler/com.sun.tools.javac.comp
  77  *          jdk.compiler/com.sun.tools.javac.file
  78  *          jdk.compiler/com.sun.tools.javac.main
  79  *          jdk.compiler/com.sun.tools.javac.tree
  80  *          jdk.compiler/com.sun.tools.javac.util
  81  * @build combo.ComboTestHelper SerializedObjectCombo
  82  * @run main/othervm --enable-preview  SerializedObjectCombo
  83  */
  84 
  85 
  86 public final class SerializedObjectCombo extends ComboInstance<SerializedObjectCombo> {
  87     private static final Map<Path, URLClassLoader> LOADER_FOR_PATH = new ConcurrentHashMap<>();
  88     private static final ParamSet KIND_SET = new ParamSet("KIND",
  89             SerializationKind.values());
  90     private static final ParamSet FIELD_SET = new ParamSet("FIELD",
  91             2, ArgumentValue.BASIC_VALUES);
  92     private static final ParamSet CLASSACCESS_SET = new ParamSet("CLASSACCESS",
  93             new ClassAccessKind[]{ClassAccessKind.PUBLIC});
  94     private static final ParamSet SPECIAL_WRITE_METHODS_SET = new ParamSet("SPECIAL_WRITE_METHODS",
  95             WriteObjectFragments.values());
  96     private static final ParamSet SPECIAL_READ_METHODS_SET = new ParamSet("SPECIAL_READ_METHODS",
  97             ReadObjectFragments.values());
  98     private static final ParamSet EXTERNALIZABLE_METHODS_SET = new ParamSet("EXTERNALIZABLE_METHODS",
  99             ExternalizableMethodFragments.values());
 100     private static final ParamSet OBJECT_CONSTRUCTOR_SET = new ParamSet("OBJECT_CONSTRUCTOR",
 101             FactoryFragment.values());
 102     private static final ParamSet VALUE_SET = new ParamSet("VALUE",
 103             ValueKind.values());
 104     private static final ParamSet TESTNAME_EXTENDS_SET = new ParamSet("TESTNAME_EXTENDS",
 105                     TestNameExtendsFragments.NONE, TestNameExtendsFragments.TESTNAME_EXTENDS_FRAGMENT);
 106     private static final ParamSet TOP_ABSTRACT_SET = new ParamSet("TOP_FRAGMENTS",
 107             TopFragments.values());
 108     /**
 109      * The base template to generate all test classes.
 110      * Each substitutable fragment is defined by an Enum of the alternatives.
 111      * Giving each a name and an array of ComboParameters with the expansion value.
 112      */
 113     private static final String TEST_SOURCE_TEMPLATE = """
 114             import java.io.*;
 115             import java.util.*;
 116             import jdk.internal.value.Deserializer;
 117 
 118             #{TOP_FRAGMENTS}
 119 
 120             #{CLASSACCESS} #{VALUE} class #{TESTNAME} #{TESTNAME_EXTENDS} #{KIND.IMPLEMENTS} {
 121                 #{FIELD[0]} f1;
 122                 #{FIELD[1]} f2;
 123                 #{FIELD_ADDITIONS}
 124                 #{CLASSACCESS} #{TESTNAME}() {
 125                     f1 = #{FIELD[0].RANDOM};
 126                     f2 = #{FIELD[1].RANDOM};
 127                     #{FIELD_CONSTRUCTOR_ADDITIONS}
 128                 }
 129             #{OBJECT_CONSTRUCTOR}
 130                 @Override public boolean equals(Object obj) {
 131                     if (obj instanceof #{TESTNAME} other) {
 132                         if (#{FIELD[0]}.class.isPrimitive()) {
 133                             if (f1 != other.f1) return false;
 134                         } else {
 135                             if (!Objects.equals(f1, other.f1)) return false;
 136                         }
 137                         if (#{FIELD[1]}.class.isPrimitive()) {
 138                             if (f2 != other.f2) return false;
 139                         } else {
 140                             if (!Objects.equals(f2, other.f2)) return false;
 141                         }
 142                         return true;
 143                     }
 144                     return false;
 145                 }
 146                 @Override public String toString() {
 147                     return "f1: " + String.valueOf(f1) +
 148                             ", f2: " + String.valueOf(f2)
 149                             #{FIELD_TOSTRING_ADDITIONS};
 150                 }
 151             #{KIND.SPECIAL_METHODS}
 152                 private static final long serialVersionUID = 1L;
 153             }
 154             """;
 155 
 156     // The unique number to qualify interface names, unique across multiple runs
 157     private static int uniqueId = 0;
 158     // Compilation errors prevent execution; set/cleared by checkCompile
 159     private ComboTask.Result<?> compilationResult = null;
 160     // The current set of parameters for the file being compiled and tested
 161     private final Set<ComboParameter> currParams = new HashSet<>();
 162 
 163     private static List<String> focusKeys = null;
 164 
 165     private enum CommandOption {
 166         SHOW_SOURCE("--show-source", "show source files"),
 167         VERBOSE("--verbose", "show extra information"),
 168         SHOW_SERIAL_STREAM("--show-serial", "show and format the serialized stream"),
 169         EVERYTHING("--everything", "run all tests"),
 170         TRACE("--trace", "set TRACE system property of ObjectInputStream (temp)"),
 171         MAX_COMBOS("--max-combo", "maximum number of values for each parameter", CommandOption::parseInt),
 172         NO_PRE_FILTER("--no-pre-filter", "disable pre-filter checks"),
 173         SELFTEST("--self-test", "run some self tests and exit"),
 174         ;
 175         private final String option;
 176         private final String usage;
 177         private final BiFunction<CommandOption, String, Boolean> parseArg;
 178         private Optional<Object> value;
 179         CommandOption(String option, String usage, BiFunction<CommandOption, String, Boolean> parseArg) {
 180             this.option = option;
 181             this.usage = usage;
 182             this.parseArg = parseArg;
 183             this.value = Optional.empty();
 184         }
 185         CommandOption(String option, String usage) {
 186             this(option, usage, null);
 187         }
 188 
 189         /**
 190          * Evaluate and parse an array of command line args
 191          * @param args array of strings
 192          * @return true if parsing succeeded
 193          */
 194         static boolean parseOptions(String[] args) {
 195             boolean unknownArg = false;
 196             for (int i = 0; i < args.length; i++) {
 197                 String arg = args[i];
 198                 Optional<CommandOption> knownOpt = Arrays.stream(CommandOption.values())
 199                         .filter(o -> o.option.equals(arg))
 200                         .findFirst();
 201                 if (knownOpt.isEmpty()) { // Not a recognized option
 202                     if (arg.startsWith("-")) {
 203                         System.err.println("Unrecognized option: " + arg);
 204                         unknownArg = true;
 205                     } else {
 206                         // Take the remaining non-option args as selectors of keys to be run
 207                         String[] keys = Arrays.copyOfRange(args, i, args.length);
 208                         focusKeys = List.of(keys);
 209                     }
 210                 } else {
 211                     CommandOption option = knownOpt.get();
 212                     if (option.parseArg == null) {
 213                         option.setValue(true);
 214                     } else {
 215                         i++;
 216                         if (i >= args.length || args[i].startsWith("--")) {
 217                             System.err.println("Missing argument for " + option.option);
 218                             continue;
 219                         }
 220                         option.parseArg.apply(option, args[i]);
 221                     }
 222                 }
 223             }
 224             return !unknownArg;
 225         }
 226         static void showUsage() {
 227             System.out.println("""
 228                 Usage:
 229                 """);
 230             Arrays.stream(CommandOption.values()).forEach(o -> System.out.printf("  %-15s: %s\n", o.option, o.usage));
 231         }
 232         boolean present() {
 233             return value != null && value.isPresent();
 234         }
 235         void setValue(Object o) {
 236             value = Optional.ofNullable(o);
 237         }
 238         private static boolean parseInt(CommandOption option, String arg) {
 239             try {
 240                 int count = Integer.parseInt(arg);
 241                 option.setValue(count);
 242             } catch (NumberFormatException nfe) {
 243                 System.out.println("--max-combo argument not a number: " + arg);
 244             }
 245             return true;
 246         }
 247         // Get the int value from the option, defaulting if not valid or present
 248         private int getInt(int otherMax) {
 249             Object obj = value == null ? otherMax : value.orElseGet(() -> otherMax);
 250             return (obj instanceof Integer i) ? i : otherMax;
 251         }
 252     }
 253 
 254     private static URLClassLoader getLoaderFor(Path path) {
 255         return LOADER_FOR_PATH.computeIfAbsent(path,
 256                 p -> {
 257                     try {
 258                         // new URLClassLoader for path
 259                         Files.createDirectories(p);
 260                         URL[] urls = {p.toUri().toURL()};
 261                         return new URLClassLoader(p.toString(), urls, null);
 262                     } catch (IOException ioe) {
 263                         throw new UncheckedIOException(ioe);
 264                     }
 265                 });
 266     }
 267 
 268     // Map an array of strings to an array of ComboParameter.Constants.
 269     @SuppressWarnings("unchecked")
 270     private static ComboParameter.Constant<String>[] paramsForStrings(String... strings) {
 271         return Arrays.stream(strings)
 272                 .map(ComboParameter.Constant::new).toArray(ComboParameter.Constant[]::new);
 273     }
 274 
 275     /**
 276      * Main to generate combinations and run the tests.
 277      *
 278      * @param args may contain "--verbose" to show source of every file
 279      * @throws Exception In case of failure
 280      */
 281     public static void main(String... args) throws Exception {
 282         if (!CommandOption.parseOptions(args)) {
 283             CommandOption.showUsage();
 284             System.exit(1);
 285         }
 286 
 287         Arrays.stream(CommandOption.values())
 288                 .filter(o -> o.present())
 289                 .forEach( o1 -> System.out.printf("   %15s: %s\n", o1.option, o1.value
 290                 ));
 291 
 292         if (CommandOption.SELFTEST.present()) {
 293             selftest();
 294             return;
 295         }
 296 
 297         // Sets of all possible ComboParameters (substitutions)
 298         Set<ParamSet> allParams = Set.of(
 299                 VALUE_SET,
 300                 KIND_SET,
 301                 TOP_ABSTRACT_SET,
 302                 OBJECT_CONSTRUCTOR_SET,
 303                 TESTNAME_EXTENDS_SET,
 304                 CLASSACCESS_SET,
 305                 SPECIAL_READ_METHODS_SET,
 306                 SPECIAL_WRITE_METHODS_SET,
 307                 EXTERNALIZABLE_METHODS_SET,
 308                 FIELD_SET
 309         );
 310 
 311         // Test variations of all code shapes
 312         var helper = new ComboTestHelper<SerializedObjectCombo>();
 313         int maxCombos = CommandOption.MAX_COMBOS.getInt(2);
 314 
 315         Set<ParamSet> subSet = CommandOption.EVERYTHING.present() ? allParams
 316                 : computeSubset(allParams, focusKeys, maxCombos);
 317         withDimensions(helper, subSet);
 318         if (CommandOption.VERBOSE.present()) {
 319             System.out.println("Keys; maximum combinations: " + maxCombos);
 320             subSet.stream()
 321                     .sorted((p, q) -> String.CASE_INSENSITIVE_ORDER.compare(p.key(), q.key()))
 322                     .forEach(p -> System.out.println("    " + p.key + ": " + Arrays.toString(p.params)));
 323         }
 324         helper.withFilter(SerializedObjectCombo::filter)
 325                 .withFailMode(ComboTestHelper.FailMode.FAIL_FAST)
 326                 .run(SerializedObjectCombo::new);
 327     }
 328 
 329     private static void withDimensions(ComboTestHelper<SerializedObjectCombo> helper, Set<ParamSet> subSet) {
 330         subSet.forEach(p -> {
 331             if (p.count() == 1)
 332                 helper.withDimension(p.key(), SerializedObjectCombo::saveParameter, p.params());
 333             else
 334                 helper.withArrayDimension(p.key(), SerializedObjectCombo::saveParameter, p.count(), p.params());
 335         });
 336     }
 337 
 338     // Return a subset of ParamSets with the non-focused ParamSet's truncated to a max number of values
 339     private static Set<ParamSet> computeSubset(Set<ParamSet> allParams, List<String> focusKeys, int maxKeys) {
 340         if (focusKeys == null || focusKeys.isEmpty())
 341             return allParams;
 342         Set<ParamSet> r = allParams.stream().map(p ->
 343                         (focusKeys.contains(p.key())) ? p
 344                                 : new ParamSet(p.key, p.count(), Arrays.copyOfRange(p.params(), 0, Math.min(p.params().length, maxKeys))))
 345                 .collect(Collectors.toUnmodifiableSet());
 346         return r;
 347     }
 348 
 349     /**
 350      * Print the source files to System out
 351      *
 352      * @param task the compilation task
 353      */
 354     static void showSources(ComboTask task) {
 355         task.getSources()
 356                 .forEach(fo -> {
 357                     System.out.println("Source: " + fo.getName());
 358                     System.out.println(getSource(fo));
 359                 });
 360     }
 361 
 362     /**
 363      * Return the contents of the source file
 364      *
 365      * @param fo a file object
 366      * @return the contents of the source file
 367      */
 368     static String getSource(JavaFileObject fo) {
 369         try (Reader reader = fo.openReader(true)) {
 370             char[] buf = new char[100000];
 371             var len = reader.read(buf);
 372             return new String(buf, 0, len);
 373         } catch (IOException ioe) {
 374             return "IOException: " + fo.getName() + ", ex: " + ioe.getMessage();
 375         }
 376     }
 377 
 378     /**
 379      * Dump the serial stream.
 380      *
 381      * @param bytes the bytes of the stream
 382      */
 383     private static void showSerialStream(byte[] bytes) {
 384         HexPrinter.simple().dest(System.out).formatter(ObjectStreamPrinter.formatter()).format(bytes);
 385     }
 386 
 387     /**
 388      * Serialize an object into byte array.
 389      */
 390     private static byte[] serialize(Object obj) throws IOException {
 391         ByteArrayOutputStream bs = new ByteArrayOutputStream();
 392         try (ObjectOutputStream out = new ObjectOutputStream(bs)) {
 393             out.writeObject(obj);
 394         }
 395         return bs.toByteArray();
 396     }
 397 
 398     /**
 399      * Deserialize an object from byte array using the requested classloader.
 400      */
 401     private static Object deserialize(byte[] ba, ClassLoader loader) throws IOException, ClassNotFoundException {
 402         try (ObjectInputStream in = new LoaderObjectInputStream(new ByteArrayInputStream(ba), loader)) {
 403             return in.readObject();
 404         }
 405     }
 406 
 407 
 408     @Override
 409     public int id() {
 410         return ++uniqueId;
 411     }
 412 
 413     private void fail(String msg, Throwable thrown) {
 414         super.fail(msg);
 415         thrown.printStackTrace(System.out);
 416     }
 417 
 418     /**
 419      * Save a parameter.
 420      *
 421      * @param param a ComboParameter
 422      */
 423     private void saveParameter(ComboParameter param) {
 424         saveParameter(param, 0);
 425     }
 426 
 427     /**
 428      * Save an indexed parameter.
 429      *
 430      * @param param a ComboParameter
 431      * @param index unused
 432      */
 433     private void saveParameter(ComboParameter param, int index) {
 434         currParams.add(param);
 435     }
 436 
 437     /**
 438      * Filter out needless tests (mostly with more variations of arguments than needed).
 439      * Usually, these are compile time failures, or code shapes that cannot succeed.
 440      *
 441      * @return true to run the test, false if not
 442      */
 443     boolean filter() {
 444         if (!CommandOption.NO_PRE_FILTER.present()) {
 445             for (CodeShape shape : CodeShape.values()) {
 446                 if (shape.test(currParams)) {
 447                     if (CommandOption.VERBOSE.present()) {
 448                         System.out.println("IGNORING: " + shape);
 449                     }
 450                     return false;
 451                 }
 452             }
 453         }
 454         if (CommandOption.VERBOSE.present()) {
 455             System.out.println("TESTING: ");
 456             showParams();
 457         }
 458         return true;
 459     }
 460 
 461     /**
 462      * Generate the source files from the parameters and test a single combination.
 463      * Two versions are compiled into different directories and separate class loaders.
 464      * They differ only with the addition of a field to the generated class.
 465      * Then each class is serialized and deserialized by the other class,
 466      * testing simple evolution in the process.
 467      *
 468      * @throws IOException catch all IOException
 469      */
 470     @Override
 471     public void doWork() throws IOException {
 472         String cp = System.getProperty("test.classes");
 473         String className = "Class_" + this.id();
 474 
 475         // new URLClassLoader for path
 476         final Path firstPath = Path.of(cp, "1st");
 477         URLClassLoader firstLoader = getLoaderFor(firstPath);
 478         final Path secondPath = Path.of(cp, "2nd");
 479         URLClassLoader secondLoader = getLoaderFor(secondPath);
 480 
 481         // Create a map of additional constants that are resolved without the combo overhead.
 482         final Map<String, ComboParameter.Constant<String>> params = new HashMap<>();
 483         params.put("TESTNAME", new ComboParameter.Constant<>(className));
 484         params.put("SPECIAL_METHODS_SERIALIZABLE", new ComboParameter.Constant<>("#{SPECIAL_READ_METHODS} #{SPECIAL_WRITE_METHODS}"));
 485         params.put("SPECIAL_METHODS_EXTERNALIZABLE", new ComboParameter.Constant<>("#{EXTERNALIZABLE_METHODS}"));
 486         params.put("FIELD_ADDITIONS", new ComboParameter.Constant<>(""));
 487         params.put("FIELD_CONSTRUCTOR_ADDITIONS", new ComboParameter.Constant<>(""));
 488         params.put("FIELD_TOSTRING_ADDITIONS", new ComboParameter.Constant<>(""));
 489 
 490         final ComboTask firstTask = generateAndCompile(firstPath, className, params);
 491 
 492         if (firstTask == null) {
 493             return; // Skip execution, errors already reported
 494         }
 495 
 496         if (CommandOption.EVERYTHING.present()) {
 497             params.put("FIELD_ADDITIONS", new ComboParameter.Constant<>("int fExtra;"));
 498             params.put("FIELD_CONSTRUCTOR_ADDITIONS", new ComboParameter.Constant<>("this.fExtra = 99;"));
 499             params.put("FIELD_TOSTRING_ADDITIONS", new ComboParameter.Constant<>("+ \", fExtra: String.valueOf(fExtra)\""));
 500             final ComboTask secondTask = generateAndCompile(secondPath, className, params);
 501             if (secondTask == null) {
 502                 return; // Skip execution, errors already reported
 503             }
 504 
 505             doTestWork(className, firstTask, firstLoader, secondLoader);
 506             doTestWork(className, secondTask, secondLoader, firstLoader);
 507         } else {
 508             doTestWork(className, firstTask, firstLoader, firstLoader);
 509         }
 510     }
 511 
 512     /**
 513      * Test that two versions of the class can be serialized using one version and deserialized
 514      * by the other version.
 515      * The two classes have the same name and have been compiled into different classloaders.
 516      * The original and result objects are compared using .equals if there is only 1 classloader.
 517      * If the classloaders are different the `toString()` output for each object is compared loosely.
 518      * (One must be the prefix of the other)
 519      *
 520      * @param className    the class name
 521      * @param task         the task context (for source and parameters to report failures)
 522      * @param firstLoader  the first classloader
 523      * @param secondLoader the second classloader
 524      */
 525     private void doTestWork(String className, ComboTask task, ClassLoader firstLoader, ClassLoader secondLoader) {
 526         byte[] bytes = null;
 527         try {
 528             Class<?> tc = Class.forName(className, true, firstLoader);
 529             Object testObj = tc.getDeclaredConstructor().newInstance();
 530             bytes = serialize(testObj);
 531             if (CommandOption.VERBOSE.present()) {
 532                 System.out.println("Testing: " + task.getSources());
 533                 if (CommandOption.SHOW_SOURCE.present()) {
 534                     showParams();
 535                     showSources(task);
 536                 }
 537                 if (CommandOption.SHOW_SERIAL_STREAM.present()) {
 538                     showSerialStream(bytes);
 539                 }
 540             }
 541 
 542             if (CodeShape.BAD_SO_CONSTRUCTOR.test(currParams)) {
 543                 // should have thrown ICE due to mismatch between value class and missing constructor
 544                 System.out.println(CodeShape.BAD_SO_CONSTRUCTOR.explain(currParams));
 545                 fail(CodeShape.BAD_SO_CONSTRUCTOR.explain(currParams));
 546             }
 547 
 548             Object actual = deserialize(bytes, secondLoader);
 549             if (testObj.getClass().getClassLoader().equals(actual.getClass().getClassLoader())) {
 550                 assertEquals(testObj, actual, "Round-trip comparison fail using .equals");
 551             } else {
 552                 // The instances are from different classloaders and can't be compared directly
 553                 final String s1 = testObj.toString();
 554                 final String s2 = actual.toString();
 555                 assertTrue(s1.startsWith(s2) || s2.startsWith(s1),
 556                         "Round-trip comparison fail using toString(): s1: " + s1 + ", s2: " + s2);
 557             }
 558         } catch (InvalidClassException ice) {
 559             for (CodeShape shape : CodeShape.values()){
 560                 if (ice.equals(shape.exception)) {
 561                     if (shape.test(currParams)) {
 562                         if (CommandOption.VERBOSE.present()) {
 563                             System.out.println("OK: " + shape.explain(currParams));
 564                         } else {
 565                             // unexpected ICE
 566                             ice.printStackTrace(System.out);
 567                             showParams();
 568                             showSources(task);
 569                             if (bytes != null)
 570                                 showSerialStream(bytes);
 571                             fail(ice.getMessage());
 572                         }
 573                     }
 574                 }
 575             }
 576         } catch (EOFException | OptionalDataException eof) {
 577             // Ignore if conditions of the source invite EOF
 578             if (0 == CodeShape.shapesThrowing(EOFException.class).peek(s -> {
 579                 // Ignore: Serialized Object to reads custom data but none written
 580                 if (CommandOption.VERBOSE.present()) {
 581                     System.out.println("OK: " + s.explain(currParams));
 582                 }
 583             }).count()) {
 584                 eof.printStackTrace(System.out);
 585                 showParams();
 586                 showSources(task);
 587                 showSerialStream(bytes);
 588                 fail(eof.getMessage(), eof);
 589             }
 590         } catch (ClassFormatError cfe) {
 591             System.out.println(cfe.toString());
 592         } catch (NotSerializableException nse) {
 593             if (CodeShape.BAD_EXT_VALUE.test(currParams)) {
 594                 // Expected Value class that is Externalizable w/o writeReplace
 595             } else {
 596                 // unexpected NSE
 597                 nse.printStackTrace(System.out);
 598                 showParams();
 599                 showSources(task);
 600                 fail(nse.getMessage(), nse);
 601             }
 602         } catch (Throwable ex) {
 603             ex.printStackTrace(System.out);
 604             showParams();
 605             showSources(task);
 606             fail(ex.getMessage());
 607         }
 608     }
 609 
 610     // Side effect of error is compilationResult.hasErrors() > 0
 611     private ComboTask generateAndCompile(Path path, String className, Map<String, ComboParameter.Constant<String>> params) {
 612         ComboTask task = newCompilationTask()
 613                 .withSourceFromTemplate(className,
 614                         TEST_SOURCE_TEMPLATE,
 615                         r -> params.computeIfAbsent(r, s -> new ComboParameter.Constant<>("UNKNOWN_" + s)))
 616                 .withOption("-d")
 617                 .withOption(path.toString())
 618                 .withOption("--enable-preview")
 619                 .withOption("--add-modules")
 620                 .withOption("java.base")
 621                 .withOption("--add-exports")
 622                 .withOption("java.base/jdk.internal=ALL-UNNAMED")
 623                 .withOption("--add-exports")
 624                 .withOption("java.base/jdk.internal.value=ALL-UNNAMED")
 625                 .withOption("--source")
 626                 .withOption(Integer.toString(Runtime.version().feature()));
 627         ;
 628         task.generate(this::checkCompile);
 629         if (compilationResult.hasErrors()) {
 630             boolean match = false;
 631             for (CodeShape shape : CodeShape.values()){
 632                 if (CompileException.class.equals(shape.exception)) {
 633                     if (shape.test(currParams)) {
 634                         // shape matches known error
 635                         if (!uniqueParams.contains(shape))  {
 636                             System.out.println("// Unique: " + shape);
 637                             uniqueParams.add(shape);
 638                         }
 639                         match = true;
 640                     }
 641                 }
 642             }
 643             if (match)
 644                 return null;
 645             // Unexpected compilation error
 646             showDiags(compilationResult);
 647             showSources(task);
 648             showParams();
 649             fail("Compilation failure");
 650         }
 651         return task;
 652     }
 653 
 654     private static Set<CodeShape> uniqueParams = new HashSet<>();
 655 
 656     private String paramToString(ComboParameter param) {
 657         String name = param.getClass().getName();
 658         return name.substring(name.indexOf('$') + 1) + "::" +
 659                 param + ": " + truncate(param.expand(null), 60);
 660     }
 661 
 662     private void showParams() {
 663         currParams.stream()
 664                 .sorted((p, q) -> String.CASE_INSENSITIVE_ORDER.compare(paramToString(p), paramToString(q)))
 665                 .forEach(p -> System.out.println("    " + paramToString(p)));
 666     }
 667 
 668     private void showParams(ComboParameter... params) {
 669         for (ComboParameter param : params) {
 670             System.out.println(">>> " + paramToString(param) + ", present: "
 671                     + currParams.contains(param));
 672         }
 673     }
 674 
 675     private static String truncate(String s, int maxLen) {
 676         int nl = s.indexOf("\n");
 677         if (nl >= 0)
 678             maxLen = nl;
 679         if (maxLen < s.length()) {
 680             return s.substring(0, maxLen).concat("...");
 681         } else {
 682             return s;
 683         }
 684     }
 685 
 686     /**
 687      * Report any compilation errors.
 688      *
 689      * @param res the result
 690      */
 691     void checkCompile(ComboTask.Result<?> res) {
 692         compilationResult = res;
 693     }
 694 
 695     void showDiags(ComboTask.Result<?> res) {
 696         res.diagnosticsForKind(Diagnostic.Kind.ERROR).forEach(SerializedObjectCombo::showDiag);
 697         res.diagnosticsForKind(Diagnostic.Kind.WARNING).forEach(SerializedObjectCombo::showDiag);
 698     }
 699 
 700     static void showDiag(Diagnostic<? extends JavaFileObject> diag) {
 701         System.out.println(diag.getKind() + ": " + diag.getMessage(Locale.ROOT));
 702         System.out.println("File: " + diag.getSource() +
 703                 " line: " + diag.getLineNumber() + ", col: " + diag.getColumnNumber());
 704     }
 705 
 706     private static class CodeShapePredicateOp<T> implements Predicate<T> {
 707         private final Predicate<T> first;
 708         private final Predicate<T> other;
 709         private final String op;
 710 
 711         CodeShapePredicateOp(Predicate<T> first, Predicate<T> other, String op) {
 712             if ("OR" != op && "AND" != op && "NOT" != op)
 713                 throw new IllegalArgumentException("unknown op: " + op);
 714             this.first = first;
 715             this.other = other;
 716             this.op = op;
 717         }
 718 
 719         @Override
 720         public boolean test(T comboParameters) {
 721             return switch (op) {
 722                 case "NOT" -> !first.test(comboParameters);
 723                 case "OR" -> first.test(comboParameters) || other.test(comboParameters);
 724                 case "AND" -> first.test(comboParameters) && other.test(comboParameters);
 725                 default -> throw new IllegalArgumentException("unknown op: " + op);
 726             };
 727         }
 728         @Override
 729         public Predicate<T> and(Predicate<? super T> other) {
 730             return new CodeShapePredicateOp(this, other,"AND");
 731         }
 732 
 733 
 734         @Override
 735         public Predicate<T>  negate() {
 736             return new CodeShapePredicateOp(this, null,"NOT");
 737         }
 738 
 739         @Override
 740         public Predicate<T> or(Predicate<? super T> other) {
 741             return new CodeShapePredicateOp(this, other,"OR");
 742         }
 743         public String toString() {
 744             return switch (op) {
 745                 case "NOT" -> op + " " + first;
 746                 case "OR" -> "(" + first + " " + op + " " + other + ")";
 747                 case "AND" -> "(" + first + " " + op + " " + other + ")";
 748                 default -> throw new IllegalArgumentException("unknown op: " + op);
 749             };
 750         }
 751     }
 752 
 753     interface CodeShapePredicate extends Predicate<Set<ComboParameter>> {
 754         @Override
 755         default boolean test(Set<ComboParameter> comboParameters) {
 756             return comboParameters.contains(this);
 757         }
 758 
 759         @Override
 760         default Predicate<Set<ComboParameter>> and(Predicate<? super Set<ComboParameter>> other) {
 761             return new CodeShapePredicateOp(this, other,"AND");
 762         }
 763 
 764 
 765         @Override
 766         default Predicate<Set<ComboParameter>>  negate() {
 767             return new CodeShapePredicateOp(this, null,"NOT");
 768         }
 769 
 770         @Override
 771         default Predicate<Set<ComboParameter>> or(Predicate<? super Set<ComboParameter>> other) {
 772             return new CodeShapePredicateOp(this, other,"OR");
 773         }
 774     }
 775 
 776     /**
 777      * A set of code shapes that are interesting, usually indicating an error
 778      * compile time, or runtime based on the shape of the code and the dependencies between
 779      * the code fragments.
 780      * The descriptive text may be easier to understand than the boolean expression of the fragments.
 781      * They can also be to filter out test cases that would not succeed.
 782      * Or can be used after a successful deserialization to check
 783      * if an exception should have been thrown.
 784      */
 785     private enum CodeShape implements Predicate<Set<ComboParameter>> {
 786         BAD_SO_CONSTRUCTOR("Value class does not have a constructor annotated with Deserializer",
 787                 InvalidClassException.class,
 788                 ValueKind.VALUE,
 789                 FactoryFragment.NONE
 790                 ),
 791         BAD_EXT_VALUE("Externalizable can not be a value class",
 792                 CompileException.class,
 793                 SerializationKind.EXTERNALIZABLE,
 794                 ValueKind.VALUE),
 795         BAD_EXT_METHODS("Externalizable methods but not Externalizable",
 796                 CompileException.class,
 797                 ExternalizableMethodFragments.EXTERNALIZABLE_METHODS,
 798                 SerializationKind.EXTERNALIZABLE.negate()),
 799         BAD_EXT_NO_METHODS("Externalizable but no implementation of readExternal or writeExternal",
 800                 CompileException.class,
 801                 SerializationKind.EXTERNALIZABLE,
 802                 ExternalizableMethodFragments.EXTERNALIZABLE_METHODS.negate()),
 803         BAD_VALUE_NON_ABSTRACT_SUPER("Can't inherit from non-abstract super or abstract super with fields",
 804                 CompileException.class,
 805                 ValueKind.VALUE,
 806                 TestNameExtendsFragments.TESTNAME_EXTENDS_FRAGMENT,
 807                 TopFragments.ABSTRACT_NO_FIELDS.negate()),
 808         BAD_MISSING_SUPER("Extends TOP_ without TOP_ superclass",
 809                 CompileException.class,
 810                 TestNameExtendsFragments.TESTNAME_EXTENDS_FRAGMENT,
 811                 TopFragments.NONE),
 812         BAD_READ_CUSTOM_METHODS("Custom read fragment but no custom write fragment",
 813                 EOFException.class,
 814                 ReadObjectFragments.READ_OBJECT_FIELDS_CUSTOM_FRAGMENT
 815                         .or(ReadObjectFragments.READ_OBJECT_DEFAULT_CUSTOM_FRAGMENT),
 816                 WriteObjectFragments.WRITE_OBJECT_FIELDS_CUSTOM_FRAGMENT
 817                         .or(WriteObjectFragments.WRITE_OBJECT_DEFAULT_CUSTOM_FRAGMENT).negate()
 818                 ),
 819         BAD_RW_CUSTOM_METHODS("Custom write fragment but no custom read fragment",
 820                 null,
 821                 WriteObjectFragments.WRITE_OBJECT_FIELDS_CUSTOM_FRAGMENT
 822                         .or(WriteObjectFragments.WRITE_OBJECT_DEFAULT_CUSTOM_FRAGMENT),
 823                 ReadObjectFragments.READ_OBJECT_FIELDS_CUSTOM_FRAGMENT
 824                         .or(ReadObjectFragments.READ_OBJECT_DEFAULT_CUSTOM_FRAGMENT).negate()),
 825         BAD_VALUE_READOBJECT_METHODS("readObjectXXX(OIS) methods incompatible with Value class",
 826                 CompileException.class,
 827                 ReadObjectFragments.READ_OBJECT_FIELDS_FRAGMENT
 828                         .or(ReadObjectFragments.READ_OBJECT_DEFAULT_FRAGMENT)
 829                         .or(ReadObjectFragments.READ_OBJECT_FIELDS_CUSTOM_FRAGMENT)
 830                         .or(ReadObjectFragments.READ_OBJECT_DEFAULT_CUSTOM_FRAGMENT),
 831                 ValueKind.VALUE),
 832         ;
 833 
 834         private final String description;
 835         private final Class<? extends Exception> exception;
 836         private final List<Predicate<Set<ComboParameter>>> predicates;
 837         CodeShape(String desc, Class<? extends Exception> exception, Predicate<Set<ComboParameter>>... predicates) {
 838             this.description = desc;
 839             this.exception = exception;
 840             this.predicates = List.of(predicates);
 841         }
 842 
 843         // Return a stream of CodeShapes throwing the exception
 844         static Stream<CodeShape> shapesThrowing(Class<?> exception) {
 845             return Arrays.stream(values()).filter(s -> exception.equals(s.exception));
 846 
 847         }
 848 
 849         /**
 850          * {@return true if all of the predicates are true in the set of ComboParameters}
 851          * @param comboParameters a set of ComboParameters
 852          */
 853         @Override
 854         public boolean test(Set<ComboParameter> comboParameters) {
 855             for (Predicate<Set<ComboParameter>> p : predicates) {
 856                 if (!p.test(comboParameters))
 857                     return false;
 858             }
 859             return true;
 860         }
 861 
 862         /**
 863          * {@return a string describing the predicate in relation to a set of parameters}
 864          * @param comboParameters a set of active ComboParameters.
 865          */
 866         public String explain(Set<ComboParameter> comboParameters) {
 867             StringBuffer sbTrue = new StringBuffer();
 868             StringBuffer sbFalse = new StringBuffer();
 869             for (Predicate<Set<ComboParameter>> p : predicates) {
 870                 ((p.test(comboParameters)) ? sbTrue : sbFalse)
 871                         .append(p).append(", ");
 872             }
 873             return description + "\n" +"Missing: " + sbFalse + "\nTrue: " + sbTrue;
 874         }
 875         public String toString() {
 876             return super.toString() + "::" + description + ", params: " + predicates;
 877         }
 878     }
 879 
 880     /**
 881      * TopAbstract Fragments
 882      */
 883     enum TopFragments implements ComboParameter, CodeShapePredicate {
 884         NONE(""),
 885         ABSTRACT_NO_FIELDS("""
 886                 abstract #{VALUE} class TOP_#{TESTNAME} implements Serializable {
 887                     #{CLASSACCESS} TOP_#{TESTNAME}() {}
 888                 }
 889                 """),
 890         ABSTRACT_ONE_FIELD("""
 891                 abstract #{VALUE} class TOP_#{TESTNAME} implements Serializable {
 892                     private int t1;
 893                     #{CLASSACCESS} TOP_#{TESTNAME}() {
 894                         t1 = 1;
 895                     }
 896                 }
 897                 """),
 898         NO_FIELDS("""
 899                 #{VALUE} class TOP_#{TESTNAME} implements Serializable {
 900                     #{CLASSACCESS} TOP_#{TESTNAME}() {}
 901                 }
 902                 """),
 903         ONE_FIELD("""
 904                 #{VALUE} class TOP_#{TESTNAME} implements Serializable {
 905                     private int t1;
 906                     #{CLASSACCESS} TOP_#{TESTNAME}() {
 907                         t1 = 1;
 908                     }
 909                 }
 910                 """),
 911         ;
 912 
 913         private final String template;
 914 
 915         TopFragments(String template) {
 916             this.template = template;
 917         }
 918 
 919         @Override
 920         public String expand(String optParameter) {
 921             return template;
 922         }
 923     }
 924 
 925     /**
 926      * TopAbstract Fragments
 927      */
 928     enum TestNameExtendsFragments implements ComboParameter, CodeShapePredicate {
 929         NONE(""),
 930         TESTNAME_EXTENDS_FRAGMENT("extends TOP_#{TESTNAME}"),
 931         ;
 932 
 933         private final String template;
 934 
 935         TestNameExtendsFragments(String template) {
 936             this.template = template;
 937         }
 938 
 939         @Override
 940         public String expand(String optParameter) {
 941             return template;
 942         }
 943     }
 944 
 945     /**
 946      * SerializedObjectCustom Fragments
 947      */
 948     enum SerializedObjectCustomFragments implements ComboParameter, CodeShapePredicate {
 949         NONE(""),
 950         ;
 951 
 952         private final String template;
 953 
 954         SerializedObjectCustomFragments(String template) {
 955             this.template = template;
 956         }
 957 
 958         @Override
 959         public String expand(String optParameter) {
 960             return template;
 961         }
 962     }
 963 
 964     /**
 965      * ExternalizableMethod Fragments
 966      */
 967     enum ExternalizableMethodFragments implements ComboParameter, CodeShapePredicate {
 968         NONE(""),
 969         EXTERNALIZABLE_METHODS("""
 970                     public void writeExternal(ObjectOutput oos) throws IOException {
 971                         oos.write#{FIELD[0].READFIELD}(f1);
 972                         oos.write#{FIELD[1].READFIELD}(f2);
 973                     }
 974 
 975                     public void readExternal(ObjectInput ois) throws IOException, ClassNotFoundException {
 976                         f1 = (#{FIELD[0]})ois.read#{FIELD[0].READFIELD}();
 977                         f2 = (#{FIELD[1]})ois.read#{FIELD[1].READFIELD}();
 978                     }
 979                 """),
 980         ;
 981 
 982         private final String template;
 983 
 984         ExternalizableMethodFragments(String template) {
 985             this.template = template;
 986         }
 987 
 988         @Override
 989         public String expand(String optParameter) {
 990             return template;
 991         }
 992     }
 993 
 994     /**
 995      * FactoryFragment Fragments
 996      */
 997     enum FactoryFragment implements ComboParameter, CodeShapePredicate {
 998         NONE(""),
 999         ANNOTATED_OBJECT_CONSTRUCTOR_FRAGMENT("""
1000                     @Deserializer({"f1", "f2"})
1001                     #{CLASSACCESS} #{TESTNAME}(#{FIELD[0]} f1, #{FIELD[1]} f2) {
1002                         this.f1 = f1;
1003                         this.f2 = f2;
1004                         #{FIELD_CONSTRUCTOR_ADDITIONS}
1005                     }
1006 
1007                     @Deserializer({"f1", "f2"})
1008                     #{CLASSACCESS} #{TESTNAME}(#{FIELD[0]} f1, #{FIELD[1]} f2, int fExtra) {
1009                         this.f1 = f1;
1010                         this.f2 = f2;
1011                         #{FIELD_CONSTRUCTOR_ADDITIONS}
1012                     }
1013                 """),
1014         ANNOTATED_FACTORY_METHOD_FRAGMENT("""
1015                     private #{TESTNAME}(#{FIELD[0]} f1, #{FIELD[1]} f2) {
1016                         this.f1 = f1;
1017                         this.f2 = f2;
1018                         #{FIELD_CONSTRUCTOR_ADDITIONS}
1019                     }
1020 
1021                     @Deserializer({"f1", "f2"})
1022                     #{CLASSACCESS} static #{TESTNAME} create#{TESTNAME}(#{FIELD[0]} f1, #{FIELD[1]} f2) {
1023                         return new #{TESTNAME}(f1, f2);
1024                     }
1025 
1026                     @Deserializer({"f1", "f2"})
1027                     #{CLASSACCESS} static #{TESTNAME} create#{TESTNAME}(#{FIELD[0]} f1, #{FIELD[1]} f2, int fExtra) {
1028                         return new #{TESTNAME}(f1, f2);
1029                     }
1030                 """),
1031 
1032         ;
1033 
1034         private final String template;
1035 
1036         FactoryFragment(String template) {
1037             this.template = template;
1038         }
1039 
1040         @Override
1041         public String expand(String optParameter) {
1042             return template;
1043         }
1044     }
1045 
1046     /**
1047      * WriteObject templates
1048      */
1049     enum WriteObjectFragments implements ComboParameter, CodeShapePredicate {
1050         NONE(""),
1051         WRITE_OBJECT_DEFAULT_FRAGMENT("""
1052                     private void writeObject(ObjectOutputStream oos) throws IOException {
1053                         oos.defaultWriteObject();
1054                     }
1055                 """),
1056         WRITE_OBJECT_FIELDS_FRAGMENT("""
1057                     private void writeObject(ObjectOutputStream oos) throws IOException {
1058                         ObjectOutputStream.PutField fields = oos.putFields();
1059                         fields.put("f1", f1);
1060                         fields.put("f2", f2);
1061                         oos.writeFields();
1062                     }
1063                 """),
1064         WRITE_OBJECT_DEFAULT_CUSTOM_FRAGMENT("""
1065                     private void writeObject(ObjectOutputStream oos) throws IOException {
1066                         oos.defaultWriteObject();
1067                         // Write custom data
1068                         oos.write#{FIELD[0].READFIELD}(#{FIELD[0].DEFAULT});
1069                         oos.write#{FIELD[1].READFIELD}(#{FIELD[1].DEFAULT});
1070                     }
1071                 """),
1072         WRITE_OBJECT_FIELDS_CUSTOM_FRAGMENT("""
1073                     private void writeObject(ObjectOutputStream oos) throws IOException {
1074                         ObjectOutputStream.PutField fields = oos.putFields();
1075                         fields.put("f1", f1);
1076                         fields.put("f2", f2);
1077                         oos.writeFields();
1078                         // Write custom data
1079                         oos.write#{FIELD[0].READFIELD}(#{FIELD[0].DEFAULT});
1080                         oos.write#{FIELD[1].READFIELD}(#{FIELD[1].DEFAULT});
1081                     }
1082                 """),
1083         ;
1084 
1085         private final String template;
1086 
1087         WriteObjectFragments(String template) {
1088             this.template = template;
1089         }
1090 
1091         @Override
1092         public String expand(String optParameter) {
1093             return template;
1094         }
1095     }
1096 
1097     /**
1098      * ReadObject templates
1099      */
1100     enum ReadObjectFragments implements ComboParameter, CodeShapePredicate {
1101         NONE(""),
1102         READ_OBJECT_DEFAULT_FRAGMENT("""
1103                     private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
1104                         ois.defaultReadObject();
1105                     }
1106                 """),
1107         READ_OBJECT_FIELDS_FRAGMENT("""
1108                     private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
1109                         ObjectInputStream.GetField fields = ois.readFields();
1110                         this.f1 = (#{FIELD[0]})fields.get("f1", #{FIELD[0].DEFAULT});
1111                         this.f2 = (#{FIELD[1]})fields.get("f2", #{FIELD[1].DEFAULT});
1112                     }
1113                  """),
1114         READ_OBJECT_DEFAULT_CUSTOM_FRAGMENT("""
1115                     private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
1116                         ois.defaultReadObject();
1117                         // Read custom data
1118                         #{FIELD[0]} d1 = (#{FIELD[0]})ois.read#{FIELD[0].READFIELD}();
1119                         #{FIELD[1]} d2 = (#{FIELD[1]})ois.read#{FIELD[1].READFIELD}();
1120                         assert Objects.equals(#{FIELD[0].DEFAULT}, d1) : "reading custom data1, actual: " + d1;
1121                         assert Objects.equals(#{FIELD[1].DEFAULT}, d2) : "reading custom data2, actual: " + d2;
1122                     }
1123                 """),
1124         READ_OBJECT_FIELDS_CUSTOM_FRAGMENT("""
1125                     private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
1126                         ObjectInputStream.GetField fields = ois.readFields();
1127                         this.f1 = (#{FIELD[0]})fields.get("f1", #{FIELD[0].DEFAULT});
1128                         this.f2 = (#{FIELD[1]})fields.get("f2", #{FIELD[1].DEFAULT});
1129                         // Read custom data
1130                         #{FIELD[0]} d1 = (#{FIELD[0]})ois.read#{FIELD[0].READFIELD}();
1131                         #{FIELD[1]} d2 = (#{FIELD[1]})ois.read#{FIELD[1].READFIELD}();
1132                         assert Objects.equals(#{FIELD[0].DEFAULT}, d1) : "reading custom data1, actual: " + d1;
1133                         assert Objects.equals(#{FIELD[1].DEFAULT}, d2) : "reading custom data2, actual: " + d2;
1134                     }
1135                  """),
1136         ;
1137 
1138         private final String template;
1139 
1140         ReadObjectFragments(String template) {
1141             this.template = template;
1142         }
1143 
1144         @Override
1145         public String expand(String optParameter) {
1146             return template;
1147         }
1148     }
1149 
1150     /**
1151      * Value and Identity kinds.
1152      */
1153     enum ValueKind implements ComboParameter, CodeShapePredicate {
1154         VALUE("value"),
1155         IDENTITY(""),
1156         ;
1157 
1158         private final String template;
1159 
1160         ValueKind(String template) {
1161             this.template = template;
1162         }
1163 
1164         @Override
1165         public String expand(String optParameter) {
1166             return template;
1167         }
1168     }
1169 
1170     enum SerializationKind implements ComboParameter, CodeShapePredicate {
1171         SERIALIZABLE("SER", "implements Serializable"),
1172         EXTERNALIZABLE("EXT", "implements Externalizable"),
1173         ;
1174 
1175         private final String key;
1176         private final String declaration;
1177 
1178         SerializationKind(String key, String declaration) {
1179             this.key = key;
1180             this.declaration = declaration;
1181         }
1182 
1183         public String expand(String optParameter) {
1184             return switch (optParameter) {
1185                 case null -> key;
1186                 case "IMPLEMENTS" -> declaration;
1187                 default ->
1188                         "#{" + optParameter + "_" + this + "}";   // everything ELSE turn into requested key with suffix
1189             };
1190         }
1191     }
1192 
1193     /**
1194      * Class Access kinds.
1195      */
1196     enum ClassAccessKind implements ComboParameter, CodeShapePredicate {
1197         PUBLIC("public"),
1198         PACKAGE(""),
1199         ;
1200 
1201         private final String classAccessTemplate;
1202 
1203         ClassAccessKind(String classAccessTemplate) {
1204             this.classAccessTemplate = classAccessTemplate;
1205         }
1206 
1207         @Override
1208         public String expand(String optParameter) {
1209             return classAccessTemplate;
1210         }
1211     }
1212 
1213     /**
1214      * Type of arguments to insert in method signatures
1215      */
1216     enum ArgumentValue implements ComboParameter, CodeShapePredicate {
1217         BOOLEAN("boolean", true),
1218         BYTE("byte", (byte) 127),
1219         CHAR("char", 'Z'),
1220         SHORT("short", (short) 0x7fff),
1221         INT("int", 0x7fffffff),
1222         LONG("long", 0x7fffffffffffffffL),
1223         FLOAT("float", 1.0F),
1224         DOUBLE("double", 1.0d),
1225         STRING("String", "xyz");
1226 
1227         static final ArgumentValue[] BASIC_VALUES = {INT, STRING};
1228 
1229         private final String argumentsValueTemplate;
1230         private final Object value;
1231 
1232         ArgumentValue(String argumentsValueTemplate, Object value) {
1233             this.argumentsValueTemplate = argumentsValueTemplate;
1234             this.value = value;
1235         }
1236 
1237         @Override
1238         public String expand(String optParameter) {
1239             return switch (optParameter) {
1240                 case null -> argumentsValueTemplate;
1241                 case "TITLECASE" -> Character.toTitleCase(argumentsValueTemplate.charAt(0)) +
1242                         argumentsValueTemplate.substring(1);
1243                 case "DEFAULT" -> switch (this) {
1244                     case BOOLEAN -> "false";
1245                     case BYTE -> "(byte)-1";
1246                     case CHAR -> "'" + "!" + "'";
1247                     case SHORT -> "(short)-1";
1248                     case INT -> "-1";
1249                     case LONG -> "-1L";
1250                     case FLOAT -> "-1.0f";
1251                     case DOUBLE -> "-1.0d";
1252                     case STRING -> '"' + "n/a" + '"';
1253                 };
1254                 case "READFIELD" -> switch (this) {
1255                     case BOOLEAN -> "Boolean";
1256                     case BYTE -> "Byte";
1257                     case CHAR -> "Char";
1258                     case SHORT -> "Short";
1259                     case INT -> "Int";
1260                     case LONG -> "Long";
1261                     case FLOAT -> "Float";
1262                     case DOUBLE -> "Double";
1263                     case STRING -> "Object";
1264                 };
1265                 case "RANDOM" -> switch (this) {  // or can be Random
1266                     case BOOLEAN -> Boolean.toString(!(boolean) value);
1267                     case BYTE -> "(byte)" + value + 1;
1268                     case CHAR -> "'" + value + "'";
1269                     case SHORT -> "(short)" + value + 1;
1270                     case INT -> "-2";
1271                     case LONG -> "-2L";
1272                     case FLOAT -> (1.0f + (float) value) + "f";
1273                     case DOUBLE -> (1.0d + (float) value) + "d";
1274                     case STRING -> "\"" + value + "!\"";
1275                 };
1276                 default -> switch (this) {
1277                     case BOOLEAN -> value.toString();
1278                     case BYTE -> "(byte)" + value;
1279                     case CHAR -> "'" + value + "'";
1280                     case SHORT -> "(short)" + value;
1281                     case INT -> "-1";
1282                     case LONG -> "-1L";
1283                     case FLOAT -> value + "f";
1284                     case DOUBLE -> value + "d";
1285                     case STRING -> '"' + (String) value + '"';
1286                 };
1287             };
1288         }
1289     }
1290 
1291     /**
1292      * Set of Parameters to fill in template.
1293      *
1294      * @param key    the key
1295      * @param params the ComboParameters (one or more)
1296      */
1297     record ParamSet(String key, int count, ComboParameter... params) {
1298         /**
1299          * Set of parameter strings for fill in template.
1300          * The strings are mapped to CompboParameter.Constants.
1301          *
1302          * @param key     the key
1303          * @param strings varargs strings
1304          */
1305         ParamSet(String key, String... strings) {
1306             this(key, 1, paramsForStrings(strings));
1307         }
1308 
1309         /**
1310          * Set of parameter strings for fill in template.
1311          * The strings are mapped to CompboParameter.Constants.
1312          *
1313          * @param key     the key
1314          * @param strings varargs strings
1315          */
1316         ParamSet(String key, int count, String... strings) {
1317             this(key, count, paramsForStrings(strings));
1318         }
1319 
1320         /**
1321          * Set of parameters for fill in template.
1322          * The strings are mapped to CompboParameter.Constants.
1323          *
1324          * @param key    the key
1325          * @param params varargs strings
1326          */
1327         ParamSet(String key, ComboParameter... params) {
1328             this(key, 1, params);
1329         }
1330     }
1331 
1332     /**
1333      * Marks conditions that should match compile time errors
1334      */
1335     static class CompileException extends RuntimeException {
1336         CompileException(String msg) {
1337             super(msg);
1338         }
1339     }
1340 
1341     /**
1342      * Custom ObjectInputStream to be resolve classes from a specific class loader.
1343      */
1344     private static class LoaderObjectInputStream extends ObjectInputStream {
1345         private final ClassLoader loader;
1346 
1347         public LoaderObjectInputStream(InputStream in, ClassLoader loader) throws IOException {
1348             super(in);
1349             this.loader = loader;
1350         }
1351 
1352         /**
1353          * Override resolveClass to be resolve classes from the specified loader.
1354          *
1355          * @param desc an instance of class {@code ObjectStreamClass}
1356          * @return the class
1357          * @throws ClassNotFoundException if the class is not found
1358          */
1359         @Override
1360         protected Class<?> resolveClass(ObjectStreamClass desc) throws ClassNotFoundException {
1361             String name = desc.getName();
1362             try {
1363                 return Class.forName(name, false, loader);
1364             } catch (ClassNotFoundException ex) {
1365                 Class<?> cl = Class.forPrimitiveName(name);
1366                 if (cl != null) {
1367                     return cl;
1368                 } else {
1369                     throw ex;
1370                 }
1371             }
1372         }
1373     }
1374 
1375     private abstract class MyCompilationTask extends ComboInstance {
1376 
1377     }
1378     private static void selftest() {
1379         Set<ComboParameter> params = Set.of(ValueKind.VALUE, SerializationKind.EXTERNALIZABLE);
1380         assertTrue(ValueKind.VALUE.test(params), "VALUE");
1381         assertTrue(SerializationKind.EXTERNALIZABLE.test(params), "SerializationKind.EXTERNALIZABLE");
1382         assertFalse(CodeShape.BAD_EXT_VALUE.test(params));
1383     }
1384 }