1 package eu.fbk.dkm.pikes.rdf.util;
2
3 import com.google.common.base.Charsets;
4 import com.google.common.base.Joiner;
5 import com.google.common.base.Strings;
6 import com.google.common.base.Throwables;
7 import com.google.common.collect.*;
8 import com.google.common.html.HtmlEscapers;
9 import eu.fbk.rdfpro.util.*;
10 import org.eclipse.rdf4j.model.*;
11 import org.eclipse.rdf4j.model.vocabulary.RDF;
12 import org.eclipse.rdf4j.model.vocabulary.XMLSchema;
13 import org.slf4j.Logger;
14 import org.slf4j.LoggerFactory;
15
16 import javax.annotation.Nullable;
17 import java.io.*;
18 import java.util.Comparator;
19 import java.util.List;
20 import java.util.Map;
21 import java.util.Set;
22
23 public final class RDFGraphvizRenderer {
24
25 private static final Logger LOGGER = LoggerFactory.getLogger(RDFGraphvizRenderer.class);
26
27 private static final String NEWLINE = " ";
28
29 private final Set<String> nodeNamespaces;
30
31 private final Set<Resource> nodeTypes;
32
33 private final Set<IRI> ignoredProperties;
34
35 private final Set<IRI> collapsedProperties;
36
37 private final Map<? super Resource, String> colorMap;
38
39 private final Map<? super Resource, String> styleMap;
40
41 private final Namespaces namespaces;
42
43 private final Ordering<? super Value> valueComparator;
44
45 private final String graphvizCommand;
46
47 private RDFGraphvizRenderer(final Builder builder) {
48 this.nodeNamespaces = builder.nodeNamespaces == null ? ImmutableSet.of()
49 : ImmutableSet.copyOf(builder.nodeNamespaces);
50 this.nodeTypes = builder.nodeTypes == null ? ImmutableSet.of()
51 : ImmutableSet.copyOf(builder.nodeTypes);
52 this.ignoredProperties = builder.ignoredProperties == null ? ImmutableSet.of()
53 : ImmutableSet.copyOf(builder.ignoredProperties);
54 this.collapsedProperties = builder.collapsedProperties == null ? ImmutableSet.of()
55 : ImmutableSet.copyOf(builder.collapsedProperties);
56 this.colorMap = builder.colorMap == null ? null : ImmutableMap.copyOf(builder.colorMap);
57 this.styleMap = builder.styleMap == null ? null : ImmutableMap.copyOf(builder.styleMap);
58 this.namespaces = builder.namespaces == null ? Namespaces.DEFAULT : builder.namespaces;
59 this.valueComparator = builder.valueComparator != null ? Ordering
60 .from(builder.valueComparator) : Ordering.from(Statements.valueComparator());
61 this.graphvizCommand = builder.graphvizCommand == null ? "neato" : builder.graphvizCommand;
62 }
63
64 public <T extends Appendable> T emitSVG(final T out, final QuadModel model) throws IOException {
65
66 Process process = null;
67 File dotFile = null;
68
69 try {
70 dotFile = File.createTempFile("graphviz-", ".dot");
71 dotFile.deleteOnExit();
72
73 try (Writer writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(
74 dotFile), Charsets.UTF_8))) {
75 emitDot(writer, model);
76 }
77
78 process = new ProcessBuilder().command(this.graphvizCommand,
79 dotFile.getAbsolutePath(), "-Tsvg").start();
80
81 final BufferedReader in = new BufferedReader(new InputStreamReader(
82 process.getInputStream()));
83 final BufferedReader err = new BufferedReader(new InputStreamReader(
84 process.getErrorStream()));
85
86 Environment.getPool().submit(new Runnable() {
87
88 @Override
89 public void run() {
90 String line;
91 try {
92 while ((line = err.readLine()) != null) {
93 LOGGER.error("[" + RDFGraphvizRenderer.this.graphvizCommand + "] "
94 + line);
95 }
96 } catch (final IOException ex) {
97 LOGGER.error(
98 "[" + RDFGraphvizRenderer.this.graphvizCommand + "] "
99 + ex.getMessage(), ex);
100 }
101 }
102
103 });
104
105 boolean insideHeader = true;
106 String line;
107 while ((line = in.readLine()) != null) {
108 if (insideHeader && line.startsWith("<svg")) {
109 insideHeader = false;
110 }
111 if (!insideHeader) {
112 out.append(line).append('\n');
113 }
114 }
115 process.waitFor();
116
117 } catch (final Throwable ex) {
118 Throwables.propagateIfPossible(ex, IOException.class);
119 Throwables.propagate(ex);
120
121 } finally {
122 if (dotFile != null) {
123 dotFile.delete();
124 }
125 if (process != null) {
126 process.destroy();
127 }
128 }
129
130 return out;
131 }
132
133 public void emitDot(final Appendable out, final QuadModel model) throws IOException {
134
135
136 final Set<Resource> nodes = Sets.newHashSet();
137 if (!this.nodeNamespaces.isEmpty()) {
138 for (final Value value : Iterables.concat(model.subjects(), model.objects())) {
139 if (value instanceof IRI
140 && this.nodeNamespaces.contains(((IRI) value).getNamespace())) {
141 nodes.add((IRI) value);
142 }
143 }
144 }
145 if (!this.nodeTypes.isEmpty()) {
146 for (final Statement stmt : model.filter(null, RDF.TYPE, null)) {
147 if (stmt.getObject() instanceof Resource
148 && this.nodeTypes.contains(stmt.getObject())) {
149 nodes.add(stmt.getSubject());
150 }
151 }
152 }
153
154
155 out.append("digraph \"\"\n{\n");
156 out.append("graph [nodesep=1, ranksep=.02, pad=0, margin=0, pack=clust, width=1000];\n");
157 out.append("node [shape=plaintext, height=0, width=0, fontname=helvetica, fontsize=10];\n");
158 out.append("edge [arrowsize=.5, fontsize=8, fontname=helvetica, len=1.5];\n\n");
159
160
161 for (final Resource node : nodes) {
162 final String id = hash(node);
163 final Set<Value> types = model.filter(node, RDF.TYPE, null).objects();
164 out.append(id).append(" [");
165 out.append("label=<<table border=\"0\" cellborder=\"0\" cellpadding=\"0\" bgcolor=\"");
166 out.append(select(this.colorMap, types, "#FFFFFF"));
167 out.append("\" href=\"\"><tr><td>");
168 out.append(format(node));
169 out.append("</td></tr></table>>");
170 out.append(" tooltip=\"");
171 final Set<Resource> expandedNodes = Sets.newHashSet();
172 if (!emitDotTooltip(out, model, node, 0, nodes, expandedNodes)) {
173 out.append(" ");
174 }
175 out.append("\" ");
176 out.append(select(this.styleMap, types, ""));
177 out.append("];\n");
178 }
179
180
181 final Set<List<Value>> encounteredEdges = Sets.newHashSet();
182 for (final Statement stmt : model) {
183
184
185 if (this.ignoredProperties.contains(stmt.getPredicate())) {
186 continue;
187 }
188
189
190 if (!(stmt.getObject() instanceof Resource)) {
191 continue;
192 }
193 final Resource sourceNode = stmt.getSubject();
194 final Resource targetNode = (Resource) stmt.getObject();
195 if (!nodes.contains(sourceNode) || !nodes.contains(targetNode)
196 || !encounteredEdges.add(ImmutableList.of(sourceNode, targetNode))) {
197 continue;
198 }
199 final String sourceId = hash(sourceNode);
200 final String targetId = hash(targetNode);
201
202
203 final List<IRI> properties = this.valueComparator.sortedCopy(model.filter(sourceNode,
204 null, targetNode).predicates());
205
206
207 final List<IRI> keys = Lists.newArrayList(properties);
208 for (final Value sourceType : model.filter(sourceNode, RDF.TYPE, null).objects()) {
209 if (sourceType instanceof IRI) {
210 keys.add(Statements.VALUE_FACTORY.createIRI(sourceType.stringValue() + "-from"));
211 }
212 }
213 for (final Value targetType : model.filter(targetNode, RDF.TYPE, null).objects()) {
214 if (targetType instanceof IRI) {
215 keys.add(Statements.VALUE_FACTORY.createIRI(targetType.stringValue() + "-to"));
216 }
217 }
218
219
220 out.append(" ").append(sourceId).append(" -> ").append(targetId).append(" [");
221 out.append("label=<<table border=\"0\" cellborder=\"0\" cellpadding=\"0\" href=\"\" tooltip=\"");
222 out.append(Joiner.on(" | ").join(format(properties)));
223 out.append("\"><tr><td>");
224 out.append(format(properties.get(0)));
225 out.append("</td></tr></table>>");
226 out.append(" ");
227 out.append(select(this.styleMap, keys, ""));
228 out.append("];\n");
229 }
230
231
232 out.append("}");
233 }
234
235 private boolean emitDotTooltip(final Appendable out, final QuadModel model,
236 final Resource node, final int indent, final Set<Resource> excludedNodes,
237 final Set<Resource> expandedNodes) throws IOException {
238 boolean notEmpty = false;
239 for (final IRI pred : this.valueComparator.sortedCopy(model.filter(node, null, null)
240 .predicates())) {
241 if (this.ignoredProperties.contains(pred)) {
242 continue;
243 }
244 expandedNodes.add(node);
245 for (final Value object : this.valueComparator.sortedCopy(model.filter(node, pred,
246 null).objects())) {
247 if (!excludedNodes.contains(object)) {
248 out.append(Strings.repeat(" ", indent));
249 out.append(format(pred));
250 out.append(" ");
251 out.append(format(object));
252 final boolean expanded = expandedNodes.contains(object);
253 if (expanded) {
254 out.append(" [...]");
255 }
256 out.append(NEWLINE);
257 notEmpty = true;
258 if (object instanceof Resource && !expanded
259 && !this.collapsedProperties.contains(pred)) {
260 emitDotTooltip(out, model, (Resource) object, indent + 1, excludedNodes,
261 expandedNodes);
262 }
263 }
264 }
265 }
266 return notEmpty;
267 }
268
269 private List<String> format(final Iterable<? extends Value> values) {
270 final List<String> result = Lists.newArrayList();
271 for (final Value value : values) {
272 result.add(format(value));
273 }
274 return result;
275 }
276
277 private String format(final Value value) {
278 if (value instanceof IRI) {
279 final IRI IRI = (IRI) value;
280 final String ns = IRI.getNamespace();
281 final String prefix = this.namespaces.prefixFor(ns);
282 if (prefix == null) {
283 return escape("<.." + ns.charAt(ns.length() - 1) + IRI.getLocalName() + ">");
284 } else {
285 return prefix + ":" + escape(IRI.getLocalName());
286 }
287 }
288 return escape(Statements.formatValue(value, this.namespaces));
289 }
290
291 @Nullable
292 private String shorten(@Nullable final IRI IRI) {
293 if (IRI == null) {
294 return null;
295 }
296 final String prefix = this.namespaces.prefixFor(IRI.getNamespace());
297 if (prefix != null) {
298 return prefix + ':' + IRI.getLocalName();
299 }
300 return "<../" + IRI.getLocalName() + ">";
301 }
302
303 private static String select(@Nullable final Map<? super Resource, String> map,
304 final Iterable<? extends Value> keys, final String defaultColor) {
305 String color = null;
306 if (map != null) {
307 for (final Value key : keys) {
308 if (key instanceof Resource) {
309 final String mappedColor = map.get(key);
310 if (mappedColor != null) {
311 if (color == null) {
312 color = mappedColor;
313 } else {
314 break;
315 }
316 }
317 }
318 }
319 }
320 return color != null ? color : defaultColor;
321 }
322
323 private static String escape(final String string) {
324 return HtmlEscapers.htmlEscaper().escape(string);
325 }
326
327 private static String hash(final Value value) {
328 final StringBuilder builder = new StringBuilder();
329 if (value instanceof IRI) {
330 builder.append((char) 1);
331 builder.append(value.stringValue());
332 } else if (value instanceof BNode) {
333 builder.append((char) 2);
334 builder.append(((BNode) value).getID());
335 } else if (value instanceof Literal) {
336 final Literal literal = (Literal) value;
337 builder.append((char) 3);
338 builder.append(literal.getLabel());
339 if (literal.getLanguage().isPresent()) {
340 builder.append((char) 4);
341 builder.append(literal.getLanguage().get());
342 } else if (!literal.getDatatype().equals(XMLSchema.STRING)) {
343 builder.append((char) 5);
344 builder.append(literal.getDatatype().stringValue());
345 }
346 }
347 return "N" + Hash.murmur3(builder.toString()).toString().replace('-', 'x');
348 }
349
350 public static Builder builder() {
351 return new Builder();
352 }
353
354 public static final class Builder {
355
356 @Nullable
357 private Iterable<String> nodeNamespaces;
358
359 @Nullable
360 private Iterable<? extends Resource> nodeTypes;
361
362 @Nullable
363 private Iterable<? extends IRI> ignoredProperties;
364
365 @Nullable
366 private Iterable<? extends IRI> collapsedProperties;
367
368 @Nullable
369 private Map<? super Resource, String> colorMap;
370
371 @Nullable
372 private Map<? super Resource, String> styleMap;
373
374 @Nullable
375 private Namespaces namespaces;
376
377 @Nullable
378 private Comparator<? super Value> valueComparator;
379
380 @Nullable
381 private String graphvizCommand;
382
383 public Builder withNodeNamespaces(@Nullable final Iterable<String> nodeNamespaces) {
384 this.nodeNamespaces = nodeNamespaces;
385 return this;
386 }
387
388 public Builder withNodeTypes(@Nullable final Iterable<? extends IRI> nodeTypes) {
389 this.nodeTypes = nodeTypes;
390 return this;
391 }
392
393 public Builder withIgnoredProperties(
394 @Nullable final Iterable<? extends IRI> ignoredProperties) {
395 this.ignoredProperties = ignoredProperties;
396 return this;
397 }
398
399 public Builder withCollapsedProperties(
400 @Nullable final Iterable<? extends IRI> collapsedProperties) {
401 this.collapsedProperties = collapsedProperties;
402 return this;
403 }
404
405 public Builder withColorMap(@Nullable final Map<? super Resource, String> colorMap) {
406 this.colorMap = colorMap;
407 return this;
408 }
409
410 public Builder withStyleMap(@Nullable final Map<? super Resource, String> styleMap) {
411 this.styleMap = styleMap;
412 return this;
413 }
414
415 public Builder withNamespaces(@Nullable final Namespaces namespaces) {
416 this.namespaces = namespaces;
417 return this;
418 }
419
420 public Builder withValueComparator(
421 @Nullable final Comparator<? super Value> valueComparator) {
422 this.valueComparator = valueComparator;
423 return this;
424 }
425
426 public Builder withGraphvizCommand(@Nullable final String graphvizCommand) {
427 this.graphvizCommand = graphvizCommand;
428 return this;
429 }
430
431 public RDFGraphvizRenderer build() {
432 return new RDFGraphvizRenderer(this);
433 }
434
435 }
436
437 }