blob: 29eb674a3fd326292202e98fb809ccf3aff2d2e2 [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.helper;
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.SequenceInputStream;
import java.util.Calendar;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.jcr.Node;
import javax.jcr.NodeIterator;
import javax.jcr.RepositoryException;
import javax.jcr.nodetype.NodeType;
import javax.jcr.nodetype.NodeTypeManager;
import javax.servlet.ServletContext;
import org.apache.commons.io.IOUtils;
import org.apache.jackrabbit.util.Text;
import org.apache.sling.api.request.RequestParameter;
import org.apache.sling.api.resource.ModifiableValueMap;
import org.apache.sling.api.resource.PersistenceException;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.servlets.post.Modification;
import org.apache.sling.servlets.post.SlingPostConstants;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Handles file uploads.
* <p/>
*
* Simple example:
* <xmp>
* <form action="/home/admin" method="POST" enctype="multipart/form-data">
* <input type="file" name="./portrait" />
* </form>
* </xmp>
*
* this will create a nt:file node below "/home/admin" if the node type of
* "admin" is (derived from) nt:folder, a nt:resource node otherwise.
* <p/>
*
* Filename example:
* <xmp>
* <form action="/home/admin" method="POST" enctype="multipart/form-data">
* <input type="file" name="./*" />
* </form>
* </xmp>
*
* same as above, but uses the filename of the uploaded file as name for the
* new node.
* <p/>
*
* Type hint example:
* <xmp>
* <form action="/home/admin" method="POST" enctype="multipart/form-data">
* <input type="file" name="./portrait" />
* <input type="hidden" name="./portrait@TypeHint" value="my:file" />
* </form>
* </xmp>
*
* this will create a new node with the type my:file below admin. if the hinted
* type extends from nt:file an intermediate file node is created otherwise
* directly a resource node.
*/
public class SlingFileUploadHandler {
// nodetype name string constants
public static final String NT_FOLDER = "nt:folder";
public static final String NT_FILE = "nt:file";
public static final String NT_RESOURCE = "nt:resource";
public static final String NT_UNSTRUCTURED = "nt:unstructured";
// item name string constants
public static final String JCR_CONTENT = "jcr:content";
public static final String JCR_LASTMODIFIED = "jcr:lastModified";
public static final String JCR_MIMETYPE = "jcr:mimeType";
public static final String JCR_ENCODING = "jcr:encoding";
public static final String JCR_DATA = "jcr:data";
private final Logger log = LoggerFactory.getLogger(getClass());
/**
* The servlet context.
*/
private ServletContext servletContext;
public void setServletContext(ServletContext servletContext) {
this.servletContext = servletContext;
}
/**
* Uses the file(s) in the request parameter for creation of new nodes.
* if the parent node is a nt:folder a new nt:file is created. otherwise
* just a nt:resource. if the <code>name</code> is '*', the filename of
* the uploaded file is used.
*
* @param parent the parent node
* @param prop the assembled property info
* @throws RepositoryException if an error occurs
*/
private void setFile(final Resource parentResource, final Node parent, final RequestProperty prop, RequestParameter value, final List<Modification> changes, String name, final String contentType)
throws RepositoryException, PersistenceException {
// check type hint. if the type is ok and extends from nt:file,
// create an nt:file with that type. if it's invalid, drop it and let
// the parent node type decide.
boolean createNtFile = parent.isNodeType(NT_FOLDER);
String typeHint = prop.getTypeHint();
if (typeHint != null) {
try {
NodeTypeManager ntMgr = parent.getSession().getWorkspace().getNodeTypeManager();
NodeType nt = ntMgr.getNodeType(typeHint);
createNtFile = nt.isNodeType(NT_FILE);
} catch (RepositoryException e) {
// assuming type not valid.
typeHint = null;
}
}
// also create an nt:file if the name contains an extension
// the rationale is that if the file name is "important" we want
// an nt:file, and an image name with an extension is probably "important"
if(!createNtFile && name.indexOf('.') > 0) {
createNtFile = true;
}
// set empty type
if (typeHint == null) {
typeHint = createNtFile ? NT_FILE : NT_RESOURCE;
}
// create nt:file node if needed
Resource resParent;
if (createNtFile) {
// create nt:file
resParent = getOrCreateChildResource(parentResource, name, typeHint, changes);
name = JCR_CONTENT;
typeHint = NT_RESOURCE;
} else {
resParent = parentResource;
}
// create resource node
Resource newResource = getOrCreateChildResource(resParent, name, typeHint, changes);
Node res = newResource.adaptTo(Node.class);
// set properties
changes.add(Modification.onModified(
res.setProperty(JCR_LASTMODIFIED, Calendar.getInstance()).getPath()
));
changes.add(Modification.onModified(
res.setProperty(JCR_MIMETYPE, contentType).getPath()
));
try {
// process chunk upload request separately
if (prop.isChunkUpload()) {
processChunk(resParent, res, prop, value, changes);
} else {
changes.add(Modification.onModified(res.setProperty(JCR_DATA,
value.getInputStream()).getPath()));
}
} catch (IOException e) {
throw new RepositoryException("Error while retrieving inputstream from parameter value.", e);
}
}
/**
* Uses the file(s) in the request parameter for creation of new nodes.
* if the parent node is a nt:folder a new nt:file is created. otherwise
* just a nt:resource. if the <code>name</code> is '*', the filename of
* the uploaded file is used.
*
* @param parent the parent node
* @param prop the assembled property info
* @throws RepositoryException if an error occurs
*/
private void setFile(final Resource parentResource, final RequestProperty prop, final RequestParameter value, final List<Modification> changes, String name, final String contentType)
throws PersistenceException, RepositoryException {
String typeHint = prop.getTypeHint();
if ( typeHint == null ) {
typeHint = NT_FILE;
}
if(prop.isChunkUpload()){
// cannot process chunk upload if parent node doesn't
// exists. throw exception
throw new RepositoryException(
"Cannot process chunk upload request. Parent resource ["
+ parentResource.getPath() + "] doesn't exists");
}
// create properties
final Map<String, Object> props = new HashMap<String, Object>();
props.put("sling:resourceType", typeHint);
props.put(JCR_LASTMODIFIED, Calendar.getInstance());
props.put(JCR_MIMETYPE, contentType);
try {
props.put(JCR_DATA, value.getInputStream());
} catch (final IOException e) {
throw new PersistenceException("Error while retrieving inputstream from parameter value.", e);
}
// get or create resource
Resource result = parentResource.getChild(name);
if ( result != null ) {
final ModifiableValueMap vm = result.adaptTo(ModifiableValueMap.class);
if ( vm == null ) {
throw new PersistenceException("Resource at " + parentResource.getPath() + '/' + name + " is not modifiable.");
}
vm.putAll(props);
} else {
result = parentResource.getResourceResolver().create(parentResource, name, props);
}
for(final String key : props.keySet()) {
changes.add(Modification.onModified(result.getPath() + '/' + key));
}
}
/**
* Process chunk upload. For first and intermediate chunks request persists
* chunks at jcr:content/chunk_start_end/jcr:data or
* nt:resource/chunk_start_end/jcr:data. For last last chunk,
* merge all previous chunks and last chunk and replace binary at
* destination.
*/
private void processChunk(final Resource resParent, final Node res,
final RequestProperty prop, RequestParameter value,
final List<Modification> changes) throws RepositoryException {
try {
long chunkOffset = prop.getChunk().getOffset();
if (chunkOffset == 0) {
// first chunk
// check if another chunk upload is already in progress. throw
// exception
NodeIterator itr = res.getNodes(SlingPostConstants.CHUNK_NODE_NAME
+ "*");
if (itr.hasNext()) {
throw new RepositoryException(
"Chunk upload already in progress at {" + res.getPath()
+ "}");
}
res.addMixin(SlingPostConstants.NT_SLING_CHUNK_MIXIN);
changes.add(Modification.onModified(res.setProperty(
SlingPostConstants.NT_SLING_CHUNKS_LENGTH, 0).getPath()));
if (!res.hasProperty(JCR_DATA)) {
// create a empty jcr:data property
res.setProperty(JCR_DATA,
new ByteArrayInputStream("".getBytes()));
}
}
if (!res.hasProperty(SlingPostConstants.NT_SLING_CHUNKS_LENGTH)) {
throw new RepositoryException("no chunk upload found at {"
+ res.getPath() + "}");
}
long currentLength = res.getProperty(
SlingPostConstants.NT_SLING_CHUNKS_LENGTH).getLong();
long totalLength = prop.getChunk().getLength();
if (chunkOffset != currentLength) {
throw new RepositoryException("Chunk's offset {"
+ chunkOffset
+ "} doesn't match expected offset {"
+ res.getProperty(
SlingPostConstants.NT_SLING_CHUNKS_LENGTH).getLong()
+ "}");
}
if (totalLength != 0) {
if (res.hasProperty(SlingPostConstants.NT_SLING_FILE_LENGTH)) {
long expectedLength = res.getProperty(
SlingPostConstants.NT_SLING_FILE_LENGTH).getLong();
if (totalLength != expectedLength) {
throw new RepositoryException("File length {"
+ totalLength + "} doesn't match expected length {"
+ expectedLength + "}");
}
} else {
res.setProperty(SlingPostConstants.NT_SLING_FILE_LENGTH,
totalLength);
}
}
NodeIterator itr = res.getNodes(SlingPostConstants.CHUNK_NODE_NAME
+ "_" + String.valueOf(chunkOffset) + "*");
if (itr.hasNext()) {
throw new RepositoryException("Chunk already present at {"
+ itr.nextNode().getPath() + "}");
}
String nodeName = SlingPostConstants.CHUNK_NODE_NAME + "_"
+ String.valueOf(chunkOffset) + "_"
+ String.valueOf(chunkOffset + value.getSize() - 1);
if (totalLength == (currentLength + value.getSize())
|| prop.getChunk().isCompleted()) {
File file = null;
InputStream fileIns = null;
try {
file = mergeChunks(res, value.getInputStream());
fileIns = new FileInputStream(file);
changes.add(Modification.onModified(res.setProperty(
JCR_DATA, fileIns).getPath()));
NodeIterator nodeItr = res.getNodes(SlingPostConstants.CHUNK_NODE_NAME
+ "*");
while (nodeItr.hasNext()) {
Node nodeRange = nodeItr.nextNode();
changes.add(Modification.onDeleted(nodeRange.getPath()));
nodeRange.remove();
}
if (res.hasProperty(SlingPostConstants.NT_SLING_FILE_LENGTH)) {
javax.jcr.Property expLenProp = res.getProperty(SlingPostConstants.NT_SLING_FILE_LENGTH);
changes.add(Modification.onDeleted(expLenProp.getPath()));
expLenProp.remove();
}
if (res.hasProperty(SlingPostConstants.NT_SLING_CHUNKS_LENGTH)) {
javax.jcr.Property currLenProp = res.getProperty(SlingPostConstants.NT_SLING_CHUNKS_LENGTH);
changes.add(Modification.onDeleted(currLenProp.getPath()));
currLenProp.remove();
}
res.removeMixin(SlingPostConstants.NT_SLING_CHUNK_MIXIN);
} finally {
try {
fileIns.close();
file.delete();
} catch (IOException ign) {
}
}
} else {
Node rangeNode = res.addNode(nodeName,
SlingPostConstants.NT_SLING_CHUNK_NODETYPE);
changes.add(Modification.onCreated(rangeNode.getPath()));
changes.add(Modification.onModified(rangeNode.setProperty(
JCR_DATA, value.getInputStream()).getPath()));
changes.add(Modification.onModified(rangeNode.setProperty(
SlingPostConstants.NT_SLING_CHUNK_OFFSET, chunkOffset).getPath()));
changes.add(Modification.onModified(res.setProperty(
SlingPostConstants.NT_SLING_CHUNKS_LENGTH,
currentLength + value.getSize()).getPath()));
}
} catch (IOException e) {
throw new RepositoryException(
"Error while retrieving inputstream from parameter value.", e);
}
}
/**
* Merge all previous chunks with last chunk's stream into a temporary file
* and return it.
*/
private File mergeChunks(final Node parentNode,
final InputStream lastChunkStream) throws PersistenceException,
RepositoryException {
OutputStream out = null;
SequenceInputStream mergeStrm = null;
File file = null;
try {
file = File.createTempFile("tmp-", "-mergechunk");
out = new FileOutputStream(file);
String startPattern = SlingPostConstants.CHUNK_NODE_NAME + "_"
+ "0_*";
NodeIterator nodeItr = parentNode.getNodes(startPattern);
InputStream ins = null;
int i = 0;
Set<InputStream> inpStrmSet = new LinkedHashSet<InputStream>();
while (nodeItr.hasNext()) {
if (nodeItr.getSize() > 1) {
throw new RepositoryException(
"more than one node found for pattern: " + startPattern);
}
Node rangeNode = nodeItr.nextNode();
inpStrmSet.add(rangeNode.getProperty(
javax.jcr.Property.JCR_DATA).getBinary().getStream());
log.debug("added chunk {} to merge stream", rangeNode.getName());
String[] indexBounds = rangeNode.getName().substring(
(SlingPostConstants.CHUNK_NODE_NAME + "_").length()).split(
"_");
startPattern = SlingPostConstants.CHUNK_NODE_NAME + "_"
+ String.valueOf(Long.valueOf(indexBounds[1]) + 1) + "_*";
nodeItr = parentNode.getNodes(startPattern);
}
inpStrmSet.add(lastChunkStream);
mergeStrm = new SequenceInputStream(
Collections.enumeration(inpStrmSet));
IOUtils.copyLarge(mergeStrm, out);
} catch (IOException e) {
throw new PersistenceException("excepiton occured", e);
} finally {
IOUtils.closeQuietly(out);
IOUtils.closeQuietly(mergeStrm);
}
return file;
}
/**
* Delete all chunks saved within a node. If no chunks exist, it is no-op.
*/
public void deleteChunks(final Node node) throws RepositoryException {
// parent node containing all chunks and has mixin sling:chunks applied
// on it.
Node chunkParent = null;
Node jcrContentNode = null;
if (hasChunks(node)) {
chunkParent = node;
} else if (node.hasNode(JCR_CONTENT)
&& hasChunks((jcrContentNode = node.getNode(JCR_CONTENT)))) {
chunkParent = jcrContentNode;
}
if (chunkParent != null) {
NodeIterator nodeItr = chunkParent.getNodes(SlingPostConstants.CHUNK_NODE_NAME
+ "*");
while (nodeItr.hasNext()) {
Node rangeNode = nodeItr.nextNode();
rangeNode.remove();
}
if (chunkParent.hasProperty(SlingPostConstants.NT_SLING_FILE_LENGTH)) {
chunkParent.getProperty(SlingPostConstants.NT_SLING_FILE_LENGTH).remove();
}
if (chunkParent.hasProperty(SlingPostConstants.NT_SLING_CHUNKS_LENGTH)) {
chunkParent.getProperty(
SlingPostConstants.NT_SLING_CHUNKS_LENGTH).remove();
}
chunkParent.removeMixin(SlingPostConstants.NT_SLING_CHUNK_MIXIN);
}
}
/**
* Get the last {@link SlingPostConstants#NT_SLING_CHUNK_NODETYPE}
* {@link Node}.
*
* @param node {@link Node} containing
* {@link SlingPostConstants#NT_SLING_CHUNK_NODETYPE}
* {@link Node}s
* @return the {@link SlingPostConstants#NT_SLING_CHUNK_NODETYPE} chunk
* node.
* @throws RepositoryException
*/
public Node getLastChunk(Node node) throws RepositoryException {
// parent node containing all chunks and has mixin sling:chunks applied
// on it.
Node chunkParent = null;
Node jcrContentNode = null;
if (hasChunks(node)) {
chunkParent = node;
} else if (node.hasNode(JCR_CONTENT)
&& hasChunks((jcrContentNode = node.getNode(JCR_CONTENT)))) {
chunkParent = jcrContentNode;
}
String startPattern = SlingPostConstants.CHUNK_NODE_NAME + "_" + "0_*";
NodeIterator nodeItr = chunkParent.getNodes(startPattern);
Node chunkNode = null;
while (nodeItr.hasNext()) {
if (nodeItr.getSize() > 1) {
throw new RepositoryException(
"more than one node found for pattern: " + startPattern);
}
chunkNode = nodeItr.nextNode();
String[] indexBounds = chunkNode.getName().substring(
(SlingPostConstants.CHUNK_NODE_NAME + "_").length()).split("_");
startPattern = SlingPostConstants.CHUNK_NODE_NAME + "_"
+ String.valueOf(Long.valueOf(indexBounds[1]) + 1) + "_*";
nodeItr = chunkParent.getNodes(startPattern);
}
return chunkNode;
}
/**
* Return true if node has chunks stored in it, otherwise false.
*/
private boolean hasChunks(final Node node) throws RepositoryException {
for (NodeType nodeType : node.getMixinNodeTypes()) {
if (nodeType.getName().equals(
SlingPostConstants.NT_SLING_CHUNK_MIXIN)) {
return true;
}
}
return false;
}
private static final String MT_APP_OCTET = "application/octet-stream";
/**
* Uses the file(s) in the request parameter for creation of new nodes.
* if the parent node is a nt:folder a new nt:file is created. otherwise
* just a nt:resource. if the <code>name</code> is '*', the filename of
* the uploaded file is used.
*
* @param parent the parent node
* @param prop the assembled property info
* @throws RepositoryException if an error occurs
*/
public void setFile(final Resource parent, final RequestProperty prop, final List<Modification> changes)
throws RepositoryException, PersistenceException {
for (final RequestParameter value : prop.getValues()) {
// ignore if a plain form field or empty
if (value.isFormField() || value.getSize() <= 0) {
continue;
}
// get node name
String name = prop.getName();
if (name.equals("*")) {
name = value.getFileName();
// strip of possible path (some browsers include the entire path)
name = name.substring(name.lastIndexOf('/') + 1);
name = name.substring(name.lastIndexOf('\\') + 1);
}
name = Text.escapeIllegalJcrChars(name);
// get content type
String contentType = value.getContentType();
if (contentType != null) {
int idx = contentType.indexOf(';');
if (idx > 0) {
contentType = contentType.substring(0, idx);
}
}
if (contentType == null || contentType.equals(MT_APP_OCTET)) {
// try to find a better content type
ServletContext ctx = this.servletContext;
if (ctx != null) {
contentType = ctx.getMimeType(value.getFileName());
}
if (contentType == null || contentType.equals(MT_APP_OCTET)) {
contentType = MT_APP_OCTET;
}
}
final Node node = parent.adaptTo(Node.class);
if ( node == null ) {
this.setFile(parent, prop, value, changes, name, contentType);
} else {
this.setFile(parent, node, prop, value, changes, name, contentType);
}
}
}
private Resource getOrCreateChildResource(final Resource parent, final String name,
final String typeHint,
final List<Modification> changes)
throws PersistenceException, RepositoryException {
Resource result = parent.getChild(name);
if ( result != null ) {
final Node existing = result.adaptTo(Node.class);
if ( existing != null && !existing.isNodeType(typeHint)) {
existing.remove();
result = createWithChanges(parent, name, typeHint, changes);
}
} else {
result = createWithChanges(parent, name, typeHint, changes);
}
return result;
}
private Resource createWithChanges(final Resource parent, final String name,
final String typeHint,
final List<Modification> changes)
throws PersistenceException {
Map<String, Object> properties = null;
if ( typeHint != null ) {
properties = new HashMap<String, Object>();
if ( parent.adaptTo(Node.class) != null ) {
properties.put("jcr:primaryType", typeHint);
} else {
properties.put("sling:resourceType", typeHint);
}
}
final Resource result = parent.getResourceResolver().create(parent, name, properties);
changes.add(Modification.onCreated(result.getPath()));
return result;
}
}