aboutsummaryrefslogtreecommitdiffstats
path: root/configgen/src/main/java/com/yahoo/config/codegen/JavaClassBuilder.java
blob: 78977f43bdd512d9d4c39665275cf2895066f720 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.config.codegen;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.PrintStream;
import java.util.Arrays;
import java.util.Random;
import java.util.Set;
import java.util.stream.Collectors;

import static com.yahoo.config.codegen.ConfigGenerator.indentCode;
import static com.yahoo.config.codegen.ConfiggenUtil.createClassName;
import static com.yahoo.config.codegen.DefParser.DEFAULT_PACKAGE_PREFIX;

/**
 * Builds one Java class based on the given CNode tree.
 *
 * @author gjoranv
 * @author Tony Vaagenes
 * @author ollivir
 */
public class JavaClassBuilder implements ClassBuilder {

    static final String INDENTATION = "  ";

    private final InnerCNode root;
    private final NormalizedDefinition nd;
    private final String javaPackage;
    private final String className;
    private final File destDir;

    public JavaClassBuilder(InnerCNode root, NormalizedDefinition nd, File destDir, String rawPackagePrefix) {
        this.root = root;
        this.nd = nd;
        String packagePrefix = (rawPackagePrefix != null) ? rawPackagePrefix : DEFAULT_PACKAGE_PREFIX;
        this.javaPackage = (root.getPackage() != null) ? root.getPackage() : packagePrefix + root.getNamespace();
        this.className = createClassName(root.getName());
        this.destDir = destDir;
    }

    @Override
    public void createConfigClasses() {
        try {
            File outFile = new File(getDestPath(destDir, javaPackage), className + ".java");
            try (PrintStream out = new PrintStream(new FileOutputStream(outFile))) {
                out.print(getConfigClass(className));
            }
        } catch (FileNotFoundException e) {
            throw new CodegenRuntimeException(e);
        }
    }

    public String getConfigClass(String className) {
        return getHeader() + "\n\n" + //
                getRootClassDeclaration(root, className) + "\n\n" + //
                indentCode(INDENTATION, getFrameworkCode()) + "\n\n" + //
                ConfigGenerator.generateContent(INDENTATION, root, true) + "\n" + //
                "}\n";
    }

    private String getHeader() {
        return "/**\n" + //
                " * This file is generated from a config definition file.\n" + //
                " * ------------   D O   N O T   E D I T !   ------------\n" + //
                " */\n" + //
                "\n" + //
                "package " + javaPackage + ";\n" + //
                "\n" + //
                "import java.util.*;\n" + //
                "import java.io.File;\n" + //
                "import java.nio.file.Path;\n" + //
                "import com.yahoo.config.*;";
    }

    // TODO: remove the extra comment line " *" if root.getCommentBlock is empty
    private String getRootClassDeclaration(InnerCNode root, String className) {
        return "/**\n" + //
                " * This class represents the root node of " + root.getFullName() + "\n" + //
                " *\n" + //
                "" + root.getCommentBlock(" *") + " */\n" + //
                "public final class " + className + " extends ConfigInstance {\n" + //
                "\n" + //
                "  public final static String CONFIG_DEF_MD5 = \"" + root.getMd5() + "\";\n" + //
                "  public final static String CONFIG_DEF_NAME = \"" + root.getName() + "\";\n" + //
                "  public final static String CONFIG_DEF_NAMESPACE = \"" + root.getNamespace() + "\";\n" + //
                "  public final static String[] CONFIG_DEF_SCHEMA = {\n" + //
                "" + indentCode(INDENTATION + INDENTATION, getDefSchema()) + "\n" + //
                "  };\n" + //
                "\n" + //
                "  public static String getDefMd5()       { return CONFIG_DEF_MD5; }\n" + //
                "  public static String getDefName()      { return CONFIG_DEF_NAME; }\n" + //
                "  public static String getDefNamespace() { return CONFIG_DEF_NAMESPACE; }";
    }

    private String getDefSchema() {
        return nd.getNormalizedContent().stream().map(l -> "\"" + l.replace("\"", "\\\"") + "\"").collect(Collectors.joining(",\n"));
    }

    private String getFrameworkCode() {
        return "public interface Producer extends ConfigInstance.Producer {\n" + //
                "  void getConfig(Builder builder);\n" + //
                "}";
    }

    /**
     * @param rootDir the root directory for the destination path.
     * @param javaPackage the java package
     * @return the destination path for the generated config file, including the given rootDir.
     */
    private File getDestPath(File rootDir, String javaPackage) {
        File dir = rootDir;
        for (String subDir : javaPackage.split("\\.")) {
            dir = new File(dir, subDir);
            synchronized (this) {
                if (!dir.isDirectory() && !dir.mkdir()) {
                    throw new CodegenRuntimeException("Could not create " + dir.getPath());
                }
            }
        }
        return dir;
    }

    /**
     * Returns a name that can be safely used as a local variable in the generated
     * config class for the given node. The name will be based on the given basis
     * string, but the basis itself is not a possible return value.
     *
     * @param node the node to find a unused symbol name for.
     * @param basis the basis for the generated symbol name.
     * @return a name that is not used in the given config node.
     */
    static String createUniqueSymbol(CNode node, String basis) {
        Set<String> usedSymbols = Arrays.stream(node.getChildren()).map(CNode::getName).collect(Collectors.toSet());
        Random rng = new Random();

        for (int i = 1;; i++) {
            String candidate = (i < basis.length()) ? basis.substring(0, i)
                    : ReservedWords.INTERNAL_PREFIX + basis + rng.nextInt(Integer.MAX_VALUE);
            if ( ! usedSymbols.contains(candidate)) {
                return candidate;
            }
        }
    }

    public String className() {
        return className;
    }

    public String javaPackage() {
        return javaPackage;
    }

}