Java tutorial
/* * 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.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); 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; } if (chunkParent == null) { return null; } NodeIterator nodeItr = chunkParent.getNodes(SlingPostConstants.CHUNK_NODE_NAME + "_*"); Node chunkNode = null; long lastChunkStartIndex = -1; while (nodeItr.hasNext()) { Node currentNode = nodeItr.nextNode(); String[] indexBounds = currentNode.getName() .substring((SlingPostConstants.CHUNK_NODE_NAME + "_").length()).split("_"); long chunkStartIndex = Long.valueOf(indexBounds[0]); if (chunkStartIndex > lastChunkStartIndex) { chunkNode = currentNode; lastChunkStartIndex = chunkStartIndex; } } 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; } }