diff --git a/config/alfresco/public-rest-context.xml b/config/alfresco/public-rest-context.xml
index 0bae52055f..0c4cd9b243 100644
--- a/config/alfresco/public-rest-context.xml
+++ b/config/alfresco/public-rest-context.xml
@@ -509,7 +509,26 @@
+
+
+
+
+
+
+
+
+ org.alfresco.rest.api.Downloads
+
+
+
+
+
+
+
+
+
+
@@ -771,6 +790,10 @@
+
+
+
+
diff --git a/source/java/org/alfresco/rest/api/Downloads.java b/source/java/org/alfresco/rest/api/Downloads.java
new file mode 100644
index 0000000000..dea9eb4552
--- /dev/null
+++ b/source/java/org/alfresco/rest/api/Downloads.java
@@ -0,0 +1,59 @@
+/*
+ * #%L
+ * Alfresco Remote API
+ * %%
+ * Copyright (C) 2005 - 2016 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ *
+ * Alfresco 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 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco 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 Alfresco. If not, see .
+ * #L%
+ */
+package org.alfresco.rest.api;
+
+import org.alfresco.rest.api.model.Download;
+
+/**
+ * downloads API
+ *
+ * @author cpopa
+ *
+ */
+public interface Downloads
+{
+ /**
+ * Creates a download:download node.
+ *
+ * @param download
+ * @return information about the newly created download:download node
+ */
+ Download createDownloadNode(Download download);
+
+ /**
+ * Get status info about a download node.
+ *
+ * @param downloadNodeId
+ * @return status info about a download:download node
+ */
+ Download getDownloadStatus(String downloadNodeId);
+
+ /**
+ * Stop the zip creation if still in progress
+ * @param downloadNodeId
+ */
+ void cancel(String downloadNodeId);
+}
diff --git a/source/java/org/alfresco/rest/api/downloads/DownloadsEntityResource.java b/source/java/org/alfresco/rest/api/downloads/DownloadsEntityResource.java
new file mode 100644
index 0000000000..7cb2616671
--- /dev/null
+++ b/source/java/org/alfresco/rest/api/downloads/DownloadsEntityResource.java
@@ -0,0 +1,91 @@
+/*
+ * #%L
+ * Alfresco Remote API
+ * %%
+ * Copyright (C) 2005 - 2016 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ *
+ * Alfresco 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 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco 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 Alfresco. If not, see .
+ * #L%
+ */
+package org.alfresco.rest.api.downloads;
+
+import java.util.Collections;
+import java.util.List;
+
+import javax.servlet.http.HttpServletResponse;
+
+import org.alfresco.rest.api.Downloads;
+import org.alfresco.rest.api.model.Download;
+import org.alfresco.rest.framework.WebApiDescription;
+import org.alfresco.rest.framework.WebApiParam;
+import org.alfresco.rest.framework.core.ResourceParameter;
+import org.alfresco.rest.framework.core.exceptions.EntityNotFoundException;
+import org.alfresco.rest.framework.resource.EntityResource;
+import org.alfresco.rest.framework.resource.actions.interfaces.EntityResourceAction;
+import org.alfresco.rest.framework.resource.parameters.Parameters;
+import org.alfresco.util.ParameterCheck;
+import org.springframework.beans.factory.InitializingBean;
+
+/**
+ *
+ * @author cpopa
+ *
+ */
+@EntityResource(name = "downloads", title = "Downloads")
+public class DownloadsEntityResource implements EntityResourceAction.Create, EntityResourceAction.ReadById, EntityResourceAction.Delete, InitializingBean
+{
+ private Downloads downloads;
+
+ public void setDownloads(Downloads downloads)
+ {
+ this.downloads = downloads;
+ }
+
+ @Override
+ public void afterPropertiesSet()
+ {
+ ParameterCheck.mandatory("downloads", this.downloads);
+ }
+
+ @Override
+ @WebApiDescription(title = "Create download", description = "Create a download node whose content will be a zip which is being created asynchronously.", successStatus = HttpServletResponse.SC_ACCEPTED)
+ @WebApiParam(name = "entity", title = "Download request", description = "Download request which contains the node ids for the zip elements.",
+ kind = ResourceParameter.KIND.HTTP_BODY_OBJECT, allowMultiple = false)
+ public List create(List entity, Parameters parameters)
+ {
+ Download downloadNode = downloads.createDownloadNode(entity.get(0));
+ return Collections.singletonList(downloadNode);
+ }
+
+ @Override
+ @WebApiDescription(title = "Get download information", description = "Get information about the progress of the zip creation.")
+ @WebApiParam(name = "nodeId", title = "Download nodeId")
+ public Download readById(String nodeId, Parameters parameters) throws EntityNotFoundException
+ {
+ return downloads.getDownloadStatus(nodeId);
+ }
+
+ @WebApiDescription(title = "Cancel download", description = "Stop the zip creation if still in progress.", successStatus = HttpServletResponse.SC_ACCEPTED)
+ @Override
+ public void delete(String nodeId, Parameters parameters)
+ {
+ downloads.cancel(nodeId);
+ }
+
+}
\ No newline at end of file
diff --git a/source/java/org/alfresco/rest/api/downloads/package-info.java b/source/java/org/alfresco/rest/api/downloads/package-info.java
new file mode 100644
index 0000000000..7583142401
--- /dev/null
+++ b/source/java/org/alfresco/rest/api/downloads/package-info.java
@@ -0,0 +1,30 @@
+/*
+ * #%L
+ * Alfresco Remote API
+ * %%
+ * Copyright (C) 2005 - 2016 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ *
+ * Alfresco 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 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco 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 Alfresco. If not, see .
+ * #L%
+ */
+
+@WebApi(name="alfresco", scope=Api.SCOPE.PUBLIC, version=1)
+package org.alfresco.rest.api.downloads;
+import org.alfresco.rest.framework.Api;
+import org.alfresco.rest.framework.WebApi;
\ No newline at end of file
diff --git a/source/java/org/alfresco/rest/api/impl/DownloadsImpl.java b/source/java/org/alfresco/rest/api/impl/DownloadsImpl.java
new file mode 100644
index 0000000000..7dd1a9930d
--- /dev/null
+++ b/source/java/org/alfresco/rest/api/impl/DownloadsImpl.java
@@ -0,0 +1,175 @@
+/*
+ * #%L
+ * Alfresco Remote API
+ * %%
+ * Copyright (C) 2005 - 2016 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ *
+ * Alfresco 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 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco 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 Alfresco. If not, see .
+ * #L%
+ */
+package org.alfresco.rest.api.impl;
+
+import java.util.HashSet;
+
+import org.alfresco.model.ContentModel;
+import org.alfresco.repo.download.DownloadModel;
+import org.alfresco.rest.api.Downloads;
+import org.alfresco.rest.api.Nodes;
+import org.alfresco.rest.api.model.Download;
+import org.alfresco.rest.framework.core.exceptions.InvalidArgumentException;
+import org.alfresco.rest.framework.core.exceptions.PermissionDeniedException;
+import org.alfresco.service.cmr.download.DownloadService;
+import org.alfresco.service.cmr.download.DownloadStatus;
+import org.alfresco.service.cmr.repository.NodeRef;
+import org.alfresco.service.cmr.repository.NodeService;
+import org.alfresco.service.cmr.security.AccessStatus;
+import org.alfresco.service.cmr.security.PermissionService;
+import org.alfresco.service.namespace.QName;
+
+/**
+ *
+ * @author cpopa
+ *
+ */
+public class DownloadsImpl implements Downloads
+{
+ private DownloadService downloadService;
+ private NodeService nodeService;
+ private Nodes nodes;
+ private PermissionService permissionService;
+ public static final String DEFAULT_ARCHIVE_NAME = "archive.zip";
+ public static final String DEFAULT_ARCHIVE_EXTENSION = ".zip";
+
+ public void setDownloadService(DownloadService downloadService)
+ {
+ this.downloadService = downloadService;
+ }
+
+ public void setNodeService(NodeService nodeService)
+ {
+ this.nodeService = nodeService;
+ }
+
+ public void setNodes(Nodes nodes)
+ {
+ this.nodes = nodes;
+ }
+
+ public void setPermissionService(PermissionService permissionService)
+ {
+ this.permissionService = permissionService;
+ }
+
+ @Override
+ public Download createDownloadNode(Download download)
+ {
+ checkEmptyNodeIds(download);
+
+ checkDuplicateNodeId(download);
+
+ NodeRef[] zipContentNodeRefs = validateAndGetNodeRefs(download);
+
+ checkNodeIdsReadPermission(zipContentNodeRefs);
+
+ NodeRef zipNodeRef = downloadService.createDownload(zipContentNodeRefs, true);
+
+ String archiveName = zipContentNodeRefs.length > 1 ?
+ DEFAULT_ARCHIVE_NAME :
+ nodeService.getProperty(zipContentNodeRefs[0], ContentModel.PROP_NAME) + DEFAULT_ARCHIVE_EXTENSION;
+
+ nodeService.setProperty(zipNodeRef, ContentModel.PROP_NAME, archiveName);
+ Download downloadInfo = getStatus(zipNodeRef);
+ return downloadInfo;
+ }
+
+ @Override
+ public Download getDownloadStatus(String downloadNodeId)
+ {
+ NodeRef downloadNodeRef = nodes.validateNode(downloadNodeId);
+
+ checkIsDownloadNodeType(downloadNodeRef);
+
+ Download downloadInfo = getStatus(downloadNodeRef);
+ return downloadInfo;
+ }
+
+ @Override
+ public void cancel(String downloadNodeId)
+ {
+ NodeRef downloadNodeRef = nodes.validateNode(downloadNodeId);
+ checkIsDownloadNodeType(downloadNodeRef);
+
+ downloadService.cancelDownload(downloadNodeRef);
+ }
+
+ protected NodeRef[] validateAndGetNodeRefs(Download download)
+ {
+ return download.getNodeIds().stream()
+ .map(nodeRef -> nodes.validateNode(nodeRef))
+ .toArray(NodeRef[]::new);
+ }
+
+ protected void checkNodeIdsReadPermission(NodeRef[] zipContentNodeRefs)
+ {
+ for (NodeRef nodeRef : zipContentNodeRefs)
+ {
+ if (permissionService.hasReadPermission(nodeRef).equals(AccessStatus.DENIED)){
+ throw new PermissionDeniedException();
+ }
+ }
+ }
+
+ protected void checkDuplicateNodeId(Download download)
+ {
+ if(download.getNodeIds().size() != new HashSet(download.getNodeIds()).size()){
+ throw new InvalidArgumentException("Cannot specify the same nodeId twice");
+ }
+ }
+
+ protected void checkEmptyNodeIds(Download download)
+ {
+ if (download.getNodeIds().size() == 0)
+ {
+ throw new InvalidArgumentException("Cannot create an archive with 0 entries.");
+ }
+ }
+
+ protected void checkIsDownloadNodeType(NodeRef downloadNodeRef)
+ {
+ QName nodeIdType = this.nodeService.getType(downloadNodeRef);
+
+ if(!nodeIdType.equals(DownloadModel.TYPE_DOWNLOAD)){
+ throw new InvalidArgumentException("Please specify the nodeId of a download node.");
+ }
+ }
+
+ private Download getStatus(NodeRef downloadNodeRef)
+ {
+ DownloadStatus status = downloadService.getDownloadStatus(downloadNodeRef);
+ Download downloadInfo = new Download();
+ downloadInfo.setDownloadId(downloadNodeRef.getId());
+ downloadInfo.setDone(status.getDone());
+ downloadInfo.setFilesAdded(status.getFilesAdded());
+ downloadInfo.setStatus(status.getStatus());
+ downloadInfo.setTotalFiles(status.getTotalFiles());
+ downloadInfo.setTotal(status.getTotal());
+ return downloadInfo;
+ }
+
+}
diff --git a/source/java/org/alfresco/rest/api/model/Download.java b/source/java/org/alfresco/rest/api/model/Download.java
new file mode 100644
index 0000000000..4a00789790
--- /dev/null
+++ b/source/java/org/alfresco/rest/api/model/Download.java
@@ -0,0 +1,131 @@
+/*
+ * #%L
+ * Alfresco Remote API
+ * %%
+ * Copyright (C) 2005 - 2016 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ *
+ * Alfresco 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 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco 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 Alfresco. If not, see .
+ * #L%
+ */
+
+package org.alfresco.rest.api.model;
+
+import java.util.List;
+
+import org.alfresco.service.cmr.download.DownloadStatus;
+
+/**
+ * Represents a download entity
+ *
+ */
+public class Download
+{
+ private String downloadId;
+ private List nodeIds;
+ private DownloadStatus.Status status;
+ private long done;
+ private long total;
+ private long filesAdded;
+ private long totalFiles;
+
+ public String getDownloadId()
+ {
+ return downloadId;
+ }
+
+ public void setDownloadId(String downloadId)
+ {
+ this.downloadId = downloadId;
+ }
+
+ public List getNodeIds()
+ {
+ return nodeIds;
+ }
+
+ public void setNodeIds(List nodeIds)
+ {
+ this.nodeIds = nodeIds;
+ }
+
+ public DownloadStatus.Status getStatus()
+ {
+ return status;
+ }
+
+ public void setStatus(DownloadStatus.Status status)
+ {
+ this.status = status;
+ }
+
+ public long getDone()
+ {
+ return done;
+ }
+
+ public void setDone(long done)
+ {
+ this.done = done;
+ }
+
+ public long getTotal()
+ {
+ return total;
+ }
+
+ public void setTotal(long total)
+ {
+ this.total = total;
+ }
+
+ public long getFilesAdded()
+ {
+ return filesAdded;
+ }
+
+ public void setFilesAdded(long filesAdded)
+ {
+ this.filesAdded = filesAdded;
+ }
+
+ public long getTotalFiles()
+ {
+ return totalFiles;
+ }
+
+ public void setTotalFiles(long totalFiles)
+ {
+ this.totalFiles = totalFiles;
+ }
+
+ @Override
+ public String toString()
+ {
+ StringBuilder builder = new StringBuilder(150);
+ builder.append("Download [downloadId=").append(downloadId)
+ .append(", nodeIds=").append(nodeIds)
+ .append(", status=").append(status)
+ .append(", done=").append(done)
+ .append(", total=").append(total)
+ .append(", filesAdded=").append(filesAdded)
+ .append(", totalFiles=").append(totalFiles)
+ .append("]");
+ return builder.toString();
+ }
+}
diff --git a/source/test-java/org/alfresco/rest/api/tests/ApiTest.java b/source/test-java/org/alfresco/rest/api/tests/ApiTest.java
index 1adcc035bc..21d32ee8e0 100644
--- a/source/test-java/org/alfresco/rest/api/tests/ApiTest.java
+++ b/source/test-java/org/alfresco/rest/api/tests/ApiTest.java
@@ -74,7 +74,8 @@ import org.junit.runners.Suite;
TestSiteMembershipRequests.class,
TestFavourites.class,
TestPublicApi128.class,
- TestPublicApiCaching.class
+ TestPublicApiCaching.class,
+ TestDownloads.class
})
public class ApiTest
{
diff --git a/source/test-java/org/alfresco/rest/api/tests/TestDownloads.java b/source/test-java/org/alfresco/rest/api/tests/TestDownloads.java
new file mode 100644
index 0000000000..24369d7f1c
--- /dev/null
+++ b/source/test-java/org/alfresco/rest/api/tests/TestDownloads.java
@@ -0,0 +1,520 @@
+/*
+ * #%L
+ * Alfresco Remote API
+ * %%
+ * Copyright (C) 2005 - 2016 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ *
+ * Alfresco 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 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco 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 Alfresco. If not, see .
+ * #L%
+ */
+package org.alfresco.rest.api.tests;
+
+import static java.lang.String.format;
+import static java.util.Arrays.asList;
+import static javax.servlet.http.HttpServletResponse.SC_ACCEPTED;
+import static javax.servlet.http.HttpServletResponse.SC_OK;
+import static org.alfresco.rest.api.impl.DownloadsImpl.DEFAULT_ARCHIVE_EXTENSION;
+import static org.alfresco.rest.api.impl.DownloadsImpl.DEFAULT_ARCHIVE_NAME;
+import static org.alfresco.rest.api.tests.util.RestApiUtil.toJsonAsStringNonNull;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Map;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
+
+import javax.servlet.http.HttpServletResponse;
+
+import org.alfresco.repo.tenant.TenantUtil;
+import org.alfresco.repo.tenant.TenantUtil.TenantRunAsWork;
+import org.alfresco.rest.api.model.AssocChild;
+import org.alfresco.rest.api.model.Download;
+import org.alfresco.rest.api.nodes.NodesEntityResource;
+import org.alfresco.rest.api.tests.client.HttpResponse;
+import org.alfresco.rest.api.tests.client.PublicApiException;
+import org.alfresco.rest.api.tests.client.RequestContext;
+import org.alfresco.rest.api.tests.client.data.Document;
+import org.alfresco.rest.api.tests.client.data.Folder;
+import org.alfresco.rest.api.tests.util.RestApiUtil;
+import org.alfresco.rest.framework.core.exceptions.ApiException;
+import org.alfresco.service.cmr.download.DownloadStatus;
+import org.alfresco.service.cmr.site.SiteVisibility;
+import org.json.simple.JSONObject;
+import org.junit.Before;
+import org.junit.FixMethodOrder;
+import org.junit.Test;
+import org.junit.runners.MethodSorters;
+
+/**
+ * Tests the /downloads API
+ *
+ * @author cpopa
+ *
+ */
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+public class TestDownloads extends AbstractBaseApiTest
+{
+ public static final String NODES_SECONDARY_CHILDREN = "nodes/%s/secondary-children";
+
+ public static final String API_DOWNLOADS = "downloads";
+
+ private static final String DOC4_NAME = "docTest4.txt";
+ private static final String SUB_FOLDER1_NAME = "subFolder1";
+ private static final String DOC3_NAME = "docTest3.txt";
+ private static final String FOLDER1_NAME = "folder1";
+ private static final String FOLDER3_NAME = "folder3";
+ private static final String ZIPPABLE_DOC1_NAME = "docTest1.txt";
+ private static final String DUMMY_CONTENT = "dummy content";
+ private org.alfresco.rest.api.Nodes nodesApi;
+ private String zippableDocId1;
+ private String zippableDocId2;
+ private String zippableDocId3_InFolder1;
+ private String zippableFolderId1;
+ private String zippableFolderId2_InFolder1;
+ private String zippableDocId4_InFolder2;
+ private String zippableFolderId3;
+ private String zippableDoc_user2;
+
+ @Before
+ public void setupTest() throws IOException, Exception{
+ nodesApi = applicationContext.getBean("Nodes", org.alfresco.rest.api.Nodes.class);
+
+ setRequestContext(user1);
+
+ Document zippableDoc1 = createTextFile(tDocLibNodeId, ZIPPABLE_DOC1_NAME, DUMMY_CONTENT);
+ zippableDocId1 = zippableDoc1.getId();
+ zippableDocId2 = createTextFile(tDocLibNodeId, "docTest2", DUMMY_CONTENT).getId();
+
+ Folder zippableFolder1 = createFolder(tDocLibNodeId, FOLDER1_NAME);
+ zippableFolderId1 = zippableFolder1.getId();
+ zippableDocId3_InFolder1 = createTextFile(zippableFolderId1, DOC3_NAME, DUMMY_CONTENT).getId();
+
+ Folder zippableFolder2_InFolder1 = createFolder(zippableFolderId1, SUB_FOLDER1_NAME);
+ zippableFolderId2_InFolder1 = zippableFolder2_InFolder1.getId();
+
+ zippableDocId4_InFolder2 = createTextFile(zippableFolderId2_InFolder1, DOC4_NAME, DUMMY_CONTENT).getId();
+
+ Folder zippableFolder3 = createFolder(tDocLibNodeId, FOLDER3_NAME);
+ zippableFolderId3 = zippableFolder3.getId();
+
+ setRequestContext(user2);
+ String user2Site = createSite ("TestSite B - " + RUNID, SiteVisibility.PRIVATE).getId();
+ String user2DocLib = getSiteContainerNodeId(user2Site, "documentLibrary");
+ zippableDoc_user2 = createTextFile(user2DocLib, "user2doc", DUMMY_CONTENT).getId();
+
+ setRequestContext(user1);
+ AssocChild secChild = new AssocChild(zippableDoc1.getId(), ASSOC_TYPE_CM_CONTAINS);
+ post(format(NODES_SECONDARY_CHILDREN, zippableFolder3.getId()), toJsonAsStringNonNull(secChild), HttpServletResponse.SC_CREATED);
+ }
+
+
+ /**
+ * Tests the creation of download nodes.
+ *
+ * POST:
+ * {@literal :/alfresco/api/-default-/private/alfresco/versions/1/downloads}
+ *
+ */
+ @Test
+ public void test001CreateDownload() throws Exception
+ {
+ //test creating a download with a single file
+ Download download = createDownload(HttpServletResponse.SC_ACCEPTED, zippableDocId1);
+
+ assertPendingDownloadProps(download);
+
+ assertValidZipNodeid(download);
+
+ assertDoneDownload(download, 1, 13);
+
+ //test creating a multiple file archive
+ download = createDownload(HttpServletResponse.SC_ACCEPTED, zippableDocId1, zippableDocId2);
+
+ assertPendingDownloadProps(download);
+
+ assertValidZipNodeid(download);
+
+ assertDoneDownload(download, 2, 26);
+
+ //test creating a zero file archive
+ createDownload(HttpServletResponse.SC_BAD_REQUEST);
+
+ //test creating an archive with the same file twice
+ download = createDownload(HttpServletResponse.SC_BAD_REQUEST, zippableDocId1, zippableDocId1);
+
+ //test creating an archive with a folder and a file which is contained in the folder
+ download = createDownload(HttpServletResponse.SC_ACCEPTED, zippableFolderId1, zippableDocId3_InFolder1);
+
+ assertPendingDownloadProps(download);
+
+ assertValidZipNodeid(download);
+
+ assertDoneDownload(download, 3, 39);
+
+ //test creating an archive with a file and a folder containing that file but only as a secondary parent child association
+ download = createDownload(HttpServletResponse.SC_ACCEPTED, zippableDocId1, zippableFolderId3);
+
+ assertPendingDownloadProps(download);
+
+ assertValidZipNodeid(download);
+
+ assertDoneDownload(download, 2, 26);
+
+ //test creating an archive with two files, one of which user1 does not have permissions for
+ download = createDownload(HttpServletResponse.SC_FORBIDDEN, zippableDocId1, zippableDoc_user2);
+ }
+
+ /**
+ * Tests retrieving info about a download node
+ *
+ * GET:
+ * {@literal :/alfresco/api/-default-/private/alfresco/versions/1/downloads/}
+ *
+ */
+ @Test
+ public void test002GetDownloadInfo() throws Exception
+ {
+ Download download = createDownload(HttpServletResponse.SC_ACCEPTED, zippableFolderId1, zippableFolderId2_InFolder1, zippableDocId4_InFolder2);
+
+ //test retrieving information about an ongoing download
+ assertInProgressDownload(download, 4, 52);
+
+ //test retrieving information about a finished download
+ assertDoneDownload(download, 4, 52);
+
+ //test retrieving the status of a cancelled download
+ download = createDownload(HttpServletResponse.SC_ACCEPTED, zippableFolderId1, zippableDocId3_InFolder1);
+ assertCancelledDownload(download, 3, 39);
+ }
+
+ /**
+ * Tests canceling a download.
+ *
+ * DELETE:
+ * {@literal :/alfresco/api/-default-/private/alfresco/versions/1/downloads/}
+ *
+ */
+ @Test
+ public void test003CancelDownload() throws Exception
+ {
+ //cancel a running download operation
+ Download download = createDownload(HttpServletResponse.SC_ACCEPTED, zippableDocId1, zippableDocId2);
+
+ cancel(download.getDownloadId());
+
+ assertCancelledDownload(download, 2, 26);
+
+ //cancel a completed download - should have no effect
+ download = createDownload(HttpServletResponse.SC_ACCEPTED, zippableDocId1, zippableDocId2);
+
+ assertDoneDownload(download, 2, 26);
+
+ cancel(download.getDownloadId());
+
+ Thread.sleep(500);
+
+ Download downloadStatus = getDownload(download.getDownloadId());
+
+ assertTrue("A cancel operation on a DONE download has no effect.", downloadStatus.getStatus().equals(DownloadStatus.Status.DONE));
+
+ //cancel a node which is not of a download type
+ cancel(HttpServletResponse.SC_BAD_REQUEST, zippableDocId1);
+
+ //user2 canceling user1 download operation - should not be allowed
+ download = createDownload(HttpServletResponse.SC_ACCEPTED, zippableDocId1);
+
+ publicApiClient.setRequestContext(new RequestContext(networkOne.getId(), user2));
+
+ cancel(HttpServletResponse.SC_FORBIDDEN, download.getDownloadId());
+ }
+
+ /**
+ * Tests downloading the content of a download node(a zip) using the /nodes API:
+ *
+ * GET:
+ * {@literal :/alfresco/api//public/alfresco/versions/1/nodes//content}
+ *
+ */
+ @Test
+ public void test004GetDownloadContent() throws Exception{
+
+ //test downloading the content of a 1 file zip
+ Download download = createDownload(HttpServletResponse.SC_ACCEPTED, zippableDocId1);
+
+ assertDoneDownload(download, 1, 13);
+
+ HttpResponse response = downloadContent(download);
+
+ ZipInputStream zipStream = getZipStreamFromResponse(response);
+
+ ZipEntry zipEntry = zipStream.getNextEntry();
+
+ assertEquals("Zip entry name is not correct", ZIPPABLE_DOC1_NAME, zipEntry.getName());
+
+ assertTrue("Zip entry size is not correct", zipEntry.getCompressedSize() <= 13);
+
+ assertTrue("No more entries should be in this zip", zipStream.getNextEntry() == null);
+ zipStream.close();
+
+ Map responseHeaders = response.getHeaders();
+
+ assertNotNull(responseHeaders);
+
+ assertEquals(format("attachment; filename=\"%s\"; filename*=UTF-8''%s", ZIPPABLE_DOC1_NAME + DEFAULT_ARCHIVE_EXTENSION,
+ ZIPPABLE_DOC1_NAME + DEFAULT_ARCHIVE_EXTENSION),
+ responseHeaders.get("Content-Disposition"));
+
+ //test downloading the content of a multiple file zip
+ download = createDownload(HttpServletResponse.SC_ACCEPTED, zippableFolderId1, zippableDocId3_InFolder1);
+
+ assertDoneDownload(download, 3, 39);
+
+ response = downloadContent(download);
+
+ zipStream = getZipStreamFromResponse(response);
+
+ assertEquals("Zip entry name is not correct", FOLDER1_NAME + "/", zipStream.getNextEntry().getName());
+ assertEquals("Zip entry name is not correct", FOLDER1_NAME + "/" + DOC3_NAME, zipStream.getNextEntry().getName());
+ assertEquals("Zip entry name is not correct", FOLDER1_NAME + "/" + SUB_FOLDER1_NAME + "/", zipStream.getNextEntry().getName());
+ assertEquals("Zip entry name is not correct", FOLDER1_NAME + "/" + SUB_FOLDER1_NAME + "/" + DOC4_NAME, zipStream.getNextEntry().getName());
+ assertEquals("Zip entry name is not correct", DOC3_NAME, zipStream.getNextEntry().getName());
+
+ assertTrue("No more entries should be in this zip", zipStream.getNextEntry() == null);
+ zipStream.close();
+
+ responseHeaders = response.getHeaders();
+
+ assertNotNull(responseHeaders);
+
+ assertEquals(format("attachment; filename=\"%s\"; filename*=UTF-8''%s", DEFAULT_ARCHIVE_NAME,
+ DEFAULT_ARCHIVE_NAME),
+ responseHeaders.get("Content-Disposition"));
+
+ //test download the content of a zip which has a secondary child
+ download = createDownload(HttpServletResponse.SC_ACCEPTED, zippableDocId1, zippableFolderId3);
+ assertDoneDownload(download, 2, 26);
+
+ response = downloadContent(download);
+
+ zipStream = getZipStreamFromResponse(response);
+
+ assertEquals("Zip entry name is not correct", ZIPPABLE_DOC1_NAME, zipStream.getNextEntry().getName());
+ assertEquals("Zip entry name is not correct", FOLDER3_NAME + "/", zipStream.getNextEntry().getName());
+ assertEquals("Zip entry name is not correct", FOLDER3_NAME + "/" + ZIPPABLE_DOC1_NAME, zipStream.getNextEntry().getName());
+ assertTrue("No more entries should be in this zip", zipStream.getNextEntry() == null);
+ }
+
+
+ protected ZipInputStream getZipStreamFromResponse(HttpResponse response)
+ {
+ return new ZipInputStream(new ByteArrayInputStream(response.getResponseAsBytes()));
+ }
+
+
+ protected HttpResponse downloadContent(Download download) throws Exception
+ {
+ return getSingle(NodesEntityResource.class, download.getDownloadId() + "/content", null, HttpServletResponse.SC_OK);
+ }
+
+ /**
+ * Tests deleting a download node using the /nodes API:
+ *
+ * DELETE:
+ * {@literal :/alfresco/api//public/alfresco/versions/1/nodes/}
+ *
+ */
+ @Test
+ public void test005DeleteDownloadNode() throws Exception{
+
+ //test deleting a download node
+ Download download = createDownload(HttpServletResponse.SC_ACCEPTED, zippableDocId1);
+
+ assertDoneDownload(download, 1, 13);
+
+ deleteNode(download.getDownloadId(), true, HttpServletResponse.SC_NO_CONTENT);
+
+ getDownload(download.getDownloadId(), HttpServletResponse.SC_NOT_FOUND);
+
+ //test user2 deleting a download node created by user1
+ download = createDownload(HttpServletResponse.SC_ACCEPTED, zippableDocId1);
+
+ assertDoneDownload(download, 1, 13);
+
+ publicApiClient.setRequestContext(new RequestContext(networkOne.getId(), user2));
+
+ deleteNode(download.getDownloadId(), true, HttpServletResponse.SC_FORBIDDEN);
+
+ assertDoneDownload(download, 1, 13);
+ }
+
+ private void assertDoneDownload(Download download, int expectedFilesAdded, int expectedTotal) throws Exception, InterruptedException
+ {
+ for(int i = 0; i<=40; i++){
+ if (i == 40)
+ {
+ fail("Download should be DONE by now.");
+ }
+ Download downloadStatus = getDownload(download.getDownloadId());
+ if (!downloadStatus.getStatus().equals(DownloadStatus.Status.DONE)){
+ Thread.sleep(50);
+ }else{
+ assertTrue("The number of bytes added in the archive does not match the total", downloadStatus.getDone() == downloadStatus.getTotal());
+ assertEquals("The number of files added in the archive should be " + expectedFilesAdded, expectedFilesAdded, downloadStatus.getFilesAdded());
+ assertEquals("The total number of bytes should be " + expectedTotal, expectedTotal, downloadStatus.getTotal());
+ assertEquals("The total number of files of the final archive should be " + expectedFilesAdded, expectedFilesAdded, downloadStatus.getTotalFiles());
+ break;
+ }
+ }
+ }
+
+ protected void assertCancelledDownload(Download download, int expectedTotalFiles, int expectedTotal) throws PublicApiException, Exception, InterruptedException
+ {
+ cancel(download.getDownloadId());
+ for(int i = 0; i<=40; i++){
+ if (i == 40)
+ {
+ fail("Download should be CANCELLED by now.");
+ }
+ Download downloadStatus = getDownload(download.getDownloadId());
+ if (!downloadStatus.getStatus().equals(DownloadStatus.Status.CANCELLED)){
+ Thread.sleep(50);
+ }else{
+ assertTrue("The total bytes added to the archive by now should be greater than 0", downloadStatus.getDone() > 0 && downloadStatus.getDone() <= downloadStatus.getTotal());
+ assertTrue("The download is in progress, there should still be files to be added.", downloadStatus.getFilesAdded() < downloadStatus.getTotalFiles());
+ assertEquals("The total number of bytes should be " + expectedTotal, expectedTotal, downloadStatus.getTotal());
+ assertEquals("The total number of files to be added to the archive should be " + expectedTotalFiles, expectedTotalFiles, downloadStatus.getTotalFiles());
+ break;
+ }
+ }
+ }
+
+ private void assertInProgressDownload(Download download, int expectedTotalFiles, int expectedTotal) throws Exception, InterruptedException
+ {
+ for(int i = 0; i<=40; i++){
+ if (i == 40)
+ {
+ fail("Download creation is taking too long.Download status should be at least IN_PROGRESS by now.");
+ }
+ Download downloadStatus = getDownload(download.getDownloadId());
+ if (!downloadStatus.getStatus().equals(DownloadStatus.Status.IN_PROGRESS)){
+ Thread.sleep(50);
+ }else{
+ //'done' can be equal to the 'total' even though the status is IN_PROGRESS. See ZipDownloadExporter line 239
+ assertTrue("The total bytes added to the archive by now should be greater than 0", downloadStatus.getDone() > 0 && downloadStatus.getDone() <= downloadStatus.getTotal());
+ assertTrue("The download is in progress, there should still be files to be added.", downloadStatus.getFilesAdded() < downloadStatus.getTotalFiles());
+ assertEquals("The total number of bytes should be " + expectedTotal, expectedTotal, downloadStatus.getTotal());
+ assertEquals("The total number of files to be added to the archive should be " + expectedTotalFiles, expectedTotalFiles, downloadStatus.getTotalFiles());
+ break;
+ }
+ }
+ }
+
+ protected void setRequestContext(String user)
+ {
+ setRequestContext(networkOne.getId(), user, null);
+ }
+
+ private void assertValidZipNodeid(Download download)
+ {
+ try{
+ TenantUtil.runAsUserTenant(new TenantRunAsWork()
+ {
+
+ @Override
+ public Void doWork() throws Exception
+ {
+ nodesApi.validateNode(download.getDownloadId());
+ return null;
+ }
+ }, user1, networkOne.getId());
+
+ }catch(ApiException ex){
+ org.junit.Assert.fail("The download nodeid is not valid." + ex.getMessage());
+ }
+ }
+
+ private void assertPendingDownloadProps(Download download)
+ {
+ assertEquals("The download request hasn't been processed yet, the status is not correct", DownloadStatus.Status.PENDING, download.getStatus());
+ assertEquals("Should be 0, the download req hasn't been processed yet", 0, download.getDone());
+ assertEquals("Should be 0, the download req hasn't been processed yet", 0, download.getFilesAdded());
+ assertEquals("Should be 0, the download req hasn't been processed yet", 0, download.getTotal());
+ assertEquals("Should be 0, the download req hasn't been processed yet", 0, download.getTotalFiles());
+ }
+
+
+ @Override
+ public String getScope()
+ {
+ return "public";
+ }
+
+ private Download createDownload(int expectedStatus, String ... nodeIds) throws Exception
+ {
+ Download downloadRequest = new Download();
+ downloadRequest.setNodeIds(Arrays.asList(nodeIds));
+
+ setRequestContext(user1);
+
+ Download download = create(downloadRequest, expectedStatus);
+ return download;
+ }
+
+ public Download create(Download download, int expectedStatus) throws Exception
+ {
+ HttpResponse response = post(API_DOWNLOADS, RestApiUtil.toJsonAsStringNonNull(download), expectedStatus);
+ return getDownloadFromResponse(response);
+ }
+
+ public Download getDownload(String downloadId, int expectedStatus) throws Exception
+ {
+ HttpResponse response = getSingle(API_DOWNLOADS, downloadId, expectedStatus);
+
+ return getDownloadFromResponse(response);
+ }
+
+ public Download getDownload(String downloadId) throws Exception
+ {
+ return getDownload(downloadId, HttpServletResponse.SC_OK);
+ }
+
+ public void cancel(String downloadId) throws Exception
+ {
+ cancel(HttpServletResponse.SC_ACCEPTED, downloadId);
+ }
+
+ public void cancel(int expectedStatusCode, String downloadId) throws Exception
+ {
+ delete(API_DOWNLOADS, downloadId, expectedStatusCode);
+ }
+
+ protected Download getDownloadFromResponse(HttpResponse response) throws Exception
+ {
+ if (asList(SC_ACCEPTED, SC_OK).contains(response.getStatusCode()))
+ {
+ return RestApiUtil.parseRestApiEntry((JSONObject) response.getJsonResponse(), Download.class);
+ }
+ return null;
+ }
+}