package org.nightlabs.jfire.personrelation.ui.tree; import java.util.ArrayList; import java.util.Deque; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import org.apache.log4j.Logger; import org.eclipse.jface.viewers.ISelection; import org.eclipse.swt.SWT; import org.eclipse.swt.widgets.Event; import org.eclipse.swt.widgets.Listener; import org.eclipse.swt.widgets.TreeItem; import org.nightlabs.jdo.ObjectID; import org.nightlabs.jfire.base.ui.jdo.tree.lazy.JDOLazyTreeNodesChangedEvent; import org.nightlabs.jfire.base.ui.jdo.tree.lazy.JDOLazyTreeNodesChangedListener; import org.nightlabs.jfire.personrelation.id.PersonRelationID; import org.nightlabs.jfire.prop.id.PropertySetID; /** * Puts together the series of methods that when combined, controls and manages the hierarchical expansion * of the nodes, following a pre-determined path (or paths) embedded in the {@link PersonRelationTree}. * * @author khaireel (at) nightlabs (dot) de */ public class NodalHierarchyHandler<N extends PersonRelationTreeNode, PRTree extends PersonRelationTree<N>> { private static final Logger logger = Logger.getLogger(NodalHierarchyHandler.class); private Map<Integer, Deque<ObjectID>> pathsToExpand_PRID; private Map<Integer, Deque<ObjectID>> expandedPaths_PRID; private PRTree personRelationTree = null; /** * Creates a new instance of the NodalHiearchyHandler with a reference to the {@link PersonRelationTree} whose * nodes are to be handled. */ public NodalHierarchyHandler(PRTree personRelationTree) { this.personRelationTree = personRelationTree; initAutoNodeExpansionLazyBehaviour(); } /** * Sets up the system of listeners, working with the directive-paths indicated in 'relatablePathsToRoots', from which * the tree will react: To expand to the exact node, whichever the level, of the input LegalEntity. */ protected void initAutoNodeExpansionLazyBehaviour() { // This listener is expected to unravel the tree, appropriately following the pre-identified paths in pathsToBeExpanded. // Each path contains a Deque of PropertySetID, and will guide the lazy-expansion events once data becomes available, and // becomes necessary to be displayed. personRelationTree.getTree().addListener(SWT.SetData, new Listener() { @Override public void handleEvent(Event event) { handleSWTSetDataEvent(event); } }); // This gives us a higher overall view of ALL nodes that have just been loaded than // the "personRelationTree.getTree().addListener(SWT.SetData, new Listener() {..." method above. personRelationTree.getPersonRelationTreeController().addJDOLazyTreeNodesChangedListener(new JDOLazyTreeNodesChangedListener<ObjectID, Object, N>() { @Override public void onJDOObjectsChanged(JDOLazyTreeNodesChangedEvent<ObjectID, N> changedEvent) { handleOnJDOObjectsChangedEvent(changedEvent); } }); } /** * Handles the listener concerning the notification of an SWT-related event. * Plug this into getPersonRelationTree().getTree().addListener(SWT.SetData, new Listener()... */ @SuppressWarnings("unchecked") public void handleSWTSetDataEvent(Event event) { // Guard #1. if (arePathsToBeExpandedEmpty() || !(event.item instanceof TreeItem)) return; // Guard #2. TreeItem treeItem = (TreeItem) event.item; if (treeItem == null || !(treeItem.getData() instanceof PersonRelationTreeNode)) return; // Guard #3. N node = (N) treeItem.getData(); if (node == null) return; // Guard #4. ObjectID nodeObjectID = node.getJdoObjectID(); if (nodeObjectID == null) return; // Fine-tuned, recursive version II. Deque<ObjectID> objectIDsToRoot = (LinkedList<ObjectID>) node.getJDOObjectIDsToRoot(); boolean isNodeMarkedForExpansion = false; boolean isNodeMarkedForSelection = false; for (int index : pathsToExpand_PRID.keySet()) { Deque<ObjectID> pathToExpand = pathsToExpand_PRID.get(index); Deque<ObjectID> expandedPath = expandedPaths_PRID.get(index); if (!pathToExpand.isEmpty() && pathToExpand.peekFirst().equals(nodeObjectID)) { if (logger.isDebugEnabled()) { logger.debug("*** *** *** Checking: nodeObjectID=" + PersonRelationTreeUtil.showObjectID(nodeObjectID) + " *** *** ***"); logger.debug("Checking: " + PersonRelationTreeUtil.showDequePaths("pathToExpand", pathToExpand, true)); logger.debug("Checking: " + PersonRelationTreeUtil.showDequePaths("expandedPath", expandedPath, true)); logger.debug("---"); } boolean isMatch = isMatchingSubPath(objectIDsToRoot, expandedPath, true, true); if (isMatch || expandedPath.isEmpty()) { isNodeMarkedForExpansion |= isMatch; expandedPath.push( pathToExpand.pop() ); isNodeMarkedForSelection |= pathToExpand.isEmpty(); } if (logger.isDebugEnabled()) { logger.debug("Checking: isMatch=" + (isMatch ? "T" : "F")); logger.debug("Checking: isNodeMarkedFor*Expansion*=" + (isNodeMarkedForExpansion ? "T" : "F")); logger.debug("Checking: isNodeMarkedForSelection=" + (isNodeMarkedForSelection ? "T" : "F")); logger.debug("---"); } } } // Reflect changes on the node, if any. if (isNodeMarkedForSelection) { personRelationTree.getTree().setSelection(treeItem); if (logger.isDebugEnabled()) logger.debug("----------------------------->> SELECTED!! Should no longer expand the bloody node!"); return; } if (logger.isDebugEnabled()) logger.debug("----------------------------->> ChEcK!!"); treeItem.setExpanded(isNodeMarkedForExpansion); // if (isNodeMarkedForExpansion) // treeItem.setExpanded(true); } /** * Handles the event upon the notification from a {@link JDOLazyTreeNodesChangedEvent}. * Plug this into getPersonRelationTree().getPersonRelationTreeController().addJDOLazyTreeNodesChangedListener(new JDOLazyTreeNodesChangedListener<ObjectID, Object, PersonRelationTreeNode>()... */ @SuppressWarnings("unchecked") public void handleOnJDOObjectsChangedEvent(JDOLazyTreeNodesChangedEvent<ObjectID, N> changedEvent) { // Guard #1. if (getNumberOfEmptyPaths() >= 1 || pathsToExpand_PRID == null) return; // Guard #2. List<ObjectID> nextObjectIDsOnPaths = getNextObjectIDsOnPaths(); if (nextObjectIDsOnPaths.isEmpty()) return; // Current experimental setup: // A PropertySetID in one of the pathsToExpand is not within the view, and thus was not loaded. // Find out it's index (position), with respect to its parent. List<N> loadedTreeNodes = changedEvent.getLoadedTreeNodes(); if (loadedTreeNodes != null && !loadedTreeNodes.isEmpty()) { if (logger.isDebugEnabled()) { logger.debug(" ----------------->>>> On addJDOLazyTreeNodesChangedListener: [Checking loaded nodes, " + loadedTreeNodes.size() + "]"); for (int index : pathsToExpand_PRID.keySet()) { logger.debug(" --> Checking: " + PersonRelationTreeUtil.showDequePaths("pathToExpand", pathsToExpand_PRID.get(index), true)); logger.debug(" --> Checking: " + PersonRelationTreeUtil.showDequePaths("expandedPath", expandedPaths_PRID.get(index), true)); logger.debug(" --> Checking: " + PersonRelationTreeUtil.showObjectIDs("nextObjectIDsOnPaths", nextObjectIDsOnPaths, 5)); } } int posIndex = 0; N parNode = null; for (N node : loadedTreeNodes) { if (node != null) { if (parNode == null) parNode = (N) node.getParent(); // Save parent for later use. ObjectID nodeObjID = node.getJdoObjectID(); boolean isOnNextPath = nextObjectIDsOnPaths.contains(nodeObjID); if (isOnNextPath) { if (logger.isDebugEnabled()) logger.debug(" :: @" + posIndex + ", (loaded) nodeObjID:" + PersonRelationTreeUtil.showObjectID(nodeObjID) + (isOnNextPath ? " <-- Match!" : "")); personRelationTree.setSelection(node); ISelection selection = personRelationTree.getTreeViewer().getSelection(); personRelationTree.getTreeViewer().setSelection(selection, true); break; } } else { if (logger.isDebugEnabled()) logger.debug(" :: @" + posIndex + ", node: [null]"); } posIndex++; } // If we make it through to here, then we need to force the node we want to be loaded. // But only if the parentNode is NOT null. if (parNode != null) { List<ObjectID> childrenJDOObjectIDs = parNode.getChildrenJDOObjectIDs(); long nodeCount = parNode.getChildNodeCount(); int childNodeCnt = childrenJDOObjectIDs != null ? childrenJDOObjectIDs.size() : -1; if (logger.isDebugEnabled()) { logger.debug(" -->> parNode.childNodeCount: " + nodeCount + ", childNodeCnt: " + childNodeCnt); logger.debug(" -->> " + PersonRelationTreeUtil.showObjectIDs("childrenJDOObjectIDs", childrenJDOObjectIDs, 5)); } // Locate the index of the childnode we want to force to be loaded. posIndex = 0; for (ObjectID objectID : childrenJDOObjectIDs) { if (nextObjectIDsOnPaths.contains(objectID)) { if (logger.isDebugEnabled()) { logger.debug(" -->>->> FOUND! @posIndex:" + posIndex + ", objectID:" + PersonRelationTreeUtil.showObjectID(objectID)); } // Force the child to be loaded, and duly have it selected. N node = (N) parNode.getChildNodes().get(posIndex); personRelationTree.setSelection(node); ISelection selection = personRelationTree.getTreeViewer().getSelection(); personRelationTree.getTreeViewer().setSelection(selection, true); } posIndex++; } } } } /** * @return true if and only if the given expandedPath is a proper sub-path of the given pathToRoot. */ protected boolean isMatchingSubPath(Deque<? extends ObjectID> pathToRoot, Deque<? extends ObjectID> expandedPath, boolean isReversePathToRoot, boolean isReverseExpandedPath) { if (pathToRoot.size() < expandedPath.size()) return false; Iterator<? extends ObjectID> iterToRoot = isReversePathToRoot ? pathToRoot.descendingIterator() : pathToRoot.iterator(); Iterator<? extends ObjectID> iterToExpand = isReverseExpandedPath ? expandedPath.descendingIterator() : expandedPath.iterator(); while (iterToExpand.hasNext()) { ObjectID oid_1 = iterToRoot.next(); ObjectID oid_2 = iterToExpand.next(); if (!oid_1.equals(oid_2)) return false; } return true; } /** * @return true if all the paths in pathsToExpand are empty. */ protected boolean arePathsToBeExpandedEmpty() { if (pathsToExpand_PRID != null && !pathsToExpand_PRID.isEmpty()) { for (Deque<ObjectID> path : pathsToExpand_PRID.values()) if (!path.isEmpty()) return false; } return true; } /** * A more informative method. One that checks every Deque in pathsToExpand, and counts the number of those * paths that are empty. * @return the number of empty paths in pathsToExpand. Returns 0 if pathsToExpand is null. */ protected int getNumberOfEmptyPaths() { int cnt = 0; if (pathsToExpand_PRID != null && !pathsToExpand_PRID.isEmpty()) { for (Deque<ObjectID> path : pathsToExpand_PRID.values()) if (path.isEmpty()) cnt++; } return cnt; } /** * @return the next {@link ObjectID}s on the path(s) to the root(s). */ protected List<ObjectID> getNextObjectIDsOnPaths() { List<ObjectID> nextObjIDs = new ArrayList<ObjectID>(pathsToExpand_PRID.size()); for (int index : pathsToExpand_PRID.keySet()) { Deque<ObjectID> path_PRID = pathsToExpand_PRID.get(index); if (path_PRID != null && !path_PRID.isEmpty()) nextObjIDs.add(path_PRID.peekFirst()); } return nextObjIDs; } /** * Prepares the data from 'relatablePathsToRoots', ready for use in the path-expansion manipulations. * @return the set of {@link PropertySetID}s for the root(s) of the tree. */ public Set<PropertySetID> initRelatablePathsToRoots(Map<Class<? extends ObjectID>, List<Deque<ObjectID>>> relatablePathsToRoots) { List<Deque<ObjectID>> pathsToRoot_PSID = relatablePathsToRoots.get(PropertySetID.class); // <-- mixed PropertySetID & PersonRelationID. List<Deque<ObjectID>> pathsToRoot_PRID = relatablePathsToRoots.get(PersonRelationID.class); // <-- PropertySetID only. // Initialise the path-expansion trackers. pathsToExpand_PRID = new HashMap<Integer, Deque<ObjectID>>(pathsToRoot_PRID.size()); expandedPaths_PRID = new HashMap<Integer, Deque<ObjectID>>(pathsToRoot_PRID.size()); Set<PropertySetID> rootIDs = new HashSet<PropertySetID>(); Iterator<Deque<ObjectID>> iterPaths_PSID = pathsToRoot_PSID.iterator(); Iterator<Deque<ObjectID>> iterPaths_PRID = pathsToRoot_PRID.iterator(); int index = 0; if (logger.isDebugEnabled()) logger.debug("*** *** *** initRelatablePathsToRoots() :: [PSid:" + pathsToRoot_PSID.size() + "][PRid:" + pathsToRoot_PRID.size() + "] *** *** ***"); while (iterPaths_PSID.hasNext()) { Deque<ObjectID> path_PSID = iterPaths_PSID.next(); Deque<ObjectID> path_PRID = iterPaths_PRID.next(); pathsToExpand_PRID.put(index, new LinkedList<ObjectID>(path_PRID)); // Maintain a copy. expandedPaths_PRID.put(index, new LinkedList<ObjectID>()); if (logger.isDebugEnabled()) { logger.debug("@index:" + index + " " + PersonRelationTreeUtil.showDequePaths("PSID", path_PSID, true)); logger.debug("@index:" + index + " " + PersonRelationTreeUtil.showDequePaths("PRID", path_PRID, true)); logger.debug("--------------------"); } rootIDs.add((PropertySetID) path_PSID.peekFirst()); index++; } // Done. return rootIDs; } }