org.artifactory.webapp.wicket.actionable.tree.ActionableItemsTree.java Source code

Java tutorial

Introduction

Here is the source code for org.artifactory.webapp.wicket.actionable.tree.ActionableItemsTree.java

Source

/*
 * Artifactory is a binaries repository manager.
 * Copyright (C) 2012 JFrog Ltd.
 *
 * Artifactory 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.
 *
 * Artifactory 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 Artifactory.  If not, see <http://www.gnu.org/licenses/>.
 */

package org.artifactory.webapp.wicket.actionable.tree;

import org.apache.commons.lang.StringUtils;
import org.apache.wicket.Component;
import org.apache.wicket.MarkupContainer;
import org.apache.wicket.ajax.AjaxEventBehavior;
import org.apache.wicket.ajax.AjaxRequestTarget;
import org.apache.wicket.ajax.IAjaxCallDecorator;
import org.apache.wicket.extensions.markup.html.tree.Tree;
import org.apache.wicket.markup.html.WebMarkupContainer;
import org.apache.wicket.markup.html.panel.Panel;
import org.apache.wicket.markup.html.tree.ITreeState;
import org.apache.wicket.model.Model;
import org.artifactory.api.repo.exception.ItemNotFoundRuntimeException;
import org.artifactory.common.wicket.ajax.CancelDefaultDecorator;
import org.artifactory.common.wicket.behavior.CssClass;
import org.artifactory.repo.RepoPath;
import org.artifactory.util.PathUtils;
import org.artifactory.webapp.actionable.ActionableItem;
import org.artifactory.webapp.actionable.RepoAwareActionableItem;
import org.artifactory.webapp.actionable.action.DeleteAction;
import org.artifactory.webapp.actionable.action.ItemAction;
import org.artifactory.webapp.actionable.action.ItemActionListener;
import org.artifactory.webapp.actionable.event.ItemEvent;
import org.artifactory.webapp.actionable.model.Compactable;
import org.artifactory.webapp.actionable.model.HierarchicActionableItem;
import org.artifactory.webapp.actionable.model.ZipFileActionableItem;
import org.artifactory.webapp.wicket.actionable.tree.menu.ActionsMenuPanel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.TreeNode;
import java.util.Collection;
import java.util.Enumeration;
import java.util.List;
import java.util.Set;

import static java.lang.String.format;

/**
 * @author Yoav Landman
 */
public class ActionableItemsTree extends Tree implements ItemActionListener, Compactable {
    private static final Logger log = LoggerFactory.getLogger(ActionableItemsTree.class);

    private final ActionableItemsProvider itemsProvider;

    /**
     * Builds a tree and set the selected path to the input repo path. If the repoPath is null or not found, we use the
     * default view.
     *
     * @param id               The wicket id
     * @param itemsProvider    Actionable items provider
     * @param defaultSelection The path to select
     * @param compactAllowed   Is folder nodes compacting allowed
     */
    public ActionableItemsTree(String id, ActionableItemsProvider itemsProvider,
            DefaultTreeSelection defaultSelection, boolean compactAllowed) {
        super(id);
        this.itemsProvider = itemsProvider;
        setOutputMarkupId(true);

        setRootLess(true);
        setCompactAllowed(compactAllowed);
        selectPath(defaultSelection);
    }

    @Override
    public void setCompactAllowed(boolean compactAllowed) {
        HierarchicActionableItem root = this.itemsProvider.getRoot();
        if (root != null) {
            root.setCompactAllowed(compactAllowed);
        }
        ActionableItemTreeNode rootNode = new ActionableItemTreeNode(root);
        DefaultTreeModel treeModel = new DefaultTreeModel(rootNode);
        setDefaultModel(new Model<>(treeModel));
        List<? extends ActionableItem> children = this.itemsProvider.getChildren(root);
        setChildren(rootNode, children);
        getTreeState().expandNode(rootNode);
        if (rootNode.getChildCount() > 0) {
            selectNode(rootNode.getFirstChild());
        }
    }

    @Override
    protected void detachModel() {
        super.detachModel();
        detachNodes();
    }

    @SuppressWarnings({ "unchecked" })
    private void detachNodes() {
        ActionableItemTreeNode root = (ActionableItemTreeNode) getTreeModel().getRoot();
        Enumeration<ActionableItemTreeNode> nodes = root.depthFirstEnumeration();
        while (nodes.hasMoreElements()) {
            ActionableItemTreeNode node = nodes.nextElement();
            ActionableItem userObject = node.getUserObject();
            if (userObject != null) {
                userObject.detach();
            }
        }
    }

