Java Code
/* * Copyright: (c) 2004-2009 Mayo Foundation for Medical Education and * Research (MFMER). All rights reserved. MAYO, MAYO CLINIC, and the * triple-shield Mayo logo are trademarks and service marks of MFMER. * * Except as contained in the copyright notice above, or as used to identify * MFMER as the author of this software, the trade names, trademarks, service * marks, or product names of the copyright holder shall not be used in * advertising, promotion or otherwise in connection with this software without * prior written authorization of the copyright holder. * * Licensed under the Eclipse Public License, Version 1.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.eclipse.org/legal/epl-v10.html * */ package org.LexGrid.LexBIG.example; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeMap; import org.LexGrid.LexBIG.DataModel.Collections.AssociatedConceptList; import org.LexGrid.LexBIG.DataModel.Collections.AssociationList; import org.LexGrid.LexBIG.DataModel.Collections.LocalNameList; import org.LexGrid.LexBIG.DataModel.Collections.ResolvedConceptReferenceList; import org.LexGrid.LexBIG.DataModel.Core.AssociatedConcept; import org.LexGrid.LexBIG.DataModel.Core.Association; import org.LexGrid.LexBIG.DataModel.Core.CodingSchemeSummary; import org.LexGrid.LexBIG.DataModel.Core.CodingSchemeVersionOrTag; import org.LexGrid.LexBIG.DataModel.Core.ConceptReference; import org.LexGrid.LexBIG.DataModel.Core.ResolvedConceptReference; import org.LexGrid.LexBIG.Exceptions.LBException; import org.LexGrid.LexBIG.Exceptions.LBResourceUnavailableException; import org.LexGrid.LexBIG.Extensions.Generic.LexBIGServiceConvenienceMethods; import org.LexGrid.LexBIG.Extensions.Generic.LexBIGServiceConvenienceMethods.HierarchyPathResolveOption; import org.LexGrid.LexBIG.Impl.LexBIGServiceImpl; import org.LexGrid.LexBIG.LexBIGService.CodedNodeGraph; import org.LexGrid.LexBIG.LexBIGService.CodedNodeSet; import org.LexGrid.LexBIG.LexBIGService.LexBIGService; import org.LexGrid.LexBIG.Utility.Constructors; import org.LexGrid.codingSchemes.CodingScheme; import org.LexGrid.commonTypes.EntityDescription; import org.LexGrid.naming.SupportedHierarchy; import org.apache.commons.lang.StringUtils; /** * Attempts to provide a tree, based on a focus code, that includes the * following information: * * * - All paths from the hierarchy root to one or more focus codes. * - Immediate children of every node in path to root * - Indicator to show whether any unexpanded node can be further expanded * * * This example accepts two parameters... The first parameter is required, and * must contain at least one code in a comma-delimited list. A tree is produced * for each code. Time to produce the tree for each code is printed in * milliseconds. In order to factor out costs of startup and shutdown, resolving * multiple codes may offer a better overall estimate performance. * * The second parameter is optional, and can indicate a hierarchy ID to navigate * when resolving child nodes. If not provided, "is_a" is assumed. */ public class BuildTreeForCode { LocalNameList noopList_ = Constructors.createLocalNameList("_noop_"); public BuildTreeForCode() { super(); } /** * Entry point for processing. * * @param args */ public static void main(String[] args) { if (args.length < 1) { System.out.println("Example: BuildTreeForCode \"C0000,C0001\""); return; } try { // Prompt for a coding scheme to run against... CodingSchemeSummary css = Util.promptForCodeSystem(); if (css != null) { // Declare common service classes for processing ... LexBIGService lbsvc = LexBIGServiceImpl.defaultInstance(); LexBIGServiceConvenienceMethods lbscm = (LexBIGServiceConvenienceMethods) lbsvc .getGenericExtension("LexBIGServiceConvenienceMethods"); // Pull the URI and version from the selected value ... String scheme = css.getCodingSchemeURI(); CodingSchemeVersionOrTag csvt = new CodingSchemeVersionOrTag(); // For any given ontology, we know the hierarchy // information. But to keep the code as generic // as possible and protect against changes, we // resolve the supported hierarchy info from the // coding scheme... String hierarchyID = args.length > 1 ? args[1] : "is_a"; SupportedHierarchy hierarchyDefn = getSupportedHierarchy(lbsvc, scheme, csvt, hierarchyID); // Loop through each provided code. // The time to produce the tree for each code is printed in // milliseconds. In order to factor out costs of startup // and shutdown, resolving multiple codes may offer a // better overall estimate performance. String[] codes = args[0].split(","); for (String code : codes) new BuildTreeForCode().run(lbsvc, lbscm, scheme, csvt, hierarchyDefn, code); } } catch (Exception e) { Util.displayAndLogError("REQUEST FAILED !!!", e); } } /** * Prints the tree for an individual code. */ public void run(LexBIGService lbsvc, LexBIGServiceConvenienceMethods lbscm, String scheme, CodingSchemeVersionOrTag csvt, SupportedHierarchy hierarchyDefn, String focusCode) throws LBException { // Print a header and define a new tree for the code being processed. Util.displayMessage("============================================================"); Util.displayMessage("Focus code: " + focusCode); Util.displayMessage("============================================================"); TreeItem ti = new TreeItem("<Root>", "Root node"); long ms = System.currentTimeMillis(); int pathsResolved = 0; try { // Resolve 'is_a' hierarchy info. This example will // need to make some calls outside of what is covered // by existing convenience methods, but we use the // registered hierarchy to prevent need to hard code // relationship and direction info used on lookup ... String hierarchyID = hierarchyDefn.getLocalId(); String[] associationsToNavigate = hierarchyDefn.getAssociationNames(); boolean associationsNavigatedFwd = hierarchyDefn.getIsForwardNavigable(); // Identify the set of all codes on path from root // to the focus code ... Map<String, EntityDescription> codesToDescriptions = new HashMap<String, EntityDescription>(); AssociationList pathsFromRoot = getPathsFromRoot(lbsvc, lbscm, scheme, csvt, hierarchyID, focusCode, codesToDescriptions); // Typically there will be one path, but handle multiple just in // case. Each path from root provides a 'backbone', from focus // code to root, for additional nodes to hang off of in our // printout. For every backbone node, one level of children is // printed, along with an indication of whether those nodes can // be expanded. for (Iterator<Association> paths = pathsFromRoot.iterateAssociation(); paths.hasNext();) { addPathFromRoot(ti, lbsvc, lbscm, scheme, csvt, paths.next(), associationsToNavigate, associationsNavigatedFwd, codesToDescriptions); pathsResolved++; } } finally { System.out.println("Run time (milliseconds): " + (System.currentTimeMillis() - ms) + " to resolve " + pathsResolved + " paths from root."); } // Print the result .. printTree(ti, focusCode, 0); } /** * The given path represents a multi-tier association with associated * concepts and targets. This method expands the association content and * adds results to the given tree item, recursing as necessary to process * each level in the path. * * Nodes in the association acts as the backbone for the display. For each * backbone node, additional children are resolved one level deep along with * an indication of further expandability. */ protected void addPathFromRoot(TreeItem ti, LexBIGService lbsvc, LexBIGServiceConvenienceMethods lbscm, String scheme, CodingSchemeVersionOrTag csvt, Association path, String[] associationsToNavigate, boolean associationsNavigatedFwd, Map<String, EntityDescription> codesToDescriptions) throws LBException { // First, add the branch point from the path ... ConceptReference branchRoot = path.getAssociationReference(); String branchCode = branchRoot.getCode(); String branchCodeDescription = codesToDescriptions.containsKey(branchCode) ? codesToDescriptions .get(branchCode).getContent() : getCodeDescription(lbsvc, scheme, csvt, branchCode); TreeItem branchPoint = new TreeItem(branchCode, branchCodeDescription); String branchNavText = getDirectionalLabel(lbscm, scheme, csvt, path, associationsNavigatedFwd); // Now process elements in the branch ... AssociatedConceptList concepts = path.getAssociatedConcepts(); for (int i = 0; i < concepts.getAssociatedConceptCount(); i++) { // Add all immediate leafs in the branch, and indication of // sub-nodes. Do not process codes already in the backbone here; // they will be printed in the recursive call ... AssociatedConcept concept = concepts.getAssociatedConcept(i); String code = concept.getCode(); TreeItem branchItem = new TreeItem(code, getCodeDescription(concept)); branchPoint.addChild(branchNavText, branchItem); addChildren(branchItem, lbsvc, lbscm, scheme, csvt, code, codesToDescriptions.keySet(), associationsToNavigate, associationsNavigatedFwd); // Recurse to process the remainder of the backbone ... AssociationList nextLevel = concept.getSourceOf(); if (nextLevel != null) { if (nextLevel.getAssociationCount() != 0) { // More levels left to process ... for (int j = 0; j < nextLevel.getAssociationCount(); j++) addPathFromRoot(branchPoint, lbsvc, lbscm, scheme, csvt, nextLevel.getAssociation(j), associationsToNavigate, associationsNavigatedFwd, codesToDescriptions); } else { // End of the line ... // Always add immediate children ot the focus code. addChildren(branchItem, lbsvc, lbscm, scheme, csvt, concept.getCode(), Collections.EMPTY_SET, associationsToNavigate, associationsNavigatedFwd); } } } // Add the populated tree item to those tracked from root. ti.addChild(branchNavText, branchPoint); } /** * Populate child nodes for a single branch of the tree, and indicates * whether further expansion (to grandchildren) is possible. */ protected void addChildren(TreeItem ti, LexBIGService lbsvc, LexBIGServiceConvenienceMethods lbscm, String scheme, CodingSchemeVersionOrTag csvt, String branchRootCode, Set<String> codesToExclude, String[] associationsToNavigate, boolean associationsNavigatedFwd) throws LBException { // Resolve the next branch, representing children of the given // code, navigated according to the provided relationship and // direction. Resolve the children as a code graph, looking 2 // levels deep but leaving the final level unresolved. CodedNodeGraph cng = lbsvc.getNodeGraph(scheme, csvt, null); ConceptReference focus = Constructors.createConceptReference(branchRootCode, scheme); cng = cng.restrictToAssociations(Constructors.createNameAndValueList(associationsToNavigate), null); ResolvedConceptReferenceList branch = cng.resolveAsList(focus, associationsNavigatedFwd, !associationsNavigatedFwd, -1, 2, noopList_, null, null, null, -1, true); // The resolved branch will be represented by the first node in // the resolved list. The node will be subdivided by source or // target associations (depending on direction). The associated // nodes define the children. for (Iterator<ResolvedConceptReference> nodes = branch.iterateResolvedConceptReference(); nodes.hasNext();) { ResolvedConceptReference node = nodes.next(); AssociationList childAssociationList = associationsNavigatedFwd ? node.getSourceOf() : node.getTargetOf(); // Process each association defining children ... for (Iterator<Association> pathsToChildren = childAssociationList.iterateAssociation(); pathsToChildren .hasNext();) { Association child = pathsToChildren.next(); String childNavText = getDirectionalLabel(lbscm, scheme, csvt, child, associationsNavigatedFwd); // Each association may have multiple children ... AssociatedConceptList branchItemList = child.getAssociatedConcepts(); for (Iterator<AssociatedConcept> branchNodes = branchItemList.iterateAssociatedConcept(); branchNodes .hasNext();) { AssociatedConcept branchItemNode = branchNodes.next(); String branchItemCode = branchItemNode.getCode(); // Add here if not in the list of excluded codes. // This is also where we look to see if another level // was indicated to be available. If so, mark the // entry with a '+' to indicate it can be expanded. if (!codesToExclude.contains(branchItemCode)) { TreeItem childItem = new TreeItem(branchItemCode, getCodeDescription(branchItemNode)); AssociationList grandchildBranch = associationsNavigatedFwd ? branchItemNode.getSourceOf() : branchItemNode.getTargetOf(); if (grandchildBranch != null) childItem.expandable = true; ti.addChild(childNavText, childItem); } } } } } /** * Prints the given tree item, recursing through all branches. * * @param ti */ protected void printTree(TreeItem ti, String focusCode, int depth) { StringBuffer indent = new StringBuffer(); for (int i = 0; i < depth * 2; i++) indent.append("| "); StringBuffer codeAndText = new StringBuffer(indent).append(focusCode.equals(ti.code) ? ">>>>" : "").append( ti.code).append(':').append(ti.text.length() > 64 ? ti.text.substring(0, 62) + "..." : ti.text).append( ti.expandable ? " [+]" : ""); Util.displayMessage(codeAndText.toString()); indent.append("| "); for (String association : ti.assocToChildMap.keySet()) { Util.displayMessage(indent.toString() + association); List<TreeItem> children = ti.assocToChildMap.get(association); Collections.sort(children); for (TreeItem childItem : children) printTree(childItem, focusCode, depth + 1); } } // ///////////////////////////////////////////////////// // Helper Methods // ///////////////////////////////////////////////////// /** * Returns the entity description for the given code. */ protected String getCodeDescription(LexBIGService lbsvc, String scheme, CodingSchemeVersionOrTag csvt, String code) throws LBException { CodedNodeSet cns = lbsvc.getCodingSchemeConcepts(scheme, csvt); cns = cns.restrictToCodes(Constructors.createConceptReferenceList(code, scheme)); ResolvedConceptReferenceList rcrl = cns.resolveToList(null, noopList_, null, 1); if (rcrl.getResolvedConceptReferenceCount() > 0) { EntityDescription desc = rcrl.getResolvedConceptReference(0).getEntityDescription(); if (desc != null) return desc.getContent(); } return "<Not assigned>"; } /** * Returns the entity description for the given resolved concept reference. */ protected String getCodeDescription(ResolvedConceptReference ref) throws LBException { EntityDescription desc = ref.getEntityDescription(); if (desc != null) return desc.getContent(); return "<Not assigned>"; } /** * Returns the label to display for the given association and directional * indicator. */ protected String getDirectionalLabel(LexBIGServiceConvenienceMethods lbscm, String scheme, CodingSchemeVersionOrTag csvt, Association assoc, boolean navigatedFwd) throws LBException { String assocLabel = navigatedFwd ? lbscm.getAssociationForwardName(assoc.getAssociationName(), scheme, csvt) : lbscm.getAssociationReverseName(assoc.getAssociationName(), scheme, csvt); if (StringUtils.isBlank(assocLabel)) assocLabel = (navigatedFwd ? "" : "[Inverse]") + assoc.getAssociationName(); return assocLabel; } /** * Resolves one or more paths from the hierarchy root to the given code * through a list of connected associations defined by the hierarchy. */ protected AssociationList getPathsFromRoot(LexBIGService lbsvc, LexBIGServiceConvenienceMethods lbscm, String scheme, CodingSchemeVersionOrTag csvt, String hierarchyID, String focusCode, Map<String, EntityDescription> codesToDescriptions) throws LBException { // Get paths from the focus code to the root from the // convenience method. All paths are resolved. If only // one path is required, it would be possible to use // HierarchyPathResolveOption.ONE to reduce processing // and improve overall performance. AssociationList pathToRoot = lbscm.getHierarchyPathToRoot(scheme, csvt, null, focusCode, false, HierarchyPathResolveOption.ALL, null); // But for purposes of this example we need to display info // in order coming from root direction. Process the paths to root // recursively to reverse the order for processing ... AssociationList pathFromRoot = new AssociationList(); for (int i = pathToRoot.getAssociationCount() - 1; i >= 0; i--) reverseAssoc(lbsvc, lbscm, scheme, csvt, pathToRoot.getAssociation(i), pathFromRoot, codesToDescriptions); return pathFromRoot; } /** * Returns a description of the hierarchy defined by the given coding scheme * and matching the specified ID. */ protected static SupportedHierarchy getSupportedHierarchy(LexBIGService lbsvc, String scheme, CodingSchemeVersionOrTag csvt, String hierarchyID) throws LBException { CodingScheme cs = lbsvc.resolveCodingScheme(scheme, csvt); if (cs == null) { throw new LBResourceUnavailableException("Coding scheme not found: " + scheme); } for (SupportedHierarchy h : cs.getMappings().getSupportedHierarchy()) if (h.getLocalId().equals(hierarchyID)) return h; throw new LBResourceUnavailableException("Hierarchy not defined: " + hierarchyID); } /** * Recursive call to reverse order of the given association list, adding * results to the given list. In context of this program we use this * technique to determine the path from root, starting from the path to root * provided by the standard convenience method. */ protected AssociationList reverseAssoc(LexBIGService lbsvc, LexBIGServiceConvenienceMethods lbscm, String scheme, CodingSchemeVersionOrTag csvt, Association assoc, AssociationList addTo, Map<String, EntityDescription> codeToEntityDescriptionMap) throws LBException { ConceptReference acRef = assoc.getAssociationReference(); AssociatedConcept acFromRef = new AssociatedConcept(); acFromRef.setCodingSchemeName(acRef.getCodingSchemeName()); acFromRef.setConceptCode(acRef.getConceptCode()); AssociationList acSources = new AssociationList(); acFromRef.setIsNavigable(Boolean.TRUE); acFromRef.setSourceOf(acSources); // Use cached description if available (should be cached // for all but original root) ... if (codeToEntityDescriptionMap.containsKey(acRef.getConceptCode())) acFromRef.setEntityDescription(codeToEntityDescriptionMap.get(acRef.getConceptCode())); // Otherwise retrieve on demand ... else acFromRef.setEntityDescription(Constructors.createEntityDescription(getCodeDescription(lbsvc, scheme, csvt, acRef.getConceptCode()))); AssociatedConceptList acl = assoc.getAssociatedConcepts(); for (AssociatedConcept ac : acl.getAssociatedConcept()) { // Create reverse association (same non-directional name) Association rAssoc = new Association(); rAssoc.setAssociationName(assoc.getAssociationName()); // On reverse, old associated concept is new reference point. ConceptReference ref = new ConceptReference(); ref.setCodingSchemeName(ac.getCodingSchemeName()); ref.setConceptCode(ac.getConceptCode()); rAssoc.setAssociationReference(ref); // And old reference is new associated concept. AssociatedConceptList rAcl = new AssociatedConceptList(); rAcl.addAssociatedConcept(acFromRef); rAssoc.setAssociatedConcepts(rAcl); // Set reverse directional name, if available. String dirName = assoc.getDirectionalName(); if (dirName != null) try { rAssoc.setDirectionalName(lbscm.isForwardName(scheme, csvt, dirName) ? lbscm .getAssociationReverseName(assoc.getAssociationName(), scheme, csvt) : lbscm .getAssociationReverseName(assoc.getAssociationName(), scheme, csvt)); } catch (LBException e) { } // Save code desc for future reference when setting up // concept references in recursive calls ... codeToEntityDescriptionMap.put(ac.getConceptCode(), ac.getEntityDescription()); AssociationList sourceOf = ac.getSourceOf(); if (sourceOf != null) for (Association sourceAssoc : sourceOf.getAssociation()) { AssociationList pos = reverseAssoc(lbsvc, lbscm, scheme, csvt, sourceAssoc, addTo, codeToEntityDescriptionMap); pos.addAssociation(rAssoc); } else addTo.addAssociation(rAssoc); } return acSources; } // ///////////////////////////////////////////////////// // Helper classes // ///////////////////////////////////////////////////// /** * Inner class to hold tree items for printout. */ protected class TreeItem implements Comparable<TreeItem> { public String code = null; public String text = null; public boolean expandable = false; public Map<String, List<TreeItem>> assocToChildMap = new TreeMap<String, List<TreeItem>>(); public boolean equals(Object o) { return o instanceof TreeItem && code.compareTo(((TreeItem) o).code) == 0; } public int compareTo(TreeItem ti) { String c1 = code; String c2 = ti.code; if (c1.startsWith("@")) return 1; if (c2.startsWith("@")) return -1; return c1.compareTo(c2); } public TreeItem(String code, String text) { super(); this.code = code; this.text = text; } public void addAll(String assocText, List<TreeItem> children) { for (TreeItem item : children) addChild(assocText, item); } public void addChild(String assocText, TreeItem child) { List<TreeItem> children = assocToChildMap.get(assocText); if (children == null) { children = new ArrayList<TreeItem>(); assocToChildMap.put(assocText, children); } int i; if ((i = children.indexOf(child)) >= 0) { TreeItem existingTreeItem = children.get(i); for (String assoc : child.assocToChildMap.keySet()) { List<TreeItem> toAdd = child.assocToChildMap.get(assoc); if (!toAdd.isEmpty()) { existingTreeItem.addAll(assoc, toAdd); existingTreeItem.expandable = false; } } } else children.add(child); } } }