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 @@
+
+