package org.jboss.errai.cdi.server.gwt.util;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import javax.xml.namespace.QName;
import javax.xml.stream.XMLEventFactory;
import javax.xml.stream.XMLEventReader;
import javax.xml.stream.XMLEventWriter;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLOutputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.events.Attribute;
import javax.xml.stream.events.EndElement;
import javax.xml.stream.events.StartElement;
import javax.xml.stream.events.XMLEvent;
import com.google.gwt.thirdparty.guava.common.collect.HashMultimap;
import com.google.gwt.thirdparty.guava.common.collect.Multimap;
/**
* Copies XML file to another location. Can filter out tags with provided names
* and attributes. Can insert tags into a parent with a given tag name.
*
* @author Max Barkley <mbarkley@redhat.com>
*/
public class SimpleTranslator {
/**
* An attribute key-value pair.
*
* @author Max Barkley
*/
public static class AttributeEntry {
private final String key;
private final String value;
public AttributeEntry(String key, String value) {
this.key = key;
this.value = value;
}
public String getKey() {
return key;
}
public String getValue() {
return value;
}
}
/**
* A simple xml tag with a local name and attribute key-value pairs.
*
* @author Max Barkley
*/
public static class Tag extends ArrayList<AttributeEntry> {
private static final long serialVersionUID = 1L;
private final String name;
public Tag(String name) {
this.name = name;
}
public Tag(String name, Collection<AttributeEntry> entries) {
super(entries);
this.name = name;
}
public Tag(String name, AttributeEntry... entries) {
this(name, Arrays.asList(entries));
}
public String getName() {
return name;
}
}
private static class XMLMatcher {
private Tag tag;
public XMLMatcher(Tag tag) {
this.tag = tag;
}
public boolean matches(StartElement element) {
if (!element.getName().getLocalPart().equals(tag.getName())) {
return false;
}
boolean matches = true;
for (final AttributeEntry entry : tag) {
Attribute attr = element.getAttributeByName(QName.valueOf(entry.getKey()));
if (attr == null || !attr.getValue().equals(entry.getValue())) {
matches = false;
break;
}
}
return matches;
}
}
private Multimap<String, XMLMatcher> toRemove = HashMultimap.create();
private Multimap<String, Tag> toAdd = HashMultimap.create();
/**
* Copy an xml file, removing any xml tags matching those provided with
* {@link SimpleTranslator#addFilter(Tag)} and adding any tags to provided
* with {@link SimpleTranslator#addNewTag(String, Tag)}.
*
* @param in
* A stream to an xml file.
* @param out
* A stream to an empty file.
*/
public void translate(InputStream in, OutputStream out) throws XMLStreamException {
final XMLInputFactory inFactory = XMLInputFactory.newInstance();
final XMLOutputFactory outFactory = XMLOutputFactory.newInstance();
final XMLEventReader reader = inFactory.createXMLEventReader(in);
final XMLEventWriter writer = outFactory.createXMLEventWriter(out);
try {
EventLoop: while (reader.hasNext()) {
XMLEvent event = reader.nextEvent();
if (event.isStartElement()) {
StartElement start = event.asStartElement();
if (isRemovable(start)) {
while (reader.hasNext()) {
XMLEvent next = reader.nextEvent();
if (next.isEndElement() && next.asEndElement().getName().equals(start.getName()))
continue EventLoop;
}
// If this is reached, we finished the document before the tag ended
throw new RuntimeException(String.format("End of file was reached before %s closing tag was found.",
start.getName()));
}
else if (toAdd.containsKey(start.getName().getLocalPart())
&& !toAdd.get(start.getName().getLocalPart()).isEmpty()) {
writer.add(event);
final XMLEventFactory eventFactory = XMLEventFactory.newInstance();
for (final Tag tag : toAdd.get(start.getName().getLocalPart())) {
List<Attribute> attrs = new ArrayList<Attribute>();
for (final AttributeEntry entry : tag) {
attrs.add(eventFactory.createAttribute(entry.getKey(), entry.getValue()));
}
StartElement newStart = eventFactory.createStartElement(QName.valueOf(tag.getName()), attrs.iterator(),
null);
EndElement newEnd = eventFactory.createEndElement(newStart.getName(), null);
writer.add(newStart);
writer.add(newEnd);
}
}
else {
writer.add(event);
}
}
else {
writer.add(event);
}
}
} finally {
writer.close();
reader.close();
}
}
private boolean isRemovable(StartElement start) {
Collection<XMLMatcher> matchers = toRemove.get(start.getName().getLocalPart());
if (matchers == null)
return false;
for (final XMLMatcher matcher : matchers) {
if (matcher.matches(start))
return true;
}
return false;
}
/**
* Add a description of a tag to be filtered on a subsequent call to
* {@link SimpleTranslator#translate(InputStream, OutputStream)}. {@code tagA}
* filters out {@code tagB} if both tags have the same name (ignoring
* namespaces) and the set of attribute key-value pairs in {@code tagB} is a
* superset of those in {@code tagA}.
*
* @param tag
* The tag description to be filtered.
*/
public void addFilter(Tag tag) {
toRemove.put(tag.getName(), new XMLMatcher(tag));
}
/**
* Add a description for a new tag to be added on a subsequent call to
* {@link SimpleTranslator#translate(InputStream, OutputStream)}. This tag
* will be added a child of any tag with given parent name (ignoring
* namespaces).
*
* @param parentName
* The name (ignoring namespaces) of a parent element, under which to
* insert a new tag.
* @param tag
* The new tag to be inserted.
*/
public void addNewTag(String parentName, Tag tag) {
toAdd.put(parentName, tag);
}
}