/*
* Copyright 2008 Fedora Commons, Inc.
*
* Licensed 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.mulgara.protocol;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.URI;
import org.jrdf.graph.BlankNode;
import org.jrdf.graph.Literal;
import org.jrdf.graph.URIReference;
import org.mulgara.query.Answer;
import org.mulgara.query.BooleanAnswer;
import org.mulgara.query.TuplesException;
import org.mulgara.query.Variable;
/**
* Represents an Answer as JSON.
* The format is specified at: {@link http://www.w3.org/TR/rdf-sparql-json-res/}
*
* @created Sep 1, 2008
* @author Paula Gearon
* @copyright © 2008 <a href="http://www.fedora-commons.org/">Fedora Commons</a>
*/
public class StreamedSparqlJSONAnswer extends AbstractStreamedAnswer implements StreamedJSONAnswer {
/** Additional metadata about the results. */
URI additionalMetadata = null;
/** Boolean answer. */
boolean booleanResult = false;
/** Internal flag to indicate that a comma may be needed. */
boolean prependComma = false;
/**
* Creates a JSON Answer conforming to SPARQL JSON results.
* @param answer The Answer to wrap.
* @param output The stream to write to.
*/
public StreamedSparqlJSONAnswer(Answer answer, OutputStream output) {
super((answer instanceof BooleanAnswer) ? null : answer, output);
if (answer instanceof BooleanAnswer) booleanResult = ((BooleanAnswer)answer).getResult();
}
/**
* Creates an JSON Answer with additional metadata.
* @param answer The Answer to wrap.
* @param metadata Additional metadata for the answer.
* @param output The stream to write to.
*/
public StreamedSparqlJSONAnswer(Answer answer, URI metadata, OutputStream output) {
this(answer, output);
additionalMetadata = metadata;
}
/**
* Creates a JSON Answer conforming to SPARQL JSON results.
* @param result The boolean result to encode.
* @param output The stream to write to.
*/
public StreamedSparqlJSONAnswer(boolean result, OutputStream output) {
super((Answer)null, output);
booleanResult = result;
}
/**
* Creates a JSON Answer with additional metadata.
* @param result The boolean result to encode.
* @param metadata Additional metadata for the answer.
* @param output The stream to write to.
*/
public StreamedSparqlJSONAnswer(boolean result, URI metadata, OutputStream output) {
super((Answer)null, output);
booleanResult = result;
additionalMetadata = metadata;
}
/**
* Creates a JSON Answer conforming to SPARQL JSON results.
* @param answer The Answer to wrap.
* @param output The stream to write to.
* @param charsetName The name of the character set to use, if not the default of UTF-8.
*/
public StreamedSparqlJSONAnswer(Answer answer, OutputStream output, String charsetName) {
super((answer instanceof BooleanAnswer) ? null : answer, output, charsetName);
if (answer instanceof BooleanAnswer) booleanResult = ((BooleanAnswer)answer).getResult();
}
/**
* Creates an JSON Answer with additional metadata.
* @param answer The Answer to wrap.
* @param metadata Additional metadata for the answer.
* @param output The stream to write to.
* @param charsetName The name of the character set to use, if not the default of UTF-8.
*/
public StreamedSparqlJSONAnswer(Answer answer, URI metadata, OutputStream output, String charsetName) {
this(answer, output, charsetName);
additionalMetadata = metadata;
}
/**
* Creates a JSON Answer conforming to SPARQL JSON results.
* @param result The boolean result to encode.
* @param output The stream to write to.
* @param charsetName The name of the character set to use, if not the default of UTF-8.
*/
public StreamedSparqlJSONAnswer(boolean result, OutputStream output, String charsetName) {
super((Answer)null, output, charsetName);
booleanResult = result;
}
/**
* Creates a JSON Answer with additional metadata.
* @param result The boolean result to encode.
* @param metadata Additional metadata for the answer.
* @param output The stream to write to.
* @param charsetName The name of the character set to use, if not the default of UTF-8.
*/
public StreamedSparqlJSONAnswer(boolean result, URI metadata, OutputStream output, String charsetName) {
super((Answer)null, output, charsetName);
booleanResult = result;
additionalMetadata = metadata;
}
/** {@inheritDoc} */
public void addDocHeader() throws IOException {
s.append("{ ");
}
/** {@inheritDoc} */
public void addDocFooter() throws IOException {
s.append(" }");
}
/** {@inheritDoc} */
protected void addHeader(Answer answer) throws IOException {
s.append("\"head\": {");
boolean wroteVars = false;
if (answer != null && answer.getVariables() != null) {
s.append("\"vars\": [");
prependComma = false;
for (Variable v: answer.getVariables()) addHeaderVariable(v);
s.append("]");
wroteVars = true;
}
if (additionalMetadata != null) {
if (wroteVars) s.append(", ");
s.append("\"link\": [\"").append(additionalMetadata.toString()).append("\"]");
}
s.append("}");
prependComma = true;
}
/** {@inheritDoc} */
protected void addHeaderVariable(Variable var) throws IOException {
comma().append("\"").append(var.getName()).append("\"");
}
/**
* No Answer footer needed for this type of document
*/
protected void addFooter(Answer answer) throws IOException {
}
/** {@inheritDoc} */
protected void addResults(Answer answer) throws TuplesException, IOException {
if (answer != null) {
comma().append("\"results\": { ");
s.append("\"bindings\": [ ");
answer.beforeFirst();
prependComma = false;
while (answer.next()) addResult(answer);
s.append(" ] }");
} else {
comma().append("\"boolean\": ").append(Boolean.toString(booleanResult));
}
}
/** {@inheritDoc} */
protected void addResult(Answer answer) throws TuplesException, IOException {
comma().append("{ ");
prependComma = false;
for (int c = 0; c < width; c++) addBinding(vars[c], answer.getObject(c));
s.append(" }");
}
/**
* {@inheritDoc}
* No binding will be emitted if the value is null (unbound).
*/
protected void addBinding(Variable var, Object value) throws IOException {
if (value != null) {
comma().append("\"").append(var.getName()).append("\": { ");
// no dynamic dispatch, so use if/then
if (value instanceof URIReference) addURI((URIReference)value);
else if (value instanceof Literal) addLiteral((Literal)value);
else if (value instanceof BlankNode) addBNode((BlankNode)value);
else throw new IllegalArgumentException("Unable to create a SPARQL response with an answer containing: " + value.getClass().getSimpleName());
s.append(" }");
}
}
/** {@inheritDoc} */
protected void addURI(URIReference uri) throws IOException {
s.append("\"type\": \"uri\", \"value\": \"").append(uri.getURI().toString()).append("\"");
}
/** {@inheritDoc} */
protected void addBNode(BlankNode bnode) throws IOException {
s.append("\"type\": \"bnode\", \"value\": \"").append(bnode.toString()).append("\"");
}
/** {@inheritDoc} */
protected void addLiteral(Literal literal) throws IOException {
if (literal.getDatatype() != null) {
s.append("\"type\": \"typed-literal\", \"datatype\": \"").append(literal.getDatatype().toString()).append("\", ");
} else {
s.append("\"type\": \"literal\", ");
if (literal.getLanguage() != null) s.append("\"xml:lang\": \"").append(literal.getLanguage()).append("\", ");
}
s.append("\"value\": \"").append(jsonEscape(literal.getLexicalForm())).append("\"");
}
/**
* Escapes strings to be JSON compatible. JSON only expects 16 bit characters.
* @param in The string to be escaped.
* @return The escaped string.
*/
protected String jsonEscape(String in) {
StringBuffer out = new StringBuffer();
for (int i = 0; i < in.length(); i++) {
char c = in.charAt(i);
if (c == '/') out.append("\\/");
else if (c == '\\') out.append("\\\\");
else if (c == '"') out.append("\\\"");
else if (Character.isISOControl(c)) {
if (c == '\b') out.append("\\b");
else if (c == '\t') out.append("\\t");
else if (c == '\n') out.append("\\n");
else if (c == '\f') out.append("\\f");
else if (c == '\r') out.append("\\r");
else out.append("\\u").append(String.format("%04x", (int)c));
} else {
out.append(c);
}
}
return out.toString();
}
/**
* Adds a comma if needed at this point. Commas are usually needed.
* @throws IOException An error writing to the stream.
*/
protected OutputStreamWriter comma() throws IOException {
if (prependComma) s.append(", ");
prependComma = true;
return s;
}
public void addAnswer(Answer data) throws TuplesException, IOException {
addResults(data);
}
}