package nl.knaw.huygens.alexandria.service;
/*
* #%L
* alexandria-service
* =======
* Copyright (C) 2015 - 2017 Huygens ING (KNAW)
* =======
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public
* License along with this program. If not, see
* <http://www.gnu.org/licenses/gpl-3.0.html>.
* #L%
*/
import static java.util.stream.Collectors.joining;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.time.Instant;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import javax.ws.rs.core.StreamingOutput;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.tinkerpop.gremlin.structure.Direction;
import org.apache.tinkerpop.gremlin.structure.Vertex;
import org.apache.tinkerpop.gremlin.tinkergraph.structure.TinkerGraph;
import org.junit.Before;
import org.junit.Test;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import nl.knaw.huygens.Log;
import nl.knaw.huygens.alexandria.api.model.text.TextRangeAnnotation;
import nl.knaw.huygens.alexandria.api.model.text.TextRangeAnnotation.AbsolutePosition;
import nl.knaw.huygens.alexandria.api.model.text.TextRangeAnnotation.Position;
import nl.knaw.huygens.alexandria.config.MockConfiguration;
import nl.knaw.huygens.alexandria.endpoint.EndpointPathResolver;
import nl.knaw.huygens.alexandria.endpoint.LocationBuilder;
import nl.knaw.huygens.alexandria.model.AlexandriaResource;
import nl.knaw.huygens.alexandria.model.TentativeAlexandriaProvenance;
import nl.knaw.huygens.alexandria.storage.EdgeLabels;
import nl.knaw.huygens.alexandria.storage.Storage;
import nl.knaw.huygens.alexandria.storage.frames.TextRangeAnnotationVF;
import nl.knaw.huygens.alexandria.test.AlexandriaTest;
import nl.knaw.huygens.alexandria.textgraph.DotFactory;
import nl.knaw.huygens.alexandria.textgraph.ParseResult;
import nl.knaw.huygens.alexandria.textgraph.TextAnnotation;
import nl.knaw.huygens.alexandria.textgraph.TextGraphUtil;
public class TextGraphServiceTest extends AlexandriaTest {
private static final LocationBuilder LOCATION_BUILDER = new LocationBuilder(new MockConfiguration(), new EndpointPathResolver());
private Storage storage = new Storage(TinkerGraph.open());
private TinkerPopService service = new TinkerGraphService(LOCATION_BUILDER);
private TextGraphService tgs;
@Before
public void before() {
service.setStorage(storage);
tgs = new TextGraphService(storage);
}
@Test
public void testUpdateTextAnnotationLink1() {
String xml = "<text><p xml:id=\"p-1\">This is a paragraph.</p></text>";
String expected = "<text><p xml:id=\"p-1\">This <word resp=\"#ed\">is</word> a paragraph.</p></text>";
assertAnnotationCorrectlyInserted(xml, expected);
}
@Test
public void testUpdateTextAnnotationLink2() {
String xml = "<text><p xml:id=\"p-1\">This <b>is a</b> different paragraph.</p></text>";
String expected = "<text><p xml:id=\"p-1\">This <b><word resp=\"#ed\">is</word> a</b> different paragraph.</p></text>";
assertAnnotationCorrectlyInserted(xml, expected);
}
@Test
public void testUpdateTextAnnotationLink3() {
String xml = "<text><p xml:id=\"p-1\"><i>This is</i> a third paragraph.</p></text>";
String expected = "<text><p xml:id=\"p-1\"><i>This <word resp=\"#ed\">is</word></i> a third paragraph.</p></text>";
assertAnnotationCorrectlyInserted(xml, expected);
}
@Test
public void testUpdateTextAnnotationLink4() {
String xml = "<text><p xml:id=\"p-1\">This <i>is</i> a test.</p></text>";
String expected = "<text><p xml:id=\"p-1\"><lang value=\"?\" resp=\"#tool\">This <i>is</i> a test.</lang></p></text>";
TextRangeAnnotation textRangeAnnotation = new TextRangeAnnotation()//
.setId(UUID.randomUUID())//
.setName("lang")//
.setAnnotator("tool")//
.setAttributes(ImmutableMap.of("value", "?"))//
.setPosition(new Position().setXmlId("p-1").setOffset(1).setLength(15));
setAbsolutePosition(textRangeAnnotation);
UUID resourceUUID = UUID.randomUUID();
Log.info("xml={}", xml);
String xmlOut = storage.runInTransaction(() -> {
createResourceWithText(resourceUUID, xml);
TextRangeAnnotationVF vf = storage.createVF(TextRangeAnnotationVF.class);
dumpDot(resourceUUID);
tgs.updateTextAnnotationLink(vf, textRangeAnnotation, resourceUUID);
dumpDot(resourceUUID);
return getXML(resourceUUID);
});
softly.assertThat(xmlOut).isEqualTo(expected);
}
@Test
public void testUpdateTextAnnotationLink5() {
String xml = "<text xml:id=\"text-1\"><p xml:id=\"p-1\"><i>This</i> is a test.</p></text>";
String expected = "<text xml:id=\"text-1\"><lang value=\"?\" resp=\"#tool\"><p xml:id=\"p-1\"><i>This</i> is a test.</p></lang></text>";
Position position = new Position().setXmlId("text-1").setOffset(1).setLength(15);
ImmutableMap<String, String> attributes = ImmutableMap.of("value", "?");
UUID resourceUUID = UUID.randomUUID();
TextRangeAnnotation textRangeAnnotation = new TextRangeAnnotation()//
.setId(resourceUUID)//
.setAnnotator("tool")//
.setName("lang")//
.setAttributes(attributes)//
.setUseOffset(false)//
.setPosition(position);
assertExpectation(xml, expected, resourceUUID, textRangeAnnotation);
}
@Test
public void testUpdateTextAnnotationLink6() {
String xml = "<text xml:id=\"text-1\"><p xml:id=\"p-1\"><i>This</i> is encroyable.</p></text>";
String expected = "<text xml:id=\"text-1\"><p xml:id=\"p-1\"><lang value=\"en\" resp=\"#tool\"><i>This</i> is</lang> encroyable.</p></text>";
Position position = new Position().setXmlId("text-1").setOffset(1).setLength(7);
ImmutableMap<String, String> attributes = ImmutableMap.of("value", "en");
UUID resourceUUID = UUID.randomUUID();
TextRangeAnnotation textRangeAnnotation = new TextRangeAnnotation()//
.setId(resourceUUID)//
.setAnnotator("tool")//
.setName("lang")//
.setAttributes(attributes)//
.setPosition(position);
assertExpectation(xml, expected, resourceUUID, textRangeAnnotation);
}
@Test
public void testUpdateTextAnnotationLinkNLA318() {
String xml = //
singleQuotesToDouble("<p xml:id='p-1'>...epouse mad<sup>le</sup> "//
+ "de <sic>Gendrin</sic>"//
+ " soeur du feu archevesque de Sens...</p>");
String expected = //
singleQuotesToDouble("<p xml:id='p-1'>...epouse mad<sup>le</sup> "//
+ "<persName key='S0328208' resp='#ckcc'>de <sic>Gendrin</sic></persName>"//
+ " soeur du feu archevesque de Sens...</p>");
Position position1 = new Position()//
.setXmlId("p-1")//
.setOffset(17)//
.setLength(10);
Map<String, String> attributes = ImmutableMap.of("key", "S0328208");
UUID annotationUUID = UUID.randomUUID();
TextRangeAnnotation textRangeAnnotation = new TextRangeAnnotation()//
.setId(annotationUUID)//
.setName("persName")//
.setAnnotator("ckcc")//
.setPosition(position1)//
.setAttributes(attributes);
assertExpectation(xml, expected, annotationUUID, textRangeAnnotation);
}
@Test
public void testUpdateTextAnnotationLinkNLA318a() {
String xml = //
singleQuotesToDouble("<ae xml:id='a-1'>A <be>B <c>C</c> D <e>E</e></be></ae>");
String expected = //
singleQuotesToDouble("<ae xml:id='a-1'>A <be>B <cd resp='#ckcc'><c>C</c> D</cd> <e>E</e></be></ae>");
Position position1 = new Position()//
.setXmlId("a-1")//
.setOffset(5)//
.setLength(3);
UUID annotationUUID = UUID.randomUUID();
TextRangeAnnotation textRangeAnnotation = new TextRangeAnnotation()//
.setId(annotationUUID)//
.setName("cd")//
.setAnnotator("ckcc")//
.setPosition(position1);
assertExpectation(xml, expected, annotationUUID, textRangeAnnotation);
}
@Test
public void testUpdateTextAnnotationLinkbNLA318a() {
String xml = //
singleQuotesToDouble("<ae xml:id='a-1'>A <be>B <c>C</c> D <e>E</e></be></ae>");
String expected = //
singleQuotesToDouble("<ae xml:id='a-1'>A <be><bd resp='#ckcc'>B <c>C</c> D</bd> <e>E</e></be></ae>");
Position position1 = new Position()//
.setXmlId("a-1")//
.setOffset(3)//
.setLength(5);
UUID annotationUUID = UUID.randomUUID();
TextRangeAnnotation textRangeAnnotation = new TextRangeAnnotation()//
.setId(annotationUUID)//
.setName("bd")//
.setAnnotator("ckcc")//
.setPosition(position1);
assertExpectation(xml, expected, annotationUUID, textRangeAnnotation);
}
@Test
public void testCreatePairsWithTwoLayers() throws Exception {
Vertex v1 = mockVertex("1");
Vertex v2 = mockVertex("2");
Vertex va = mockVertex("a");
Vertex vb = mockVertex("b");
List<Vertex> layer1 = ImmutableList.of(v1, v2);
List<Vertex> layer2 = ImmutableList.of(va, vb);
List<List<Vertex>> vertexListPerLayer = ImmutableList.of(layer1, layer2);
List<Pair<Vertex, Vertex>> pairs = tgs.createPairs(vertexListPerLayer);
Log.info("pairs={}", pairs);
assertThat(pairs).containsExactly(//
Pair.of(v1, va), //
Pair.of(v1, vb), //
Pair.of(v2, va), //
Pair.of(v2, vb)//
);
}
@Test
public void testCreatePairsWithThreeLayers() throws Exception {
Vertex v1 = mockVertex("1");
Vertex v2 = mockVertex("2");
Vertex va = mockVertex("a");
Vertex vb = mockVertex("b");
Vertex vx = mockVertex("X");
Vertex vy = mockVertex("Y");
List<Vertex> layer1 = ImmutableList.of(v1, v2);
List<Vertex> layer2 = ImmutableList.of(va, vb);
List<Vertex> layer3 = ImmutableList.of(vx, vy);
List<List<Vertex>> vertexListPerLayer = ImmutableList.of(layer1, layer2, layer3);
List<Pair<Vertex, Vertex>> pairs = tgs.createPairs(vertexListPerLayer);
Log.info("pairs={}", pairs);
assertThat(pairs).containsExactly(//
Pair.of(v1, va), //
Pair.of(v1, vb), //
Pair.of(v2, va), //
Pair.of(v2, vb), //
Pair.of(v1, vx), //
Pair.of(v1, vy), //
Pair.of(v2, vx), //
Pair.of(v2, vy), //
Pair.of(va, vx), //
Pair.of(va, vy), //
Pair.of(vb, vx), //
Pair.of(vb, vy) //
);
}
@Test
public void testGroupByDecreasingDepth() throws Exception {
Vertex v1 = mockVertex("1", 0);
Vertex v2 = mockVertex("2", 0);
Vertex va = mockVertex("a", 1);
Vertex vb = mockVertex("b", 1);
Vertex vx = mockVertex("X", 2);
Vertex vy = mockVertex("Y", 2);
List<Vertex> vertexList = ImmutableList.of(va, vb, v1, v2, vx, vy);
List<List<Vertex>> layers = tgs.groupByDecreasingDepth(vertexList);
List<Vertex> layer0 = ImmutableList.of(v1, v2);
List<Vertex> layer1 = ImmutableList.of(va, vb);
List<Vertex> layer2 = ImmutableList.of(vx, vy);
assertThat(layers).containsExactly(layer2, layer1, layer0);
}
@Test
public void testHasSameTextRangePasses() throws Exception {
Vertex firstTextSegment = mock(Vertex.class);
List<Vertex> firstTextSegmentList = ImmutableList.of(firstTextSegment);
Vertex lastTextSegment = mock(Vertex.class);
List<Vertex> lastTextSegmentList = ImmutableList.of(lastTextSegment);
Vertex v1 = mockVertex("1");
when(v1.vertices(Direction.OUT, EdgeLabels.FIRST_TEXT_SEGMENT)).thenReturn(firstTextSegmentList.iterator());
when(v1.vertices(Direction.OUT, EdgeLabels.LAST_TEXT_SEGMENT)).thenReturn(lastTextSegmentList.iterator());
Vertex va = mockVertex("a");
when(va.vertices(Direction.OUT, EdgeLabels.FIRST_TEXT_SEGMENT)).thenReturn(firstTextSegmentList.iterator());
when(va.vertices(Direction.OUT, EdgeLabels.LAST_TEXT_SEGMENT)).thenReturn(lastTextSegmentList.iterator());
Pair<Vertex, Vertex> pair = Pair.of(v1, va);
assertThat(tgs.hasSameTextRange(pair)).isTrue();
}
@Test
public void testHasSameTextRangeFails() throws Exception {
Vertex firstTextSegment = mock(Vertex.class);
List<Vertex> firstTextSegmentList = ImmutableList.of(firstTextSegment);
Vertex lastTextSegment = mock(Vertex.class);
List<Vertex> lastTextSegmentList = ImmutableList.of(lastTextSegment);
Vertex v1 = mockVertex("1");
when(v1.vertices(Direction.OUT, EdgeLabels.FIRST_TEXT_SEGMENT)).thenReturn(firstTextSegmentList.iterator());
when(v1.vertices(Direction.OUT, EdgeLabels.LAST_TEXT_SEGMENT)).thenReturn(firstTextSegmentList.iterator());
Vertex va = mockVertex("a");
when(va.vertices(Direction.OUT, EdgeLabels.FIRST_TEXT_SEGMENT)).thenReturn(firstTextSegmentList.iterator());
when(va.vertices(Direction.OUT, EdgeLabels.LAST_TEXT_SEGMENT)).thenReturn(lastTextSegmentList.iterator());
Pair<Vertex, Vertex> pair = Pair.of(v1, va);
assertThat(tgs.hasSameTextRange(pair)).isFalse();
}
private void assertExpectation(String xml, String expected, UUID resourceUUID, TextRangeAnnotation textRangeAnnotation) {
Log.info("xml={}", xml);
setAbsolutePosition(textRangeAnnotation);
String xmlOut = storage.runInTransaction(() -> {
createResourceWithText(resourceUUID, xml);
showTextAnnotationList(resourceUUID);
TextRangeAnnotationVF vf = storage.createVF(TextRangeAnnotationVF.class);
// dumpDot(resourceUUID);
tgs.updateTextAnnotationLink(vf, textRangeAnnotation, resourceUUID);
// dumpDot(resourceUUID);
showTextAnnotationList(resourceUUID);
return getXML(resourceUUID);
});
softly.assertThat(xmlOut).isEqualTo(expected);
}
private void showTextAnnotationList(UUID resourceUUID) {
TextGraphService tgs = new TextGraphService(storage);
String textAnnotations = tgs.getTextAnnotationStream(resourceUUID)//
.map(this::logTextAnnotation)//
.collect(joining());
Log.info("TextAnnotations = {}", textAnnotations);
}
private String logTextAnnotation(TextAnnotation ta) {
String string = "<" + ta.getName() + " depth=" + ta.getDepth()
+ ta.getAttributes()//
.entrySet()//
.stream()//
.map(kv -> " " + kv.getKey() + "='" + kv.getValue() + "'")//
.collect(joining(" "))
+ ">";
Log.info(string);
return string;
}
private void assertAnnotationCorrectlyInserted(String xml, String expected) {
String xmlOut = getAnnotatedText(xml);
softly.assertThat(xmlOut).isEqualTo(expected);
}
private String getAnnotatedText(String xml) {
TextRangeAnnotation textRangeAnnotation = new TextRangeAnnotation()//
.setId(UUID.randomUUID())//
.setName("word")//
.setAnnotator("ed")//
.setPosition(new Position().setXmlId("p-1").setOffset(6).setLength(2));
setAbsolutePosition(textRangeAnnotation);
UUID resourceUUID = UUID.randomUUID();
Log.info("xml={}", xml);
return storage.runInTransaction(() -> {
createResourceWithText(resourceUUID, xml);
TextRangeAnnotationVF vf = storage.createVF(TextRangeAnnotationVF.class);
// dumpDot(resourceUUID);
tgs.updateTextAnnotationLink(vf, textRangeAnnotation, resourceUUID);
dumpDot(resourceUUID);
return getXML(resourceUUID);
});
}
private void setAbsolutePosition(TextRangeAnnotation textRangeAnnotation) {
setAbsolutePosition(textRangeAnnotation, 1);
}
private void setAbsolutePosition(TextRangeAnnotation textRangeAnnotation, int length) {
Position position = textRangeAnnotation.getPosition();
AbsolutePosition absolutePosition = new AbsolutePosition()//
.setXmlId(position.getXmlId().get())//
.setOffset(position.getOffset().orElse(1))//
.setLength(position.getLength().orElse(length))//
;
textRangeAnnotation.setAbsolutePosition(absolutePosition);
}
private String getXML(UUID resourceUUID) {
StreamingOutput outputStream = TextGraphUtil.streamXML(service, resourceUUID);
return TextGraphUtil.asString(outputStream);
}
private void createResourceWithText(UUID resourceUUID, String xml) {
TentativeAlexandriaProvenance provenance = new TentativeAlexandriaProvenance("who", Instant.now(), "why");
AlexandriaResource resource = new AlexandriaResource(resourceUUID, provenance);
service.createOrUpdateResource(resource);
ParseResult parseresult = TextGraphUtil.parse(xml);
service.storeTextGraph(resourceUUID, parseresult);
}
void dumpDb() {
Log.info("dumping server graph as graphML:");
try {
ByteArrayOutputStream outputstream = new ByteArrayOutputStream();
service.dumpToGraphML(outputstream);
outputstream.flush();
outputstream.close();
System.out.println(outputstream.toString());
} catch (IOException e) {
e.printStackTrace();
}
Log.info("dumping done");
}
private void dumpDot(UUID resourceUUID) {
Log.info("dumping textgraph as dot:");
String dot = DotFactory.createDot(service, resourceUUID);
Log.info("dot={}", dot);
Log.info("dumping done");
}
private Vertex mockVertex(String value) {
Vertex v1 = mock(Vertex.class);
when(v1.value("name")).thenReturn(value);
return v1;
}
private Vertex mockVertex(String value, int depth) {
Vertex v1 = mockVertex(value);
when(v1.value("depth")).thenReturn(depth);
return v1;
}
}