http://git-wip-us.apache.org/repos/asf/jena/blob/36855e1b/jena-fuseki/src/main/java/org/apache/jena/fuseki/servlets/REST_Quads.java ---------------------------------------------------------------------- diff --git a/jena-fuseki/src/main/java/org/apache/jena/fuseki/servlets/REST_Quads.java b/jena-fuseki/src/main/java/org/apache/jena/fuseki/servlets/REST_Quads.java new file mode 100644 index 0000000..06c38b7 --- /dev/null +++ b/jena-fuseki/src/main/java/org/apache/jena/fuseki/servlets/REST_Quads.java @@ -0,0 +1,211 @@ +/** + * 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 java.io.IOException ; + +import javax.servlet.ServletOutputStream ; + +import org.apache.jena.atlas.web.MediaType ; +import org.apache.jena.atlas.web.TypedOutputStream ; +import org.apache.jena.fuseki.Fuseki ; +import org.apache.jena.fuseki.HttpNames ; +import org.apache.jena.riot.Lang ; +import org.apache.jena.riot.RDFDataMgr ; +import org.apache.jena.riot.RDFLanguages ; +import org.apache.jena.riot.ReaderRIOT ; +import org.apache.jena.riot.system.StreamRDF ; +import org.apache.jena.riot.system.StreamRDFLib ; + +import com.hp.hpl.jena.graph.Graph ; +import com.hp.hpl.jena.graph.Node ; +import com.hp.hpl.jena.graph.NodeFactory ; +import com.hp.hpl.jena.sparql.core.DatasetGraph ; + +/** + * Servlet that serves up quads for a dataset. + */ + +public class REST_Quads extends SPARQL_REST +{ + public REST_Quads() { super(); } + + @Override + protected void validate(HttpAction action) + { + // already checked? + } + + @Override + protected void doGet(HttpAction action) + { + MediaType mediaType = HttpAction.contentNegotationQuads(action) ; + ServletOutputStream output ; + try { output = action.response.getOutputStream() ; } + catch (IOException ex) { errorOccurred(ex) ; output = null ; } + + TypedOutputStream out = new TypedOutputStream(output, mediaType) ; + Lang lang = RDFLanguages.contentTypeToLang(mediaType.getContentType()) ; + if ( lang == null ) + lang = RDFLanguages.TRIG ; + + if ( action.verbose ) + log.info(format("[%d] Get: Content-Type=%s, Charset=%s => %s", + action.id, mediaType.getContentType(), mediaType.getCharset(), lang.getName())) ; + if ( ! RDFLanguages.isQuads(lang) ) + errorBadRequest("Not a quads format: "+mediaType) ; + + action.beginRead() ; + try { + DatasetGraph dsg = action.getActiveDSG() ; + RDFDataMgr.write(out, dsg, lang) ; + success(action) ; + } finally { action.endRead() ; } + } + + @Override + protected void doOptions(HttpAction action) + { + action.response.setHeader(HttpNames.hAllow, "GET, HEAD, OPTIONS") ; + action.response.setHeader(HttpNames.hContentLengh, "0") ; + success(action) ; + } + + @Override + protected void doHead(HttpAction action) + { + action.beginRead() ; + try { + MediaType mediaType = HttpAction.contentNegotationQuads(action) ; + success(action) ; + } finally { action.endRead() ; } + } + + static int counter = 0 ; + @Override + protected void doPost(HttpAction action) + { + if ( ! action.getDatasetRef().allowDatasetUpdate ) + errorMethodNotAllowed("POST") ; + + // Graph Store Protocol mode - POST triples to dataset causes + // a new graph to be created and the new URI returned via Location. + // Normally off. + // When off, POST of triples goes to default graph. + boolean gspMode = Fuseki.graphStoreProtocolPostCreate ; + + // Code to pass the GSP test suite. + // Not necessarily good code. + String x = action.request.getContentType() ; + if ( x == null ) + errorBadRequest("Content-type required for data format") ; + + MediaType mediaType = MediaType.create(x) ; + Lang lang = RDFLanguages.contentTypeToLang(mediaType.getContentType()) ; + if ( lang == null ) + lang = RDFLanguages.TRIG ; + + if ( action.verbose ) + log.info(format("[%d] Post: Content-Type=%s, Charset=%s => %s", + action.id, mediaType.getContentType(), mediaType.getCharset(), lang.getName())) ; + + if ( RDFLanguages.isQuads(lang) ) + doPostQuads(action, lang) ; + else if ( gspMode && RDFLanguages.isTriples(lang) ) + doPostTriplesGSP(action, lang) ; + else if ( RDFLanguages.isTriples(lang) ) + doPostTriples(action, lang) ; + else + errorBadRequest("Not a triples or quads format: "+mediaType) ; + } + + protected void doPostQuads(HttpAction action, Lang lang) + { + action.beginWrite() ; + try { + String name = action.request.getRequestURL().toString() ; + DatasetGraph dsg = action.getActiveDSG() ; + StreamRDF dest = StreamRDFLib.dataset(dsg) ; + ReaderRIOT reader = RDFDataMgr.createReader(lang) ; + reader.read(action.request.getInputStream(), name, null, dest, null); + action.commit(); + success(action) ; + } catch (IOException ex) { action.abort() ; } + finally { action.endWrite() ; } + } + + + // POST triples to dataset -- send to default graph. + protected void doPostTriples(HttpAction action, Lang lang) + { + action.beginWrite() ; + try { + DatasetGraph dsg = action.getActiveDSG() ; + // This should not be anythign other than the datasets name via this route. + String name = action.request.getRequestURL().toString() ; + //log.info(format("[%d] ** Content-length: %d", action.id, action.request.getContentLength())) ; + Graph g = dsg.getDefaultGraph() ; + StreamRDF dest = StreamRDFLib.graph(g) ; + ReaderRIOT reader = RDFDataMgr.createReader(lang) ; + reader.read(action.request.getInputStream(), name, null, dest, null); + action.commit(); + success(action) ; + } catch (IOException ex) { action.abort() ; } + finally { action.endWrite() ; } + } + + protected void doPostTriplesGSP(HttpAction action, Lang lang) + { + action.beginWrite() ; + try { + DatasetGraph dsg = action.getActiveDSG() ; + //log.info(format("[%d] ** Content-length: %d", action.id, action.request.getContentLength())) ; + + String name = action.request.getRequestURL().toString() ; + if ( ! name.endsWith("/") ) + name = name+ "/" ; + name = name+(++counter) ; + Node gn = NodeFactory.createURI(name) ; + Graph g = dsg.getGraph(gn) ; + StreamRDF dest = StreamRDFLib.graph(g) ; + ReaderRIOT reader = RDFDataMgr.createReader(lang) ; + reader.read(action.request.getInputStream(), name, null, dest, null); + log.info(format("[%d] Location: %s", action.id, name)) ; + action.response.setHeader("Location", name) ; + action.commit(); + successCreated(action) ; + } catch (IOException ex) { action.abort() ; } + finally { action.endWrite() ; } + } + + @Override + protected void doDelete(HttpAction action) + { errorMethodNotAllowed("DELETE") ; } + + @Override + protected void doPut(HttpAction action) + { errorMethodNotAllowed("PUT") ; } + + @Override + protected void doPatch(HttpAction action) + { errorMethodNotAllowed("PATCH") ; } +} +
http://git-wip-us.apache.org/repos/asf/jena/blob/36855e1b/jena-fuseki/src/main/java/org/apache/jena/fuseki/servlets/ResponseCallback.java ---------------------------------------------------------------------- diff --git a/jena-fuseki/src/main/java/org/apache/jena/fuseki/servlets/ResponseCallback.java b/jena-fuseki/src/main/java/org/apache/jena/fuseki/servlets/ResponseCallback.java new file mode 100644 index 0000000..1a78627 --- /dev/null +++ b/jena-fuseki/src/main/java/org/apache/jena/fuseki/servlets/ResponseCallback.java @@ -0,0 +1,24 @@ +/* + * 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 ; + +public interface ResponseCallback +{ + public void callback(boolean successfulOperation) ; +} http://git-wip-us.apache.org/repos/asf/jena/blob/36855e1b/jena-fuseki/src/main/java/org/apache/jena/fuseki/servlets/ResponseModel.java ---------------------------------------------------------------------- diff --git a/jena-fuseki/src/main/java/org/apache/jena/fuseki/servlets/ResponseModel.java b/jena-fuseki/src/main/java/org/apache/jena/fuseki/servlets/ResponseModel.java new file mode 100644 index 0000000..f2172f0 --- /dev/null +++ b/jena-fuseki/src/main/java/org/apache/jena/fuseki/servlets/ResponseModel.java @@ -0,0 +1,143 @@ +/* + * 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 org.apache.jena.fuseki.servlets.ServletBase.error ; +import static org.apache.jena.fuseki.servlets.ServletBase.errorBadRequest ; +import static org.apache.jena.fuseki.servlets.ServletBase.errorOccurred ; + +import java.util.HashMap ; +import java.util.Map ; + +import javax.servlet.ServletOutputStream ; +import javax.servlet.http.HttpServletRequest ; +import javax.servlet.http.HttpServletResponse ; + +import org.apache.jena.atlas.web.MediaType ; +import org.apache.jena.fuseki.DEF ; +import org.apache.jena.fuseki.Fuseki ; +import org.apache.jena.fuseki.conneg.ConNeg ; +import org.apache.jena.fuseki.conneg.WebLib ; +import org.apache.jena.riot.Lang ; +import org.apache.jena.riot.RDFDataMgr ; +import org.apache.jena.riot.RDFLanguages ; +import org.apache.jena.riot.WebContent ; +import org.apache.jena.web.HttpSC ; +import org.slf4j.Logger ; + +import com.hp.hpl.jena.rdf.model.Model ; + +public class ResponseModel +{ + private static Logger slog = ServletBase.log ; + + // Short names for "output=" + private static final String contentOutputJSONLD = "json-ld" ; + private static final String contentOutputJSONRDF = "json-rdf" ; + private static final String contentOutputJSON = "json" ; + private static final String contentOutputXML = "xml" ; + private static final String contentOutputText = "text" ; + private static final String contentOutputTTL = "ttl" ; + private static final String contentOutputNT = "nt" ; + + public static Map<String,String> shortNamesModel = new HashMap<String, String>() ; + static { + + // Some short names. keys are lowercase. + ResponseOps.put(shortNamesModel, contentOutputJSONLD, WebContent.contentTypeJSONLD) ; + ResponseOps.put(shortNamesModel, contentOutputJSONRDF, WebContent.contentTypeRDFJSON) ; + ResponseOps.put(shortNamesModel, contentOutputJSON, WebContent.contentTypeJSONLD) ; + ResponseOps.put(shortNamesModel, contentOutputXML, WebContent.contentTypeRDFXML) ; + ResponseOps.put(shortNamesModel, contentOutputText, WebContent.contentTypeTurtle) ; + ResponseOps.put(shortNamesModel, contentOutputTTL, WebContent.contentTypeTurtle) ; + ResponseOps.put(shortNamesModel, contentOutputNT, WebContent.contentTypeNTriples) ; + } + + public static void doResponseModel(HttpAction action, Model model) + { + HttpServletRequest request = action.request ; + HttpServletResponse response = action.response ; + + String mimeType = null ; // Header request type + + // TODO Use MediaType throughout. + MediaType i = ConNeg.chooseContentType(request, DEF.rdfOffer, DEF.acceptRDFXML) ; + if ( i != null ) + mimeType = i.getContentType() ; + + String outputField = ResponseOps.paramOutput(request, shortNamesModel) ; + if ( outputField != null ) + mimeType = outputField ; + + String writerMimeType = mimeType ; + + if ( mimeType == null ) + { + Fuseki.requestLog.warn("Can't find MIME type for response") ; + String x = WebLib.getAccept(request) ; + String msg ; + if ( x == null ) + msg = "No Accept: header" ; + else + msg = "Accept: "+x+" : Not understood" ; + error(HttpSC.NOT_ACCEPTABLE_406, msg) ; + } + + String contentType = mimeType ; + String charset = WebContent.charsetUTF8 ; + + String forceAccept = ResponseOps.paramForceAccept(request) ; + if ( forceAccept != null ) + { + contentType = forceAccept ; + charset = WebContent.charsetUTF8 ; + } + + Lang lang = RDFLanguages.contentTypeToLang(contentType) ; + if ( lang == null ) + errorBadRequest("Can't determine output content type: "+contentType) ; + +// if ( rdfw instanceof RDFXMLWriterI ) +// rdfw.setProperty("showXmlDeclaration", "true") ; + + // // Write locally to check it's possible. + // // Time/space tradeoff. + // try { + // OutputStream out = new NullOutputStream() ; + // RDFDataMgr.write(out, model, lang) ; + // IO.flush(out) ; + // } catch (JenaException ex) + // { + // SPARQL_ServletBase.errorOccurred(ex) ; + // } + + try { + ResponseResultSet.setHttpResponse(request, response, contentType, charset) ; + response.setStatus(HttpSC.OK_200) ; + ServletOutputStream out = response.getOutputStream() ; + RDFDataMgr.write(out, model, lang) ; + out.flush() ; + } + catch (Exception ex) { + slog.info("Exception while writing the response model: "+ex.getMessage(), ex) ; + errorOccurred("Exception while writing the response model: "+ex.getMessage(), ex) ; + } + } +} + http://git-wip-us.apache.org/repos/asf/jena/blob/36855e1b/jena-fuseki/src/main/java/org/apache/jena/fuseki/servlets/ResponseOps.java ---------------------------------------------------------------------- diff --git a/jena-fuseki/src/main/java/org/apache/jena/fuseki/servlets/ResponseOps.java b/jena-fuseki/src/main/java/org/apache/jena/fuseki/servlets/ResponseOps.java new file mode 100644 index 0000000..62ad6d5 --- /dev/null +++ b/jena-fuseki/src/main/java/org/apache/jena/fuseki/servlets/ResponseOps.java @@ -0,0 +1,94 @@ +/* + * 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 java.io.IOException ; +import java.util.Locale ; +import java.util.Map ; + +import javax.servlet.http.HttpServletRequest ; + +import org.apache.jena.fuseki.HttpNames ; + +public class ResponseOps +{ + // Helpers + public static void put(Map<String, String> map, String key, String value) + { + map.put(key.toLowerCase(Locale.ROOT), value) ; + } + + public static boolean isEOFexception(IOException ioEx) + { + if ( ioEx.getClass().getName().equals("org.mortbay.jetty.EofException eofEx") ) + return true ; + if ( ioEx instanceof java.io.EOFException ) + return true ; + return false ; + } + + public static String paramForceAccept(HttpServletRequest request) + { + String x = fetchParam(request, HttpNames.paramForceAccept) ; + return x ; + } + + public static String paramStylesheet(HttpServletRequest request) + { return fetchParam(request, HttpNames.paramStyleSheet) ; } + + public static String paramOutput(HttpServletRequest request, Map<String,String> map) + { + // Two names. + String x = fetchParam(request, HttpNames.paramOutput1) ; + if ( x == null ) + x = fetchParam(request, HttpNames.paramOutput2) ; + return expandShortName(x, map) ; + } + + public static String expandShortName(String str, Map<String,String> map) + { + if ( str == null ) + return null ; + // Force keys to lower case. See put() above. + String key = str.toLowerCase(Locale.ROOT) ; + String str2 = map.get(key) ; + if ( str2 == null ) + return str ; + return str2 ; + } + + public static String paramCallback(HttpServletRequest request) + { + return fetchParam(request, HttpNames.paramCallback) ; + } + + public static String fetchParam(HttpServletRequest request, String parameterName) + { + String value = request.getParameter(parameterName) ; + if ( value != null ) + { + value = value.trim() ; + if ( value.length() == 0 ) + value = null ; + } + return value ; + } + +} + http://git-wip-us.apache.org/repos/asf/jena/blob/36855e1b/jena-fuseki/src/main/java/org/apache/jena/fuseki/servlets/ResponseResultSet.java ---------------------------------------------------------------------- diff --git a/jena-fuseki/src/main/java/org/apache/jena/fuseki/servlets/ResponseResultSet.java b/jena-fuseki/src/main/java/org/apache/jena/fuseki/servlets/ResponseResultSet.java new file mode 100644 index 0000000..c42378b --- /dev/null +++ b/jena-fuseki/src/main/java/org/apache/jena/fuseki/servlets/ResponseResultSet.java @@ -0,0 +1,320 @@ +/* + * 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.atlas.lib.Lib.equal ; +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 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.riot.ResultSetMgr ; +import org.apache.jena.riot.WebContent ; +import org.apache.jena.riot.resultset.ResultSetLang ; +import org.apache.jena.web.HttpSC ; +import org.slf4j.Logger ; +import org.slf4j.LoggerFactory ; + +import com.hp.hpl.jena.query.QueryCancelledException ; +import com.hp.hpl.jena.query.ResultSet ; +import com.hp.hpl.jena.query.ResultSetFormatter ; +import com.hp.hpl.jena.sparql.core.Prologue ; + +/** 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 && equal(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 ( equal(serializationType, WebContent.contentTypeResultsXML) ) + sparqlXMLOutput(action, contentType, resultSet, stylesheetURL, booleanResult) ; + else if ( equal(serializationType, WebContent.contentTypeResultsJSON) ) + jsonOutput(action, contentType, resultSet, booleanResult) ; + else if ( equal(serializationType, WebContent.contentTypeTextPlain) ) + textOutput(action, contentType, resultSet, qPrologue, booleanResult) ; + else if ( equal(serializationType, WebContent.contentTypeTextCSV) ) + csvOutput(action, contentType, resultSet, booleanResult) ; + else if (equal(serializationType, WebContent.contentTypeTextTSV) ) + tsvOutput(action, contentType, resultSet, booleanResult) ; + else if (equal(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. + } +} http://git-wip-us.apache.org/repos/asf/jena/blob/36855e1b/jena-fuseki/src/main/java/org/apache/jena/fuseki/servlets/SPARQL_Protocol.java ---------------------------------------------------------------------- diff --git a/jena-fuseki/src/main/java/org/apache/jena/fuseki/servlets/SPARQL_Protocol.java b/jena-fuseki/src/main/java/org/apache/jena/fuseki/servlets/SPARQL_Protocol.java new file mode 100644 index 0000000..ed57a37 --- /dev/null +++ b/jena-fuseki/src/main/java/org/apache/jena/fuseki/servlets/SPARQL_Protocol.java @@ -0,0 +1,101 @@ +/** + * 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 org.apache.jena.fuseki.HttpNames.paramDefaultGraphURI ; +import static org.apache.jena.fuseki.HttpNames.paramNamedGraphURI ; + +import java.util.Arrays ; +import java.util.Collections ; +import java.util.List ; + +import javax.servlet.http.HttpServletRequest ; + +import org.apache.jena.atlas.iterator.Filter ; +import org.apache.jena.atlas.iterator.Iter ; +import org.apache.jena.atlas.lib.Lib ; + +import com.hp.hpl.jena.query.Query ; +import com.hp.hpl.jena.query.QueryParseException ; +import com.hp.hpl.jena.sparql.core.DatasetDescription ; + +/** Support for the SPARQL protocol (SPARQL Query, SPARQL Update) + */ +public abstract class SPARQL_Protocol extends SPARQL_ServletBase +{ + protected SPARQL_Protocol() { super() ; } + + protected static String messageForQPE(QueryParseException ex) + { + if ( ex.getMessage() != null ) + return ex.getMessage() ; + if ( ex.getCause() != null ) + return Lib.classShortName(ex.getCause().getClass()) ; + return null ; + } + + protected static DatasetDescription getDatasetDescription(HttpAction action) + { + List<String> graphURLs = toStrList(action.request.getParameterValues(paramDefaultGraphURI)) ; + List<String> namedGraphs = toStrList(action.request.getParameterValues(paramNamedGraphURI)) ; + + graphURLs = removeEmptyValues(graphURLs) ; + namedGraphs = removeEmptyValues(namedGraphs) ; + + if ( graphURLs.size() == 0 && namedGraphs.size() == 0 ) + return null ; + return DatasetDescription.create(graphURLs, namedGraphs) ; + } + + protected static DatasetDescription getDatasetDescription(Query query) + { + return DatasetDescription.create(query) ; + } + + private static List<String> toStrList(String[] array) + { + if ( array == null ) + return Collections.emptyList() ; + return Arrays.asList(array) ; + } + + private static List<String> removeEmptyValues(List<String> list) + { + return Iter.iter(list).filter(acceptNonEmpty).toList() ; + } + + private static Filter<String> acceptNonEmpty = new Filter<String>(){ + @Override + public boolean accept(String item) + { + return item != null && item.length() != 0 ; + } + } ; + + protected static int countParamOccurences(HttpServletRequest request, String param) + { + String[] x = request.getParameterValues(param) ; + if ( x == null ) + return 0 ; + return x.length ; + } + + +} + http://git-wip-us.apache.org/repos/asf/jena/blob/36855e1b/jena-fuseki/src/main/java/org/apache/jena/fuseki/servlets/SPARQL_Query.java ---------------------------------------------------------------------- diff --git a/jena-fuseki/src/main/java/org/apache/jena/fuseki/servlets/SPARQL_Query.java b/jena-fuseki/src/main/java/org/apache/jena/fuseki/servlets/SPARQL_Query.java new file mode 100644 index 0000000..27e76ac --- /dev/null +++ b/jena-fuseki/src/main/java/org/apache/jena/fuseki/servlets/SPARQL_Query.java @@ -0,0 +1,386 @@ +/* + * 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.HttpNames.* ; +import static org.apache.jena.fuseki.server.CounterName.QueryExecErrors ; +import static org.apache.jena.fuseki.server.CounterName.QueryTimeouts ; +import static org.apache.jena.fuseki.server.CounterName.RequestsBad ; + +import java.io.IOException ; +import java.io.InputStream ; +import java.util.* ; + +import javax.servlet.http.HttpServletRequest ; +import javax.servlet.http.HttpServletResponse ; + +import org.apache.jena.atlas.RuntimeIOException ; +import org.apache.jena.atlas.io.IO ; +import org.apache.jena.atlas.io.IndentedLineBuffer ; +import org.apache.jena.atlas.web.ContentType ; +import org.apache.jena.fuseki.FusekiException ; +import org.apache.jena.fuseki.FusekiLib ; +import org.apache.jena.fuseki.HttpNames ; +import org.apache.jena.riot.WebContent ; +import org.apache.jena.riot.web.HttpOp ; +import org.apache.jena.web.HttpSC ; + +import com.hp.hpl.jena.query.* ; +import com.hp.hpl.jena.rdf.model.Model ; +import com.hp.hpl.jena.sparql.core.Prologue ; +import com.hp.hpl.jena.sparql.resultset.SPARQLResult ; + +/** + * Handles SPARQL Query requests. + */ +public abstract class SPARQL_Query extends SPARQL_Protocol +{ + public SPARQL_Query() { super() ; } + + // Choose REST verbs to support. + + @Override + protected void doPost(HttpServletRequest request, HttpServletResponse response) + { doCommon(request, response) ; } + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) + { doCommon(request, response) ; } + + // HEAD + + @Override + protected void doOptions(HttpServletRequest request, HttpServletResponse response) + { + //response.setHeader(HttpNames.hAllow, "GET,HEAD,OPTIONS,POST"); + setCommonHeaders(response) ; + response.setHeader(HttpNames.hAllow, "GET,OPTIONS,POST"); + response.setHeader(HttpNames.hContentLengh, "0") ; + } + + @Override + protected final void perform(HttpAction action) + { + // GET + if ( action.request.getMethod().equals(HttpNames.METHOD_GET) ) + { + executeWithParameter(action) ; + return ; + } + + ContentType ct = FusekiLib.getContentType(action) ; + String incoming = ct.getContentType() ; + + // POST application/sparql-query + if (WebContent.contentTypeSPARQLQuery.equals(incoming)) + { + executeBody(action) ; + return ; + } + // POST application/x-www-form-url + if (WebContent.contentTypeHTMLForm.equals(incoming)) + { + executeWithParameter(action) ; + return ; + } + + error(HttpSC.UNSUPPORTED_MEDIA_TYPE_415, "Bad content type: "+incoming) ; + } + + // All the params we support + + protected static List<String> allParams = Arrays.asList(paramQuery, + paramDefaultGraphURI, paramNamedGraphURI, + paramQueryRef, + paramStyleSheet, + paramAccept, + paramOutput1, paramOutput2, + paramCallback, + paramForceAccept, + paramTimeout) ; + + @Override + protected void validate(HttpAction action) + { + String method = action.request.getMethod().toUpperCase(Locale.ROOT) ; + + if ( ! HttpNames.METHOD_POST.equals(method) && ! HttpNames.METHOD_GET.equals(method) ) + errorMethodNotAllowed("Not a GET or POST request") ; + + if ( HttpNames.METHOD_GET.equals(method) && action.request.getQueryString() == null ) + { + warning("Service Description / SPARQL Query / "+action.request.getRequestURI()) ; + errorNotFound("Service Description: "+action.request.getRequestURI()) ; + } + + // Use of the dataset describing parameters is check later. + try { + validateParams(action.request, allParams) ; + validateRequest(action) ; + } catch (ActionErrorException ex) { + throw ex ; + } + // Query not yet parsed. + } + + /** + * Validate the request after checking HTTP method and HTTP Parameters. + * @param action HTTP Action + */ + protected abstract void validateRequest(HttpAction action) ; + + /** + * Helper method for validating request. + * @param request HTTP request + * @param params parameters in a collection of Strings + */ + protected void validateParams(HttpServletRequest request, Collection<String> params) + { + ContentType ct = FusekiLib.getContentType(request) ; + boolean mustHaveQueryParam = true ; + if ( ct != null ) + { + String incoming = ct.getContentType() ; + + if ( WebContent.contentTypeSPARQLQuery.equals(incoming) ) + { + mustHaveQueryParam = false ; + //error(HttpSC.UNSUPPORTED_MEDIA_TYPE_415, "Unofficial "+WebContent.contentTypeSPARQLQuery+" not supported") ; + } + else if ( WebContent.contentTypeHTMLForm.equals(incoming) ) {} + else + error(HttpSC.UNSUPPORTED_MEDIA_TYPE_415, "Unsupported: "+incoming) ; + } + + // GET/POST of a form at this point. + + if ( mustHaveQueryParam ) + { + int N = countParamOccurences(request, paramQuery) ; + + if ( N == 0 ) errorBadRequest("SPARQL Query: No 'query=' parameter") ; + if ( N > 1 ) errorBadRequest("SPARQL Query: Multiple 'query=' parameters") ; + + // application/sparql-query does not use a query param. + String queryStr = request.getParameter(HttpNames.paramQuery) ; + + if ( queryStr == null ) + errorBadRequest("SPARQL Query: No query specified (no 'query=' found)") ; + if ( queryStr.isEmpty() ) + errorBadRequest("SPARQL Query: Empty query string") ; + } + + if ( params != null ) + { + Enumeration<String> en = request.getParameterNames() ; + for ( ; en.hasMoreElements() ; ) + { + String name = en.nextElement() ; + if ( ! params.contains(name) ) + warning("SPARQL Query: Unrecognize request parameter (ignored): "+name) ; + } + } + } + + private void executeWithParameter(HttpAction action) + { + String queryString = action.request.getParameter(paramQuery) ; + execute(queryString, action) ; + } + + private void executeBody(HttpAction action) + { + String queryString = null ; + try { + InputStream input = action.request.getInputStream() ; + queryString = IO.readWholeFileAsUTF8(input) ; + } + catch (IOException ex) { errorOccurred(ex) ; } + execute(queryString, action) ; + } + + private void execute(String queryString, HttpAction action) + { + String queryStringLog = formatForLog(queryString) ; + if ( action.verbose ) + log.info(format("[%d] Query = \n%s", action.id, queryString)); + else + log.info(format("[%d] Query = %s", action.id, queryStringLog)); + + Query query = null ; + try { + // NB syntax is ARQ (a superset of SPARQL) + query = QueryFactory.create(queryString, "http://example/query-base", Syntax.syntaxARQ) ; + queryStringLog = formatForLog(query) ; + validateQuery(action, query) ; + } catch (ActionErrorException ex) { + incCounter(action.srvRef, RequestsBad) ; + throw ex ; + } catch (QueryParseException ex) { + incCounter(action.srvRef, RequestsBad) ; + errorBadRequest("Parse error: \n" + queryString + "\n\r" + messageForQPE(ex)) ; + } catch (RuntimeIOException ex) { + errorBadRequest("Runtime IO Exception: \n" + queryString + "\n\r" + ex.getMessage()) ; + } + // Should not happen. + catch (QueryException ex) { + errorBadRequest("Error: \n" + queryString + "\n\r" + ex.getMessage()) ; + } + + // Assumes finished whole thing by end of sendResult. + action.beginRead() ; + try { + Dataset dataset = decideDataset(action, query, queryStringLog) ; + try(QueryExecution qExec = createQueryExecution(query, dataset)) { + SPARQLResult result = executeQuery(action, qExec, query, queryStringLog) ; + // Deals with exceptions itself. + sendResults(action, result, query.getPrologue()) ; + } + } catch (QueryCancelledException ex) { + // Additional counter information. + incCounter(action.srvRef, QueryTimeouts) ; + throw ex ; + } catch (RuntimeIOException ex) { + incCounter(action.srvRef, QueryExecErrors) ; + throw ex ; + } catch (QueryExecException ex) { + // Additional counter information. + incCounter(action.srvRef, QueryExecErrors) ; + throw ex ; + } finally { + action.endRead() ; + } + } + + /** + * Check the query, throwing ActionErrorException when not valid, or calling super#error. + * @param action HTTP Action + * @param query the Query + */ + protected abstract void validateQuery(HttpAction action, Query query) ; + + protected QueryExecution createQueryExecution(Query query, Dataset dataset) + { + return QueryExecutionFactory.create(query, dataset) ; + } + + protected SPARQLResult executeQuery(HttpAction action, QueryExecution qExec, Query query, String queryStringLog) + { + setAnyTimeouts(qExec, action); + + if ( query.isSelectType() ) + { + ResultSet rs = qExec.execSelect() ; + + // Force some query execution now. + // + // If the timeout-first-row goes off, the output stream has not + // been started so the HTTP error code is sent. + + rs.hasNext() ; + + // If we wanted perfect query time cancellation, we could consume the result now + // to see if the timeout-end-of-query goes off. + + //rs = ResultSetFactory.copyResults(rs) ; + + log.info(format("[%d] exec/select", action.id)) ; + return new SPARQLResult(rs) ; + } + + if ( query.isConstructType() ) + { + Model model = qExec.execConstruct() ; + log.info(format("[%d] exec/construct", action.id)) ; + return new SPARQLResult(model) ; + } + + if ( query.isDescribeType() ) + { + Model model = qExec.execDescribe() ; + log.info(format("[%d] exec/describe",action.id)) ; + return new SPARQLResult(model) ; + } + + if ( query.isAskType() ) + { + boolean b = qExec.execAsk() ; + log.info(format("[%d] exec/ask",action.id)) ; + return new SPARQLResult(b) ; + } + + errorBadRequest("Unknown query type - "+queryStringLog) ; + return null ; + } + + private void setAnyTimeouts(QueryExecution qexec, HttpAction action) { + if (!(action.getDatasetRef().allowTimeoutOverride)) + return; + + long desiredTimeout = Long.MAX_VALUE; + String timeoutHeader = action.request.getHeader("Timeout"); + String timeoutParameter = action.request.getParameter("timeout"); + if (timeoutHeader != null) { + try { + desiredTimeout = (int) Float.parseFloat(timeoutHeader) * 1000; + } catch (NumberFormatException e) { + throw new FusekiException("Timeout header must be a number", e); + } + } else if (timeoutParameter != null) { + try { + desiredTimeout = (int) Float.parseFloat(timeoutParameter) * 1000; + } catch (NumberFormatException e) { + throw new FusekiException("timeout parameter must be a number", e); + } + } + + desiredTimeout = Math.min(action.getDatasetRef().maximumTimeoutOverride, desiredTimeout); + if (desiredTimeout != Long.MAX_VALUE) + qexec.setTimeout(desiredTimeout); + } + + protected abstract Dataset decideDataset(HttpAction action, Query query, String queryStringLog) ; + + protected void sendResults(HttpAction action, SPARQLResult result, Prologue qPrologue) + { + if ( result.isResultSet() ) + ResponseResultSet.doResponseResultSet(action, result.getResultSet(), qPrologue) ; + else if ( result.isGraph() ) + ResponseModel.doResponseModel(action, result.getModel()) ; + else if ( result.isBoolean() ) + ResponseResultSet.doResponseResultSet(action, result.getBooleanResult()) ; + else + errorOccurred("Unknown or invalid result type") ; + } + + private String formatForLog(Query query) + { + IndentedLineBuffer out = new IndentedLineBuffer() ; + out.setFlatMode(true) ; + query.serialize(out) ; + return out.asString() ; + } + + private String getRemoteString(String queryURI) + { + return HttpOp.execHttpGetString(queryURI) ; + } + +} http://git-wip-us.apache.org/repos/asf/jena/blob/36855e1b/jena-fuseki/src/main/java/org/apache/jena/fuseki/servlets/SPARQL_QueryDataset.java ---------------------------------------------------------------------- diff --git a/jena-fuseki/src/main/java/org/apache/jena/fuseki/servlets/SPARQL_QueryDataset.java b/jena-fuseki/src/main/java/org/apache/jena/fuseki/servlets/SPARQL_QueryDataset.java new file mode 100644 index 0000000..9e9df36 --- /dev/null +++ b/jena-fuseki/src/main/java/org/apache/jena/fuseki/servlets/SPARQL_QueryDataset.java @@ -0,0 +1,60 @@ +/* + * 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 com.hp.hpl.jena.query.Dataset ; +import com.hp.hpl.jena.query.DatasetFactory ; +import com.hp.hpl.jena.query.Query ; +import com.hp.hpl.jena.sparql.core.DatasetDescription ; +import com.hp.hpl.jena.sparql.core.DatasetGraph ; +import com.hp.hpl.jena.sparql.core.DynamicDatasets ; + +public class SPARQL_QueryDataset extends SPARQL_Query +{ + public SPARQL_QueryDataset(boolean verbose) { super() ; } + + public SPARQL_QueryDataset() + { this(false) ; } + + @Override + protected void validateRequest(HttpAction action) + { } + + @Override + protected void validateQuery(HttpAction action, Query query) + { } + + @Override + protected Dataset decideDataset(HttpAction action, Query query, String queryStringLog) + { + DatasetGraph dsg = action.getActiveDSG() ; + + // query.getDatasetDescription() ; + + // Protocol. + DatasetDescription dsDesc = getDatasetDescription(action) ; + if (dsDesc != null ) + { + //errorBadRequest("SPARQL Query: Dataset description in the protocol request") ; + dsg = DynamicDatasets.dynamicDataset(dsDesc, dsg, false) ; + } + + return DatasetFactory.create(dsg) ; + } +} http://git-wip-us.apache.org/repos/asf/jena/blob/36855e1b/jena-fuseki/src/main/java/org/apache/jena/fuseki/servlets/SPARQL_QueryGeneral.java ---------------------------------------------------------------------- diff --git a/jena-fuseki/src/main/java/org/apache/jena/fuseki/servlets/SPARQL_QueryGeneral.java b/jena-fuseki/src/main/java/org/apache/jena/fuseki/servlets/SPARQL_QueryGeneral.java new file mode 100644 index 0000000..a022e96 --- /dev/null +++ b/jena-fuseki/src/main/java/org/apache/jena/fuseki/servlets/SPARQL_QueryGeneral.java @@ -0,0 +1,143 @@ +/* + * 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 java.util.List ; + +import org.apache.jena.atlas.lib.InternalErrorException ; +import org.apache.jena.fuseki.migrate.GraphLoadUtils ; +import org.apache.jena.riot.RiotException ; + +import com.hp.hpl.jena.query.Dataset ; +import com.hp.hpl.jena.query.DatasetFactory ; +import com.hp.hpl.jena.query.Query ; +import com.hp.hpl.jena.rdf.model.Model ; +import com.hp.hpl.jena.rdf.model.ModelFactory ; +import com.hp.hpl.jena.sparql.core.DatasetDescription ; + +public class SPARQL_QueryGeneral extends SPARQL_Query +{ + final static int MaxTriples = 100*1000 ; + + public SPARQL_QueryGeneral() { super() ; } + + @Override + protected void validateRequest(HttpAction action) {} + + @Override + protected void validateQuery(HttpAction action, Query query) {} + + @Override + protected String mapRequestToDataset(String uri) + { return null ; } + + @Override + protected Dataset decideDataset(HttpAction action, Query query, String queryStringLog) + { + DatasetDescription datasetDesc = getDatasetDescription(action) ; + if ( datasetDesc == null ) + datasetDesc = getDatasetDescription(query) ; + if ( datasetDesc == null ) + errorBadRequest("No dataset description in protocol request or in the query string") ; + + return datasetFromDescription(action, datasetDesc) ; + } + + /** + * Construct a Dataset based on a dataset description. + */ + + protected static Dataset datasetFromDescription(HttpAction action, DatasetDescription datasetDesc) + { + try { + if ( datasetDesc == null ) + return null ; + if ( datasetDesc.isEmpty() ) + return null ; + + List<String> graphURLs = datasetDesc.getDefaultGraphURIs() ; + List<String> namedGraphs = datasetDesc.getNamedGraphURIs() ; + + if ( graphURLs.size() == 0 && namedGraphs.size() == 0 ) + return null ; + + Dataset dataset = DatasetFactory.createMem() ; + // Look in cache for loaded graphs!! + + // ---- Default graph + { + Model model = ModelFactory.createDefaultModel() ; + for ( String uri : graphURLs ) + { + if ( uri == null || uri.equals("") ) + throw new InternalErrorException("Default graph URI is null or the empty string") ; + + try { + //TODO Clearup - RIOT integration. + GraphLoadUtils.loadModel(model, uri, MaxTriples) ; + log.info(format("[%d] Load (default graph) %s", action.id, uri)) ; + } catch (RiotException ex) { + log.info(format("[%d] Parsing error loading %s: %s", action.id, uri, ex.getMessage())) ; + errorBadRequest("Failed to load URL (parse error) "+uri+" : "+ex.getMessage()) ; + } catch (Exception ex) + { + log.info(format("[%d] Failed to load (default) %s: %s", action.id, uri, ex.getMessage())) ; + errorBadRequest("Failed to load URL "+uri) ; + } + } + dataset.setDefaultModel(model) ; + } + // ---- Named graphs + if ( namedGraphs != null ) + { + for ( String uri : namedGraphs ) + { + if ( uri == null || uri.equals("") ) + throw new InternalErrorException("Named graph URI is null or the empty string") ; + + try { + Model model = ModelFactory.createDefaultModel() ; + GraphLoadUtils.loadModel(model, uri, MaxTriples) ; + log.info(format("[%d] Load (named graph) %s", action.id, uri)) ; + dataset.addNamedModel(uri, model) ; + } catch (RiotException ex) { + log.info(format("[%d] Parsing error loading %s: %s", action.id, uri, ex.getMessage())) ; + errorBadRequest("Failed to load URL (parse error) "+uri+" : "+ex.getMessage()) ; + } catch (Exception ex) + { + log.info(format("[%d] Failed to load (named graph) %s: %s", action.id, uri, ex.getMessage())) ; + errorBadRequest("Failed to load URL "+uri) ; + } + } + } + + return dataset ; + + } + catch (ActionErrorException ex) { throw ex ; } + catch (Exception ex) + { + log.info(format("[%d] SPARQL parameter error: "+ex.getMessage(),action.id, ex)) ; + errorBadRequest("Parameter error: "+ex.getMessage()); + return null ; + } + } +} http://git-wip-us.apache.org/repos/asf/jena/blob/36855e1b/jena-fuseki/src/main/java/org/apache/jena/fuseki/servlets/SPARQL_REST.java ---------------------------------------------------------------------- diff --git a/jena-fuseki/src/main/java/org/apache/jena/fuseki/servlets/SPARQL_REST.java b/jena-fuseki/src/main/java/org/apache/jena/fuseki/servlets/SPARQL_REST.java new file mode 100644 index 0000000..4ab386b --- /dev/null +++ b/jena-fuseki/src/main/java/org/apache/jena/fuseki/servlets/SPARQL_REST.java @@ -0,0 +1,354 @@ +/* + * 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 org.apache.jena.fuseki.HttpNames.* ; + +import java.io.IOException ; +import java.io.InputStream ; +import java.util.Enumeration ; +import java.util.Locale ; + +import javax.servlet.ServletException ; +import javax.servlet.http.HttpServletRequest ; +import javax.servlet.http.HttpServletResponse ; + +import org.apache.jena.fuseki.HttpNames ; +import org.apache.jena.fuseki.server.CounterName ; +import org.apache.jena.riot.Lang ; +import org.apache.jena.riot.RDFDataMgr ; +import org.apache.jena.riot.ReaderRIOT ; +import org.apache.jena.riot.RiotException ; +import org.apache.jena.riot.system.ErrorHandler ; +import org.apache.jena.riot.system.ErrorHandlerFactory ; +import org.apache.jena.riot.system.IRIResolver ; +import org.apache.jena.riot.system.StreamRDF ; +import org.slf4j.Logger ; +import org.slf4j.LoggerFactory ; + +import com.hp.hpl.jena.graph.Graph ; +import com.hp.hpl.jena.graph.Node ; +import com.hp.hpl.jena.graph.NodeFactory ; +import com.hp.hpl.jena.sparql.core.DatasetGraph ; + +public abstract class SPARQL_REST extends SPARQL_ServletBase +{ + protected static Logger classLog = LoggerFactory.getLogger(SPARQL_REST.class) ; + + protected static ErrorHandler errorHandler = ErrorHandlerFactory.errorHandlerStd(log) ; + + protected final static Target determineTarget(HttpAction action) { + // Delayed until inside a transaction. + if ( action.getActiveDSG() == null ) + errorOccurred("Internal error : No action graph (not in a transaction?)") ; + + boolean dftGraph = getOneOnly(action.request, HttpNames.paramGraphDefault) != null ; + String uri = getOneOnly(action.request, HttpNames.paramGraph) ; + + if ( !dftGraph && uri == null ) { + // Direct naming or error. + uri = action.request.getRequestURL().toString() ; + if ( action.request.getRequestURI().equals(action.getDatasetRef().name) ) + // No name + errorBadRequest("Neither default graph nor named graph specified; no direct name") ; + } + + if ( dftGraph ) + return Target.createDefault(action.getActiveDSG()) ; + + // Named graph + if ( uri.equals(HttpNames.valueDefault ) ) + // But "named" default + return Target.createDefault(action.getActiveDSG()) ; + + // Strictly, a bit naughty on the URI resolution. But more sensible. + // Base is dataset. + String base = action.request.getRequestURL().toString() ; //wholeRequestURL(request) ; + // Make sure it ends in "/", ie. dataset as container. + if ( action.request.getQueryString() != null && ! base.endsWith("/") ) + base = base + "/" ; + + String absUri = IRIResolver.resolveString(uri, base) ; + Node gn = NodeFactory.createURI(absUri) ; + return Target.createNamed(action.getActiveDSG(), absUri, gn) ; + } + + + // struct for target + protected static final class Target + { + final boolean isDefault ; + final DatasetGraph dsg ; + private Graph _graph ; + final String name ; + final Node graphName ; + + static Target createNamed(DatasetGraph dsg, String name, Node graphName) { + return new Target(false, dsg, name, graphName) ; + } + + static Target createDefault(DatasetGraph dsg) { + return new Target(true, dsg, null, null) ; + } + + private Target(boolean isDefault, DatasetGraph dsg, String name, Node graphName) { + this.isDefault = isDefault ; + this.dsg = dsg ; + this._graph = null ; + this.name = name ; + this.graphName = graphName ; + + if ( isDefault ) + { + if ( name != null || graphName != null ) + throw new IllegalArgumentException("Inconsistent: default and a graph name/node") ; + } + else + { + if ( name == null || graphName == null ) + throw new IllegalArgumentException("Inconsistent: not default and/or no graph name/node") ; + } + } + + /** Get a graph for the action - this may create a graph in the dataset - this is not a test for graph existence */ + public Graph graph() { + if ( ! isGraphSet() ) + { + if ( isDefault ) + _graph = dsg.getDefaultGraph() ; + else + _graph = dsg.getGraph(graphName) ; + } + return _graph ; + } + + public boolean exists() + { + if ( isDefault ) return true ; + return dsg.containsGraph(graphName) ; + } + + public boolean isGraphSet() + { + return _graph != null ; + } + + @Override + public String toString() + { + if ( isDefault ) return "default" ; + return name ; + } + } + + public SPARQL_REST() + { super() ; } + + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + // Direct all verbs to our common framework. + doCommon(request, response) ; + } + + private void maybeSetLastModified(HttpServletResponse resp, long lastModified) { + if (resp.containsHeader(HEADER_LASTMOD)) return ; + if (lastModified >= 0) resp.setDateHeader(HEADER_LASTMOD, lastModified); + } + + @Override + protected void perform(HttpAction action) { + dispatch(action) ; + } + + private void dispatch(HttpAction action) { + HttpServletRequest req = action.request ; + HttpServletResponse resp = action.response ; + String method = req.getMethod().toUpperCase(Locale.ROOT) ; + + if (method.equals(METHOD_GET)) + doGet$(action); + else if (method.equals(METHOD_HEAD)) + doHead$(action); + else if (method.equals(METHOD_POST)) + doPost$(action); + else if (method.equals(METHOD_PATCH)) + doPatch$(action) ; + else if (method.equals(METHOD_OPTIONS)) + doOptions$(action) ; + else if (method.equals(METHOD_TRACE)) + //doTrace(action) ; + errorMethodNotAllowed("TRACE") ; + else if (method.equals(METHOD_PUT)) + doPut$(action) ; + else if (method.equals(METHOD_DELETE)) + doDelete$(action) ; + else + errorNotImplemented("Unknown method: "+method) ; + } + + // Counter wrappers + + protected void doGet$(HttpAction action) { + incCounter(action.srvRef, CounterName.GSPget) ; + try { + doGet(action) ; + incCounter(action.srvRef, CounterName.GSPgetGood) ; + } catch ( ActionErrorException ex) { + incCounter(action.srvRef, CounterName.GSPgetBad) ; + throw ex ; + } + } + + protected void doHead$(HttpAction action) { + incCounter(action.srvRef, CounterName.GSPhead) ; + try { + doHead(action) ; + incCounter(action.srvRef, CounterName.GSPheadGood) ; + } catch ( ActionErrorException ex) { + incCounter(action.srvRef, CounterName.GSPheadBad) ; + throw ex ; + } + } + + protected void doPost$(HttpAction action) { + incCounter(action.srvRef, CounterName.GSPpost) ; + try { + doPost(action) ; + incCounter(action.srvRef, CounterName.GSPpostGood) ; + } catch ( ActionErrorException ex) { + incCounter(action.srvRef, CounterName.GSPpostBad) ; + throw ex ; + } + } + + protected void doPatch$(HttpAction action) { + incCounter(action.srvRef, CounterName.GSPpatch) ; + try { + doPatch(action) ; + incCounter(action.srvRef, CounterName.GSPpatchGood) ; + } catch ( ActionErrorException ex) { + incCounter(action.srvRef, CounterName.GSPpatchBad) ; + throw ex ; + } + } + + protected void doDelete$(HttpAction action) { + incCounter(action.srvRef, CounterName.GSPdelete) ; + try { + doDelete(action) ; + incCounter(action.srvRef, CounterName.GSPdeleteGood) ; + } catch ( ActionErrorException ex) { + incCounter(action.srvRef, CounterName.GSPdeleteBad) ; + throw ex ; + } + } + + protected void doPut$(HttpAction action) { + incCounter(action.srvRef, CounterName.GSPput) ; + try { + doPut(action) ; + incCounter(action.srvRef, CounterName.GSPputGood) ; + } catch ( ActionErrorException ex) { + incCounter(action.srvRef, CounterName.GSPputBad) ; + throw ex ; + } + } + + protected void doOptions$(HttpAction action) { + incCounter(action.srvRef, CounterName.GSPoptions) ; + try { + doOptions(action) ; + incCounter(action.srvRef, CounterName.GSPoptionsGood) ; + } catch ( ActionErrorException ex) { + incCounter(action.srvRef, CounterName.GSPoptionsBad) ; + throw ex ; + } + } + + protected abstract void doGet(HttpAction action) ; + protected abstract void doHead(HttpAction action) ; + protected abstract void doPost(HttpAction action) ; + protected abstract void doPatch(HttpAction action) ; + protected abstract void doDelete(HttpAction action) ; + protected abstract void doPut(HttpAction action) ; + protected abstract void doOptions(HttpAction action) ; + + // @@ Move to SPARQL_ServletBase + // Check for all RiotReader + public static void parse(HttpAction action, StreamRDF dest, InputStream input, Lang lang, String base) { + try { + ReaderRIOT r = RDFDataMgr.createReader(lang) ; + if ( r == null ) + errorBadRequest("No parser for language '"+lang.getName()+"'") ; + r.setErrorHandler(errorHandler); + r.read(input, base, null, dest, null) ; + } + catch (RiotException ex) { errorBadRequest("Parse error: "+ex.getMessage()) ; } + } + + @Override + protected void validate(HttpAction action) + { + HttpServletRequest request = action.request ; + // Direct naming. + if ( request.getQueryString() == null ) + //errorBadRequest("No query string") ; + return ; + + String g = request.getParameter(HttpNames.paramGraph) ; + String d = request.getParameter(HttpNames.paramGraphDefault) ; + + if ( g != null && d !=null ) + errorBadRequest("Both ?default and ?graph in the query string of the request") ; + + if ( g == null && d == null ) + errorBadRequest("Neither ?default nor ?graph in the query string of the request") ; + + int x1 = SPARQL_Protocol.countParamOccurences(request, HttpNames.paramGraph) ; + int x2 = SPARQL_Protocol.countParamOccurences(request, HttpNames.paramGraphDefault) ; + + if ( x1 > 1 ) + errorBadRequest("Multiple ?default in the query string of the request") ; + if ( x2 > 1 ) + errorBadRequest("Multiple ?graph in the query string of the request") ; + + Enumeration<String> en = request.getParameterNames() ; + for ( ; en.hasMoreElements() ; ) + { + String h = en.nextElement() ; + if ( ! HttpNames.paramGraph.equals(h) && ! HttpNames.paramGraphDefault.equals(h) ) + errorBadRequest("Unknown parameter '"+h+"'") ; + // one of ?default and &graph + if ( request.getParameterValues(h).length != 1 ) + errorBadRequest("Multiple parameters '"+h+"'") ; + } + } + + protected static String getOneOnly(HttpServletRequest request, String name) + { + String[] values = request.getParameterValues(name) ; + if ( values == null ) + return null ; + if ( values.length == 0 ) + return null ; + if ( values.length > 1 ) + errorBadRequest("Multiple occurrences of '"+name+"'") ; + return values[0] ; + } +} http://git-wip-us.apache.org/repos/asf/jena/blob/36855e1b/jena-fuseki/src/main/java/org/apache/jena/fuseki/servlets/SPARQL_REST_R.java ---------------------------------------------------------------------- diff --git a/jena-fuseki/src/main/java/org/apache/jena/fuseki/servlets/SPARQL_REST_R.java b/jena-fuseki/src/main/java/org/apache/jena/fuseki/servlets/SPARQL_REST_R.java new file mode 100644 index 0000000..3c5c926 --- /dev/null +++ b/jena-fuseki/src/main/java/org/apache/jena/fuseki/servlets/SPARQL_REST_R.java @@ -0,0 +1,125 @@ +/* + * 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 java.io.IOException ; + +import javax.servlet.ServletOutputStream ; + +import org.apache.jena.atlas.web.MediaType ; +import org.apache.jena.atlas.web.TypedOutputStream ; +import org.apache.jena.fuseki.HttpNames ; +import org.apache.jena.riot.* ; + +import com.hp.hpl.jena.graph.Graph ; + +/** Only the READ operations */ +public class SPARQL_REST_R extends SPARQL_REST +{ + public SPARQL_REST_R() + { super() ; } + + + @Override + protected String mapRequestToDataset(String uri) { return mapRequestToDatasetLongest$(uri) ; } + + @Override + protected void doGet(HttpAction action) + { + // Assume success - do the set up before grabbing the lock. + // Sets content type. + MediaType mediaType = HttpAction.contentNegotationRDF(action) ; + + ServletOutputStream output ; + try { output = action.response.getOutputStream() ; } + catch (IOException ex) { errorOccurred(ex) ; output = null ; } + + TypedOutputStream out = new TypedOutputStream(output, mediaType) ; + Lang lang = RDFLanguages.contentTypeToLang(mediaType.getContentType()) ; + + if ( action.verbose ) + log.info(format("[%d] Get: Content-Type=%s, Charset=%s => %s", + action.id, mediaType.getContentType(), mediaType.getCharset(), lang.getName())) ; + + action.beginRead() ; + + try { + Target target = determineTarget(action) ; + if ( log.isDebugEnabled() ) + log.debug("GET->"+target) ; + boolean exists = target.exists() ; + if ( ! exists ) + errorNotFound("No such graph: <"+target.name+">") ; + // If we want to set the Content-Length, we need to buffer. + //response.setContentLength(??) ; + String ct = lang.getContentType().toHeaderString() ; + action.response.setContentType(ct) ; + Graph g = target.graph() ; + //Special case RDF/XML to be the plain (faster, less readable) form + RDFFormat fmt = + ( lang == Lang.RDFXML ) ? RDFFormat.RDFXML_PLAIN : RDFWriterRegistry.defaultSerialization(lang) ; + RDFDataMgr.write(out, g, fmt) ; + success(action) ; + } finally { action.endRead() ; } + } + + @Override + protected void doOptions(HttpAction action) + { + action.response.setHeader(HttpNames.hAllow, "GET,HEAD,OPTIONS") ; + action.response.setHeader(HttpNames.hContentLengh, "0") ; + success(action) ; + } + + @Override + protected void doHead(HttpAction action) + { + action.beginRead() ; + try { + Target target = determineTarget(action) ; + if ( log.isDebugEnabled() ) + log.debug("HEAD->"+target) ; + if ( ! target.exists() ) + { + successNotFound(action) ; + return ; + } + MediaType mediaType = HttpAction.contentNegotationRDF(action) ; + success(action) ; + } finally { action.endRead() ; } + } + + @Override + protected void doPost(HttpAction action) + { errorMethodNotAllowed("POST") ; } + + @Override + protected void doDelete(HttpAction action) + { errorMethodNotAllowed("DELETE") ; } + + @Override + protected void doPut(HttpAction action) + { errorMethodNotAllowed("PUT") ; } + + @Override + protected void doPatch(HttpAction action) + { errorMethodNotAllowed("PATCH") ; } +}
