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