/*
* JBoss, Home of Professional Open Source
*
* Distributable under LGPL license.
* See terms of license at gnu.org.
*/
package org.jboss.seam.wiki.core.action;
import org.jboss.seam.Component;
import org.jboss.seam.ScopeType;
import org.jboss.seam.framework.EntityNotFoundException;
import org.jboss.seam.annotations.*;
import org.jboss.seam.annotations.Observer;
import org.jboss.seam.annotations.security.Restrict;
import org.jboss.seam.international.Messages;
import org.jboss.seam.international.StatusMessages;
import org.jboss.seam.log.Log;
import org.jboss.seam.security.Identity;
import org.jboss.seam.wiki.core.model.*;
import org.jboss.seam.wiki.core.dao.WikiNodeDAO;
import org.jboss.seam.wiki.core.dao.UserDAO;
import org.jboss.seam.wiki.core.exception.InvalidWikiRequestException;
import org.jboss.seam.wiki.util.WikiUtil;
import org.richfaces.component.UITree;
import org.richfaces.component.html.HtmlTree;
import org.richfaces.event.NodeExpandedEvent;
import org.richfaces.event.NodeSelectedEvent;
import static org.jboss.seam.international.StatusMessage.Severity.WARN;
import static org.jboss.seam.international.StatusMessage.Severity.INFO;
import javax.persistence.EntityManager;
import java.io.Serializable;
import java.util.*;
/**
* AJAX-oriented backend for browsing directories and copy/pasting files.
*
* @author Christian Bauer
*/
@Name("directoryBrowser")
@Scope(ScopeType.CONVERSATION)
public class DirectoryBrowser implements Serializable {
@Logger
Log log;
@In
Clipboard clipboard;
@In
StatusMessages statusMessages;
@In
EntityManager restrictedEntityManager;
@In
WikiNodeDAO wikiNodeDAO;
@In
WikiDirectory wikiRoot;
@In(value = "directoryBrowserSettings")
DirectoryBrowserSettings settings;
@Create
@Begin // This conversation ends through timeout only
public void create() {
log.debug("instantiating directory browser conversation");
resetPager();
}
private Long directoryId;
private WikiDirectory instance;
// TODO: No more nested set, rewriting this with the same functionality is more difficult...
//private NestedSetNodeWrapper<WikiDirectory> treeRoot;
private List<WikiNode> childNodes;
private Map<WikiNode, Boolean> selectedNodes = new HashMap<WikiNode,Boolean>();
private Pager pager;
public Long getDirectoryId() { return directoryId; }
public void setDirectoryId(Long directoryId) { this.directoryId = directoryId; }
public List<WikiNode> getChildNodes() { return childNodes; }
public Map<WikiNode, Boolean> getSelectedNodes() { return selectedNodes; }
public EntityManager getEntityManager() { return restrictedEntityManager; }
public WikiNodeDAO getWikiNodeDAO() { return wikiNodeDAO; }
public WikiDirectory getInstance() {
if (instance == null) findInstance();
return instance;
}
public void setInstance(WikiDirectory instance) {
this.instance = instance;
}
public Pager getPager() {
return pager;
}
public void setPager(Pager pager) {
this.pager = pager;
}
private void resetPager() {
pager = new Pager(settings.getPageSize());
}
/*
public NestedSetNodeWrapper<WikiDirectory> getTreeRoot() {
if (treeRoot == null) loadTree();
return treeRoot;
}
*/
public void showTree() {
settings.setTreeVisible(true);
}
public void hideTree() {
settings.setTreeVisible(false);
}
/*
// Open a node in the visible UI tree if its identifier is in the current path
public boolean adviseTreeNodeOpened(UITree tree) {
// We need to call the undocumented getRowData() and not getTreeNode() because we
// use the rich:recursiveTreeNodeAdapter...
if (tree.getRowData() == null) return false; // Safety against RichFaces behavior
Long currentTreeNodeId = ((NestedSetNodeWrapper<WikiDirectory>) tree.getRowData()).getWrappedNode().getId();
if (settings.getExpandedTreeNodes().contains(currentTreeNodeId)) {
log.debug("node is stored as expanded in session: " + currentTreeNodeId);
return true;
}
if (getInstance().getPathIdentifiers().contains(currentTreeNodeId)) {
log.debug("node is in parent path of current directory, hence expanded: " + currentTreeNodeId);
return true;
}
log.debug("node is not expanded: " + currentTreeNodeId);
return false;
}
// Select the node in the visible UI tree if its identifier is the same as the current directory
public boolean adviseTreeNodeSelected(UITree tree) {
if (tree.getRowData() == null) return false; // Safety against RichFaces behavior
Long currentTreeNodeId = ((NestedSetNodeWrapper<WikiDirectory>) tree.getRowData()).getWrappedNode().getId();
return getInstance().getId().equals(currentTreeNodeId);
}
public void listenTreeNodeExpand(NodeExpandedEvent event) {
Long currentTreeNodeId =
((NestedSetNodeWrapper<WikiDirectory>) ((HtmlTree)event.getSource()).getRowData()).getWrappedNode().getId();
boolean isExpanded = ((HtmlTree)event.getSource()).isExpanded();
if (isExpanded) {
log.debug("expanding tree node: " + currentTreeNodeId);
settings.getExpandedTreeNodes().add(currentTreeNodeId);
} else {
log.debug("collapsing tree node: " + currentTreeNodeId);
settings.getExpandedTreeNodes().remove(currentTreeNodeId);
}
}
public void listenTreeNodeSelected(NodeSelectedEvent event) {
Long currentTreeNodeId =
((NestedSetNodeWrapper<WikiDirectory>) ((HtmlTree)event.getSource()).getRowData()).getWrappedNode().getId();
log.debug("selecting tree node: " + currentTreeNodeId);
selectDirectory(currentTreeNodeId);
}
*/
public void findInstance() {
if (getDirectoryId() == null)
throw new InvalidWikiRequestException("Missing directoryId parameter");
instance = wikiNodeDAO.findWikiDirectory(getDirectoryId());
if (instance == null)
throw new EntityNotFoundException(getDirectoryId(), WikiDirectory.class);
afterNodeFound();
}
public void afterNodeFound() {
refreshChildNodes();
}
public void selectDirectory(Long nodeId) {
resetPager();
setDirectoryId(nodeId);
instance = null;
findInstance();
settings.getExpandedTreeNodes().add(nodeId);
}
public void sortBy(String propertyName) {
resetPager();
settings.setOrderByProperty(WikiNode.SortableProperty.valueOf(propertyName));
settings.setOrderDescending(!settings.isOrderDescending());
refreshChildNodes();
}
public void changePageSize() {
pager.setPage(0);
refreshChildNodes();
}
/*
@Observer(value = {"Node.removed"}, create = false)
public void loadTree() {
WikiDirectory wikiRoot = (WikiDirectory) Component.getInstance("wikiRoot");
treeRoot = wikiNodeDAO.findWikiDirectoryTree(wikiRoot);
}
*/
@Observer(value = {"Node.removed", "Pager.pageChanged"}, create = false)
public void refreshChildNodes() {
log.debug("refreshing child nodes of current directory: " + getInstance());
getPager().setNumOfRecords(wikiNodeDAO.findChildrenCount(getInstance()));
getPager().setPageSize(settings.getPageSize());
log.debug("number of children: " + getPager().getNumOfRecords());
if (getPager().getNumOfRecords() > 0) {
log.debug("loading children page from: " + getPager().getNextRecord() + " size: " + getPager().getPageSize());
childNodes =
wikiNodeDAO.findChildren(
getInstance(),
settings.getOrderByProperty(),
!settings.isOrderDescending(),
getPager().getQueryFirstResult(),
getPager().getQueryMaxResults()
);
} else {
childNodes = Collections.emptyList();
}
}
// TODO: Most of this clipboard stuff is based on the hope that nobody modifies anything while we have it in the clipboard...
public void clearClipboard() {
clipboard.clear();
}
public void copy() {
for (Map.Entry<WikiNode, Boolean> entry : selectedNodes.entrySet()) {
if (entry.getValue()) { // Has to be true for a selected node
log.debug("copying to clipboard: " + entry.getKey());
clipboard.add(entry.getKey().getId(), false);
}
}
selectedNodes.clear();
}
@Restrict("#{s:hasPermission('Node', 'edit', directoryBrowser.instance)}")
public void cut() {
for (Map.Entry<WikiNode, Boolean> entry : selectedNodes.entrySet()) {
if (entry.getValue()) { // Has to be true for a selected node
log.debug("cutting to clipboard: " + entry.getKey());
clipboard.add(entry.getKey().getId(), true);
}
}
selectedNodes.clear();
refreshChildNodes();
}
@Restrict("#{s:hasPermission('Node', 'create', directoryBrowser.instance)}")
public void paste() {
if (getInstance().getId().equals(wikiRoot.getId())) return; // Can't paste in wiki root
// Batch the work
int batchSize = 2;
int i = 0;
List<Long> batchIds = new ArrayList<Long>();
for (Long clipboardNodeId : clipboard.getItems()) {
i++;
batchIds.add(clipboardNodeId);
if (i % batchSize == 0) {
List<WikiNode> nodesForPasteBatch = wikiNodeDAO.findWikiNodes(batchIds);
pasteNodes(nodesForPasteBatch);
batchIds.clear();
}
}
// Last batch
if (batchIds.size() != 0) {
List<WikiNode> nodesForPasteBatch = wikiNodeDAO.findWikiNodes(batchIds);
pasteNodes(nodesForPasteBatch);
}
log.debug("completed executing paste, refreshing...");
selectedNodes.clear();
clipboard.clear();
refreshChildNodes();
}
private void pasteNodes(List<WikiNode> nodes) {
log.debug("executing paste batch");
for (WikiNode n: nodes) {
log.debug("pasting clipboard item: " + n);
String pastedName = n.getName();
// Check unique name if we are not cutting and pasting into the same area
if (!(clipboard.isCut(n.getId()) && n.getParent().getAreaNumber().equals(getInstance().getAreaNumber()))) {
log.debug("pasting node into different area, checking wikiname");
if (!wikiNodeDAO.isUniqueWikiname(getInstance().getAreaNumber(), WikiUtil.convertToWikiName(pastedName))) {
log.debug("wikiname is not unique, renaming");
if (pastedName.length() > 245) {
statusMessages.addToControlFromResourceBundleOrDefault(
"name",
WARN,
"lacewiki.msg.Clipboard.DuplicatePasteNameFailure",
"The name '{0}' was already in use in this area and is too long to be renamed, skipping paste.",
pastedName
);
continue; // Jump to next loop iteration when we can't append a number to the name
}
// Now try to add "Copy 1", "Copy 2" etc. to the name until it is unique
int i = 1;
String attemptedName = pastedName + " " + Messages.instance().get("lacewiki.label.Clipboard.CopySuffix") + i;
while (!wikiNodeDAO.isUniqueWikiname(getInstance().getAreaNumber(), WikiUtil.convertToWikiName(attemptedName))) {
attemptedName = pastedName + " " + Messages.instance().get("lacewiki.label.Clipboard.CopySuffix") + (++i);
}
pastedName = attemptedName;
statusMessages.addToControlFromResourceBundleOrDefault(
"name",
INFO,
"lacewiki.msg.Clipboard.DuplicatePasteName",
"The name '{0}' was already in use in this area, renamed item to '{1}'.",
n.getName(), pastedName
);
}
}
if (clipboard.isCut(n.getId())) {
log.debug("cut pasting: " + n);
// Check if the cut item was a default file for its parent
if ( ((WikiDirectory)n.getParent()).getDefaultFile() != null &&
((WikiDirectory)n.getParent()).getDefaultFile().getId().equals(n.getId())) {
log.debug("cutting default file of directory: " + n.getParent());
((WikiDirectory)n.getParent()).setDefaultFile(null);
}
n.setName(pastedName);
n.setWikiname(WikiUtil.convertToWikiName(pastedName));
n.setParent(getInstance());
// If we cut and paste into a new area, all children must be updated as well
if (!getInstance().getAreaNumber().equals(n.getAreaNumber())) {
n.setAreaNumber(getInstance().getAreaNumber());
// TODO: Ugly and memory intensive, better use a database query but HQL updates are limited with joins
if (n.isInstance(WikiDocument.class)) {
List<WikiComment> comments = wikiNodeDAO.findWikiComments((WikiDocument)n, true);
for (WikiComment comment : comments) {
comment.setAreaNumber(n.getAreaNumber());
}
}
}
} else {
log.debug("copy pasting: " + n);
WikiNode newNode = n.duplicate(true);
newNode.setName(pastedName);
newNode.setWikiname(WikiUtil.convertToWikiName(pastedName));
newNode.setParent(getInstance());
newNode.setAreaNumber(getInstance().getAreaNumber());
UserDAO userDAO = (UserDAO)Component.getInstance(UserDAO.class);
newNode.setCreatedBy(userDAO.findUser(n.getCreatedBy().getId()));
if (n.getLastModifiedBy() != null) {
newNode.setLastModifiedBy(userDAO.findUser(n.getLastModifiedBy().getId()));
}
restrictedEntityManager.persist(newNode);
}
}
log.debug("completed executing of paste batch");
}
@Restrict("#{s:hasPermission('Trash', 'empty', trashArea)}")
public void emptyTrash() {
WikiDirectory trashArea = (WikiDirectory) Component.getInstance("trashArea");
if (getInstance() == null || !trashArea.getId().equals(getInstance().getId())) return;
log.debug("emptying trash");
List<WikiNode> children = wikiNodeDAO.findChildren(getInstance(), WikiNode.SortableProperty.name, false, 0, Integer.MAX_VALUE);
// TODO: This should be batched with a database cursor!
for (WikiNode child : children) {
log.debug("trashing item: " + child);
if (child.isInstance(WikiDocument.class)) {
NodeRemover documentRemover = (NodeRemover)Component.getInstance(DocumentNodeRemover.class);
documentRemover.removeDependencies(child);
} else if (child.isInstance(WikiUpload.class)) {
NodeRemover uploadRemover = (NodeRemover)Component.getInstance(UploadNodeRemover.class);
uploadRemover.removeDependencies(child);
}
restrictedEntityManager.remove(child);
}
restrictedEntityManager.flush();
statusMessages.addFromResourceBundleOrDefault(
INFO,
"lacewiki.msg.Trash.Emptied",
"All items in the trash have been permanently deleted."
);
selectedNodes.clear();
refreshChildNodes();
}
// TODO: I'm not too happy with this, maybe we should call the NodeRemovers directly from the XHTML
// Cache removablity information, speeds up large lists
Map<Long, Boolean> childNodesRemovability = new HashMap<Long, Boolean>();
public boolean isRemovable(WikiNode node) {
if (childNodesRemovability.containsKey(node.getId())) {
// Return cached result
return childNodesRemovability.get(node.getId());
}
log.debug("checking removablity of node: " + node);
// Check if the current directory is the trash area, delete doesn't make sense here
WikiDirectory trashArea = (WikiDirectory)Component.getInstance("trashArea");
if (trashArea.getId().equals(getInstance().getId()))
return false;
// Check permissions TODO: This duplicates the check
if (!Identity.instance().hasPermission("Node", "edit", node)) {
log.debug("user doesn't have edit permissions for this node: " + node);
return false;
}
NodeRemover remover;
if (node.isInstance(WikiDocument.class)) {
remover = (NodeRemover) Component.getInstance(DocumentNodeRemover.class);
} else if (node.isInstance(WikiUpload.class)) {
remover = (NodeRemover) Component.getInstance(UploadNodeRemover.class);
} else {
log.warn("no remover found for node type: " + node);
return false;
}
boolean removable = remover.isRemovable(node);
log.debug("remover said it's removable: " + removable);
childNodesRemovability.put(node.getId(), removable);
return removable;
}
}