blob: 0844afcd39a1785f583f25513872e0726190d2eb [file] [log] [blame]
/*
* Copyright 2005 The Apache Software Foundation.
*
* Licensed 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.jackrabbit.server.io;
import org.apache.log4j.Logger;
import org.apache.jackrabbit.util.Text;
import org.apache.jackrabbit.JcrConstants;
import javax.jcr.Node;
import javax.jcr.RepositoryException;
import javax.jcr.NodeIterator;
import javax.jcr.Item;
import javax.jcr.Property;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
/**
* <code>ZipHandler</code> imports and extracts Zip files and exported nodes
* (an their subnodes) to a Zip file. Please not that for the export the selected
* export root must have the property {@link #ZIP_MIMETYPE} defined with its
* content. Furthermore the content must not represent a zip-file that has
* been imported to a binary {@link Property property}, which is properly
* handled by the {@link DefaultHandler}.
*/
public class ZipHandler extends DefaultHandler {
private static Logger log = Logger.getLogger(ZipHandler.class);
/**
* the zip mimetype
*/
public static final String ZIP_MIMETYPE = "application/zip";
private boolean intermediateSave;
/**
* Creates a new <code>ZipHandler</code> with default nodetype definitions:<br>
* <ul>
* <li>Nodetype for Collection: {@link JcrConstants#NT_UNSTRUCTURED nt:unstructured}</li>
* <li>Nodetype for Non-Collection: {@link JcrConstants#NT_FILE nt:file}</li>
* <li>Nodetype for Non-Collection content: {@link JcrConstants#NT_UNSTRUCTURED nt:unstructured}</li>
* </ul>
*
* @param ioManager
* @throws IllegalArgumentException if the specified <code>IOManager</code>
* is <code>null</code>
*/
public ZipHandler(IOManager ioManager) {
this(ioManager, JcrConstants.NT_FOLDER, JcrConstants.NT_FILE, JcrConstants.NT_UNSTRUCTURED);
}
/**
* Creates a new <code>ZipHandler</code>
*
* @param ioManager
* @param collectionNodetype
* @param defaultNodetype
* @param contentNodetype
* @throws IllegalArgumentException if the specified <code>IOManager</code>
* is <code>null</code>
*/
public ZipHandler(IOManager ioManager, String collectionNodetype, String defaultNodetype, String contentNodetype) {
super(ioManager, collectionNodetype, defaultNodetype, contentNodetype);
if (ioManager == null) {
throw new IllegalArgumentException("The IOManager must not be null.");
}
}
/**
* If set to <code>true</code> the import root will be {@link Item#save() saved}
* after every imported zip entry. Note however, that this removes the possibility
* to revert all modifications if the import cannot be completed successfully.
* By default the intermediate save is disabled.
*
* @param intermediateSave
*/
public void setIntermediateSave(boolean intermediateSave) {
this.intermediateSave = intermediateSave;
}
/**
* @see IOHandler#canImport(ImportContext, boolean)
*/
public boolean canImport(ImportContext context, boolean isCollection) {
if (context == null || context.isCompleted()) {
return false;
}
boolean isZip = ZIP_MIMETYPE.equals(context.getMimeType());
return isZip && context.hasStream() && super.canImport(context, isCollection);
}
/**
* @see DefaultHandler#importData(ImportContext, boolean, Node)
*/
protected boolean importData(ImportContext context, boolean isCollection, Node contentNode) throws IOException, RepositoryException {
boolean success = true;
InputStream in = context.getInputStream();
ZipInputStream zin = new ZipInputStream(in);
try {
ZipEntry entry;
while ((entry=zin.getNextEntry())!=null && success) {
importZipEntry(zin, entry, context, contentNode);
zin.closeEntry();
}
} finally {
zin.close();
in.close();
}
return success;
}
/**
* @see IOHandler#canExport(ExportContext, boolean)
*/
public boolean canExport(ExportContext context, boolean isCollection) {
if (super.canExport(context, isCollection)) {
// mimetype must be application/zip
String mimeType = null;
// if zip-content has not been extracted -> delegate to some other handler
boolean hasDataProperty = false;
try {
Node contentNode = getContentNode(context, isCollection);
// jcr:data property indicates that the zip-file has been imported as binary (not extracted)
hasDataProperty = contentNode.hasProperty(JcrConstants.JCR_DATA);
if (contentNode.hasProperty(JcrConstants.JCR_MIMETYPE)) {
mimeType = contentNode.getProperty(JcrConstants.JCR_MIMETYPE).getString();
} else {
mimeType = IOUtil.MIME_RESOLVER.getMimeType(context.getExportRoot().getName());
}
} catch (RepositoryException e) {
// ignore and return false
}
return ZIP_MIMETYPE.equals(mimeType) && !hasDataProperty;
}
return false;
}
/**
* @see DefaultHandler#exportData(ExportContext,boolean,Node)
*/
protected void exportData(ExportContext context, boolean isCollection, Node contentNode) throws IOException, RepositoryException {
ZipOutputStream zout = new ZipOutputStream(context.getOutputStream());
zout.setMethod(ZipOutputStream.DEFLATED);
try {
exportZipEntry(context, zout, contentNode, contentNode.getPath().length()+1);
} finally {
zout.finish();
}
}
/**
* If the specified node is the defined non-collection nodetype a new
* Zip entry is created and the exportContent is called on the IOManager
* defined with this handler. If in contrast the specified node does not
* represent a non-collection this method is called recursively for all
* child nodes.
*
* @param context
* @param zout
* @param node
* @param pos
* @throws IOException
*/
private void exportZipEntry(ExportContext context, ZipOutputStream zout, Node node, int pos) throws IOException{
try {
if (node.isNodeType(getNodeType())) {
ZipEntryExportContext subctx = new ZipEntryExportContext(node, zout, context, pos);
// try if iomanager can treat node as zip entry otherwise recurse.
zout.putNextEntry(subctx.entry);
getIOManager().exportContent(subctx, false);
} else {
// recurse
NodeIterator niter = node.getNodes();
while (niter.hasNext()) {
exportZipEntry(context, zout, niter.nextNode(), pos);
}
}
} catch (RepositoryException e) {
log.fatal(e.getMessage());
// should never occur
}
}
/**
* Creates a new sub context for the specified Zip entry and passes it to
* the IOManager defined with this handler.
*
* @param zin
* @param entry
* @param context
* @param node
* @return
* @throws RepositoryException
* @throws IOException
*/
private boolean importZipEntry(ZipInputStream zin, ZipEntry entry, ImportContext context, Node node) throws RepositoryException, IOException {
boolean success = false;
log.debug("entry: " + entry.getName() + " size: " + entry.getSize());
if (entry.isDirectory()) {
IOUtil.mkDirs(node, makeValidJCRPath(entry.getName(), false), getCollectionNodeType());
} else {
// import zip entry as file
BoundedInputStream bin = new BoundedInputStream(zin);
bin.setPropagateClose(false);
ImportContext entryContext = new ZipEntryImportContext(context, entry, bin, node);
// let the iomanager deal with the individual entries.
IOManager ioManager = getIOManager();
success = (ioManager != null) ? ioManager.importContent(entryContext, false) : false;
// intermediate save in order to avoid problems with large zip files
if (intermediateSave) {
context.getImportRoot().save();
}
}
return success;
}
/**
* Creates a valid jcr label from the given one
*
* @param label
* @return
*/
private static String makeValidJCRPath(String label, boolean appendLeadingSlash) {
if (appendLeadingSlash && !label.startsWith("/")) {
label = "/" + label;
}
StringBuffer ret = new StringBuffer(label.length());
for (int i=0; i<label.length(); i++) {
char c = label.charAt(i);
if (c=='*' || c=='\'' || c=='\"') {
c='_';
/* not quite correct: [] may be the index of a previously exported item. */
} else if (c=='[') {
c='(';
} else if (c==']') {
c=')';
}
ret.append(c);
}
return ret.toString();
}
//--------------------------------------------------------< inner class >---
/**
* Inner class used to create subcontexts for the import of the individual
* zip file entries.
*/
private class ZipEntryImportContext extends ImportContextImpl {
private final Item importRoot;
private final ZipEntry entry;
private ZipEntryImportContext(ImportContext context, ZipEntry entry, BoundedInputStream bin, Node contentNode) throws IOException, RepositoryException {
super(contentNode, Text.getName(makeValidJCRPath(entry.getName(), true)), bin, context.getIOListener());
this.entry = entry;
String path = makeValidJCRPath(entry.getName(), true);
importRoot = IOUtil.mkDirs(contentNode, Text.getRelativeParent(path, 1), getCollectionNodeType());
}
public Item getImportRoot() {
return importRoot;
}
public long getModificationTime() {
return entry.getTime();
}
public long getContentLength() {
return entry.getSize();
}
}
/**
* Inner class used to create subcontexts for the export of the individual
* zip file entries.
*/
private class ZipEntryExportContext extends AbstractExportContext {
private ZipEntry entry;
private OutputStream out;
private ZipEntryExportContext(Item exportRoot, OutputStream out, ExportContext context, int pos) {
super(exportRoot, out != null, context.getIOListener());
this.out = out;
try {
String entryPath = (exportRoot.getPath().length() > pos) ? exportRoot.getPath().substring(pos) : "";
entry = new ZipEntry(entryPath);
} catch (RepositoryException e) {
// should never occur
}
}
/**
* Returns the Zip output stream. Note, that this context does not
* deal properly with multiple IOHandlers writing to the stream.
*
* @return
*/
public OutputStream getOutputStream() {
return out;
}
public void setContentType(String mimeType, String encoding) {
if (entry != null) {
entry.setComment(mimeType);
}
}
public void setContentLanguage(String contentLanguage) {
// ignore
}
public void setContentLength(long contentLength) {
if (entry != null) {
entry.setSize(contentLength);
}
}
public void setCreationTime(long creationTime) {
// ignore
}
public void setModificationTime(long modificationTime) {
if (entry != null) {
entry.setTime(modificationTime);
}
}
public void setETag(String etag) {
// ignore
}
public void setProperty(Object propertyName, Object propertyValue) {
// ignore
}
}
}