1   package eu.fbk.knowledgestore.server.http.jaxrs;
2   
3   import com.google.common.base.Function;
4   import com.google.common.base.Predicate;
5   import com.google.common.base.Predicates;
6   import com.google.common.collect.*;
7   import com.google.common.escape.Escaper;
8   import com.google.common.net.UrlEscapers;
9   import eu.fbk.knowledgestore.OperationException;
10  import eu.fbk.knowledgestore.Outcome;
11  import eu.fbk.knowledgestore.data.Data;
12  import eu.fbk.knowledgestore.data.Record;
13  import eu.fbk.knowledgestore.data.Representation;
14  import eu.fbk.knowledgestore.data.Stream;
15  import eu.fbk.knowledgestore.internal.Util;
16  import eu.fbk.knowledgestore.internal.rdf.RDFUtil;
17  import eu.fbk.knowledgestore.server.http.UIConfig.Example;
18  import eu.fbk.knowledgestore.vocabulary.KS;
19  import eu.fbk.knowledgestore.vocabulary.NIE;
20  import eu.fbk.knowledgestore.vocabulary.NIF;
21  import org.codehaus.enunciate.Facet;
22  import org.glassfish.jersey.server.mvc.Viewable;
23  import org.openrdf.model.Literal;
24  import org.openrdf.model.Statement;
25  import org.openrdf.model.URI;
26  import org.openrdf.model.Value;
27  import org.openrdf.model.impl.BooleanLiteralImpl;
28  import org.openrdf.model.impl.URIImpl;
29  import org.openrdf.query.BindingSet;
30  import org.openrdf.query.impl.ListBindingSet;
31  import org.slf4j.Logger;
32  import org.slf4j.LoggerFactory;
33  
34  import javax.annotation.Nullable;
35  import javax.ws.rs.*;
36  import javax.ws.rs.core.CacheControl;
37  import javax.ws.rs.core.Response;
38  import javax.ws.rs.core.Response.Status;
39  import java.io.InputStream;
40  import java.lang.management.GarbageCollectorMXBean;
41  import java.lang.management.ManagementFactory;
42  import java.lang.management.MemoryPoolMXBean;
43  import java.lang.management.MemoryUsage;
44  import java.util.*;
45  
46  @Path("/")
47  @Facet(name = "internal")
48  public class Root extends Resource {
49  
50      private static final Logger LOGGER = LoggerFactory.getLogger(Root.class);
51  
52      private static final String VERSION = Util.getVersion("eu.fbk.knowledgestore", "ks-core",
53              "devel");
54  
55      private static final URI NUM_MENTIONS = new URIImpl(KS.NAMESPACE + "numMentions");
56  
57      private static final List<String> DESCRIBE_VARS = ImmutableList.of("subject", "predicate",
58              "object", "graph");
59  
60      private static final int MAX_FETCHED_RESULTS = 10000;
61  
62      // private static final Pattern NIF_OFFSET_PATTERN = Pattern.compile("char=(\\d+),(\\d+)");
63  
64      @GET
65      public Response getStatus() {
66          String uri = getUriInfo().getRequestUri().toString();
67          uri = (uri.endsWith("/") ? uri : uri + "/") + "ui";
68          final Response redirect = Response.status(Status.FOUND).location(java.net.URI.create(uri))
69                  .build();
70          throw new WebApplicationException(redirect);
71      }
72  
73      @GET
74      @Path("/static/{name:.*}")
75      public Response download(@PathParam("name") final String name) throws Throwable {
76          final InputStream stream = Root.class.getResourceAsStream(name);
77          if (stream == null) {
78              throw new WebApplicationException("No resource named " + name, Status.NOT_FOUND);
79          }
80          final String type = Data.extensionToMimeType(name);
81          init(false, type, null, null);
82          final CacheControl control = new CacheControl();
83          control.setMaxAge(3600 * 24);
84          control.setMustRevalidate(true);
85          control.setPrivate(false);
86          return newResponseBuilder(Status.OK, stream, null).cacheControl(control).build();
87      }
88  
89      @GET
90      @Path("/ui")
91      @Produces("text/html;charset=UTF-8")
92      public Viewable ui() throws Throwable {
93  
94          final Map<String, Object> model = Maps.newHashMap();
95          model.put("maxTriples", getUIConfig().getResultLimit());
96          String view = "/status";
97  
98          final String action = getParameter("action", String.class, null, model);
99          final Long timeoutSec = getParameter("timeout", Long.class, null, model);
100         final Long timeout = timeoutSec == null ? null : timeoutSec * 1000;
101         final int limit = getParameter("limit", Integer.class, getUIConfig().getResultLimit(),
102                 model);
103 
104         try {
105             if ("lookup".equals(action)) {
106                 final URI id = getParameter("id", URI.class, null, model);
107                 final URI selection = getParameter("selection", URI.class, null, model);
108                 view = "/lookup";
109                 model.put("tabLookup", Boolean.TRUE);
110                 uiLookup(model, id, selection, limit);
111 
112             } else if ("sparql".equals(action)) {
113                 final String query = getParameter("query", String.class, null, model);
114                 view = "/sparql";
115                 model.put("tabSparql", Boolean.TRUE);
116                 uiSparql(model, query, timeout);
117 
118             } else if ("entity-mentions".equals(action)) {
119                 final URI entityID = getParameter("entity", URI.class, null, model);
120                 final URI property = getParameter("property", URI.class, null, model);
121                 final Value value = getParameter("value", Value.class, null, model);
122                 view = "/entity-mentions";
123                 model.put("tabReports", Boolean.TRUE);
124                 model.put("subtabEntityMentions", Boolean.TRUE);
125                 uiReportEntityMentions(model, entityID, property, value, limit);
126 
127             } else if ("entity-mentions-aggregate".equals(action)) {
128                 final URI entityID = getParameter("entity", URI.class, null, model);
129                 view = "/entity-mentions-aggregate";
130                 model.put("tabReports", Boolean.TRUE);
131                 model.put("subtabEntityMentionsAggregate", Boolean.TRUE);
132                 uiReportEntityMentionsAggregate(model, entityID);
133 
134             } else if ("mention-value-occurrences".equals(action)) {
135                 final URI entityID = getParameter("entity", URI.class, null, model);
136                 final URI property = getParameter("property", URI.class, null, model);
137                 view = "/mention-value-occurrences";
138                 model.put("tabReports", Boolean.TRUE);
139                 model.put("subtabMentionValueOccurrences", Boolean.TRUE);
140                 uiReportMentionValueOccurrences(model, entityID, property);
141 
142             } else if ("mention-property-occurrences".equals(action)) {
143                 final URI entityID = getParameter("entity", URI.class, null, model);
144                 view = "/mention-property-occurrences";
145                 model.put("tabReports", Boolean.TRUE);
146                 model.put("subtabMentionPropertyOccurrences", Boolean.TRUE);
147                 uiReportMentionPropertyOccurrences(model, entityID);
148 
149             } else {
150                 uiStatus(model);
151             }
152 
153         } catch (final Throwable ex) {
154             if (ex instanceof OperationException) {
155                 final OperationException oex = (OperationException) ex;
156                 model.put("error", oex.getOutcome().toString());
157                 if (oex.getOutcome().getStatus() == Outcome.Status.ERROR_UNEXPECTED) {
158                     LOGGER.error("Unexpected error", ex);
159                 }
160             } else {
161                 model.put("error", ex.getMessage());
162                 LOGGER.error("Unexpected error", ex);
163             }
164         }
165 
166         return new Viewable(view, model);
167     }
168 
169     @SuppressWarnings("unchecked")
170     private <T> T getParameter(final String name, final Class<T> clazz,
171             @Nullable final T defaultValue, @Nullable final Map<String, Object> model) {
172         T result = defaultValue;
173         final String stringValue = getUriInfo().getQueryParameters().getFirst(name);
174         if (stringValue != null && !"".equals(stringValue)) {
175             if (Value.class.isAssignableFrom(clazz)) {
176                 final char c = stringValue.charAt(0);
177                 if (c == '\'' || c == '"' || c == '<' || //
178                         stringValue.indexOf(':') >= 0 && stringValue.indexOf('/') < 0) {
179                     try {
180                         final Value value = Data.parseValue(stringValue, Data.getNamespaceMap());
181                         if (clazz.isInstance(value)) {
182                             result = clazz.cast(value);
183                         }
184                     } catch (final Throwable ex) {
185                         // ignore
186                     }
187                 }
188                 if (result == defaultValue) {
189                     if (URI.class.equals(clazz)) {
190                         result = (T) Data.getValueFactory().createURI(Data.cleanIRI(stringValue));
191                     } else if (clazz.isAssignableFrom(Literal.class)) {
192                         result = (T) Data.getValueFactory().createLiteral(stringValue);
193                     }
194                 }
195             } else {
196                 result = Data.convert(stringValue, clazz, defaultValue);
197             }
198         }
199         if (result != null) {
200             model.put(name, result);
201         }
202         return result;
203     }
204 
205     private void uiStatus(final Map<String, Object> model) {
206 
207         // Emit uptime and percentage spent in GC
208         final StringBuilder builder = new StringBuilder();
209         final long uptime = ManagementFactory.getRuntimeMXBean().getUptime();
210         final long days = uptime / (24 * 60 * 60 * 1000);
211         final long hours = uptime / (60 * 60 * 1000) - days * 24;
212         final long minutes = uptime / (60 * 1000) - (days * 24 + hours) * 60;
213         long gctime = 0;
214         for (final GarbageCollectorMXBean bean : ManagementFactory.getGarbageCollectorMXBeans()) {
215             gctime += bean.getCollectionTime(); // assume 1 bean or they don't work in parallel
216         }
217         builder.append(days == 0 ? "" : days + "d").append(hours == 0 ? "" : hours + "h")
218                 .append(minutes).append("m uptime, ").append(gctime * 100 / uptime).append("% gc");
219 
220         // Emit memory usage
221         final MemoryUsage heap = ManagementFactory.getMemoryMXBean().getHeapMemoryUsage();
222         final MemoryUsage nonHeap = ManagementFactory.getMemoryMXBean().getNonHeapMemoryUsage();
223         final long used = heap.getUsed() + nonHeap.getUsed();
224         final long committed = heap.getCommitted() + nonHeap.getCommitted();
225         final long mb = 1024 * 1024;
226         long max = 0;
227         for (final MemoryPoolMXBean bean : ManagementFactory.getMemoryPoolMXBeans()) {
228             max += bean.getPeakUsage().getUsed(); // assume maximum at same time in all pools
229         }
230         builder.append("; ").append(used / mb).append("/").append(max / mb).append("/")
231                 .append(committed / mb).append(" MB memory used/peak/committed");
232 
233         // Emit thread numbers
234         final int numThreads = ManagementFactory.getThreadMXBean().getThreadCount();
235         final int maxThreads = ManagementFactory.getThreadMXBean().getPeakThreadCount();
236         final long startedThreads = ManagementFactory.getThreadMXBean()
237                 .getTotalStartedThreadCount();
238         builder.append("; ").append(numThreads).append("/").append(maxThreads).append("/")
239                 .append(startedThreads).append(" threads active/peak/started");
240 
241         model.put("version", VERSION);
242         model.put("status", builder.toString());
243     }
244 
245     private void uiSparql(final Map<String, Object> model, @Nullable final String query,
246             @Nullable final Long timeout) throws Throwable {
247 
248         // Emit the example queries
249         if (!getUIConfig().getSparqlExamples().isEmpty()) {
250             final List<String> links = Lists.newArrayList();
251             final StringBuilder script = new StringBuilder();
252             int index = 0;
253             for (final Example example : getUIConfig().getSparqlExamples()) {
254                 links.add("<a href=\"#\" onclick=\"$('#query').val(sparqlExample(" + index
255                         + "))\">" + RenderUtils.escapeJavaScriptString(example.getLabel()) + "</a>");
256                 script.append("if (queryNum == ").append(index).append(") {\n");
257                 script.append("  return \"")
258                         .append(example.getValue().replace("\n", "\\n").replace("\"", "\\\""))
259                         .append("\";\n");
260                 script.append("}\n");
261                 ++index;
262             }
263             script.append("return \"\";\n");
264             model.put("examplesScript", script.toString());
265             model.put("examplesLinks", links);
266         }
267 
268         // Emit the query and evaluate its results, if possible
269         if (query != null) {
270 
271             // Emit the query results (only partially materialized).
272             final long ts = System.currentTimeMillis();
273             final Stream<BindingSet> stream = sendQuery(query, timeout);
274             @SuppressWarnings("unchecked")
275             final List<String> vars = stream.getProperty("variables", List.class);
276             final Iterator<BindingSet> iterator = stream.iterator();
277             final List<BindingSet> fetched = ImmutableList.copyOf(Iterators.limit(iterator,
278                     MAX_FETCHED_RESULTS));
279             model.put(
280                     "results",
281                     RenderUtils.renderSolutionTable(vars,
282                             Iterables.concat(fetched, Stream.create(iterator))));
283             final long elapsed = System.currentTimeMillis() - ts;
284 
285             // Emit the results message
286             final StringBuilder builder = new StringBuilder();
287             if (fetched.size() < MAX_FETCHED_RESULTS) {
288                 builder.append(fetched.size()).append(" results in ");
289                 builder.append(elapsed).append(" ms");
290             } else {
291                 builder.append("more than ").append(MAX_FETCHED_RESULTS);
292             }
293             if (timeout != null && elapsed > timeout) {
294                 builder.append(" (timed out, more results may be available)");
295             }
296             model.put("resultsMessage", builder.toString());
297         }
298     }
299 
300     private void uiLookup(final Map<String, Object> model, @Nullable final URI id,
301             @Nullable final URI selection, final int limit) throws Throwable {
302 
303         if (!getUIConfig().getLookupExamples().isEmpty()) {
304             model.put("examplesCount", getUIConfig().getLookupExamples().size());
305             model.put("examples", getUIConfig().getLookupExamples());
306         }
307 
308         if (id != null) {
309             if (!uiLookupResource(model, id, selection, limit) //
310                     && !uiLookupMention(model, id, limit) //
311                     && !uiLookupEntity(model, id, limit)) {
312                 model.put("text", "NO ENTRY FOR ID " + id);
313             }
314         }
315     }
316 
317     private boolean uiLookupResource(final Map<String, Object> model, final URI resourceID,
318             final URI selection, final int limit) throws Throwable {
319 
320         // Retrieve the resource record for the URI specified. Return false if not found
321         final Record resource = getRecord(KS.RESOURCE, resourceID);
322         if (resource == null) {
323             return false;
324         }
325 
326         // Get mentions, generating links and identifying selected mentions and entities
327         final List<Record> mentions = getResourceMentions(resourceID);
328         URI selectedEntityID = null;
329         Record selectedMention = null;
330         final List<String> mentionLinks = Lists.newArrayList();
331         final Set<String> entityLinks = Sets.newTreeSet();
332         final String linkTemplate = "<a onclick=\"select('%s')\" href=\"#\">%s</a>";
333         for (final Record mention : mentions) {
334             final URI mentionID = mention.getID();
335             mentionLinks.add(String.format(linkTemplate, RenderUtils.escapeJavaScriptString(mentionID),
336                     RenderUtils.shortenURI(mentionID)));
337             if (mention.getID().equals(selection)) {
338                 selectedMention = mention;
339             }
340             for (final URI entityID : mention.get(KS.REFERS_TO, URI.class)) {
341                 entityLinks.add(String.format(linkTemplate, RenderUtils.escapeJavaScriptString(entityID),
342                         RenderUtils.shortenURI(entityID)));
343                 if (entityID.equals(selection)) {
344                     selectedEntityID = selection;
345                 }
346             }
347         }
348 
349         // Select the resource template
350         model.put("resource", Boolean.TRUE);
351 
352         // Emit mentions and entities dropdown lists
353         if (!mentionLinks.isEmpty()) {
354             model.put("resourceMentionsCount", mentionLinks.size());
355             model.put("resourceMentions", mentionLinks);
356         }
357         if (!entityLinks.isEmpty()) {
358             model.put("resourceEntitiesCount", entityLinks.size());
359             model.put("resourceEntities", entityLinks);
360         }
361 
362         // Emit resource text box
363         final Representation representation = getRepresentation(resourceID);
364         if (representation != null) {
365             final String text = representation.writeToString();
366             final StringBuilder builder = new StringBuilder();
367             if (!mentions.isEmpty()) {
368                 RenderUtils.renderText(text, mentions, selection, true, false, getUIConfig(),
369                         builder);
370             } else {
371                 final Record metadata = representation.getMetadata();
372                 model.put("resourcePrettyPrint", Boolean.TRUE);
373                 RenderUtils.renderText(text, metadata.getUnique(NIE.MIME_TYPE, String.class),
374                         builder);
375             }
376             model.put("resourceText", builder.toString());
377         }
378 
379         // Emit the details box (mention / entity / resource metadata)
380         if (selectedEntityID != null) {
381             // One entity selected - emit its describe triples
382             final List<BindingSet> bindings = getEntityDescribeTriples(selection, limit);
383             final int total = bindings.size() < limit ? bindings.size()
384                     : countEntityDescribeTriples(selection);
385             model.put("resourceDetailsBody",
386                     String.join("", RenderUtils.renderSolutionTable(DESCRIBE_VARS, bindings)));
387             model.put("resourceDetailsTitle", String.format("<strong> Entity %s "
388                             + "(%d triples out of %d)</strong>", RenderUtils.render(selection),
389                     bindings.size(), total));
390 
391         } else if (selectedMention != null) {
392             // One mention selected - emit its details
393             final StringBuilder builder = new StringBuilder("<strong>Mention ");
394             RenderUtils.render(selection, builder);
395             builder.append("</strong>");
396             final List<URI> entityURIs = selectedMention.get(KS.REFERS_TO, URI.class);
397             if (!entityURIs.isEmpty()) {
398                 builder.append("&nbsp;&nbsp;&#10143;&nbsp;&nbsp;<strong>")
399                         .append(entityURIs.size() == 1 ? "Entity" : "Entities")
400                         .append("</strong>");
401                 for (final URI entityURI : entityURIs) {
402                     builder.append("&nbsp;&nbsp;<strong>");
403                     RenderUtils.render(entityURI, builder);
404                     builder.append("</strong> <a href=\"#\" onclick=\"select('")
405                             .append(RenderUtils.escapeJavaScriptString(entityURI)).append("')\">(select)</a>");
406                 }
407             }
408             model.put("resourceDetailsTitle", builder.toString());
409             model.put("resourceDetailsBody", RenderUtils.render(selectedMention));
410 
411         } else {
412             // Nothing selected - emit resource metadata
413             model.put("resourceDetailsTitle", "<strong>Resource metadata</strong>");
414             model.put("resourceDetailsBody", RenderUtils.render(resource));
415         }
416 
417         // Signal success
418         return true;
419     }
420 
421     private boolean uiLookupMention(final Map<String, Object> model, final URI mentionID,
422             final int limit) throws Throwable {
423 
424         // Retrieve the mention for the URI specified. Return false if not found
425         final Record mention = getRecord(KS.MENTION, mentionID);
426         if (mention == null) {
427             return false;
428         }
429 
430         // Select the mention template
431         model.put("mention", Boolean.TRUE);
432 
433         // Emit the mention description box
434         model.put("mentionData", RenderUtils.render(mention));
435 
436         // Emit the resource box, including the mention snipped
437         final URI resourceID = mention.getUnique(KS.MENTION_OF, URI.class, null);
438         if (resourceID != null) {
439             model.put("mentionResourceLink",
440                     RenderUtils.render(resourceID, mentionID, new StringBuilder()).toString());
441             final Representation representation = getRepresentation(resourceID);
442             if (representation == null) {
443                 model.put("mentionResourceExcerpt", "RESOURCE CONTENT NOT AVAILABLE");
444             } else {
445                 final String text = representation.writeToString();
446                 model.put("mentionResourceExcerpt", RenderUtils.renderText(text,
447                         ImmutableList.of(mention), null, false, true, getUIConfig(),
448                         new StringBuilder()));
449             }
450         }
451 
452         // Emit the denoted entities box
453         final List<URI> entityIDs = mention.get(KS.REFERS_TO, URI.class);
454         if (!entityIDs.isEmpty()) {
455 
456             // Emit the triples of the first denoted entity, including their total number
457             final URI entityID = entityIDs.iterator().next();
458             final List<BindingSet> describeTriples = getEntityDescribeTriples(entityID, limit);
459             final int total = describeTriples.size() < limit ? describeTriples.size()
460                     : countEntityDescribeTriples(entityID);
461             model.put("mentionEntityTriplesShown", describeTriples.size());
462             model.put("mentionEntityTriplesTotal", total);
463             model.put("mentionEntityTriples", String.join("", RenderUtils.renderSolutionTable( //
464                     ImmutableList.of("subject", "predicate", "object", "graph"), describeTriples)));
465 
466             // Emit the link(s) to the pages for all the denoted entities
467             if (entityIDs.size() == 1) {
468                 model.put("mentionEntityLink", RenderUtils.render(entityID));
469             } else {
470                 final StringBuilder builder = new StringBuilder();
471                 for (final URI id : entityIDs) {
472                     builder.append(builder.length() > 0 ? "&nbsp;&nbsp;" : "");
473                     RenderUtils.render(id, builder);
474                 }
475                 model.put("mentionEntityLinks", builder.toString());
476             }
477         }
478 
479         // Signal success
480         return true;
481     }
482 
483     private boolean uiLookupEntity(final Map<String, Object> model, final URI entityID,
484             final int limit) throws Throwable {
485 
486         // Lookup (a subset of) describe triples and graph triples for the specified entity
487         final List<BindingSet> describeTriples = getEntityDescribeTriples(entityID, limit);
488         final List<BindingSet> graphTriples = getEntityGraphTriples(entityID, limit);
489         if (describeTriples.isEmpty() && graphTriples.isEmpty()) {
490             return false;
491         }
492 
493         // Select the entity template
494         model.put("entity", Boolean.TRUE);
495 
496         // Emit the describe box
497         if (!describeTriples.isEmpty()) {
498             final int total = describeTriples.size() < limit ? describeTriples.size()
499                     : countEntityDescribeTriples(entityID);
500             model.put("entityTriplesShown", describeTriples.size());
501             model.put("entityTriplesTotal", total);
502             model.put("entityTriples", String.join("", RenderUtils.renderSolutionTable( //
503                     ImmutableList.of("subject", "predicate", "object", "graph"), describeTriples)));
504         }
505 
506         // Emit the graph box
507         if (!graphTriples.isEmpty()) {
508             final int total = graphTriples.size() < limit ? graphTriples.size()
509                     : countEntityGraphTriples(entityID);
510             model.put("entityGraphShown", graphTriples.size());
511             model.put("entityGraphTotal", total);
512             model.put("entityGraph", String.join("", RenderUtils.renderSolutionTable( //
513                     ImmutableList.of("subject", "predicate", "object"), graphTriples)));
514         }
515 
516         // Emit the resources box
517         final List<Record> resources = getEntityResources(entityID, getUIConfig().getResultLimit());
518         if (!resources.isEmpty()) {
519 
520             // Emit resource and mention counts
521             final int[] counts = countEntityResourcesAndMentions(entityID);
522             model.put("entityResourcesShown", resources.size());
523             model.put("entityResourcesCount", counts[0]);
524             model.put("entityMentionsCount", counts[1]);
525 
526             // Emit the resources table
527             final StringBuilder builder = new StringBuilder();
528             final List<URI> overviewProperties = getUIConfig().getResourceOverviewProperties();
529             final int width = 75 / (overviewProperties.size() + 2);
530             final String th = "<th style=\"width: " + width + "%\">";
531             builder.append("<table class=\"sparql table table-condensed tablesorter\"><thead>\n");
532             builder.append("<tr>").append(th).append("resource ID</th>");
533             for (final URI property : overviewProperties) {
534                 builder.append(th)
535                         .append(RenderUtils.escapeHtml(Data.toString(property,
536                                 Data.getNamespaceMap()))).append("</th>");
537             }
538             builder.append(th);
539             if (resources.size() < getUIConfig().getResultLimit()) {
540                 builder.append("# mentions");
541             } else {
542                 builder.append("<span title=\"Number of mentions per resource may be lower than "
543                         + "the exact value as only a subset of all the entity mentions has been "
544                         + "considered for building this page\"># mentions (truncated)</title>");
545             }
546             builder.append("</th></tr>\n</thead><tbody>\n");
547             for (final Record resource : resources) {
548                 builder.append("<tr><td>");
549                 RenderUtils.render(resource.getID(), entityID, builder);
550                 for (final URI property : overviewProperties) {
551                     builder.append("</td><td>");
552                     RenderUtils.render(resource.get(property), builder);
553                 }
554                 builder.append("</td><td>");
555                 RenderUtils.render(resource.getUnique(NUM_MENTIONS, Integer.class, null), builder);
556                 builder.append("</td></tr>\n");
557             }
558             builder.append("</tbody></table>");
559             model.put("entityResources", builder.toString());
560         }
561 
562         // Signal success
563         return true;
564     }
565 
566     private void uiReportEntityMentions(final Map<String, Object> model,
567             @Nullable final URI entityID, @Nullable final URI property,
568             @Nullable final Value value, final int limit) throws Throwable {
569 
570         // Do nothing in case the entity ID is missing
571         if (entityID == null) {
572             return;
573         }
574 
575         // Retrieve all the mentions satisfying the property[=value] optional filter
576         int numMentions = 0;
577         final List<Record> mentions = Lists.newArrayList();
578         for (final Record mention : getEntityMentions(entityID, Integer.MAX_VALUE, null)) {
579             if (property == null || !mention.isNull(property)
580                     && (value == null || mention.get(property).contains(value))) {
581                 ++numMentions;
582                 if (mentions.size() < limit) {
583                     mentions.add(mention);
584                 }
585             }
586         }
587 
588         // Render the mention table, including column toggling functionality
589         model.put("message", mentions.size() + " mentions shown out of " + numMentions);
590         model.put("mentionTable",
591                 RenderUtils.renderRecordsTable(new StringBuilder(), mentions, null, null));
592     }
593 
594     private void uiReportEntityMentionsAggregate(final Map<String, Object> model,
595             final URI entityID) throws Throwable {
596 
597         // Do nothing in case the entity ID is missing
598         if (entityID == null) {
599             return;
600         }
601 
602         // Render the table
603         final Stream<Record> mentions = getEntityMentions(entityID, Integer.MAX_VALUE, null);
604         final Predicate<URI> filter = Predicates.not(Predicates.in(ImmutableSet.<URI>of(
605                 NIF.BEGIN_INDEX, NIF.END_INDEX, KS.MENTION_OF)));
606         final String linkTemplate = "ui?action=entity-mentions&entity="
607                 + UrlEscapers.urlFormParameterEscaper().escape(entityID.stringValue())
608                 + "&property=${property}&value=${value}";
609         model.put("propertyValuesTable", RenderUtils.renderRecordsAggregateTable(
610                 new StringBuilder(), mentions, filter, linkTemplate, null));
611     }
612 
613     private void uiReportMentionValueOccurrences(final Map<String, Object> model,
614             final URI entityID, @Nullable final URI property) throws Throwable {
615 
616         // Do nothing in case the entity ID is missing
617         if (entityID == null || property == null) {
618             return;
619         }
620 
621         // Compute the # of occurrences of all the values of the given property in entity mentions
622         final Multiset<Value> propertyValues = HashMultiset.create();
623         for (final Record mention : getEntityMentions(entityID, Integer.MAX_VALUE, null)) {
624             propertyValues.addAll(mention.get(property, Value.class));
625         }
626 
627         // Render the table
628         final Escaper esc = UrlEscapers.urlFormParameterEscaper();
629         final String linkTemplate = "ui?action=entity-mentions&entity="
630                 + esc.escape(entityID.stringValue()) + "&property="
631                 + esc.escape(Data.toString(property, Data.getNamespaceMap()))
632                 + "&value=${element}";
633         model.put("valueOccurrencesTable", RenderUtils.renderMultisetTable(new StringBuilder(),
634                 propertyValues, "Property value", "# Mentions", linkTemplate));
635     }
636 
637     private void uiReportMentionPropertyOccurrences(final Map<String, Object> model,
638             final URI entityID) throws Throwable {
639 
640         // Do nothing in case the entity ID is missing
641         if (entityID == null) {
642             return;
643         }
644 
645         // Compute the # of occurrences of each property URI in entity mentions
646         final Multiset<URI> propertyURIs = HashMultiset.create();
647         for (final Record mention : getEntityMentions(entityID, Integer.MAX_VALUE, null)) {
648             propertyURIs.addAll(mention.getProperties());
649         }
650 
651         // Render the table
652         final Escaper esc = UrlEscapers.urlFormParameterEscaper();
653         final String linkTemplate = "ui?action=entity-mentions&entity="
654                 + esc.escape(entityID.stringValue()) + "&property=${element}";
655         model.put("propertyOccurrencesTable", RenderUtils.renderMultisetTable(new StringBuilder(),
656                 propertyURIs, "Property", "# Mentions", linkTemplate));
657     }
658 
659     // DATA ACCESS METHODS
660 
661     @Nullable
662     private Record getRecord(final URI layer, @Nullable final URI id) throws Throwable {
663         final Record record = id == null ? null : getSession().retrieve(layer).ids(id).exec()
664                 .getUnique();
665         if (record != null && layer.equals(KS.MENTION)) {
666             final String template = "SELECT ?e WHERE { ?e $$ $$ "
667                     + (getUIConfig().isDenotedByAllowsGraphs() ? ""
668                     : "FILTER NOT EXISTS { GRAPH ?e { ?s ?p ?o } } ") + "}";
669             for (final URI entityID : getSession()
670                     .sparql(template, getUIConfig().getDenotedByProperty(), id).execTuples()
671                     .transform(URI.class, true, "e")) {
672                 record.add(KS.REFERS_TO, entityID);
673             }
674         }
675         return record;
676     }
677 
678     @Nullable
679     private Representation getRepresentation(@Nullable final URI resourceID) throws Throwable {
680         final Representation representation = resourceID == null ? null : getSession().download(
681                 resourceID).exec();
682         if (representation != null) {
683             closeOnCompletion(representation);
684         }
685         return representation;
686     }
687 
688     private List<Record> getResourceMentions(final URI resourceID) throws Throwable {
689 
690         final Record resource;
691         resource = getSession().retrieve(KS.RESOURCE).ids(resourceID).exec().getUnique();
692         if (resource == null) {
693             return Collections.emptyList();
694         }
695 
696         final Map<URI, Record> mentions = Maps.newHashMap();
697         final List<URI> mentionIDs = resource.get(KS.HAS_MENTION, URI.class);
698         if (mentionIDs.isEmpty()) {
699             return Collections.emptyList();
700         }
701 
702         for (final Record mention : getSession().retrieve(KS.MENTION).ids(mentionIDs).exec()) {
703             mentions.put(mention.getID(), mention);
704         }
705 
706         final Set<URI> entityIDs = Sets.newHashSet();
707         for (final Record mention : mentions.values()) {
708             for (final URI entityID : mention.get(KS.REFERS_TO, URI.class)) {
709                 entityIDs.add(entityID);
710             }
711         }
712 
713         for (final List<URI> ids : Stream.create(mentionIDs).chunk(128)) {
714             final StringBuilder builder = new StringBuilder();
715             builder.append("SELECT ?m ?e WHERE { ?e ");
716             builder.append(Data.toString(getUIConfig().getDenotedByProperty(), null));
717             builder.append(" ?m VALUES ?m {");
718             for (final URI mentionID : ids) {
719                 builder.append(' ').append(Data.toString(mentionID, null));
720             }
721             builder.append(" } ");
722             if (!getUIConfig().isDenotedByAllowsGraphs()) {
723                 builder.append("FILTER NOT EXISTS { GRAPH ?e { ?s ?p ?o } } ");
724             }
725             builder.append("}");
726             for (final BindingSet bindings : getSession().sparql(builder.toString()).execTuples()) {
727                 final URI mentionID = (URI) bindings.getValue("m");
728                 final URI entityID = (URI) bindings.getValue("e");
729                 Record record = mentions.get(mentionID);
730 //                if (record == null) {
731 //                    continue;
732 //                }
733                 record.add(KS.REFERS_TO, entityID);
734                 entityIDs.add(entityID);
735             }
736         }
737 
738         // FOLLOWING CODE CAN INCREASE THE NUMBER OF MENTIONS RETRIEVED, BUT THE QUERY USED MAY
739         // TAKE UP TO SOME HUNDRED OF SECONDS (AND USING A TIMEOUT PRODUCES A NON-DETERMINISTIC
740         // OUTPUT
741         // if (!entityIDs.isEmpty()) {
742         // builder = new StringBuilder();
743         // builder.append("SELECT ?m ?e WHERE { "
744         // + "VALUES ?p { sem:hasActor sem:hasTime sem:hasPlace } VALUES ?e0 {");
745         // for (final URI entityID : entityIDs) {
746         // builder.append(' ').append(Data.toString(entityID, null));
747         // }
748         // builder.append(" } ?e0 ?p ?e . ?e $$ ?m FILTER(STRSTARTS(STR(?m), $$)) }");
749         // for (final BindingSet bindings : getSession()
750         // .sparql(builder.toString(), getUIConfig().getDenotedByProperty(),
751         // resourceID.stringValue()).timeout(1000L).execTuples()) {
752         // final URI mentionID = (URI) bindings.getValue("m");
753         // final URI entityID = (URI) bindings.getValue("e");
754         // Record mention = mentions.get(mentionID);
755         // if (mention == null) {
756         // mention = Record.create(mentionID, KS.MENTION);
757         // final Matcher matcher = NIF_OFFSET_PATTERN.matcher(mentionID.stringValue());
758         // if (matcher.find()) {
759         // mention.set(NIF.BEGIN_INDEX, Integer.parseInt(matcher.group(1)));
760         // mention.set(NIF.END_INDEX, Integer.parseInt(matcher.group(2)));
761         // }
762         // mentions.put(mentionID, mention);
763         // }
764         // mention.add(KS.REFERS_TO, entityID);
765         // }
766         // }
767 
768         final List<Record> sortedMentions = Lists.newArrayList(mentions.values());
769         Collections.sort(sortedMentions, new Comparator<Record>() {
770 
771             @Override
772             public int compare(final Record r1, final Record r2) {
773                 final int begin1 = r1.getUnique(NIF.BEGIN_INDEX, Integer.class, 0);
774                 final int begin2 = r2.getUnique(NIF.BEGIN_INDEX, Integer.class, 0);
775                 int result = Integer.compare(begin1, begin2);
776                 if (result == 0) {
777                     final int end1 = r1.getUnique(NIF.END_INDEX, Integer.class, Integer.MAX_VALUE);
778                     final int end2 = r2.getUnique(NIF.END_INDEX, Integer.class, Integer.MAX_VALUE);
779                     result = Integer.compare(end1, end2); // longest mention last
780                 }
781                 return result;
782             }
783 
784         });
785         return sortedMentions;
786     }
787 
788     private Stream<Record> getEntityMentions(final URI entityID, final int maxResults,
789             @Nullable final int[] numMentions) throws Throwable {
790 
791         // First retrieve all the URIs of the mentions denoting the entity, via SPARQL query
792         final List<URI> mentionURIs = getSession()
793                 .sparql("SELECT ?m WHERE { $$ $$ ?m}", entityID,
794                         getUIConfig().getDenotedByProperty()).execTuples()
795                 .transform(URI.class, true, "m").toList();
796 
797         // Return the total number of mentions, if an holder variable has been supplied
798         if (numMentions != null) {
799             numMentions[0] = mentionURIs.size();
800         }
801 
802         // Then return a stream that returns the mention records as they are fetched from the KS
803         return getSession().retrieve(KS.MENTION).limit((long) maxResults).ids(mentionURIs).exec();
804     }
805 
806     private List<Record> getEntityResources(final URI entityID, final int maxResults)
807             throws Throwable {
808 
809         // Retrieve up to maxResults IDs of resources mentioning the entity
810         final Multiset<URI> resourceIDs = HashMultiset.create();
811         try (Stream<URI> stream = getSession()
812                 .sparql("SELECT ?m WHERE { $$ $$ ?m }", entityID,
813                         getUIConfig().getDenotedByProperty()).execTuples()
814                 .transform(URI.class, true, "m")) {
815             for (final URI mentionID : stream) {
816                 final String string = mentionID.stringValue();
817                 final int index = string.indexOf("#");
818                 if (index > 0) {
819                     final URI resourceID = Data.getValueFactory().createURI(
820                             string.substring(0, index));
821                     if (resourceIDs.elementSet().size() == maxResults
822                             && !resourceIDs.contains(resourceID)) {
823                         break;
824                     }
825                     resourceIDs.add(resourceID);
826                 }
827             }
828         }
829 
830         // Lookup the resources in the KS
831         final List<Record> resources;
832         resources = getSession().retrieve(KS.RESOURCE).ids(resourceIDs).exec().toList();
833         for (final Record resource : resources) {
834             resource.set(NUM_MENTIONS, resourceIDs.count(resource.getID()));
835         }
836         return resources;
837     }
838 
839     private List<BindingSet> getEntityDescribeTriples(final URI entityID, final int limit)
840             throws Throwable {
841         return getSession()
842                 .sparql("SELECT (COALESCE(?s, $$) AS ?subject) ?predicate "
843                                 + "(COALESCE(?o, $$) AS ?object) ?graph "
844                                 + "WHERE { { GRAPH ?graph { $$ ?predicate ?o } } UNION "
845                                 + "{ GRAPH ?graph { ?s ?predicate $$ } } } LIMIT $$", entityID, entityID,
846                         entityID, entityID, limit).execTuples().toList();
847     }
848 
849     private List<BindingSet> getEntityGraphTriples(final URI entityID, final int limit)
850             throws Throwable {
851         return getSession()
852                 .sparql("SELECT ?subject ?predicate ?object "
853                                 + "WHERE { GRAPH $$ { ?subject ?predicate ?object } } LIMIT $$", entityID,
854                         limit).execTuples().toList();
855     }
856 
857     private int countEntityDescribeTriples(final URI entityID) throws Throwable {
858         return getSession()
859                 .sparql("SELECT (COUNT(*) AS ?n) "
860                                 + "WHERE { { GRAPH ?g { $$ ?p ?o } } UNION { GRAPH ?g { ?s ?p $$ } } }",
861                         entityID, entityID).execTuples().transform(Integer.class, true, "n")
862                 .getUnique();
863     }
864 
865     private int countEntityGraphTriples(final URI entityID) throws Throwable {
866         return getSession()
867                 .sparql("SELECT (COUNT(*) AS ?n) WHERE { GRAPH $$ { ?s ?p ?o } }", entityID)
868                 .execTuples().transform(Integer.class, true, "n").getUnique();
869     }
870 
871     private int[] countEntityResourcesAndMentions(final URI entityID) throws Throwable {
872         final BindingSet tuple = getSession()
873                 .sparql("SELECT (COUNT(DISTINCT ?r) AS ?nr) (COUNT(*) AS ?nm) "
874                                 + "WHERE { $$ $$ ?m . BIND(IRI(STRBEFORE(STR(?m), \"#\")) AS ?r) }",
875                         entityID, getUIConfig().getDenotedByProperty()).execTuples().getUnique();
876         return new int[] { ((Literal) tuple.getValue("nr")).intValue(),
877                 ((Literal) tuple.getValue("nm")).intValue() };
878     }
879 
880     private Stream<BindingSet> sendQuery(final String query, final Long timeout) throws Throwable {
881 
882         final String form = RDFUtil.detectSparqlForm(query);
883         if (form.equalsIgnoreCase("select")) {
884             return closeOnCompletion(getSession().sparql(query).timeout(timeout).execTuples());
885 
886         } else if (form.equalsIgnoreCase("construct") || form.equals("describe")) {
887             final List<String> variables = ImmutableList.of("subject", "predicate", "object");
888             final Function<Statement, BindingSet> transformer = new Function<Statement, BindingSet>() {
889 
890                 @Override
891                 public BindingSet apply(final Statement statement) {
892                     return new ListBindingSet(variables, statement.getSubject(),
893                             statement.getPredicate(), statement.getObject());
894                 }
895 
896             };
897             final Stream<BindingSet> stream = getSession().sparql(query).timeout(timeout)
898                     .execTriples().transform(transformer, 1);
899             stream.setProperty("variables", variables);
900             return closeOnCompletion(stream);
901 
902         } else {
903             final boolean result = getSession().sparql(query).timeout(timeout).execBoolean();
904             final List<String> variables = ImmutableList.of("result");
905             final BindingSet bindings = new ListBindingSet(variables,
906                     BooleanLiteralImpl.valueOf(result));
907             return closeOnCompletion(Stream.create(new BindingSet[] { bindings }).setProperty(
908                     "variables", variables));
909         }
910     }
911 
912 }