org.atteo.xmlcombiner.XmlCombiner Maven / Gradle / Ivy
/*
* 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 org.atteo.xmlcombiner;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map.Entry;
import java.util.Set;
import javax.annotation.Nullable;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.Result;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
import com.google.common.base.Splitter;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Lists;
/**
* Combines two or more XML DOM trees.
*
*
* The merging algorithm is as follows:
* First direct subelements of selected node are examined.
* The elements from both trees with matching keys are paired.
* Based on selected behavior the content of the paired elements is then merged.
* Finally the paired elements are recursively combined. Any not paired elements are appended.
*
*
* You can control merging behavior using {@link CombineSelf 'combine.self'}
* and {@link CombineChildren 'combine.children'} attributes.
*
*
* The merging algorithm was inspired by similar functionality in Plexus Utils.
*
*
* @see merging in Maven
* @see Plexus utils implementation of merging
*/
public class XmlCombiner {
private final DocumentBuilder documentBuilder;
private final Document document;
private final List defaultAttributeNames;
public static void main(String[] args) throws ParserConfigurationException, SAXException, IOException,
TransformerException {
List files = new ArrayList<>();
List ids = new ArrayList<>();
boolean onlyFiles = false;
for (int i = 0; i < args.length; i++) {
if (!onlyFiles) {
switch (args[i]) {
case "--key":
ids.add(args[i+1]);
i++;
break;
case "--":
onlyFiles = true;
break;
default:
files.add(Paths.get(args[i]));
}
} else {
files.add(Paths.get(args[i]));
}
}
XmlCombiner xmlCombiner = new XmlCombiner(ids);
for (Path file : files) {
xmlCombiner.combine(file);
}
xmlCombiner.buildDocument(System.out);
}
/**
* Creates XML combiner using default {@link DocumentBuilder}.
* @throws ParserConfigurationException when {@link DocumentBuilder} creation fails
*/
public XmlCombiner() throws ParserConfigurationException {
this(DocumentBuilderFactory.newInstance().newDocumentBuilder());
}
public XmlCombiner(DocumentBuilder documentBuilder) {
this(documentBuilder, Lists.newArrayList());
}
/**
* Creates XML combiner using given attribute as an id.
*/
public XmlCombiner(String idAttributeName) throws ParserConfigurationException {
this(Lists.newArrayList(idAttributeName));
}
public XmlCombiner(List keyAttributeNames) throws ParserConfigurationException {
this(DocumentBuilderFactory.newInstance().newDocumentBuilder(), keyAttributeNames);
}
/**
* Creates XML combiner using given document builder and an id attribute name.
*/
public XmlCombiner(DocumentBuilder documentBuilder, String keyAttributeNames) {
this(documentBuilder, Lists.newArrayList(keyAttributeNames));
}
public XmlCombiner(DocumentBuilder documentBuilder, List keyAttributeNames) {
this.documentBuilder = documentBuilder;
document = documentBuilder.newDocument();
this.defaultAttributeNames = keyAttributeNames;
}
/**
* Combine given file.
* @param file file to combine
*/
public void combine(Path file) throws SAXException, IOException {
combine(documentBuilder.parse(file.toFile()));
}
/**
* Combine given input stream.
* @param stream input stream to combine
*/
public void combine(InputStream stream) throws SAXException, IOException {
combine(documentBuilder.parse(stream));
}
/**
* Combine given document.
* @param document document to combine
*/
public void combine(Document document) {
combine(document.getDocumentElement());
}
/**
* Combine given element.
* @param element element to combine
*/
public void combine(Element element) {
Element parent = document.getDocumentElement();
if (parent != null) {
document.removeChild(parent);
}
Context result = combine(Context.fromElement(parent), Context.fromElement(element));
result.addAsChildTo(document);
}
/**
* Return the result of the merging process.
*/
public Document buildDocument() {
filterOutDefaults(Context.fromElement(document.getDocumentElement()));
filterOutCombines(document.getDocumentElement());
return document;
}
/**
* Stores the result of the merging process.
*/
public void buildDocument(OutputStream out) throws TransformerException {
Document result = buildDocument();
Transformer transformer = TransformerFactory.newInstance().newTransformer();
Result output = new StreamResult(out);
Source input = new DOMSource(result);
transformer.transform(input, output);
}
/**
* Stores the result of the merging process.
*/
public void buildDocument(Path path) throws TransformerException, FileNotFoundException {
buildDocument(new FileOutputStream(path.toFile()));
}
private Context combine(Context recessive, Context dominant) {
CombineSelf dominantCombineSelf = getCombineSelf(dominant.getElement());
CombineSelf recessiveCombineSelf = getCombineSelf(recessive.getElement());
if (dominantCombineSelf == CombineSelf.REMOVE) {
return null;
} else if (dominantCombineSelf == CombineSelf.OVERRIDE
|| (recessiveCombineSelf == CombineSelf.OVERRIDABLE)) {
Context result = copyRecursively(dominant);
result.getElement().removeAttribute(CombineSelf.ATTRIBUTE_NAME);
return result;
}
CombineChildren combineChildren = getCombineChildren(dominant.getElement());
if (combineChildren == null) {
combineChildren = getCombineChildren(recessive.getElement());
if (combineChildren == null) {
combineChildren = CombineChildren.MERGE;
}
}
if (combineChildren == CombineChildren.APPEND) {
if (recessive.getElement() != null) {
removeWhitespaceTail(recessive.getElement());
appendRecursively(dominant, recessive);
return recessive;
} else {
return copyRecursively(dominant);
}
}
Element resultElement = document.createElement(dominant.getElement().getTagName());
copyAttributes(recessive.getElement(), resultElement);
copyAttributes(dominant.getElement(), resultElement);
// when dominant combineSelf is null or DEFAULTS use combineSelf from recessive
CombineSelf combineSelf = dominantCombineSelf;
if ((combineSelf == null && recessiveCombineSelf != CombineSelf.DEFAULTS)) {
//|| (combineSelf == CombineSelf.DEFAULTS && recessive.getElement() != null)) {
combineSelf = recessiveCombineSelf;
}
if (combineSelf != null) {
resultElement.setAttribute(CombineSelf.ATTRIBUTE_NAME, combineSelf.name().toLowerCase());
} else {
resultElement.removeAttribute(CombineSelf.ATTRIBUTE_NAME);
}
List keys = defaultAttributeNames;
if (recessive.getElement() != null) {
Attr keysNode = recessive.getElement().getAttributeNode(Context.KEYS_ATTRIBUTE_NAME);
if (keysNode != null) {
keys = Splitter.on(",").splitToList(keysNode.getValue());
}
}
if (dominant.getElement() != null) {
Attr keysNode = dominant.getElement().getAttributeNode(Context.KEYS_ATTRIBUTE_NAME);
if (keysNode != null) {
keys = Splitter.on(",").splitToList(keysNode.getValue());
}
}
ListMultimap recessiveContexts = recessive.mapChildContexts(keys);
ListMultimap dominantContexts = dominant.mapChildContexts(keys);
Set tagNamesInDominant = getTagNames(dominantContexts);
// Execute only if there is at least one subelement in recessive
if (!recessiveContexts.isEmpty()) {
for (Entry entry : recessiveContexts.entries()) {
Key key = entry.getKey();
Context recessiveContext = entry.getValue();
if (key == Key.BEFORE_END) {
continue;
}
if (getCombineSelf(recessiveContext.getElement()) == CombineSelf.OVERRIDABLE_BY_TAG) {
if (!tagNamesInDominant.contains(key.getName())) {
recessiveContext.addAsChildTo(resultElement);
}
continue;
}
if (dominantContexts.get(key).size() == 1 && recessiveContexts.get(key).size() == 1) {
Context dominantContext = dominantContexts.get(key).iterator().next();
Context combined = combine(recessiveContext, dominantContext);
if (combined != null) {
combined.addAsChildTo(resultElement);
}
} else {
recessiveContext.addAsChildTo(resultElement);
}
}
}
for (Entry entry : dominantContexts.entries()) {
Key key = entry.getKey();
Context dominantContext = entry.getValue();
if (key == Key.BEFORE_END) {
dominantContext.addAsChildTo(resultElement, document);
// break? this should be the last anyway...
continue;
}
List associatedRecessives = recessiveContexts.get(key);
if (dominantContexts.get(key).size() == 1 && associatedRecessives.size() == 1
&& getCombineSelf(associatedRecessives.get(0).getElement()) != CombineSelf.OVERRIDABLE_BY_TAG) {
// already added
} else {
Context combined = combine(Context.fromElement(null), dominantContext);
if (combined != null) {
combined.addAsChildTo(resultElement);
}
}
}
Context result = new Context();
result.setElement(resultElement);
appendNeighbours(dominant, result);
return result;
}
/**
* Copy element recursively.
* @param context context to copy, it is assumed it is from unrelated document
* @return copied element in current document
*/
private Context copyRecursively(Context context) {
Context copy = new Context();
appendNeighbours(context, copy);
Element element = (Element) document.importNode(context.getElement(), false);
copy.setElement(element);
appendRecursively(context, copy);
return copy;
}
/**
* Append neighbors from source to destination
* @param source source element, it is assumed it is from unrelated document
* @param destination destination element
*/
private void appendNeighbours(Context source, Context destination) {
for (Node neighbour : source.getNeighbours()) {
destination.addNeighbour(document.importNode(neighbour, true));
}
}
/**
* Appends all attributes and subelements from source element do destination element.
* @param source source element, it is assumed it is from unrelated document
* @param destination destination element
*/
private void appendRecursively(Context source, Context destination) {
copyAttributes(source.getElement(), destination.getElement());
List contexts = source.groupChildContexts();
for (Context context : contexts) {
if (context.getElement() == null) {
context.addAsChildTo(destination.getElement(), document);
continue;
}
Context combined = combine(Context.fromElement(null), context);
if (combined != null) {
combined.addAsChildTo(destination.getElement());
}
}
}
/**
* Copies attributes from one {@link Element} to the other.
* @param source source element
* @param destination destination element
*/
private void copyAttributes(@Nullable Element source, Element destination) {
if (source == null) {
return;
}
NamedNodeMap attributes = source.getAttributes();
for (int i = 0; i < attributes.getLength(); i++) {
Attr attribute = (Attr) attributes.item(i);
Attr destAttribute = destination.getAttributeNodeNS(attribute.getNamespaceURI(), attribute.getName());
if (destAttribute == null) {
destination.setAttributeNodeNS((Attr) document.importNode(attribute, true));
} else {
destAttribute.setValue(attribute.getValue());
}
}
}
private static CombineSelf getCombineSelf(@Nullable Element element) {
CombineSelf combine = null;
if (element == null) {
return null;
}
Attr combineAttribute = element.getAttributeNode(CombineSelf.ATTRIBUTE_NAME);
if (combineAttribute != null) {
try {
combine = CombineSelf.valueOf(combineAttribute.getValue().toUpperCase());
} catch (IllegalArgumentException e) {
throw new RuntimeException("The attribute 'combine' of element '"
+ element.getTagName() + "' has invalid value '"
+ combineAttribute.getValue(), e);
}
}
return combine;
}
private static CombineChildren getCombineChildren(@Nullable Element element) {
CombineChildren combine = null;
if (element == null) {
return null;
}
Attr combineAttribute = element.getAttributeNode(CombineChildren.ATTRIBUTE_NAME);
if (combineAttribute != null) {
try {
combine = CombineChildren.valueOf(combineAttribute.getValue().toUpperCase());
} catch (IllegalArgumentException e) {
throw new RuntimeException("The attribute 'combine' of element '"
+ element.getTagName() + "' has invalid value '"
+ combineAttribute.getValue(), e);
}
}
return combine;
}
private static void removeWhitespaceTail(Element element) {
NodeList list = element.getChildNodes();
for (int i = list.getLength() - 1; i >= 0; i--) {
Node node = list.item(i);
if (node instanceof Element) {
break;
}
element.removeChild(node);
}
}
private static void filterOutDefaults(Context context) {
Element element = context.getElement();
List childContexts = context.groupChildContexts();
for (Context childContext : childContexts) {
if (childContext.getElement() == null) {
continue;
}
CombineSelf combineSelf = getCombineSelf(childContext.getElement());
if (combineSelf == CombineSelf.DEFAULTS) {
for (Node neighbour : childContext.getNeighbours()) {
element.removeChild(neighbour);
}
element.removeChild(childContext.getElement());
} else {
filterOutDefaults(childContext);
}
}
}
private static void filterOutCombines(Element element) {
element.removeAttribute(CombineSelf.ATTRIBUTE_NAME);
element.removeAttribute(CombineChildren.ATTRIBUTE_NAME);
element.removeAttribute(Context.KEYS_ATTRIBUTE_NAME);
element.removeAttribute(Context.ID_ATTRIBUTE_NAME);
NodeList childNodes = element.getChildNodes();
for (int i = 0; i < childNodes.getLength(); i++) {
Node item = childNodes.item(i);
if (item instanceof Element) {
filterOutCombines((Element) item);
}
}
}
private static Set getTagNames(ListMultimap dominantContexts) {
Set names = new HashSet<>();
for (Key key : dominantContexts.keys()) {
names.add(key.getName());
}
return names;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy