diff --git a/config/alfresco/messages/webclient.properties b/config/alfresco/messages/webclient.properties index 3dc181ee0f..da271c7044 100644 --- a/config/alfresco/messages/webclient.properties +++ b/config/alfresco/messages/webclient.properties @@ -778,6 +778,7 @@ original_location=Original Location deleted_date=Date Deleted deleted_user=Deleted by User recover=Recover +clear_search_results=Clear Search Results search_deleted_items=Search Deleted Items delete_item=Delete Item delete_item_info=Permanently delete an item from the deleted file store @@ -797,6 +798,12 @@ delete_listed_items_confirm=Are you sure you want to permanently delete the list recover_listed_items=Recover Listed Items recover_listed_items_info=Recover the listed files and spaces from the deleted file store recover_listed_items_confirm=Are you sure you want to recover the listed deleted files and spaces from the deleted file store? +recovered_item_success=The item \"{0}\" has been successfully recovered. +recovered_item_parent=Failed to recover the item \"{0}\" as the original parent folder is missing, please select a new folder destination. +recovered_item_permission=Failed to recover the item \"{0}\" as you do not have the appropriate permissions to restore the item to the original parent folder, please select a new folder destination. +recovered_item_integrity=Failed to recover the item \"{0}\" as there is now an item in the original parent folder with the same name, please select a new folder destination. +recovered_item_failure=Failed to recover the item \"{0}\" due to error: {1} +delete_item_success=The item \"{0}\" has been permanently deleted. # Admin Console messages title_admin_console=Administration Console diff --git a/config/alfresco/web-client-config-actions.xml b/config/alfresco/web-client-config-actions.xml index cb3ac79ca3..4817bbb163 100644 --- a/config/alfresco/web-client-config-actions.xml +++ b/config/alfresco/web-client-config-actions.xml @@ -378,6 +378,7 @@ manage_deleted_items /images/icons/trashcan.gif + #{TrashcanBean.setupTrashcan} dialog:manageDeletedItems diff --git a/source/java/org/alfresco/web/bean/TrashcanBean.java b/source/java/org/alfresco/web/bean/TrashcanBean.java index 3e4882b9e2..cd212494f4 100644 --- a/source/java/org/alfresco/web/bean/TrashcanBean.java +++ b/source/java/org/alfresco/web/bean/TrashcanBean.java @@ -16,13 +16,41 @@ */ package org.alfresco.web.bean; +import java.text.MessageFormat; +import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Map; +import javax.faces.application.FacesMessage; +import javax.faces.context.FacesContext; +import javax.faces.event.ActionEvent; +import javax.transaction.UserTransaction; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.node.archive.NodeArchiveService; +import org.alfresco.repo.node.archive.RestoreNodeReport; +import org.alfresco.repo.node.archive.RestoreNodeReport.RestoreStatus; +import org.alfresco.repo.search.impl.lucene.QueryParser; +import org.alfresco.service.cmr.dictionary.DictionaryService; +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.Path; +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.QName; +import org.alfresco.web.app.Application; import org.alfresco.web.app.context.IContextListener; +import org.alfresco.web.bean.repository.MapNode; import org.alfresco.web.bean.repository.Node; +import org.alfresco.web.bean.repository.NodePropertyResolver; +import org.alfresco.web.bean.repository.QNameNodeMap; +import org.alfresco.web.bean.repository.Repository; +import org.alfresco.web.ui.common.Utils; +import org.alfresco.web.ui.common.component.UIActionLink; import org.alfresco.web.ui.common.component.data.UIRichList; /** @@ -30,17 +58,52 @@ import org.alfresco.web.ui.common.component.data.UIRichList; */ public class TrashcanBean implements IContextListener { + private static final String MSG_RECOVERED_ITEM_INTEGRITY = "recovered_item_integrity"; + private static final String MSG_RECOVERED_ITEM_PERMISSION = "recovered_item_permission"; + private static final String MSG_RECOVERED_ITEM_PARENT = "recovered_item_parent"; + private static final String MSG_RECOVERED_ITEM_FAILURE = "recovered_item_failure"; + private static final String MSG_RECOVERED_ITEM_SUCCESS = "recovered_item_success"; + + private static final String OUTCOME_DIALOGCLOSE = "dialog:close"; + + private static final String RICHLIST_ID = "trashcan-list"; + private static final String RICHLIST_MSG_ID = "trashcan" + ':' + RICHLIST_ID; + + private final static String NAME_ATTR = Repository.escapeQName(ContentModel.PROP_NAME); + + private final static String SEARCH_ALL = "PARENT:\"%s\" AND ASPECT:\"%s\""; + private final static String SEARCH_NAME = "PARENT:\"%s\" AND ASPECT:\"%s\" AND (@" + NAME_ATTR + ":*%s* TEXT:%s)"; + private final static String SEARCH_NAME_QUOTED = "PARENT:\"%s\" AND ASPECT:\"%s\" AND (@" + NAME_ATTR + ":\"%s\" TEXT:\"%s\")"; + /** NodeService bean reference */ protected NodeService nodeService; + + /** NodeArchiveService bean reference */ + protected NodeArchiveService nodeArchiveService; /** SearchService bean reference */ protected SearchService searchService; + /** The DictionaryService bean reference */ + protected DictionaryService dictionaryService; + /** Component reference for Deleted Items RichList control */ protected UIRichList itemsRichList; /** Search text */ - private String searchText; + private String searchText = null; + + /** We show an empty list until a Search or Show All is executed */ + private boolean showItems = false; + + /** Currently listed items */ + private List listedItems = Collections.emptyList(); + + /** Current action context Node */ + private Node actionNode; + + /** Root node to the spaces store archive store*/ + private NodeRef archiveRootRef = null; // ------------------------------------------------------------------------------ @@ -55,13 +118,29 @@ public class TrashcanBean implements IContextListener } /** - * @param searchService the search service + * @param searchService The search service */ public void setSearchService(SearchService searchService) { this.searchService = searchService; } + /** + * @param nodeArchiveService The nodeArchiveService to set. + */ + public void setNodeArchiveService(NodeArchiveService nodeArchiveService) + { + this.nodeArchiveService = nodeArchiveService; + } + + /** + * @param dictionaryService The DictionaryService to set. + */ + public void setDictionaryService(DictionaryService dictionaryService) + { + this.dictionaryService = dictionaryService; + } + /** * @return Returns the itemsRichList. */ @@ -94,31 +173,382 @@ public class TrashcanBean implements IContextListener this.searchText = searchText; } + /** + * @return Returns the listed items. + */ + public List getListedItems() + { + return this.listedItems; + } + + /** + * @param listedItems The listed items to set. + */ + public void setListedItems(List listedItems) + { + this.listedItems = listedItems; + } + + /** + * @param node The item context for the current action + */ + public void setItem(Node node) + { + this.actionNode = node; + } + + /** + * @return the item context for the current action + */ + public Node getItem() + { + return this.actionNode; + } + /** * @return the list of deleted items to display */ public List getItems() { - // TODO: need the following MapNode properties: - // deletedDate, locationPath, displayPath, deletedUsername [only for admin user] - // TODO: get deleted items from deleted items store - // use a search - also use filters by name/username - return Collections.emptyList(); + // to get deleted items from deleted items store + // use a search to find the items - also filters by name/username + List itemNodes = null; + + UserTransaction tx = null; + ResultSet results = null; + try + { + tx = Repository.getUserTransaction(FacesContext.getCurrentInstance(), true); + tx.begin(); + + // get the root node to the deleted items store + if (getArchiveRootRef() != null && this.showItems == true) + { + String query = getSearchQuery(); + SearchParameters sp = new SearchParameters(); + sp.setLanguage(SearchService.LANGUAGE_LUCENE); + sp.setQuery(query); + sp.addStore(getArchiveRootRef().getStoreRef()); // the Archived Node store + + results = this.searchService.query(sp); + itemNodes = new ArrayList(results.length()); + } + + if (results != null && results.length() != 0) + { + for (ResultSetRow row : results) + { + NodeRef nodeRef = row.getNodeRef(); + + if (this.nodeService.exists(nodeRef)) + { + QName type = this.nodeService.getType(nodeRef); + + if (this.dictionaryService.isSubClass(type, ContentModel.TYPE_FOLDER) == true && + this.dictionaryService.isSubClass(type, ContentModel.TYPE_SYSTEM_FOLDER) == false) + { + MapNode node = new MapNode(nodeRef, this.nodeService, false); + node.addPropertyResolver("locationPath", resolverLocationPath); + node.addPropertyResolver("displayPath", resolverDisplayPath); + node.addPropertyResolver("deletedDate", resolverDeletedDate); + node.addPropertyResolver("deletedBy", resolverDeletedBy); + node.addPropertyResolver("typeIcon", this.resolverSmallIcon); + itemNodes.add(node); + } + else + { + MapNode node = new MapNode(nodeRef, this.nodeService, false); + node.addPropertyResolver("locationPath", resolverLocationPath); + node.addPropertyResolver("displayPath", resolverDisplayPath); + node.addPropertyResolver("deletedDate", resolverDeletedDate); + node.addPropertyResolver("deletedBy", resolverDeletedBy); + node.addPropertyResolver("typeIcon", this.resolverFileType16); + itemNodes.add(node); + } + } + } + } + + tx.commit(); + } + catch (Throwable err) + { + Utils.addErrorMessage(MessageFormat.format(Application.getMessage( + FacesContext.getCurrentInstance(), Repository.ERROR_GENERIC), new Object[] {err.getMessage()}), err ); + try { if (tx != null) {tx.rollback();} } catch (Exception tex) {} + } + finally + { + if (results != null) + { + results.close(); + } + } + + this.listedItems = (itemNodes != null ? itemNodes : Collections.emptyList()); + + return this.listedItems; } + private NodePropertyResolver resolverLocationPath = new NodePropertyResolver() { + public Object get(Node node) { + //ChildAssociationRef childRef = (ChildAssociationRef)node.getProperties().get(ContentModel.PROP_ARCHIVED_ORIGINAL_PARENT_ASSOC); + //return nodeService.getPath(childRef.getChildRef()); + return (Path)node.getProperties().get(ContentModel.PROP_ARCHIVED_ORIGINAL_PATH); + } + }; + + private NodePropertyResolver resolverDisplayPath = new NodePropertyResolver() { + public Object get(Node node) { + //ChildAssociationRef childRef = (ChildAssociationRef)node.getProperties().get(ContentModel.PROP_ARCHIVED_ORIGINAL_PARENT_ASSOC); + return Repository.getDisplayPath((Path)node.getProperties().get(ContentModel.PROP_ARCHIVED_ORIGINAL_PATH)); + } + }; + + private NodePropertyResolver resolverFileType16 = new NodePropertyResolver() { + public Object get(Node node) { + return Utils.getFileTypeImage(node.getName(), true); + } + }; + + private NodePropertyResolver resolverSmallIcon = new NodePropertyResolver() { + public Object get(Node node) { + QNameNodeMap props = (QNameNodeMap)node.getProperties(); + String icon = (String)props.getRaw("app:icon"); + return "/images/icons/" + (icon != null ? icon + "-16.gif" : BrowseBean.SPACE_SMALL_DEFAULT + ".gif"); + } + }; + + private NodePropertyResolver resolverDeletedDate = new NodePropertyResolver() { + public Object get(Node node) { + return node.getProperties().get(ContentModel.PROP_ARCHIVED_DATE); + } + }; + + private NodePropertyResolver resolverDeletedBy = new NodePropertyResolver() { + public Object get(Node node) { + return node.getProperties().get(ContentModel.PROP_ARCHIVED_BY); + } + }; + // ------------------------------------------------------------------------------ // Action handlers // TODO: - // need the following navigation outcomes - // DONE deleteItem, recoverItem, recoverAllItems, deleteAllItems, recoverListedItems, deleteListedItems // need the following Action Handlers: // deleteItemOK, recoverItemOK, deleteAllItemsOK, recoverAllItemsOK, recoverListedItemsOK, deleteListedItemsOK - // and following Action Event Handlers: - // setupItemAction, search - // and following getters: - // listedItems, item (setup by setupItemAction!) + + /** + * Search the deleted item store by name + */ + public void search(ActionEvent event) + { + // simply clear the current list and refresh the screen + // the search query text will be found and processed by the getItems() method + contextUpdated(); + this.showItems = true; + } + + /** + * Action handler to clear the current search results and show all items + */ + public void clearSearch(ActionEvent event) + { + contextUpdated(); + this.searchText = null; + this.showItems = true; + } + + /** + * Action handler called to prepare the selected item for an action + */ + public void setupItemAction(ActionEvent event) + { + UIActionLink link = (UIActionLink)event.getComponent(); + Map params = link.getParameterMap(); + String id = params.get("id"); + if (id != null && id.length() != 0) + { + try + { + // create the node ref, then our node representation + NodeRef ref = new NodeRef(getArchiveRootRef().getStoreRef(), id); + Node node = new Node(ref); + + // resolve icon in-case one has not been set + //node.addPropertyResolver("icon", this.resolverSpaceIcon); + + // prepare a node for the action context + setItem(node); + } + catch (InvalidNodeRefException refErr) + { + Utils.addErrorMessage(MessageFormat.format(Application.getMessage( + FacesContext.getCurrentInstance(), Repository.ERROR_NODEREF), new Object[] {id}) ); + } + } + else + { + setItem(null); + } + + // clear the UI state in preparation for finishing the next action + contextUpdated(); + } + + public String deleteItemOK() + { + Node item = getItem(); + if (item != null) + { + try + { + this.nodeArchiveService.purgeArchivedNode(item.getNodeRef()); + + FacesContext fc = FacesContext.getCurrentInstance(); + String msg = MessageFormat.format( + Application.getMessage(fc, "delete_item_success"), item.getName()); + FacesMessage facesMsg = new FacesMessage(FacesMessage.SEVERITY_INFO, msg, msg); + fc.addMessage(RICHLIST_MSG_ID, facesMsg); + } + catch (Throwable err) + { + Utils.addErrorMessage(MessageFormat.format(Application.getMessage( + FacesContext.getCurrentInstance(), Repository.ERROR_GENERIC), err.getMessage()), err); + } + } + return OUTCOME_DIALOGCLOSE; + } + + public String recoverItemOK() + { + String outcome = null; + + Node item = getItem(); + if (item != null) + { + FacesContext fc = FacesContext.getCurrentInstance(); + try + { + String msg; + FacesMessage errorfacesMsg = null; + + RestoreNodeReport report = this.nodeArchiveService.restoreArchivedNode(item.getNodeRef()); + switch (report.getStatus()) + { + case SUCCESS: + msg = MessageFormat.format( + Application.getMessage(fc, MSG_RECOVERED_ITEM_SUCCESS), item.getName()); + FacesMessage facesMsg = new FacesMessage(FacesMessage.SEVERITY_INFO, msg, msg); + fc.addMessage(RICHLIST_MSG_ID, facesMsg); + outcome = OUTCOME_DIALOGCLOSE; + break; + + case FAILURE_INVALID_PARENT: + msg = MessageFormat.format( + Application.getMessage(fc, MSG_RECOVERED_ITEM_PARENT), item.getName()); + errorfacesMsg = new FacesMessage(FacesMessage.SEVERITY_ERROR, msg, msg); + break; + + case FAILURE_PERMISSION: + msg = MessageFormat.format( + Application.getMessage(fc, MSG_RECOVERED_ITEM_PERMISSION), item.getName()); + errorfacesMsg = new FacesMessage(FacesMessage.SEVERITY_ERROR, msg, msg); + break; + + case FAILURE_INTEGRITY: + msg = MessageFormat.format( + Application.getMessage(fc, MSG_RECOVERED_ITEM_INTEGRITY), item.getName()); + errorfacesMsg = new FacesMessage(FacesMessage.SEVERITY_ERROR, msg, msg); + break; + + default: + String reason = report.getCause().getMessage(); + msg = MessageFormat.format( + Application.getMessage(fc, MSG_RECOVERED_ITEM_FAILURE), item.getName(), reason); + errorfacesMsg = new FacesMessage(FacesMessage.SEVERITY_ERROR, msg, msg); + break; + } + + // report the failure if one occured we stay on the current screen + if (errorfacesMsg != null) + { + fc.addMessage(null, errorfacesMsg); + } + } + catch (Throwable err) + { + // most exceptions will be caught and returned as RestoreNodeReport objects by the service + String reason = err.getMessage(); + String msg = MessageFormat.format( + Application.getMessage(fc, MSG_RECOVERED_ITEM_FAILURE), item.getName(), reason); + FacesMessage facesMsg = new FacesMessage(FacesMessage.SEVERITY_ERROR, msg, msg); + fc.addMessage(null, facesMsg); + } + } + + return outcome; + } + + /** + * Action handler to reset all filters and search + */ + public void resetAll(ActionEvent event) + { + // TODO: reset all filter and search + } + + /** + * Action handler to initially setup the trashcan screen + */ + public void setupTrashcan(ActionEvent event) + { + contextUpdated(); + } + + + // ------------------------------------------------------------------------------ + // Private helpers + + /** + * @return the archive store root node ref + */ + private NodeRef getArchiveRootRef() + { + if (this.archiveRootRef == null) + { + this.archiveRootRef = this.nodeArchiveService.getStoreArchiveNode(Repository.getStoreRef()); + } + return this.archiveRootRef; + } + + /** + * @return the search query to use when displaying the list of deleted items + */ + private String getSearchQuery() + { + String query; + if (this.searchText == null || this.searchText.length() == 0) + { + // search for ALL items in the archive store + query = String.format(SEARCH_ALL, archiveRootRef, ContentModel.ASPECT_ARCHIVED); + } + else + { + // search by name in the archive store + String safeText = QueryParser.escape(this.searchText); + if (safeText.indexOf(' ') == -1) + { + query = String.format(SEARCH_NAME, archiveRootRef, ContentModel.ASPECT_ARCHIVED, safeText, safeText); + } + else + { + query = String.format(SEARCH_NAME_QUOTED, archiveRootRef, ContentModel.ASPECT_ARCHIVED, safeText, safeText); + } + } + return query; + } // ------------------------------------------------------------------------------ @@ -133,5 +563,6 @@ public class TrashcanBean implements IContextListener { this.itemsRichList.setValue(null); } + this.showItems = false; } } diff --git a/source/java/org/alfresco/web/ui/repo/renderer/NodePathLinkRenderer.java b/source/java/org/alfresco/web/ui/repo/renderer/NodePathLinkRenderer.java index 4736f9bd22..75c5d131a6 100644 --- a/source/java/org/alfresco/web/ui/repo/renderer/NodePathLinkRenderer.java +++ b/source/java/org/alfresco/web/ui/repo/renderer/NodePathLinkRenderer.java @@ -244,7 +244,7 @@ public class NodePathLinkRenderer extends BaseRenderer } } - if (disabled == false) + if (disabled == false && lastElementRef != null) { return renderPathElement(context, component, lastElementRef, buf.toString()); } diff --git a/source/web/WEB-INF/faces-config-beans.xml b/source/web/WEB-INF/faces-config-beans.xml index 3812693aba..fd1d2f52a9 100644 --- a/source/web/WEB-INF/faces-config-beans.xml +++ b/source/web/WEB-INF/faces-config-beans.xml @@ -1483,10 +1483,18 @@ nodeService #{NodeService} + + nodeArchiveService + #{nodeArchiveService} + searchService #{SearchService} + + dictionaryService + #{DictionaryService} + diff --git a/source/web/images/icons/recover.gif b/source/web/images/icons/recover.gif index c49ece5db0..2cc19c2f10 100644 Binary files a/source/web/images/icons/recover.gif and b/source/web/images/icons/recover.gif differ diff --git a/source/web/images/icons/recover_all.gif b/source/web/images/icons/recover_all.gif index c49ece5db0..29c35e8a24 100644 Binary files a/source/web/images/icons/recover_all.gif and b/source/web/images/icons/recover_all.gif differ diff --git a/source/web/images/icons/recover_all_large.gif b/source/web/images/icons/recover_all_large.gif index 035d076420..aef3ea4484 100644 Binary files a/source/web/images/icons/recover_all_large.gif and b/source/web/images/icons/recover_all_large.gif differ diff --git a/source/web/images/icons/recover_large.gif b/source/web/images/icons/recover_large.gif index 035d076420..63326be7a4 100644 Binary files a/source/web/images/icons/recover_large.gif and b/source/web/images/icons/recover_large.gif differ diff --git a/source/web/images/icons/trashcan.gif b/source/web/images/icons/trashcan.gif index 4ee6ccfce4..9b8f4211c9 100644 Binary files a/source/web/images/icons/trashcan.gif and b/source/web/images/icons/trashcan.gif differ diff --git a/source/web/images/icons/trashcan_large.gif b/source/web/images/icons/trashcan_large.gif index cfe75d348b..2ddf47312e 100644 Binary files a/source/web/images/icons/trashcan_large.gif and b/source/web/images/icons/trashcan_large.gif differ diff --git a/source/web/jsp/trashcan/trash-list.jsp b/source/web/jsp/trashcan/trash-list.jsp index e457277401..eab545002e 100644 --- a/source/web/jsp/trashcan/trash-list.jsp +++ b/source/web/jsp/trashcan/trash-list.jsp @@ -140,6 +140,7 @@
    +
<%-- Filter controls --%> @@ -172,7 +173,7 @@ - + @@ -195,12 +196,12 @@ - <%-- Username column --%> + <%-- Deleted by user column --%> - + - + <%-- Actions column --%> @@ -208,10 +209,10 @@ - + - + @@ -219,6 +220,8 @@ + +