blob: ac68e43f2f86e3f6fafb10f92bab4ed45c4a49e0 [file] [log] [blame]
/*
* 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();
}
}