/** * Copyright 2010 Bing Ran<bing_ran@hotmail.com> * * Licensed 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 cn.bran.japid.classmeta; import japa.parser.ast.body.Parameter; import java.lang.annotation.Annotation; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; //import java.util.TreeMap; import java.util.regex.Matcher; import java.util.regex.Pattern; import play.mvc.Http.Context; import cn.bran.japid.compiler.JapidAbstractCompiler; //import cn.bran.japid.compiler.Tag; import cn.bran.japid.compiler.Tag.TagDef; import cn.bran.japid.template.ActionRunner; import cn.bran.japid.template.JapidRenderer; import cn.bran.japid.template.JapidTemplateBaseStreaming; import cn.bran.japid.template.JapidTemplateBaseWithoutPlay; import cn.bran.japid.template.RenderResult; import cn.bran.japid.template.RenderResultPartial; import cn.bran.japid.util.DirUtil; import cn.bran.japid.util.JapidFlags; /** * lots of the code block generation is done here * * @author Bing Ran<bing_ran@hotmail.com> * */ public abstract class AbstractTemplateClassMetaData { /** * */ public static final String VERSION_HEADER = "//version: "; private static final String PUBLIC = "public "; private static final String COMMA = ";"; private static final String SPACE = " "; private static final String STATIC = "static"; private static final String IMPORT = "import"; private static Set<String> globalStaticImports = new HashSet<String>(); private Set<String> staticImports = new HashSet<String>(); private String originalTemplate; private static Pattern partialImport = Pattern.compile("import\\s+\\.(.+)"); // control if we use a streaming based API or StringBuilder based API public static boolean streaming = false; // if we need to track the time to render boolean stopWatch = false; // control whether to allow safe expression navigation public boolean suppressNull = false; public boolean useWithPlay = true; public String getOriginalTemplate() { return originalTemplate; } public void setOriginalTemplate(String originalTemplate) { this.originalTemplate = originalTemplate.replace('\\', '/'); } public StringBuilder sb = new StringBuilder(); protected static final String SEMI = COMMA; protected static final String TAB = "\t"; protected static final String RENDER_RESULT = RenderResult.class.getName(); protected static final String RENDER_RESULT_PARTIAL = RenderResultPartial.class.getName(); public static final String ACTION_RUNNERS = "actionRunners"; private static final String IMPORT_SPACE = IMPORT + SPACE; private static final String CONTENT_TYPE = "Content-Type"; /** * */ public static final String END_DO_LAYOUT = "endDoLayout"; /** * */ public static final String BEGIN_DO_LAYOUT = "beginDoLayout"; public String packageName; protected String className; public String getClassName() { return className; } public void setClassName(String className) { this.className = className; } // each line: byte[] _lineXXX=new byte[]{12, 23, 45}; List<String> statics = new ArrayList<String>(); // the source of the plain text line List<String> staticsSrc = new ArrayList<String>(); int staticCounter = 0; // List<String>importsLines = new ArrayList<String>(); /** * the main body part */ public String body; protected List<InnerClassMeta> innersforTagCalls = new ArrayList<InnerClassMeta>(); protected List<InnerClassMeta> innersInvokeCalls = new ArrayList<InnerClassMeta>(); protected boolean isAbstract; public static String curVersion; public AbstractTemplateClassMetaData() { super(); } protected void pln(Object... ss) { for (Object o : ss) { sb.append(o); } sb.append("\n"); } void p(String s) { sb.append(s); } public InnerClassMeta addCallTagBodyInnerClass(String className, int count, String callbackArgs, String body) { if (specialTags.contains(className)) return null; InnerClassMeta inner = new InnerClassMeta(className, count, callbackArgs, body); this.innersforTagCalls.add(inner); return inner; } public void removeLastCallTagBodyInnerClass() { this.innersforTagCalls.remove(this.innersforTagCalls.size() - 1); } private static Set<String> specialTags = new HashSet<String>(); static { specialTags.add("set"); specialTags.add("get"); specialTags.add("invoke"); specialTags.add("doBody"); specialTags.add("doLayout"); specialTags.add("extends"); } /** * */ public void printHeaders() { printVersion(); if (packageName != null) { pln("package " + packageName + SEMI); } pln("import java.util.*;"); pln("import java.io.*;"); // some nameing convention suport // cannot // pln("import japidviews._tags.*;"); // pln("import japidviews._layouts.*;"); if (streaming) pln(IMPORT_SPACE + cn.bran.japid.tags.streaming.Each.class.getName() + COMMA); else pln(IMPORT_SPACE + cn.bran.japid.tags.Each.class.getName() + COMMA); if (hasActionInvocation && useWithPlay) { pln(IMPORT_SPACE + ActionRunner.class.getName() + COMMA); } for (String l : imports) { l = l.trim(); if (!l.endsWith(COMMA)) l = l + COMMA; if (!l.startsWith(IMPORT)) l = IMPORT_SPACE + l; // extension: allow partial imports. e.g.: import .sub.* // which requires the current package name to prefix it. l = expandPartialImport(l); if (considerPlayDependency(l)) pln(l); } for (String l : globalStaticImports) { l = l.trim(); if (!l.startsWith(IMPORT)) l = IMPORT_SPACE + STATIC + SPACE + l; if (!l.endsWith(".*;")) { l += ".*;"; } l = expandPartialImport(l); if (considerPlayDependency(l)) pln(l); } for (String l : staticImports) { l = l.trim(); if (!l.startsWith(IMPORT)) l = IMPORT_SPACE + STATIC + SPACE + l; if (!l.endsWith(".*;")) { l += ".*;"; } l = expandPartialImport(l); if (considerPlayDependency(l)) pln(l); } // pln("import java.math.*;"); // pln("import static java.lang.Math.*;"); // // should decouple with JavaExtensions // pln("import static play.templates.JavaExtensions.*;"); for (String l : globalImports) { l = l.trim(); if (!l.endsWith(COMMA)) l = l + COMMA; if (!l.startsWith(IMPORT)) l = IMPORT_SPACE + l; l = expandPartialImport(l); if (considerPlayDependency(l)) pln(l); } pln("//"); pln("// NOTE: This file was generated from: " + originalTemplate); pln("// Change to this file will be lost next time the template file is compiled."); pln("//"); } /** * @author Bing Ran (bing.ran@gmail.com) */ private void printVersion() { String v = VERSION_HEADER + curVersion; // JapidFlags.debug("wrote version tag to Java derives: " + v ); pln (v); } private boolean considerPlayDependency(String l) { if (useWithPlay) return true; // filter out all Play related imports if (l.startsWith(IMPORT)) { l = l.substring(IMPORT.length()).trim(); } if (l.startsWith(STATIC)) { l = l.substring(STATIC.length()).trim(); } if (l.startsWith("play")) return false; if (l.startsWith("cn.bran.play")) return false; // if (l.startsWith("japidviews")) // return false; // if (l.startsWith("models")) // return false; // if (l.contains("JapidWebUtil")) // return false; // if (l.startsWith("controllers")) // return false; return true; } /** * @param l * a partially specified import line such as: import .tags.*; * @return */ public String expandPartialImport(String l) { Matcher matcher = partialImport.matcher(l); if (matcher.matches()) { l = "import " + packageName + "." + matcher.group(1); } return l; } protected void embedSourceTemplateName() { pln("\t" + "public static final String sourceTemplate = \"" + originalTemplate + "\";"); } // protected void embedContentType() { // String t = contentType == null ? "text/html" : contentType; // pln("\t" + "public static final String contentType = \"" + t + "\";"); // } /** * */ // protected void callTags() { // // inners // for (InnerClassMeta inner : this.innersforTagCalls) { // // create a reusable instance _tagName_indexand a instance // // initializer // String tagClassName = inner.tagName; // String field = "private " + tagClassName + " _" + // inner.getInnerClassName() + inner.counter + " = new " + tagClassName + // "(getOut());"; // pln("\t" + field); // // if (inner.renderBody != null) { // // body class // pln(inner.toString()); // } // } // } // /** // * @deprecated declare it as fields instead // */ // protected void setupTagObjectsAsVariables() { // boolean hasTags = this.innersforTagCalls.size() > 0; // if (hasTags) // pln("\n// -- set up the tag objects"); // for (InnerClassMeta inner : this.innersforTagCalls) { // // create a reusable instance _tagName_indexand a instance // // initializer // String tagClassName = inner.tagName; // String var = "_" + inner.getVarRoot() + inner.counter; // String decl = "final " + tagClassName + " " + var + " = new " + tagClassName + "(getOut());"; // pln(decl); // if (useWithPlay) { // String addRunner = var + ".setActionRunners(getActionRunners());"; // pln(addRunner); // } // pln(); // } // if (hasTags) // pln("// -- end of the tag objects\n"); // } // can be used to create local variables too! /** * commented out * * // don't declare in the front. always declare where it is used for safety // see JapidAbstractCompiler#regularTagInvoke */ protected void printAnnotations() { for (Class<? extends Annotation> anno : typeAnnotations) { pln("@" + anno.getName()); } } /** * add import lines to the to be generated imports lines, import and the * ending ; are optional * * @param imp */ public static void addImportLineGlobal(String imp) { imp = imp.trim(); if (imp.startsWith(IMPORT)) { imp = imp.substring(IMPORT.length()).trim(); } globalImports.add(imp); } protected void buildStatics() { for (int i = 0; i < statics.size(); i++) { if (streaming) pln("static private final byte[] static_" + i + " = getBytes(" + statics.get(i) + ");"); else pln("static private final String static_" + i + " = " + statics.get(i) + COMMA); } } protected void addConstructors() { if (!streaming) { // for StringBuilder data collection, create a default constructor pln(TAB + PUBLIC + className + "() {"); pln(TAB + "super((StringBuilder)null);"); if (useWithPlay) pln(TAB + "initHeaders();"); pln(TAB + "}"); } if (streaming) pln(TAB + PUBLIC + className + "(OutputStream out) {"); else pln(TAB + PUBLIC + className + "(StringBuilder out) {"); pln(TAB + TAB + "super(out);"); if (useWithPlay) pln(TAB + TAB + "initHeaders();"); pln(TAB + "}"); pln(TAB + PUBLIC + className + "(" + JapidTemplateBaseWithoutPlay.class.getName() + " caller) {\n" + " super(caller);\n" + " }\n" + ""); } /** * */ private void classDeclare() { if (superClass == null) { if (useWithPlay) { // superClass = JapidTemplateBase.class.getName(); superClass = "cn.bran.play.JapidTemplateBase"; if (streaming) superClass = JapidTemplateBaseStreaming.class.getName(); } else { superClass = JapidTemplateBaseWithoutPlay.class.getName(); } } String abs = isAbstract ? "abstract " : ""; pln("public " + abs + "class " + className + " extends " + superClass); } /** * set the generated class to be abstract * * @param isAbstract */ public void setAbstract(boolean isAbstract) { this.isAbstract = isAbstract; } public String superClass; public static void addImportStatic(Class<?> class1) { String className = class1.getName(); globalStaticImports.add(className); } /** * this is for globally adding static imports, usually by tools. * * @param imp */ public static void addImportStaticGlobal(String imp) { if (imp.startsWith(IMPORT)) imp = imp.substring(IMPORT.length()).trim(); if (imp.startsWith(STATIC)) imp = imp.substring(IMPORT.length()).trim(); globalStaticImports.add(imp); } public void addImport(Class<?> class1) { String className = class1.getName(); addImportLine(className); } private static Set<String> globalImports = new HashSet<String>(); private Set<String> imports = new HashSet<String>(); private String contentType = ""; /** * * @param text * something like \"hello\" * @param src * @return */ public String addStaticText(String text, String src) { if (text != null && !text.isEmpty()) { if (trimStaticContent) { if (text.trim().length() == 0) { return null; } } this.statics.add(text); this.staticsSrc.add(src); return "static_" + (statics.size() - 1); } else return null; } /** * add class level annotation * * @param anno */ public static void addAnnotation(Class<? extends Annotation> anno) { typeAnnotations.add(anno); } static Set<Class<? extends Annotation>> typeAnnotations = new HashSet<Class<? extends Annotation>>(); public void setContentType(String contentType) { if (contentType != null) { this.headers.put(CONTENT_TYPE, contentType); this.contentType = contentType; } } // String contentType; private boolean trimStaticContent = false; protected boolean hasActionInvocation; private Map<String, String> headers = new HashMap<String, String>(); private List<TagDef> defTags = new ArrayList<TagDef>(); public String renderArgs; // to support extends layout (arg1, arg2) public String superClassRenderArgs = ""; protected int argsLineNum; private Boolean traceFile = null; public void turnOnStopwatch() { this.stopWatch = true; } /** * suppress all NPE in expression ${} and display empty string */ public void suppressNull() { this.suppressNull = true; } public void addStaticImports(String im) { staticImports.add(im); } public void addImportLine(String line) { this.imports.add(line); } /** * ignore static content that contains whitespace chars only, including * space, tab, \n etc. */ public void trimStaticContent() { this.trimStaticContent = true; } public boolean getTrimStaticContent() { return this.trimStaticContent; } public void setHasActionInvocation() { this.hasActionInvocation = true; } public void setHeader(String name, String value) { this.headers.put(name, value); } public void printInitializer() { // now we use the headers var the template base, for slightly // performance penalty // pln(" private static final Map<String, String> headers = new HashMap<String, String>();"); if (useWithPlay && headers.size() > 0) { // pln(" static {"); pln("\t private void initHeaders() {"); for (String k : headers.keySet()) { String v = headers.get(k); pln("\t\tputHeader(\"" + k + "\", \"" + v + "\");"); } pln("\t\tsetContentType(\"" + contentType + "\");"); pln("\t}"); } pln("\t{"); if (traceFile != null) if (traceFile) pln("\t\tsetTraceFile(true);"); else pln("\t\tsetTraceFile(false);"); pln("\t}"); } public void addDefTag(TagDef tag) { this.defTags.add(tag); } protected void processDefTags() { for (TagDef tag : this.defTags) { String meth = tag.args.trim(); if (meth.endsWith(")")) { pln("public String " + meth + " {"); } else { pln("public String " + meth + "() {"); } pln("StringBuilder sb = new StringBuilder();"); pln("StringBuilder ori = getOut();"); pln("this.setOut(sb);"); if (useWithPlay) pln("TreeMap<Integer, cn.bran.japid.template.ActionRunner> parentActionRunners = actionRunners;\n" + "actionRunners = new TreeMap<Integer, cn.bran.japid.template.ActionRunner>();" ); pln(tag.getBodyText()); pln("this.setOut(ori);"); if (useWithPlay) pln("if (actionRunners.size() > 0) {\n" + " StringBuilder _sb2 = new StringBuilder();\n" + " int segStart = 0;\n" + " for (Map.Entry<Integer, cn.bran.japid.template.ActionRunner> _arEntry : actionRunners.entrySet()) {\n" + " int pos = _arEntry.getKey();\n" + " _sb2.append(sb.substring(segStart, pos));\n" + " segStart = pos;\n" + " cn.bran.japid.template.ActionRunner _a_ = _arEntry.getValue();\n" + " _sb2.append(_a_.run().getContent().toString());\n" + " }\n" + " _sb2.append(sb.substring(segStart));\n" + " actionRunners = parentActionRunners;\n" + " return _sb2.toString();\n" + "} else {\n" + " actionRunners = parentActionRunners;\n" + " return sb.toString();\n" + "}"); else pln(" return sb.toString();" ); pln("}"); } } // /** // * @param t // */ // protected void declareTagInstance(Tag t) { // String tagClassName = t.tagName; // String var = t.getTagVarName(); // // // if (tagClassName.equals("this")) { // tagClassName = this.getClassName(); // } // // String decl = "final " + tagClassName + " " + var + " = new " + tagClassName + "(getOut());"; // pln(decl); // // // commented out. now runners are set just before use; //// if (useWithPlay && !tagClassName.equals("Each")) { //// String addRunner = "{ " + var + ".setActionRunners(getActionRunners()); }"; //// pln(addRunner); //// } // pln(); // } /** * added field declarations such as request, response, errors * Some of the implicit objects are defined in the JapidPlayAdapter * Note: this basically removes the possibility to reuse the same instance of renderer to render multiple requests. */ protected void addImplicitFields() { if (useWithPlay) { pln("\n// - add implicit fields with Play\n" + "boolean hasHttpContext = play.mvc.Http.Context.current.get() != null ? true : false;\n"); pln(" final Request request = hasHttpContext? Implicit.request() : null;\n" + " final Response response = hasHttpContext ? Implicit.response() : null;\n" + " final Session session = hasHttpContext ? Implicit.session() : null;\n" + " final Flash flash = hasHttpContext ? Implicit.flash() : null;\n" + // " final Lang lang = hasHttpContext ? Implicit.lang() : null;\n" + // a performance hit. replace by a lang() call " final play.Play _play = new play.Play(); \n" + ""); pln("// - end of implicit fields with Play \n\n"); } } /** * remove the plain text line between two consecutive script line, if the * plain text is made of space chars only . * * return true if that's the case */ public String removeLastSingleEmptyLine() { int last = staticsSrc.size() - 1; String s = this.staticsSrc.get(last); char[] charArray = s.toCharArray(); for (char c : charArray) { if (!Character.isWhitespace(c)) return null; } if (s.contains("\n")) { if (s.indexOf('\n') == s.lastIndexOf('\n')) { // it contains only one newline this.statics.remove(last); this.staticsSrc.remove(last); return s; } else return null; } else { this.statics.remove(last); this.staticsSrc.remove(last); return s; } } /** * output the java code * * @return */ public String generateCode() { printHeaders(); printAnnotations(); classDeclare(); pln("{"); embedSourceTemplateName(); printInitializer(); // buildStatics(); if (useWithPlay) { addImplicitFields(); } // now tags are local variables in dolayout for better safety // setupTagObjectsAsFields(); addConstructors(); renderMethod(); layoutMethod(); getterSetter(); childLayout(); processDefTags(); p("}"); return sb.toString(); } abstract void renderMethod(); abstract void layoutMethod(); abstract void getterSetter(); abstract void childLayout(); @Override public String toString() { return generateCode(); } /** * @param p */ protected void addField(Parameter p) { // no need String defaultVal = "=" /*+ JavaSyntaxTool.getDefault(p)*/; pln(TAB + "private " + p.getType() + " " + p.getId() + (defaultVal.equals("=") ? "":defaultVal) + "; " +getLineMarker()); } protected String getLineMarker() { // return JapidAbstractCompiler.makeLineMarker(argsLineNum); return DirUtil.LINE_MARKER + argsLineNum + DirUtil.OF + originalTemplate; } public static void clearImports() { globalImports.clear(); globalStaticImports.clear(); } public void setArgsLineNum(int startLine) { this.argsLineNum = startLine; } /** * @author Bing Ran (bing.ran@hotmail.com) */ public void turnOnTraceFile() { this.traceFile = true; } /** * @author Bing Ran (bing.ran@hotmail.com) */ public void turnOffTraceFile() { this.traceFile = false; } /** * @author Bing Ran (bing.ran@gmail.com) * @param templateClassMetaData */ public void merge(AbstractTemplateClassMetaData a) { this.imports.addAll(a.imports); this.innersforTagCalls.addAll(a.innersforTagCalls); this.innersInvokeCalls.addAll(a.innersInvokeCalls); } protected void restOfBody() { pln(TAB + TAB + BEGIN_DO_LAYOUT + "(sourceTemplate);"); pln(body); pln(TAB + TAB + END_DO_LAYOUT + "(sourceTemplate);"); pln("\t}"); } }