| /** |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for ad |
| * ditional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package org.apache.jena.riot.writer ; |
| |
| import static org.apache.jena.graph.Node.ANY; |
| import static org.apache.jena.riot.writer.WriterConst.*; |
| |
| import java.util.*; |
| |
| import org.apache.jena.atlas.io.IndentedWriter; |
| import org.apache.jena.atlas.iterator.Iter; |
| import org.apache.jena.atlas.lib.InternalErrorException; |
| import org.apache.jena.atlas.lib.Pair; |
| import org.apache.jena.atlas.lib.SetUtils; |
| import org.apache.jena.graph.Graph; |
| import org.apache.jena.graph.Node; |
| import org.apache.jena.graph.Node_Triple; |
| import org.apache.jena.graph.Triple; |
| import org.apache.jena.riot.RIOT; |
| import org.apache.jena.riot.other.GLib; |
| import org.apache.jena.riot.out.NodeFormatter; |
| import org.apache.jena.riot.out.NodeFormatterTTL; |
| import org.apache.jena.riot.out.NodeFormatterTTL_MultiLine; |
| import org.apache.jena.riot.out.NodeToLabel; |
| import org.apache.jena.riot.system.PrefixMap; |
| import org.apache.jena.riot.system.PrefixMapFactory; |
| import org.apache.jena.riot.system.RiotLib; |
| import org.apache.jena.sparql.core.DatasetGraph; |
| import org.apache.jena.sparql.core.Quad; |
| import org.apache.jena.sparql.util.Context; |
| import org.apache.jena.util.iterator.ExtendedIterator; |
| import org.apache.jena.vocabulary.RDF; |
| import org.apache.jena.vocabulary.RDFS; |
| |
| /** |
| * Base class to support the pretty forms of Turtle-related languages (Turtle, TriG) |
| */ |
| public abstract class TurtleShell { |
| protected final IndentedWriter out ; |
| protected final NodeFormatter nodeFmt ; |
| protected final PrefixMap prefixMap ; |
| protected final String baseURI ; |
| protected final Context context ; |
| protected final DirectiveStyle prefixStyle; |
| |
| protected TurtleShell(IndentedWriter out, PrefixMap pmap, String baseURI, NodeFormatter nodeFmt, Context context) { |
| this.out = out ; |
| if ( pmap == null ) |
| pmap = PrefixMapFactory.emptyPrefixMap() ; |
| this.prefixMap = pmap ; |
| this.baseURI = baseURI ; |
| this.nodeFmt = nodeFmt ; |
| this.context = context; |
| this.prefixStyle = WriterLib.directiveStyle(context) ; |
| } |
| |
| protected TurtleShell(IndentedWriter out, PrefixMap pmap, String baseURI, Context context) { |
| this(out, pmap, baseURI, createNodeFormatter(pmap,baseURI,context), context) ; |
| } |
| |
| static public NodeFormatter createNodeFormatter(PrefixMap pmap, String baseURI, Context context) { |
| if ( context != null && context.isTrue(RIOT.multilineLiterals) ) |
| return new NodeFormatterTTL_MultiLine(baseURI, pmap, NodeToLabel.createScopeByDocument()) ; |
| else |
| return new NodeFormatterTTL(baseURI, pmap, NodeToLabel.createScopeByDocument()) ; |
| } |
| |
| protected void writeBase(String base) { |
| if ( context == null || ! context.isTrue(RIOT.symTurtleOmitBase) ) |
| RiotLib.writeBase(out, base, prefixStyle==DirectiveStyle.SPARQL) ; |
| } |
| |
| protected void writePrefixes(PrefixMap prefixMap) { |
| RiotLib.writePrefixes(out, prefixMap, prefixStyle==DirectiveStyle.SPARQL) ; |
| } |
| |
| /** Write graph in Turtle syntax (or part of TriG) */ |
| protected void writeGraphTTL(Graph graph) { |
| ShellGraph x = new ShellGraph(graph, null, null, null) ; |
| x.writeGraph() ; |
| } |
| |
| /** Write graph in Turtle syntax (or part of TriG). graphName is null for default graph. */ |
| protected void writeGraphTTL(DatasetGraph dsg, Node graphName, Set<Node> graphNames) { |
| Graph g = (graphName == null || Quad.isDefaultGraph(graphName)) |
| ? dsg.getDefaultGraph() |
| : dsg.getGraph(graphName) ; |
| ShellGraph x = new ShellGraph(g, graphName, dsg, graphNames) ; |
| x.writeGraph() ; |
| } |
| |
| // Write one graph - using an inner object class to isolate |
| // the state variables for writing a single graph. |
| private final class ShellGraph { |
| // Dataset (for writing graphs in datasets) -- may be null |
| private final DatasetGraph dsg ; |
| private final Collection<Node> graphNames ; |
| private final Node graphName ; |
| private final Graph graph ; |
| |
| // Blank nodes that have one incoming triple |
| private final Set<Node> nestedObjects ; |
| private final Set<Node> nestedObjectsWritten ; |
| |
| // Blank node subjects that are not referenced as objects or graph names |
| // excluding unlinked lists. |
| private final Set<Node> freeBnodes ; |
| |
| // The head node in each well-formed list -> list elements |
| private final Map<Node, List<Node>> lists ; |
| |
| // List that do not have any incoming triples |
| private final Map<Node, List<Node>> freeLists ; |
| |
| // Lists that have more than one incoming triple |
| private final Map<Node, List<Node>> nLinkedLists ; |
| |
| // All nodes that are part of list structures. |
| private final Collection<Node> listElts ; |
| |
| // Allow lists and nest bnode objects. |
| // This is true for the main pretty printing then |
| // false when we are clearing up unwritten triples. |
| private boolean allowDeepPretty = true ; |
| |
| private ShellGraph(Graph graph, Node graphName, DatasetGraph dsg, Set<Node> graphNames) { |
| this.dsg = dsg ; |
| this.graphName = graphName ; |
| this.graphNames = graphNames; |
| this.graph = graph ; |
| this.nestedObjects = new HashSet<>() ; |
| this.nestedObjectsWritten = new HashSet<>() ; |
| this.freeBnodes = new HashSet<>() ; |
| |
| this.lists = new HashMap<>() ; |
| this.freeLists = new HashMap<>() ; |
| this.nLinkedLists = new HashMap<>() ; |
| this.listElts = new HashSet<>() ; |
| this.allowDeepPretty = true ; |
| |
| // ?? Single pass? |
| // <<>> - and nested - bnodes can't be PP. |
| |
| // Must be in this order. |
| findLists() ; |
| findBNodesSyntax1() ; |
| // Stop head of lists printed as triples going all the way to the |
| // good part. |
| nestedObjects.removeAll(listElts) ; |
| |
| //printDetails() ; |
| } |
| |
| // Debug |
| private void printDetails() { |
| printDetails("nestedObjects", nestedObjects) ; |
| //printDetails("nestedObjectsWritten", nestedObjectsWritten) ; |
| printDetails("freeBnodes", freeBnodes) ; |
| |
| printDetails("lists", lists) ; |
| printDetails("freeLists", freeLists) ; |
| printDetails("nLinkedLists", nLinkedLists) ; |
| printDetails("listElts", listElts) ; |
| } |
| |
| private void printDetails(String label, Map<Node, List<Node>> map) { |
| System.err.print("## ") ; |
| System.err.print(label) ; |
| System.err.print(" = ") ; |
| System.err.println(map) ; |
| } |
| |
| private void printDetails(String label, Collection<Node> nodes) { |
| System.err.print("## ") ; |
| System.err.print(label) ; |
| System.err.print(" = ") ; |
| System.err.println(nodes) ; |
| } |
| |
| // ---- Data access |
| /** Get all the triples for the graph.find */ |
| private List<Triple> triples(Node s, Node p, Node o) { |
| List<Triple> acc = new ArrayList<>() ; |
| RiotLib.accTriples(acc, graph, s, p, o) ; |
| return acc ; |
| } |
| |
| /** Get exactly one triple or null for none or more than one. */ |
| private Triple triple1(Node s, Node p, Node o) { |
| if ( dsg != null ) |
| return RiotLib.triple1(dsg, s, p, o) ; |
| else |
| return RiotLib.triple1(graph, s, p, o) ; |
| } |
| |
| /** Get exactly one triple, or null for none or more than one. */ |
| private Triple triple1(DatasetGraph dsg, Node s, Node p, Node o) { |
| Iterator<Quad> iter = dsg.find(ANY, s, p, o) ; |
| if ( !iter.hasNext() ) |
| return null ; |
| Quad q = iter.next() ; |
| if ( iter.hasNext() ) |
| return null ; |
| return q.asTriple() ; |
| } |
| |
| private long countTriples(Node s, Node p, Node o) { |
| if ( dsg != null ) |
| return RiotLib.countTriples(dsg, s, p, o) ; |
| else |
| return RiotLib.countTriples(graph, s, p, o) ; |
| } |
| |
| private ExtendedIterator<Triple> find(Node s, Node p, Node o) { |
| return graph.find(s, p, o) ; |
| } |
| |
| /** returns 0,1,2 (where 2 really means "more than 1") */ |
| private int inLinks(Node obj) { |
| if ( dsg != null ) { |
| Iterator<Quad> iter = dsg.find(ANY, ANY, ANY, obj) ; |
| return count012(iter) ; |
| } else { |
| ExtendedIterator<Triple> iter = graph.find(ANY, ANY, obj) ; |
| try { return count012(iter) ; } |
| finally { iter.close() ; } |
| } |
| } |
| |
| // Unused |
| // /** returns 0,1,2 (where 2 really means "more than 1") */ |
| // private int outLinks(Node subj) { |
| // if ( dsg != null ) { |
| // Iterator<Quad> iter = dsg.find(ANY, subj, ANY, ANY) ; |
| // return count012(iter) ; |
| // } else { |
| // ExtendedIterator<Triple> iter = graph.find(subj, ANY, ANY) ; |
| // try { return count012(iter) ; } |
| // finally { iter.close() ; } |
| // } |
| // } |
| |
| private int count012(Iterator<? > iter) { |
| if ( !iter.hasNext() ) |
| return 0 ; |
| iter.next() ; |
| if ( !iter.hasNext() ) |
| return 1 ; |
| return 2 ; |
| } |
| |
| /** Check whether a node is used only in the graph we're working on */ |
| private boolean containedInOneGraph(Node node) { |
| if ( dsg == null ) |
| // Single graph |
| return true ; |
| |
| if ( graphNames.contains(node) ) |
| // Used as a graph name. |
| return false ; |
| |
| Iterator<Quad> iter = dsg.find(ANY, node, ANY, ANY) ; |
| if ( ! quadsThisGraph(iter) ) |
| return false ; |
| |
| iter = dsg.find(ANY, ANY, node, ANY) ; |
| if ( ! quadsThisGraph(iter) ) |
| return false ; |
| |
| iter = dsg.find(ANY, ANY, ANY, node) ; |
| if ( ! quadsThisGraph(iter) ) |
| return false ; |
| return true ; |
| } |
| |
| /** Check whether an iterator of quads is all in the same graph (dataset assumed) */ |
| private boolean quadsThisGraph(Iterator<Quad> iter) { |
| if ( ! iter.hasNext() ) |
| // Empty iterator |
| return true ; |
| Quad q = iter.next() ; |
| Node gn = q.getGraph() ; |
| |
| // Test first quad - both default graph (various forms) or same named graph |
| if ( isDefaultGraph(gn) ) { |
| if ( ! isDefaultGraph(graphName) ) |
| return false ; |
| } else { |
| if ( ! Objects.equals(gn, graphName) ) |
| // Not both same named graph |
| return false ; |
| } |
| // Check rest of iterator. |
| for ( ; iter.hasNext() ; ) { |
| Quad q2 = iter.next() ; |
| if ( ! Objects.equals(gn, q2.getGraph()) ) |
| return false ; |
| } |
| return true ; |
| } |
| |
| private boolean isDefaultGraph(Node node) { |
| return node == null || Quad.isDefaultGraph(node) ; |
| } |
| |
| /** Get triples with the same subject */ |
| private Collection<Triple> triplesOfSubject(Node subj) { |
| return RiotLib.triplesOfSubject(graph, subj) ; |
| } |
| |
| private Iterator<Node> listSubjects() { |
| return GLib.listSubjects(graph) ; |
| } |
| |
| // ---- Data access |
| |
| /** Find Bnodes that can written as [] |
| * Subject position (top level) - only used for subject position anywhere in the dataset |
| * Object position (any level) - only used as object once anywhere in the dataset |
| * Not used in triple terms. |
| * These must be written with _: syntax or [] no contents. |
| * We do not cover the latter case (and it is not legal in PG mode where the |
| * triple term must refer to a triple in the graph so blank node used elsewhere.) |
| */ |
| private void findBNodesSyntax1() { |
| // Set of all bnodes used into triple terms (RDF*) |
| Set<Node> blankNodesInTripleTerms = new HashSet<>(); |
| // Nodes known not to meet the requirement. |
| Set<Node> rejects = new HashSet<>() ; |
| ExtendedIterator<Triple> iter = find(ANY, ANY, ANY) ; |
| try { |
| for ( ; iter.hasNext() ; ) { |
| Triple t = iter.next() ; |
| Node subj = t.getSubject() ; |
| Node obj = t.getObject() ; |
| |
| if ( subj.isBlank() ) |
| { |
| int sConn = inLinks(subj) ; |
| if ( sConn == 0 && containedInOneGraph(subj) ) |
| // Not used as an object in this graph. |
| freeBnodes.add(subj) ; |
| } else if ( subj.isNodeTriple() ) { |
| extractBlankNodesInTripleTerms(blankNodesInTripleTerms, subj); |
| } |
| |
| if ( obj.isNodeTriple() ) { |
| extractBlankNodesInTripleTerms(blankNodesInTripleTerms, obj); |
| continue; |
| } |
| |
| if ( ! obj.isBlank() ) |
| continue ; |
| if ( rejects.contains(obj) ) |
| continue ; |
| |
| int connectivity = inLinks(obj) ; |
| if ( connectivity == 1 && containedInOneGraph(obj) ) { |
| // If not used in another graph (or as graph name) |
| nestedObjects.add(obj) ; |
| } |
| else |
| // Uninteresting object connected multiple times. |
| rejects.add(obj) ; |
| } |
| // Remove any blank nodes in triple terms. These have to be done |
| // without nesting' we also do not abbreviate as [],. |
| freeBnodes.removeAll(blankNodesInTripleTerms); |
| nestedObjects.removeAll(blankNodesInTripleTerms); |
| } finally { iter.close() ; } |
| } |
| |
| // Helper for findBNodeSyntax1 |
| private void extractBlankNodesInTripleTerms(Set<Node> blankNodesInTripleTerms, Node nodeTriple) { |
| // Needs to recurse. |
| Triple triple = Node_Triple.triple(nodeTriple); |
| Node tSubj = triple.getSubject(); |
| Node tObj = triple.getObject(); |
| |
| if ( tSubj.isBlank() ) |
| blankNodesInTripleTerms.add(tSubj); |
| else if ( tSubj.isNodeTriple() ) |
| extractBlankNodesInTripleTerms(blankNodesInTripleTerms, tSubj); |
| |
| if ( tObj.isBlank() ) |
| blankNodesInTripleTerms.add(tObj); |
| else if ( tObj.isNodeTriple() ) |
| extractBlankNodesInTripleTerms(blankNodesInTripleTerms, tObj); |
| } |
| |
| // --- Lists setup |
| /* |
| * Find all list heads and all nodes in well-formed lists. Return a |
| * (list head -> Elements map), list elements) |
| */ |
| private void findLists() { |
| List<Triple> tails = triples(ANY, RDF_Rest, RDF_Nil) ; |
| for ( Triple t : tails ) { |
| // Returns the elements, reversed. |
| Collection<Node> listElts2 = new HashSet<>() ; |
| Pair<Node, List<Node>> p = followTailToHead(t.getSubject(), listElts2) ; |
| if ( p != null ) { |
| Node headElt = p.getLeft() ; |
| // Free standing/private |
| List<Node> elts = p.getRight() ; |
| long numLinks = countTriples(null, null, headElt) ; |
| if ( numLinks == 1 ) |
| lists.put(headElt, elts) ; |
| else if ( numLinks == 0 ) |
| // 0 connected lists |
| freeLists.put(headElt, elts) ; |
| else |
| // Two triples to this list. |
| nLinkedLists.put(headElt, elts) ; |
| listElts.addAll(listElts2) ; |
| } |
| } |
| } |
| |
| // return head elt node, list of elements. |
| private Pair<Node, List<Node>> followTailToHead(Node lastListElt, Collection<Node> listElts) { |
| List<Node> listCells = new ArrayList<>() ; |
| List<Node> eltsReversed = new ArrayList<>() ; |
| List<Triple> acc = new ArrayList<>() ; |
| Node x = lastListElt ; |
| |
| for ( ; ; ) { |
| if ( !validListElement(x, acc) ) { |
| if ( listCells.size() == 0 ) |
| // No earlier valid list. |
| return null ; |
| // Fix up to previous valid list cell. |
| x = listCells.remove(listCells.size() - 1) ; |
| break ; |
| } |
| |
| Triple t = triple1(x, RDF_First, null) ; |
| if ( t == null ) |
| return null ; |
| eltsReversed.add(t.getObject()) ; |
| listCells.add(x) ; |
| |
| // Try to move up the list. |
| List<Triple> acc2 = triples(null, null, x) ; |
| long numRest = countTriples(null, RDF_Rest, x) ; |
| if ( numRest != 1 ) { |
| // Head of well-formed list. |
| // Classified by 0,1,more links later. |
| listCells.add(x) ; |
| break ; |
| } |
| // numRest == 1 |
| int numLinks = acc2.size() ; |
| if ( numLinks > 1 ) |
| // Non-list links to x |
| break ; |
| // Valid. |
| Triple tLink = acc2.get(0) ; |
| x = tLink.getSubject() ; |
| } |
| // Success. |
| listElts.addAll(listCells) ; |
| Collections.reverse(eltsReversed) ; |
| return Pair.create(x, eltsReversed) ; |
| } |
| |
| /** Return the triples of the list element, or null if invalid list */ |
| private boolean validListElement(Node x, List<Triple> acc) { |
| Triple t1 = triple1(x, RDF_Rest, null) ; // Which we came up to get |
| // here :-( |
| if ( t1 == null ) |
| return false ; |
| Triple t2 = triple1(x, RDF_First, null) ; |
| if ( t2 == null ) |
| return false ; |
| long N = countTriples(x, null, null) ; |
| if ( N != 2 ) |
| return false ; |
| acc.add(t1) ; |
| acc.add(t2) ; |
| return true ; |
| } |
| |
| // ---- |
| |
| private void writeGraph() { |
| Iterator<Node> subjects = listSubjects() ; |
| boolean somethingWritten = writeBySubject(subjects) ; |
| // Write remainders |
| // 1 - Shared lists |
| somethingWritten = writeRemainingNLinkedLists(somethingWritten) ; |
| |
| // 2 - Free standing lists |
| somethingWritten = writeRemainingFreeLists(somethingWritten) ; |
| |
| // 3 - Blank nodes that are unwritten single objects. |
| // System.err.println("## ## ##") ; |
| // printDetails("nestedObjects", nestedObjects) ; |
| // printDetails("nestedObjectsWritten", nestedObjectsWritten) ; |
| Set<Node> singleNodes = SetUtils.difference(nestedObjects, nestedObjectsWritten) ; |
| somethingWritten = writeRemainingNestedObjects(singleNodes, somethingWritten) ; |
| } |
| |
| private boolean writeRemainingNLinkedLists(boolean somethingWritten) { |
| // Print carefully - need a label for the first cell. |
| // So we write out the first element of the list in triples, then |
| // put the remainer as a pretty list |
| for ( Node n : nLinkedLists.keySet() ) { |
| if ( somethingWritten ) |
| out.println() ; |
| somethingWritten = true ; |
| |
| List<Node> x = nLinkedLists.get(n) ; |
| writeNode(n) ; |
| |
| write_S_P_Gap(); |
| out.pad() ; |
| |
| writeNode(RDF_First) ; |
| print(" ") ; |
| writeNode(x.get(0)) ; |
| print(" ;") ; |
| println() ; |
| writeNode(RDF_Rest) ; |
| print(" ") ; |
| x = x.subList(1, x.size()) ; |
| writeList(x) ; |
| print(" .") ; |
| out.decIndent(INDENT_PREDICATE) ; |
| println() ; |
| } |
| return somethingWritten ; |
| } |
| |
| // Write free standing lists - ones where the head is not an object of |
| // some other triple. Turtle does not allow free standing (... ) . |
| // so write as a predicateObjectList for one element. |
| private boolean writeRemainingFreeLists(boolean somethingWritten) { |
| for ( Node n : freeLists.keySet() ) { |
| if ( somethingWritten ) |
| out.println() ; |
| somethingWritten = true ; |
| |
| List<Node> x = freeLists.get(n) ; |
| // Print first element for the [ ... ] |
| out.print("[ ") ; |
| |
| writeNode(RDF_First) ; |
| print(" ") ; |
| writeNode(x.get(0)) ; |
| print(" ; ") ; |
| writeNode(RDF_Rest) ; |
| print(" ") ; |
| x = x.subList(1, x.size()) ; |
| // Print remainder. |
| writeList(x) ; |
| out.println(" ] .") ; |
| } |
| return somethingWritten ; |
| } |
| |
| // Write any left over nested objects |
| // These come from blank node cycles : _:a <p> _:b . _b: <p> _:a . |
| // Also from from blank node cycles + tail: _:a <p> _:b . _:a <p> "" . _b: <p> _:a . |
| private boolean writeRemainingNestedObjects(Set<Node> objects, boolean somethingWritten) { |
| for ( Node n : objects ) { |
| if ( somethingWritten ) |
| out.println() ; |
| somethingWritten = true ; |
| |
| Triple t = triple1(null, null, n) ; |
| if ( t == null ) |
| throw new InternalErrorException("Expected exactly one triple") ; |
| |
| Node subj = t.getSubject() ; |
| boolean b = allowDeepPretty ; |
| try { |
| allowDeepPretty = false; |
| Collection<Triple> triples = triples(subj, null, null) ; |
| writeCluster(subj, triples); |
| } finally { allowDeepPretty = b ; } |
| } |
| |
| return somethingWritten ; |
| } |
| |
| // Write triples, flat and simply. |
| // Reset the state variables so "isPretty" return false. |
| private void writeTriples(Node subj, Iterator<Triple> iter) { |
| allowDeepPretty = false; |
| writeCluster(subj, Iter.toList(iter)); |
| } |
| |
| // return true if did write something. |
| private boolean writeBySubject(Iterator<Node> subjects) { |
| boolean first = true ; |
| for ( ; subjects.hasNext() ; ) { |
| Node subj = subjects.next() ; |
| if ( nestedObjects.contains(subj) ) |
| continue ; |
| if ( listElts.contains(subj) ) |
| continue ; |
| if ( !first ) |
| out.println() ; |
| first = false ; |
| if ( freeBnodes.contains(subj) ) { |
| // Top level: write in "[....]" on "[] :p" form. |
| writeNestedObjectTopLevel(subj) ; |
| continue ; |
| } |
| |
| Collection<Triple> cluster = triplesOfSubject(subj) ; |
| writeCluster(subj, cluster) ; |
| } |
| return !first ; |
| } |
| |
| // A Cluster is a collection of triples with the same subject. |
| private void writeCluster(Node subject, Collection<Triple> cluster) { |
| if ( cluster.isEmpty() ) |
| return ; |
| writeNode(subject) ; |
| writeClusterPredicateObjectList(INDENT_PREDICATE, cluster) ; |
| } |
| |
| // Write the PredicateObjectList for a subject already output. |
| // The subject may have been a "[]" or a URI - the indentation is passed in. |
| private void writeClusterPredicateObjectList(int indent, Collection<Triple> cluster) { |
| write_S_P_Gap() ; |
| out.incIndent(indent) ; |
| out.pad() ; |
| writePredicateObjectList(cluster) ; |
| out.decIndent(indent) ; |
| print(" .") ; |
| println() ; |
| } |
| |
| // Writing predicate-object lists. |
| // We group the cluster by predicate and within each group |
| // we print: |
| // literals, then simple objects, then pretty objects |
| |
| private void writePredicateObjectList(Collection<Triple> cluster) { |
| Map<Node, List<Node>> pGroups = groupByPredicates(cluster) ; |
| Collection<Node> predicates = pGroups.keySet() ; |
| |
| // Find longest predicate URI |
| int predicateMaxWidth = RiotLib.calcWidth(prefixMap, baseURI, predicates, MIN_PREDICATE, LONG_PREDICATE) ; |
| |
| boolean first = true ; |
| |
| if ( !OBJECT_LISTS ) { |
| for ( Node p : predicates ) { |
| for ( Node o : pGroups.get(p) ) { |
| writePredicateObject(p, o, predicateMaxWidth, first) ; |
| first = false ; |
| } |
| } |
| return ; |
| } |
| |
| for ( Node p : predicates ) { |
| // Literals in the group |
| List<Node> rdfLiterals = new ArrayList<>() ; |
| // Non-literals, printed |
| List<Node> rdfSimpleNodes = new ArrayList<>() ; |
| // Non-literals, printed (), or []-embedded |
| List<Node> rdfComplexNodes = new ArrayList<>() ; |
| |
| for ( Node o : pGroups.get(p) ) { |
| if ( o.isLiteral() ) { |
| rdfLiterals.add(o) ; |
| continue ; |
| } |
| if ( isPrettyNode(o) ) { |
| rdfComplexNodes.add(o) ; |
| continue ; |
| } |
| rdfSimpleNodes.add(o) ; |
| } |
| |
| if ( ! rdfLiterals.isEmpty() ) { |
| writePredicateObjectList(p, rdfLiterals, predicateMaxWidth, first) ; |
| first = false ; |
| } |
| if ( ! rdfSimpleNodes.isEmpty() ) { |
| writePredicateObjectList(p, rdfSimpleNodes, predicateMaxWidth, first) ; |
| first = false ; |
| } |
| for ( Node o : rdfComplexNodes ) { |
| writePredicateObject(p, o, predicateMaxWidth, first) ; |
| first = false ; |
| } |
| } |
| } |
| |
| private void writePredicateObject(Node p, Node obj, int predicateMaxWidth, boolean first) { |
| writePredicate(p, predicateMaxWidth, first) ; |
| out.incIndent(INDENT_OBJECT) ; |
| writeNodePretty(obj) ; |
| out.decIndent(INDENT_OBJECT) ; |
| } |
| |
| private void writePredicateObjectList(Node p, List<Node> objects, int predicateMaxWidth, boolean first) { |
| writePredicate(p, predicateMaxWidth, first) ; |
| out.incIndent(INDENT_OBJECT) ; |
| |
| boolean lastObjectMultiLine = false ; |
| boolean firstObject = true ; |
| for ( Node o : objects ) { |
| if ( !firstObject ) { |
| if ( out.getCurrentOffset() > 0 ) |
| out.print(" , ") ; |
| else |
| // Before the current indent, due to a multiline literal being written raw. |
| // We will pad spaces to indent on output spaces. Don't add a first " " |
| out.print(", ") ; |
| } |
| else |
| firstObject = false ; |
| int row1 = out.getRow() ; |
| writeNode(o) ; |
| int row2 = out.getRow(); |
| lastObjectMultiLine = (row2 > row1) ; |
| } |
| out.decIndent(INDENT_OBJECT) ; |
| } |
| |
| /** Write a predicate - jump to next line if deemed long */ |
| private void writePredicate(Node p, int predicateMaxWidth, boolean first) { |
| if ( ! first ) { |
| print(" ;") ; |
| println() ; |
| } |
| int colPredicateStart = out.getAbsoluteIndent() ; |
| |
| if ( !prefixMap.containsPrefix(rdfNS) && RDF_type.equals(p) ) |
| print("a") ; |
| else |
| writeNode(p) ; |
| int colPredicateFinish = out.getCol() ; |
| int wPredicate = (colPredicateFinish - colPredicateStart) ; |
| |
| if ( wPredicate > LONG_PREDICATE ) |
| println() ; |
| else { |
| out.pad(predicateMaxWidth) ; |
| // out.print(' ', predicateMaxWidth-wPredicate) ; |
| gap(GAP_P_O) ; |
| } |
| } |
| |
| private Map<Node, List<Node>> groupByPredicates(Collection<Triple> cluster) { |
| SortedMap<Node, List<Node>> x = new TreeMap<>(compPredicates) ; |
| for ( Triple t : cluster ) { |
| Node p = t.getPredicate() ; |
| if ( !x.containsKey(p) ) |
| x.put(p, new ArrayList<Node>()) ; |
| x.get(p).add(t.getObject()) ; |
| } |
| |
| return x ; |
| } |
| |
| // Compact if one triple, or one predicate and several non-pretty objects. |
| private boolean isCompact(Collection<Triple> cluster) { |
| Node predicate = null; |
| for ( Triple t : cluster ) { |
| Node p = t.getPredicate() ; |
| Node o = t.getObject(); |
| if ( isPrettyNode(o) ) |
| return false; |
| if ( predicate != null ) { |
| if ( ! predicate.equals(p)) |
| // 2+ different predicates. |
| return false ; |
| } else |
| predicate = p; |
| } |
| return true; |
| } |
| |
| // [ :p "abc" ] . or [] : "abc" . |
| private void writeNestedObjectTopLevel(Node subject) { |
| if ( true ) { |
| writeNestedObject(subject) ; |
| out.println(" .") ; |
| } else { |
| // Alternative. |
| Collection<Triple> cluster = triplesOfSubject(subject) ; |
| print("[]") ; |
| writeClusterPredicateObjectList(0, cluster) ; |
| } |
| } |
| |
| private void writeNestedObject(Node node) { |
| Collection<Triple> x = triplesOfSubject(node) ; |
| |
| if ( x.isEmpty() ) { |
| print("[] ") ; |
| return ; |
| } |
| |
| if ( isCompact(x) ) { |
| print("[ ") ; |
| out.incIndent(2) ; |
| writePredicateObjectList(x) ; |
| out.decIndent(2) ; |
| print(" ]") ; |
| return ; |
| } |
| |
| int indent0 = out.getAbsoluteIndent() ; |
| int here = out.getCol() ; |
| out.setAbsoluteIndent(here) ; |
| print("[ ") ; |
| out.incIndent(2) ; |
| writePredicateObjectList(x) ; |
| out.decIndent(2) ; |
| if ( true ) { |
| println() ; // Newline for "]" |
| print("]") ; |
| } else { // Compact |
| print(" ]") ; |
| } |
| out.setAbsoluteIndent(indent0) ; |
| } |
| |
| // Write a list |
| private void writeList(List<Node> elts) { |
| if ( elts.size() == 0 ) { |
| out.print("()") ; |
| return ; |
| } |
| |
| if ( false ) { |
| out.print("(") ; |
| for ( Node n : elts ) { |
| out.print(" ") ; |
| writeNodePretty(n) ; |
| } |
| out.print(" )") ; |
| } |
| |
| if ( true ) { |
| // "fresh line mode" means printed one on new line |
| // Multi line items are ones that can be multiple lines. Non-literals. |
| // Was the previous row a multiLine? |
| boolean lastItemFreshLine = false ; |
| // Have there been any items that causes "fresh line" mode? |
| boolean multiLineAny = false ; |
| boolean first = true ; |
| |
| // Where we started. |
| int originalIndent = out.getAbsoluteIndent() ; |
| // Rebase indent here. |
| int x = out.getCol() ; |
| out.setAbsoluteIndent(x); |
| |
| out.print("(") ; |
| out.incIndent(2); |
| for ( Node n : elts ) { |
| |
| // Print this item on a fresh line? (still to check: first line) |
| boolean thisItemFreshLine = /* multiLineAny | */ n.isBlank() ; |
| |
| // Special case List in List. |
| // Start on this line if last item was on this line. |
| if ( lists.containsKey(n) ) |
| thisItemFreshLine = lastItemFreshLine ; |
| |
| // Starting point. |
| if ( ! first ) { |
| if ( lastItemFreshLine | thisItemFreshLine ) |
| out.println() ; |
| else |
| out.print(" ") ; |
| } |
| |
| first = false ; |
| //Literals with newlines: int x1 = out.getRow() ; |
| // Adds INDENT_OBJECT even for a [ one triple ] |
| // Special case [ one triple ]?? |
| writeNodePretty(n) ; |
| //Literals with newlines:int x2 = out.getRow() ; |
| //Literals with newlines: boolean multiLineAnyway = ( x1 != x2 ) ; |
| lastItemFreshLine = thisItemFreshLine ; |
| multiLineAny = multiLineAny | thisItemFreshLine ; |
| |
| } |
| if ( multiLineAny ) |
| out.println() ; |
| else |
| out.print(" ") ; |
| out.decIndent(2); |
| out.setAbsoluteIndent(x); |
| out.print(")") ; |
| out.setAbsoluteIndent(originalIndent) ; |
| } |
| } |
| |
| private boolean isPrettyNode(Node n) { |
| // Maybe ought to be the same test as writePredicateObjectList |
| // Order matters? - one connected objects may include list elements. |
| if ( allowDeepPretty ) { |
| if ( lists.containsKey(n) ) |
| return true ; |
| if ( nestedObjects.contains(n) ) |
| return true ; |
| } |
| if ( RDF_Nil.equals(n) ) |
| return true ; |
| return false ; |
| } |
| |
| // --> write S or O?? |
| private void writeNodePretty(Node obj) { |
| // Assumes "isPrettyNode" is true. |
| // Order matters? - one connected objects may include list elements. |
| if ( lists.containsKey(obj) ) |
| writeList(lists.get(obj)) ; |
| else if ( nestedObjects.contains(obj) ) |
| writeNestedObject(obj) ; |
| else if ( RDF_Nil.equals(obj) ) |
| out.print("()") ; |
| else |
| writeNode(obj) ; |
| if ( nestedObjects.contains(obj) ) |
| nestedObjectsWritten.add(obj) ; |
| |
| } |
| |
| // Order of properties. |
| // rdf:type ("a") |
| // RDF and RDFS |
| // Other. |
| // Sorted by URI. |
| |
| private void write_S_P_Gap() { |
| if ( out.getCol() > LONG_SUBJECT ) |
| out.println() ; |
| else |
| gap(GAP_S_P) ; |
| } |
| } |
| |
| // Order of properties. |
| // rdf:type ("a") |
| // RDF and RDFS |
| // Other. |
| // Sor0ted by URI. |
| |
| private static int classification(Node p) { |
| if ( p.equals(RDF_type) ) |
| return 0 ; |
| if ( p.getURI().startsWith(RDF.getURI()) || p.getURI().startsWith(RDFS.getURI()) ) |
| return 1 ; |
| return 2 ; |
| } |
| |
| private static Comparator<Node> compPredicates = (t1,t2) -> { |
| int class1 = classification(t1) ; |
| int class2 = classification(t2) ; |
| if ( class1 != class2 ) |
| return Integer.compare(class1, class2) ; |
| String p1 = t1.getURI() ; |
| String p2 = t2.getURI() ; |
| return p1.compareTo(p2) ; |
| }; |
| |
| protected final void writeNode(Node node) { |
| nodeFmt.format(out, node) ; |
| } |
| |
| private void print(String x) { |
| out.print(x) ; |
| } |
| |
| private void gap(int gap) { |
| out.print(' ', gap) ; |
| } |
| |
| // flush aggressively (debugging) |
| private void println() { |
| out.println() ; |
| // out.flush() ; |
| } |
| } |