// Copyright 2017 Yahoo Holdings. 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, FileNotFoundException, FileOutputStream, PrintStream}
import com.yahoo.config.codegen.ConfigGenerator.{createClassName, indentCode}
import com.yahoo.config.codegen.DefParser.DEFAULT_PACKAGE_PREFIX
import scala.collection.JavaConverters._
import scala.util.Random
/**
* Builds one Java class based on the given CNode tree.
*
* @author gjoranv
* @author tonytv
*/
class JavaClassBuilder(
root: InnerCNode,
nd: NormalizedDefinition,
destDir: File,
rawPackagePrefix: String)
extends ClassBuilder
{
import JavaClassBuilder._
val packagePrefix = if (rawPackagePrefix != null) rawPackagePrefix else DEFAULT_PACKAGE_PREFIX
val javaPackage = packagePrefix + root.getNamespace
val className = createClassName(root.getName)
override def createConfigClasses() {
try {
val outFile = new File(getDestPath(destDir, root.getNamespace), className + ".java")
var out: PrintStream = null
try {
out = new PrintStream(new FileOutputStream(outFile))
out.print(getConfigClass(className))
} finally {
if (out != null) out.close()
}
System.err.println(outFile.getPath + " successfully written.")
}
catch {
case e: FileNotFoundException => {
throw new CodegenRuntimeException(e)
}
}
}
def getConfigClass(className:String): String = {
val ret = new StringBuilder
ret.append(getHeader).append("\n\n")
ret.append(getRootClassDeclaration(root, className)).append("\n\n")
ret.append(indentCode(Indentation, getFrameworkCode(className))).append("\n\n")
ret.append(ConfigGenerator.generateContent(Indentation, root)).append("\n")
ret.append("}\n")
ret.toString()
}
private def getHeader: String = {
|/**
| * This file is generated from a config definition file.
| * ------------ D O N O T E D I T ! ------------
| */
|
|package {javaPackage};
|
|import java.util.*;
|import java.nio.file.Path;
|import edu.umd.cs.findbugs.annotations.NonNull;
|{getImportFrameworkClasses(root.getNamespace)}
.text.stripMargin.trim
}
private def getImportFrameworkClasses(namespace: String): String = {
if (namespace != CNode.DEFAULT_NAMESPACE)
"import " + packagePrefix + CNode.DEFAULT_NAMESPACE + ".*;\n"
else
""
}
// TODO: remove the extra comment line " *" if root.getCommentBlock is empty
private def getRootClassDeclaration(root:InnerCNode, className: String): String = {
|/**
| * This class represents the root node of {root.getFullName}
| *
|{root.getCommentBlock(" *")} */
|public final class {className} extends ConfigInstance {{
|
| public final static String CONFIG_DEF_MD5 = "{root.getMd5}";
| public final static String CONFIG_DEF_NAME = "{root.getName}";
| public final static String CONFIG_DEF_NAMESPACE = "{root.getNamespace}";
| public final static String CONFIG_DEF_VERSION = "{root.getVersion}";
| public final static String[] CONFIG_DEF_SCHEMA = {{
|{indentCode(Indentation * 2, getDefSchema)}
| }};
|
| public static String getDefMd5() {{ return CONFIG_DEF_MD5; }}
| public static String getDefName() {{ return CONFIG_DEF_NAME; }}
| public static String getDefNamespace() {{ return CONFIG_DEF_NAMESPACE; }}
| public static String getDefVersion() {{ return CONFIG_DEF_VERSION; }}
.text.stripMargin.trim
}
private def getDefSchema: String = {
nd.getNormalizedContent.asScala.map { line =>
"\"" +
line.replace("\"", "\\\"") +
"\""
}.mkString(",\n")
}
private def getFrameworkCode(className: String): String = {
getProducerBase
}
private def getProducerBase = {
"""
|public interface Producer extends ConfigInstance.Producer {
| void getConfig(Builder builder);
|}
""".stripMargin.trim
}
/**
* @param rootDir The root directory for the destination path.
* @param namespace The namespace from the def file
* @return the destination path for the generated config file, including the given rootDir.
*/
private def getDestPath(rootDir: File, namespace: String): File = {
var dir: File = rootDir
val subDirs: Array[String] = (packagePrefix + namespace).split("""\.""")
for (subDir <- subDirs) {
dir = new File(dir, subDir)
this.synchronized {
if (!dir.isDirectory && !dir.mkdir) throw new CodegenRuntimeException("Could not create " + dir.getPath)
}
}
dir
}
}
object JavaClassBuilder {
val Indentation = " "
/**
* 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.
*/
def createUniqueSymbol(node: CNode, basis: String) = {
def getCandidate(cnt: Int) = {
if (cnt < basis.length())
basis.substring(0, cnt)
else
ReservedWords.INTERNAL_PREFIX + basis + Random.nextInt().abs
}
def getUsedSymbols: Set[String] = {
(node.getChildren map (child => child.getName)).toSet
}
// TODO: refactoring potential
val usedSymbols = getUsedSymbols
var count = 1
var candidate = getCandidate(count)
while (usedSymbols contains(candidate)) {
count += 1
candidate = getCandidate(count)
}
candidate
}
}