/*
* Ext GWT 2.2.3 - Ext for GWT
* Copyright(c) 2007-2010, Ext JS, LLC.
* licensing@extjs.com
*
* http://extjs.com/license
*/
package com.extjs.gxt.ui.rebind.core;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.nio.charset.Charset;
import java.util.HashSet;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import com.extjs.gxt.ui.client.core.El;
import com.extjs.gxt.ui.client.core.Markup;
import com.extjs.gxt.ui.client.core.MarkupBase;
import com.extjs.gxt.ui.client.core.Template;
import com.extjs.gxt.ui.client.core.Templates;
import com.extjs.gxt.ui.client.core.TemplatesBase;
import com.extjs.gxt.ui.client.core.TemplatesCache;
import com.extjs.gxt.ui.client.core.Templates.Cache;
import com.extjs.gxt.ui.client.core.Templates.Compress;
import com.extjs.gxt.ui.client.core.Templates.Resource;
import com.extjs.gxt.ui.client.core.Templates.Selector;
import com.google.gwt.core.client.GWT;
import com.google.gwt.core.ext.Generator;
import com.google.gwt.core.ext.GeneratorContext;
import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.core.ext.typeinfo.JClassType;
import com.google.gwt.core.ext.typeinfo.JMethod;
import com.google.gwt.core.ext.typeinfo.JParameter;
import com.google.gwt.core.ext.typeinfo.JType;
import com.google.gwt.core.ext.typeinfo.NotFoundException;
import com.google.gwt.core.ext.typeinfo.TypeOracle;
import com.google.gwt.user.client.Element;
import com.google.gwt.user.rebind.ClassSourceFileComposerFactory;
import com.google.gwt.user.rebind.SourceWriter;
/**
* This is the thread-safe Generator for
* {@link com.extjs.gxt.ui.client.core.Templates} subinterfaces.
*/
public class TemplatesGenerator extends Generator {
@Override
public String generate(TreeLogger logger, GeneratorContext context, String typeName)
throws UnableToCompleteException {
return new SourceGenerator(logger, context).generate(typeName);
}
/**
* This class is responsible for creating the implementing class of a
* {@link com.extjs.gxt.ui.client.core.Templates} subinterface.
*/
public static class SourceGenerator {
private TreeLogger logger;
private GeneratorContext context;
private JClassType type;
private ClassSourceFileComposerFactory composer;
private SourceWriter sw;
private JType templateType;
private JType stringType;
private JClassType elementType;
private JClassType templatesType;
private JClassType markupType;
private JClassType elType;
public SourceGenerator(TreeLogger logger, GeneratorContext context) {
this.logger = logger;
this.context = context;
}
public String generate(String typeName) throws UnableToCompleteException {
TypeOracle oracle = context.getTypeOracle();
try {
type = oracle.getType(typeName);
elementType = oracle.getType(Element.class.getName());
templatesType = oracle.getType(Templates.class.getName());
templateType = oracle.getType(Template.class.getName());
stringType = oracle.getType(String.class.getName());
markupType = oracle.getType(Markup.class.getName());
elType = oracle.getType(El.class.getName());
} catch (NotFoundException e) {
logger.log(TreeLogger.ERROR, "Class " + typeName + " not found.", e);
throw new UnableToCompleteException();
}
validateType();
final String genPackageName = type.getPackage().getName();
final String genClassName = (type.getName().replace('.', '_')) + "Impl";
composer = new ClassSourceFileComposerFactory(genPackageName, genClassName);
if (type.isAssignableTo(markupType)) {
composer.setSuperclass(MarkupBase.class.getName());
} else {
composer.setSuperclass(TemplatesBase.class.getName());
}
composer.addImplementedInterface(type.getQualifiedSourceName());
composer.addImport(Template.class.getName());
composer.addImport(GWT.class.getName());
composer.addImport(MarkupBase.class.getName());
composer.addImport(TemplatesBase.class.getName());
composer.addImport(elementType.getQualifiedSourceName());
composer.addImport(elType.getQualifiedSourceName());
composer.addImport(templatesType.getQualifiedSourceName());
composer.addImport(markupType.getQualifiedSourceName());
composer.addImport(TemplatesCache.class.getName());
PrintWriter pw = context.tryCreate(logger, genPackageName, genClassName);
if (pw != null) {
sw = composer.createSourceWriter(context, pw);
for (JMethod method : type.getOverridableMethods()) {
if (type.isAssignableTo(templatesType)) {
createTemplatesMethod(method);
} else if (type.isAssignableTo(markupType)) {
createMarkupMethod(method);
}
}
sw.commit(logger);
}
return composer.getCreatedClassName();
}
private void validateType() throws UnableToCompleteException {
// check for duplicate method names
Set<String> methodNames = new HashSet<String>();
for (JMethod method : type.getOverridableMethods()) {
if (methodNames.contains(method.getName())) {
logger.log(TreeLogger.ERROR, "Class " + type
+ " must not contain multiple methods of the same name. [" + method.getName() + "]");
throw new UnableToCompleteException();
}
methodNames.add(method.getName());
}
}
private void createMarkupMethod(JMethod method) throws UnableToCompleteException {
final boolean returnsEl = method.getReturnType() == elType;
final boolean returnsElement = method.getReturnType() == elementType;
if (!(returnsEl || returnsElement)) {
return;
}
String selector;
Selector selectorAnn = method.getAnnotation(Selector.class);
if (selectorAnn == null || "".equals(selectorAnn.value())) {
return;
}
selector = selectorAnn.value();
sw.indent();
sw.print(method.getReadableDeclaration(false, false, false, false, true));
sw.println(" {");
sw.indent();
sw.println("Element element = select(\"" + escape(selector) + "\");");
// do all selectors have to return a node ? Should null checking be
// done here, or in the caller ?
// sw.println("assert element != null : \"" + method.getName() + " using
// selector '" + escape(selector) + "' returned a null element\";");
sw.println("return " + (returnsEl ? "new El(element);" : "element;"));
sw.outdent();
sw.println("}");
}
private void createTemplatesMethod(JMethod method) throws UnableToCompleteException {
final boolean returnString = method.getReturnType() == stringType;
final boolean returnsMarkup = method.getReturnType().isInterface() != null
&& method.getReturnType().isInterface().isAssignableTo(markupType);
final boolean returnsTemplate = method.getReturnType() == templateType;
if (!(returnsTemplate || returnString || returnsMarkup)) {
logger.log(TreeLogger.ERROR, "Method " + method.getName() + " does not return "
+ templateType + " or " + stringType + " or " + markupType);
throw new UnableToCompleteException();
}
// System.out.println(cacheKeyExpression);
sw.indent();
// System.out.println(method.getReadableDeclaration(false, false, false,
// false, true));
sw.print(method.getReadableDeclaration(false, false, false, false, true));
sw.println(" {");
sw.indent();
// return String or subclass of Markup
if (returnString) {
boolean cache = isCache(method);
String content = readResource(method);
String cacheKeyExpression = createCacheKeyExpression(method);
if (cache) {
sw.print("String cached = (String)TemplatesCache.INSTANCE.get(" + cacheKeyExpression
+ ");");
sw.println("if (cached != null) return cached;");
}
generateResultBody(content);
if (cache) {
sw.print("TemplatesCache.INSTANCE.put(result, " + cacheKeyExpression + ");");
}
sw.println("return result;");
} else if (returnsMarkup) {
boolean cache = isCache(method);
String content = readResource(method);
String cacheKeyExpression = createCacheKeyExpression(method);
generateReturnsMarkupMethod(method, cache, content, cacheKeyExpression);
} else if (returnsTemplate) {
boolean cache = isCache(method);
String content = readResource(method);
String cacheKeyExpression = createCacheKeyExpression(method);
// if Cache enabled, generate source to lookup the cached value
if (cache) {
sw.print("Template cached = (Template)TemplatesCache.INSTANCE.get(" + cacheKeyExpression
+ ");");
sw.println("if (cached != null) return cached;");
}
generateResultBody(content);
sw.println("Template template = new Template(result);");
// if cache enabled, compile the template and store it in the cache
if (cache) {
sw.print("template.compile();");
sw.print("TemplatesCache.INSTANCE.put(template, " + cacheKeyExpression + ");");
}
sw.println("return template;");
}
sw.outdent();
sw.println("}");
sw.outdent();
}
private boolean isCache(JMethod method) {
boolean cache = false;
Cache cacheAnn = method.getAnnotation(Cache.class);
cache = (cacheAnn != null);
return cache;
}
private void generateReturnsMarkupMethod(JMethod method, boolean cache, String content,
String cacheKeyExpression) throws UnableToCompleteException {
String returnTypeName = method.getReturnType().getParameterizedQualifiedSourceName();
String newMarkupBaseSource = method.getReturnType() == markupType ? "new MarkupBase();"
: "(MarkupBase)GWT.create(" + returnTypeName + ".class);";
if (cache) {
sw.print("MarkupBase cached = (MarkupBase)TemplatesCache.INSTANCE.get("
+ cacheKeyExpression + ");");
sw.println("if (cached != null) {");
sw.println(" MarkupBase clone = " + newMarkupBaseSource);
sw.println(" clone.init(cached.getHtml(), (Element)cached.getRootElement().cloneNode(true));");
sw.println(" return (" + returnTypeName + ") clone;");
sw.println("}");
}
generateResultBody(content);
sw.print("MarkupBase markup = " + newMarkupBaseSource);
sw.print("markup.init(result, MarkupBase.createRootElement(result));");
if (cache) {
sw.print("TemplatesCache.INSTANCE.put(markup, " + cacheKeyExpression + ");");
}
sw.println("return (" + returnTypeName + ") markup;");
}
private void generateResultBody(String content) throws UnableToCompleteException {
sw.println("StringBuilder sb = new StringBuilder();");
// put the template code in a nested block to limit the scope of its
// variables
sw.println("{");
sw.indent();
new TemplateToJavaSourceConverter(logger, sw, content).convert();
sw.outdent();
sw.println("}");
sw.println("String result = sb.toString();");
}
private String createCacheKeyExpression(JMethod method) {
String cacheKeyExpression = "\"" + type.getParameterizedQualifiedSourceName() + "#"
+ method.getName() + "\"";
for (JParameter parameter : method.getParameters()) {
cacheKeyExpression += "," + parameter.getName();
}
return cacheKeyExpression;
}
private String readResource(JMethod method) throws UnableToCompleteException {
String content = null;
// try and read from a file
String resource;
Resource resourceAnn = method.getAnnotation(Resource.class);
if (resourceAnn != null && !resourceAnn.value().equals("")) {
resource = resourceAnn.value();
} else {
resource = type.getName() + "#" + method.getName() + ".html";
}
resource = type.getPackage().getName().replace(".", "/") + "/" + resource;
InputStream is = Thread.currentThread().getContextClassLoader().getResourceAsStream(resource);
if (is == null) {
logger.log(TreeLogger.ERROR, "Unable to find source template file " + resource
+ " for method " + method.getName() + ".");
throw new UnableToCompleteException();
}
StringBuilder sb = new StringBuilder();
BufferedReader br = new BufferedReader(new InputStreamReader(is, Charset.forName("UTF-8")));
try {
String buffer = br.readLine();
while (buffer != null) {
sb.append(buffer);
buffer = br.readLine();
if (buffer != null) {
sb.append('\n');
}
}
} catch (IOException e) {
throw new UnableToCompleteException();
}
content = sb.toString();
if (method.getAnnotation(Compress.class) != null) {
content = compressHtml(content);
}
return content;
}
}
/**
* <p/>this class encapsulates all of the code to transform a single template
* into java source
*
* <p/>When the {@link TemplateToJavaSourceConverter#convert()} method is
* called the generated source already contains a variable "StringBuilder sb"
* initialized to a new instance
*
* <p/>The {@link TemplateToJavaSourceConverter#convert()} method should
* generate source of the form
*
* <pre>
* sb.append("some text");
* if (true) {
* sb.append("some more text");
* }
* </pre>
*/
public static class TemplateToJavaSourceConverter {
private final TreeLogger logger;
private final SourceWriter sw;
private final String content;
private static Pattern codeStartPattern = Pattern.compile("(.*?)((?:<%)|(?:<#)|(?:\\$\\{))",
Pattern.DOTALL);
private static Pattern fmListPattern = Pattern.compile("(.+?)\\s+as\\s+(?:(.+?):)?([^:]+)");
private static Pattern iterableRangePattern = Pattern.compile("\\[?(.+)\\.\\.([^\\]]+)");
private static Pattern iterableTablePattern = Pattern.compile("\\[([^(?:\\.\\.)]*)\\]",
Pattern.DOTALL);
private char codeType;
private String codeEnd;
private String iterableName;
private String variableName;
private String variableType;
private TemplateToJavaSourceConverter(TreeLogger logger, SourceWriter sw, String content) {
this.logger = logger;
this.sw = sw;
this.content = content;
}
private void convert() throws UnableToCompleteException {
int currentPosition = 0;
int contentLength = content.length();
while (currentPosition < contentLength) {
// Find next code start
int nextCodeStartPosition = findNextCodeStart(content, currentPosition);
// If there is text then add it
if (nextCodeStartPosition > currentPosition || nextCodeStartPosition == -1) {
sw.print("sb.append(\"");
if (nextCodeStartPosition == -1) {
sw.print(Generator.escape(content.substring(currentPosition)));
} else {
sw.print(Generator.escape(content.substring(currentPosition, nextCodeStartPosition)));
}
sw.println("\");");
}
if (nextCodeStartPosition == -1) {
break;
}
currentPosition = nextCodeStartPosition;
int codeEndPosition = findCodeEndPosition(content, currentPosition);
if (codeEndPosition == -1) {
logger.log(TreeLogger.ERROR, "Code end not found.");
throw new UnableToCompleteException();
}
generateCode(content.substring(currentPosition + 2, codeEndPosition));
currentPosition = codeEndPosition + codeEnd.length();
}
}
private void generateCode(String code) throws UnableToCompleteException {
switch (codeType) {
case '%':
case '{':
generateJspCode(code);
break;
case '#':
generateFMCode(code);
break;
}
}
private void generateFMCode(String code) throws UnableToCompleteException {
int firstWordEnd = code.indexOf(' ');
if (firstWordEnd == -1) {
firstWordEnd = code.length();
}
String firstWord = code.substring(0, firstWordEnd);
String parameters = code.substring(firstWordEnd).trim();
if ("if".equals(firstWord)) {
fmIf(parameters);
} else if ("end".equals(firstWord)) {
fmEnd(parameters);
} else if ("else".equals(firstWord)) {
fmElse(parameters);
} else if ("elseif".equals(firstWord)) {
fmElseif(parameters);
} else if ("list".equals(firstWord)) {
fmList(parameters);
} else {
logger.log(TreeLogger.ERROR, "Unknown FM code " + firstWord + ".");
}
}
private void fmElseif(String parameters) {
sw.outdent();
sw.print("} else if (");
sw.print(parameters);
sw.println(") {");
sw.indent();
}
private void fmElse(String parameters) {
sw.println("} else {");
}
private void fmList(String parameters) throws UnableToCompleteException {
Matcher matcher = fmListPattern.matcher(parameters);
if (!matcher.matches()) {
logger.log(TreeLogger.ERROR, "Incorrect parameters for list function.");
throw new UnableToCompleteException();
}
iterableName = matcher.group(1);
variableType = matcher.group(2);
variableName = matcher.group(3);
analyseIterable();
sw.print("for(");
sw.print(variableType == null ? "Object" : variableType);
sw.print(" ");
sw.print(variableName);
sw.print(" : ");
sw.print(iterableName);
sw.println(") {");
sw.indent();
}
private void analyseIterable() {
// Is it a range?
Matcher matcher = iterableRangePattern.matcher(iterableName);
if (matcher.matches()) {
iterableName = "(new IterableRange<Integer>(" + matcher.group(1) + "," + matcher.group(2)
+ "))";
variableType = "int";
return;
}
// Is it a table?
matcher = iterableTablePattern.matcher(iterableName);
if (matcher.matches()) {
String type = variableType == null ? "Object" : variableType;
iterableName = "new " + type + "[] {" + matcher.group(1) + "}";
return;
}
}
private void fmEnd(String parameters) {
sw.println("}");
sw.outdent();
}
private void fmIf(String parameters) {
sw.print("if (");
sw.print(parameters);
sw.println(") {");
sw.indent();
}
private void generateJspCode(String code) {
if (codeType == '{' || code.charAt(0) == '=') {
sw.print("sb.append(");
if (codeType == '{') {
sw.print(code);
} else {
sw.print(code.substring(1));
}
sw.println(");");
} else {
sw.println(code);
}
}
private int findCodeEndPosition(String content, int currentPosition) {
switch (codeType) {
case '%':
codeEnd = "%>";
break;
case '#':
codeEnd = "#>";
break;
case '{':
codeEnd = "}";
break;
}
return content.indexOf(codeEnd, currentPosition);
}
private int findNextCodeStart(String content, int currentPosition) {
Matcher matcher = codeStartPattern.matcher(content);
if (!matcher.find(currentPosition)) {
return -1;
} else {
codeType = matcher.group(2).charAt(1);
return matcher.start(2);
}
}
}
public static String compressHtml(String html) {
if (html == null) return null;
html = html.replaceAll("(?s)<!--.*?-->", "");
html = html.replaceAll("(?s)>[\\t\\n ]*<", "><");
html = html.replaceAll("(?s)^[\\t\\n ]*<", "<");
html = html.replaceAll("(?s)>[\\t\\n ]*$", ">");
return html;
}
}