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      
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                         
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         
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(); 
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         
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(); 
229         }
230         builder.append("; ").append(used / mb).append("/").append(max / mb).append("/")
231                 .append(committed / mb).append(" MB memory used/peak/committed");
232 
233         
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         
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         
269         if (query != null) {
270 
271             
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             
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         
321         final Record resource = getRecord(KS.RESOURCE, resourceID);
322         if (resource == null) {
323             return false;
324         }
325 
326         
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         
350         model.put("resource", Boolean.TRUE);
351 
352         
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         
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         
380         if (selectedEntityID != null) {
381             
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             
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("  ➟  <strong>")
399                         .append(entityURIs.size() == 1 ? "Entity" : "Entities")
400                         .append("</strong>");
401                 for (final URI entityURI : entityURIs) {
402                     builder.append("  <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             
413             model.put("resourceDetailsTitle", "<strong>Resource metadata</strong>");
414             model.put("resourceDetailsBody", RenderUtils.render(resource));
415         }
416 
417         
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         
425         final Record mention = getRecord(KS.MENTION, mentionID);
426         if (mention == null) {
427             return false;
428         }
429 
430         
431         model.put("mention", Boolean.TRUE);
432 
433         
434         model.put("mentionData", RenderUtils.render(mention));
435 
436         
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         
453         final List<URI> entityIDs = mention.get(KS.REFERS_TO, URI.class);
454         if (!entityIDs.isEmpty()) {
455 
456             
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             
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 ? "  " : "");
473                     RenderUtils.render(id, builder);
474                 }
475                 model.put("mentionEntityLinks", builder.toString());
476             }
477         }
478 
479         
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         
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         
494         model.put("entity", Boolean.TRUE);
495 
496         
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         
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         
517         final List<Record> resources = getEntityResources(entityID, getUIConfig().getResultLimit());
518         if (!resources.isEmpty()) {
519 
520             
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             
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         
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         
571         if (entityID == null) {
572             return;
573         }
574 
575         
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         
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         
598         if (entityID == null) {
599             return;
600         }
601 
602         
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         
617         if (entityID == null || property == null) {
618             return;
619         }
620 
621         
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         
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         
641         if (entityID == null) {
642             return;
643         }
644 
645         
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         
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     
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 
731 
732 
733                 record.add(KS.REFERS_TO, entityID);
734                 entityIDs.add(entityID);
735             }
736         }
737 
738         
739         
740         
741         
742         
743         
744         
745         
746         
747         
748         
749         
750         
751         
752         
753         
754         
755         
756         
757         
758         
759         
760         
761         
762         
763         
764         
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); 
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         
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         
798         if (numMentions != null) {
799             numMentions[0] = mentionURIs.size();
800         }
801 
802         
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         
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         
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 }