make/langtools/src/classes/build/tools/symbolgenerator/CreateSymbols.java
changeset 47703 dbfac941197a
parent 47216 71c04702a3d5
child 50027 69aadf0c1e69
--- a/make/langtools/src/classes/build/tools/symbolgenerator/CreateSymbols.java	Mon Nov 06 13:10:43 2017 +0100
+++ b/make/langtools/src/classes/build/tools/symbolgenerator/CreateSymbols.java	Mon Nov 06 14:10:39 2017 +0100
@@ -25,6 +25,12 @@
 
 package build.tools.symbolgenerator;
 
+import build.tools.symbolgenerator.CreateSymbols
+                                  .ModuleHeaderDescription
+                                  .ProvidesDescription;
+import build.tools.symbolgenerator.CreateSymbols
+                                  .ModuleHeaderDescription
+                                  .RequiresDescription;
 import java.io.BufferedReader;
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
@@ -80,6 +86,8 @@
 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_Module_info;
+import com.sun.tools.classfile.ConstantPool.CONSTANT_Package_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;
@@ -93,6 +101,13 @@
 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.ModuleResolution_attribute;
+import com.sun.tools.classfile.ModuleTarget_attribute;
+import com.sun.tools.classfile.Module_attribute;
+import com.sun.tools.classfile.Module_attribute.ExportsEntry;
+import com.sun.tools.classfile.Module_attribute.OpensEntry;
+import com.sun.tools.classfile.Module_attribute.ProvidesEntry;
+import com.sun.tools.classfile.Module_attribute.RequiresEntry;
 import com.sun.tools.classfile.RuntimeAnnotations_attribute;
 import com.sun.tools.classfile.RuntimeInvisibleAnnotations_attribute;
 import com.sun.tools.classfile.RuntimeInvisibleParameterAnnotations_attribute;
@@ -103,6 +118,7 @@
 import com.sun.tools.javac.jvm.Target;
 import com.sun.tools.javac.util.Assert;
 import com.sun.tools.javac.util.Pair;