    @Override
    public boolean isCompactAllowed() {
        return ((Compactable) getTreeModel().getRoot()).isCompactAllowed();
    }

    private void selectPath(DefaultTreeSelection defaultSelection) {
        if (defaultSelection == null) {
            return;
        }

        String treePath = defaultSelection.getDefaultSelectionTreePath();
        if (StringUtils.isNotBlank(treePath)) {
            try {
                // now build all the nodes on the way to the destination path and
                // expand only the nodes to the destination path
                DefaultTreeModel treeModel = getTreeModel();
                ActionableItemTreeNode parentNode = (ActionableItemTreeNode) treeModel.getRoot();
                String remainingPath = treePath;
                ActionableItemTreeNode currentNode = null;
                while (PathUtils.hasText(remainingPath)) {

                    // get deepest node for the path (will also take care of compacted paths)
                    currentNode = defaultSelection.getNodeAt(parentNode, remainingPath);
                    if (currentNode == parentNode) {
                        throw new ItemNotFoundRuntimeException(format("Child node %s not found under %s",
                                remainingPath, parentNode.getUserObject().getDisplayName()));
                    }

                    ActionableItem userObject = currentNode.getUserObject();
                    if (userObject instanceof HierarchicActionableItem
                            && !(userObject instanceof ZipFileActionableItem)) {
                        // the node found is hierarchical, meaning it can have children
                        // so we get and create all the current node children
                        List<? extends ActionableItem> folderChildren = itemsProvider
                                .getChildren((HierarchicActionableItem) userObject);
                        setChildren(currentNode, folderChildren);
                        getTreeState().expandNode(currentNode);
                        parentNode = currentNode;
                    }

                    // subtract the resolved path from the remaining path
                    // we are currently relying on the display name as there is
                    // no better way to know if the node was compacted or not
                    String displayName = userObject.getDisplayName();
                    remainingPath = remainingPath.substring(displayName.length());
                    // just make sure we don't have '/' at the beginning
                    remainingPath = PathUtils.trimLeadingSlashes(remainingPath);
                }

                // everything went well and we have the destination node. now select it
                selectNode(currentNode);

            } catch (Exception e) {
                String message = "Unable to find path: " + treePath;
                error(message);
                log.error(message, e);
                getTreeState().collapseAll();
            }
        }
    }

    private DefaultTreeModel getTreeModel() {
        return (DefaultTreeModel) getDefaultModelObject();
    }

    @Override
    protected Component newNodeIcon(MarkupContainer parent, String id, TreeNode node) {
        WebMarkupContainer icon = new WebMarkupContainer(id);
        ActionableItemTreeNode treeNode = (ActionableItemTreeNode) node;
        ActionableItem item = treeNode.getUserObject();
        icon.add(new CssClass(item.getCssClass()));
        return icon;
    }

    @Override
    protected void populateTreeItem(final WebMarkupContainer item, int level) {
        super.populateTreeItem(item, level);
        item.get("nodeLink:label").add(new CssClass("node-label"));

        item.get("nodeLink").add(new AjaxEventBehavior("oncontextmenu") {
            @Override
            protected void onEvent(AjaxRequestTarget target) {
                onContextMenu(item, target);
            }

            @Override
            protected IAjaxCallDecorator getAjaxCallDecorator() {
                return new CancelDefaultDecorator();
            }
        });
    }

    protected void onContextMenu(Component item, AjaxRequestTarget target) {
        ActionableItemTreeNode node = (ActionableItemTreeNode) item.getDefaultModelObject();

        // check at least one action is enabled
        Set<ItemAction> actions = node.getUserObject().getContextMenuActions();
        for (ItemAction action : actions) {
            if (action.isEnabled()) {
                showContextMenu(item, node, target);
                return;
            }
        }
    }

    private void showContextMenu(Component item, ActionableItemTreeNode node, AjaxRequestTarget target) {
        ActionsMenuPanel menuPanel = new ActionsMenuPanel("contextMenu", node);
        getParent().replace(menuPanel);
        target.add(menuPanel);
        target.appendJavaScript(format("ActionsMenuPanel.show('%s');", item.getMarkupId()));
    }

    /**
     * User clicked on the junction link to expand/collapse a (hierarchical) node. In case of expand, refresh the node
     * children.
     *
     * @param node The node to expand/collapse.
     */
    @Override
    public void onJunctionLinkClicked(AjaxRequestTarget target, TreeNode node) {
        super.onJunctionLinkClicked(target, node);
        boolean expanded = isNodeExpanded(node);
        if (expanded) {
            refreshChildren((ActionableItemTreeNode) node);
        }
        adjustLayout(target);
    }

