/* * 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 ; } }