diff -r 4ebc2e2fb97c -r 71c04702a3d5 make/langtools/tools/propertiesparser/gen/ClassGenerator.java --- /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>> groupedEntries = + messageFile.messages.entrySet().stream() + .collect(Collectors.groupingBy(e -> FactoryKind.parseFrom(e.getKey().split("\\.")[1]))); + //generate nested classes + List nestedDecls = new ArrayList<>(); + Set importedTypes = new TreeSet<>(); + for (Map.Entry>> 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 generateImports(Set importedTypes) { + List 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 generateFactoryMethodsAndFields(String key, Message msg) { + MessageInfo msgInfo = msg.getMessageInfo(); + List 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 factoryMethods = new ArrayList<>(); + for (List msgTypes : normalizeTypes(0, msgInfo.getTypes())) { + List types = generateTypes(msgTypes); + List 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 argDecls(List types, List args) { + List 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 argNames(int size) { + List 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 generateTypes(List msgTypes) { + return msgTypes.stream().map(t -> t.accept(stringVisitor, null)).collect(Collectors.toList()); + } + //where + Visitor stringVisitor = new Visitor() { + @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 msgTypes) { + return msgTypes.stream().anyMatch(t -> t.accept(suppressWarningsVisitor, null)); + } + //where + Visitor suppressWarningsVisitor = new Visitor() { + @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 importedTypes(List msgTypes) { + Set imports = new TreeSet<>(); + msgTypes.forEach(t -> t.accept(importVisitor, imports)); + return imports; + } + //where + Visitor> importVisitor = new Visitor>() { + @Override + public Void visitCustomType(CustomType t, Set imports) { + imports.add(t.typeString); + return null; + } + + @Override + public Void visitSimpleType(SimpleType t, Set imports) { + if (t.qualifier != null) { + imports.add(t.qualifier + "." + t.clazz); + } + return null; + } + + @Override + public Void visitCompoundType(CompoundType t, Set imports) { + visitSimpleType(t.kind.clazz, imports); + t.elemtype.accept(this, imports); + return null; + } + + @Override + public Void visitUnionType(UnionType t, Set 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> normalizeTypes(int idx, List msgTypes) { + if (msgTypes.size() == idx) return Collections.singletonList(Collections.emptyList()); + MessageType head = msgTypes.get(idx); + List> buf = new ArrayList<>(); + for (MessageType alternative : head.accept(normalizeVisitor, null)) { + for (List rest : normalizeTypes(idx + 1, msgTypes)) { + List temp = new ArrayList<>(rest); + temp.add(0, alternative); + buf.add(temp); + } + } + return buf; + } + //where + Visitor, Void> normalizeVisitor = new Visitor, Void>() { + @Override + public List visitCustomType(CustomType t, Void aVoid) { + return Collections.singletonList(t); + } + + @Override + public List visitSimpleType(SimpleType t, Void aVoid) { + return Collections.singletonList(t); + } + + @Override + public List 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 visitUnionType(UnionType t, Void aVoid) { + return Stream.of(t.choices) + .flatMap(t2 -> t2.accept(this, null).stream()) + .collect(Collectors.toList()); + } + }; +}