1 package eu.fbk.dkm.pikes.rdf.naf;
2
3 import com.github.mustachejava.DefaultMustacheFactory;
4 import com.github.mustachejava.Mustache;
5 import com.github.mustachejava.util.LatchedWriter;
6 import com.google.common.base.Charsets;
7 import com.google.common.base.MoreObjects;
8 import com.google.common.base.Preconditions;
9 import com.google.common.base.Strings;
10 import com.google.common.collect.*;
11 import com.google.common.html.HtmlEscapers;
12 import eu.fbk.dkm.pikes.naflib.NafRenderUtils;
13 import eu.fbk.dkm.pikes.naflib.NafRenderUtils.Markable;
14 import eu.fbk.dkm.pikes.rdf.api.Renderer;
15 import eu.fbk.dkm.pikes.rdf.util.ModelUtil;
16 import eu.fbk.dkm.pikes.rdf.util.RDFGraphvizRenderer;
17 import eu.fbk.dkm.pikes.rdf.vocab.KS;
18 import eu.fbk.dkm.pikes.resources.NAFUtils;
19 import eu.fbk.dkm.pikes.rdf.vocab.NIF;
20 import eu.fbk.dkm.pikes.rdf.vocab.NWR;
21 import eu.fbk.dkm.pikes.rdf.vocab.OWLTIME;
22 import eu.fbk.dkm.pikes.rdf.vocab.SUMO;
23 import eu.fbk.rdfpro.util.Hash;
24 import eu.fbk.rdfpro.util.Namespaces;
25 import eu.fbk.rdfpro.util.QuadModel;
26 import eu.fbk.rdfpro.util.Statements;
27 import ixa.kaflib.KAFDocument;
28 import ixa.kaflib.Term;
29 import org.eclipse.rdf4j.model.*;
30 import org.eclipse.rdf4j.model.vocabulary.RDF;
31 import org.eclipse.rdf4j.model.vocabulary.XMLSchema;
32 import org.slf4j.Logger;
33 import org.slf4j.LoggerFactory;
34
35 import javax.annotation.Nullable;
36 import java.io.*;
37 import java.net.URL;
38 import java.util.Arrays;
39 import java.util.List;
40 import java.util.Map;
41 import java.util.Set;
42 import java.util.concurrent.Callable;
43
44 public class NAFRenderer implements Renderer {
45
46 private static final Logger LOGGER = LoggerFactory.getLogger(Renderer.class);
47
48 private static final Set<IRI> DEFAULT_NODE_TYPES = ImmutableSet.of(KS.INSTANCE, KS.ATTRIBUTE);
49
50 private static final Set<String> DEFAULT_NODE_NAMESPACES = ImmutableSet
51 .of("http://dbpedia.org/resource/");
52
53 private static final Map<Object, String> DEFAULT_COLOR_MAP = ImmutableMap
54 .<Object, String>builder()
55 .put("node", "#F0F0F0")
56 .put(NWR.PERSON, "#FFC8C8")
57 .put(NWR.ORGANIZATION, "#FFFF84")
58 .put(NWR.LOCATION, "#A9C5EB")
59 .put(KS.ATTRIBUTE, "#EEBBEE")
60 .put(SUMO.PROCESS, "#CFE990")
61 .put(SUMO.RELATION, "#FFFFFF")
62 .put(OWLTIME.INTERVAL, "#B4D1B6")
63 .put(OWLTIME.DATE_TIME_INTERVAL, "#B4D1B6")
64 .put(OWLTIME.PROPER_INTERVAL, "#B4D1B6")
65 .put(NWR.MISC, "#D1BAA2")
66 .build();
67
68 private static final Map<Object, String> DEFAULT_STYLE_MAP = ImmutableMap.of();
69
70 private static final Mustache DEFAULT_TEMPLATE = loadTemplate("Renderer.html");
71
72 private static final List<String> DEFAULT_RANKED_NAMESPACES = ImmutableList.of(
73 "http://www.newsreader-project.eu/ontologies/propbank/",
74 "http://www.newsreader-project.eu/ontologies/nombank/");
75
76 private final Ordering<Value> valueComparator;
77
78 private final Ordering<Statement> statementComparator;
79
80 private final Map<Object, String> colorMap;
81
82 private final Mustache template;
83
84 private final Map<String, ?> templateParameters;
85
86 private final RDFGraphvizRenderer graphvizRenderer;
87
88 private NAFRenderer(final Builder builder) {
89 this.colorMap = builder.colorMap == null ? DEFAULT_COLOR_MAP : ImmutableMap
90 .copyOf(builder.colorMap);
91 this.valueComparator = Ordering.from(Statements.valueComparator(Iterables.toArray(
92 builder.rankedNamespaces == null ? DEFAULT_RANKED_NAMESPACES
93 : builder.rankedNamespaces, String.class)));
94 this.statementComparator = Ordering.from(Statements.statementComparator("spoc",
95 this.valueComparator));
96 this.graphvizRenderer = RDFGraphvizRenderer
97 .builder()
98 .withNodeNamespaces(
99 builder.nodeNamespaces == null ? DEFAULT_NODE_NAMESPACES
100 : builder.nodeNamespaces)
101 .withNodeTypes(builder.nodeTypes == null ? DEFAULT_NODE_TYPES : builder.nodeTypes)
102 .withValueComparator(this.valueComparator)
103 .withColorMap(this.colorMap)
104 .withStyleMap(MoreObjects.firstNonNull(builder.styleMap, DEFAULT_STYLE_MAP))
105 .withGraphvizCommand("neato").build();
106 this.template = MoreObjects.firstNonNull(builder.template, DEFAULT_TEMPLATE);
107 this.templateParameters = builder.templateParameters;
108 }
109
110 @Override
111 public void render(final Object document, final QuadModel model, final Appendable out)
112 throws IOException {
113
114 final long ts = System.currentTimeMillis();
115 final KAFDocument doc = (KAFDocument) document;
116
117 final List<Map<String, Object>> sentencesModel = Lists.newArrayList();
118 for (int i = 1; i <= doc.getNumSentences(); ++i) {
119 final int sentenceID = i;
120 final Map<String, Object> sm = Maps.newHashMap();
121 sm.put("id", i);
122 sm.put("markup", (Callable<String>) () -> {
123 return renderText(new StringBuilder(), doc, doc.getTermsBySent(sentenceID), model)
124 .toString();
125 });
126 sm.put("parsing", (Callable<String>) () -> {
127 return renderParsing(new StringBuilder(), doc, model, sentenceID).toString();
128 });
129 sm.put("graph",
130 (Callable<String>) () -> {
131 int begin = Integer.MAX_VALUE;
132 int end = Integer.MIN_VALUE;
133 for (final Term term : doc.getSentenceTerms(sentenceID)) {
134 begin = Math.min(begin, NAFUtils.getBegin(term));
135 end = Math.max(end, NAFUtils.getEnd(term));
136 }
137 final QuadModel sentenceModel = ModelUtil.getSubModel(model,
138 ModelUtil.getMentions(model, begin, end));
139 return renderGraph(new StringBuilder(), sentenceModel).toString();
140
141 });
142 sentencesModel.add(sm);
143 }
144
145 final Map<String, Object> documentModel = Maps.newHashMap();
146 documentModel.put("title", doc.getPublic().uri);
147 documentModel.put("sentences", sentencesModel);
148 documentModel.put("metadata", (Callable<String>) () -> {
149 return renderProperties(new StringBuilder(), model,
150 Statements.VALUE_FACTORY.createIRI(doc.getPublic().uri), true).toString();
151 });
152 documentModel.put("mentions", (Callable<String>) () -> {
153 return renderMentionsTable(new StringBuilder(), model).toString();
154 });
155 documentModel.put("triples", (Callable<String>) () -> {
156 return renderTriplesTable(new StringBuilder(), model).toString();
157 });
158 documentModel.put("graph", (Callable<String>) () -> {
159 return renderGraph(new StringBuilder(), model).toString();
160 });
161 documentModel.put("naf", (Callable<String>) () -> {
162 return doc.toString();
163 });
164
165 documentModel.putAll(this.templateParameters);
166 if (this.templateParameters != null) {
167 documentModel.putAll(this.templateParameters);
168 }
169
170 final Mustache actualTemplate = this.template != null ? loadTemplate(this.template)
171 : this.template;
172 if (out instanceof Writer) {
173 Writer outWriter;
174 outWriter = actualTemplate.execute((Writer) out, documentModel);
175 if (outWriter instanceof LatchedWriter) {
176 ((LatchedWriter) outWriter).await();
177 outWriter.flush();
178 }
179 } else {
180 final StringWriter writer = new StringWriter();
181 actualTemplate.execute(writer, documentModel).close();
182 out.append(writer.toString());
183 }
184
185 if (LOGGER.isDebugEnabled()) {
186 LOGGER.debug("Done in {} ms", System.currentTimeMillis() - ts);
187 }
188 }
189
190 private <T extends Appendable> T renderGraph(final T out, final QuadModel model)
191 throws IOException {
192 return this.graphvizRenderer.emitSVG(out, model);
193 }
194
195 private <T extends Appendable> T renderText(final T out, final KAFDocument document,
196 final Iterable<Term> terms, final QuadModel model) throws IOException {
197 final List<Term> termList = Ordering.from(Term.OFFSET_COMPARATOR).sortedCopy(terms);
198 NafRenderUtils.renderText(out, document, terms,
199 extractMarkables(termList, model, this.colorMap));
200 return out;
201 }
202
203 private <T extends Appendable> T renderParsing(final T out, final KAFDocument document,
204 @Nullable final QuadModel model, final int sentence) throws IOException {
205 NafRenderUtils.renderParsing(out, document, sentence, true, true,
206 extractMarkables(document.getTermsBySent(sentence), model, this.colorMap));
207 return out;
208 }
209
210 private <T extends Appendable> T renderProperties(final T out, final QuadModel model,
211 final Resource node, final boolean emitID, final IRI... excludedProperties)
212 throws IOException {
213
214 final Set<Resource> seen = Sets.newHashSet(node);
215 renderPropertiesHelper(out, model, node, emitID, seen,
216 ImmutableSet.copyOf(excludedProperties));
217 return out;
218 }
219
220 private <T extends Appendable> T renderPropertiesHelper(final T out, final QuadModel model,
221 final Resource node, final boolean emitID, final Set<Resource> seen,
222 final Set<IRI> excludedProperties) throws IOException {
223
224
225 out.append("<table class=\"properties table table-condensed\">\n<tbody>\n");
226
227
228 if (emitID) {
229 out.append("<tr><td><a>ID</a>:</td><td>");
230 renderObject(out, node, model);
231 out.append("</td></tr>\n");
232 }
233
234
235 for (final IRI pred : this.valueComparator.sortedCopy(model.filter(node, null, null)
236 .predicates())) {
237 if (excludedProperties.contains(pred)) {
238 continue;
239 }
240 out.append("<tr><td>");
241 renderObject(out, pred, model);
242 out.append(":</td><td>");
243 final List<Resource> nested = Lists.newArrayList();
244 String separator = "";
245 for (final Value obj : this.valueComparator.sortedCopy(model.filter(node, pred, null)
246 .objects())) {
247 if (obj instanceof Literal || model.filter((Resource) obj, null, null).isEmpty()) {
248 out.append(separator);
249 renderObject(out, obj, model);
250 separator = ", ";
251 } else {
252 nested.add((Resource) obj);
253 }
254 }
255 out.append("".equals(separator) ? "" : "<br/>");
256 for (final Resource obj : nested) {
257 out.append(separator);
258 if (seen.add(obj)) {
259 renderPropertiesHelper(out, model, obj, true, seen, excludedProperties);
260 } else {
261 renderObject(out, obj, model);
262 }
263 }
264 out.append("</td></tr>\n");
265 }
266
267
268 out.append("</tbody>\n</table>\n");
269 return out;
270 }
271
272 private <T extends Appendable> T renderTriplesTable(final T out, final QuadModel model)
273 throws IOException {
274
275 out.append("<table class=\"table table-condensed datatable\">\n<thead>\n");
276 out.append("<tr><th width='25%' class='col-ts'>");
277 out.append(shorten(RDF.SUBJECT));
278 out.append("</th><th width='25%' class='col-tp'>");
279 out.append(shorten(RDF.PREDICATE));
280 out.append("</th><th width='25%' class='col-to'>");
281 out.append(shorten(RDF.OBJECT));
282 out.append("</th><th width='25%' class='col-te'>");
283 out.append(shorten(KS.EXPRESSES)).append("<sup>-1</sup>");
284 out.append("</th></tr>\n");
285 out.append("</thead>\n<tbody>\n");
286
287 for (final Statement statement : this.statementComparator.sortedCopy(model)) {
288 if (statement.getContext() != null) {
289 out.append("<tr><td>");
290 renderObject(out, statement.getSubject(), model);
291 out.append("</td><td>");
292 renderObject(out, statement.getPredicate(), model);
293 out.append("</td><td>");
294 renderObject(out, statement.getObject(), model);
295 out.append("</td><td>");
296 String separator = "";
297 for (final Resource mentionID : model.filter(statement.getContext(), KS.EXPRESSES,
298 null).subjects()) {
299 final String extent = model.filter(mentionID, NIF.ANCHOR_OF, null)
300 .objectLiteral().stringValue();
301 out.append(separator);
302 renderObject(out, mentionID, model);
303 out.append(" '").append(escape(extent)).append("'");
304 separator = "<br/>";
305 }
306 out.append("</ol></td></tr>\n");
307 }
308 }
309
310 out.append("</tbody>\n</table>\n");
311 return out;
312 }
313
314 private <T extends Appendable> T renderMentionsTable(final T out, final QuadModel model)
315 throws IOException {
316
317 out.append("<table class=\"table table-condensed datatable\">\n<thead>\n");
318 out.append("<tr><th width='12%' class='col-mi'>id</th><th width='18%' class='col-ma'>");
319 out.append(shorten(NIF.ANCHOR_OF));
320 out.append("</th><th width='11%' class='col-mt'>");
321 out.append(shorten(RDF.TYPE));
322 out.append("</th><th width='18%' class='col-mo'>mention attributes</th><th width='11%' class='col-md'>");
323 out.append(shorten(KS.DENOTES)).append("/").append(shorten(KS.IMPLIES));
324 out.append("</th><th width='30%' class='col-me'>");
325 out.append(shorten(KS.EXPRESSES));
326 out.append("</th></tr>\n</thead>\n<tbody>\n");
327
328 for (final Resource mentionID : this.valueComparator.sortedCopy(ModelUtil
329 .getMentions(model))) {
330 out.append("<tr><td>");
331 renderObject(out, mentionID, model);
332 out.append("</td><td>");
333 out.append(model.filter(mentionID, NIF.ANCHOR_OF, null).objectString());
334 out.append("</td><td>");
335 renderObject(out, model.filter(mentionID, RDF.TYPE, null).objects(), model);
336 out.append("</td><td>");
337 final QuadModel mentionModel = QuadModel.create();
338 for (final Statement statement : model.filter(mentionID, null, null)) {
339 final IRI pred = statement.getPredicate();
340 if (!NIF.BEGIN_INDEX.equals(pred) && !NIF.END_INDEX.equals(pred)
341 && !NIF.ANCHOR_OF.equals(pred) && !RDF.TYPE.equals(pred)
342 && !KS.MENTION_OF.equals(pred)) {
343 mentionModel.add(statement);
344 }
345 }
346 if (!mentionModel.isEmpty()) {
347 renderProperties(out, mentionModel, mentionID, false);
348 }
349 out.append("</td><td>");
350 renderObject(out, Iterables.concat(
351 model.filter(mentionID, KS.DENOTES, null).objects(),
352 model.filter(mentionID, KS.IMPLIES, null).objects()), model);
353 out.append("</td><td><ol>");
354 for (final Value factID : model.filter(mentionID, KS.EXPRESSES, null).objects()) {
355 for (final Statement statement : model.filter(null, null, null, (Resource) factID)) {
356 out.append("<li>");
357 renderObject(out, statement.getSubject(), model);
358 out.append(", ");
359 renderObject(out, statement.getPredicate(), model);
360 out.append(", ");
361 renderObject(out, statement.getObject(), model);
362 out.append("</li>");
363 }
364 }
365 out.append("</ol></td></tr>\n");
366 }
367
368 out.append("</tbody>\n</table>\n");
369 return out;
370 }
371
372 private <T extends Appendable> T renderObject(final T out, final Object object,
373 @Nullable final QuadModel model) throws IOException {
374
375 if (object instanceof IRI) {
376 final IRI uri = (IRI) object;
377 out.append("<a>").append(shorten(uri)).append("</a>");
378
379 } else if (object instanceof Literal) {
380 final Literal literal = (Literal) object;
381 out.append("<span");
382 if (literal.getLanguage().isPresent()) {
383 out.append(" title=\"@").append(literal.getLanguage().get()).append("\"");
384 } else if (!literal.getDatatype().equals(XMLSchema.STRING)) {
385 out.append(" title=\"").append(shorten(literal.getDatatype())).append("\"");
386 }
387 out.append(">").append(literal.stringValue()).append("</span>");
388
389 } else if (object instanceof BNode) {
390 final BNode bnode = (BNode) object;
391 out.append("_:").append(bnode.getID());
392
393 } else if (object instanceof Iterable<?>) {
394 String separator = "";
395 for (final Object element : (Iterable<?>) object) {
396 out.append(separator);
397 renderObject(out, element, model);
398 separator = "<br/>";
399 }
400
401 } else if (object != null) {
402 out.append(object.toString());
403 }
404 return out;
405 }
406
407 private static List<Markable> extractMarkables(final List<Term> terms, final QuadModel model,
408 final Map<Object, String> colorMap) {
409
410 final int[] offsets = new int[terms.size()];
411 for (int i = 0; i < terms.size(); ++i) {
412 offsets[i] = terms.get(i).getOffset();
413 }
414
415 final List<Markable> markables = Lists.newArrayList();
416 for (final Statement stmt : model.filter(null, KS.DENOTES, null)) {
417 final Resource instance = (Resource) stmt.getObject();
418 final String color = select(colorMap,
419 model.filter(instance, RDF.TYPE, null).objects(), null);
420 if (stmt.getSubject() instanceof IRI && color != null) {
421 final IRI mentionIRI = (IRI) stmt.getSubject();
422 final String name = mentionIRI.getLocalName();
423 if (name.indexOf(';') < 0) {
424 final int index = name.indexOf(',');
425 final int start = Integer.parseInt(name.substring(5, index));
426 final int end = Integer.parseInt(name.substring(index + 1));
427 final int s = Arrays.binarySearch(offsets, start);
428 if (s >= 0) {
429 int e = s;
430 while (e < offsets.length && offsets[e] < end) {
431 ++e;
432 }
433 markables.add(new Markable(ImmutableList.copyOf(terms.subList(s, e)),
434 color));
435 }
436 }
437 }
438 }
439
440 return markables;
441 }
442
443 private static String select(final Map<Object, String> map,
444 final Iterable<? extends Value> keys, final String defaultColor) {
445 String color = null;
446 for (final Value key : keys) {
447 if (key instanceof IRI) {
448 final String mappedColor = map.get(key);
449 if (mappedColor != null) {
450 if (color == null) {
451 color = mappedColor;
452 } else {
453 break;
454 }
455 }
456 }
457 }
458 return color != null ? color : defaultColor;
459 }
460
461 private static String escape(final String string) {
462 return HtmlEscapers.htmlEscaper().escape(string);
463 }
464
465 @Nullable
466 private static String shorten(@Nullable final IRI uri) {
467 if (uri == null) {
468 return null;
469 }
470 final String prefix = Namespaces.DEFAULT.prefixFor(uri.getNamespace());
471 if (prefix != null) {
472 return prefix + ':' + uri.getLocalName();
473 }
474 return "<../" + uri.getLocalName() + ">";
475 }
476
477 private static Mustache loadTemplate(final Object spec) {
478
479 Preconditions.checkNotNull(spec);
480 try {
481 if (spec instanceof Mustache) {
482 return (Mustache) spec;
483 }
484 final DefaultMustacheFactory factory = new DefaultMustacheFactory();
485
486 URL url = spec instanceof URL ? (URL) spec : null;
487 if (url == null) {
488 try {
489 url = Renderer.class.getResource(spec.toString());
490 } catch (final Throwable ex) {
491
492 }
493 }
494 if (url == null) {
495 final File file = spec instanceof File ? (File) spec : new File(spec.toString());
496 if (file.exists()) {
497 url = file.toURI().toURL();
498 }
499 }
500 if (url != null) {
501 return factory.compile(new InputStreamReader(url.openStream(), Charsets.UTF_8),
502 url.toString());
503 } else {
504 return factory.compile(new StringReader(spec.toString()),
505 Hash.murmur3(spec.toString()).toString());
506 }
507 } catch (final IOException ex) {
508 throw new IllegalArgumentException("Could not create Mustache template for " + spec);
509 }
510 }
511
512 public static Builder builder() {
513 return new Builder();
514 }
515
516 public static final class Builder {
517
518 @Nullable
519 private Iterable<? extends IRI> nodeTypes;
520
521 @Nullable
522 private Iterable<String> nodeNamespaces;
523
524 @Nullable
525 private Iterable<String> rankedNamespaces;
526
527 @Nullable
528 private Map<Object, String> colorMap;
529
530 @Nullable
531 private Map<Object, String> styleMap;
532
533 @Nullable
534 private Mustache template;
535
536 private final Map<String, Object> templateParameters;
537
538 Builder() {
539 this.templateParameters = Maps.newHashMap();
540 }
541
542 public Builder withProperties(final Map<?, ?> properties, @Nullable final String prefix) {
543 final String p = prefix == null ? "" : prefix.endsWith(".") ? prefix : prefix + ".";
544 for (final Map.Entry<?, ?> entry : properties.entrySet()) {
545 if (entry.getKey() != null && entry.getValue() != null
546 && entry.getKey().toString().startsWith(p)) {
547 final String name = entry.getKey().toString().substring(p.length());
548 final String value = Strings.emptyToNull(entry.getValue().toString());
549 if ("template".equals(name)) {
550 withTemplate(value);
551 } else if (name.startsWith("template.")) {
552 withTemplateParameter(name.substring("template.".length()), value);
553 }
554 }
555 }
556 return this;
557 }
558
559 public Builder withNodeTypes(@Nullable final Iterable<? extends IRI> nodeTypes) {
560 this.nodeTypes = nodeTypes;
561 return this;
562 }
563
564 public Builder withNodeNamespaces(@Nullable final Iterable<String> nodeNamespaces) {
565 this.nodeNamespaces = nodeNamespaces;
566 return this;
567 }
568
569 public Builder withRankedNamespaces(@Nullable final Iterable<String> rankedNamespaces) {
570 this.rankedNamespaces = rankedNamespaces;
571 return this;
572 }
573
574 public Builder withColorMap(@Nullable final Map<Object, String> colorMap) {
575 this.colorMap = colorMap;
576 return this;
577 }
578
579 public Builder withStyleMap(@Nullable final Map<Object, String> styleMap) {
580 this.styleMap = styleMap;
581 return this;
582 }
583
584 public Builder withTemplate(@Nullable final Object template) {
585 this.template = template == null ? null : loadTemplate(template);
586 return this;
587 }
588
589 public Builder withTemplateParameter(final String name, @Nullable final Object value) {
590 this.templateParameters.put(name, value);
591 return this;
592 }
593
594 public Renderer build() {
595 return new NAFRenderer(this);
596 }
597
598 }
599
600 }