    public void adjustLayout(AjaxRequestTarget target) {
        target.appendJavaScript("dijit.byId('browseTree').layout();");
    }

    private void refreshChildren(ActionableItemTreeNode actionableItemTreeNode) {
        HierarchicActionableItem item = (HierarchicActionableItem) actionableItemTreeNode.getUserObject();
        debugGetChildren(item, "Getting children for");
        List<? extends ActionableItem> children = itemsProvider.getChildren(item);
        setChildren(actionableItemTreeNode, children);
        debugGetChildren(item, "Got children for");
    }

    @Override
    protected void onNodeLinkClicked(AjaxRequestTarget target, TreeNode node) {
        super.onNodeLinkClicked(target, node);
        selectNode(node);
        target.add(itemsProvider.getItemDisplayPanel());
    }

    private void selectNode(TreeNode node) {
        getTreeState().selectNode(node, true);
        refreshDisplayPanel();
    }

    public Panel refreshDisplayPanel() {
        ActionableItemTreeNode mutableTreeNode = (ActionableItemTreeNode) getSelectedNode();
        ActionableItem item = mutableTreeNode.getUserObject();
        Panel oldDisplayPanel = itemsProvider.getItemDisplayPanel();
        Panel newDisplayPanel = item.newItemDetailsPanel(oldDisplayPanel.getId());
        newDisplayPanel.setOutputMarkupId(true);
        oldDisplayPanel.replaceWith(newDisplayPanel);
        itemsProvider.setItemDisplayPanel(newDisplayPanel);
        return newDisplayPanel;
    }

    @Override
    protected Component newJunctionLink(MarkupContainer parent, String id, String imageId, TreeNode node) {
        //Collapse empty nodes
        ActionableItemTreeNode mutableTreeNode = (ActionableItemTreeNode) node;
        ActionableItem userObject = mutableTreeNode.getUserObject();
        if (userObject instanceof HierarchicActionableItem) {
            boolean hasChildren = itemsProvider.hasChildren((HierarchicActionableItem) userObject);
            //Must be set before the call to super
            mutableTreeNode.setLeaf(!hasChildren);
        } else {
            mutableTreeNode.setLeaf(true);
        }
        return super.newJunctionLink(parent, id, imageId, node);
    }

    @Override
    public void onTargetRespond(AjaxRequestTarget target) {
        log.debug("Beginning tree update ajax response.");
        super.onTargetRespond(target);
        log.debug("Finished tree update ajax response.");
    }

    @Override
    public void actionPerformed(ItemEvent e) {
        String command = e.getActionCommand();

        if (DeleteAction.ACTION_NAME.equals(command) || "Discard from Results".equals(command)) {

            ActionableItem item = e.getSource();
            ActionableItemTreeNode itemNode = searchForNodeByItem(item);
            if (itemNode == null || itemNode.getParent() == null) {
                return;
            }

            ActionableItemTreeNode parentNode = itemNode.getParent();

            if (parentNode.isRoot()) {
                // if this is a repository node just remove all its children, not the node itself
                itemNode.removeAllChildren();
            } else {
                itemNode.removeFromParent();
            }

            expandNode(e.getTarget(), parentNode);
        }
    }

    private void expandNode(AjaxRequestTarget target, ActionableItemTreeNode node) {
        getTreeState().expandNode(node);
        target.add(this);
        adjustLayout(target);
    }

    /**
     * @param item Item to look for
     * @return Tree node containing this item as the user object, null if not found
     */
    public ActionableItemTreeNode searchForNodeByItem(ActionableItem item) {
        DefaultTreeModel model = getTreeModel();
        ActionableItemTreeNode root = (ActionableItemTreeNode) model.getRoot();
        return searchForNodeByItem(root, item);
    }

    /**
     * Removes the node containing the item from its parent node and refreshe the tree.
     *
     * @param item The item to look for
     */
    public void removeItemNodeFromParent(ActionableItem item) {
        ActionableItemTreeNode node = searchForNodeByItem(item);
        if (node != null && node.getParent() != null) {
            ActionableItemTreeNode parent = node.getParent();
            node.removeFromParent();
            if (AjaxRequestTarget.get() != null) {
                expandNode(AjaxRequestTarget.get(), parent);
            }

        }
    }

