package org.nightlabs.jfire.personrelation.trade.ui.tucked.compact; import java.util.ArrayList; import java.util.Collection; 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 javax.jdo.JDOHelper; import org.apache.log4j.Logger; import org.nightlabs.jdo.ObjectID; import org.nightlabs.jfire.personrelation.PersonRelationManagerRemote.TuckedQueryCount; import org.nightlabs.jfire.personrelation.dao.PersonRelationDAO; import org.nightlabs.jfire.personrelation.id.PersonRelationID; import org.nightlabs.jfire.personrelation.trade.ui.tucked.TuckedNodeStatus; import org.nightlabs.jfire.personrelation.trade.ui.tucked.TuckedPersonRelationTreeController; import org.nightlabs.jfire.personrelation.trade.ui.tucked.TuckedPersonRelationTreeNode; import org.nightlabs.jfire.personrelation.ui.tree.PersonRelationTreeController; import org.nightlabs.jfire.personrelation.ui.tree.PersonRelationTreeUtil; import org.nightlabs.jfire.prop.id.PropertySetID; import org.nightlabs.progress.ProgressMonitor; import org.nightlabs.util.CollectionUtil; /** * Another specialised {@link PersonRelationTreeController}. This time tailored to handle the more complicated {@link CompactedPersonRelationTree} * and its {@link CompactedPersonRelationTreeNode}s. * * ASSUMPTIONS to be LIFTED: * 1. All roots provided in the tuckedPath are unique; i.e. no forking paths allowed. * * * ON LIFTING the above assumptions: * 1. Only on initiation: When determining which of the paths to instantiate, paths with same ancestor roots (or sub-paths to roots) * can be checked against the known nodes that are already created. In other words, these instantiated root nodes will already have * within them one of the (multiple, forking) tucked-paths. And so we select the one that has not previously used. * * @author khaireel at nightlabs dot de */ public class CompactedPersonRelationTreeController extends PersonRelationTreeController<CompactedPersonRelationTreeNode> { private static final Logger logger = Logger.getLogger(CompactedPersonRelationTreeController.class); private int sequenceCtr = 0; // Maintain some sort of synchronicity between the two paths; and one that will provide constant time reference in // retrieving relatable information between a PRID and its corresponding PSID (and vice-versa). private Map<Integer, Deque<ObjectID>> tuckedPRIDPathMaps = null; // <-- mixed PropertySetID & PersonRelationID. private Map<Integer, Deque<ObjectID>> tuckedPSIDPathMaps = null; // <-- PropertySetID only. /** * Set the controller's internal information manipulator for the original tuckedPaths. * @return the {@link PropertySetID}s of the roots of all the known tucked-paths from this controller. * @see PersonRelationDAO.getRelationRootNodes(). */ public Set<PropertySetID> setTuckedPaths(Map<Class<? extends ObjectID>, List<Deque<ObjectID>>> tuckedPaths) { List<Deque<ObjectID>> pathsToRoot_PRID = tuckedPaths.get(PersonRelationID.class); // <-- mixed PropertySetID & PersonRelationID. List<Deque<ObjectID>> pathsToRoot_PSID = tuckedPaths.get(PropertySetID.class); // <-- PropertySetID only. // Initialise the path-expansion trackers. tuckedPRIDPathMaps = new HashMap<Integer, Deque<ObjectID>>(pathsToRoot_PRID.size()); tuckedPSIDPathMaps = new HashMap<Integer, Deque<ObjectID>>(pathsToRoot_PRID.size()); Iterator<Deque<ObjectID>> iterPaths_PRID = pathsToRoot_PRID.iterator(); Iterator<Deque<ObjectID>> iterPaths_PSID = pathsToRoot_PSID.iterator(); int index = 0; while (iterPaths_PSID.hasNext()) { tuckedPRIDPathMaps.put(index, iterPaths_PRID.next()); tuckedPSIDPathMaps.put(index, iterPaths_PSID.next()); index++; } if (logger.isDebugEnabled()) { logger.debug("=============================================================================================="); logger.debug("@setTuckedPaths :: paths received = " + index); logger.debug("=============================================================================================="); for(int i=0; i<index; i++) logger.debug(PersonRelationTreeUtil.showObjectIDs("PSID-path @ref=" + i, tuckedPSIDPathMaps.get(i), 10)); logger.debug(".............................................................................................."); for(int i=0; i<index; i++) logger.debug(PersonRelationTreeUtil.showObjectIDs("PRID-path @ref=" + i, tuckedPRIDPathMaps.get(i), 10)); logger.debug("=============================================================================================="); } return getTuckedPathsRootIDs(); } /** * @return the {@link PropertySetID}s of the roots of all the known tucked-paths from this controller. */ protected Set<PropertySetID> getTuckedPathsRootIDs() { if (tuckedPRIDPathMaps == null) return null; Set<PropertySetID> rootIDs = new HashSet<PropertySetID>(tuckedPSIDPathMaps.size()); for (Deque<ObjectID> tuckedPSIDPath : tuckedPSIDPathMaps.values()) rootIDs.add((PropertySetID) tuckedPSIDPath.peekFirst()); return rootIDs; // Consider having this somehow sorted? } /** * @return the tucked-path of {@link PropertySetID}s, in which the {@link ObjectID} represented in the given node is within * the corresponding tucked-path of {@link ObjectID}s. Returns null otherwise. */ protected Deque<ObjectID> getTuckedPath(CompactedPersonRelationTreeNode node) { List<Integer> indexReferences = getPathIndexReferences(node); if (!indexReferences.isEmpty()) return tuckedPSIDPathMaps.get(indexReferences.get(0)); // TODO See ASSUMPTION 1. Also see notes on how to lift ASSUMPTION 1 cleanly. return null; } /** * @return the {@link TuckedPathDosier} in which the {@link ObjectID} represented in the given node is within * the returned tucked-path. Returns null otherwise. */ protected TuckedPathDosier getTuckedPathDosier(CompactedPersonRelationTreeNode node) { List<Integer> indexReferences = getPathIndexReferences(node); if (!indexReferences.isEmpty()) { TuckedPathDosier tpDosier = new TuckedPathDosier(); tpDosier.controllerIndexRef = indexReferences.get(0); // TODO See ASSUMPTION 1. Also see notes on how to lift ASSUMPTION 1 cleanly. tpDosier.pathPSID = CollectionUtil.collection2TypedArray(tuckedPSIDPathMaps.get(tpDosier.controllerIndexRef), ObjectID.class); tpDosier.pathPRID = CollectionUtil.collection2TypedArray(tuckedPRIDPathMaps.get(tpDosier.controllerIndexRef), ObjectID.class); return tpDosier; } return null; } // ------------------------------------------------------------------------------------- ++ ------------------------------->> // [Section] Tucked-path management // ------------------------------------------------------------------------------------- ++ ------------------------------->> // These manage the the correlated correspondence between the tuckedPSIDPaths and tuckedPRIDPaths. // Notes on the multiple forked paths: // Let P1, P2, ..., Pn be the n unique paths in PSIDPaths. // Let P' \subset PSIDPaths so that P' is the set of unique paths containing an element e. // Then the following is true: // (i) Each subPath from the element e to the root, for all paths in P', is unique; and // (ii) Each subPath from the element e to the final-child, for all paths in P', is the same. // // This means that we can correctly identify the correct path P \elementOf PSIDPaths, given an element e, if and only if we // we have the subPath from the element e to the root. /** * @return the reference indexes to all the paths containing the mathing subPath-to-the-root based on the given tuckedNode. * Returns an empty list if no reference(s) to the path can be found. */ private List<Integer> getPathIndexReferences(CompactedPersonRelationTreeNode node) { LinkedList<Integer> indexRefs = new LinkedList<Integer>(); // For the correct identification of the path's index, the given tuckedNode must have already been duly and correctly initialised. LinkedList<ObjectID> objectIDsToRoot = (LinkedList<ObjectID>) node.getJDOObjectIDsToRoot(); // The root reference is at the beginning of this List. for (int index = 0; index < tuckedPRIDPathMaps.size(); index++) { Deque<ObjectID> tuckedPRIDPath = tuckedPRIDPathMaps.get(index); // [Note. B.] The root reference is at the END of this Deque. Yeah, I know, how stupidly annoying... Kai :/ Iterator<ObjectID> iterPRIDPath = tuckedPRIDPath.iterator(); // This loop terminates on three conditions: // 1. CLEANLY. When both paths we are comparing are the same length, and have exactly the same elements. // 2. CLEANLY. When objectIDsToRoot is exactly a subPath of iterPRIDPath. --> In which case, we simply keep the index value at where we stopped. // 3. When we find disequality between elements. boolean isSubPath = true; for(Iterator<ObjectID> iterObjectID = objectIDsToRoot.descendingIterator(); iterObjectID.hasNext(); ) { // See reason in [Note. B.] above. ObjectID objectID = iterObjectID.next(); if (!iterPRIDPath.hasNext()) // [Case 2.] If we survive up till here, which means we have exhausted the elements in objectIDsToRoot, which means we have found the correct index reference. break; if (!iterPRIDPath.next().equals(objectID)) { // [Case 3.] isSubPath = false; break; } } if (isSubPath) // [Case 1.] indexRefs.add(index); } return indexRefs; } /** * @return the next {@link PropertySetID}s on the related tuckedPSIDpath, based on the {@link ObjectID} from the given compactedTuckedNode. * Returns null, if no match is found. */ private Set<PropertySetID> getNextRelatedPropertySetIDOnPath(CompactedPersonRelationTreeNode compactedTuckedNode) { List<Integer> indexes = getPathIndexReferences(compactedTuckedNode); if (indexes.isEmpty()) return null; Set<PropertySetID> propertySetIDs = null; for (Integer index : indexes) { Deque<ObjectID> tuckedPRIDPath = tuckedPRIDPathMaps.get(index); Deque<ObjectID> tuckedPSIDPath = tuckedPSIDPathMaps.get(index); Iterator<ObjectID> iterPRID = tuckedPRIDPath.iterator(); Iterator<ObjectID> iterPSID = tuckedPSIDPath.iterator(); while (iterPRID.hasNext()) { iterPSID.next(); if (iterPRID.next().equals(compactedTuckedNode.getJdoObjectID()) && iterPSID.hasNext()) { // return (PropertySetID) (iterPSID.hasNext() ? iterPSID.next() : null); // <-- Original: Handles the singular form. if (propertySetIDs == null) propertySetIDs = new HashSet<PropertySetID>(); propertySetIDs.add((PropertySetID) iterPSID.next()); break; } } } return propertySetIDs; } /** * @return the next {@link ObjectID}s on the related tuckedPRIDPath, based on the {@link ObjectID} from the given compactedTuckedNode. * Returns null, if not match is found. */ private Set<ObjectID> getNextObjectIDOnPath(CompactedPersonRelationTreeNode compactedTuckedNode) { List<Integer> indexes = getPathIndexReferences(compactedTuckedNode); if (indexes.isEmpty()) return null; Set<ObjectID> objectIDs = null; for (Integer index : indexes) { Deque<ObjectID> tuckedPRIDPath = tuckedPRIDPathMaps.get(index); Iterator<ObjectID> iterPRID = tuckedPRIDPath.iterator(); while (iterPRID.hasNext()) { if (iterPRID.next().equals(compactedTuckedNode.getJdoObjectID()) && iterPRID.hasNext()) { // return iterPRID.hasNext() ? iterPRID.next() : null; // <-- Original: Handles the 'singular' form. if (objectIDs == null) objectIDs = new HashSet<ObjectID>(); objectIDs.add(iterPRID.next()); break; } } } return objectIDs; } /** * @return the corresponding {@link PropertySetID}, based on the {@link ObjectID} from the given compactedTuckedNode. * Returns null, if no match is found. */ private PropertySetID getCorrespondingPSID(CompactedPersonRelationTreeNode compactedTuckedNode) { List<Integer> indexes = getPathIndexReferences(compactedTuckedNode); if (indexes.isEmpty()) return null; Deque<ObjectID> tuckedPRIDPath = tuckedPRIDPathMaps.get(indexes.get(0)); Deque<ObjectID> tuckedPSIDPath = tuckedPSIDPathMaps.get(indexes.get(0)); Iterator<ObjectID> iterPRID = tuckedPRIDPath.iterator(); Iterator<ObjectID> iterPSID = tuckedPSIDPath.iterator(); while (iterPRID.hasNext()) { ObjectID psID = iterPSID.next(); if (iterPRID.next().equals(compactedTuckedNode.getJdoObjectID())) return (PropertySetID) psID; } return null; } // ------------------------------------------------------------------------------------- ++ ------------------------------->> // [Section] Preparation of the related CompactedPersonRelationTreeNode. // ------------------------------------------------------------------------------------- ++ ------------------------------->> @Override protected CompactedPersonRelationTreeNode createNode() { CompactedPersonRelationTreeNode node = new CompactedPersonRelationTreeNode(); logger.debug(" ~~~ sequenceCtr: " + (sequenceCtr++) + " ~~~ ********* @createNode() :: meNodeID = " + node.meNodeID); return node; // return new CompactedPersonRelationTreeNode(); } // ------------------------------------------------------------------------------------- ++ ------------------------------->> // [Section] Handles the retrieval of information, based on specific compacted-tucked situations. // ------------------------------------------------------------------------------------- ++ ------------------------------->> /** * Consolidated. Idea from original {@link TuckedPersonRelationTreeController}. * Given a proper {@link TuckedPersonRelationTreeNode}, we check to see if the {@link ObjectID} it carries lie on the * known tuckedPath. If so, we retrieve the childCounts for both actual and tucked. This, however, shall NOT set the * status of the tuckedNode. * @return true if the given tuckedNode has had both its childCounts updated for a nodeStatus other than NORMAL. */ protected boolean updateTuckedNodeChildCounts(CompactedPersonRelationTreeNode compactedTuckedNode, ProgressMonitor monitor) { Set<PropertySetID> propertySetIDsToRoot = CollectionUtil.createHashSetFromCollection(compactedTuckedNode.getPropertySetIDsToRoot()); // <-- We can use this here, no problems! See notes to figure out why, or simply ask Kai. PropertySetID nodePropertySetID = compactedTuckedNode.getPropertySetID(); if (nodePropertySetID == null) // <-- It is possible that parentNode.getPropertySetID() returns null. But at this point, we know that the next element on the tuckedPath exists! nodePropertySetID = getCorrespondingPSID(compactedTuckedNode); // [Note. A.] When our tuckedPath ends, we should get nextIDOnPath == null. In this case, the personIDs we come across should not be affect // with our tucked-node methodologies. We gather these IDs and then later on relegate them back to the super class's method. Set<PropertySetID> nextIDsOnPath = getNextRelatedPropertySetIDOnPath(compactedTuckedNode); TuckedQueryCount tqCount = PersonRelationDAO.sharedInstance().getTuckedPersonRelationCount( null, nodePropertySetID, propertySetIDsToRoot, nextIDsOnPath, monitor); compactedTuckedNode.setActualChildCount(tqCount.actualChildCount); compactedTuckedNode.setTuckedChildCount(tqCount.tuckedChildCount); // Special NORMAL case. if (nextIDsOnPath == null) { // In our implementation, this would mean that the compactedTuckedNode in the parameter is a NORMAL node. // So we simply take the value of the actualChildCount, and take that as the node's official childCount. if (logger.isDebugEnabled()) { logger.debug(" !!! ~~~ @updateTuckedNodeChildCounts, where nextIDsOnPath == null: " + PersonRelationTreeUtil.showQuickNodeInfo(compactedTuckedNode) + ", meNodeID=" + compactedTuckedNode.meNodeID); logger.debug(PersonRelationTreeUtil.showObjectIDs(" !!! ~~~ propertySetIDsToRoot", propertySetIDsToRoot, 10)); logger.debug(" !!! ~~~ actualChildCount: " + tqCount.actualChildCount + ", tuckedChildCount: " + tqCount.tuckedChildCount); } compactedTuckedNode.setChildNodeCount(tqCount.actualChildCount); return false; } return true; } @Override protected Set<CompactedPersonRelationTreeNode> fillUpNodeCounts (Map<ObjectID, Long> parentObjectID2NodeCount, Map<ObjectID, List<CompactedPersonRelationTreeNode>> parentObjectID2ParentTreeNodeList, ProgressMonitor monitor) { // Note: This method should return those nodes that needs to be refreshed. // When filling up the node's childCount, we have to be careful whenever we encounter COLLECTIVE nodes. if (logger.isDebugEnabled()) { logger.debug(" ~~~ sequenceCtr: " + (sequenceCtr++) + " ~~~ ********* @fillUpNodeCounts() ### ******* " + PersonRelationTreeUtil.showObjectIDs("NodeOIDs", parentObjectID2NodeCount.keySet(), 10)); } Set<CompactedPersonRelationTreeNode> nodesToBeRefreshed = super.fillUpNodeCounts(parentObjectID2NodeCount, parentObjectID2ParentTreeNodeList, monitor); for (CompactedPersonRelationTreeNode treeNode : nodesToBeRefreshed) { // Check to see if the treeNode is a COLLECTIVE node. If so, we amend its childCounts appropriately through the data // that should already be available in its node-representative. CompactedPersonRelationTreeNode nodeRepresentative = treeNode.getNodeRepresentative(); if (nodeRepresentative != null) { treeNode.setChildNodeCount(nodeRepresentative.getChildNodeCount()); treeNode.setNodeStatus(nodeRepresentative.getNodeStatus()); if (logger.isDebugEnabled()) logger.debug(" ### ##### FOUND a COLLECTIVE node while updating child-counts. Node identified as COLLECTIVE: meNodeID = " + treeNode.meNodeID + ", nodeRepresentative.meNodeID = " + nodeRepresentative.meNodeID); } } return nodesToBeRefreshed; } @Override protected Collection<Object> retrieveJDOObjectsByPropertySetIDs(Collection<Object> result, Set<PropertySetID> personIDs, ProgressMonitor monitor, int tix) { if (logger.isDebugEnabled()) { logger.debug(" ~~~ sequenceCtr: " + (sequenceCtr++) + " ~~~ ********* @retrieveJDOObjectsByPropertySetIDs() -- [I]"); logger.debug(" ### ######################## Called! ### :: retrieveJDOObjectsByPropertySetIDs()"); logger.debug(PersonRelationTreeUtil.showObjectIDs(" ### @retrieveJDOObjectsByPropertySetIDs: personIDs", personIDs, 10)); } Collection<Object> results = super.retrieveJDOObjectsByPropertySetIDs(result, personIDs, monitor, tix); // Note: This is where we do the FIRST initialisation of the root CompactedPersonRelationTreeNodes. // i.e. All compacted-tucked root nodes first appearance, should be loaded through here. // // If the above note is true, then we WILL be able to have access to COLLECTIVE shell of the CompactedPersonRelationTreeNodes, // based on the personID. This shell MUST be empty, an completely uninitialised. for (PropertySetID personID : personIDs) { List<CompactedPersonRelationTreeNode> treeNodeList = getTreeNodeList(personID); // If there are multiple elements in the returned list, then this means that the COLLECTIVE shell we were after // has already been initialised, and thus, if this happens, then we skip it. if (treeNodeList != null && treeNodeList.size() == 1) { CompactedPersonRelationTreeNode rootShellNode = treeNodeList.get(0); // Also, if we now check its nodeStatus, it should tell us that it is UNSET. if (logger.isDebugEnabled()) { logger.debug(" ~~~ sequenceCtr: " + (sequenceCtr++) + " ~~~ ********* @retrieveJDOObjectsByPropertySetIDs() -- [II]"); logger.debug(" ### Found SHELL node @personID: " + PersonRelationTreeUtil.showObjectID(personID)); logger.debug(" ### Before init: " + rootShellNode.toDebugString()); } // So, now we go about our business of setting up the UNSET node. // A. Handle its internal nodes that are part of the tucked-path. These are already arranged in order of path traversal from root to the end-child element. int pathLen = rootShellNode.tuckedPathDosier.pathPSID.length; Set<PersonRelationID> personRelationIDsInPathToLoad = new HashSet<PersonRelationID>(pathLen-1); for (int i=0; i<pathLen; i++) { // B. Create a new node to represent its related JDOObjectID, an initialise it as we know it up until now. CompactedPersonRelationTreeNode tuckedNodeInPath = createNode(); tuckedNodeInPath.setActiveJDOObjectLazyTreeController(CompactedPersonRelationTreeController.this); tuckedNodeInPath.setPropertySetID((PropertySetID) rootShellNode.tuckedPathDosier.pathPSID[i]); tuckedNodeInPath.setJdoObjectID(rootShellNode.tuckedPathDosier.pathPRID[i]); tuckedNodeInPath.setNodeStatus(i < pathLen-1 ? TuckedNodeStatus.TUCKED: TuckedNodeStatus.NORMAL); // C. Set the new tuckedNodeInPath into rootShellNode. The following will take care of the parentage of the node. rootShellNode.setTuckedNodeInPath(tuckedNodeInPath); // D. We should let the framework of the super class to take care of filling up the reference into the node, // whenever there are future events that may cause the information within the node to change. addTreeNode(tuckedNodeInPath); // E. For all other node except the root, we assemble their JDOObjectIDs and load the JDOObjects appropriately. // i.e. In this case, (i) the JDOObjectID = PersonRelationID, and // (ii) the JDOObject = PersonRelation. if (i > 0) personRelationIDsInPathToLoad.add((PersonRelationID) tuckedNodeInPath.getJdoObjectID()); // F. Perform the specialised actual and tucked childCounts. boolean isCountSuccessful = updateTuckedNodeChildCounts(tuckedNodeInPath, monitor); if (isCountSuccessful) { rootShellNode.setCollectiveChildCounts(tuckedNodeInPath); } else { // This is the case where the tuckedNodeInPath is the last element in the known tucked-path; i.e. its // nodeStatus would have been set to NORMAL. In this case, this node would be handled 'normally', and its correct childCount // appropriately set. logger.debug("!!! Node counting reverted for NORMAL node. " + PersonRelationTreeUtil.showObjectID(tuckedNodeInPath.getPropertySetID())); // H. Set the COLLECTIVE shell-node's own sync-ed information. // This tuckedNodeInPath now corresponds to the last element in the tucked-path of the collective node, and thus this node // is the "node-representative" of the rootShellNode. rootShellNode.setChildNodeCount(tuckedNodeInPath.getChildNodeCount()); rootShellNode.setNodeStatus(tuckedNodeInPath.getNodeStatus()); } } // G. Load the JDOObjects, and place the references in the correct nodes. if (!personRelationIDsInPathToLoad.isEmpty()) { Collection<Object> resultInPath = new ArrayList<Object>(personRelationIDsInPathToLoad.size()); resultInPath = super.retrieveJDOObjectsByPersonRelationIDs(resultInPath, personRelationIDsInPathToLoad, monitor, tix); for (Object jdoObject : resultInPath) { ObjectID objectID = (ObjectID) JDOHelper.getObjectId(jdoObject); List<CompactedPersonRelationTreeNode> treeNodes = getTreeNodeList(objectID); if (treeNodes == null) continue; for (CompactedPersonRelationTreeNode treeNode : treeNodes) treeNode.setJdoObject(jdoObject); } } // Done. if (logger.isDebugEnabled()) { logger.debug(" ~~~ sequenceCtr: " + (sequenceCtr++) + " ~~~ ********* @retrieveJDOObjectsByPropertySetIDs() -- [III]"); logger.debug(" ### After init: " + rootShellNode.toDebugString()); } } } return results; } @Override protected Collection<ObjectID> retrieveChildObjectIDsByPropertySetIDs(CompactedPersonRelationTreeNode parentNode, ProgressMonitor monitor) { // Interceptor: If the parentNode is a COLLECTIVE node (i.e. it has a valid node-representative reference), then the child ObjectIDs that // need to be returned are not the parentNode's, but rather, the child ObjectIDs of the node-representative's. CompactedPersonRelationTreeNode nodeRepresentative = parentNode.getNodeRepresentative(); if (nodeRepresentative != null) { if (logger.isDebugEnabled()) { logger.debug(" ### ##### FOUND a COLLECTIVE node while retrieveChildObjectIDsBy~~~PropertySetIDs() !!! +++++++++ <Poss. 1>"); logger.debug(" ### ##### Node identified as COLLECTIVE: meNodeID = " + parentNode.meNodeID + ", nodeRepresentative.meNodeID = " + nodeRepresentative.meNodeID); } // There are two independent cases here to consider: // [Case 1] We check to see if the node-representative carries a PersonRelationID. If so, we divert it to call the super class's method to handle // the child-ObjectIDs' retrieval by PersonRelationID. // [Case 2] If the node-representative carries a PropertySetID, then the parentNode (which incidentally is the COLLECTIVE shell-node) has only one // tucked-path node, which is this node-representative. In this case, we relinquish control back to the original method. // <-- See [Case 1] if (nodeRepresentative.getJdoObjectID() instanceof PersonRelationID) return super.retrieveChildObjectIDsByPersonRelationIDs(nodeRepresentative, monitor); } // <-- See [Case 2] return super.retrieveChildObjectIDsByPropertySetIDs(parentNode, monitor); } @Override protected Collection<ObjectID> retrieveChildObjectIDsByPersonRelationIDs(CompactedPersonRelationTreeNode parentNode, ProgressMonitor monitor) { // Interceptor: If the parentNode is a COLLECTIVE node (i.e. it has a valid node-representative reference), then the child ObjectIDs that // need to be returned are not the parentNode's, but rather, the child ObjectIDs of the node-representative's. CompactedPersonRelationTreeNode nodeRepresentative = parentNode.getNodeRepresentative(); if (nodeRepresentative != null) { logger.debug(" ### ##### FOUND a COLLECTIVE node while retrieveChildObjectIDsBy~~~PersonRelationIDs() !!! +++++++++ <Poss. 2>"); logger.debug(" ### ##### Node identified as COLLECTIVE: meNodeID = " + parentNode.meNodeID + ", nodeRepresentative.meNodeID = " + nodeRepresentative.meNodeID); // With respect to the two independent cases above, it is impossible that the node-representative // at this point here carries a PropertySetID. return super.retrieveChildObjectIDsByPersonRelationIDs(nodeRepresentative, monitor); } return super.retrieveChildObjectIDsByPersonRelationIDs(parentNode, monitor); } }