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         // Open properties table
225         out.append("<table class=\"properties table table-condensed\">\n<tbody>\n");
226 
227         // Emit a raw for the node ID, if requested
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         // Emit other properties
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         // Close properties table
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 "&lt;../" + uri.getLocalName() + "&gt;";
475     }
476 
477     private static Mustache loadTemplate(final Object spec) {
478         // Accepts Mustache, URL, File, String (url / filename / template)
479         Preconditions.checkNotNull(spec);
480         try {
481             if (spec instanceof Mustache) {
482                 return (Mustache) spec;
483             }
484             final DefaultMustacheFactory factory = new DefaultMustacheFactory();
485             // factory.setExecutorService(Environment.getPool()); // BROKEN
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                     // ignore
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 }