org.jopendocument.util.JDOMUtils Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of jOpenDocument Show documentation
Show all versions of jOpenDocument Show documentation
jOpenDocument is a free library for developers looking to use
Open Document files without OpenOffice.org.
The newest version!
/*
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
*
* Copyright 2008-2013 jOpenDocument, by ILM Informatique. All rights reserved.
*
* The contents of this file are subject to the terms of the GNU
* General Public License Version 3 only ("GPL").
* You may not use this file except in compliance with the License.
* You can obtain a copy of the License at http://www.gnu.org/licenses/gpl-3.0.html
* See the License for the specific language governing permissions and limitations under the License.
*
* When distributing the software, include this License Header Notice in each file.
*
*/
package org.jopendocument.util;
import org.jopendocument.util.cc.IPredicate;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.StringReader;
import java.io.UnsupportedEncodingException;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.RandomAccess;
import javax.xml.transform.stream.StreamSource;
import javax.xml.validation.Schema;
import javax.xml.validation.Validator;
import org.jdom.Attribute;
import org.jdom.Content;
import org.jdom.Document;
import org.jdom.Element;
import org.jdom.JDOMException;
import org.jdom.Namespace;
import org.jdom.Text;
import org.jdom.filter.Filter;
import org.jdom.input.SAXBuilder;
import org.jdom.output.Format;
import org.jdom.output.XMLOutputter;
import org.xml.sax.ErrorHandler;
import org.xml.sax.SAXException;
/**
* @author ILM Informatique 26 juil. 2004
*/
public final class JDOMUtils {
public static final XMLOutputter OUTPUTTER;
private static final SAXBuilder BUILDER;
static {
final Format rawFormat = Format.getRawFormat();
// JDOM has \r\n hardcoded
rawFormat.setLineSeparator("\n");
OUTPUTTER = new XMLOutputter(rawFormat);
BUILDER = new SAXBuilder();
BUILDER.setValidation(false);
}
/**
* Analyse la chaine passée et retourne l'Element correspondant.
*
* @param xml une chaine contenant un élément XML.
* @param namespaces les namespaces utilisés dans la chaine.
* @return l'Element correspondant à la chaine passée.
* @throws JDOMException si l'xml n'est pas bien formé.
*/
public static Element parseElementString(String xml, Namespace[] namespaces) throws JDOMException {
// l'element passé est le seul enfant de dummy
// to be sure that the 0th can be cast use trim(), otherwise we might get a org.jdom.Text
return (Element) parseString(xml.trim(), namespaces).get(0);
}
/**
* Analyse la chaine passée et retourne la liste correspondante.
*
* @param xml une chaine contenant de l'XML.
* @param namespaces les namespaces utilisés dans la chaine.
* @return la liste correspondant à la chaine passée.
* @throws JDOMException si l'xml n'est pas bien formé.
*/
public static List parseString(String xml, Namespace[] namespaces) throws JDOMException {
// construit le dummy pour déclarer les namespaces
String dummy = "" + xml + " ";
return parseStringDocument(xml).getRootElement().removeContent();
}
/**
* Analyse la chaine passée et retourne l'Element correspondant.
*
* @param xml une chaine contenant de l'XML.
* @return l'Element correspondant à la chaine passée.
* @throws JDOMException si l'xml n'est pas bien formé.
* @see #parseElementString(String, Namespace[])
*/
public static Element parseString(String xml) throws JDOMException {
return parseElementString(xml, new Namespace[0]);
}
/**
* Analyse la chaine passée avec un builder par défaut et retourne le Document correspondant.
*
* @param xml une chaine représentant un document XML.
* @return le document correspondant.
* @throws JDOMException si l'xml n'est pas bien formé.
* @see #parseStringDocument(String, SAXBuilder)
*/
public static synchronized Document parseStringDocument(String xml) throws JDOMException {
// BUILDER is not thread safe
return parseStringDocument(xml, BUILDER);
}
/**
* Analyse la chaine passée et retourne le Document correspondant.
*
* @param xml une chaine représentant un document XML.
* @param builder le builder à utiliser.
* @return le document correspondant.
* @throws JDOMException si l'xml n'est pas bien formé.
*/
public static Document parseStringDocument(String xml, SAXBuilder builder) throws JDOMException {
Document doc = null;
try {
doc = builder.build(new StringReader(xml));
} catch (IOException e) {
// peut pas arriver, lis depuis une String
e.printStackTrace();
}
return doc;
}
/**
* Ecrit l'XML en chaine, contrairement a toString().
*
* @param xml l'élément à écrire.
* @return l'XML en tant que chaine.
*/
public static String output(Element xml) {
return OUTPUTTER.outputString(xml);
}
/**
* Ecrit l'XML en chaine, contrairement a toString().
*
* @param xml l'élément à écrire.
* @return l'XML en tant que chaine.
*/
public static String output(Document xml) {
return OUTPUTTER.outputString(xml);
}
public static Element getAncestor(Element element, final String name, final Namespace ns) {
return getAncestor(element, new IPredicate() {
@Override
public boolean evaluateChecked(Element elem) {
return elem.getName().equals(name) && elem.getNamespace().equals(ns);
}
});
}
public static Element getAncestor(Element element, final IPredicate pred) {
Element current = element;
while (current != null) {
if (pred.evaluateChecked(current))
return current;
current = current.getParentElement();
}
return null;
}
/**
* Add namespace declaration to elem
if needed. Necessary since JDOM uses a simple
* list.
*
* @param elem the element where namespaces should be available.
* @param c the namespaces to add.
* @see Element#addNamespaceDeclaration(Namespace)
*/
public static void addNamespaces(final Element elem, final Collection c) {
if (c instanceof RandomAccess && c instanceof List) {
final List list = (List) c;
final int stop = c.size() - 1;
for (int i = 0; i < stop; i++) {
final Namespace ns = list.get(i);
if (elem.getNamespace(ns.getPrefix()) == null)
elem.addNamespaceDeclaration(ns);
}
} else {
for (final Namespace ns : c) {
if (elem.getNamespace(ns.getPrefix()) == null)
elem.addNamespaceDeclaration(ns);
}
}
}
public static void addNamespaces(final Element elem, final Namespace... l) {
for (final Namespace ns : l) {
if (elem.getNamespace(ns.getPrefix()) == null)
elem.addNamespaceDeclaration(ns);
}
}
/**
* Get the requested child of parent
or create one if necessary. The created child
* is {@link Element#addContent(Content) added at the end}.
*
* @param parent the parent.
* @param name the name of the requested child.
* @param ns the namespace of the requested child.
* @return an existing or new child of parent
.
* @see Element#getChild(String, Namespace)
*/
public static Element getOrCreateChild(final Element parent, final String name, final Namespace ns) {
return getOrCreateChild(parent, name, ns, -1);
}
public static Element getOrCreateChild(final Element parent, final String name, final Namespace ns, final int index) {
Element res = parent.getChild(name, ns);
if (res == null) {
res = new Element(name, ns);
if (index < 0)
parent.addContent(res);
else
parent.addContent(index, res);
}
assert res.getParent() == parent;
return res;
}
/**
* Aka mkdir -p.
*
* @param current l'élément dans lequel créer la hierarchie.
* @param path le chemin des éléments à créer, chaque niveau séparé par "/".
* @return le dernier élément créé.
*/
public Element mkElem(Element current, String path) {
String[] items = path.split("/");
for (int i = 0; i < items.length; i++) {
String item = items[i];
String[] qname = item.split(":");
final Element elem;
if (qname.length == 1)
elem = new Element(item);
else
// MAYBE check if getNS return null and throw exn
elem = new Element(qname[1], current.getNamespace(qname[0]));
current.addContent(elem);
current = elem;
}
return current;
}
public static void insertAfter(final Element insertAfter, final Collection toAdd) {
insertSiblings(insertAfter, toAdd, true);
}
public static void insertBefore(final Element insertBefore, final Collection toAdd) {
insertSiblings(insertBefore, toAdd, false);
}
/**
* Add content before or after an element.
*
* @param sibling an element with a parent.
* @param toAdd the content to add alongside sibling
.
* @param after true
to add it after sibling
.
*/
public static void insertSiblings(final Element sibling, final Collection toAdd, final boolean after) {
final Element parentElement = sibling.getParentElement();
final int index = parentElement.indexOf(sibling);
parentElement.addContent(after ? index + 1 : index, toAdd);
}
/**
* Test if two elements have the same namespace and name.
*
* @param elem1 an element, can be null
.
* @param elem2 an element, can be null
.
* @return true
if both elements have the same name and namespace, or if both are
* null
.
*/
public static boolean equals(Element elem1, Element elem2) {
if (elem1 == elem2 || elem1 == null && elem2 == null)
return true;
else if (elem1 == null || elem2 == null)
return false;
else
return elem1.getName().equals(elem2.getName()) && elem1.getNamespace().equals(elem2.getNamespace());
}
/**
* Compare two elements and their descendants (only Element and Text). Texts are merged and
* normalized.
*
* @param elem1 first element.
* @param elem2 second element.
* @return true
if both elements are equal.
* @see #getContent(Element, IPredicate, boolean)
*/
public static boolean equalsDeep(Element elem1, Element elem2) {
return equalsDeep(elem1, elem2, true);
}
public static boolean equalsDeep(Element elem1, Element elem2, final boolean normalizeText) {
return getDiff(elem1, elem2, normalizeText) == null;
}
static String getDiff(Element elem1, Element elem2, final boolean normalizeText) {
if (elem1 == elem2)
return null;
if (!equals(elem1, elem2))
return "element name or namespace";
// ignore attributes order
@SuppressWarnings("unchecked")
final List attr1 = elem1.getAttributes();
@SuppressWarnings("unchecked")
final List attr2 = elem2.getAttributes();
if (attr1.size() != attr2.size())
return "attributes count";
for (final Attribute attr : attr1) {
if (!attr.getValue().equals(elem2.getAttributeValue(attr.getName(), attr.getNamespace())))
return "attribute value";
}
// use content order
final IPredicate filter = new IPredicate() {
@Override
public boolean evaluateChecked(Content input) {
return input instanceof Text || input instanceof Element;
}
};
// only check Element and Text (also merge them)
final Iterator contents1 = getContent(elem1, filter, true);
final Iterator contents2 = getContent(elem2, filter, true);
while (contents1.hasNext() && contents2.hasNext()) {
final Content content1 = contents1.next();
final Content content2 = contents2.next();
if (content1.getClass() != content2.getClass())
return "content";
if (content1 instanceof Text) {
final String s1 = normalizeText ? ((Text) content1).getTextNormalize() : content1.getValue();
final String s2 = normalizeText ? ((Text) content2).getTextNormalize() : content2.getValue();
if (!s1.equals(s2))
return "text";
} else {
final String rec = getDiff((Element) content1, (Element) content2, normalizeText);
if (rec != null)
return rec;
}
}
if (contents1.hasNext() || contents2.hasNext())
return "content size";
return null;
}
/**
* Get the filtered content of an element, optionnaly merging adjacent {@link Text}. Adjacent
* text can only happen programmatically.
*
* @param elem the parent.
* @param pred which content to return.
* @param mergeText true
if adjacent Text should be merged into one,
* false
to leave the list as it is.
* @return the filtered content (not supportting {@link Iterator#remove()}).
*/
public static Iterator getContent(final Element elem, final IPredicate pred, final boolean mergeText) {
@SuppressWarnings("unchecked")
final Iterator iter = (Iterator) elem.getContent(new Filter() {
@Override
public boolean matches(Object obj) {
return pred.evaluateChecked((Content) obj);
}
}).iterator();
if (!mergeText)
return iter;
return new Iterator() {
private Content next = null;
@Override
public boolean hasNext() {
return this.next != null || iter.hasNext();
}
@Override
public Content next() {
if (this.next != null) {
final Content res = this.next;
this.next = null;
return res;
}
Content res = iter.next();
assert res != null;
if (res instanceof Text && iter.hasNext()) {
this.next = iter.next();
Text concatText = null;
while (this.next instanceof Text) {
if (concatText == null) {
concatText = new Text(res.getValue());
}
concatText.append((Text) this.next);
this.next = iter.hasNext() ? iter.next() : null;
}
assert this.next != null;
if (concatText != null)
res = concatText;
}
return res;
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
};
}
// @return SAXException If a SAX error occurs during parsing of doc.
static SAXException validate(final Document doc, final Schema schema, final ErrorHandler errorHandler) {
ByteArrayInputStream ins;
try {
ins = new ByteArrayInputStream(output(doc).getBytes("UTF8"));
} catch (UnsupportedEncodingException e) {
throw new IllegalStateException("unicode not found ", e);
}
final Validator validator = schema.newValidator();
// ATTN workaround : contrary to documentation setting to null swallow exceptions
if (errorHandler != null)
validator.setErrorHandler(errorHandler);
try {
// don't use JDOMSource since it's as inefficient as this plus we can't control the
// output.
validator.validate(new StreamSource(ins));
return null;
} catch (IOException e) {
throw new IllegalStateException("Unable to read the document", e);
} catch (SAXException e) {
return e;
}
}
static void validateDTD(final Document doc, final SAXBuilder b, final ErrorHandler errorHandler) throws JDOMException {
final ErrorHandler origEH = b.getErrorHandler();
final boolean origValidation = b.getValidation();
try {
b.setErrorHandler(errorHandler);
b.setValidation(true);
JDOMUtils.parseStringDocument(output(doc), b);
} finally {
b.setErrorHandler(origEH);
b.setValidation(origValidation);
}
}
}