| /* |
| * 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.sling.servlets.post.impl.operations; |
| |
| import java.io.IOException; |
| import java.util.ArrayList; |
| import java.util.HashMap; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Map; |
| |
| import javax.servlet.ServletContext; |
| import javax.servlet.http.Part; |
| |
| import org.apache.commons.io.IOUtils; |
| import org.apache.jackrabbit.util.Text; |
| import org.apache.sling.api.SlingHttpServletRequest; |
| import org.apache.sling.api.resource.PersistenceException; |
| import org.apache.sling.api.resource.Resource; |
| import org.apache.sling.api.resource.ResourceResolver; |
| import org.apache.sling.api.resource.ResourceUtil; |
| import org.apache.sling.servlets.post.Modification; |
| import org.apache.sling.servlets.post.PostResponse; |
| import org.apache.sling.servlets.post.impl.helper.StreamedChunk; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| /** |
| * Performs a streamed modification of the content. |
| * Each File body encountered will result in a session save operation, to cause the underlying Resource implementation |
| * to stream content from the request to the target. |
| * |
| * This implements PostOperation but does not touch the normal Sling Request processing which is not streamed. |
| * |
| * The map of available fields is built up as the request is streamed. It is advisable to submit the request with all the form |
| * fields at the start of the request (normally based on DOM order) to ensure they are available before the streamed bodies are processed. |
| * |
| * The implementation does not implement the full Sling protocol aiming to keep it simple, and just deal with a streaming upload operation. |
| * The implementation binds to the Sling Resource API rather than JCR to keep it independent of the type of persistence. |
| */ |
| public class StreamedUploadOperation extends AbstractPostOperation { |
| private static final Logger LOG = LoggerFactory.getLogger(StreamedUploadOperation.class); |
| public static final String NT_FILE = "nt:file"; |
| private ServletContext servletContext; |
| |
| public void setServletContext(final ServletContext servletContext) { |
| this.servletContext = servletContext; |
| } |
| |
| |
| /** |
| * Check the request and return true if there is a parts iterator attribute present. This attribute |
| * will have been put there by the Sling Engine ParameterSupport class. If its not present, the request |
| * is not streamed and cant be processed by this class. Check this first before using this class. |
| * @param request the request. |
| * @return true if the request can be streamed. |
| */ |
| public boolean isRequestStreamed(SlingHttpServletRequest request) { |
| return request.getAttribute("request-parts-iterator") != null; |
| } |
| |
| @Override |
| protected void doRun(SlingHttpServletRequest request, PostResponse response, List<Modification> changes) |
| throws PersistenceException { |
| @SuppressWarnings("unchecked") |
| Iterator<Part> partsIterator = (Iterator<Part>) request.getAttribute("request-parts-iterator"); |
| Map<String, List<String>> formFields = new HashMap<>(); |
| boolean streamingBodies = false; |
| while (partsIterator.hasNext()) { |
| Part part = partsIterator.next(); |
| String name = part.getName(); |
| |
| if (isFormField(part)) { |
| addField(formFields, name, part); |
| if (streamingBodies) { |
| LOG.warn("Form field {} was sent after the bodies started to be streamed. " + |
| "Will not have been available to all streamed bodies. " + |
| "It is recommended to send all form fields before streamed bodies in the POST ", name); |
| } |
| } else { |
| streamingBodies = true; |
| // process the file body and commit. |
| writeContent(request.getResourceResolver(), part, formFields, response, changes); |
| |
| } |
| } |
| } |
| |
| /** |
| * Add a field to the store of formFields. |
| * @param formFields the formFileds |
| * @param name the name of the field. |
| * @param part the part. |
| */ |
| private void addField(Map<String, List<String>> formFields, String name, Part part) { |
| List<String> values = formFields.get(name); |
| if ( values == null ) { |
| values = new ArrayList<>(); |
| formFields.put(name, values); |
| } |
| try { |
| values.add(IOUtils.toString(part.getInputStream(),"UTF-8")); |
| } catch (IOException e) { |
| LOG.error("Failed to read form field "+name,e); |
| } |
| } |
| |
| |
| /** |
| * Write content to the resource API creating a standard JCR structure of nt:file - nt:resource - jcr:data. |
| * This method will commit to the repository to force the repository to read from the input stream and write |
| * to the target. How efficient that is depends on the repository implementation. |
| * @param resolver the resource resolver. |
| * @param part the part containing the file body. |
| * @param formFields form fields collected so far. |
| * @param response the response object, updated by the operation. |
| * @param changes changes made to the repo. |
| * @throws PersistenceException |
| */ |
| private void writeContent(final ResourceResolver resolver, |
| final Part part, |
| final Map<String, List<String>> formFields, |
| final PostResponse response, |
| final List<Modification> changes) |
| throws PersistenceException { |
| |
| final String path = response.getPath(); |
| final Resource parentResource = resolver.getResource(path); |
| if ( !resourceExists(parentResource)) { |
| throw new IllegalArgumentException("Parent resource must already exist to be able to stream upload content. Please create first "); |
| } |
| String name = getUploadName(part); |
| Resource fileResource = parentResource.getChild(name); |
| Map<String, Object> fileProps = new HashMap<>(); |
| if (fileResource == null) { |
| fileProps.put("jcr:primaryType", NT_FILE); |
| fileResource = parentResource.getResourceResolver().create(parentResource, name, fileProps); |
| } |
| |
| |
| StreamedChunk chunk = new StreamedChunk(part, formFields, servletContext); |
| Resource result = chunk.store(fileResource, changes); |
| result.getResourceResolver().commit(); |
| |
| } |
| |
| /** |
| * Is the part a form field ? |
| * @param part |
| * @return |
| */ |
| private boolean isFormField(Part part) { |
| return (part.getSubmittedFileName() == null); |
| } |
| |
| /** |
| * Get the upload file name from the part. |
| * @param part |
| * @return |
| */ |
| private String getUploadName(Part part) { |
| // only return non null if the submitted file name is non null. |
| // the Sling API states that if the field name is '*' then the submitting file name is used, |
| // otherwise the field name is used. |
| String name = part.getName(); |
| String fileName = part.getSubmittedFileName(); |
| if ("*".equals(name)) { |
| name = fileName; |
| } |
| // strip of possible path (some browsers include the entire path) |
| name = name.substring(name.lastIndexOf('/') + 1); |
| name = name.substring(name.lastIndexOf('\\') + 1); |
| return Text.escapeIllegalJcrChars(name); |
| } |
| |
| /** |
| * Does the resource exist ? |
| * @param resource |
| * @return |
| */ |
| private boolean resourceExists(final Resource resource) { |
| return (resource != null && !ResourceUtil.isSyntheticResource(resource)); |
| } |
| |
| |
| |
| |
| |
| } |