/*
* Copyright (C) 2013-2016 The Rythm Engine project
* for LICENSE and other details see:
* https://github.com/rythmengine/rythmengine
*/
package org.rythmengine.internal.compiler;
import java.io.File;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.rythmengine.Rythm;
import org.rythmengine.RythmEngine;
import org.rythmengine.RythmEngine.TemplateTestResult;
import org.rythmengine.exception.CompileException;
import org.rythmengine.exception.RythmException;
import org.rythmengine.extension.IByteCodeEnhancer;
import org.rythmengine.extension.ICodeType;
import org.rythmengine.extension.ITemplateResourceLoader;
import org.rythmengine.internal.CodeBuilder;
import org.rythmengine.internal.IDialect;
import org.rythmengine.internal.RythmEvents;
import org.rythmengine.logger.ILogger;
import org.rythmengine.logger.Logger;
import org.rythmengine.resource.ITemplateResource;
import org.rythmengine.resource.StringTemplateResource;
import org.rythmengine.template.ITemplate;
import org.rythmengine.template.TagBase;
import org.rythmengine.template.TemplateBase;
import org.rythmengine.utils.S;
/**
* Define the data structure hold template class/template src/generated java src
*/
public class TemplateClass {
private static final ILogger logger = Logger.get(TemplateClass.class);
public static final String CN_SUFFIX = "__R_T_C__";
private static final String NO_INCLUDE_CLASS = "NO_INCLUDE_CLASS";
private static final ITemplate NULL_TEMPLATE = new TagBase() {
@Override
public ITemplate __cloneMe(RythmEngine engine, ITemplate caller) {
return null;
}
};
/**
* Store root level template class, e.g. the one that is not an embedded class
*/
private TemplateClass root;
private boolean inner = false;
private RythmEngine engine = null;
private boolean enhancing = false;
private transient List<TemplateClass> embeddedClasses = new ArrayList<TemplateClass>();
/**
* The fully qualified class name
*/
private String name;
public TemplateClass extendedTemplateClass;
private Set<TemplateClass> includedTemplateClasses = new HashSet<TemplateClass>();
private String includeTemplateClassNames = null;
private Map<String, String> includeTagTypes = new HashMap<String, String>();
private String tagName;
/**
* The Java source
*/
public String javaSource;
/**
* The compiled byteCode
*/
public byte[] javaByteCode;
/**
* The enhanced byteCode
*/
public byte[] enhancedByteCode;
/**
* Store a list of import path, i.e. those imports ends with ".*"
*/
public Set<String> importPaths;
/**
* The in JVM loaded class
*/
public Class<ITemplate> javaClass;
/**
* The in JVM loaded package
*/
public Package javaPackage;
/**
* The code type could be HTML, JS, JSON etc
*/
public ICodeType codeType;
/**
* Is this class compiled
*/
private boolean compiled;
/**
* Signatures checksum
*/
public int sigChecksum;
/**
* Mark if this is a valid Rythm Template
*/
private boolean isValid = true;
/**
* CodeBuilder to generate java source code
* <p/>
* Could be used to merge state into including template class codeBuilder
*/
public CodeBuilder codeBuilder;
/**
* The ITemplate instance
*/
private TemplateBase templateInstance;
/**
* Store the resource loader class name
*/
private String resourceLoaderClass;
/**
* the template resource
*/
public ITemplateResource templateResource;
/**
* specify the dialect for the template
*/
transient private IDialect dialect;
private String magic = S.random(4);
public TemplateClass root() {
return root;
}
private TemplateClass() {
}
public boolean isInner() {
return inner;
}
private RythmEngine engine() {
return null == engine ? Rythm.engine() : engine;
}
public String name0() {
return name();
}
public String name() {
return name;
}
/*
* WRITE : includedTemplateClasses, includeTagTypes
*/
public void addIncludeTemplateClass(TemplateClass tc) {
includedTemplateClasses.add(tc);
includeTagTypes.putAll(tc.includeTagTypes);
}
/*
* WRITE : includeTemplateClassNames
*/
public String refreshIncludeTemplateClassNames() {
if (includedTemplateClasses.isEmpty()) {
includeTemplateClassNames = NO_INCLUDE_CLASS;
return NO_INCLUDE_CLASS;
}
StringBuilder sb = new StringBuilder();
boolean first = true;
for (TemplateClass tc : includedTemplateClasses) {
if (!first) {
sb.append(",");
} else {
first = false;
}
sb.append(tc.tagName);
}
includeTemplateClassNames = sb.toString();
return sb.toString();
}
/*
* WRITE : includeTagTypes
*/
public void setTagType(String tagName, String type) {
includeTagTypes.put(tagName, type);
}
public boolean returnObject(String tagName) {
String retType = includeTagTypes.get(tagName);
if (null != retType) {
return !"void".equals(retType);
}
if (null != extendedTemplateClass) {
return extendedTemplateClass.returnObject(tagName);
}
return true;
}
public String serializeIncludeTagTypes() {
if (includeTagTypes.isEmpty()) {
return "";
}
StringBuilder sb = new StringBuilder();
boolean empty = true;
for (Map.Entry<String, String> entry : includeTagTypes.entrySet()) {
if (!empty) {
sb.append(";");
} else {
empty = false;
}
sb.append(entry.getKey()).append(":").append(entry.getValue());
}
return sb.toString();
}
/*
* WRITE : includeTagTypes
*/
public void deserializeIncludeTagTypes(String s) {
includeTagTypes = new HashMap<String, String>();
if (S.isEmpty(s)) {
return;
}
String[] sa = s.split(";");
for (String s0 : sa) {
String[] sa0 = s0.split(":");
if (sa0.length != 2) {
throw new IllegalArgumentException("Unknown include tag types string: " + s);
}
includeTagTypes.put(sa0[0], sa0[1]);
}
}
/**
* If not null then this template is a tag
*/
public String getTagName() {
return tagName;
}
/**
* The template source
*/
public String getTemplateSource() {
return getTemplateSource(false);
}
public String getTemplateSource(boolean includeRoot) {
if (null != templateResource) {
return templateResource.asTemplateContent();
}
if (!includeRoot) {
return "";
}
TemplateClass parent = root;
while ((null != parent) && parent.isInner()) {
parent = parent.root;
}
return null == parent ? "" : parent.getTemplateSource();
}
/**
* Is this template resource coming from a literal String or from a loaded resource like file
*/
public boolean isStringTemplate() {
return templateResource instanceof StringTemplateResource;
}
public String getResourceLoaderClass() {
return resourceLoaderClass;
}
private TemplateClass(RythmEngine engine) {
this.engine = null == engine ? null : engine.isSingleton() ? null : engine;
}
/**
* Construct a TemplateClass instance using template source file
*
* @param file the template source file
*/
public TemplateClass(File file, RythmEngine engine) {
this(engine.resourceManager().get(file), engine);
}
/**
* Construct a TemplateClass instance using template source content or file path
*
* @param template
*/
public TemplateClass(String template, RythmEngine engine) {
this(engine.resourceManager().get(template), engine);
}
/**
* Construct a TemplateClass instance using template source content or file path
*
* @param template
*/
public TemplateClass(String template, RythmEngine engine, IDialect dialect) {
this(engine.resourceManager().get(template), engine, dialect);
}
public TemplateClass(ITemplateResource resource, RythmEngine engine) {
this(resource, engine, false);
}
public TemplateClass(ITemplateResource resource, RythmEngine engine, IDialect dialect) {
this(resource, engine, false, dialect);
}
/*
* WRITE : templateResource
*/
public TemplateClass(ITemplateResource resource, RythmEngine engine, boolean noRefresh) {
this(engine);
if (null == resource) {
throw new NullPointerException();
}
//resource.setEngine(engine());
templateResource = resource;
if (!noRefresh) {
refresh(false);
}
}
/*
* WRITE : templateResource
*/
public TemplateClass(ITemplateResource resource, RythmEngine engine, boolean noRefresh, IDialect dialect) {
this(engine);
if (null == resource) {
throw new NullPointerException();
}
//resource.setEngine(engine());
templateResource = resource;
this.dialect = dialect;
if (!noRefresh) {
refresh(false);
}
}
/**
* Return the name or key of the template resource
*
* @return the key
*/
public String getKey() {
return null == templateResource ? name : templateResource.getKey().toString();
}
@SuppressWarnings("unchecked")
private Class<?> loadJavaClass() throws Exception {
if (null == javaSource) {
if (null == javaSource) {
refresh(false);
}
}
RythmEngine engine = engine();
TemplateClassLoader cl = engine.classLoader();
if (null == cl) {
throw new NullPointerException();
}
Class<?> c = cl.loadClass(name, true);
if (null == javaClass) {
javaClass = (Class<ITemplate>) c;
}
return c;
}
private ITemplate templateInstance_(RythmEngine engine) {
if (!isValid) {
return NULL_TEMPLATE;
}
if (null == templateInstance) {
try {
Class<?> clz = loadJavaClass();
TemplateBase tmpl = (TemplateBase) clz.newInstance();
tmpl.__setTemplateClass(this);
engine.registerTemplate(tmpl);
//engine.registerTemplate(getFullName(true), tmpl);
templateInstance = tmpl;
} catch (RythmException e) {
throw e;
} catch (Exception e) {
throw new RuntimeException("Error load template instance for " + getKey(), e);
}
}
if (!engine.isProdMode()) {
engine.registerTemplate(templateInstance);
// check parent class change
Class<?> c = templateInstance.getClass();
Class<?> pc = c.getSuperclass();
if (null != pc && !Modifier.isAbstract(pc.getModifiers())) {
engine.classes().getByClassName(pc.getName());
}
}
return templateInstance;
}
public ITemplate asTemplate(ICodeType type, Locale locale, RythmEngine engine) {
if (null == name || engine.isDevMode()) {
refresh(false);
}
TemplateBase tmpl = (TemplateBase) templateInstance_(engine).__cloneMe(engine(), null);
if (tmpl!=null) {
tmpl.__prepareRender(type, locale, engine);
}
return tmpl;
}
public ITemplate asTemplate(RythmEngine engine) {
return asTemplate(null, null, engine);
}
public ITemplate asTemplate(ITemplate caller, RythmEngine engine) {
TemplateBase tb = (TemplateBase) caller;
TemplateBase tmpl = (TemplateBase) templateInstance_(engine).__cloneMe(engine, caller);
tmpl.__prepareRender(tb.__curCodeType(), tb.__curLocale(), engine);
return tmpl;
}
public boolean refresh() {
return refresh(false);
}
public void buildSourceCode(String includingClassName) {
long start = System.currentTimeMillis();
importPaths = new HashSet<String>();
// Possible bug here?
if (null != codeBuilder) codeBuilder.clear();
codeBuilder = new CodeBuilder(templateResource.asTemplateContent(), name(), tagName, this, engine, dialect);
codeBuilder.includingCName = includingClassName;
codeBuilder.build();
extendedTemplateClass = codeBuilder.getExtendedTemplateClass();
javaSource = codeBuilder.toString();
if (logger.isTraceEnabled()) {
logger.trace("%s ms to generate java source for template: %s", System.currentTimeMillis() - start, getKey());
}
}
public void buildSourceCode() {
long start = System.currentTimeMillis();
importPaths = new HashSet<String>();
// Possible bug here?
if (null != codeBuilder) {
codeBuilder.clear();
}
if (null == dialect) {
codeBuilder = new CodeBuilder(templateResource.asTemplateContent(), name, tagName, this, engine, null);
}
else {
codeBuilder = dialect.createCodeBuilder(templateResource.asTemplateContent(), name, tagName, this, engine);
}
codeBuilder.build();
extendedTemplateClass = codeBuilder.getExtendedTemplateClass();
javaSource = codeBuilder.toString();
engine();
if (RythmEngine.insideSandbox()) {
javaSource = CodeBuilder.preventInfiniteLoop(javaSource);
}
if (logger.isTraceEnabled()) {
logger.trace("%s ms to generate java source for template: %s", System.currentTimeMillis() - start, getKey());
}
}
public void addImportPath(String path) {
if (path == null || path.isEmpty()) {
return;
}
this.importPaths.add(path);
}
public void replaceImportPath(Set<String> paths) {
this.importPaths = paths;
}
/**
* @return true if this class has changes refreshed, otherwise this class has not been changed yet
*/
public synchronized boolean refresh(boolean forceRefresh) {
if (inner) {
return false;
}
final ITemplateResource templateResource = this.templateResource;
RythmEngine engine = engine();
if (!templateResource.isValid()) {
// it is removed?
isValid = false;
engine.classes().remove(this);
return false;
}
ICodeType type = engine.renderSettings.codeType();
if (null == type) {
type = templateResource.codeType(engine());
}
if (null == type || ICodeType.DefImpl.RAW == type) {
type = engine.conf().defaultCodeType();
}
codeType = type;
if (null == name) {
// this is the root level template class
root = this;
name = canonicalClassName(templateResource.getSuggestedClassName()) + CN_SUFFIX;
if (engine.conf().typeInferenceEnabled()) {
name += ParamTypeInferencer.uuid();
}
ITemplateResourceLoader loader = engine().resourceManager().whichLoader(templateResource);
if (null != loader) {
Object k = templateResource.getKey();
tagName = toCanonicalName(k.toString(), loader.getResourceLoaderRoot());
}
//name = templateResource.getSuggestedClassName();
engine.registerTemplateClass(this);
}
if (null == javaSource) {
engine.classCache().loadTemplateClass(this);
if (null != javaSource) {
// try refresh extended template class if there is
Pattern p = Pattern.compile(".*extends\\s+([a-zA-Z0-9_]+)\\s*\\{\\s*\\/\\/<extended_resource_key\\>(.*)\\<\\/extended_resource_key\\>.*", Pattern.DOTALL);
Matcher m = p.matcher(javaSource);
if (m.matches()) {
String extended = m.group(1);
TemplateClassManager tcm = engine().classes();
extendedTemplateClass = tcm.getByClassName(extended);
if (null == extendedTemplateClass) {
String extendedResourceKey = m.group(2);
extendedTemplateClass = tcm.getByTemplate(extendedResourceKey);
if (null == extendedTemplateClass) {
extendedTemplateClass = new TemplateClass(extendedResourceKey, engine());
extendedTemplateClass.refresh();
}
}
engine.addExtendRelationship(extendedTemplateClass, this);
}
}
}
boolean extendedTemplateChanged = false;
if (extendedTemplateClass != null) {
extendedTemplateChanged = extendedTemplateClass.refresh(forceRefresh);
}
boolean includedTemplateChanged = false;
boolean includedTemplateClassesIsEmpty;
includedTemplateClassesIsEmpty = includedTemplateClasses.isEmpty();
if (includedTemplateClassesIsEmpty && !S.isEmpty(includeTemplateClassNames) && !NO_INCLUDE_CLASS.equals(includeTemplateClassNames)) {
// just loaded from persistent store
for (String tcName : includeTemplateClassNames.split(",")) {
if (S.isEmpty(tcName)) {
continue;
}
tcName = tcName.trim();
TemplateTestResult testResult = engine().testTemplate(tcName, this, null);
if (null == testResult) {
logger.warn("Unable to load included template class from name: %s", tcName);
continue;
}
TemplateClass tc = engine().getRegisteredTemplateClass(testResult.getFullName());
if (null == tc) {
logger.warn("Unable to load included template class from name: %s", tcName);
continue;
}
includedTemplateClasses.add(tc);
}
}
for (TemplateClass tc : includedTemplateClasses) {
if (tc.refresh(forceRefresh)) {
includedTemplateChanged = true;
break;
}
}
if (extendedTemplateChanged && !forceRefresh) {
reset();
compiled = false;
engine().restart(new ClassReloadException("extended class changed"));
refresh(forceRefresh);
return true; // pass refresh state to sub template
}
// templateResource.refresh() must be put at first so we make sure resource get refreshed
boolean resourceChanged = templateResource.refresh();
boolean refresh = resourceChanged || forceRefresh || (null == javaSource) || includedTemplateChanged || extendedTemplateChanged;
if (!refresh) {
return false;
}
// now start generate source and compile source to byte code
reset();
buildSourceCode();
engine().classCache().cacheTemplateClassSource(this); // cache source code for debugging purpose
if (!codeBuilder.isRythmTemplate()) {
isValid = false;
engine().classes().remove(this);
return false;
}
isValid = true;
//if (!engine().isProd Mode()) System.err.println(javaSource);
compiled = false;
return true;
}
/**
* Is this class already compiled but not defined ?
*
* @return if the class is compiled but not defined
*/
public boolean isDefinable() {
return compiled && javaClass != null;
}
/**
* Remove all java source/ byte code and cache
*/
public void reset() {
javaByteCode = null;
enhancedByteCode = null;
javaSource = null;
templateInstance = null;
for (TemplateClass tc : embeddedClasses) {
tc.reset();
engine().classes().remove(tc);
}
embeddedClasses.clear();
engine().classCache().deleteCache(this);
engine().invalidate(this);
javaClass = null;
}
@SuppressWarnings("unused")
private String magic() {
return name + magic;
}
/**
* Compile the class from Java source
*
* @return the bytes that comprise the class file
*/
public synchronized byte[] compile() {
long start = System.currentTimeMillis();
try {
if (null != javaByteCode) {
return javaByteCode;
}
if (null == javaSource) {
throw new IllegalStateException("Cannot find java source when compiling " + getKey());
}
engine().classes().compiler.compile(new String[]{name});
if (logger.isTraceEnabled()) {
logger.trace("%sms to compile template: %s", System.currentTimeMillis() - start, getKey());
}
return javaByteCode;
} catch (CompileException.CompilerException e) {
String cn = e.className;
TemplateClass tc = S.isEqual(cn, name) ? this : engine().classes().getByClassName(cn);
if (null == tc) {
tc = this;
}
CompileException ce = new CompileException(engine(), tc, e.javaLineNumber, e.message); // init ce before reset java source to get template line info
javaSource = null; // force parser to regenerate source. This helps to reload after fixing the tag file compilation failure
logger.warn("compile failed for %s at line %d",tc.tagName,e.javaLineNumber);
for (TemplateClass itc:this.includedTemplateClasses) {
logger.info("\tincluded: %s",itc.tagName);
}
throw ce;
} catch (NullPointerException e) {
String clazzName = name;
TemplateClass tc = engine().classes().getByClassName(clazzName);
if (this != tc) {
logger.error("tc is not this");
}
if (!this.equals(tc)) {
logger.error("tc not match this");
}
logger.error("NPE encountered when compiling template class:" + name);
throw e;
} finally {
if (logger.isTraceEnabled()) {
logger.trace("%sms to compile template class %s", System.currentTimeMillis() - start, getKey());
}
}
}
/**
* Used to instruct embedded class byte code needs to be enhanced, but for now
* let's just use the java byte code as the enhanced bytecode
*/
public void delayedEnhance(TemplateClass root) {
enhancedByteCode = javaByteCode;
root.embeddedClasses.add(this);
}
public byte[] enhance() {
if (enhancing) {
throw new IllegalStateException("reenter enhance() call");
}
enhancing = true;
try {
byte[] bytes = enhancedByteCode;
if (null == bytes) {
bytes = javaByteCode;
if (null == bytes) {
bytes = compile();
}
long start = System.currentTimeMillis();
IByteCodeEnhancer en = engine().conf().byteCodeEnhancer();
if (null != en) {
try {
bytes = en.enhance(name, bytes);
} catch (Exception e) {
logger.warn(e, "Error enhancing template class: %s", getKey());
}
if (logger.isTraceEnabled()) {
logger.trace("%sms to enhance template class %s", System.currentTimeMillis() - start, getKey());
}
}
enhancedByteCode = bytes;
engine().classCache().cacheTemplateClass(this);
}
for (TemplateClass embedded : embeddedClasses) {
embedded.enhancedByteCode = null;
embedded.enhance();
}
return bytes;
} finally {
enhancing = false;
}
}
/**
* Unload the class
*/
public void uncompile() {
javaClass = null;
}
public boolean isClass() {
return !name.endsWith("package-info");
}
public String getPackage() {
int dot = name.lastIndexOf('.');
return dot > -1 ? name.substring(0, dot) : "";
}
public void loadCachedByteCode(byte[] code) {
enhancedByteCode = code;
}
/**
* Call back when a class is compiled.
*
* @param code The bytecode.
*/
public void compiled(byte[] code) {
javaByteCode = code;
//enhancedByteCode = code;
compiled = true;
RythmEvents.COMPILED.trigger(engine(), code);
enhance();
//compiled(code, false);
}
@Override
public String toString() {
return "(compiled:" + compiled + ") " + name;
}
@Override
public boolean equals(Object o) {
if (o == this) {
return true;
}
if (o instanceof TemplateClass) {
TemplateClass that = (TemplateClass) o;
return that.getKey().equals(getKey());
}
return false;
}
@Override
public int hashCode() {
return getKey().hashCode();
}
private static String canonicalClassName(String name) {
if (S.empty(name)) {
return "";
}
StringBuilder sb = new StringBuilder();
char[] ca = name.toCharArray();
int len = ca.length;
char c = ca[0];
if (!Character.isJavaIdentifierStart(c)) {
sb.append('_');
}
else {
sb.append(c);
}
for (int i = 1; i < len; ++i) {
c = ca[i];
if (!Character.isJavaIdentifierPart(c)) {
sb.append('_');
}
else {
sb.append(c);
}
}
return sb.toString();
}
/**
* Convert the key to canonical template name
*
* @param key the resource key
* @param root the resource loader root path
* @return the canonical name
*/
private static String toCanonicalName(String key, String root) {
if (key.startsWith("/") || key.startsWith("\\")) {
key = key.substring(1);
}
if (key.startsWith(root)) {
key = key.replace(root, "");
}
if (key.startsWith("/") || key.startsWith("\\")) {
key = key.substring(1);
}
//if (-1 != pos) key = key.substring(0, pos);
key = key.replace('/', '.').replace('\\', '.');
return key;
}
public static TemplateClass createInnerClass(String className, byte[] byteCode, TemplateClass parent) {
TemplateClass tc = new TemplateClass();
tc.name = className;
tc.javaByteCode = byteCode;
//tc.enhancedByteCode = byteCode;
tc.inner = true;
tc.root = parent.root();
return tc;
}
public ITemplateResource getTemplateResource() {
return templateResource;
}
public ICodeType getCodeType() {
return codeType;
}
public Set<String> getImportPaths() {
if (null == importPaths) {
return Collections.emptySet();
}
return Collections.unmodifiableSet(importPaths);
}
public String getJavaSource() {
return javaSource;
}
@Deprecated
public void setJavaPackage(Package javaPackage) {
this.javaPackage = javaPackage;
}
@Deprecated
public void setJavaClass(Class<ITemplate> javaClass) {
this.javaClass = javaClass;
}
public CodeBuilder getCodeBuilder() {
return codeBuilder;
}
public Class<ITemplate> getJavaClass() {
return javaClass;
}
public byte[] getEnhancedByteCode() {
return enhancedByteCode;
}
public byte[] getJavaByteCode() {
return javaByteCode;
}
public int getSigChecksum() {
return sigChecksum;
}
@Deprecated
public void setJavaSource(String javaSource) {
this.javaSource = javaSource;
}
@Deprecated
public void setExtendedTemplateClass(TemplateClass extendedTemplateClass) {
this.extendedTemplateClass = extendedTemplateClass;
}
public void setIncludeTemplateClassNames(String includeTemplateClassNames) {
this.includeTemplateClassNames = includeTemplateClassNames;
}
}