+import java.io.StringWriter;
 
 /**
  * A tool for processing the .sym.txt files. It allows to:
@@ -164,11 +180,22 @@
      */
     @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) {
+        Pair<ClassList, List<ModuleDescription>> data = load(Paths.get(ctDescriptionFile));
+
+        splitHeaders(data.fst);
+
+        for (ModuleDescription md : data.snd) {
+            for (ModuleHeaderDescription mhd : md.header) {
+                List<String> versionsList =
+                        Collections.singletonList(mhd.versions);
+                writeModulesForVersions(ctSymLocation,
+                                        md,
+                                        mhd,
+                                        versionsList);
+            }
+        }
+
+        for (ClassDescription classDescription : data.fst) {
             for (ClassHeaderDescription header : classDescription.header) {
                 switch (ctSymKind) {
                     case JOINED_VERSIONS:
@@ -192,7 +219,7 @@
 
     public static String EXTENSION = ".sig";
 
-    ClassList load(Path ctDescription) throws IOException {
+    Pair<ClassList, List<ModuleDescription>> load(Path ctDescription) throws IOException {
         List<PlatformInput> platforms = new ArrayList<>();
         Set<String> generatePlatforms = null;
 
@@ -215,6 +242,7 @@
         }
 
         Map<String, ClassDescription> classes = new LinkedHashMap<>();
+        Map<String, ModuleDescription> modules = new LinkedHashMap<>();
 
         for (PlatformInput platform: platforms) {
             for (ClassDescription cd : classes.values()) {
@@ -222,19 +250,34 @@
                 addNewVersion(cd.fields, platform.basePlatform, platform.version);
                 addNewVersion(cd.methods, platform.basePlatform, platform.version);
             }
+            //XXX: enhance module versions
             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;
+                        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;
                         }
-                        cd.read(reader, platform.basePlatform, platform.version);
                     }
                 }
             }
@@ -243,7 +286,9 @@
         ClassList result = new ClassList();
 
         for (ClassDescription desc : classes.values()) {
-            for (Iterator<ClassHeaderDescription> chdIt = desc.header.iterator(); chdIt.hasNext();) {
+            Iterator<ClassHeaderDescription> chdIt = desc.header.iterator();
+
+            while (chdIt.hasNext()) {
                 ClassHeaderDescription chd = chdIt.next();
 
                 chd.versions = reduce(chd.versions, generatePlatforms);
@@ -255,7 +300,9 @@
                 continue;
             }
 
-            for (Iterator<MethodDescription> methodIt = desc.methods.iterator(); methodIt.hasNext();) {
+            Iterator<MethodDescription> methodIt = desc.methods.iterator();
+
+            while (methodIt.hasNext()) {
                 MethodDescription method = methodIt.next();
 
                 method.versions = reduce(method.versions, generatePlatforms);
@@ -263,7 +310,9 @@
                     methodIt.remove();
             }
 
-            for (Iterator<FieldDescription> fieldIt = desc.fields.iterator(); fieldIt.hasNext();) {
+            Iterator<FieldDescription> fieldIt = desc.fields.iterator();
+
+            while (fieldIt.hasNext()) {
                 FieldDescription field = fieldIt.next();
 
                 field.versions = reduce(field.versions, generatePlatforms);
@@ -274,7 +323,27 @@
             result.add(desc);
         }
 
-        return result;
+        List<ModuleDescription> moduleList = new ArrayList<>();
+
+        for (ModuleDescription desc : modules.values()) {
+            Iterator<ModuleHeaderDescription> mhdIt = desc.header.iterator();
+
+            while (mhdIt.hasNext()) {
+                ModuleHeaderDescription mhd = mhdIt.next();
+
+                mhd.versions = reduce(mhd.versions, generatePlatforms);
+                if (mhd.versions.isEmpty())
+                    mhdIt.remove();
+            }
+
+            if (desc.header.isEmpty()) {
+                continue;
+            }
+
+            moduleList.add(desc);
+        }
+
+        return Pair.of(result, moduleList);
     }
 
     static final class LineBasedReader implements AutoCloseable {
@@ -488,18 +557,70 @@
     void writeClassesForVersions(String ctSymLocation,
                                  ClassDescription classDescription,
                                  ClassHeaderDescription header,
-                                 Iterable<String> versions) throws IOException {
+                                 Iterable<String> versions)
+            throws IOException {
         for (String ver : versions) {
             writeClass(ctSymLocation, classDescription, header, ver);
         }
     }
 
+    void writeModulesForVersions(String ctSymLocation,
+                                 ModuleDescription moduleDescription,
+                                 ModuleHeaderDescription header,
+                                 Iterable<String> versions)
+            throws IOException {
+        for (String ver : versions) {
+            writeModule(ctSymLocation, moduleDescription, header, ver);
+        }
+    }
+
     public enum CtSymKind {
         JOINED_VERSIONS,
         SEPARATE;
     }
 
     //<editor-fold defaultstate="collapsed" desc="Class Writing">
+    void writeModule(String ctSymLocation,
+                    ModuleDescription moduleDescription,
+                    ModuleHeaderDescription header,
+                    String version) throws IOException {
+        List<CPInfo> constantPool = new ArrayList<>();
+        constantPool.add(null);
+        int currentClass = addClass(constantPool, "module-info");
+        int superclass = 0;
+        int[] interfaces = new int[0];
+        AccessFlags flags = new AccessFlags(header.flags);
+        Map<String, Attribute> attributesMap = new HashMap<>();
+        addAttributes(moduleDescription, header, constantPool, attributesMap);
+        Attributes attributes = new Attributes(attributesMap);
+        CPInfo[] cpData = constantPool.toArray(new CPInfo[constantPool.size()]);
+        ConstantPool cp = new ConstantPool(cpData);
+        ClassFile classFile = new ClassFile(0xCAFEBABE,
+                Target.DEFAULT.minorVersion,
+                Target.DEFAULT.majorVersion,
+                cp,
+                flags,
+                currentClass,
+                superclass,
+                interfaces,
+                new Field[0],
+                new Method[0],
+                attributes);
+
+        Path outputClassFile = Paths.get(ctSymLocation,
+                                         version + "-modules",
+                                         moduleDescription.name,
+                                         "module-info" + EXTENSION);
+
+        Files.createDirectories(outputClassFile.getParent());
+
+        try (OutputStream out = Files.newOutputStream(outputClassFile)) {
+            ClassWriter w = new ClassWriter();
+
+            w.write(classFile, out);
+        }
+    }
+
     void writeClass(String ctSymLocation,
                     ClassDescription classDescription,
                     ClassHeaderDescription header,
@@ -566,8 +687,93 @@
         }
     }
 
-    private void addAttributes(ClassHeaderDescription header, List<CPInfo> constantPool, Map<String, Attribute> attributes) {
+    private void addAttributes(ModuleDescription md,
+                               ModuleHeaderDescription header,
+                               List<CPInfo> cp,
+                               Map<String, Attribute> attributes) {
+        addGenericAttributes(header, cp, attributes);
+        if (header.moduleResolution != null) {
+            int attrIdx = addString(cp, Attribute.ModuleResolution);
+            final ModuleResolution_attribute resIdx =
+                    new ModuleResolution_attribute(attrIdx,
+                                                   header.moduleResolution);
+            attributes.put(Attribute.ModuleResolution, resIdx);
+        }
+        if (header.moduleTarget != null) {
+            int attrIdx = addString(cp, Attribute.ModuleTarget);
+            int targetIdx = addString(cp, header.moduleTarget);
+            attributes.put(Attribute.ModuleTarget,
+                           new ModuleTarget_attribute(attrIdx, targetIdx));
+        }
+        int attrIdx = addString(cp, Attribute.Module);
+        attributes.put(Attribute.Module,
+                       new Module_attribute(attrIdx,
+                             addModuleName(cp, md.name),
+                             0,
+                             0,
+                             header.requires
+                                   .stream()
+                                   .map(r -> createRequiresEntry(cp, r))
+                                   .collect(Collectors.toList())
+                                   .toArray(new RequiresEntry[0]),
+                             header.exports
+                                   .stream()
+                                   .map(e -> createExportsEntry(cp, e))
+                                   .collect(Collectors.toList())
+                                   .toArray(new ExportsEntry[0]),
+                             header.opens
+                                   .stream()
+                                   .map(e -> createOpensEntry(cp, e))
+                                   .collect(Collectors.toList())
+                                   .toArray(new OpensEntry[0]),
+                             header.uses
+                                   .stream()
+                                   .mapToInt(u -> addClassName(cp, u))
+                                   .toArray(),
+                             header.provides
+                                   .stream()
+                                   .map(p -> createProvidesEntry(cp, p))
+                                   .collect(Collectors.toList())
+                                   .toArray(new ProvidesEntry[0])));
+        addInnerClassesAttribute(header, cp, attributes);
+    }
+
+    private static RequiresEntry createRequiresEntry(List<CPInfo> cp,
+            RequiresDescription r) {
+        final int idx = addModuleName(cp, r.moduleName);
+        return new RequiresEntry(idx,
+                                 r.flags,
+                                 r.version != null
+                                         ? addInt(cp, r.version)
+                                         : 0);
+    }
+
+    private static ExportsEntry createExportsEntry(List<CPInfo> cp,
+                                                   String e) {
+        return new ExportsEntry(addPackageName(cp, e), 0, new int[0]);
+    }
+
+    private static OpensEntry createOpensEntry(List<CPInfo> cp, String e) {
+        return new OpensEntry(addPackageName(cp, e), 0, new int[0]);
+    }
+
+    private static ProvidesEntry createProvidesEntry(List<CPInfo> cp,
+            ModuleHeaderDescription.ProvidesDescription p) {
+        final int idx = addClassName(cp, p.interfaceName);
+        return new ProvidesEntry(idx, p.implNames
+                                       .stream()
+                                       .mapToInt(i -> addClassName(cp, i))
+                                       .toArray());
+    }
+
+    private void addAttributes(ClassHeaderDescription header,
+            List<CPInfo> constantPool, Map<String, Attribute> attributes) {
         addGenericAttributes(header, constantPool, attributes);
+        addInnerClassesAttribute(header, constantPool, attributes);
+    }
+
+    private void addInnerClassesAttribute(HeaderDescription header,
+            List<CPInfo> constantPool, Map<String, Attribute> attributes) {
         if (header.innerClasses != null && !header.innerClasses.isEmpty()) {
             Info[] innerClasses = new Info[header.innerClasses.size()];
             int i = 0;
@@ -777,6 +983,65 @@
         return addToCP(constantPool, new CONSTANT_Utf8_info(string));
     }
 
+    private static int addInt(List<CPInfo> constantPool, int value) {
+        int i = 0;
+        for (CPInfo info : constantPool) {
+            if (info instanceof CONSTANT_Integer_info) {
+                if (((CONSTANT_Integer_info) info).value == value) {
+                    return i;
+                }
+            }
+            i++;
+        }
+
+        return addToCP(constantPool, new CONSTANT_Integer_info(value));
+    }
+
+    private static int addModuleName(List<CPInfo> constantPool, String moduleName) {
+        int nameIdx = addString(constantPool, moduleName);
+        int i = 0;
+        for (CPInfo info : constantPool) {
+            if (info instanceof CONSTANT_Module_info) {
+                if (((CONSTANT_Module_info) info).name_index == nameIdx) {
+                    return i;
+                }
+            }
+            i++;
+        }
+
+        return addToCP(constantPool, new CONSTANT_Module_info(null, nameIdx));
+    }
+
+    private static int addPackageName(List<CPInfo> constantPool, String packageName) {
+        int nameIdx = addString(constantPool, packageName);
+        int i = 0;
+        for (CPInfo info : constantPool) {
+            if (info instanceof CONSTANT_Package_info) {
+                if (((CONSTANT_Package_info) info).name_index == nameIdx) {
+                    return i;
+                }
+            }
+            i++;
+        }
+
+        return addToCP(constantPool, new CONSTANT_Package_info(null, nameIdx));
+    }
+
+    private static int addClassName(List<CPInfo> constantPool, String className) {
+        int nameIdx = addString(constantPool, className);
+        int i = 0;
+        for (CPInfo info : constantPool) {
+            if (info instanceof CONSTANT_Class_info) {
+                if (((CONSTANT_Class_info) info).name_index == nameIdx) {
+                    return i;
+                }
+            }
+            i++;
+        }
+
+        return addToCP(constantPool, new CONSTANT_Class_info(null, nameIdx));
+    }
+
     private static int addToCP(List<CPInfo> constantPool, CPInfo entry) {
         int result = constantPool.size();
 
@@ -808,27 +1073,92 @@
     //</editor-fold>
 
     //<editor-fold defaultstate="collapsed" desc="Create Symbol Description">
-    public void createBaseLine(List<VersionDescription> versions, ExcludeIncludeList excludesIncludes, Path descDest, Path jdkRoot) throws IOException {
+    public void createBaseLine(List<VersionDescription> versions,
+                               ExcludeIncludeList excludesIncludes,
+                               Path descDest,
+                               String[] args) throws IOException {
         ClassList classes = new ClassList();
+        Map<String, ModuleDescription> modules = new HashMap<>();
 
         for (VersionDescription desc : versions) {
-            ClassList currentVersionClasses = new ClassList();
-            try (BufferedReader descIn = Files.newBufferedReader(Paths.get(desc.classes))) {
+            Map<String, ModuleDescription> currentVersionModules =
+                    new HashMap<>();
+            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));
+                        String hex = classFileData.substring(i, i + 2);
+                        data.write(Integer.parseInt(hex, 16));
                     }
-                    try (InputStream in = new ByteArrayInputStream(data.toByteArray())) {
-                        inspectClassFile(in, currentVersionClasses, excludesIncludes, desc.version);
+                    try (InputStream in =
+                            new ByteArrayInputStream(data.toByteArray())) {
+                        inspectModuleInfoClassFile(in,
+                                currentVersionModules, desc.version);
                     } catch (IOException | ConstantPoolException ex) {
                         throw new IllegalStateException(ex);
                     }
                 }
             }
 
+            ExcludeIncludeList currentEIList = excludesIncludes;
+
+            if (!currentVersionModules.isEmpty()) {
+                Set<String> includes = new HashSet<>();
+
+                for (ModuleDescription md : currentVersionModules.values()) {
+                    md.header.get(0).exports.stream().map(e -> e + '/')
+                                            .forEach(includes::add);
+                }
+
+                currentEIList = new ExcludeIncludeList(includes,
+                                                       Collections.emptySet());
+            }
+
+            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) {
+                        String hex = classFileData.substring(i, i + 2);
+                        data.write(Integer.parseInt(hex, 16));
+                    }
+                    try (InputStream in =
+                            new ByteArrayInputStream(data.toByteArray())) {
+                        inspectClassFile(in, currentVersionClasses,
+                                         currentEIList, desc.version);
+                    } catch (IOException | ConstantPoolException ex) {
+                        throw new IllegalStateException(ex);
+                    }
+                }
+            }
+
+            ModuleDescription unsupported =
+                    currentVersionModules.get("jdk.unsupported");
+
+            if (unsupported != null) {
+                for (ClassDescription cd : currentVersionClasses.classes) {
+                    if (unsupported.header
+                                   .get(0)
+                                   .exports
+                                   .contains(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<String> includedClasses = new HashSet<>();
             boolean modified;
 
@@ -895,21 +1225,52 @@
                     classes.add(clazz);
                 }
             }
+
+            for (ModuleDescription module : currentVersionModules.values()) {
+                ModuleHeaderDescription header = module.header.get(0);
+
+                if (header.innerClasses != null) {
+                    Iterator<InnerClassInfo> innerClassIt =
+                            header.innerClasses.iterator();
+
+                    while(innerClassIt.hasNext()) {
+                        InnerClassInfo ici = innerClassIt.next();
+                        if (!includedClasses.contains(ici.innerClass))
+                            innerClassIt.remove();
+                    }
+                }
+
+                ModuleDescription existing = modules.get(module.name);
+
+                if (existing != null) {
+                    addModuleHeader(existing, header, desc.version);
+                } else {
+                    modules.put(module.name, module);
+                }
+            }
         }
 
         classes.sort();
 
-        Map<String, String> package2Modules = buildPackage2Modules(jdkRoot);
+        Map<String, String> package2Modules = new HashMap<>();
+
+        for (ModuleDescription md : modules.values()) {
+            md.header
+              .stream()
+              .filter(h -> h.versions.contains("9"))
+              .flatMap(h -> h.exports.stream())
+              .map(p -> p.replace('/', '.'))
+              .forEach(p -> package2Modules.put(p, md.name));
+        }
+
+        package2Modules.put("java.awt.dnd.peer", "java.desktop");
+        package2Modules.put("java.awt.peer", "java.desktop");
+        package2Modules.put("jdk", "java.base");
+
         Map<String, List<ClassDescription>> 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 pack = clazz.packge();
             String module = package2Modules.get(pack);
 
             if (module == null) {
@@ -932,6 +1293,11 @@
                     .add(clazz);
         }
 
+        modules.keySet()
+               .stream()
+               .filter(m -> !module2Classes.containsKey(m))
+               .forEach(m -> module2Classes.put(m, Collections.emptyList()));
+
         Path symbolsFile = descDest.resolve("symbols");
 
         Files.createDirectories(symbolsFile.getParent());
@@ -941,19 +1307,44 @@
 
             for (Entry<String, List<ClassDescription>> e : module2Classes.entrySet()) {
                 for (VersionDescription desc : versions) {
+                    StringWriter data = new StringWriter();
+                    ModuleDescription module = modules.get(e.getKey());
+
+                    module.write(data, desc.primaryBaseline, desc.version);
+
+                    for (ClassDescription clazz : e.getValue()) {
+                        clazz.write(data, desc.primaryBaseline, desc.version);
+                    }
+
                     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);
+
+                    String dataString = data.toString();
+
+                    if (!dataString.isEmpty()) {
+                        try (Writer out = Files.newBufferedWriter(f)) {
+                            out.append(DO_NO_MODIFY);
+                            out.write(dataString);
                         }
+
+                        outputFiles.computeIfAbsent(desc, d -> new ArrayList<>())
+                                   .add(f);
                     }
-                    outputFiles.computeIfAbsent(desc, d -> new ArrayList<>())
-                               .add(f);
                 }
             }
+            symbolsOut.append(DO_NO_MODIFY);
+            symbolsOut.append("#command used to generate this file:\n");
+            symbolsOut.append("#")
+                      .append(CreateSymbols.class.getName())
+                      .append(" ")
+                      .append(Arrays.asList(args)
+                                    .stream()
+                                    .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<VersionDescription, List<Path>> versionFileEntry : outputFiles.entrySet()) {
@@ -973,6 +1364,14 @@
             }
         }
     }
+    //where:
+        private static final String DO_NO_MODIFY =
+            "# ##########################################################\n" +
+            "# ### THIS FILE IS AUTOMATICALLY GENERATED. DO NOT EDIT. ###\n" +
+            "# ##########################################################\n" +
+            "#\n";
+        private static final String PROPERITARY_ANNOTATION =
+                "Lsun/Proprietary+Annotation;";
 
     //<editor-fold defaultstate="collapsed" desc="Class Reading">
     //non-final for tests:
@@ -982,6 +1381,10 @@
     private void inspectClassFile(InputStream in, ClassList classes, ExcludeIncludeList excludesIncludes, String version) throws IOException, ConstantPoolException {
         ClassFile cf = ClassFile.read(in);
 
+        if (cf.access_flags.is(AccessFlags.ACC_MODULE)) {
+            return ;
+        }
+
         if (!excludesIncludes.accepts(cf.getName())) {
             return ;
         }
@@ -1046,6 +1449,57 @@
         }
     }
 
+    private void inspectModuleInfoClassFile(InputStream in,
+            Map<String, ModuleDescription> modules,
+            String version) throws IOException, ConstantPoolException {
+        ClassFile cf = ClassFile.read(in);
+
+        if (!cf.access_flags.is(AccessFlags.ACC_MODULE)) {
+            return ;
+        }
+
+        ModuleHeaderDescription headerDesc = new ModuleHeaderDescription();
+
+        headerDesc.versions = version;
+        headerDesc.flags = cf.access_flags.flags;
+
+        for (Attribute attr : cf.attributes) {
+            if (!readAttribute(cf, headerDesc, attr))
+                return ;
+        }
+
+        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);
+    }
+
+    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;
+            }
+        }
+
+        headerDesc.versions += version;
+
+        if (!existed) {
+            moduleDesc.header.add(headerDesc);
+        }
+    }
+
     private boolean include(int accessFlags) {
         return (accessFlags & (AccessFlags.ACC_PUBLIC | AccessFlags.ACC_PROTECTED)) != 0;
     }
