/** * 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.compiler; import japa.parser.ast.body.Parameter; import java.util.List; import java.util.Stack; import java.util.regex.Matcher; import java.util.regex.Pattern; import cn.bran.japid.classmeta.AbstractTemplateClassMetaData; import cn.bran.japid.classmeta.InnerClassMeta; import cn.bran.japid.classmeta.MimeTypeEnum; import cn.bran.japid.compiler.JapidParser.Token; import cn.bran.japid.compiler.Tag.TagDef; import cn.bran.japid.compiler.Tag.TagIf; import cn.bran.japid.compiler.Tag.TagInTag; import cn.bran.japid.compiler.Tag.TagSet; import cn.bran.japid.tags.Each; import cn.bran.japid.template.ActionRunner; import cn.bran.japid.template.JapidTemplate; import cn.bran.japid.template.RenderResult; import cn.bran.japid.util.DirUtil; import cn.bran.japid.util.WebUtils; /** * based on the original code from the Play! Framework * * the parent class for all three type compilers: regular template compiler, the * LayoutCompiler and the TagCompiler. * * @author original Play! authors * @author Bing Ran<bing_ran@hotmail.com> * */ public abstract class JapidAbstractCompiler { /** * */ private static final String INCLUDE = "include"; public static final String DO_LAYOUT = "doLayout"; private static final String ELVIS = "?:"; // pattern: } else if xxx { static final String ELSE_IF_PATTERN_STRING = "\\s*\\}\\s*else\\s*if\\s+([^\\(].*)\\s*"; static final Pattern ELSE_IF_PATTERN = Pattern.compile(ELSE_IF_PATTERN_STRING); // static final String OPEN_ELSE_IF_PATTERN_STRING = "\\s*else\\s*if\\s+([^\\(].*)\\s*"; // relax the excluding of the () static final String OPEN_ELSE_IF_PATTERN_STRING = "\\s*else\\s*if\\s+(.*)\\s*"; static final Pattern OPEN_ELSE_IF_PATTERN = Pattern.compile(OPEN_ELSE_IF_PATTERN_STRING); static final String OPEN_ELSE_STRING = "\\s*else\\s*"; static final String SPACE_OR_NONE = "\\s*"; static final String SPACE_AT_LEAST_ONE = "\\s+"; // please trim the string before applying the pattern // the idea is to substitute the open for with "each" tag static final String OPEN_FOR_PATTERN_STRING = "for\\s+([^\\(].+)\\s*:\\s*(.+[^\\{])"; static final Pattern OPEN_FOR_PATTERN = Pattern.compile(OPEN_FOR_PATTERN_STRING); // pattern: if xxx { // should really use java parser static final String OPEN_IF_PATTERN1 = "if\\s+[^\\(].*"; static final String IF_PATTERN_STRING = "if\\s*\\((.*)\\).*"; static final Pattern IF_PATTERN = Pattern.compile(IF_PATTERN_STRING); private static final String JAPID_RESULT = "cn.bran.play.JapidResult"; private static final String ARGS = "args"; protected static final String HTML = ".html"; // private static final String DO_BODY = "doBody"; protected static final String SPACE = " "; protected static final String NEW_LINE = "\n"; protected JapidTemplate template; protected JapidParser parser; protected boolean doNextScan = true; // the tagsStack only tracks the regular tags, particularly not including // the open if private Stack<Tag> tagsStack = new Stack<Tag>(); // the shadow is not used to keep the stacking of open if and regular tag private Stack<Tag> tagsStackShadow = new Stack<Tag>(); protected int tagIndex; protected boolean skipLineBreak; protected boolean useWithPlay = true; private String templateShortName; public void compile(JapidTemplate t) { template = t; String tname = t.name; int lastSlash = tname.lastIndexOf("/"); if (lastSlash >= 0) { tname = tname.substring(lastSlash + 1); } this.templateShortName = tname; getTemplateClassMetaData().setOriginalTemplate(t.name); getTemplateClassMetaData().useWithPlay = this.useWithPlay; hop(); } /** * */ protected void parse() { // Parse loop: for (;;) { if (doNextScan) { stateBeforePreviousState = previousState; previousState = state; state = parser.nextToken(); } else { doNextScan = true; } String token = parser.getToken(); switch (state) { case EOF: break loop; case PLAIN: plain(token); break; case VERBATIM: plain(token); break; case SCRIPT: script(token); break; case CLOSING_BRACE: closingBrace(token); break; case SCRIPT_LINE: if (previousState == Token.PLAIN && stateBeforePreviousState == Token.SCRIPT_LINE) { String spacer = this.getTemplateClassMetaData().removeLastSingleEmptyLine(); if (spacer != null) { Tag currentScope = this.tagsStack.peek(); // currentScope.bodyBuffer.append(text); int lastIndex = currentScope.bodyTextList.size() - 1; currentScope.bodyTextList.remove(lastIndex); currentScope.bodyTextList.set(lastIndex - 1, spacer); } } scriptline(token); break; case EXPR: expr(token, false); break; case EXPR_NATURAL_ESCAPED: expr(token, true); break; case EXPR_ESCAPED: expr(token, true); break; case MESSAGE: message(token); break; case ACTION: action(token, false); break; case ABS_ACTION: action(token, true); break; case COMMENT: skipLineBreak = true; break; case START_TAG: startTag(buildTag(token)); break; case END_TAG: String tagName = token.trim(); if (tagsStack.isEmpty()) { throw new JapidCompilationException(template, parser.getLineNumber(), "#{/" + tagName + "} is not opened."); } Tag tag = popStack(); endTag(tag); break; case TEMPLATE_ARGS: templateArgs(token); break; } } } /** * @return */ private Tag popStack() { // tagsStackShadow.pop(); return tagsStack.pop(); } protected void closingBrace(String token) { // ok a } after some space in a line // treat it as MARKER } print("}"); } protected void plain(String token) { String text = token.replace("\\", "\\\\").replaceAll("\"", "\\\\\""); if (skipLineBreak && text.startsWith(NEW_LINE)) { text = text.substring(1); } // add static content to classmetadata and print the ref to the // generated source code if (this.getTemplateClassMetaData().getTrimStaticContent()) { String r = text.trim(); if (r.length() == 0) return; else { text = text.trim(); } } String lines = composeValidMultiLines(text); String ref = this.getTemplateClassMetaData().addStaticText(lines, text); if (ref != null) { // print the static content via the variable // print("p(" + ref + ");"); // print the static content directly print("p(" + lines + ");"); markLine(); println(); } } /** * break a line with newlines to multiple lines valid in java source code * * <pre> * "line1\n" + * "line2\n" + * "line3"; * * </pre> * * @param src * @return */ public static String composeValidMultiLines(String text) { // multi-line String[] lines = text.split(NEW_LINE, 10000); String result = ""; for (int i = 0; i < lines.length; i++) { String line = lines[i]; if (line.length() > 0 && (int) line.charAt(line.length() - 1) == 13) { // remove the last newline line = line.substring(0, line.length() - 1); } result += "\"" + line; if (i == lines.length - 1 && !text.endsWith(NEW_LINE)) { // last line result += "\""; } else if (i == lines.length - 1 && line.equals("")) { result += "\""; } else { // regular line result += "\\n\" + \n"; } // markLine(parser.getLine() + i); } String emptySuffix = " + \n\"\""; if (result.endsWith(emptySuffix)) { result = result.substring(0, result.length() - emptySuffix.length()); } return result; } protected abstract void startTag(Tag tag); protected void println() { // print(NEW_LINE); Tag currentScope = this.tagsStack.peek(); currentScope.bodyTextList.add("\t\t"); currentLine++; } /** * always append to the last line * * @param text */ protected void print(String text) { Tag currentScope = this.tagsStack.peek(); // currentScope.bodyBuffer.append(text); int lastIndex = currentScope.bodyTextList.size() - 1; String lastLine = currentScope.bodyTextList.get(lastIndex); lastLine += text; currentScope.bodyTextList.set(lastIndex, lastLine); // else if (this.currentInnerClassName != null) // this.currentInnerClassRenderBody.append(text); // else // mainRenderBodySource.append(text); } protected void println(String text) { print(text); println(); int i = 0; while (i++ < indentLevel) { print("\t"); } } protected void markLine(int line) { if (!this.getTemplateClassMetaData().getTrimStaticContent()) { print(makeLineMarker(line)); } template.linesMatrix.put(currentLine, line); } public String makeLineMarker(int line) { if (line <= 0) return ""; return DirUtil.LINE_MARKER + line + DirUtil.OF + templateShortName; } protected void scriptline(String token) { // String line = token.trim(); // if () script(token); } protected void script(String token) { String[] lines = new String[] { token }; if (token.indexOf(NEW_LINE) > -1) { lines = parser.getToken().split(NEW_LINE); } for (int i = 0; i < lines.length; i++) { String line = lines[i];// .trim(); if (lines.length > 1 && line.trim().length() == 0) line = "//japid compiler: artificial line to avoid being treated as a terminating line"; if (startsWithIgnoreSpace(line, "import")) { getTemplateClassMetaData().addImportLine(line); } else if (startsWithIgnoreSpace(line, "//")) { // ignore } else if (startsWithIgnoreSpace(line, "extends")) { String layout = line.trim().substring("extends".length()).trim(); // remove quotes if they present boolean hasParam = false; int p = 0; for (; p < layout.length(); p++) { char c = layout.charAt(p); if (c == ' ' || c == '\t' || c == '(') { hasParam = true; break; } } if (!hasParam) { layout = layout.replace("'", ""); layout = layout.replace("\"", ""); layout = removeEndingString(layout, ";"); layout = removeEndingString(layout, HTML); layout = removeEndingString(layout, "/"); if (layout.startsWith(".")) { // new feature allow extends .sub.layout.html if (layout.startsWith("./")) { layout = getTemplateClassMetaData().packageName + layout.substring(1); } else { layout = getTemplateClassMetaData().packageName + layout; } } getTemplateClassMetaData().superClass = layout.replace('/', '.'); } else { String layoutName = layout.substring(0, p); layoutName = layoutName.replace("'", ""); layoutName = layoutName.replace("\"", ""); layoutName = layoutName.replace('/', '.'); layoutName = removeEndingString(layoutName, HTML); try { // due to similarity, let's borrow a tag parsing Tag tag = new TagInvocationLineParser().parse(layoutName + layout.substring(p)); if (tag.tagName.startsWith(".")) { // partial path, use current package as the root and // append the path to it tag.tagName = getTemplateClassMetaData().packageName + tag.tagName; } getTemplateClassMetaData().superClass = tag.tagName; getTemplateClassMetaData().superClassRenderArgs = tag.args; } catch (RuntimeException e) { throw new JapidCompilationException(template, parser.getLineNumber(), e.getMessage()); } } } else if (startsWithIgnoreSpace(line, "contentType")) { // TODO: should also take standard tag name: Content-Type String contentType = line.trim().substring("contentType".length()).trim().replace("'", "").replace("\"", ""); if (contentType.endsWith(";")) contentType = contentType.substring(0, contentType.length()); getTemplateClassMetaData().setContentType(contentType); } else if (startsWithIgnoreSpace(line, "setHeader")) { String headerkv = line.trim().substring("setHeader".length()).trim(); String[] split = headerkv.split("[ |\t]"); if (split.length < 2) { throw new JapidCompilationException(template, parser.getLineNumber(), "setHeaader must take a key and a value string"); } String name = split[0]; String value = headerkv.substring(name.length()).trim(); getTemplateClassMetaData().setHeader(name, value); } else if (startsWithIgnoreSpace(line, ARGS)) { String args = line.trim().substring(ARGS.length()).trim(); templateArgs(args); } else if (startsWithIgnoreSpace(line, "trim")) { String sw = line.trim().substring("trim".length()).trim().replace(";", "").replace("'", "").replace("\"", ""); if ("on".equals(sw) || "true".equals(sw)) { getTemplateClassMetaData().trimStaticContent(); } } else if (startsWithIgnoreSpace(line, "stopwatch")) { String sw = line.trim().substring("stopwatch".length()).trim().replace(";", "").replace("'", "").replace("\"", ""); if ("on".equals(sw) || "yes".equals(sw) || "true".equals(sw)) getTemplateClassMetaData().turnOnStopwatch(); } else if (startsWithIgnoreSpace(line, "tracefile")) { String sw = line.trim().substring("tracefile".length()).trim().replace(";", "").replace("'", "").replace("\"", ""); if ("on".equals(sw) || "yes".equals(sw) || "true".equals(sw)) getTemplateClassMetaData().turnOnTraceFile(); else getTemplateClassMetaData().turnOffTraceFile(); } else if (startsWithIgnoreSpace(line, "log") || line.trim().equals("log")) { String args = line.trim().substring("log".length()).trim().replace(";", ""); if (args.trim().length() == 0) args = "\"\""; String logLine = "System.out.println(\"" + this.template.name.replace('\\', '/') + "(line " + (parser.getLineNumber() + i) + "): \" + " + args + ");"; println(logLine); } else if (startsWithIgnoreSpace(line, "invoke")) { String args = line.trim().substring("invoke".length()).trim().replace(";", ""); doActionInvokeDirective(args); markLine(parser.getLineNumber() + i); println(); } else if (startsWith(line, "a")) { // `a == `invoke, the a must be // the first char to avoid // collision String args = line.substring(2).trim().replace(";", ""); doActionInvokeDirective(args); markLine(parser.getLineNumber() + i); println(); } else if (startsWithIgnoreSpace(line, "suppressNull") || line.trim().equals("suppressNull")) { String npe = line.trim().substring("suppressNull".length()).trim().replace(";", "").replace("'", "").replace("\"", ""); if ("on".equals(npe) || "yes".equals(npe) || "".equals(npe)) getTemplateClassMetaData().suppressNull(); } else if (line.trim().equals("abstract")) { getTemplateClassMetaData().setAbstract(true); } else if (startsWithIgnoreSpace(line, "tag")) { String tagline = line.trim().substring(4); doTagDirective(tagline); } else if (startsWith(line, "t")) { // `t == `tag, the t must be the // first char to avoid collision String tagline = line.substring(2); doTagDirective(tagline); } else if (startsWithIgnoreSpace(line, "each") || startsWithIgnoreSpace(line, "Each")) { // support one line type of tag invocation. Tag tag = buildTagDirective(line); tag.tagName = "Each"; tag.hasBody = true; startTag(tag); } else if (startsWithIgnoreSpace(line, "set")) { Tag set = buildTagDirective(line); if (line.contains(":") || line.contains("=")) set.hasBody = false; else set.hasBody = true; startTag(set); if (!set.hasBody) { // one liner. set = popStack(); } } else if (startsWithIgnoreSpace(line, "get")) { Tag get = buildTagDirective(line); get.hasBody = false; startTag(get); get = popStack(); } else if (startsWithIgnoreSpace(line, "def")) { // a function definition block Tag get = buildTagDirective(line); get.hasBody = true; startTag(get); } else if (startsWithIgnoreSpace(line, INCLUDE)) { // the include directive // compile the target and include the layout part in the current output flow String target = parseInclude(line); String src; try { src = DirUtil.readFileAsString(target); JapidTemplate template = new JapidTemplate(target, src); JapidAbstractCompiler c = JapidTemplateTransformer.selectCompiler(src); c.setUseWithPlay(getTemplateClassMetaData().useWithPlay); c.compile(template); String jsrc = template.javaSource; getTemplateClassMetaData().merge(c.getTemplateClassMetaData()); println("/* include %s */", target); String code = extractRenderingCode(jsrc); println(code); println("/* end of %s */", target); } catch (Exception e) { throw new JapidCompilationException(template, parser.getLineNumber() + i, "The target of include does not exist: " + target + ". " + e); } } else if (line.trim().startsWith("noplay")) { // template is play independent getTemplateClassMetaData().useWithPlay = this.useWithPlay = false; } else if (line.trim().equalsIgnoreCase("xml")) { // template is play independent getTemplateClassMetaData().setContentType(MimeTypeEnum.xml.header); } else if (line.trim().equalsIgnoreCase("json")) { // template is play independent getTemplateClassMetaData().setContentType(MimeTypeEnum.json.header); } else if (line.trim().equalsIgnoreCase("css")) { // template is play independent getTemplateClassMetaData().setContentType(MimeTypeEnum.css.header); } else if (line.trim().equalsIgnoreCase("txt") || line.trim().equalsIgnoreCase("text")) { // template is play independent getTemplateClassMetaData().setContentType(MimeTypeEnum.txt.header); } else if (line.trim().equalsIgnoreCase("js") || line.trim().equalsIgnoreCase("javascript")) { // template is play independent getTemplateClassMetaData().setContentType(MimeTypeEnum.js.header); } else if (line.trim().startsWith("verbatim")) { parser.verbatim = true; Tag get = buildTagDirective(line); get.hasBody = true; startTag(get); } else if (startsWithIgnoreSpace(line, "if")) { // `if expr {, the last { is optional String expr = line.trim(); String clause = expr.substring(2); if (JavaSyntaxTool.isIf(clause)) { print(expr); markLine(parser.getLineNumber() + i); println(); } else if (JavaSyntaxTool.isOpenIf(clause)) { handleOpenIf(i, expr); } // String expr = line.trim(); // if (expr.matches(OPEN_IF_PATTERN1)) { // handleOpenIf(i, expr); // } else { // // plain Java if // // wait! but may be open due to regex limitation. // Matcher m = IF_PATTERN.matcher(expr); // if (m.matches()){ // String condition = m.group(1); // if (JavaSyntaxTool.isValidExpr(condition)){ // // true classic if // print(expr); // markLine(parser.getLineNumber() + i); // println(); // } // else { // // is actually open if // handleOpenIf(i, expr); // } // } // } } else if (line.matches(ELSE_IF_PATTERN_STRING)) { // semi open String expr = line.trim(); Matcher matcher = ELSE_IF_PATTERN.matcher(line); if (matcher.matches()) { expr = matcher.group(1).trim(); boolean negative = expr.startsWith("!"); if (negative) expr = expr.substring(1).trim(); String cleanExpr = removeEndingString(expr, "{"); verifyExpr(cleanExpr); expr = "} else if(" + (negative ? "!" : "") + "asBoolean(" + cleanExpr + ")) {"; } print(expr); markLine(parser.getLineNumber() + i); println(); } else if (line.matches(OPEN_ELSE_IF_PATTERN_STRING)) { // open String expr = line.trim(); Matcher matcher = OPEN_ELSE_IF_PATTERN.matcher(line); if (matcher.matches()) { expr = matcher.group(1).trim(); boolean negative = expr.startsWith("!"); if (negative) { handleOpenElseIf(i, expr.substring(1), negative); } else { if (expr.startsWith("(") && expr.endsWith(")")){ // test if the part is classic if String ex = expr.substring(1, expr.length() - 1); if (JavaSyntaxTool.isValidExpr(ex)){ //OK, the insider is a valid expression (better be boolean!) // end previous if shadow and star a new one Tag tagShadow = tagsStackShadow.peek(); if (tagShadow instanceof TagIf) { tagsStackShadow.pop(); // to close an open if // start a new if Tag.TagIf iftag = new Tag.TagIf(expr, parser.getLineNumber()); pushToStack(iftag); expr = "} else if(" + ex + ")) {"; print(expr); markLine(parser.getLineNumber() + i); println(); } else { throw new JapidCompilationException(template, parser.getLineNumber() + i, "the open \"else if\" statement is not properly matched to a previous if"); } } else { handleOpenElseIf(i, expr, negative); } } else { handleOpenElseIf(i, expr, negative); } } } else { throw new RuntimeException("JapidAbstractCompiler bug: Should never be here!"); } } else if (line.matches(OPEN_ELSE_STRING)) { Tag tagShadow = tagsStackShadow.peek(); if (tagShadow instanceof TagIf) { tagsStackShadow.pop(); // to close an open if // start a new if print("} else {"); markLine(parser.getLineNumber() + i); println(); Tag.TagIf iftag = new Tag.TagIf("", parser.getLineNumber()); pushToStack(iftag); } else { throw new JapidCompilationException(template, parser.getLineNumber() + i, "the open \"else\" statement is not properly matched to a previous if"); } } else if (line.trim().matches(OPEN_FOR_PATTERN_STRING)) { // simply replace it with a "each" tag call String expr = line.trim(); Matcher matcher = OPEN_FOR_PATTERN.matcher(expr); if (matcher.matches()) { String instanceDecl = matcher.group(1); if (!JavaSyntaxTool.isValidSingleVarDecl(instanceDecl)) throw new JapidCompilationException(template, parser.getLineNumber() + i, "loop variable declaration error: " + instanceDecl); instanceDecl = JavaSyntaxTool.cleanDeclPrimitive(instanceDecl); String collection = matcher.group(2); if (!JavaSyntaxTool.isValidExpr(collection)) throw new JapidCompilationException(template, parser.getLineNumber() + i, "syntax error: " + collection); expr = "each " + collection + " | " + instanceDecl; Tag tag = buildTagDirective(expr); tag.tagName = "Each"; tag.hasBody = true; startTag(tag); } else { // should not happen print(expr); markLine(parser.getLineNumber() + i); println(); } } else if (line.trim().length() == 0) { // a single ` empty line, treated as the closing for `tag try { Tag tagShadow = tagsStackShadow.peek(); if (tagShadow.isRoot()) { // System.out.println(""); } else { tagShadow = tagsStackShadow.pop(); if (tagShadow instanceof TagIf) { // to close an open if print("}"); markLine(parser.getLineNumber() + i); println(); } else { if (!tagsStack.empty()) { Tag tag = tagsStack.peek(); if (!tag.isRoot()) { tag = popStack(); endTag(tag); } } } } } catch (Exception e) { // should throw it out? e.printStackTrace(); } } else { // OK plain Java code if (line.length() > 0) { print(line); markLine(parser.getLineNumber() + i); println(); } } } skipLineBreak = true; } /** * @author Bing Ran (bing.ran@gmail.com) * @param jsrc * @return */ private String extractRenderingCode(String jsrc) { String ret = ""; String[] lines = jsrc.split("\\n"); boolean shouldUse = false; for (String l : lines) { String line = l.trim(); if (line.startsWith(AbstractTemplateClassMetaData.BEGIN_DO_LAYOUT)) { shouldUse = true; continue; } if (line.startsWith(AbstractTemplateClassMetaData.END_DO_LAYOUT)) { break; } if (shouldUse) { ret += l + "\n"; } } return ret; } /** * @author Bing Ran (bing.ran@gmail.com) * @param line * @return */ private String parseInclude(String line) { line = line.trim(); if (line.startsWith(INCLUDE + " ") || line.startsWith(INCLUDE + "\t")) { line = line.substring(INCLUDE.length()).trim(); } if (line.endsWith("." + INCLUDE)) { return line; } else { throw new JapidCompilationException(template, parser.getLineNumber(), "the include target must end with .include"); } } /** * @author Bing Ran (bing.ran@gmail.com) * @param string * @param target */ private void println(String format, String target) { println(String.format(format, target)); } private void handleOpenElseIf(int i, String expr, boolean negative) { // expr = expr.substring(1).trim(); // end previous if shadow and star a new one verifyExpr(expr); Tag tagShadow = tagsStackShadow.peek(); if (tagShadow instanceof TagIf) { tagsStackShadow.pop(); // to close an open if // start a new if Tag.TagIf iftag = new Tag.TagIf(expr, parser.getLineNumber()); pushToStack(iftag); expr = "} else if(" + (negative ? "!" : "") + "asBoolean(" + expr + ")) {"; print(expr); markLine(parser.getLineNumber() + i); println(); } else { throw new JapidCompilationException(template, parser.getLineNumber(), "the open \"else if\" statement is not properly matched to a previous if"); } } /** * * @param i * @param expr the open if statement: if my_condition... */ private void handleOpenIf(int i, String expr) { // get the expression String ex = expr.substring(2).trim(); boolean negative = ex.startsWith("!"); if (negative) ex = ex.substring(1).trim(); if (!ex.endsWith("{")) { // true open. out a shadow tag, since we want reuse the // ` as the end delimiter Tag.TagIf iftag = new Tag.TagIf(ex, parser.getLineNumber()); pushToStack(iftag); } ex = removeEndingString(ex, "{").trim(); verifyExpr(ex); ex = "if(" + (negative ? "!" : "") + "asBoolean(" + ex + ")) {"; print(ex); markLine(parser.getLineNumber() + i); println(); } /** * @param string * @return */ private String removeEndingString(String string, String ending) { if (string.endsWith(ending)) return string.substring(0, string.lastIndexOf(ending)); else return string; } /** * @param line * @param string * @return */ protected static boolean startsWithIgnoreSpace(String line, String string) { line = line.trim(); return line.startsWith(string + " ") || line.startsWith(string + "\t"); } /** * @param line * @param string * @return */ private boolean startsWith(String line, String string) { return line.startsWith(string + " ") || line.startsWith(string + "\t"); } /** * @param args */ private void doActionInvokeDirective(String args) { if (!getTemplateClassMetaData().useWithPlay) { throw new JapidCompilationException(template, parser.getLineNumber(), "action invocation is only supported in Play environment. "); } else { this.getTemplateClassMetaData().setHasActionInvocation(); if (args.trim().length() == 0) args = "whatyouwantoinvoke()"; printActionInvocation(args); } } /** * @param tagline */ private void doTagDirective(String tagline) { Tag tag = buildTagDirective(tagline); startTag(tag); if (!tag.hasBody) { // one liner. tag = tagsStackShadow.pop(); tag = popStack(); endTag(tag); } } protected void expr(String token, boolean escape) { String expr = token; int i = token.indexOf(ELVIS); String substitute = null; if (i > 0) { expr = token.substring(0, i); substitute = token.substring(i + ELVIS.length()).trim(); // if (substitute.startsWith("\"")) // substitute = substitute.substring(1); // if (substitute.endsWith("\"")) // substitute = substitute.substring(0, substitute.length() - 1); } verifyExpr(expr); if (escape) { expr = "escape(" + expr + ")"; substitute = "escape(" + substitute + ")"; } if (substitute != null) { // trap any null or empty string and use the substitute printLine("try {" + " Object o = " + expr + "; " + "if (o.toString().length() ==0) { " + "p(" + substitute + "); } " + "else { p(o); } } " + "catch (NullPointerException npe) { " + "p(" + substitute + "); }"); // printLine("try { Object o = expr; p(" + expr + "); } " + // "catch (NullPointerException npe) { " + // "p(\"" + substitute + "\"); }"); } else { if (getTemplateClassMetaData().suppressNull) printLine("try { p(" + expr + "); } catch (NullPointerException npe) {}"); else printLine("p(" + expr + ");"); } } private void verifyExpr(String expr) { if (!JavaSyntaxTool.isValidExpr(expr)) { throw new JapidCompilationException(template, parser.getLineNumber(), "invalid Java expression: " + expr); } } protected void printLine(String string) { print(string); markLine(); println(); } protected void markLine() { markLine(parser.getLineNumber()); } protected void message(String token) { token = token.trim(); List<String> args = null; try { args = JavaSyntaxTool.parseArgs(token); } catch (RuntimeException e) { throw new JapidCompilationException( template, parser.getLineNumber(), "Message lookup commmand takes arguments like in a Java method call. Don't use single quotation marks to quote a message name for instance. " + token); } String expr = ""; if (args.size() == 1) { expr = decorQuote(args.get(0)); } else if (args.size() >= 2) { expr = decorQuote(args.get(0)); for (int i = 1; i < args.size(); i++) { expr += ", " + args.get(i); } } else { throw new JapidCompilationException( template, parser.getLineNumber(), "Message lookup commmand must take arguments. Bad number of args: " + token); } print(";p(getMessage(" + expr + "));"); markLine(); println(); } private String decorQuote(String token) { String expr = token.replace('\'', '"'); if (!expr.startsWith("\"")) expr = "\"" + expr; if (!expr.endsWith("\"")) expr += "\""; return expr; } /** * * @param token * * @param absolute */ protected void action(String token, boolean absolute) { String action = token.trim(); if (action.matches("^'.*'$") || action.matches("^\".*\"$") || action.startsWith("/")) { // static content like @{'my.css'} action = action.replace('\'', '"'); if (action.startsWith("/")) { action = '"' + action + '"'; } if (absolute) { print("p(lookupStaticAbs(" + action + "));"); } else { print("p(lookupStatic(" + action + "));"); } } else { if (!action.endsWith(")")) { throw new JapidCompilationException(template, parser.getLineNumber(), "action argument must be a method call. It was: " + action); } try { List<String> parseArgs = JavaSyntaxTool.parseArgs(action); if (parseArgs.size() != 1) { throw new JapidCompilationException(template, parser.getLineNumber(), "action argument must be a method call. It was: " + action); } } catch (RuntimeException e) { throw new JapidCompilationException(template, parser.getLineNumber(), "action argument must be a method call. It was: " + action); } // extract params if any int indexOfParam = action.indexOf("("); if (indexOfParam < 1) { throw new JapidCompilationException(template, parser.getLineNumber(), "action arguments must be enclosed in parenthesis."); } String actionPart = action.substring(0, indexOfParam).trim(); // extract the param list part String params = action.substring(indexOfParam + 1); params = params.substring(0, params.length() - 1).trim(); if (params.length() == 0) params = "new Object[]{}"; if (absolute) { print("p(lookupAbs(\"" + actionPart + "\", " + params + "));"); } else { print("p(lookup(\"" + actionPart + "\", " + params + "));"); } } markLine(); println(); } protected void hop() { String source = template.source; Tag rootTag = new Tag() { { tagName = ROOT_TAGNAME; startLine = 0; hasBody = true; } }; this.tagsStack.push(rootTag); this.tagsStackShadow.push(rootTag); this.parser = new JapidParser(source); getTemplateClassMetaData().setContentType(template.contentTypeHeader); getTemplateClassMetaData().packageName = template.packageName; getTemplateClassMetaData().setClassName(template.className); parse(); Tag tag = popStack(); if (!tagsStack.empty()) throw new JapidCompilationException(template, parser.getLineNumber(), "There is(are) " + tagsStack.size() + " unclosed tag(s) in the template: " + this.template.name); // remove print nothing statement to save a few CPU cycles this.getTemplateClassMetaData().body = tag.getBodyText().replace("p(\"\")", "").replace("pln(\"\")", "pln()"); postParsing(tag); template.javaSource = this.getTemplateClassMetaData().generateCode(); } /** * add anything before the java source generation */ protected void postParsing(Tag tag) { this.getTemplateClassMetaData().renderArgs = tag.callbackArgs; this.getTemplateClassMetaData().setArgsLineNum(tag.startLine); } abstract protected AbstractTemplateClassMetaData getTemplateClassMetaData(); protected void templateArgs(String token) { Integer lineNumber = parser.getLineNumber(); try { JavaSyntaxTool.parseParams(token); } catch (RuntimeException e) { throw new JapidCompilationException(template, lineNumber, e.getMessage()); } Tag currentTag = this.tagsStack.peek(); currentTag.callbackArgs = token; currentTag.startLine = lineNumber; } /** * @return */ protected Tag buildTag(String token) { String tagText = token.trim().replaceAll(NEW_LINE, SPACE); boolean hasBody = !parser.checkNext().endsWith("/"); Integer lineNumber = parser.getLineNumber(); try { Tag tag = new TagInvocationLineParser().parse(tagText); if (tag.tagName == null || tag.tagName.length() == 0) throw new JapidCompilationException(template, lineNumber, "tag name was empty: " + tagText); if (tag.tagName.startsWith(".")) { // partial path, use current package as the root and append the path // to it tag.tagName = getTemplateClassMetaData().packageName + tag.tagName; } tag.startLine = lineNumber; tag.hasBody = hasBody; tag.tagIndex = tagIndex++; return tag; } catch (RuntimeException e) { throw new JapidCompilationException(template, lineNumber, e.getMessage()); } } /** * e.g.: * * <pre> * `tag myTag a, c |String c * </pre> * * @return */ protected Tag buildTagDirective(String token) { String tagText = token.trim(); try { Tag tag = new TagInvocationLineParser().parse(tagText); if (tag.tagName == null || tag.tagName.length() == 0) throw new JapidCompilationException(template, tag.startLine, "tag name was empty: " + tagText); if (tag.tagName.startsWith(".")) { // partial path, use current package as the root and append the path // to it tag.tagName = getTemplateClassMetaData().packageName + tag.tagName; } tag.startLine = parser.getLineNumber(); tag.tagIndex = tagIndex++; return tag; } catch (RuntimeException e) { if (e instanceof JapidCompilationException) throw e; else throw new JapidCompilationException(template, parser.getLineNumber(), e.getMessage()); } } /** * @param actionString */ protected String createActionRunner(String actionString) { List<String> params = JavaSyntaxTool.parseArgs(actionString); String action = params.get(0); // remove the argument part to extract action string as key base int left = action.indexOf('('); if (left < 1) { throw new JapidCompilationException(template, parser.getLineNumber(), "invoke: action arguments must be enclosed in parenthesis."); } int right = action.lastIndexOf(')'); String actionPath = "\"" + action.substring(0, left) + "\""; String args = action.substring(left + 1, right).trim(); String ttl = "\"\""; if (params.size() >= 2) { ttl = params.get(1); if (params.size() > 2) { for (int i = 2; i < params.size(); i++) { args += "," + params.get(i); } if (args.startsWith(",")) args = args.substring(1); if (args.endsWith(",")) args = args.substring(0, args.length() - 1); } } return createActionRunner(action, ttl, actionPath, args); } protected void printActionInvocation(String action) { try { print(createActionRunner(action)); } catch (RuntimeException e) { throw new JapidCompilationException(template, parser.getLineNumber(), "invalid argument syntax to invoke an action: " + action); } } /** * @param tag */ protected void regularTagInvoke(Tag tag) { if ("extends".equals(tag.tagName)) { String layoutName = tag.args; layoutName = layoutName.replace("'", ""); layoutName = layoutName.replace("\"", ""); layoutName = removeEndingString(layoutName, HTML); layoutName = removeEndingString(layoutName, "/"); getTemplateClassMetaData().superClass = layoutName.replace('/', '.'); } else if (tag.tagName.equals("invoke")) { invokeAction(tag); } else { // the safest thing to do is to create a new instance of the tag // class // this however comes at a performance cost String tagClassName = tag.tagName; if (tagClassName.equals("this")) { // call itself tagClassName = this.getTemplateClassMetaData().getClassName(); } // String tagVar = tag.getTagVarName(); // String tagline = "final " + tagClassName + " " + tagVar + " = new " + tagClassName + "(getOut()); "; // tagline += tagVar; String tagline = "new " + tagClassName + "(" + getTemplateClassMetaData().getClassName() + ".this)" ; if (!tag.hasBody) { // if (useWithPlay && !tag.tagName.equals(Each.class.getSimpleName())) { // tagline += ".setActionRunners(getActionRunners())"; // } // tagline += ".setOut(getOut()); " + tagVar + ".render(" + tag.args + "); " // + makeLineMarker(tag.startLine); tagline += ".render(" + tag.args + "); " + makeLineMarker(tag.startLine); // String tagClassName = tag.tagName; // if (tagClassName.equals("this")) { // tagClassName = getTemplateClassMetaData().getClassName(); // } // // tagline = "final " + tagClassName + " " + tagVar + // " = new " + tagClassName + "(getOut());"; // // tagline += " " + tagVar + ".render(" + tag.args + ");"; // // if (getTemplateClassMetaData().useWithPlay) { // tagline = "((" + tagClassName + ")(new " + tagClassName + // "(getOut()).setActionRunners(getActionRunners()))).render(" + // tag.args + ");"; // } // else { // tagline = "new " + tagClassName + "(getOut()).render(" + // tag.args + ");"; // } print(tagline); } } } /** * @param tag */ protected void invokeAction(Tag tag) { if (tag.hasBody) { throw new JapidCompilationException(template, parser.getLineNumber(), "invoke tag cannot have a body. Must be ended with /}"); } this.getTemplateClassMetaData().setHasActionInvocation(); String action = tag.args; printActionInvocation(action); } /** * @param tag */ protected void endRegularTag(Tag tag) { if (tag.hasBody) { InnerClassMeta bodyInner = this.getTemplateClassMetaData().addCallTagBodyInnerClass(tag.tagName, tag.tagIndex, tag.callbackArgs, tag.getBodyText()); if (bodyInner == null) throw new RuntimeException("compiler bug? " + tag.tagName + " not allowed to have instance of this tag"); String tagVar = tag.getTagVarName(); String tagClassName = tag.tagName; if (tagClassName.equals("this")) { // call itself tagClassName = this.getTemplateClassMetaData().getClassName(); } // String tagline = "final " + tagClassName + " " + tagVar + " = new " + tagClassName + "(getOut()); " // + tagVar; String tagline = "new " + tagClassName + "(" + getTemplateClassMetaData().getClassName() + ".this)"; // make sure the tag always use the current output buffer; // if (useWithPlay && !tag.tagName.equals(Each.class.getSimpleName())) { // tagline += ".setActionRunners(getActionRunners())"; // } // tagline += ".setOut(getOut()); " + tagVar; if (tag.argsNamed()) { tagline += ".render( " + makeLineMarker(tag.startLine) + "\n" + bodyInner.getAnonymous(makeLineMarker(tag.startLine)) + ", " + (WebUtils.asBoolean(tag.args) ? tag.args : "") + ");"; } else { tagline += ".render(" + makeLineMarker(tag.startLine) + "\n" + (WebUtils.asBoolean(tag.args) ? tag.args + ", " : "") + bodyInner.getAnonymous(makeLineMarker(tag.startLine)) + ");"; } // tagline += makeLineMarker(tag.startLine); print(tagline); } else { // for simple tag call without call back: this.getTemplateClassMetaData().addCallTagBodyInnerClass(tag.tagName, tag.tagIndex, null, null); // the calling statement has been added in the regularTagInvoke() // method } // is inside of a tag of own scope and retract the tag inner body class TagInTag def = getTagInTag(); if (def != null) { this.getTemplateClassMetaData().removeLastCallTagBodyInnerClass(); } } // this won't work for nested for loops. too bad protected void endEach(Tag tag) { String line = "new Runnable() {public void run() {\n"; line += "int _size = -100; int _index = 0; boolean _isOdd = false; String _parity = \"\"; boolean _isFirst = true; Boolean _isLast = _index == _size;\n"; line += "for (" + tag.callbackArgs + " : " + tag.args + ") { " + makeLineMarker(tag.startLine) + "\n"; line += " _index++; _isOdd = !_isOdd; _parity = _isOdd? \"odd\" : \"even\"; _isFirst = _index == 1; if (_size == -100) _size = getCollectionSize(" + tag.args + "); _isLast = (_size < 0 ? null : _index == _size);\n"; line += tag.getBodyText() + "\n"; line += "}\n"; line += "}}.run();\n"; print(line); } private TagInTag getTagInTag() { // recursively search the stack for TagInTag Tag t; try { for (int i = tagsStack.size(); i > 0; i--) { t = tagsStack.get(i - 1); if (t instanceof TagInTag) { return (TagInTag) t; } } } catch (Exception e) { } return null; } /** * define a string returning method from a block * * @param tag */ protected void def(Tag tag) { } protected void endDef(Tag tag) { if (tag.hasBody) { // must always this.getTemplateClassMetaData().addDefTag((TagDef) tag); } else { throw new JapidCompilationException(template, tag.startLine, "def tag must have a body"); } } protected void endTag(Tag tag) { String lastInStack = tag.tagName; String tagName = lastInStack; // if (!lastInStack.equals(tagName)) { // throw new JapidCompilationException(template, tag.startLine, "#{" + // tag.tagName + "} is not closed."); // } if (tagName.equals("def")) { endDef(tag); } else if (tagName.equals("set")) { endSet((TagSet) tag); } else if (tagName.equals("doBody")) { } else if (tagName.equals("extends")) { } else if (tagName.equals("get")) { // } else if (tagName.equals("set")) { // the set is handled in the // JapidTemplateCompiler endTagSpecial() } else if (tagName.equals("invoke")) { } else if (tagName.equals(Each.class.getSimpleName())) { endEach(tag); } else if (tagName.equals(DO_LAYOUT)) { } else if (endTagSpecial(tag)) { } else { endRegularTag(tag); } markLine(tag.startLine); println(); // tagIndex--; skipLineBreak = true; } abstract void endSet(TagSet tag); /** * sub class can detect special tag and return true to indicate the tag has * been processed. * * @param tag * @return */ protected boolean endTagSpecial(Tag tag) { return false; } String createActionRunner(String action, String ttl, String base, String keys) { String actionEscaped = action.replace("\"", "\\\""); String controllerActionPart = action.substring(0, action.indexOf('(')); int lastDot = controllerActionPart.lastIndexOf('.'); String controllerName = controllerActionPart.substring(0, lastDot); String actionName = controllerActionPart.substring(lastDot + 1); if (ttl == null) { // should be deprecated String template = " %s.put(getOut().length(), new %s() {\n" + " @Override\n" + " public %s run() {\n" + " try {\n" + " play.classloading.enhancers.ControllersEnhancer.ControllerInstrumentation.initActionCall();\n" + " %s;\n" + " } catch (%s jr) {\n" + " return jr.getRenderResult();\n" + " }\n" + " throw new RuntimeException(\"No render result from running: %s\");\n" + " }\n" + " });"; return String.format(template, AbstractTemplateClassMetaData.ACTION_RUNNERS, ActionRunner.class.getName(), RenderResult.class.getName(), action, JAPID_RESULT, actionEscaped); } else { String template = " %s.put(getOut().length(), new %s(%s, %s, %s, %s) {\n" + " @Override\r\n" + " public %s runPlayAction() {\n" + // " super.checkActionCacheFor(%s.class, \"%s\");\n" // + " return (%s)%s; " + makeLineMarker(parser.getLineNumber()) + "\n" + " }\n" + " }); p(\"\\n\");"; // hack: a new line char to stand for the action position. // Should really change the action runner collection to <int, List<ActionRunner>> // hard-code the cache action runner name to avoid dependency on the // Play jar return String.format(template, AbstractTemplateClassMetaData.ACTION_RUNNERS, "cn.bran.play.CacheablePlayActionRunner", ttl, controllerName + ".class", "\"" + actionName + "\"", "".equals(keys) ? "\"\"" : keys, JAPID_RESULT, JAPID_RESULT, action ); // return String.format(template, // AbstractTemplateClassMetaData.ACTION_RUNNERS, // "cn.bran.play.CacheablePlayActionRunner", // ttl, // base, // "".equals(keys) ? "\"\"" : keys, // JAPID_RESULT, // controllerName, // actionName, // action // ); } } protected int currentLine = 1; protected int indentLevel = 0; JapidParser.Token state; JapidParser.Token previousState; JapidParser.Token stateBeforePreviousState; public JapidAbstractCompiler() { super(); } public void setUseWithPlay(boolean play) { this.useWithPlay = play; } /** * @param tag */ protected void pushToStack(Tag tag) { // if calling inside a TagInTag tag, put it in the scope TagInTag tagtagf = getTagInTag(); if (tagtagf != null) { if (tag instanceof TagInTag) { throw new JapidCompilationException(template, tag.startLine, "Syntax error: def/set tag cannot be nested in another def/set tag."); } if (!(tag instanceof TagIf)) tagtagf.tags.add(tag); } tagsStackShadow.push(tag); if (!(tag instanceof TagIf)) tagsStack.push(tag); } }