/* * 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()); } }; }