@@ -1060,23 +1514,18 @@
             } 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<AnnotationDescription> annots = headerDesc.classAnnotations;
+                List<AnnotationDescription> annots = existing.classAnnotations;
 
                 if (annots != null) {
                     for (AnnotationDescription ad : annots) {
                         if (PROFILE_ANNOTATION.equals(ad.annotationType)) {
-                            annots.remove(ad);
+                            existing.classAnnotations = new ArrayList<>(annots);
+                            existing.classAnnotations.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);
                             }
+                            existing.classAnnotations = annots;
                             break;
                         }
                     }
@@ -1144,6 +1593,8 @@
                 ((MethodDescription) feature).thrownTypes = thrownTypes;
                 break;
             case Attribute.InnerClasses:
+                if (feature instanceof ModuleHeaderDescription)
+                    break; //XXX
                 assert feature instanceof ClassHeaderDescription;
                 List<InnerClassInfo> innerClasses = new ArrayList<>();
                 InnerClasses_attribute innerClassesAttr = (InnerClasses_attribute) attr;
@@ -1201,13 +1652,112 @@
                 ((MethodDescription) feature).classParameterAnnotations =
                         parameterAnnotations2Description(cf.constant_pool, attr);
                 break;
+            case Attribute.Module: {
+                assert feature instanceof ModuleHeaderDescription;
+                ModuleHeaderDescription header =
+                        (ModuleHeaderDescription) feature;
+                Module_attribute mod = (Module_attribute) attr;
+
+                header.name = cf.constant_pool
+                                .getModuleInfo(mod.module_name)
+                                .getName();
+
+                header.exports =
+                        Arrays.stream(mod.exports)
+                              .filter(ee -> ee.exports_to_count == 0)
+                              .map(ee -> getPackageName(cf, ee.exports_index))
+                              .collect(Collectors.toList());
+                header.requires =
+                        Arrays.stream(mod.requires)
+                              .map(r -> RequiresDescription.create(cf, r))
+                              .collect(Collectors.toList());
+                header.uses = Arrays.stream(mod.uses_index)
+                                    .mapToObj(use -> getClassName(cf, use))
+                                    .collect(Collectors.toList());
+                header.provides =
+                        Arrays.stream(mod.provides)
+                              .map(p -> ProvidesDescription.create(cf, p))
+                              .collect(Collectors.toList());
+                break;
+            }
+            case Attribute.ModuleTarget: {
+                assert feature instanceof ModuleHeaderDescription;
+                ModuleHeaderDescription header =
+                        (ModuleHeaderDescription) feature;
+                ModuleTarget_attribute mod = (ModuleTarget_attribute) attr;
+                if (mod.target_platform_index != 0) {
+                    header.moduleTarget =
+                            cf.constant_pool
+                              .getUTF8Value(mod.target_platform_index);
+                }
+                break;
+            }
+            case Attribute.ModuleResolution: {
+                assert feature instanceof ModuleHeaderDescription;
+                ModuleHeaderDescription header =
+                        (ModuleHeaderDescription) feature;
+                ModuleResolution_attribute mod =
+                        (ModuleResolution_attribute) attr;
+                header.moduleResolution = mod.resolution_flags;
+                break;
+            }
+            case Attribute.ModulePackages:
+            case Attribute.ModuleHashes:
+                break;
             default:
-                throw new IllegalStateException("Unhandled attribute: " + attrName);
+                throw new IllegalStateException("Unhandled attribute: " +
+                                                attrName);
         }
 
         return true;
     }
 
+    private static String getClassName(ClassFile cf, int idx) {
+        try {
+            return cf.constant_pool.getClassInfo(idx).getName();
+        } catch (InvalidIndex ex) {
+            throw new IllegalStateException(ex);
+        } catch (ConstantPool.UnexpectedEntry ex) {
+            throw new IllegalStateException(ex);
+        } catch (ConstantPoolException ex) {
+            throw new IllegalStateException(ex);
+        }
+    }
+
+    private static String getPackageName(ClassFile cf, int idx) {
+        try {
+            return cf.constant_pool.getPackageInfo(idx).getName();
+        } catch (InvalidIndex ex) {
+            throw new IllegalStateException(ex);
+        } catch (ConstantPool.UnexpectedEntry ex) {
+            throw new IllegalStateException(ex);
+        } catch (ConstantPoolException ex) {
+            throw new IllegalStateException(ex);
+        }
+    }
+
+    private static String getModuleName(ClassFile cf, int idx) {
+        try {
+            return cf.constant_pool.getModuleInfo(idx).getName();
+        } catch (InvalidIndex ex) {
+            throw new IllegalStateException(ex);
+        } catch (ConstantPool.UnexpectedEntry ex) {
+            throw new IllegalStateException(ex);
+        } catch (ConstantPoolException ex) {
+            throw new IllegalStateException(ex);
+        }
+    }
+
+    private static Integer getVersion(ClassFile cf, int idx) {
+        if (idx == 0)
+            return null;
+        try {
+            return ((CONSTANT_Integer_info) cf.constant_pool.get(idx)).value;
+        } catch (InvalidIndex ex) {
+            throw new IllegalStateException(ex);
+        }
+    }
+
     Object convertConstantValue(CPInfo info, String descriptor) throws ConstantPoolException {
         if (info instanceof CONSTANT_Integer_info) {
             if ("Z".equals(descriptor))
@@ -1345,57 +1895,6 @@
 
     static final Pattern OUTPUT_TYPE_PATTERN = Pattern.compile("L([^;<]+)(;|<)");
 
-    Map<String, String> buildPackage2Modules(Path jdkRoot) throws IOException {
-        if (jdkRoot == null) //in tests
-            return Collections.emptyMap();
-
-        Map<String, String> result = new HashMap<>();
-        try (DirectoryStream<Path> repositories = Files.newDirectoryStream(jdkRoot)) {
-            for (Path repository : repositories) {
-                Path src = repository.resolve("src");
-                if (!Files.isDirectory(src))
-                    continue;
-                try (DirectoryStream<Path> modules = Files.newDirectoryStream(src)) {
-                    for (Path module : modules) {
-                        Path shareClasses = module.resolve("share/classes");
-
-                        if (!Files.isDirectory(shareClasses))
-                            continue;
-
-                        Set<String> 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<String> packages) throws IOException {
-        try (DirectoryStream<Path> 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;
@@ -1448,6 +1947,13 @@
     //</editor-fold>
 
     //<editor-fold defaultstate="collapsed" desc="Class Data Structures">
+    static boolean checkChange(String versions, String version,
+                               String baselineVersion) {
+        return versions.contains(version) ^
+               (baselineVersion != null &&
+                versions.contains(baselineVersion));
+    }
+
     static abstract class FeatureDescription {
         int flags;
         boolean deprecated;
@@ -1550,17 +2056,319 @@
 
     }
 
+    public static class ModuleDescription {
+        String name;
+        List<ModuleHeaderDescription> 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<? extends FeatureDescription> 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.versions = version;
+                        header.add(mhd);
+                        break;
+                    case "class":
+                    case "-class":
+                    case "module":
+                    case "-module":
+                        break OUTER;
+                    default:
+                        throw new IllegalStateException(reader.lineKey);
+                }
+            }
+        }
+    }
+
+    static class ModuleHeaderDescription extends HeaderDescription {
+        String name;
+        List<String> exports = new ArrayList<>();
+        List<String> opens = new ArrayList<>();
+        List<RequiresDescription> requires = new ArrayList<>();
+        List<String> uses = new ArrayList<>();
+        List<ProvidesDescription> provides = new ArrayList<>();
+        Integer moduleResolution;
+        String moduleTarget;
+
+        @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.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);
+            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.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;
+            }
+            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())
+                output.append(" exports " + serializeList(exports));
+            if (opens != null && !opens.isEmpty())
+                output.append(" opens " + serializeList(opens));
+            if (requires != null && !requires.isEmpty()) {
+                List<String> 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<String> 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));
+            writeAttributes(output);
+            output.append("\n");
+            writeInnerClasses(output, baselineVersion, version);
+        }
+
+        private static Map<String, String> splitAttributes(String data) {
+            String[] parts = data.split(" ");
+
+            Map<String, String> 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;
+
+            exports = deserializeList(reader.attributes.get("exports"));
+            opens = deserializeList(reader.attributes.get("opens"));
+            List<String> requiresList =
+                    deserializeList(reader.attributes.get("requires"));
+            requires = requiresList.stream()
+                                   .map(RequiresDescription::deserialize)
+                                   .collect(Collectors.toList());
+            uses = deserializeList(reader.attributes.get("uses"));
+            List<String> 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);
+            }
+
+            readAttributes(reader);
+            reader.moveNext();
+            readInnerClasses(reader);
+
+            return true;
+        }
+
+        static class RequiresDescription {
+            final String moduleName;
+            final int flags;
+            final Integer version;
+
+            public RequiresDescription(String moduleName, int flags,
+                                       Integer version) {
+                this.moduleName = moduleName;
+                this.flags = flags;
+                this.version = version;
+            }
+
+            public String serialize() {
+                String versionKeyValue = version != null
+                        ? " version " + quote(String.valueOf(version), true)
+                        : "";
+                return "name " + quote(moduleName, true) +
+                       " flags " + quote(Integer.toHexString(flags), true) +
+                       versionKeyValue;
+            }
+
+            public static RequiresDescription deserialize(String data) {
+                Map<String, String> attributes = splitAttributes(data);
+
+                Integer ver = attributes.containsKey("version")
+                        ? Integer.parseInt(attributes.get("version"))
+                        : null;
+                int flags = Integer.parseInt(attributes.get("flags"), 16);
+                return new RequiresDescription(attributes.get("name"),
+                                               flags,
+                                               ver);
+            }
+
+            public static RequiresDescription create(ClassFile cf,
+                                                     RequiresEntry req) {
+                String mod = getModuleName(cf, req.requires_index);
+                Integer ver = getVersion(cf, req.requires_version_index);
+                return new RequiresDescription(mod,
+                                               req.requires_flags,
+                                               ver);
+            }
+        }
+
+        static class ProvidesDescription {
+            final String interfaceName;
+            final List<String> implNames;
+
+            public ProvidesDescription(String interfaceName,
+                                       List<String> 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<String, String> attributes = splitAttributes(data);
+                List<String> implsList =
+                        deserializeList(attributes.get("impls"),
+                                        false);
+                return new ProvidesDescription(attributes.get("interface"),
+                                               implsList);
+            }
+
+            public static ProvidesDescription create(ClassFile cf,
+                                                     ProvidesEntry prov) {
+                String api = getClassName(cf, prov.provides_index);
+                List<String> impls =
+                        Arrays.stream(prov.with_index)
+                              .mapToObj(wi -> getClassName(cf, wi))
+                              .collect(Collectors.toList());
+                return new ProvidesDescription(api, impls);
+            }
+        }
+    }
+
     public static class ClassDescription {
         String name;
         List<ClassHeaderDescription> header = new ArrayList<>();
         List<MethodDescription> methods = new ArrayList<>();
         List<FieldDescription> fields = new ArrayList<>();
 
-        public void write(Appendable output, String baselineVersion, String version) throws IOException {
+        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)) {
+                if (baselineVersion != null &&
+                    chd.versions.contains(baselineVersion)) {
                     inBaseline = true;
                 }
                 if (chd.versions.contains(version)) {
@@ -1592,15 +2400,18 @@
             output.append("\n");
         }
 
-        boolean hasChange(List<? extends FeatureDescription> hasChange, String version, String baselineVersion) {
+        boolean hasChange(List<? extends FeatureDescription> hasChange,
+                          String version,
+                          String baseline) {
             return hasChange.stream()
                             .map(fd -> fd.versions)
-                            .anyMatch(versions -> versions.contains(version) ^
-                                                  (baselineVersion != null &&
-                                                   versions.contains(baselineVersion)));
+                            .anyMatch(versions -> checkChange(versions,
+                                                              version,
+                                                              baseline));
         }
 
-        public void read(LineBasedReader reader, String baselineVersion, String version) throws IOException {
+        public void read(LineBasedReader reader, String baselineVersion,
+                         String version) throws IOException {
             if (!"class".equals(reader.lineKey))
                 return ;
 
@@ -1647,25 +2458,37 @@
                     }
                     case "class":
                     case "-class":
+                    case "module":
+                    case "-module":
                         break OUTER;
                     default:
                         throw new IllegalStateException(reader.lineKey);
                 }
             }
         }
