/*
* Copyright (C) 2009 eXo Platform SAS.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.exoplatform.services.jcr.webdav.command;
import org.apache.commons.lang.StringUtils;
import org.exoplatform.common.http.HTTPStatus;
import org.exoplatform.services.jcr.ext.utils.VersionHistoryUtils;
import org.exoplatform.services.jcr.webdav.MimeTypeRecognizer;
import org.exoplatform.services.jcr.webdav.lock.NullResourceLocksHolder;
import org.exoplatform.services.jcr.webdav.util.TextUtil;
import org.exoplatform.services.log.ExoLogger;
import org.exoplatform.services.log.Log;
import java.io.InputStream;
import java.util.Calendar;
import java.util.List;
import javax.jcr.AccessDeniedException;
import javax.jcr.Node;
import javax.jcr.PathNotFoundException;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.lock.LockException;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;
/**
* Created by The eXo Platform SAS.
* @author Vitaly Guly - gavrikvetal@gmail.com
*
* @version $Id: $
*/
public class PutCommand
{
/**
* Logger.
*/
private static Log log = ExoLogger.getLogger("exo.jcr.component.webdav.command.PutCommand");
/**
* resource locks.
*/
private final NullResourceLocksHolder nullResourceLocks;
/**
* Provides URI information needed for 'location' header in 'CREATED'
* response
*/
private final UriBuilder uriBuilder;
/**
* To access mime-type and encoding
*/
private final MimeTypeRecognizer mimeTypeRecognizer;
private final String CHECKOUT_CHECKIN = "checkout-checkin";
private final String CHECKIN_CHECKOUT = "checkin-checkout";
private final String CHECKOUT = "checkout";
/**
* Constructor.
*
* @param nullResourceLocks resource locks.
* @param uriBuilder - provides data used in 'location' header
* @param mimeTypeRecognizer - provides mime-type recognizer
*/
public PutCommand(NullResourceLocksHolder nullResourceLocks, UriBuilder uriBuilder,
MimeTypeRecognizer mimeTypeRecognizer)
{
this.nullResourceLocks = nullResourceLocks;
this.uriBuilder = uriBuilder;
this.mimeTypeRecognizer = mimeTypeRecognizer;
}
/**
* Webdav Put method implementation.
*
* @param session current session
* @param path resource path
* @param inputStream stream that contains resource content
* @param fileNodeType the node type of file node
* @param contentNodeType the node type of content
* @param mixins the list of mixins
* @param updatePolicyType update policy
* @param tokens tokens
* @return the instance of javax.ws.rs.core.Response
*/
public Response put(Session session, String path, InputStream inputStream, String fileNodeType,
String contentNodeType, List<String> mixins, String updatePolicyType, String autoVersion, List<String> tokens)
{
try
{
Node node = null;
try
{
node = (Node)session.getItem(path);
}
catch (PathNotFoundException pexc)
{
nullResourceLocks.checkLock(session, path, tokens);
}
if (node == null)
{
node = session.getRootNode().addNode(TextUtil.relativizePath(path, false), fileNodeType);
// We set the new path
path = node.getPath();
node.addNode("jcr:content", contentNodeType);
updateContent(node, inputStream, mixins);
}
else
{
if ("add".equals(updatePolicyType))
{
node = session.getRootNode().getNode(TextUtil.relativizePath(path));
if (!node.isNodeType(VersionHistoryUtils.MIX_VERSIONABLE))
{
node = session.getRootNode().addNode(TextUtil.relativizePath(path, false), fileNodeType);
// We set the new path
path = node.getPath();
node.addNode("jcr:content", contentNodeType);
updateContent(node, inputStream, mixins);
}
else
{
updateVersion(node, inputStream, autoVersion, mixins);
}
}
else if ("create-version".equals(updatePolicyType))
{
createVersion(node, inputStream, autoVersion, mixins);
}
else
{
if (!node.isNodeType(VersionHistoryUtils.MIX_VERSIONABLE))
{
updateContent(node, inputStream, mixins);
}
else
{
updateVersion(node, inputStream, autoVersion, mixins);
}
}
}
session.save();
}
catch (LockException exc)
{
if(log.isDebugEnabled())
{
log.debug(exc.getMessage(), exc);
}
return Response.status(HTTPStatus.LOCKED).entity(exc.getMessage()).build();
}
catch (AccessDeniedException exc)
{
if(log.isDebugEnabled())
{
log.debug(exc.getMessage(), exc);
}
return Response.status(HTTPStatus.FORBIDDEN).entity(exc.getMessage()).build();
}
catch (RepositoryException exc)
{
if(log.isDebugEnabled())
{
log.debug(exc.getMessage(), exc);
}
return Response.status(HTTPStatus.CONFLICT).entity(exc.getMessage()).build();
}
if (uriBuilder != null)
{
return Response.created(uriBuilder.path(session.getWorkspace().getName()).path(path).build()).build();
}
// to save compatibility if uriBuilder is not provided
return Response.status(HTTPStatus.CREATED).build();
}
/**
* Webdav Put method implementation.
*
* @param session current session
* @param path resource path
* @param inputStream stream that contains resource content
* @param fileNodeType the node type of file node
* @param contentNodeType the node type of content
* @param mixins the list of mixins
* @param tokens tokens
* @param allowedAutoVersionPath
* @return the instance of javax.ws.rs.core.Response
*/
public Response put(Session session, String path, InputStream inputStream, String fileNodeType,
String contentNodeType, List<String> mixins, List<String> tokens, MultivaluedMap<String, String> allowedAutoVersionPath)
{
try
{
Node node = null;
boolean isVersioned;
try
{
node = (Node)session.getItem(path);
}
catch (PathNotFoundException pexc)
{
nullResourceLocks.checkLock(session, path, tokens);
}
if (node == null)
{
node = session.getRootNode().addNode(TextUtil.relativizePath(path, false), fileNodeType);
// We set the new path
path = node.getPath();
node.addNode("jcr:content", contentNodeType);
updateContent(node, inputStream, mixins);
isVersioned = isVersionSupported(node.getPath(), session.getWorkspace().getName(), allowedAutoVersionPath);
if (isVersioned && node.canAddMixin(VersionHistoryUtils.MIX_VERSIONABLE))
{
node.addMixin(VersionHistoryUtils.MIX_VERSIONABLE);
}
node.getSession().save();
}
else
{
isVersioned = isVersionSupported(node.getPath(), session.getWorkspace().getName(), allowedAutoVersionPath);
if (isVersioned)
{
VersionHistoryUtils.createVersion(node);
updateContent(node, inputStream, mixins);
}
else
{
updateContent(node, inputStream, mixins);
}
}
session.save();
}
catch (LockException exc)
{
if(log.isDebugEnabled())
{
log.debug(exc.getMessage(), exc);
}
return Response.status(HTTPStatus.LOCKED).entity(exc.getMessage()).build();
}
catch (AccessDeniedException exc)
{
if(log.isDebugEnabled())
{
log.debug(exc.getMessage(), exc);
}
return Response.status(HTTPStatus.FORBIDDEN).entity(exc.getMessage()).build();
}
catch (RepositoryException exc)
{
if(log.isDebugEnabled())
{
log.debug(exc.getMessage(), exc);
}
return Response.status(HTTPStatus.CONFLICT).entity(exc.getMessage()).build();
}
catch (Exception exc)
{
if(log.isDebugEnabled())
{
log.debug(exc.getMessage(), exc);
}
return Response.status(HTTPStatus.CONFLICT).entity(exc.getMessage()).build();
}
if (uriBuilder != null)
{
return Response.created(uriBuilder.path(session.getWorkspace().getName()).path(path).build()).build();
}
// to save compatibility if uriBuilder is not provided
return Response.status(HTTPStatus.CREATED).build();
}
/**
* Creates the new version of file.
*
* @param fileNode file node
* @param inputStream input stream that contains the content of file
* @param autoVersion auto-version value
* @param mixins list of mixins
* @throws RepositoryException {@link RepositoryException}
*/
private void createVersion(Node fileNode, InputStream inputStream, String autoVersion, List<String> mixins) throws RepositoryException
{
if (!fileNode.isNodeType(VersionHistoryUtils.MIX_VERSIONABLE))
{
if (fileNode.canAddMixin(VersionHistoryUtils.MIX_VERSIONABLE))
{
fileNode.addMixin(VersionHistoryUtils.MIX_VERSIONABLE);
fileNode.getSession().save();
}
if (!(CHECKIN_CHECKOUT.equals(autoVersion)))
{
fileNode.checkin();
fileNode.getSession().save();
}
}
if (CHECKIN_CHECKOUT.equals(autoVersion))
{
fileNode.checkin();
fileNode.checkout();
fileNode.getSession().save();
updateContent(fileNode, inputStream, mixins);
fileNode.getSession().save();
}
else
{
createVersion(fileNode, inputStream, mixins);
}
}
/**
* Creates the new version of file.
*
* @param fileNode file node
* @param inputStream input stream that contains the content of file
* @param mixins list of mixins
* @throws RepositoryException {@link RepositoryException}
*/
private void createVersion(Node fileNode, InputStream inputStream, List<String> mixins) throws RepositoryException
{
if (!fileNode.isCheckedOut())
{
fileNode.checkout();
fileNode.getSession().save();
}
updateContent(fileNode, inputStream, mixins);
fileNode.getSession().save();
fileNode.checkin();
fileNode.getSession().save();
}
/**
* Updates jcr:content node.
*
* @param node parent node
* @param inputStream inputStream input stream that contains the content of
* file
* @param mixins list of mixins
* @throws RepositoryException {@link RepositoryException}
*/
private void updateContent(Node node, InputStream inputStream, List<String> mixins) throws RepositoryException
{
Node content = node.getNode("jcr:content");
if (mimeTypeRecognizer.isMimeTypeRecognized() || !content.hasProperty("jcr:mimeType"))
{
content.setProperty("jcr:mimeType", mimeTypeRecognizer.getMimeType());
}
if (mimeTypeRecognizer.isEncodingSet())
{
content.setProperty("jcr:encoding", mimeTypeRecognizer.getEncoding());
}
content.setProperty("jcr:lastModified", Calendar.getInstance());
content.setProperty("jcr:data", inputStream);
for (String mixinName : mixins)
{
if (content.canAddMixin(mixinName))
{
content.addMixin(mixinName);
}
}
}
/**
* Updates the content of the versionable file according to auto-version value.
*
* @param fileNode Node to update
* @param inputStream input stream that contains the content of
* file
* @param autoVersion auto-version value
* @param mixins list of mixins
* @throws RepositoryException {@link RepositoryException}
*/
private void updateVersion(Node fileNode, InputStream inputStream, String autoVersion, List<String> mixins)
throws RepositoryException
{
if (!fileNode.isCheckedOut())
{
fileNode.checkout();
fileNode.getSession().save();
}
if (CHECKOUT.equals(autoVersion))
{
updateContent(fileNode, inputStream, mixins);
}
else if (CHECKOUT_CHECKIN.equals(autoVersion))
{
updateContent(fileNode, inputStream, mixins);
fileNode.getSession().save();
fileNode.checkin();
}
fileNode.getSession().save();
}
private boolean isVersionSupported(String nodePath, String workspaceName, MultivaluedMap<String, String> allowedAutoVersionPath)
{
if (StringUtils.isEmpty(nodePath) || allowedAutoVersionPath.isEmpty())
{
return false;
}
List<String> paths = allowedAutoVersionPath.get(workspaceName);
if(paths == null)
{
return false;
}
for (String p : paths)
{
if (!StringUtils.isEmpty(p) && nodePath.startsWith(p))
{
return true;
}
}
return false;
}
}