/*
* This file is part of GumTree.
*
* GumTree 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 3 of the License, or
* (at your option) any later version.
*
* GumTree 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 GumTree. If not, see <http://www.gnu.org/licenses/>.
*
* Copyright 2011-2015 Jean-Rémy Falleri <jr.falleri@gmail.com>
* Copyright 2011-2015 Floréal Morandat <florealm@gmail.com>
*/
package com.github.gumtreediff.io;
import com.github.gumtreediff.gen.Register;
import com.github.gumtreediff.gen.TreeGenerator;
import com.github.gumtreediff.matchers.MappingStore;
import com.github.gumtreediff.tree.ITree;
import com.github.gumtreediff.tree.TreeContext;
import com.github.gumtreediff.tree.TreeContext.MetadataSerializers;
import com.github.gumtreediff.tree.TreeContext.MetadataUnserializers;
import com.github.gumtreediff.tree.TreeUtils;
import com.google.gson.stream.JsonWriter;
import javax.xml.namespace.QName;
import javax.xml.stream.*;
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 java.io.*;
import java.util.Iterator;
import java.util.Map.Entry;
import java.util.Stack;
import java.util.regex.Pattern;
public final class TreeIoUtils {
private TreeIoUtils() {} // Forbids instantiation of TreeIOUtils
public static TreeGenerator fromXml() {
return new XmlInternalGenerator();
}
public static TreeGenerator fromXml(MetadataUnserializers unserializers) {
XmlInternalGenerator generator = new XmlInternalGenerator();
generator.getUnserializers().addAll(unserializers);
return generator;
}
public static TreeSerializer toXml(TreeContext ctx) {
return new TreeSerializer(ctx) {
@Override
protected TreeFormatter newFormatter(TreeContext ctx, MetadataSerializers serializers, Writer writer)
throws XMLStreamException {
return new XmlFormatter(writer, ctx);
}
};
}
public static TreeSerializer toAnnotatedXml(TreeContext ctx, boolean isSrc, MappingStore m) {
return new TreeSerializer(ctx) {
@Override
protected TreeFormatter newFormatter(TreeContext ctx, MetadataSerializers serializers, Writer writer)
throws XMLStreamException {
return new XmlAnnotatedFormatter(writer, ctx, isSrc, m);
}
};
}
public static TreeSerializer toCompactXml(TreeContext ctx) {
return new TreeSerializer(ctx) {
@Override
protected TreeFormatter newFormatter(TreeContext ctx, MetadataSerializers serializers, Writer writer)
throws Exception {
return new XmlCompactFormatter(writer, ctx);
}
};
}
public static TreeSerializer toJson(TreeContext ctx) {
return new TreeSerializer(ctx) {
@Override
protected TreeFormatter newFormatter(TreeContext ctx, MetadataSerializers serializers, Writer writer)
throws Exception {
return new JsonFormatter(writer, ctx);
}
};
}
public static TreeSerializer toLisp(TreeContext ctx) {
return new TreeSerializer(ctx) {
@Override
protected TreeFormatter newFormatter(TreeContext ctx, MetadataSerializers serializers, Writer writer)
throws Exception {
return new LispFormatter(writer, ctx);
}
};
}
public static TreeSerializer toDot(TreeContext ctx) {
return new TreeSerializer(ctx) {
@Override
protected TreeFormatter newFormatter(TreeContext ctx, MetadataSerializers serializer, Writer writer)
throws Exception {
return new DotFormatter(writer, ctx);
}
};
}
public abstract static class AbstractSerializer {
public abstract void writeTo(Writer writer) throws Exception;
public void writeTo(OutputStream writer) throws Exception {
OutputStreamWriter os = new OutputStreamWriter(writer);
try {
writeTo(os);
} finally {
os.close();
}
}
public String toString() {
StringWriter s = new StringWriter();
try {
writeTo(s);
s.close(); // FIXME this is useless (do nothing) but
// throws an exception, thus I dont' put it in the finally block where it belongs
} catch (Exception e) {
throw new RuntimeException(e);
}
return s.toString();
}
public void writeTo(String file) throws Exception {
FileWriter w = new FileWriter(file);
try {
writeTo(w);
} finally {
w.close();
}
}
public void writeTo(File file) throws Exception {
FileWriter w = new FileWriter(file);
try {
writeTo(w);
} finally {
w.close();
}
}
}
public abstract static class TreeSerializer extends AbstractSerializer {
final TreeContext context;
final MetadataSerializers serializers = new MetadataSerializers();
public TreeSerializer(TreeContext ctx) {
context = ctx;
serializers.addAll(ctx.getSerializers());
}
protected abstract TreeFormatter newFormatter(TreeContext ctx, MetadataSerializers serializers, Writer writer)
throws Exception;
public void writeTo(Writer writer) throws Exception {
TreeFormatter formatter = newFormatter(context, serializers, writer);
try {
writeTree(formatter, context.getRoot());
} finally {
formatter.close();
}
}
private void forwardException(Exception e) {
throw new FormatException(e);
}
protected void writeTree(TreeFormatter formatter, ITree root) throws Exception {
formatter.startSerialization();
writeAttributes(formatter, context.getMetadata());
formatter.endProlog();
try {
TreeUtils.visitTree(root, new TreeUtils.TreeVisitor() {
@Override
public void startTree(ITree tree) {
try {
assert formatter != null;
assert tree != null;
formatter.startTree(tree);
writeAttributes(formatter, tree.getMetadata());
formatter.endTreeProlog(tree);
} catch (Exception e) {
forwardException(e);
}
}
@Override
public void endTree(ITree tree) {
try {
formatter.endTree(tree);
} catch (Exception e) {
forwardException(e);
}
}
});
} catch (FormatException e) {
throw e.getCause();
}
formatter.stopSerialization();
}
protected void writeAttributes(TreeFormatter formatter, Iterator<Entry<String, Object>> it) throws Exception {
while (it.hasNext()) {
Entry<String, Object> entry = it.next();
String k = entry.getKey();
serializers.serialize(formatter, entry.getKey(), entry.getValue());
}
}
public TreeSerializer export(String name, MetadataSerializer serializer) {
serializers.add(name, serializer);
return this;
}
public TreeSerializer export(String... name) {
for (String n: name)
serializers.add(n, x -> x.toString());
return this;
}
}
public interface TreeFormatter {
void startSerialization() throws Exception;
void endProlog() throws Exception;
void stopSerialization() throws Exception;
void startTree(ITree tree) throws Exception;
void endTreeProlog(ITree tree) throws Exception;
void endTree(ITree tree) throws Exception;
void close() throws Exception;
void serializeAttribute(String name, String value) throws Exception;
}
@FunctionalInterface
public interface MetadataSerializer {
String toString(Object object);
}
@FunctionalInterface
public interface MetadataUnserializer {
Object fromString(String value);
}
static class FormatException extends RuntimeException {
private static final long serialVersionUID = 593766540545763066L;
Exception cause;
public FormatException(Exception cause) {
super(cause);
this.cause = cause;
}
@Override
public Exception getCause() {
return cause;
}
}
static class TreeFormatterAdapter implements TreeFormatter {
protected final TreeContext context;
protected TreeFormatterAdapter(TreeContext ctx) {
context = ctx;
}
@Override
public void startSerialization() throws Exception { }
@Override
public void endProlog() throws Exception { }
@Override
public void startTree(ITree tree) throws Exception { }
@Override
public void endTreeProlog(ITree tree) throws Exception { }
@Override
public void endTree(ITree tree) throws Exception { }
@Override
public void stopSerialization() throws Exception { }
@Override
public void close() throws Exception { }
@Override
public void serializeAttribute(String name, String value) throws Exception { }
}
abstract static class AbsXmlFormatter extends TreeFormatterAdapter {
protected final XMLStreamWriter writer;
protected AbsXmlFormatter(Writer w, TreeContext ctx) throws XMLStreamException {
super(ctx);
XMLOutputFactory f = XMLOutputFactory.newInstance();
writer = new IndentingXMLStreamWriter(f.createXMLStreamWriter(w));
}
@Override
public void startSerialization() throws XMLStreamException {
writer.writeStartDocument();
}
@Override
public void stopSerialization() throws XMLStreamException {
writer.writeEndDocument();
}
@Override
public void close() throws XMLStreamException {
writer.close();
}
}
static class XmlFormatter extends AbsXmlFormatter {
public XmlFormatter(Writer w, TreeContext ctx) throws XMLStreamException {
super(w, ctx);
}
@Override
public void startSerialization() throws XMLStreamException {
super.startSerialization();
writer.writeStartElement("root");
writer.writeStartElement("context");
}
@Override
public void endProlog() throws XMLStreamException {
writer.writeEndElement();
}
@Override
public void stopSerialization() throws XMLStreamException {
writer.writeEndElement();
super.stopSerialization();
}
@Override
public void serializeAttribute(String name, String value) throws XMLStreamException {
writer.writeStartElement(name);
writer.writeCharacters(value);
writer.writeEndElement();
}
@Override
public void startTree(ITree tree) throws XMLStreamException {
writer.writeStartElement("tree");
writer.writeAttribute("type", Integer.toString(tree.getType()));
if (tree.hasLabel()) writer.writeAttribute("label", tree.getLabel());
if (context.hasLabelFor(tree.getType()))
writer.writeAttribute("typeLabel", context.getTypeLabel(tree.getType()));
if (ITree.NO_VALUE != tree.getPos()) {
writer.writeAttribute("pos", Integer.toString(tree.getPos()));
writer.writeAttribute("length", Integer.toString(tree.getLength()));
}
}
@Override
public void endTree(ITree tree) throws XMLStreamException {
writer.writeEndElement();
}
}
static class XmlAnnotatedFormatter extends XmlFormatter {
final SearchOther searchOther;
public XmlAnnotatedFormatter(Writer w, TreeContext ctx, boolean isSrc,
MappingStore m) throws XMLStreamException {
super(w, ctx);
if (isSrc)
searchOther = (tree) -> {
return m.hasSrc(tree) ? m.getDst(tree) : null;
};
else
searchOther = (tree) -> {
return m.hasDst(tree) ? m.getSrc(tree) : null;
};
}
interface SearchOther {
ITree lookup(ITree tree);
}
@Override
public void startTree(ITree tree) throws XMLStreamException {
super.startTree(tree);
ITree o = searchOther.lookup(tree);
if (o != null) {
if (ITree.NO_VALUE != o.getPos()) {
writer.writeAttribute("other_pos", Integer.toString(o.getPos()));
writer.writeAttribute("other_length", Integer.toString(o.getLength()));
}
}
}
}
static class XmlCompactFormatter extends AbsXmlFormatter {
public XmlCompactFormatter(Writer w, TreeContext ctx) throws XMLStreamException {
super(w, ctx);
}
@Override
public void startSerialization() throws XMLStreamException {
super.startSerialization();
writer.writeStartElement("root");
}
@Override
public void stopSerialization() throws XMLStreamException {
writer.writeEndElement();
super.stopSerialization();
}
@Override
public void serializeAttribute(String name, String value) throws XMLStreamException {
writer.writeAttribute(name, value);
}
@Override
public void startTree(ITree tree) throws XMLStreamException {
writer.writeStartElement(context.getTypeLabel(tree.getType()));
if (tree.hasLabel()) writer.writeAttribute("label", tree.getLabel());
}
@Override
public void endTree(ITree tree) throws XMLStreamException {
writer.writeEndElement();
}
}
static class LispFormatter extends TreeFormatterAdapter {
protected final Writer writer;
protected final Pattern replacer = Pattern.compile("[\\\\\"]");
int level = 0;
protected LispFormatter(Writer w, TreeContext ctx) {
super(ctx);
writer = w;
}
@Override
public void startSerialization() throws IOException {
writer.write("((");
}
@Override
public void startTree(ITree tree) throws IOException {
if (!tree.isRoot())
writer.write("\n");
for (int i = 0; i < level; i ++)
writer.write(" ");
level ++;
String pos = (ITree.NO_VALUE == tree.getPos() ? "" : String.format("(%d %d)",
tree.getPos(), tree.getLength()));
String matched = tree.isMatched() ? ":matched " : "";
writer.write(String.format("(%d %s %s %s(%s",
tree.getType(), protect(context.getTypeLabel(tree)), protect(tree.getLabel()),
matched, pos));
}
@Override
public void endProlog() throws Exception {
writer.append(") ");
}
@Override
public void endTreeProlog(ITree tree) throws Exception {
writer.append(") (");
}
@Override
public void serializeAttribute(String name, String value) throws Exception {
writer.append(String.format("(:%s %s) ", name, protect(value)));
}
protected String protect(String val) {
return String.format("\"%s\"", replacer.matcher(val).replaceAll("\\\\$0"));
}
@Override
public void endTree(ITree tree) throws IOException {
writer.write(")");
level --;
}
@Override
public void stopSerialization() throws IOException {
writer.write(")");
}
}
static class DotFormatter extends TreeFormatterAdapter {
protected final Writer writer;
protected DotFormatter(Writer w, TreeContext ctx) {
super(ctx);
writer = w;
}
@Override
public void startSerialization() throws Exception {
writer.write("digraph G {\n");
}
@Override
public void startTree(ITree tree) throws Exception {
String label = tree.toPrettyString(context);
if (label.contains("\"") || label.contains("\\s"))
label = label.replaceAll("\"", "").replaceAll("\\s", "").replaceAll("\\\\", "");
if (label.length() > 30)
label = label.substring(0, 30);
writer.write(tree.getId() + " [label=\"" + label + "\"");
if (tree.isMatched())
writer.write(",style=filled,fillcolor=cadetblue1");
writer.write("];\n");
if (tree.getParent() != null)
writer.write(tree.getParent().getId() + " -> " + tree.getId() + ";\n");
}
@Override
public void stopSerialization() throws Exception {
writer.write("}");
}
}
static class JsonFormatter extends TreeFormatterAdapter {
private final JsonWriter writer;
public JsonFormatter(Writer w, TreeContext ctx) {
super(ctx);
writer = new JsonWriter(w);
}
@Override
public void startTree(ITree t) throws IOException {
writer.beginObject();
writer.name("type").value(Integer.toString(t.getType()));
if (t.hasLabel()) writer.name("label").value(t.getLabel());
if (context.hasLabelFor(t.getType())) writer.name("typeLabel").value(context.getTypeLabel(t.getType()));
if (ITree.NO_VALUE != t.getPos()) {
writer.name("pos").value(Integer.toString(t.getPos()));
writer.name("length").value(Integer.toString(t.getLength()));
}
}
@Override
public void endTreeProlog(ITree tree) throws IOException {
writer.name("children");
writer.beginArray();
}
@Override
public void endTree(ITree tree) throws IOException {
writer.endArray();
writer.endObject();
}
@Override
public void startSerialization() throws IOException {
writer.beginObject();
writer.setIndent("\t");
}
@Override
public void endProlog() throws IOException {
writer.name("root");
}
@Override
public void serializeAttribute(String key, String value) throws IOException {
writer.name(key).value(value);
}
@Override
public void stopSerialization() throws IOException {
writer.endObject();
}
@Override
public void close() throws IOException {
writer.close();
}
}
@Register(id = "xml", accept = "\\.gxml$")
// TODO Since it is not in the right package, I'm not even sure it is visible in the registry
// TODO should we move this class elsewhere (another package)
public static class XmlInternalGenerator extends TreeGenerator {
static MetadataUnserializers defaultUnserializers = new MetadataUnserializers();
final MetadataUnserializers unserializers = new MetadataUnserializers(); // FIXME should it be pushed up or not?
private static final QName TYPE = new QName("type");
private static final QName LABEL = new QName("label");
private static final QName TYPE_LABEL = new QName("typeLabel");
private static final String POS = "pos";
private static final String LENGTH = "length";
static {
defaultUnserializers.add(POS, x -> Integer.parseInt(x));
defaultUnserializers.add(LENGTH, x -> Integer.parseInt(x));
}
public XmlInternalGenerator() {
unserializers.addAll(defaultUnserializers);
}
@Override
protected TreeContext generate(Reader source) throws IOException {
XMLInputFactory fact = XMLInputFactory.newInstance();
TreeContext context = new TreeContext();
try {
Stack<ITree> trees = new Stack<>();
XMLEventReader r = fact.createXMLEventReader(source);
while (r.hasNext()) {
XMLEvent e = r.nextEvent();
if (e instanceof StartElement) {
StartElement s = (StartElement) e;
if (!s.getName().getLocalPart().equals("tree")) // FIXME need to deal with options
continue;
int type = Integer.parseInt(s.getAttributeByName(TYPE).getValue());
ITree t = context.createTree(type,
labelForAttribute(s, LABEL), labelForAttribute(s, TYPE_LABEL));
Iterator<Attribute> it = s.getAttributes();
while (it.hasNext()) {
Attribute a = it.next();
unserializers.load(t, a.getName().getLocalPart(), a.getValue());
}
if (trees.isEmpty())
context.setRoot(t);
else
t.setParentAndUpdateChildren(trees.peek());
trees.push(t);
} else if (e instanceof EndElement) {
if (!((EndElement)e).getName().getLocalPart().equals("tree")) // FIXME need to deal with options
continue;
trees.pop();
}
}
context.validate();
return context;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
private static String labelForAttribute(StartElement s, QName attrName) {
Attribute attr = s.getAttributeByName(attrName);
return attr == null ? ITree.NO_LABEL : attr.getValue();
}
public MetadataUnserializers getUnserializers() {
return unserializers;
}
}
}