make/langtools/tools/propertiesparser/gen/ClassGenerator.java
changeset 47216 71c04702a3d5
parent 28801 2f1c998c3fcc
child 49158 f62d1d1c2d9c
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/make/langtools/tools/propertiesparser/gen/ClassGenerator.java	Tue Sep 12 19:03:39 2017 +0200
@@ -0,0 +1,442 @@
+/*
+ * Copyright (c) 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.
+ *
+ * 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 propertiesparser.gen;
+
+import propertiesparser.parser.Message;
+import propertiesparser.parser.MessageFile;
+import propertiesparser.parser.MessageInfo;
+import propertiesparser.parser.MessageLine;
+import propertiesparser.parser.MessageType;
+import propertiesparser.parser.MessageType.CompoundType;
+import propertiesparser.parser.MessageType.CustomType;
+import propertiesparser.parser.MessageType.SimpleType;
+import propertiesparser.parser.MessageType.UnionType;
+import propertiesparser.parser.MessageType.Visitor;
+
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.TreeSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.Properties;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+public class ClassGenerator {
+
+    /** Empty string - used to generate indentation padding. */
+    private final static String INDENT_STRING = "                                                                   ";
+
+    /** Default indentation step. */
+    private final static int INDENT_WIDTH = 4;
+
+    /** File-backed property file containing basic code stubs. */
+    static Properties stubs;
+
+    static {
+        //init properties from file
+        stubs = new Properties();
+        String resourcePath = "/propertiesparser/resources/templates.properties";
+        try (InputStream in = ClassGenerator.class.getResourceAsStream(resourcePath)) {
+            stubs.load(in);
+        } catch (IOException ex) {
+            throw new AssertionError(ex);
+        }
+    }
+
+    /**
+     * Supported stubs in the property file.
+     */
+    enum StubKind {
+        TOPLEVEL("toplevel.decl"),
+        FACTORY_CLASS("nested.decl"),
+        IMPORT("import.decl"),
+        FACTORY_METHOD_DECL("factory.decl.method"),
+        FACTORY_METHOD_ARG("factory.decl.method.arg"),
+        FACTORY_METHOD_BODY("factory.decl.method.body"),
+        FACTORY_FIELD("factory.decl.field"),
+        WILDCARDS_EXTENDS("wildcards.extends"),
+        SUPPRESS_WARNINGS("suppress.warnings");
+
+        /** stub key (as it appears in the property file) */
+        String key;
+
+        StubKind(String key) {
+            this.key = key;
+        }
+
+        /**
+         * Subst a list of arguments into a given stub.
+         */
+        String format(Object... args) {
+            return MessageFormat.format((String)stubs.get(key), args);
+        }
+    }
+
+    /**
+     * Nested factory class kind. There are multiple sub-factories, one for each kind of commonly used
+     * diagnostics (i.e. error, warnings, note, fragment). An additional category is defined for
+     * those resource keys whose prefix doesn't match any predefined category.
+     */
+    enum FactoryKind {
+        ERR("err", "Error", "Errors"),
+        WARN("warn", "Warning", "Warnings"),
+        NOTE("note", "Note", "Notes"),
+        MISC("misc", "Fragment", "Fragments"),
+        OTHER(null, null, null);
+
+        /** The prefix for this factory kind (i.e. 'err'). */
+        String prefix;
+
+        /** The type of the factory method/fields in this class. */
+        String keyClazz;
+
+        /** The class name to be used for this factory. */
+        String factoryClazz;
+
+        FactoryKind(String prefix, String keyClazz, String factoryClazz) {
+            this.prefix = prefix;
+            this.keyClazz = keyClazz;
+            this.factoryClazz = factoryClazz;
+        }
+
+        /**
+         * Utility method for parsing a factory kind from a resource key prefix.
+         */
+        static FactoryKind parseFrom(String prefix) {
+            for (FactoryKind k : FactoryKind.values()) {
+                if (k.prefix == null || k.prefix.equals(prefix)) {
+                    return k;
+                }
+            }
+            return null;
+        }
+    }
+
+    /**
+     * Main entry-point: generate a Java enum-like set of nested factory classes into given output
+     * folder. The factories are populated as mandated by the comments in the input resource file.
+     */
+    public void generateFactory(MessageFile messageFile, File outDir) {
+        Map<FactoryKind, List<Map.Entry<String, Message>>> groupedEntries =
+                messageFile.messages.entrySet().stream()
+                        .collect(Collectors.groupingBy(e -> FactoryKind.parseFrom(e.getKey().split("\\.")[1])));
+        //generate nested classes
+        List<String> nestedDecls = new ArrayList<>();
+        Set<String> importedTypes = new TreeSet<>();
+        for (Map.Entry<FactoryKind, List<Map.Entry<String, Message>>> entry : groupedEntries.entrySet()) {
+            if (entry.getKey() == FactoryKind.OTHER) continue;
+            //emit members
+            String members = entry.getValue().stream()
+                    .flatMap(e -> generateFactoryMethodsAndFields(e.getKey(), e.getValue()).stream())
+                    .collect(Collectors.joining("\n\n"));
+            //emit nested class
+            String factoryDecl =
+                    StubKind.FACTORY_CLASS.format(entry.getKey().factoryClazz, indent(members, 1));
+            nestedDecls.add(indent(factoryDecl, 1));
+            //add imports
+            entry.getValue().stream().forEach(e ->
+                    importedTypes.addAll(importedTypes(e.getValue().getMessageInfo().getTypes())));
+        }
+        String clazz = StubKind.TOPLEVEL.format(
+                packageName(messageFile.file),
+                String.join("\n", generateImports(importedTypes)),
+                toplevelName(messageFile.file),
+                String.join("\n", nestedDecls));
+        try (FileWriter fw = new FileWriter(new File(outDir, toplevelName(messageFile.file) + ".java"))) {
+            fw.append(clazz);
+        } catch (Throwable ex) {
+            throw new AssertionError(ex);
+        }
+    }
+
+    /**
+     * Indent a string to a given level.
+     */
+    String indent(String s, int level) {
+        return Stream.of(s.split("\n"))
+                .map(sub -> INDENT_STRING.substring(0, level * INDENT_WIDTH) + sub)
+                .collect(Collectors.joining("\n"));
+    }
+
+    /**
+     * Retrieve package part of given file object.
+     */
+    String packageName(File file) {
+        String path = file.getAbsolutePath();
+        int begin = path.lastIndexOf(File.separatorChar + "com" + File.separatorChar);
+        String packagePath = path.substring(begin + 1, path.lastIndexOf(File.separatorChar));
+        String packageName =  packagePath.replace(File.separatorChar, '.');
+        return packageName;
+    }
+
+    /**
+     * Form the name of the toplevel factory class.
+     */
+    public static String toplevelName(File file) {
+        return Stream.of(file.getName().split("\\."))
+                .map(s -> Character.toUpperCase(s.charAt(0)) + s.substring(1))
+                .collect(Collectors.joining(""));
+    }
+
+    /**
+     * Generate a list of import declarations given a set of imported types.
+     */
+    List<String> generateImports(Set<String> importedTypes) {
+        List<String> importDecls = new ArrayList<>();
+        for (String it : importedTypes) {
+            importDecls.add(StubKind.IMPORT.format(it));
+        }
+        return importDecls;
+    }
+
+    /**
+     * Generate a list of factory methods/fields to be added to a given factory nested class.
+     */
+    List<String> generateFactoryMethodsAndFields(String key, Message msg) {
+        MessageInfo msgInfo = msg.getMessageInfo();
+        List<MessageLine> lines = msg.getLines(false);
+        String javadoc = lines.stream()
+                .filter(ml -> !ml.isInfo() && !ml.isEmptyOrComment())
+                .map(ml -> ml.text)
+                .collect(Collectors.joining("\n *"));
+        String[] keyParts = key.split("\\.");
+        FactoryKind k = FactoryKind.parseFrom(keyParts[1]);
+        String factoryName = factoryName(key);
+        if (msgInfo.getTypes().isEmpty()) {
+            //generate field
+            String factoryField = StubKind.FACTORY_FIELD.format(k.keyClazz, factoryName,
+                    "\"" + keyParts[0] + "\"",
+                    "\"" + Stream.of(keyParts).skip(2).collect(Collectors.joining(".")) + "\"",
+                    javadoc);
+            return Collections.singletonList(factoryField);
+        } else {
+            //generate method
+            List<String> factoryMethods = new ArrayList<>();
+            for (List<MessageType> msgTypes : normalizeTypes(0, msgInfo.getTypes())) {
+                List<String> types = generateTypes(msgTypes);
+                List<String> argNames = argNames(types.size());
+                String suppressionString = needsSuppressWarnings(msgTypes) ?
+                        StubKind.SUPPRESS_WARNINGS.format() : "";
+                String factoryMethod = StubKind.FACTORY_METHOD_DECL.format(suppressionString, k.keyClazz,
+                        factoryName, argDecls(types, argNames).stream().collect(Collectors.joining(", ")),
+                        indent(StubKind.FACTORY_METHOD_BODY.format(k.keyClazz,
+                                "\"" + keyParts[0] + "\"",
+                                "\"" + Stream.of(keyParts).skip(2).collect(Collectors.joining(".")) + "\"",
+                                argNames.stream().collect(Collectors.joining(", "))), 1),
+                        javadoc);
+                factoryMethods.add(factoryMethod);
+            }
+            return factoryMethods;
+        }
+    }
+
+    /**
+     * Form the name of a factory method/field given a resource key.
+     */
+    String factoryName(String key) {
+        return Stream.of(key.split("[\\.-]"))
+                .skip(2)
+                .map(s -> Character.toUpperCase(s.charAt(0)) + s.substring(1))
+                .collect(Collectors.joining(""));
+    }
+
+    /**
+     * Generate a formal parameter list given a list of types and names.
+     */
+    List<String> argDecls(List<String> types, List<String> args) {
+        List<String> argNames = new ArrayList<>();
+        for (int i = 0 ; i < types.size() ; i++) {
+            argNames.add(types.get(i) + " " + args.get(i));
+        }
+        return argNames;
+    }
+
+    /**
+     * Generate a list of formal parameter names given a size.
+     */
+    List<String> argNames(int size) {
+        List<String> argNames = new ArrayList<>();
+        for (int i = 0 ; i < size ; i++) {
+            argNames.add(StubKind.FACTORY_METHOD_ARG.format(i));
+        }
+        return argNames;
+    }
+
+    /**
+     * Convert a (normalized) parsed type into a string-based representation of some Java type.
+     */
+    List<String> generateTypes(List<MessageType> msgTypes) {
+        return msgTypes.stream().map(t -> t.accept(stringVisitor, null)).collect(Collectors.toList());
+    }
+    //where
+        Visitor<String, Void> stringVisitor = new Visitor<String, Void>() {
+            @Override
+            public String visitCustomType(CustomType t, Void aVoid) {
+                String customType = t.typeString;
+                return customType.substring(customType.lastIndexOf('.') + 1);
+            }
+
+            @Override
+            public String visitSimpleType(SimpleType t, Void aVoid) {
+                return t.clazz;
+            }
+
+            @Override
+            public String visitCompoundType(CompoundType t, Void aVoid) {
+                return StubKind.WILDCARDS_EXTENDS.format(t.kind.clazz.clazz,
+                        t.elemtype.accept(this, null));
+            }
+
+            @Override
+            public String visitUnionType(UnionType t, Void aVoid) {
+                throw new AssertionError("Union types should have been denormalized!");
+            }
+        };
+
+    /**
+     * See if any of the parsed types in the given list needs warning suppression.
+     */
+    boolean needsSuppressWarnings(List<MessageType> msgTypes) {
+        return msgTypes.stream().anyMatch(t -> t.accept(suppressWarningsVisitor, null));
+    }
+    //where
+    Visitor<Boolean, Void> suppressWarningsVisitor = new Visitor<Boolean, Void>() {
+        @Override
+        public Boolean visitCustomType(CustomType t, Void aVoid) {
+            //play safe
+            return true;
+        }
+        @Override
+        public Boolean visitSimpleType(SimpleType t, Void aVoid) {
+            switch (t) {
+                case LIST:
+                case SET:
+                    return true;
+                default:
+                    return false;
+            }
+        }
+
+        @Override
+        public Boolean visitCompoundType(CompoundType t, Void aVoid) {
+            return t.elemtype.accept(this, null);
+        }
+
+        @Override
+        public Boolean visitUnionType(UnionType t, Void aVoid) {
+            return needsSuppressWarnings(Arrays.asList(t.choices));
+        }
+    };
+
+    /**
+     * Retrieve a list of types that need to be imported, so that the factory body can refer
+     * to the types in the given list using simple names.
+     */
+    Set<String> importedTypes(List<MessageType> msgTypes) {
+        Set<String> imports = new TreeSet<>();
+        msgTypes.forEach(t -> t.accept(importVisitor, imports));
+        return imports;
+    }
+    //where
+    Visitor<Void, Set<String>> importVisitor = new Visitor<Void, Set<String>>() {
+        @Override
+        public Void visitCustomType(CustomType t, Set<String> imports) {
+            imports.add(t.typeString);
+            return null;
+        }
+
+        @Override
+        public Void visitSimpleType(SimpleType t, Set<String> imports) {
+            if (t.qualifier != null) {
+                imports.add(t.qualifier + "." + t.clazz);
+            }
+            return null;
+        }
+
+        @Override
+        public Void visitCompoundType(CompoundType t, Set<String> imports) {
+            visitSimpleType(t.kind.clazz, imports);
+            t.elemtype.accept(this, imports);
+            return null;
+        }
+
+        @Override
+        public Void visitUnionType(UnionType t, Set<String> imports) {
+            Stream.of(t.choices).forEach(c -> c.accept(this, imports));
+            return null;
+        }
+    };
+
+    /**
+     * Normalize parsed types in a comment line. If one or more types in the line contains alternatives,
+     * this routine generate a list of 'overloaded' normalized signatures.
+     */
+    List<List<MessageType>> normalizeTypes(int idx, List<MessageType> msgTypes) {
+        if (msgTypes.size() == idx) return Collections.singletonList(Collections.emptyList());
+        MessageType head = msgTypes.get(idx);
+        List<List<MessageType>> buf = new ArrayList<>();
+        for (MessageType alternative : head.accept(normalizeVisitor, null)) {
+            for (List<MessageType> rest : normalizeTypes(idx + 1, msgTypes)) {
+                List<MessageType> temp = new ArrayList<>(rest);
+                temp.add(0, alternative);
+                buf.add(temp);
+            }
+        }
+        return buf;
+    }
+    //where
+    Visitor<List<MessageType>, Void> normalizeVisitor = new Visitor<List<MessageType>, Void>() {
+        @Override
+        public List<MessageType> visitCustomType(CustomType t, Void aVoid) {
+            return Collections.singletonList(t);
+        }
+
+        @Override
+        public List<MessageType> visitSimpleType(SimpleType t, Void aVoid) {
+            return Collections.singletonList(t);
+        }
+
+        @Override
+        public List<MessageType> visitCompoundType(CompoundType t, Void aVoid) {
+            return t.elemtype.accept(this, null).stream()
+                    .map(nt -> new CompoundType(t.kind, nt))
+                    .collect(Collectors.toList());
+        }
+
+        @Override
+        public List<MessageType> visitUnionType(UnionType t, Void aVoid) {
+            return Stream.of(t.choices)
+                    .flatMap(t2 -> t2.accept(this, null).stream())
+                    .collect(Collectors.toList());
+        }
+    };
+}