package org.ff4j.conf;
/*
* #%L
* ff4j-core
* %%
* Copyright (C) 2013 Ff4J
* %%
* Licensed 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.
* #L%
*/
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.ff4j.core.Feature;
import org.ff4j.core.FlippingStrategy;
import org.ff4j.property.Property;
import org.ff4j.property.PropertyString;
import org.ff4j.utils.MappingUtil;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.NodeList;
/**
* Allow to parse XML files to load {@link Feature}.
*
* @author Cedrick Lunven (@clunven)
*/
public final class XmlParser {
/** TAG XML. */
public static final String FEATURES_TAG = "features";
/** TAG XML. */
public static final String FEATURE_TAG = "feature";
/** TAG XML. */
public static final String FEATURE_ATT_UID = "uid";
/** TAG XML. */
public static final String FEATURE_ATT_DESC = "description";
/** TAG XML. */
public static final String FEATURE_ATT_ENABLE = "enable";
/** TAG XML. */
public static final String FEATUREGROUP_TAG = "feature-group";
/** TAG XML. */
public static final String FEATUREGROUP_ATTNAME = "name";
/** TAG XML. */
public static final String FLIPSTRATEGY_TAG = "flipstrategy";
/** TAG XML. */
public static final String PROPERTIES_TAG = "properties";
/** TAG XML. */
public static final String PROPERTIES_CUSTOM_TAG = "custom-properties";
/** TAG XML. */
public static final String PROPERTY_TAG = "property";
/** TAG XML. */
public static final String PROPERTY_PARAMTYPE = "type";
/** TAG XML. */
public static final String PROPERTY_PARAMNAME = "name";
/** TAG XML. */
public static final String PROPERTY_PARAMDESCRIPTION = "description";
/** TAG XML. */
public static final String PROPERTY_PARAMVALUE = "value";
/** TAG XML. */
public static final String PROPERTY_PARAMFIXED_VALUES = "fixedValues";
/** TAG XML. */
public static final String FLIPSTRATEGY_ATTCLASS = "class";
/** TAG XML. */
public static final String FLIPSTRATEGY_PARAMTAG = "param";
/** TAG XML. */
public static final String FLIPSTRATEGY_PARAMNAME = "name";
/** TAG XML. */
public static final String FLIPSTRATEGY_PARAMVALUE = "value";
/** TAG XML. */
public static final String SECURITY_TAG = "security";
/** TAG XML. */
public static final String SECURITY_ROLE_TAG = "role";
/** TAG XML. */
public static final String SECURITY_ROLE_ATTNAME = "name";
/** TAG XML. */
public static final String CDATA_START = "<![CDATA[";
/** TAG XML. */
public static final String CDATA_END = "]]>";
/** XML Generation constants. */
private static final String ENCODING = "UTF-8";
/** XML Generation constants. */
private static final String XML_HEADER =
"<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n"//
+ "<ff4j xmlns=\"http://www.ff4j.org/schema/ff4j\""//
+ "\n xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\""//
+ "\n xsi:schemaLocation=\"http://www.ff4j.org/schema/ff4j http://ff4j.org/schema/ff4j-1.4.0.xsd\">"
+ ">\n\n";
/** XML Generation constants. */
private static final String XML_FEATURE = " <feature uid=\"{0}\" description=\"{1}\" enable=\"{2}\">\n";
/** XML Generation constants. */
private static final String XML_AUTH = " <role name=\"{0}\" />\n";
/** XML Generation constants. */
private static final String END_FEATURE = " </feature>\n\n";
/** XML Generation constants. */
private static final String BEGIN_FEATURES = " <features>\n\n";
/** XML Generation constants. */
private static final String END_FEATURES = " </features>\n\n";
/** XML Generation constants. */
private static final String BEGIN_PROPERTIES = " <properties>\n\n";
/** XML Generation constants. */
private static final String BEGIN_CUSTOMPROPERTIES = " <custom-properties>\n";
/** XML Generation constants. */
private static final String END_CUSTOMPROPERTIES = " </custom-properties>\n";
/** XML Generation constants. */
private static final String END_PROPERTIES = " </properties>\n\n";
/** XML Generation constants. */
private static final String END_FF4J = "</ff4j>\n\n";
public static final String ERROR_SYNTAX_IN_CONFIGURATION_FILE = "Error syntax in configuration file : ";
/** Document Builder use to parse XML. */
private static DocumentBuilder builder = null;
/**
* Parsing of XML Configuration file.
*
* @param file
* target file
* @return
* features and properties find within file
*/
public XmlConfig parseConfigurationFile(InputStream in) {
try {
// Object to be build by parsing
XmlConfig xmlConf = new XmlConfig();
// Load XML as a Document
Document ff4jDocument = getDocumentBuilder().parse(in);
// Features Tag
NodeList fList = ff4jDocument.getElementsByTagName(FEATURES_TAG);
if (fList.getLength() > 1) {
throw new IllegalArgumentException("Root Tag is 'features' and must be unique, please check");
} else if (fList.getLength() == 1) {
xmlConf.setFeatures(parseFeaturesTag( (Element) fList.item(0)));
}
// Properties Tag
NodeList pList = ff4jDocument.getElementsByTagName(PROPERTIES_TAG);
if (pList.getLength() > 1) {
throw new IllegalArgumentException("Root Tag is 'properties' and must be unique, please check");
} else if (pList.getLength() == 1) {
xmlConf.setProperties(parsePropertiesTag((Element) pList.item(0)));
}
return xmlConf;
} catch (Exception e) {
throw new IllegalArgumentException("Cannot parse XML data, please check file access ", e);
}
}
/**
* Load map of {@link Feature} from an inpustream (containing xml text).
*
* @param in
* inpustream with XML text
* @return the sorted map of features
* @throws IOException
* exception raised when reading inputstream
*/
public Map<String, Feature> parseFeaturesTag(Element featuresTag) {
Map<String, Feature> xmlFeatures = new LinkedHashMap<String, Feature>();
NodeList firstLevelNodes = featuresTag.getChildNodes();
for (int i = 0; i < firstLevelNodes.getLength(); i++) {
if (firstLevelNodes.item(i) instanceof Element) {
Element currentCore = (Element) firstLevelNodes.item(i);
if (FEATURE_TAG.equals(currentCore.getNodeName())) {
Feature singleFeature = parseFeatureTag(currentCore);
xmlFeatures.put(singleFeature.getUid(), singleFeature);
} else if (FEATUREGROUP_TAG.equals(currentCore.getNodeName())) {
xmlFeatures.putAll(parseFeatureGroupTag(currentCore));
} else {
throw new IllegalArgumentException("Invalid XML Format, Features sub nodes are [feature,feature-group]");
}
}
}
return xmlFeatures;
}
/**
* Parse TAG <feature-group>.
*
* @param featGroupTag
* feature group tag
* @return map of features
*/
private Map<String, Feature> parseFeatureGroupTag(Element featGroupTag) {
NamedNodeMap nnm = featGroupTag.getAttributes();
String groupName;
if (nnm.getNamedItem(FEATUREGROUP_ATTNAME) == null) {
throw new IllegalArgumentException("Error syntax in configuration featuregroup : must have 'name' attribute");
}
groupName = nnm.getNamedItem(FEATUREGROUP_ATTNAME).getNodeValue();
Map<String, Feature> groupFeatures = new HashMap<String, Feature>();
NodeList listOfFeat = featGroupTag.getElementsByTagName(FEATURE_TAG);
for (int k = 0; k < listOfFeat.getLength(); k++) {
Feature f = parseFeatureTag((Element) listOfFeat.item(k));
// Insert feature into group
f.setGroup(groupName);
groupFeatures.put(f.getUid(), f);
}
return groupFeatures;
}
/**
* Build a Feature from XML TAG.
*
* @param featXmlTag
* xml tag to nuild feature
* @return current feature
*/
private Feature parseFeatureTag(Element featXmlTag) {
NamedNodeMap nnm = featXmlTag.getAttributes();
// Identifier
String uid;
if (nnm.getNamedItem(FEATURE_ATT_UID) == null) {
throw new IllegalArgumentException(ERROR_SYNTAX_IN_CONFIGURATION_FILE + "'uid' is required for each feature");
}
uid = nnm.getNamedItem(FEATURE_ATT_UID).getNodeValue();
// Enable
if (nnm.getNamedItem(FEATURE_ATT_ENABLE) == null) {
throw new IllegalArgumentException(ERROR_SYNTAX_IN_CONFIGURATION_FILE
+ "'enable' is required for each feature (check " + uid + ")");
}
boolean enable = Boolean.parseBoolean(nnm.getNamedItem(FEATURE_ATT_ENABLE).getNodeValue());
// Create Feature with description
Feature f = new Feature(uid, enable, parseDescription(nnm));
// Strategy
NodeList flipStrategies = featXmlTag.getElementsByTagName(FLIPSTRATEGY_TAG);
if (flipStrategies.getLength() > 0) {
f.setFlippingStrategy(parseFlipStrategy((Element) flipStrategies.item(0), f.getUid()));
}
// Security
NodeList securities = featXmlTag.getElementsByTagName(SECURITY_TAG);
if (securities.getLength() > 0) {
f.setPermissions(parseListAuthorizations((Element) securities.item(0)));
}
// Properties
NodeList properties = featXmlTag.getElementsByTagName(PROPERTIES_CUSTOM_TAG);
if (properties.getLength() > 0) {
f.setCustomProperties(parsePropertiesTag((Element) properties.item(0)));
}
return f;
}
/**
* Parse Properties.
*
* @param properties tag
* @param uid
* current featureid
* @return
* properties map
*/
private Map < String , Property<?>> parsePropertiesTag(Element propertiesTag) {
Map< String , Property<?>> properties = new HashMap<String, Property<?>>();
// <properties>
NodeList lisOfProperties = propertiesTag.getElementsByTagName(PROPERTY_TAG);
for (int k = 0; k < lisOfProperties.getLength(); k++) {
// <property name='' value='' (type='') >
Element propertyTag = (Element) lisOfProperties.item(k);
NamedNodeMap attMap = propertyTag.getAttributes();
if (attMap.getNamedItem(PROPERTY_PARAMNAME) == null) {
throw new IllegalArgumentException("Invalid XML Syntax, 'name' is a required attribute of 'property' TAG");
}
if (attMap.getNamedItem(PROPERTY_PARAMVALUE) == null) {
throw new IllegalArgumentException("Invalid XML Syntax, 'value' is a required attribute of 'property' TAG");
}
String name = attMap.getNamedItem(PROPERTY_PARAMNAME).getNodeValue();
String value = attMap.getNamedItem(PROPERTY_PARAMVALUE).getNodeValue();
Property<?> ap = new PropertyString(name, value);
// If specific type defined ?
if (null != attMap.getNamedItem(PROPERTY_PARAMTYPE)) {
String optionalType = attMap.getNamedItem(PROPERTY_PARAMTYPE).getNodeValue();
// Substitution if relevant (e.g. 'int' -> 'org.ff4j.property.PropertyInt')
optionalType = MappingUtil.mapPropertyType(optionalType);
try {
// Constructor (String, String) is mandatory in Property interface
Constructor<?> constr = Class.forName(optionalType).getConstructor(String.class, String.class);
ap = (Property<?>) constr.newInstance(name, value);
} catch (Exception e) {
throw new IllegalArgumentException("Cannot instantiate '" + optionalType + "' check default constructor", e);
}
}
if (null != attMap.getNamedItem(PROPERTY_PARAMDESCRIPTION)) {
ap.setDescription(attMap.getNamedItem(PROPERTY_PARAMDESCRIPTION).getNodeValue());
}
// Is there any fixed Value ?
NodeList listOfFixedValue = propertyTag.getElementsByTagName(PROPERTY_PARAMFIXED_VALUES);
if (listOfFixedValue.getLength() != 0) {
Element fixedValueTag = (Element) listOfFixedValue.item(0);
NodeList listOfValues = fixedValueTag.getElementsByTagName(PROPERTY_PARAMVALUE);
for (int l = 0; l < listOfValues.getLength(); l++) {
Element valueTag = (Element) listOfValues.item(l);
ap.add2FixedValueFromString(valueTag.getTextContent());
}
}
// Check fixed value
if (ap.getFixedValues() != null && !ap.getFixedValues().contains(ap.getValue())) {
throw new IllegalArgumentException("Cannot create property <" + ap.getName() +
"> invalid value <" + ap.getValue() +
"> expected one of " + ap.getFixedValues());
}
properties.put(name, ap);
}
return properties;
}
/**
* Parsing strategy TAG.
*
* @param nnm
* current parend node
* @param uid
* current feature uid
* @return flipstrategy related to current feature.
*/
private FlippingStrategy parseFlipStrategy(Element flipStrategyTag, String uid) {
NamedNodeMap nnm = flipStrategyTag.getAttributes();
FlippingStrategy flipStrategy;
if (nnm.getNamedItem(FLIPSTRATEGY_ATTCLASS) == null) {
throw new IllegalArgumentException("Error syntax in configuration file : '" + FLIPSTRATEGY_ATTCLASS
+ "' is required for each flipstrategy (feature=" + uid + ")");
}
try {
// Attribute CLASS
String clazzName = nnm.getNamedItem(FLIPSTRATEGY_ATTCLASS).getNodeValue();
flipStrategy = (FlippingStrategy) Class.forName(clazzName).newInstance();
// LIST OF PARAMS
Map<String, String> parameters = new LinkedHashMap<String, String>();
NodeList initparamsNodes = flipStrategyTag.getElementsByTagName(FLIPSTRATEGY_PARAMTAG);
for (int k = 0; k < initparamsNodes.getLength(); k++) {
Element param = (Element) initparamsNodes.item(k);
NamedNodeMap nnmap = param.getAttributes();
// Check for required attribute name
String currentParamName;
if (nnmap.getNamedItem(FLIPSTRATEGY_PARAMNAME) == null) {
throw new IllegalArgumentException(ERROR_SYNTAX_IN_CONFIGURATION_FILE
+ "'name' is required for each param in flipstrategy(check " + uid + ")");
}
currentParamName = nnmap.getNamedItem(FLIPSTRATEGY_PARAMNAME).getNodeValue();
// Check for value attribute
if (nnmap.getNamedItem(FLIPSTRATEGY_PARAMVALUE) != null) {
parameters.put(currentParamName, nnmap.getNamedItem(FLIPSTRATEGY_PARAMVALUE).getNodeValue());
} else if (param.getFirstChild() != null) {
parameters.put(currentParamName, param.getFirstChild().getNodeValue());
} else {
throw new IllegalArgumentException("Parameter '" + currentParamName + "' in feature '" + uid
+ "' has no value, please check XML");
}
}
flipStrategy.init(uid, parameters);
} catch (Exception e) {
throw new IllegalArgumentException("An error occurs during flipstrategy parsing TAG" + uid, e);
}
return flipStrategy;
}
/**
* Parser target description.
*
* @param nnm
* current working tag
* @return description of the feature
*/
private static String parseDescription(NamedNodeMap nnm) {
String desc = null;
if (nnm.getNamedItem(FEATURE_ATT_DESC) != null) {
desc = nnm.getNamedItem(FEATURE_ATT_DESC).getNodeValue();
}
return desc;
}
/**
* Parsing autorization tag.
*
* @param featXmlTag
* current TAG
* @return list of authorizations.
*/
private static Set<String> parseListAuthorizations(Element securityTag) {
Set<String> authorizations = new TreeSet<String>();
NodeList lisOfAuth = securityTag.getElementsByTagName(SECURITY_ROLE_TAG);
for (int k = 0; k < lisOfAuth.getLength(); k++) {
Element role = (Element) lisOfAuth.item(k);
authorizations.add(role.getAttributes().getNamedItem(SECURITY_ROLE_ATTNAME).getNodeValue());
}
return authorizations;
}
/**
* Build {@link DocumentBuilder} to parse XML.
*
* @return current document builder.
* @throws ParserConfigurationException
* error during initialization
*/
public static DocumentBuilder getDocumentBuilder() throws ParserConfigurationException {
if (builder == null) {
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
// -- Prevent against XXE @see https://www.owasp.org/index.php/XML_External_Entity_(XXE)_Processing
dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
// If you can't completely disable DTDs, then at least do the following:
// Xerces 1 - http://xerces.apache.org/xerces-j/features.html#external-general-entities
// Xerces 2 - http://xerces.apache.org/xerces2-j/features.html#external-general-entities
// JDK7+ - http://xml.org/sax/features/external-general-entities
dbf.setFeature("http://xml.org/sax/features/external-general-entities", false);
// Xerces 1 - http://xerces.apache.org/xerces-j/features.html#external-parameter-entities
// Xerces 2 - http://xerces.apache.org/xerces2-j/features.html#external-parameter-entities
// JDK7+ - http://xml.org/sax/features/external-parameter-entities
dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
// Disable external DTDs as well
dbf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
// and these as well, per Timothy Morgan's 2014 paper: "XML Schema, DTD, and Entity Attacks" (see reference below)
dbf.setXIncludeAware(false);
dbf.setExpandEntityReferences(false);
builder = dbf.newDocumentBuilder();
builder.setErrorHandler(new XmlParserErrorHandler());
}
return builder;
}
/**
* Create XML output stream from a map of {@link Feature}.
*
* @param mapOfFeatures
* map of features
* @return streams
* @throws IOException
* error occurs when generating output
*/
public InputStream exportFeatures(Map<String, Feature> mapOfFeatures) throws IOException {
return new ByteArrayInputStream(exportFeaturesPart(mapOfFeatures).getBytes(ENCODING));
}
/**
* Create XML output stream from a map of {@link PropertyString}.
*
* @param mapOfProperties
* map of properties
* @return streams
* @throws IOException
* error occurs when generating output
*/
public InputStream exportProperties(Map < String, Property<?>> mapOfProperties) throws IOException {
return new ByteArrayInputStream(exportPropertiesPart(mapOfProperties).getBytes(ENCODING));
}
/**
* Create XML output stream with both {@link Feature} and {@link PropertyString}.
*
* @param f
* map of features
* @return streams
* @throws IOException
* error occurs when generating output
*/
public InputStream exportAll(Map<String, Feature> mapOfFeatures, Map < String, Property<?>> mapOfProperties) throws IOException {
// Create output
StringBuilder sb = new StringBuilder(XML_HEADER);
sb.append(exportFeaturesPart(mapOfFeatures));
sb.append(exportPropertiesPart(mapOfProperties));
sb.append(END_FF4J);
return new ByteArrayInputStream(sb.toString().getBytes(ENCODING));
}
/**
* Utility method to export from configuration.
*
* @param conf
* target configuration
* @return
* target stream
* @throws IOException
* error during marshalling
*/
public InputStream exportAll(XmlConfig conf) throws IOException {
return exportAll(conf.getFeatures(), conf.getProperties());
}
/**
* Create dedicated output for Properties.
*
* @param mapOfProperties
* target properties
* @return
* XML Flow
*/
private String exportPropertiesPart(Map < String, Property<?>> mapOfProperties) {
// Create <features>
StringBuilder sb = new StringBuilder(BEGIN_PROPERTIES);
if (mapOfProperties != null && !mapOfProperties.isEmpty()) {
sb.append(buildPropertiesPart(mapOfProperties));
}
sb.append(END_PROPERTIES);
return sb.toString();
}
/**
* Export Features part of the XML.
*
* @param mapOfFeatures
* current map of feaures.
*
* @return
* all XML
*/
private String exportFeaturesPart(Map<String, Feature> mapOfFeatures) {
// Create <features>
StringBuilder sb = new StringBuilder(BEGIN_FEATURES);
// Recreate Groups
Map<String, List<Feature>> featuresPerGroup = new HashMap<String, List<Feature>>();
if (mapOfFeatures != null && !mapOfFeatures.isEmpty()) {
for (Feature feat : mapOfFeatures.values()) {
String groupName = feat.getGroup();
if (!featuresPerGroup.containsKey(groupName)) {
featuresPerGroup.put(groupName, new ArrayList<Feature>());
}
featuresPerGroup.get(groupName).add(feat);
}
}
for (Map.Entry<String,List<Feature>> groupName : featuresPerGroup.entrySet()) {
/// Building featureGroup
if (null != groupName.getKey() && !groupName.getKey().isEmpty()) {
sb.append(" <" + FEATUREGROUP_TAG + " " + FEATUREGROUP_ATTNAME + "=\"" + groupName.getKey() + "\" >\n\n");
}
// Loop on feature
for (Feature feat : groupName.getValue()) {
sb.append(MessageFormat.format(XML_FEATURE, feat.getUid(), feat.getDescription(), feat.isEnable()));
// <security>
if (null != feat.getPermissions() && !feat.getPermissions().isEmpty()) {
sb.append(" <" + SECURITY_TAG + ">\n");
for (String auth : feat.getPermissions()) {
sb.append(MessageFormat.format(XML_AUTH, auth));
}
sb.append(" </" + SECURITY_TAG + ">\n");
}
// <flipstrategy>
FlippingStrategy fs = feat.getFlippingStrategy();
if (null != fs) {
sb.append(" <" + FLIPSTRATEGY_TAG + " class=\"" + fs.getClass().getCanonicalName() + "\" >\n");
for (String p : fs.getInitParams().keySet()) {
sb.append(" <" + FLIPSTRATEGY_PARAMTAG + " " + FLIPSTRATEGY_PARAMNAME + "=\"");
sb.append(p);
sb.append("\" " + FLIPSTRATEGY_PARAMVALUE + "=\"");
// Escape special characters to build XML
// https://github.com/clun/ff4j/issues/63
String paramValue = fs.getInitParams().get(p);
sb.append(escapeXML(paramValue));
sb.append("\" />\n");
}
sb.append(" </" + FLIPSTRATEGY_TAG + ">\n");
}
// <custom-properties>
Map < String, Property<?>> props = feat.getCustomProperties();
if (props != null && !props.isEmpty()) {
sb.append(BEGIN_CUSTOMPROPERTIES);
sb.append(buildPropertiesPart(feat.getCustomProperties()));
sb.append(END_CUSTOMPROPERTIES);
}
sb.append(END_FEATURE);
}
if (null != groupName.getKey() && !groupName.getKey().isEmpty()) {
sb.append(" </" + FEATUREGROUP_TAG + ">\n\n");
}
}
sb.append(END_FEATURES);
return sb.toString();
}
/**
* Create XML content of the properties or custom properties elements.
*
* @param props
* properties elements.
* @return
*/
private String buildPropertiesPart(Map < String, Property<?>> props) {
StringBuilder sb = new StringBuilder();
if (props != null && !props.isEmpty()) {
// Loop over property
for (Property<?> property : props.values()) {
sb.append(" <" + PROPERTY_TAG + " " + PROPERTY_PARAMNAME + "=\"" + property.getName() + "\" ");
sb.append(PROPERTY_PARAMVALUE + "=\"" + property.asString() + "\" ");
if (!(property instanceof PropertyString)) {
sb.append(PROPERTY_PARAMTYPE + "=\"" + property.getClass().getCanonicalName() + "\"");
}
// Processing fixedValue is present
if (property.getFixedValues() != null && !property.getFixedValues().isEmpty()) {
sb.append(">\n");
sb.append(" <fixedValues>\n");
for (Object o : property.getFixedValues()) {
sb.append(" <value>" + o.toString() + "</value>\n");
}
sb.append(" </fixedValues>\n");
sb.append(" </property>\n");
} else {
sb.append("/>\n");
}
}
}
return sb.toString();
}
/**
* Substitution to create XML.
*
* @param value
* target XML
* @return
*/
public String escapeXML(String value) {
if (value == null) {
return null;
}
return value.replaceAll("&", "&")
.replaceAll(">", ">")
.replaceAll("<", "<");
}
}