--- /dev/null 2015-04-26 06:51:08.003313989 -0700 +++ new/make/src/classes/build/tools/symbolgenerator/CreateSymbols.java 2015-06-01 08:15:46.558368252 -0700 @@ -0,0 +1,2481 @@ +/* + * Copyright (c) 2006, 2015, 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 java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.Writer; +import java.nio.file.DirectoryStream; +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.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Objects; +import java.util.Set; +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 com.sun.tools.classfile.AccessFlags; +import com.sun.tools.classfile.Annotation; +import com.sun.tools.classfile.Annotation.Annotation_element_value; +import com.sun.tools.classfile.Annotation.Array_element_value; +import com.sun.tools.classfile.Annotation.Class_element_value; +import com.sun.tools.classfile.Annotation.Enum_element_value; +import com.sun.tools.classfile.Annotation.Primitive_element_value; +import com.sun.tools.classfile.Annotation.element_value; +import com.sun.tools.classfile.Annotation.element_value_pair; +import com.sun.tools.classfile.AnnotationDefault_attribute; +import com.sun.tools.classfile.Attribute; +import com.sun.tools.classfile.Attributes; +import com.sun.tools.classfile.ClassFile; +import com.sun.tools.classfile.ClassWriter; +import com.sun.tools.classfile.ConstantPool; +import com.sun.tools.classfile.ConstantPool.CONSTANT_Class_info; +import com.sun.tools.classfile.ConstantPool.CONSTANT_Double_info; +import com.sun.tools.classfile.ConstantPool.CONSTANT_Float_info; +import com.sun.tools.classfile.ConstantPool.CONSTANT_Integer_info; +import com.sun.tools.classfile.ConstantPool.CONSTANT_Long_info; +import com.sun.tools.classfile.ConstantPool.CONSTANT_String_info; +import com.sun.tools.classfile.ConstantPool.CONSTANT_Utf8_info; +import com.sun.tools.classfile.ConstantPool.CPInfo; +import com.sun.tools.classfile.ConstantPool.InvalidIndex; +import com.sun.tools.classfile.ConstantPoolException; +import com.sun.tools.classfile.ConstantValue_attribute; +import com.sun.tools.classfile.Deprecated_attribute; +import com.sun.tools.classfile.Descriptor; +import com.sun.tools.classfile.Exceptions_attribute; +import com.sun.tools.classfile.Field; +import com.sun.tools.classfile.InnerClasses_attribute; +import com.sun.tools.classfile.InnerClasses_attribute.Info; +import com.sun.tools.classfile.Method; +import com.sun.tools.classfile.RuntimeAnnotations_attribute; +import com.sun.tools.classfile.RuntimeInvisibleAnnotations_attribute; +import com.sun.tools.classfile.RuntimeInvisibleParameterAnnotations_attribute; +import com.sun.tools.classfile.RuntimeParameterAnnotations_attribute; +import com.sun.tools.classfile.RuntimeVisibleAnnotations_attribute; +import com.sun.tools.classfile.RuntimeVisibleParameterAnnotations_attribute; +import com.sun.tools.classfile.Signature_attribute; +import com.sun.tools.javac.jvm.Target; +import com.sun.tools.javac.util.Assert; +import com.sun.tools.javac.util.Pair; + +/** + * A tool for processing the .sym.txt files. It 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 + * + * To convert the .sym.txt files to class/sig files from ct.sym, run: + * java build.tool.symbolgenerator.CreateSymbols build-ctsym [JOINED_VERSIONS|SEPARATE] + * + * 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 langtools/make/data/symbols $TOPDIR langtools/make/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. + */ + @SuppressWarnings("unchecked") + public void createSymbols(String ctDescriptionFile, String ctSymLocation, CtSymKind ctSymKind) throws IOException { + ClassList classes = load(Paths.get(ctDescriptionFile)); + + splitHeaders(classes); + + for (ClassDescription classDescription : classes) { + for (ClassHeaderDescription header : classDescription.header) { + switch (ctSymKind) { + case JOINED_VERSIONS: + Set jointVersions = new HashSet<>(); + jointVersions.add(header.versions); + limitJointVersion(jointVersions, classDescription.fields); + limitJointVersion(jointVersions, classDescription.methods); + writeClassesForVersions(ctSymLocation, classDescription, header, jointVersions); + break; + case SEPARATE: + Set versions = new HashSet<>(); + for (char v : header.versions.toCharArray()) { + versions.add("" + v); + } + writeClassesForVersions(ctSymLocation, classDescription, header, versions); + break; + } + } + } + } + + public static String EXTENSION = ".sig"; + + ClassList load(Path ctDescription) throws IOException { + List platforms = new ArrayList<>(); + Set generatePlatforms = null; + + try (LineBasedReader reader = new LineBasedReader(ctDescription)) { + while (reader.hasNext()) { + switch (reader.lineKey) { + case "generate": + String[] platformsAttr = reader.attributes.get("platforms").split(":"); + generatePlatforms = new HashSet<>(Arrays.asList(platformsAttr)); + reader.moveNext(); + break; + case "platform": + platforms.add(PlatformInput.load(reader)); + reader.moveNext(); + break; + default: + throw new IllegalStateException("Unknown key: " + reader.lineKey); + } + } + } + + Map classes = new LinkedHashMap<>(); + + for (PlatformInput platform: platforms) { + 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 (String input : platform.files) { + Path inputFile = ctDescription.getParent().resolve(input); + try (LineBasedReader reader = new LineBasedReader(inputFile)) { + while (reader.hasNext()) { + String nameAttr = reader.attributes.get("name"); + 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); + } + } + } + } + + ClassList result = new ClassList(); + + for (ClassDescription desc : classes.values()) { + for (Iterator chdIt = desc.header.iterator(); chdIt.hasNext();) { + ClassHeaderDescription chd = chdIt.next(); + + chd.versions = reduce(chd.versions, generatePlatforms); + if (chd.versions.isEmpty()) + chdIt.remove(); + } + + if (desc.header.isEmpty()) { + continue; + } + + for (Iterator methodIt = desc.methods.iterator(); methodIt.hasNext();) { + MethodDescription method = methodIt.next(); + + method.versions = reduce(method.versions, generatePlatforms); + if (method.versions.isEmpty()) + methodIt.remove(); + } + + for (Iterator fieldIt = desc.fields.iterator(); fieldIt.hasNext();) { + FieldDescription field = fieldIt.next(); + + field.versions = reduce(field.versions, generatePlatforms); + if (field.versions.isEmpty()) + fieldIt.remove(); + } + + result.add(desc); + } + + return result; + } + + 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 class PlatformInput { + public final String version; + public final String basePlatform; + public final List files; + public PlatformInput(String version, String basePlatform, List files) { + this.version = version; + this.basePlatform = basePlatform; + this.files = files; + } + + public static PlatformInput load(LineBasedReader in) throws IOException { + return new PlatformInput(in.attributes.get("version"), + in.attributes.get("base"), + Arrays.asList(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(versions, header.versions); + + newHeaders.add(newHeader); + } + } + + cd.header = newHeaders; + } + } + + void limitJointVersion(Set jointVersions, List features) { + for (FeatureDescription feature : features) { + for (String version : jointVersions) { + if (!containsAll(feature.versions, version) && + !disjoint(feature.versions, version)) { + StringBuilder featurePart = new StringBuilder(); + StringBuilder otherPart = new StringBuilder(); + for (char v : version.toCharArray()) { + if (feature.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(String ctSymLocation, + ClassDescription classDescription, + ClassHeaderDescription header, + Iterable versions) throws IOException { + for (String ver : versions) { + writeClass(ctSymLocation, classDescription, header, ver); + } + } + + public enum CtSymKind { + JOINED_VERSIONS, + SEPARATE; + } + + // + void writeClass(String ctSymLocation, + ClassDescription classDescription, + ClassHeaderDescription header, + String version) throws IOException { + List constantPool = new ArrayList<>(); + constantPool.add(null); + List methods = new ArrayList<>(); + for (MethodDescription methDesc : classDescription.methods) { + if (disjoint(methDesc.versions, version)) + continue; + Descriptor descriptor = new Descriptor(addString(constantPool, methDesc.descriptor)); + //TODO: LinkedHashMap to avoid param annotations vs. Signature problem in javac's ClassReader: + Map attributesMap = new LinkedHashMap<>(); + addAttributes(methDesc, constantPool, attributesMap); + Attributes attributes = new Attributes(attributesMap); + AccessFlags flags = new AccessFlags(methDesc.flags); + int nameString = addString(constantPool, methDesc.name); + methods.add(new Method(flags, nameString, descriptor, attributes)); + } + List fields = new ArrayList<>(); + for (FieldDescription fieldDesc : classDescription.fields) { + if (disjoint(fieldDesc.versions, version)) + continue; + Descriptor descriptor = new Descriptor(addString(constantPool, fieldDesc.descriptor)); + Map attributesMap = new HashMap<>(); + addAttributes(fieldDesc, constantPool, attributesMap); + Attributes attributes = new Attributes(attributesMap); + AccessFlags flags = new AccessFlags(fieldDesc.flags); + int nameString = addString(constantPool, fieldDesc.name); + fields.add(new Field(flags, nameString, descriptor, attributes)); + } + int currentClass = addClass(constantPool, classDescription.name); + int superclass = header.extendsAttr != null ? addClass(constantPool, header.extendsAttr) : 0; + int[] interfaces = new int[header.implementsAttr.size()]; + int i = 0; + for (String intf : header.implementsAttr) { + interfaces[i++] = addClass(constantPool, intf); + } + AccessFlags flags = new AccessFlags(header.flags); + Map attributesMap = new HashMap<>(); + addAttributes(header, constantPool, attributesMap); + Attributes attributes = new Attributes(attributesMap); + ConstantPool cp = new ConstantPool(constantPool.toArray(new CPInfo[constantPool.size()])); + ClassFile classFile = new ClassFile(0xCAFEBABE, + Target.DEFAULT.minorVersion, + Target.DEFAULT.majorVersion, + cp, + flags, + currentClass, + superclass, + interfaces, + fields.toArray(new Field[0]), + methods.toArray(new Method[0]), + attributes); + + Path outputClassFile = Paths.get(ctSymLocation, version, classDescription.name + EXTENSION); + + Files.createDirectories(outputClassFile.getParent()); + + try (OutputStream out = Files.newOutputStream(outputClassFile)) { + ClassWriter w = new ClassWriter(); + + w.write(classFile, out); + } + } + + private void addAttributes(ClassHeaderDescription header, List constantPool, Map attributes) { + addGenericAttributes(header, constantPool, attributes); + if (header.innerClasses != null && !header.innerClasses.isEmpty()) { + Info[] innerClasses = new Info[header.innerClasses.size()]; + int i = 0; + for (InnerClassInfo info : header.innerClasses) { + innerClasses[i++] = + new Info(info.innerClass == null ? 0 : addClass(constantPool, info.innerClass), + info.outerClass == null ? 0 : addClass(constantPool, info.outerClass), + info.innerClassName == null ? 0 : addString(constantPool, info.innerClassName), + new AccessFlags(info.innerClassFlags)); + } + int attributeString = addString(constantPool, Attribute.InnerClasses); + attributes.put(Attribute.InnerClasses, + new InnerClasses_attribute(attributeString, innerClasses)); + } + } + + private void addAttributes(MethodDescription desc, List constantPool, Map attributes) { + addGenericAttributes(desc, constantPool, attributes); + if (desc.thrownTypes != null && !desc.thrownTypes.isEmpty()) { + int[] exceptions = new int[desc.thrownTypes.size()]; + int i = 0; + for (String exc : desc.thrownTypes) { + exceptions[i++] = addClass(constantPool, exc); + } + int attributeString = addString(constantPool, Attribute.Exceptions); + attributes.put(Attribute.Exceptions, + new Exceptions_attribute(attributeString, exceptions)); + } + if (desc.annotationDefaultValue != null) { + int attributeString = addString(constantPool, Attribute.AnnotationDefault); + element_value attributeValue = createAttributeValue(constantPool, + desc.annotationDefaultValue); + attributes.put(Attribute.AnnotationDefault, + new AnnotationDefault_attribute(attributeString, attributeValue)); + } + if (desc.classParameterAnnotations != null && !desc.classParameterAnnotations.isEmpty()) { + int attributeString = + addString(constantPool, Attribute.RuntimeInvisibleParameterAnnotations); + Annotation[][] annotations = + createParameterAnnotations(constantPool, desc.classParameterAnnotations); + attributes.put(Attribute.RuntimeInvisibleParameterAnnotations, + new RuntimeInvisibleParameterAnnotations_attribute(attributeString, + annotations)); + } + if (desc.runtimeParameterAnnotations != null && !desc.runtimeParameterAnnotations.isEmpty()) { + int attributeString = + addString(constantPool, Attribute.RuntimeVisibleParameterAnnotations); + Annotation[][] annotations = + createParameterAnnotations(constantPool, desc.runtimeParameterAnnotations); + attributes.put(Attribute.RuntimeVisibleParameterAnnotations, + new RuntimeVisibleParameterAnnotations_attribute(attributeString, + annotations)); + } + } + + private void addAttributes(FieldDescription desc, List constantPool, Map attributes) { + addGenericAttributes(desc, constantPool, attributes); + if (desc.constantValue != null) { + Pair constantPoolEntry = + addConstant(constantPool, desc.constantValue, false); + Assert.checkNonNull(constantPoolEntry); + int constantValueString = addString(constantPool, Attribute.ConstantValue); + attributes.put(Attribute.ConstantValue, + new ConstantValue_attribute(constantValueString, constantPoolEntry.fst)); + } + } + + private void addGenericAttributes(FeatureDescription desc, List constantPool, Map attributes) { + if (desc.deprecated) { + int attributeString = addString(constantPool, Attribute.Deprecated); + attributes.put(Attribute.Deprecated, + new Deprecated_attribute(attributeString)); + } + if (desc.signature != null) { + int attributeString = addString(constantPool, Attribute.Signature); + int signatureString = addString(constantPool, desc.signature); + attributes.put(Attribute.Signature, + new Signature_attribute(attributeString, signatureString)); + } + if (desc.classAnnotations != null && !desc.classAnnotations.isEmpty()) { + int attributeString = addString(constantPool, Attribute.RuntimeInvisibleAnnotations); + Annotation[] annotations = createAnnotations(constantPool, desc.classAnnotations); + attributes.put(Attribute.RuntimeInvisibleAnnotations, + new RuntimeInvisibleAnnotations_attribute(attributeString, annotations)); + } + if (desc.runtimeAnnotations != null && !desc.runtimeAnnotations.isEmpty()) { + int attributeString = addString(constantPool, Attribute.RuntimeVisibleAnnotations); + Annotation[] annotations = createAnnotations(constantPool, desc.runtimeAnnotations); + attributes.put(Attribute.RuntimeVisibleAnnotations, + new RuntimeVisibleAnnotations_attribute(attributeString, annotations)); + } + } + + private Annotation[] createAnnotations(List constantPool, List desc) { + Annotation[] result = new Annotation[desc.size()]; + int i = 0; + + for (AnnotationDescription ad : desc) { + result[i++] = createAnnotation(constantPool, ad); + } + + return result; + } + + private Annotation[][] createParameterAnnotations(List constantPool, List> desc) { + Annotation[][] result = new Annotation[desc.size()][]; + int i = 0; + + for (List paramAnnos : desc) { + result[i++] = createAnnotations(constantPool, paramAnnos); + } + + return result; + } + + private Annotation createAnnotation(List constantPool, AnnotationDescription desc) { + return new Annotation(null, + addString(constantPool, desc.annotationType), + createElementPairs(constantPool, desc.values)); + } + + private element_value_pair[] createElementPairs(List constantPool, Map annotationAttributes) { + element_value_pair[] pairs = new element_value_pair[annotationAttributes.size()]; + int i = 0; + + for (Entry e : annotationAttributes.entrySet()) { + int elementNameString = addString(constantPool, e.getKey()); + element_value value = createAttributeValue(constantPool, e.getValue()); + pairs[i++] = new element_value_pair(elementNameString, value); + } + + return pairs; + } + + private element_value createAttributeValue(List constantPool, Object value) { + Pair constantPoolEntry = addConstant(constantPool, value, true); + if (constantPoolEntry != null) { + return new Primitive_element_value(constantPoolEntry.fst, constantPoolEntry.snd); + } else if (value instanceof EnumConstant) { + EnumConstant ec = (EnumConstant) value; + return new Enum_element_value(addString(constantPool, ec.type), + addString(constantPool, ec.constant), + 'e'); + } else if (value instanceof ClassConstant) { + ClassConstant cc = (ClassConstant) value; + return new Class_element_value(addString(constantPool, cc.type), 'c'); + } else if (value instanceof AnnotationDescription) { + Annotation annotation = createAnnotation(constantPool, ((AnnotationDescription) value)); + return new Annotation_element_value(annotation, '@'); + } else if (value instanceof Collection) { + @SuppressWarnings("unchecked") + Collection array = (Collection) value; + element_value[] values = new element_value[array.size()]; + int i = 0; + + for (Object elem : array) { + values[i++] = createAttributeValue(constantPool, elem); + } + + return new Array_element_value(values, '['); + } + throw new IllegalStateException(value.getClass().getName()); + } + + private static Pair addConstant(List constantPool, Object value, boolean annotation) { + if (value instanceof Boolean) { + return Pair.of(addToCP(constantPool, new CONSTANT_Integer_info(((Boolean) value) ? 1 : 0)), 'Z'); + } else if (value instanceof Byte) { + return Pair.of(addToCP(constantPool, new CONSTANT_Integer_info((byte) value)), 'B'); + } else if (value instanceof Character) { + return Pair.of(addToCP(constantPool, new CONSTANT_Integer_info((char) value)), 'C'); + } else if (value instanceof Short) { + return Pair.of(addToCP(constantPool, new CONSTANT_Integer_info((short) value)), 'S'); + } else if (value instanceof Integer) { + return Pair.of(addToCP(constantPool, new CONSTANT_Integer_info((int) value)), 'I'); + } else if (value instanceof Long) { + return Pair.of(addToCP(constantPool, new CONSTANT_Long_info((long) value)), 'J'); + } else if (value instanceof Float) { + return Pair.of(addToCP(constantPool, new CONSTANT_Float_info((float) value)), 'F'); + } else if (value instanceof Double) { + return Pair.of(addToCP(constantPool, new CONSTANT_Double_info((double) value)), 'D'); + } else if (value instanceof String) { + int stringIndex = addString(constantPool, (String) value); + if (annotation) { + return Pair.of(stringIndex, 's'); + } else { + return Pair.of(addToCP(constantPool, new CONSTANT_String_info(null, stringIndex)), 's'); + } + } + + return null; + } + + private static int addString(List constantPool, String string) { + Assert.checkNonNull(string); + + int i = 0; + for (CPInfo info : constantPool) { + if (info instanceof CONSTANT_Utf8_info) { + if (((CONSTANT_Utf8_info) info).value.equals(string)) { + return i; + } + } + i++; + } + + return addToCP(constantPool, new CONSTANT_Utf8_info(string)); + } + + private static int addToCP(List constantPool, CPInfo entry) { + int result = constantPool.size(); + + constantPool.add(entry); + + if (entry.size() > 1) { + constantPool.add(null); + } + + return result; + } + + private static int addClass(List constantPool, String className) { + int classNameIndex = addString(constantPool, className); + + int i = 0; + for (CPInfo info : constantPool) { + if (info instanceof CONSTANT_Class_info) { + if (((CONSTANT_Class_info) info).name_index == classNameIndex) { + return i; + } + } + i++; + } + + return addToCP(constantPool, new CONSTANT_Class_info(null, classNameIndex)); + } + // + // + + // + public void createBaseLine(List versions, ExcludeIncludeList excludesIncludes, Path descDest, Path jdkRoot) throws IOException { + ClassList classes = new ClassList(); + + for (VersionDescription desc : versions) { + ClassList currentVersionClasses = new ClassList(); + try (BufferedReader descIn = Files.newBufferedReader(Paths.get(desc.classes))) { + String classFileData; + + while ((classFileData = descIn.readLine()) != null) { + ByteArrayOutputStream data = new ByteArrayOutputStream(); + for (int i = 0; i < classFileData.length(); i += 2) { + data.write(Integer.parseInt(classFileData.substring(i, i + 2), 16)); + } + try (InputStream in = new ByteArrayInputStream(data.toByteArray())) { + inspectClassFile(in, currentVersionClasses, excludesIncludes, desc.version); + } catch (IOException | ConstantPoolException ex) { + throw new IllegalStateException(ex); + } + } + } + + Set includedClasses = new HashSet<>(); + boolean modified; + + do { + modified = false; + + for (ClassDescription clazz : currentVersionClasses) { + ClassHeaderDescription header = clazz.header.get(0); + + if (includeEffectiveAccess(currentVersionClasses, clazz)) { + modified |= include(includedClasses, currentVersionClasses, clazz.name); + } + + if (includedClasses.contains(clazz.name)) { + modified |= include(includedClasses, currentVersionClasses, header.extendsAttr); + for (String i : header.implementsAttr) { + modified |= include(includedClasses, currentVersionClasses, i); + } + + modified |= includeOutputType(Collections.singleton(header), + h -> "", + includedClasses, + currentVersionClasses); + modified |= includeOutputType(clazz.fields, + f -> f.descriptor, + includedClasses, + currentVersionClasses); + modified |= includeOutputType(clazz.methods, + m -> m.descriptor, + includedClasses, + currentVersionClasses); + } + } + } while (modified); + + for (ClassDescription clazz : currentVersionClasses) { + if (!includedClasses.contains(clazz.name)) { + continue; + } + + ClassHeaderDescription header = clazz.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(); + } + } + + ClassDescription existing = classes.find(clazz.name, true); + + if (existing != null) { + addClassHeader(existing, header, desc.version); + for (MethodDescription currentMethod : clazz.methods) { + addMethod(existing, currentMethod, desc.version); + } + for (FieldDescription currentField : clazz.fields) { + addField(existing, currentField, desc.version); + } + } else { + classes.add(clazz); + } + } + } + + classes.sort(); + + Map package2Modules = buildPackage2Modules(jdkRoot); + Map> module2Classes = new HashMap<>(); + + for (ClassDescription clazz : classes) { + String pack; + int lastSlash = clazz.name.lastIndexOf('/'); + if (lastSlash != (-1)) { + pack = clazz.name.substring(0, lastSlash).replace('/', '.'); + } else { + pack = ""; + } + 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); + } + + Path symbolsFile = descDest.resolve("symbols"); + + Files.createDirectories(symbolsFile.getParent()); + + try (Writer symbolsOut = Files.newBufferedWriter(symbolsFile)) { + Map> outputFiles = new LinkedHashMap<>(); + + for (Entry> e : module2Classes.entrySet()) { + for (VersionDescription desc : versions) { + Path f = descDest.resolve(e.getKey() + "-" + desc.version + ".sym.txt"); + try (Writer out = Files.newBufferedWriter(f)) { + for (ClassDescription clazz : e.getValue()) { + clazz.write(out, desc.primaryBaseline, desc.version); + } + } + outputFiles.computeIfAbsent(desc, d -> new ArrayList<>()) + .add(f); + } + } + symbolsOut.append("generate platforms ") + .append(versions.stream() + .map(v -> v.version) + .collect(Collectors.joining(":"))) + .append("\n"); + for (Entry> versionFileEntry : outputFiles.entrySet()) { + symbolsOut.append("platform version ") + .append(versionFileEntry.getKey().version); + if (versionFileEntry.getKey().primaryBaseline != null) { + symbolsOut.append(" base ") + .append(versionFileEntry.getKey().primaryBaseline); + } + symbolsOut.append(" files ") + .append(versionFileEntry.getValue() + .stream() + .map(p -> p.getFileName().toString()) + .sorted() + .collect(Collectors.joining(":"))) + .append("\n"); + } + } + } + + // + //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, ConstantPoolException { + ClassFile cf = ClassFile.read(in); + + if (!excludesIncludes.accepts(cf.getName())) { + return ; + } + + ClassHeaderDescription headerDesc = new ClassHeaderDescription(); + + headerDesc.flags = cf.access_flags.flags; + + if (cf.super_class != 0) { + headerDesc.extendsAttr = cf.getSuperclassName(); + } + List interfaces = new ArrayList<>(); + for (int i = 0; i < cf.interfaces.length; i++) { + interfaces.add(cf.getInterfaceName(i)); + } + headerDesc.implementsAttr = interfaces; + for (Attribute attr : cf.attributes) { + if (!readAttribute(cf, headerDesc, attr)) + return ; + } + + ClassDescription clazzDesc = null; + + for (ClassDescription cd : classes) { + if (cd.name.equals(cf.getName())) { + clazzDesc = cd; + break; + } + } + + if (clazzDesc == null) { + clazzDesc = new ClassDescription(); + clazzDesc.name = cf.getName(); + classes.add(clazzDesc); + } + + addClassHeader(clazzDesc, headerDesc, version); + + for (Method m : cf.methods) { + if (!include(m.access_flags.flags)) + continue; + MethodDescription methDesc = new MethodDescription(); + methDesc.flags = m.access_flags.flags; + methDesc.name = m.getName(cf.constant_pool); + methDesc.descriptor = m.descriptor.getValue(cf.constant_pool); + for (Attribute attr : m.attributes) { + readAttribute(cf, methDesc, attr); + } + addMethod(clazzDesc, methDesc, version); + } + for (Field f : cf.fields) { + if (!include(f.access_flags.flags)) + continue; + FieldDescription fieldDesc = new FieldDescription(); + fieldDesc.flags = f.access_flags.flags; + fieldDesc.name = f.getName(cf.constant_pool); + fieldDesc.descriptor = f.descriptor.getValue(cf.constant_pool); + for (Attribute attr : f.attributes) { + readAttribute(cf, fieldDesc, attr); + } + addField(clazzDesc, fieldDesc, version); + } + } + + private boolean include(int accessFlags) { + return (accessFlags & (AccessFlags.ACC_PUBLIC | AccessFlags.ACC_PROTECTED)) != 0; + } + + private void addClassHeader(ClassDescription clazzDesc, ClassHeaderDescription headerDesc, String version) { + //normalize: + boolean existed = false; + for (ClassHeaderDescription existing : clazzDesc.header) { + if (existing.equals(headerDesc)) { + headerDesc = existing; + existed = true; + } else { + //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 + List annots = headerDesc.classAnnotations; + + if (annots != null) { + for (AnnotationDescription ad : annots) { + if (PROFILE_ANNOTATION.equals(ad.annotationType)) { + annots.remove(ad); + if (existing.equals(headerDesc)) { + headerDesc = existing; + annots = headerDesc.classAnnotations; + if (annots == null) { + headerDesc.classAnnotations = annots = new ArrayList<>(); + } + annots.add(ad); + existed = true; + } else { + annots.add(ad); + } + break; + } + } + } + } + } + + headerDesc.versions += version; + + if (!existed) { + clazzDesc.header.add(headerDesc); + } + } + + private void addMethod(ClassDescription clazzDesc, MethodDescription methDesc, String version) { + //normalize: + boolean methodExisted = false; + for (MethodDescription existing : clazzDesc.methods) { + if (existing.equals(methDesc)) { + methodExisted = true; + methDesc = existing; + break; + } + } + methDesc.versions += version; + if (!methodExisted) { + clazzDesc.methods.add(methDesc); + } + } + + private void addField(ClassDescription clazzDesc, FieldDescription fieldDesc, String version) { + boolean fieldExisted = false; + for (FieldDescription existing : clazzDesc.fields) { + if (existing.equals(fieldDesc)) { + fieldExisted = true; + fieldDesc = existing; + break; + } + } + fieldDesc.versions += version; + if (!fieldExisted) { + clazzDesc.fields.add(fieldDesc); + } + } + + private boolean readAttribute(ClassFile cf, FeatureDescription feature, Attribute attr) throws ConstantPoolException { + String attrName = attr.getName(cf.constant_pool); + switch (attrName) { + case Attribute.AnnotationDefault: + assert feature instanceof MethodDescription; + element_value defaultValue = ((AnnotationDefault_attribute) attr).default_value; + ((MethodDescription) feature).annotationDefaultValue = + convertElementValue(cf.constant_pool, defaultValue); + break; + case "Deprecated": + feature.deprecated = true; + break; + case "Exceptions": + assert feature instanceof MethodDescription; + List thrownTypes = new ArrayList<>(); + Exceptions_attribute exceptionAttr = (Exceptions_attribute) attr; + for (int i = 0; i < exceptionAttr.exception_index_table.length; i++) { + thrownTypes.add(exceptionAttr.getException(i, cf.constant_pool)); + } + ((MethodDescription) feature).thrownTypes = thrownTypes; + break; + case Attribute.InnerClasses: + assert feature instanceof ClassHeaderDescription; + List innerClasses = new ArrayList<>(); + InnerClasses_attribute innerClassesAttr = (InnerClasses_attribute) attr; + for (int i = 0; i < innerClassesAttr.number_of_classes; i++) { + CONSTANT_Class_info outerClassInfo = + innerClassesAttr.classes[i].getOuterClassInfo(cf.constant_pool); + InnerClassInfo info = new InnerClassInfo(); + CONSTANT_Class_info innerClassInfo = + innerClassesAttr.classes[i].getInnerClassInfo(cf.constant_pool); + info.innerClass = innerClassInfo != null ? innerClassInfo.getName() : null; + info.outerClass = outerClassInfo != null ? outerClassInfo.getName() : null; + info.innerClassName = innerClassesAttr.classes[i].getInnerName(cf.constant_pool); + info.innerClassFlags = innerClassesAttr.classes[i].inner_class_access_flags.flags; + innerClasses.add(info); + } + ((ClassHeaderDescription) feature).innerClasses = innerClasses; + break; + case "RuntimeInvisibleAnnotations": + feature.classAnnotations = annotations2Description(cf.constant_pool, attr); + break; + case "RuntimeVisibleAnnotations": + feature.runtimeAnnotations = annotations2Description(cf.constant_pool, attr); + break; + case "Signature": + feature.signature = ((Signature_attribute) attr).getSignature(cf.constant_pool); + break; + case "ConstantValue": + assert feature instanceof FieldDescription; + Object value = convertConstantValue(cf.constant_pool.get(((ConstantValue_attribute) attr).constantvalue_index), ((FieldDescription) feature).descriptor); + if (((FieldDescription) feature).descriptor.equals("C")) { + value = (char) (int) value; + } + ((FieldDescription) feature).constantValue = value; + break; + case "SourceFile": + //ignore, not needed + break; + case "BootstrapMethods": + //ignore, not needed + break; + case "Code": + //ignore, not needed + break; + case "EnclosingMethod": + return false; + case "Synthetic": + break; + case "RuntimeVisibleParameterAnnotations": + assert feature instanceof MethodDescription; + ((MethodDescription) feature).runtimeParameterAnnotations = + parameterAnnotations2Description(cf.constant_pool, attr); + break; + case "RuntimeInvisibleParameterAnnotations": + assert feature instanceof MethodDescription; + ((MethodDescription) feature).classParameterAnnotations = + parameterAnnotations2Description(cf.constant_pool, attr); + break; + default: + throw new IllegalStateException("Unhandled attribute: " + attrName); + } + + return true; + } + + Object convertConstantValue(CPInfo info, String descriptor) throws ConstantPoolException { + if (info instanceof CONSTANT_Integer_info) { + if ("Z".equals(descriptor)) + return ((CONSTANT_Integer_info) info).value == 1; + else + return ((CONSTANT_Integer_info) info).value; + } else if (info instanceof CONSTANT_Long_info) { + return ((CONSTANT_Long_info) info).value; + } else if (info instanceof CONSTANT_Float_info) { + return ((CONSTANT_Float_info) info).value; + } else if (info instanceof CONSTANT_Double_info) { + return ((CONSTANT_Double_info) info).value; + } else if (info instanceof CONSTANT_String_info) { + return ((CONSTANT_String_info) info).getString(); + } + throw new IllegalStateException(info.getClass().getName()); + } + + Object convertElementValue(ConstantPool cp, element_value val) throws InvalidIndex, ConstantPoolException { + switch (val.tag) { + case 'Z': + return ((CONSTANT_Integer_info) cp.get(((Primitive_element_value) val).const_value_index)).value != 0; + case 'B': + return (byte) ((CONSTANT_Integer_info) cp.get(((Primitive_element_value) val).const_value_index)).value; + case 'C': + return (char) ((CONSTANT_Integer_info) cp.get(((Primitive_element_value) val).const_value_index)).value; + case 'S': + return (short) ((CONSTANT_Integer_info) cp.get(((Primitive_element_value) val).const_value_index)).value; + case 'I': + return ((CONSTANT_Integer_info) cp.get(((Primitive_element_value) val).const_value_index)).value; + case 'J': + return ((CONSTANT_Long_info) cp.get(((Primitive_element_value) val).const_value_index)).value; + case 'F': + return ((CONSTANT_Float_info) cp.get(((Primitive_element_value) val).const_value_index)).value; + case 'D': + return ((CONSTANT_Double_info) cp.get(((Primitive_element_value) val).const_value_index)).value; + case 's': + return ((CONSTANT_Utf8_info) cp.get(((Primitive_element_value) val).const_value_index)).value; + + case 'e': + return new EnumConstant(cp.getUTF8Value(((Enum_element_value) val).type_name_index), + cp.getUTF8Value(((Enum_element_value) val).const_name_index)); + case 'c': + return new ClassConstant(cp.getUTF8Value(((Class_element_value) val).class_info_index)); + + case '@': + return annotation2Description(cp, ((Annotation_element_value) val).annotation_value); + + case '[': + List values = new ArrayList<>(); + for (element_value elem : ((Array_element_value) val).values) { + values.add(convertElementValue(cp, elem)); + } + return values; + default: + throw new IllegalStateException("Currently unhandled tag: " + val.tag); + } + } + + private List annotations2Description(ConstantPool cp, Attribute attr) throws ConstantPoolException { + RuntimeAnnotations_attribute annotationsAttr = (RuntimeAnnotations_attribute) attr; + List descs = new ArrayList<>(); + for (Annotation a : annotationsAttr.annotations) { + descs.add(annotation2Description(cp, a)); + } + return descs; + } + + private List> parameterAnnotations2Description(ConstantPool cp, Attribute attr) throws ConstantPoolException { + RuntimeParameterAnnotations_attribute annotationsAttr = + (RuntimeParameterAnnotations_attribute) attr; + List> descs = new ArrayList<>(); + for (Annotation[] attrAnnos : annotationsAttr.parameter_annotations) { + List paramDescs = new ArrayList<>(); + for (Annotation ann : attrAnnos) { + paramDescs.add(annotation2Description(cp, ann)); + } + descs.add(paramDescs); + } + return descs; + } + + private AnnotationDescription annotation2Description(ConstantPool cp, Annotation a) throws ConstantPoolException { + String annotationType = cp.getUTF8Value(a.type_index); + Map values = new HashMap<>(); + + for (element_value_pair e : a.element_value_pairs) { + values.put(cp.getUTF8Value(e.element_name_index), convertElementValue(cp, 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; + } + + boolean include(Set includedClasses, ClassList classes, String clazzName) { + if (clazzName == null) + return false; + + boolean modified = includedClasses.add(clazzName); + + for (ClassDescription outer : classes.enclosingClasses(classes.find(clazzName, true))) { + modified |= includedClasses.add(outer.name); + } + + return modified; + } + + boolean includeOutputType(Iterable features, + Function feature2Descriptor, + Set includedClasses, + ClassList classes) { + boolean modified = false; + + for (T feature : features) { + CharSequence sig = + feature.signature != null ? feature.signature : feature2Descriptor.apply(feature); + Matcher m = OUTPUT_TYPE_PATTERN.matcher(sig); + while (m.find()) { + modified |= include(includedClasses, classes, m.group(1)); + } + } + + return modified; + } + + static final Pattern OUTPUT_TYPE_PATTERN = Pattern.compile("L([^;<]+)(;|<)"); + + Map buildPackage2Modules(Path jdkRoot) throws IOException { + if (jdkRoot == null) //in tests + return Collections.emptyMap(); + + Map result = new HashMap<>(); + try (DirectoryStream repositories = Files.newDirectoryStream(jdkRoot)) { + for (Path repository : repositories) { + Path src = repository.resolve("src"); + if (!Files.isDirectory(src)) + continue; + try (DirectoryStream modules = Files.newDirectoryStream(src)) { + for (Path module : modules) { + Path shareClasses = module.resolve("share/classes"); + + if (!Files.isDirectory(shareClasses)) + continue; + + Set packages = new HashSet<>(); + + packages(shareClasses, new StringBuilder(), packages); + + for (String p : packages) { + if (result.containsKey(p)) + throw new IllegalStateException("Duplicate package mapping."); + result.put(p, module.getFileName().toString()); + } + } + } + } + } + + return result; + } + + void packages(Path dir, StringBuilder soFar, Set packages) throws IOException { + try (DirectoryStream c = Files.newDirectoryStream(dir)) { + for (Path f : c) { + if (Files.isReadable(f) && f.getFileName().toString().endsWith(".java")) { + packages.add(soFar.toString()); + } + if (Files.isDirectory(f)) { + int len = soFar.length(); + if (len > 0) soFar.append("."); + soFar.append(f.getFileName().toString()); + packages(f, soFar, packages); + soFar.delete(len, soFar.length()); + } + } + } + } + + 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 excludeList; + + protected ExcludeIncludeList(Set includeList, Set excludeList) { + this.includeList = includeList; + 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) { + return matches(includeList, 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 abstract class FeatureDescription { + 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(unquote(inClassAnnotations), new int[1]); + } + String inRuntimeAnnotations = reader.attributes.get("runtimeAnnotations"); + if (inRuntimeAnnotations != null) { + runtimeAnnotations = parseAnnotations(unquote(inRuntimeAnnotations), new int[1]); + } + } + + public abstract boolean read(LineBasedReader reader) throws IOException; + + @Override + public int hashCode() { + int hash = 3; + hash = 89 * hash + this.flags; + 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 != other.flags) { + 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 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) { + field.write(output, baselineVersion, version); + } + for (MethodDescription method : methods) { + method.write(output, baselineVersion, version); + } + output.append("\n"); + } + + boolean hasChange(List hasChange, String version, String baselineVersion) { + return hasChange.stream() + .map(fd -> fd.versions) + .anyMatch(versions -> versions.contains(version) ^ + (baselineVersion != null && + versions.contains(baselineVersion))); + } + + 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": + break OUTER; + default: + throw new IllegalStateException(reader.lineKey); + } + } + } + } + + static class ClassHeaderDescription extends FeatureDescription { + String extendsAttr; + List implementsAttr; + List innerClasses; + + @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.innerClasses); + 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 (!listEquals(this.innerClasses, other.innerClasses)) { + 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)); + writeAttributes(output); + output.append("\n"); + 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"); + } + } + } + + @Override + public boolean read(LineBasedReader reader) throws IOException { + if (!"header".equals(reader.lineKey)) + return false; + + extendsAttr = reader.attributes.get("extends"); + implementsAttr = deserializeList(reader.attributes.get("implements")); + + readAttributes(reader); + + innerClasses = new ArrayList<>(); + + reader.moveNext(); + + 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(); + } + + return true; + } + + } + + static class MethodDescription extends FeatureDescription { + String name; + String descriptor; + List thrownTypes; + Object annotationDefaultValue; + List> classParameterAnnotations; + List> runtimeParameterAnnotations; + + @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(";"); + } + } + 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"); + + thrownTypes = deserializeList(reader.attributes.get("thrownTypes")); + + 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; + } + + reader.moveNext(); + + return true; + } + + } + + static class FieldDescription extends FeatureDescription { + String name; + String descriptor; + Object constantValue; + + @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("-field"); + output.append(" name " + quote(name, false)); + output.append(" descriptor " + quote(descriptor, false)); + output.append("\n"); + return ; + } + output.append("field"); + 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 (!"field".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 = Byte.parseByte(inConstantValue); break; + case "C": constantValue = inConstantValue.charAt(0); break; + case "S": constantValue = Short.parseShort(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 IllegalStateException("Unrecognized field type: " + descriptor); + } + } + + readAttributes(reader); + + reader.moveNext(); + + return true; + } + + } + + 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 IllegalStateException("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) { + serialized = unquote(serialized); + if (serialized == null) + return new ArrayList<>(); + return new ArrayList<>(Arrays.asList(serialized.split(","))); + } + + private static String quote(String value, boolean quoteQuotes) { + StringBuilder result = new StringBuilder(); + + for (char c : value.toCharArray()) { + if (c <= 32 || c >= 127 || c == '\\' || (quoteQuotes && 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 IllegalStateException("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 IllegalStateException("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); + } + // + + 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 < 4) { + help(); + return ; + } + + Path descDest = Paths.get(args[1]); + List versions = new ArrayList<>(); + + for (int i = 4; 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; + } + }); + + new CreateSymbols().createBaseLine(versions, ExcludeIncludeList.create(args[3]), descDest, Paths.get(args[2])); + break; + case "build-ctsym": + if (args.length < 3 || args.length > 4) { + help(); + return ; + } + + CtSymKind createKind = CtSymKind.JOINED_VERSIONS; + int argIndex = 1; + + if (args.length == 4) { + createKind = CtSymKind.valueOf(args[1]); + argIndex++; + } + + new CreateSymbols().createSymbols(args[argIndex], args[argIndex + 1], createKind); + break; + } + } + +}