/******************************************************************************* * Copyright (c) 2008 Cambridge Semantics Incorporated. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Cambridge Semantics Incorporated *******************************************************************************/ package org.openanzo.client.cli; import java.io.IOException; import java.io.OutputStreamWriter; import java.io.PrintWriter; import java.util.Arrays; import org.apache.commons.lang.ObjectUtils; import org.apache.commons.lang.StringUtils; import org.openanzo.glitter.query.PatternSolution; import org.openanzo.glitter.query.SolutionSet; import org.openanzo.rdf.Constants; import org.openanzo.rdf.Literal; import org.openanzo.rdf.PlainLiteral; import org.openanzo.rdf.TypedLiteral; import org.openanzo.rdf.URI; import org.openanzo.rdf.Value; /** * Writes human readable sparql solution set as a textual table. The output is lossy--some RDF Values will be abbreviated. For non-abbreviated solutions please * use the SPARQL Results XML format (srx). * * Core features: * * <ul> * <li>Use CURIE prefixes where possible.</li> * <li>Attribute Promotion: If all bindings in a column share a common datatype or language, put it in the column header, not each entry.</li> * <li>Dynamically limit column widths so the table width is something reasonable.</li> * <ul> * * @author Joe Betz <jpbetz@cambridgesemantics.com> * */ class SolutionTextWriter { private static class Table { final SolutionSet solutionSet; final CommandContext context; int columnCount; int rowCount; String[][] output; Column[] columns; public Table(CommandContext context, SolutionSet solutionSet) { this.context = context; this.solutionSet = solutionSet; this.columnCount = solutionSet.getBindings().size(); this.rowCount = solutionSet.size(); this.output = new String[this.rowCount][this.columnCount]; this.columns = new Column[this.columnCount]; for (int j = 0; j < this.columnCount; j++) { this.columns[j] = new Column(); } initialize(); } public void initialize() { promoteCommonAttributes(); arrangeHeaders(); arrangeTable(); constrainWidth(); } private void promoteCommonAttributes() { int j = 0; for (String name : this.solutionSet.getBindingNames()) { for (PatternSolution sol : this.solutionSet) { // for each entry in column, check if attribute is common // if this becomes false, quit checking for column Value binding = sol.getBinding(name); if (!this.columns[j].hasCommonAttributes(binding)) { break; } } j++; } } private void arrangeHeaders() { int j = 0; for (String name : this.solutionSet.getBindingNames()) { this.columns[j].arrangeHeader(name); j++; } } private void arrangeTable() { int i = 0; for (PatternSolution sol : this.solutionSet) { int j = 0; for (String name : this.solutionSet.getBindingNames()) { Value binding = sol.getBinding(name); String val; if (binding == null) { val = ""; } else { val = this.columns[j].arrangeEntry(binding); } this.columns[j].width = Math.max(this.columns[j].width, val.length()); this.output[i][j] = val; j++; } i++; } } private void constrainWidth() { if (this.columns.length == 0) return; int[] ordered = new int[this.columns.length]; long totalWidth = 0; for (int i = 0; i < this.columnCount; i++) { totalWidth += this.columns[i].width; ordered[i] = this.columns[i].width; } if (totalWidth < 180) return; Arrays.sort(ordered); // find a reasonable column width // it should be greater than the median column length, unless that is too large int roughMedian = ordered[(int) Math.ceil(ordered.length / 2)]; // this isn't really the mathematical median, but it's close enough int maxColumnLength = Math.min(roughMedian, 50); // widen out the max column length if there is room while (maxColumnLength * this.columns.length < 160) { maxColumnLength++; } for (int j = 0; j < this.columnCount; j++) { if (this.columns[j].width < maxColumnLength) continue; this.columns[j].width = maxColumnLength; for (int i = 0; i < this.rowCount; i++) { String val = this.output[i][j]; // abbreviate strings to keep table width under control Value binding = this.solutionSet.get(i).getBinding(this.solutionSet.getBindingNames().get(j)); if (binding instanceof URI) { if (val.length() > maxColumnLength) { val = StringUtils.abbreviate(val, val.length(), maxColumnLength); } } else { if (val.length() > maxColumnLength) { val = StringUtils.abbreviate(val, maxColumnLength); } } this.output[i][j] = val; } } } public void write(PrintWriter out) { out.println(); for (int j = 0; j < this.columnCount; j++) { out.format("%-" + (this.columns[j].width + 2) + "s", this.columns[j].header); } out.println(); for (int j = 0; j < this.columnCount; j++) { out.print(StringUtils.rightPad("", this.columns[j].width, "-")); out.print(" "); } out.println(); for (int i = 0; i < this.output.length; i++) { for (int j = 0; j < this.columnCount; j++) { out.format("%-" + (this.columns[j].width + 2) + "s", this.output[i][j]); } out.println(); } out.println(); } public class Column { int width; URI promotedDatatype; String promotedLang; String header; boolean hasCommonDatatype = true; boolean hasCommonLang = true; public void arrangeHeader(String name) { this.header = "?" + name; if (this.promotedDatatype != null) { this.header += (" (" + Table.this.context.applyPrefixes(this.promotedDatatype) + ")"); } else if (this.promotedLang != null) { this.header += (" (@" + this.promotedLang + ")"); } this.width = this.header.length(); } public boolean hasCommonAttributes(Value binding) { if (binding instanceof TypedLiteral && this.promotedLang == null) { TypedLiteral lit = (TypedLiteral) binding; if (!hasCommonDatatype(lit)) return false; } else if (binding instanceof PlainLiteral && this.promotedDatatype == null) { PlainLiteral lit = (PlainLiteral) binding; if (!hasCommonLang(lit)) return false; } return true; } public boolean hasCommonDatatype(TypedLiteral lit) { if (this.hasCommonDatatype == true) { if (this.promotedDatatype == null) { this.promotedDatatype = lit.getDatatypeURI(); } else if (!this.promotedDatatype.equals(lit.getDatatypeURI())) { this.promotedDatatype = null; this.hasCommonDatatype = false; } } return this.hasCommonDatatype; } public boolean hasCommonLang(PlainLiteral lit) { if (this.hasCommonLang == true) { if (this.promotedLang == null) { this.promotedLang = lit.getLanguage(); } else if (!ObjectUtils.equals(this.promotedLang, (lit.getLanguage()))) { this.promotedLang = null; this.hasCommonLang = false; } } return this.hasCommonLang; } public String arrangeEntry(Value binding) { String val = Table.this.context.applyPrefixes(binding); if (binding instanceof Literal) { Literal lit = (Literal) binding; if (lit instanceof TypedLiteral) { if (this.promotedDatatype != null) { val = lit.getLabel(); } } else { if (this.promotedLang != null) { val = lit.getLabel(); } } } return val; } } } public static void write(CommandContext context, SolutionSet solutionSet) throws IOException { Table table = new Table(context, solutionSet); PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out,Constants.byteEncoding)); table.write(out); out.flush(); } }