blob: bc74a98079703cdc5e76353dd7343126e07970fc [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.JcrConstants;
import org.apache.jackrabbit.webdav.DavResource;
import javax.jcr.Node;
import javax.jcr.RepositoryException;
import javax.jcr.Property;
import javax.jcr.Item;
import javax.jcr.NodeIterator;
import java.util.Calendar;
import java.util.Date;
import java.io.IOException;
import java.io.InputStream;
/**
* <code>DefaultHandler</code> implements a simple IOHandler that creates 'file'
* and 'folder' nodes. This handler will create the following nodes:
* <ul>
* <li>New <b>Collection</b>: creates a new node with the {@link #getCollectionNodeType()
* collection nodetype}. The name of the node corresponds to the systemId
* present on the import context.</li>
*
* <li>New <b>Non-Collection</b>: first creates a new node with the {@link #getNodeType()
* non-collection nodetype}. The name of the node corresponds to the systemId
* present on the import context. Below it creates a node with name
* {@link JcrConstants#JCR_CONTENT jcr:content} and the nodetype specified
* by {@link #getContentNodeType()}.</li>
* </ul>
* <p/>
* Import of the content:<br>
* The content is imported to the {@link JcrConstants#JCR_DATA} property of the
* content node. By default this handler will fail on a attempt to create/replace
* a collection if {@link ImportContext#hasStream()} is <code>true</code>.
* Subclasses therefore should provide their own {@link #importData(ImportContext, boolean, Node)
* importData} method, that handles the data according their needs.
*/
public class DefaultHandler implements IOHandler {
private static Logger log = Logger.getLogger(DefaultHandler.class);
private String collectionNodetype = JcrConstants.NT_FOLDER;
private String defaultNodetype = JcrConstants.NT_FILE;
private String contentNodetype = JcrConstants.NT_RESOURCE;
private IOManager ioManager;
/**
* Creates a new <code>DefaultHandler</code> with default nodetype definitions:<br>
* <ul>
* <li>Nodetype for Collection: {@link JcrConstants#NT_FOLDER nt:folder}</li>
* <li>Nodetype for Non-Collection: {@link JcrConstants#NT_FILE nt:file}</li>
* <li>Nodetype for Non-Collection content: {@link JcrConstants#NT_RESOURCE nt:resource}</li>
* </ul>
*
* @param ioManager
*/
public DefaultHandler(IOManager ioManager) {
this.ioManager = ioManager;
}
/**
* Creates a new <code>DefaultHandler</code>. Please note that the specified
* nodetypes must match the definitions of the defaults.
*
* @param ioManager
* @param collectionNodetype
* @param defaultNodetype
* @param contentNodetype
*/
public DefaultHandler(IOManager ioManager, String collectionNodetype, String defaultNodetype, String contentNodetype) {
this.ioManager = ioManager;
this.collectionNodetype = collectionNodetype;
this.defaultNodetype = defaultNodetype;
this.contentNodetype = contentNodetype;
}
/**
* @see IOHandler#getIOManager()
*/
public IOManager getIOManager() {
return ioManager;
}
/**
* @see IOHandler#getName()
*/
public String getName() {
return getClass().getName();
}
/**
* @see IOHandler#canImport(ImportContext, boolean)
*/
public boolean canImport(ImportContext context, boolean isCollection) {
if (context == null || context.isCompleted()) {
return false;
}
Item contextItem = context.getImportRoot();
return contextItem != null && contextItem.isNode() && context.getSystemId() != null;
}
/**
* @see IOHandler#canImport(ImportContext, DavResource)
*/
public boolean canImport(ImportContext context, DavResource resource) {
if (resource == null) {
return false;
}
return canImport(context, resource.isCollection());
}
/**
* @see IOHandler#importContent(ImportContext, boolean)
*/
public boolean importContent(ImportContext context, boolean isCollection) throws IOException {
if (!canImport(context, isCollection)) {
throw new IOException(getName() + ": Cannot import " + context.getSystemId());
}
boolean success = false;
try {
Node contentNode = getContentNode(context, isCollection);
success = importData(context, isCollection, contentNode);
if (success) {
success = importProperties(context, isCollection, contentNode);
}
} catch (RepositoryException e) {
success = false;
throw new IOException(e.getMessage());
} finally {
// revert any changes made in case the import failed.
if (!success) {
try {
context.getImportRoot().refresh(false);
} catch (RepositoryException e) {
throw new IOException(e.getMessage());
}
}
}
return success;
}
/**
* @see IOHandler#importContent(ImportContext, DavResource)
*/
public boolean importContent(ImportContext context, DavResource resource) throws IOException {
if (!canImport(context, resource)) {
throw new IOException(getName() + ": Cannot import " + context.getSystemId());
}
return importContent(context, resource.isCollection());
}
/**
* Imports the data present on the import context to the specified content
* node.
*
* @param context
* @param isCollection
* @param contentNode
* @return
* @throws IOException
*/
protected boolean importData(ImportContext context, boolean isCollection, Node contentNode) throws IOException, RepositoryException {
InputStream in = context.getInputStream();
if (in != null) {
// NOTE: with the default folder-nodetype (nt:folder) no inputstream
// is allowed. setting the property would therefore fail.
if (isCollection) {
return false;
}
try {
contentNode.setProperty(JcrConstants.JCR_DATA, in);
} finally {
in.close();
}
}
// success if no data to import.
return true;
}
/**
* Imports the properties present on the specified context to the content
* node.
*
* @param context
* @param isCollection
* @param contentNode
* @return
*/
protected boolean importProperties(ImportContext context, boolean isCollection, Node contentNode) {
try {
// if context-mimetype is null -> remove the property
contentNode.setProperty(JcrConstants.JCR_MIMETYPE, context.getMimeType());
} catch (RepositoryException e) {
// ignore: property may not be present on the node
}
try {
// if context-encoding is null -> remove the property
contentNode.setProperty(JcrConstants.JCR_ENCODING, context.getEncoding());
} catch (RepositoryException e) {
// ignore: property may not be present on the node
}
try {
Calendar lastMod = Calendar.getInstance();
if (context.getModificationTime() != IOUtil.UNDEFINED_TIME) {
lastMod.setTimeInMillis(context.getModificationTime());
} else {
lastMod.setTime(new Date());
}
contentNode.setProperty(JcrConstants.JCR_LASTMODIFIED, lastMod);
} catch (RepositoryException e) {
// ignore: property may not be present on the node.
// deliberately not rethrowing as IOException.
}
return true;
}
/**
* Retrieves/creates the node that will be used to import properties and
* data. In case of a non-collection this includes and additional content node
* to be created beside the 'file' node.<br>
* Please note: If the jcr:content node already exists and contains child
* nodes, those will be removed in order to make sure, that the import
* really replaces the existing content of the file-node.
*
* @param context
* @param isCollection
* @return
* @throws RepositoryException
*/
protected Node getContentNode(ImportContext context, boolean isCollection) throws RepositoryException {
Node parentNode = (Node)context.getImportRoot();
String name = context.getSystemId();
if (parentNode.hasNode(name)) {
parentNode = parentNode.getNode(name);
} else {
String ntName = (isCollection) ? getCollectionNodeType() : getNodeType();
parentNode = parentNode.addNode(name, ntName);
}
Node contentNode;
if (isCollection) {
contentNode = parentNode;
} else {
if (parentNode.hasNode(JcrConstants.JCR_CONTENT)) {
contentNode = parentNode.getNode(JcrConstants.JCR_CONTENT);
// remove all entries in the jcr:content since replacing content
// includes properties (DefaultHandler) and nodes (e.g. ZipHandler)
if (contentNode.hasNodes()) {
NodeIterator it = contentNode.getNodes();
while (it.hasNext()) {
it.nextNode().remove();
}
}
} else {
contentNode = parentNode.addNode(JcrConstants.JCR_CONTENT, getContentNodeType());
}
}
return contentNode;
}
/**
* Returns true if the export root is a node and if it contains a child node
* with name {@link JcrConstants#JCR_CONTENT jcr:content} in case this
* export is not intended for a collection.
*
* @return true if the export root is a node. If the specified boolean paramter
* is false (not a collection export) the given export root must contain a
* child node with name {@link JcrConstants#JCR_CONTENT jcr:content}.
*
* @see IOHandler#canExport(ExportContext, boolean)
*/
public boolean canExport(ExportContext context, boolean isCollection) {
if (context == null || context.isCompleted()) {
return false;
}
Item exportRoot = context.getExportRoot();
boolean success = exportRoot != null && exportRoot.isNode();
if (success && !isCollection) {
try {
Node n = ((Node)exportRoot);
success = n.hasNode(JcrConstants.JCR_CONTENT);
} catch (RepositoryException e) {
// should never occur.
success = false;
}
}
return success;
}
/**
* @see IOHandler#canExport(ExportContext, DavResource)
*/
public boolean canExport(ExportContext context, DavResource resource) {
if (resource == null) {
return false;
}
return canExport(context, resource.isCollection());
}
/**
* Retrieves the content node that will be used for exporting properties and
* data and calls the corresponding methods.
*
* @param context
* @param isCollection
* @see #exportProperties(ExportContext, boolean, Node)
* @see #exportData(ExportContext, boolean, Node)
*/
public boolean exportContent(ExportContext context, boolean isCollection) throws IOException {
if (!canExport(context, isCollection)) {
throw new IOException(getName() + ": Cannot export " + context.getExportRoot());
}
try {
Node contentNode = getContentNode(context, isCollection);
exportProperties(context, isCollection, contentNode);
if (context.hasStream()) {
exportData(context, isCollection, contentNode);
} // else: missing stream. ignore.
return true;
} catch (RepositoryException e) {
// should never occur, since the proper structure of the content
// node must be asserted in the 'canExport' call.
throw new IOException(e.getMessage());
}
}
/**
* Same as (@link #exportContent(ExportContext, boolean)} where the boolean
* values is defined by {@link DavResource#isCollection()}.
*
* @see IOHandler#exportContent(ExportContext, DavResource)
*/
public boolean exportContent(ExportContext context, DavResource resource) throws IOException {
if (!canExport(context, resource)) {
throw new IOException(getName() + ": Cannot export " + context.getExportRoot());
}
return exportContent(context, resource.isCollection());
}
/**
* Checks if the given content node contains a jcr:data property
* and spools its value to the output stream fo the export context.<br>
* Please note, that subclasses that define a different structure of the
* content node should create their own
* {@link #exportData(ExportContext, boolean, Node) exportData} method.
*
* @param context
* @param isCollection
* @param contentNode
* @throws IOException
*/
protected void exportData(ExportContext context, boolean isCollection, Node contentNode) throws IOException, RepositoryException {
if (contentNode.hasProperty(JcrConstants.JCR_DATA)) {
Property p = contentNode.getProperty(JcrConstants.JCR_DATA);
IOUtil.spool(p.getStream(), context.getOutputStream());
} // else: stream undefined -> contentlength was not set
}
/**
* Retrieves mimetype, encoding and modification time from the content node.
* The content length is determined by the length of the jcr:data property
* if it is present. The creation time however is retrieved from the parent
* node (in case of isCollection == false only).
*
* @param context
* @param isCollection
* @param contentNode
*/
protected void exportProperties(ExportContext context, boolean isCollection, Node contentNode) throws IOException {
try {
// only non-collections: 'jcr:created' is present on the parent 'fileNode' only
if (!isCollection && contentNode.getDepth() > 0 && contentNode.getParent().hasProperty(JcrConstants.JCR_CREATED)) {
context.setCreationTime(contentNode.getParent().getProperty(JcrConstants.JCR_CREATED).getValue().getLong());
}
long length = IOUtil.UNDEFINED_LENGTH;
if (contentNode.hasProperty(JcrConstants.JCR_DATA)) {
Property p = contentNode.getProperty(JcrConstants.JCR_DATA);
length = p.getLength();
context.setContentLength(length);
}
String mimeType = null;
String encoding = null;
if (contentNode.hasProperty(JcrConstants.JCR_MIMETYPE)) {
mimeType = contentNode.getProperty(JcrConstants.JCR_MIMETYPE).getString();
}
if (contentNode.hasProperty(JcrConstants.JCR_ENCODING)) {
encoding = contentNode.getProperty(JcrConstants.JCR_ENCODING).getString();
// ignore "" encodings (although this is avoided during import)
if ("".equals(encoding)) {
encoding = null;
}
}
context.setContentType(mimeType, encoding);
long modTime = IOUtil.UNDEFINED_TIME;
if (contentNode.hasProperty(JcrConstants.JCR_LASTMODIFIED)) {
modTime = contentNode.getProperty(JcrConstants.JCR_LASTMODIFIED).getLong();
context.setModificationTime(modTime);
} else {
context.setModificationTime(System.currentTimeMillis());
}
if (length > IOUtil.UNDEFINED_LENGTH && modTime > IOUtil.UNDEFINED_TIME) {
String etag = "\"" + length + "-" + modTime + "\"";
context.setETag(etag);
}
} catch (RepositoryException e) {
// should never occur
throw new IOException(e.getMessage());
}
}
/**
* Retrieves the content node that contains the data to be exported. In case
* isCollection is true, this corresponds to the export root. Otherwise there
* must be a child node with name {@link JcrConstants#JCR_CONTENT jcr:content}.
*
* @param context
* @param isCollection
* @return content node used for the export
* @throws RepositoryException
*/
protected Node getContentNode(ExportContext context, boolean isCollection) throws RepositoryException {
Node contentNode = (Node)context.getExportRoot();
// 'file' nodes must have an jcr:content child node (see canExport)
if (!isCollection) {
contentNode = contentNode.getNode(JcrConstants.JCR_CONTENT);
}
return contentNode;
}
/**
* Name of the nodetype to be used to create a new collection node (folder)
*
* @return nodetype name
*/
protected String getCollectionNodeType() {
return collectionNodetype;
}
/**
* Name of the nodetype to be used to create a new non-collection node (file)
*
* @return nodetype name
*/
protected String getNodeType() {
return defaultNodetype;
}
/**
* Name of the nodetype to be used to create the content node below
* a new non-collection node, whose name is always {@link JcrConstants#JCR_CONTENT
* jcr:content}.
*
* @return nodetype name
*/
protected String getContentNodeType() {
return contentNodetype;
}
}