/*
* 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 java.util.ArrayList ;
import java.util.Iterator ;
import java.util.List ;
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 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
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 ;
}
}
private void flush(BasicPattern bgp) {
formatTriples(bgp) ;
bgp.getList().clear();
}
@Override
public void visit(ElementDataset el)
{
// if ( el.getDataset() != null)
// {
// DatasetGraph dsNamed = el.getDataset() ;
// out.print("DATASET ") ;
// out.incIndent(INDENT) ;
// Iterator iter = dsNamed.listNames() ;
// if ( iter.hasNext() )
// {
// boolean first = false ;
// for ( ; iter.hasNext() ; )
// {
// if ( ! first )
// out.newline() ;
// out.print("FROM <") ;
// String s = (String)iter.next() ;
// out.print(s) ;
// 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(ElementData el) {
QuerySerializer.outputDataBlock(out, el.getVars(), el.getRows(), context) ;
}
@Override
public void visit(ElementUnion el) {
if ( el.getElements().size() == 1 ) {
// If this is an element of just one, just do it inplace
// Can't happen from a parsed query.
// Now can :-)
// SPARQL 1.1 inline UNION.
// Same as OPTIONAL, MINUS
out.print("UNION") ;
out.incIndent(INDENT) ;
out.newline() ;
visitAsGroup(el.getElements().get(0)) ;
out.decIndent(INDENT) ;
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("}") ;
}
}
int subjectWidth = -1 ;
int predicateWidth = -1 ;
@Override
protected void formatTriples(BasicPattern triples) {
if ( !PRETTY_PRINT ) {
super.formatTriples(triples) ;
return ;
}
// TODO RDF Collections - spot the parsers pattern
if ( triples.isEmpty() )
return ;
setWidths(triples) ;
if ( subjectWidth > TRIPLES_SUBJECT_COLUMN )
subjectWidth = TRIPLES_SUBJECT_COLUMN ;
if ( predicateWidth > TRIPLES_PROPERTY_COLUMN )
predicateWidth = TRIPLES_PROPERTY_COLUMN ;
// Loops:
List<Triple> subjAcc = new ArrayList<>() ; // Accumulate all triples
// with the same subject.
Node subj = null ; // Subject being accumulated
boolean first = true ; // Print newlines between blocks.
int indent = -1 ;
for ( Triple t : triples ) {
if ( subj != null && t.getSubject().equals(subj) ) {
subjAcc.add(t) ;
continue ;
}
if ( subj != null ) {
if ( !first )
out.println(" .") ;
formatSameSubject(subj, subjAcc) ;
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) ;
}
}
private void formatSameSubject(Node subject, List<Triple> triples) {
if ( triples == null || triples.size() == 0 )
return ;
// Do the first triple.
Iterator<Triple> iter = triples.iterator() ;
Triple t1 = iter.next() ;
// int indent = TRIPLES_SUBJECT_COLUMN+TRIPLES_COLUMN_GAP ;
// // Long subject => same line. Works for single triple as well.
// int s1_len = printSubject(t1.getSubject()) ;
// //int x = out.getCol() ;
int indent = subjectWidth + TRIPLES_COLUMN_GAP ;
int s1_len = printSubject(t1.getSubject()) ;
if ( s1_len > TRIPLES_SUBJECT_LONG ) {
// Too long - start a new line.
out.incIndent(indent) ;
out.println() ;
} else {
printGap() ;
out.incIndent(indent) ;
}
// Remainder of first triple
printProperty(t1.getPredicate()) ;
printGap() ;
printObject(t1.getObject()) ;
// Do the rest
for ( ; iter.hasNext() ; ) {
Triple t = iter.next() ;
out.println(" ;") ;
printProperty(t.getPredicate()) ;
printGap() ;
printObject(t.getObject()) ;
continue ;
// print property list
}
// Finish off the block.
out.decIndent(indent) ;
// out.print(" .") ;
}
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 = slotToString(t.getPredicate()) ;
if ( p.length() > predicateWidth )
predicateWidth = p.length() ;
}
}
private void printGap() {
out.print(' ', TRIPLES_COLUMN_GAP) ;
}
// Indent must be set first.
private int printSubject(Node s) {
String str = slotToString(s) ;
out.print(str) ;
out.pad(subjectWidth) ;
return str.length() ;
}
// Assumes the indent is TRIPLES_SUBJECT_COLUMN+GAP
private static String RDFTYPE = FmtUtils.stringForNode(RDF.Nodes.type, new SerializationContext()) ;
private int printProperty(Node p) {
String str = slotToString(p) ;
if ( p.equals(RDF.Nodes.type) && str.equals(RDFTYPE) )
out.print("a") ;
else
out.print(str) ;
out.pad(predicateWidth) ;
return str.length() ;
}
private int printObject(Node obj) {
return printNoCol(obj) ;
}
private int printNoCol(Node node) {
String str = slotToString(node) ;
out.print(str) ;
return str.length() ;
}
}