/*
* ******************************************************************************
* 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 java.util.*;
import de.monticore.ast.ASTNode;
import com.google.common.base.Strings;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import de.monticore.generating.templateengine.freemarker.SimpleHashFactory;
import de.monticore.generating.templateengine.reporting.Reporting;
import de.se_rwth.commons.logging.Log;
import freemarker.ext.beans.BeansWrapper;
import freemarker.template.SimpleHash;
import freemarker.template.TemplateCollectionModel;
import freemarker.template.TemplateModelException;
/**
* Class for managing hook points, features and (global) variables in templates.
*
* @author Pedram Nazari, Alexander Roth
*/
public class GlobalExtensionManagement {
private SimpleHash globalData = SimpleHashFactory.getInstance().createSimpleHash();
// use these list to handle replacements aka template forwardings
private final Multimap<String, HookPoint> before = ArrayListMultimap.create();
private final Multimap<String, HookPoint> replace = ArrayListMultimap.create();
private final Multimap<String, HookPoint> after = ArrayListMultimap.create();
private final Map<String, Map<ASTNode, HookPoint>> specificReplacement = Maps.newHashMap();
/**
* Map of all hook points
*/
private final Map<String, HookPoint> hookPoints = Maps.newHashMap();
public GlobalExtensionManagement() {
}
/**
* Set a list of global data. The parameter should not be null.
*
* @param data list of global data
*/
public void setGlobalData(SimpleHash data) {
Log.errorIfNull(data);
this.globalData = data;
}
/**
* Retrieve a list of all global data
*/
SimpleHash getGlobalData(){
return this.globalData;
}
/**
* Returns a list of all registered global value names
*
* @return collection of all names of defined values.
*/
public TemplateCollectionModel getGlobalValueNames() {
return globalData.keys();
}
/**
* Checks whether a value with the given name is defined and is not null
*
* @param name of the value to check
* @return true if a variable exists and its value is not null
*/
public boolean hasGlobalVar(String name) {
try {
return globalData.get(name) != null;
}
catch (TemplateModelException e) {
Log.error("0xA7123 Internal Error on global value for \"" + name + "\"");
return false;
}
}
/**
* Sets a new value which can be accessed with the given name in the
* templates. If the name is already in use an error is reported the previous
* value is overridden.
*
* @param name of the value to set
* @param value the actual content
*/
public void setGlobalValue(String name, Object value) {
Log.errorIfNull(name);
Reporting.reportSetValue(name, value);
globalData.put(name, value);
}
/**
* Defines a new value which can be accessed with the given name in the
* templates. If the name is already in use an error is reported.
*
* @param name of the value to set
* @param value the actual content
*/
public void defineGlobalVar(String name, Object value) {
if (hasGlobalVar(name)) {
// TODO: in which template? hinzufügen
Log.error("0xA0122 Global Value '" + name + "' has already been set.\n Old value: " +
getGlobalVar(name) + "\n New value: " + value);
}
setGlobalValue(name, value);
}
/**
* Defines a new global variable with no value set.
* If the name is already in use an error is reported.
*
* @param name of the value to set
*/
public void defineGlobalVar(String name) {
setGlobalValue(name, null); // TODO: clarify what the default value is!
}
/**
* Defines a list of global variables with the given name in the
* templates. If the name is already in use an error is reported.
*
* @param names list of names to set
*/
public void defineGlobalVars(List<String> names) {
names.forEach(this::defineGlobalVar);
}
/**
* Changes the value of an existing global variable. If the name is
* not in use an error is reported.
*
* @param name of the value to set
* @param value the actual content
*/
public void changeGlobalVar(String name, Object value) {
if (!hasGlobalVar(name)) {
Log.error("0xA0124 Global Value '" + name + "' has not been defined."); // TODO: identify valid error code
} else {
setGlobalValue(name, value);
}
}
/**
* Adds a new value to the given name. It converts a single value into a list
* if necessary.
*
* @param name of the value to set
* @param value the actual content
*/
@SuppressWarnings({ "unchecked", "deprecation" })
public void addToGlobalVar(String name, Object value) {
if (hasGlobalVar(name)){
Object currentValue = null;
try {
currentValue = BeansWrapper.getDefaultInstance().unwrap(globalData.get(name));
}
catch (TemplateModelException e) {
Log.error("0xA8123 Internal Error on global value for \"" + name + "\"");
}
Collection<Object> newValue = new ArrayList<>();
// the global variable has already a value assigned
if (currentValue != null) {
if (currentValue instanceof Collection<?>) {
newValue.addAll((Collection<Object>) currentValue);
}
else {
newValue.add(currentValue);
}
if (value instanceof Collection<?>) {
newValue.addAll((Collection<Object>) value);
Reporting.reportAddValue(name, value, newValue.size());
}
else {
newValue.add(value);
Reporting.reportAddValue(name, value, 1);
}
setGlobalValue(name, newValue);
}
// the variable has been defined but it has no value assigned
else {
if (value instanceof Collection<?>) {
newValue.addAll((Collection<Object>) value);
}else {
newValue.add(value);
}
setGlobalValue(name, newValue);
}
} else {
Log.error("0xA8124 Global value with name \"" + name + "\" does not exist!");
}
}
/**
* Returns the value of the given name.
*
* @param name of the value
* @return the value
*/
@SuppressWarnings("deprecation")
public Object getGlobalVar(String name) {
try {
return BeansWrapper.getDefaultInstance().unwrap(globalData.get(name));
}
catch (TemplateModelException e) {
Log.error("0xA0123 Internal Error on global value for \"" + name + "\"");
}
return null;
}
/**
* check whether the variable name (parameter) is defined: if not issue an
* error and continue
*
* @param name variable name
*/
public void requiredGlobalVar(String name) {
if (getGlobalVar(name) == null) {
// TODO: in which template? hinzufügen
// Sollte dieser Fehler kommen, dann wird auch das Template ausgeworfen,
// das zuletzt verarbeitet wurde (also den Fehler produziert hat)
Log.error("0xA0126 Missing required value \"" + name + "\"");
}
}
/**
* check whether the list of variable names (parameter) is defined: if not
* issue an error and continue
*
* @param names list of variable names
*/
public void requiredGlobalVars(String... names) {
for (int i = 0; i < names.length; i++){
requiredGlobalVar(names[i]);
}
}
/**
* @param hookName name of the hook point
* @param hp
*/
public void bindHookPoint(String hookName, HookPoint hp) {
Reporting.reportSetHookPoint(hookName, hp);
warnIfHookPointExists(hookName);
hookPoints.put(hookName, hp);
}
/**
* @param hookName name of the hook point
* @return the (processed) value of the hook point
*/
public String defineHookPoint(TemplateController controller, String hookName, ASTNode ast) {
String result = null;
HookPoint hp = hookPoints.get(hookName);
Reporting.reportCallHookPointStart(hookName, hp, ast);
if (hookPoints.containsKey(hookName)) {
result = hp.processValue(controller, ast);
}
Reporting.reportCallHookPointEnd(hookName);
return Strings.nullToEmpty(result);
}
/**
* @param hookName name of the hook point
* @return the (processed) value of the hook point
*/
public String defineHookPoint(TemplateController controller, String hookName) {
return defineHookPoint(controller, hookName, controller.getAST());
}
/**
* @param hookName name of the hook point
* @return the (processed) value of the hook point
*/
public boolean existsHookPoint(String hookName) {
return hookPoints.containsKey(hookName);
}
/**
* Returns a set of templates that have been defined to replace the template
* <code>templateName</code>. If no template forwardings have been defined,
* then <code>templateName</code> is returned.
*
* @param templateName The name of the template
* @return A list of templates that have been defined to replace the
* 'templateNames' templates
*/
protected List<HookPoint> getTemplateForwardings(String templateName, ASTNode ast) {
List<HookPoint> replacements = Lists.newArrayList();
Collection<HookPoint> before = this.before.get(templateName);
Collection<HookPoint> after = this.after.get(templateName);
if (before != null) {
replacements.addAll(before);
Reporting.reportCallBeforeHookPoint(templateName, before, ast);
}
List<HookPoint> hps = getSpecificReplacement(templateName, ast);
if(hps != null){
Reporting.reportCallSpecificReplacementHookPoint(templateName, hps, ast);
}
else {
hps = getTemplateForwardingsX(templateName, ast);
}
replacements.addAll(hps);
if (after != null) {
replacements.addAll(after);
Reporting.reportCallAfterHookPoint(templateName, after, ast);
}
return replacements;
}
protected List<HookPoint> getSpecificReplacement(String templateName, ASTNode ast) {
Map<ASTNode, HookPoint> replacedTemplates = this.specificReplacement.get(templateName);
if (replacedTemplates != null && replacedTemplates.containsKey(ast)) {
return Lists.newArrayList(replacedTemplates.get(ast));
}
return null;
}
/**
* Returns a set of templates that have been defined to replace the
* 'templateName' template. If no template forwardings have been defined, then
* the 'templateName' template is returned.
*
* @param templateName The name of the template
* @return A list of templats that have been defined to replace the
* 'templateName' template
*/
protected List<HookPoint> getTemplateForwardingsX(String templateName, ASTNode ast) {
List<HookPoint> forwardings = Lists.newArrayList();
if (containsTemplateForwarding(templateName)) {
if(this.replace.containsKey(templateName)){
forwardings.addAll(this.replace.get(templateName));
Reporting.reportCallReplacementHookPoint(templateName, forwardings, ast);
} else{
forwardings.addAll(Lists.newArrayList(new TemplateHookPoint(templateName)));
}
}
else {
forwardings.add(new TemplateHookPoint(templateName));
Reporting.reportExecuteStandardTemplate(templateName, ast);
}
return forwardings;
}
private boolean containsTemplateForwarding(String templateName) {
return this.before.containsKey(templateName)
| this.replace.containsKey(templateName)
| this.after.containsKey(templateName);
}
/**
* Future inclusion of 'oldTemplate' will be replaced by 'newTemplate'. NOTE:
* This replacement has only an effect if 'oldTemplate' is included directly.
*
* @param oldTemplate qualified name of template to be replaced
*/
public void replaceTemplate(String oldTemplate, HookPoint hp) {
replaceTemplate(oldTemplate, Lists.newArrayList(hp));
}
/**
* Future inclusion of 'oldTemplate' will be replaced by list of
* 'newTemplates'. NOTE: This replacement has only an effect if 'oldTemplate'
* is included directly.
*
* @param oldTemplate qualified name of template to be replaced
*/
public void replaceTemplate(String oldTemplate, List<? extends HookPoint> newHps) {
Reporting.reportTemplateReplacement(oldTemplate, newHps);
if (!newHps.isEmpty()) {
// remove all previous replacements
this.replace.removeAll(oldTemplate);
this.replace.putAll(oldTemplate, newHps);
}
else {
this.replace.removeAll(oldTemplate);
}
}
public void replaceTemplate(String oldTemplate, ASTNode node, HookPoint newHp) {
Reporting.reportASTSpecificTemplateReplacement(oldTemplate, node, newHp);
Map<ASTNode, HookPoint> replacedTemplates = this.specificReplacement.get(oldTemplate);
if (replacedTemplates != null && !replacedTemplates.containsKey(node)) {
replacedTemplates.put(node, newHp);
}
else {
Map<ASTNode, HookPoint> specificTemplate = Maps.newHashMap();
specificTemplate.put(node, newHp);
this.specificReplacement.put(oldTemplate, specificTemplate);
}
}
/**
* Everytime 'template' is included directly (e.g. by
* {@link TemplateController#include(String, ASTNode)}), 'beforeTemplate' will
* be included before it.
*
* @param template qualified name of the template
*/
public void setBeforeTemplate(String template, HookPoint beforeHp) {
setBeforeTemplate(template, Lists.newArrayList(beforeHp));
}
/**
* Everytime 'template' is included directly (e.g. by
* {@link TemplateController#include(String, ASTNode)}), the templates in
* 'beforeTemplate' will be included before it.
*
* @param template qualified name of the template
*/
public void setBeforeTemplate(String template, List<? extends HookPoint> beforeHps) {
Reporting.reportSetBeforeTemplate(template, beforeHps);
// remove all previous replacements
this.before.removeAll(template);
if (!beforeHps.isEmpty()) {
this.before.putAll(template, beforeHps);
}
}
/**
* Everytime 'template' is included directly (e.g. by
* {@link TemplateController#include(String, ASTNode)}), 'afterTemplate' will
* be included after it.
*
* @param template qualified name of the template
*/
public void setAfterTemplate(String template, HookPoint afterHp) {
setAfterTemplate(template, Lists.newArrayList(afterHp));
}
/**
* Everytime 'template' is included directly (e.g. by
* {@link TemplateController#include(String, ASTNode)}), the templates in
* 'afterTemplate' will be included after it.
*
* @param template qualified name of the template
*/
public void setAfterTemplate(String template, List<? extends HookPoint> afterHps) {
Reporting.reportSetAfterTemplate(template, afterHps);
// remove all previous replacements
this.after.removeAll(template);
if (!afterHps.isEmpty()) {
this.after.putAll(template, afterHps);
}
}
private void warnIfHookPointExists(String hookName) {
if (hookPoints.containsKey(hookName)) {
Log.warn("0xA1036 Hook point '" + hookName + "' is already defined. It will be overwritten.");
}
}
}