/* 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.temp.tree.remote; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.flowerplatform.common.log.AuditDetails; import org.flowerplatform.common.log.LogUtil; import org.flowerplatform.common.util.RunnableWithParam; import org.flowerplatform.communication.channel.CommunicationChannel; import org.flowerplatform.communication.stateful_service.IStatefulClientLocalState; import org.flowerplatform.communication.stateful_service.NamedLockPool; 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.NodeInfo; import org.flowerplatform.communication.tree.NodeInfoClient; import org.flowerplatform.communication.tree.remote.GenericTreeStatefulClientLocalState; import org.flowerplatform.communication.tree.remote.PathFragment; import org.flowerplatform.communication.tree.TreeInfoClient; import org.flowerplatform.communication.tree.remote.TreeNode; /** * Service used to provide functionality for generic trees. * * <p> * There are 3 different types of trees: * <ul> * <li> non-dispatched trees (simple trees) -> only shows tree data * <li> dispatched trees -> shows and updates tree data by dispatching notifications to * a list of subscribed clients * <li> partial dispatched trees -> only a limited number of tree nodes are dispatched; * if a node is dispatched, all its upper structure must be also in dispatched mode. * </ul> * * To provide functionality for a non-dispatched tree, the following methods must be implemented: * <ul> * <li> {@link #populateTreeNode()} * <li> {@link #getPathFragmentForNode()} * <li> {@link #getNodeByPathFragment()} * <li> {@link #getChildrenForNode()} * <li> {@link #getParent()} * </ul> * * To provide functionality for a dispatched/partial dispatched tree, the following methods must be implemented * in addition to the list mentioned above: * <ul> * <li> {@link #isDispatchEnabled()()} * </ul> * Also, a service for dispatched trees is responsible to listen for label/content updates * and to call {@link #dispatchLabelUpdate()}/{@link #dispatchContentUpdate()}. * * <p> * For a dispatched tree, when its lifeline (on client side) ends, it must perform cleanup, i.e. * close the root node. E.g. in Eclipse, a listener must be put when the dialog is closing and a call to <code>closeNode(null, -1, null)</code> * must be done in order to clean the data used. <br> * In web, all services must extend <code>WebGenericTreeService</code> which already performs this requirement when a client is destroyed. * This stands for trees that are always open (e.g. Project Explorer). If you use dispatched trees with limited lifeline (e.g. in a dialog) * the same remarks for the above Eclipse example still stands. * * * <p> * For trees with inplace editing active, their services must implement the following methods: * <ul> * <li> {@link #setInplaceEditorText(List, String)} * <li> {@link #getInplaceEditorText(List)} * </ul> * * @author Cristi * @author Cristina * * */ public abstract class GenericTreeStatefulService extends StatefulService { /** * */ public static final String WHOLE_TREE_KEY = "wholeTree"; /** * */ private static final String EXPAND_NODE_KEY = "expandNode"; /** * */ private static final String SELECT_NODE_KEY = "selectNode"; /** * Adding this key to <code>context</code> will retrieve the node's * path by going up the {@link NodeInfo} hierarchy, instead of using * {@link #getParent(Object)}. Should be used when the node was deleted * to ensure that it will be removed from {@link #openNodes} map, for * example, if deleting model files. * * @see #dispatchExpandedUpdate(Object, CommunicationChannel, boolean) * @see #getPathForNode(Object, Map) * * @author Mariana * */ private static final String GO_UP_ON_NODE_INFO_KEY = "goUpOnNodeInfo"; /** * This key will be used in the getNodeByPath method. * If the context parameter will have this key set, then the node * will be search only by using the rootNodeInfo and the pathFragments * making no use for the stored tree structure * * @see #getNodeByPath(List, Map) * @see ProjectExplorerTreeService#getNodeByPath(List, Map) * */ public static final String GO_DOWN_ON_PATH_FRAGMENT_KEY = "goDownOnPathFragment"; /** * Notifications will only be dispatched to the client with {@link CommunicationChannel#getClientId()} * mapped by this key in the <code>context</code> map. * * @see #dispatchContentUpdate(Object, ClientInvocationOptions, Map) * * @author Mariana */ protected static final String DISPATCH_ONLY_FOR_CLIENT = "dispatchOnlyForClient"; /** * */ protected Map<Object, NodeInfo> openNodes = new ConcurrentHashMap<Object, NodeInfo>(); /** * Holds the tree structure of all displayed nodes on clients (opened or not). * * */ private NodeInfo rootNodeInfo; /** * Holds the contexts of all subscribed trees. */ protected Map<TreeInfoClient, GenericTreeContext> treeContexts = new ConcurrentHashMap<TreeInfoClient, GenericTreeContext>(); /** * We use this instead of normal locking, because, during subscription there is a small * time window where 2 threads subscribing for the same resource could create the {@link NodeInfo} * twice. Of course, we could have locked on the entire map, which would have solved this, but with * a big performance impact. * * @see NamedLockPool * @see #subscribe() */ protected NamedLockPool namedLockPool = new NamedLockPool(); /** * */ private static final Logger logger = LoggerFactory.getLogger(GenericTreeStatefulService.class); /////////////////////////////////////////////////////////////// // JMX Methods /////////////////////////////////////////////////////////////// /** * */ public String printNodeInfos() { StringBuffer sb = new StringBuffer(); for (NodeInfo node : openNodes.values()) { sb.append(node).append("\n"); for (NodeInfoClient client : node.getClients()) { sb.append(" ").append(client).append("\n"); } } return sb.toString(); } // public String printTreeStatefulContext(String webCommunicationChannelIdFilter, String linePrefix) { // // clean parameters // if ("".equals(webCommunicationChannelIdFilter) || "String".equals(webCommunicationChannelIdFilter)) { // webCommunicationChannelIdFilter = null; // } // if ("String".equals(linePrefix)) { // linePrefix = ""; // } // // StringBuffer sb = new StringBuffer(); // // for (TreeInfoClient tree : treeContexts.keySet()) { // // execute if no filter or if filter matches // if (webCommunicationChannelIdFilter == null || webCommunicationChannelIdFilter.equals(tree.getCommunicationChannel().getClientId())) { // GenericTreeContext treeContext = getTreeContext(tree.getCommunicationChannel(), tree.getStatefulClientId()); // sb.append(linePrefix).append(" ").append(treeContext.getStatefulContext()).append("\n"); // } // } // return sb.toString(); // } private class CommunicationChannelAndNodeInfos { private CommunicationChannel communicationChannel; private List<NodeInfo> nodeInfos = new ArrayList<NodeInfo>(); } // /** // * // */ // public String printStatefulDataPerCommunicationChannel(String webCommunicationChannelIdFilter, String linePrefix) { // // clean parameters // if ("".equals(webCommunicationChannelIdFilter) || "String".equals(webCommunicationChannelIdFilter)) { // webCommunicationChannelIdFilter = null; // } // if ("String".equals(linePrefix)) { // linePrefix = ""; // } // // StringBuffer sb = new StringBuffer(); // Map<String, CommunicationChannelAndNodeInfos> map = new HashMap<String, CommunicationChannelAndNodeInfos>(); // // // build the inverse hierarchy // for (NodeInfo node : openNodes.values()) { // for (NodeInfoClient client : node.getClients()) { // // execute if no filter or if filter matches // if (webCommunicationChannelIdFilter == null || webCommunicationChannelIdFilter.equals(client.getCommunicationChannel().getClientId())) { // // // find or create entry // CommunicationChannelAndNodeInfos entry = map.get(client.getCommunicationChannel().getClientId()); // if (entry == null) { // entry = new CommunicationChannelAndNodeInfos(); // entry.communicationChannel = client.getCommunicationChannel(); // map.put(client.getCommunicationChannel().getClientId(), entry); // } // // add node to the list // entry.nodeInfos.add(node); // } // } // } // // print // for (CommunicationChannelAndNodeInfos entry : map.values()) { // sb.append(linePrefix).append(entry.communicationChannel).append("\n"); // for (NodeInfo nodeInfo : entry.nodeInfos) { // sb.append(linePrefix).append(" ").append(nodeInfo).append("\n"); // } // } // return sb.toString(); // } public Collection<String> getStatefulClientIdsForCommunicationChannel(CommunicationChannel communicationChannel) { List<String> ids = new ArrayList<String>(); for (NodeInfo nodeInfo : openNodes.values()) { for (NodeInfoClient clientInfo : nodeInfo.getClients()) { if (clientInfo.getCommunicationChannel().equals(communicationChannel) && !ids.contains(clientInfo.getStatefulClientId(this))) { ids.add(clientInfo.getStatefulClientId(this)); } } } return ids; } /////////////////////////////////////////////////////////////// // Normal methods /////////////////////////////////////////////////////////////// /** * Should return <code>true</code> if the service provides functionality * for a dispatched tree. <br> * By default, returns <code>false</code> (functionality for simple tree). * <p> * If a node is provided, then it should return whether or not * the given node will be seen as dispatched. <br> * This is the case of a "partial dispatched tree": * some nodes are dispatched, but not all. * <p> * Note: all the above structure of a dispatched node must be in dispatched mode * (the parents must be seen as dispatched nodes) in order to work properly. * */ protected boolean isDispatchEnabled(Object node) { return false; } /** * Factory method returning the instance of tree node used. * * @see #openNode() * @see #dispatchContentUpdate() * @see #dispatchLabelUpdate() * */ protected TreeNode createTreeNode() { return new TreeNode(); } public abstract String getStatefulClientPrefixId(); /** * 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); /** * 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. * */ private void populateChildren(CommunicationChannel channel, String statefulClientId, Object node, TreeNode treeNode, GenericTreeContext context, boolean recurse) { // create and populate the children list for (Object child : getChildrenForNode(node, context)) { TreeNode childNode = createTreeNode(); populateTreeNodeInternal(child, childNode, context); treeNode.getChildren().add(childNode); childNode.setParent(treeNode); if (recurse) { // get child whole structure populateChildren(channel, statefulClientId, child, childNode, context, recurse); } // for dispatched trees, update the server tree structure if (isDispatchEnabled(child)) { addNodeInfo(channel, statefulClientId, child, false, false, context); } } } /** * Adds node information to tree structure and * updates the {@link #openNodes} map if requested. * <p> * At the end, subscribe the given channel and clientId to node. * * */ private void addNodeInfo(CommunicationChannel channel, String statefulClientId, Object node, boolean isRoot, boolean addInOpenNodes, GenericTreeContext context) { NodeInfo nodeInfo = openNodes.get(node); if (nodeInfo == null) { namedLockPool.lock(node); try { if (isRoot) { // must create the root node logger.trace("Root node never opened; adding it to structure & map"); // if root node, create one, don't add it to open nodes rootNodeInfo = new NodeInfo(); rootNodeInfo.setNode(node); nodeInfo = rootNodeInfo; openNodes.put(node, nodeInfo); } else { // not opened // get parent open node Object parent = getParent(node, context); if (parent == null) { // this shouldn't happen throw new RuntimeException("Parent node not found for node " + getLabelForLog(node)); } NodeInfo parentInfo = openNodes.get(parent); // parent must be opened if (parentInfo == null) { logger.debug("Parent info for {} was already closed!", parent); return; } // search node info in list of parent's children for (NodeInfo childInfo : parentInfo.getChildren()) { if (childInfo.getNode().equals(node)) { nodeInfo = childInfo; break; } } if (nodeInfo == null) { // not found in parent, add it logger.trace("Node {} never added in structure; adding it", getLabelForLog(node)); // create new open node entry and populate it nodeInfo = new NodeInfo(); nodeInfo.setNode(node); nodeInfo.setParent(parentInfo); nodeInfo.setPathFragment(getPathFragmentForNode(node, context)); // add node to tree parentInfo.getChildren().add(nodeInfo); } if (addInOpenNodes) { // add node to map logger.trace("Node {} added to openNodes", getLabelForLog(node)); openNodes.put(node, nodeInfo); } } } finally { namedLockPool.unlock(node); } } // add new subscribed client if necessary boolean exists = false; for (NodeInfoClient nodeClient : nodeInfo.getClients()) { if (nodeClient.getCommunicationChannel().equals(channel) && nodeClient.getStatefulClientId(this).equals(statefulClientId)) { exists = true; break; } } if (!exists) { logger.trace("Subscribing client [{} with statefulClientId={}] to node {}", new Object[] { channel, statefulClientId, getLabelForLog(node)}); // nodeInfo.addNodeInfoClient(new NodeInfoClient(channel, statefulClientId, this)); } else { logger.trace("Client [{} with statefulClientId={}] already subscribed to node {}", new Object[] { channel, statefulClientId, getLabelForLog(node)}); } } /** * Should be called by java listener for label changes. * Available only for dispatched trees. * * <p> * Verifies if the parent is opened. If <code>true</code>, * creates and populates a {@link TreeNode} with new data and sends updates to all * subscribed clients. * */ public void dispatchLabelUpdate(Object node) { NodeInfo nodeInfo = openNodes.get(node); // check if opened if (nodeInfo == null) { // not found, check if parent is opened and gets its info // check if parent is opened Object parent = getParent(node); if (parent == null) { // this shouldn't normally happen, because the root node is not visible // so a label update doesn't make sense return; } nodeInfo = openNodes.get(parent); } if (nodeInfo != null) { for (NodeInfoClient nodeClient : nodeInfo.getClients()) { dispatchLabelUpdateForClient(node, nodeClient); } } } protected void dispatchLabelUpdateForClient(Object node, NodeInfoClient nodeClient) { GenericTreeContext context = getTreeContext(nodeClient.getCommunicationChannel(), nodeClient.getStatefulClientId(this)); List<PathFragment> path = getPathForNode(node, context); TreeNode treeNode = createTreeNode(); populateTreeNodeInternal(node, treeNode, context); if (logger.isTraceEnabled()) { logger.trace("Dispatching label update for node {} to client [{} with statefulClientId={}]", new Object[] { getLabelForLog(node), nodeClient.getCommunicationChannel(), nodeClient.getStatefulClientId(this) }); } updateNode( nodeClient.getCommunicationChannel(), nodeClient.getStatefulClientId(this), path, treeNode, false, false, false, false); } /** * Should be called by java listener for content changes. * Available only for dispatched trees. * * <p> * Verifies if the node is opened. * If <code>true</code>, compares the list of new children with * the one stored in {@link NodeInfo}. <br> * The ones displayed but not found in the new list are considered to be deleted, * so a cleanup is called. <br> * The ones displayed and opened but not found in the new list will be closed. * All their children structure it will be also updated to close their opened nodes. * * <p> * Also, creates and populates a new tree node (including object children) and * sends updates to all subscribed clients. * */ public void dispatchContentUpdate(Object node, Object clientInvocationOptions) { NodeInfo nodeInfo = openNodes.get(node); if (nodeInfo == null) { // not opened, return return; } // send updates to all subscribed clients for (NodeInfoClient nodeClient : nodeInfo.getClients()) { GenericTreeContext context = getTreeContext(nodeClient.getCommunicationChannel(), nodeClient.getStatefulClientId(this)); if (context != null && context.containsKey(DISPATCH_ONLY_FOR_CLIENT)) { // if (!nodeClient.getCommunicationChannel().getClientId().equals(context.get(DISPATCH_ONLY_FOR_CLIENT))) { // continue; // don't send this update to the other clients except the client in the context map // } } dispatchContentUpdateForClient(node, nodeClient); } } protected void dispatchContentUpdateForClient(Object node, NodeInfoClient nodeClient) { GenericTreeContext context = getTreeContext(nodeClient.getCommunicationChannel(), nodeClient.getStatefulClientId(this)); NodeInfo nodeInfo = openNodes.get(node); // cleanup map if unavailable nodes Collection<?> allChildren = getChildrenForNode(node, context); List<NodeInfo> openChildren = nodeInfo.getChildren(); HashMap<Object, NodeInfo> oldNodes = new HashMap<Object, NodeInfo>(); for (NodeInfo oldChild : openChildren) { oldNodes.put(oldChild.getNode(), oldChild); } // create/populate tree TreeNode treeNode = createTreeNode(); treeNode.setChildren(new ArrayList<TreeNode>()); populateTreeNodeInternal(node, treeNode, context); // create/populate children for (Object child : allChildren) { TreeNode childNode = createTreeNode(); populateTreeNodeInternal(child, childNode, context); treeNode.getChildren().add(childNode); childNode.setParent(treeNode); oldNodes.remove(child); } // cleanup old nodes for (NodeInfo oldChild : oldNodes.values()) { cleanupAfterNodeClosed(oldChild, nodeClient.getStatefulClientId(this), nodeClient.getCommunicationChannel(), null); } List<PathFragment> path = getPathForNode(node, context); if (logger.isTraceEnabled()) { logger.trace( "Dispatching content update for node {} to client [{} with statefulClientId={}]", new Object[] { getLabelForLog(node), nodeClient.getCommunicationChannel(), nodeClient.getStatefulClientId(this) }); } updateNode(nodeClient.getCommunicationChannel(), nodeClient.getStatefulClientId(this), path, treeNode, false, false, false, true); } /** * Notifies the client to expand or collapse the node in its active tree. * * @param node node to expand/collapse * @param client * @param expandNode true to expand the node, false to collapse * @author Mariana * */ public void dispatchExpandedUpdate(Object node, CommunicationChannel client, boolean expandNode) { NodeInfo nodeInfo = openNodes.get(node); if (nodeInfo == null) { return; } for (NodeInfoClient nodeClient : nodeInfo.getClients()) { if (nodeClient.getCommunicationChannel().equals(client)) { dispatchExpandedUpdateForClient(nodeInfo.getNode(), expandNode, nodeClient); break; } } } protected void dispatchExpandedUpdateForClient(Object node, boolean expandNode, NodeInfoClient nodeClient) { GenericTreeContext treeContext = getTreeContext(nodeClient.getCommunicationChannel(), nodeClient.getStatefulClientId(this)); TreeNode treeNode = createTreeNode(); populateTreeNodeInternal(node, treeNode, treeContext); Map<Object, Object> context = new HashMap<Object, Object>(); // adding this key will ensure that the correct path will be found using the NodeInfo hierarchy context.put(GO_UP_ON_NODE_INFO_KEY, true); treeContext.setClientContext(context); List<PathFragment> path = getPathForNode(node, treeContext); updateNode( nodeClient.getCommunicationChannel(), nodeClient.getStatefulClientId(this), path, treeNode, expandNode, !expandNode, false, false); if (!expandNode) { closeNode(new StatefulServiceInvocationContext(nodeClient.getCommunicationChannel(), null, nodeClient.getStatefulClientId(this)), path, context); } } public GenericTreeContext getTreeContext(CommunicationChannel channel, String statefulClientId) { TreeInfoClient treeInfoClient = new TreeInfoClient(channel, statefulClientId); if (!treeContexts.containsKey(treeInfoClient)) { treeContexts.put(treeInfoClient, new GenericTreeContext(null)); } return treeContexts.get(treeInfoClient); } public void revealNode(StatefulServiceInvocationContext context, Object node) { if (logger.isTraceEnabled()) { logger.trace("Revealing node {} to client [{} with statefulClientId={}]", new Object[] { getLabelForLog(node), context.getCommunicationChannel(), context.getStatefulClientId() }); } invokeClientMethod( context.getCommunicationChannel(), context.getStatefulClientId(), "revealNode", new Object[] {getPathForNode(node, getTreeContext(context.getCommunicationChannel(), context.getStatefulClientId()))}); } /** * Cleans up the {@link #openNodes} and {@link #rootNodeInfo}. * <p> * Removes the given client form list of subscribed clients. <br> * After that, verifies if the node has multiple subscribed clients. * If not, deletes the node and the entry found on parent's list of children. * * <p> * This steps are done by iterating recursively on children list. * * @param channel - if <code>null</code>, the node is * considered to be deleted so it will be removed from map. * * @see #closeNode() * @see #dispatchContentUpdate() * @see #cleanupChildren() * * */ protected void cleanupAfterNodeClosed(Object node, String statefulClientId, CommunicationChannel channel, RunnableWithParam<Void, NodeInfo> removeNodeInfoRunnable) { if (logger.isTraceEnabled()) { logger.trace("Cleanup node {} to client [{} with statefulClientId={}]", new Object[] { getLabelForLog(node), channel, statefulClientId }); } if (removeNodeInfoRunnable == null) { removeNodeInfoRunnable = new RunnableWithParam<Void, NodeInfo>() { public Void run(NodeInfo nodeInfo) { logger.debug("Removing node from openNodes {}", nodeInfo); openNodes.remove(nodeInfo.getNode()); return null; } }; } NodeInfo nodeInfo = (NodeInfo) ((node instanceof NodeInfo) ? node : openNodes.get(node)); if (nodeInfo == null) { if (rootNodeInfo.getNode().equals(node)) { nodeInfo = rootNodeInfo; } else { NodeInfo parentNodeInfo = openNodes.get(getParent(node, getTreeContext(channel,statefulClientId))); for (NodeInfo child : parentNodeInfo.getChildren()) { if (node.equals(child.getNode())) { nodeInfo = child; break; } } } } if (openNodes.containsKey(nodeInfo.getNode())) { // open node for (final Iterator<NodeInfo> it = nodeInfo.getChildren().iterator(); it.hasNext();) { NodeInfo childInfo = it.next(); // NodeInfoClient childNodeInfoClient = childInfo.getNodeInfoClientByCommunicationChannelThreadSafe(channel, statefulClientId, this); // if (childNodeInfoClient != null) { // cleanupAfterNodeClosed(childInfo, statefulClientId, channel, new RunnableWithParam<Void, NodeInfo>() { // // public Void run(NodeInfo nodeInfo) { // logger.debug("Removing node from openNodes & rootNode {}", nodeInfo); // openNodes.remove(nodeInfo.getNode()); // it.remove(); // return null; // } // }); // } } } boolean removeOpenNode = false; if (channel == null) { // no channel, no open node, mark to be deleted removeOpenNode = true; } else { if (nodeInfo.equals(rootNodeInfo)) { for (Iterator<TreeInfoClient> iter = treeContexts.keySet().iterator(); iter.hasNext(); ) { TreeInfoClient treeInfoClient = iter.next(); if (treeInfoClient.getCommunicationChannel().equals(channel) && (statefulClientId == null || treeInfoClient.getStatefulClientId().equals(statefulClientId))) { // found treeContexts.remove(treeInfoClient); if (statefulClientId != null) { // only this node must be removed, so return break; } } } } // NodeInfoClient client = nodeInfo.removeNodeInfoClientByCommunicationChannel(channel, statefulClientId, this); // if (statefulClientId != null && client == null) { // a specific client wasn't found, maybe it was removed while executing this method // logger.debug("The client = {} is not subscribed to the Node Info with path = {}", channel, nodeInfo.getPathFragment()); // } if (logger.isTraceEnabled()) { logger.trace("Removing client = {} to NodeInfo with path = {}. Now there are {} clients subscribed to this resource.", new Object[] {channel, nodeInfo.getPathFragment(), nodeInfo.getClients().size() }); } if (nodeInfo.getClients().size() == 0) { // no other clients, mark to be deleted removeOpenNode = true; } } if (removeOpenNode) { namedLockPool.lock(nodeInfo.getNode()); try { if (logger.isTraceEnabled()) { logger.trace("Removing open node {} for statefulClientId={}]", new Object[] { getLabelForLog(node), statefulClientId }); } removeNodeInfoRunnable.run(nodeInfo); if (nodeInfo.equals(rootNodeInfo)) { treeContexts.clear(); } } finally { namedLockPool.unlock(nodeInfo.getNode()); } } } /** * */ public List<PathFragment> getPathForNode(Object node) { return getPathForNode(node, null); } /** * 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<?> getChildrenForNode(Object node, 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, GenericTreeContext context) { return false; } /** * Subclasses that implement this method must provide * a parent for given node. * * */ public abstract Object getParent(Object node, GenericTreeContext context); /** * Populates {@link TreeNode#getPathFragment()} with data form {@link #getPathFragmentForNode()} and * then delegates to the abstract method {@link #populateChildren()}. * * @author Cristi * */ private void populateTreeNodeInternal(Object source, TreeNode destination, GenericTreeContext context) { destination.setPathFragment(getPathFragmentForNode(source, context)); destination.setHasChildren(nodeHasChildren(source, context)); populateTreeNode(source, destination, 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 node for given {@link PathFragment}. * * */ public abstract Object getNodeByPathFragment(Object parent, PathFragment pathFragment, GenericTreeContext context); /** * Subclasses that implement this method must provide * a {@link PathFragment} for given node. * * */ public abstract PathFragment getPathFragmentForNode(Object node); /** * @author Mariana */ public PathFragment getPathFragmentForNode(Object node, GenericTreeContext context) { return getPathFragmentForNode(node); } /** * Returns an object for the given path, * including for root (i.e. <code>fullPath = null</code>. * * <p> * In the case of root, a dummy * object is accepted as well (e.g. an instance of this * service, etc.), but it shouldn't be null. * * <br> * Note: * This method must be implemented if other implementation seems to be more effective. * * */ /** * @param fullPath * @param context * @return * */ public Object getNodeByPath(List<PathFragment> fullPath, GenericTreeContext context) { NodeInfo nodeInfo; NodeInfo parentInfo; if (isDispatchEnabled(null) && rootNodeInfo != null) { // get the root node nodeInfo = rootNodeInfo; } else { // create a dummy root node nodeInfo = new NodeInfo(); nodeInfo.setParent(null); nodeInfo.setNode(getNodeByPathFragment(null, null, context)); } if (fullPath != null) { for (PathFragment pathFragment : fullPath) { // hold the parent parentInfo = nodeInfo; // this will be filled if node found nodeInfo = null; // if in the call context for this method there is no special // requirement for taking the node in real time by using only its // pathFragment and if node dispatched we first try to search in // the stored tree structure if (context != null && !context.containsKey(GO_DOWN_ON_PATH_FRAGMENT_KEY) && parentInfo != null && isDispatchEnabled(parentInfo.getNode())) { for (NodeInfo child : parentInfo.getChildren()) { if (child.getPathFragment().getName().equals(pathFragment.getName()) && child.getPathFragment().getType().equals(pathFragment.getType())) { nodeInfo = child; break; } } } // if special requirement for taking the node in real time by using only its // pathFragment or not dispatched or not found in tree structure if (nodeInfo == null) { // get node from fragment and create a dummy nodeInfo // to be used in next iteration Object node = getNodeByPathFragment(parentInfo.getNode(), pathFragment, context); // There was a problem with the given path because no node is uniquely identify by it if (node == null) return null; else { // create a dummy nodeInfo nodeInfo = new NodeInfo(); nodeInfo.setParent(parentInfo); nodeInfo.setNode(node); } } } } return nodeInfo.getNode(); } /** * Returns a path for give node by iterating recursively up through parents. * Recursive method. * * @see #getParent() * * */ public List<PathFragment> getPathForNode(Object node, GenericTreeContext context) { List<PathFragment> path = new ArrayList<PathFragment>(); // when renaming occurs, the node is destroyed and recreated in another thread // because if this, there are cases when it comes in this method as null if (node == null) { return null; } while (node != null && !node.equals(getNodeByPath(null, context))) { // search in map NodeInfo parentNodeInfo = openNodes.get(node); if (parentNodeInfo != null) { // found, get its path fragment path.add(0, parentNodeInfo.getPathFragment()); } else { // not found path.add(0, getPathFragmentForNode(node, context)); } // go up // in case the node was deleted, we won't find the correct path using the resource's parents // instead we go up using NodeInfo if (context != null && context.get(GO_UP_ON_NODE_INFO_KEY) != null) { node = parentNodeInfo.getParent().getNode(); } else { node = getParent(node, context); } } return path; } /** * */ public Object getNodeByPath(List<PathFragment> fullPath) { return getNodeByPath(fullPath, null); } public Object getParent(Object node) { return getParent(node, null); } /** * */ protected void updateNode(CommunicationChannel channel, String statefulClientId, List<PathFragment> path, TreeNode treeNode, boolean expandNode, boolean colapseNode, boolean selectNode, boolean isContentUpdate, Object clientInvocationOptions) { invokeClientMethod( channel, statefulClientId, "updateNode", new Object[] {path, treeNode, expandNode, colapseNode, selectNode}); } /** * */ public void startInplaceEditor(StatefulServiceInvocationContext context, String contributionId, List<PathFragment> nodePath, Boolean autoCreateElementAfterEditing) { invokeClientMethod( context.getCommunicationChannel(), context.getStatefulClientId(), "startInplaceEditor", new Object[] {contributionId, nodePath, autoCreateElementAfterEditing}); } /** * Creates the client tree structure and * modifies the server structure by updating {@link #rootNodeInfo} and {@link #openNodes}. * * @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 populateTreeNodeInternal(source, treeNode, treeContext); } else { treeNode.setHasChildren(true); } // for dispatched trees, update the server tree structure if (isDispatchEnabled(source)) { addNodeInfo(channel, statefulClientId, source, fullPath == null, true, treeContext); } // 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; } private 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; } /////////////////////////////////////////////////////////////// // @RemoteInvocation methods /////////////////////////////////////////////////////////////// /** * */ @RemoteInvocation public void subscribe(StatefulServiceInvocationContext context, IStatefulClientLocalState statefulClientLocalState) { logger.info("Subscribing to {} with {}", context.getStatefulClientId(), context.getCommunicationChannel()); GenericTreeStatefulClientLocalState localState = (GenericTreeStatefulClientLocalState) statefulClientLocalState; Set<String> existingNodes = new HashSet<String>(); for (List<PathFragment> path : localState.getOpenNodes()) { String fullPath = ""; for (PathFragment pathFragment : path) { if (fullPath != "") { fullPath += "/"; } fullPath += pathFragment.getName(); } existingNodes.add(fullPath); } // set tree context GenericTreeContext treeContext = getTreeContext(context.getCommunicationChannel(), context.getStatefulClientId()); treeContext.setStatefulContext(localState.getStatefulContext()); treeContext.setClientContext(localState.getClientContext()); if (existingNodes.size() > 0) { TreeNode node = openNodeInternalFromExistingNodes( context.getCommunicationChannel(), context.getStatefulClientId(), null, treeContext.getClientContext(), existingNodes); updateNode(context.getCommunicationChannel(), context.getStatefulClientId(), null, node, false, false, false, true); } } /** * */ @RemoteInvocation public void unsubscribe(StatefulServiceInvocationContext context, IStatefulClientLocalState statefulClientLocalState) { logger.info("Unsubscribing from {} with {}", context.getStatefulClientId(), context.getCommunicationChannel()); if (isDispatchEnabled(null)) { cleanupAfterNodeClosed(rootNodeInfo.getNode(), context.getStatefulClientId(), context.getCommunicationChannel(), null); } } /** * */ @RemoteInvocation public abstract String getInplaceEditorText(StatefulServiceInvocationContext context, List<PathFragment> fullPath); /** * * */ @RemoteInvocation public abstract boolean setInplaceEditorText(StatefulServiceInvocationContext context, List<PathFragment> path, String text); /** * */ @RemoteInvocation public boolean performDrop(StatefulServiceInvocationContext context, List<PathFragment> target, List<List<PathFragment>> selectedResources) { return false; } /** * 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 #openNodes} 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); } public void dispatchContentUpdate(Object node) { dispatchContentUpdate(node, null); } protected void updateNode(CommunicationChannel channel, String statefulClientId, List<PathFragment> path, TreeNode treeNode, boolean expandNode, boolean colapseNode, boolean selectNode, boolean isContentUpdate) { updateNode(channel, statefulClientId, path, treeNode, expandNode, colapseNode, selectNode, isContentUpdate, null); } /** * Called from client when a node is closed. * Used only for dispatched trees. * * <p> * Cleans the {@link #openNodes} map. * * @param context * @param path - path of the node that must be closed. * If <code>null</code>, the node is considered to be the root. * * */ @RemoteInvocation public void closeNode(StatefulServiceInvocationContext context, List<PathFragment> path, Map<Object, Object> clientContext) { GenericTreeContext treeContext = getTreeContext(context.getCommunicationChannel(), context.getStatefulClientId()); treeContext.setClientContext(clientContext); Object source = getNodeByPath(path, treeContext); if (source == null) { // something happened throw new RuntimeException("Source node not found for path " + path); } if (isDispatchEnabled(source)) { if (logger.isTraceEnabled()) { logger.trace("Closing node with path {} for client [{} with statefulClientId={}]", new Object[] { path, context.getCommunicationChannel(), context.getStatefulClientId() }); } cleanupAfterNodeClosed(source, context.getStatefulClientId(), context.getCommunicationChannel(), null); } } @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(null); } treeContext.getStatefulContext().put(key, value); treeContexts.put(treeInfo, treeContext); return true; } }