+
+        public String packge() {
+            String pack;
+            int lastSlash = name.lastIndexOf('/');
+            if (lastSlash != (-1)) {
+                pack = name.substring(0, lastSlash).replace('/', '.');
+            } else {
+                pack = "";
+            }
+
+            return pack;
+        }
     }
 
-    static class ClassHeaderDescription extends FeatureDescription {
+    static class ClassHeaderDescription extends HeaderDescription {
         String extendsAttr;
         List<String> implementsAttr;
-        List<InnerClassInfo> 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;
         }
 
@@ -1684,9 +2507,6 @@
             if (!Objects.equals(this.implementsAttr, other.implementsAttr)) {
                 return false;
             }
-            if (!listEquals(this.innerClasses, other.innerClasses)) {
-                return false;
-            }
             return true;
         }
 
@@ -1702,6 +2522,55 @@
                 output.append(" implements " + serializeList(implementsAttr));
             writeAttributes(output);
             output.append("\n");
+            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);
+
+            readAttributes(reader);
+            reader.moveNext();
+            readInnerClasses(reader);
+
+            return true;
+        }
+
+    }
+
+    static abstract class HeaderDescription extends FeatureDescription {
+        List<InnerClassInfo> 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");
@@ -1714,20 +2583,9 @@
             }
         }
 
