/* * 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.fuseki.servlets; import static java.lang.String.format ; import static org.apache.jena.fuseki.servlets.ServletBase.errorBadRequest ; import static org.apache.jena.fuseki.servlets.ServletBase.errorOccurred ; import static org.apache.jena.fuseki.servlets.ServletBase.log ; import java.io.IOException ; import java.util.HashMap ; import java.util.Map ; import java.util.Objects; import javax.servlet.ServletOutputStream ; import javax.servlet.http.HttpServletRequest ; import javax.servlet.http.HttpServletResponse ; import org.apache.commons.lang.StringUtils ; import org.apache.jena.atlas.web.AcceptList ; import org.apache.jena.atlas.web.MediaType ; import org.apache.jena.fuseki.DEF ; import org.apache.jena.fuseki.FusekiException ; import org.apache.jena.fuseki.conneg.ConNeg ; import org.apache.jena.query.QueryCancelledException ; import org.apache.jena.query.ResultSet ; import org.apache.jena.query.ResultSetFormatter ; import org.apache.jena.riot.ResultSetMgr ; import org.apache.jena.riot.WebContent ; import org.apache.jena.riot.resultset.ResultSetLang ; import org.apache.jena.sparql.core.Prologue ; import org.apache.jena.web.HttpSC ; import org.slf4j.Logger ; import org.slf4j.LoggerFactory ; /** This is the content negotiation for each kind of SPARQL query result */ public class ResponseResultSet { private static Logger xlog = LoggerFactory.getLogger(ResponseResultSet.class) ; private static Logger slog = ServletBase.log ; // Short names for "output=" private static final String contentOutputJSON = "json" ; private static final String contentOutputXML = "xml" ; private static final String contentOutputSPARQL = "sparql" ; private static final String contentOutputText = "text" ; private static final String contentOutputCSV = "csv" ; private static final String contentOutputTSV = "tsv" ; private static final String contentOutputThrift = "thrift" ; public static Map<String,String> shortNamesResultSet = new HashMap<String, String>() ; static { // Some short names. keys are lowercase. ResponseOps.put(shortNamesResultSet, contentOutputJSON, WebContent.contentTypeResultsJSON) ; ResponseOps.put(shortNamesResultSet, contentOutputSPARQL, WebContent.contentTypeResultsXML) ; ResponseOps.put(shortNamesResultSet, contentOutputXML, WebContent.contentTypeResultsXML) ; ResponseOps.put(shortNamesResultSet, contentOutputText, WebContent.contentTypeTextPlain) ; ResponseOps.put(shortNamesResultSet, contentOutputCSV, WebContent.contentTypeTextCSV) ; ResponseOps.put(shortNamesResultSet, contentOutputTSV, WebContent.contentTypeTextTSV) ; ResponseOps.put(shortNamesResultSet, contentOutputThrift, WebContent.contentTypeResultsThrift) ; } interface OutputContent { void output(ServletOutputStream out) ; } public static void doResponseResultSet(HttpAction action, Boolean booleanResult) { doResponseResultSet$(action, null, booleanResult, null, DEF.rsOfferTable) ; } public static void doResponseResultSet(HttpAction action, ResultSet resultSet, Prologue qPrologue) { doResponseResultSet$(action, resultSet, null, qPrologue, DEF.rsOfferTable) ; } // If we refactor the conneg into a single function, we can split boolean and result set handling. // One or the other argument must be null private static void doResponseResultSet$(HttpAction action, ResultSet resultSet, Boolean booleanResult, Prologue qPrologue, AcceptList contentTypeOffer) { HttpServletRequest request = action.request ; HttpServletResponse response = action.response ; long id = action.id ; if ( resultSet == null && booleanResult == null ) { xlog.warn("doResponseResult: Both result set and boolean result are null") ; throw new FusekiException("Both result set and boolean result are null") ; } if ( resultSet != null && booleanResult != null ) { xlog.warn("doResponseResult: Both result set and boolean result are set") ; throw new FusekiException("Both result set and boolean result are set") ; } String mimeType = null ; MediaType i = ConNeg.chooseContentType(request, contentTypeOffer, DEF.acceptRSXML) ; if ( i != null ) mimeType = i.getContentType() ; // Override content type // Does &output= override? // Requested output type by the web form or &output= in the request. String outputField = ResponseOps.paramOutput(request, shortNamesResultSet) ; // Expands short names if ( outputField != null ) mimeType = outputField ; String serializationType = mimeType ; // Choose the serializer based on this. String contentType = mimeType ; // Set the HTTP respose header to this. // Stylesheet - change to application/xml. final String stylesheetURL = ResponseOps.paramStylesheet(request) ; if ( stylesheetURL != null && Objects.equals(serializationType,WebContent.contentTypeResultsXML) ) contentType = WebContent.contentTypeXML ; // Force to text/plain? String forceAccept = ResponseOps.paramForceAccept(request) ; if ( forceAccept != null ) contentType = WebContent.contentTypeTextPlain ; // Better : dispatch on MediaType // Fuseki2 uses the SPARQL parser/write registry. if ( Objects.equals(serializationType, WebContent.contentTypeResultsXML) ) sparqlXMLOutput(action, contentType, resultSet, stylesheetURL, booleanResult) ; else if ( Objects.equals(serializationType, WebContent.contentTypeResultsJSON) ) jsonOutput(action, contentType, resultSet, booleanResult) ; else if ( Objects.equals(serializationType, WebContent.contentTypeTextPlain) ) textOutput(action, contentType, resultSet, qPrologue, booleanResult) ; else if ( Objects.equals(serializationType, WebContent.contentTypeTextCSV) ) csvOutput(action, contentType, resultSet, booleanResult) ; else if (Objects.equals(serializationType, WebContent.contentTypeTextTSV) ) tsvOutput(action, contentType, resultSet, booleanResult) ; else if (Objects.equals(serializationType, WebContent.contentTypeResultsThrift) ) thriftOutput(action, contentType, resultSet, booleanResult) ; else errorBadRequest("Can't determine output serialization: "+serializationType) ; } public static void setHttpResponse(HttpServletRequest httpRequest, HttpServletResponse httpResponse, String contentType, String charset) { // ---- Set up HTTP Response // Stop caching (not that ?queryString URLs are cached anyway) if ( true ) { httpResponse.setHeader("Cache-Control", "no-cache") ; httpResponse.setHeader("Pragma", "no-cache") ; } // See: http://www.w3.org/International/O-HTTP-charset.html if ( contentType != null ) { if ( charset != null && ! isXML(contentType) ) contentType = contentType+"; charset="+charset ; log.trace("Content-Type for response: "+contentType) ; httpResponse.setContentType(contentType) ; } } private static boolean isXML(String contentType) { return contentType.equals(WebContent.contentTypeRDFXML) || contentType.equals(WebContent.contentTypeResultsXML) || contentType.equals(WebContent.contentTypeXML) ; } private static void sparqlXMLOutput(HttpAction action, String contentType, final ResultSet resultSet, final String stylesheetURL, final Boolean booleanResult) { OutputContent proc = new OutputContent(){ @Override public void output(ServletOutputStream out) { if ( resultSet != null ) ResultSetFormatter.outputAsXML(out, resultSet, stylesheetURL) ; if ( booleanResult != null ) ResultSetFormatter.outputAsXML(out, booleanResult, stylesheetURL) ; }} ; output(action, contentType, null, proc) ; } private static void jsonOutput(HttpAction action, String contentType, final ResultSet resultSet, final Boolean booleanResult) { OutputContent proc = new OutputContent(){ @Override public void output(ServletOutputStream out) { if ( resultSet != null ) ResultSetFormatter.outputAsJSON(out, resultSet) ; if ( booleanResult != null ) ResultSetFormatter.outputAsJSON(out, booleanResult ) ; } } ; try { String callback = ResponseOps.paramCallback(action.request) ; ServletOutputStream out = action.response.getOutputStream() ; if ( callback != null ) { callback = StringUtils.replaceChars(callback, "\r", "") ; callback = StringUtils.replaceChars(callback, "\n", "") ; out.print(callback) ; out.println("(") ; } output(action, contentType, WebContent.charsetUTF8, proc) ; if ( callback != null ) out.println(")") ; } catch (IOException ex) { errorOccurred(ex) ; } } private static void textOutput(HttpAction action, String contentType, final ResultSet resultSet, final Prologue qPrologue, final Boolean booleanResult) { // Text is not streaming. OutputContent proc = new OutputContent(){ @Override public void output(ServletOutputStream out) { if ( resultSet != null ) ResultSetFormatter.out(out, resultSet, qPrologue) ; if ( booleanResult != null ) ResultSetFormatter.out(out, booleanResult ) ; } }; output(action, contentType, WebContent.charsetUTF8, proc) ; } private static void csvOutput(HttpAction action, String contentType, final ResultSet resultSet, final Boolean booleanResult) { OutputContent proc = new OutputContent(){ @Override public void output(ServletOutputStream out) { if ( resultSet != null ) ResultSetFormatter.outputAsCSV(out, resultSet) ; if ( booleanResult != null ) ResultSetFormatter.outputAsCSV(out, booleanResult ) ; } } ; output(action, contentType, WebContent.charsetUTF8, proc) ; } private static void tsvOutput(HttpAction action, String contentType, final ResultSet resultSet, final Boolean booleanResult) { OutputContent proc = new OutputContent(){ @Override public void output(ServletOutputStream out) { if ( resultSet != null ) ResultSetFormatter.outputAsTSV(out, resultSet) ; if ( booleanResult != null ) ResultSetFormatter.outputAsTSV(out, booleanResult ) ; } } ; output(action, contentType, WebContent.charsetUTF8, proc) ; } private static void thriftOutput(HttpAction action, String contentType, final ResultSet resultSet, final Boolean booleanResult) { OutputContent proc = new OutputContent(){ @Override public void output(ServletOutputStream out) { if ( resultSet != null ) ResultSetMgr.write(out, resultSet, ResultSetLang.SPARQLResultSetThrift) ; if ( booleanResult != null ) slog.error("Can't write boolen result in thrift") ; } } ; output(action, contentType, WebContent.charsetUTF8, proc) ; } private static void output(HttpAction action, String contentType, String charset, OutputContent proc) { try { setHttpResponse(action.request, action.response, contentType, charset) ; action.response.setStatus(HttpSC.OK_200) ; ServletOutputStream out = action.response.getOutputStream() ; try { proc.output(out) ; out.flush() ; } catch (QueryCancelledException ex) { // Bother. Status code 200 already sent. slog.info(format("[%d] Query Cancelled - results truncated (but 200 already sent)", action.id)) ; out.println() ; out.println("## Query cancelled due to timeout during execution ##") ; out.println("## **** Incomplete results **** ##") ; out.flush() ; // No point raising an exception - 200 was sent already. //errorOccurred(ex) ; } // Includes client gone. } catch (IOException ex) { errorOccurred(ex) ; } // Do not call httpResponse.flushBuffer(); here - Jetty closes the stream if it is a gzip stream // then the JSON callback closing details can't be added. } }