| /* |
| * 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.sparql.serializer; |
| |
| import static org.apache.jena.graph.Node.ANY; |
| import static org.apache.jena.sparql.serializer.FmtEltLib.count; |
| import static org.apache.jena.sparql.serializer.FmtEltLib.createTriplesListBlock; |
| import static org.apache.jena.sparql.serializer.FmtEltLib.rdfFirst; |
| |
| import java.util.*; |
| |
| import org.apache.jena.atlas.io.IndentedLineBuffer; |
| import org.apache.jena.atlas.io.IndentedWriter; |
| import org.apache.jena.graph.Node; |
| import org.apache.jena.graph.Triple; |
| import org.apache.jena.query.Query; |
| import org.apache.jena.query.QueryVisitor; |
| import org.apache.jena.query.Syntax; |
| import org.apache.jena.sparql.core.BasicPattern; |
| import org.apache.jena.sparql.core.PathBlock; |
| import org.apache.jena.sparql.core.TriplePath; |
| import org.apache.jena.sparql.expr.Expr; |
| import org.apache.jena.sparql.path.PathWriter; |
| import org.apache.jena.sparql.syntax.*; |
| import org.apache.jena.sparql.util.FmtUtils; |
| import org.apache.jena.vocabulary.RDF; |
| |
| public class FormatterElement extends FormatterBase implements ElementVisitor { |
| public static final int INDENT = 2; |
| |
| /** Control whether to show triple pattern boundaries - creates extra nesting */ |
| public static final boolean PATTERN_MARKERS = false; |
| |
| // /** Control whether triple patterns always have a final dot - it can be dropped in some cases */ |
| // public static boolean PATTERN_FINAL_DOT = false ; |
| |
| /** Control whether (non-triple) patterns have a final dot - it can be dropped */ |
| public static final boolean GROUP_SEP_DOT = false; |
| |
| /** Control whether the first item of a group is on the same line as the { */ |
| public static final boolean GROUP_FIRST_ON_SAME_LINE = true; |
| |
| /** Control pretty printing */ |
| public static final boolean PRETTY_PRINT = true; |
| |
| /** Control pretty printing of RDF lists */ |
| public static final boolean FMT_LISTS = true; |
| |
| /** Control pretty printing of free standing RDF lists */ |
| // Do *not* set "true" - argument of property paths can be lists and these spane |
| // PathBlocks and Basic Graph Patterns and this is not handled prop. |
| public static final boolean FMT_FREE_STANDING_LISTS = false; |
| |
| /** |
| * Control whether disjunction has set of delimiters - as it's a group usually, these |
| * aren't needed |
| */ |
| public static final boolean UNION_MARKERS = false; |
| |
| /** Control whether GRAPH indents in a fixed way or based on the layout size */ |
| public static final boolean GRAPH_FIXED_INDENT = true; |
| |
| /** |
| * Control whether NOT EXIST/EXISTS indents in a fixed way or based on the layout size |
| */ |
| public static final boolean ELEMENT1_FIXED_INDENT = true; |
| |
| /** Control triples pretty printing */ |
| public static final int TRIPLES_SUBJECT_COLUMN = 8; |
| |
| // Less than this => rest of triple on the same line |
| // Could be smart and make it depend on the property length as well. Later. |
| public static final int TRIPLES_SUBJECT_LONG = 12; |
| |
| public static final int TRIPLES_PROPERTY_COLUMN = 20; |
| |
| public static final int TRIPLES_COLUMN_GAP = 2; |
| |
| public FormatterElement(IndentedWriter out, SerializationContext context) { |
| super(out, context); |
| } |
| |
| public static void format(IndentedWriter out, SerializationContext cxt, Element el) { |
| FormatterElement fmt = new FormatterElement(out, cxt); |
| fmt.startVisit(); |
| el.visit(fmt); |
| fmt.finishVisit(); |
| } |
| |
| public static String asString(Element el) { |
| SerializationContext cxt = new SerializationContext(); |
| IndentedLineBuffer b = new IndentedLineBuffer(); |
| FormatterElement.format(b, cxt, el); |
| return b.toString(); |
| } |
| |
| public boolean topMustBeGroup() { |
| return false; |
| } |
| |
| @Override |
| public void visit(ElementTriplesBlock el) { |
| if ( el.isEmpty() ) { |
| out.println("# Empty BGP"); |
| return; |
| } |
| formatTriples(el.getPattern()); |
| } |
| |
| @Override |
| public void visit(ElementPathBlock el) { |
| // Write path block - don't put in a final trailing "." |
| if ( el.isEmpty() ) { |
| out.println("# Empty BGP"); |
| return; |
| } |
| |
| // Split into BGP-path-BGP-... |
| // where the BGPs may be empty. |
| PathBlock pBlk = el.getPattern(); |
| BasicPattern bgp = new BasicPattern(); |
| boolean first = true; // Has anything been output? |
| for ( TriplePath tp : pBlk ) { |
| if ( tp.isTriple() ) { |
| bgp.add(tp.asTriple()); |
| continue; |
| } |
| |
| if ( !bgp.isEmpty() ) { |
| if ( !first ) |
| out.println(" ."); |
| flush(bgp); |
| first = false; |
| } |
| if ( !first ) |
| out.println(" ."); |
| // Path (no RDF list output). |
| printSubject(tp.getSubject()); |
| out.print(" "); |
| PathWriter.write(out, tp.getPath(), context.getPrologue()); |
| out.print(" "); |
| printObject(tp.getObject()); |
| first = false; |
| } |
| // Flush any stored triple patterns. |
| if ( !bgp.isEmpty() ) { |
| if ( !first ) |
| out.println(" ."); |
| flush(bgp); |
| first = false; |
| } |
| } |
| |
| @Override |
| public void visit(ElementDataset el) { |
| // Not implemented. |
| // if ( el.getDataset() != null ) { |
| // DatasetGraph dsNamed = el.getDataset(); |
| // out.print("DATASET "); |
| // out.incIndent(INDENT); |
| // Iterator<Node> iter = dsNamed.listGraphNodes(); |
| // if ( iter.hasNext() ) { |
| // boolean first = false; |
| // for ( ; iter.hasNext() ; ) { |
| // if ( !first ) |
| // out.newline(); |
| // out.print("FROM <"); |
| // Node n = iter.next(); |
| // out.print(slotToString(n)); |
| // out.print(">"); |
| // } |
| // } |
| // out.decIndent(INDENT); |
| // out.newline(); |
| // } |
| // if ( el.getElement() != null ) |
| // visitAsGroup(el.getElement()); |
| } |
| |
| @Override |
| public void visit(ElementFilter el) { |
| out.print("FILTER "); |
| Expr expr = el.getExpr(); |
| FmtExprSPARQL v = new FmtExprSPARQL(out, context); |
| |
| // This assumes that complex expressions are bracketted |
| // (parens) as necessary except for some cases: |
| // Plain variable or constant |
| |
| boolean addParens = false; |
| if ( expr.isVariable() ) |
| addParens = true; |
| if ( expr.isConstant() ) |
| addParens = true; |
| |
| if ( addParens ) |
| out.print("( "); |
| v.format(expr); |
| if ( addParens ) |
| out.print(" )"); |
| } |
| |
| @Override |
| public void visit(ElementAssign el) { |
| out.print("LET ("); |
| out.print("?" + el.getVar().getVarName()); |
| out.print(" := "); |
| FmtExprSPARQL v = new FmtExprSPARQL(out, context); |
| v.format(el.getExpr()); |
| out.print(")"); |
| } |
| |
| @Override |
| public void visit(ElementBind el) { |
| out.print("BIND("); |
| FmtExprSPARQL v = new FmtExprSPARQL(out, context); |
| v.format(el.getExpr()); |
| out.print(" AS "); |
| out.print("?" + el.getVar().getVarName()); |
| out.print(")"); |
| } |
| |
| @Override |
| public void visit(ElementFind el) { |
| out.print("FIND("); |
| out.print("<< "); |
| formatTriple(el.getTriple()); |
| out.print(" >>"); |
| out.print(" AS "); |
| out.print("?" + el.getVar().getVarName()); |
| out.print(")"); |
| } |
| |
| @Override |
| public void visit(ElementData el) { |
| QuerySerializer.outputDataBlock(out, el.getVars(), el.getRows(), context); |
| } |
| |
| @Override |
| public void visit(ElementUnion el) { |
| if ( el.getElements().size() == 0 ) { |
| // If this is a union of zero elements, do nothing. |
| return ; |
| } |
| |
| if ( el.getElements().size() == 0 ) { |
| // If this is a union of one elements, put in a {}-group |
| visitAsGroup(el.getElements().get(0)); |
| return; |
| } |
| |
| if ( UNION_MARKERS ) { |
| out.print("{"); |
| out.newline(); |
| out.pad(); |
| } |
| |
| out.incIndent(INDENT); |
| |
| boolean first = true; |
| for ( Element subElement : el.getElements() ) { |
| if ( !first ) { |
| out.decIndent(INDENT); |
| out.newline(); |
| out.print("UNION"); |
| out.newline(); |
| out.incIndent(INDENT); |
| } |
| visitAsGroup(subElement); |
| first = false; |
| } |
| |
| out.decIndent(INDENT); |
| |
| if ( UNION_MARKERS ) { |
| out.newline(); |
| out.print("}"); |
| } |
| } |
| |
| @Override |
| public void visit(ElementGroup el) { |
| out.print("{"); |
| int initialRowNumber = out.getRow(); |
| out.incIndent(INDENT); |
| if ( !GROUP_FIRST_ON_SAME_LINE ) |
| out.newline(); |
| |
| int row1 = out.getRow(); |
| out.pad(); |
| |
| boolean first = true; |
| Element lastElt = null; |
| |
| for ( Element subElement : el.getElements() ) { |
| // Some adjacent elements need a DOT: |
| // ElementTriplesBlock, ElementPathBlock |
| if ( !first ) { |
| // Need to move on after the last thing printed. |
| // Check for necessary DOT as separator |
| if ( GROUP_SEP_DOT || needsDotSeparator(lastElt, subElement) ) |
| out.print(" . "); |
| out.newline(); |
| } |
| subElement.visit(this); |
| first = false; |
| lastElt = subElement; |
| } |
| out.decIndent(INDENT); |
| |
| // Where to put the closing "}" |
| int row2 = out.getRow(); |
| if ( row1 != row2 ) |
| out.newline(); |
| |
| // Finally, close the group. |
| if ( out.getRow() == initialRowNumber ) |
| out.print(" "); |
| out.print("}"); |
| } |
| |
| private static boolean needsDotSeparator(Element el1, Element el2) { |
| return needsDotSeparator(el1) && needsDotSeparator(el2); |
| } |
| |
| private static boolean needsDotSeparator(Element el) { |
| return (el instanceof ElementTriplesBlock) || (el instanceof ElementPathBlock); |
| } |
| |
| @Override |
| public void visit(ElementOptional el) { |
| out.print("OPTIONAL"); |
| out.incIndent(INDENT); |
| out.newline(); |
| visitAsGroup(el.getOptionalElement()); |
| out.decIndent(INDENT); |
| } |
| |
| @Override |
| public void visit(ElementNamedGraph el) { |
| visitNodePattern("GRAPH", el.getGraphNameNode(), el.getElement()); |
| } |
| |
| @Override |
| public void visit(ElementService el) { |
| String x = "SERVICE"; |
| if ( el.getSilent() ) |
| x = "SERVICE SILENT"; |
| visitNodePattern(x, el.getServiceNode(), el.getElement()); |
| } |
| |
| private void visitNodePattern(String label, Node node, Element subElement) { |
| int len = label.length(); |
| out.print(label); |
| out.print(" "); |
| String nodeStr = (node == null) ? "*" : slotToString(node); |
| out.print(nodeStr); |
| len += nodeStr.length(); |
| if ( GRAPH_FIXED_INDENT ) { |
| out.incIndent(INDENT); |
| out.newline(); // NB and newline |
| } else { |
| out.print(" "); |
| len++; |
| out.incIndent(len); |
| } |
| visitAsGroup(subElement); |
| |
| if ( GRAPH_FIXED_INDENT ) |
| out.decIndent(INDENT); |
| else |
| out.decIndent(len); |
| } |
| |
| private void visitElement1(String label, Element1 el) { |
| |
| int len = label.length(); |
| out.print(label); |
| len += label.length(); |
| if ( ELEMENT1_FIXED_INDENT ) { |
| out.incIndent(INDENT); |
| out.newline(); // NB and newline |
| } else { |
| out.print(" "); |
| len++; |
| out.incIndent(len); |
| } |
| visitAsGroup(el.getElement()); |
| if ( ELEMENT1_FIXED_INDENT ) |
| out.decIndent(INDENT); |
| else |
| out.decIndent(len); |
| } |
| |
| @Override |
| public void visit(ElementExists el) { |
| visitElement1("EXISTS", el); |
| } |
| |
| @Override |
| public void visit(ElementNotExists el) { |
| visitElement1("NOT EXISTS", el); |
| } |
| |
| @Override |
| public void visit(ElementMinus el) { |
| out.print("MINUS"); |
| out.incIndent(INDENT); |
| out.newline(); |
| visitAsGroup(el.getMinusElement()); |
| out.decIndent(INDENT); |
| } |
| |
| @Override |
| public void visit(ElementSubQuery el) { |
| out.print("{ "); |
| out.incIndent(INDENT); |
| Query q = el.getQuery(); |
| |
| // Serialize with respect to the existing context |
| QuerySerializerFactory factory = SerializerRegistry.get().getQuerySerializerFactory(Syntax.syntaxARQ); |
| QueryVisitor serializer = factory.create(Syntax.syntaxARQ, context, out); |
| q.visit(serializer); |
| |
| out.decIndent(INDENT); |
| out.print("}"); |
| } |
| |
| public void visitAsGroup(Element el) { |
| boolean needBraces = !((el instanceof ElementGroup) || (el instanceof ElementSubQuery)); |
| |
| if ( needBraces ) { |
| out.print("{ "); |
| out.incIndent(INDENT); |
| } |
| |
| el.visit(this); |
| |
| if ( needBraces ) { |
| out.decIndent(INDENT); |
| out.print("}"); |
| } |
| } |
| |
| // -------- Formatting a basic graph pattern |
| // Triple order is preserved. |
| |
| int subjectWidth = -1; |
| int predicateWidth = -1; |
| |
| @Override |
| protected void formatTriples(BasicPattern triples) { |
| if ( !PRETTY_PRINT ) { |
| super.formatTriples(triples); |
| return; |
| } |
| |
| if ( triples.isEmpty() ) |
| return; |
| |
| // Lists in this BasicPattern. |
| // TriplesListBlock is a record of lists in this BGP. |
| // Formatting is off for list if there is an empty TriplesListBlock. |
| // This is cautionsly spotting the triples from RDF lists as generated by the |
| // parser. |
| |
| TriplesListBlock block = FMT_LISTS |
| ? createTriplesListBlock(triples) |
| : new TriplesListBlock(); |
| |
| Set<Node> freeStanding = new HashSet<>(); |
| for ( Node head : block.listElementsMap.keySet() ) { |
| // Check for suitablity to print. |
| // See also FmtEltLib#collectList |
| // |
| // Subject-list : inCount = 0, outCount = 3 |
| // Object-list : inCount = 1, outCount = 2 |
| // Free-standing list : inCount = 0, outCount = 2 |
| // Free-standing list is handled as a special case. |
| int inCount = count(triples.getList(), ANY, ANY, head); |
| int outCount = count(triples.getList(), head, ANY, ANY); |
| if ( inCount == 0 && outCount == 2 ) |
| // Free standing. |
| freeStanding.add(head); |
| } |
| |
| setWidths(triples); |
| if ( subjectWidth > TRIPLES_SUBJECT_COLUMN ) |
| subjectWidth = TRIPLES_SUBJECT_COLUMN; |
| if ( predicateWidth > TRIPLES_PROPERTY_COLUMN ) |
| predicateWidth = TRIPLES_PROPERTY_COLUMN; |
| |
| // Accumulate all triples with the same subject. |
| List<Triple> subjAcc = new ArrayList<>(); |
| // Subject being accumulated |
| Node subj = null; |
| // Print newlines between blocks. |
| boolean first = true; |
| |
| int indent = -1; |
| for ( Triple t : triples ) { |
| if ( block.triplesInLists.contains(t) ) { |
| if ( rdfFirst.equals(t.getPredicate()) ) { |
| if ( freeStanding.contains(t.getSubject()) ) |
| printNodeOrList(t.getSubject(), block.listElementsMap); |
| } |
| continue; |
| } |
| |
| if ( subj != null && t.getSubject().equals(subj) ) { |
| subjAcc.add(t); |
| continue; |
| } |
| |
| if ( subj != null ) { |
| if ( !first ) |
| out.println(" ."); |
| formatSameSubject(subj, subjAcc, block.listElementsMap); |
| first = false; |
| // At end of line of a block of triples with same subject. |
| // Drop through and start new block of same subject triples. |
| } |
| |
| // New subject |
| subj = t.getSubject(); |
| subjAcc.clear(); |
| subjAcc.add(t); |
| } |
| |
| // Flush accumulator |
| if ( subj != null && subjAcc.size() != 0 ) { |
| if ( !first ) |
| out.println(" ."); |
| first = false; |
| formatSameSubject(subj, subjAcc, block.listElementsMap); |
| } |
| } |
| |
| // ---- |
| |
| private void flush(BasicPattern bgp) { |
| formatTriples(bgp); |
| bgp.getList().clear(); |
| } |
| |
| private void formatSameSubject(Node subject, List<Triple> triples, Map<Node, List<Node>> lists) { |
| |
| if ( triples == null || triples.size() == 0 ) |
| return; |
| |
| // Do the first triple. |
| Iterator<Triple> iter = triples.iterator(); |
| Triple t1 = iter.next(); |
| |
| int indent = subjectWidth + TRIPLES_COLUMN_GAP; |
| |
| int s1_len = printNodeOrList(subject, lists); |
| if ( s1_len > TRIPLES_SUBJECT_LONG ) { |
| out.incIndent(indent); |
| out.println(); |
| } else { |
| printGap(); |
| out.incIndent(indent); |
| } |
| |
| // Remainder of first triple |
| printProperty(t1.getPredicate()); |
| printGap(); |
| |
| printNodeOrList(t1.getObject(), lists); |
| |
| // Do the rest |
| for ( ; iter.hasNext() ; ) { |
| Triple t = iter.next(); |
| out.println(" ;"); |
| printProperty(t.getPredicate()); |
| printGap(); |
| printNodeOrList(t.getObject(), lists); |
| continue; |
| } |
| |
| // Finish off the block. |
| out.decIndent(indent); |
| // out.print(" .") ; |
| } |
| |
| private int printNodeOrList(Node node, Map<Node, List<Node>> lists) { |
| if ( lists.containsKey(node) ) |
| return printList(lists.get(node), lists); |
| else |
| return printNoCol(node); |
| } |
| |
| private void setWidths(BasicPattern triples) { |
| subjectWidth = -1; |
| predicateWidth = -1; |
| |
| for ( Triple t : triples ) { |
| String s = slotToString(t.getSubject()); |
| if ( s.length() > subjectWidth ) |
| subjectWidth = s.length(); |
| |
| String p = slotToStringProperty(t.getPredicate()); |
| if ( p.length() > predicateWidth ) |
| predicateWidth = p.length(); |
| } |
| } |
| |
| private void printGap() { |
| out.print(' ', TRIPLES_COLUMN_GAP); |
| } |
| |
| // printSubject, printObject - used in ElementPathBlock. |
| private int printSubject(Node s) { |
| return printNoCol(s); |
| } |
| |
| // Unadorned "<http://www.w3.org/1999/02/22-rdf-syntax-ns#type>" |
| private static String RDFTYPE = FmtUtils.stringForNode(RDF.Nodes.type, new SerializationContext()); |
| |
| private int printProperty(Node p) { |
| String str = slotToStringProperty(p); |
| out.print(str); |
| out.pad(predicateWidth); |
| return out.getCol(); |
| } |
| |
| // String for property - includes the rule for whether to use "a" or not. |
| private String slotToStringProperty(Node p) { |
| String str = slotToString(p); |
| // If rdf:type but no rdf: then str is full URI and we use the "a" form |
| if ( str.equals(RDFTYPE) ) |
| return "a"; |
| return str; |
| } |
| |
| private int printObject(Node obj) { |
| return printNoCol(obj); |
| } |
| |
| private int printList(List<Node> list, Map<Node, List<Node>> lists) { |
| if ( list.isEmpty() ) { |
| out.print("()"); |
| return 2; |
| } |
| |
| int col0 = out.getCol(); |
| out.print("( "); |
| for ( Node n : list ) { |
| printNodeOrList(n, lists); |
| out.print(" "); |
| } |
| out.print(")"); |
| int col1 = out.getCol(); |
| return col1 - col0; |
| } |
| |
| private int printNoCol(Node node) { |
| String str = slotToString(node); |
| out.print(str); |
| return str.length(); |
| } |
| } |