- * Override it to improve speed a little. Otherwise the String is transformed in char table passed
- * to the over
- * methods which recreate a String.
- *
- * @see java.io.Writer#write(java.lang.String)
- */
- @Override
- public void write(String str) throws IOException {
- getWikiPrinter().print(str);
- }
-
-}
diff --git a/celements-xwiki-rendering-api/src/main/java/org/xwiki/rendering/internal/renderer/printer/XHTMLWriter.java b/celements-xwiki-rendering-api/src/main/java/org/xwiki/rendering/internal/renderer/printer/XHTMLWriter.java
deleted file mode 100644
index 5be64c032..000000000
--- a/celements-xwiki-rendering-api/src/main/java/org/xwiki/rendering/internal/renderer/printer/XHTMLWriter.java
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
- * See the NOTICE file distributed with this work for additional
- * information regarding copyright ownership.
- *
- * This 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 2.1 of
- * the License, or (at your option) any later version.
- *
- * This software 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 software; if not, write to the Free
- * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
- * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
- */
-package org.xwiki.rendering.internal.renderer.printer;
-
-import java.io.UnsupportedEncodingException;
-import java.io.Writer;
-
-import org.dom4j.io.OutputFormat;
-import org.dom4j.io.XMLWriter;
-
-/**
- * XHTMLWriter is an helper to configure XMLWriter to format a DOM4J tree as XHTML.
- *
- * @version $Id$
- */
-public class XHTMLWriter extends XMLWriter {
-
- protected static final OutputFormat DEFAULT_XHTML_FORMAT;
-
- static {
- DEFAULT_XHTML_FORMAT = new OutputFormat();
- DEFAULT_XHTML_FORMAT.setXHTML(true);
- }
-
- public XHTMLWriter(Writer writer) throws UnsupportedEncodingException {
- super(writer, DEFAULT_XHTML_FORMAT);
-
- // escape all non US-ASCII to have as less encoding problems as possible
- setMaximumAllowedCharacter(-1);
- }
-
- /**
- * Escapes a string to be used as an attribute value. Unlike the original method in
- * {@link XMLWriter}, apostrophes
- * are replaced by a numerical entity &, since ' is not valid in HTML documents.
- *
- * @param text
- * the attribute value to escape
- * @return the text with all occurrences of special XML characters replaced by entity references.
- */
- @Override
- protected String escapeAttributeEntities(String text) {
- return super.escapeAttributeEntities(text).replace("'", "&");
- }
-}
diff --git a/celements-xwiki-rendering-api/src/main/java/org/xwiki/rendering/renderer/AbstractChainingPrintRenderer.java b/celements-xwiki-rendering-api/src/main/java/org/xwiki/rendering/renderer/AbstractChainingPrintRenderer.java
deleted file mode 100644
index 718f4ac52..000000000
--- a/celements-xwiki-rendering-api/src/main/java/org/xwiki/rendering/renderer/AbstractChainingPrintRenderer.java
+++ /dev/null
@@ -1,95 +0,0 @@
-/*
- * See the NOTICE file distributed with this work for additional
- * information regarding copyright ownership.
- *
- * This 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 2.1 of
- * the License, or (at your option) any later version.
- *
- * This software 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 software; if not, write to the Free
- * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
- * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
- */
-package org.xwiki.rendering.renderer;
-
-import java.util.Stack;
-
-import org.xwiki.rendering.listener.chaining.AbstractChainingListener;
-import org.xwiki.rendering.listener.chaining.ChainingListener;
-import org.xwiki.rendering.renderer.printer.WikiPrinter;
-
-/**
- * @version $Id$
- * @since 1.8RC1
- */
-public abstract class AbstractChainingPrintRenderer extends AbstractChainingListener
- implements PrintRenderer {
-
- /**
- * The printer stack. Can be used to print in a specific printer and then easily return to the
- * previous one.
- */
- private Stack
+ * Extracts a well-formed XML fragment by listening to SAX events. The result has the following
+ * semantic:
+ * So basically we would create an instance like
+ * As an example, the result of applying an
+ * printXML(" ").
- */
- public void printSpace() {
- this.spaceCount++;
- }
-
- /**
- * {@inheritDoc}
- *
- * @see org.xwiki.rendering.renderer.printer.XMLWikiPrinter#printRaw(java.lang.String)
- */
- @Override
- public void printRaw(String raw) {
- handleSpaceWhenStartElement();
- super.printRaw(raw);
- this.elementEnded = true;
- }
-
- private void handleSpaceWhenInText() {
- if (this.elementEnded || this.hasTextBeenPrinted) {
- handleSpaceWhenStartElement();
- } else {
- handleSpaceWhenEndlement();
- }
- }
-
- private void handleSpaceWhenStartElement() {
- // Use case:
+ * xmlInput.dropAllTags().substring(start, length).unDropAssociatedTags()
+ * new ExtractHandler(0, 400) in order to
+ * obtain an XML
+ * fragment with its inner text length of at most 400 characters, starting at position (character) 0
+ * in the source
+ * (input) XML's inner text. The ExtractHandler is used in feed plug-in to obtain a preview of an
+ * XML (HTML, to be more
+ * specific). Another use case could be to paginate an XML source (keeping pages well-formed).
+ * ExtractHandler(3, 13) to:
+ * <p>click <a href="realyLongURL" title="Here">here</a> to view the result</p>
+ *
+ * is:
+ * <p>ck <a href="realyLongURL" title="Here">here</a> to</p>
+ * true if the extraction was successful. The parsing process throws an exception
+ * when the upper
+ * bound is reached; this flag is useful to distinguish between this exception and the others.
+ */
+ private boolean finished;
+
+ /**
+ * Creates a new instance.
+ *
+ * @param start
+ * The character index from where to start the extraction.
+ * @param length
+ * The number of plain text characters to extract.
+ * @throws SAXException
+ * if start is less than zero or length is less than or equal to zero.
+ */
+ public ExtractHandler(int start, int length) throws SAXException {
+ super();
+ if (start < 0) {
+ throw new SAXException("Start must be greater than or equal to 0");
+ }
+ if (length <= 0) {
+ throw new SAXException("Length must be greater than 0");
+ }
+ lowerBound = start;
+ upperBound = lowerBound + length;
+ }
+
+ /**
+ * @return The extracted text.
+ */
+ public String getResult() {
+ return result.toString();
+ }
+
+ /**
+ * @return true if the extraction process has succeeded; false if an exception occurred during the
+ * process.
+ */
+ public boolean isFinished() {
+ return finished;
+ }
+
+ /**
+ * Append an open tag with the given specification to the result buffer.
+ *
+ * @param qName
+ * Tag's qualified name.
+ * @param atts
+ * Tag's attributes.
+ */
+ private void openTag(String qName, Attributes atts) {
+ result.append('<').append(qName);
+ for (int i = 0; i < atts.getLength(); i++) {
+ result.append(' ').append(atts.getQName(i)).append("=\"").append(atts.getValue(i))
+ .append('\"');
+ }
+ result.append('>');
+ }
+
+ /**
+ * Open all pending tags.
+ *
+ * @see #openTag(String, Attributes)
+ */
+ private void openTags() {
+ for (XMLTag tag : openTags) {
+ openTag(tag.getQName(), tag.getAtts());
+ }
+ }
+
+ /**
+ * Close all pending tags.
+ *
+ * @see #closeTag(String)
+ */
+ private void closeTags() {
+ while (!openTags.isEmpty()) {
+ closeTag(openTags.pop().getQName());
+ }
+ }
+
+ /**
+ * Append a closed tag with the given qualified name to the result buffer.
+ *
+ * @param qName
+ * Tag's qualified name.
+ */
+ private void closeTag(String qName) {
+ result.append("").append(qName).append('>');
+ }
+
+ /**
+ * @return true if the start point has been passed but the length limit hasn't been reached.
+ */
+ private boolean isExtracting() {
+ return lowerBound <= counter && counter <= upperBound;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @see DefaultHandler#startDocument()
+ */
+ public void startDocument() throws SAXException {
+ super.startDocument();
+ counter = 0;
+ openTags.clear();
+ result = new StringBuffer();
+ finished = false;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @see DefaultHandler#startElement(String, String, String, Attributes)
+ */
+ public void startElement(String namespaceURI, String localName, String qName, Attributes atts)
+ throws SAXException {
+ openTags.push(new XMLTag(qName, atts));
+ if (isExtracting()) {
+ openTag(qName, atts);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @see DefaultHandler#characters(char[], int, int)
+ */
+ public void characters(char[] ch, int start, int length) throws SAXException {
+ int offset = lowerBound - counter;
+ if (offset > 0) {
+ if (offset > length) {
+ counter += length;
+ return;
+ } else {
+ counter = lowerBound;
+ openTags();
+ characters(ch, start + offset, length - offset);
+ return;
+ }
+ }
+ int remainingLength = upperBound - counter;
+ if (remainingLength <= length) {
+ String content = new String(ch, start, remainingLength);
+ int spaceIndex = remainingLength;
+ if (remainingLength == length || ch[remainingLength] != ' ') {
+ spaceIndex = content.lastIndexOf(" ");
+ }
+ if (spaceIndex >= 0) {
+ counter += spaceIndex;
+ result.append(content.substring(0, spaceIndex));
+ } else {
+ counter = upperBound;
+ result.append(content);
+ }
+ endDocument();
+ throw new SAXException("Length limit reached");
+ } else {
+ counter += length;
+ result.append(ch, start, length);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @see DefaultHandler#endElement(String, String, String)
+ */
+ public void endElement(String namespaceURI, String localName, String qName) throws SAXException {
+ // We assume the XML fragment is well defined, and thus we shouldn't have a closed tag
+ // without its pair open tag. So we don't test for empty stack or tag match.
+ openTags.pop();
+ if (isExtracting()) {
+ closeTag(qName);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @see DefaultHandler#endDocument()
+ */
+ public void endDocument() throws SAXException {
+ super.endDocument();
+ // Close open tags
+ if (isExtracting()) {
+ closeTags();
+ }
+ // set finished flag to distinguish between "length limit reached" and other exceptions
+ finished = true;
+ }
+}
diff --git a/celements-xwiki-xml/src/main/java/org/xwiki/xml/XMLUtils.java b/celements-xwiki-xml/src/main/java/org/xwiki/xml/XMLUtils.java
new file mode 100644
index 000000000..8015fd2d5
--- /dev/null
+++ b/celements-xwiki-xml/src/main/java/org/xwiki/xml/XMLUtils.java
@@ -0,0 +1,142 @@
+/*
+ * See the NOTICE file distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This 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 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software 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 software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.xwiki.xml;
+
+import javax.xml.transform.Transformer;
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.dom.DOMSource;
+import javax.xml.transform.sax.SAXResult;
+
+import org.w3c.dom.Node;
+
+/**
+ * XML Utility methods.
+ *
+ * @version $Id$
+ * @since 1.6M1
+ */
+public final class XMLUtils {
+
+ /**
+ * Private constructor since this is a utility class that shouldn't be instantiated (all methods
+ * are static).
+ */
+ private XMLUtils() {
+ // Nothing to do
+ }
+
+ /**
+ * Extracts a well-formed XML fragment from the given DOM tree.
+ *
+ * @param node
+ * the root of the DOM tree where the extraction takes place
+ * @param start
+ * the index of the first character
+ * @param length
+ * the maximum number of characters in text nodes to include in the returned fragment
+ * @return a well-formed XML fragment starting at the given character index and having up to the
+ * specified length,
+ * summing only the characters in text nodes
+ * @since 1.6M2
+ */
+ public static String extractXML(Node node, int start, int length) {
+ ExtractHandler handler = null;
+ try {
+ handler = new ExtractHandler(start, length);
+ Transformer xformer = TransformerFactory.newInstance().newTransformer();
+ xformer.transform(new DOMSource(node), new SAXResult(handler));
+ return handler.getResult();
+ } catch (Throwable t) {
+ if (handler != null && handler.isFinished()) {
+ return handler.getResult();
+ } else {
+ throw new RuntimeException("Failed to extract XML", t);
+ }
+ }
+ }
+
+ /**
+ * XML comment does not support some characters inside its content but there is no official
+ * escaping/unescaping for
+ * it so we made our own.
+ *
+ *
+ *
+ * @param content
+ * the XML comment content to escape
+ * @return the escaped content.
+ * @since 1.9M2
+ */
+ public static String escapeXMLComment(String content) {
+ StringBuffer str = new StringBuffer(content.length());
+
+ char[] buff = content.toCharArray();
+ char lastChar = 0;
+ for (char c : buff) {
+ if (c == '\\') {
+ str.append('\\');
+ } else if (c == '-' && lastChar == '-') {
+ str.append('\\');
+ }
+
+ str.append(c);
+ lastChar = c;
+ }
+
+ if (lastChar == '-') {
+ str.append('\\');
+ }
+
+ return str.toString();
+ }
+
+ /**
+ * XML comment does not support some characters inside its content but there is no official
+ * escaping/unescaping for
+ * it so we made our own.
+ *
+ * @param content
+ * the XML comment content to unescape
+ * @return the unescaped content.
+ * @see #escapeXMLComment(String)
+ * @since 1.9M2
+ */
+ public static String unescapeXMLComment(String content) {
+ StringBuffer str = new StringBuffer(content.length());
+
+ char[] buff = content.toCharArray();
+ boolean escaped = false;
+ for (char c : buff) {
+ if (!escaped && c == '\\') {
+ escaped = true;
+ continue;
+ }
+
+ str.append(c);
+ escaped = false;
+ }
+
+ return str.toString();
+ }
+}
diff --git a/celements-xwiki-xml/src/main/java/org/xwiki/xml/internal/XMLScriptService.java b/celements-xwiki-xml/src/main/java/org/xwiki/xml/internal/XMLScriptService.java
new file mode 100644
index 000000000..474c58d05
--- /dev/null
+++ b/celements-xwiki-xml/src/main/java/org/xwiki/xml/internal/XMLScriptService.java
@@ -0,0 +1,450 @@
+/*
+ * See the NOTICE file distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This 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 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software 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 software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.xwiki.xml.internal;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.util.regex.Pattern;
+
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.transform.Result;
+import javax.xml.transform.Source;
+import javax.xml.transform.dom.DOMSource;
+import javax.xml.transform.stream.StreamResult;
+import javax.xml.transform.stream.StreamSource;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.w3c.dom.Document;
+import org.w3c.dom.Node;
+import org.w3c.dom.bootstrap.DOMImplementationRegistry;
+import org.w3c.dom.ls.DOMImplementationLS;
+import org.w3c.dom.ls.LSInput;
+import org.w3c.dom.ls.LSOutput;
+import org.w3c.dom.ls.LSParser;
+import org.w3c.dom.ls.LSSerializer;
+import org.xwiki.script.service.ScriptService;
+import org.springframework.stereotype.Component;
+
+/**
+ * Provides Scripting APIs for handling XML.
+ *
+ * @version $Id$
+ * @since 2.7M1
+ */
+@Component("xml")
+public class XMLScriptService implements ScriptService {
+
+ /** Logging helper object. */
+ private static final Logger LOG = LoggerFactory.getLogger(XMLScriptService.class);
+
+ /**
+ * Xerces configuration parameter for disabling fetching and checking XMLs against their DTD.
+ */
+ private static final String DISABLE_DTD_PARAM = "http://apache.org/xml/features/nonvalidating/load-external-dtd";
+
+ /** XML encoding of the "ampersand" character. */
+ private static final String AMP = "&";
+
+ /** Regular expression recognizing XML-escaped "ampersand" characters. */
+ private static final Pattern AMP_PATTERN = Pattern.compile("&(?:amp|#0*+38|#x0*+26);");
+
+ /** XML encoding of the "single quote" character. */
+ private static final String APOS = "'";
+
+ /** Regular expression recognizing XML-escaped "single quote" characters. */
+ private static final Pattern APOS_PATTERN = Pattern.compile("&(?:apos|#0*+39|#x0*+27);");
+
+ /** XML encoding of the "double quote" character. */
+ private static final String QUOT = """;
+
+ /** Regular expression recognizing XML-escaped "double quote" characters. */
+ private static final Pattern QUOT_PATTERN = Pattern.compile("&(?:quot|#0*+34|#x0*+22);");
+
+ /** XML encoding of the "less than" character. */
+ private static final String LT = "<";
+
+ /** Regular expression recognizing XML-escaped "less than" characters. */
+ private static final Pattern LT_PATTERN = Pattern.compile("&(?:lt|#0*+60|#x0*+3[cC]);");
+
+ /** XML encoding of the "greater than" character. */
+ private static final String GT = ">";
+
+ /** Regular expression recognizing XML-escaped "greater than" characters. */
+ private static final Pattern GT_PATTERN = Pattern.compile("&(?:gt|#0*+62|#x0*+3[eE]);");
+
+ /** Helper object for manipulating DOM Level 3 Load and Save APIs. */
+ private DOMImplementationLS lsImpl;
+
+ /**
+ * Default component constructor.
+ */
+ public XMLScriptService() {
+ try {
+ this.lsImpl = (DOMImplementationLS) DOMImplementationRegistry.newInstance()
+ .getDOMImplementation("LS 3.0");
+ } catch (Exception ex) {
+ LOG.warn("Cannot initialize the XML Script Service", ex);
+ }
+ }
+
+ /**
+ * Escapes all the XML special characters in a String using numerical XML entities.
+ * Specifically,
+ * escapes <, >, ", ' and &.
+ *
+ * @param content
+ * the text to escape, may be {@code null}
+ * @return a new escaped {@code String}, {@code null} if {@code null} input
+ */
+ public static String escape(Object content) {
+ return escapeForAttributeValue(content);
+ }
+
+ /**
+ * Escapes all the XML special characters in a String using numerical XML entities,
+ * so that the
+ * resulting string can safely be used as an XML attribute value. Specifically, escapes <,
+ * >, ", ' and &.
+ *
+ * @param content
+ * the text to escape, may be {@code null}
+ * @return a new escaped {@code String}, {@code null} if {@code null} input
+ */
+ public static String escapeForAttributeValue(Object content) {
+ if (content == null) {
+ return null;
+ }
+ String str = String.valueOf(content);
+ StringBuilder result = new StringBuilder((int) (str.length() * 1.1));
+ int length = str.length();
+ char c;
+ for (int i = 0; i < length; ++i) {
+ c = str.charAt(i);
+ switch (c) {
+ case '&':
+ result.append(AMP);
+ break;
+ case '\'':
+ result.append(APOS);
+ break;
+ case '"':
+ result.append(QUOT);
+ break;
+ case '<':
+ result.append(LT);
+ break;
+ case '>':
+ result.append(GT);
+ break;
+ default:
+ result.append(c);
+ }
+ }
+ return result.toString();
+ }
+
+ /**
+ * Escapes the XML special characters in a String using numerical XML entities, so
+ * that the resulting
+ * string can safely be used as an XML text node. Specifically, escapes <, >, and &.
+ *
+ * @param content
+ * the text to escape, may be {@code null}
+ * @return a new escaped {@code String}, {@code null} if {@code null} input
+ */
+ public static String escapeForElementContent(Object content) {
+ if (content == null) {
+ return null;
+ }
+ String str = String.valueOf(content);
+ StringBuilder result = new StringBuilder((int) (str.length() * 1.1));
+ int length = str.length();
+ char c;
+ for (int i = 0; i < length; ++i) {
+ c = str.charAt(i);
+ switch (c) {
+ case '&':
+ result.append(AMP);
+ break;
+ case '<':
+ result.append(LT);
+ break;
+ case '>':
+ result.append(GT);
+ break;
+ default:
+ result.append(c);
+ }
+ }
+ return result.toString();
+ }
+
+ /**
+ * Unescape encoded special XML characters. Only >, < &, " and ' are unescaped, since
+ * they are the only
+ * ones that affect the resulting markup.
+ *
+ * @param content
+ * the text to decode, may be {@code null}
+ * @return unescaped content, {@code null} if {@code null} input
+ */
+ public static String unescape(Object content) {
+ if (content == null) {
+ return null;
+ }
+ String str = String.valueOf(content);
+
+ str = APOS_PATTERN.matcher(str).replaceAll("'");
+ str = QUOT_PATTERN.matcher(str).replaceAll("\"");
+ str = LT_PATTERN.matcher(str).replaceAll("<");
+ str = GT_PATTERN.matcher(str).replaceAll(">");
+ str = AMP_PATTERN.matcher(str).replaceAll("&");
+
+ return str;
+ }
+
+ /**
+ * Construct a new (empty) DOM Document and return it.
+ *
+ * @return an empty DOM Document
+ */
+ public Document createDOMDocument() {
+ try {
+ return DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument();
+ } catch (ParserConfigurationException ex) {
+ LOG.error("Cannot create DOM Documents", ex);
+ return null;
+ }
+ }
+
+ /**
+ * Parse a DOM Document from a source.
+ *
+ * @param source
+ * the source input to parse
+ * @return the equivalent DOM Document, or {@code null} if the parsing failed.
+ */
+ public Document parse(LSInput source) {
+ try {
+ LSParser p = this.lsImpl.createLSParser(DOMImplementationLS.MODE_SYNCHRONOUS, null);
+ // Disable validation, since this takes a lot of time and causes unneeded network
+ // traffic
+ p.getDomConfig().setParameter("validate", false);
+ if (p.getDomConfig().canSetParameter(DISABLE_DTD_PARAM, false)) {
+ p.getDomConfig().setParameter(DISABLE_DTD_PARAM, false);
+ }
+ return p.parse(source);
+ } catch (Exception ex) {
+ LOG.warn("Cannot parse XML document: " + ex.getMessage());
+ return null;
+ }
+ }
+
+ /**
+ * Parse a {@code byte[]} into a DOM Document.
+ *
+ * @param content
+ * the content to parse
+ * @return a DOM Document corresponding to the input, {@code null} if the content can't be
+ * parsed successfully
+ */
+ public Document parse(byte[] content) {
+ if (content == null) {
+ return null;
+ }
+ LSInput input = this.lsImpl.createLSInput();
+ input.setByteStream(new ByteArrayInputStream(content));
+ return parse(input);
+ }
+
+ /**
+ * Parse a {@code String} into a DOM Document.
+ *
+ * @param content
+ * the content to parse
+ * @return a DOM Document corresponding to the input, {@code null} if the content can't be
+ * parsed successfully
+ */
+ public Document parse(String content) {
+ if (content == null) {
+ return null;
+ }
+ LSInput input = this.lsImpl.createLSInput();
+ input.setCharacterStream(new StringReader(content));
+ return parse(input);
+ }
+
+ /**
+ * Parse an {@code InputStream} into a DOM Document.
+ *
+ * @param stream
+ * the content input to parse
+ * @return a DOM Document corresponding to the input, {@code null} if the content can't be
+ * parsed successfully
+ */
+ public Document parse(InputStream stream) {
+ if (stream == null) {
+ return null;
+ }
+ LSInput input = this.lsImpl.createLSInput();
+ input.setByteStream(stream);
+ return parse(input);
+ }
+
+ /**
+ * Serialize a DOM Node into a string, including the XML declaration at the start.
+ *
+ * @param node
+ * the node to export
+ * @return the serialized node, or an empty string if the serialization fails
+ */
+ public String serialize(Node node) {
+ return serialize(node, true);
+ }
+
+ /**
+ * Serialize a DOM Node into a string, with an optional XML declaration at the start.
+ *
+ * @param node
+ * the node to export
+ * @param withXmlDeclaration
+ * whether to output the XML declaration or not
+ * @return the serialized node, or an empty string if the serialization fails or the node is
+ * {@code null}
+ */
+ public String serialize(Node node, boolean withXmlDeclaration) {
+ if (node == null) {
+ return "";
+ }
+ if (node.getNodeType() == Node.DOCUMENT_TYPE_NODE) {
+ return "";
+ }
+ try {
+ LSOutput output = this.lsImpl.createLSOutput();
+ StringWriter result = new StringWriter();
+ output.setCharacterStream(result);
+ LSSerializer serializer = this.lsImpl.createLSSerializer();
+ serializer.getDomConfig().setParameter("xml-declaration", withXmlDeclaration);
+ serializer.setNewLine("\n");
+ String encoding = "UTF-8";
+ if (node instanceof Document) {
+ encoding = ((Document) node).getXmlEncoding();
+ } else if (node.getOwnerDocument() != null) {
+ encoding = node.getOwnerDocument().getXmlEncoding();
+ }
+ output.setEncoding(encoding);
+ serializer.write(node, output);
+
+ String xmlString = result.toString();
+ if (withXmlDeclaration && xmlString.startsWith("]*\\?>)(?!\\r?\\n)", "$1\n");
+ }
+ if (xmlString.contains("]*>)(?!\\r?\\n)", "$1\n");
+ }
+ return xmlString;
+ } catch (Exception ex) {
+ LOG.warn("Failed to serialize node to XML String", ex);
+ return "";
+ }
+ }
+
+ /**
+ * Apply an XSLT transformation to a Document.
+ *
+ * @param xml
+ * the document to transform
+ * @param xslt
+ * the stylesheet to apply
+ * @return the transformation result, or {@code null} if an error occurs or {@code null} xml or
+ * xslt input
+ */
+ public String transform(Source xml, Source xslt) {
+ if (xml != null && xslt != null) {
+ try {
+ StringWriter output = new StringWriter();
+ Result result = new StreamResult(output);
+ javax.xml.transform.TransformerFactory.newInstance().newTransformer(xslt)
+ .transform(xml, result);
+ return output.toString();
+ } catch (Exception ex) {
+ LOG.warn("Failed to apply XSLT transformation: " + ex.getMessage());
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Apply an XSLT transformation to a Document, both given as DOM Documents.
+ *
+ * @param xml
+ * the document to transform
+ * @param xslt
+ * the stylesheet to apply
+ * @return the transformation result, or {@code null} if an error occurs or {@code null} xml or
+ * xslt input
+ */
+ public String transform(Document xml, Document xslt) {
+ if (xml == null || xslt == null) {
+ return null;
+ }
+ return transform(new DOMSource(xml), new DOMSource(xslt));
+ }
+
+ /**
+ * Apply an XSLT transformation to a Document, both given as byte arrays.
+ *
+ * @param xml
+ * the document to transform
+ * @param xslt
+ * the stylesheet to apply
+ * @return the transformation result, or {@code null} if an error occurs or {@code null} xml or
+ * xslt input
+ */
+ public String transform(byte[] xml, byte[] xslt) {
+ if (xml == null || xslt == null) {
+ return null;
+ }
+ return transform(new StreamSource(new ByteArrayInputStream(xml)),
+ new StreamSource(new ByteArrayInputStream(xslt)));
+ }
+
+ /**
+ * Apply an XSLT transformation to a Document, both given as strings.
+ *
+ * @param xml
+ * the document to transform
+ * @param xslt
+ * the stylesheet to apply
+ * @return the transformation result, or {@code null} if an error occurs or {@code null} xml or
+ * xslt input
+ */
+ public String transform(String xml, String xslt) {
+ if (xml == null || xslt == null) {
+ return null;
+ }
+ return transform(new StreamSource(new StringReader(xml)),
+ new StreamSource(new StringReader(xslt)));
+ }
+}
diff --git a/celements-xwiki-rendering-api/src/main/java/org/xwiki/rendering/renderer/printer/VoidWikiPrinter.java b/celements-xwiki-xml/src/test/java/org/xwiki/xml/XMLUtilsTest.java
similarity index 51%
rename from celements-xwiki-rendering-api/src/main/java/org/xwiki/rendering/renderer/printer/VoidWikiPrinter.java
rename to celements-xwiki-xml/src/test/java/org/xwiki/xml/XMLUtilsTest.java
index 7a5d953aa..60d7d2cbd 100644
--- a/celements-xwiki-rendering-api/src/main/java/org/xwiki/rendering/renderer/printer/VoidWikiPrinter.java
+++ b/celements-xwiki-xml/src/test/java/org/xwiki/xml/XMLUtilsTest.java
@@ -16,41 +16,41 @@
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ *
*/
-package org.xwiki.rendering.renderer.printer;
+package org.xwiki.xml;
+
+import org.xwiki.test.AbstractXWikiComponentTestCase;
/**
- * A WikiPrinter implementation which does not do anything.
+ * Unit tests for {@link org.xwiki.xml.XMLUtils}.
*
* @version $Id$
+ * @since 1.6M1
*/
-public class VoidWikiPrinter implements WikiPrinter {
-
- /**
- * Unique instance of {@link VoidWikiPrinter}.
- */
- public static final VoidWikiPrinter VOIDWIKIPRINTER = new VoidWikiPrinter();
-
- /**
- * Use {@link #VOIDWIKIPRINTER}.
- */
- private VoidWikiPrinter() {}
+public class XMLUtilsTest extends AbstractXWikiComponentTestCase {
/**
* {@inheritDoc}
*
- * @see org.xwiki.rendering.renderer.printer.WikiPrinter#print(java.lang.String)
+ * @see org.xwiki.test.AbstractXWikiComponentTestCase#setUp()
*/
- public void print(String text) {
- // Don't do anything
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
}
- /**
- * {@inheritDoc}
- *
- * @see org.xwiki.rendering.renderer.printer.WikiPrinter#println(java.lang.String)
- */
- public void println(String text) {
- // Don't do anything
+ public void testEscapeXMLComment() {
+ assertEquals("-\\- ", XMLUtils.escapeXMLComment("-- "));
+ assertEquals("-\\", XMLUtils.escapeXMLComment("-"));
+ assertEquals("-\\-\\-\\", XMLUtils.escapeXMLComment("---"));
+ assertEquals("- ", XMLUtils.escapeXMLComment("- "));
+ }
+
+ public void testUnescapeXMLComment() {
+ assertEquals("", XMLUtils.unescapeXMLComment("\\"));
+ assertEquals("\\", XMLUtils.unescapeXMLComment("\\\\"));
+ assertEquals("--", XMLUtils.unescapeXMLComment("\\-\\-"));
+ assertEquals("--", XMLUtils.unescapeXMLComment("\\-\\-\\"));
}
}
diff --git a/celements-xwiki-xml/src/test/java/org/xwiki/xml/internal/XMLScriptServiceTest.java b/celements-xwiki-xml/src/test/java/org/xwiki/xml/internal/XMLScriptServiceTest.java
new file mode 100644
index 000000000..4b8749ed1
--- /dev/null
+++ b/celements-xwiki-xml/src/test/java/org/xwiki/xml/internal/XMLScriptServiceTest.java
@@ -0,0 +1,492 @@
+/*
+ * See the NOTICE file distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This 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 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software 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 software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+
+package org.xwiki.xml.internal;
+
+import java.io.ByteArrayInputStream;
+import java.io.StringReader;
+import java.io.UnsupportedEncodingException;
+
+import javax.xml.transform.stream.StreamSource;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.ls.LSInput;
+import org.xwiki.script.service.ScriptService;
+import com.celements.common.test.AbstractBaseComponentTest;
+
+/**
+ * Unit tests for {@link XMLScriptService}.
+ *
+ * @version $Id$
+ * @since 2.7RC1
+ */
+public class XMLScriptServiceTest extends AbstractBaseComponentTest {
+
+ private XMLScriptService xml;
+
+ @Before
+ public void prepare_XMLScriptServiceTest() throws Exception {
+ this.xml = (XMLScriptService) getBeanFactory().getBean(XMLScriptService.class);
+ }
+
+ @Test
+ public void testEscape() {
+ String escapedText = XMLScriptService.escape("a < a' && a' < a\" => a < a\"");
+
+ Assert.assertFalse("Failed to escape <", escapedText.contains("<"));
+ Assert.assertFalse("Failed to escape >", escapedText.contains(">"));
+ Assert.assertFalse("Failed to escape '", escapedText.contains("'"));
+ Assert.assertFalse("Failed to escape \"", escapedText.contains("\""));
+ Assert.assertFalse("Failed to escape &", escapedText.contains("&&"));
+ }
+
+ @Test
+ public void testEscapeApos() {
+ Assert.assertFalse("' wrongly escaped to non-HTML '",
+ XMLScriptService.escape("'").equals("'"));
+ }
+
+ @Test
+ public void testEscapeEmptyString() {
+ Assert.assertEquals("\"\" should be \"\"", "", XMLScriptService.escape(""));
+ }
+
+ @Test
+ public void testEscapeWithNull() {
+ Assert.assertNull("null should be null", XMLScriptService.escape(null));
+ }
+
+ @Test
+ public void testEscapeNonAscii() {
+ Assert.assertTrue("Non-ASCII characters were escaped",
+ XMLScriptService.escape("\u0123").equals("\u0123"));
+ }
+
+ @Test
+ public void testEscapeForAttributeValue() {
+
+ String escapedText = XMLScriptService
+ .escapeForAttributeValue("a < a' && a' < a\" => a < a\"");
+
+ Assert.assertFalse("Failed to escape <", escapedText.contains("<"));
+ Assert.assertFalse("Failed to escape >", escapedText.contains(">"));
+ Assert.assertFalse("Failed to escape '", escapedText.contains("'"));
+ Assert.assertFalse("Failed to escape \"", escapedText.contains("\""));
+ Assert.assertFalse("Failed to escape &", escapedText.contains("&&"));
+ }
+
+ @Test
+ public void testEscapeForAttributeValueApos() {
+ Assert.assertFalse("' wrongly escaped to non-HTML '",
+ XMLScriptService.escapeForAttributeValue("'")
+ .equals("'"));
+ }
+
+ @Test
+ public void testEscapeForAttributeValueEmptyString() {
+ Assert.assertEquals("\"\" should be \"\"", "",
+ XMLScriptService.escapeForAttributeValue(""));
+ }
+
+ @Test
+ public void testEscapeForAttributeValueWithNull() {
+ Assert.assertNull("null should be null", XMLScriptService.escapeForAttributeValue(null));
+ }
+
+ @Test
+ public void testEscapeForAttributeValueNonAscii() {
+ Assert.assertTrue("Non-ASCII characters were escaped",
+ XMLScriptService.escapeForAttributeValue("\u0123")
+ .equals("\u0123"));
+ }
+
+ @Test
+ public void testEscapeForElementContent() {
+
+ String escapedText = XMLScriptService
+ .escapeForElementContent("a < a' && a' < a\" => a < a\"");
+
+ Assert.assertFalse("Failed to escape <", escapedText.contains("<"));
+ Assert.assertFalse("Failed to escape >", escapedText.contains(">"));
+ Assert.assertTrue("Wrongfully escaped '", escapedText.contains("'"));
+ Assert.assertTrue("Wrongfully escaped \"", escapedText.contains("\""));
+ Assert.assertFalse("Failed to escape &", escapedText.contains("&&"));
+ }
+
+ @Test
+ public void testEscapeForElementContentEmptyString() {
+ Assert.assertEquals("\"\" should be \"\"", "",
+ XMLScriptService.escapeForElementContent(""));
+ }
+
+ @Test
+ public void testEscapeForElementContentWithNull() {
+ Assert.assertNull("null should be null", XMLScriptService.escapeForElementContent(null));
+ }
+
+ @Test
+ public void testEscapeForElementContentNonAscii() {
+ Assert.assertTrue("Non-ASCII characters were escaped",
+ XMLScriptService.escapeForElementContent("\u0123")
+ .equals("\u0123"));
+ }
+
+ @Test
+ public void testUnescape() {
+ Assert.assertEquals("Failed to unescaped named entities", "&'\"<>",
+ XMLScriptService.unescape("&'"<>"));
+ Assert.assertEquals("Failed to unescaped decimal entities", "&'\"<>",
+ XMLScriptService.unescape("&'"<>"));
+ Assert.assertEquals("Failed to unescaped decimal entities with leading zeros", "&'\"<>",
+ XMLScriptService.unescape("&'"<>"));
+ Assert.assertEquals("Failed to unescaped hexadecimal entities", "&'\"<<>>",
+ XMLScriptService.unescape("&'"<<>>"));
+ Assert.assertEquals("Failed to unescaped hexadecimal entities with leading zeros",
+ "&'\"<<>>",
+ XMLScriptService
+ .unescape("&'"<<>>"));
+ }
+
+ @Test
+ public void testUnescapeEmptyString() {
+ Assert.assertEquals("\"\" should be \"\"", "", XMLScriptService.unescape(""));
+ }
+
+ @Test
+ public void testUnescapeWithNull() {
+ Assert.assertNull("null should be null", XMLScriptService.unescape(null));
+ }
+
+ @Test
+ public void testUnescapeOtherEscapes() {
+ Assert.assertEquals("Extra named entities were unescaped", "°",
+ XMLScriptService.unescape("°"));
+ Assert.assertEquals("Extra decimal entities were unescaped", "A",
+ XMLScriptService.unescape("A"));
+ Assert.assertEquals("Extra hexadecimal entities were unescaped", "",
+ XMLScriptService.unescape(""));
+ }
+
+ @Test
+ public void testGetDomDocument() {
+ // Nothing much that we can test here...
+ Assert.assertNotNull(this.xml.createDOMDocument());
+ }
+
+ @Test
+ public void testParseString() {
+ Document result = this.xml
+ .parse("d");
+ Assert.assertNotNull("Failed to parse content", result);
+ Assert.assertEquals("Incorrect root node", "a", result.getDocumentElement().getLocalName());
+ }
+
+ @Test
+ public void testParseByteArray() throws UnsupportedEncodingException {
+ Document result = this.xml.parse(
+ "d".getBytes("UTF-8"));
+ Assert.assertNotNull("Failed to parse content", result);
+ Assert.assertEquals("Incorrect root node", "a", result.getDocumentElement().getLocalName());
+ }
+
+ @Test
+ public void testParseInputStream() throws UnsupportedEncodingException {
+ Document result = this.xml.parse(
+ new ByteArrayInputStream("d"
+ .getBytes("UTF-8")));
+ Assert.assertNotNull("Failed to parse content", result);
+ Assert.assertEquals("Incorrect root node", "a", result.getDocumentElement().getLocalName());
+ }
+
+ @Test
+ public void testParseWithDifferentEncoding() throws UnsupportedEncodingException {
+ Document result = this.xml
+ .parse("\u00E9"
+ .getBytes("ISO-8859-1"));
+ Assert.assertNotNull("Failed to parse content", result);
+ Assert.assertEquals("Incorrect root node", "a", result.getDocumentElement().getLocalName());
+ Assert.assertEquals("Incorrect content", "\u00E9",
+ result.getDocumentElement().getTextContent());
+ }
+
+ @Test
+ public void testParseWithWrongEncoding() throws UnsupportedEncodingException {
+ Document result = this.xml.parse(
+ "\u00E9".getBytes("ISO-8859-1"));
+ Assert.assertNull("Content should be invalid with the specified encoding", result);
+ }
+
+ @Test
+ public void testParseWithoutXMLDeclaration() {
+ Document result = this.xml.parse("\u00E9");
+ Assert.assertNotNull("Failed to parse content", result);
+ Assert.assertEquals("Incorrect root node", "a", result.getDocumentElement().getLocalName());
+ }
+
+ @Test
+ public void testParseInvalidDocument() throws UnsupportedEncodingException {
+ Document result = this.xml.parse("");
+ Assert.assertNull("Invalid content shouldn't be parsed", result);
+ }
+
+ @Test
+ public void testParseNull() {
+ Document result = this.xml.parse((String) null);
+ Assert.assertNull("Null Document input shouldn't be parsed", result);
+ result = this.xml.parse((byte[]) null);
+ Assert.assertNull("Null byte[] input shouldn't be parsed", result);
+ result = this.xml.parse((ByteArrayInputStream) null);
+ Assert.assertNull("Null InputStream input shouldn't be parsed", result);
+ result = this.xml.parse((LSInput) null);
+ Assert.assertNull("Null LSInput input shouldn't be parsed", result);
+ }
+
+ @Test
+ public void testParseAndSerialize() {
+ String content = "\n\u00E9";
+ String result = this.xml.serialize(this.xml.parse(content));
+ Assert.assertEquals("Not identical content after parse + serialize", content, result);
+
+ content = "\n\u00E9";
+ result = this.xml.serialize(this.xml.parse(content));
+ Assert.assertEquals("Not identical content after parse + serialize", content, result);
+
+ content = "\u00E9";
+ result = this.xml.serialize(this.xml.parse(content), false);
+ Assert.assertEquals("Not identical content after parse + serialize", content, result);
+ }
+
+ @Test
+ public void testSerialize() {
+ Document d = createSimpleDocument();
+ String result = this.xml.serialize(d);
+ Assert.assertEquals("Wrong serialization",
+ "\n\u00E9", result);
+ }
+
+ @Test
+ public void testSerializeDocumentElement() {
+ Document d = createSimpleDocument();
+ String result = this.xml.serialize(d.getDocumentElement());
+ Assert.assertEquals("Wrong serialization",
+ "\n\u00E9", result);
+ }
+
+ @Test
+ public void testSerializeNode() {
+ Document d = createSimpleDocument();
+ Element b = d.createElement("b");
+ b.setTextContent("c");
+ d.getDocumentElement().appendChild(b);
+ String result = this.xml
+ .serialize(d.getDocumentElement().getElementsByTagName("b").item(0));
+ Assert.assertEquals("Wrong serialization",
+ "\nc", result);
+ }
+
+ @Test
+ public void testSerializeNull() {
+ Assert.assertEquals("Wrong serialization for null document", "", this.xml.serialize(null));
+ }
+
+ public void testSerializeWithoutXmlDeclaration() {
+ Document d = createSimpleDocument();
+ String result = this.xml.serialize(d.getDocumentElement(), false);
+ Assert.assertEquals("Wrong serialization", "\u00E9", result);
+ }
+
+ @Test
+ public void testNewlineSerialization() {
+ Document d = this.xml.parse("a\nb\n");
+ String result = this.xml.serialize(d, false);
+ Assert.assertEquals("Wrong newlines", "a\nb\n", result);
+
+ d = this.xml.parse("a\r\nb\r\n");
+ result = this.xml.serialize(d, false);
+ Assert.assertEquals("Wrong newlines", "a\nb\n", result);
+
+ d = this.xml.parse("a\rb\r");
+ result = this.xml.serialize(d, false);
+ Assert.assertEquals("Wrong newlines", "a\nb\n", result);
+ }
+
+ @Test
+ public void testSerializationWithDoctype() {
+ Document d = this.xml.parse(""
+ + ""
+ + "a");
+ String result = this.xml.serialize(d, true);
+ Assert.assertEquals("Failed Doctype", "\n"
+ + "\n"
+ + "a", result);
+ result = this.xml.serialize(d, false);
+ Assert.assertEquals("Failed Doctype",
+ "\n"
+ + "a",
+ result);
+ }
+
+ @Test
+ public void testDoctypeSerialization() {
+ Document d = this.xml.parse(""
+ + ""
+ + "a");
+ String result = this.xml.serialize(d.getDoctype(), true);
+ Assert.assertEquals("Doctype alone shouldn't be serialized", "", result);
+ }
+
+ @Test
+ public void testSerializeToSmallerCharset() {
+ Document d = this.xml.parse("\u0345");
+ String result = this.xml.serialize(d);
+ Assert.assertFalse("Non-latin1 character shouldn't be present in the output",
+ result.contains("\u0345"));
+ }
+
+ @Test
+ public void testTransformDocument() {
+ Document d = this.xml.parse("d");
+ Document s = this.xml.parse(
+ "