blob: e4d8dbcccc8c61e453e5206f73831da4e7141bf7 [file] [log] [blame]
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hadoop.yarn.webapp.hamlet;
import com.google.common.collect.Sets;
import java.io.IOException;
import java.io.PrintWriter;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Locale;
import java.util.Set;
import java.util.regex.Pattern;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.GnuParser;
import org.apache.commons.cli.HelpFormatter;
import org.apache.commons.cli.Options;
import org.apache.hadoop.yarn.webapp.WebAppException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Generates a specific hamlet implementation class from a spec class
* using a generic hamlet implementation class.
*/
public class HamletGen {
static final Logger LOG = LoggerFactory.getLogger(HamletGen.class);
static final Options opts = new Options();
static {
opts.addOption("h", "help", false, "Print this help message").
addOption("s", "spec-class", true,
"The class that holds the spec interfaces. e.g. HamletSpec").
addOption("i", "impl-class", true,
"An implementation class. e.g. HamletImpl").
addOption("o", "output-class", true, "Output class name").
addOption("p", "output-package", true, "Output package name");
};
static final Pattern elementRegex = Pattern.compile("^[A-Z][A-Z0-9]*$");
int bytes = 0;
PrintWriter out;
final Set<String> endTagOptional = Sets.newHashSet();
final Set<String> inlineElements = Sets.newHashSet();
Class<?> top; // html top-level interface
String hamlet; // output class simple name;
boolean topMode;
/**
* Generate a specific Hamlet implementation from a spec.
* @param specClass holds hamlet interfaces. e.g. {@link HamletSpec}
* @param implClass a generic hamlet implementation. e.g. {@link HamletImpl}
* @param outputName name of the output class. e.g. {@link Hamlet}
* @param outputPkg package name of the output class.
* @throws IOException
*/
public void generate(Class<?> specClass, Class<?> implClass,
String outputName, String outputPkg) throws IOException {
LOG.info("Generating {} using {} and {}", new Object[]{outputName,
specClass, implClass});
out = new PrintWriter(outputName +".java", "UTF-8");
hamlet = basename(outputName);
String pkg = pkgName(outputPkg, implClass.getPackage().getName());
puts(0, "// Generated by HamletGen. Do NOT edit!\n",
"package ", pkg, ";\n",
"import java.io.PrintWriter;\n",
"import java.util.EnumSet;\n",
"import static java.util.EnumSet.*;\n",
"import static ", implClass.getName(), ".EOpt.*;\n",
"import org.apache.hadoop.yarn.webapp.SubView;");
String implClassName = implClass.getSimpleName();
if (!implClass.getPackage().getName().equals(pkg)) {
puts(0, "import ", implClass.getName(), ';');
}
puts(0, "\n",
"public class ", hamlet, " extends ", implClassName,
" implements ", specClass.getSimpleName(), "._Html {\n",
" public ", hamlet, "(PrintWriter out, int nestLevel,",
" boolean wasInline) {\n",
" super(out, nestLevel, wasInline);\n",
" }\n\n", // inline is context sensitive
" static EnumSet<EOpt> opt(boolean endTag, boolean inline, ",
"boolean pre) {\n",
" EnumSet<EOpt> opts = of(ENDTAG);\n",
" if (!endTag) opts.remove(ENDTAG);\n",
" if (inline) opts.add(INLINE);\n",
" if (pre) opts.add(PRE);\n",
" return opts;\n",
" }");
initLut(specClass);
genImpl(specClass, implClassName, 1);
LOG.info("Generating {} methods", hamlet);
genMethods(hamlet, top, 1);
puts(0, "}");
out.close();
LOG.info("Wrote {} bytes to {}.java", bytes, outputName);
}
String basename(String path) {
return path.substring(path.lastIndexOf('/') + 1);
}
String pkgName(String pkg, String defaultPkg) {
if (pkg == null || pkg.isEmpty()) return defaultPkg;
return pkg;
}
void initLut(Class<?> spec) {
endTagOptional.clear();
inlineElements.clear();
for (Class<?> cls : spec.getClasses()) {
Annotation a = cls.getAnnotation(HamletSpec.Element.class);
if (a != null && !((HamletSpec.Element) a).endTag()) {
endTagOptional.add(cls.getSimpleName());
}
if (cls.getSimpleName().equals("Inline")) {
for (Method method : cls.getMethods()) {
String retName = method.getReturnType().getSimpleName();
if (isElement(retName)) {
inlineElements.add(retName);
}
}
}
}
}
void genImpl(Class<?> spec, String implClassName, int indent) {
String specName = spec.getSimpleName();
for (Class<?> cls : spec.getClasses()) {
String className = cls.getSimpleName();
if (cls.isInterface()) {
genFactoryMethods(cls, indent);
}
if (isElement(className)) {
LOG.info("Generating class {}<T>", className);
puts(indent, "\n",
"public class ", className, "<T extends _>",
" extends EImp<T> implements ", specName, ".", className, " {\n",
" public ", className, "(String name, T parent,",
" EnumSet<EOpt> opts) {\n",
" super(name, parent, opts);\n",
" }");
genMethods(className, cls, indent + 1);
puts(indent, "}");
} else if (className.equals("_Html")) {
top = cls;
}
}
}
void genFactoryMethods(Class<?> cls, int indent) {
for (Method method : cls.getDeclaredMethods()) {
String retName = method.getReturnType().getSimpleName();
String methodName = method.getName();
if (methodName.charAt(0) == '$') continue;
if (isElement(retName) && method.getParameterTypes().length == 0) {
genFactoryMethod(retName, methodName, indent);
}
}
}
void genMethods(String className, Class<?> cls, int indent) {
topMode = (top != null && cls.equals(top));
for (Method method : cls.getMethods()) {
String retName = method.getReturnType().getSimpleName();
if (method.getName().charAt(0) == '$') {
genAttributeMethod(className, method, indent);
} else if (isElement(retName)) {
genNewElementMethod(className, method, indent);
} else {
genCurElementMethod(className, method, indent);
}
}
}
void genAttributeMethod(String className, Method method, int indent) {
String methodName = method.getName();
String attrName = methodName.substring(1).replace('_', '-');
Type[] params = method.getGenericParameterTypes();
echo(indent, "\n",
"@Override\n",
"public ", className, topMode ? " " : "<T> ", methodName, "(");
if (params.length == 0) {
puts(0, ") {");
puts(indent,
" addAttr(\"", attrName, "\", null);\n",
" return this;\n", "}");
} else if (params.length == 1) {
String typeName = getTypeName(params[0]);
puts(0, typeName, " value) {");
if (typeName.equals("EnumSet<LinkType>")) {
puts(indent,
" addRelAttr(\"", attrName, "\", value);\n",
" return this;\n", "}");
} else if (typeName.equals("EnumSet<Media>")) {
puts(indent,
" addMediaAttr(\"", attrName, "\", value);\n",
" return this;\n", "}");
} else {
puts(indent,
" addAttr(\"", attrName, "\", value);\n",
" return this;\n", "}");
}
} else {
throwUnhandled(className, method);
}
}
String getTypeName(Type type) {
if (type instanceof Class<?>) {
return ((Class<?>)type).getSimpleName();
}
ParameterizedType pt = (ParameterizedType) type;
return ((Class<?>)pt.getRawType()).getSimpleName() +"<"+
((Class<?>)pt.getActualTypeArguments()[0]).getSimpleName() +">";
}
void genFactoryMethod(String retName, String methodName, int indent) {
puts(indent, "\n",
"private <T extends _> ", retName, "<T> ", methodName,
"_(T e, boolean inline) {\n",
" return new ", retName, "<T>(\"", retName.toLowerCase(Locale.US),
"\", e, opt(", !endTagOptional.contains(retName), ", inline, ",
retName.equals("PRE"), ")); }");
}
void genNewElementMethod(String className, Method method, int indent) {
String methodName = method.getName();
String retName = method.getReturnType().getSimpleName();
Class<?>[] params = method.getParameterTypes();
echo(indent, "\n",
"@Override\n",
"public ", retName, "<", className, topMode ? "> " : "<T>> ",
methodName, "(");
if (params.length == 0) {
puts(0, ") {");
puts(indent,
topMode ? "" : " closeAttrs();\n",
" return ", retName.toLowerCase(Locale.US), "_(this, ",
isInline(className, retName), ");\n", "}");
} else if (params.length == 1) {
puts(0, "String selector) {");
puts(indent,
" return setSelector(", methodName, "(), selector);\n", "}");
} else {
throwUnhandled(className, method);
}
}
boolean isInline(String container, String className) {
if ((container.equals("BODY") || container.equals(hamlet) ||
container.equals("HEAD") || container.equals("HTML")) &&
(className.equals("INS") || className.equals("DEL") ||
className.equals("SCRIPT"))) {
return false;
}
return inlineElements.contains(className);
}
void genCurElementMethod(String className, Method method, int indent) {
String methodName = method.getName();
Class<?>[] params = method.getParameterTypes();
if (topMode || params.length > 0) {
echo(indent, "\n",
"@Override\n",
"public ", className, topMode ? " " : "<T> ", methodName, "(");
}
if (params.length == 0) {
if (topMode) {
puts(0, ") {");
puts(indent, " return this;\n", "}");
}
} else if (params.length == 1) {
if (methodName.equals("base")) {
puts(0, "String href) {");
puts(indent,
" return base().$href(href)._();\n", "}");
} else if (methodName.equals("script")) {
puts(0, "String src) {");
puts(indent,
" return setScriptSrc(script(), src)._();\n", "}");
} else if (methodName.equals("style")) {
puts(0, "Object... lines) {");
puts(indent,
" return style().$type(\"text/css\")._(lines)._();\n", "}");
} else if (methodName.equals("img")) {
puts(0, "String src) {");
puts(indent,
" return ", methodName, "().$src(src)._();\n", "}");
} else if (methodName.equals("br") || methodName.equals("hr") ||
methodName.equals("col")) {
puts(0, "String selector) {");
puts(indent,
" return setSelector(", methodName, "(), selector)._();\n", "}");
} else if (methodName.equals("link")) {
puts(0, "String href) {");
puts(indent,
" return setLinkHref(", methodName, "(), href)._();\n", "}");
} else if (methodName.equals("_")) {
if (params[0].getSimpleName().equals("Class")) {
puts(0, "Class<? extends SubView> cls) {");
puts(indent,
" ", topMode ? "subView" : "_v", "(cls);\n",
" return this;\n", "}");
} else {
puts(0, "Object... lines) {");
puts(indent,
" _p(", needsEscaping(className), ", lines);\n",
" return this;\n", "}");
}
} else if (methodName.equals("_r")) {
puts(0, "Object... lines) {");
puts(indent,
" _p(false, lines);\n",
" return this;\n", "}");
} else {
puts(0, "String cdata) {");
puts(indent,
" return ", methodName, "()._(cdata)._();\n", "}");
}
} else if (params.length == 2) {
if (methodName.equals("meta")) {
puts(0, "String name, String content) {");
puts(indent,
" return meta().$name(name).$content(content)._();\n", "}");
} else if (methodName.equals("meta_http")) {
puts(0, "String header, String content) {");
puts(indent,
" return meta().$http_equiv(header).$content(content)._();\n",
"}");
} else if (methodName.equals("a")) {
puts(0, "String href, String anchorText) {");
puts(indent,
" return a().$href(href)._(anchorText)._();\n", "}");
} else if (methodName.equals("bdo")) {
puts(0, "Dir dir, String cdata) {");
puts(indent, " return bdo().$dir(dir)._(cdata)._();\n", "}");
} else if (methodName.equals("label")) {
puts(0, "String forId, String cdata) {");
puts(indent, " return label().$for(forId)._(cdata)._();\n", "}");
} else if (methodName.equals("param")) {
puts(0, "String name, String value) {");
puts(indent,
" return param().$name(name).$value(value)._();\n", "}");
} else {
puts(0, "String selector, String cdata) {");
puts(indent,
" return setSelector(", methodName,
"(), selector)._(cdata)._();\n", "}");
}
} else if (params.length == 3) {
if (methodName.equals("a")) {
puts(0, "String selector, String href, String anchorText) {");
puts(indent,
" return setSelector(a(), selector)",
".$href(href)._(anchorText)._();\n", "}");
}
} else {
throwUnhandled(className, method);
}
}
static boolean needsEscaping(String eleName) {
return !eleName.equals("SCRIPT") && !eleName.equals("STYLE");
}
static void throwUnhandled(String className, Method method) {
throw new WebAppException("Unhandled " + className + "#" + method);
}
void echo(int indent, Object... args) {
String prev = null;
for (Object o : args) {
String s = String.valueOf(o);
if (!s.isEmpty() && !s.equals("\n") &&
(prev == null || prev.endsWith("\n"))) {
indent(indent);
}
prev = s;
out.print(s);
bytes += s.length();
}
}
void indent(int indent) {
for (int i = 0; i < indent; ++i) {
out.print(" ");
bytes += 2;
}
}
void puts(int indent, Object... args) {
echo(indent, args);
out.println();
++bytes;
}
boolean isElement(String s) {
return elementRegex.matcher(s).matches();
}
public static void main(String[] args) throws Exception {
CommandLine cmd = new GnuParser().parse(opts, args);
if (cmd.hasOption("help")) {
new HelpFormatter().printHelp("Usage: hbgen [OPTIONS]", opts);
return;
}
// defaults
Class<?> specClass = HamletSpec.class;
Class<?> implClass = HamletImpl.class;
String outputClass = "HamletTmp";
String outputPackage = implClass.getPackage().getName();
if (cmd.hasOption("spec-class")) {
specClass = Class.forName(cmd.getOptionValue("spec-class"));
}
if (cmd.hasOption("impl-class")) {
implClass = Class.forName(cmd.getOptionValue("impl-class"));
}
if (cmd.hasOption("output-class")) {
outputClass = cmd.getOptionValue("output-class");
}
if (cmd.hasOption("output-package")) {
outputPackage = cmd.getOptionValue("output-package");
}
new HamletGen().generate(specClass, implClass, outputClass, outputPackage);
}
}