-        @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);
-
+        protected void readInnerClasses(LineBasedReader reader) throws IOException {
             innerClasses = new ArrayList<>();
 
-            reader.moveNext();
-
             while ("innerclass".equals(reader.lineKey)) {
                 InnerClassInfo info = new InnerClassInfo();
 
@@ -1743,8 +2601,6 @@
 
                 reader.moveNext();
             }
-
-            return true;
         }
 
     }
@@ -2277,17 +3133,28 @@
     }
 
     private static List<String> deserializeList(String serialized) {
-        serialized = unquote(serialized);
+        return deserializeList(serialized, true);
+    }
+
+    private static List<String> deserializeList(String serialized,
+                                                boolean unquote) {
+        serialized = unquote ? unquote(serialized) : serialized;
         if (serialized == null)
             return new ArrayList<>();
         return new ArrayList<>(Arrays.asList(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 == '"')) {
+            if (c <= 32 || c >= 127 || c == '\\' ||
+                (quoteQuotes && c == '"') || (quoteCommas && c == ',')) {
                 result.append("\\u" + String.format("%04X", (int) c) + ";");
             } else {
                 result.append(c);
@@ -2425,7 +3292,7 @@
 
         switch (args[0]) {
             case "build-description":
-                if (args.length < 4) {
+                if (args.length < 3) {
                     help();
                     return ;
                 }
@@ -2433,7 +3300,7 @@
                 Path descDest = Paths.get(args[1]);
                 List<VersionDescription> versions = new ArrayList<>();
 
-                for (int i = 4; i + 2 < args.length; i += 3) {
+                for (int i = 3; i + 2 < args.length; i += 3) {
                     versions.add(new VersionDescription(args[i + 1], args[i], args[i + 2]));
                 }
 
@@ -2457,23 +3324,23 @@
                     }
                 });
 
-                new CreateSymbols().createBaseLine(versions, ExcludeIncludeList.create(args[3]), descDest, Paths.get(args[2]));
+                ExcludeIncludeList excludeList =
+                        ExcludeIncludeList.create(args[2]);
+
+                new CreateSymbols().createBaseLine(versions,
+                                                   excludeList,
+                                                   descDest,
+                                                   args);
                 break;
             case "build-ctsym":
-                if (args.length < 3 || args.length > 4) {
+                if (args.length != 3) {
                     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);
+                new CreateSymbols().createSymbols(args[1],
+                                                  args[2],
+                                                  CtSymKind.JOINED_VERSIONS);
                 break;
         }
     }