    /**
     * Collapse the parent node of the node containing the item and refresh the tree.
     *
     * @param item The item the node contains
     */
    public void collapseItemNode(ActionableItem item) {
        ActionableItemTreeNode node = searchForNodeByItem(item);
        if (node != null && node.getParent() != null) {
            ActionableItemTreeNode parent = node.getParent();
            getTreeState().collapseNode(node);
            if (AjaxRequestTarget.get() != null) {
                expandNode(AjaxRequestTarget.get(), parent);
            }
        }
    }

    /**
     * Expand the node and refresh the node children.
     *
     * @param item The item the node contains
     */
    public void refreshAndExpandItemNode(ActionableItem item) {
        ActionableItemTreeNode node = searchForNodeByItem(item);
        if (AjaxRequestTarget.get() != null) {
            getTreeState().collapseNode(node);
            refreshChildren(node);
            expandNode(AjaxRequestTarget.get(), node);
        }
    }

    private ActionableItemTreeNode searchForNodeByItem(ActionableItemTreeNode node, ActionableItem item) {
        if (node.getUserObject().equals(item)) {
            // found the node containing the input item
            return node;
        } else if (node.getChildCount() <= 0) {
            // not this node and doesn't have any children, return null
            return null;
        } else {
            // search in children
            Enumeration children = node.children();
            while (children.hasMoreElements()) {
                ActionableItemTreeNode result = searchForNodeByItem((ActionableItemTreeNode) children.nextElement(),
                        item);
                if (result != null) {
                    // node found under the current children, return it
                    return result;
                }
            }
            // not found
            return null;
        }
    }

    public void selectNode(TreeNode selectedNode, TreeNode newSelection, AjaxRequestTarget target) {
        ITreeState state = getTreeState();
        if (selectedNode != null && newSelection != null) {
            state.selectNode(selectedNode, false);
            state.selectNode(newSelection, true);
            target.add(this);
            selectNode(newSelection);
            target.add(itemsProvider.getItemDisplayPanel());
            target.appendJavaScript("Browser.scrollToSelectedNode();");
        }
    }

    public TreeNode getNextTreeNode(TreeNode node) {
        ITreeState state = getTreeState();
        DefaultMutableTreeNode parent = (DefaultMutableTreeNode) node.getParent();
        if (parent == null) {
            return null;
        }
        if (!node.isLeaf() && node.getAllowsChildren() && state.isNodeExpanded(node)) {
            return node.getChildAt(0);
        }

        TreeNode nextNode = parent.getChildAfter(node);
        if (nextNode == null) {
            return getNextParent(parent);
        }
        return nextNode;
    }

    public TreeNode getNextParent(DefaultMutableTreeNode node) {
        DefaultMutableTreeNode parent = (DefaultMutableTreeNode) node.getParent();
        if (parent == null) {
            return null;
        }
        TreeNode nextNode = parent.getChildAfter(node);
        if (nextNode == null) {
            return getNextParent(parent);
        }
        return nextNode;
    }

    public TreeNode getPrevTreeNode(TreeNode node) {
        DefaultMutableTreeNode parent = (DefaultMutableTreeNode) node.getParent();
        if (parent == null) {
            return null;
        }

        TreeNode prevNode = parent.getChildBefore(node);
        if (prevNode != null) {
            ITreeState state = getTreeState();
            node = prevNode;
            while (!node.isLeaf() && node.getAllowsChildren() && state.isNodeExpanded(node)) {
                node = node.getChildAt(node.getChildCount() - 1);
            }
            return node;
        }

        DefaultTreeModel treeModel = getTreeModel();
        if (parent == treeModel.getRoot()) {
            return null;
        }
        return parent;
    }

    @SuppressWarnings({ "unchecked" })
    public TreeNode getSelectedNode() {
        Collection<Object> selectedNodes = getTreeState().getSelectedNodes();
        if (selectedNodes.isEmpty()) {
            return null;
        }
        return (TreeNode) selectedNodes.iterator().next();
    }

    private static void setChildren(ActionableItemTreeNode node, List<? extends ActionableItem> children) {
        node.removeAllChildren();
        for (ActionableItem child : children) {
            ActionableItemTreeNode newChildNode = new ActionableItemTreeNode(child);
            node.add(newChildNode);
        }
    }

    private static void debugGetChildren(HierarchicActionableItem item, String msg) {
        if (!log.isDebugEnabled()) {
            return;
        }
        if (item instanceof RepoAwareActionableItem) {
            RepoAwareActionableItem raai = (RepoAwareActionableItem) item;
            RepoPath repoPath = raai.getRepoPath();
            log.debug(msg + " '" + repoPath + "'...");
        }
    }
}