/* * Copyright (c) 2006, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. Oracle designates this * particular file as subject to the "Classpath" exception as provided * by Oracle in the LICENSE file that accompanied this code. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ package build.tools.symbolgenerator; import build.tools.symbolgenerator.CreateSymbols.ModuleHeaderDescription.ExportsDescription; import build.tools.symbolgenerator.CreateSymbols .ModuleHeaderDescription .ProvidesDescription; import build.tools.symbolgenerator.CreateSymbols .ModuleHeaderDescription .RequiresDescription; import java.io.BufferedInputStream; import java.io.BufferedReader; import java.io.BufferedOutputStream; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.StringWriter; import java.io.Writer; import java.lang.classfile.*; import java.lang.classfile.attribute.*; import java.lang.classfile.constantpool.ClassEntry; import java.lang.classfile.constantpool.ConstantPoolBuilder; import java.lang.classfile.constantpool.ConstantValueEntry; import java.lang.classfile.constantpool.IntegerEntry; import java.lang.classfile.constantpool.Utf8Entry; import java.lang.constant.ClassDesc; import java.lang.constant.MethodTypeDesc; import java.lang.constant.ModuleDesc; import java.lang.constant.PackageDesc; import java.lang.reflect.AccessFlag; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.FileVisitResult; import java.nio.file.FileVisitor; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.attribute.BasicFileAttributes; import java.util.stream.Stream; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.EnumSet; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Map.Entry; import java.util.Objects; import java.util.Set; import java.util.TimeZone; import java.util.TreeMap; import java.util.TreeSet; import java.util.function.Function; import java.util.function.Predicate; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; import javax.tools.JavaFileManager; import javax.tools.JavaFileManager.Location; import javax.tools.JavaFileObject; import javax.tools.JavaFileObject.Kind; import javax.tools.StandardLocation; import com.sun.source.util.JavacTask; import com.sun.tools.javac.api.JavacTool; import com.sun.tools.javac.jvm.Target; import com.sun.tools.javac.util.Assert; import com.sun.tools.javac.util.Context; import com.sun.tools.javac.util.Pair; import java.nio.file.DirectoryStream; import java.util.Optional; import java.util.function.Consumer; import static java.lang.classfile.ClassFile.ACC_PROTECTED; import static java.lang.classfile.ClassFile.ACC_PUBLIC; /** * A tool for processing the .sym.txt files. * * To add historical data for JDK N, N >= 11, do the following: * * cd /src/jdk.compiler/share/data/symbols * * /bin/java --add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED \ * --add-exports jdk.compiler/com.sun.tools.javac.jvm=ALL-UNNAMED \ * --add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED \ * --add-modules jdk.jdeps \ * ../../../make/langtools/src/classes/build/tools/symbolgenerator/CreateSymbols.java \ * build-description-incremental symbols include.list * * sanity-check the new and updates files in src/jdk.compiler/share/data/symbols and commit them * * The tools allows to: * * convert the .sym.txt into class/sig files for ct.sym * * in cooperation with the adjacent history Probe, construct .sym.txt files for previous platforms * * enhance existing .sym.txt files with a new set .sym.txt for the current platform * * To convert the .sym.txt files to class/sig files from ct.sym, run: * java build.tool.symbolgenerator.CreateSymbols build-ctsym * * The is a file of this format: * generate platforms * platform version files <.sym.txt files containing history data for given platform, separate with ':'> * platform version base files <.sym.txt files containing history data for given platform, separate with ':'> * * The content of platform "" is also automatically added to the content of * platform "", unless explicitly excluded in ""'s .sym.txt files. * * To create the .sym.txt files, first run the history Probe for all the previous platforms: * /bin/java build.tools.symbolgenerator.Probe * * Where is a name of a file into which the classes from the bootclasspath of * will be written. * * Then create the file and the .sym.txt files like this: * java build.tools.symbolgenerator.CreateSymbols build-description * "" * * * ... * * The is a file that specifies classes that should be included/excluded. * Lines that start with '+' represent class or package that should be included, '-' class or package * that should be excluded. '/' should be used as package name delimiter, packages should end with '/'. * Several include list files may be specified, separated by File.pathSeparator. * * When is specified, the .sym.txt files for platform N will only contain * differences between platform N and the specified platform. The first platform (denoted F further) * that is specified should use literal value "", to have all the APIs of the platform written to * the .sym.txt files. If there is an existing platform with full .sym.txt files in the repository, * that platform should be used as the first platform to avoid unnecessary changes to the .sym.txt * files. The for platform N should be determined as follows: if N < F, then * should be N + 1. If F < N, then should be N - 1. * If N is a custom/specialized sub-version of another platform N', then should be N'. * * To generate the .sym.txt files for OpenJDK 7 and 8: * /bin/java build.tools.symbolgenerator.Probe OpenJDK7.classes * /bin/java build.tools.symbolgenerator.Probe OpenJDK8.classes * java build.tools.symbolgenerator.CreateSymbols build-description src/jdk.compiler/share/data/symbols * $TOPDIR src/jdk.compiler/share/data/symbols/include.list * 8 OpenJDK8.classes '' * 7 OpenJDK7.classes 8 * * Note: the versions are expected to be a single character. * */ public class CreateSymbols { // /**Create sig files for ct.sym reading the classes description from the directory that contains * {@code ctDescriptionFile}, using the file as a recipe to create the sigfiles. */ public void createSymbols(String ctDescriptionFileExtra, String ctDescriptionFile, String ctSymLocation, long timestamp, String currentVersion, String preReleaseTag, String moduleClasses, String includedModulesFile) throws IOException { LoadDescriptions data = load(ctDescriptionFileExtra != null ? Paths.get(ctDescriptionFileExtra) : null, Paths.get(ctDescriptionFile)); int currentVersionParsed = Integer.parseInt(currentVersion); currentVersion = Integer.toString(currentVersionParsed, Character.MAX_RADIX); currentVersion = currentVersion.toUpperCase(Locale.ROOT); String previousVersion = Integer.toString(currentVersionParsed - 1, Character.MAX_RADIX); previousVersion = previousVersion.toUpperCase(Locale.ROOT); //load current version classes: Path moduleClassPath = Paths.get(moduleClasses); Set includedModules = Files.lines(Paths.get(includedModulesFile)) .flatMap(l -> Arrays.stream(l.split(" "))) .collect(Collectors.toSet()); loadVersionClassesFromDirectory(data.classes, data.modules, moduleClassPath, includedModules, currentVersion, previousVersion); stripNonExistentAnnotations(data); splitHeaders(data.classes); Map> package2Version2Module = new HashMap<>(); Map> directory2FileData = new TreeMap<>(); for (ModuleDescription md : data.modules.values()) { for (ModuleHeaderDescription mhd : md.header) { writeModulesForVersions(directory2FileData, md, mhd, mhd.versions, version -> { String versionString = Character.toString(version); int versionNumber = Integer.parseInt(versionString, Character.MAX_RADIX); versionString = Integer.toString(versionNumber); if (versionNumber == currentVersionParsed && !preReleaseTag.isEmpty()) { versionString = versionString + "-" + preReleaseTag; } return versionString; }); List packages = new ArrayList<>(); mhd.exports.stream() .map(ExportsDescription::packageName) .forEach(packages::add); if (mhd.extraModulePackages != null) { packages.addAll(mhd.extraModulePackages); } packages.stream().forEach(pkg -> { for (char v : mhd.versions.toCharArray()) { package2Version2Module.computeIfAbsent(pkg, dummy -> new HashMap<>()).put(v, md.name); } }); } } for (ClassDescription classDescription : data.classes) { Map version2Module = package2Version2Module.getOrDefault(classDescription.packge().replace('.', '/'), Collections.emptyMap()); for (ClassHeaderDescription header : classDescription.header) { Set jointVersions = new HashSet<>(); jointVersions.add(header.versions); limitJointVersion(jointVersions, classDescription.fields); limitJointVersion(jointVersions, classDescription.methods); Map module2Versions = new HashMap<>(); for (char v : header.versions.toCharArray()) { String module = version2Module.get(v); if (module == null) { if (v >= '9') { throw new AssertionError("No module for " + classDescription.name + " and version " + v); } module = version2Module.get('9'); if (module == null) { module = "java.base"; } } module2Versions.computeIfAbsent(module, dummy -> new StringBuilder()).append(v); } for (Entry e : module2Versions.entrySet()) { Set currentVersions = new HashSet<>(jointVersions); limitJointVersion(currentVersions, e.getValue().toString()); currentVersions = currentVersions.stream().filter(vers -> !disjoint(vers, e.getValue().toString())).collect(Collectors.toSet()); writeClassesForVersions(directory2FileData, classDescription, header, e.getKey(), currentVersions); } } } try (OutputStream fos = new FileOutputStream(ctSymLocation); OutputStream bos = new BufferedOutputStream(fos); ZipOutputStream jos = new ZipOutputStream(bos)) { for (Entry> e : directory2FileData.entrySet()) { jos.putNextEntry(createZipEntry(e.getKey(), timestamp)); for (FileData fd : e.getValue()) { jos.putNextEntry(createZipEntry(fd.fileName, timestamp)); jos.write(fd.fileData); } } } } private static final String PREVIEW_FEATURE_ANNOTATION_OLD = "Ljdk/internal/PreviewFeature;"; private static final String PREVIEW_FEATURE_ANNOTATION_NEW = "Ljdk/internal/javac/PreviewFeature;"; private static final String PREVIEW_FEATURE_ANNOTATION_INTERNAL = "Ljdk/internal/PreviewFeature+Annotation;"; private static final String RESTRICTED_ANNOTATION = "Ljdk/internal/javac/Restricted;"; private static final String RESTRICTED_ANNOTATION_INTERNAL = "Ljdk/internal/javac/Restricted+Annotation;"; private static final String VALUE_BASED_ANNOTATION = "Ljdk/internal/ValueBased;"; private static final String VALUE_BASED_ANNOTATION_INTERNAL = "Ljdk/internal/ValueBased+Annotation;"; public static final Set HARDCODED_ANNOTATIONS = new HashSet<>( List.of("Ljdk/Profile+Annotation;", "Lsun/Proprietary+Annotation;", PREVIEW_FEATURE_ANNOTATION_OLD, PREVIEW_FEATURE_ANNOTATION_NEW, VALUE_BASED_ANNOTATION, RESTRICTED_ANNOTATION)); private void stripNonExistentAnnotations(LoadDescriptions data) { Set allClasses = data.classes.name2Class.keySet(); data.modules.values().forEach(mod -> { stripNonExistentAnnotations(allClasses, mod.header); }); data.classes.classes.forEach(clazz -> { stripNonExistentAnnotations(allClasses, clazz.header); stripNonExistentAnnotations(allClasses, clazz.fields); stripNonExistentAnnotations(allClasses, clazz.methods); }); } private void stripNonExistentAnnotations(Set allClasses, Iterable descs) { descs.forEach(d -> stripNonExistentAnnotations(allClasses, d)); } private void stripNonExistentAnnotations(Set allClasses, FeatureDescription d) { stripNonExistentAnnotations(allClasses, d.classAnnotations); stripNonExistentAnnotations(allClasses, d.runtimeAnnotations); } private void stripNonExistentAnnotations(Set allClasses, List annotations) { if (annotations != null) annotations.removeIf(ann -> !HARDCODED_ANNOTATIONS.contains(ann.annotationType) && !allClasses.contains(ann.annotationType.substring(1, ann.annotationType.length() - 1))); } private ZipEntry createZipEntry(String name, long timestamp) { ZipEntry ze = new ZipEntry(name); ze.setTime(timestamp); return ze; } public static String EXTENSION = ".sig"; LoadDescriptions load(Path ctDescriptionWithExtraContent, Path ctDescriptionOpen) throws IOException { Map platforms = new LinkedHashMap<>(); if (ctDescriptionWithExtraContent != null && Files.isRegularFile(ctDescriptionWithExtraContent)) { try (LineBasedReader reader = new LineBasedReader(ctDescriptionWithExtraContent)) { while (reader.hasNext()) { switch (reader.lineKey) { case "generate": //ignore reader.moveNext(); break; case "platform": PlatformInput platform = PlatformInput.load(ctDescriptionWithExtraContent, reader); platforms.put(platform.version, platform); reader.moveNext(); break; default: throw new IllegalArgumentException("Unknown key: " + reader.lineKey); } } } } Set generatePlatforms = null; try (LineBasedReader reader = new LineBasedReader(ctDescriptionOpen)) { while (reader.hasNext()) { switch (reader.lineKey) { case "generate": String[] platformsAttr = reader.attributes.get("platforms").split(":"); generatePlatforms = new HashSet<>(List.of(platformsAttr)); reader.moveNext(); break; case "platform": PlatformInput platform = PlatformInput.load(ctDescriptionOpen, reader); if (!platforms.containsKey(platform.version)) platforms.put(platform.version, platform); reader.moveNext(); break; default: throw new IllegalArgumentException("Unknown key: " + reader.lineKey); } } } Map classes = new LinkedHashMap<>(); Map modules = new LinkedHashMap<>(); for (PlatformInput platform : platforms.values()) { for (ClassDescription cd : classes.values()) { addNewVersion(cd.header, platform.basePlatform, platform.version); addNewVersion(cd.fields, platform.basePlatform, platform.version); addNewVersion(cd.methods, platform.basePlatform, platform.version); } for (ModuleDescription md : modules.values()) { addNewVersion(md.header, platform.basePlatform, platform.version); } for (String input : platform.files) { Path inputFile = platform.ctDescription.getParent().resolve(input); try (LineBasedReader reader = new LineBasedReader(inputFile)) { while (reader.hasNext()) { String nameAttr = reader.attributes.get("name"); switch (reader.lineKey) { case "class": case "-class": ClassDescription cd = classes.computeIfAbsent(nameAttr, n -> new ClassDescription()); if ("-class".equals(reader.lineKey)) { removeVersion(cd.header, h -> true, platform.version); reader.moveNext(); continue; } cd.read(reader, platform.basePlatform, platform.version); break; case "module": { ModuleDescription md = modules.computeIfAbsent(nameAttr, n -> new ModuleDescription()); md.read(reader, platform.basePlatform, platform.version); break; } case "-module": { ModuleDescription md = modules.computeIfAbsent(nameAttr, n -> new ModuleDescription()); removeVersion(md.header, h -> true, platform.version); reader.moveNext(); break; } } } } } } ClassList result = new ClassList(); classes.values().forEach(result::add); return new LoadDescriptions(result, modules, new ArrayList<>(platforms.values())); } private static void removeVersion(LoadDescriptions load, String deletePlatform) { for (Iterator it = load.classes.iterator(); it.hasNext();) { ClassDescription desc = it.next(); Iterator chdIt = desc.header.iterator(); while (chdIt.hasNext()) { ClassHeaderDescription chd = chdIt.next(); chd.versions = removeVersion(chd.versions, deletePlatform); if (chd.versions.isEmpty()) { chdIt.remove(); } } if (desc.header.isEmpty()) { it.remove(); continue; } Iterator methodIt = desc.methods.iterator(); while (methodIt.hasNext()) { MethodDescription method = methodIt.next(); method.versions = removeVersion(method.versions, deletePlatform); if (method.versions.isEmpty()) methodIt.remove(); } Iterator fieldIt = desc.fields.iterator(); while (fieldIt.hasNext()) { FieldDescription field = fieldIt.next(); field.versions = removeVersion(field.versions, deletePlatform); if (field.versions.isEmpty()) fieldIt.remove(); } } for (Iterator it = load.modules.values().iterator(); it.hasNext();) { ModuleDescription desc = it.next(); Iterator mhdIt = desc.header.iterator(); while (mhdIt.hasNext()) { ModuleHeaderDescription mhd = mhdIt.next(); mhd.versions = removeVersion(mhd.versions, deletePlatform); if (mhd.versions.isEmpty()) mhdIt.remove(); } if (desc.header.isEmpty()) { it.remove(); continue; } } } static final class LoadDescriptions { public final ClassList classes; public final Map modules; public final List versions; public LoadDescriptions(ClassList classes, Map modules, List versions) { this.classes = classes; this.modules = modules; this.versions = versions; } } static final class LineBasedReader implements AutoCloseable { private final BufferedReader input; public String lineKey; public Map attributes = new HashMap<>(); public LineBasedReader(Path input) throws IOException { this.input = Files.newBufferedReader(input); moveNext(); } public void moveNext() throws IOException { String line = input.readLine(); if (line == null) { lineKey = null; return ; } if (line.trim().isEmpty() || line.startsWith("#")) { moveNext(); return ; } String[] parts = line.split(" "); lineKey = parts[0]; attributes.clear(); for (int i = 1; i < parts.length; i += 2) { attributes.put(parts[i], unquote(parts[i + 1])); } } public boolean hasNext() { return lineKey != null; } @Override public void close() throws IOException { input.close(); } } private static String reduce(String original, String other) { Set otherSet = new HashSet<>(); for (char v : other.toCharArray()) { otherSet.add("" + v); } return reduce(original, otherSet); } private static String reduce(String original, Set generate) { StringBuilder sb = new StringBuilder(); for (char v : original.toCharArray()) { if (generate.contains("" + v)) { sb.append(v); } } return sb.toString(); } private static String removeVersion(String original, String remove) { StringBuilder sb = new StringBuilder(); for (char v : original.toCharArray()) { if (v != remove.charAt(0)) { sb.append(v); } } return sb.toString(); } private static class PlatformInput { public final String version; public final String basePlatform; public final List files; public final Path ctDescription; public PlatformInput(Path ctDescription, String version, String basePlatform, List files) { this.ctDescription = ctDescription; this.version = version; this.basePlatform = basePlatform; this.files = files; } public static PlatformInput load(Path ctDescription, LineBasedReader in) throws IOException { return new PlatformInput(ctDescription, in.attributes.get("version"), in.attributes.get("base"), List.of(in.attributes.get("files").split(":"))); } } static void addNewVersion(Collection features, String baselineVersion, String version) { features.stream() .filter(f -> f.versions.contains(baselineVersion)) .forEach(f -> f.versions += version); } static void removeVersion(Collection features, Predicate shouldRemove, String version) { for (T existing : features) { if (shouldRemove.test(existing) && existing.versions.endsWith(version)) { existing.versions = existing.versions.replace(version, ""); return; } } } /**Changes to class header of an outer class (like adding a new type parameter) may affect * its innerclasses. So if the outer class's header is different for versions A and B, need to * split its innerclasses headers to also be different for versions A and B. */ static void splitHeaders(ClassList classes) { Set ctVersions = new HashSet<>(); for (ClassDescription cd : classes) { for (ClassHeaderDescription header : cd.header) { for (char c : header.versions.toCharArray()) { ctVersions.add("" + c); } } } classes.sort(); for (ClassDescription cd : classes) { Map outerSignatures2Version = new HashMap<>(); for (String version : ctVersions) { //XXX ClassDescription outer = cd; String outerSignatures = ""; while ((outer = classes.enclosingClass(outer)) != null) { for (ClassHeaderDescription outerHeader : outer.header) { if (outerHeader.versions.contains(version)) { outerSignatures += outerHeader.signature; } } } outerSignatures2Version.compute(outerSignatures, (key, value) -> value != null ? value + version : version); } List newHeaders = new ArrayList<>(); HEADER_LOOP: for (ClassHeaderDescription header : cd.header) { for (String versions : outerSignatures2Version.values()) { if (containsAll(versions, header.versions)) { newHeaders.add(header); continue HEADER_LOOP; } if (disjoint(versions, header.versions)) { continue; } ClassHeaderDescription newHeader = new ClassHeaderDescription(); newHeader.classAnnotations = header.classAnnotations; newHeader.deprecated = header.deprecated; newHeader.extendsAttr = header.extendsAttr; newHeader.flags = header.flags; newHeader.implementsAttr = header.implementsAttr; newHeader.innerClasses = header.innerClasses; newHeader.runtimeAnnotations = header.runtimeAnnotations; newHeader.signature = header.signature; newHeader.versions = reduce(header.versions, versions); newHeaders.add(newHeader); } } cd.header = newHeaders; } } void limitJointVersion(Set jointVersions, List features) { for (FeatureDescription feature : features) { limitJointVersion(jointVersions, feature.versions); } } void limitJointVersion(Set jointVersions, String versions) { for (String version : jointVersions) { if (!containsAll(versions, version) && !disjoint(versions, version)) { StringBuilder featurePart = new StringBuilder(); StringBuilder otherPart = new StringBuilder(); for (char v : version.toCharArray()) { if (versions.indexOf(v) != (-1)) { featurePart.append(v); } else { otherPart.append(v); } } jointVersions.remove(version); if (featurePart.length() == 0 || otherPart.length() == 0) { throw new AssertionError(); } jointVersions.add(featurePart.toString()); jointVersions.add(otherPart.toString()); break; } } } private static boolean containsAll(String versions, String subVersions) { for (char c : subVersions.toCharArray()) { if (versions.indexOf(c) == (-1)) return false; } return true; } private static boolean disjoint(String version1, String version2) { for (char c : version2.toCharArray()) { if (version1.indexOf(c) != (-1)) return false; } return true; } void writeClassesForVersions(Map> directory2FileData, ClassDescription classDescription, ClassHeaderDescription header, String module, Iterable versions) throws IOException { for (String ver : versions) { writeClass(directory2FileData, classDescription, header, module, ver); } } void writeModulesForVersions(Map> directory2FileData, ModuleDescription moduleDescription, ModuleHeaderDescription header, String versions, Function version2ModuleVersion) throws IOException { //ensure every module-info.class is written separatelly, //so that the correct version is used for it: for (char ver : versions.toCharArray()) { writeModule(directory2FileData, moduleDescription, header, ver, version2ModuleVersion); } } // void writeModule(Map> directory2FileData, ModuleDescription moduleDescription, ModuleHeaderDescription header, char version, Function version2ModuleVersion) throws IOException { var classFile = ClassFile.of().build(ClassDesc.of("module-info"), clb -> { clb.withFlags(header.flags); addAttributes(moduleDescription, header, clb, version2ModuleVersion.apply(version)); }); String versionString = Character.toString(version); doWrite(directory2FileData, versionString, moduleDescription.name, "module-info" + EXTENSION, classFile); } void writeClass(Map> directory2FileData, ClassDescription classDescription, ClassHeaderDescription header, String module, String version) throws IOException { var classFile = ClassFile.of().build(ClassDesc.ofInternalName(classDescription.name), clb -> { if (header.extendsAttr != null) clb.withSuperclass(ClassDesc.ofInternalName(header.extendsAttr)); clb.withInterfaceSymbols(header.implementsAttr.stream().map(ClassDesc::ofInternalName).collect(Collectors.toList())) .withFlags(header.flags); for (FieldDescription fieldDesc : classDescription.fields) { if (disjoint(fieldDesc.versions, version)) continue; clb.withField(fieldDesc.name, ClassDesc.ofDescriptor(fieldDesc.descriptor), fb -> { addAttributes(fieldDesc, fb); fb.withFlags(fieldDesc.flags); }); } for (MethodDescription methDesc : classDescription.methods) { if (disjoint(methDesc.versions, version)) continue; clb.withMethod(methDesc.name, MethodTypeDesc.ofDescriptor(methDesc.descriptor), methDesc.flags, mb -> addAttributes(methDesc, mb)); } addAttributes(header, clb); }); doWrite(directory2FileData, version, module, classDescription.name + EXTENSION, classFile); } private void doWrite(Map> directory2FileData, String version, String moduleName, String fileName, byte[] classFile) throws IOException { int lastSlash = fileName.lastIndexOf('/'); String pack = lastSlash != (-1) ? fileName.substring(0, lastSlash + 1) : "/"; String directory = version + "/" + moduleName + "/" + pack; String fullFileName = version + "/" + moduleName + "/" + fileName; openDirectory(directory2FileData, directory) .add(new FileData(fullFileName, classFile)); } private Set openDirectory(Map> directory2FileData, String directory) { Comparator fileCompare = (fd1, fd2) -> fd1.fileName.compareTo(fd2.fileName); return directory2FileData.computeIfAbsent(directory, d -> new TreeSet<>(fileCompare)); } private static class FileData { public final String fileName; public final byte[] fileData; public FileData(String fileName, byte[] fileData) { this.fileName = fileName; this.fileData = fileData; } } private void addAttributes(ModuleDescription md, ModuleHeaderDescription header, ClassBuilder builder, String moduleVersion) { addGenericAttributes(header, builder); if (header.moduleResolution != null) { builder.with(ModuleResolutionAttribute.of(header.moduleResolution)); } if (header.moduleTarget != null) { builder.with(ModuleTargetAttribute.of(header.moduleTarget)); } if (header.moduleMainClass != null) { builder.with(ModuleMainClassAttribute.of(ClassDesc.ofInternalName(header.moduleMainClass))); } builder.with(ModuleAttribute.of(ModuleDesc.of(md.name), mb -> { mb.moduleVersion(moduleVersion); for (var req : header.requires) { mb.requires(ModuleDesc.of(req.moduleName), req.flags, req.version); // nullable version } for (var exp : header.exports) { if (exp.isQualified()) { mb.exports(PackageDesc.ofInternalName(exp.packageName()), 0, exp.to.stream().map(ModuleDesc::of).toArray(ModuleDesc[]::new)); } else { mb.exports(PackageDesc.ofInternalName(exp.packageName()), 0); } } for (var open : header.opens) { mb.opens(PackageDesc.ofInternalName(open), 0); } for (var use : header.uses) { mb.uses(ClassDesc.ofInternalName(use)); } for (var provide : header.provides) { mb.provides(ClassDesc.ofInternalName(provide.interfaceName), provide.implNames.stream().map(ClassDesc::ofInternalName).toArray(ClassDesc[]::new)); } })); addInnerClassesAttribute(header, builder); } private void addAttributes(ClassHeaderDescription header, ClassBuilder builder) { addGenericAttributes(header, builder); if (header.nestHost != null) { builder.with(NestHostAttribute.of(ClassDesc.ofInternalName(header.nestHost))); } if (header.nestMembers != null && !header.nestMembers.isEmpty()) { builder.with(NestMembersAttribute.ofSymbols(header.nestMembers.stream().map(ClassDesc::ofInternalName).collect(Collectors.toList()))); } if (header.isRecord) { builder.with(RecordAttribute.of(header.recordComponents.stream().map(desc -> { List> attributes = new ArrayList<>(); addGenericAttributes(desc, attributes::add, builder.constantPool()); return RecordComponentInfo.of(desc.name, ClassDesc.ofDescriptor(desc.descriptor), attributes); }).collect(Collectors.toList()))); } if (header.isSealed) { builder.with(PermittedSubclassesAttribute.ofSymbols(header.permittedSubclasses.stream().map(ClassDesc::ofInternalName).collect(Collectors.toList()))); } addInnerClassesAttribute(header, builder); } private void addInnerClassesAttribute(HeaderDescription header, ClassBuilder builder) { if (header.innerClasses != null && !header.innerClasses.isEmpty()) { builder.with(InnerClassesAttribute.of(header.innerClasses.stream() .map(info -> java.lang.classfile.attribute.InnerClassInfo.of( ClassDesc.ofInternalName(info.innerClass), Optional.ofNullable(info.outerClass).map(ClassDesc::ofInternalName), Optional.ofNullable(info.innerClassName), info.innerClassFlags )).collect(Collectors.toList()))); } } private void addAttributes(MethodDescription desc, MethodBuilder builder) { addGenericAttributes(desc, builder); if (desc.thrownTypes != null) { builder.with(ExceptionsAttribute.ofSymbols(desc.thrownTypes.stream() .map(ClassDesc::ofInternalName).collect(Collectors.toList()))); } if (desc.annotationDefaultValue != null) { builder.with(AnnotationDefaultAttribute.of(createAttributeValue(desc.annotationDefaultValue))); } if (desc.classParameterAnnotations != null && !desc.classParameterAnnotations.isEmpty()) { builder.with(RuntimeInvisibleParameterAnnotationsAttribute.of(createParameterAnnotations(desc.classParameterAnnotations))); } if (desc.runtimeParameterAnnotations != null && !desc.runtimeParameterAnnotations.isEmpty()) { builder.with(RuntimeVisibleParameterAnnotationsAttribute.of(createParameterAnnotations(desc.runtimeParameterAnnotations))); } if (desc.methodParameters != null && !desc.methodParameters.isEmpty()) { builder.with(MethodParametersAttribute.of(desc.methodParameters.stream() .map(mp -> MethodParameterInfo.ofParameter(Optional.ofNullable(mp.name), mp.flags)).collect(Collectors.toList()))); } } private void addAttributes(FieldDescription desc, FieldBuilder builder) { addGenericAttributes(desc, builder); if (desc.constantValue != null) { var cp = builder.constantPool(); ConstantValueEntry entry = switch (desc.constantValue) { case Boolean v -> cp.intEntry(v ? 1 : 0); case Character v -> cp.intEntry(v); case Integer v -> cp.intEntry(v); case Long v -> cp.longEntry(v); case Float v -> cp.floatEntry(v); case Double v -> cp.doubleEntry(v); case String v -> cp.stringEntry(v); default -> throw new IllegalArgumentException(desc.constantValue.getClass().toString()); }; builder.with(ConstantValueAttribute.of(entry)); } } @SuppressWarnings("unchecked") private void addGenericAttributes(FeatureDescription desc, ClassFileBuilder builder) { addGenericAttributes(desc, (Consumer>) builder, builder.constantPool()); } private void addGenericAttributes(FeatureDescription desc, Consumer> sink, ConstantPoolBuilder cpb) { @SuppressWarnings("unchecked") var builder = (Consumer>) sink; if (desc.deprecated) { builder.accept(DeprecatedAttribute.of()); } if (desc.signature != null) { builder.accept(SignatureAttribute.of(cpb.utf8Entry(desc.signature))); } if (desc.classAnnotations != null && !desc.classAnnotations.isEmpty()) { builder.accept(RuntimeInvisibleAnnotationsAttribute.of(createAnnotations(desc.classAnnotations))); } if (desc.runtimeAnnotations != null && !desc.runtimeAnnotations.isEmpty()) { builder.accept(RuntimeVisibleAnnotationsAttribute.of(createAnnotations(desc.runtimeAnnotations))); } } private List createAnnotations(List desc) { return desc.stream().map(this::createAnnotation).collect(Collectors.toList()); } private List> createParameterAnnotations(List> desc) { return desc.stream().map(this::createAnnotations).collect(Collectors.toList()); } private Annotation createAnnotation(AnnotationDescription desc) { String annotationType = desc.annotationType; Map values = desc.values; if (PREVIEW_FEATURE_ANNOTATION_NEW.equals(annotationType)) { //the non-public PreviewFeature annotation will not be available in ct.sym, //replace with purely synthetic javac-internal annotation: annotationType = PREVIEW_FEATURE_ANNOTATION_INTERNAL; } if (PREVIEW_FEATURE_ANNOTATION_OLD.equals(annotationType)) { //the non-public PreviewFeature annotation will not be available in ct.sym, //replace with purely synthetic javac-internal annotation: annotationType = PREVIEW_FEATURE_ANNOTATION_INTERNAL; values = new HashMap<>(values); Boolean essentialAPI = (Boolean) values.remove("essentialAPI"); values.put("reflective", essentialAPI != null && !essentialAPI); } if (VALUE_BASED_ANNOTATION.equals(annotationType)) { //the non-public ValueBased annotation will not be available in ct.sym, //replace with purely synthetic javac-internal annotation: annotationType = VALUE_BASED_ANNOTATION_INTERNAL; } if (RESTRICTED_ANNOTATION.equals(annotationType)) { //the non-public Restricted annotation will not be available in ct.sym, //replace with purely synthetic javac-internal annotation: annotationType = RESTRICTED_ANNOTATION_INTERNAL; } return Annotation.of(ClassDesc.ofDescriptor(annotationType), createElementPairs(values)); } private List createElementPairs(Map annotationAttributes) { return annotationAttributes.entrySet().stream() .map(e -> AnnotationElement.of(e.getKey(), createAttributeValue(e.getValue()))) .collect(Collectors.toList()); } private AnnotationValue createAttributeValue(Object value) { return switch (value) { case Boolean v -> AnnotationValue.ofBoolean(v); case Byte v -> AnnotationValue.ofByte(v); case Character v -> AnnotationValue.ofChar(v); case Short v -> AnnotationValue.ofShort(v); case Integer v -> AnnotationValue.ofInt(v); case Long v -> AnnotationValue.ofLong(v); case Float v -> AnnotationValue.ofFloat(v); case Double v -> AnnotationValue.ofDouble(v); case String v -> AnnotationValue.ofString(v); case EnumConstant v -> AnnotationValue.ofEnum(ClassDesc.ofDescriptor(v.type), v.constant); case ClassConstant v -> AnnotationValue.ofClass(ClassDesc.ofDescriptor(v.type)); case AnnotationDescription v -> AnnotationValue.ofAnnotation(createAnnotation(v)); case Collection v -> AnnotationValue.ofArray(v.stream().map(this::createAttributeValue).collect(Collectors.toList())); default -> throw new IllegalArgumentException(value.getClass().getName()); }; } // // // public void createBaseLine(List versions, ExcludeIncludeList excludesIncludes, Path descDest, String[] args) throws IOException { ClassList classes = new ClassList(); Map modules = new HashMap<>(); for (VersionDescription desc : versions) { Iterable classFileData = loadClassData(desc.classes); loadVersionClasses(classes, modules, classFileData, excludesIncludes, desc.version, null); } List platforms = versions.stream() .map(desc -> new PlatformInput(null, desc.version, desc.primaryBaseline, null)) .collect(Collectors.toList()); dumpDescriptions(classes, modules, platforms, Set.of(), descDest.resolve("symbols"), args); } //where: public static String DO_NOT_MODIFY = "#\n" + "# Copyright (c) {YEAR}, Oracle and/or its affiliates. All rights reserved.\n" + "# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.\n" + "#\n" + "# This code is free software; you can redistribute it and/or modify it\n" + "# under the terms of the GNU General Public License version 2 only, as\n" + "# published by the Free Software Foundation. Oracle designates this\n" + "# particular file as subject to the \"Classpath\" exception as provided\n" + "# by Oracle in the LICENSE file that accompanied this code.\n" + "#\n" + "# This code is distributed in the hope that it will be useful, but WITHOUT\n" + "# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or\n" + "# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License\n" + "# version 2 for more details (a copy is included in the LICENSE file that\n" + "# accompanied this code).\n" + "#\n" + "# You should have received a copy of the GNU General Public License version\n" + "# 2 along with this work; if not, write to the Free Software Foundation,\n" + "# Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.\n" + "#\n" + "# Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA\n" + "# or visit www.oracle.com if you need additional information or have any\n" + "# questions.\n" + "#\n" + "# ##########################################################\n" + "# ### THIS FILE IS AUTOMATICALLY GENERATED. DO NOT EDIT. ###\n" + "# ##########################################################\n" + "#\n"; private Iterable loadClassData(String path) { List classFileData = new ArrayList<>(); try (BufferedReader descIn = Files.newBufferedReader(Paths.get(path))) { String line; while ((line = descIn.readLine()) != null) { ByteArrayOutputStream data = new ByteArrayOutputStream(); for (int i = 0; i < line.length(); i += 2) { String hex = line.substring(i, i + 2); data.write(Integer.parseInt(hex, 16)); } classFileData.add(data.toByteArray()); } } catch (IOException ex) { throw new IllegalArgumentException(ex); } return classFileData; } private void loadVersionClasses(ClassList classes, Map modules, Iterable classData, ExcludeIncludeList excludesIncludes, String version, String baseline) { Map currentVersionModules = new HashMap<>(); for (byte[] classFileData : classData) { inspectModuleInfoClassFile(classFileData, currentVersionModules, version); } ExcludeIncludeList currentEIList; if (!currentVersionModules.isEmpty()) { Set privateIncludes = enhancedIncludesListBasedOnClassHeaders(classes, classData); Set includes = new HashSet<>(); for (ModuleDescription md : currentVersionModules.values()) { md.header.get(0).exports.stream() .filter(e -> !e.isQualified()) .map(e -> e.packageName + '/') .forEach(includes::add); } currentEIList = new ExcludeIncludeList(includes, privateIncludes, Collections.emptySet()); } else { currentEIList = excludesIncludes; } ClassList currentVersionClasses = new ClassList(); Map> extraModulesPackagesToDerive = new HashMap<>(); for (byte[] classFileData : classData) { try (InputStream in = new ByteArrayInputStream(classFileData)) { inspectClassFile(in, currentVersionClasses, currentEIList, version, cm -> { var permitted = cm.findAttribute(Attributes.permittedSubclasses()).orElse(null); if (permitted != null) { var name = cm.thisClass().asInternalName(); String currentPack = name.substring(0, name.lastIndexOf('/')); for (var sub : permitted.permittedSubclasses()) { String permittedClassName = sub.asInternalName(); if (!currentEIList.accepts(permittedClassName, false)) { String permittedPack = permittedClassName.substring(0, permittedClassName.lastIndexOf('/')); extraModulesPackagesToDerive.computeIfAbsent(permittedPack, x -> new HashSet<>()) .add(currentPack); } } } }); } catch (IOException ex) { throw new IllegalArgumentException(ex); } } //derive extra module packages for permitted types based on on their supertypes: boolean modified; do { modified = false; for (Iterator>> it = extraModulesPackagesToDerive.entrySet().iterator(); it.hasNext();) { Entry> e = it.next(); for (String basePackage : e.getValue()) { Optional module = currentVersionModules.values().stream().map(md -> md.header.get(0)).filter(d -> containsPackage(d, basePackage)).findAny(); if (module.isPresent()) { if (!module.get().extraModulePackages.contains(e.getKey())) { module.get().extraModulePackages.add(e.getKey()); } it.remove(); modified = true; break; } } } } while (modified); if (!extraModulesPackagesToDerive.isEmpty()) { throw new AssertionError("Cannot derive some owning modules: " + extraModulesPackagesToDerive); } finishClassLoading(classes, modules, currentVersionModules, currentVersionClasses, currentEIList, version, baseline); } private boolean containsPackage(ModuleHeaderDescription module, String pack) { return module.exports.stream().filter(ed -> ed.packageName().equals(pack)).findAny().isPresent() || module.extraModulePackages.contains(pack); } private void loadVersionClassesFromDirectory(ClassList classes, Map modules, Path modulesDirectory, Set includedModules, String version, String baseline) { Map currentVersionModules = new HashMap<>(); ClassList currentVersionClasses = new ClassList(); Set privateIncludes = new HashSet<>(); Set includes = new HashSet<>(); ExcludeIncludeList currentEIList = new ExcludeIncludeList(includes, privateIncludes, Collections.emptySet()); try { Map modulePath2Header = new HashMap<>(); List pendingExportedDirectories = new ArrayList<>(); try (DirectoryStream ds = Files.newDirectoryStream(modulesDirectory)) { for (Path p : ds) { if (!includedModules.contains(p.getFileName().toString())) { continue; } Path moduleInfo = p.resolve("module-info.class"); if (Files.isReadable(moduleInfo)) { ModuleDescription md = inspectModuleInfoClassFile(Files.readAllBytes(moduleInfo), currentVersionModules, version); if (md == null) { continue; } modulePath2Header.put(p, md.header.get(0)); Set currentModuleExports = md.header.get(0).exports.stream() .filter(e -> !e.isQualified()) .map(e -> e.packageName + '/') .collect(Collectors.toSet()); for (String dir : currentModuleExports) { includes.add(dir); pendingExportedDirectories.add(p.resolve(dir)); } } else { throw new IllegalArgumentException("Included module: " + p.getFileName() + " does not have a module-info.class"); } } } List pendingExtraClasses = new ArrayList<>(); for (Path exported : pendingExportedDirectories) { try (DirectoryStream ds = Files.newDirectoryStream(exported)) { for (Path p2 : ds) { if (!Files.isRegularFile(p2) || !p2.getFileName().toString().endsWith(".class")) { continue; } loadFromDirectoryHandleClassFile(p2, currentVersionClasses, currentEIList, version, pendingExtraClasses); } } } while (!pendingExtraClasses.isEmpty()) { String current = pendingExtraClasses.remove(pendingExtraClasses.size() - 1); if (currentVersionClasses.find(current, true) != null) { continue; } for (Entry e : modulePath2Header.entrySet()) { Path currentPath = e.getKey().resolve(current + ".class"); if (Files.isReadable(currentPath)) { String pack = current.substring(0, current.lastIndexOf('/')); e.getValue().extraModulePackages.add(pack); loadFromDirectoryHandleClassFile(currentPath, currentVersionClasses, currentEIList, version, pendingExtraClasses); } } } } catch (IOException ex) { throw new IllegalArgumentException(ex); } finishClassLoading(classes, modules, currentVersionModules, currentVersionClasses, currentEIList, version, baseline); } private void loadFromDirectoryHandleClassFile(Path path, ClassList currentVersionClasses, ExcludeIncludeList currentEIList, String version, List todo) throws IOException { try (InputStream in = Files.newInputStream(path)) { inspectClassFile(in, currentVersionClasses, currentEIList, version, cf -> { Set superTypes = otherRelevantTypesWithOwners(cf); currentEIList.privateIncludeList.addAll(superTypes); todo.addAll(superTypes); }); } } private void finishClassLoading(ClassList classes, Map modules, Map currentVersionModules, ClassList currentVersionClasses, ExcludeIncludeList currentEIList, String version, String baseline) { ModuleDescription unsupported = currentVersionModules.get("jdk.unsupported"); if (unsupported != null) { for (ClassDescription cd : currentVersionClasses.classes) { if (unsupported.header .get(0) .exports .stream() .map(ed -> ed.packageName) .anyMatch(pack -> pack.equals(cd.packge().replace('.', '/')))) { ClassHeaderDescription ch = cd.header.get(0); if (ch.classAnnotations == null) { ch.classAnnotations = new ArrayList<>(); } AnnotationDescription ad; ad = new AnnotationDescription(PROPERITARY_ANNOTATION, Collections.emptyMap()); ch.classAnnotations.add(ad); } } } Set includedClasses = new HashSet<>(); Map> package2ModulesUsingIt = new HashMap<>(); Map package2Module = new HashMap<>(); currentVersionModules.values() .forEach(md -> { md.header.get(0).allPackages().forEach(pack -> { package2Module.put(pack, md.name); }); }); boolean modified; do { modified = false; for (ClassDescription clazz : currentVersionClasses) { Set thisClassIncludedClasses = new HashSet<>(); ClassHeaderDescription header = clazz.header.get(0); if (includeEffectiveAccess(currentVersionClasses, clazz) && currentEIList.accepts(clazz.name, false)) { include(thisClassIncludedClasses, currentVersionClasses, clazz.name); } if (includedClasses.contains(clazz.name)) { include(thisClassIncludedClasses, currentVersionClasses, header.extendsAttr); for (String i : header.implementsAttr) { include(thisClassIncludedClasses, currentVersionClasses, i); } if (header.permittedSubclasses != null) { for (String i : header.permittedSubclasses) { include(thisClassIncludedClasses, currentVersionClasses, i); } } includeOutputType(Collections.singleton(header), h -> "", thisClassIncludedClasses, currentVersionClasses); includeOutputType(clazz.fields, f -> f.descriptor, thisClassIncludedClasses, currentVersionClasses); includeOutputType(clazz.methods, m -> m.descriptor, thisClassIncludedClasses, currentVersionClasses); } if (includedClasses.addAll(thisClassIncludedClasses)) { modified |= true; } for (String includedClass : thisClassIncludedClasses) { int lastSlash = includedClass.lastIndexOf('/'); String pack; if (lastSlash != (-1)) { pack = includedClass.substring(0, lastSlash) .replace('.', '/'); } else { pack = ""; } package2ModulesUsingIt.computeIfAbsent(pack, p -> new HashSet<>()) .add(package2Module.get(clazz.packge())); } } } while (modified); Set allIncludedPackages = new HashSet<>(); for (ClassDescription clazz : currentVersionClasses) { if (!includedClasses.contains(clazz.name)) { continue; } ClassHeaderDescription header = clazz.header.get(0); if (header.nestMembers != null) { Iterator nestMemberIt = header.nestMembers.iterator(); while(nestMemberIt.hasNext()) { String member = nestMemberIt.next(); if (!includedClasses.contains(member)) nestMemberIt.remove(); } } if (header.innerClasses != null) { Iterator innerClassIt = header.innerClasses.iterator(); while(innerClassIt.hasNext()) { InnerClassInfo ici = innerClassIt.next(); if (!includedClasses.contains(ici.innerClass)) innerClassIt.remove(); } } ClassDescription existing = classes.find(clazz.name, true); if (existing != null) { addClassHeader(existing, header, version, baseline); for (MethodDescription currentMethod : clazz.methods) { addMethod(existing, currentMethod, version, baseline); } for (FieldDescription currentField : clazz.fields) { addField(existing, currentField, version, baseline); } } else { classes.add(clazz); } allIncludedPackages.add(clazz.packge().replace('.', '/')); } for (ModuleDescription module : currentVersionModules.values()) { ModuleHeaderDescription header = module.header.get(0); if (header.innerClasses != null) { Iterator innerClassIt = header.innerClasses.iterator(); while(innerClassIt.hasNext()) { InnerClassInfo ici = innerClassIt.next(); if (!includedClasses.contains(ici.innerClass)) innerClassIt.remove(); } } for (Iterator it = header.exports.iterator(); it.hasNext();) { ExportsDescription ed = it.next(); if (!ed.isQualified()) { continue; } if (ed.packageName.equals("jdk/internal/javac")) { //keep jdk/internal/javac untouched. It is used to determine participates in preview: continue; } Set usingModules = package2ModulesUsingIt.getOrDefault(ed.packageName(), Set.of()); ed.to.retainAll(usingModules); if (ed.to.isEmpty()) { it.remove(); if (allIncludedPackages.contains(ed.packageName())) { header.extraModulePackages.add(ed.packageName()); } } } if (header.extraModulePackages != null) { header.extraModulePackages.retainAll(allIncludedPackages); } ModuleDescription existing = modules.get(module.name); if (existing != null) { addModuleHeader(existing, header, version); } else { modules.put(module.name, module); } } } //where: private static final String PROPERITARY_ANNOTATION = "Lsun/Proprietary+Annotation;"; private void dumpDescriptions(ClassList classes, Map modules, List versions, Set forceWriteVersions, Path ctDescriptionFile, String[] args) throws IOException { classes.sort(); Map package2Modules = new HashMap<>(); versions.stream() .filter(v -> "9".compareTo(v.version) <= 0) .sorted((v1, v2) -> v1.version.compareTo(v2.version)) .forEach(v -> { for (ModuleDescription md : modules.values()) { md.header .stream() .filter(h -> h.versions.contains(v.version)) .flatMap(ModuleHeaderDescription::allPackages) .forEach(p -> package2Modules.putIfAbsent(p, md.name)); } }); package2Modules.put("java.awt.dnd.peer", "java.desktop"); package2Modules.put("java.awt.peer", "java.desktop"); package2Modules.put("jdk", "java.base"); Map> module2Classes = new HashMap<>(); for (ClassDescription clazz : classes) { String pack = clazz.packge(); String module = package2Modules.get(pack); if (module == null) { module = "java.base"; OUTER: while (!pack.isEmpty()) { for (Entry p2M : package2Modules.entrySet()) { if (p2M.getKey().startsWith(pack)) { module = p2M.getValue(); break OUTER; } } int dot = pack.lastIndexOf('.'); if (dot == (-1)) break; pack = pack.substring(0, dot); } } module2Classes.computeIfAbsent(module, m -> new ArrayList<>()) .add(clazz); } modules.keySet() .stream() .filter(m -> !module2Classes.containsKey(m)) .forEach(m -> module2Classes.put(m, Collections.emptyList())); Files.createDirectories(ctDescriptionFile.getParent()); int year = Calendar.getInstance(TimeZone.getTimeZone("UTF"), Locale.ROOT) .get(Calendar.YEAR); try (Writer symbolsOut = Files.newBufferedWriter(ctDescriptionFile)) { Map> outputFiles = new LinkedHashMap<>(); for (PlatformInput desc : versions) { List files = desc.files; if (files == null || forceWriteVersions.contains(desc.version)) { files = new ArrayList<>(); for (Entry> e : module2Classes.entrySet()) { StringWriter data = new StringWriter(); ModuleDescription module = modules.get(e.getKey()); if (module != null) { //module == null should only be in tests. module.write(data, desc.basePlatform, desc.version); } for (ClassDescription clazz : e.getValue()) { clazz.write(data, desc.basePlatform, desc.version); } String fileName = e.getKey() + "-" + desc.version + ".sym.txt"; Path f = ctDescriptionFile.getParent().resolve(fileName); String dataString = data.toString(); if (!dataString.isEmpty()) { String existingYear = null; boolean hasChange = true; if (Files.isReadable(f)) { String oldContent = Files.readString(f, StandardCharsets.UTF_8); int yearPos = DO_NOT_MODIFY.indexOf("{YEAR}"); String headerPattern = Pattern.quote(DO_NOT_MODIFY.substring(0, yearPos)) + "([0-9]+)(, [0-9]+)?" + Pattern.quote(DO_NOT_MODIFY.substring(yearPos + "{YEAR}".length())); String pattern = headerPattern + Pattern.quote(dataString); Matcher m = Pattern.compile(pattern, Pattern.MULTILINE).matcher(oldContent); if (m.matches()) { hasChange = false; } else { m = Pattern.compile(headerPattern).matcher(oldContent); if (m.find()) { existingYear = m.group(1); } } } if (hasChange) { try (Writer out = Files.newBufferedWriter(f, StandardCharsets.UTF_8)) { String currentYear = String.valueOf(year); String yearSpec = (existingYear != null && !currentYear.equals(existingYear) ? existingYear + ", " : "") + currentYear; out.append(DO_NOT_MODIFY.replace("{YEAR}", yearSpec)); out.write(dataString); } } files.add(f.getFileName().toString()); } } } outputFiles.put(desc, files); } symbolsOut.append(DO_NOT_MODIFY.replace("{YEAR}", "2015, " + year)); symbolsOut.append("#command used to generate this file:\n"); symbolsOut.append("#") .append(CreateSymbols.class.getName()) .append(" ") .append(Arrays.stream(args) .collect(Collectors.joining(" "))) .append("\n"); symbolsOut.append("#\n"); symbolsOut.append("generate platforms ") .append(versions.stream() .map(v -> v.version) .sorted() .collect(Collectors.joining(":"))) .append("\n"); for (Entry> versionFileEntry : outputFiles.entrySet()) { symbolsOut.append("platform version ") .append(versionFileEntry.getKey().version); if (versionFileEntry.getKey().basePlatform != null) { symbolsOut.append(" base ") .append(versionFileEntry.getKey().basePlatform); } symbolsOut.append(" files ") .append(versionFileEntry.getValue() .stream() .map(p -> p) .sorted() .collect(Collectors.joining(":"))) .append("\n"); } } } private void incrementalUpdate(String ctDescriptionFile, String excludeFile, String platformVersion, Iterable classBytes, Function baseline, String[] args) throws IOException { String currentVersion = Integer.toString(Integer.parseInt(platformVersion), Character.MAX_RADIX); String version = currentVersion.toUpperCase(Locale.ROOT); Path ctDescriptionPath = Paths.get(ctDescriptionFile).toAbsolutePath(); LoadDescriptions data = load(null, ctDescriptionPath); ClassList classes = data.classes; Map modules = data.modules; List versions = data.versions; ExcludeIncludeList excludeList = ExcludeIncludeList.create(excludeFile); String computedBaseline = baseline.apply(data); loadVersionClasses(classes, modules, classBytes, excludeList, "$", computedBaseline); removeVersion(data, version); for (ModuleDescription md : data.modules.values()) { for (ModuleHeaderDescription header : md.header) { header.versions = header.versions.replace("$", version); } } for (ClassDescription clazzDesc : data.classes) { for (ClassHeaderDescription header : clazzDesc.header) { header.versions = header.versions.replace("$", version); } for (MethodDescription method : clazzDesc.methods) { method.versions = method.versions.replace("$", version); } for (FieldDescription field : clazzDesc.fields) { field.versions = field.versions.replace("$", version); } } if (versions.stream().noneMatch(inp -> version.equals(inp.version))) { versions.add(new PlatformInput(null, version, computedBaseline, null)); } Set writeVersions = new HashSet<>(); writeVersions.add(version); //re-write all platforms that have version as their baseline: versions.stream() .filter(inp -> version.equals(inp.basePlatform)) .map(inp -> inp.version) .forEach(writeVersions::add); dumpDescriptions(classes, modules, versions, writeVersions, ctDescriptionPath, args); } public void createIncrementalBaseLineFromDataFile(String ctDescriptionFile, String excludeFile, String version, String dataFile, String baseline, String[] args) throws IOException { incrementalUpdate(ctDescriptionFile, excludeFile, version, loadClassData(dataFile), x -> baseline, args); } public void createIncrementalBaseLine(String ctDescriptionFile, String excludeFile, String[] args) throws IOException { String platformVersion = System.getProperty("java.specification.version"); String currentVersion = Integer.toString(Integer.parseInt(platformVersion), Character.MAX_RADIX); String version = currentVersion.toUpperCase(Locale.ROOT); Iterable classBytes = dumpCurrentClasses(); Function baseline = data -> { if (data.versions.isEmpty()) { return null; } else { return data.versions.stream() .filter(v -> v.version.compareTo(version) < 0) .sorted((v1, v2) -> v2.version.compareTo(v1.version)) .findFirst() .get() .version; } }; incrementalUpdate(ctDescriptionFile, excludeFile, platformVersion, classBytes, baseline, args); } private List dumpCurrentClasses() throws IOException { Set includedModuleNames = new HashSet<>(); String version = System.getProperty("java.specification.version"); JavaFileManager moduleFM = setupJavac("--release", version); for (Location modLoc : LOCATIONS) { for (Set module : moduleFM.listLocationsForModules(modLoc)) { for (JavaFileManager.Location loc : module) { includedModuleNames.add(moduleFM.inferModuleName(loc)); } } } JavaFileManager dumpFM = setupJavac("--source", version); List data = new ArrayList<>(); for (Location modLoc : LOCATIONS) { for (String moduleName : includedModuleNames) { Location loc = dumpFM.getLocationForModule(modLoc, moduleName); if (loc == null) { continue; } Iterable files = dumpFM.list(loc, "", EnumSet.of(Kind.CLASS), true); for (JavaFileObject jfo : files) { try (InputStream is = jfo.openInputStream(); InputStream in = new BufferedInputStream(is)) { ByteArrayOutputStream baos = new ByteArrayOutputStream(); in.transferTo(baos); data.add(baos.toByteArray()); } } } } return data; } //where: private static final List LOCATIONS = List.of(StandardLocation.SYSTEM_MODULES, StandardLocation.UPGRADE_MODULE_PATH); private JavaFileManager setupJavac(String... options) { JavacTool tool = JavacTool.create(); Context ctx = new Context(); JavacTask task = tool.getTask(null, null, null, List.of(options), null, null, ctx); task.getElements().getTypeElement("java.lang.Object"); return ctx.get(JavaFileManager.class); } // //non-final for tests: public static String PROFILE_ANNOTATION = "Ljdk/Profile+Annotation;"; public static boolean ALLOW_NON_EXISTING_CLASSES = false; private void inspectClassFile(InputStream in, ClassList classes, ExcludeIncludeList excludesIncludes, String version) throws IOException { inspectClassFile(in, classes, excludesIncludes, version, cf -> {}); } private void inspectClassFile(InputStream in, ClassList classes, ExcludeIncludeList excludesIncludes, String version, Consumer extraTask) throws IOException { ClassModel cm = ClassFile.of().parse(in.readAllBytes()); if (cm.isModuleInfo()) { return ; } if (!excludesIncludes.accepts(cm.thisClass().asInternalName(), true)) { return ; } extraTask.accept(cm); ClassHeaderDescription headerDesc = new ClassHeaderDescription(); headerDesc.flags = cm.flags().flagsMask(); if (cm.superclass().isPresent()) { headerDesc.extendsAttr = cm.superclass().get().asInternalName(); } headerDesc.implementsAttr = cm.interfaces().stream().map(ClassEntry::asInternalName).collect(Collectors.toList()); for (var attr : cm.attributes()) { if (!readAttribute(headerDesc, attr)) return ; } ClassDescription clazzDesc = null; for (ClassDescription cd : classes) { if (cd.name.equals(cm.thisClass().asInternalName())) { clazzDesc = cd; break; } } if (clazzDesc == null) { clazzDesc = new ClassDescription(); clazzDesc.name = cm.thisClass().asInternalName(); classes.add(clazzDesc); } addClassHeader(clazzDesc, headerDesc, version, null); for (var m : cm.methods()) { if (!include(m.flags().flagsMask())) continue; MethodDescription methDesc = new MethodDescription(); methDesc.flags = m.flags().flagsMask(); methDesc.name = m.methodName().stringValue(); methDesc.descriptor = m.methodType().stringValue(); for (var attr : m.attributes()) { readAttribute(methDesc, attr); } addMethod(clazzDesc, methDesc, version, null); } for (var f : cm.fields()) { if (!include(f.flags().flagsMask())) continue; FieldDescription fieldDesc = new FieldDescription(); fieldDesc.flags = f.flags().flagsMask(); fieldDesc.name = f.fieldName().stringValue(); fieldDesc.descriptor = f.fieldType().stringValue(); for (var attr : f.attributes()) { readAttribute(fieldDesc, attr); } addField(clazzDesc, fieldDesc, version, null); } } private ModuleDescription inspectModuleInfoClassFile(byte[] data, Map modules, String version) { ClassModel cm = ClassFile.of().parse(data); if (!cm.flags().has(AccessFlag.MODULE)) { return null; } ModuleHeaderDescription headerDesc = new ModuleHeaderDescription(); headerDesc.versions = version; headerDesc.flags = cm.flags().flagsMask(); for (var attr : cm.attributes()) { if (!readAttribute(headerDesc, attr)) return null; } String name = headerDesc.name; ModuleDescription moduleDesc = modules.get(name); if (moduleDesc == null) { moduleDesc = new ModuleDescription(); moduleDesc.name = name; modules.put(moduleDesc.name, moduleDesc); } addModuleHeader(moduleDesc, headerDesc, version); return moduleDesc; } private Set enhancedIncludesListBasedOnClassHeaders(ClassList classes, Iterable classData) { Set additionalIncludes = new HashSet<>(); for (byte[] classFileData : classData) { additionalIncludes.addAll(otherRelevantTypesWithOwners(ClassFile.of().parse(classFileData))); } return additionalIncludes; } private Set otherRelevantTypesWithOwners(ClassModel cm) { Set supertypes = new HashSet<>(); if (cm.flags().has(AccessFlag.MODULE)) { return supertypes; } Set additionalClasses = new HashSet<>(); if (cm.superclass().isPresent()) { additionalClasses.add(cm.superclass().get().asInternalName()); } for (var iface : cm.interfaces()) { additionalClasses.add(iface.asInternalName()); } var permitted = cm.findAttribute(Attributes.permittedSubclasses()).orElse(null); if (permitted != null) { for (var sub : permitted.permittedSubclasses()) { additionalClasses.add(sub.asInternalName()); } } for (String additional : additionalClasses) { int dollar; supertypes.add(additional); while ((dollar = additional.lastIndexOf('$')) != (-1)) { additional = additional.substring(0, dollar); supertypes.add(additional); } } return supertypes; } private void addModuleHeader(ModuleDescription moduleDesc, ModuleHeaderDescription headerDesc, String version) { //normalize: boolean existed = false; for (ModuleHeaderDescription existing : moduleDesc.header) { if (existing.equals(headerDesc)) { headerDesc = existing; existed = true; } } if (!headerDesc.versions.contains(version)) { headerDesc.versions += version; } if (!existed) { moduleDesc.header.add(headerDesc); } } private boolean include(int accessFlags) { return (accessFlags & (ACC_PUBLIC | ACC_PROTECTED)) != 0; } private void addClassHeader(ClassDescription clazzDesc, ClassHeaderDescription headerDesc, String version, String baseline) { //normalize: Iterable headers = sortedHeaders(clazzDesc.header, baseline); boolean existed = false; for (ClassHeaderDescription existing : headers) { if (existing.equals(headerDesc)) { headerDesc = existing; existed = true; break; } } if (!existed) { //check if the only difference between the 7 and 8 version is the Profile annotation //if so, copy it to the pre-8 version, so save space for (ClassHeaderDescription existing : headers) { List annots = existing.classAnnotations; if (annots != null) { for (AnnotationDescription ad : annots) { if (PROFILE_ANNOTATION.equals(ad.annotationType)) { existing.classAnnotations = new ArrayList<>(annots); existing.classAnnotations.remove(ad); if (existing.equals(headerDesc)) { headerDesc = existing; existed = true; } existing.classAnnotations = annots; break; } } } } } if (!headerDesc.versions.contains(version)) { headerDesc.versions += version; } if (!existed) { clazzDesc.header.add(headerDesc); } } private Iterable sortedHeaders(List headers, String baseline) { if (baseline == null) { return headers; } //move the description whose version contains baseline to the front: List result = new ArrayList<>(headers); for (Iterator it = result.iterator(); it.hasNext();) { T fd = it.next(); if (fd.versions.contains(baseline)) { it.remove(); result.add(0, fd); break; } } return result; } private void addMethod(ClassDescription clazzDesc, MethodDescription methDesc, String version, String baseline) { //normalize: boolean methodExisted = false; for (MethodDescription existing : clazzDesc.methods) { if (existing.equals(methDesc) && (!methodExisted || (baseline != null && existing.versions.contains(baseline)))) { methodExisted = true; methDesc = existing; } } methDesc.versions += version; if (!methodExisted) { clazzDesc.methods.add(methDesc); } } private void addField(ClassDescription clazzDesc, FieldDescription fieldDesc, String version, String baseline) { boolean fieldExisted = false; for (FieldDescription existing : clazzDesc.fields) { if (existing.equals(fieldDesc) && (!fieldExisted || (baseline != null && existing.versions.contains(baseline)))) { fieldExisted = true; fieldDesc = existing; } } fieldDesc.versions += version; if (!fieldExisted) { clazzDesc.fields.add(fieldDesc); } } private boolean readAttribute(FeatureDescription feature, Attribute attr) { switch (attr) { case AnnotationDefaultAttribute a -> ((MethodDescription) feature).annotationDefaultValue = convertElementValue(a.defaultValue()); case DeprecatedAttribute _ -> feature.deprecated = true; case ExceptionsAttribute a -> ((MethodDescription) feature).thrownTypes = a.exceptions().stream().map(ClassEntry::asInternalName).collect(Collectors.toList()); case InnerClassesAttribute a -> { if (feature instanceof ModuleHeaderDescription) break; //XXX ((ClassHeaderDescription) feature).innerClasses = a.classes().stream().map(cfi -> { var info = new InnerClassInfo(); info.innerClass = cfi.innerClass().asInternalName(); info.outerClass = cfi.outerClass().map(ClassEntry::asInternalName).orElse(null); info.innerClassName = cfi.innerName().map(Utf8Entry::stringValue).orElse(null); info.innerClassFlags = cfi.flagsMask(); return info; }).collect(Collectors.toList()); } case RuntimeInvisibleAnnotationsAttribute a -> feature.classAnnotations = annotations2Description(a.annotations()); case RuntimeVisibleAnnotationsAttribute a -> feature.runtimeAnnotations = annotations2Description(a.annotations()); case SignatureAttribute a -> feature.signature = a.signature().stringValue(); case ConstantValueAttribute a -> { var f = (FieldDescription) feature; f.constantValue = convertConstantValue(a.constant(), f.descriptor); } case SourceFileAttribute _, BootstrapMethodsAttribute _, CodeAttribute _, SyntheticAttribute _ -> {} case EnclosingMethodAttribute _ -> { return false; } case RuntimeVisibleParameterAnnotationsAttribute a -> ((MethodDescription) feature).runtimeParameterAnnotations = parameterAnnotations2Description(a.parameterAnnotations()); case RuntimeInvisibleParameterAnnotationsAttribute a -> ((MethodDescription) feature).classParameterAnnotations = parameterAnnotations2Description(a.parameterAnnotations()); case ModuleAttribute a -> { ModuleHeaderDescription header = (ModuleHeaderDescription) feature; header.name = a.moduleName().name().stringValue(); header.exports = a.exports().stream().map(ExportsDescription::create).collect(Collectors.toList()); if (header.extraModulePackages != null) { header.exports.forEach(ed -> header.extraModulePackages.remove(ed.packageName())); } header.requires = a.requires().stream().map(RequiresDescription::create).collect(Collectors.toList()); header.uses = a.uses().stream().map(ClassEntry::asInternalName).collect(Collectors.toList()); header.provides = a.provides().stream().map(ProvidesDescription::create).collect(Collectors.toList()); } case ModuleTargetAttribute a -> ((ModuleHeaderDescription) feature).moduleTarget = a.targetPlatform().stringValue(); case ModuleResolutionAttribute a -> ((ModuleHeaderDescription) feature).moduleResolution = a.resolutionFlags(); case ModulePackagesAttribute a -> { var header = (ModuleHeaderDescription) feature; header.extraModulePackages = a.packages().stream().mapMulti((packageItem, sink) -> { var packageName = packageItem.name().stringValue(); if (header.exports == null || header.exports.stream().noneMatch(ed -> ed.packageName().equals(packageName))) { sink.accept(packageName); } }).collect(Collectors.toList()); } case ModuleHashesAttribute _ -> {} case NestHostAttribute a -> ((ClassHeaderDescription) feature).nestHost = a.nestHost().asInternalName(); case NestMembersAttribute a -> ((ClassHeaderDescription) feature).nestMembers = a.nestMembers().stream().map(ClassEntry::asInternalName).collect(Collectors.toList()); case RecordAttribute a -> { var chd = (ClassHeaderDescription) feature; chd.isRecord = true; chd.recordComponents = a.components().stream().map(rci -> { var rcd = new RecordComponentDescription(); rcd.name = rci.name().stringValue(); rcd.descriptor = rci.descriptor().stringValue(); rci.attributes().forEach(child -> readAttribute(rcd, child)); return rcd; }).collect(Collectors.toList()); } case MethodParametersAttribute a -> ((MethodDescription) feature).methodParameters = a.parameters().stream() .map(mpi -> new MethodDescription.MethodParam(mpi.flagsMask(), mpi.name().map(Utf8Entry::stringValue).orElse(null))) .collect(Collectors.toList()); case PermittedSubclassesAttribute a -> { var chd = (ClassHeaderDescription) feature; chd.isSealed = true; chd.permittedSubclasses = a.permittedSubclasses().stream().map(ClassEntry::asInternalName).collect(Collectors.toList()); } case ModuleMainClassAttribute a -> ((ModuleHeaderDescription) feature).moduleMainClass = a.mainClass().asInternalName(); default -> throw new IllegalArgumentException("Unhandled attribute: " + attr.attributeName()); // Do nothing } return true; } public static String INJECTED_VERSION = null; private static String getVersion(Optional version) { if (INJECTED_VERSION != null) { return INJECTED_VERSION; } return version.map(Utf8Entry::stringValue).orElse(null); } Object convertConstantValue(ConstantValueEntry info, String descriptor) { if (descriptor.length() == 1 && info instanceof IntegerEntry ie) { var i = ie.intValue(); return switch (descriptor.charAt(0)) { case 'I', 'B', 'S' -> i; case 'C' -> (char) i; case 'Z' -> i == 1; default -> throw new IllegalArgumentException(descriptor); }; } return info.constantValue(); } Object convertElementValue(AnnotationValue val) { return switch (val) { case AnnotationValue.OfConstant oc -> oc.resolvedValue(); case AnnotationValue.OfEnum oe -> new EnumConstant(oe.className().stringValue(), oe.constantName().stringValue()); case AnnotationValue.OfClass oc -> new ClassConstant(oc.className().stringValue()); case AnnotationValue.OfArray oa -> oa.values().stream().map(this::convertElementValue).collect(Collectors.toList()); case AnnotationValue.OfAnnotation oa -> annotation2Description(oa.annotation()); }; } private List annotations2Description(List annos) { return annos.stream().map(this::annotation2Description).collect(Collectors.toList()); } private List> parameterAnnotations2Description(List> annos) { return annos.stream().map(this::annotations2Description).collect(Collectors.toList()); } private AnnotationDescription annotation2Description(java.lang.classfile.Annotation a) { String annotationType = a.className().stringValue(); Map values = new HashMap<>(); for (var e : a.elements()) { values.put(e.name().stringValue(), convertElementValue(e.value())); } return new AnnotationDescription(annotationType, values); } // protected boolean includeEffectiveAccess(ClassList classes, ClassDescription clazz) { if (!include(clazz.header.get(0).flags)) return false; for (ClassDescription outer : classes.enclosingClasses(clazz)) { if (!include(outer.header.get(0).flags)) return false; } return true; } void include(Set includedClasses, ClassList classes, String clazzName) { if (clazzName == null) return ; ClassDescription desc = classes.find(clazzName, true); if (desc == null) { return ; } includedClasses.add(clazzName); for (ClassDescription outer : classes.enclosingClasses(desc)) { includedClasses.add(outer.name); } } void includeOutputType(Iterable features, Function feature2Descriptor, Set includedClasses, ClassList classes) { for (T feature : features) { CharSequence sig = feature.signature != null ? feature.signature : feature2Descriptor.apply(feature); Matcher m = OUTPUT_TYPE_PATTERN.matcher(sig); while (m.find()) { include(includedClasses, classes, m.group(1)); } } } static final Pattern OUTPUT_TYPE_PATTERN = Pattern.compile("L([^;<]+)(;|<)"); public static class VersionDescription { public final String classes; public final String version; public final String primaryBaseline; public VersionDescription(String classes, String version, String primaryBaseline) { this.classes = classes; this.version = version; this.primaryBaseline = "".equals(primaryBaseline) ? null : primaryBaseline; } } public static class ExcludeIncludeList { public final Set includeList; public final Set privateIncludeList; public final Set excludeList; protected ExcludeIncludeList(Set includeList, Set excludeList) { this(includeList, Set.of(), excludeList); } protected ExcludeIncludeList(Set includeList, Set privateIncludeList, Set excludeList) { this.includeList = includeList; this.privateIncludeList = privateIncludeList; this.excludeList = excludeList; } public static ExcludeIncludeList create(String files) throws IOException { Set includeList = new HashSet<>(); Set excludeList = new HashSet<>(); for (String file : files.split(File.pathSeparator)) { try (Stream lines = Files.lines(Paths.get(file))) { lines.map(l -> l.substring(0, l.indexOf('#') != (-1) ? l.indexOf('#') : l.length())) .filter(l -> !l.trim().isEmpty()) .forEach(l -> { Set target = l.startsWith("+") ? includeList : excludeList; target.add(l.substring(1)); }); } } return new ExcludeIncludeList(includeList, excludeList); } public boolean accepts(String className, boolean includePrivateClasses) { return (matches(includeList, className) || (includePrivateClasses && matches(privateIncludeList, className))) && !matches(excludeList, className); } private static boolean matches(Set list, String className) { if (list.contains(className)) return true; String pack = className.substring(0, className.lastIndexOf('/') + 1); return list.contains(pack); } } // // static boolean checkChange(String versions, String version, String baselineVersion) { return versions.contains(version) ^ (baselineVersion != null && versions.contains(baselineVersion)); } static abstract class FeatureDescription { int flagsNormalization = ~0; int flags; boolean deprecated; String signature; String versions = ""; List classAnnotations; List runtimeAnnotations; protected void writeAttributes(Appendable output) throws IOException { if (flags != 0) output.append(" flags " + Integer.toHexString(flags)); if (deprecated) { output.append(" deprecated true"); } if (signature != null) { output.append(" signature " + quote(signature, false)); } if (classAnnotations != null && !classAnnotations.isEmpty()) { output.append(" classAnnotations "); for (AnnotationDescription a : classAnnotations) { output.append(quote(a.toString(), false)); } } if (runtimeAnnotations != null && !runtimeAnnotations.isEmpty()) { output.append(" runtimeAnnotations "); for (AnnotationDescription a : runtimeAnnotations) { output.append(quote(a.toString(), false)); } } } protected boolean shouldIgnore(String baselineVersion, String version) { return (!versions.contains(version) && (baselineVersion == null || !versions.contains(baselineVersion))) || (baselineVersion != null && versions.contains(baselineVersion) && versions.contains(version)); } public abstract void write(Appendable output, String baselineVersion, String version) throws IOException; protected void readAttributes(LineBasedReader reader) { String inFlags = reader.attributes.get("flags"); if (inFlags != null && !inFlags.isEmpty()) { flags = Integer.parseInt(inFlags, 16); } String inDeprecated = reader.attributes.get("deprecated"); if ("true".equals(inDeprecated)) { deprecated = true; } signature = reader.attributes.get("signature"); String inClassAnnotations = reader.attributes.get("classAnnotations"); if (inClassAnnotations != null) { classAnnotations = parseAnnotations(inClassAnnotations, new int[1]); } String inRuntimeAnnotations = reader.attributes.get("runtimeAnnotations"); if (inRuntimeAnnotations != null) { runtimeAnnotations = parseAnnotations(inRuntimeAnnotations, new int[1]); } } public abstract boolean read(LineBasedReader reader) throws IOException; @Override public int hashCode() { int hash = 3; hash = 89 * hash + (this.flags & flagsNormalization); hash = 89 * hash + (this.deprecated ? 1 : 0); hash = 89 * hash + Objects.hashCode(this.signature); hash = 89 * hash + listHashCode(this.classAnnotations); hash = 89 * hash + listHashCode(this.runtimeAnnotations); return hash; } @Override public boolean equals(Object obj) { if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } final FeatureDescription other = (FeatureDescription) obj; if ((this.flags & flagsNormalization) != (other.flags & flagsNormalization)) { return false; } if (this.deprecated != other.deprecated) { return false; } if (!Objects.equals(this.signature, other.signature)) { return false; } if (!listEquals(this.classAnnotations, other.classAnnotations)) { return false; } if (!listEquals(this.runtimeAnnotations, other.runtimeAnnotations)) { return false; } return true; } } public static class ModuleDescription { String name; List header = new ArrayList<>(); public void write(Appendable output, String baselineVersion, String version) throws IOException { boolean inBaseline = false; boolean inVersion = false; for (ModuleHeaderDescription mhd : header) { if (baselineVersion != null && mhd.versions.contains(baselineVersion)) { inBaseline = true; } if (mhd.versions.contains(version)) { inVersion = true; } } if (!inVersion && !inBaseline) return ; if (!inVersion) { output.append("-module name " + name + "\n\n"); return; } boolean hasChange = hasChange(header, version, baselineVersion); if (!hasChange) return; output.append("module name " + name + "\n"); for (ModuleHeaderDescription header : header) { header.write(output, baselineVersion, version); } output.append("\n"); } boolean hasChange(List hasChange, String version, String baseline) { return hasChange.stream() .map(fd -> fd.versions) .anyMatch(versions -> checkChange(versions, version, baseline)); } public void read(LineBasedReader reader, String baselineVersion, String version) throws IOException { if (!"module".equals(reader.lineKey)) return ; name = reader.attributes.get("name"); reader.moveNext(); OUTER: while (reader.hasNext()) { switch (reader.lineKey) { case "header": removeVersion(header, h -> true, version); ModuleHeaderDescription mhd = new ModuleHeaderDescription(); mhd.read(reader); mhd.name = name; mhd.versions = version; header.add(mhd); break; case "class": case "-class": case "module": case "-module": break OUTER; default: throw new IllegalArgumentException(reader.lineKey); } } } } static class ModuleHeaderDescription extends HeaderDescription { String name; List exports = new ArrayList<>(); List opens = new ArrayList<>(); List extraModulePackages = new ArrayList<>(); List requires = new ArrayList<>(); List uses = new ArrayList<>(); List provides = new ArrayList<>(); Integer moduleResolution; String moduleTarget; String moduleMainClass; @Override public int hashCode() { int hash = super.hashCode(); hash = 83 * hash + Objects.hashCode(this.name); hash = 83 * hash + Objects.hashCode(this.exports); hash = 83 * hash + Objects.hashCode(this.opens); hash = 83 * hash + Objects.hashCode(this.extraModulePackages); hash = 83 * hash + Objects.hashCode(this.requires); hash = 83 * hash + Objects.hashCode(this.uses); hash = 83 * hash + Objects.hashCode(this.provides); hash = 83 * hash + Objects.hashCode(this.moduleResolution); hash = 83 * hash + Objects.hashCode(this.moduleTarget); hash = 83 * hash + Objects.hashCode(this.moduleMainClass); return hash; } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (!super.equals(obj)) { return false; } final ModuleHeaderDescription other = (ModuleHeaderDescription) obj; if (!Objects.equals(this.name, other.name)) { return false; } if (!listEquals(this.exports, other.exports)) { return false; } if (!listEquals(this.opens, other.opens)) { return false; } if (!listEquals(this.extraModulePackages, other.extraModulePackages)) { return false; } if (!listEquals(this.requires, other.requires)) { return false; } if (!listEquals(this.uses, other.uses)) { return false; } if (!listEquals(this.provides, other.provides)) { return false; } if (!Objects.equals(this.moduleTarget, other.moduleTarget)) { return false; } if (!Objects.equals(this.moduleResolution, other.moduleResolution)) { return false; } if (!Objects.equals(this.moduleMainClass, other.moduleMainClass)) { return false; } return true; } @Override public void write(Appendable output, String baselineVersion, String version) throws IOException { if (!versions.contains(version) || (baselineVersion != null && versions.contains(baselineVersion) && versions.contains(version))) return ; output.append("header"); if (exports != null && !exports.isEmpty()) { List exportsList = exports.stream() .map(exp -> exp.serialize()) .collect(Collectors.toList()); output.append(" exports " + serializeList(exportsList)); } if (opens != null && !opens.isEmpty()) output.append(" opens " + serializeList(opens)); if (extraModulePackages != null && !extraModulePackages.isEmpty()) output.append(" extraModulePackages " + serializeList(extraModulePackages)); if (requires != null && !requires.isEmpty()) { List requiresList = requires.stream() .map(req -> req.serialize()) .collect(Collectors.toList()); output.append(" requires " + serializeList(requiresList)); } if (uses != null && !uses.isEmpty()) output.append(" uses " + serializeList(uses)); if (provides != null && !provides.isEmpty()) { List providesList = provides.stream() .map(p -> p.serialize()) .collect(Collectors.toList()); output.append(" provides " + serializeList(providesList)); } if (moduleTarget != null) output.append(" target " + quote(moduleTarget, true)); if (moduleResolution != null) output.append(" resolution " + quote(Integer.toHexString(moduleResolution), true)); if (moduleMainClass != null) output.append(" moduleMainClass " + quote(moduleMainClass, true)); writeAttributes(output); output.append("\n"); writeInnerClasses(output, baselineVersion, version); } private static Map splitAttributes(String data) { String[] parts = data.split(" "); Map attributes = new HashMap<>(); for (int i = 0; i < parts.length; i += 2) { attributes.put(parts[i], unquote(parts[i + 1])); } return attributes; } @Override public boolean read(LineBasedReader reader) throws IOException { if (!"header".equals(reader.lineKey)) return false; List exportsList = deserializeList(reader.attributes.get("exports"), false); exports = exportsList.stream() .map(ExportsDescription::deserialize) .collect(Collectors.toList()); opens = deserializeList(reader.attributes.get("opens")); extraModulePackages = deserializeList(reader.attributes.get("extraModulePackages")); List requiresList = deserializeList(reader.attributes.get("requires")); requires = requiresList.stream() .map(RequiresDescription::deserialize) .collect(Collectors.toList()); uses = deserializeList(reader.attributes.get("uses")); List providesList = deserializeList(reader.attributes.get("provides"), false); provides = providesList.stream() .map(ProvidesDescription::deserialize) .collect(Collectors.toList()); moduleTarget = reader.attributes.get("target"); if (reader.attributes.containsKey("resolution")) { final String resolutionFlags = reader.attributes.get("resolution"); moduleResolution = Integer.parseInt(resolutionFlags, 16); } moduleMainClass = reader.attributes.get("moduleMainClass"); readAttributes(reader); reader.moveNext(); readInnerClasses(reader); return true; } public Stream allPackages() { List packages = new ArrayList<>(); exports.stream() .map(ExportsDescription::packageName) .forEach(packages::add); if (extraModulePackages != null) { packages.addAll(extraModulePackages); } return packages.stream() .map(p -> p.replace('/', '.')); } record ExportsDescription(String packageName, List to) { public String serialize() { return packageName + (isQualified() ? "[" + quote(serializeList(to), true, true) + "]" : ""); } public static ExportsDescription deserialize(String data) { int bracket = data.indexOf("["); String packageName; List to; if (bracket != (-1)) { packageName = data.substring(0, bracket); to = deserializeList(unquote(data.substring(bracket + 1, data.length() - 1))); } else { packageName = data; to = null; } return new ExportsDescription(packageName, to); } public static ExportsDescription create(ModuleExportInfo ee) { String packageName = ee.exportedPackage().name().stringValue(); List to = null; if (!ee.exportsTo().isEmpty()) { to = ee.exportsTo().stream().map(m -> m.name().stringValue()).collect(Collectors.toList()); } return new ExportsDescription(packageName, to); } public boolean isQualified() { return to != null && !to.isEmpty(); } } static class RequiresDescription { final String moduleName; final int flags; final String version; public RequiresDescription(String moduleName, int flags, String version) { this.moduleName = moduleName; this.flags = flags; this.version = version; } public String serialize() { String versionKeyValue = version != null ? " version " + quote(version, true) : ""; return "name " + quote(moduleName, true) + " flags " + quote(Integer.toHexString(flags), true) + versionKeyValue; } public static RequiresDescription deserialize(String data) { Map attributes = splitAttributes(data); String ver = attributes.containsKey("version") ? attributes.get("version") : null; int flags = Integer.parseInt(attributes.get("flags"), 16); return new RequiresDescription(attributes.get("name"), flags, ver); } public static RequiresDescription create(ModuleRequireInfo req) { String mod = req.requires().name().stringValue(); String ver = getVersion(req.requiresVersion()); return new RequiresDescription(mod, req.requiresFlagsMask(), ver); } @Override public int hashCode() { int hash = 7; hash = 53 * hash + Objects.hashCode(this.moduleName); hash = 53 * hash + this.flags; hash = 53 * hash + Objects.hashCode(this.version); return hash; } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } final RequiresDescription other = (RequiresDescription) obj; if (this.flags != other.flags) { return false; } if (!Objects.equals(this.moduleName, other.moduleName)) { return false; } if (!Objects.equals(this.version, other.version)) { return false; } return true; } } static class ProvidesDescription { final String interfaceName; final List implNames; public ProvidesDescription(String interfaceName, List implNames) { this.interfaceName = interfaceName; this.implNames = implNames; } public String serialize() { return "interface " + quote(interfaceName, true) + " impls " + quote(serializeList(implNames), true, true); } public static ProvidesDescription deserialize(String data) { Map attributes = splitAttributes(data); List implsList = deserializeList(attributes.get("impls"), false); return new ProvidesDescription(attributes.get("interface"), implsList); } public static ProvidesDescription create(ModuleProvideInfo prov) { String api = prov.provides().asInternalName(); List impls = prov.providesWith().stream().map(ClassEntry::asInternalName).collect(Collectors.toList()); return new ProvidesDescription(api, impls); } @Override public int hashCode() { int hash = 5; hash = 53 * hash + Objects.hashCode(this.interfaceName); hash = 53 * hash + Objects.hashCode(this.implNames); return hash; } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } final ProvidesDescription other = (ProvidesDescription) obj; if (!Objects.equals(this.interfaceName, other.interfaceName)) { return false; } if (!Objects.equals(this.implNames, other.implNames)) { return false; } return true; } } } public static class ClassDescription { String name; List header = new ArrayList<>(); List methods = new ArrayList<>(); List fields = new ArrayList<>(); public void write(Appendable output, String baselineVersion, String version) throws IOException { boolean inBaseline = false; boolean inVersion = false; for (ClassHeaderDescription chd : header) { if (baselineVersion != null && chd.versions.contains(baselineVersion)) { inBaseline = true; } if (chd.versions.contains(version)) { inVersion = true; } } if (!inVersion && !inBaseline) return ; if (!inVersion) { output.append("-class name " + name + "\n\n"); return; } boolean hasChange = hasChange(header, version, baselineVersion) || hasChange(fields, version, baselineVersion) || hasChange(methods, version, baselineVersion); if (!hasChange) return; output.append("class name " + name + "\n"); for (ClassHeaderDescription header : header) { header.write(output, baselineVersion, version); } for (FieldDescription field : fields) { if (!field.versions.contains(version)) { field.write(output, baselineVersion, version); } } for (MethodDescription method : methods) { if (!method.versions.contains(version)) { method.write(output, baselineVersion, version); } } for (FieldDescription field : fields) { if (field.versions.contains(version)) { field.write(output, baselineVersion, version); } } for (MethodDescription method : methods) { if (method.versions.contains(version)) { method.write(output, baselineVersion, version); } } output.append("\n"); } boolean hasChange(List hasChange, String version, String baseline) { return hasChange.stream() .map(fd -> fd.versions) .anyMatch(versions -> checkChange(versions, version, baseline)); } public void read(LineBasedReader reader, String baselineVersion, String version) throws IOException { if (!"class".equals(reader.lineKey)) return ; name = reader.attributes.get("name"); reader.moveNext(); OUTER: while (reader.hasNext()) { switch (reader.lineKey) { case "header": removeVersion(header, h -> true, version); ClassHeaderDescription chd = new ClassHeaderDescription(); chd.read(reader); chd.versions = version; header.add(chd); break; case "field": FieldDescription field = new FieldDescription(); field.read(reader); field.versions += version; fields.add(field); break; case "-field": { removeVersion(fields, f -> Objects.equals(f.name, reader.attributes.get("name")) && Objects.equals(f.descriptor, reader.attributes.get("descriptor")), version); reader.moveNext(); break; } case "method": MethodDescription method = new MethodDescription(); method.read(reader); method.versions += version; methods.add(method); break; case "-method": { removeVersion(methods, m -> Objects.equals(m.name, reader.attributes.get("name")) && Objects.equals(m.descriptor, reader.attributes.get("descriptor")), version); reader.moveNext(); break; } case "class": case "-class": case "module": case "-module": break OUTER; default: throw new IllegalArgumentException(reader.lineKey); } } } public String packge() { String pack; int lastSlash = name.lastIndexOf('/'); if (lastSlash != (-1)) { pack = name.substring(0, lastSlash).replace('/', '.'); } else { pack = ""; } return pack; } @Override public String toString() { return name; } } static class ClassHeaderDescription extends HeaderDescription { String extendsAttr; List implementsAttr; String nestHost; List nestMembers; boolean isRecord; List recordComponents; boolean isSealed; List permittedSubclasses; @Override public int hashCode() { int hash = super.hashCode(); hash = 17 * hash + Objects.hashCode(this.extendsAttr); hash = 17 * hash + Objects.hashCode(this.implementsAttr); hash = 17 * hash + Objects.hashCode(this.nestHost); hash = 17 * hash + Objects.hashCode(this.nestMembers); hash = 17 * hash + Objects.hashCode(this.isRecord); hash = 17 * hash + Objects.hashCode(this.recordComponents); hash = 17 * hash + Objects.hashCode(this.isSealed); hash = 17 * hash + Objects.hashCode(this.permittedSubclasses); return hash; } @Override public boolean equals(Object obj) { if (obj == null) { return false; } if (!super.equals(obj)) { return false; } final ClassHeaderDescription other = (ClassHeaderDescription) obj; if (!Objects.equals(this.extendsAttr, other.extendsAttr)) { return false; } if (!Objects.equals(this.implementsAttr, other.implementsAttr)) { return false; } if (!Objects.equals(this.nestHost, other.nestHost)) { return false; } if (!listEquals(this.nestMembers, other.nestMembers)) { return false; } if (this.isRecord != other.isRecord) { return false; } if (!listEquals(this.recordComponents, other.recordComponents)) { return false; } if (this.isSealed != other.isSealed) { return false; } if (!listEquals(this.permittedSubclasses, other.permittedSubclasses)) { return false; } return true; } @Override public void write(Appendable output, String baselineVersion, String version) throws IOException { if (!versions.contains(version) || (baselineVersion != null && versions.contains(baselineVersion) && versions.contains(version))) return ; output.append("header"); if (extendsAttr != null) output.append(" extends " + extendsAttr); if (implementsAttr != null && !implementsAttr.isEmpty()) output.append(" implements " + serializeList(implementsAttr)); if (nestHost != null) output.append(" nestHost " + nestHost); if (nestMembers != null && !nestMembers.isEmpty()) output.append(" nestMembers " + serializeList(nestMembers)); if (isRecord) { output.append(" record true"); } if (isSealed) { output.append(" sealed true"); output.append(" permittedSubclasses " + serializeList(permittedSubclasses)); } writeAttributes(output); output.append("\n"); writeRecordComponents(output, baselineVersion, version); writeInnerClasses(output, baselineVersion, version); } @Override public boolean read(LineBasedReader reader) throws IOException { if (!"header".equals(reader.lineKey)) return false; extendsAttr = reader.attributes.get("extends"); String elementsList = reader.attributes.get("implements"); implementsAttr = deserializeList(elementsList); nestHost = reader.attributes.get("nestHost"); String nestMembersList = reader.attributes.get("nestMembers"); nestMembers = deserializeList(nestMembersList); isRecord = reader.attributes.containsKey("record"); isSealed = reader.attributes.containsKey("permittedSubclasses"); if (isSealed) { String subclassesList = reader.attributes.get("permittedSubclasses"); permittedSubclasses = deserializeList(subclassesList); } readAttributes(reader); reader.moveNext(); if (isRecord) { readRecordComponents(reader); } readInnerClasses(reader); return true; } protected void writeRecordComponents(Appendable output, String baselineVersion, String version) throws IOException { if (recordComponents != null) { for (RecordComponentDescription rcd : recordComponents) { rcd.write(output, "", ""); } } } protected void readRecordComponents(LineBasedReader reader) throws IOException { recordComponents = new ArrayList<>(); while ("recordcomponent".equals(reader.lineKey)) { RecordComponentDescription rcd = new RecordComponentDescription(); rcd.read(reader); recordComponents.add(rcd); } } } static abstract class HeaderDescription extends FeatureDescription { List innerClasses; @Override public int hashCode() { int hash = super.hashCode(); hash = 19 * hash + Objects.hashCode(this.innerClasses); return hash; } @Override public boolean equals(Object obj) { if (obj == null) { return false; } if (!super.equals(obj)) { return false; } final HeaderDescription other = (HeaderDescription) obj; if (!listEquals(this.innerClasses, other.innerClasses)) { return false; } return true; } protected void writeInnerClasses(Appendable output, String baselineVersion, String version) throws IOException { if (innerClasses != null && !innerClasses.isEmpty()) { for (InnerClassInfo ici : innerClasses) { output.append("innerclass"); output.append(" innerClass " + ici.innerClass); output.append(" outerClass " + ici.outerClass); output.append(" innerClassName " + ici.innerClassName); output.append(" flags " + Integer.toHexString(ici.innerClassFlags)); output.append("\n"); } } } protected void readInnerClasses(LineBasedReader reader) throws IOException { innerClasses = new ArrayList<>(); while ("innerclass".equals(reader.lineKey)) { InnerClassInfo info = new InnerClassInfo(); info.innerClass = reader.attributes.get("innerClass"); info.outerClass = reader.attributes.get("outerClass"); info.innerClassName = reader.attributes.get("innerClassName"); String inFlags = reader.attributes.get("flags"); if (inFlags != null && !inFlags.isEmpty()) info.innerClassFlags = Integer.parseInt(inFlags, 16); innerClasses.add(info); reader.moveNext(); } } } static class MethodDescription extends FeatureDescription { static int METHODS_FLAGS_NORMALIZATION = ~0; String name; String descriptor; List thrownTypes; Object annotationDefaultValue; List> classParameterAnnotations; List> runtimeParameterAnnotations; List methodParameters; public MethodDescription() { flagsNormalization = METHODS_FLAGS_NORMALIZATION; } @Override public int hashCode() { int hash = super.hashCode(); hash = 59 * hash + Objects.hashCode(this.name); hash = 59 * hash + Objects.hashCode(this.descriptor); hash = 59 * hash + Objects.hashCode(this.thrownTypes); hash = 59 * hash + Objects.hashCode(this.annotationDefaultValue); return hash; } @Override public boolean equals(Object obj) { if (obj == null) { return false; } if (!super.equals(obj)) { return false; } final MethodDescription other = (MethodDescription) obj; if (!Objects.equals(this.name, other.name)) { return false; } if (!Objects.equals(this.descriptor, other.descriptor)) { return false; } if (!Objects.equals(this.thrownTypes, other.thrownTypes)) { return false; } if (!Objects.equals(this.annotationDefaultValue, other.annotationDefaultValue)) { return false; } return true; } @Override public void write(Appendable output, String baselineVersion, String version) throws IOException { if (shouldIgnore(baselineVersion, version)) return ; if (!versions.contains(version)) { output.append("-method"); output.append(" name " + quote(name, false)); output.append(" descriptor " + quote(descriptor, false)); output.append("\n"); return ; } output.append("method"); output.append(" name " + quote(name, false)); output.append(" descriptor " + quote(descriptor, false)); if (thrownTypes != null) output.append(" thrownTypes " + serializeList(thrownTypes)); if (annotationDefaultValue != null) output.append(" annotationDefaultValue " + quote(AnnotationDescription.dumpAnnotationValue(annotationDefaultValue), false)); writeAttributes(output); if (classParameterAnnotations != null && !classParameterAnnotations.isEmpty()) { output.append(" classParameterAnnotations "); for (List pa : classParameterAnnotations) { for (AnnotationDescription a : pa) { output.append(quote(a.toString(), false)); } output.append(";"); } } if (runtimeParameterAnnotations != null && !runtimeParameterAnnotations.isEmpty()) { output.append(" runtimeParameterAnnotations "); for (List pa : runtimeParameterAnnotations) { for (AnnotationDescription a : pa) { output.append(quote(a.toString(), false)); } output.append(";"); } } if (methodParameters != null && !methodParameters.isEmpty()) { Function param2String = p -> Integer.toHexString(p.flags) + ":" + p.name; List paramsAsStrings = methodParameters.stream() .map(param2String) .collect(Collectors.toList()); output.append(" methodParameters " + serializeList(paramsAsStrings)); } output.append("\n"); } @Override public boolean read(LineBasedReader reader) throws IOException { if (!"method".equals(reader.lineKey)) return false; name = reader.attributes.get("name"); descriptor = reader.attributes.get("descriptor"); String thrownTypesValue = reader.attributes.get("thrownTypes"); if (thrownTypesValue != null) { thrownTypes = deserializeList(thrownTypesValue); } String inAnnotationDefaultValue = reader.attributes.get("annotationDefaultValue"); if (inAnnotationDefaultValue != null) { annotationDefaultValue = parseAnnotationValue(inAnnotationDefaultValue, new int[1]); } readAttributes(reader); String inClassParamAnnotations = reader.attributes.get("classParameterAnnotations"); if (inClassParamAnnotations != null) { List> annos = new ArrayList<>(); int[] pointer = new int[1]; do { annos.add(parseAnnotations(inClassParamAnnotations, pointer)); assert pointer[0] == inClassParamAnnotations.length() || inClassParamAnnotations.charAt(pointer[0]) == ';'; } while (++pointer[0] < inClassParamAnnotations.length()); classParameterAnnotations = annos; } String inRuntimeParamAnnotations = reader.attributes.get("runtimeParameterAnnotations"); if (inRuntimeParamAnnotations != null) { List> annos = new ArrayList<>(); int[] pointer = new int[1]; do { annos.add(parseAnnotations(inRuntimeParamAnnotations, pointer)); assert pointer[0] == inRuntimeParamAnnotations.length() || inRuntimeParamAnnotations.charAt(pointer[0]) == ';'; } while (++pointer[0] < inRuntimeParamAnnotations.length()); runtimeParameterAnnotations = annos; } String inMethodParameters = reader.attributes.get("methodParameters"); if (inMethodParameters != null) { Function string2Param = p -> { int sep = p.indexOf(':'); return new MethodParam(Integer.parseInt(p.substring(0, sep), 16), p.substring(sep + 1)); }; methodParameters = deserializeList(inMethodParameters).stream() .map(string2Param) .collect(Collectors.toList()); } reader.moveNext(); return true; } public static class MethodParam { public final int flags; public final String name; public MethodParam(int flags, String name) { this.flags = flags; this.name = name; } } } static class FieldDescription extends FeatureDescription { String name; String descriptor; Object constantValue; // Uses (unsigned) Integer for byte/short String keyName = "field"; @Override public int hashCode() { int hash = super.hashCode(); hash = 59 * hash + Objects.hashCode(this.name); hash = 59 * hash + Objects.hashCode(this.descriptor); hash = 59 * hash + Objects.hashCode(this.constantValue); return hash; } @Override public boolean equals(Object obj) { if (obj == null) { return false; } if (!super.equals(obj)) { return false; } final FieldDescription other = (FieldDescription) obj; if (!Objects.equals(this.name, other.name)) { return false; } if (!Objects.equals(this.descriptor, other.descriptor)) { return false; } if (!Objects.equals(this.constantValue, other.constantValue)) { return false; } return true; } @Override public void write(Appendable output, String baselineVersion, String version) throws IOException { if (shouldIgnore(baselineVersion, version)) return ; if (!versions.contains(version)) { output.append("-" + keyName); output.append(" name " + quote(name, false)); output.append(" descriptor " + quote(descriptor, false)); output.append("\n"); return ; } output.append(keyName); output.append(" name " + name); output.append(" descriptor " + descriptor); if (constantValue != null) { output.append(" constantValue " + quote(constantValue.toString(), false)); } writeAttributes(output); output.append("\n"); } @Override public boolean read(LineBasedReader reader) throws IOException { if (!keyName.equals(reader.lineKey)) return false; name = reader.attributes.get("name"); descriptor = reader.attributes.get("descriptor"); String inConstantValue = reader.attributes.get("constantValue"); if (inConstantValue != null) { switch (descriptor) { case "Z": constantValue = "true".equals(inConstantValue); break; case "B": constantValue = Integer.parseInt(inConstantValue); break; case "C": constantValue = inConstantValue.charAt(0); break; case "S": constantValue = Integer.parseInt(inConstantValue); break; case "I": constantValue = Integer.parseInt(inConstantValue); break; case "J": constantValue = Long.parseLong(inConstantValue); break; case "F": constantValue = Float.parseFloat(inConstantValue); break; case "D": constantValue = Double.parseDouble(inConstantValue); break; case "Ljava/lang/String;": constantValue = inConstantValue; break; default: throw new IllegalArgumentException("Unrecognized field type: " + descriptor); } } readAttributes(reader); reader.moveNext(); return true; } } static final class RecordComponentDescription extends FieldDescription { public RecordComponentDescription() { this.keyName = "recordcomponent"; } @Override protected boolean shouldIgnore(String baselineVersion, String version) { return false; } } static final class AnnotationDescription { String annotationType; Map values; public AnnotationDescription(String annotationType, Map values) { this.annotationType = annotationType; this.values = values; } @Override public int hashCode() { int hash = 7; hash = 47 * hash + Objects.hashCode(this.annotationType); hash = 47 * hash + Objects.hashCode(this.values); return hash; } @Override public boolean equals(Object obj) { if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } final AnnotationDescription other = (AnnotationDescription) obj; if (!Objects.equals(this.annotationType, other.annotationType)) { return false; } if (!Objects.equals(this.values, other.values)) { return false; } return true; } @Override public String toString() { StringBuilder result = new StringBuilder(); result.append("@" + annotationType); if (!values.isEmpty()) { result.append("("); boolean first = true; for (Entry e : values.entrySet()) { if (!first) { result.append(","); } first = false; result.append(e.getKey()); result.append("="); result.append(dumpAnnotationValue(e.getValue())); result.append(""); } result.append(")"); } return result.toString(); } private static String dumpAnnotationValue(Object value) { if (value instanceof List) { StringBuilder result = new StringBuilder(); result.append("{"); for (Object element : ((List) value)) { result.append(dumpAnnotationValue(element)); } result.append("}"); return result.toString(); } if (value instanceof String) { return "\"" + quote((String) value, true) + "\""; } else if (value instanceof Boolean) { return "Z" + value; } else if (value instanceof Byte) { return "B" + value; } if (value instanceof Character) { return "C" + value; } if (value instanceof Short) { return "S" + value; } if (value instanceof Integer) { return "I" + value; } if (value instanceof Long) { return "J" + value; } if (value instanceof Float) { return "F" + value; } if (value instanceof Double) { return "D" + value; } else { return value.toString(); } } } static final class EnumConstant { String type; String constant; public EnumConstant(String type, String constant) { this.type = type; this.constant = constant; } @Override public String toString() { return "e" + type + constant + ";"; } @Override public int hashCode() { int hash = 7; hash = 19 * hash + Objects.hashCode(this.type); hash = 19 * hash + Objects.hashCode(this.constant); return hash; } @Override public boolean equals(Object obj) { if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } final EnumConstant other = (EnumConstant) obj; if (!Objects.equals(this.type, other.type)) { return false; } if (!Objects.equals(this.constant, other.constant)) { return false; } return true; } } static final class ClassConstant { String type; public ClassConstant(String type) { this.type = type; } @Override public String toString() { return "c" + type; } @Override public int hashCode() { int hash = 3; hash = 53 * hash + Objects.hashCode(this.type); return hash; } @Override public boolean equals(Object obj) { if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } final ClassConstant other = (ClassConstant) obj; if (!Objects.equals(this.type, other.type)) { return false; } return true; } } static final class InnerClassInfo { String innerClass; String outerClass; String innerClassName; int innerClassFlags; @Override public int hashCode() { int hash = 3; hash = 11 * hash + Objects.hashCode(this.innerClass); hash = 11 * hash + Objects.hashCode(this.outerClass); hash = 11 * hash + Objects.hashCode(this.innerClassName); hash = 11 * hash + Objects.hashCode(this.innerClassFlags); return hash; } @Override public boolean equals(Object obj) { if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } final InnerClassInfo other = (InnerClassInfo) obj; if (!Objects.equals(this.innerClass, other.innerClass)) { return false; } if (!Objects.equals(this.outerClass, other.outerClass)) { return false; } if (!Objects.equals(this.innerClassName, other.innerClassName)) { return false; } if (!Objects.equals(this.innerClassFlags, other.innerClassFlags)) { return false; } return true; } } public static final class ClassList implements Iterable { private final List classes = new ArrayList<>(); private final Map name2Class = new HashMap<>(); private final Map inner2Outter = new HashMap<>(); @Override public Iterator iterator() { return classes.iterator(); } public void add(ClassDescription desc) { classes.add(desc); name2Class.put(desc.name, desc); } public ClassDescription find(String name) { return find(name, ALLOW_NON_EXISTING_CLASSES); } public ClassDescription find(String name, boolean allowNull) { ClassDescription desc = name2Class.get(name); if (desc != null || allowNull) return desc; throw new IllegalArgumentException("Cannot find: " + name); } private static final ClassDescription NONE = new ClassDescription(); public ClassDescription enclosingClass(ClassDescription clazz) { if (clazz == null) return null; ClassDescription desc = inner2Outter.computeIfAbsent(clazz, c -> { ClassHeaderDescription header = clazz.header.get(0); if (header.innerClasses != null) { for (InnerClassInfo ici : header.innerClasses) { if (ici.innerClass.equals(clazz.name)) { return find(ici.outerClass); } } } return NONE; }); return desc != NONE ? desc : null; } public Iterable enclosingClasses(ClassDescription clazz) { List result = new ArrayList<>(); ClassDescription outer = enclosingClass(clazz); while (outer != null) { result.add(outer); outer = enclosingClass(outer); } return result; } public void sort() { Collections.sort(classes, (cd1, cd2) -> cd1.name.compareTo(cd2.name)); } } private static int listHashCode(Collection c) { return c == null || c.isEmpty() ? 0 : c.hashCode(); } private static boolean listEquals(Collection c1, Collection c2) { if (c1 == c2) return true; if (c1 == null && c2.isEmpty()) return true; if (c2 == null && c1.isEmpty()) return true; return Objects.equals(c1, c2); } private static String serializeList(List list) { StringBuilder result = new StringBuilder(); String sep = ""; for (Object o : list) { result.append(sep); result.append(o); sep = ","; } return quote(result.toString(), false); } private static List deserializeList(String serialized) { return deserializeList(serialized, true); } private static List deserializeList(String serialized, boolean unquote) { serialized = unquote ? unquote(serialized) : serialized; if (serialized == null) return new ArrayList<>(); return new ArrayList<>(List.of(serialized.split(","))); } private static String quote(String value, boolean quoteQuotes) { return quote(value, quoteQuotes, false); } private static String quote(String value, boolean quoteQuotes, boolean quoteCommas) { StringBuilder result = new StringBuilder(); for (char c : value.toCharArray()) { if (c <= 32 || c >= 127 || c == '\\' || (quoteQuotes && c == '"') || (quoteCommas && c == ',')) { result.append("\\u" + String.format("%04X", (int) c) + ";"); } else { result.append(c); } } return result.toString(); } private static final Pattern unicodePattern = Pattern.compile("\\\\u([0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F])"); private static String unquote(String value) { if (value == null) return null; StringBuilder result = new StringBuilder(); Matcher m = unicodePattern.matcher(value); int lastStart = 0; while (m.find(lastStart)) { result.append(value.substring(lastStart, m.start())); result.append((char) Integer.parseInt(m.group(1), 16)); lastStart = m.end() + 1; } result.append(value.substring(lastStart, value.length())); return result.toString(); } private static String readDigits(String value, int[] valuePointer) { int start = valuePointer[0]; if (value.charAt(valuePointer[0]) == '-') valuePointer[0]++; while (valuePointer[0] < value.length() && Character.isDigit(value.charAt(valuePointer[0]))) valuePointer[0]++; return value.substring(start, valuePointer[0]); } private static String className(String value, int[] valuePointer) { int start = valuePointer[0]; while (value.charAt(valuePointer[0]++) != ';') ; return value.substring(start, valuePointer[0]); } private static Object parseAnnotationValue(String value, int[] valuePointer) { switch (value.charAt(valuePointer[0]++)) { case 'Z': if ("true".equals(value.substring(valuePointer[0], valuePointer[0] + 4))) { valuePointer[0] += 4; return true; } else if ("false".equals(value.substring(valuePointer[0], valuePointer[0] + 5))) { valuePointer[0] += 5; return false; } else { throw new IllegalArgumentException("Unrecognized boolean structure: " + value); } case 'B': return Byte.parseByte(readDigits(value, valuePointer)); case 'C': return value.charAt(valuePointer[0]++); case 'S': return Short.parseShort(readDigits(value, valuePointer)); case 'I': return Integer.parseInt(readDigits(value, valuePointer)); case 'J': return Long.parseLong(readDigits(value, valuePointer)); case 'F': return Float.parseFloat(readDigits(value, valuePointer)); case 'D': return Double.parseDouble(readDigits(value, valuePointer)); case 'c': return new ClassConstant(className(value, valuePointer)); case 'e': return new EnumConstant(className(value, valuePointer), className(value, valuePointer).replaceFirst(";$", "")); case '{': List elements = new ArrayList<>(); //TODO: a good test for this would be highly desirable while (value.charAt(valuePointer[0]) != '}') { elements.add(parseAnnotationValue(value, valuePointer)); } valuePointer[0]++; return elements; case '"': int start = valuePointer[0]; while (value.charAt(valuePointer[0]) != '"') valuePointer[0]++; return unquote(value.substring(start, valuePointer[0]++)); case '@': return parseAnnotation(value, valuePointer); default: throw new IllegalArgumentException("Unrecognized signature type: " + value.charAt(valuePointer[0] - 1) + "; value=" + value); } } public static List parseAnnotations(String encoded, int[] pointer) { ArrayList result = new ArrayList<>(); while (pointer[0] < encoded.length() && encoded.charAt(pointer[0]) == '@') { pointer[0]++; result.add(parseAnnotation(encoded, pointer)); } return result; } private static AnnotationDescription parseAnnotation(String value, int[] valuePointer) { String className = className(value, valuePointer); Map attribute2Value = new HashMap<>(); if (valuePointer[0] < value.length() && value.charAt(valuePointer[0]) == '(') { while (value.charAt(valuePointer[0]) != ')') { int nameStart = ++valuePointer[0]; while (value.charAt(valuePointer[0]++) != '='); String name = value.substring(nameStart, valuePointer[0] - 1); attribute2Value.put(name, parseAnnotationValue(value, valuePointer)); } valuePointer[0]++; } return new AnnotationDescription(className, attribute2Value); } // /**Create sig files for ct.sym reading the classes description from the directory that contains * {@code ctDescriptionFile}, using the file as a recipe to create the sigfiles. */ public void createJavadocData(String ctDescriptionFileExtra, String ctDescriptionFile, String targetDir, int startVersion) throws IOException { LoadDescriptions data = load(ctDescriptionFileExtra != null ? Paths.get(ctDescriptionFileExtra) : null, Paths.get(ctDescriptionFile)); Path target = Paths.get(targetDir); for (PlatformInput version : data.versions) { int versionNumber = Integer.parseInt(version.version, Character.MAX_RADIX); if (versionNumber < startVersion) { continue; } Path outputFile = target.resolve("element-list-" + versionNumber + ".txt"); Files.createDirectories(outputFile.getParent()); try (Writer w = Files.newBufferedWriter(outputFile, StandardCharsets.UTF_8)) { Set modules = new TreeSet<>((m1, m2) -> m1.name.compareTo(m2.name)); modules.addAll(data.modules.values()); for (ModuleDescription module : modules) { if ("jdk.unsupported".equals(module.name)) { continue; } Optional header = module.header.stream().filter(h -> h.versions.contains(version.version)).findAny(); if (header.isEmpty()) { continue; } w.write("module:" + module.name); w.write("\n"); for (ExportsDescription export : header.get().exports) { if (export.isQualified()) { continue; } w.write(export.packageName.replace('/', '.')); w.write("\n"); } } } } } private static void help() { System.err.println("Help..."); } public static void main(String... args) throws IOException { if (args.length < 1) { help(); return ; } switch (args[0]) { case "build-description": { if (args.length < 3) { help(); return ; } Path descDest = Paths.get(args[1]); List versions = new ArrayList<>(); for (int i = 3; i + 2 < args.length; i += 3) { versions.add(new VersionDescription(args[i + 1], args[i], args[i + 2])); } Files.walkFileTree(descDest, new FileVisitor() { @Override public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { return FileVisitResult.CONTINUE; } @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { Files.delete(file); return FileVisitResult.CONTINUE; } @Override public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException { return FileVisitResult.CONTINUE; } @Override public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { Files.delete(dir); return FileVisitResult.CONTINUE; } }); ExcludeIncludeList excludeList = ExcludeIncludeList.create(args[2]); new CreateSymbols().createBaseLine(versions, excludeList, descDest, args); break; } case "build-description-incremental-file": { if (args.length != 6 && args.length != 7) { help(); return ; } if (args.length == 7) { if ("--normalize-method-flags".equals(args[6])) { MethodDescription.METHODS_FLAGS_NORMALIZATION = ~(0x100 | 0x20); } else { help(); return ; } } new CreateSymbols().createIncrementalBaseLineFromDataFile(args[1], args[2], args[3], args[4], "".equals(args[5]) ? null : args[5], args); break; } case "build-description-incremental": { if (args.length != 3) { help(); return ; } new CreateSymbols().createIncrementalBaseLine(args[1], args[2], args); break; } case "build-ctsym": { String ctDescriptionFileExtra; String ctDescriptionFile; String ctSymLocation; String timestampSpec; String currentVersion; String preReleaseTag; String moduleClasses; String includedModules; if (args.length == 8) { ctDescriptionFileExtra = null; ctDescriptionFile = args[1]; ctSymLocation = args[2]; timestampSpec = args[3]; currentVersion = args[4]; preReleaseTag = args[5]; moduleClasses = args[6]; includedModules = args[7]; } else if (args.length == 9) { ctDescriptionFileExtra = args[1]; ctDescriptionFile = args[2]; ctSymLocation = args[3]; timestampSpec = args[4]; currentVersion = args[5]; preReleaseTag = args[6]; moduleClasses = args[7]; includedModules = args[8]; } else { help(); return ; } long timestamp = Long.parseLong(timestampSpec); //SOURCE_DATE_EPOCH is in seconds, convert to milliseconds: timestamp *= 1000; new CreateSymbols().createSymbols(ctDescriptionFileExtra, ctDescriptionFile, ctSymLocation, timestamp, currentVersion, preReleaseTag, moduleClasses, includedModules); break; } case "build-javadoc-data": { String ctDescriptionFileExtra; String ctDescriptionFile; String targetDir; int startVersion; if (args.length == 4) { ctDescriptionFileExtra = null; ctDescriptionFile = args[1]; targetDir = args[2]; startVersion = Integer.parseInt(args[3]); } else if (args.length == 5) { ctDescriptionFileExtra = args[1]; ctDescriptionFile = args[2]; targetDir = args[3]; startVersion = Integer.parseInt(args[4]); } else { help(); return ; } if (startVersion < 9) { System.err.println("The start version must be at least 9!"); return ; } new CreateSymbols().createJavadocData(ctDescriptionFileExtra, ctDescriptionFile, targetDir, startVersion); break; } } } }