/* license-start * * Copyright (C) 2008 - 2013 Crispico, <http://www.crispico.com/>. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation version 3. * * This program 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 General Public License for more details, at <http://www.gnu.org/licenses/>. * * Contributors: * Crispico - Initial API and implementation * * license-end */ package org.flowerplatform.communication.tree.remote; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import org.flowerplatform.common.log.AuditDetails; import org.flowerplatform.common.log.LogUtil; import org.flowerplatform.common.util.Pair; import org.flowerplatform.communication.channel.CommunicationChannel; import org.flowerplatform.communication.stateful_service.RemoteInvocation; import org.flowerplatform.communication.stateful_service.StatefulService; import org.flowerplatform.communication.stateful_service.StatefulServiceInvocationContext; import org.flowerplatform.communication.tree.GenericTreeContext; import org.flowerplatform.communication.tree.TreeInfoClient; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public abstract class AbstractTreeStatefulService extends StatefulService { public final static String NODE_TYPE_ROOT = "r"; public static final String WHOLE_TREE_KEY = "wholeTree"; public static final String EXPAND_NODE_KEY = "expandNode"; public static final String SELECT_NODE_KEY = "selectNode"; /** * Useful, for trees that want to display a subset of a bigger tree, with data * taken in "one-shot". E.g. some GIT tree that displays only a part of what * is already displayed in the explorer tree. */ public static final String DONT_UPDATE_MAP_KEY = "dontUpdateMap"; private static final Logger logger = LoggerFactory.getLogger(AbstractTreeStatefulService.class); /** * Holds the contexts of all subscribed trees. */ protected Map<TreeInfoClient, GenericTreeContext> treeContexts = new ConcurrentHashMap<TreeInfoClient, GenericTreeContext>(); public abstract String getStatefulClientPrefixId(); protected String getNodeType(TreeNode treeNode) { if (treeNode.getPathFragment() == null) { // root node return NODE_TYPE_ROOT; } else { return treeNode.getPathFragment().getType(); } } protected String getNodeType(PathFragment pathFragment) { if (pathFragment == null) { return NODE_TYPE_ROOT; } else { return pathFragment.getType(); } } /** * Factory method returning the instance of tree node used. * * @see #openNode() * @see #dispatchContentUpdate() * @see #dispatchLabelUpdate() * */ protected TreeNode createTreeNode() { return new TreeNode(); } /** * Populates {@link TreeNode#getPathFragment()} with data form {@link #getPathFragmentForNode()} and * then delegates to the abstract method {@link #populateChildren()}. * * @author Cristi * */ protected void populateTreeNodeInternal(Object source, TreeNode destination, GenericTreeContext context) { destination.setPathFragment(getPathFragmentForNode(source, getNodeType(destination), context)); destination.setHasChildren(nodeHasChildren(source, destination, context)); populateTreeNode(source, destination, context); } /** * This method might be used for trees that send all the data at the beginning (i.e. no * more openNode/closeNode afterwards). * * @param recurse - if <code>true</code>, creates the whole tree structure for given node. * Otherwise creates only its direct children. * */ protected void populateChildren(CommunicationChannel channel, String statefulClientId, Object node, TreeNode treeNode, GenericTreeContext context, boolean recurse) { // create and populate the children list for (Pair<Object, String> pair : getChildrenForNode(node, treeNode, context)) { Object child = pair.a; TreeNode childNode = createTreeNode(); childNode.setPathFragment(new PathFragment(null, pair.b)); populateTreeNodeInternal(child, childNode, context); treeNode.getChildren().add(childNode); childNode.setParent(treeNode); if (recurse) { // get child whole structure childNode.setChildren(new ArrayList<TreeNode>()); populateChildren(channel, statefulClientId, child, childNode, context, recurse); } } } /** * Subclasses that implement this method must provide * a list of children for given node. If the parameter * is the dummy object used as root, that means that the content for * the root node should be returned. * * <p> * Mandatory for all tree types. * * <p> * Also a context can be provided to filter the children list. * */ public abstract Collection<Pair<Object, String>> getChildrenForNode(Object node, TreeNode treeNode, GenericTreeContext context); /** * Should return whether the current node has children or not, preferably * by an efficient method (i.e. something better than <code>getChildrenForNode() != null</code>). * This recommendation is related to possible performance impact. E.g. this method * may trigger the load mechanism of a resource. * * @author Cristi * @return a <code>Boolean</code> which has 3 states. The 3rd state (i.e. null) may * be handy if implementing services that delegate to other "sub"-services. */ public abstract Boolean nodeHasChildren(Object node, TreeNode treeNode, GenericTreeContext context); /** * Subclasses that implement this method must * populate the <code>destination</code> tree node with * information stored in <code>source</code>. * * <p> * This must operate only on current node properties, not on its children * (e.g. label, icon, etc.). {@link TreeNode#isHasChildren()} and {@link TreeNode#getPathFragment()) * are already automatically populated. * * <p> * This method is never invoked for the root node. * * * @author Cristi * @return The return result is not taken into account by the platform. By convention, everyone * should return <code>true</code>. The return value may be used by tree services that have "sub" * tree services that they use for delegation (e.g. if result == null => the sub-service didn't know * how to handle the call). */ public abstract boolean populateTreeNode(Object source, TreeNode destination, GenericTreeContext context); /** * Subclasses that implement this method must provide * a {@link PathFragment} for given node. * @author Mariana */ public abstract PathFragment getPathFragmentForNode(Object node, String nodeType, GenericTreeContext context); /** * Subclasses that implement this method must the path fragment for given node. * <p> * If path fragment isn't human readable, subclasses must return a suggestive string instead. * * */ public abstract String getLabelForLog(Object node, String nodeType); public abstract Object getNodeByPath(List<PathFragment> fullPath, GenericTreeContext context); public abstract List<PathFragment> getPathForNode(Object node, String nodeType, GenericTreeContext context); /** * Creates the client tree structure and * modifies the server structure by updating {@link #rootNodeInfo} and {@link #visibleNodes}. * * @return - the {@link TreeNode} created * * @see #populateChildren() * @see #addNodeInfo() * */ public TreeNode openNodeInternal(CommunicationChannel channel, String statefulClientId, List<PathFragment> fullPath, Map<Object, Object> context) { GenericTreeContext treeContext = getTreeContext(channel, statefulClientId); treeContext.setClientContext(context); // gets the source node corresponding to given path Object source = getNodeByPath(fullPath, treeContext); // create and populate the destination node TreeNode treeNode = createTreeNode(); treeNode.setChildren(new ArrayList<TreeNode>()); if (fullPath != null) { // we populate the node only for non-root nodes if (fullPath.isEmpty()) { throw new IllegalArgumentException("Trying to open with an empty path. It should be null or non-empty"); } treeNode.setPathFragment(fullPath.get(fullPath.size() - 1)); populateTreeNodeInternal(source, treeNode, treeContext); } else { treeNode.setHasChildren(true); } // create structure for current tree node or create structure for entire tree boolean entireStructure = false; if (treeContext.get(WHOLE_TREE_KEY) != null) { entireStructure = ((Boolean) context.get(WHOLE_TREE_KEY)).booleanValue(); } // populate node with children data populateChildren(channel, statefulClientId, source, treeNode, treeContext, entireStructure); return treeNode; } protected TreeNode openNodeInternalFromExistingNodes(CommunicationChannel channel, String statefulClientId, List<PathFragment> fullPath, Map<Object, Object> context, Set<String> existingNodes) { TreeNode node = openNodeInternal(channel, statefulClientId, fullPath, context); List<TreeNode> children = new ArrayList<TreeNode>(); for (TreeNode child : node.getChildren()) { // get child path as list of pathFragment List<PathFragment> childFullPath = new ArrayList<PathFragment>(); if (fullPath != null) { childFullPath.addAll(fullPath); } childFullPath.add(child.getPathFragment()); // get child path as string String path = ""; for (PathFragment pathFragment : childFullPath) { if (path != "") { path += "/"; } path += pathFragment.getName(); } if (existingNodes.contains(path)) { children.add(openNodeInternalFromExistingNodes(channel, statefulClientId, childFullPath, context, existingNodes)); } else { children.add(child); } } node.setChildren(children); return node; } protected void updateNode(CommunicationChannel channel, String statefulClientId, List<PathFragment> path, TreeNode treeNode, boolean expandNode, boolean colapseNode, boolean selectNode, boolean isContentUpdate) { invokeClientMethod( channel, statefulClientId, "updateNode", new Object[] {path, treeNode, expandNode, colapseNode, selectNode}); } public GenericTreeContext getTreeContext(CommunicationChannel channel, String statefulClientId) { TreeInfoClient treeInfoClient = new TreeInfoClient(channel, statefulClientId); if (!treeContexts.containsKey(treeInfoClient)) { treeContexts.put(treeInfoClient, new GenericTreeContext(this)); } return treeContexts.get(treeInfoClient); } public void revealNode(StatefulServiceInvocationContext context, Object node, String nodeType) { if (logger.isTraceEnabled()) { logger.trace("Revealing node {} to client [{} with statefulClientId={}]", new Object[] { getLabelForLog(node, nodeType), context.getCommunicationChannel(), context.getStatefulClientId() }); } invokeClientMethod( context.getCommunicationChannel(), context.getStatefulClientId(), "revealNode", new Object[] {getPathForNode(node, nodeType, getTreeContext(context.getCommunicationChannel(), context.getStatefulClientId()))}); } /** * Called from client when a node is opened. * Used for both types of trees. * * <p> * Creates the corresponding {@link TreeNode} and its children and sends * an update command to client. * * <p> * For dispatched trees, registers the object in {@link #visibleNodes} map * and adds it in the list of parent's children (the parent must be already in map). * * @param siContext * @param fullPath - path of the node that must be opened. * If <code>null</code>, the node is considered to be the root. * @param context - context used to customize the method (e.g. filter for the children list) * * @see #openNodeInternal() * * */ @RemoteInvocation public void openNode(StatefulServiceInvocationContext context, List<PathFragment> path, Map<Object, Object> clientContext) { AuditDetails auditDetails = new AuditDetails(logger, "OPEN_NODE", path, context.getStatefulClientId()); // create structure TreeNode treeNode = openNodeInternal(context.getCommunicationChannel(), context.getStatefulClientId(), path, clientContext); updateNode( context.getCommunicationChannel(), context.getStatefulClientId(), path, treeNode, clientContext != null && clientContext.get(EXPAND_NODE_KEY) != null, false, clientContext != null && clientContext.get(SELECT_NODE_KEY) != null, true); LogUtil.audit(auditDetails); } @RemoteInvocation public boolean updateTreeStatefulContext(StatefulServiceInvocationContext context, String key, Object value) { TreeInfoClient treeInfo = new TreeInfoClient(context.getCommunicationChannel(), context.getStatefulClientId()); GenericTreeContext treeContext = treeContexts.get(treeInfo); if (treeContext == null) { treeContext = new GenericTreeContext(this); } treeContext.getStatefulContext().put(key, value); treeContexts.put(treeInfo, treeContext); return true; } }