blob: 8d0d26e84429c1a988c8f2aa686d88c473140ebb [file] [log] [blame]
// ***************************************************************************************************************************
// * 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.juneau.testutils;
import static org.apache.juneau.testutils.StreamUtils.*;
import java.util.*;
import java.util.regex.*;
import javax.xml.parsers.*;
import org.apache.juneau.assertions.*;
import org.apache.juneau.serializer.*;
import org.apache.juneau.xml.*;
import org.w3c.dom.*;
import org.xml.sax.*;
public class XmlUtils {
/**
* A sorted node in a DOM tree.
*/
static class SortedNode implements Comparable<SortedNode> {
public String name, text="", attrs="";
public List<SortedNode> children = new LinkedList<>();
SortedNode(Element e) {
this.name = e.getNodeName();
NamedNodeMap attrs = e.getAttributes();
if (attrs != null) {
StringBuilder sb = new StringBuilder();
Set<String> attrNames = new TreeSet<>();
for (int i = 0; i < attrs.getLength(); i++)
attrNames.add(attrs.item(i).getNodeName());
for (String n : attrNames) {
Node node = attrs.getNamedItem(n);
sb.append(" ").append(n).append("='").append(node.getNodeValue()).append("'");
}
this.attrs = sb.toString();
}
NodeList nl = e.getChildNodes();
for (int i = 0; i < nl.getLength(); i++) {
Node n = nl.item(i);
if (n instanceof Element)
children.add(new SortedNode((Element)nl.item(i)));
if (n instanceof Text)
this.text += ((Text)n).getNodeValue();
}
Collections.sort(children);
}
@Override
public int compareTo(SortedNode n) {
int i = name.compareTo(n.name);
if (i != 0)
return i;
i = attrs.compareTo(n.attrs);
if (i != 0)
return i;
i = text.compareTo(n.text);
if (i != 0)
return i;
return 0;
}
@Override
public String toString() {
return toString(0, new StringBuilder()).toString();
}
public StringBuilder toString(int depth ,StringBuilder sb) {
XmlUtils.indent(depth, sb).append("<").append(name).append(attrs);
if (children.isEmpty() && text.isEmpty()) {
sb.append("/>\n");
return sb;
}
sb.append(">\n");
if (! text.isEmpty())
XmlUtils.indent(depth+1, sb).append(text).append("\n");
for (SortedNode c : children) {
c.toString(depth+1, sb);
}
XmlUtils.indent(depth, sb).append("</").append(name).append(">\n");
return sb;
}
}
/**
* Validates that the whitespace is correct in the specified XML.
*/
public static final void checkXmlWhitespace(String out) throws SerializeException {
if (out.indexOf('\u0000') != -1) {
for (String s : out.split("\u0000"))
checkXmlWhitespace(s);
return;
}
int indent = -1;
Pattern startTag = Pattern.compile("^(\\s*)<[^/>]+(\\s+\\S+=['\"]\\S*['\"])*\\s*>$");
Pattern endTag = Pattern.compile("^(\\s*)</[^>]+>$");
Pattern combinedTag = Pattern.compile("^(\\s*)<[^>/]+(\\s+\\S+=['\"]\\S*['\"])*\\s*/>$");
Pattern contentOnly = Pattern.compile("^(\\s*)[^\\s\\<]+$");
Pattern tagWithContent = Pattern.compile("^(\\s*)<[^>]+>.*</[^>]+>$");
String[] lines = out.split("\n");
try {
for (int i = 0; i < lines.length; i++) {
String line = lines[i];
Matcher m = startTag.matcher(line);
if (m.matches()) {
indent++;
if (m.group(1).length() != indent)
throw new SerializeException("Wrong indentation detected on start tag line ''{0}''", i+1);
continue;
}
m = endTag.matcher(line);
if (m.matches()) {
if (m.group(1).length() != indent)
throw new SerializeException("Wrong indentation detected on end tag line ''{0}''", i+1);
indent--;
continue;
}
m = combinedTag.matcher(line);
if (m.matches()) {
indent++;
if (m.group(1).length() != indent)
throw new SerializeException("Wrong indentation detected on combined tag line ''{0}''", i+1);
indent--;
continue;
}
m = contentOnly.matcher(line);
if (m.matches()) {
indent++;
if (m.group(1).length() != indent)
throw new SerializeException("Wrong indentation detected on content-only line ''{0}''", i+1);
indent--;
continue;
}
m = tagWithContent.matcher(line);
if (m.matches()) {
indent++;
if (m.group(1).length() != indent)
throw new SerializeException("Wrong indentation detected on tag-with-content line ''{0}''", i+1);
indent--;
continue;
}
throw new SerializeException("Unmatched whitespace line at line number ''{0}''", i+1);
}
if (indent != -1)
throw new SerializeException("Possible unmatched tag. indent=''{0}''", indent);
} catch (SerializeException e) {
XmlUtils.printLines(lines);
throw e;
}
}
public static final void validateXml(Object o) throws Exception {
XmlUtils.validateXml(o, XmlSerializer.DEFAULT_NS_SQ);
}
/**
* Test whitespace and generated schema.
*/
public static void validateXml(Object o, XmlSerializer s) throws Exception {
s = s.copy().ws().ns().addNamespaceUrisToRoot().build();
String xml = s.serialize(o);
try {
checkXmlWhitespace(xml);
} catch (Exception e) {
System.err.println("---XML---"); // NOT DEBUG
System.err.println(xml); // NOT DEBUG
throw e;
}
}
/**
* Sort an XML document by element and attribute names.
* This method is primarily meant for debugging purposes.
*/
private static final String sortXml(String xml) throws Exception {
xml = xml.replaceAll("\\w+\\:", "").replaceAll(">\\s+<", "><"); // Strip out all namespaces and whitespace.
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
DocumentBuilder db = dbf.newDocumentBuilder();
Document doc = db.parse(new InputSource(reader(xml)));
XmlUtils.SortedNode n = new XmlUtils.SortedNode(doc.getDocumentElement());
return n.toString();
}
/**
* Compares two XML documents for equality.
* Namespaces are stripped from each and elements/attributes are ordered in alphabetical order,
* then a simple string comparison is performed.
*/
public static final void assertXmlEquals(String expected, String actual) throws Exception {
Assertions.assertString(sortXml(expected)).is(sortXml(actual));
}
private static StringBuilder indent(int depth, StringBuilder sb) {
for (int i = 0; i < depth; i++)
sb.append("\t");
return sb;
}
private static final void printLines(String[] lines) {
for (int i = 0; i < lines.length; i++)
System.err.println(String.format("%4s:" + lines[i], i+1)); // NOT DEBUG
}
}