Bug 62870 - Templates : Add ability to provide parameters

Contributed by UbikLoadPack (https://ubikloadpack.com)

This closes #432


Bugzilla Id: 62870

git-svn-id: https://svn.apache.org/repos/asf/jmeter/trunk@1847594 13f79535-47bb-0310-9956-ffa450edef68

Former-commit-id: 4909935a04
This commit is contained in:
Philippe Mouawad 2018-11-27 20:23:44 +00:00
parent 872dec2f8f
commit 60cef31c10
23 changed files with 881 additions and 123 deletions

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<jmeterTestPlan version="1.2" properties="4.0" jmeter="4.0-SNAPSHOT.20180218">
<jmeterTestPlan version="1.2" properties="5.0" jmeter="5.1-SNAPSHOT.20181118">
<hashTree>
<TestPlan guiclass="TestPlanGui" testclass="TestPlan" testname="Test Plan" enabled="true">
<stringProp name="TestPlan.comments"></stringProp>
@ -12,16 +12,27 @@
</TestPlan>
<hashTree>
<Arguments guiclass="ArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
<collectionProp name="Arguments.arguments"/>
<collectionProp name="Arguments.arguments">
<elementProp name="host" elementType="Argument">
<stringProp name="Argument.name">host</stringProp>
<stringProp name="Argument.value">[=hostToRecord]</stringProp>
<stringProp name="Argument.metadata">=</stringProp>
</elementProp>
<elementProp name="scheme" elementType="Argument">
<stringProp name="Argument.name">scheme</stringProp>
<stringProp name="Argument.value">[=schemeToRecord]</stringProp>
<stringProp name="Argument.metadata">=</stringProp>
</elementProp>
</collectionProp>
</Arguments>
<hashTree/>
<ConfigTestElement guiclass="HttpDefaultsGui" testclass="ConfigTestElement" testname="HTTP Request Defaults" enabled="true">
<elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
<collectionProp name="Arguments.arguments"/>
</elementProp>
<stringProp name="HTTPSampler.domain"></stringProp>
<stringProp name="HTTPSampler.domain">[=hostToRecord]</stringProp>
<stringProp name="HTTPSampler.port"></stringProp>
<stringProp name="HTTPSampler.protocol"></stringProp>
<stringProp name="HTTPSampler.protocol">[=schemeToRecord]</stringProp>
<stringProp name="HTTPSampler.contentEncoding"></stringProp>
<stringProp name="HTTPSampler.path"></stringProp>
<stringProp name="HTTPSampler.concurrentPool">6</stringProp>
@ -90,9 +101,8 @@
<ProxyControl guiclass="ProxyControlGui" testclass="ProxyControl" testname="HTTP(S) Test Script Recorder" enabled="false">
<stringProp name="ProxyControlGui.port">8888</stringProp>
<collectionProp name="ProxyControlGui.exclude_list">
<stringProp name="1301401588">.*toolbar\.live\.com.*</stringProp>
<stringProp name="1179605444">(?i).*\.(bmp|css|js|gif|ico|jpe?g|png|swf|eot|otf|ttf|mp4|woff|woff2)</stringProp>
<stringProp name="1276958334">update\.microsoft\.com.*</stringProp>
<stringProp name="-88591710">www\.download\.windowsupdate\.com.*</stringProp>
<stringProp name="195066122">toolbarqueries\.google\..*</stringProp>
<stringProp name="-1570593883">clients.*\.google.*</stringProp>
<stringProp name="339269285">api\.bing\.com.*</stringProp>
@ -171,7 +181,7 @@
<connectTime>true</connectTime>
</value>
</objProp>
<stringProp name="filename">recording.xml</stringProp>
<stringProp name="filename">[=recordingOutputFile]</stringProp>
</ResultCollector>
<hashTree/>
</hashTree>

View File

@ -15,10 +15,10 @@
limitations under the License.
-->
<!-- Basic DTD for podlings.xml -->
<!-- Basic DTD for templates.xml -->
<!ELEMENT templates (template+)>
<!ELEMENT template (name, fileName, description)>
<!ELEMENT template (name, fileName, description, parameters?)>
<!-- Whether the template is a complete test plan or not -->
<!ATTLIST template isTestPlan (true|false) #REQUIRED>
@ -27,3 +27,11 @@
<!ELEMENT fileName (#PCDATA) >
<!ELEMENT description (#PCDATA) >
<!ELEMENT parameters (parameter*)>
<!ELEMENT parameter EMPTY>
<!ATTLIST parameter key CDATA #REQUIRED>
<!ATTLIST parameter defaultValue CDATA #REQUIRED>

View File

@ -18,7 +18,7 @@
<!--
See the DTD for allowable elements and attributes.
-->
<!DOCTYPE templates SYSTEM "templates.dtd">
<!DOCTYPE templates SYSTEM "templates.dtd">
<templates>
<template isTestPlan="true">
<name>Recording</name>
@ -42,6 +42,11 @@
<li><a href="http://jmeter.apache.org/usermanual/component_reference.html#HTTP_Proxy_Server" >http://jmeter.apache.org/usermanual/component_reference.html#HTTP_Proxy_Server</a></li>
</ul>
]]></description>
<parameters>
<parameter defaultValue="recording.xml" key="recordingOutputFile"/>
<parameter defaultValue="www.example.com" key="hostToRecord"/>
<parameter defaultValue="https" key="schemeToRecord"/>
</parameters>
</template>
<template isTestPlan="true">
<name>Recording with Think Time</name>

View File

@ -1057,8 +1057,8 @@ run JMeter unless all the JMeter jars are added.
<fileset dir="${src.core}" includes="**/*.properties">
<exclude name="*eucJP*"/>
</fileset>
<fileset dir="${src.core}" includes="**/*.xml">
</fileset>
<fileset dir="${src.core}" includes="**/*.xml" />
<fileset dir="${src.core}" includes="**/*.dtd" />
<!-- This file is used by the jmeter -h option -->
<fileset dir="${src.core}" includes="org/apache/jmeter/help.txt"/>
</jar>
@ -1703,6 +1703,7 @@ run JMeter unless all the JMeter jars are added.
<zipfileset dir="${dest.jar.jmeter}" prefix="bin" includes="log4j2.xml"/>
<zipfileset dir="${dest.jar.jmeter}" prefix="bin" includes="proxyserver.jks"/>
<zipfileset dir="${dest.jar.jmeter}" prefix="bin" includes="users.dtd"/>
<zipfileset dir="${dest.jar.jmeter}" prefix="bin" includes="templates/templates.dtd"/>
<zipfileset dir="${dest.jar.jmeter}" prefix="bin" includes="users.xml"/>
<zipfileset dir="${dest.jar.jmeter}" prefix="bin" includes="report-template/**/*.*" />
</jar>

View File

@ -22,10 +22,20 @@ import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.Font;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.HeadlessException;
import java.awt.Insets;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.File;
import java.io.IOException;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import javax.swing.AbstractAction;
import javax.swing.Action;
@ -35,6 +45,7 @@ import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JDialog;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JRootPane;
@ -50,16 +61,21 @@ import org.apache.jmeter.gui.action.template.Template;
import org.apache.jmeter.gui.action.template.TemplateManager;
import org.apache.jmeter.swing.HtmlPane;
import org.apache.jmeter.util.JMeterUtils;
import org.apache.jmeter.util.TemplateUtil;
import org.apache.jorphan.gui.ComponentUtil;
import org.apache.jorphan.gui.JLabeledChoice;
import org.apache.jorphan.gui.JLabeledTextField;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import freemarker.template.Configuration;
import freemarker.template.TemplateException;
/**
* Dialog used for Templates selection
* @since 2.10
*/
public class SelectTemplatesDialog extends JDialog implements ChangeListener, ActionListener, HyperlinkListener {
public class SelectTemplatesDialog extends JDialog implements ChangeListener, ActionListener, HyperlinkListener { // NOSONAR Ignore inheritence warning
private static final long serialVersionUID = 1;
@ -83,7 +99,13 @@ public class SelectTemplatesDialog extends JDialog implements ChangeListener, Ac
private final JButton cancelButton = new JButton(JMeterUtils.getResString("cancel")); //$NON-NLS-1$
private final JScrollPane scroller = new JScrollPane(helpDoc);
private final JButton previous = new JButton(JMeterUtils.getResString("previous")); //$NON-NLS-1$
private final JButton validateButton = new JButton(JMeterUtils.getResString("validate_threadgroup")); //$NON-NLS-1$
private Map<String, JLabeledTextField> parametersTextFields = new LinkedHashMap<>();
private JPanel actionBtnBar = new JPanel(new FlowLayout());
public SelectTemplatesDialog() {
super((JFrame) null, JMeterUtils.getResString("template_title"), true); //$NON-NLS-1$
@ -127,7 +149,8 @@ public class SelectTemplatesDialog extends JDialog implements ChangeListener, Ac
/**
* Check if existing Test Plan has been modified and ask user
* what he wants to do if test plan is dirty
* what he wants to do if test plan is dirty.
* Also ask user for parameters in case of customizable templates.
* @param actionEvent {@link ActionEvent}
*/
private void checkDirtyAndLoad(final ActionEvent actionEvent)
@ -137,67 +160,138 @@ public class SelectTemplatesDialog extends JDialog implements ChangeListener, Ac
if (template == null) {
return;
}
templateList.setValues(TemplateManager.getInstance().reset().getTemplateNames()); // reload the templates before loading
final boolean isTestPlan = template.isTestPlan();
// Check if the user wants to drop any changes
if (isTestPlan) {
ActionRouter.getInstance().doActionNow(new ActionEvent(actionEvent.getSource(), actionEvent.getID(), ActionNames.CHECK_DIRTY));
GuiPackage guiPackage = GuiPackage.getInstance();
if (guiPackage.isDirty()) {
// Check if the user wants to create from template
int response = JOptionPane.showConfirmDialog(GuiPackage.getInstance().getMainFrame(),
JMeterUtils.getResString("cancel_new_from_template"), // $NON-NLS-1$
JMeterUtils.getResString("template_load?"), // $NON-NLS-1$
JOptionPane.YES_NO_CANCEL_OPTION,
JOptionPane.QUESTION_MESSAGE);
if(response == JOptionPane.YES_OPTION) {
ActionRouter.getInstance().doActionNow(new ActionEvent(actionEvent.getSource(), actionEvent.getID(), ActionNames.SAVE));
}
if (response == JOptionPane.CLOSED_OPTION || response == JOptionPane.CANCEL_OPTION) {
return; // Don't clear the plan
}
}
if (isTestPlan && !checkDirty(actionEvent)) {
return;
}
ActionRouter.getInstance().doActionNow(new ActionEvent(actionEvent.getSource(), actionEvent.getID(), ActionNames.STOP_THREAD));
final File parent = template.getParent();
final File fileToCopy = parent != null
File fileToCopy = parent != null
? new File(parent, template.getFileName())
: new File(JMeterUtils.getJMeterHome(), template.getFileName());
Load.loadProjectFile(actionEvent, fileToCopy, !isTestPlan, false);
this.setVisible(false);
: new File(JMeterUtils.getJMeterHome(), template.getFileName());
replaceTemplateParametersAndLoad(actionEvent, template, isTestPlan, fileToCopy);
}
/**
* @param actionEvent {@link ActionEvent}
* @param template {@link Template} definition
* @param isTestPlan If it's a full test plan or a part
* @param templateFile Template file to load
*/
void replaceTemplateParametersAndLoad(final ActionEvent actionEvent, final Template template,
final boolean isTestPlan, File templateFile) {
File temporaryGeneratedFile = null;
try {
// handle customized templates (the .jmx.fmkr files)
if (template.getParameters() != null && !template.getParameters().isEmpty()) {
File jmxFile = new File(templateFile.getAbsolutePath());
Map<String, String> userParameters = getUserParameters();
Configuration templateCfg = TemplateUtil.getTemplateConfig();
try {
temporaryGeneratedFile = File.createTempFile(template.getName(), ".output");
templateFile = temporaryGeneratedFile;
TemplateUtil.processTemplate(jmxFile, temporaryGeneratedFile, templateCfg, userParameters);
} catch (IOException | TemplateException ex) {
log.error("Error generating output file {} from template {}", temporaryGeneratedFile, jmxFile, ex);
return;
}
}
Load.loadProjectFile(actionEvent, templateFile, !isTestPlan, false);
this.dispose();
} finally {
if (temporaryGeneratedFile != null && !temporaryGeneratedFile.delete()) {
log.warn("Could not delete generated output file {} from template {}", temporaryGeneratedFile, templateFile);
}
}
}
/**
* @param actionEvent {@link ActionEvent}
* @return true if plan is not dirty or has been saved
*/
boolean checkDirty(final ActionEvent actionEvent) {
ActionRouter.getInstance().doActionNow(new ActionEvent(actionEvent.getSource(), actionEvent.getID(), ActionNames.CHECK_DIRTY));
GuiPackage guiPackage = GuiPackage.getInstance();
if (guiPackage.isDirty()) {
// Check if the user wants to create from template
int response = JOptionPane.showConfirmDialog(GuiPackage.getInstance().getMainFrame(),
JMeterUtils.getResString("cancel_new_from_template"), // $NON-NLS-1$
JMeterUtils.getResString("template_load?"), // $NON-NLS-1$
JOptionPane.YES_NO_CANCEL_OPTION,
JOptionPane.QUESTION_MESSAGE);
if (response == JOptionPane.YES_OPTION) {
ActionRouter.getInstance().doActionNow(new ActionEvent(actionEvent.getSource(), actionEvent.getID(), ActionNames.SAVE));
return true;
}
if (response == JOptionPane.CLOSED_OPTION || response == JOptionPane.CANCEL_OPTION) {
return false; // Don't clear the plan
}
}
return true;
}
private Map<String, String> getUserParameters(){
Map<String, String> userParameters = new LinkedHashMap<>();
for (Entry<String, JLabeledTextField> entry : parametersTextFields.entrySet()) {
userParameters.put(entry.getKey(), entry.getValue().getText());
}
return userParameters;
}
private void init() { // WARNING: called from ctor so must not be overridden (i.e. must be private or final)
templateList.setValues(TemplateManager.getInstance().getTemplateNames());
templateList.setValues(TemplateManager.getInstance().getTemplateNames());
templateList.addChangeListener(this);
reloadTemplateButton.addActionListener(this);
reloadTemplateButton.setFont(FONT_SMALL);
this.getContentPane().setLayout(new BorderLayout(10, 0));
JPanel templateBar = new JPanel(new BorderLayout());
templateBar.add(templateList, BorderLayout.CENTER);
JPanel reloadBtnBar = new JPanel();
reloadBtnBar.add(reloadTemplateButton);
templateBar.add(reloadBtnBar, BorderLayout.EAST);
this.getContentPane().add(templateBar, BorderLayout.NORTH);
helpDoc.setContentType("text/html"); //$NON-NLS-1$
helpDoc.setEditable(false);
helpDoc.addHyperlinkListener(this);
this.getContentPane().add(scroller, BorderLayout.CENTER);
applyTemplateButton.addActionListener(this);
cancelButton.addActionListener(this);
// Bottom buttons bar
JPanel actionBtnBar = new JPanel(new FlowLayout());
actionBtnBar.add(applyTemplateButton);
actionBtnBar.add(cancelButton);
this.getContentPane().add(actionBtnBar, BorderLayout.SOUTH);
previous.addActionListener(this);
validateButton.addActionListener(this);
// allow to reset the JDialog if the user click on the close button while
// it was displaying templates parameters
this.addWindowListener(new WindowAdapter(){
@Override
public void windowClosing(WindowEvent evt){
resetJDialog();
dispose();
}
});
this.setContentPane(templateSelectionPanel());
this.pack();
this.setMinimumSize(new Dimension(MINIMAL_BOX_WIDTH, MINIMAL_BOX_HEIGHT));
ComponentUtil.centerComponentInWindow(this, 50); // center position and 50% of screen size
populateTemplatePage();
}
private JPanel templateSelectionPanel() {
JPanel panel = new JPanel(new BorderLayout());
JScrollPane scroller = new JScrollPane();
scroller.setViewportView(helpDoc);
JPanel templateBar = new JPanel(new BorderLayout());
templateBar.add(templateList, BorderLayout.CENTER);
JPanel reloadBtnBar = new JPanel();
reloadBtnBar.add(reloadTemplateButton);
templateBar.add(reloadBtnBar, BorderLayout.EAST);
// Bottom buttons bar
actionBtnBar.add(applyTemplateButton);
actionBtnBar.add(cancelButton);
panel.add(templateBar, BorderLayout.NORTH);
panel.add(scroller, BorderLayout.CENTER);
panel.add(actionBtnBar, BorderLayout.SOUTH);
return panel;
}
/**
* Do search
@ -207,19 +301,44 @@ public class SelectTemplatesDialog extends JDialog implements ChangeListener, Ac
public void actionPerformed(ActionEvent e) {
final Object source = e.getSource();
if (source == cancelButton) {
this.setVisible(false);
return;
resetJDialog();
this.dispose();
} else if (source == applyTemplateButton) {
checkDirtyAndLoad(e);
} else if (source == reloadTemplateButton) {
templateList.setValues(TemplateManager.getInstance().reset().getTemplateNames());
String selectedTemplate = templateList.getText();
Template template = TemplateManager.getInstance().getTemplateByName(selectedTemplate);
if (hasParameters(template)) {
this.setContentPane(configureParametersPanel(template.getParameters()));
this.revalidate();
} else {
checkDirtyAndLoad(e);
}
} else if (source == reloadTemplateButton || source == previous) {
resetJDialog();
} else if (source == validateButton) {
checkDirtyAndLoad(e);
resetJDialog();
}
}
/**
*
* @param template {@link Template}
* @return true if template has not parameter
*/
private boolean hasParameters(Template template) {
return !(template.getParameters() == null || template.getParameters().isEmpty());
}
@Override
public void stateChanged(ChangeEvent event) {
populateTemplatePage();
}
private void resetJDialog() {
templateList.setValues(TemplateManager.getInstance().reset().getTemplateNames()); // reload templates
this.setContentPane(templateSelectionPanel());
this.revalidate();
}
private void populateTemplatePage() {
String selectedTemplate = templateList.getText();
@ -230,6 +349,65 @@ public class SelectTemplatesDialog extends JDialog implements ChangeListener, Ac
: JMeterUtils.getResString("template_merge_from") );
}
/**
* @param parameters {@link Map} parameters map
* @return JPanel from parameters
*/
private JPanel configureParametersPanel(Map<String, String> parameters) {
JPanel panel = new JPanel(new BorderLayout());
JPanel northPanel = new JPanel(new FlowLayout());
JLabel label = new JLabel(JMeterUtils.getResString("template_fill_parameters"));
label.setPreferredSize(new Dimension(150,35));
northPanel.add(label);
panel.add(northPanel, BorderLayout.NORTH);
parametersTextFields.clear();
GridBagConstraints gbc = new GridBagConstraints();
initConstraints(gbc);
int parameterCount = 0;
JPanel gridbagpanel = new JPanel(new GridBagLayout());
for (Entry<String, String> entry : parameters.entrySet()) {
String key = entry.getKey();
String value = entry.getValue();
JLabeledTextField paramLabel = new JLabeledTextField(key + " : ");
paramLabel.setText(value);
parametersTextFields.put(key, paramLabel);
gbc.gridy = parameterCount++;
List<JComponent> listedParamLabel = paramLabel.getComponentList();
gridbagpanel.add(listedParamLabel.get(0), gbc.clone());
gbc.gridx = 1;
gridbagpanel.add(listedParamLabel.get(1), gbc.clone());
gbc.gridx = 0;
}
JPanel actionBtnBarParameterPanel = new JPanel(new FlowLayout());
actionBtnBarParameterPanel.add(validateButton);
actionBtnBarParameterPanel.add(cancelButton);
actionBtnBarParameterPanel.add(previous);
JScrollPane scroller = new JScrollPane(gridbagpanel);
panel.add(scroller, BorderLayout.CENTER);
panel.add(actionBtnBarParameterPanel, BorderLayout.SOUTH);
return panel;
}
private void initConstraints(GridBagConstraints gbc) {
gbc.anchor = GridBagConstraints.WEST;
gbc.insets = new Insets(0,0,5,0);
gbc.fill = GridBagConstraints.NONE;
gbc.gridheight = 1;
gbc.gridwidth = 1;
gbc.gridx = 0;
gbc.gridy = 0;
gbc.weightx = 0;
gbc.weighty = 0;
}
@Override
public void hyperlinkUpdate(HyperlinkEvent e) {
if (e.getEventType() == HyperlinkEvent.EventType.ACTIVATED && java.awt.Desktop.isDesktopSupported()) {
@ -240,5 +418,4 @@ public class SelectTemplatesDialog extends JDialog implements ChangeListener, Ac
}
}
}
}

View File

@ -19,10 +19,12 @@
package org.apache.jmeter.gui.action.template;
import java.io.File;
import java.util.Map;
import java.util.Map.Entry;
/**
* Template Bean
* @since 2.10
* @since 2.10
*/
public class Template {
private boolean isTestPlan;
@ -30,6 +32,7 @@ public class Template {
private String fileName;
private String description;
private transient File parent; // for relative links
private Map<String, String> parameters;
/**
* @return the name
*/
@ -78,4 +81,116 @@ public class Template {
public void setParent(File parent) {
this.parent = parent;
}
public Map<String, String> getParameters() {
return parameters;
}
public void setParameters(Map<String, String> parameters) {
this.parameters = parameters;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((description == null) ? 0 : description.hashCode());
result = prime * result + ((fileName == null) ? 0 : fileName.hashCode());
result = prime * result + (isTestPlan ? 1231 : 1237);
result = prime * result + ((name == null) ? 0 : name.hashCode());
result = prime * result + ((parameters == null) ? 0 : parameters.hashCode());
result = prime * result + ((parent == null) ? 0 : parent.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
Template other = (Template) obj;
if (description == null) {
if (other.description != null) {
return false;
}
} else if (!description.equals(other.description)) {
return false;
}
if (fileName == null) {
if (other.fileName != null) {
return false;
}
} else if (!fileName.equals(other.fileName)) {
return false;
}
if (isTestPlan != other.isTestPlan) {
return false;
}
if (name == null) {
if (other.name != null) {
return false;
}
} else if (!name.equals(other.name)) {
return false;
}
if (!mapsEquals(parameters, other.parameters)) {
return false;
}
if (parent == null) {
if (other.parent != null) {
return false;
}
} else if (!parent.equals(other.parent)) {
return false;
}
return true;
}
private boolean mapsEquals(Map<String, String> map1, Map<String, String> map2) {
if(map1 == null) {
return map2 == null;
}else if(map2 == null) {
return false;
}
if(map1.size() != map2.size()) {
return false;
}
for(Entry<String, String> entry : map1.entrySet()) {
String key = entry.getKey();
String value = entry.getValue();
if(map2.containsKey(key)) {
if(!map2.get(key).equals(value)) {
return false;
}
}else {
return false;
}
}
return true;
}
/* (non-Javadoc)
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append("Template [isTestPlan=");
builder.append(isTestPlan);
builder.append(", name=");
builder.append(name);
builder.append(", fileName=");
builder.append(fileName);
builder.append(", description=");
builder.append(description);
builder.append(", parameters=");
builder.append(parameters);
builder.append("]");
return builder.toString();
}
}

View File

@ -19,9 +19,12 @@
package org.apache.jmeter.gui.action.template;
import java.io.File;
import java.util.LinkedHashMap;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.TreeMap;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
@ -29,24 +32,21 @@ import org.apache.commons.lang3.StringUtils;
import org.apache.jmeter.util.JMeterUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.thoughtworks.xstream.XStream;
import com.thoughtworks.xstream.io.StreamException;
import com.thoughtworks.xstream.io.xml.DomDriver;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.EntityResolver;
import org.xml.sax.ErrorHandler;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
/**
* Manages Test Plan templates
* @since 2.10
*/
public class TemplateManager {
// Created by XStream reading templates.xml
private static class Templates {
/*
* N.B. Must use LinkedHashMap for field type
* XStream creates a plain HashMap if one uses Map as the field type.
*/
private final LinkedHashMap<String, Template> templates = new LinkedHashMap<>();
}
private static final String TEMPLATE_FILES = JMeterUtils.getPropDefault("template.files", // $NON-NLS-1$
"/bin/templates/templates.xml");
@ -56,52 +56,12 @@ public class TemplateManager {
private final Map<String, Template> allTemplates;
private final XStream xstream = initXStream();
public static TemplateManager getInstance() {
return SINGLETON;
}
private TemplateManager() {
allTemplates = readTemplates();
}
private XStream initXStream() {
XStream xstream = new XStream(new DomDriver(){
/**
* Create the DocumentBuilderFactory instance.
* See https://blog.compass-security.com/2012/08/secure-xml-parser-configuration/
* See https://github.com/x-stream/xstream/issues/25
* @return the new instance
*/
@Override
protected DocumentBuilderFactory createDocumentBuilderFactory() {
final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
try {
factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
} catch (ParserConfigurationException e) {
throw new StreamException(e);
}
factory.setExpandEntityReferences(false);
return factory;
}
});
JMeterUtils.setupXStreamSecurityPolicy(xstream);
xstream.alias("template", Template.class);
xstream.alias("templates", Templates.class);
xstream.useAttributeFor(Template.class, "isTestPlan");
// templates i
xstream.addImplicitMap(Templates.class,
// field TemplateManager#templates
"templates", // $NON-NLS-1$
Template.class,
// field Template#name
"name" // $NON-NLS-1$
);
return xstream;
allTemplates = readTemplates();
}
public void addTemplate(Template template) {
@ -120,26 +80,26 @@ public class TemplateManager {
}
/**
* @return the templates names
* @return the templates names sorted in alphabetical order
*/
public String[] getTemplateNames() {
return allTemplates.keySet().toArray(new String[allTemplates.size()]);
}
private Map<String, Template> readTemplates() {
final Map<String, Template> temps = new LinkedHashMap<>();
final Map<String, Template> temps = new TreeMap<>();
final String[] templateFiles = TEMPLATE_FILES.split(",");
for (String templateFile : templateFiles) {
if(!StringUtils.isEmpty(templateFile)) {
final File f = new File(JMeterUtils.getJMeterHome(), templateFile);
final File file = new File(JMeterUtils.getJMeterHome(), templateFile);
try {
if(f.exists() && f.canRead()) {
if(file.exists() && file.canRead()) {
if (log.isInfoEnabled()) {
log.info("Reading templates from: {}", f.getAbsolutePath());
log.info("Reading templates from: {}", file.getAbsolutePath());
}
final File parent = f.getParentFile();
final LinkedHashMap<String, Template> templates = ((Templates) xstream.fromXML(f)).templates;
Map<String, Template> templates = parseTemplateFile(file);
final File parent = file.getParentFile();
for(Template t : templates.values()) {
if (!t.getFileName().startsWith("/")) {
t.setParent(parent);
@ -149,12 +109,12 @@ public class TemplateManager {
} else {
if (log.isWarnEnabled()) {
log.warn("Ignoring template file:'{}' as it does not exist or is not readable",
f.getAbsolutePath());
file.getAbsolutePath());
}
}
} catch(Exception ex) {
if (log.isWarnEnabled()) {
log.warn("Ignoring template file:'{}', an error occurred parsing the file", f.getAbsolutePath(),
log.warn("Ignoring template file:'{}', an error occurred parsing the file", file.getAbsolutePath(),
ex);
}
}
@ -162,6 +122,100 @@ public class TemplateManager {
}
return temps;
}
public final class LoggingErrorHandler implements ErrorHandler {
private Logger logger;
private File file;
public LoggingErrorHandler(Logger logger, File file) {
this.logger = logger;
this.file = file;
}
@Override
public void error(SAXParseException ex) throws SAXException {
throw ex;
}
@Override
public void fatalError(SAXParseException ex) throws SAXException {
throw ex;
}
@Override
public void warning(SAXParseException ex) throws SAXException {
logger.warn("Warning parsing file {}", file, ex);
}
}
public static class DefaultEntityResolver implements EntityResolver {
public DefaultEntityResolver() {
super();
}
public InputSource resolveEntity(String publicId, String systemId) throws SAXException, IOException {
if(systemId.endsWith("templates.dtd")) {
return new InputSource(TemplateManager.class.getResourceAsStream("/org/apache/jmeter/gui/action/template/templates.dtd"));
} else {
return null;
}
}
}
public Map<String, Template> parseTemplateFile(File file) throws IOException, SAXException, ParserConfigurationException{
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setValidating(true);
dbf.setNamespaceAware(true);
dbf.setFeature("http://xml.org/sax/features/external-general-entities", false);
dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
DocumentBuilder bd = dbf.newDocumentBuilder();
bd.setEntityResolver(new DefaultEntityResolver());
LoggingErrorHandler errorHandler = new LoggingErrorHandler(log, file);
bd.setErrorHandler(errorHandler);
Document document = bd.parse(file.getAbsolutePath());
document.getDocumentElement().normalize();
Map<String, Template> templates = new TreeMap<>();
NodeList templateNodes = document.getElementsByTagName("template");
for (int i = 0; i < templateNodes.getLength(); i++) {
Node node = templateNodes.item(i);
parseTemplateNode(templates, node);
}
return templates;
}
/**
* @param templates Map of {@link Template} referenced by name
* @param templateNode {@link Node} the xml template node
*/
void parseTemplateNode(Map<String, Template> templates, Node templateNode) {
if (templateNode.getNodeType() == Node.ELEMENT_NODE) {
Template template = new Template();
Element element = (Element) templateNode;
template.setTestPlan("true".equals(element.getAttribute("isTestPlan")));
template.setName(textOfFirstTag(element, "name"));
template.setDescription(textOfFirstTag(element, "description"));
template.setFileName(textOfFirstTag(element, "fileName"));
NodeList nl = element.getElementsByTagName("parameters");
if(nl.getLength()>0) {
NodeList parameterNodes = ((Element) nl.item(0)).getElementsByTagName("parameter");
Map<String, String> parameters = parseParameterNodes(parameterNodes);
template.setParameters(parameters);
}
templates.put(template.getName(), template);
}
}
private String textOfFirstTag(Element element, String tagName) {
return element.getElementsByTagName(tagName).item(0).getTextContent();
}
private Map<String, String> parseParameterNodes(NodeList parameterNodes) {
Map<String, String> parametersMap = new HashMap<>();
for (int i = 0; i < parameterNodes.getLength(); i++) {
Element element = (Element) parameterNodes.item(i);
parametersMap.put(element.getAttribute("key"), element.getAttribute("defaultValue"));
}
return parametersMap;
}
/**
* @param selectedTemplate Template name

View File

@ -0,0 +1,37 @@
<!--
Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You under the Apache License, Version 2.0
(the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<!-- Basic DTD for templates.xml -->
<!ELEMENT templates (template+)>
<!ELEMENT template (name, fileName, description, parameters?)>
<!-- Whether the template is a complete test plan or not -->
<!ATTLIST template isTestPlan (true|false) #REQUIRED>
<!ELEMENT name (#PCDATA) >
<!ELEMENT fileName (#PCDATA) >
<!ELEMENT description (#PCDATA) >
<!ELEMENT parameters (parameter*)>
<!ELEMENT parameter EMPTY>
<!ATTLIST parameter key CDATA #REQUIRED>
<!ATTLIST parameter defaultValue CDATA #REQUIRED>

View File

@ -1206,6 +1206,7 @@ teardown_on_shutdown=Run tearDown Thread Groups after shutdown of main threads
template_choose=Select Template
template_create_from=Create
template_field=Template ($i$ where i is capturing group number, starts at 1):
template_fill_parameters=Fill your parameters\:
template_load?=Load template?
template_menu=Templates...
template_merge_from=Merge

View File

@ -1195,6 +1195,7 @@ teardown_on_shutdown=Ex\u00E9cuter le Groupe d'unit\u00E9s de fin m\u00EAme apr\
template_choose=Choisir le mod\u00E8le
template_create_from=Cr\u00E9er
template_field=Canevas \:
template_fill_parameters=Remplir les param\u00E8tres \:
template_load?=Charger le mod\u00E8le ?
template_menu=Mod\u00E8les...
template_merge_from=Fusionner

View File

@ -0,0 +1,85 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package org.apache.jmeter.util;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import freemarker.template.Configuration;
import freemarker.template.TemplateException;
import freemarker.template.TemplateExceptionHandler;
/**
* Class used to process freemarkers templates
* @since 5.1
*/
public final class TemplateUtil {
private static Configuration templateConfiguration = init();
private TemplateUtil() {
super();
}
private static Configuration init() {
Configuration templateConfiguration = new Configuration(Configuration.getVersion());
templateConfiguration.setDefaultEncoding(StandardCharsets.UTF_8.name());
templateConfiguration.setInterpolationSyntax(Configuration.SQUARE_BRACKET_INTERPOLATION_SYNTAX);
templateConfiguration.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER);
return templateConfiguration;
}
/**
* Give a basic templateConfiguration
* @return a Configuration
*/
public static Configuration getTemplateConfig() {
return templateConfiguration;
}
/**
* Process a given freemarker template and put its result in a new folder.
*
* @param template file that contains the freemarker template to process
* @param outputFile {@link File} created from template
* @param templateConfig Configuration of the template
* @param data to inject in the template
* @throws IOException if an I/O exception occurs during writing to the writer
* @throws TemplateException if an exception occurs during template processing
*/
public static void processTemplate(File template,
File outputFile,
Configuration templateConfig, Map<String, String> data)
throws IOException, TemplateException {
templateConfig.setDirectoryForTemplateLoading(template.getParentFile());
freemarker.template.Template temp = templateConfig.getTemplate(template.getName());
try (FileOutputStream stream = new FileOutputStream(outputFile);
Writer writer = new OutputStreamWriter(stream, StandardCharsets.UTF_8);
BufferedWriter bufferedWriter = new BufferedWriter(writer)){
temp.process(data, bufferedWriter);
}
}
}

View File

@ -0,0 +1,29 @@
<?xml version="1.0"?>
<!DOCTYPE templates SYSTEM "templates.dtd">
<templates>
<template isTestPlan="true">
<name>Buggy</name>
<fileName>/bin/templates/testTemplate.jmx.fmkr</fileName>
<description><![CDATA[
<h1>Test</h1>
<h2>Overview</h2>
This is a sample BeanShell sampler which shows how to use some of its
features.
<br />
Please select a suitable location in the tree before merging.
<h2>Useful links</h2>
<ul>
<li>
<a
href="http://jmeter.apache.org/usermanual/component_reference.html#BeanShell_Sampler">
http://jmeter.apache.org/usermanual/component_reference.html#BeanShell_Sampler
</a>
</li>
</ul>
]]></description>
<parameters>
<key>citrixPortalHost</key>
<defaultValue>https://foo.com</defaultValue>
</parameters>
</template>
</templates>

View File

@ -0,0 +1,23 @@
<!DOCTYPE templates SYSTEM "templates.dtd">
<templates>
<template isTestPlan="false">
<name>testTemplateNotTestPlan</name>
<fileName>/bin/templates/testTemplateNotTestPlan.jmx</fileName>
<description>testTemplateNotTestPlan desc</description>
</template>
<template isTestPlan="true">
<name>testTemplate</name>
<fileName>/bin/templates/testTemplate.jmx</fileName>
<description>testTemplate desc</description>
</template>
<template isTestPlan="true">
<name>testTemplateWithParameters</name>
<fileName>/bin/templates/testTemplate.jmx.fmkr</fileName>
<description>Template with parameters</description>
<parameters>
<parameter key="testKey1" defaultValue="n 1" />
<parameter key="testKey2" defaultValue="n 2" />
<parameter key="testKey3" defaultValue="n 3" />
</parameters>
</template>
</templates>

View File

@ -0,0 +1,101 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package org.apache.jmeter.gui.action.template;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.Map;
import javax.xml.parsers.ParserConfigurationException;
import org.apache.jmeter.junit.JMeterTestCase;
import org.junit.Test;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
/**
* Test TemplateManager Class
*/
public class TestTemplateManager extends JMeterTestCase {
/**
* Test a valid templateFile.
*/
@Test
public void testTemplateFile() throws IOException, SAXException, ParserConfigurationException {
File xmlTemplate = new File(this.getClass().getResource("validTemplates.xml").getFile());
TemplateManager templateManager = TemplateManager.getInstance();
Map<String, Template> templateMap = templateManager.parseTemplateFile(xmlTemplate);
assertEquals(3, templateMap.size());
Template testTemplate = templateMap.get("testTemplateWithParameters");
assertTrue(testTemplate.isTestPlan());
assertEquals("testTemplateWithParameters", testTemplate.getName());
assertEquals("/bin/templates/testTemplate.jmx.fmkr", testTemplate.getFileName());
assertEquals("Template with parameters", testTemplate.getDescription());
Map<String, String> testTemplateParameters = testTemplate.getParameters();
assertEquals("n 1", testTemplateParameters.get("testKey1"));
assertEquals("n 2", testTemplateParameters.get("testKey2"));
assertEquals("n 3", testTemplateParameters.get("testKey3"));
testTemplate = templateMap.get("testTemplateNotTestPlan");
assertFalse(testTemplate.isTestPlan());
assertEquals("testTemplateNotTestPlan", testTemplate.getName());
assertEquals("/bin/templates/testTemplateNotTestPlan.jmx", testTemplate.getFileName());
assertEquals("testTemplateNotTestPlan desc", testTemplate.getDescription());
assertNull(testTemplate.getParameters());
testTemplate = templateMap.get("testTemplate");
assertTrue(testTemplate.isTestPlan());
assertEquals("testTemplate", testTemplate.getName());
assertEquals("/bin/templates/testTemplate.jmx", testTemplate.getFileName());
assertEquals("testTemplate desc", testTemplate.getDescription());
assertNull(testTemplate.getParameters());
}
/**
* Check that a wrong xml file throws a FileNotFoundException
*/
@Test(expected = FileNotFoundException.class)
public void testInvalidTemplateFile() throws Exception {
String xmlTemplatePath = "missing.xml";
File templateFile = new File(xmlTemplatePath);
TemplateManager templateManager = TemplateManager.getInstance();
templateManager.parseTemplateFile(templateFile);
}
@Test
public void testInvalidTemplateXml() throws IOException, SAXException, ParserConfigurationException {
try {
String xmlTemplatePath = this.getClass().getResource("invalidTemplates.xml").getFile();
File templateFile = new File(xmlTemplatePath);
TemplateManager templateManager = TemplateManager.getInstance();
templateManager.parseTemplateFile(templateFile);
} catch (SAXParseException ex) {
assertTrue("Exception did not contains expected message, got:"+ex.getMessage(),
ex.getMessage().indexOf("Element type \"key\" must be declared.")>=0);
}
}
}

View File

@ -125,6 +125,7 @@ of previous time slot as a base. Starting with this version, each time slot is i
<li><pr>411</pr>Use <code>SHA-1</code> instead of <code>SHA1</code> in <code>org.apache.jmeter.save.SaveService</code>. Contributed by Paco (paco.xu at daocloud.io)</li>
<li><bug>62914</bug>Add a hint in Thread Group UI about duration of test</li>
<li><bug>62925</bug>Add support for ThreadDump to the JMeter non-GUI</li>
<li><bug>62870</bug>Templates : Add ability to provide parameters. Contributed by Ubik Load Pack (support at ubikloadpack.com)</li>
<li><bug>62829</bug>Allow specifying Proxy server scheme for HTTP request sampler, Advanced tab and command line option. Contributed by Hitesh Patel (hitesh.h.patel at gmail.com)</li>
</ul>

View File

@ -0,0 +1,110 @@
<?xml version="1.0"?>
<!-- Licensed to the Apache Software Foundation (ASF) under one or more contributor
license agreements. See the NOTICE file distributed with this work for additional
information regarding copyright ownership. The ASF licenses this file to
You under the Apache License, Version 2.0 (the "License"); you may not use
this file except in compliance with the License. You may obtain a copy of
the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required
by applicable law or agreed to in writing, software distributed under the
License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS
OF ANY KIND, either express or implied. See the License for the specific
language governing permissions and limitations under the License. -->
<!DOCTYPE document[
<!ENTITY hellip "&#x02026;" >
]>
<document id="$Id$">
<properties>
<title>User guide: Customizables templates</title>
</properties>
<body>
<section name="Customizable template">
<p>
This document describes how to create a customizable template.
</p>
<subsection name="1 Folder structure" anchor="folder_structure">
<p>
The template feature uses the bin/templates folder which contains :<br/>
<ul>
<li>templates.xml, the file where you declare the templates you want to be able to use</li>
<li>some .jmx and .jmx.fmkr files which are the templates</li>
</ul>
</p>
<p>Here is how it looks like:
<figure image="templates/template_folder.png">Figure 1 - template folder</figure>
</p>
</subsection>
<subsection name="2 Template declaration" anchor="template_creation">
<subsection name="2.1 Basic template declaration" anchor="basic_template_declaration">
<p>
First of all you must declare your template. To do that, look into the templates.xml file.<br/>
This file respect a DTD <br/>
Below is the already existing Recording template declaration inside the templates.xml :
<figure image="templates/templates_xml.png">Figure 2 - recording template declaration</figure>
A template declaration is made as follow :<br/>
<ul>
<li><code>template</code> element which contains the informations described in the followings tags</li>
<li><code>name</code> element which contains the template name the user will see</li>
<li><code>fileName</code> element which contains the relative path of the template.</li>
<li><code>description</code> element which uses html to describe the template</li>
<li><code>optional</code> parameters tag (will be discussed later)</li>
</ul>
</p>
</subsection>
<subsection name="2.2 Customizable template declaration" anchor="customizable_template_declaration">
<p>
Let's say we want the exact same Recording template as in the 2.1 section, but we want to choose the name
of the xml file where the recording of view result tree will be saved.<br/><br/>
To do so we will use the parameters tag to tell JMeter to ask the user about a name for the concerned file :
<figure image="templates/templates_xml_parameters.png">Figure 3 - recording template with parameters</figure>
<note>You can put as many parameter tags as you want in the parameters tag.</note>
Let's see what changed here.<br/>
Firstly, customs templates are <code>.jmx.fmkr</code> files and not only <code>.jmx</code>.<br/><br/>
Lastly, we added a <code>parameters</code> tag.<br/> As you can see in the image, a <code>parameters</code> tag contains <code>parameter</code> tags.<br/>
Parameter tags are empty and contains 2 attributes :
<ul>
<li><code>key</code> is the name of the parameter you will ask the user to fill.</li>
<li><code>defaultValue</code> is as its name says, the default value the user will see for the parameter.</li>
</ul>
</p>
</subsection>
</subsection>
<subsection name="3 Template file" anchor="template_file">
<subsection name="3.1 Basic template file" anchor="basic_template_file">
<p>
The template file is the one you used in the fileName tag when you declared your template.
A template file is just the saving of a JMeter test plan.
</p>
</subsection>
<subsection name="3.2 Customizable template file" anchor="customizable_template_file">
<p>
In the 2.2 section we saw that a custom template file is a .jmx.fmkr file.<br/>
The single difference between them is the .jmx.fmkr will be analyzed by JMeter to<br/>
detect customs tag. If a custom tag is found, JMeter will try to replace it by the corresponding
given value from the user.<br/><br/>
A custom tag is defined as follow :
<source><![CDATA[[=<key>]]]></source>
This is based on <a href="https://freemarker.apache.org/docs/dgui_misc_alternativesyntax.html#dgui_misc_alternativesyntax_interpolation">Freemarker alternative Interpolation syntax</a>.
Let's illustrate how it works with an example.<br/>
Consider the following part of the recording.jmx template file : <br/>
<figure image="templates/template_recording_filename.png">Figure 4 - recording.jmx save file</figure>
The surrounded area correspond to the name of the xml file where the View Results Tree output will be saved.
As it is, when you use the template you will always have the same saving filename : <code>recording.xml</code>.<br/><br/>
To make it customizable, change your recording template declaration in the templates.xml files by the<br/>
one shown in the 2.2 section. Then, rename the recording.jmx file to recording.jmx.fmkr.<br/><br/>
When it's done, Change the above selected line by this one :
<source><![CDATA[<stringProp name="filename">[=xmlFileName]</stringProp>]]></source>
<br/>
It's over ! With this configuration, if you chose to use the recording template, JMeter will ask you
a xmlFileName (correspond to the key value in the declaration).
<figure image="templates/template_parameters_window.png">Figure 5 - JMeter asks you the value you want to put for the key</figure>
Then, you will find the expected value in the created template where you placed the [=xmlFileName] tag :
<figure image="templates/template_recording_retrieved_value.png">Figure 6 - the value changed</figure>
</p>
</subsection>
</subsection>
</section>
</body>
</document>

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1015 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB