| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional 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.n3; |
| |
| //import org.apache.commons.logging.*; |
| import java.util.* ; |
| |
| import org.apache.jena.rdf.model.* ; |
| import org.apache.jena.shared.JenaException ; |
| import org.apache.jena.util.iterator.* ; |
| import org.apache.jena.vocabulary.RDF ; |
| import org.apache.jena.vocabulary.RDFS ; |
| |
| /** An N3 pretty printer. |
| * Tries to make N3 data look readable - works better on regular data. |
| */ |
| |
| |
| |
| public class N3JenaWriterPP extends N3JenaWriterCommon |
| /*implements RDFWriter*/ |
| { |
| // This N3 writer proceeds in 2 stages. First, it analysises the model to be |
| // written to extract information that is going to be specially formatted |
| // (RDF lists, small anon nodes) and to calculate the prefixes that will be used. |
| |
| final protected boolean doObjectListsAsLists = getBooleanValue("objectLists", true) ; |
| |
| // Data structures used in controlling the formatting |
| |
| protected Set<Resource> rdfLists = null ; // Heads of lists |
| protected Set<Resource> rdfListsAll = null ; // Any resources in a collection |
| protected Set<Resource> rdfListsDone = null ; // RDF lists written |
| protected Set<RDFNode> oneRefObjects = null ; // Bnodes referred to once as an object - can inline |
| protected Set<Resource> oneRefDone = null ; // Things done - so we can check for missed items |
| |
| // Do we do nested (one reference) nodes? |
| protected boolean allowDeep = true ; |
| |
| static final protected String objectListSep = " , " ; |
| |
| // ---------------------------------------------------- |
| // Prepatation stage |
| |
| @Override |
| protected void prepare(Model model) |
| { |
| prepareLists(model) ; |
| prepareOneRefBNodes(model) ; |
| } |
| |
| // Find well-formed RDF lists - does not find empty lists (this is intentional) |
| // Works by finding all tails, and work backwards to the head. |
| // RDF lists may, or may not, have a type element. |
| // Should do this during preparation, not as objects found during the write |
| // phase. |
| |
| protected void prepareLists(Model model) |
| { |
| Set<Resource> thisListAll = new HashSet<>(); |
| |
| StmtIterator listTailsIter = model.listStatements(null, RDF.rest, RDF.nil); |
| |
| // For every tail of a list |
| //tailLoop: |
| for ( ; listTailsIter.hasNext() ; ) |
| { |
| // The resource for the current element being considered. |
| Resource listElement = listTailsIter.nextStatement().getSubject() ; |
| // The resource pointing to the link we have just looked at. |
| Resource validListHead = null ; |
| |
| // Chase to head of list |
| for ( ; ; ) |
| { |
| boolean isOK = checkListElement(listElement) ; |
| if ( ! isOK ) |
| break ; |
| |
| // At this point the element is exactly a collection element. |
| if ( N3JenaWriter.DEBUG ) out.println("# RDF list all: "+formatResource(listElement)) ; |
| validListHead = listElement ; |
| thisListAll.add(listElement) ; |
| |
| // Find the previous node. |
| StmtIterator sPrev = model.listStatements(null, RDF.rest, listElement) ; |
| |
| if ( ! sPrev.hasNext() ) |
| // No rdf:rest link |
| break ; |
| |
| // Valid pretty-able list. Might be longer. |
| listElement = sPrev.nextStatement().getSubject() ; |
| if ( sPrev.hasNext() ) |
| { |
| if ( N3JenaWriter.DEBUG ) out.println("# RDF shared tail from "+formatResource(listElement)) ; |
| break ; |
| } |
| } |
| // At head of a pretty-able list - add its elements and its head. |
| if ( N3JenaWriter.DEBUG ) out.println("# Collection list head: "+formatResource(validListHead)) ; |
| rdfListsAll.addAll(thisListAll) ; |
| if ( validListHead != null ) |
| rdfLists.add(validListHead) ; |
| } |
| listTailsIter.close() ; |
| } |
| |
| // Validate one list element. |
| protected boolean checkListElement(Resource listElement) |
| { |
| // Must be a blank node for abbreviated form. |
| if ( ! listElement.isAnon() ) |
| return false ; |
| |
| if (!listElement.hasProperty(RDF.rest) |
| || !listElement.hasProperty(RDF.first)) |
| { |
| if (N3JenaWriter.DEBUG) |
| out.println( |
| "# RDF list element does not have required properties: " |
| + formatResource(listElement)); |
| return false; |
| } |
| |
| // Must be exactly two properties (the ones we just tested for) |
| // or three including the RDF.type RDF.List statement. |
| int numProp = countProperties(listElement); |
| |
| if ( numProp == 2) |
| // Must have exactly the properties we just tested for. |
| return true ; |
| |
| // rdf:type is not implicit. |
| // if (numProp == 3) |
| // { |
| // if (listElement.hasProperty(RDF.type, RDF.List)) |
| // return true; |
| // if (N3JenaWriter.DEBUG) |
| // out.println( |
| // "# RDF list element: 3 properties but no rdf:type rdf:List" |
| // + formatResource(listElement)); |
| // return false; |
| // } |
| |
| if (N3JenaWriter.DEBUG) |
| out.println( |
| "# RDF list element does not right number of properties: " |
| + formatResource(listElement)); |
| return false; |
| } |
| |
| // Find bnodes that are objects of only one statement (and hence can be inlined) |
| // which are not RDF lists. |
| // Could do this testing at write time (unlike lists) |
| |
| protected void prepareOneRefBNodes(Model model) |
| { |
| |
| NodeIterator objIter = model.listObjects() ; |
| for ( ; objIter.hasNext() ; ) |
| { |
| RDFNode n = objIter.nextNode() ; |
| |
| if ( testOneRefBNode(n) ) |
| oneRefObjects.add(n) ; |
| objIter.close() ; |
| |
| // N3JenaWriter.DEBUG |
| if ( N3JenaWriter.DEBUG ) |
| { |
| out.println("# RDF Lists = "+rdfLists.size()) ; |
| out.println("# RDF ListsAll = "+rdfListsAll.size()) ; |
| out.println("# oneRefObjects = "+oneRefObjects.size()) ; |
| } |
| } |
| } |
| |
| protected boolean testOneRefBNode(RDFNode n) |
| { |
| if ( ! ( n instanceof Resource ) ) |
| return false ; |
| |
| Resource obj = (Resource)n ; |
| |
| if ( ! obj.isAnon() ) |
| return false ; |
| |
| // In a list - done as list, not as embedded bNode. |
| if ( rdfListsAll.contains(obj) ) |
| // RDF list (head or element) |
| return false ; |
| |
| StmtIterator pointsToIter = obj.getModel().listStatements(null, null, obj) ; |
| if ( ! pointsToIter.hasNext() ) |
| // Corrupt graph! |
| throw new JenaException("N3: found object with no arcs!") ; |
| |
| pointsToIter.nextStatement() ; |
| |
| if ( pointsToIter.hasNext() ) |
| return false ; |
| |
| if ( N3JenaWriter.DEBUG ) |
| out.println("# OneRef: "+formatResource(obj)) ; |
| return true ; |
| } |
| |
| // ---------------------------------------------------- |
| // Output stage |
| |
| // Property order is: |
| // 1 - rdf:type (as "a") |
| // 2 - other rdf: rdfs: namespace items (sorted) |
| // 3 - all other properties, sorted by URI (not qname) |
| |
| |
| |
| @Override |
| protected ClosableIterator<Property> preparePropertiesForSubject(Resource r) |
| { |
| Set<Property> seen = new HashSet<>() ; |
| boolean hasTypes = false ; |
| SortedMap<String, Property> tmp1 = new TreeMap<>() ; |
| SortedMap<String, Property> tmp2 = new TreeMap<>() ; |
| |
| StmtIterator sIter = r.listProperties(); |
| for ( ; sIter.hasNext() ; ) |
| { |
| Property p = sIter.nextStatement().getPredicate() ; |
| if ( seen.contains(p) ) |
| continue ; |
| seen.add(p) ; |
| |
| if ( p.equals(RDF.type) ) |
| { |
| hasTypes = true ; |
| continue ; |
| } |
| |
| if ( p.getURI().startsWith(RDF.getURI()) || |
| p.getURI().startsWith(RDFS.getURI()) ) |
| { |
| tmp1.put(p.getURI(), p) ; |
| continue ; |
| } |
| |
| tmp2.put(p.getURI(), p) ; |
| } |
| sIter.close() ; |
| |
| ExtendedIterator<Property> eIter = null ; |
| |
| if ( hasTypes ) |
| eIter = new SingletonIterator<>(RDF.type) ; |
| |
| ExtendedIterator<Property> eIter2 = WrappedIterator.create(tmp1.values().iterator()) ; |
| |
| eIter = (eIter == null) ? eIter2 : eIter.andThen(eIter2) ; |
| |
| eIter2 = WrappedIterator.create(tmp2.values().iterator()) ; |
| |
| eIter = (eIter == null) ? eIter2 : eIter.andThen(eIter2) ; |
| return eIter ; |
| } |
| |
| @Override |
| protected boolean skipThisSubject(Resource subj) |
| { |
| return rdfListsAll.contains(subj) || |
| oneRefObjects.contains(subj) ; |
| } |
| |
| // protected void writeModel(Model model) |
| // { |
| // super.writeModel(model) ; |
| // |
| // |
| |
| // Before ... |
| |
| @Override |
| protected void startWriting() |
| { |
| allocateDatastructures() ; |
| } |
| |
| // Flush any unwritten objects. |
| // 1 - OneRef objects |
| // Normally there are "one ref" objects left |
| // However loops of "one ref" are possible. |
| // 2 - Lists |
| |
| @Override |
| protected void finishWriting() |
| { |
| |
| // Are there any unattached RDF lists? |
| // e..g ([] [] []) . in the data. |
| // We missed these earlier. |
| for ( Resource r : rdfLists ) |
| { |
| if ( rdfListsDone.contains( r ) ) |
| { |
| continue; |
| } |
| out.println(); |
| if ( N3JenaWriter.DEBUG ) |
| { |
| out.println( "# RDF List" ); |
| } |
| |
| // Includes the case of shared lists-as-objects. |
| // if (!r.isAnon() || countArcsTo(r) > 0 ) |
| // { |
| // // Name it. |
| // out.print(formatResource(r)); |
| // out.print(" :- "); |
| // } |
| // writeList(r); |
| // out.println(" ."); |
| // Turtle does not have :- |
| writeListUnpretty( r ); |
| } |
| |
| // Finally, panic. |
| // Dump anything that has not been output yet. |
| oneRefObjects.removeAll(oneRefDone); |
| for ( RDFNode oneRefObject : oneRefObjects ) |
| { |
| out.println(); |
| if ( N3JenaWriter.DEBUG ) |
| { |
| out.println( "# One ref" ); |
| } |
| // Don't allow further one ref objects to be inlined. |
| allowDeep = false; |
| writeOneGraphNode( (Resource) oneRefObject ); |
| allowDeep = true; |
| } |
| |
| //out.println() ; |
| //writeModelSimple(model, bNodesMap, base) ; |
| out.flush(); |
| clearDatastructures() ; |
| } |
| |
| |
| |
| // Need to decide between one line or many. |
| // Very hard to do a pretty thing here because the objects may be large or small or a mix. |
| |
| @Override |
| protected void writeObjectList(Resource subject, Property property) |
| { |
| // if ( ! doObjectListsAsLists ) |
| // { |
| // super.writeObjectList(resource, property) ; |
| // return ; |
| // } |
| |
| String propStr = formatProperty(property); |
| |
| // Find which objects are simple (i.e. not nested structures) |
| |
| StmtIterator sIter = subject.listProperties(property); |
| Set<RDFNode> simple = new HashSet<>() ; |
| Set<RDFNode> complex = new HashSet<>() ; |
| |
| for (; sIter.hasNext();) |
| { |
| Statement stmt = sIter.nextStatement(); |
| RDFNode obj = stmt.getObject() ; |
| if ( isSimpleObject(obj) ) |
| simple.add(obj) ; |
| else |
| complex.add(obj) ; |
| } |
| sIter.close() ; |
| // Write property/simple objects |
| |
| if ( simple.size() > 0 ) |
| { |
| String padSp = null ; |
| // Simple objects - allow property to be long and alignment to be lost |
| if ((propStr.length()+minGap) <= widePropertyLen) |
| padSp = pad(calcPropertyPadding(propStr)) ; |
| |
| if ( doObjectListsAsLists ) |
| { |
| // Write all simple objects as one list. |
| out.print(propStr); |
| out.incIndent(indentObject) ; |
| |
| if ( padSp != null ) |
| out.print(padSp) ; |
| else |
| out.println() ; |
| |
| for (Iterator<RDFNode> iter = simple.iterator(); iter.hasNext();) |
| { |
| RDFNode n = iter.next(); |
| writeObject(n); |
| |
| // As an object list |
| if (iter.hasNext()) |
| out.print(objectListSep); |
| } |
| |
| out.decIndent(indentObject) ; |
| } |
| else |
| { |
| for (Iterator<RDFNode> iter = simple.iterator(); iter.hasNext();) |
| { |
| // This is also the same as the complex case |
| // except the width the property can go in is different. |
| out.print(propStr); |
| out.incIndent(indentObject) ; |
| if ( padSp != null ) |
| out.print(padSp) ; |
| else |
| out.println() ; |
| |
| RDFNode n = iter.next(); |
| writeObject(n); |
| out.decIndent(indentObject) ; |
| |
| // As an object list |
| if (iter.hasNext()) |
| out.println(" ;"); |
| } |
| |
| } |
| } |
| // Now do complex objects. |
| // Write property each time for a complex object. |
| // Do not allow over long properties but same line objects. |
| |
| if (complex.size() > 0) |
| { |
| // Finish the simple list if there was one |
| if ( simple.size() > 0 ) |
| out.println(" ;"); |
| |
| int padding = -1 ; |
| String padSp = null ; |
| |
| // Can we fit teh start of teh complex object on this line? |
| |
| // Complex objects - do not allow property to be long and alignment to be lost |
| if ((propStr.length()+minGap) <= propertyCol) |
| { |
| padding = calcPropertyPadding(propStr) ; |
| padSp = pad(padding) ; |
| } |
| |
| for (Iterator<RDFNode> iter = complex.iterator(); iter.hasNext();) |
| { |
| int thisIndent = indentObject ; |
| //if ( i ) |
| out.incIndent(thisIndent); |
| out.print(propStr); |
| if ( padSp != null ) |
| out.print(padSp) ; |
| else |
| out.println() ; |
| |
| RDFNode n = iter.next(); |
| writeObject(n); |
| out.decIndent(thisIndent); |
| if ( iter.hasNext() ) |
| out.println(" ;"); |
| } |
| } |
| return; |
| } |
| |
| |
| protected boolean isSimpleObject(RDFNode node) |
| { |
| if (node instanceof Literal) |
| return true ; |
| Resource rObj = (Resource) node; |
| if ( allowDeep && oneRefObjects.contains(rObj) ) |
| return false ; |
| return true ; |
| } |
| |
| @Override |
| protected void writeObject(RDFNode node) |
| { |
| if (node instanceof Literal) |
| { |
| writeLiteral((Literal) node); |
| return; |
| } |
| |
| Resource rObj = (Resource) node; |
| if ( allowDeep && ! isSimpleObject(rObj)) |
| { |
| oneRefDone.add(rObj); |
| ClosableIterator<Property> iter = preparePropertiesForSubject(rObj); |
| if (! iter.hasNext() ) |
| { |
| // No properties. |
| out.print("[]"); |
| } |
| else |
| { |
| out.print("[ "); |
| out.incIndent(2); |
| writePropertiesForSubject(rObj, iter); |
| out.decIndent(2); |
| out.println() ; |
| // Line up [] |
| out.print("]"); |
| } |
| iter.close(); |
| return ; |
| } |
| |
| if (rdfLists.contains(rObj)) |
| if (countArcsTo(rObj) <= 1) |
| { |
| writeList(rObj); |
| return; |
| } |
| |
| out.print(formatResource(rObj)); |
| } |
| |
| |
| |
| // Need to out.print in short (all on one line) and long forms (multiple lines) |
| protected void writeList(Resource resource) |
| { |
| out.print( "("); |
| out.incIndent(2) ; |
| boolean listFirst = true; |
| for (Iterator<RDFNode> iter = rdfListIterator(resource); iter.hasNext();) |
| { |
| if (!listFirst) |
| out.print( " "); |
| listFirst = false; |
| RDFNode n = iter.next(); |
| writeObject(n) ; |
| } |
| out.print( ")"); |
| out.decIndent(2) ; |
| rdfListsDone.add(resource); |
| |
| } |
| |
| // Need to out.print in long (triples) form. |
| protected void writeListUnpretty(Resource r) |
| { |
| //for ( ; ! r.equals(RDF.nil); ) |
| { |
| // Write statements at this node. |
| StmtIterator sIter = r.getModel().listStatements(r, null, (RDFNode)null) ; |
| for ( ; sIter.hasNext() ; ) |
| { |
| Statement s = sIter.next() ; |
| writeStatement(s) ; |
| } |
| |
| // Look for rdf:rest. |
| sIter = r.getModel().listStatements(r, RDF.rest, (RDFNode)null) ; |
| for ( ; sIter.hasNext() ; ) |
| { |
| Statement s = sIter.next() ; |
| RDFNode nextNode = s.getObject() ; |
| if ( nextNode instanceof Resource ) |
| { |
| Resource r2 = (Resource)nextNode ; |
| writeListUnpretty(r2) ; |
| } |
| else |
| writeStatement(s) ; |
| } |
| } |
| } |
| |
| private void writeStatement(Statement s) |
| { |
| out.print(formatResource(s.getSubject())); |
| out.print(" ") ; |
| |
| out.print(formatResource(s.getPredicate())) ; |
| out.print(" ") ; |
| |
| out.print(formatNode(s.getObject())) ; |
| out.println(" .") ; |
| |
| } |
| |
| // Called before each writing run. |
| protected void allocateDatastructures() |
| { |
| rdfLists = new HashSet<>() ; |
| rdfListsAll = new HashSet<>() ; |
| rdfListsDone = new HashSet<>() ; |
| oneRefObjects = new HashSet<>() ; |
| oneRefDone = new HashSet<>() ; |
| } |
| |
| // Especially release large intermediate memory objects |
| protected void clearDatastructures() |
| { |
| rdfLists = null ; |
| rdfListsAll = null ; |
| rdfListsDone = null ; |
| oneRefObjects = null ; |
| oneRefDone = null ; |
| } |
| } |