// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.config.codegen; import com.yahoo.config.codegen.LeafCNode.BooleanLeaf; import com.yahoo.config.codegen.LeafCNode.DoubleLeaf; import com.yahoo.config.codegen.LeafCNode.EnumLeaf; import com.yahoo.config.codegen.LeafCNode.FileLeaf; import com.yahoo.config.codegen.LeafCNode.IntegerLeaf; import com.yahoo.config.codegen.LeafCNode.LongLeaf; import com.yahoo.config.codegen.LeafCNode.OptionalPathLeaf; import com.yahoo.config.codegen.LeafCNode.PathLeaf; import com.yahoo.config.codegen.LeafCNode.ReferenceLeaf; import com.yahoo.config.codegen.LeafCNode.StringLeaf; import com.yahoo.config.codegen.LeafCNode.UrlLeaf; import com.yahoo.config.codegen.LeafCNode.ModelLeaf; import java.util.LinkedList; import java.util.List; import java.util.stream.Collectors; import static com.yahoo.config.codegen.BuilderGenerator.getBuilder; import static com.yahoo.config.codegen.JavaClassBuilder.INDENTATION; import static com.yahoo.config.codegen.ReservedWords.INTERNAL_PREFIX; import static java.util.Arrays.stream; /** * @author gjoranv * @author Tony Vaagenes * @author ollivir */ public class ConfigGenerator { // TODO: don't take indent as method param - the caller should indent public static String generateContent(String indent, InnerCNode node, boolean isOuter) { CNode[] children = node.getChildren(); return indentCode(indent, getBuilder(node) + "\n\n" + stream(children).map(ConfigGenerator::getFieldDefinition).collect(Collectors.joining("\n")) + "\n\n" + getConstructors(node) + "\n\n" + getAccessors(children) + "\n\n" + getGetChangesRequiringRestart(node) + "\n\n" + getContainsFieldsFlaggedWithRestart(node, isOuter) + getStaticMethods(node) + generateCodeForChildren(children, indent) ); } private static String generateCodeForChildren(CNode[] children, String indent) { List pieces = new LinkedList<>(); for (CNode child : children) { if (child instanceof EnumLeaf) { pieces.add(getEnumCode((EnumLeaf) child) + "\n"); } else if (child instanceof InnerCNode) { pieces.add(getInnerDefinition((InnerCNode) child, indent) + "\n"); } } return String.join("\n", pieces); } private static String getInnerDefinition(InnerCNode inner, String indent) { return (getClassDoc(inner) + "\n" +// getClassDeclaration(inner) + "\n" +// generateContent(indent, inner, false)).trim() + "\n}"; } private static String getClassDeclaration(CNode node) { return "public final static class " + nodeClass(node) + " extends InnerNode { \n"; } private static String getFieldDefinition(CNode node) { String fieldDef; if (node instanceof LeafCNode && node.isArray) { fieldDef = String.format("LeafNodeVector<%s, %s> %s;", boxedDataType(node), nodeClass(node), node.getName()); } else if (node instanceof InnerCNode && node.isArray) { fieldDef = String.format("InnerNodeVector<%s> %s;", nodeClass(node), node.getName()); } else if (node.isMap) { fieldDef = String.format("Map %s;", nodeClass(node), node.getName()); } else { fieldDef = String.format("%s %s;", nodeClass(node), node.getName()); } return node.getCommentBlock("//") + "private final " + fieldDef; } private static String getStaticMethods(InnerCNode node) { if (node.isArray) { return getStaticMethodsForInnerArray(node) + "\n\n"; } else if (node.isMap) { return getStaticMethodsForInnerMap(node) + "\n\n"; } else { return ""; } } private static String getContainsFieldsFlaggedWithRestart(CNode node, boolean isOuter) { if (isOuter) { return String.format("private static boolean containsFieldsFlaggedWithRestart() {\n" +// " return %b;\n" +// "}\n\n", node.needRestart()); } else { return ""; } } private static String getGetChangesRequiringRestart(InnerCNode node) { List comparisons = new LinkedList<>(); for (CNode child : node.getChildren()) { if (child.needRestart()) { comparisons.add("\n " + getComparison(child)); } } return "private ChangesRequiringRestart getChangesRequiringRestart(" + nodeClass(node) + " newConfig) {\n" +// " ChangesRequiringRestart changes = new ChangesRequiringRestart(\"" + node.getName() + "\");" + String.join("", comparisons) + "\n" +// " return changes;\n" +// "}"; } private static String quotedComment(CNode node) { return node.getComment().replace("\n", "\\n").replace("\"", "\\\""); } private static String getComparison(CNode node) { if (node instanceof InnerCNode && node.isArray) { return " changes.compareArray(this." + node.getName() + ", newConfig." + node.getName() + ", \"" + node.getName() + "\", \"" + quotedComment(node) + "\",\n" +// " (a,b) -> ((" + nodeClass(node) + ")a).getChangesRequiringRestart((" + nodeClass(node) + ")b));"; } else if (node instanceof InnerCNode && node.isMap) { return " changes.compareMap(this." + node.getName() + ", newConfig." + node.getName() + ", \"" + node.getName() + "\", \"" + quotedComment(node) + "\",\n" +// " (a,b) -> ((" + nodeClass(node) + ")a).getChangesRequiringRestart((" + nodeClass(node) + ")b));"; } else if (node instanceof InnerCNode) { return " changes.mergeChanges(\"" + node.getName() + "\", this." + node.getName() + ".getChangesRequiringRestart(newConfig." + node.getName() + "));"; } else if (node.isArray) { return " changes.compareArray(this." + node.getName() + ", newConfig." + node.getName() + ", \"" + node.getName() + "\", \"" + quotedComment(node) + "\",\n" +// " (a,b) -> new ChangesRequiringRestart(\"" + node.getName() + "\").compare(a,b,\"\",\"" + quotedComment(node) + "\"));"; } else if (node.isMap) { return " changes.compareMap(this." + node.getName() + ", newConfig." + node.getName() + ", \"" + node.getName() + "\", \"" + quotedComment(node) + "\",\n" +// " (a,b) -> new ChangesRequiringRestart(\"" + node.getName() + "\").compare(a,b,\"\",\"" + quotedComment(node) + "\"));"; } else { return " changes.compare(this." + node.getName() + ", newConfig." + node.getName() + ", \"" + node.getName() + "\", \"" + quotedComment(node) + "\");"; } } private static String scalarDefault(LeafCNode scalar) { if (scalar.getDefaultValue() == null) { return ""; } else if (scalar instanceof EnumLeaf && scalar.getDefaultValue().getValue() == null) { return ""; } else if (scalar instanceof EnumLeaf) { return nodeClass(scalar) + "." + scalar.getDefaultValue().getStringRepresentation(); } else if (scalar instanceof LongLeaf) { return scalar.getDefaultValue().getStringRepresentation() + "L"; } else if (scalar instanceof DoubleLeaf) { return scalar.getDefaultValue().getStringRepresentation() + "D"; } else { return scalar.getDefaultValue().getStringRepresentation(); } } private static String assignFromBuilder(CNode child) { final String name = child.getName(); final String className = nodeClass(child); final boolean isArray = child.isArray; final boolean isMap = child.isMap; if (child instanceof FileLeaf && isArray) { return name + " = LeafNodeVector.createFileNodeVector(builder." + name + ");"; } else if (child instanceof PathLeaf && isArray) { return name + " = LeafNodeVector.createPathNodeVector(builder." + name + ");"; } else if (child instanceof OptionalPathLeaf && isArray) { return name + " = LeafNodeVector.createOptionalPathNodeVector(builder." + name + ");"; } else if (child instanceof UrlLeaf && isArray) { return name + " = LeafNodeVector.createUrlNodeVector(builder." + name + ");"; } else if (child instanceof ModelLeaf && isArray) { return name + " = LeafNodeVector.createModelNodeVector(builder." + name + ");"; } else if (child instanceof LeafCNode && isArray) { return name + " = new LeafNodeVector<>(builder." + name + ", new " + className + "());"; } else if (child instanceof FileLeaf && isMap) { return name + " = LeafNodeMaps.asFileNodeMap(builder." + name + ");"; } else if (child instanceof PathLeaf && isMap) { return name + " = LeafNodeMaps.asPathNodeMap(builder." + name + ");"; } else if (child instanceof OptionalPathLeaf && isMap) { return name + " = LeafNodeMaps.asOptionalPathNodeMap(builder." + name + ");"; } else if (child instanceof UrlLeaf && isMap) { return name + " = LeafNodeMaps.asUrlNodeMap(builder." + name + ");"; } else if (child instanceof ModelLeaf && isMap) { return name + " = LeafNodeMaps.asModelNodeMap(builder." + name + ");"; } else if (child instanceof LeafCNode && isMap) { return name + " = LeafNodeMaps.asNodeMap(builder." + name + ", new " + className + "());"; } else if (child instanceof InnerCNode && isArray) { return name + " = " + className + ".createVector(builder." + name + ");"; } else if (child instanceof InnerCNode && isMap) { return name + " = " + className + ".createMap(builder." + name + ");"; } else if (child instanceof InnerCNode) { return name + " = new " + className + "(builder." + name + ", throwIfUninitialized);"; } else if (child instanceof LeafCNode) { return name + " = (builder." + name + " == null) ?\n" + " new " + className + "(" + scalarDefault((LeafCNode) child) + ") : new " + className + "(builder." + name + ");"; } else { throw new IllegalStateException("Cannot create assignment for node"); // should not happen } } private static String getConstructors(InnerCNode inner) { // TODO: merge these two constructors into one when the config library uses builders to set values from payload. return "public " + nodeClass(inner) + "(Builder builder) {\n" +// " this(builder, true);\n" +// "}\n" +// "\n" +// "private " + nodeClass(inner) + "(Builder builder, boolean throwIfUninitialized) {\n" +// " if (throwIfUninitialized && ! builder." + INTERNAL_PREFIX + "uninitialized.isEmpty())\n" +// " throw new IllegalArgumentException(\"The following builder parameters for \" +\n" +// " \"" + inner.getFullName() + " must be initialized: \" + builder." + INTERNAL_PREFIX + "uninitialized);\n" +// "\n" +// indentCode(INDENTATION, stream(inner.getChildren()).map(ConfigGenerator::assignFromBuilder).collect(Collectors.joining("\n"))) + "\n" +// "}"; } private static String getAccessorCode(CNode node) { if (node.isArray) { return accessorsForArray(node); } else if (node.isMap) { return accessorsForMap(node); } else { return accessorForStructOrScalar(node); } } private static String valueAccessor(CNode node) { if (node instanceof LeafCNode) { return ".value()"; } else { return ""; } } private static String listAccessor(CNode node) { if (node instanceof LeafCNode) { return node.getName() + ".asList()"; } else { return node.getName(); } } private static String mapAccessor(CNode node) { if (node instanceof LeafCNode) { return "LeafNodeMaps.asValueMap(" + node.getName() + ")"; } else { return "Collections.unmodifiableMap(" + node.getName() + ")"; } } private static String accessorsForArray(CNode node) { final String name = node.getName(); final String fullName = node.getFullName(); return "/**\n" +// " * @return " + fullName + "\n" +// " */\n" +// "public List<" + boxedDataType(node) + "> " + name + "() {\n" +// " return " + listAccessor(node) + ";\n" +// "}\n" +// "\n" +// "/**\n" +// " * @param i the index of the value to return\n" +// " * @return " + fullName + "\n" +// " */\n" +// "public " + userDataType(node) + " " + name + "(int i) {\n" +// " return " + name + ".get(i)" + valueAccessor(node) + ";\n" +// "}"; } private static String accessorsForMap(CNode node) { final String name = node.getName(); final String fullName = node.getFullName(); return "/**\n" +// " * @return " + fullName + "\n" +// " */\n" +// "public Map " + name + "() {\n" +// " return " + mapAccessor(node) + ";\n" +// "}\n" +// "\n" +// "/**\n" +// " * @param key the key of the value to return\n" +// " * @return " + fullName + "\n" +// " */\n" +// "public " + userDataType(node) + " " + name + "(String key) {\n" +// " return " + name + ".get(key)" + valueAccessor(node) + ";\n" +// "}"; } private static String accessorForStructOrScalar(CNode node) { return "/**\n" +// " * @return " + node.getFullName() + "\n" +// " */\n" +// "public " + userDataType(node) + " " + node.getName() + "() {\n" +// " return " + node.getName() + valueAccessor(node) + ";\n" +// "}"; } private static String getAccessors(CNode[] children) { List accessors = new LinkedList<>(); for (CNode child : children) { String accessor = getAccessorCode(child); if (! accessor.isEmpty()) { accessors.add(accessor); } } return String.join("\n\n", accessors); } private static String getStaticMethodsForInnerArray(InnerCNode inner) { final String nc = nodeClass(inner); return String.format("private static InnerNodeVector<%s> createVector(List builders) {\n" +// " List<%s> elems = new ArrayList<>();\n" +// " for (Builder b : builders) {\n" +// " elems.add(new %s(b));\n" +// " }\n" +// " return new InnerNodeVector<%s>(elems);\n" +// "}", nc, nc, nc, nc); } private static String getStaticMethodsForInnerMap(InnerCNode inner) { final String nc = nodeClass(inner); return String.format( "private static Map createMap(Map builders) {\n" +// " Map ret = new LinkedHashMap<>();\n" +// " for(String key : builders.keySet()) {\n" +// " ret.put(key, new %s(builders.get(key)));\n" +// " }\n" +// " return Collections.unmodifiableMap(ret);\n" +// "}", nc, nc, nc); } private static String getEnumCode(EnumLeaf en) { String enumValues = stream(en.getLegalValues()).map(e -> String.format(" public final static Enum %s = Enum.%s;", e, e)).collect(Collectors.joining("\n")); String code = String.format("%s\n" +// "public final static class %s extends EnumNode<%s> {\n" +// "\n" +// " public %s(){\n" +// " this.value = null;\n" +// " }\n" +// "\n" +// " public %s(Enum enumValue) {\n" +// " super(enumValue != null);\n" +// " this.value = enumValue;\n" +// " }\n" +// "\n" +// " public enum Enum {%s}\n" +// "%s\n" +// "\n" +// " @Override\n" +// " protected boolean doSetValue(String name) {\n" +// " try {\n" +// " value = Enum.valueOf(name);\n" +// " return true;\n" +// " } catch (IllegalArgumentException e) {\n" +// " }\n" +// " return false;\n" +// " }\n" +// "}", getClassDoc(en), nodeClass(en), nodeClass(en) + ".Enum", nodeClass(en), nodeClass(en), String.join(", ", en.getLegalValues()), enumValues); return indentCode("", code); } private static String getClassDoc(CNode node) { String header = "/**\n" + " * This class represents " + node.getFullName(); String nodeComment = node.getCommentBlock(" *"); if (nodeComment.isEmpty()) { return header + "\n */"; } else { if (nodeComment.endsWith("\n")) { nodeComment = nodeComment.substring(0, nodeComment.length() - 1); } return header + "\n * \n" + nodeComment + "\n */"; } } static String indentCode(String indent, String code) { List indented = new LinkedList<>(); for (String line : code.split("\n", -1)) { indented.add(line.length() > 0 ? indent + line : line); } return String.join("\n", indented); } /** * @return the name of the class that is generated by this node. */ static String nodeClass(CNode node) { if (node.getName().length() == 0) { throw new CodegenRuntimeException("Node with empty name, under parent " + node.getParent().getName()); } else if (node instanceof InnerCNode && node.getParent() == null) { return ConfiggenUtil.createClassName(node.getName()); } else if (node instanceof BooleanLeaf) { return "BooleanNode"; } else if (node instanceof DoubleLeaf) { return "DoubleNode"; } else if (node instanceof FileLeaf) { return "FileNode"; } else if (node instanceof PathLeaf) { return "PathNode"; } else if (node instanceof OptionalPathLeaf) { return "OptionalPathNode"; } else if (node instanceof UrlLeaf) { return "UrlNode"; } else if (node instanceof ModelLeaf) { return "ModelNode"; } else if (node instanceof IntegerLeaf) { return "IntegerNode"; } else if (node instanceof LongLeaf) { return "LongNode"; } else if (node instanceof ReferenceLeaf) { return "ReferenceNode"; } else if (node instanceof StringLeaf) { return "StringNode"; } else { return ConfiggenUtil.capitalize(node.getName()); } } static String userDataType(CNode node) { if (node instanceof InnerCNode) { return nodeClass(node); } else if (node instanceof EnumLeaf) { return nodeClass(node) + ".Enum"; } else if (node instanceof BooleanLeaf) { return "boolean"; } else if (node instanceof DoubleLeaf) { return "double"; } else if (node instanceof FileLeaf) { return "FileReference"; } else if (node instanceof PathLeaf) { return "Path"; } else if (node instanceof OptionalPathLeaf) { return "Optional"; } else if (node instanceof UrlLeaf) { return "File"; } else if (node instanceof ModelLeaf) { return "Path"; } else if (node instanceof IntegerLeaf) { return "int"; } else if (node instanceof LongLeaf) { return "long"; } else if (node instanceof StringLeaf) { return "String"; } else { throw new IllegalStateException("Cannot determine user data type for node '" + node + "'"); // should not occur } } /** * @return the boxed java data type, e.g. Integer for int */ static String boxedDataType(CNode node) { String rawType = userDataType(node); if ("int".equals(rawType)) { return "Integer"; } else if (rawType.toLowerCase().equals(rawType)) { return ConfiggenUtil.capitalize(rawType); } else if (rawType.startsWith("Optional<")) { return "Optional"; } else { return rawType; } } }