package nebula.simpletemplate;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.antlr.runtime.ANTLRFileStream;
import org.antlr.runtime.ANTLRInputStream;
import org.antlr.runtime.ANTLRStringStream;
import org.antlr.runtime.CharStream;
import org.antlr.runtime.CommonToken;
import org.antlr.runtime.CommonTokenStream;
import org.antlr.runtime.RecognitionException;
import org.antlr.runtime.Token;
import org.stringtemplate.v4.STErrorListener;
import org.stringtemplate.v4.misc.ErrorBuffer;
import org.stringtemplate.v4.misc.ErrorManager;
import org.stringtemplate.v4.misc.ErrorType;
import org.stringtemplate.v4.compiler.STException;
import org.stringtemplate.v4.compiler.STLexer;
/**
* A directory or directory tree of {@code .st} template files and/or group
* files. Individual template files contain formal template definitions. In a
* sense, it's like a single group file broken into multiple files, one for each
* template. ST v3 had just the pure template inside, not the template name and
* header. Name inside must match filename (minus suffix).
*/
public class STGroup {
public static final String GROUP_FILE_EXTENSION;
public static final String TEMPLATE_FILE_EXTENSION;
static {
GROUP_FILE_EXTENSION = ".stg";
TEMPLATE_FILE_EXTENSION = ".st";
}
/** When we use key as a value in a dictionary, this is how we signify. */
public static final String DICT_KEY = "key";
public static final String DEFAULT_KEY = "default";
/** The encoding to use for loading files. Defaults to UTF-8. */
public String encoding = "UTF-8";
/**
* Every group can import templates/dictionaries from other groups. The list
* must be synchronized (see {@link STGroup#importTemplates}).
*/
protected final List<STGroup> imports = Collections.synchronizedList(new ArrayList<STGroup>());
protected final List<STGroup> importsToClearOnUnload = Collections.synchronizedList(new ArrayList<STGroup>());
public char delimiterStartChar = '<'; // Use <expr> by default
public char delimiterStopChar = '>';
/**
* Maps template name to {@link CompiledST} object. This map is
* synchronized.
*/
protected Map<String, CompiledST> templates = Collections.synchronizedMap(new LinkedHashMap<String, CompiledST>());
/**
* Used to indicate that the template doesn't exist. Prevents duplicate
* group file loads and unnecessary file checks.
*/
protected static final CompiledST NOT_FOUND_ST = new CompiledST();
public static final ErrorManager DEFAULT_ERR_MGR = new ErrorManager();
/** Watch loading of groups and templates. */
public static boolean verbose = false;
public static boolean trackCreationEvents = false;
/**
* v3 compatibility; used to iterate across {@link Map#values()} instead of
* v4's default {@link Map#keySet()}. But to convert ANTLR templates, it's
* too hard to find without static typing in templates.
*/
public boolean iterateAcrossValues = false;
public static STGroup defaultGroup = new STGroup();
/**
* The {@link ErrorManager} for entire group; all compilations and
* executions. This gets copied to parsers, walkers, and interpreters.
*/
public ErrorManager errMgr = STGroup.DEFAULT_ERR_MGR;
public STGroup() {
}
public STGroup(char delimiterStartChar, char delimiterStopChar) {
this.delimiterStartChar = delimiterStartChar;
this.delimiterStopChar = delimiterStopChar;
}
/**
* The primary means of getting an instance of a template from this group.
* Names must be absolute, fully-qualified names like {@code /a/b}.
*/
public ST getInstanceOf(String name) {
if (name == null) return null;
if (verbose) System.out.println(getName() + ".getInstanceOf(" + name + ")");
if (name.charAt(0) != '/') name = "/" + name;
CompiledST c = lookupTemplate(name);
if (c != null) {
return createStringTemplate(c);
}
return null;
}
// protected ST getEmbeddedInstanceOf(Interpreter interp, InstanceScope
// scope, String name) {
// String fullyQualifiedName = name;
// if (name.charAt(0) != '/') {
// fullyQualifiedName = scope.st.impl.prefix + name;
// }
// if (verbose) System.out.println("getEmbeddedInstanceOf(" +
// fullyQualifiedName + ")");
// ST st = getInstanceOf(fullyQualifiedName);
// if (st == null) {
// errMgr.runTimeError(interp, scope, ErrorType.NO_SUCH_TEMPLATE,
// fullyQualifiedName);
// return createStringTemplateInternally(new CompiledST());
// }
// // this is only called internally. wack any debug ST create events
// if (trackCreationEvents) {
// st.debugState.newSTEvent = null; // toss it out
// }
// return st;
// }
/** Create singleton template for use with dictionary values. */
public ST createSingleton(Token templateToken) {
String template;
if (templateToken.getType() == GroupParser.BIGSTRING) {
template = Misc.strip(templateToken.getText(), 2);
} else {
template = Misc.strip(templateToken.getText(), 1);
}
CompiledST impl = compile(getFileName(), null, null, template, templateToken);
ST st = createStringTemplateInternally(impl);
st.groupThatCreatedThisInstance = this;
st.impl.hasFormalArgs = false;
st.impl.name = ST.UNKNOWN_NAME;
// st.impl.defineImplicitlyDefinedTemplates(this);
return st;
}
/**
* Is this template defined in this group or from this group below? Names
* must be absolute, fully-qualified names like {@code /a/b}.
*/
public boolean isDefined(String name) {
return lookupTemplate(name) != null;
}
/** Look up a fully-qualified name. */
public CompiledST lookupTemplate(String name) {
if (name.charAt(0) != '/') name = "/" + name;
if (verbose) System.out.println(getName() + ".lookupTemplate(" + name + ")");
CompiledST code = rawGetTemplate(name);
if (code == NOT_FOUND_ST) {
if (verbose) System.out.println(name + " previously seen as not found");
return null;
}
// try to load from disk and look up again
if (code == null) code = load(name);
if (code == null) code = lookupImportedTemplate(name);
if (code == null) {
if (verbose) System.out.println(name + " recorded not found");
templates.put(name, NOT_FOUND_ST);
}
if (verbose) if (code != null) System.out.println(getName() + ".lookupTemplate(" + name + ") found");
return code;
}
/**
* Unload all templates, dictionaries and import relationships, but leave
* renderers and adaptors. This essentially forces the next call to
* {@link #getInstanceOf} to reload templates. Call {@code unload()} on each
* group in the {@link #imports} list, and remove all elements in
* {@link #importsToClearOnUnload} from {@link #imports}.
*/
public synchronized void unload() {
templates.clear();
// dictionaries.clear();
for (STGroup imp : imports) {
imp.unload();
}
for (STGroup imp : importsToClearOnUnload) {
imports.remove(imp);
}
importsToClearOnUnload.clear();
}
/**
* Load st from disk if directory or load whole group file if .stg file
* (then return just one template). {@code name} is fully-qualified.
*/
protected CompiledST load(String name) {
return null;
}
/** Force a load if it makes sense for the group. */
public void load() {
}
protected CompiledST lookupImportedTemplate(String name) {
if (imports.size() == 0) return null;
for (STGroup g : imports) {
if (verbose) System.out.println("checking " + g.getName() + " for imported " + name);
CompiledST code = g.lookupTemplate(name);
if (code != null) {
if (verbose) System.out.println(g.getName() + ".lookupImportedTemplate(" + name + ") found");
return code;
}
}
if (verbose) System.out.println(name + " not found in " + getName() + " imports");
return null;
}
public CompiledST rawGetTemplate(String name) {
return templates.get(name);
}
// /** for testing */
// public CompiledST defineTemplate(String templateName, String template) {
// if (templateName.charAt(0) != '/') templateName = "/" + templateName;
// try {
// CompiledST impl = defineTemplate(templateName, new
// CommonToken(GroupParser.ID, templateName), null,
// template, null);
// return impl;
// } catch (STException se) {
// // we have reported the error; the exception just blasts us
// // out of parsing this template
// }
// return null;
// }
//
// /** for testing */
// public CompiledST defineTemplate(String name, String argsS, String
// template) {
// if (name.charAt(0) != '/') name = "/" + name;
// String[] args = argsS.split(",");
// List<FormalArgument> a = new ArrayList<FormalArgument>();
// for (String arg : args) {
// a.add(new FormalArgument(arg));
// }
// return defineTemplate(name, new CommonToken(GroupParser.ID, name), a,
// template, null);
// }
/** for testing */
public CompiledST defineTemplate(String templateName, String template) {
if (templateName.charAt(0) != '/') templateName = "/" + templateName;
try {
CompiledST impl = defineTemplate(templateName, new CommonToken(GroupParser.ID, templateName), null, template, null);
return impl;
} catch (STException se) {
// we have reported the error; the exception just blasts us
// out of parsing this template
}
return null;
}
/** for testing */
public CompiledST defineTemplate(String name, String argsS, String template) {
if (name.charAt(0) != '/') name = "/" + name;
String[] args = argsS.split(",");
List<FormalArgument> a = new ArrayList<FormalArgument>();
for (String arg : args) {
a.add(new FormalArgument(arg));
}
return defineTemplate(name, new CommonToken(GroupParser.ID, name), a, template, null);
}
public CompiledST defineTemplate(String fullyQualifiedTemplateName, Token nameT, List<FormalArgument> args, String template, Token templateToken) {
if (verbose) System.out.println("defineTemplate(" + fullyQualifiedTemplateName + ")");
if (fullyQualifiedTemplateName == null || fullyQualifiedTemplateName.length() == 0) {
throw new IllegalArgumentException("empty template name");
}
if (fullyQualifiedTemplateName.indexOf('.') >= 0) {
throw new IllegalArgumentException("cannot have '.' in template names");
}
template = Misc.trimOneStartingNewline(template);
template = Misc.trimOneTrailingNewline(template);
// compile, passing in templateName as enclosing name for any embedded
// regions
CompiledST code = compile(getFileName(), fullyQualifiedTemplateName, args, template, templateToken);
code.name = fullyQualifiedTemplateName;
rawDefineTemplate(fullyQualifiedTemplateName, code, nameT);
// code.defineArgDefaultValueTemplates(this);
// code.defineImplicitlyDefinedTemplates(this); // define any anonymous
// // subtemplates
return code;
}
/** Make name and alias for target. Replace any previous definition of name. */
public CompiledST defineTemplateAlias(Token aliasT, Token targetT) {
String alias = aliasT.getText();
String target = targetT.getText();
CompiledST targetCode = rawGetTemplate("/" + target);
if (targetCode == null) {
errMgr.compileTimeError(ErrorType.ALIAS_TARGET_UNDEFINED, null, aliasT, alias, target);
return null;
}
rawDefineTemplate("/" + alias, targetCode, aliasT);
return targetCode;
}
public void defineTemplateOrRegion(String fullyQualifiedTemplateName, String regionSurroundingTemplateName, Token templateToken, String template,
Token nameToken, List<FormalArgument> args) {
try {
if (regionSurroundingTemplateName != null) {
// defineRegion(regionSurroundingTemplateName, nameToken,
// template, templateToken);
} else {
defineTemplate(fullyQualifiedTemplateName, nameToken, args, template, templateToken);
}
} catch (STException e) {
// after getting syntax error in a template, we emit msg
// and throw exception to blast all the way out to here.
}
}
public void rawDefineTemplate(String name, CompiledST code, Token defT) {
// CompiledST prev = rawGetTemplate(name);
// if (prev != null) {
// if (!prev.isRegion) {
// errMgr.compileTimeError(ErrorType.TEMPLATE_REDEFINITION, null, defT);
// return;
// }
// if (prev.isRegion) {
// if (code.regionDefType != ST.RegionType.IMPLICIT &&
// prev.regionDefType == ST.RegionType.EMBEDDED) {
// errMgr.compileTimeError(ErrorType.EMBEDDED_REGION_REDEFINITION, null,
// defT,
// getUnMangledTemplateName(name));
// return;
// } else if (code.regionDefType == ST.RegionType.IMPLICIT ||
// prev.regionDefType == ST.RegionType.EXPLICIT) {
// errMgr.compileTimeError(ErrorType.REGION_REDEFINITION, null, defT,
// getUnMangledTemplateName(name));
// return;
// }
// }
// }
code.nativeGroup = this;
// code.templateDefStartToken = defT;
templates.put(name, code);
}
public void undefineTemplate(String name) {
templates.remove(name);
}
/** Compile a template. */
public CompiledST compile(String srcName, String name, List<FormalArgument> args, String template, Token templateToken) // for
// error
// location
{
// System.out.println("STGroup.compile: "+enclosingTemplateName);
// Compiler c = new Compiler(this);
// return c.compile(srcName, name, args, template, templateToken);
return parse(srcName, name, args, template, templateToken);
}
/** Return {@code "t.foo"} from {@code "/region__/t__foo"} */
public static String getUnMangledTemplateName(String mangledName) {
String t = mangledName.substring("/region__".length(), mangledName.lastIndexOf("__"));
String r = mangledName.substring(mangledName.lastIndexOf("__") + 2, mangledName.length());
return t + '.' + r;
}
/**
* Make this group import templates/dictionaries from {@code g}.
* <p/>
* On unload imported templates are unloaded but stay in the
* {@link #imports} list.
*/
public void importTemplates(STGroup g) {
importTemplates(g, false);
}
/**
* Import template files, directories, and group files. Priority is given to
* templates defined in the current group; this, in effect, provides
* inheritance. Polymorphism is in effect so that if an inherited template
* references template {@code t()} then we search for {@code t()} in the
* subgroup first.
* <p/>
* Templates are loaded on-demand from import dirs. Imported groups are
* loaded on-demand when searching for a template.
* <p/>
* The listener of this group is passed to the import group so errors found
* while loading imported element are sent to listener of this group.
* <p/>
* On unload imported templates are unloaded and removed from the imports
* list.
* <p/>
* This method is called when processing import statements specified in
* group files. Use {@link #importTemplates(STGroup)} to import templates
* 'programmatically'.
*/
public void importTemplates(Token fileNameToken) {
if (verbose) System.out.println("importTemplates(" + fileNameToken.getText() + ")");
String fileName = fileNameToken.getText();
// do nothing upon syntax error
if (fileName == null || fileName.equals("<missing STRING>")) return;
fileName = Misc.strip(fileName, 1);
// System.out.println("import "+fileName);
boolean isGroupFile = fileName.endsWith(GROUP_FILE_EXTENSION);
boolean isTemplateFile = fileName.endsWith(TEMPLATE_FILE_EXTENSION);
boolean isGroupDir = !(isGroupFile || isTemplateFile);
STGroup g = null;
// search path is: working dir, g.stg's dir, CLASSPATH
URL thisRoot = getRootDirURL();
URL fileUnderRoot;
// System.out.println("thisRoot="+thisRoot);
try {
fileUnderRoot = new URL(thisRoot + "/" + fileName);
} catch (MalformedURLException mfe) {
errMgr.internalError(null, "can't build URL for " + thisRoot + "/" + fileName, mfe);
return;
}
if (isTemplateFile) {
g = new STGroup();
// g.setListener(this.getListener());
URL fileURL;
if (Misc.urlExists(fileUnderRoot)) fileURL = fileUnderRoot;
else fileURL = getURL(fileName); // try CLASSPATH
if (fileURL != null) {
try {
InputStream s = fileURL.openStream();
ANTLRInputStream templateStream = new ANTLRInputStream(s);
templateStream.name = fileName;
CompiledST code = g.loadTemplateFile("/", fileName, templateStream);
if (code == null) g = null;
} catch (IOException ioe) {
errMgr.internalError(null, "can't read from " + fileURL, ioe);
g = null;
}
} else {
g = null;
}
} else if (isGroupFile) {
// System.out.println("look for fileUnderRoot: "+fileUnderRoot);
if (Misc.urlExists(fileUnderRoot)) {
g = new STGroupFile(fileUnderRoot, encoding, delimiterStartChar, delimiterStopChar);
// g.setListener(this.getListener());
} else {
g = new STGroupFile(fileName, delimiterStartChar, delimiterStopChar);
// g.setListener(this.getListener());
}
} else if (isGroupDir) {
// System.out.println("try dir "+fileUnderRoot);
if (Misc.urlExists(fileUnderRoot)) {
g = new STGroupDir(fileUnderRoot, encoding, delimiterStartChar, delimiterStopChar);
// g.setListener(this.getListener());
} else {
// try in CLASSPATH
// System.out.println("try dir in CLASSPATH "+fileName);
g = new STGroupDir(fileName, delimiterStartChar, delimiterStopChar);
// g.setListener(this.getListener());
}
}
if (g == null) {
errMgr.compileTimeError(ErrorType.CANT_IMPORT, null, fileNameToken, fileName);
} else {
importTemplates(g, true);
}
}
protected void importTemplates(STGroup g, boolean clearOnUnload) {
if (g == null) return;
imports.add(g);
if (clearOnUnload) {
importsToClearOnUnload.add(g);
}
}
public List<STGroup> getImportedGroups() {
return imports;
}
/**
* Load a group file with full path {@code fileName}; it's relative to root
* by {@code prefix}.
*/
public void loadGroupFile(String prefix, String fileName) {
if (verbose) System.out.println(this.getClass().getSimpleName() + ".loadGroupFile(group-file-prefix=" + prefix + ", fileName=" + fileName + ")");
GroupParser parser;
try {
URL f = new URL(fileName);
ANTLRInputStream fs = new ANTLRInputStream(f.openStream(), encoding);
GroupLexer lexer = new GroupLexer(fs);
fs.name = fileName;
CommonTokenStream tokens = new CommonTokenStream(lexer);
parser = new GroupParser(tokens);
parser.group(this, prefix);
} catch (Exception e) {
errMgr.IOError(null, ErrorType.CANT_LOAD_GROUP_FILE, e, fileName);
}
}
/** Load template file into this group using absolute {@code fileName}. */
public CompiledST loadAbsoluteTemplateFile(String fileName) {
ANTLRFileStream fs;
try {
fs = new ANTLRFileStream(fileName, encoding);
fs.name = fileName;
} catch (IOException ioe) {
// doesn't exist
// errMgr.IOError(null, ErrorType.NO_SUCH_TEMPLATE, ioe, fileName);
return null;
}
return loadTemplateFile("", fileName, fs);
}
/**
* Load template stream into this group. {@code unqualifiedFileName} is
* {@code "a.st"}. The {@code prefix} is path from group root to
* {@code unqualifiedFileName} like {@code "/subdir"} if file is in
* {@code /subdir/a.st}.
*/
public CompiledST loadTemplateFile(String prefix, String unqualifiedFileName, CharStream templateStream) {
GroupLexer lexer = new GroupLexer(templateStream);
CommonTokenStream tokens = new CommonTokenStream(lexer);
GroupParser parser = new GroupParser(tokens);
parser.group = this;
lexer.group = this;
try {
parser.templateDef(prefix);
} catch (RecognitionException re) {
errMgr.groupSyntaxError(ErrorType.SYNTAX_ERROR, unqualifiedFileName, re, re.getMessage());
}
String templateName = Misc.getFileNameNoSuffix(unqualifiedFileName);
if (prefix != null && prefix.length() > 0) templateName = prefix + templateName;
CompiledST impl = rawGetTemplate(templateName);
impl.prefix = prefix;
return impl;
}
public ST createStringTemplate(CompiledST impl) {
ST st = new ST();
st.impl = impl;
st.groupThatCreatedThisInstance = this;
if (impl.formalArguments != null) {
// st.locals = new Object[impl.formalArguments.size()];
// Arrays.fill(st.locals, ST.EMPTY_ATTR);
}
return st;
}
/**
* Differentiate so we can avoid having creation events for regions, map
* operations, and other implicit "new ST" events during rendering.
*/
public ST createStringTemplateInternally(CompiledST impl) {
ST st = createStringTemplate(impl);
// if (trackCreationEvents && st.debugState != null) {
// st.debugState.newSTEvent = null; // toss it out
// }
return st;
}
public ST createStringTemplateInternally(ST proto) {
return new ST(proto); // no need to wack debugState; not set in
// ST(proto).
}
public String getName() {
return "<no name>;";
}
public String getFileName() {
return null;
}
/**
* Return root dir if this is group dir; return dir containing group file if
* this is group file. This is derived from original incoming dir or
* filename. If it was absolute, this should come back as full absolute
* path. If only a URL is available, return URL of one dir up.
*/
public URL getRootDirURL() {
return null;
}
public URL getURL(String fileName) {
URL url;
ClassLoader cl = Thread.currentThread().getContextClassLoader();
url = cl.getResource(fileName);
if (url == null) {
cl = this.getClass().getClassLoader();
url = cl.getResource(fileName);
}
return url;
}
@Override
public String toString() {
return getName();
}
public String show() {
StringBuilder buf = new StringBuilder();
if (imports.size() != 0) buf.append(" : " + imports);
for (String name : templates.keySet()) {
CompiledST c = rawGetTemplate(name);
if (c.isAnonSubtemplate || c == NOT_FOUND_ST) continue;
int slash = name.lastIndexOf('/');
name = name.substring(slash + 1, name.length());
buf.append(name);
buf.append('(');
// if (c.formalArguments != null)
// buf.append(Misc.join(c.formalArguments.values().iterator(),
// ","));
buf.append(')');
buf.append(" ::= <<" + Misc.newline);
buf.append(c.code + Misc.newline);
buf.append(">>" + Misc.newline);
}
return buf.toString();
}
public Set<String> getTemplateNames() {
load();
HashSet<String> result = new HashSet<String>();
for (Map.Entry<String, CompiledST> e : templates.entrySet()) {
if (e.getValue() != NOT_FOUND_ST) {
result.add(e.getKey());
}
}
return result;
}
/*
* public CompiledST parse(String name, String filepath) { try { STLexer
* lexer = new STLexer(org.stringtemplate.v4.STGroup.DEFAULT_ERR_MGR, new
* ANTLRFileStream(filepath), null, delimiterStartChar, delimiterStopChar);
* CommonTokenStream tokens = new CommonTokenStream(lexer); SParser p = new
* SParser(tokens, this); return p.templateAndEOF(); } catch
* (RecognitionException e) { throw new RuntimeException(e); } catch
* (IOException e) { throw new RuntimeException(e); } }
*/
public CompiledST parse(String srcName, String name, List<FormalArgument> args, String template, Token templateToken) // for
// error
// location)
{
return this.parse(srcName, name, args, template, templateToken, delimiterStartChar, delimiterStopChar);
}
public CompiledST parse(String srcName, String name, List<FormalArgument> args, String template, Token templateToken, char delimiterStartChar,
char delimiterStopChar) {
try {
STLexer lexer = new STLexer(STGroup.DEFAULT_ERR_MGR, new ANTLRStringStream(template), null, delimiterStartChar, delimiterStopChar);
CommonTokenStream tokens = new CommonTokenStream(lexer);
SParser p = new SParser(tokens, this);
if (args != null && args.size() > 0) {
for (int i = 0; i < args.size(); i++) {
FormalArgument arg = args.get(i);
p.arg(arg.name);
}
}
return p.templateAndEOF();
} catch (RecognitionException e) {
throw new RuntimeException(e);
}
}
/*
* protected void parseGroupFile(String filename) { try { GroupLexer lexer =
* new GroupLexer(new ANTLRFileStream(filename)); CommonTokenStream tokens =
* new CommonTokenStream(lexer); GroupParser p = new GroupParser(tokens);
* p.group(this, ""); } catch (RecognitionException e) { throw new
* RuntimeException(e); } catch (IOException e) { throw new
* RuntimeException(e); } }
*
* protected void parseGroupFile(URL fileUnderRoot) { try { GroupLexer lexer
* = new GroupLexer(new ANTLRInputStream(fileUnderRoot.openStream()));
* CommonTokenStream tokens = new CommonTokenStream(lexer); GroupParser p =
* new GroupParser(tokens); p.group(this, ""); } catch (RecognitionException
* e) { throw new RuntimeException(e); } catch (IOException e) { throw new
* RuntimeException(e); } }
*
* protected void parseGroupString(String template) { try { GroupLexer lexer
* = new GroupLexer(new ANTLRStringStream(template)); CommonTokenStream
* tokens = new CommonTokenStream(lexer); GroupParser p = new
* GroupParser(tokens); p.group(this, ""); } catch (RecognitionException e)
* { throw new RuntimeException(e); } }
*/
public void setListener(ErrorBuffer errors) {
// TODO Auto-generated method stub
}
public void setListener(STErrorListener errors) {
// TODO Auto-generated method stub
}
}