/* 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.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.flowerplatform.common.log.AuditDetails; import org.flowerplatform.common.log.LogUtil; import org.flowerplatform.common.util.Pair; import org.flowerplatform.common.util.RunnableWithParam; import org.flowerplatform.communication.CommunicationPlugin; 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.IChildrenProvider; import org.flowerplatform.communication.tree.INodeByPathRetriever; import org.flowerplatform.communication.tree.INodeDataProvider; import org.flowerplatform.communication.tree.NodeInfo; import org.flowerplatform.communication.tree.NodeInfoClient; import org.flowerplatform.communication.tree.TreeInfoClient; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Service used to provide functionality for generic dispatched trees -> shows and updates tree data by dispatching notifications to * a list of subscribed clients. * * 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 AbstractTreeStatefulService implements INodeInfoStatefulServiceMXBean, IChildrenProvider, INodeDataProvider, INodeByPathRetriever { public final static Object ROOT_NODE_MARKER = GenericTreeStatefulService.class; /** * NO USED * 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 #visibleNodes} 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"; /** * 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> visibleNodes = new ConcurrentHashMap<Object, NodeInfo>(); /** * Holds the tree structure of all displayed nodes on clients (opened or not). * * */ private NodeInfo rootNodeInfo; /** * 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 : visibleNodes.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().getId())) { 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 : visibleNodes.values()) { for (NodeInfoClient client : node.getClients()) { // execute if no filter or if filter matches if (webCommunicationChannelIdFilter == null || webCommunicationChannelIdFilter.equals(client.getCommunicationChannel().getId())) { // find or create entry CommunicationChannelAndNodeInfos entry = map.get(client.getCommunicationChannel().getId()); if (entry == null) { entry = new CommunicationChannelAndNodeInfos(); entry.communicationChannel = client.getCommunicationChannel(); map.put((String) client.getCommunicationChannel().getId(), 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 : visibleNodes.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 /////////////////////////////////////////////////////////////// public GenericTreeStatefulService() { super(); rootNodeInfo = new NodeInfo(); rootNodeInfo.setNode(ROOT_NODE_MARKER); visibleNodes.put(ROOT_NODE_MARKER, rootNodeInfo); } public Map<Object, NodeInfo> getVisibleNodes() { return visibleNodes; } /** * 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; // get the root node nodeInfo = rootNodeInfo; if (fullPath != null) { for (PathFragment pathFragment : fullPath) { // hold the parent parentInfo = nodeInfo; // this will be filled if node found nodeInfo = null; for (NodeInfo child : parentInfo.getChildren()) { if (child.getPathFragment().getName().equals(pathFragment.getName()) && child.getPathFragment().getType().equals(pathFragment.getType())) { nodeInfo = child; break; } } if (nodeInfo == null) { return null; } } } return nodeInfo.getNode(); } /** * */ @RemoteInvocation public abstract String getInplaceEditorText(StatefulServiceInvocationContext context, List<PathFragment> fullPath); /** * * */ @RemoteInvocation public abstract boolean setInplaceEditorText(StatefulServiceInvocationContext context, List<PathFragment> path, String text); /** * Adds NodeInfo in map + in the parent's NodeInfo.children. * * <p> * At the end, subscribe the given channel and clientId to node. * * */ private void addNodeInfo(CommunicationChannel channel, String statefulClientId, Object parent, Object node, String nodeType, GenericTreeContext context) { boolean updateMap = true; if (context.get(DONT_UPDATE_MAP_KEY) != null) { updateMap = !((Boolean) context.get(DONT_UPDATE_MAP_KEY)).booleanValue(); } if (!updateMap) { return; } NodeInfo nodeInfo = visibleNodes.get(node); if (nodeInfo == null) { // the node was not yet visible to the client namedLockPool.lock(node); try { // get parent open node NodeInfo parentInfo = visibleNodes.get(parent); // parent must be opened if (parentInfo == null) { logger.error("Parent info for {} was already closed!", parent); return; } // create new open node entry and populate it nodeInfo = new NodeInfo(); nodeInfo.setNode(node); nodeInfo.setParent(parentInfo); nodeInfo.setPathFragment(getPathFragmentForNode(node, nodeType, context)); parentInfo.getChildren().add(nodeInfo); visibleNodes.put(node, nodeInfo); logger.trace("Node {} added to openNodes", getLabelForLog(node, nodeType)); } 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, nodeType)}); nodeInfo.addNodeInfoClient(new NodeInfoClient(channel, statefulClientId, this)); } else { logger.trace("Client [{} with statefulClientId={}] already subscribed to node {}", new Object[] { channel, statefulClientId, getLabelForLog(node, nodeType)}); } } /** * 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, String nodeType) { NodeInfo nodeInfo = visibleNodes.get(node); // check if opened if (nodeInfo != null) { for (NodeInfoClient nodeClient : nodeInfo.getClients()) { dispatchLabelUpdateForClient(node, nodeInfo, nodeClient); } } } protected void dispatchLabelUpdateForClient(Object node, NodeInfo nodeInfo, NodeInfoClient nodeClient) { GenericTreeContext context = getTreeContext(nodeClient.getCommunicationChannel(), nodeClient.getStatefulClientId(this)); List<PathFragment> path = getPathForNode(node, nodeInfo.getPathFragment().getType(), context); TreeNode treeNode = createTreeNode(); treeNode.setPathFragment(path.get(path.size() - 1)); populateTreeNodeInternal(node, treeNode, context); if (logger.isTraceEnabled()) { logger.trace("Dispatching label update for node {} to client [{} with statefulClientId={}]", new Object[] { getLabelForLog(node, nodeInfo.getPathFragment().getType()), 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) { NodeInfo nodeInfo = visibleNodes.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().getId().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 = visibleNodes.get(node); // cleanup map if unavailable nodes 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.setPathFragment(nodeInfo.getPathFragment()); treeNode.setChildren(new ArrayList<TreeNode>()); populateTreeNodeInternal(node, treeNode, context); // create/populate children 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); oldNodes.remove(child); } // cleanup old nodes for (NodeInfo oldChild : oldNodes.values()) { cleanupAfterNodeClosed(oldChild, oldChild.getPathFragment().getType(), nodeClient.getStatefulClientId(this), nodeClient.getCommunicationChannel(), null, true); } List<PathFragment> path = getPathForNode(node, nodeInfo.getPathFragment().getType(), context); if (logger.isTraceEnabled()) { logger.trace( "Dispatching content update for node {} to client [{} with statefulClientId={}]", new Object[] { getLabelForLog(node, nodeInfo.getPathFragment().getType()), 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 = visibleNodes.get(node); if (nodeInfo == null) { return; } for (NodeInfoClient nodeClient : nodeInfo.getClients()) { if (nodeClient.getCommunicationChannel().equals(client)) { dispatchExpandedUpdateForClient(nodeInfo.getNode(), expandNode, nodeInfo, nodeClient); break; } } } protected void dispatchExpandedUpdateForClient(Object node, boolean expandNode, NodeInfo nodeInfo, NodeInfoClient nodeClient) { GenericTreeContext treeContext = getTreeContext(nodeClient.getCommunicationChannel(), nodeClient.getStatefulClientId(this)); List<PathFragment> path = getPathForNode(node, nodeInfo.getPathFragment().getType(), treeContext); TreeNode treeNode = createTreeNode(); treeNode.setPathFragment(path.get(path.size() - 1)); 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 NodeInfo2 hierarchy context.put(GO_UP_ON_NODE_INFO_KEY, true); treeContext.setClientContext(context); 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); } } /** * Cleans up the {@link #visibleNodes} 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 nodeType, String statefulClientId, CommunicationChannel channel, RunnableWithParam<Void, NodeInfo> removeNodeInfoRunnable, final boolean removeFromMapAndParentAndUnsubscribe) { if (logger.isTraceEnabled()) { logger.trace("Cleanup node {} to client [{} with statefulClientId={}]", new Object[] { getLabelForLog(node, nodeType), channel, statefulClientId }); } NodeInfo nodeInfo = (NodeInfo) ((node instanceof NodeInfo) ? node : visibleNodes.get(node)); if (nodeInfo == null) { if (rootNodeInfo.getNode().equals(node)) { nodeInfo = rootNodeInfo; } else { // CS: cf. remarca mm return; // NodeInfo2 parentNodeInfo = openNodes.get(getParent(node, getTreeContext(channel,statefulClientId))); // for (NodeInfo2 child : parentNodeInfo.getChildren()) { // if (node.equals(child.getNode())) { // nodeInfo = child; // break; // } // } } } if (visibleNodes.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, childInfo.getPathFragment().getType(), statefulClientId, channel, new RunnableWithParam<Void, NodeInfo>() { public Void run(NodeInfo nodeInfo) { logger.debug("Removing node from visibleNodes & parent node {}", nodeInfo); visibleNodes.remove(nodeInfo.getNode()); it.remove(); return null; } }, true); } } } boolean removeOpenNode = false; if (channel == null) { // no channel, no open node, mark to be deleted removeOpenNode = true; } else if (removeFromMapAndParentAndUnsubscribe) { 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 NodeInfo2 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 && nodeInfo != rootNodeInfo) { // don't delete the rootNodeInfo namedLockPool.lock(nodeInfo.getNode()); try { if (logger.isTraceEnabled()) { logger.trace("Removing open node {} for statefulClientId={}]", new Object[] { getLabelForLog(node, nodeType), statefulClientId }); } if (removeNodeInfoRunnable == null) { removeNodeInfoRunnable = new RunnableWithParam<Void, NodeInfo>() { public Void run(NodeInfo nodeInfo) { logger.debug("Removing node from openNodes {}", nodeInfo); // remove from parent nodeInfo.getParent().getChildren().remove(nodeInfo); // remove from map visibleNodes.remove(nodeInfo.getNode()); return null; } }; } removeNodeInfoRunnable.run(nodeInfo); if (nodeInfo.equals(rootNodeInfo)) { treeContexts.clear(); } } finally { namedLockPool.unlock(nodeInfo.getNode()); } } } /** * 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; addNodeInfo(channel, statefulClientId, node, child, pair.b, context); 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); } } } /** * Returns a path for give node by iterating recursively up through parents. * Recursive method. * * @see #getParent() * * */ public List<PathFragment> getPathForNode(Object node, String nodeType, GenericTreeContext context) { List<PathFragment> path = new ArrayList<PathFragment>(); NodeInfo nodeInfo = visibleNodes.get(node); while (nodeInfo != null && !nodeInfo.equals(rootNodeInfo)) { path.add(0, nodeInfo.getPathFragment()); nodeInfo = nodeInfo.getParent(); } return path; } /** * */ public void startInplaceEditor(StatefulServiceInvocationContext context, String contributionId, List<PathFragment> nodePath, Boolean autoCreateElementAfterEditing) { invokeClientMethod( context.getCommunicationChannel(), context.getStatefulClientId(), "startInplaceEditor", new Object[] {contributionId, nodePath, autoCreateElementAfterEditing}); } public static Object getNodeByPathFor(List<PathFragment> path, GenericTreeContext context) { GenericTreeStatefulService service = getServiceFromPathWithRoot(path); if (service != null) { List<PathFragment> pathWithoutRootFragment = path.subList(1, path.size()); return service.getNodeByPath(pathWithoutRootFragment, context); } return null; } public static GenericTreeStatefulService getServiceFromPathWithRoot(List<PathFragment> path) { PathFragment firstNodePath = path.get(0); if (NODE_TYPE_ROOT.equals(firstNodePath.getType())) { String[] informations = firstNodePath.getName().split("\\|"); return (GenericTreeStatefulService) CommunicationPlugin.getInstance().getServiceRegistry().getService(informations[0]); } return null; } public static String getClientIDFromPathWithRoot(List<PathFragment> path) { PathFragment firstNodePath = path.get(0); if (NODE_TYPE_ROOT.equals(firstNodePath.getType())) { String[] informations = firstNodePath.getName().split("\\|"); return informations[1]; } return null; } /////////////////////////////////////////////////////////////// // @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()); cleanupAfterNodeClosed(rootNodeInfo.getNode(), null, context.getStatefulClientId(), context.getCommunicationChannel(), null, true); } /** * */ @RemoteInvocation public boolean performDrop(StatefulServiceInvocationContext context, List<PathFragment> target, List<List<PathFragment>> selectedResources) { return false; } /** * Called from client when a node is closed. * Used only for dispatched trees. * * <p> * Cleans the {@link #visibleNodes} 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 (logger.isTraceEnabled()) { logger.trace("Closing node with path {} for client [{} with statefulClientId={}]", new Object[] { path, context.getCommunicationChannel(), context.getStatefulClientId() }); } String nodeType = null; if (path != null && path.size() > 0) { nodeType = path.get(path.size() - 1).getType(); } cleanupAfterNodeClosed(source, nodeType, context.getStatefulClientId(), context.getCommunicationChannel(), null, false); } }