package org.bndtools.templating.engine.st; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.PrintWriter; import java.io.Reader; import java.io.StringReader; import java.io.StringWriter; import java.io.UnsupportedEncodingException; import java.util.Enumeration; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Map.Entry; import java.util.Properties; import org.bndtools.templating.Resource; import org.bndtools.templating.ResourceMap; import org.bndtools.templating.ResourceType; import org.bndtools.templating.StringResource; import org.bndtools.templating.TemplateEngine; import org.eclipse.core.runtime.IProgressMonitor; import org.osgi.service.component.annotations.Component; import org.stringtemplate.v4.AutoIndentWriter; import org.stringtemplate.v4.InstanceScope; import org.stringtemplate.v4.Interpreter; import org.stringtemplate.v4.ST; import org.stringtemplate.v4.STGroup; import org.stringtemplate.v4.compiler.CompiledST; import org.stringtemplate.v4.compiler.Compiler; import org.stringtemplate.v4.compiler.STLexer; import org.stringtemplate.v4.misc.ErrorBuffer; import aQute.bnd.osgi.Constants; import aQute.bnd.osgi.Instructions; import st4hidden.org.antlr.runtime.ANTLRInputStream; import st4hidden.org.antlr.runtime.CommonToken; @Component(name = "org.bndtools.templating.engine.st", property = { "name=stringtemplate", "version=4.0.8" }) public class StringTemplateEngine implements TemplateEngine { private static final String TEMPLATE_PROPERTIES = "_template.properties"; private static final String TEMPLATE_DEFS_PREFIX = "_defs/"; private static final String TEMPLATE_FILE_SUFFIX = ".st"; private static class TemplateSettings { char leftDelim = '$'; char rightDelim = '$'; Instructions preprocessMatch = new Instructions("*"); Instructions ignore = null; private TemplateSettings() {} static TemplateSettings readFrom(Properties props) { TemplateSettings settings = new TemplateSettings(); if (props != null) { settings.leftDelim = readSingleChar(props, "leftDelim", settings.leftDelim); settings.rightDelim = readSingleChar(props, "rightDelim", settings.rightDelim); String process = props.getProperty("process", Constants.DEFAULT_PREPROCESSS_MATCHERS); String processBefore = props.getProperty("process.before", null); if (processBefore != null) process = processBefore + ", " + process; String processAfter = props.getProperty("process.after", null); if (processAfter != null) process = process + ", " + processAfter; settings.preprocessMatch = new Instructions(process); String ignore = props.getProperty("ignore", null); settings.ignore = ignore != null ? new Instructions(ignore) : null; } return settings; } private static char readSingleChar(Properties props, String propName, char dflt) { String str = props.getProperty(propName, String.valueOf(dflt)).trim(); if (str.length() != 1) throw new IllegalArgumentException("Setting value for " + propName + "must be a single character"); return str.charAt(0); } } @Override public Map<String,String> getTemplateParameters(ResourceMap inputs, IProgressMonitor monitor) throws Exception { Map<String,String> params = new HashMap<>(); // Initialise the engine TemplateSettings settings = readSettings(inputs); STGroup stg = new STGroup(settings.leftDelim, settings.rightDelim); // Assemble a mapping properties file of outputPath=sourcePath String mappingTemplate = loadMappingTemplate(inputs, settings, stg); extractAttrs(compile(stg, "_mapping", new StringResource(mappingTemplate)), params); // Iterate the entries Properties contentProps = new Properties(); contentProps.load(new StringReader(mappingTemplate)); @SuppressWarnings("unchecked") Enumeration<String> contentEnum = (Enumeration<String>) contentProps.propertyNames(); while (contentEnum.hasMoreElements()) { String outputPath = contentEnum.nextElement().trim(); String sourcePath = contentProps.getProperty(outputPath); Resource source = inputs.get(sourcePath); if (source == null) throw new RuntimeException(String.format("Internal error in template engine: could not find input resource '%s'", sourcePath)); if (settings.ignore == null || !settings.ignore.matches(sourcePath)) { if (source.getType() == ResourceType.File) { if (settings.preprocessMatch.matches(sourcePath)) { extractAttrs(compile(stg, sourcePath, source), params); } } } } return params; } @Override public ResourceMap generateOutputs(ResourceMap inputs, Map<String,List<Object>> parameters, IProgressMonitor monitor) throws Exception { TemplateSettings settings = readSettings(inputs); STGroup stg = new STGroup(settings.leftDelim, settings.rightDelim); // Assemble a mapping properties file of outputPath=sourcePath String mappingTemplate = loadMappingTemplate(inputs, settings, stg); String renderedMapping = render(compile(stg, "_mapping", new StringResource(mappingTemplate)), parameters); Properties contentProps = new Properties(); contentProps.load(new StringReader(renderedMapping)); // Iterate the content entries ResourceMap outputs = new ResourceMap(); @SuppressWarnings("unchecked") Enumeration<String> contentEnum = (Enumeration<String>) contentProps.propertyNames(); while (contentEnum.hasMoreElements()) { String outputName = contentEnum.nextElement().trim(); String sourceName = contentProps.getProperty(outputName, "").trim(); Resource source = inputs.get(sourceName); if (source == null) throw new RuntimeException(String.format("Internal error in template engine: could not find input resource '%s'", sourceName)); Resource output; if (settings.ignore == null || !settings.ignore.matches(sourceName)) { if (source.getType() == ResourceType.Folder) { output = source; } else if (settings.preprocessMatch.matches(sourceName)) { // This file is a candidate for preprocessing with ST String rendered = render(compile(stg, sourceName, source), parameters); output = new StringResource(rendered); } else { // This file should be directly copied output = source; } outputs.put(outputName, output); } } return outputs; } private String loadMappingTemplate(ResourceMap inputs, TemplateSettings settings, STGroup stg) throws IOException { StringWriter buf = new StringWriter(); try (PrintWriter bufPrint = new PrintWriter(buf)) { for (String inputPath : inputs.getPaths()) { if (inputPath.startsWith(TEMPLATE_DEFS_PREFIX)) { if (inputPath.endsWith(TEMPLATE_FILE_SUFFIX)) { // Definition... load into StringTemplate group and don't generate output String inputPathRelative = inputPath.substring(TEMPLATE_DEFS_PREFIX.length()); Resource resource = inputs.get(inputPath); if (resource != null && resource.getType() == ResourceType.File) loadTemplate(stg, inputPathRelative, resource.getContent(), resource.getTextEncoding()); } } else { // Mapping to output file String outputPath = inputPath; String escapedSourcePath = escapeDelimiters(inputPath, settings); bufPrint.printf("%s=%s%n", outputPath, escapedSourcePath); } } } String mappingTemplate = buf.toString(); return mappingTemplate; } private TemplateSettings readSettings(ResourceMap inputs) throws IOException, UnsupportedEncodingException { Properties settingsProp = new Properties(); Resource settingsResource = inputs.remove(TEMPLATE_PROPERTIES); if (settingsResource != null) { if (settingsResource.getType() != ResourceType.File) throw new IllegalArgumentException(String.format("Template settings resource %s must be a file; found resource type %s.", TEMPLATE_PROPERTIES, settingsResource.getType())); try (Reader reader = new InputStreamReader(settingsResource.getContent(), settingsResource.getTextEncoding())) { settingsProp.load(reader); } } TemplateSettings settings = TemplateSettings.readFrom(settingsProp); return settings; } private void loadTemplate(STGroup stg, String fileName, InputStream is, String encoding) throws IOException { ANTLRInputStream charStream = new ANTLRInputStream(is, encoding); charStream.name = fileName; try { stg.loadTemplateFile("/", fileName, charStream); } catch (NullPointerException e) { throw new IOException(String.format("Error loading template file: %s. Ensure the file contains a template definition matching the file name.", fileName), e); } } private CompiledST loadRawTemplate(STGroup stg, String name, Resource resource) throws IOException { if (resource.getType() != ResourceType.File) throw new IllegalArgumentException(String.format("Cannot build resource from resource of type %s (name='%s').", resource.getType(), name)); try (InputStream is = resource.getContent()) { ANTLRInputStream templateStream = new ANTLRInputStream(is, resource.getTextEncoding()); String template = templateStream.substring(0, templateStream.size() - 1); CompiledST impl = new Compiler(stg).compile(name, template); CommonToken nameT = new CommonToken(STLexer.SEMI); nameT.setInputStream(templateStream); stg.rawDefineTemplate("/" + name, impl, nameT); impl.defineImplicitlyDefinedTemplates(stg); return impl; } } private void extractAttrs(ST st, final Map<String,String> attrs) throws Exception { Interpreter interpreter = new Interpreter(st.groupThatCreatedThisInstance, Locale.getDefault(), true) { @Override public Object getAttribute(InstanceScope scope, String name) { attrs.put(name, null); return "X"; } }; StringWriter writer = new StringWriter(); interpreter.exec(new AutoIndentWriter(writer), new InstanceScope(null, st)); } private String render(ST st, Map<String,List<Object>> params) throws Exception { for (Entry<String,List<Object>> entry : params.entrySet()) { for (Object value : entry.getValue()) { st.add(entry.getKey(), value); } } return st.render(); } private ST compile(STGroup group, String name, Resource resource) throws Exception { ErrorBuffer errors = new ErrorBuffer(); group.setListener(errors); ST st; try { loadRawTemplate(group, name, resource); st = group.getInstanceOf(name); } catch (Exception e) { // Wrap the ST exception, which gives us no detail in its message throw new IllegalArgumentException(String.format("Failed to compile template '%s': %s", name, errors.toString())); } if (st == null) throw new Exception("Template name not loaded: " + name); return st; } static String escapeDelimiters(String string, TemplateSettings settings) { StringBuilder builder = new StringBuilder(); for (int i = 0; i < string.length(); i++) { char c = string.charAt(i); if (settings.leftDelim == c || settings.rightDelim == c) { builder.append('\\'); } builder.append(c); } return builder.toString(); } }