/* * ****************************************************************************** * MontiCore Language Workbench * Copyright (c) 2015, MontiCore, All rights reserved. * * This project is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3.0 of the License, or (at your option) any later version. * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this project. If not, see <http://www.gnu.org/licenses/>. * ****************************************************************************** */ package de.monticore.generating.templateengine; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Strings.isNullOrEmpty; import static com.google.common.collect.Lists.newArrayList; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; import java.util.List; import java.util.Map.Entry; import java.util.Set; import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; import de.monticore.ast.ASTNode; import de.monticore.generating.templateengine.freemarker.FreeMarkerTemplateEngine; import de.monticore.generating.templateengine.freemarker.SimpleHashFactory; import de.monticore.generating.templateengine.reporting.Reporting; import de.monticore.io.FileReaderWriter; import de.se_rwth.commons.Names; import de.se_rwth.commons.logging.Log; import freemarker.core.Macro; import freemarker.ext.beans.BeansWrapper; import freemarker.template.SimpleHash; import freemarker.template.Template; import freemarker.template.TemplateModelException; // TODO: describe methods better for JavaDoc /** * Provides methods for manipulating the content of templates, mainly for * calling and including of templates. * * @author (last commit) $Author$ * @version $Revision$, $Date$ */ // TODO PN check if all method docs are up to date. public class TemplateController { private final TemplateControllerConfiguration config; private final GlobalExtensionManagement glex; private final FileReaderWriter fileHandler; private final FreeMarkerTemplateEngine freeMarkerTemplateEngine; private ITemplateControllerFactory tcFactory; private final String currPackage; private String templatename; private boolean isIncludeRunning = false; boolean signatureInitialized = false; private List<String> signature = newArrayList(); private List<Object> arguments = newArrayList(); public static final String DEFAULT_FILE_EXTENSION = "java"; /** * A list of all freemarker functions that serve as aliases for Java methods, * e.g. 'include' as alias for 'tc.include' */ private List<Macro> aliases; private SimpleHash data = SimpleHashFactory.getInstance().createSimpleHash(); protected TemplateController(TemplateControllerConfiguration tcConfig, String templatename) { this.config = tcConfig; this.glex = tcConfig.getGlEx(); this.fileHandler = tcConfig.getFileHandler(); this.freeMarkerTemplateEngine = tcConfig.getFreeMarkerTemplateEngine(); this.templatename = templatename; this.currPackage = Names.getQualifier(templatename); this.tcFactory = tcConfig.getTemplateControllerFactory(); // TODO PN blocker // GenLogger.updateTemplateOperator(this); } public String getTemplatename() { return templatename; } protected void setTemplatename(String templatename) { this.templatename = templatename; } /** * @return isIncludeRunning */ protected boolean isIncludeRunning() { return this.isIncludeRunning; } /** * @param isIncludeRunning the isIncludeRunning to set */ protected void setIncludeRunning(boolean isIncludeRunning) { this.isIncludeRunning = isIncludeRunning; } /** * Execute each of the templates on each ASTNode of the list. Concatenate the * results together in one big String and include that into the currently * processed output. We iterate on the templates and ASTNodes. In case order * is important: The iteration goes like this for ( templates ) { for ( * ASTNodes ) {...} } Template filename may be qualified (using "."). When it * is not qualified, the filename is taken from the current package (same as * the calling template). * * @param templatenames list of filenames, qualified or not * @param astlist where we execute the template on in an iteration * @return produced output */ public String include(List<String> templatenames, List<ASTNode> astlist) { setIncludeRunning(true); StringBuilder ret = new StringBuilder(); for (String template : templatenames) { for (ASTNode ast : astlist) { List<HookPoint> templateForwardings = glex.getTemplateForwardings(template, ast); for (HookPoint templateHp : templateForwardings) { ret.append(templateHp.processValue(this, ast)); } } } setIncludeRunning(false); return ret.toString(); } /** * Execute a template without resolving the forwardings. This method is * package default and should only be used by the template hook point * * @param templateName the name of the template * @param ast the ast node * @return produced output */ String includeWithoutForwarding(String templateName, ASTNode ast) { setIncludeRunning(true); StringBuilder ret = new StringBuilder(); ret.append(processTemplate(templateName, ast, new ArrayList<>())); setIncludeRunning(false); return ret.toString(); } /** * Defines the signature of a template. <br /> * <br /> * Note that, due to technical constraints, at first, the current template is * included and the arguments are passed. Second, the signature is defined. * * @param parameterNames the list of the parameter names (=signature) */ public void signature(List<String> parameterNames) { checkArgument(!signatureInitialized, "0xA5297 Template '" + templatename + "': tried to invoke signature() twice"); Log.errorIfNull(parameterNames); checkArgument(parameterNames.size() == arguments.size(), "0xA5298 Template '" + templatename + "': Signature size (#" + parameterNames.size() + ") and number of arguments (#" + arguments.size() + ") mismatch."); this.signature = newArrayList(parameterNames); // bind values (arguments) to names (parameters/signature) // and inject into template for (int i = 0; i < parameterNames.size(); i++) { data.put(parameterNames.get(i), arguments.get(i)); } signatureInitialized = true; } /** * Delegates to {@link #signature(List)}. */ public void signature(String... parameterName) { signature(Lists.newArrayList(parameterName)); } List<String> getSignature() { return ImmutableList.copyOf(signature); } List<Object> getArguments() { return ImmutableList.copyOf(arguments); } boolean isSignatureInitialized() { return signatureInitialized; } /** * Includes a template without an explicit ast. (ast is current ast node) * * @param templateName * @return */ public String include(String templateName) { return include(newArrayList(templateName), newArrayList(getAST())); } /** * Execute each of the templates on the ASTNode. Concatenate the results * together in one big String and include that into the currently processed * output. We iterate on the templates. Template filename may be qualified * (using "."). When it is not qualified, the filename is taken from the * current package (same as the calling template). * * @param templateNames list of filenames, qualified or not * @param ast ast-node the template is operating on * @return produced output */ public String include(List<String> templateNames, ASTNode ast) { return include(templateNames, newArrayList(ast)); } /** * Execute the single template on each ASTNode of the list. Concatenate the * results together in one big String and include that into the currently * processed output. We iterate on the ASTNodes. Template filename may be * qualified (using "."). When it is not qualified, the filename is taken from * the current package (same as the calling template). * * @param templateName filename, qualified or not * @param astlist where we execute the template on in an iteration * @return produced output */ public String include(String templateName, List<ASTNode> astlist) { return include(newArrayList(templateName), astlist); } /** * Include the template into the currently processed output. The template is * executed on ASTNode ast. Remark: even though the name suggests to run * several templates, this is the version that executes on a single template * given as string. We only handle one template on one node. Template filename * may be qualified (using "."). When it is not qualified, the filename is * taken from the current package (same as the calling template). * * @param templateName name of the template to be executed, qualified or not * @param ast ast-node the template is operating on * @return output for the file (may be part of a file only) */ public String include(String templateName, ASTNode ast) { return include(newArrayList(templateName), newArrayList(ast)); } /** * Include the template into the currently processed output. Remark: even * though the name suggests to run several templates, this is the version that * executes on a single template given as string. We only handle one template * on one node. Template filename may be qualified (using "."). When it is not * qualified, the filename is taken from the current package (same as the * calling template). * * @param templateName name of the template to be executed, qualified or not * @param templateArguments additional data that is passed to the called * template * @return output for the file (may be part of a file only) */ public String includeArgs(String templateName, List<Object> templateArguments) { setIncludeRunning(true); StringBuilder ret = new StringBuilder(); List<HookPoint> templateForwardings = glex.getTemplateForwardings(templateName, getAST()); for (HookPoint tn : templateForwardings) { ret.append(tn.processValue(this, templateArguments)); } setIncludeRunning(false); return ret.toString(); } /** * Processes the template with a list of additional arguments. This method is * package default and should only be called by the TemplateHookPoint class. * * @param templateName the template name * @param templateArguments the template arguments * @return */ String includeArgsWithoutForwarding(String templateName, List<Object> templateArguments) { setIncludeRunning(true); StringBuilder ret = new StringBuilder(); ret.append(processTemplate(templateName, getAST(), templateArguments)); setIncludeRunning(false); return ret.toString(); } /** * Delegates to {@link #includeArgs(String, List)} */ public String includeArgs(String templateName, String... templateArgument) { return includeArgs(templateName, Lists.newArrayList(templateArgument)); } /** * This is an unusual method. It does nothing and should not be used. It will * be called from freemarker automatically, when one of the arguments (either * the filename or the ast) is null. In this case an error is issued. * * @param empty1 * @param empty2 */ public void include(Object empty1, Object empty2) { // Extension points in templates (holes) can be unused. In this case // includeTemplates // is called with null which is no error and can therefore be ignored. String msg; if (empty1 == null && empty2 == null) { msg = "0xA2936 Template name and ast node are null in " + templatename; } else if (empty1 == null) { msg = "0xA2937 Template name is null in " + templatename + " using " + empty2.getClass(); } else { msg = "0xA2938 Ast node is null in " + templatename + " calling template " + empty1; } Log.error(msg + " ## You made an illegal call of includeTemplate. As the error says at least " + "one argument was null. That shouldn't happen. ##"); } /** * Execute the template and put the result into a file. The template is * executed on ASTNode ast. File qualifiedFileName + default file extension is * opened, written and closed again! * * @param templateName full qualified filename * @param ast where we execute the template on * @return none (= empty string within Freemarker) */ public void write(String templateName, String qualifiedFileName, ASTNode ast) { writeArgs(templateName, qualifiedFileName, config.getDefaultFileExtension(), ast, new ArrayList<>()); } public void write(String templateName, String qualifiedFileName, String fileExtension, ASTNode ast) { writeArgs(templateName, qualifiedFileName, fileExtension, ast, new ArrayList<>()); } public void write(final String templateName, final Path filePath, final ASTNode ast) { writeArgs(templateName, filePath, ast, new ArrayList<>()); } /** * Execute the template and put the result into a file. The template is * executed on ASTNode ast. And the file qualifiedFileName + fileExtension is * opened, written and closed again! If fileExtension == null, then use "". * fileExtension may start with ".", otherwise one is added. So ".java" and * "java" have the same effect. Template filename may be qualified (using * "."). When it is not qualified, the filename is taken from the current * package (same as the calling template). * * @param templateName full qualified filename * @param ast where we execute the template on * @return none (= empty string within Freemarker) */ public void writeArgs(final String templateName, final String qualifiedFileName, final String fileExtension, final ASTNode ast, final List<Object> templateArguments) { String fileExtensionWithDot = Strings.nullToEmpty(fileExtension); if (fileExtensionWithDot.startsWith(".")) { fileExtensionWithDot = fileExtensionWithDot.substring(1); } final String filePathStr = Names.getPathFromPackage(qualifiedFileName) + "." + fileExtensionWithDot; final Path filePath = Paths.get(filePathStr); writeArgs(templateName, filePath, ast, templateArguments); } /** * Processes the template <code>templateName</code> with the <code>ast</code> * and the given <code>templateArguments</code> and writes the content into * the <code>filePath</code>. Note: Unless not absolute, the * <code>filePath</code> is relative to the configured target directory (i.e., * {@link TemplateControllerConfiguration#getTargetDir()}) * * @param templateName the template to be processes * @param filePath the file path in which the content is to be written * @param ast the ast * @param templateArguments additional template arguments (if needed). */ public void writeArgs(final String templateName, final Path filePath, final ASTNode ast, final List<Object> templateArguments) { final String qualifiedTemplateName = completeQualifiedName(templateName); StringBuilder content = new StringBuilder(); content.append(processTemplate(qualifiedTemplateName, ast, templateArguments)); if (Strings.isNullOrEmpty(content.toString())) { Log.error("0xA4057 Template " + qualifiedTemplateName + " produced no content for."); } // add trace to source-model: if (config.isTracing() && config.getModelName().isPresent()) { content.insert(0, config.getCommentStart() + " generated from model " + config.getModelName().get() + config.getCommentEnd() + "\n"); } Path completeFilePath; if (filePath.isAbsolute()) { completeFilePath = filePath; } else { completeFilePath = Paths.get(config.getOutputDirectory().getAbsolutePath(), filePath.toString()); } Reporting.reportFileCreation(qualifiedTemplateName, filePath, ast); fileHandler.storeInFile(completeFilePath, content.toString()); Log.debug(completeFilePath + " written successfully!", this.getClass().getName()); Reporting.reportFileFinalization(qualifiedTemplateName, filePath, ast); } /** * Include a template with additional data: We only handle one template on one * node. This method allows to parameterize templates. Template filename may * be qualified (using "."). When it is not qualified, the filename is taken * from the current package. The resulting output may be stored or included in * another generation process. * * @param templateName name of the template to be executed, qualified or not * @param astNode ast-node the template is operating on * @param passedArguments additional data that is passed to the included * template * @return output for the file (may be part of a file only) */ protected String processTemplate(String templateName, ASTNode astNode, List<Object> passedArguments) { String qualifiedTemplateName = completeQualifiedName(templateName); ASTNode ast = astNode; // If no ast is passed, get current ast, if exists if (ast == null) { ast = getAST(); } Reporting.reportTemplateStart(qualifiedTemplateName, ast); StringBuilder ret = runInEngine(passedArguments, qualifiedTemplateName, ast); Reporting.reportTemplateEnd(qualifiedTemplateName, ast); return ret.toString(); } StringBuilder runInEngine(List<Object> passedArguments, String templateName, ASTNode ast) { initAliases(); // Load template // TODO: // It's pretty inefficient each time to create a new instance of the // FreeMarker configuration by FreeMarkerTemplateEngine.loadTemplate(...) // method // Template t = FreeMarkerTemplateEngine.loadTemplate(templatename, // classLoader, MontiCoreTemplateExceptionHandler.THROW_ERROR, // externalTemplatePath); Template template = freeMarkerTemplateEngine.loadTemplate(templateName); // add static functions to template for (Macro macro : aliases) { template.addMacro(macro); } return runInEngine(passedArguments, template, ast); } /** * Init template aliases if not already done. This happens once (for all main * templates) */ @SuppressWarnings("rawtypes") protected void initAliases() { if (aliases == null) { aliases = newArrayList(); Template aliasesTemplate = freeMarkerTemplateEngine.loadTemplate( TemplateControllerConfiguration.ALIASES_TEMPLATE); Set macros = aliasesTemplate.getMacros().entrySet(); for (Object o : macros) { Entry e = (Entry) o; Macro macro = (Macro) e.getValue(); aliases.add(macro); } } } StringBuilder runInEngine(List<Object> passedArguments, Template template, ASTNode ast) { StringBuilder ret = new StringBuilder(); if (template != null) { // Initialize standard-data for template // get ast if (ast == null) { ast = getAST(); } TemplateController tc = createTemplateController(template.getName()); tc.tcFactory = this.tcFactory; SimpleHash d = SimpleHashFactory.getInstance().createSimpleHash(); d.put(TemplateControllerConstants.AST, ast); d.put(TemplateControllerConstants.TC, tc); d.put(TemplateControllerConstants.GLEX, glex); d.put(TemplateControllerConstants.LOG, config.getLog()); // add all global data to be accessible in the template try { d.putAll(glex.getGlobalData().toMap()); } catch (TemplateModelException e) { String usage = this.templatename != null ? " (" + this.templatename + ")" : ""; Log.error("0xA0128 Globally defined data could not be passed to the called template " + usage + ". ## This is an internal" + "error that should not happen. Try to remove all global data. ##"); } tc.data = d; tc.arguments = newArrayList(passedArguments); if (aliases != null) { tc.setAliases(aliases); } // Run template with data to create output freeMarkerTemplateEngine.run(ret, d, template); } else { // no template String usage = this.templatename != null ? " (used in " + this.templatename + ")" : ""; Log.error("0xA0127 Missing template " + usage + " ## You have tried to use a template that " + "doesn't exist. It may be in another package? The name printed is the qualified version, " + "but you may have used the unqualified name. ##"); } // add trace to template: if (ret.length() != 0 && config.isTracing()) { ret.insert(0, config.getCommentStart() + " generated by template " + template.getName() + config.getCommentEnd() + "\n"); } return ret; } protected TemplateController createTemplateController(String templateName) { return tcFactory.create(this.config, templateName); } /** * @param tcFactory the factory that should be used when new template * controllers are created. */ public void setTemplateControllerFactory(ITemplateControllerFactory tcFactory) { this.tcFactory = tcFactory; } /** * checks if the name seems to be already qualified: if not, adds the current * package (of the template it operates on) */ private String completeQualifiedName(String name) { Log.errorIfNull(!isNullOrEmpty(name)); if (name.contains(".")) { return name; } return currPackage + "." + name; } ASTNode getAST() { ASTNode ast = null; Object o = getValueFromData(TemplateControllerConstants.AST); if ((o != null) && (o instanceof ASTNode)) { ast = (ASTNode) o; } return ast; } private Object getValueFromData(String name) { try { return BeansWrapper.getDefaultInstance().unwrap(data.get(name)); } catch (TemplateModelException e) { Log.error("0xA0124 Could not find value for \"" + name + "\" in template \"" + templatename + "\"", e); } return null; } protected void logTemplateCallOrInclude(String templateName, ASTNode ast) { if (isIncludeRunning()) { Reporting.reportTemplateInclude(templateName, ast); } else { Reporting.reportTemplateWrite(templateName, ast); } } // TODO: can we remove this one? public String defineHookPoint(String hookName) { return glex.defineHookPoint(this, hookName, getAST()); } // TODO AR <- PN Actually,the plan was to move both instantiate() methods to // HpFV. // But as the className may be unqualified, the package name is needed. // But, HpFV does not now anything about the template or its package /** * Can be used to instantiate any Java-class with a default constructor (no * args). The passed className is either in the same package as the calling * template, or it needs to be qualified (dot-separated). * * @param className name of the class to be instantiated * @return an object of the given className */ public Object instantiate(String className) { Reporting.reportInstantiate(className, new ArrayList<>()); return ObjectFactory.createObject(completeQualifiedName(className)); } /** * @param aliases the staticFunctions to set */ public void setAliases(List<Macro> aliases) { this.aliases = newArrayList(aliases); } /** * @return aliases */ List<Macro> getAliases() { return aliases; } public boolean existsHWC(String fileName) { return existsHWC(fileName, DEFAULT_FILE_EXTENSION); } public boolean existsHWC(String fileName, String extension) { checkArgument(!isNullOrEmpty(fileName)); checkArgument(!isNullOrEmpty(extension)); Path filePath = Paths.get(Names.getFileName(Names.getPathFromPackage(fileName), extension)); return config.getHandcodedPath().exists(filePath); } /** * Can be used to instantiate any Java-class with constructor of a signature * fitting to the params. The passed className is either in the same package * as the calling template, or it needs to be qualified (dot-separated). * * @param className name of the class to be instantiated * @param params parameters provided for the constructor-call * @return an object of the given className */ public Object instantiate(String className, List<Object> params) { Reporting.reportInstantiate(className, params); return ObjectFactory.createObject(completeQualifiedName(className), params); } }