| /** |
| * 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.system; |
| |
| import static java.lang.String.format; |
| import static org.apache.jena.riot.WebContent.ctMultipartFormData; |
| import static org.apache.jena.riot.WebContent.ctMultipartMixed; |
| import static org.apache.jena.riot.WebContent.ctTextPlain; |
| import static org.apache.jena.riot.WebContent.matchContentType; |
| |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.util.zip.GZIPInputStream; |
| |
| import org.apache.commons.fileupload2.core.FileItemInput; |
| import org.apache.commons.fileupload2.core.FileItemInputIterator; |
| import org.apache.commons.fileupload2.jakarta.servlet6.JakartaServletFileUpload; |
| import org.apache.jena.atlas.io.IO; |
| import org.apache.jena.atlas.web.ContentType; |
| import org.apache.jena.fuseki.servlets.*; |
| import org.apache.jena.riot.Lang; |
| import org.apache.jena.riot.RDFLanguages; |
| import org.apache.jena.riot.RiotParseException; |
| import org.apache.jena.riot.lang.StreamRDFCounting; |
| import org.apache.jena.riot.system.StreamRDF; |
| import org.apache.jena.riot.system.StreamRDFLib; |
| |
| /** |
| * Functions to load data to a StreamRDF. |
| * <p> |
| * This is used for {@link GSP_RW Graph Store Protocol PUT/POST}, |
| * including the extension to dataset (quads) loads |
| * and also {@link UploadRDF} the direct upload service. |
| * @see UploadRDF |
| */ |
| public class DataUploader { |
| |
| /** |
| * Parse the body contents to the {@link StreamRDF}. |
| * <p> |
| * This function is used by {@link GSP_RW} and {@link UploadRDF}. |
| * <p> |
| * This function assumes it is inside a transaction. |
| * <p> |
| * Supports Content-Type, with data in the body, as well as |
| * {@code multipart/mixed} (<a href="https://tools.ietf.org/html/rfc204">RFC 2046</a>) and |
| * {@code multipart/form-data} (<a href="https://tools.ietf.org/html/rfc1867">RFC 1867</a>) |
| * with a file part. |
| * @throws RiotParseException |
| */ |
| public static UploadDetails incomingData(HttpAction action, StreamRDF dest) { |
| ContentType ct = ActionLib.getContentType(action); |
| |
| if ( ct == null ) { |
| ServletOps.errorBadRequest("No content type"); |
| return null; |
| } |
| |
| if ( matchContentType(ctMultipartFormData, ct) || matchContentType(ctMultipartMixed, ct) ) { |
| // Multipart |
| return multipartCommonsFileUpload(action, dest); |
| } |
| |
| // Single graph or quads in body. |
| String base = ActionLib.wholeRequestURL(action.getRequest()); |
| Lang lang = RDFLanguages.contentTypeToLang(ct.getContentTypeStr()); |
| if ( lang == null ) { |
| ServletOps.errorBadRequest("Unknown content type for triples: " + ct); |
| return null; |
| } |
| long len = action.getRequestContentLengthLong(); |
| |
| StreamRDFCounting countingDest = StreamRDFLib.count(dest); |
| try { |
| ActionLib.parse(action, countingDest, lang, base); |
| UploadDetails details = new UploadDetails(countingDest.count(), countingDest.countTriples(),countingDest.countQuads()); |
| action.log.info(format("[%d] Body: Content-Length=%d, Content-Type=%s, Charset=%s => %s : %s", |
| action.id, len, ct.getContentTypeStr(), ct.getCharset(), lang.getName(), |
| details.detailsStr())); |
| return details; |
| } catch (RiotParseException ex) { |
| action.log.info(format("[%d] Attempt to load: Content-Length=%d, Content-Type=%s, Charset=%s => %s", |
| action.id, len, ct.getContentTypeStr(), ct.getCharset(), ex.getMessage())); |
| // Exhaust input. |
| ActionLib.consumeBody(action); |
| throw ex; |
| } |
| } |
| |
| // Previously, Jena has used HttpServletRequests.getParts. |
| // Each application server (Tomcat and Jetty) has special configuration. |
| // Use Apache Commons FileUpload as the mulipart parser as it is portable. |
| |
| /** |
| * Process an HTTP upload of RDF files (triples or quads) with content type |
| * "multipart/form-data" or "multipart/mixed". |
| * <p> |
| * Form data (content-disposition: form-data; name="...") is rejected. |
| * <p> |
| * Data is streamed straight into the destination graph or dataset. |
| * <p> |
| * This function assumes it is inside a transaction. |
| */ |
| private static UploadDetails multipartCommonsFileUpload(HttpAction action, StreamRDF dest) { |
| String base = ActionLib.wholeRequestURL(action.getRequest()); |
| StreamRDFCounting countingDest = StreamRDFLib.count(dest); |
| |
| // We only use the request body parsing capability of a ServletFileUpload. |
| JakartaServletFileUpload<?,?> upload = new JakartaServletFileUpload<>(); |
| try { |
| FileItemInputIterator iter = upload.getItemIterator(action.getRequest()); |
| while (iter.hasNext()) { |
| FileItemInput part = iter.next(); |
| |
| if (part.isFormField()) { |
| // Form field - this code only supports multipart file upload. |
| String fieldName = part.getFieldName(); |
| InputStream stream = part.getInputStream(); |
| String value = IO.readWholeFileAsUTF8(stream); |
| // This code is currently used to put multiple files into a single destination. |
| // Additional field/values do not make sense. |
| ServletOps.errorBadRequest(format("Only files accepted in multipart file upload (got %s=%s)", fieldName, value)); |
| // errorBadRequest does not return. |
| return null; |
| } |
| |
| InputStream input = part.getInputStream(); |
| String contentTypeHeader = part.getContentType(); |
| ContentType ct = ContentType.create(contentTypeHeader); |
| String fieldName = part.getFieldName(); // Corresponds to ServletAPI Part.getName |
| String submittedFileName = part.getName(); // Corresponds to ServletAPI Part.getSubmittedFileName |
| |
| handlePart(action, input, base, ct, submittedFileName, countingDest); |
| } |
| } |
| catch (ActionErrorException ex) { throw ex; } |
| catch (Exception ex) { ServletOps.errorOccurred(ex.getMessage()); } |
| // Overall results. |
| UploadDetails details = new UploadDetails(countingDest.count(), countingDest.countTriples(),countingDest.countQuads()); |
| return details; |
| } |
| |
| /** |
| * Process one item in a multiple part file upload. |
| * This does not depend on the choice of multi-part parser. |
| */ |
| private static void handlePart(HttpAction action, InputStream input, |
| String base, ContentType ct, String submittedFileName, |
| StreamRDF dest) throws IOException { |
| Lang lang = null; |
| if ( ! matchContentType(ctTextPlain, ct) ) |
| lang = RDFLanguages.contentTypeToLang(ct.getContentTypeStr()); |
| |
| if ( lang == null ) { |
| // Not a recognized Content-Type. Look at file extension. |
| if ( submittedFileName == null || submittedFileName.equals("") ) |
| ServletOps.errorBadRequest("No name for content - can't determine RDF syntax"); |
| lang = RDFLanguages.pathnameToLang(submittedFileName); |
| if (submittedFileName.endsWith(".gz")) |
| input = new GZIPInputStream(input); |
| } |
| if ( lang == null ) |
| // Desperate. |
| lang = RDFLanguages.RDFXML; |
| |
| String printfilename = submittedFileName; |
| if ( printfilename == null || printfilename.equals("") ) |
| printfilename = "<none>"; |
| |
| // count just this step |
| StreamRDFCounting countingDest2 = StreamRDFLib.count(dest); |
| try { |
| ActionLib.parse(action, countingDest2, input, lang, base); |
| UploadDetails details1 = new UploadDetails(countingDest2.count(), countingDest2.countTriples(),countingDest2.countQuads()); |
| action.log.info(format("[%d] Filename: %s, Content-Type=%s, Charset=%s => %s : %s", |
| action.id, printfilename, ct.getContentTypeStr(), ct.getCharset(), lang.getName(), |
| details1.detailsStr())); |
| } catch (RiotParseException ex) { |
| action.log.info(format("[%d] Filename: %s, Content-Type=%s, Charset=%s => %s : %s", |
| action.id, printfilename, ct.getContentTypeStr(), ct.getCharset(), lang.getName(), |
| ex.getMessage())); |
| ActionLib.consumeBody(action); |
| throw ex; |
| } |
| } |
| } |