/* * 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; import org.exoplatform.common.http.HTTPStatus; import org.exoplatform.common.util.HierarchicalProperty; import org.exoplatform.commons.utils.MimeTypeResolver; import org.exoplatform.commons.utils.PropertyManager; import org.exoplatform.container.xml.InitParams; import org.exoplatform.services.jcr.RepositoryService; import org.exoplatform.services.jcr.core.ManageableRepository; import org.exoplatform.services.jcr.ext.app.SessionProviderService; import org.exoplatform.services.jcr.ext.common.SessionProvider; import org.exoplatform.services.jcr.ext.utils.VersionHistoryUtils; import org.exoplatform.services.jcr.webdav.command.AclCommand; import org.exoplatform.services.jcr.webdav.command.CopyCommand; import org.exoplatform.services.jcr.webdav.command.DeleteCommand; import org.exoplatform.services.jcr.webdav.command.GetCommand; import org.exoplatform.services.jcr.webdav.command.HeadCommand; import org.exoplatform.services.jcr.webdav.command.LockCommand; import org.exoplatform.services.jcr.webdav.command.MkColCommand; import org.exoplatform.services.jcr.webdav.command.MoveCommand; import org.exoplatform.services.jcr.webdav.command.OrderPatchCommand; import org.exoplatform.services.jcr.webdav.command.PropFindCommand; import org.exoplatform.services.jcr.webdav.command.PropPatchCommand; import org.exoplatform.services.jcr.webdav.command.PutCommand; import org.exoplatform.services.jcr.webdav.command.SearchCommand; import org.exoplatform.services.jcr.webdav.command.UnLockCommand; import org.exoplatform.services.jcr.webdav.command.deltav.CheckInCommand; import org.exoplatform.services.jcr.webdav.command.deltav.CheckOutCommand; import org.exoplatform.services.jcr.webdav.command.deltav.ReportCommand; import org.exoplatform.services.jcr.webdav.command.deltav.UnCheckOutCommand; import org.exoplatform.services.jcr.webdav.command.deltav.VersionControlCommand; import org.exoplatform.services.jcr.webdav.lock.NullResourceLocksHolder; import org.exoplatform.services.jcr.webdav.util.InitParamsDefaults; import org.exoplatform.services.jcr.webdav.util.NodeTypeUtil; import org.exoplatform.services.jcr.webdav.util.TextUtil; import org.exoplatform.services.log.ExoLogger; import org.exoplatform.services.log.Log; import org.exoplatform.services.rest.ExtHttpHeaders; import org.exoplatform.services.rest.ext.webdav.method.ACL; import org.exoplatform.services.rest.ext.webdav.method.CHECKIN; import org.exoplatform.services.rest.ext.webdav.method.CHECKOUT; import org.exoplatform.services.rest.ext.webdav.method.COPY; import org.exoplatform.services.rest.ext.webdav.method.LOCK; import org.exoplatform.services.rest.ext.webdav.method.MKCOL; import org.exoplatform.services.rest.ext.webdav.method.MOVE; import org.exoplatform.services.rest.ext.webdav.method.OPTIONS; import org.exoplatform.services.rest.ext.webdav.method.ORDERPATCH; import org.exoplatform.services.rest.ext.webdav.method.PROPFIND; import org.exoplatform.services.rest.ext.webdav.method.PROPPATCH; import org.exoplatform.services.rest.ext.webdav.method.REPORT; import org.exoplatform.services.rest.ext.webdav.method.SEARCH; import org.exoplatform.services.rest.ext.webdav.method.UNCHECKOUT; import org.exoplatform.services.rest.ext.webdav.method.UNLOCK; import org.exoplatform.services.rest.ext.webdav.method.VERSIONCONTROL; import org.exoplatform.services.rest.resource.ResourceContainer; import java.io.InputStream; import java.lang.annotation.Annotation; import java.lang.reflect.Method; import java.net.URI; import java.net.URISyntaxException; import java.util.ArrayList; import java.util.List; import java.util.Map; import javax.jcr.NoSuchWorkspaceException; import javax.jcr.PathNotFoundException; import javax.jcr.RepositoryException; import javax.jcr.Session; import javax.jcr.nodetype.NoSuchNodeTypeException; import javax.jcr.nodetype.NodeType; import javax.jcr.nodetype.NodeTypeManager; import javax.ws.rs.DELETE; import javax.ws.rs.GET; import javax.ws.rs.HEAD; import javax.ws.rs.HeaderParam; import javax.ws.rs.PUT; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.QueryParam; import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import javax.ws.rs.core.UriInfo; /** * Created by The eXo Platform SAS. * WebDavServiceImpl is the WebDav connector on top of eXo JCR. * It allows to execute CRUD operations on the JCR thanks to the Webdav protocol. * * @author Gennady Azarenkov */ @Path("/jcr") public class WebDavServiceImpl implements WebDavService, ResourceContainer { /** * Logger. */ private static Log log = ExoLogger.getLogger("exo.jcr.component.webdav.WebDavServiceImpl"); /** * Local Thread SessionProvider. */ private final SessionProviderService sessionProviderService; /** * Repository service. */ private final RepositoryService repositoryService; /** * NullResourceLocksHolder. */ private final NullResourceLocksHolder nullResourceLocks; /** * Encapsulates WebDAV service initial parameters. */ protected WebDavServiceInitParams webDavServiceInitParams; /** * The list of allowed methods. */ private static final String ALLOW; private final MimeTypeResolver mimeTypeResolver; static { StringBuffer sb = new StringBuffer(); for (Method m : WebDavServiceImpl.class.getMethods()) { for (Annotation a : m.getAnnotations()) { javax.ws.rs.HttpMethod ma = null; if ((ma = a.annotationType().getAnnotation(javax.ws.rs.HttpMethod.class)) != null) { if (sb.length() > 0) sb.append(", "); sb.append(ma.value()); } } } ALLOW = sb.toString(); } /** * Constructor. * * @param params Initialization parameters * @param repositoryService repository service * @param sessionProviderService session provider service * @throws Exception {@link Exception} */ public WebDavServiceImpl(InitParams params, RepositoryService repositoryService, SessionProviderService sessionProviderService) throws Exception { this(repositoryService, sessionProviderService); this.webDavServiceInitParams = new WebDavServiceInitParams(params); } /** * Constructor. * * @param params Initialization params * @param repositoryService repository service * @param sessionProviderService session provider service */ protected WebDavServiceImpl(Map<String, String> params, RepositoryService repositoryService, SessionProviderService sessionProviderService) throws Exception { this(repositoryService, sessionProviderService); this.webDavServiceInitParams = new WebDavServiceInitParams(params); } /** * Constructor. * * @param repositoryService repository service * @param sessionProviderService session provider service */ protected WebDavServiceImpl(RepositoryService repositoryService, SessionProviderService sessionProviderService) { this.sessionProviderService = sessionProviderService; this.repositoryService = repositoryService; this.nullResourceLocks = new NullResourceLocksHolder(); this.mimeTypeResolver = new MimeTypeResolver(); this.mimeTypeResolver.setDefaultMimeType(InitParamsDefaults.FILE_MIME_TYPE); this.webDavServiceInitParams = new WebDavServiceInitParams(); } /** * @param repoName repository name * @param repoPath path in repository * @param lockTokenHeader Lock-Token HTTP header * @param ifHeader If- HTTP Header * @return the instance of javax.ws.rs.core.Response * @LevelAPI Platform */ @CHECKIN @Path("/{repoName}/{repoPath:.*}/") public Response checkin(@PathParam("repoName") String repoName, @PathParam("repoPath") String repoPath, @HeaderParam(ExtHttpHeaders.LOCKTOKEN) String lockTokenHeader, @HeaderParam(ExtHttpHeaders.IF) String ifHeader) { if (log.isDebugEnabled()) { log.debug("CHECKIN " + repoName + "/" + repoPath); } repoPath = normalizePath(repoPath); Session session; try { session = session(repoName, workspaceName(repoPath), lockTokens(lockTokenHeader, ifHeader)); return new CheckInCommand().checkIn(session, path(repoPath)); } catch (Exception exc) { log.error(exc.getMessage(), exc); return Response.serverError().entity(exc.getMessage()).build(); } } /** * @param repoName repository name * @param repoPath path in repository * @param lockTokenHeader Lock-Token HTTP header * @param ifHeader If- HTTP Header * @return the instance of javax.ws.rs.core.Response * @LevelAPI Platform */ @CHECKOUT @Path("/{repoName}/{repoPath:.*}/") public Response checkout(@PathParam("repoName") String repoName, @PathParam("repoPath") String repoPath, @HeaderParam(ExtHttpHeaders.LOCKTOKEN) String lockTokenHeader, @HeaderParam(ExtHttpHeaders.IF) String ifHeader) { if (log.isDebugEnabled()) { log.debug("CHECKOUT " + repoName + "/" + repoPath); } repoPath = normalizePath(repoPath); Session session; try { session = session(repoName, workspaceName(repoPath), lockTokens(lockTokenHeader, ifHeader)); return new CheckOutCommand().checkout(session, path(repoPath)); } catch (Exception exc) { log.error(exc.getMessage(), exc); return Response.serverError().entity(exc.getMessage()).build(); } } /** * @param repoName repository name * @param repoPath path in repository * @param destinationHeader Destination HTTP Header * @param lockTokenHeader Lock-Token HTTP header * @param ifHeader If- HTTP Header * @param depthHeader Depth HTTP header * @param overwriteHeader Overwrite HTTP header * @param uriInfo base URI info * @param body Request body * @return the instance of javax.ws.rs.core.Response * @LevelAPI Platform */ @COPY @Path("/{repoName}/{repoPath:.*}/") public Response copy(@PathParam("repoName") String repoName, @PathParam("repoPath") String repoPath, @HeaderParam(ExtHttpHeaders.DESTINATION) String destinationHeader, @HeaderParam(ExtHttpHeaders.LOCKTOKEN) String lockTokenHeader, @HeaderParam(ExtHttpHeaders.IF) String ifHeader, @HeaderParam(ExtHttpHeaders.DEPTH) String depthHeader, @HeaderParam(ExtHttpHeaders.OVERWRITE) String overwriteHeader, @Context UriInfo uriInfo, HierarchicalProperty body) { // to trace if an item on destination path exists boolean itemExisted = false; if (log.isDebugEnabled()) { log.debug("COPY " + repoName + "/" + repoPath); } repoPath = normalizePath(repoPath); try { String serverURI = uriInfo.getBaseUriBuilder().path(getClass()).path(repoName).build().toString(); // destinationHeader could begins from workspace name (passed from cms // WebDAVServiceImpl) and doesn't contain neither host no repository name URI dest = buildURI(destinationHeader); URI base = buildURI(serverURI); String destPath = dest.getPath(); int repoIndex = destPath.indexOf(repoName); // check if destination corresponds to base uri // if the destination is on another server // or destination header is malformed // we return BAD_GATEWAY(502) HTTP status // more info here http://www.webdav.org/specs/rfc2518.html#METHOD_COPY if (dest.getHost() != null && !base.getHost().equals(dest.getHost())) { return Response.status(HTTPStatus.BAD_GATEWAY).entity("Bad Gateway").build(); } destPath = normalizePath(repoIndex == -1 ? destPath : destPath.substring(repoIndex + repoName.length() + 1)); String srcWorkspace = workspaceName(repoPath); String srcNodePath = path(repoPath); String destWorkspace = workspaceName(destPath); String destNodePath = path(destPath, false); List<String> lockTokens = lockTokens(lockTokenHeader, ifHeader); Depth depth = new Depth(depthHeader); boolean overwrite = true; if (overwriteHeader != null && overwriteHeader.equalsIgnoreCase("F")) { overwrite = false; } repoName = getRepositoryName(repoName); if (overwrite) { Response delResponse = delete(repoName, destPath, lockTokenHeader, ifHeader); itemExisted = (delResponse.getStatus() == HTTPStatus.NO_CONTENT); } else { Session session = session(repoName, srcWorkspace, null); if (session.getRootNode().hasNode(TextUtil.relativizePath(path(destPath)))) { return Response.status(HTTPStatus.PRECON_FAILED) .entity("Item exists on destination path, while overwriting is forbidden").build(); } } if (depth.getStringValue().equalsIgnoreCase("infinity")) { if (srcWorkspace.equals(destWorkspace)) { Session session = session(repoName, destWorkspace, lockTokens); return new CopyCommand(uriInfo.getBaseUriBuilder().path(getClass()).path(repoName), itemExisted).copy( session, srcNodePath, destNodePath); } Session destSession = session(repoName, destWorkspace, lockTokens); return new CopyCommand(uriInfo.getBaseUriBuilder().path(getClass()).path(repoName), itemExisted).copy( destSession, srcWorkspace, srcNodePath, destNodePath); } else if (depth.getIntValue() == 0) { int nodeNameStart = srcNodePath.lastIndexOf('/') + 1; String nodeName = srcNodePath.substring(nodeNameStart); int indexStart = nodeName.indexOf('['); if (indexStart != -1) { nodeName = nodeName.substring(0, indexStart); } Session session = session(repoName, destWorkspace, lockTokens); return new MkColCommand(nullResourceLocks, uriInfo.getBaseUriBuilder().path(getClass()).path(repoName)) .mkCol(session, destNodePath + "/" + nodeName, webDavServiceInitParams.getDefaultFolderNodeType(), null, lockTokens); } else { return Response.status(HTTPStatus.BAD_REQUEST).entity("Bad Request").build(); } } catch (PreconditionException exc) { return Response.status(HTTPStatus.BAD_REQUEST).entity(exc.getMessage()).build(); } catch (NoSuchWorkspaceException e) { log.error("NoSuchWorkspaceException " + e.getMessage(), e); return Response.status(HTTPStatus.CONFLICT).entity(e.getMessage()).build(); } catch (Exception exc) { log.error(exc.getMessage(), exc); return Response.serverError().entity(exc.getMessage()).build(); } } /** * @param repoName repository name * @param repoPath path in repository * @param lockTokenHeader Lock-Token HTTP header * @param ifHeader If HTTP Header * @return the instance of javax.ws.rs.core.Response * @LevelAPI Platform */ @DELETE @Path("/{repoName}/{repoPath:.*}/") public Response delete(@PathParam("repoName") String repoName, @PathParam("repoPath") String repoPath, @HeaderParam(ExtHttpHeaders.LOCKTOKEN) String lockTokenHeader, @HeaderParam(ExtHttpHeaders.IF) String ifHeader) { if (log.isDebugEnabled()) { log.debug("DELETE " + repoName + "/" + repoPath); } repoPath = normalizePath(repoPath); try { Session session = session(repoName, workspaceName(repoPath), lockTokens(lockTokenHeader, ifHeader)); if (lockTokenHeader != null) { lockTokenHeader = lockTokenHeader.substring(1, lockTokenHeader.length() - 1); if (lockTokenHeader.contains(WebDavConst.Lock.OPAQUE_LOCK_TOKEN)) { lockTokenHeader = lockTokenHeader.split(":")[1]; } } return new DeleteCommand().delete(session, path(repoPath), lockTokenHeader); } catch (NoSuchWorkspaceException exc) { log.error("NoSuchWorkspaceException " + exc.getMessage(), exc); return Response.status(HTTPStatus.NOT_FOUND).entity(exc.getMessage()).build(); } catch (Exception exc) { log.error(exc.getMessage(), exc); return Response.serverError().entity(exc.getMessage()).build(); } } /** * WedDAV "GET" method. See <a href='http://www.ietf.org/rfc/rfc2518.txt'>HTTP * methods for distributed authoring sec. 8.4 "GET, HEAD for Collections"</a>. * * @param repoName repository name * @param repoPath path in repository * @param rangeHeader Range HTTP header * @param version version name * @param ifModifiedSince if-modified-since header * @param ifNoneMatch if-none-match header * @return the instance of javax.ws.rs.core.Response * @LevelAPI Platform */ @GET @Path("/{repoName}/{repoPath:.*}/") public Response get(@PathParam("repoName") String repoName, @PathParam("repoPath") String repoPath, @HeaderParam(ExtHttpHeaders.RANGE) String rangeHeader, @HeaderParam(ExtHttpHeaders.IF_MODIFIED_SINCE) String ifModifiedSince, @HeaderParam(ExtHttpHeaders.IF_NONE_MATCH) String ifNoneMatch, @QueryParam("version") String version, @Context UriInfo uriInfo) { if (log.isDebugEnabled()) { log.debug("GET " + repoName + "/" + repoPath); } repoPath = normalizePath(repoPath); try { repoName = getRepositoryName(repoName); Session session = session(repoName, workspaceName(repoPath), null); ArrayList<Range> ranges = new ArrayList<Range>(); if (rangeHeader != null) { if (log.isDebugEnabled()) { log.debug(rangeHeader); } if (rangeHeader.startsWith("bytes=")) { String rangeString = rangeHeader.substring(rangeHeader.indexOf("=") + 1); String[] tokens = rangeString.split(","); for (String token : tokens) { Range range = new Range(); token = token.trim(); int dash = token.indexOf("-"); if (dash == -1) { return Response.status(HTTPStatus.REQUESTED_RANGE_NOT_SATISFIABLE) .entity("Requested Range Not Satisfiable").build(); } else if (dash == 0) { range.setStart(Long.parseLong(token)); range.setEnd(-1L); } else if (dash > 0) { range.setStart(Long.parseLong(token.substring(0, dash))); if (dash < token.length() - 1) range.setEnd(Long.parseLong(token.substring(dash + 1, token.length()))); else range.setEnd(-1L); } ranges.add(range); } } } String uri = uriInfo.getBaseUriBuilder().path(getClass()).path(repoName).path(workspaceName(repoPath)).build() .toString(); return new GetCommand(webDavServiceInitParams.getXsltParams()).get(session, path(repoPath), version, uri, ranges, ifModifiedSince, ifNoneMatch, webDavServiceInitParams.getCacheControlMap()); } catch (PathNotFoundException exc) { log.error("NoSuchWorkspaceException " + exc.getMessage(), exc); return Response.status(HTTPStatus.NOT_FOUND).entity(exc.getMessage()).build(); } catch (Exception exc) { log.error(exc.getMessage(), exc); return Response.serverError().entity(exc.getMessage()).build(); } } /** * WedDAV "HEAD" method. see <a * href='http://www.ietf.org/rfc/rfc2518.txt'>HTTP methods for distributed * authoring sec. 8.4 "GET, HEAD for Collections"</a>. * * @param repoName repository name * @param repoPath path in repository * @return the instance of javax.ws.rs.core.Response * @LevelAPI Platform */ @HEAD @Path("/{repoName}/{repoPath:.*}/") public Response head(@PathParam("repoName") String repoName, @PathParam("repoPath") String repoPath, @Context UriInfo uriInfo) { if (log.isDebugEnabled()) { log.debug("HEAD " + repoName + "/" + repoPath); } repoPath = normalizePath(repoPath); try { repoName = getRepositoryName(repoName); Session session = session(repoName, workspaceName(repoPath), null); String uri = uriInfo.getBaseUriBuilder().path(getClass()).path(repoName).path(workspaceName(repoPath)).build() .toString(); return new HeadCommand().head(session, path(repoPath), uri); } catch (NoSuchWorkspaceException exc) { log.error("NoSuchWorkspaceException " + exc.getMessage(), exc); return Response.status(HTTPStatus.NOT_FOUND).entity(exc.getMessage()).build(); } catch (Exception exc) { log.error(exc.getMessage(), exc); return Response.serverError().entity(exc.getMessage()).build(); } } /** * @param repoName repository name * @param repoPath path in repository * @param lockTokenHeader Lock-Token HTTP header * @param ifHeader If- HTTP Header * @param depthHeader Depth HTTP header * @param body Request body * @return the instance of javax.ws.rs.core.Response * @LevelAPI Platform */ @LOCK @Path("/{repoName}/{repoPath:.*}/") public Response lock(@PathParam("repoName") String repoName, @PathParam("repoPath") String repoPath, @HeaderParam(ExtHttpHeaders.LOCKTOKEN) String lockTokenHeader, @HeaderParam(ExtHttpHeaders.IF) String ifHeader, @HeaderParam(ExtHttpHeaders.DEPTH) String depthHeader, HierarchicalProperty body) { if (log.isDebugEnabled()) { log.debug("LOCK " + repoName + "/" + repoPath); } repoPath = normalizePath(repoPath); try { Session session = session(repoName, workspaceName(repoPath), lockTokens(lockTokenHeader, ifHeader)); return new LockCommand(nullResourceLocks).lock(session, path(repoPath), body, new Depth(depthHeader), "86400"); } catch (PreconditionException exc) { log.error("PreconditionException " + exc.getMessage(), exc); return Response.status(HTTPStatus.PRECON_FAILED).entity(exc.getMessage()).build(); } catch (NoSuchWorkspaceException exc) { log.error("NoSuchWorkspaceException " + exc.getMessage(), exc); return Response.status(HTTPStatus.CONFLICT).entity(exc.getMessage()).build(); } catch (Exception exc) { log.error(exc.getMessage(), exc); return Response.serverError().entity(exc.getMessage()).build(); } } /** * @param repoName repository name * @param repoPath path in repository * @param lockTokenHeader Lock-Token HTTP header * @param ifHeader If- HTTP Header * @return the instance of javax.ws.rs.core.Response * @LevelAPI Platform */ @UNLOCK @Path("/{repoName}/{repoPath:.*}/") public Response unlock(@PathParam("repoName") String repoName, @PathParam("repoPath") String repoPath, @HeaderParam(ExtHttpHeaders.LOCKTOKEN) String lockTokenHeader, @HeaderParam(ExtHttpHeaders.IF) String ifHeader) { if (log.isDebugEnabled()) { log.debug("UNLOCK " + repoName + "/" + repoPath); } repoPath = normalizePath(repoPath); Session session; List<String> tokens = lockTokens(lockTokenHeader, ifHeader); try { session = session(repoName, workspaceName(repoPath), tokens); return new UnLockCommand(nullResourceLocks).unLock(session, path(repoPath), tokens); } catch (NoSuchWorkspaceException exc) { log.error("NoSuchWorkspaceException " + exc.getMessage(), exc); return Response.status(HTTPStatus.NOT_FOUND).entity(exc.getMessage()).build(); } catch (Exception exc) { log.error(exc.getMessage(), exc); return Response.serverError().entity(exc.getMessage()).build(); } } /** * @param repoName repository name * @param repoPath path in repository * @param lockTokenHeader Lock-Token HTTP header * @param ifHeader If- HTTP Header * @param folderNodeTypeHeader JCR Node-Type header * @param mixinTypesHeader JCR Mixin-Types header * @return the instance of javax.ws.rs.core.Response * @LevelAPI Platform */ @MKCOL @Path("/{repoName}/{repoPath:.*}/") public Response mkcol(@PathParam("repoName") String repoName, @PathParam("repoPath") String repoPath, @HeaderParam(ExtHttpHeaders.LOCKTOKEN) String lockTokenHeader, @HeaderParam(ExtHttpHeaders.IF) String ifHeader, @HeaderParam(ExtHttpHeaders.FOLDER_NODETYPE) String folderNodeTypeHeader, @HeaderParam(ExtHttpHeaders.CONTENT_MIXINTYPES) String mixinTypesHeader, @Context UriInfo uriInfo) { if (log.isDebugEnabled()) { log.debug("MKCOL " + repoName + "/" + repoPath); } repoPath = normalizePath(repoPath); try { repoName = getRepositoryName(repoName); List<String> tokens = lockTokens(lockTokenHeader, ifHeader); Session session = session(repoName, workspaceName(repoPath), tokens); String folderNodeType = NodeTypeUtil.getNodeType(folderNodeTypeHeader, webDavServiceInitParams.getDefaultFolderNodeType(), webDavServiceInitParams.getAllowedFolderNodeTypes()); return new MkColCommand(nullResourceLocks, uriInfo.getBaseUriBuilder().path(getClass()).path(repoName)).mkCol( session, path(repoPath, false), folderNodeType, NodeTypeUtil.getMixinTypes(mixinTypesHeader), tokens); } catch (NoSuchWorkspaceException exc) { log.error("NoSuchWorkspaceException " + exc.getMessage(), exc); return Response.status(HTTPStatus.CONFLICT).entity(exc.getMessage()).build(); } catch (NoSuchNodeTypeException exc) { log.error("NoSuchNodeTypeException " + exc.getMessage(), exc); return Response.status(HTTPStatus.UNSUPPORTED_TYPE).entity(exc.getMessage()).build(); } catch (Exception exc) { log.error(exc.getMessage(), exc); return Response.serverError().entity(exc.getMessage()).build(); } } /** * @param repoName repository name * @param repoPath path in repository * @param destinationHeader Destination HTTP header * @param lockTokenHeader Lock-Token HTTP header * @param ifHeader If- HTTP Header * @param depthHeader Depth HTTP header * @param overwriteHeader Overwrite HTTP header * @param uriInfo base URI info * @param body Request body * @return the instance of javax.ws.rs.core.Response * @LevelAPI Platform */ @MOVE @Path("/{repoName}/{repoPath:.*}/") public Response move(@PathParam("repoName") String repoName, @PathParam("repoPath") String repoPath, @HeaderParam(ExtHttpHeaders.DESTINATION) String destinationHeader, @HeaderParam(ExtHttpHeaders.LOCKTOKEN) String lockTokenHeader, @HeaderParam(ExtHttpHeaders.IF) String ifHeader, @HeaderParam(ExtHttpHeaders.DEPTH) String depthHeader, @HeaderParam(ExtHttpHeaders.OVERWRITE) String overwriteHeader, @Context UriInfo uriInfo, HierarchicalProperty body) { // to trace if an item on destination path exists boolean itemExisted = false; if (log.isDebugEnabled()) { log.debug("MOVE " + repoName + "/" + repoPath); } repoPath = normalizePath(repoPath); try { String serverURI = uriInfo.getBaseUriBuilder().path(getClass()).path(repoName).build().toString(); // destinationHeader could begins from workspace name (passed from cms // WebDAVServiceImpl) and doesn't contain neither host no repository name URI dest = buildURI(destinationHeader); URI base = buildURI(serverURI); String destPath = dest.getPath(); int repoIndex = destPath.indexOf(repoName); // check if destination corresponds to base uri // if the destination is on another server // or destination header is malformed // we return BAD_GATEWAY(502) HTTP status // more info here http://www.webdav.org/specs/rfc2518.html#METHOD_MOVE if (dest.getHost() != null && !base.getHost().equals(dest.getHost())) { return Response.status(HTTPStatus.BAD_GATEWAY).entity("Bad Gateway").build(); } destPath = normalizePath(repoIndex == -1 ? destPath : destPath.substring(repoIndex + repoName.length() + 1)); String destWorkspace = workspaceName(destPath); String destNodePath = path(destPath, false); String srcWorkspace = workspaceName(repoPath); String srcNodePath = path(repoPath); List<String> lockTokens = lockTokens(lockTokenHeader, ifHeader); Depth depth = new Depth(depthHeader); boolean overwrite = true; if (overwriteHeader != null && overwriteHeader.equalsIgnoreCase("F")) { overwrite = false; } repoName = getRepositoryName(repoName); if (overwrite) { Response delResponse = delete(repoName, destPath, lockTokenHeader, ifHeader); itemExisted = (delResponse.getStatus() == HTTPStatus.NO_CONTENT); } else { Session session = session(repoName, srcWorkspace, null); String uri = uriInfo.getBaseUriBuilder().path(getClass()).path(repoName).path(srcWorkspace).build().toString(); Response prpfind = new PropFindCommand().propfind(session, path(destPath), body, depth.getIntValue(), uri); if (prpfind.getStatus() != HTTPStatus.NOT_FOUND) { return Response.status(HTTPStatus.PRECON_FAILED) .entity("Item exists on destination path, while overwriting is forbidden").build(); } } if (depth.getStringValue().equalsIgnoreCase("Infinity")) { if (srcWorkspace.equals(destWorkspace)) { Session session = session(repoName, srcWorkspace, lockTokens); return new MoveCommand(uriInfo.getBaseUriBuilder().path(getClass()).path(repoName), itemExisted).move( session, srcNodePath, destNodePath); } Session srcSession = session(repoName, srcWorkspace, lockTokens); Session destSession = session(repoName, destWorkspace, lockTokens); return new MoveCommand(uriInfo.getBaseUriBuilder().path(getClass()).path(repoName), itemExisted).move( srcSession, destSession, srcNodePath, destNodePath); } else { return Response.status(HTTPStatus.BAD_REQUEST).entity("Bad Request").build(); } } catch (NoSuchWorkspaceException e) { log.error("NoSuchWorkspaceException " + e.getMessage(), e); return Response.status(HTTPStatus.CONFLICT).entity(e.getMessage()).build(); } catch (Exception exc) { log.error(exc.getMessage(), exc); return Response.serverError().entity(exc.getMessage()).build(); } } /** * @param path * @return the instance of javax.ws.rs.core.Response * @LevelAPI Platform */ @OPTIONS @Path("/{repoName}/{path:.*}/") public Response options(@PathParam("path") String path) { if (log.isDebugEnabled()) { log.debug("OPTIONS " + path); } String DASL_VALUE = "<DAV:basicsearch>" + "<exo:sql xmlns:exo=\"http://exoplatform.com/jcr\"/>" + "<exo:xpath xmlns:exo=\"http://exoplatform.com/jcr\"/>"; return Response.ok().header(ExtHttpHeaders.ALLOW, /* allowCommands */ALLOW) .header(ExtHttpHeaders.DAV, "1, 2, ordered-collections, access-control") .header(ExtHttpHeaders.DASL, DASL_VALUE).header(ExtHttpHeaders.MSAUTHORVIA, "DAV").build(); } /** * @param repoName repository name * @param repoPath path in repository * @param lockTokenHeader Lock-Token HTTP header * @param ifHeader If- HTTP Header * @param uriInfo base URI info * @param body Request body * @return the instance of javax.ws.rs.core.Response * @LevelAPI Platform */ @ORDERPATCH @Path("/{repoName}/{repoPath:.*}/") public Response order(@PathParam("repoName") String repoName, @PathParam("repoPath") String repoPath, @HeaderParam(ExtHttpHeaders.LOCKTOKEN) String lockTokenHeader, @HeaderParam(ExtHttpHeaders.IF) String ifHeader, @Context UriInfo uriInfo, HierarchicalProperty body) { if (log.isDebugEnabled()) { log.debug("ORDERPATCH " + repoName + "/" + repoPath); } repoPath = normalizePath(repoPath); try { repoName = getRepositoryName(repoName); List<String> lockTokens = lockTokens(lockTokenHeader, ifHeader); Session session = session(repoName, workspaceName(repoPath), lockTokens); String uri = uriInfo.getBaseUriBuilder().path(getClass()).path(repoName).path(workspaceName(repoPath)).build() .toString(); return new OrderPatchCommand().orderPatch(session, path(repoPath), body, uri); } catch (Exception exc) { log.error(exc.getMessage(), exc); return Response.serverError().entity(exc.getMessage()).build(); } } /** * @param repoName repository name * @param repoPath path in repository * @param depthHeader Depth HTTP header * @param uriInfo base URI info * @param body Request body * @return the instance of javax.ws.rs.core.Response HTTP response * @LevelAPI Platform */ @PROPFIND @Path("/{repoName}/{repoPath:.*}/") public Response propfind(@PathParam("repoName") String repoName, @PathParam("repoPath") String repoPath, @HeaderParam(ExtHttpHeaders.DEPTH) String depthHeader, @Context UriInfo uriInfo, HierarchicalProperty body) { if (log.isDebugEnabled()) { log.debug("PROPFIND " + repoName + "/" + repoPath); } repoPath = normalizePath(repoPath); try { repoName = getRepositoryName(repoName); Session session = session(repoName, workspaceName(repoPath), null); String uri = uriInfo.getBaseUriBuilder().path(getClass()).path(repoName).path(workspaceName(repoPath)).build() .toString(); Depth depth = new Depth(depthHeader); return new PropFindCommand().propfind(session, path(repoPath), body, depth.getIntValue(), uri); } catch (NoSuchWorkspaceException exc) { return Response.status(HTTPStatus.CONFLICT).entity(exc.getMessage()).build(); } catch (PreconditionException exc) { return Response.status(HTTPStatus.BAD_REQUEST).entity(exc.getMessage()).build(); } catch (Exception exc) { log.error(exc.getMessage(), exc); return Response.serverError().entity(exc.getMessage()).build(); } } /** * @param repoName repository name * @param repoPath path in repository * @param lockTokenHeader Lock-Token HTTP header * @param ifHeader If- HTTP Header * @param uriInfo base URI info * @param body Request body * @return the instance of javax.ws.rs.core.Response HTTP response * @LevelAPI Platform */ @PROPPATCH @Path("/{repoName}/{repoPath:.*}/") public Response proppatch(@PathParam("repoName") String repoName, @PathParam("repoPath") String repoPath, @HeaderParam(ExtHttpHeaders.LOCKTOKEN) String lockTokenHeader, @HeaderParam(ExtHttpHeaders.IF) String ifHeader, @Context UriInfo uriInfo, HierarchicalProperty body) { if (log.isDebugEnabled()) { log.debug("PROPPATCH " + repoName + "/" + repoPath); } repoPath = normalizePath(repoPath); try { repoName = getRepositoryName(repoName); List<String> lockTokens = lockTokens(lockTokenHeader, ifHeader); Session session = session(repoName, workspaceName(repoPath), lockTokens); String uri = uriInfo.getBaseUriBuilder().path(getClass()).path(repoName).path(workspaceName(repoPath)).build() .toString(); return new PropPatchCommand(nullResourceLocks).propPatch(session, path(repoPath), body, lockTokens, uri); } catch (NoSuchWorkspaceException exc) { log.error("NoSuchWorkspace. " + exc.getMessage()); return Response.status(HTTPStatus.NOT_FOUND).entity(exc.getMessage()).build(); } catch (Exception exc) { log.error(exc.getMessage(), exc); return Response.serverError().entity(exc.getMessage()).build(); } } /** * WedDAV "PUT" method. See <a * href='http://www.ietf.org/rfc/rfc2518.txt'>HTTP methods for distributed * authoring sec. 8.7 "PUT"</a>. * * @param repoName repository name * @param repoPath path in repository * @param lockTokenHeader Lock-Token HTTP header * @param ifHeader If HTTP Header * @param fileNodeTypeHeader JCR NodeType header * @param contentNodeTypeHeader JCR Content-NodeType header * @param mixinTypes JCR Mixin types header * @param mediaType Content-Type HTTP header * @param userAgent User-Agent HTTP header * @param inputStream stream that contain incoming data * @param uriInfo URI info * @return the instance of javax.ws.rs.core.Response * @LevelAPI Platform */ @PUT @Path("/{repoName}/{repoPath:.*}/") public Response put(@PathParam("repoName") String repoName, @PathParam("repoPath") String repoPath, @HeaderParam(ExtHttpHeaders.LOCKTOKEN) String lockTokenHeader, @HeaderParam(ExtHttpHeaders.IF) String ifHeader, @HeaderParam(ExtHttpHeaders.FILE_NODETYPE) String fileNodeTypeHeader, @HeaderParam(ExtHttpHeaders.CONTENT_NODETYPE) String contentNodeTypeHeader, @HeaderParam(ExtHttpHeaders.CONTENT_MIXINTYPES) String mixinTypes, @HeaderParam(ExtHttpHeaders.CONTENT_TYPE) MediaType mediaType, @HeaderParam(ExtHttpHeaders.USER_AGENT) String userAgent, InputStream inputStream, @Context UriInfo uriInfo) { if (log.isDebugEnabled()) { log.debug("PUT " + repoName + "/" + repoPath); } repoPath = normalizePath(repoPath); MimeTypeRecognizer mimeTypeRecognizer = new MimeTypeRecognizer(TextUtil.nameOnly(repoPath), mimeTypeResolver, mediaType, webDavServiceInitParams .isUntrustedUserAgent(userAgent)); try { repoName = getRepositoryName(repoName); List<String> tokens = lockTokens(lockTokenHeader, ifHeader); Session session = session(repoName, workspaceName(repoPath), tokens); String fileNodeType = NodeTypeUtil.getNodeType(fileNodeTypeHeader, webDavServiceInitParams.getDefaultFileNodeType(), webDavServiceInitParams.getAllowedFileNodeTypes()); String contentNodeType = NodeTypeUtil.getContentNodeType(contentNodeTypeHeader); NodeTypeManager ntm = session.getWorkspace().getNodeTypeManager(); NodeType nodeType = ntm.getNodeType(contentNodeType); NodeTypeUtil.checkContentResourceType(nodeType); if (webDavServiceInitParams.isEnableAutoVersion()) { return new PutCommand(nullResourceLocks, uriInfo.getBaseUriBuilder().path(getClass()).path(repoName), mimeTypeRecognizer).put(session, path(repoPath), inputStream, fileNodeType, contentNodeType, NodeTypeUtil.getMixinTypes(mixinTypes), tokens, webDavServiceInitParams.getAllowedAutoVersionPath()); } else { return new PutCommand(nullResourceLocks, uriInfo.getBaseUriBuilder().path(getClass()).path(repoName), mimeTypeRecognizer).put(session, path(repoPath), inputStream, fileNodeType, contentNodeType, NodeTypeUtil.getMixinTypes(mixinTypes), webDavServiceInitParams.getDefaultUpdatePolicyType(), webDavServiceInitParams.getDefaultAutoVersionType(), tokens); } } catch (NoSuchWorkspaceException exc) { log.error("NoSuchWorkspaceException " + exc.getMessage(), exc); return Response.status(HTTPStatus.CONFLICT).entity(exc.getMessage()).build(); } catch (NoSuchNodeTypeException exc) { log.error("NoSuchNodeTypeException " + exc.getMessage(), exc); return Response.status(HTTPStatus.BAD_REQUEST).entity(exc.getMessage()).build(); } catch (Exception exc) { log.error(exc.getMessage(), exc); return Response.serverError().entity(exc.getMessage()).build(); } } /** * @param repoName repository name * @param repoPath path in repository * @param depthHeader Depth HTTP header * @param uriInfo base URI info * @param body Request body * @return the instance of javax.ws.rs.core.Response * @LevelAPI Platform */ @REPORT @Path("/{repoName}/{repoPath:.*}/") public Response report(@PathParam("repoName") String repoName, @PathParam("repoPath") String repoPath, @HeaderParam(ExtHttpHeaders.DEPTH) String depthHeader, @Context UriInfo uriInfo, HierarchicalProperty body) { if (log.isDebugEnabled()) { log.debug("REPORT " + repoName + "/" + repoPath); } repoPath = normalizePath(repoPath); try { repoName = getRepositoryName(repoName); Depth depth = new Depth(depthHeader); Session session = session(repoName, workspaceName(repoPath), null); String uri = uriInfo.getBaseUriBuilder().path(getClass()).path(repoName).path(workspaceName(repoPath)).build() .toString(); return new ReportCommand().report(session, path(repoPath), body, depth, uri); } catch (NoSuchWorkspaceException exc) { log.error("NoSuchWorkspaceException " + exc.getMessage(), exc); return Response.status(HTTPStatus.NOT_FOUND).entity(exc.getMessage()).build(); } catch (Exception exc) { log.error(exc.getMessage(), exc); return Response.serverError().entity(exc.getMessage()).build(); } } /** * @param repoName repository name * @param repoPath path in repository * @param uriInfo base URI info * @param body Request body * @return the instance of javax.ws.rs.core.Response * @LevelAPI Platform */ @SEARCH @Path("/{repoName}/{repoPath:.*}/") public Response search(@PathParam("repoName") String repoName, @PathParam("repoPath") String repoPath, @Context UriInfo uriInfo, HierarchicalProperty body) { if (log.isDebugEnabled()) { log.debug("SEARCH " + repoName + "/" + repoPath); } repoPath = normalizePath(repoPath); try { repoName = getRepositoryName(repoName); Session session = session(repoName, workspaceName(repoPath), null); String uri = uriInfo.getBaseUriBuilder().path(getClass()).path(repoName).path(workspaceName(repoPath)).build() .toString(); return new SearchCommand().search(session, body, uri); } catch (NoSuchWorkspaceException exc) { log.error("NoSuchWorkspaceException " + exc.getMessage(), exc); return Response.status(HTTPStatus.NOT_FOUND).entity(exc.getMessage()).build(); } catch (Exception exc) { log.error(exc.getMessage(), exc); return Response.serverError().entity(exc.getMessage()).build(); } } /** * @param repoName repository name * @param repoPath path in repository * @param lockTokenHeader Lock-Token HTTP header * @param ifHeader If- HTTP Header * @return the instance of javax.ws.rs.core.Response * @LevelAPI Platform */ @UNCHECKOUT @Path("/{repoName}/{repoPath:.*}/") public Response uncheckout(@PathParam("repoName") String repoName, @PathParam("repoPath") String repoPath, @HeaderParam(ExtHttpHeaders.LOCKTOKEN) String lockTokenHeader, @HeaderParam(ExtHttpHeaders.IF) String ifHeader) { if (log.isDebugEnabled()) { log.debug("UNCHECKOUT " + repoName + "/" + repoPath); } repoPath = normalizePath(repoPath); try { Session session = session(repoName, workspaceName(repoPath), lockTokens(lockTokenHeader, ifHeader)); return new UnCheckOutCommand().uncheckout(session, path(repoPath)); } catch (NoSuchWorkspaceException exc) { log.error("NoSuchWorkspaceException " + exc.getMessage(), exc); return Response.status(HTTPStatus.NOT_FOUND).entity(exc.getMessage()).build(); } catch (Exception exc) { log.error(exc.getMessage(), exc); return Response.serverError().entity(exc.getMessage()).build(); } } /** * @param repoName repository name * @param repoPath path in repository * @param lockTokenHeader Lock-Token HTTP header * @param ifHeader If- HTTP Header * @return the instance of javax.ws.rs.core.Response * @LevelAPI Platform */ @VERSIONCONTROL @Path("/{repoName}/{repoPath:.*}/") public Response versionControl(@PathParam("repoName") String repoName, @PathParam("repoPath") String repoPath, @HeaderParam(ExtHttpHeaders.LOCKTOKEN) String lockTokenHeader, @HeaderParam(ExtHttpHeaders.IF) String ifHeader) { if (log.isDebugEnabled()) { log.debug("VERSION-CONTROL " + repoName + "/" + repoPath); } repoPath = normalizePath(repoPath); Session session; try { session = session(repoName, workspaceName(repoPath), lockTokens(lockTokenHeader, ifHeader)); } catch (Exception exc) { log.error(exc.getMessage(), exc); return Response.serverError().entity(exc.getMessage()).build(); } return new VersionControlCommand().versionControl(session, path(repoPath)); } /** * WebDAV ACL method according to protocol extension - Access Control Protocol: RFC3744 * More details here: <a href='http://www.webdav.org/specs/rfc3744.html'>Web Distributed * Authoring and Versioning (WebDAV) Access Control Protocol</a> * @param repoName repository name * @param repoPath path in repository * @param lockTokenHeader Lock-Token HTTP header * @param ifHeader If- HTTP Header * @param body Request body * @return the instance of javax.ws.rs.core.Response * @LevelAPI Provisional */ @ACL @Path("/{repoName}/{repoPath:.*}/") public Response acl(@PathParam("repoName") String repoName, @PathParam("repoPath") String repoPath, @HeaderParam(ExtHttpHeaders.LOCKTOKEN) String lockTokenHeader, @HeaderParam(ExtHttpHeaders.IF) String ifHeader, HierarchicalProperty body) { if (log.isDebugEnabled()) { log.debug("ACL " + repoName + "/" + repoPath); } repoPath = normalizePath(repoPath); try { List<String> lockTokens = lockTokens(lockTokenHeader, ifHeader); Session session = session(repoName, workspaceName(repoPath), lockTokens); return new AclCommand().acl(session, path(repoPath), body); } catch (NoSuchWorkspaceException exc) { log.error("NoSuchWorkspace. " + exc.getMessage()); return Response.status(HTTPStatus.NOT_FOUND).entity(exc.getMessage()).build(); } catch (Exception exc) { log.error(exc.getMessage(), exc); return Response.status(HTTPStatus.INTERNAL_ERROR).entity(exc.getMessage()).build(); } } /** * Gives access to the current session. * * @param repoName repository name * @param wsName workspace name * @param lockTokens Lock tokens * @return current session * @throws Exception {@link Exception} */ protected Session session(String repoName, String wsName, List<String> lockTokens) throws Exception, NoSuchWorkspaceException { // To be cloud compliant we need now to ignore the provided repository name (more details in JCR-2138) ManageableRepository repo = repositoryService.getCurrentRepository(); if (PropertyManager.isDevelopping() && log.isWarnEnabled()) { String currentRepositoryName = repo.getConfiguration().getName(); if (!currentRepositoryName.equals(repoName)) { log.warn("The expected repository was '" + repoName + "' but we will use the current repository instead which is '" + currentRepositoryName + "'"); } } SessionProvider sp = sessionProviderService.getSessionProvider(null); if (sp == null) throw new RepositoryException("SessionProvider is not properly set. Make the application calls" + "SessionProviderService.setSessionProvider(..) somewhere before (" + "for instance in Servlet Filter for WEB application)"); Session session = sp.getSession(wsName, repo); if (lockTokens != null) { String[] presentLockTokens = session.getLockTokens(); ArrayList<String> presentLockTokensList = new ArrayList<String>(); for (int i = 0; i < presentLockTokens.length; i++) { presentLockTokensList.add(presentLockTokens[i]); } for (int i = 0; i < lockTokens.size(); i++) { String lockToken = lockTokens.get(i); if (!presentLockTokensList.contains(lockToken)) { session.addLockToken(lockToken); } } } return session; } /** * Gives the name of the repository to access. * @param repoName the name of the expected repository. * @return the name of the repository to access. */ protected String getRepositoryName(String repoName) throws RepositoryException { // To be cloud compliant we need now to ignore the provided repository name (more details in JCR-2138) ManageableRepository repo = repositoryService.getCurrentRepository(); String currentRepositoryName = repo.getConfiguration().getName(); if (PropertyManager.isDevelopping() && log.isWarnEnabled()) { if (!currentRepositoryName.equals(repoName)) { log.warn("The expected repository was '" + repoName + "' but we will use the current repository instead which is '" + currentRepositoryName + "'"); } } return currentRepositoryName; } /** * Extracts workspace name from repository path. * * @param repoPath repository path * @return workspace name */ protected String workspaceName(String repoPath) { return repoPath.split("/")[0]; } /** * Normalizes path. * * @param repoPath repository path * @return normalized path. */ protected String normalizePath(String repoPath) { if (repoPath.length() > 0 && repoPath.endsWith("/")) { return repoPath.substring(0, repoPath.length() - 1); } return repoPath; } /** * Extracts path from repository path. * * @param repoPath repository path * @return path */ protected String path(String repoPath) { return path(repoPath, true); } /** * Extracts path from repository path. * * @param repoPath repository path * @param withIndex indicates whether the index must be removed or not * @return path */ protected String path(String repoPath, boolean withIndex) { String path = repoPath.substring(workspaceName(repoPath).length()); if (path.length() > 0) { if (!withIndex) { return TextUtil.removeIndexFromPath(path); } return path; } return "/"; } /** * Creates the list of Lock tokens from Lock-Token and If headers. * * @param lockTokenHeader Lock-Token HTTP header * @param ifHeader If HTTP header * @return the list of lock tokens */ protected List<String> lockTokens(String lockTokenHeader, String ifHeader) { ArrayList<String> lockTokens = new ArrayList<String>(); if (lockTokenHeader != null) { if (lockTokenHeader.startsWith("<")) { lockTokenHeader = lockTokenHeader.substring(1, lockTokenHeader.length() - 1); } if (lockTokenHeader.contains(WebDavConst.Lock.OPAQUE_LOCK_TOKEN)) { lockTokenHeader = lockTokenHeader.split(":")[1]; } lockTokens.add(lockTokenHeader); } if (ifHeader != null) { String headerLockToken = ifHeader.substring(ifHeader.indexOf("(")); headerLockToken = headerLockToken.substring(2, headerLockToken.length() - 2); if (headerLockToken.contains(WebDavConst.Lock.OPAQUE_LOCK_TOKEN)) { headerLockToken = headerLockToken.split(":")[1]; } lockTokens.add(headerLockToken); } return lockTokens; } /** * Build URI from string. */ private URI buildURI(String path) throws URISyntaxException { try { return new URI(path); } catch (URISyntaxException e) { return new URI(TextUtil.escape(path, '%', true)); } } }