/* * 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.node.archive; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; import org.alfresco.error.AlfrescoRuntimeException; import org.alfresco.model.ContentModel; import org.alfresco.repo.batch.BatchProcessWorkProvider; import org.alfresco.repo.batch.BatchProcessor; import org.alfresco.repo.batch.BatchProcessor.BatchProcessWorker; import org.alfresco.repo.lock.JobLockService; import org.alfresco.repo.lock.LockAcquisitionException; import org.alfresco.repo.node.archive.RestoreNodeReport.RestoreStatus; import org.alfresco.repo.security.authentication.AuthenticationUtil; import org.alfresco.repo.security.permissions.AccessDeniedException; import org.alfresco.repo.transaction.RetryingTransactionHelper; import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; import org.alfresco.service.cmr.repository.ChildAssociationRef; import org.alfresco.service.cmr.repository.InvalidNodeRefException; import org.alfresco.service.cmr.repository.NodeRef; import org.alfresco.service.cmr.repository.NodeService; import org.alfresco.service.cmr.repository.StoreRef; import org.alfresco.service.cmr.search.ResultSet; import org.alfresco.service.cmr.search.ResultSetRow; import org.alfresco.service.cmr.search.SearchParameters; import org.alfresco.service.cmr.search.SearchService; import org.alfresco.service.namespace.NamespaceService; import org.alfresco.service.namespace.QName; import org.alfresco.service.transaction.TransactionService; import org.alfresco.util.EqualsHelper; import org.alfresco.util.VmShutdownListener; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; /** * Implementation of the node archive abstraction. * * @author Derek Hulley */ public class NodeArchiveServiceImpl implements NodeArchiveService { private static final QName LOCK_QNAME = QName.createQName(NamespaceService.ALFRESCO_URI, "NodeArchive"); private static final long LOCK_TTL = 60000; private static final String MSG_BUSY = "node.archive.msg.busy"; private static Log logger = LogFactory.getLog(NodeArchiveServiceImpl.class); private NodeService nodeService; private SearchService searchService; private TransactionService transactionService; private JobLockService jobLockService; public void setNodeService(NodeService nodeService) { this.nodeService = nodeService; } public void setTransactionService(TransactionService transactionService) { this.transactionService = transactionService; } public void setSearchService(SearchService searchService) { this.searchService = searchService; } public NodeRef getStoreArchiveNode(StoreRef originalStoreRef) { return nodeService.getStoreArchiveNode(originalStoreRef); } public void setJobLockService(JobLockService jobLockService) { this.jobLockService = jobLockService; } public NodeRef getArchivedNode(NodeRef originalNodeRef) { StoreRef orginalStoreRef = originalNodeRef.getStoreRef(); NodeRef archiveRootNodeRef = nodeService.getStoreArchiveNode(orginalStoreRef); // create the likely location of the archived node NodeRef archivedNodeRef = new NodeRef( archiveRootNodeRef.getStoreRef(), originalNodeRef.getId()); return archivedNodeRef; } /** * Get all the nodes that were archived from the given store. * * @param originalStoreRef the original store to process * @param skipCount the number of results to skip (used for paging) * @param limit the number of items to retrieve or -1 to get the all * * @deprecated To be replaced with a limiting search against the database */ private ResultSet getArchivedNodes(StoreRef originalStoreRef, int skipCount, int limit) { // Get the archive location NodeRef archiveParentNodeRef = nodeService.getStoreArchiveNode(originalStoreRef); StoreRef archiveStoreRef = archiveParentNodeRef.getStoreRef(); // build the query String query = String.format("PARENT:\"%s\" AND ASPECT:\"%s\"", archiveParentNodeRef, ContentModel.ASPECT_ARCHIVED); // search parameters SearchParameters params = new SearchParameters(); params.addStore(archiveStoreRef); params.setLanguage(SearchService.LANGUAGE_LUCENE); params.setQuery(query); params.setSkipCount(skipCount); params.setMaxItems(limit); // get all archived children using a search ResultSet rs = searchService.query(params); // done return rs; } /** * @return Returns a work provider for batch processing * * @since 3.3.4 */ private BatchProcessWorkProvider getArchivedNodesWorkProvider(final StoreRef originalStoreRef, final String lockToken) { return new BatchProcessWorkProvider() { private VmShutdownListener vmShutdownLister = new VmShutdownListener("getArchivedNodesWorkProvider"); private Integer workSize; private int skipResults = 0; public synchronized int getTotalEstimatedWorkSize() { if (workSize == null) { workSize = Integer.valueOf(0); ResultSet rs = null; try { rs = getArchivedNodes(originalStoreRef, 0, -1); workSize = rs.length(); } catch (Throwable e) { logger.error("Failed to get archive size", e); } finally { if (rs != null) { rs.close(); } } } return workSize; } public synchronized Collection getNextWork() { if (vmShutdownLister.isVmShuttingDown()) { return Collections.emptyList(); } // Make sure we still have the lock try { // TODO: Replace with joblock callback mechanism that provides shutdown hints jobLockService.refreshLock(lockToken, LOCK_QNAME, LOCK_TTL); } catch (LockAcquisitionException e) { // This is OK. We don't have the lock so just quit return Collections.emptyList(); } Collection results = new ArrayList(100); ResultSet rs = null; try { rs = getArchivedNodes(originalStoreRef, skipResults, 100); for (ResultSetRow row : rs) { results.add(row.getNodeRef()); } skipResults += results.size(); } finally { if (rs != null) { rs.close(); } } return results; } }; } /** * This is the primary restore method that all restore methods fall back on. * It executes the restore for the node in a separate transaction and attempts to catch * the known conditions that can be reported back to the client. */ public RestoreNodeReport restoreArchivedNode( final NodeRef archivedNodeRef, final NodeRef destinationNodeRef, final QName assocTypeQName, final QName assocQName) { RestoreNodeReport report = new RestoreNodeReport(archivedNodeRef); report.setTargetParentNodeRef(destinationNodeRef); try { // Transactional wrapper to attempt the restore RetryingTransactionHelper txnHelper = transactionService.getRetryingTransactionHelper(); RetryingTransactionCallback restoreCallback = new RetryingTransactionCallback() { public NodeRef execute() throws Exception { return nodeService.restoreNode(archivedNodeRef, destinationNodeRef, assocTypeQName, assocQName); } }; NodeRef newNodeRef = txnHelper.doInTransaction(restoreCallback, false, true); // success report.setRestoredNodeRef(newNodeRef); report.setStatus(RestoreStatus.SUCCESS); } catch (InvalidNodeRefException e) { report.setCause(e); NodeRef invalidNodeRef = e.getNodeRef(); if (archivedNodeRef.equals(invalidNodeRef)) { // not too serious, but the node to archive is missing report.setStatus(RestoreStatus.FAILURE_INVALID_ARCHIVE_NODE); } else if (EqualsHelper.nullSafeEquals(destinationNodeRef, invalidNodeRef)) { report.setStatus(RestoreStatus.FAILURE_INVALID_PARENT); } else if (destinationNodeRef == null) { // get the original parent of the archived node ChildAssociationRef originalParentAssocRef = (ChildAssociationRef) nodeService.getProperty( archivedNodeRef, ContentModel.PROP_ARCHIVED_ORIGINAL_PARENT_ASSOC); NodeRef originalParentNodeRef = originalParentAssocRef.getParentRef(); if (EqualsHelper.nullSafeEquals(originalParentNodeRef, invalidNodeRef)) { report.setStatus(RestoreStatus.FAILURE_INVALID_PARENT); } else { // some other invalid node was detected report.setStatus(RestoreStatus.FAILURE_OTHER); } } else { // some other invalid node was detected report.setStatus(RestoreStatus.FAILURE_OTHER); } } catch (AccessDeniedException e) { report.setCause(e); report.setStatus(RestoreStatus.FAILURE_PERMISSION); } catch (Throwable e) { report.setCause(e); report.setStatus(RestoreStatus.FAILURE_OTHER); logger.error("An unhandled exception stopped the restore", e); } // done if (logger.isDebugEnabled()) { logger.debug("Attempted node restore: "+ report); } return report; } /** * @see #restoreArchivedNode(NodeRef, NodeRef, QName, QName) */ public RestoreNodeReport restoreArchivedNode(NodeRef archivedNodeRef) { return restoreArchivedNode(archivedNodeRef, null, null, null); } /** * @see #restoreArchivedNodes(List, NodeRef, QName, QName) */ public List restoreArchivedNodes(List archivedNodeRefs) { return restoreArchivedNodes(archivedNodeRefs, null, null, null); } /** * @see #restoreArchivedNode(NodeRef, NodeRef, QName, QName) */ public List restoreArchivedNodes( List archivedNodeRefs, NodeRef destinationNodeRef, QName assocTypeQName, QName assocQName) { List results = new ArrayList(archivedNodeRefs.size()); for (NodeRef nodeRef : archivedNodeRefs) { RestoreNodeReport result = restoreArchivedNode(nodeRef, destinationNodeRef, assocTypeQName, assocQName); results.add(result); } return results; } /** * Uses batch processing and job locking to purge all archived nodes */ public List restoreAllArchivedNodes(StoreRef originalStoreRef) { final String user = AuthenticationUtil.getFullyAuthenticatedUser(); if (user == null) { throw new IllegalStateException("Cannot restore as there is no authenticated user."); } final List results = Collections.synchronizedList(new ArrayList(1000)); /** * Worker that purges each node */ BatchProcessWorker worker = new BatchProcessor.BatchProcessWorkerAdaptor() { public void process(NodeRef entry) throws Throwable { AuthenticationUtil.pushAuthentication(); try { AuthenticationUtil.setFullyAuthenticatedUser(user); if (nodeService.exists(entry)) { RestoreNodeReport report = restoreArchivedNode(entry); // Append the results (it is synchronized) results.add(report); } } finally { AuthenticationUtil.popAuthentication(); } } }; doBulkOperation(user, originalStoreRef, worker); return results; } /** * Finds the archive location for nodes that were deleted from the given store * and attempt to restore each node. * * @see NodeService#getStoreArchiveNode(StoreRef) * @see #restoreArchivedNode(NodeRef, NodeRef, QName, QName) */ public List restoreAllArchivedNodes( StoreRef originalStoreRef, NodeRef destinationNodeRef, QName assocTypeQName, QName assocQName) { // get all archived children using a search ResultSet rs = getArchivedNodes(originalStoreRef, 0, -1); try { // loop through the resultset and attempt to restore all the nodes List results = new ArrayList(1000); for (ResultSetRow row : rs) { NodeRef archivedNodeRef = row.getNodeRef(); RestoreNodeReport result = restoreArchivedNode(archivedNodeRef, destinationNodeRef, assocTypeQName, assocQName); results.add(result); } // done if (logger.isDebugEnabled()) { logger.debug("Restored " + results.size() + " nodes into store " + originalStoreRef); } return results; } finally { rs.close(); } } /** * This is the primary purge methd that all purge methods fall back on. It isolates the delete * work in a new transaction. */ public void purgeArchivedNode(final NodeRef archivedNodeRef) { RetryingTransactionHelper txnHelper = transactionService.getRetryingTransactionHelper(); RetryingTransactionCallback deleteCallback = new RetryingTransactionCallback() { public Object execute() throws Exception { try { nodeService.deleteNode(archivedNodeRef); } catch (InvalidNodeRefException e) { // ignore } return null; } }; txnHelper.doInTransaction(deleteCallback, false, true); } /** * @see #purgeArchivedNode(NodeRef) */ public void purgeArchivedNodes(List archivedNodes) { for (NodeRef archivedNodeRef : archivedNodes) { purgeArchivedNode(archivedNodeRef); } // done } /** * Uses batch processing and job locking to purge all archived nodes */ public void purgeAllArchivedNodes(StoreRef originalStoreRef) { final String user = AuthenticationUtil.getFullyAuthenticatedUser(); if (user == null) { throw new IllegalStateException("Cannot purge as there is no authenticated user."); } /** * Worker that purges each node */ BatchProcessWorker worker = new BatchProcessor.BatchProcessWorkerAdaptor() { public void process(NodeRef entry) throws Throwable { AuthenticationUtil.pushAuthentication(); try { AuthenticationUtil.setFullyAuthenticatedUser(user); if (nodeService.exists(entry)) { nodeService.deleteNode(entry); } } finally { AuthenticationUtil.popAuthentication(); } } }; doBulkOperation(user, originalStoreRef, worker); } /** * Do batch-controlled work */ private void doBulkOperation(final String user, StoreRef originalStoreRef, BatchProcessWorker worker) { String lockToken = null; try { // Get a lock to keep refreshing lockToken = jobLockService.getLock(LOCK_QNAME, LOCK_TTL); // TODO: Should merely trigger a background job i.e. perhaps it should not be // triggered by a user-based thread BatchProcessor batchProcessor = new BatchProcessor( "ArchiveBulkPurgeOrRestore", transactionService.getRetryingTransactionHelper(), getArchivedNodesWorkProvider(originalStoreRef, lockToken), 2, 20, null, null, 1000); batchProcessor.process(worker, true); } catch (LockAcquisitionException e) { throw new AlfrescoRuntimeException(MSG_BUSY); } finally { try { if (lockToken != null ) {jobLockService.releaseLock(lockToken, LOCK_QNAME); } } catch (LockAcquisitionException e) { // Ignore } } } }