1 package eu.fbk.knowledgestore.server.http.jaxrs;
2
3 import java.io.IOException;
4 import java.util.List;
5 import java.util.Map;
6 import java.util.Set;
7 import java.util.concurrent.atomic.AtomicInteger;
8
9 import javax.annotation.Nullable;
10
11 import com.google.common.base.Function;
12 import com.google.common.base.Joiner;
13 import com.google.common.base.MoreObjects;
14 import com.google.common.base.Predicate;
15 import com.google.common.base.Splitter;
16 import com.google.common.base.Strings;
17 import com.google.common.collect.HashMultiset;
18 import com.google.common.collect.ImmutableList;
19 import com.google.common.collect.ImmutableSet;
20 import com.google.common.collect.Iterables;
21 import com.google.common.collect.Lists;
22 import com.google.common.collect.Maps;
23 import com.google.common.collect.Multiset;
24 import com.google.common.collect.Ordering;
25 import com.google.common.collect.Sets;
26 import com.google.common.escape.Escaper;
27 import com.google.common.html.HtmlEscapers;
28 import com.google.common.net.UrlEscapers;
29
30 import org.openrdf.model.BNode;
31 import org.openrdf.model.Literal;
32 import org.openrdf.model.URI;
33 import org.openrdf.model.Value;
34 import org.openrdf.query.BindingSet;
35
36 import eu.fbk.knowledgestore.data.Data;
37 import eu.fbk.knowledgestore.data.Record;
38 import eu.fbk.knowledgestore.server.http.UIConfig;
39 import eu.fbk.knowledgestore.vocabulary.KS;
40 import eu.fbk.knowledgestore.vocabulary.NIF;
41 import eu.fbk.knowledgestore.vocabulary.NWR;
42
43
44
45
46 public final class RenderUtils {
47
48 private static final boolean CHAR_OFFSET_HACK = Boolean.parseBoolean(System.getProperty(
49 "ks.charOffsetHack", "false"))
50 || Boolean.parseBoolean(MoreObjects.firstNonNull(System.getenv("KS_CHAR_OFFSET_HACK"),
51 "false"));
52
53 private static final AtomicInteger COUNTER = new AtomicInteger(0);
54
55
56
57
58
59
60
61
62
63
64 public static String render(final Object object) {
65 try {
66 final StringBuilder builder = new StringBuilder();
67 render(object, builder);
68 return builder.toString();
69 } catch (final IOException ex) {
70 throw new Error(ex);
71 }
72 }
73
74
75
76
77
78
79
80
81
82 @SuppressWarnings("unchecked")
83 public static <T extends Appendable> T render(final Object object, final T out)
84 throws IOException {
85
86 if (object instanceof URI) {
87 render((URI) object, null, out);
88
89 } else if (object instanceof Literal) {
90 final Literal literal = (Literal) object;
91 out.append("<span");
92 if (literal.getLanguage() != null) {
93 out.append(" title=\"@").append(literal.getLanguage()).append("\"");
94 } else if (literal.getDatatype() != null) {
95 out.append(" title=\"<").append(literal.getDatatype().stringValue())
96 .append(">\"");
97 }
98 out.append(">").append(literal.stringValue()).append("</span>");
99
100 } else if (object instanceof BNode) {
101 final BNode bnode = (BNode) object;
102 out.append("_:").append(bnode.getID());
103
104 } else if (object instanceof Record) {
105 final Record record = (Record) object;
106 out.append("<table class=\"record table table-condensed\"><tbody>\n<tr><td>ID</td><td>");
107 render(record.getID(), out);
108 out.append("</td></tr>\n");
109 for (final URI property : Ordering.from(Data.getTotalComparator()).sortedCopy(
110 record.getProperties())) {
111 out.append("<tr><td>");
112 render(property, out);
113 out.append("</td><td>");
114 final List<Object> values = record.get(property);
115 if (values.size() == 1) {
116 render(values.get(0), out);
117 } else {
118 out.append("<div class=\"scroll\">");
119 String separator = "";
120 for (final Object value : Ordering.from(Data.getTotalComparator()).sortedCopy(
121 record.get(property))) {
122 out.append(separator);
123 render(value, out);
124 separator = "<br/>";
125 }
126 out.append("</div>");
127 }
128 out.append("</td></tr>\n");
129 }
130 out.append("</tbody></table>");
131
132 } else if (object instanceof BindingSet) {
133 render(ImmutableSet.of(object));
134
135 } else if (object instanceof Iterable<?>) {
136 final Iterable<?> iterable = (Iterable<?>) object;
137 boolean isEmpty = true;
138 boolean isIterableOfSolutions = true;
139 for (final Object element : iterable) {
140 isEmpty = false;
141 if (!(element instanceof BindingSet)) {
142 isIterableOfSolutions = false;
143 break;
144 }
145 }
146 if (!isEmpty) {
147 if (!isIterableOfSolutions) {
148 String separator = "";
149 for (final Object element : (Iterable<?>) object) {
150 out.append(separator);
151 render(element, out);
152 separator = "<br/>";
153 }
154 } else {
155 Joiner.on("").appendTo(out,
156 renderSolutionTable(null, (Iterable<BindingSet>) object).iterator());
157 }
158 }
159
160 } else if (object != null) {
161 out.append(object.toString());
162 }
163
164 return out;
165 }
166
167 public static <T extends Appendable> T render(final URI uri, @Nullable final URI selection,
168 final T out) throws IOException {
169 out.append("<a href=\"").append(RenderUtils.escapeHtml(uri.stringValue())).append("\"");
170 if (selection != null) {
171 out.append(" data-sel=\"").append(RenderUtils.escapeHtml(selection)).append("\"");
172 }
173 out.append(" class=\"uri\">").append(RenderUtils.shortenURI(uri)).append("</a>");
174 return out;
175 }
176
177 public static <T extends Appendable> T renderText(final String text, final String contentType,
178 final T out) throws IOException {
179 if (contentType.equals("text/plain")) {
180 out.append("<div class=\"text\">\n").append(RenderUtils.escapeHtml(text))
181 .append("\n</div>\n");
182 } else {
183
184 out.append("<pre class=\"text-pre pre-scrollable prettyprint linenums lang-xml\">")
185 .append(RenderUtils.escapeHtml(text)).append("</pre>");
186 }
187 return out;
188 }
189
190 public static <T extends Appendable> T renderText(final String text,
191 final List<Record> mentions, @Nullable final URI selection, final boolean canSelect,
192 final boolean onlyMention, final UIConfig config, final T out) throws IOException {
193
194 final List<String> lines = Lists.newArrayList(Splitter.on('\n').split(text));
195 if (CHAR_OFFSET_HACK) {
196 for (int i = 0; i < lines.size(); ++i) {
197 lines.set(i, lines.get(i).replaceAll("\\s+", " ") + " ");
198 }
199 }
200
201 int lineStart = CHAR_OFFSET_HACK ? 0 : -1;
202 int lineOffset = 0;
203 int mentionIndex = 0;
204
205 boolean anchorAdded = false;
206
207 out.append("<div class=\"text\">\n");
208 for (final String l : lines) {
209 final String line = CHAR_OFFSET_HACK ? l.trim() : l;
210 lineStart += CHAR_OFFSET_HACK ? 0 : 1;
211 boolean mentionFound = false;
212 while (mentionIndex < mentions.size()) {
213 final Record mention = mentions.get(mentionIndex);
214 final Integer begin = mention.getUnique(NIF.BEGIN_INDEX, Integer.class);
215 final Integer end = mention.getUnique(NIF.END_INDEX, Integer.class);
216 String cssStyle = null;
217 for (final UIConfig.Category category : config.getMentionCategories()) {
218 if (category.getCondition().evalBoolean(mention)) {
219 cssStyle = category.getStyle();
220 break;
221 }
222 }
223 if (cssStyle == null || begin == null || end == null
224 || begin < lineStart + lineOffset) {
225 ++mentionIndex;
226 continue;
227 }
228 if (end > lineStart + line.length()) {
229 break;
230 }
231 final boolean selected = mention.getID().equals(selection)
232 || mention.get(KS.REFERS_TO, URI.class).contains(selection);
233 if (!mentionFound) {
234 out.append("<p>");
235 }
236 out.append(RenderUtils.escapeHtml(line.substring(lineOffset, begin - lineStart)));
237 out.append("<a href=\"#\"");
238 if (selected && !anchorAdded) {
239 out.append(" id=\"selection\"");
240 anchorAdded = true;
241 }
242 if (canSelect) {
243 out.append(" onclick=\"select('").append(RenderUtils.escapeJavaScriptString(mention.getID()))
244 .append("')\"");
245 }
246 out.append(" class=\"mention").append(selected ? " selected" : "")
247 .append("\" style=\"").append(cssStyle).append("\" title=\"");
248 String separator = "";
249 for (final URI property : config.getMentionOverviewProperties()) {
250 final List<Value> values = mention.get(property, Value.class);
251 if (!values.isEmpty()) {
252 out.append(separator)
253 .append(Data.toString(property, Data.getNamespaceMap()))
254 .append(" = ");
255 for (final Value value : values) {
256 if (!KS.MENTION.equals(value)
257 && !NWR.TIME_OR_EVENT_MENTION.equals(value)
258 && !NWR.ENTITY_MENTION.equals(value)) {
259 out.append(" ").append(
260 Data.toString(value, Data.getNamespaceMap()));
261 }
262 }
263 separator = "\n";
264 }
265 }
266 out.append("\">");
267 out.append(RenderUtils.escapeHtml(line.substring(begin - lineStart, end
268 - lineStart)));
269 out.append("</a>");
270 lineOffset = end - lineStart;
271 ++mentionIndex;
272 mentionFound = true;
273 }
274 if (mentionFound || !onlyMention) {
275 if (!mentionFound) {
276 out.append("<p>\n");
277 }
278 out.append(RenderUtils.escapeHtml(line.substring(lineOffset, line.length())));
279 out.append("</p>\n");
280 }
281 lineStart += line.length();
282 lineOffset = 0;
283 }
284 out.append("</div>\n");
285 return out;
286 }
287
288
289
290
291
292
293
294
295
296
297
298
299 public static Iterable<String> renderSolutionTable(final List<String> variables,
300 final Iterable<? extends BindingSet> solutions) {
301
302 final List<String> actualVariables;
303 if (variables != null) {
304 actualVariables = ImmutableList.copyOf(variables);
305 } else {
306 final Set<String> variableSet = Sets.newHashSet();
307 for (final BindingSet solution : solutions) {
308 variableSet.addAll(solution.getBindingNames());
309 }
310 actualVariables = Ordering.natural().sortedCopy(variableSet);
311 }
312
313 final int width = 75 / actualVariables.size();
314 final StringBuilder builder = new StringBuilder();
315 builder.append("<table class=\"sparql table table-condensed tablesorter\"><thead>\n<tr>");
316 for (final String variable : actualVariables) {
317 builder.append("<th style=\"width: ").append(width).append("%\">")
318 .append(escapeHtml(variable)).append("</th>");
319 }
320 final Iterable<String> header = ImmutableList.of(builder.toString());
321 final Iterable<String> footer = ImmutableList.of("</tbody></table>");
322 final Function<BindingSet, String> renderer = new Function<BindingSet, String>() {
323
324 @Override
325 public String apply(final BindingSet bindings) {
326 if (Thread.interrupted()) {
327 throw new IllegalStateException("Interrupted");
328 }
329 final StringBuilder builder = new StringBuilder();
330 builder.append("<tr>");
331 for (final String variable : actualVariables) {
332 builder.append("<td>");
333 try {
334 render(bindings.getValue(variable), builder);
335 } catch (final IOException ex) {
336 throw new Error(ex);
337 }
338 builder.append("</td>");
339 }
340 builder.append("</tr>\n");
341 return builder.toString();
342 }
343
344 };
345 return Iterables.concat(header, Iterables.transform(solutions, renderer), footer);
346 }
347
348 public static <T extends Appendable> T renderMultisetTable(final T out,
349 final Multiset<?> multiset, final String elementHeader,
350 final String occurrencesHeader, @Nullable final String linkTemplate)
351 throws IOException {
352
353 final String tableID = "table" + COUNTER.getAndIncrement();
354 out.append("<table id=\"").append(tableID).append("\" class=\"display datatable\">\n");
355 out.append("<thead>\n<tr><th>").append(MoreObjects.firstNonNull(elementHeader, "Value"))
356 .append("</th><th>")
357 .append(MoreObjects.firstNonNull(occurrencesHeader, "Occurrences"))
358 .append("</th></tr>\n</thead>\n");
359 out.append("<tbody>\n");
360 for (final Object element : multiset.elementSet()) {
361 final int occurrences = multiset.count(element);
362 out.append("<tr><td>");
363 RenderUtils.render(element, out);
364 out.append("</td><td>");
365 if (linkTemplate == null) {
366 out.append(Integer.toString(occurrences));
367 } else {
368 final Escaper esc = UrlEscapers.urlFormParameterEscaper();
369 final String e = esc.escape(Data.toString(element, Data.getNamespaceMap()));
370 final String u = linkTemplate.replace("${element}", e);
371 out.append("<a href=\"").append(u).append("\">")
372 .append(Integer.toString(occurrences)).append("</a>");
373 }
374 out.append("</td></tr>\n");
375 }
376 out.append("</tbody>\n</table>\n");
377 out.append("<script>$(document).ready(function() { applyDataTable('").append(tableID)
378 .append("', false, {}); });</script>");
379 return out;
380 }
381
382 public static <T extends Appendable> T renderRecordsTable(final T out,
383 final Iterable<Record> records, @Nullable List<URI> propertyURIs,
384 @Nullable final String extraOptions) throws IOException {
385
386
387 if (propertyURIs == null) {
388 final Set<URI> uriSet = Sets.newHashSet();
389 for (final Record record : records) {
390 uriSet.addAll(record.getProperties());
391 }
392 propertyURIs = Ordering.from(Data.getTotalComparator()).sortedCopy(uriSet);
393 }
394
395
396 final String tableID = "table" + COUNTER.getAndIncrement();
397 out.append("<table id=\"").append(tableID).append("\" class=\"display datatable\">\n");
398 out.append("<thead>\n<tr><th>URI</th>");
399 for (final URI propertyURI : propertyURIs) {
400 out.append("<th>").append(RenderUtils.shortenURI(propertyURI)).append("</th>");
401 }
402 out.append("</tr>\n</thead>\n<tbody>\n");
403 for (final Record record : records) {
404 out.append("<tr><td>").append(RenderUtils.render(record.getID())).append("</td>");
405 for (final URI propertyURI : propertyURIs) {
406 out.append("<td>").append(RenderUtils.render(record.get(propertyURI)))
407 .append("</td>");
408 }
409 out.append("</tr>\n");
410 }
411 out.append("</tbody>\n</table>\n");
412 out.append("<script>$(document).ready(function() { applyDataTable('").append(tableID)
413 .append("', true, {").append(Strings.nullToEmpty(extraOptions))
414 .append("}); });</script>");
415 return out;
416 }
417
418 public static <T extends Appendable> T renderRecordsAggregateTable(final T out,
419 final Iterable<Record> records, @Nullable final Predicate<URI> propertyFilter,
420 @Nullable final String linkTemplate, @Nullable final String extraOptions)
421 throws IOException {
422
423
424 final Map<URI, Multiset<Value>> properties = Maps.newHashMap();
425 final Map<Object, URI> examples = Maps.newHashMap();
426 for (final Record record : records) {
427 for (final URI property : record.getProperties()) {
428 if (propertyFilter == null || propertyFilter.apply(property)) {
429 Multiset<Value> values = properties.get(property);
430 if (values == null) {
431 values = HashMultiset.create();
432 properties.put(property, values);
433 }
434 for (final Value value : record.get(property, Value.class)) {
435 values.add(value);
436 examples.put(ImmutableList.of(property, value), record.getID());
437 }
438 }
439 }
440 }
441
442
443 final Ordering<Object> ordering = Ordering.from(Data.getTotalComparator());
444 final String tableID = "table" + COUNTER.getAndIncrement();
445 out.append("<table id=\"").append(tableID).append("\" class=\"display datatable\">\n");
446 out.append("<thead>\n<tr><th>Property</th><th>Value</th>"
447 + "<th>Occurrences</th><th>Example</th></tr>\n</thead>\n");
448 out.append("<tbody>\n");
449 for (final URI property : ordering.sortedCopy(properties.keySet())) {
450 final Multiset<Value> values = properties.get(property);
451 for (final Value value : ordering.sortedCopy(values.elementSet())) {
452 final int occurrences = values.count(value);
453 final URI example = examples.get(ImmutableList.of(property, value));
454 out.append("<tr><td>");
455 render(property, out);
456 out.append("</td><td>");
457 render(value, out);
458 out.append("</td><td>");
459 if (linkTemplate == null) {
460 out.append(Integer.toString(occurrences));
461 } else {
462 final Escaper e = UrlEscapers.urlFormParameterEscaper();
463 final String p = e.escape(Data.toString(property, Data.getNamespaceMap()));
464 final String v = e.escape(Data.toString(value, Data.getNamespaceMap()));
465 final String u = linkTemplate.replace("${property}", p).replace("${value}", v);
466 out.append("<a href=\"").append(u).append("\">")
467 .append(Integer.toString(occurrences)).append("</a>");
468 }
469 out.append("</td><td>");
470 render(example, out);
471 out.append("</td></tr>\n");
472 }
473 }
474 out.append("</tbody>\n</table>\n");
475 out.append("<script>$(document).ready(function() { applyDataTable('").append(tableID)
476 .append("', false, {").append(Strings.nullToEmpty(extraOptions))
477 .append("}); });</script>");
478 return out;
479 }
480
481
482
483
484
485
486
487
488 @Nullable
489 public static String shortenURI(@Nullable final URI uri) {
490 if (uri == null) {
491 return null;
492 }
493 final String prefix = Data.namespaceToPrefix(uri.getNamespace(), Data.getNamespaceMap());
494 if (prefix != null) {
495 return prefix + ':' + uri.getLocalName();
496 }
497 final String ns = uri.getNamespace();
498 return "<.." + uri.stringValue().substring(ns.length() - 1) + ">";
499
500
501
502
503
504 }
505
506
507
508
509
510
511
512
513 @Nullable
514 public static String escapeHtml(@Nullable final Object object) {
515 return object == null ? null : HtmlEscapers.htmlEscaper().escape(object.toString());
516 }
517
518 public static String escapeJavaScriptString(@Nullable final Object object) {
519 return object == null ? null : object.toString().replaceAll("'", "\\\\'");
520 }
521
522 private RenderUtils() {
523 }
524
525 }