package gov.nysenate.openleg.model.law; import com.google.common.collect.Range; import org.apache.commons.lang3.StringUtils; import java.time.LocalDate; import java.util.*; import static java.util.stream.Collectors.toList; public class LawTreeNode implements Comparable<LawTreeNode> { /** This number indicates the order in which this node appears in the tree, starting at 1. */ protected int sequenceNo; /** Reference to the law info which contains details about this node. */ protected LawDocInfo lawDocInfo; /** Reference to the parent node, null if this is the chapter node. */ protected LawTreeNode parent; /** Contains references to all the immediate children of this node. The key is the document id * of the child node. */ protected LinkedHashMap<String, LawTreeNode> children = new LinkedHashMap<>(); /** Date when this law node was repealed, null if not repealed. */ protected LocalDate repealedDate; /** Instance variable used to cache the section range once computed. */ private Optional<Range<LawTreeNode>> sectionRange; /** --- Constructors --- */ public LawTreeNode(LawDocInfo lawDocInfo, int sequenceNo) { if (lawDocInfo == null) { throw new IllegalArgumentException("Cannot instantiate LawTreeNode with a null LawDocInfo"); } this.lawDocInfo = lawDocInfo; this.sequenceNo = sequenceNo; } /** --- Methods --- */ public boolean isRootNode() { return this.lawDocInfo.getDocType().equals(LawDocumentType.CHAPTER); } public void addChild(LawTreeNode node) { if (node == null) throw new IllegalArgumentException("Cannot add a null child node "); node.setParent(this); children.put(node.lawDocInfo.documentId, node); } /** * Returns a range of the sections that span the range of this node. For example if this is an article node, * this method will return the start and end sections contained under this article. * * Note: This method will recompute the range regardless of whether the 'sectionRange' instance variable is already set. * @return Optional<Range<LawTreeNode>> */ public Optional<Range<LawTreeNode>> getSectionRange() { LawTreeNode start = findFirstSection(this); LawTreeNode end = findLastSection(this); if (start != null && end != null) { sectionRange = Optional.of(Range.closed(start, end)); } else { sectionRange = Optional.empty(); } return sectionRange; } /** * Gets the first immediate section, using the cached range if available. * @return Optional<LawTreeNode> */ public Optional<LawTreeNode> getFromSection() { if (sectionRange == null) { sectionRange = getSectionRange(); } if (sectionRange.isPresent()) { return Optional.of(sectionRange.get().lowerEndpoint()); } return Optional.empty(); } /** * Gets the last immediate section, using the cached range if available. * @return Optional<LawTreeNode> */ public Optional<LawTreeNode> getToSection() { if (sectionRange == null) { sectionRange = getSectionRange(); } if (sectionRange.isPresent()) { return Optional.of(sectionRange.get().upperEndpoint()); } return Optional.empty(); } /** * Get a list of the children nodes ordered by sequence number. * * @return List<LawTreeNode> */ public List<LawTreeNode> getChildNodeList() { return this.children.values().stream().sorted().collect(toList()); } /** * Returns this node as well as all the descendants of this node in an ordered list. * This is a convenience method that creates the initial accumulator list before running * the recursive method. * * @return List<LawTreeNode> */ public List<LawTreeNode> getAllNodes() { return getAllNodes(new ArrayList<>()); } /** * Returns this node as well as all the descendants of this node in an ordered list. * * @param descNodes List<LawTreeNode> - Used to append nodes recursively. * @return List<LawTreeNode> */ public List<LawTreeNode> getAllNodes(List<LawTreeNode> descNodes) { if (descNodes == null) throw new IllegalStateException("Node list is null"); descNodes.add(this); getChildNodeList().forEach(n -> n.getAllNodes(descNodes)); return descNodes; } /** * Returns an optional containing the previous law tree node within the same level as this node. * @return Optional<LawTreeNode> */ public Optional<LawTreeNode> getPrevSibling() { if (getParent() != null) { List<LawTreeNode> childNodeList = getParent().getChildNodeList(); int index = childNodeList.indexOf(this); if (index > 0) { return Optional.of(childNodeList.get(index - 1)); } } return Optional.empty(); } /** * Returns an optional containing the next law tree node within the same level as this node. * @return Optional<LawTreeNode> */ public Optional<LawTreeNode> getNextSibling() { if (getParent() != null) { List<LawTreeNode> childNodeList = getParent().getChildNodeList(); int index = childNodeList.indexOf(this); if (index < childNodeList.size() - 1) { return Optional.of(childNodeList.get(index + 1)); } } return Optional.empty(); } /** * Return a list of all the parent nodes for this particular node. * @return LinkedList<LawTreeNode> */ public LinkedList<LawTreeNode> getAllParents() { LinkedList<LawTreeNode> parents = new LinkedList<>(); LawTreeNode lawTreeNode = this; while (lawTreeNode.getParent() != null) { lawTreeNode = lawTreeNode.getParent(); parents.addFirst(lawTreeNode); } return parents; } /** * Recursively searches for a node that matches the given documentId and returns the law doc info. * * @param documentId String - Document id of the law document. * @return Optional<LawDocInfo> - Matched node or empty if it could not be found. */ public Optional<LawDocInfo> find(String documentId) { Optional<LawTreeNode> lawTreeNode = findNode(documentId, false); return (lawTreeNode.isPresent()) ? Optional.of(lawTreeNode.get().getLawDocInfo()) : Optional.empty(); } /** * Recursively searches for a child node that matches the given documentId or returns the current node * if it happens to match the docId. The delete param can be set to true to delete this node from the tree * by removing the reference from it's parent node. * * @param documentId String - Document id of the law document. * @param delete boolean - Set to true to delete the node and it's descendants from the tree. * @return Optional<LawDocInfo> - Matched node or empty if it could not be found. */ public Optional<LawTreeNode> findNode(String documentId, boolean delete) { Optional<LawTreeNode> lawTreeNode = Optional.empty(); if (this.getDocumentId().equals(documentId)) { lawTreeNode = Optional.of(this); } else if (children.containsKey(documentId)) { lawTreeNode = Optional.of(children.get(documentId)); } else { for (LawTreeNode node : children.values()) { lawTreeNode = node.findNode(documentId, delete); if (lawTreeNode.isPresent()) break; } } if (delete && lawTreeNode.isPresent()) { LawTreeNode parentNode = lawTreeNode.get().getParent(); if (parentNode != null) { parentNode.getChildren().remove(documentId); } } return lawTreeNode; } /** * Prints out this tree with formatting to show the hierarchy. * * @return String */ public String printTree() { return printTree(1); } /** * Recursively print out this tree with formatting to show the hierarchy. * * @param level int - Number to indicate the nesting level. * @return String */ public String printTree(int level) { StringBuilder sb = new StringBuilder(); sb.append(this.lawDocInfo.toString()); getChildNodeList().forEach(n -> { sb.append("\n").append(StringUtils.repeat(" | ", level)).append(n.printTree(level + 1)); }); return sb.toString(); } /** * Finds the first valid section under this law node. * @param node LawTreeNode * @return LawTreeNode of the first immediate section (reference to self if this is a section node), or null if none. */ private LawTreeNode findFirstSection(LawTreeNode node) { if (node.getDocType().equals(LawDocumentType.SECTION)) { return node; } LinkedList<LawTreeNode> children = new LinkedList<>(node.getChildren().values()); LawTreeNode firstNode = null; while (!children.isEmpty() && firstNode == null) { firstNode = findFirstSection(children.getFirst()); if (firstNode == null) { children.removeFirst(); } } return firstNode; } /** * Finds the last valid section under this node. * @param node LawTreeNode * @return LawTreeNode of the last immediate section (reference to self if this is a section node), or null if none. */ private LawTreeNode findLastSection(LawTreeNode node) { if (node.getDocType().equals(LawDocumentType.SECTION)) { return node; } LinkedList<LawTreeNode> children = new LinkedList<>(node.getChildren().values()); LawTreeNode lastNode = null; while (!children.isEmpty() && lastNode == null) { lastNode = findLastSection(children.getLast()); if (lastNode == null) { children.removeLast(); } } return lastNode; } /** --- Overrides --- */ @Override public String toString() { return "Law Tree Node [" + this.sequenceNo + "] " + this.lawDocInfo; } /** Compare the nodes simply by using the sequence number. */ @Override public int compareTo(LawTreeNode o) { return Integer.compare(this.getSequenceNo(), o.getSequenceNo()); } /** --- Delegates --- */ public String getLawId() { return lawDocInfo.getLawId(); } public LawDocumentType getDocType() { return lawDocInfo.getDocType(); } public String getDocTypeId() { return lawDocInfo.getDocTypeId(); } public LocalDate getPublishDate() { return lawDocInfo.getPublishedDate(); } public String getDocumentId() { return lawDocInfo.getDocumentId(); } public String getLocationId() { return lawDocInfo.getLocationId(); } /** --- Basic Getters/Setters --- */ public int getSequenceNo() { return sequenceNo; } public void setSequenceNo(int sequenceNo) { this.sequenceNo = sequenceNo; } public LawDocInfo getLawDocInfo() { return lawDocInfo; } public LawTreeNode getParent() { return parent; } public void setParent(LawTreeNode parent) { this.parent = parent; } public LinkedHashMap<String, LawTreeNode> getChildren() { return children; } public LocalDate getRepealedDate() { return repealedDate; } public void setRepealedDate(LocalDate repealedDate) { this.repealedDate = repealedDate; } }