package play.templates;
import groovy.lang.Closure;
import java.io.PrintWriter;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
import play.Play;
import play.exceptions.TemplateCompilationException;
import play.templates.GroovyInlineTags.CALL;
/**
* The template compiler
*/
public class GroovyTemplateCompiler extends TemplateCompiler {
public static List<String> extensionsClassnames = new ArrayList<String>();
// [#714] The groovy-compiler complaints if a line is more than 65535 unicode units long..
// Have to split it if it is really that big
protected static final int maxPlainTextLength = 60000;
@Override
public BaseTemplate compile(BaseTemplate template) {
try {
extensionsClassnames.clear();
extensionsClassnames.addAll( Play.pluginCollection.addTemplateExtensions());
List<Class> extensionsClasses = Play.classloader.getAssignableClasses(JavaExtensions.class);
for (Class extensionsClass : extensionsClasses) {
extensionsClassnames.add(extensionsClass.getName());
}
} catch (Throwable e) {
//
}
return super.compile(template);
}
@Override
String source() {
String source = template.source;
// If a plugin has something to change in the template before the compilation
source = Play.pluginCollection.overrideTemplateSource(template, source);
// Static access
List<String> names = new ArrayList<String>();
Map<String, String> originalNames = new HashMap<String, String>();
for (Class clazz : Play.classloader.getAllClasses()) {
if (clazz.getName().endsWith("$")) {
String name = clazz.getName().substring(0, clazz.getName().length() - 1).replace('$', '.') + '$';
names.add(name);
originalNames.put(name, clazz.getName());
} else {
String name = clazz.getName().replace('$', '.');
names.add(name);
originalNames.put(name, clazz.getName());
}
}
Collections.sort(names, new Comparator<String>() {
public int compare(String o1, String o2) {
return o2.length() - o1.length();
}
});
// We're about to do many many String.replaceAll() so we do some checking first
// to try to reduce the number of needed replaceAll-calls.
// Morten: I have tried to create a single regexp that can be used instead of all the replaceAll,
// but I failed to do so.. Such a single regexp would be much faster since
// we then we only would have to have one pass.
if (!names.isEmpty()) {
if (names.size() <= 1 || source.indexOf("new ")>=0) {
for (String cName : names) { // dynamic class binding
source = source.replaceAll("new " + Pattern.quote(cName) + "(\\([^)]*\\))", "_('" + originalNames.get(cName) + "').newInstance$1");
}
}
if (names.size() <= 1 || source.indexOf("instanceof")>=0) {
for (String cName : names) { // dynamic class binding
source = source.replaceAll("([a-zA-Z0-9.-_$]+)\\s+instanceof\\s+" + Pattern.quote(cName), "_('" + originalNames.get(cName).replace("$", "\\$") + "').isAssignableFrom($1.class)");
}
}
if (names.size() <= 1 || source.indexOf(".class")>=0) {
for (String cName : names) { // dynamic class binding
source = source.replaceAll("([^.])" + Pattern.quote(cName) + ".class", "$1_('" + originalNames.get(cName) + "')");
}
}
// With the current arg0 in replaceAll, it is not possible to do a quick indexOf-check for this one,
// so we have to run all the replaceAll-calls
for (String cName : names) { // dynamic class binding
source = source.replaceAll("([^'\".])" + Pattern.quote(cName) + "([.][^'\"])", "$1_('" + originalNames.get(cName).replace("$", "\\$") + "')$2");
}
}
return source;
}
@Override
void head() {
print("class ");
//This generated classname is parsed when creating cleanStackTrace.
//The part after "Template_" is used as key when
//looking up the file on disk this template-class is generated from.
//cleanStackTrace is looking in TemplateLoader.templates
String uniqueNumberForTemplateFile = TemplateLoader.getUniqueNumberForTemplateFile(template.name);
String className = "Template_" + uniqueNumberForTemplateFile;
print(className);
println(" extends play.templates.GroovyTemplate.ExecutableTemplate {");
println("public Object run() { use(play.templates.JavaExtensions) {");
for (String n : extensionsClassnames) {
println("use(_('" + n + "')) {");
}
}
@Override
@SuppressWarnings("unused")
void end() {
for (String n : extensionsClassnames) {
println(" } ");
}
println("} }");
println("}");
}
/**
* Interesting performance observation:
* Calling print(); from java (in ExecutableTemplate) called from groovy is MUCH slower than
* java returning string to groovy
* which then prints with out.print();
*/
@Override
void plain() {
String text = parser.getToken().replace("\\", "\\\\").replaceAll("\"", "\\\\\"").replace("$", "\\$");
if (skipLineBreak && text.startsWith("\n")) {
text = text.substring(1);
}
skipLineBreak = false;
text = text.replaceAll("\r\n", "\n").replaceAll("\n", "\\\\n");
// we don't have to print line numbers here since this cannot fail - it is only text printing
// [#714] The groovy-compiler complaints if a line is more than 65535 unicode units long..
// Have to split it if it is really that big
if (text.length() <maxPlainTextLength) {
// text is "short" - just print it
println("out.print(\""+text+"\");");
} else {
// text is long - must split it
int offset = 0;
do {
int endPos = offset+maxPlainTextLength;
if (endPos>text.length()) {
endPos = text.length();
} else {
// #869 If the last char (at endPos-1) is \, we're dealing with escaped char - must include the next one also..
if ( text.charAt(endPos-1) == '\\') {
// use one more char so the escaping is not broken. Don't have to check length, since
// all '\' is used in escaping, ref replaceAll above..
endPos++;
}
}
println("out.print(\""+text.substring(offset, endPos)+"\");");
offset+= (endPos - offset);
}while(offset < text.length());
}
}
@Override
void script() {
String text = parser.getToken();
if (text.indexOf("\n") > -1) {
String[] lines = parser.getToken().split("\n");
for (int i = 0; i < lines.length; i++) {
print(lines[i]);
markLine(parser.getLine() + i);
println();
}
} else {
print(text);
markLine(parser.getLine());
println();
}
skipLineBreak = true;
}
@Override
void expr() {
String expr = parser.getToken().trim();
print(";out.print(__safeFaster("+expr+"))");
markLine(parser.getLine());
println();
}
@Override
void message() {
String expr = parser.getToken().trim();
print(";out.print(__getMessage("+expr+"))");
markLine(parser.getLine());
println();
}
@Override
void action(boolean absolute) {
String action = parser.getToken().trim();
if (action.trim().matches("^'.*'$")) {
if (absolute) {
print("\tout.print(__reverseWithCheck_absolute_true("+action+"));");
} else {
print("\tout.print(__reverseWithCheck_absolute_false("+action+"));");
}
} else {
if (!action.endsWith(")")) {
action = action + "()";
}
if (absolute) {
print("\tout.print(actionBridge._abs()." + action + ");");
} else {
print("\tout.print(actionBridge." + action + ");");
}
}
markLine(parser.getLine());
println();
}
@Override
void startTag() {
tagIndex++;
String tagText = parser.getToken().trim().replaceAll("\n", " ");
String tagName = "";
String tagArgs = "";
boolean hasBody = !parser.checkNext().endsWith("/");
if (tagText.indexOf(" ") > 0) {
tagName = tagText.substring(0, tagText.indexOf(" "));
tagArgs = tagText.substring(tagText.indexOf(" ") + 1).trim();
if (!tagArgs.matches("^[_a-zA-Z0-9]+\\s*:.*$")) {
tagArgs = "arg:" + tagArgs;
}
// We only have to try to replace the following if we find at least one
// @ in tagArgs..
if (tagArgs.indexOf('@')>=0) {
tagArgs = tagArgs.replaceAll("[:]\\s*[@]{2}", ":actionBridge._abs().");
tagArgs = tagArgs.replaceAll("(\\s)[@]{2}", "$1actionBridge._abs().");
tagArgs = tagArgs.replaceAll("[:]\\s*[@]{1}", ":actionBridge.");
tagArgs = tagArgs.replaceAll("(\\s)[@]{1}", "$1actionBridge.");
}
} else {
tagName = tagText;
tagArgs = ":";
}
Tag tag = new Tag();
tag.name = tagName;
tag.startLine = parser.getLine();
tag.hasBody = hasBody;
tagsStack.push(tag);
if (tagArgs.trim().equals("_:_")) {
print("attrs" + tagIndex + " = _attrs;");
} else {
print("attrs" + tagIndex + " = [" + tagArgs + "];");
}
// Use inlineTag if exists
try {
Method m = GroovyInlineTags.class.getDeclaredMethod("_" + tag.name, int.class, CALL.class);
print("play.templates.TagContext.enterTag('" + tag.name + "');");
print((String) m.invoke(null, new Object[]{tagIndex, CALL.START}));
tag.hasBody = false;
markLine(parser.getLine());
println();
skipLineBreak = true;
return;
} catch (Exception e) {
// do nothing here
}
if (!tag.name.equals("doBody") && hasBody) {
print("body" + tagIndex + " = {");
markLine(parser.getLine());
println();
} else {
print("body" + tagIndex + " = null;");
markLine(parser.getLine());
println();
}
skipLineBreak = true;
}
@Override
void endTag() {
String tagName = parser.getToken().trim();
if (tagsStack.isEmpty()) {
throw new TemplateCompilationException(template, parser.getLine(), "#{/" + tagName + "} is not opened.");
}
Tag tag = tagsStack.pop();
String lastInStack = tag.name;
if (tagName.equals("")) {
tagName = lastInStack;
}
if (!lastInStack.equals(tagName)) {
throw new TemplateCompilationException(template, tag.startLine, "#{" + tag.name + "} is not closed.");
}
if (tag.name.equals("doBody")) {
print("if(_body || attrs" + tagIndex + "['body']) {");
print("def toExecute = attrs" + tagIndex + "['body'] ?: _body; toUnset = []; if(attrs" + tagIndex + "['vars']) {");
print("attrs" + tagIndex + "['vars'].each() {");
print("if(toExecute.getProperty(it.key) == null) {toUnset.add(it.key);}; toExecute.setProperty(it.key, it.value);");
print("}};");
print("if(attrs" + tagIndex + "['as']) { setProperty(attrs" + tagIndex + "['as'], toExecute.toString()); } else { out.print(toExecute.toString()); }; toUnset.each() {toExecute.setProperty(it, null)} };");
markLine(tag.startLine);
template.doBodyLines.add(currentLine);
println();
} else {
if (tag.hasBody) {
print("};"); // close body closure
}
println();
// Use inlineTag if exists
try {
Method m = GroovyInlineTags.class.getDeclaredMethod("_" + tag.name, int.class, CALL.class);
println((String) m.invoke(null, new Object[]{tagIndex, CALL.END}));
print("play.templates.TagContext.exitTag();");
} catch (Exception e) {
// Use fastTag if exists
List<Class> fastClasses = new ArrayList<Class>();
try {
fastClasses = Play.classloader.getAssignableClasses(FastTags.class);
} catch (Exception xe) {
//
}
fastClasses.add(0, FastTags.class);
Method m = null;
String tName = tag.name;
String tSpace = "";
if (tName.indexOf(".") > 0) {
tSpace = tName.substring(0, tName.lastIndexOf("."));
tName = tName.substring(tName.lastIndexOf(".") + 1);
}
for (Class<?> c : fastClasses) {
if (!c.isAnnotationPresent(FastTags.Namespace.class) && tSpace.length() > 0) {
continue;
}
if (c.isAnnotationPresent(FastTags.Namespace.class) && !c.getAnnotation(FastTags.Namespace.class).value().equals(tSpace)) {
continue;
}
try {
m = c.getDeclaredMethod("_" + tName, Map.class, Closure.class, PrintWriter.class, GroovyTemplate.ExecutableTemplate.class, int.class);
} catch (NoSuchMethodException ex) {
continue;
}
}
if (m != null) {
print("play.templates.TagContext.enterTag('" + tag.name + "');");
print("_('" + m.getDeclaringClass().getName() + "')._" + tName + "(attrs" + tagIndex + ",body" + tagIndex + ", out, this, " + tag.startLine + ");");
print("play.templates.TagContext.exitTag();");
} else {
print("invokeTag(" + tag.startLine + ",'" + tagName + "',attrs" + tagIndex + ",body" + tagIndex + ");");
}
}
markLine(tag.startLine);
println();
}
tagIndex--;
skipLineBreak = true;
}
}