blob: d8753299fa843c158b35df59d010591fc5e76826 [file] [log] [blame]
/*
* 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));
}
}