All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.stringtemplate.v4.STGroup Maven / Gradle / Ivy

Go to download

StringTemplate is a java template engine for generating source code, web pages, emails, or any other formatted text output. StringTemplate is particularly good at multi-targeted code generators, multiple site skins, and internationalization/localization. It evolved over years of effort developing jGuru.com. StringTemplate also powers the ANTLR 3 and 4 code generator. Its distinguishing characteristic is that unlike other engines, it strictly enforces model-view separation. Strict separation makes websites and code generators more flexible and maintainable; it also provides an excellent defense against malicious template authors.

There is a newer version: 4.3.4
Show newest version
/*
 * [The "BSD license"]
 *  Copyright (c) 2011 Terence Parr
 *  All rights reserved.
 *
 *  Redistribution and use in source and binary forms, with or without
 *  modification, are permitted provided that the following conditions
 *  are met:
 *  1. Redistributions of source code must retain the above copyright
 *     notice, this list of conditions and the following disclaimer.
 *  2. Redistributions in binary form must reproduce the above copyright
 *     notice, this list of conditions and the following disclaimer in the
 *     documentation and/or other materials provided with the distribution.
 *  3. The name of the author may not be used to endorse or promote products
 *     derived from this software without specific prior written permission.
 *
 *  THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
 *  IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 *  OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
 *  IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
 *  INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
 *  NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 *  DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 *  THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 *  (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
 *  THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package org.stringtemplate.v4;

import org.antlr.runtime.ANTLRFileStream;
import org.antlr.runtime.ANTLRInputStream;
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.compiler.CompiledST;
import org.stringtemplate.v4.compiler.Compiler;
import org.stringtemplate.v4.compiler.FormalArgument;
import org.stringtemplate.v4.compiler.GroupLexer;
import org.stringtemplate.v4.compiler.GroupParser;
import org.stringtemplate.v4.compiler.STException;
import org.stringtemplate.v4.gui.STViz;
import org.stringtemplate.v4.misc.Aggregate;
import org.stringtemplate.v4.misc.AggregateModelAdaptor;
import org.stringtemplate.v4.misc.ErrorManager;
import org.stringtemplate.v4.misc.ErrorType;
import org.stringtemplate.v4.misc.MapModelAdaptor;
import org.stringtemplate.v4.misc.Misc;
import org.stringtemplate.v4.misc.ObjectModelAdaptor;
import org.stringtemplate.v4.misc.STModelAdaptor;
import org.stringtemplate.v4.misc.TypeRegistry;

import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

/** 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";
    }

    private static final boolean[] RESERVED_CHARACTERS = new boolean[127];
    static {
        for (char c = 'a'; c <= 'z'; c++) {
            RESERVED_CHARACTERS[c] = true;
        }

        for (char c = 'A'; c <= 'Z'; c++) {
            RESERVED_CHARACTERS[c] = true;
        }

        for (char c = '0'; c <= '9'; c++) {
            RESERVED_CHARACTERS[c] = true;
        }

        RESERVED_CHARACTERS['@'] = true;
        RESERVED_CHARACTERS['-'] = true;
        RESERVED_CHARACTERS['_'] = true;
        RESERVED_CHARACTERS['['] = true;
        RESERVED_CHARACTERS[']'] = true;
    }

    /** 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 imports = Collections.synchronizedList(new ArrayList());

    protected final List importsToClearOnUnload = Collections.synchronizedList(new ArrayList());

    public char delimiterStartChar = '<'; // Use  by default
    public char delimiterStopChar = '>';

    /** Maps template name to {@link CompiledST} object. This map is synchronized. */
    protected Map templates =
        Collections.synchronizedMap(new LinkedHashMap());

    /** Maps dictionary names to {@link Map} objects representing the dictionaries
     *  defined by the user like {@code typeInitMap ::= ["int":"0"]}.
     */
    protected Map> dictionaries =
        Collections.synchronizedMap(new HashMap>());

    /** A dictionary that allows people to register a renderer for
     *  a particular kind of object for any template evaluated relative to this
     *  group.  For example, a date should be formatted differently depending
     *  on the locale.  You can set {@code Date.class} to an object whose
     *  {@code toString(Object)} method properly formats a {@link Date} attribute
     *  according to locale.  Or you can have a different renderer object
     *  for each locale.
     *  

* Order of addition is recorded and matters. If more than one * renderer works for an object, the first registered has priority.

*

* Renderer associated with type {@code t} works for object {@code o} if

*
     *  t.isAssignableFrom(o.getClass()) // would assignment t = o work?
     *  
* So it works if {@code o} is subclass or implements {@code t}. *

* This structure is synchronized.

*/ protected Map, AttributeRenderer> renderers; /** A dictionary that allows people to register a model adaptor for * a particular kind of object (subclass or implementation). Applies * for any template evaluated relative to this group. *

* ST initializes with model adaptors that know how to pull * properties out of {@link Object}s, {@link Map}s, and {@link ST}s.

*

* The last one you register gets priority; do least to most specific.

*/ protected final Map, ModelAdaptor> adaptors; { TypeRegistry> registry = new TypeRegistry>(); registry.put(Object.class, new ObjectModelAdaptor()); registry.put(ST.class, new STModelAdaptor()); registry.put(Map.class, new MapModelAdaptor()); registry.put(Aggregate.class, new AggregateModelAdaptor()); adaptors = Collections.synchronizedMap(registry); } /** 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; /** * For debugging with {@link STViz}. Records where in code an {@link ST} was * created and where code added attributes. */ 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 || templateToken.getType()==GroupParser.BIGSTRING_NO_NL ) { 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() { } /** * Determines if a specified character may be used as a user-specified delimiter. * * @param c The character * @return {@code true} if the character is reserved by the StringTemplate * language; otherwise, {@code false} if the character may be used as a * delimiter. * * @since 4.0.9 */ public static boolean isReservedCharacter(char c) { return c >= 0 && c < RESERVED_CHARACTERS.length && RESERVED_CHARACTERS[c]; } 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); } public Map rawGetDictionary(String name) { return dictionaries.get(name); } public boolean isDictionary(String name) { return dictionaries.get(name)!=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 a = new ArrayList(); 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 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 CompiledST defineRegion(String enclosingTemplateName, Token regionT, String template, Token templateToken) { String name = regionT.getText(); template = Misc.trimOneStartingNewline(template); template = Misc.trimOneTrailingNewline(template); CompiledST code = compile(getFileName(), enclosingTemplateName, null, template, templateToken); String mangled = getMangledRegionName(enclosingTemplateName, name); if ( lookupTemplate(mangled)==null ) { errMgr.compileTimeError(ErrorType.NO_SUCH_REGION, templateToken, regionT, enclosingTemplateName, name); return new CompiledST(); } code.name = mangled; code.isRegion = true; code.regionDefType = ST.RegionType.EXPLICIT; code.templateDefStartToken = regionT; rawDefineTemplate(mangled, code, regionT); code.defineArgDefaultValueTemplates(this); code.defineImplicitlyDefinedTemplates(this); return code; } public void defineTemplateOrRegion( String fullyQualifiedTemplateName, String regionSurroundingTemplateName, Token templateToken, String template, Token nameToken, List 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; } else { 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 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); } /** The {@code "foo"} of {@code t() ::= "<@foo()>"} is mangled to * {@code "/region__/t__foo"} */ public static String getMangledRegionName(String enclosingTemplateName, String name) { if ( enclosingTemplateName.charAt(0)!='/' ) { enclosingTemplateName = '/'+enclosingTemplateName; } return "/region__"+enclosingTemplateName+"__"+name; } /** 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; } /** Define a map for this group. *

* Not thread safe...do not keep adding these while you reference them.

*/ public void defineDictionary(String name, Map mapping) { dictionaries.put(name, mapping); } /** * Make this group import templates/dictionaries from {@code g}. *

* 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. *

* Templates are loaded on-demand from import dirs. Imported groups are * loaded on-demand when searching for a template.

*

* 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.

*

* On unload imported templates are unloaded and removed from the imports * list.

*

* 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("") ) 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(delimiterStartChar, delimiterStopChar); 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, encoding); 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 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; } /** * Add an adaptor for a kind of object so ST knows how to pull properties * from them. Add adaptors in increasing order of specificity. ST adds * {@link Object}, {@link Map}, {@link ST}, and {@link Aggregate} model * adaptors for you first. Adaptors you add have priority over default * adaptors. *

* If an adaptor for type {@code T} already exists, it is replaced by the * {@code adaptor} argument.

*

* This must invalidate cache entries, so set your adaptors up before * calling {@link ST#render} for efficiency.

*/ public void registerModelAdaptor(Class attributeType, ModelAdaptor adaptor) { if ( attributeType.isPrimitive() ) { throw new IllegalArgumentException("can't register ModelAdaptor for primitive type "+ attributeType.getSimpleName()); } adaptors.put(attributeType, adaptor); } public ModelAdaptor getModelAdaptor(Class attributeType) { //noinspection unchecked return (ModelAdaptor) adaptors.get(attributeType); } /** Register a renderer for all objects of a particular "kind" for all * templates evaluated relative to this group. Use {@code r} to render if * object in question is an instance of {@code attributeType}. Recursively * set renderer into all import groups. */ public void registerRenderer(Class attributeType, AttributeRenderer r) { registerRenderer(attributeType, r, true); } public void registerRenderer(Class attributeType, AttributeRenderer r, boolean recursive) { if ( attributeType.isPrimitive() ) { throw new IllegalArgumentException("can't register renderer for primitive type "+ attributeType.getSimpleName()); } if ( renderers == null ) { renderers = Collections.synchronizedMap(new TypeRegistry>()); } renderers.put(attributeType, r); if ( recursive ) { load(); // make sure imports exist (recursively) for (STGroup g : imports) { g.registerRenderer(attributeType, r, true); } } } /** Get renderer for class {@code T} associated with this group. *

* For non-imported groups and object-to-render of class {@code T}, use renderer * (if any) registered for {@code T}. For imports, any renderer * set on import group is ignored even when using an imported template. * You should set the renderer on the main group * you use (or all to be sure). I look at import groups as * "helpers" that should give me templates and nothing else. If you * have multiple renderers for {@code String}, say, then just make uber combined * renderer with more specific format names.

*/ public AttributeRenderer getAttributeRenderer(Class attributeType) { if ( renderers==null ) { return null; } //noinspection unchecked return (AttributeRenderer) renderers.get(attributeType); } 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 ";"; } 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 = null; ClassLoader cl = Thread.currentThread().getContextClassLoader(); if ( cl!=null ) url = cl.getResource(fileName); if ( url==null ) { cl = this.getClass().getClassLoader(); if ( cl!=null ) 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.template+ Misc.newline); buf.append(">>"+Misc.newline); } return buf.toString(); } public STErrorListener getListener() { return errMgr.listener; } public void setListener(STErrorListener listener) { errMgr = new ErrorManager(listener); } public Set getTemplateNames() { load(); HashSet result = new HashSet(); for (Map.Entry e: templates.entrySet()) { if (e.getValue() != NOT_FOUND_ST) { result.add(e.getKey()); } } return result; } }