From 642d332d24192480afabfffcace27992d70e670a Mon Sep 17 00:00:00 2001 From: David Draper Date: Fri, 28 Sep 2012 13:26:36 +0000 Subject: [PATCH] Merge from BRANCHES/DEV/CLOUD1_SPRINT1 to HEAD: 40238: CLOUD-37 - Initial Commit to test Merged BRANCHES/DEV/AMILLER/CLOUD1_SPRINT1 to BRANCHES/DEV/CLOUD1_SPRINT1: 40077: CLOUD-37: Initial commit. 40101: CLOUD-37: Fix build error. 40114: CLOUD-37: Fix path names and missing files. 40122: CLOUD-37: Initial drop of UI code for investigation of progress issues 40124: CLOUD-37: A couple of minor UI tweaks (set icon and hide panel before archive download) 40125: CLOUD-37: Download files and folders as zip 40134: CLOUD-37: Updates to UI (javascript doc, CSS tweaks, intervals for requests, labels, etc). 40143: CLOUD-37: Error messages for failures, more JavaScript doc, archive naming, code tidy 40157: CLOUD-37 - Download files and folders as zip 40202: CLOUD-37: UI tweaks following UX review 40217: CLOUD-37: Add file count to status reports. 40222: CLOUD-37: Added information to download dialog to report on the number of files added to the zip 40240: CLOUD-37: Remove extraneous file, breaking build 40513: CLOUD-37: Add Action Service Metrics Merged BRANCHES/DEV/AMILLER/CLOUD1_SPRINT1 to BRANCHES/DEV/CLOUD1_SPRINT1: 40260: CLOUD-37: Add action service metrics 40309: CLOUD-37: Fix JMX configuration, pointing at renamed class. 40514: CLOUD-37: Enable the execution of the zip creation process on a remote transformation node Merged BRANCHES/DEV/AMILLER/CLOUD1_SPRINT1 to BRANCHES/DEV/CLOUD1_SPRINT1: 40369: CLOUD-37: Enable the execution of the zip creation process on a remote transformation node 40516: CLOUD-37: Implement clean up job. Merged BRANCHES/DEV/AMILLER/CLOUD1_SPRINT1 to BRANCHES/DEV/CLOUD1_SPRINT1: 40462: CLOUD-37: Implement clean up job. 40517: CLOUD-505: Add entries for folders. Merged BRANCHES/DEV/AMILLER/CLOUD1_SPRINT1 to BRANCHES/DEV/CLOUD1_SPRINT1: 40493: CLOUD-505: Add entries for folders. 40547: CLOUD-37: Fix broken test 40595: CLOUD-518: Add working copy/locked file filtering 40642: CLOUD-508: Prevent problems occurring when cancelling and restarting the same download 40643: CLOUD-507: When a single item is selected for download it the item name gets used for the archive name 41442: CLOUD-590: Limit the total size of the content which can be downloaded. This can be set via the property, download.maxContentSize. The default is 2GB. 41472: CLOUD-589: Added cancelled flag to download type and added checks in Zip creation action to act upon the setting of this flag. Also added webscript for canceling the download. 41692: Adds support to Alfresco.util.formatFileSize for file sizes with commas (as needed by zip download) 41693: Zip Download enhancements: CLOUD-590: Notifies the user when they've exceeded the maximum file size limit. CLOUD-626: Better handling when there are errors during zipping. (WIP) 41713: Zip Download Updates: CLOUD-589: A cancel download UI action now triggers a delete of the archive on the server. CLOUD-626: The UI now triggers a full download cancel (with node delete) in event of an error. 41737: Updates Alfresco.util.formatFileSize to support an optional decimal places param. (For CLOUD-685) 41739: CLOUD-685: Display total file size of files for download to two decimal places when there is an error. 41832: Fixes: CLOUD-704: new CANCELLED status is now handled correctly. 41887: CLOUD-686: Updated maximum download content size to 2152852358 bytes (2.005GB) 41965: CLOUD-703: Upload content now runs as system user, and Quota Service returns unlimited quota for system user. 42025: CLOUD-703: Fix test failures and ensure S3 content store works in the clustered and non-clustered environments git-svn-id: https://svn.alfresco.com/repos/alfresco-enterprise/alfresco/HEAD/root@42146 c4b6b30b-aa2e-2d43-bbcb-ca4b014f7261 --- config/alfresco/action-services-context.xml | 8 +- .../application-context-highlevel.xml | 1 + config/alfresco/bootstrap/downloadsSpace.xml | 31 ++ config/alfresco/download-services-context.xml | 141 ++++++ .../alfresco/ibatis/alfresco-SqlMapConfig.xml | 4 + .../query-downloads-common-SqlMap.xml | 39 ++ config/alfresco/import-export-context.xml | 6 + .../messages/bootstrap-spaces.properties | 3 + .../messages/download-model.properties | 1 + config/alfresco/model/downloadModel.xml | 107 +++++ .../public-services-security-context.xml | 25 ++ config/alfresco/repository.properties | 19 +- .../repo/action/ActionServiceImpl.java | 27 +- .../repo/action/ActionServiceMonitor.java | 105 +++++ .../repo/action/ActionStatistics.java | 87 ++++ .../alfresco/repo/action/RunningAction.java | 85 ++++ .../repo/download/AbstractExporter.java | 167 +++++++ .../repo/download/ActionServiceHelper.java | 41 ++ .../alfresco/repo/download/BaseExporter.java | 228 ++++++++++ .../repo/download/ContentServiceHelper.java | 48 +++ .../download/CreateDownloadArchiveAction.java | 311 +++++++++++++ .../download/DownloadCancelledException.java | 18 + .../alfresco/repo/download/DownloadModel.java | 48 +++ .../download/DownloadServiceException.java | 37 ++ .../repo/download/DownloadServiceImpl.java | 134 ++++++ .../DownloadServiceIntegrationTest.java | 408 ++++++++++++++++++ .../download/DownloadStatusUpdateService.java | 43 ++ .../DownloadStatusUpdateServiceImpl.java | 56 +++ .../repo/download/DownloadStorage.java | 255 +++++++++++ .../repo/download/DownloadsCleanupJob.java | 91 ++++ .../download/LocalActionServiceHelper.java | 58 +++ .../download/LocalContentServiceHelper.java | 65 +++ .../repo/download/ZipDownloadExporter.java | 300 +++++++++++++ .../download/cannedquery/DownloadEntity.java | 42 ++ .../cannedquery/GetDownloadsCannedQuery.java | 96 +++++ .../GetDownloadsCannedQueryFactory.java | 76 ++++ .../GetDownloadsCannedQueryParams.java | 42 ++ .../cmr/download/DownloadCreaterService.java | 0 .../service/cmr/download/DownloadRequest.java | 57 +++ .../service/cmr/download/DownloadService.java | 69 +++ .../service/cmr/download/DownloadStatus.java | 126 ++++++ .../download/DownloadStatusUpdateService.java | 32 ++ .../service/cmr/download/package-info.java | 30 ++ 43 files changed, 3561 insertions(+), 6 deletions(-) create mode 100644 config/alfresco/bootstrap/downloadsSpace.xml create mode 100644 config/alfresco/download-services-context.xml create mode 100644 config/alfresco/ibatis/org.hibernate.dialect.Dialect/query-downloads-common-SqlMap.xml create mode 100644 config/alfresco/messages/download-model.properties create mode 100644 config/alfresco/model/downloadModel.xml create mode 100644 source/java/org/alfresco/repo/action/ActionServiceMonitor.java create mode 100644 source/java/org/alfresco/repo/action/ActionStatistics.java create mode 100644 source/java/org/alfresco/repo/action/RunningAction.java create mode 100644 source/java/org/alfresco/repo/download/AbstractExporter.java create mode 100644 source/java/org/alfresco/repo/download/ActionServiceHelper.java create mode 100644 source/java/org/alfresco/repo/download/BaseExporter.java create mode 100644 source/java/org/alfresco/repo/download/ContentServiceHelper.java create mode 100644 source/java/org/alfresco/repo/download/CreateDownloadArchiveAction.java create mode 100644 source/java/org/alfresco/repo/download/DownloadCancelledException.java create mode 100644 source/java/org/alfresco/repo/download/DownloadModel.java create mode 100644 source/java/org/alfresco/repo/download/DownloadServiceException.java create mode 100644 source/java/org/alfresco/repo/download/DownloadServiceImpl.java create mode 100644 source/java/org/alfresco/repo/download/DownloadServiceIntegrationTest.java create mode 100644 source/java/org/alfresco/repo/download/DownloadStatusUpdateService.java create mode 100644 source/java/org/alfresco/repo/download/DownloadStatusUpdateServiceImpl.java create mode 100644 source/java/org/alfresco/repo/download/DownloadStorage.java create mode 100644 source/java/org/alfresco/repo/download/DownloadsCleanupJob.java create mode 100644 source/java/org/alfresco/repo/download/LocalActionServiceHelper.java create mode 100644 source/java/org/alfresco/repo/download/LocalContentServiceHelper.java create mode 100644 source/java/org/alfresco/repo/download/ZipDownloadExporter.java create mode 100644 source/java/org/alfresco/repo/download/cannedquery/DownloadEntity.java create mode 100644 source/java/org/alfresco/repo/download/cannedquery/GetDownloadsCannedQuery.java create mode 100644 source/java/org/alfresco/repo/download/cannedquery/GetDownloadsCannedQueryFactory.java create mode 100644 source/java/org/alfresco/repo/download/cannedquery/GetDownloadsCannedQueryParams.java create mode 100644 source/java/org/alfresco/service/cmr/download/DownloadCreaterService.java create mode 100644 source/java/org/alfresco/service/cmr/download/DownloadRequest.java create mode 100644 source/java/org/alfresco/service/cmr/download/DownloadService.java create mode 100644 source/java/org/alfresco/service/cmr/download/DownloadStatus.java create mode 100644 source/java/org/alfresco/service/cmr/download/DownloadStatusUpdateService.java create mode 100644 source/java/org/alfresco/service/cmr/download/package-info.java diff --git a/config/alfresco/action-services-context.xml b/config/alfresco/action-services-context.xml index 073475f43e..7bbe04d743 100644 --- a/config/alfresco/action-services-context.xml +++ b/config/alfresco/action-services-context.xml @@ -94,7 +94,10 @@ - + + + + @@ -124,6 +127,9 @@ + + + + diff --git a/config/alfresco/bootstrap/downloadsSpace.xml b/config/alfresco/bootstrap/downloadsSpace.xml new file mode 100644 index 0000000000..9d8449f5eb --- /dev/null +++ b/config/alfresco/bootstrap/downloadsSpace.xml @@ -0,0 +1,31 @@ + + + + + + workspace + SpacesStore + downloads_container + ${spaces.downloads.root.name} + ${spaces.downloads.root.name} + ${spaces.downloads.root.description} + + + + + + + GROUP_EVERYONE + AddChildren + + + ROLE_OWNER + FullControl + + + + + + diff --git a/config/alfresco/download-services-context.xml b/config/alfresco/download-services-context.xml new file mode 100644 index 0000000000..c413175b2e --- /dev/null +++ b/config/alfresco/download-services-context.xml @@ -0,0 +1,141 @@ + + + + + + + + + + alfresco/model/downloadModel.xml + + + + + + + + + + + + getDownloadStatus + + + + + + + + + + + cancelDownload + createDownload + deleteDownloads + + + + + + + + + org.alfresco.service.cmr.download.DownloadService + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + org.alfresco.repo.download.DownloadsCleanupJob + + + + + + + + + + + + + + + + + + + + ${download.cleaner.startDelayMins} + + + ${download.cleaner.repeatIntervalMins} + + + diff --git a/config/alfresco/ibatis/alfresco-SqlMapConfig.xml b/config/alfresco/ibatis/alfresco-SqlMapConfig.xml index a75d8c701e..b7d31a23a0 100644 --- a/config/alfresco/ibatis/alfresco-SqlMapConfig.xml +++ b/config/alfresco/ibatis/alfresco-SqlMapConfig.xml @@ -95,6 +95,9 @@ Inbound settings from iBatis + + + @@ -222,6 +225,7 @@ Inbound settings from iBatis + diff --git a/config/alfresco/ibatis/org.hibernate.dialect.Dialect/query-downloads-common-SqlMap.xml b/config/alfresco/ibatis/org.hibernate.dialect.Dialect/query-downloads-common-SqlMap.xml new file mode 100644 index 0000000000..7b00d138cf --- /dev/null +++ b/config/alfresco/ibatis/org.hibernate.dialect.Dialect/query-downloads-common-SqlMap.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/config/alfresco/import-export-context.xml b/config/alfresco/import-export-context.xml index 477ab399a3..8ed9cadca5 100644 --- a/config/alfresco/import-export-context.xml +++ b/config/alfresco/import-export-context.xml @@ -407,6 +407,7 @@ ${spaces.nodetemplates.childname} ${system.remote_credentials_container.childname} ${system.syncset_definition_container.childname} + ${system.downloads_container.childname} @@ -699,6 +700,11 @@ alfresco/messages/bootstrap-spaces + + /${system.system_container.childname} + alfresco/bootstrap/downloadsSpace.xml + alfresco/messages/bootstrap-spaces + diff --git a/config/alfresco/messages/bootstrap-spaces.properties b/config/alfresco/messages/bootstrap-spaces.properties index 0a94e53356..c6b7eee3db 100644 --- a/config/alfresco/messages/bootstrap-spaces.properties +++ b/config/alfresco/messages/bootstrap-spaces.properties @@ -170,3 +170,6 @@ spaces.templates.email.workflowNotification.description=Workflow notification em spaces.nodeTemplatesSpace.name=Node Templates spaces.nodeTemplatesSpace.description=Template Nodes for Share - Create New document + +spaces.downloads.root.name=Downloads +spaces.downloads.root.description=Root folder for downloads diff --git a/config/alfresco/messages/download-model.properties b/config/alfresco/messages/download-model.properties new file mode 100644 index 0000000000..7cf7250c3b --- /dev/null +++ b/config/alfresco/messages/download-model.properties @@ -0,0 +1 @@ +# Download related messages diff --git a/config/alfresco/model/downloadModel.xml b/config/alfresco/model/downloadModel.xml new file mode 100644 index 0000000000..3f3d41a20a --- /dev/null +++ b/config/alfresco/model/downloadModel.xml @@ -0,0 +1,107 @@ + + + + + Alfresco Download Model + Alfresco + 2012-07-31 + 1.0 + + + + + + + + + + + + + cm:content + false + + + + d:boolean + true + true + + + + d:text + true + PENDING + + + + + PENDING + IN_PROGRESS + DONE + MAX_CONTENT_SIZE_EXCEEDED + CANCELLED + + + + + + + + d:int + true + 0 + + + + d:long + true + 0 + + + + d:long + true + 0 + + + + d:long + true + 0 + + + + d:long + true + 0 + + + + d:boolean + true + false + + + + + + + false + true + + + cm:cmobject + true + true + + + + + cm:auditable + + + + diff --git a/config/alfresco/public-services-security-context.xml b/config/alfresco/public-services-security-context.xml index 1e74305a36..a308835f2d 100644 --- a/config/alfresco/public-services-security-context.xml +++ b/config/alfresco/public-services-security-context.xml @@ -1019,7 +1019,32 @@ + + + + + + + + + + + + + + + org.alfresco.service.cmr.download.DownloadService.deleteDownloads=ACL_ALLOW + + + + + + + + + + diff --git a/config/alfresco/repository.properties b/config/alfresco/repository.properties index 210683ae24..688e7f081d 100644 --- a/config/alfresco/repository.properties +++ b/config/alfresco/repository.properties @@ -1,4 +1,3 @@ - # Repository configuration repository.name=Main Repository @@ -517,6 +516,9 @@ system.remote_credentials_container.childname=sys:remote_credentials # Folder for storing syncset definitions system.syncset_definition_container.childname=sys:syncset_definitions +# Folder for storing download archives +system.downloads_container.childname=sys:downloads + # Are user names case sensitive? user.name.caseSensitive=false domain.name.caseSensitive=false @@ -665,7 +667,6 @@ content.transformer.OpenOffice.mimeTypeLimits.ppam.pdf.maxSourceSizeKBytes=4096 content.transformer.OpenOffice.mimeTypeLimits.sldx.pdf.maxSourceSizeKBytes=4096 content.transformer.OpenOffice.mimeTypeLimits.sldm.pdf.maxSourceSizeKBytes=4096 content.transformer.OpenOffice.mimeTypeLimits.vsd.pdf.maxSourceSizeKBytes=4096 -content.transformer.OpenOffice.mimeTypeLimits.xls.pdf.maxSourceSizeKBytes=10240 content.transformer.OpenOffice.mimeTypeLimits.xlsx.pdf.maxSourceSizeKBytes=1536 content.transformer.OpenOffice.mimeTypeLimits.xltx.pdf.maxSourceSizeKBytes=1536 content.transformer.OpenOffice.mimeTypeLimits.xlsm.pdf.maxSourceSizeKBytes=1536 @@ -1018,5 +1019,17 @@ ticket.cleanup.cronExpression=0 0 * * * ? # sample.site.disabled=false +# +# Download Service Cleanup +# +download.cleaner.startDelayMins=60 +download.cleaner.repeatIntervalMins=60 +download.cleaner.maxAgeMins=60 + # enable QuickShare - if false then the QuickShare-specific REST APIs will return 403 Forbidden -system.quickshare.enabled=true \ No newline at end of file +system.quickshare.enabled=true + +# +# Download Service Limits, in bytes +# +download.maxContentSize=2152852358 diff --git a/source/java/org/alfresco/repo/action/ActionServiceImpl.java b/source/java/org/alfresco/repo/action/ActionServiceImpl.java index 0c5c3350c9..b5dc17c2c9 100644 --- a/source/java/org/alfresco/repo/action/ActionServiceImpl.java +++ b/source/java/org/alfresco/repo/action/ActionServiceImpl.java @@ -108,6 +108,7 @@ public class ActionServiceImpl implements ActionService, RuntimeActionService, A private AuthenticationContext authenticationContext; private ActionTrackingService actionTrackingService; private PolicyComponent policyComponent; + private ActionServiceMonitor monitor; /** * The asynchronous action execution queues map of name, queue @@ -201,6 +202,14 @@ public class ActionServiceImpl implements ActionService, RuntimeActionService, A { this.policyComponent = policyComponent; } + + /** + * @param monitor used to monitor running actions and execution times + */ + public void setMonitor(ActionServiceMonitor monitor) + { + this.monitor = monitor; + } /** * Set the asynchronous action execution queues @@ -706,8 +715,22 @@ public class ActionServiceImpl implements ActionService, RuntimeActionService, A actionTrackingService.recordActionExecuting(action); } - // Execute the action - directActionExecution(action, actionedUponNodeRef); + RunningAction runningAction = monitor.actionStarted(action); + + try + { + // Execute the action + directActionExecution(action, actionedUponNodeRef); + } + catch (Throwable e) + { + runningAction.setException(e); + throw e; + } + finally + { + monitor.actionCompleted(runningAction); + } if (getTrackStatus(action)) { diff --git a/source/java/org/alfresco/repo/action/ActionServiceMonitor.java b/source/java/org/alfresco/repo/action/ActionServiceMonitor.java new file mode 100644 index 0000000000..e2eefc5ce9 --- /dev/null +++ b/source/java/org/alfresco/repo/action/ActionServiceMonitor.java @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2005-2011 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * 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 . + */ +package org.alfresco.repo.action; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +import org.alfresco.service.cmr.action.Action; + +/** + * Responsible for monitoring running actions and accumulating statistics on actions that have been run. + * + * @author Alex Miller + */ +public class ActionServiceMonitor +{ + private ConcurrentHashMap runningActions = new ConcurrentHashMap(); + private ConcurrentHashMap actionStatistics = new ConcurrentHashMap(); + + /** + * Called by the {@link ActionServiceImpl} when an action is started. + * + * Adds the action to the list of currently running actions. + * + * @param action The action being started + * @return A {@link RunningAction} object used to track the status of the running action. + */ + public RunningAction actionStarted(Action action) + { + RunningAction runningAction = new RunningAction(action); + + this.runningActions.put(runningAction.getId(), runningAction); + + return runningAction; + } + + /** + * Called by the {@link ActionServiceImpl} when sn action completes. + * + * Removes the actions from the list of currently running actions, and updated the accumulated statistics for that action. + * + * @param action The {@link RunningAction} object returned by actionStatred. + */ + public void actionCompleted(RunningAction action) + { + runningActions.remove(action.getId()); + updateActionStatisitcis(action); + } + + private void updateActionStatisitcis(RunningAction action) + { + String actionName = action.getActionName(); + ActionStatistics actionStats = actionStatistics.get(actionName); + if (actionStats == null) + { + actionStatistics.putIfAbsent(actionName, new ActionStatistics(actionName)); + actionStats = actionStatistics.get(actionName); + } + + actionStats.addAction(action); + } + + /** + * @return The list of currently running actions. + */ + public List getRunningActions() + { + return Collections.unmodifiableList(new ArrayList(runningActions.values())); + } + + /** + * @return a count of the currently running actions + */ + public int getRunningActionCount() + { + return runningActions.size(); + } + + /** + * @return a list of the accumulated action statistics. + */ + public List getActionStatisitcs() + { + return Collections.unmodifiableList(new ArrayList(actionStatistics.values())); + } +} diff --git a/source/java/org/alfresco/repo/action/ActionStatistics.java b/source/java/org/alfresco/repo/action/ActionStatistics.java new file mode 100644 index 0000000000..f26e511c23 --- /dev/null +++ b/source/java/org/alfresco/repo/action/ActionStatistics.java @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2005-2011 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * 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 . + */ +package org.alfresco.repo.action; + +/** + * Responsible for accumulating and providing statistics on the invocations of a particualr action. + * + * @author Alex Miller + */ +public class ActionStatistics +{ + private String actionName; + + long invocationCount = 0; + long errorCount = 0; + long totalTime = 0; + + /** + * @param actionName The name of the action this object will provide statistics for. + */ + public ActionStatistics(String actionName) + { + this.actionName = actionName; + } + + /** + * Accumulate statistics from action. + */ + public synchronized void addAction(RunningAction action) + { + invocationCount = invocationCount + 1; + if (action.hasError() == true) + { + errorCount = errorCount +1; + } + totalTime = totalTime + action.getElapsedTime(); + } + + /** + * @return The name of the actions this object has statistics for + */ + public String getActionName() + { + return actionName; + } + + /** + * @return The number of times the action has been invoked + */ + public long getInvocationCount() + { + return invocationCount; + } + + /** + * @return The number of time the invocation of this action has resulted in an exception + */ + public long getErrorCount() + { + return errorCount; + } + + /** + * @return The average time for the invocation of this action + */ + public long getAverageTime() + { + return totalTime / invocationCount; + } + +} diff --git a/source/java/org/alfresco/repo/action/RunningAction.java b/source/java/org/alfresco/repo/action/RunningAction.java new file mode 100644 index 0000000000..88011662ec --- /dev/null +++ b/source/java/org/alfresco/repo/action/RunningAction.java @@ -0,0 +1,85 @@ +package org.alfresco.repo.action; + +import java.util.Date; +import java.util.UUID; + +import org.alfresco.service.cmr.action.Action; + +/** + * Responsible for tracking the invocation of an action. + * + * @author Alex Miller + */ +public class RunningAction +{ + private UUID id = UUID.randomUUID(); + + private String name; + private Thread thread; + + private Date started; + + private boolean exceptionThrown = false; + + /** + * @param action The action being run + */ + public RunningAction(Action action) + { + this.name = action.getActionDefinitionName(); + this.started = new Date(); + this.thread = Thread.currentThread(); + } + + + /** + * @return The name of the action this object is tracking + */ + public String getActionName() + { + return name; + } + + /** + * @return The name of thread the action is being run on + */ + public String getThread() + { + return thread.toString(); + } + + + /** + * @return The generated id for the action invocation + */ + public UUID getId() + { + return id; + } + + /** + * @return The time since the action was started + */ + public long getElapsedTime() + { + return System.currentTimeMillis() - started.getTime(); + } + + + /** + * Called by the {@link ActionServiceImpl} if the action generates an exception during invocation. + */ + public void setException(Throwable e) + { + this.exceptionThrown = true; + } + + + /** + * @return true, if setException was called + */ + public boolean hasError() + { + return exceptionThrown; + } +} diff --git a/source/java/org/alfresco/repo/download/AbstractExporter.java b/source/java/org/alfresco/repo/download/AbstractExporter.java new file mode 100644 index 0000000000..24b0d151d8 --- /dev/null +++ b/source/java/org/alfresco/repo/download/AbstractExporter.java @@ -0,0 +1,167 @@ +package org.alfresco.repo.download; + +import java.io.InputStream; +import java.util.Locale; + +import org.alfresco.service.cmr.repository.ContentData; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.security.AccessPermission; +import org.alfresco.service.cmr.view.Exporter; +import org.alfresco.service.cmr.view.ExporterContext; +import org.alfresco.service.namespace.QName; + +public class AbstractExporter implements Exporter +{ + + @Override + public void start(ExporterContext context) + { + } + + @Override + public void startNamespace(String prefix, String uri) + { + } + + @Override + public void endNamespace(String prefix) + { + } + + @Override + public void startNode(NodeRef nodeRef) + { + } + + @Override + public void endNode(NodeRef nodeRef) + { + } + + @Override + public void startReference(NodeRef nodeRef, QName childName) + { + } + + @Override + public void endReference(NodeRef nodeRef) + { + } + + @Override + public void startAspects(NodeRef nodeRef) + { + } + + @Override + public void startAspect(NodeRef nodeRef, QName aspect) + { + } + + @Override + public void endAspect(NodeRef nodeRef, QName aspect) + { + } + + @Override + public void endAspects(NodeRef nodeRef) + { + } + + @Override + public void startACL(NodeRef nodeRef) + { + } + + @Override + public void permission(NodeRef nodeRef, AccessPermission permission) + { + } + + @Override + public void endACL(NodeRef nodeRef) + { + } + + @Override + public void startProperties(NodeRef nodeRef) + { + } + + @Override + public void startProperty(NodeRef nodeRef, QName property) + { + } + + @Override + public void endProperty(NodeRef nodeRef, QName property) + { + } + + @Override + public void endProperties(NodeRef nodeRef) + { + } + + @Override + public void startValueCollection(NodeRef nodeRef, QName property) + { + } + + @Override + public void startValueMLText(NodeRef nodeRef, Locale locale) + { + } + + @Override + public void endValueMLText(NodeRef nodeRef) + { + } + + @Override + public void value(NodeRef nodeRef, QName property, Object value, int index) + { + } + + @Override + public void content(NodeRef nodeRef, QName property, InputStream content, + ContentData contentData, int index) + { + } + + @Override + public void endValueCollection(NodeRef nodeRef, QName property) + { + } + + @Override + public void startAssocs(NodeRef nodeRef) + { + } + + @Override + public void startAssoc(NodeRef nodeRef, QName assoc) + { + } + + @Override + public void endAssoc(NodeRef nodeRef, QName assoc) + { + } + + @Override + public void endAssocs(NodeRef nodeRef) + { + } + + @Override + public void warning(String warning) + { + } + + @Override + public void end() + { + } + +} diff --git a/source/java/org/alfresco/repo/download/ActionServiceHelper.java b/source/java/org/alfresco/repo/download/ActionServiceHelper.java new file mode 100644 index 0000000000..42e896845c --- /dev/null +++ b/source/java/org/alfresco/repo/download/ActionServiceHelper.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2005-2011 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * 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 . + */ +package org.alfresco.repo.download; + +import org.alfresco.service.cmr.repository.NodeRef; + +/** + * ActionServiceHelper interface. + * + * Allows the download service to switch between executing the zip creation process in the current alfresco node, + * or on a remote node. + * + * @author Alex Miller + */ +public interface ActionServiceHelper +{ + + /** + * Implementations should trigger the CreateDownloadArchiveAction on the provided downloadNode + * + * @param downloadNode + */ + void executeAction(NodeRef downloadNode); + +} diff --git a/source/java/org/alfresco/repo/download/BaseExporter.java b/source/java/org/alfresco/repo/download/BaseExporter.java new file mode 100644 index 0000000000..96fd5bfdb5 --- /dev/null +++ b/source/java/org/alfresco/repo/download/BaseExporter.java @@ -0,0 +1,228 @@ +/* + * Copyright (C) 2005-2012 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * 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 . + */ +package org.alfresco.repo.download; + +import java.io.InputStream; +import java.util.Locale; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.service.cmr.coci.CheckOutCheckInService; +import org.alfresco.service.cmr.repository.ContentData; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.security.AccessPermission; +import org.alfresco.service.cmr.view.Exporter; +import org.alfresco.service.cmr.view.ExporterContext; +import org.alfresco.service.namespace.QName; + +/** + * Base {@link Exporter} providing a default implementation of all methods. + * + * @author Alex Miller + */ +abstract class BaseExporter implements Exporter +{ + private CheckOutCheckInService checkOutCheckInService; + protected NodeService nodeService; + + BaseExporter(CheckOutCheckInService checkOutCheckInService, NodeService nodeService) + { + this.checkOutCheckInService = checkOutCheckInService; + this.nodeService = nodeService; + } + + @Override + public void start(ExporterContext context) + { + } + + @Override + public void startNamespace(String prefix, String uri) + { + } + + @Override + public void endNamespace(String prefix) + { + } + + @Override + public void startNode(NodeRef nodeRef) + { + } + + @Override + public void endNode(NodeRef nodeRef) + { + } + + @Override + public void startReference(NodeRef nodeRef, QName childName) + { + } + + @Override + public void endReference(NodeRef nodeRef) + { + } + + @Override + public void startAspects(NodeRef nodeRef) + { + } + + @Override + public void startAspect(NodeRef nodeRef, QName aspect) + { + } + + @Override + public void endAspect(NodeRef nodeRef, QName aspect) + { + } + + @Override + public void endAspects(NodeRef nodeRef) + { + } + + @Override + public void startACL(NodeRef nodeRef) + { + } + + @Override + public void permission(NodeRef nodeRef, AccessPermission permission) + { + } + + @Override + public void endACL(NodeRef nodeRef) + { + } + + @Override + public void startProperties(NodeRef nodeRef) + { + } + + @Override + public void startProperty(NodeRef nodeRef, QName property) + { + } + + @Override + public void endProperty(NodeRef nodeRef, QName property) + { + } + + @Override + public void endProperties(NodeRef nodeRef) + { + } + + @Override + public void startValueCollection(NodeRef nodeRef, QName property) + { + } + + @Override + public void startValueMLText(NodeRef nodeRef, Locale locale) + { + } + + @Override + public void endValueMLText(NodeRef nodeRef) + { + } + + @Override + public void value(NodeRef nodeRef, QName property, Object value, int index) + { + } + + @Override + public void content(NodeRef nodeRef, QName property, InputStream content, ContentData contentData, int index) + { + if (checkOutCheckInService.isCheckedOut(nodeRef) == true) + { + String owner = (String) nodeService.getProperty(nodeRef, ContentModel.PROP_LOCK_OWNER); + if (AuthenticationUtil.getRunAsUser().equals(owner) == true) + { + return; + } + } + + if (checkOutCheckInService.isWorkingCopy(nodeRef) == true) + { + String owner = (String) nodeService.getProperty(nodeRef, ContentModel.PROP_WORKING_COPY_OWNER); + if (AuthenticationUtil.getRunAsUser().equals(owner) == false) + { + return; + } + } + + contentImpl(nodeRef, property, content, contentData, index); + } + + /** + * Template method for actually dealing with the content. + * + * Called by the content method, after filtering for working copies. + * + */ + protected abstract void contentImpl(NodeRef nodeRef, QName property, InputStream content, ContentData contentData, int index); + + @Override + public void endValueCollection(NodeRef nodeRef, QName property) + { + } + + @Override + public void startAssocs(NodeRef nodeRef) + { + } + + @Override + public void startAssoc(NodeRef nodeRef, QName assoc) + { + } + + @Override + public void endAssoc(NodeRef nodeRef, QName assoc) + { + } + + @Override + public void endAssocs(NodeRef nodeRef) + { + } + + @Override + public void warning(String warning) + { + } + + @Override + public void end() + { + } + +} diff --git a/source/java/org/alfresco/repo/download/ContentServiceHelper.java b/source/java/org/alfresco/repo/download/ContentServiceHelper.java new file mode 100644 index 0000000000..237bfce4d9 --- /dev/null +++ b/source/java/org/alfresco/repo/download/ContentServiceHelper.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2005-2011 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * 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 . + */ +package org.alfresco.repo.download; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; + +import org.alfresco.service.cmr.repository.ContentIOException; +import org.alfresco.service.cmr.repository.NodeRef; + +/** + * ContentServiceHelper interface. + * + * Allows us to switch between the zip creation process updating content using a local content service + * and updating the content through a remote alfresco node. + * + * @author amiller + */ +public interface ContentServiceHelper +{ + /** + * Implementations should update the content of downlaodNode with contents of archiveFile. + * + * @param downloadNode + * @param archiveFile + * @throws ContentIOException + * @throws FileNotFoundException + * @throws IOException + */ + public void updateContent(NodeRef downloadNode, File archiveFile) throws ContentIOException, FileNotFoundException, IOException; +} diff --git a/source/java/org/alfresco/repo/download/CreateDownloadArchiveAction.java b/source/java/org/alfresco/repo/download/CreateDownloadArchiveAction.java new file mode 100644 index 0000000000..45d8352766 --- /dev/null +++ b/source/java/org/alfresco/repo/download/CreateDownloadArchiveAction.java @@ -0,0 +1,311 @@ +/* + * Copyright (C) 2005-2012 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * 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 . + */ +package org.alfresco.repo.download; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.util.List; + +import org.alfresco.model.ContentModel; +import org.alfresco.model.RenditionModel; +import org.alfresco.repo.action.executer.ActionExecuter; +import org.alfresco.repo.action.executer.ActionExecuterAbstractBase; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork; +import org.alfresco.repo.transaction.RetryingTransactionHelper; +import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; +import org.alfresco.service.cmr.action.Action; +import org.alfresco.service.cmr.action.ParameterDefinition; +import org.alfresco.service.cmr.coci.CheckOutCheckInService; +import org.alfresco.service.cmr.download.DownloadRequest; +import org.alfresco.service.cmr.download.DownloadStatus; +import org.alfresco.service.cmr.download.DownloadStatus.Status; +import org.alfresco.service.cmr.repository.ContentData; +import org.alfresco.service.cmr.repository.ContentIOException; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.view.ExporterCrawlerParameters; +import org.alfresco.service.cmr.view.ExporterService; +import org.alfresco.service.cmr.view.Location; +import org.alfresco.service.namespace.QName; +import org.alfresco.util.TempFileProvider; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * {@link ActionExecuter} for creating an archive (ie. zip) file containing + * content from the repository. + * + * The maximum total size of the content which can be downloaded is controlled + * by the maximumContentSie property. -1 indicates no limit. + * + * @author Alex Miller + */ +public class CreateDownloadArchiveAction extends ActionExecuterAbstractBase +{ + private static final Logger log = LoggerFactory.getLogger(CreateDownloadArchiveAction.class); + + + private static final String CREATION_ERROR = "Unexpected error creating archive file for download"; + private static final String TEMP_FILE_PREFIX = "download"; + private static final String TEMP_FILE_SUFFIX = ".zip"; + + // Dependencies + private CheckOutCheckInService checkOutCheckInService; + private ContentServiceHelper contentServiceHelper; + private DownloadStorage downloadStorage; + private ExporterService exporterService; + private NodeService nodeService; + private RetryingTransactionHelper transactionHelper; + private DownloadStatusUpdateService updateService; + + private long maximumContentSize = -1l; + + private static class SizeEstimator extends BaseExporter + { + /** + * @param checkOutCheckInService + * @param nodeService + */ + SizeEstimator(CheckOutCheckInService checkOutCheckInService, NodeService nodeService) + { + super(checkOutCheckInService, nodeService); + } + + private long size = 0; + private long fileCount = 0; + + + @Override + protected void contentImpl(NodeRef nodeRef, QName property, InputStream content, ContentData contentData, int index) + { + size = size + contentData.getSize(); + fileCount = fileCount + 1; + } + + public long getSize() + { + return size; + } + + public long getFileCount() + { + return fileCount; + } + + } + + // Dependency setters + public void setCheckOutCheckInSerivce(CheckOutCheckInService checkOutCheckInService) + { + this.checkOutCheckInService = checkOutCheckInService; + } + + + public void setContentServiceHelper(ContentServiceHelper contentServiceHelper) + { + this.contentServiceHelper = contentServiceHelper; + } + + public void setDownloadStorage(DownloadStorage downloadStorage) + { + this.downloadStorage = downloadStorage; + } + + public void setExporterService(ExporterService exporterService) + { + this.exporterService = exporterService; + } + + /** + * Set the maximum total size of content that can be added to a single + * download. -1 indicates no limit. + */ + public void setMaximumContentSize(long maximumContentSize) + { + this.maximumContentSize = maximumContentSize; + } + + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + public void setTransactionHelper(RetryingTransactionHelper transactionHelper) + { + this.transactionHelper = transactionHelper; + } + + public void setUpdateService(DownloadStatusUpdateService updateService) + { + this.updateService = updateService; + } + + /** + * Create an archive file containing content from the repository. + * + * Uses the {@link ExporterService} with custom exporters to create the + * archive files. + * + * @param actionedUponNodeRef Download node containing information required + * to create the archive file, and which will eventually have its content + * updated with the archive file. + */ + @Override + protected void executeImpl(Action action, final NodeRef actionedUponNodeRef) + { + // Get the download request data and set up the exporter crawler parameters. + final DownloadRequest downloadRequest = downloadStorage.getDownloadRequest(actionedUponNodeRef); + + AuthenticationUtil.runAs(new RunAsWork() + { + + @Override + public Object doWork() throws Exception + { + + ExporterCrawlerParameters crawlerParameters = new ExporterCrawlerParameters(); + + Location exportFrom = new Location(downloadRequest.getRequetedNodeRefs()); + crawlerParameters.setExportFrom(exportFrom); + + crawlerParameters.setCrawlSelf(true); + crawlerParameters.setExcludeChildAssocs(new QName[] {RenditionModel.ASSOC_RENDITION}); + crawlerParameters.setExcludeAspects(new QName[] {ContentModel.ASPECT_WORKING_COPY}); + + // Get an estimate of the size for statuses + SizeEstimator estimator = new SizeEstimator(checkOutCheckInService, nodeService); + exporterService.exportView(estimator, crawlerParameters, null); + + if (maximumContentSize > 0 && estimator.getSize() > maximumContentSize) + { + maximumContentSizeExceeded(actionedUponNodeRef, estimator.getSize(), estimator.getFileCount()); + } + else + { + createDownload(actionedUponNodeRef, crawlerParameters, estimator); + } + return null; + } + + }, downloadRequest.getOwner()); + + } + + @Override + protected void addParameterDefinitions(List paramList) + { + } + + + private void maximumContentSizeExceeded(final NodeRef actionedUponNodeRef, final long size, final long fileCount) + { + log.debug("Maximum contentent size ({}), exceeded ({})", maximumContentSize, size); + + //Update the content and set the status to done. + transactionHelper.doInTransaction(new RetryingTransactionCallback() + { + + @Override + public Object execute() throws Throwable + { + DownloadStatus status = new DownloadStatus(Status.MAX_CONTENT_SIZE_EXCEEDED, maximumContentSize, size, 0, fileCount); + updateService.update(actionedUponNodeRef, status, 1); + return null; + } + }, false, true); + } + + private void createDownload(final NodeRef actionedUponNodeRef, ExporterCrawlerParameters crawlerParameters, SizeEstimator estimator) + { + // perform the actual export + final File tempFile = TempFileProvider.createTempFile(TEMP_FILE_PREFIX, TEMP_FILE_SUFFIX); + final ZipDownloadExporter handler = new ZipDownloadExporter(tempFile, checkOutCheckInService, nodeService, transactionHelper, updateService, downloadStorage, actionedUponNodeRef, estimator.getSize(), estimator.getFileCount()); + + try { + exporterService.exportView(handler, crawlerParameters, null); + archiveCreationComplete(actionedUponNodeRef, tempFile, handler); + } + catch (DownloadCancelledException ex) + { + downloadCancelled(actionedUponNodeRef, handler); + } + finally + { + tempFile.delete(); + } + } + + + private void archiveCreationComplete(final NodeRef actionedUponNodeRef, final File tempFile, + final ZipDownloadExporter handler) + { + //Update the content and set the status to done. + transactionHelper.doInTransaction(new RetryingTransactionCallback() + { + + @Override + public Object execute() throws Throwable + { + try + { + contentServiceHelper.updateContent(actionedUponNodeRef, tempFile); + DownloadStatus status = new DownloadStatus(Status.DONE, handler.getDone(), handler.getTotal(), handler.getFilesAdded(), handler.getTotalFiles()); + updateService.update(actionedUponNodeRef, status, handler.getNextSequenceNumber()); + + return null; + } + catch (ContentIOException ex) + { + throw new DownloadServiceException(CREATION_ERROR, ex); + } + catch (FileNotFoundException ex) + { + throw new DownloadServiceException(CREATION_ERROR, ex); + } + catch (IOException ex) + { + throw new DownloadServiceException(CREATION_ERROR, ex); + } + + } + }, false, true); + } + + + private void downloadCancelled(final NodeRef actionedUponNodeRef, final ZipDownloadExporter handler) + { + //Update the content and set the status to done. + transactionHelper.doInTransaction(new RetryingTransactionCallback() + { + @Override + public Object execute() throws Throwable + { + DownloadStatus status = new DownloadStatus(Status.CANCELLED, handler.getDone(), handler.getTotal(), handler.getFilesAdded(), handler.getTotalFiles()); + updateService.update(actionedUponNodeRef, status, handler.getNextSequenceNumber()); + + return null; + } + }, false, true); + + } + +} diff --git a/source/java/org/alfresco/repo/download/DownloadCancelledException.java b/source/java/org/alfresco/repo/download/DownloadCancelledException.java new file mode 100644 index 0000000000..79bd5acd6d --- /dev/null +++ b/source/java/org/alfresco/repo/download/DownloadCancelledException.java @@ -0,0 +1,18 @@ +package org.alfresco.repo.download; + +import org.alfresco.service.cmr.view.ExporterException; + +/** + * Exception thrown by ZipDownloadExporter, if a download is cancelled mid flow. + * + * @author Alex Miller + */ +public class DownloadCancelledException extends ExporterException +{ + private static final long serialVersionUID = 4694929866014032096L; + + public DownloadCancelledException() + { + super("Download Cancelled"); + } +} diff --git a/source/java/org/alfresco/repo/download/DownloadModel.java b/source/java/org/alfresco/repo/download/DownloadModel.java new file mode 100644 index 0000000000..f6e4fc61ea --- /dev/null +++ b/source/java/org/alfresco/repo/download/DownloadModel.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2005-2012 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * 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 . + */ +package org.alfresco.repo.download; + +import org.alfresco.service.namespace.QName; + +/** + * Utility interface for the downloadModel.xml + * + * @author Alex Miller + */ +public interface DownloadModel +{ + /** Download Model URI */ + static final String DOWNLOAD_MODEL_1_0_URI = "http://www.alfresco.org/model/download/1.0"; + + /** Type QName */ + static final QName TYPE_DOWNLOAD = QName.createQName(DOWNLOAD_MODEL_1_0_URI, "download"); + + // Property QNames + static final QName PROP_CANCELLED = QName.createQName(DOWNLOAD_MODEL_1_0_URI, "cancelled"); + static final QName PROP_DONE = QName.createQName(DOWNLOAD_MODEL_1_0_URI, "done"); + static final QName PROP_FILES_ADDED = QName.createQName(DOWNLOAD_MODEL_1_0_URI, "filesAdded"); + static final QName PROP_RECURSIVE = QName.createQName(DOWNLOAD_MODEL_1_0_URI, "recursive"); + static final QName PROP_SEQUENCE_NUMBER = QName.createQName(DOWNLOAD_MODEL_1_0_URI, "sequenceNumber"); + static final QName PROP_STATUS = QName.createQName(DOWNLOAD_MODEL_1_0_URI, "status"); + static final QName PROP_TOTAL = QName.createQName(DOWNLOAD_MODEL_1_0_URI, "total"); + static final QName PROP_TOTAL_FILES = QName.createQName(DOWNLOAD_MODEL_1_0_URI, "totalFiles"); + + // Associations + static final QName ASSOC_REQUESTED_NODES = QName.createQName(DOWNLOAD_MODEL_1_0_URI, "requestedNodes"); +} diff --git a/source/java/org/alfresco/repo/download/DownloadServiceException.java b/source/java/org/alfresco/repo/download/DownloadServiceException.java new file mode 100644 index 0000000000..88ee2bfc93 --- /dev/null +++ b/source/java/org/alfresco/repo/download/DownloadServiceException.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2005-2012 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * 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 . + */ +package org.alfresco.repo.download; + +import org.alfresco.error.AlfrescoRuntimeException; + +/** + * Download Service Exception class + * + * @author Alex Miller + */ +public class DownloadServiceException extends AlfrescoRuntimeException +{ + private static final long serialVersionUID = 1826926526215676002L; + + public DownloadServiceException(String message, Throwable cause) + { + super(message, cause); + } + +} diff --git a/source/java/org/alfresco/repo/download/DownloadServiceImpl.java b/source/java/org/alfresco/repo/download/DownloadServiceImpl.java new file mode 100644 index 0000000000..04fd3c206b --- /dev/null +++ b/source/java/org/alfresco/repo/download/DownloadServiceImpl.java @@ -0,0 +1,134 @@ +/* + * Copyright (C) 2005-2012 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * 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 . + */ +package org.alfresco.repo.download; + +import java.util.Date; +import java.util.List; + +import org.alfresco.repo.download.cannedquery.DownloadEntity; +import org.alfresco.repo.transaction.RetryingTransactionHelper; +import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; +import org.alfresco.service.cmr.download.DownloadService; +import org.alfresco.service.cmr.download.DownloadStatus; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.util.ParameterCheck; + +/** + * Implementation of the download service. + * + * Persists the download reqest and then uses a local action service to execute + * the {@link CreateDownloadArchiveAction}. + * + * @author Alex Miller + */ +public class DownloadServiceImpl implements DownloadService { + + // Dependencies + private ActionServiceHelper actionServiceHelper; + private DownloadStorage downloadStorage; + private RetryingTransactionHelper transactionHelper; + + // Dependency setters + public void setActionServiceHelper(ActionServiceHelper actionServiceHelper) + { + this.actionServiceHelper = actionServiceHelper; + } + + public void setTransactionHelper(RetryingTransactionHelper transactionHelper) + { + this.transactionHelper = transactionHelper; + } + + public void setDownloadStorage(DownloadStorage downloadStorage) + { + this.downloadStorage = downloadStorage; + } + + @Override + public NodeRef createDownload(final NodeRef[] requestedNodes, final boolean recursive) { + ParameterCheck.mandatory("nodeRefs", requestedNodes); + if (requestedNodes.length < 1) + { + throw new IllegalArgumentException("Need at least 1 node ref"); + } + + // This is done in a new transaction to avoid node not found errors when the zip creation occurs + // on a remote transformation server. + NodeRef downloadNode = transactionHelper.doInTransaction(new RetryingTransactionCallback() + { + + @Override + public NodeRef execute() throws Throwable + { + //Create a download node + NodeRef downloadNode = downloadStorage.createDownloadNode(recursive); + + //Add requested nodes + for (NodeRef node : requestedNodes) + { + downloadStorage.addNodeToDownload(downloadNode, node); + } + + return downloadNode; + } + }, false, true); + + //Trigger the action. + actionServiceHelper.executeAction(downloadNode); + + return downloadNode; + } + + + @Override + public DownloadStatus getDownloadStatus(NodeRef downloadNode) { + ParameterCheck.mandatory("downloadNode", downloadNode); + + return downloadStorage.getDownloadStatus(downloadNode); + } + + /* + * @see org.alfresco.service.cmr.download.DownloadService#deleteDownloads(java.util.Date) + */ + @Override + public void deleteDownloads(Date before) + { + List> downloadPages = downloadStorage.getDownloadsCreatedBefore(before); + for (List page : downloadPages) + { + for (DownloadEntity download : page) + { + downloadStorage.delete(download.getNodeRef()); + } + } + + } + + /* + * @see org.alfresco.service.cmr.download.DownloadService#cancelDownload(org.alfresco.service.cmr.repository.NodeRef) + */ + @Override + public void cancelDownload(NodeRef downloadNodeRef) + { + ParameterCheck.mandatory("downloadNodeRef", downloadNodeRef); + + downloadStorage.cancelDownload(downloadNodeRef); + } + +} diff --git a/source/java/org/alfresco/repo/download/DownloadServiceIntegrationTest.java b/source/java/org/alfresco/repo/download/DownloadServiceIntegrationTest.java new file mode 100644 index 0000000000..16a1507e76 --- /dev/null +++ b/source/java/org/alfresco/repo/download/DownloadServiceIntegrationTest.java @@ -0,0 +1,408 @@ +/* + * Copyright (C) 2005-2011 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * 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 . + */ +package org.alfresco.repo.download; + +import java.io.IOException; +import java.io.Serializable; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; + +import net.sf.acegisecurity.Authentication; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.model.Repository; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.transaction.RetryingTransactionHelper; +import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; +import org.alfresco.service.cmr.coci.CheckOutCheckInService; +import org.alfresco.service.cmr.download.DownloadService; +import org.alfresco.service.cmr.download.DownloadStatus; +import org.alfresco.service.cmr.download.DownloadStatus.Status; +import org.alfresco.service.cmr.repository.AssociationRef; +import org.alfresco.service.cmr.repository.ContentReader; +import org.alfresco.service.cmr.repository.ContentService; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.security.PermissionService; +import org.alfresco.service.namespace.QName; +import org.alfresco.util.test.junitrules.AlfrescoPerson; +import org.alfresco.util.test.junitrules.ApplicationContextInit; +import org.alfresco.util.test.junitrules.TemporaryNodes; +import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; +import org.apache.commons.compress.archivers.zip.ZipArchiveInputStream; +import org.junit.Assert; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.RuleChain; + +/** + * Integration test for DownloadServiceImpl + * + * @author Alex Miller + */ +public class DownloadServiceIntegrationTest +{ + public static final long MAX_TIME = 5000; + + private static final long PAUSE_TIME = 1000; + + // Rule to initialize the default Alfresco spring configuration + public static ApplicationContextInit APP_CONTEXT_INIT = new ApplicationContextInit(); + + // Rules to create 2 test users. + public static AlfrescoPerson TEST_USER = new AlfrescoPerson(APP_CONTEXT_INIT, "User"); + public static AlfrescoPerson TEST_USER2 = new AlfrescoPerson(APP_CONTEXT_INIT, "User 2"); + + // A rule to manage test nodes reused across all the test methods + public static TemporaryNodes STATIC_TEST_NODES = new TemporaryNodes(APP_CONTEXT_INIT); + + // Tie them together in a static Rule Chain + @ClassRule public static RuleChain ruleChain = RuleChain.outerRule(APP_CONTEXT_INIT) + .around(TEST_USER) + .around(STATIC_TEST_NODES); + + // A rule to manage test nodes use in each test method + @Rule public TemporaryNodes testNodes = new TemporaryNodes(APP_CONTEXT_INIT); + + // Service under test + public static DownloadService DOWNLOAD_SERVICE; + + // Various supporting services + private static CheckOutCheckInService CHECK_OUT_CHECK_IN_SERVICE; + private static ContentService CONTENT_SERVICE; + private static NodeService NODE_SERVICE; + private static PermissionService PERMISSION_SERVICE; + private static RetryingTransactionHelper TRANSACTION_HELPER; + + // Test Content + private NodeRef rootFolder; + private NodeRef rootFile; + + private NodeRef level1Folder1; + + private NodeRef level1Folder2; + + private Set allEntries; + + private NodeRef fileToCheckout; + + @BeforeClass public static void init() + { + // Resolve required services + CHECK_OUT_CHECK_IN_SERVICE = APP_CONTEXT_INIT.getApplicationContext().getBean("CheckOutCheckInService", CheckOutCheckInService.class); + CONTENT_SERVICE = APP_CONTEXT_INIT.getApplicationContext().getBean("contentService", ContentService.class); + DOWNLOAD_SERVICE = APP_CONTEXT_INIT.getApplicationContext().getBean("DownloadService", DownloadService.class); + NODE_SERVICE = APP_CONTEXT_INIT.getApplicationContext().getBean("NodeService", NodeService.class); + PERMISSION_SERVICE = APP_CONTEXT_INIT.getApplicationContext().getBean("PermissionService", PermissionService.class); + TRANSACTION_HELPER = APP_CONTEXT_INIT.getApplicationContext().getBean("retryingTransactionHelper", RetryingTransactionHelper.class); + } + + /** + * Create the test content + */ + @Before public void createContent() + { + allEntries = new TreeSet(); + + AuthenticationUtil.setRunAsUserSystem(); + + Repository repositoryHelper = (Repository) APP_CONTEXT_INIT.getApplicationContext().getBean("repositoryHelper"); + NodeRef COMPANY_HOME = repositoryHelper.getCompanyHome(); + + // Create some static test content + rootFolder = testNodes.createNode(COMPANY_HOME, "rootFolder", ContentModel.TYPE_FOLDER, AuthenticationUtil.getAdminUserName()); + allEntries.add("rootFolder/"); + + rootFile = testNodes.createNodeWithTextContent(COMPANY_HOME, "rootFile.txt", ContentModel.TYPE_CONTENT, AuthenticationUtil.getAdminUserName(), "Root file content"); + allEntries.add("rootFile.txt"); + + testNodes.createNodeWithTextContent(rootFolder, "level1File.txt", ContentModel.TYPE_CONTENT, AuthenticationUtil.getAdminUserName(), "Level 1 file content"); + allEntries.add("rootFolder/level1File.txt"); + + level1Folder1 = testNodes.createNode(rootFolder, "level1Folder1", ContentModel.TYPE_FOLDER, AuthenticationUtil.getAdminUserName()); + allEntries.add("rootFolder/level1Folder1/"); + + level1Folder2 = testNodes.createNode(rootFolder, "level1Folder2", ContentModel.TYPE_FOLDER, AuthenticationUtil.getAdminUserName()); + allEntries.add("rootFolder/level1Folder2/"); + + testNodes.createNode(rootFolder, "level1EmptyFolder", ContentModel.TYPE_FOLDER, AuthenticationUtil.getAdminUserName()); + allEntries.add("rootFolder/level1EmptyFolder/"); + + testNodes.createNodeWithTextContent(level1Folder1, "level2File.txt", ContentModel.TYPE_CONTENT, AuthenticationUtil.getAdminUserName(), "Level 2 file content"); + allEntries.add("rootFolder/level1Folder1/level2File.txt"); + + testNodes.createNodeWithTextContent(level1Folder2, "level2File.txt", ContentModel.TYPE_CONTENT, AuthenticationUtil.getAdminUserName(), "Level 2 file content"); + allEntries.add("rootFolder/level1Folder2/level2File.txt"); + + fileToCheckout = testNodes.createNodeWithTextContent(level1Folder2, "fileToCheckout.txt", ContentModel.TYPE_CONTENT, AuthenticationUtil.getAdminUserName(), "Level 2 file content"); + // Add the lock and version aspects to the created node + NODE_SERVICE.addAspect(fileToCheckout, ContentModel.ASPECT_VERSIONABLE, null); + NODE_SERVICE.addAspect(fileToCheckout, ContentModel.ASPECT_LOCKABLE, null); + + allEntries.add("rootFolder/level1Folder2/fileToCheckout.txt"); + PERMISSION_SERVICE.setPermission(level1Folder2, TEST_USER.getUsername(), PermissionService.ALL_PERMISSIONS, true); + PERMISSION_SERVICE.setPermission(fileToCheckout, TEST_USER.getUsername(), PermissionService.ALL_PERMISSIONS, true); + } + + @Test public void createDownload() throws IOException, InterruptedException + { + // Initiate the download + final NodeRef downloadNode = DOWNLOAD_SERVICE.createDownload(new NodeRef[] {rootFile, rootFolder}, true); + Assert.assertNotNull(downloadNode); + + testNodes.addNodeRef(downloadNode); + + // Validate that the download node has been persisted correctly. + TRANSACTION_HELPER.doInTransaction(new RetryingTransactionCallback() + { + + @Override + public Object execute() throws Throwable + { + Map properties = NODE_SERVICE.getProperties(downloadNode); + Assert.assertEquals(Boolean.TRUE, properties.get(DownloadModel.PROP_RECURSIVE)); + + List associations = NODE_SERVICE.getTargetAssocs(downloadNode, DownloadModel.ASSOC_REQUESTED_NODES); + for (AssociationRef association : associations) + { + Assert.assertTrue(association.getTargetRef().equals(rootFile) || association.getTargetRef().equals(rootFolder)); + } + return null; + } + }); + + DownloadStatus status = getDownloadStatus(downloadNode); + while (status.getStatus() == Status.PENDING) + { + Thread.sleep(PAUSE_TIME); + status = getDownloadStatus(downloadNode); + } + + Assert.assertEquals(5l, status.getTotalFiles()); + + long elapsedTime = waitForDownload(downloadNode); + + Assert.assertTrue("Maximum creation time exceeded!", elapsedTime < MAX_TIME); + + + // Validate the content. + final Set entryNames = getEntries(downloadNode); + + validateEntries(entryNames, allEntries, true); + } + + private void validateEntries(final Set entryNames, final Set expectedEntries, boolean onlyExpected) + { + Set copy = new TreeSet(entryNames); + for (String expectedEntry : expectedEntries) + { + Assert.assertTrue("Missing entry:- " + expectedEntry, copy.contains(expectedEntry)); + copy.remove(expectedEntry); + } + + if (onlyExpected == true) + { + Assert.assertTrue("Unexpected entries", copy.isEmpty()); + } + } + + private Set getEntries(final NodeRef downloadNode) + { + return TRANSACTION_HELPER.doInTransaction(new RetryingTransactionCallback>() + { + + @Override + public Set execute() throws Throwable + { + Set entryNames = new TreeSet(); + ContentReader reader = CONTENT_SERVICE.getReader(downloadNode, ContentModel.PROP_CONTENT); + ZipArchiveInputStream zipInputStream = new ZipArchiveInputStream(reader.getContentInputStream()); + try + { + ZipArchiveEntry zipEntry = zipInputStream.getNextZipEntry(); + while (zipEntry != null) + { + String name = zipEntry.getName(); + entryNames.add(name); + zipEntry = zipInputStream.getNextZipEntry(); + } + } + finally + { + zipInputStream.close(); + } + return entryNames; + } + }); + } + + private long waitForDownload(final NodeRef downloadNode) throws InterruptedException + { + long startTime = System.currentTimeMillis(); + // Wait for the staus to become done. + DownloadStatus status; + long elapsedTime; + do { + status = getDownloadStatus(downloadNode); + elapsedTime = System.currentTimeMillis() - startTime; + if (status.isComplete() == false) + { + Thread.sleep(PAUSE_TIME); + } + } while (status.isComplete() == false && elapsedTime < MAX_TIME); + return elapsedTime; + } + + + + private DownloadStatus getDownloadStatus(final NodeRef downloadNode) + { + return TRANSACTION_HELPER.doInTransaction(new RetryingTransactionCallback() + { + + @Override + public DownloadStatus execute() throws Throwable + { + return DOWNLOAD_SERVICE.getDownloadStatus(downloadNode); + } + }); + } + + @Test public void deleteBefore() throws InterruptedException + { + NodeRef beforeNodeRef; + NodeRef afterNodeRef; + Date beforeTime; + + beforeNodeRef = DOWNLOAD_SERVICE.createDownload(new NodeRef[] {level1Folder1}, true); + testNodes.addNodeRef(beforeNodeRef); + waitForDownload(beforeNodeRef); + + beforeTime = new Date(); + + afterNodeRef = DOWNLOAD_SERVICE.createDownload(new NodeRef[] {level1Folder2}, true); + testNodes.addNodeRef(afterNodeRef); + waitForDownload(afterNodeRef); + + DOWNLOAD_SERVICE.deleteDownloads(beforeTime); + + Assert.assertFalse(NODE_SERVICE.exists(beforeNodeRef)); + Assert.assertTrue(NODE_SERVICE.exists(afterNodeRef)); + + } + + @Test public void cancel() throws InterruptedException + { + // Initiate the download + final NodeRef downloadNode = DOWNLOAD_SERVICE.createDownload(new NodeRef[] {rootFile, rootFolder}, true); + Assert.assertNotNull(downloadNode); + + testNodes.addNodeRef(downloadNode); + + DOWNLOAD_SERVICE.cancelDownload(downloadNode); + + DownloadStatus status = getDownloadStatus(downloadNode); + int retryCount = 0; + while (status.getStatus() != Status.CANCELLED && retryCount < 5) + { + retryCount++; + Thread.sleep(PAUSE_TIME); + status = getDownloadStatus(downloadNode); + } + + Assert.assertEquals(Status.CANCELLED, status.getStatus()); + } + + /** + * This test verifies that a user is given the correct file, when it is checked. The user who checked out + * the file should get the working copy, while any other user should get the default version. + * @throws InterruptedException + */ + @Test public void workingCopies() throws InterruptedException + { + final Set preCheckoutExpectedEntries = new TreeSet(); + preCheckoutExpectedEntries.add("level1Folder2/"); + preCheckoutExpectedEntries.add("level1Folder2/level2File.txt"); + preCheckoutExpectedEntries.add("level1Folder2/fileToCheckout.txt"); + + validateWorkingCopyFolder(preCheckoutExpectedEntries, level1Folder2, TEST_USER.getUsername()); + validateWorkingCopyFolder(preCheckoutExpectedEntries, level1Folder2, TEST_USER2.getUsername()); + + Authentication previousAuth = AuthenticationUtil.getFullAuthentication(); + AuthenticationUtil.setFullyAuthenticatedUser(TEST_USER.getUsername()); + NodeRef workingCopy; + try + { + workingCopy = CHECK_OUT_CHECK_IN_SERVICE.checkout(fileToCheckout); + } + finally + { + AuthenticationUtil.setFullAuthentication(previousAuth); + } + + try + { + validateWorkingCopyFolder(preCheckoutExpectedEntries, level1Folder2, TEST_USER2.getUsername()); + + final Set postCheckoutExpectedEntries = new TreeSet(); + postCheckoutExpectedEntries.add("level1Folder2/"); + postCheckoutExpectedEntries.add("level1Folder2/level2File.txt"); + postCheckoutExpectedEntries.add("level1Folder2/fileToCheckout (Working Copy).txt"); + validateWorkingCopyFolder(postCheckoutExpectedEntries, level1Folder2, TEST_USER.getUsername()); + } + finally + { + previousAuth = AuthenticationUtil.getFullAuthentication(); + AuthenticationUtil.setFullyAuthenticatedUser(TEST_USER.getUsername()); + try + { + CHECK_OUT_CHECK_IN_SERVICE.checkin(workingCopy, null); + } + finally + { + AuthenticationUtil.setFullAuthentication(previousAuth); + } + } + validateWorkingCopyFolder(preCheckoutExpectedEntries, level1Folder2, TEST_USER.getUsername()); + validateWorkingCopyFolder(preCheckoutExpectedEntries, level1Folder2, TEST_USER2.getUsername()); + } + + private void validateWorkingCopyFolder(final Set expectedEntries, final NodeRef folder, final String userID) throws InterruptedException + { + Authentication previousAuthentication = AuthenticationUtil.getFullAuthentication(); + AuthenticationUtil.setFullyAuthenticatedUser(userID); + try + { + final NodeRef downloadNode = DOWNLOAD_SERVICE.createDownload(new NodeRef[] {folder}, true); + waitForDownload(downloadNode); + + validateEntries(getEntries(downloadNode), expectedEntries, true); + } + finally + { + AuthenticationUtil.setFullAuthentication(previousAuthentication); + } + } +} diff --git a/source/java/org/alfresco/repo/download/DownloadStatusUpdateService.java b/source/java/org/alfresco/repo/download/DownloadStatusUpdateService.java new file mode 100644 index 0000000000..4a775cfb71 --- /dev/null +++ b/source/java/org/alfresco/repo/download/DownloadStatusUpdateService.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2005-2011 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * 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 . + */ +package org.alfresco.repo.download; + +import org.alfresco.service.cmr.download.DownloadStatus; +import org.alfresco.service.cmr.repository.NodeRef; + +/** + * Service for updating the status of a download. + * + * @author Alex Miller + */ +public interface DownloadStatusUpdateService +{ + /** + * Update and persist the status of the download. + * + * Implementations should only do this if sequenceNumber is greater than + * the sequenceNumber of the previous update, to prevent out of order + * updates. + * + * @param nodeRef The download node, whose status is to be updated. + * @param status The new status + * @param sequenceNumber + */ + void update(NodeRef nodeRef, DownloadStatus status, int sequenceNumber); +} diff --git a/source/java/org/alfresco/repo/download/DownloadStatusUpdateServiceImpl.java b/source/java/org/alfresco/repo/download/DownloadStatusUpdateServiceImpl.java new file mode 100644 index 0000000000..ddb967258a --- /dev/null +++ b/source/java/org/alfresco/repo/download/DownloadStatusUpdateServiceImpl.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2005-2012 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * 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 . + */ +package org.alfresco.repo.download; + +import org.alfresco.service.cmr.download.DownloadStatus; +import org.alfresco.service.cmr.repository.NodeRef; +import org.mockito.internal.progress.SequenceNumber; + +/** + * Implementation class responsible for update the status of a download node. + * + * @author Alex Miller + */ +public class DownloadStatusUpdateServiceImpl implements DownloadStatusUpdateService +{ + + // Dependencies + private DownloadStorage storage; + + // Dependency setters + public void setStorage(DownloadStorage storage) + { + this.storage = storage; + } + + @Override + public void update(NodeRef nodeRef, DownloadStatus status, int sequenceNumber) + { + + // Update the status of the download node, if and only if sequenceNumber is + // greater than the sequence number of the last update. + int currentSequenceNumber = storage.getSequenceNumber(nodeRef); + + if (currentSequenceNumber < sequenceNumber) + { + storage.updateStatus(nodeRef, status); + } + } + +} diff --git a/source/java/org/alfresco/repo/download/DownloadStorage.java b/source/java/org/alfresco/repo/download/DownloadStorage.java new file mode 100644 index 0000000000..ad5177efbb --- /dev/null +++ b/source/java/org/alfresco/repo/download/DownloadStorage.java @@ -0,0 +1,255 @@ +/* + * Copyright 2005-2012 Alfresco Software, Ltd. All rights reserved. + * + * License rights for this program may be obtained from Alfresco Software, Ltd. + * pursuant to a written agreement and any use of this program without such an + * agreement is prohibited. + */ +package org.alfresco.repo.download; + +import java.io.Serializable; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.alfresco.model.ContentModel; +import org.alfresco.query.CannedQueryFactory; +import org.alfresco.query.CannedQueryResults; +import org.alfresco.repo.download.cannedquery.DownloadEntity; +import org.alfresco.repo.download.cannedquery.GetDownloadsCannedQuery; +import org.alfresco.repo.download.cannedquery.GetDownloadsCannedQueryFactory; +import org.alfresco.repo.importer.ImporterBootstrap; +import org.alfresco.repo.model.Repository; +import org.alfresco.repo.node.SystemNodeUtils; +import org.alfresco.service.cmr.download.DownloadRequest; +import org.alfresco.service.cmr.download.DownloadStatus; +import org.alfresco.service.cmr.repository.AssociationRef; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.alfresco.util.registry.NamedObjectRegistry; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * This class is responsible for the persistence of {@link DownloadDefinition} objects using lower-level + * repo services such as the {@link NodeService}. The higher-level business logic around these CRUD calls + * is contained within the {@link DownloadServiceImpl}. + * + * @author Alex Miller + */ +public class DownloadStorage +{ + private static final Log log = LogFactory.getLog(DownloadStorage.class); + + // service dependencies + private ImporterBootstrap bootstrap; + private Repository repositoryHelper; + private NodeService nodeService; + private NamespaceService namespaceService; + private NamedObjectRegistry> queryRegistry; + + public void setImporterBootstrap(ImporterBootstrap bootstrap) + { + this.bootstrap = bootstrap; + } + + public void setQueryRegistry(NamedObjectRegistry> queryRegistry) + { + this.queryRegistry = queryRegistry; + } + + public void setRepositoryHelper(Repository repositoryHelper) + { + this.repositoryHelper = repositoryHelper; + } + + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + public void setNamespaceService(NamespaceService namespaceService) + { + this.namespaceService = namespaceService; + } + + /** + * This method finds the SyncSet Definition Container NodeRef, creating one if it does not exist. + * + * @return the syncset definition container + */ + public NodeRef getOrCreateDowloadContainer() + { + NodeRef downloadsContainer = getContainer(); + + if (downloadsContainer == null) + { + if (log.isInfoEnabled()) + log.info("Lazy creating the Downloads System Container "); + + downloadsContainer = SystemNodeUtils.getOrCreateSystemChildContainer(getContainerQName(), nodeService, repositoryHelper).getFirst(); + } + return downloadsContainer; + } + + private NodeRef getContainer() + { + return SystemNodeUtils.getSystemChildContainer(getContainerQName(), nodeService, repositoryHelper); + } + + private QName getContainerQName() + { + String name = bootstrap.getConfiguration().getProperty("system.downloads_container.childname"); + QName container = QName.createQName(name, namespaceService); + return container; + } + + + public NodeRef createDownloadNode(boolean recursive) + { + NodeRef downloadsContainer = getOrCreateDowloadContainer(); + + Map downloadProperties = new HashMap(); + downloadProperties.put(DownloadModel.PROP_RECURSIVE, recursive); + + ChildAssociationRef newChildAssoc = nodeService.createNode(downloadsContainer, + ContentModel.ASSOC_CHILDREN, ContentModel.ASSOC_CHILDREN, + DownloadModel.TYPE_DOWNLOAD, + downloadProperties); + + final NodeRef downloadNodeRef = newChildAssoc.getChildRef(); + + if (log.isDebugEnabled()) + { + StringBuilder msg = new StringBuilder(); + msg.append("Created Download. ") + .append("', Download-NodeRef="); + log.debug(msg.toString()); + } + return downloadNodeRef; + } + + public void cancelDownload(NodeRef downloadNodeRef) + { + validateNode(downloadNodeRef); + + nodeService.setProperty(downloadNodeRef, DownloadModel.PROP_CANCELLED, true); + } + + public boolean isCancelled(NodeRef downloadNodeRef) + { + validateNode(downloadNodeRef); + + return (Boolean)nodeService.getProperty(downloadNodeRef, DownloadModel.PROP_CANCELLED); + } + + public void addNodeToDownload(NodeRef downloadNode, NodeRef nodeToAdd) + { + nodeService.createAssociation(downloadNode, nodeToAdd, DownloadModel.ASSOC_REQUESTED_NODES); + + if (log.isDebugEnabled()) + { + StringBuilder msg = new StringBuilder(); + msg.append("Node added to Download-NodeRef '") + .append(downloadNode).append("'. RequestedNode=") + .append(nodeToAdd); + log.debug(msg.toString()); + } + + } + + public DownloadRequest getDownloadRequest(NodeRef downloadNodeRef) + { + validateNode(downloadNodeRef); + Map properties = nodeService.getProperties(downloadNodeRef); + + List requestedNodes = nodeService.getTargetAssocs(downloadNodeRef, DownloadModel.ASSOC_REQUESTED_NODES); + + return new DownloadRequest((Boolean)properties.get(DownloadModel.PROP_RECURSIVE), requestedNodes, (String)properties.get(ContentModel.PROP_CREATOR)); + } + + private void validateNode(NodeRef downloadNodeRef) + { + if (nodeService.getType(downloadNodeRef).equals(DownloadModel.TYPE_DOWNLOAD) == false) + { + throw new IllegalArgumentException("Invlaid node type for nodeRef:-" + downloadNodeRef); + } + } + + public DownloadStatus getDownloadStatus(NodeRef downloadNodeRef) + { + validateNode(downloadNodeRef); + Map properties = nodeService.getProperties(downloadNodeRef); + + Long done = (Long)properties.get(DownloadModel.PROP_DONE); + Long total = (Long)properties.get(DownloadModel.PROP_TOTAL); + Long filesAdded = (Long)properties.get(DownloadModel.PROP_FILES_ADDED); + Long totalFiles = (Long)properties.get(DownloadModel.PROP_TOTAL_FILES); + + return new DownloadStatus(DownloadStatus.Status.valueOf((String)properties.get(DownloadModel.PROP_STATUS)), + done != null ? done.longValue() : 0l, + total != null ? total.longValue() : 0l, + filesAdded != null ? filesAdded.longValue() : 0l, + totalFiles != null ? totalFiles.longValue() : 0l); + } + + public int getSequenceNumber(NodeRef nodeRef) + { + validateNode(nodeRef); + Serializable sequenceNumber = nodeService.getProperty(nodeRef, DownloadModel.PROP_SEQUENCE_NUMBER); + + return ((Integer)sequenceNumber).intValue(); + } + + public void updateStatus(NodeRef nodeRef, DownloadStatus status) + { + validateNode(nodeRef); + + nodeService.setProperty(nodeRef, DownloadModel.PROP_STATUS, status.getStatus().toString()); + nodeService.setProperty(nodeRef, DownloadModel.PROP_DONE, new Long(status.getDone())); + nodeService.setProperty(nodeRef, DownloadModel.PROP_TOTAL, new Long(status.getTotal())); + nodeService.setProperty(nodeRef, DownloadModel.PROP_FILES_ADDED, status.getFilesAdded()); + nodeService.setProperty(nodeRef, DownloadModel.PROP_TOTAL_FILES, status.getTotalFiles()); + } + + /** + * Get all the downloads created before before. + */ + public List> getDownloadsCreatedBefore(Date before) + { + NodeRef container = getContainer(); + + if (container == null) + { + return Collections.emptyList(); + } + + // Grab the factory + GetDownloadsCannedQueryFactory getDownloadCannedQueryFactory = + (GetDownloadsCannedQueryFactory)queryRegistry.getNamedObject("downloadGetDownloadsCannedQueryFactory"); + + // Run the canned query + GetDownloadsCannedQuery cq = (GetDownloadsCannedQuery)getDownloadCannedQueryFactory.getDownloadsCannedQuery(container, before); + + // Execute the canned query + CannedQueryResults results = cq.execute(); + + return results.getPages(); + } + + /** + * Delete the download node identified by nodeRef + * @param nodeRef + */ + public void delete(NodeRef nodeRef) + { + validateNode(nodeRef); + + nodeService.deleteNode(nodeRef); + } +} diff --git a/source/java/org/alfresco/repo/download/DownloadsCleanupJob.java b/source/java/org/alfresco/repo/download/DownloadsCleanupJob.java new file mode 100644 index 0000000000..cd819c6250 --- /dev/null +++ b/source/java/org/alfresco/repo/download/DownloadsCleanupJob.java @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2005-2011 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * 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 . + */ +package org.alfresco.repo.download; + +import java.util.List; + +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork; +import org.alfresco.repo.tenant.Tenant; +import org.alfresco.repo.tenant.TenantAdminService; +import org.alfresco.repo.tenant.TenantUtil; +import org.alfresco.repo.tenant.TenantUtil.TenantRunAsWork; +import org.alfresco.service.cmr.download.DownloadService; +import org.joda.time.DateTime; +import org.quartz.Job; +import org.quartz.JobDataMap; +import org.quartz.JobExecutionContext; +import org.quartz.JobExecutionException; + +/** + * Executes the clean up of download nodes. + * + * @author Alex Miller + */ +public class DownloadsCleanupJob implements Job +{ + + private static final String KEY_DOWNLOAD_SERVICE = "downloadService"; + private static final String KEY_TENANT_ADMIN_SERVICE = "tenantAdminService"; + private static final String KEY_MAX_AGE = "maxAgeInMinutes"; + + + /* + * @see org.quartz.Job#execute(org.quartz.JobExecutionContext) + */ + @Override + public void execute(JobExecutionContext context) throws JobExecutionException + { + JobDataMap jobData = context.getJobDetail().getJobDataMap(); + + // extract the services and max age to use + final DownloadService downloadService = (DownloadService)jobData.get(KEY_DOWNLOAD_SERVICE); + final TenantAdminService tenantAdminService = (TenantAdminService)jobData.get(KEY_TENANT_ADMIN_SERVICE); + final int maxAgeInMinutes = Integer.parseInt((String)jobData.get(KEY_MAX_AGE)); + + final DateTime before = new DateTime().minusMinutes(maxAgeInMinutes); + + AuthenticationUtil.runAs(new RunAsWork() + { + public Object doWork() throws Exception + { + downloadService.deleteDownloads(before.toDate()); + return null; + } + }, AuthenticationUtil.getSystemUserName()); + + if ((tenantAdminService != null) && tenantAdminService.isEnabled()) + { + List tenants = tenantAdminService.getAllTenants(); + for (Tenant tenant : tenants) + { + TenantUtil.runAsSystemTenant(new TenantRunAsWork() + { + public Object doWork() throws Exception + { + downloadService.deleteDownloads(before.toDate()); + return null; + } + }, tenant.getTenantDomain()); + } + } + + } + +} diff --git a/source/java/org/alfresco/repo/download/LocalActionServiceHelper.java b/source/java/org/alfresco/repo/download/LocalActionServiceHelper.java new file mode 100644 index 0000000000..4b8a88f7c6 --- /dev/null +++ b/source/java/org/alfresco/repo/download/LocalActionServiceHelper.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2005-2011 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * 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 . + */ +package org.alfresco.repo.download; + +import org.alfresco.service.cmr.action.Action; +import org.alfresco.service.cmr.action.ActionService; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.util.ParameterCheck; +import org.springframework.beans.factory.InitializingBean; + +/** + * Implementation of {@link ActionServiceHelper} which schedules the zip creation process to run in the same alfresco node + * as the caller. + * + * @author Alex Miller + */ +public class LocalActionServiceHelper implements InitializingBean, ActionServiceHelper +{ + private ActionService localActionService; + + public void setLocalActionService(ActionService localActionService) + { + this.localActionService = localActionService; + } + + + @Override + public void executeAction(NodeRef downloadNode) + { + Action action = localActionService.createAction("createDownloadArchiveAction"); + action.setExecuteAsynchronously(true); + + localActionService.executeAction(action, downloadNode); + } + + @Override + public void afterPropertiesSet() throws Exception + { + ParameterCheck.mandatory("localActionServer", localActionService); + } + +} diff --git a/source/java/org/alfresco/repo/download/LocalContentServiceHelper.java b/source/java/org/alfresco/repo/download/LocalContentServiceHelper.java new file mode 100644 index 0000000000..7edba39b95 --- /dev/null +++ b/source/java/org/alfresco/repo/download/LocalContentServiceHelper.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2005-2011 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * 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 . + */ +package org.alfresco.repo.download; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork; +import org.alfresco.service.cmr.repository.ContentIOException; +import org.alfresco.service.cmr.repository.ContentService; +import org.alfresco.service.cmr.repository.ContentWriter; +import org.alfresco.service.cmr.repository.NodeRef; +import org.springframework.util.FileCopyUtils; + +/** + * {@link ContentServiceHelper} implementation which uses the local ContentService to update the content. + * + * @author Alex Miller + */ +public class LocalContentServiceHelper implements ContentServiceHelper +{ + + private ContentService contentService; + + public void setContentService(ContentService contentService) + { + this.contentService = contentService; + } + + @Override + public void updateContent(final NodeRef downloadNode, final File archiveFile) throws ContentIOException, FileNotFoundException, IOException + { + //RunAsSystem to mimic clustered behavior, and bypass quotas when using S3 storage. + AuthenticationUtil.runAsSystem(new RunAsWork() + { + @Override + public Object doWork() throws Exception + { + ContentWriter writer = contentService.getWriter(downloadNode, ContentModel.PROP_CONTENT, true); + FileCopyUtils.copy(new FileInputStream(archiveFile), writer.getContentOutputStream()); + return null; + } + }); + } +} diff --git a/source/java/org/alfresco/repo/download/ZipDownloadExporter.java b/source/java/org/alfresco/repo/download/ZipDownloadExporter.java new file mode 100644 index 0000000000..e6c9ef1573 --- /dev/null +++ b/source/java/org/alfresco/repo/download/ZipDownloadExporter.java @@ -0,0 +1,300 @@ +/* + * Copyright (C) 2005-2010 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * 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 . + */ +package org.alfresco.repo.download; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Deque; +import java.util.Iterator; +import java.util.LinkedList; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.transaction.RetryingTransactionHelper; +import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; +import org.alfresco.service.cmr.coci.CheckOutCheckInService; +import org.alfresco.service.cmr.download.DownloadStatus; +import org.alfresco.service.cmr.download.DownloadStatus.Status; +import org.alfresco.service.cmr.repository.ContentData; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.view.ExporterContext; +import org.alfresco.service.cmr.view.ExporterException; +import org.alfresco.service.namespace.QName; +import org.alfresco.util.Pair; +import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; +import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream; +import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream.UnicodeExtraFieldPolicy; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + + +/** + * Handler for exporting node content to a ZIP file + * + * @author Alex Miller + */ +public class ZipDownloadExporter extends BaseExporter +{ + private static Logger log = LoggerFactory.getLogger(ZipDownloadExporter.class); + + private static final String PATH_SEPARATOR = "/"; + + protected ZipArchiveOutputStream zipStream; + + private NodeRef downloadNodeRef; + private int sequenceNumber = 1; + private long total; + private long done; + private long totalFileCount; + private long filesAddedCount; + + private RetryingTransactionHelper transactionHelper; + private DownloadStorage downloadStorage; + private DownloadStatusUpdateService updateService; + + private Deque> path = new LinkedList>(); + private String currentName; + + private OutputStream outputStream; + + /** + * Construct + * + * @param destDir + * @param zipFile + * @param transactionHelper + * @param l + * @param actionedUponNodeRef + * @param dataFile + * @param contentDir + */ + public ZipDownloadExporter(File zipFile, CheckOutCheckInService checkOutCheckInService, NodeService nodeService, RetryingTransactionHelper transactionHelper, DownloadStatusUpdateService updateService, DownloadStorage downloadStorage, NodeRef downloadNodeRef, long total, long totalFileCount) + { + super(checkOutCheckInService, nodeService); + try + { + this.outputStream = new FileOutputStream(zipFile); + this.updateService = updateService; + this.transactionHelper = transactionHelper; + this.downloadStorage = downloadStorage; + + this.downloadNodeRef = downloadNodeRef; + this.total = total; + this.totalFileCount = totalFileCount; + } + catch (FileNotFoundException e) + { + throw new ExporterException("Failed to create zip file", e); + } + } + + @Override + public void start(final ExporterContext context) + { + zipStream = new ZipArchiveOutputStream(outputStream); + // NOTE: This encoding allows us to workaround bug... + // http://bugs.sun.com/bugdatabase/view_bug.do;:WuuT?bug_id=4820807 + zipStream.setEncoding("UTF-8"); + zipStream.setCreateUnicodeExtraFields(UnicodeExtraFieldPolicy.ALWAYS); + zipStream.setUseLanguageEncodingFlag(true); + zipStream.setFallbackToUTF8(true); + } + + + @Override + public void startNode(NodeRef nodeRef) + { + this.currentName = (String)nodeService.getProperty(nodeRef, ContentModel.PROP_NAME); + path.push(new Pair(currentName, nodeRef)); + if (ContentModel.TYPE_FOLDER.equals(nodeService.getType(nodeRef))) + { + String path = getPath() + PATH_SEPARATOR; + ZipArchiveEntry archiveEntry = new ZipArchiveEntry(path); + try + { + zipStream.putArchiveEntry(archiveEntry); + zipStream.closeArchiveEntry(); + } + catch (IOException e) + { + throw new ExporterException("Unexpected IOException adding folder entry", e); + } + } + } + + @Override + public void contentImpl(NodeRef nodeRef, QName property, InputStream content, ContentData contentData, int index) + { + // if the content stream to output is empty, then just return content descriptor as is + if (content == null) + { + return; + } + + try + { + // ALF-2016 + ZipArchiveEntry zipEntry=new ZipArchiveEntry(getPath()); + zipStream.putArchiveEntry(zipEntry); + + // copy export stream to zip + copyStream(zipStream, content); + + zipStream.closeArchiveEntry(); + filesAddedCount = filesAddedCount + 1; + } + catch (IOException e) + { + throw new ExporterException("Failed to zip export stream", e); + } + } + + + @Override + public void endNode(NodeRef nodeRef) + { + path.pop(); + } + + + + @Override + public void end() + { + try + { + zipStream.close(); + } + catch (IOException error) + { + throw new ExporterException("Unexpected error closing zip stream!", error); + } + } + + private String getPath() + { + if (path.size() < 1) + { + throw new IllegalStateException("No elements in path!"); + } + + Iterator> iter = path.descendingIterator(); + StringBuilder pathBuilder = new StringBuilder(); + + while (iter.hasNext()) + { + Pair element = iter.next(); + + pathBuilder.append(element.getFirst()); + if (iter.hasNext()) + { + pathBuilder.append(PATH_SEPARATOR); + } + } + + return pathBuilder.toString(); + } + + + /** + * Copy input stream to output stream + * + * @param output output stream + * @param in input stream + * @throws IOException + */ + private void copyStream(OutputStream output, InputStream in) + throws IOException + { + byte[] buffer = new byte[2048 * 10]; + int read = in.read(buffer, 0, 2048 *10); + while (read != -1) + { + output.write(buffer, 0, read); + done = done + read; + updateStatus(); + checkCancelled(); + read = in.read(buffer, 0, 2048 *10); + } + } + + private void checkCancelled() + { + boolean downloadCancelled = transactionHelper.doInTransaction(new RetryingTransactionCallback() + { + @Override + public Boolean execute() throws Throwable + { + return downloadStorage.isCancelled(downloadNodeRef); + } + + }, true, true); + + if ( downloadCancelled == true) + { + log.debug("Download cancelled"); + throw new DownloadCancelledException(); + } + } + + private void updateStatus() + { + transactionHelper.doInTransaction(new RetryingTransactionCallback() + { + + @Override + public Object execute() throws Throwable + { + DownloadStatus status = new DownloadStatus(Status.IN_PROGRESS, done, total, filesAddedCount, totalFileCount); + + updateService.update(downloadNodeRef, status, getNextSequenceNumber()); + return null; + } + }, false, true); + } + + public int getNextSequenceNumber() + { + return sequenceNumber++; + } + + public long getDone() + { + return done; + } + + public long getTotal() + { + return total; + } + + public long getFilesAdded() + { + return filesAddedCount; + } + + public long getTotalFiles() + { + return totalFileCount; + } +} diff --git a/source/java/org/alfresco/repo/download/cannedquery/DownloadEntity.java b/source/java/org/alfresco/repo/download/cannedquery/DownloadEntity.java new file mode 100644 index 0000000000..815291f1c3 --- /dev/null +++ b/source/java/org/alfresco/repo/download/cannedquery/DownloadEntity.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2005-2012 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * 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 . + */ +package org.alfresco.repo.download.cannedquery; + +import org.alfresco.repo.query.NodeBackedEntity; + +/** + * Download Entity - used by GetDownloads CQ + * + * @author Alex Miller + */ +public class DownloadEntity extends NodeBackedEntity +{ + /** + * Default constructor + */ + public DownloadEntity() + { + super(); + } + + public DownloadEntity(Long parentNodeId, Long nameQNameId, Long contentTypeQNameId) + { + super(parentNodeId, nameQNameId, contentTypeQNameId); + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/repo/download/cannedquery/GetDownloadsCannedQuery.java b/source/java/org/alfresco/repo/download/cannedquery/GetDownloadsCannedQuery.java new file mode 100644 index 0000000000..5df4b69822 --- /dev/null +++ b/source/java/org/alfresco/repo/download/cannedquery/GetDownloadsCannedQuery.java @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2005-2012 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * 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 . + */ +package org.alfresco.repo.download.cannedquery; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +import org.alfresco.query.CannedQueryParameters; +import org.alfresco.repo.domain.query.CannedQueryDAO; +import org.alfresco.repo.security.permissions.impl.acegi.AbstractCannedQueryPermissions; +import org.alfresco.repo.security.permissions.impl.acegi.MethodSecurityBean; +import org.alfresco.service.cmr.download.DownloadService; +import org.alfresco.service.cmr.repository.datatype.DefaultTypeConverter; + +/** + * This class provides the GetDownloads canned queries} used by the + * {@link DownloadService}.deleteDOwnloads. + * + * @author Alex Miller + */ +public class GetDownloadsCannedQuery extends AbstractCannedQueryPermissions +{ + private static final String QUERY_NAMESPACE = "alfresco.query.downloads"; + private static final String QUERY_SELECT_GET_DOWNLOADS = "select_GetDownloadsBeforeQuery"; + + private final CannedQueryDAO cannedQueryDAO; + + public GetDownloadsCannedQuery( + CannedQueryDAO cannedQueryDAO, + MethodSecurityBean methodSecurity, + CannedQueryParameters params) + { + super(params, methodSecurity); + this.cannedQueryDAO = cannedQueryDAO; + } + + @Override + protected List queryAndFilter(CannedQueryParameters parameters) + { + Object paramBeanObj = parameters.getParameterBean(); + if (paramBeanObj == null) + { + throw new NullPointerException("Null GetDownloadss query params"); + } + + GetDownloadsCannedQueryParams paramsBean = (GetDownloadsCannedQueryParams)paramBeanObj; + + // note: refer to SQL for specific DB filtering (eg.parent node and optionally blog integration aspect, etc) + List results = cannedQueryDAO.executeQuery(QUERY_NAMESPACE, QUERY_SELECT_GET_DOWNLOADS, paramBeanObj, 0, Integer.MAX_VALUE); + + List filteredResults = new ArrayList(); + for (DownloadEntity entity : results) + { + Date createdDate = DefaultTypeConverter.INSTANCE.convert(Date.class, entity.getCreatedDate()); + Date modifiedDate = DefaultTypeConverter.INSTANCE.convert(Date.class, entity.getModifiedDate()); + + if (modifiedDate == null) + { + modifiedDate = createdDate; + } + if (modifiedDate.before(paramsBean.getBefore())) + { + filteredResults.add(entity); + } + else + { + break; + } + } + return filteredResults; + } + + @Override + protected boolean isApplyPostQuerySorting() + { + // No post-query sorting. It's done within the queryAndFilter() method above. + return false; + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/repo/download/cannedquery/GetDownloadsCannedQueryFactory.java b/source/java/org/alfresco/repo/download/cannedquery/GetDownloadsCannedQueryFactory.java new file mode 100644 index 0000000000..0a1d504fc0 --- /dev/null +++ b/source/java/org/alfresco/repo/download/cannedquery/GetDownloadsCannedQueryFactory.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2005-2012 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * 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 . + */ +package org.alfresco.repo.download.cannedquery; + +import java.util.Date; + +import org.alfresco.model.ContentModel; +import org.alfresco.query.CannedQuery; +import org.alfresco.query.CannedQueryFactory; +import org.alfresco.query.CannedQueryParameters; +import org.alfresco.repo.download.DownloadModel; +import org.alfresco.repo.query.AbstractQNameAwareCannedQueryFactory; +import org.alfresco.service.cmr.download.DownloadService; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.util.ParameterCheck; + +/** + * A {@link CannedQueryFactory} for queries relating to {@link DownloadEntity download entities}. + * + * @author Alex Miller + * + * @see DownloadService#deleteDownloads(Date) + */ +public class GetDownloadsCannedQueryFactory extends AbstractQNameAwareCannedQueryFactory +{ + @Override + public void afterPropertiesSet() throws Exception + { + super.afterPropertiesSet(); + } + + public CannedQuery getDownloadsCannedQuery(NodeRef containerNode, Date before) + { + ParameterCheck.mandatory("before", before); + + GetDownloadsCannedQueryParams parameterBean = new GetDownloadsCannedQueryParams + ( + getNodeId(containerNode), + getQNameId(ContentModel.PROP_NAME), + getQNameId(DownloadModel.TYPE_DOWNLOAD), + before + ); + CannedQueryParameters params = new CannedQueryParameters(parameterBean); + + final GetDownloadsCannedQuery cq = new GetDownloadsCannedQuery( + cannedQueryDAO, methodSecurity, params + ); + + return cq; + } + + /* + * @see org.alfresco.query.CannedQueryFactory#getCannedQuery(org.alfresco.query.CannedQueryParameters) + */ + @Override + public CannedQuery getCannedQuery(CannedQueryParameters parameters) + { + return new GetDownloadsCannedQuery(cannedQueryDAO, methodSecurity, parameters); + } +} diff --git a/source/java/org/alfresco/repo/download/cannedquery/GetDownloadsCannedQueryParams.java b/source/java/org/alfresco/repo/download/cannedquery/GetDownloadsCannedQueryParams.java new file mode 100644 index 0000000000..560aa03d8a --- /dev/null +++ b/source/java/org/alfresco/repo/download/cannedquery/GetDownloadsCannedQueryParams.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2005-2012 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * 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 . + */ +package org.alfresco.repo.download.cannedquery; + +import java.util.Date; + +/** + * Query parameters for GetDownloadsCannedQuery + * + * @author Alex Miller + */ +public class GetDownloadsCannedQueryParams extends DownloadEntity +{ + private Date before; + + public GetDownloadsCannedQueryParams(Long parentNodeId, Long nameQNameId, Long contentTypeQNameId, Date before) + { + super(parentNodeId, nameQNameId, contentTypeQNameId); + this.before = before; + } + + public Date getBefore() + { + return before; + } +} diff --git a/source/java/org/alfresco/service/cmr/download/DownloadCreaterService.java b/source/java/org/alfresco/service/cmr/download/DownloadCreaterService.java new file mode 100644 index 0000000000..e69de29bb2 diff --git a/source/java/org/alfresco/service/cmr/download/DownloadRequest.java b/source/java/org/alfresco/service/cmr/download/DownloadRequest.java new file mode 100644 index 0000000000..7ea6be1a37 --- /dev/null +++ b/source/java/org/alfresco/service/cmr/download/DownloadRequest.java @@ -0,0 +1,57 @@ +/* + * Copyright 2005-2012 Alfresco Software, Ltd. All rights reserved. + * + * License rights for this program may be obtained from Alfresco Software, Ltd. + * pursuant to a written agreement and any use of this program without such an + * agreement is prohibited. + */ +package org.alfresco.service.cmr.download; + +import java.util.ArrayList; +import java.util.List; + +import org.alfresco.service.cmr.repository.AssociationRef; +import org.alfresco.service.cmr.repository.NodeRef; + +/** + * DownloadRequest data transfer object. + * + * @author Alex Miller + */ +public class DownloadRequest +{ + private String owner; + private boolean recursive; + private List requestedNodes; + + public DownloadRequest(boolean recursive, List requestedNodes, String owner) + { + this.owner = owner; + this.recursive = recursive; + this.requestedNodes = requestedNodes; + } + + public List getRequetedNodes() + { + return requestedNodes; + } + + public NodeRef[] getRequetedNodeRefs() + { + List requestedNodeRefs = new ArrayList(requestedNodes.size()); + for (AssociationRef requestedNode : requestedNodes) + { + requestedNodeRefs.add(requestedNode.getTargetRef()); + } + return requestedNodeRefs.toArray(new NodeRef[requestedNodeRefs.size()]); + } + + /** + * @return + */ + public String getOwner() + { + return owner; + } + +} diff --git a/source/java/org/alfresco/service/cmr/download/DownloadService.java b/source/java/org/alfresco/service/cmr/download/DownloadService.java new file mode 100644 index 0000000000..7b045726d3 --- /dev/null +++ b/source/java/org/alfresco/service/cmr/download/DownloadService.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2005-2011 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * 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 . + */ +package org.alfresco.service.cmr.download; + +import java.util.Date; + +import org.alfresco.service.cmr.repository.NodeRef; + +/** + * Zip download service. + * + * Implementations are responsible for triggering the Zip creation process and + * reporting on the status of the of this process. + * + * @author Alex Miller + */ +public interface DownloadService +{ + /** + * Start the creation of a downlaodable archive file containing the content + * from the given nodeRefs. + * + * Implementations are expected to do this asynchronously, with clients + * using the returned NodeRef to check on progress. + + * Initially, only zip files will be supported, however this could be + * extended in the future, to support additional archive types. + * + * @param nodeRefs NodeRefs of content to be added to the archive file + * @param recusirsive Recurse into container nodes + * @return Reference to node which will eventually contain the archive file + */ + public NodeRef createDownload(NodeRef[] nodeRefs, boolean recusirsive); + + /** + * Get the status of the of the download identified by downloadNode. + */ + public DownloadStatus getDownloadStatus(NodeRef downloadNode); + + /** + * Delete downloads created before before. + * + * @param before + */ + public void deleteDownloads(Date before); + + /** + * Cancel a download request + * + * @param downloadNodeRef NodeRef of the download to cancel + */ + public void cancelDownload(NodeRef downloadNodeRef); +} diff --git a/source/java/org/alfresco/service/cmr/download/DownloadStatus.java b/source/java/org/alfresco/service/cmr/download/DownloadStatus.java new file mode 100644 index 0000000000..3ad2e32004 --- /dev/null +++ b/source/java/org/alfresco/service/cmr/download/DownloadStatus.java @@ -0,0 +1,126 @@ +/* + * Copyright (C) 2005-2012 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * 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 . + */ +package org.alfresco.service.cmr.download; + +import java.io.Serializable; + +/** + * Immutable data transfer object representing the status of a download. + * + * Provides the current status and an indication of the progress. Prgress is + * measured by comparing done against total. Total gives an indication of the + * total work, while done indicates how much has been completed. + * + * @author amiller + */ +public class DownloadStatus implements Serializable +{ + private static final long serialVersionUID = 4513872550314507598L; + + public enum Status { + PENDING, + IN_PROGRESS, + DONE, + MAX_CONTENT_SIZE_EXCEEDED, + CANCELLED + } + + private long done; + private long total; + + private long filesAddedCount; + private long totalFileCount; + + private Status status; + + /** + * Default constructor + * @param status Current status of the download + * @param done Done count + * @param total Total to be de done + * @param filesAddedCount Number of files added to the archive + * @param totalFiles The number of files that will eventually be added to the archive + */ + public DownloadStatus(Status status, long done, long total, long filesAdded, long totalFiles) + { + this.status = status; + this.done = done; + this.total = total; + this.filesAddedCount = filesAdded; + this.totalFileCount = totalFiles; + } + + /** + * @return The percentage complete, calculated by multiplying done by 100 and dividing by total. + */ + public long getPercentageComplete() + { + return (done * 100) / total; + } + + /** + * @return true if status is DONE, false otherwise. + */ + public boolean isComplete() + { + return status == Status.DONE; + } + + /** + * @return the current status + */ + public Status getStatus() + { + return status; + } + + /** + * @return the current done count + */ + public long getDone() + { + return done; + } + + /** + * @return the total, to be done. + */ + public long getTotal() + { + return total; + } + + /** + * @return the total number of files in the download archive + */ + public long getTotalFiles() + { + return totalFileCount; + } + + /** + * @return the number of files added to the download archive + * @return + */ + public long getFilesAdded() + { + return filesAddedCount; + } + +} diff --git a/source/java/org/alfresco/service/cmr/download/DownloadStatusUpdateService.java b/source/java/org/alfresco/service/cmr/download/DownloadStatusUpdateService.java new file mode 100644 index 0000000000..37946aeff3 --- /dev/null +++ b/source/java/org/alfresco/service/cmr/download/DownloadStatusUpdateService.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2005-2011 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * 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 . + */ +package org.alfresco.service.cmr.download; + +import org.alfresco.service.cmr.repository.NodeRef; + +/** + * Service for updating the status of a download. + * + * @author Alex Miller + */ +public interface DownloadStatusUpdateService +{ + + void update(NodeRef nodeRef, DownloadStatus status, int sequenceNumber); +} diff --git a/source/java/org/alfresco/service/cmr/download/package-info.java b/source/java/org/alfresco/service/cmr/download/package-info.java new file mode 100644 index 0000000000..1749fed293 --- /dev/null +++ b/source/java/org/alfresco/service/cmr/download/package-info.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2005-2011 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * 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 . + */ +/** + * Defines the contracts for creating archive files containing specified + * content from the repository. + * + * The DownlaodService is a client (Share) facing service responsible for + * creating a node containing enough information for the download archive to + * be created, and reporting on the progress of the creation process. + * + * @author Alex Miller + */ + +package org.alfresco.service.cmr.download; \ No newline at end of file