groovy.xml.MarkupBuilder Maven / Gradle / Ivy
/*
* Copyright 2003-2013 the original author or authors.
*
* 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.
*/
package groovy.xml;
import groovy.lang.Closure;
import groovy.util.BuilderSupport;
import groovy.util.IndentPrinter;
import org.codehaus.groovy.runtime.StringGroovyMethods;
import java.io.PrintWriter;
import java.io.Writer;
import java.util.Map;
import java.util.Iterator;
/**
* A helper class for creating XML or HTML markup.
* The builder supports various 'pretty printed' formats.
*
* Example:
*
new MarkupBuilder().root {
* a( a1:'one' ) {
* b { mkp.yield( '3 < 5' ) }
* c( a2:'two', 'blah' )
* }
* }
* Will print the following to System.out:
* <root>
* <a a1='one'>
* <b>3 < 5</b>
* <c a2='two'>blah</c>
* </a>
* </root>
* Notes:
*
* mkp
is a special namespace used to escape
* away from the normal building mode of the builder and get access
* to helper markup methods such as 'yield' and 'yieldUnescaped'.
* See the javadoc for {@link #getMkp()} for further details.
* - Note that tab, newline and carriage return characters are escaped within attributes, i.e. will become ,
and
respectively
*
* @author James Strachan
* @author Stefan Matthias Aust
* @author Scott Stirling
* @author Paul King
*/
public class MarkupBuilder extends BuilderSupport {
private IndentPrinter out;
private boolean nospace;
private int state;
private boolean nodeIsEmpty = true;
private boolean useDoubleQuotes = false;
private boolean omitNullAttributes = false;
private boolean omitEmptyAttributes = false;
private boolean expandEmptyElements = false;
private boolean escapeAttributes = true;
/**
* Returns the escapeAttributes property value.
*
* @return the escapeAttributes property value
* @see #setEscapeAttributes(boolean)
*/
public boolean isEscapeAttributes() {
return escapeAttributes;
}
/**
* Defaults to true. If set to false then you must escape any special
* characters within attribute values such as '&', '<', CR/LF, single
* and double quotes etc. manually as needed. The builder will not guard
* against producing invalid XML when in this mode and the output may not
* be able to be parsed/round-tripped but it does give you full control when
* producing for instance HTML output.
*
* @param escapeAttributes the new value
*/
public void setEscapeAttributes(boolean escapeAttributes) {
this.escapeAttributes = escapeAttributes;
}
/**
* Prints markup to System.out
*
* @see IndentPrinter#IndentPrinter()
*/
public MarkupBuilder() {
this(new IndentPrinter());
}
/**
* Sends markup to the given PrintWriter
*
* @param pw the PrintWriter to use
* @see IndentPrinter#IndentPrinter(Writer)
*/
public MarkupBuilder(PrintWriter pw) {
this(new IndentPrinter(pw));
}
/**
* Sends markup to the given Writer but first wrapping it in a PrintWriter
*
* @param writer the writer to use
* @see IndentPrinter#IndentPrinter(Writer)
*/
public MarkupBuilder(Writer writer) {
this(new IndentPrinter(new PrintWriter(writer)));
}
/**
* Sends markup to the given IndentPrinter. Use this option if you want
* to customize the indent used or provide your own IndentPrinter.
*
* @param out the IndentPrinter to use
*/
public MarkupBuilder(IndentPrinter out) {
this.out = out;
}
/**
* Returns true
if attribute values are output with
* double quotes; false
if single quotes are used.
* By default, single quotes are used.
*
* @return true if double quotes are used for attributes
*/
public boolean getDoubleQuotes() {
return this.useDoubleQuotes;
}
/**
* Sets whether the builder outputs attribute values in double
* quotes or single quotes.
*
* @param useDoubleQuotes If this parameter is true
,
* double quotes are used; otherwise, single quotes are.
*/
public void setDoubleQuotes(boolean useDoubleQuotes) {
this.useDoubleQuotes = useDoubleQuotes;
}
/**
* Determine whether null attributes will appear in the produced markup.
*
* @return true
, if null attributes will be
* removed from the resulting markup.
*/
public boolean isOmitNullAttributes() {
return omitNullAttributes;
}
/**
* Allows null attributes to be removed from the generated markup.
*
* @param omitNullAttributes if true
, null
* attributes will not be included in the resulting markup.
* If false
null attributes will be included in the
* markup as empty strings regardless of the omitEmptyAttribute
* setting. Defaults to false
.
*/
public void setOmitNullAttributes(boolean omitNullAttributes) {
this.omitNullAttributes = omitNullAttributes;
}
/**
* Determine whether empty attributes will appear in the produced markup.
*
* @return true
, if empty attributes will be
* removed from the resulting markup.
*/
public boolean isOmitEmptyAttributes() {
return omitEmptyAttributes;
}
/**
* Allows empty attributes to be removed from the generated markup.
*
* @param omitEmptyAttributes if true
, empty
* attributes will not be included in the resulting markup.
* Defaults to false
.
*/
public void setOmitEmptyAttributes(boolean omitEmptyAttributes) {
this.omitEmptyAttributes = omitEmptyAttributes;
}
/**
* Whether empty elements are expanded from <tagName/> to <tagName></tagName>.
*
* @return true
, if empty elements will be represented by an opening tag
* followed immediately by a closing tag.
*/
public boolean isExpandEmptyElements() {
return expandEmptyElements;
}
/**
* Whether empty elements are expanded from to .
*
* @param expandEmptyElements if true
, empty
* elements will be represented by an opening tag
* followed immediately by a closing tag.
* Defaults to false
.
*/
public void setExpandEmptyElements(boolean expandEmptyElements) {
this.expandEmptyElements = expandEmptyElements;
}
protected IndentPrinter getPrinter() {
return this.out;
}
protected void setParent(Object parent, Object child) {
}
/**
* Property that may be called from within your builder closure to access
* helper methods, namely {@link MarkupBuilderHelper#yield(String)},
* {@link MarkupBuilderHelper#yieldUnescaped(String)},
* {@link MarkupBuilderHelper#pi(Map)},
* {@link MarkupBuilderHelper#xmlDeclaration(Map)} and
* {@link MarkupBuilderHelper#comment(String)}.
*
* @return this MarkupBuilder
*/
public Object getMkp() {
return new MarkupBuilderHelper(this);
}
/**
* Produce an XML processing instruction in the output.
* For example:
*
* mkp.pi("xml-stylesheet":[href:"mystyle.css", type:"text/css"])
*
*
* @param args a map with a single entry whose key is the name of the
* processing instruction and whose value is the attributes
* for the processing instruction.
*/
void pi(Map> args) {
Iterator>> iterator = args.entrySet().iterator();
if (iterator.hasNext()) {
Map.Entry> mapEntry = iterator.next();
createNode("?" + mapEntry.getKey(), mapEntry.getValue());
state = 2;
out.println("?>");
}
}
void yield(String value, boolean escaping) {
if (state == 1) {
state = 2;
this.nodeIsEmpty = false;
out.print(">");
}
if (state == 2 || state == 3) {
out.print(escaping ? escapeElementContent(value) : value);
}
}
protected Object createNode(Object name) {
Object theName = getName(name);
toState(1, theName);
this.nodeIsEmpty = true;
return theName;
}
protected Object createNode(Object name, Object value) {
Object theName = getName(name);
if (value == null) {
return createNode(theName);
} else {
toState(2, theName);
this.nodeIsEmpty = false;
out.print(">");
out.print(escapeElementContent(value.toString()));
return theName;
}
}
protected Object createNode(Object name, Map attributes, Object value) {
Object theName = getName(name);
toState(1, theName);
for (Object p : attributes.entrySet()) {
Map.Entry entry = (Map.Entry) p;
Object attributeValue = entry.getValue();
boolean skipNull = attributeValue == null && omitNullAttributes;
boolean skipEmpty = attributeValue != null && omitEmptyAttributes &&
attributeValue.toString().length() == 0;
if (!skipNull && !skipEmpty) {
out.print(" ");
// Output the attribute name,
print(entry.getKey().toString());
// Output the attribute value within quotes. Use whichever
// type of quotes are currently configured.
out.print(useDoubleQuotes ? "=\"" : "='");
print(attributeValue == null ? "" : escapeAttributes ? escapeAttributeValue(attributeValue.toString()) : attributeValue.toString());
out.print(useDoubleQuotes ? "\"" : "'");
}
}
if (value != null) {
yield(value.toString(), true);
} else {
nodeIsEmpty = true;
}
return theName;
}
protected Object createNode(Object name, Map attributes) {
return createNode(name, attributes, null);
}
protected void nodeCompleted(Object parent, Object node) {
toState(3, node);
out.flush();
}
protected void print(Object node) {
out.print(node == null ? "null" : node.toString());
}
protected Object getName(String methodName) {
return super.getName(methodName);
}
/**
* Escapes a string so that it can be used directly as an XML
* attribute value.
*
* @param value The string to escape.
* @return A new string in which all characters that require escaping
* have been replaced with the corresponding XML entities.
* @see #escapeXmlValue(String, boolean)
*/
private String escapeAttributeValue(String value) {
return escapeXmlValue(value, true);
}
/**
* Escapes a string so that it can be used directly in XML element
* content.
*
* @param value The string to escape.
* @return A new string in which all characters that require escaping
* have been replaced with the corresponding XML entities.
* @see #escapeXmlValue(String, boolean)
*/
private String escapeElementContent(String value) {
return escapeXmlValue(value, false);
}
/**
* Escapes a string so that it can be used in XML text successfully.
* It replaces the following characters with the corresponding XML
* entities:
*
* - & as &
* - < as <
* - > as >
*
* If the string is to be added as an attribute value, these
* characters are also escaped:
*
* - ' as '
*
*
* @param value The string to escape.
* @param isAttrValue true
if the string is to be used
* as an attribute value, otherwise false
.
* @return A new string in which all characters that require escaping
* have been replaced with the corresponding XML entities.
*/
private String escapeXmlValue(String value, boolean isAttrValue) {
if (value == null)
throw new IllegalArgumentException();
return StringGroovyMethods.collectReplacements(value, new ReplacingClosure(isAttrValue, useDoubleQuotes));
}
private static class ReplacingClosure extends Closure {
private final boolean isAttrValue;
private final boolean useDoubleQuotes;
public ReplacingClosure(boolean isAttrValue, boolean useDoubleQuotes) {
super(null);
this.isAttrValue = isAttrValue;
this.useDoubleQuotes = useDoubleQuotes;
}
public String doCall(Character ch) {
switch (ch) {
case '&':
return "&";
case '<':
return "<";
case '>':
return ">";
case '\n':
if (isAttrValue) return "
";
break;
case '\r':
if (isAttrValue) return "
";
break;
case '\t':
if (isAttrValue) return " ";
break;
case '"':
// The double quote is only escaped if the value is for
// an attribute and the builder is configured to output
// attribute values inside double quotes.
if (isAttrValue && useDoubleQuotes) return """;
break;
case '\'':
// The apostrophe is only escaped if the value is for an
// attribute, as opposed to element content, and if the
// builder is configured to surround attribute values with
// single quotes.
if (isAttrValue && !useDoubleQuotes) return "'";
break;
}
return null;
}
}
private void toState(int next, Object name) {
switch (state) {
case 0:
switch (next) {
case 1:
case 2:
out.print("<");
print(name);
break;
case 3:
throw new Error();
}
break;
case 1:
switch (next) {
case 1:
case 2:
out.print(">");
if (nospace) {
nospace = false;
} else {
out.println();
out.incrementIndent();
out.printIndent();
}
out.print("<");
print(name);
break;
case 3:
if (nodeIsEmpty) {
if (expandEmptyElements) {
out.print(">");
print(name);
out.print(">");
} else {
out.print(" />");
}
}
break;
}
break;
case 2:
switch (next) {
case 1:
case 2:
if (!nodeIsEmpty) {
out.println();
out.incrementIndent();
out.printIndent();
}
out.print("<");
print(name);
break;
case 3:
out.print("");
print(name);
out.print(">");
break;
}
break;
case 3:
switch (next) {
case 1:
case 2:
if (nospace) {
nospace = false;
} else {
out.println();
out.printIndent();
}
out.print("<");
print(name);
break;
case 3:
if (nospace) {
nospace = false;
} else {
out.println();
out.decrementIndent();
out.printIndent();
}
out.print("");
print(name);
out.print(">");
break;
}
break;
}
state = next;
}
private Object getName(Object name) {
if (name instanceof QName) {
return ((QName) name).getQualifiedName();
}
return name;
}
}