Package com.almworks.jira.structure.api.forest

Source Code of com.almworks.jira.structure.api.forest.Forest

package com.almworks.jira.structure.api.forest;

import com.almworks.integers.*;
import com.almworks.integers.util.LongSetBuilder;
import com.almworks.jira.structure.api.StructureError;
import com.almworks.jira.structure.api.StructureException;
import com.almworks.jira.structure.util.La;
import com.almworks.jira.structure.util.StructureUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import static com.almworks.jira.structure.api.StructureError.INVALID_MOVE;
import static com.almworks.jira.structure.api.StructureError.ISSUE_MISSING_FROM_STRUCTURE;

/**
* @author Igor Sereda
*/
public class Forest implements Cloneable {
  private static final Logger logger = LoggerFactory.getLogger(Forest.class);

  private WritableLongList myIssues;
  private WritableIntList myDepths;
  private boolean myImmutable;

  public Forest() {
    this(new LongArray(), new IntArray(), true);
  }

  /**
   * Creates a singular tree. Singular tree cannot be stored (the issue is a standalone issue), but can be used between
   * operations on other trees.
   *
   * @param issue issue ID of the root
   */
  public Forest(long issue) {
    this(LongArray.create(issue), IntArray.create(0), true);
  }

  /**
   * Constructs a forest based on the given issues and depths.
   * <p/>
   * Issue list and depth list must conform to the IssueTree invariants. If invariants are violated, then either an
   * error is thrown (if assertions are on), or further behavior is undefined.
   *
   * @param issues     list of issue IDs
   * @param depths     list of corresponding depths
   * @param reuseLists if true, passed instances of List can be used by this IssueTree (otherwise a copy is made)
   */
  public Forest(WritableLongList issues, WritableIntList depths, boolean reuseLists) {
    myIssues = reuseLists ? issues : new LongArray(issues);
    myDepths = reuseLists ? depths : new IntArray(depths);
    assert checkInvariants();
  }

  public Forest(LongList issues, IntList depths) {
    this(new LongArray(issues), new IntArray(depths), true);
  }

  /**
   * Checks whether IssueTree invariants hold. Throws AssertionError if not, with a diagnostic message.
   *
   * @return true
   */
  boolean checkInvariants() {
    String problem = getDiagnosics();
    assert problem == null : problem;
    return true;
  }

  /**
   * Checks whether IssueTree invariants hold.
   *
   * @return null if all invariants are true, otherwise return a message with description of the problem
   */
  public String getDiagnosics() {
    int size = myIssues.size();
    if (size != myDepths.size()) return "array size mismatch";
    if (size == 0) return null;
    if (myDepths.get(0) != 0) return "root not at 0 depth";

    Map<Long, Integer> issues = new HashMap<Long, Integer>();
    int depth = -1;
    for (int i = 0; i < size; i++) {
      long issue = myIssues.get(i);
      if (issue <= 0) {
        return "bad issue @" + i + " " + issue + " " + this;
      }
      Integer prev = issues.put(issue, i);
      if (prev != null) {
        return "duplicate issue @" + i + " " + issue + " " + this;
      }
      int d = myDepths.get(i);
      if (d < 0) {
        return "bad depth @" + i + " " + d + " " + issue + " " + this;
      }
      if (d > depth + 1) {
        return "bad depth change @" + i + " " + depth + " " + d + " " + issue + " " + this;
      }
      depth = d;
    }
    return null;
  }

  /**
   * Checks if an issue is in the tree.
   *
   * @param issueId issue ID
   * @return true if this tree contains the issue
   */
  public boolean containsIssue(long issueId) {
    return myIssues.contains(issueId);
  }

  public void mergeForest(Forest forest, long under, long after) throws StructureException {
    mergeForest(forest, under, after, null);
  }

  /**
   * Invariant: after calling this method, either all issues from forest are in this forest, or exception is thrown.
   * <p/>
   * Merge is slow - more efficient algorithm can be employed if needed
   *
   * @param forest          the source forest to merge into this forest
   * @param under           under position
   * @param after           after position
   * @param previousParents if not null, then issue ids that used to be direct parents of the moved issues are added there
   * @throws StructureException if operation is not possible
   */
  public void mergeForest(Forest forest, long under, long after, LongCollector previousParents)
    throws StructureException
  {
    if (forest == null || forest.isEmpty()) return;
    assert checkInvariants();
    assert forest.checkInvariants();
    checkModification();

    if (isMutuallyExclusiveWith(forest)) {
      addForestMutuallyExclusive(forest, under, after);
      return;
    }

    if (under > 0) {
      int parentIndex = myIssues.indexOf(under);
      for (int p = parentIndex; p >= 0; p = getParentIndex(p)) {
        long pathIssue = myIssues.get(p);
        if (forest.containsIssue(pathIssue)) {
          throw new StructureException(INVALID_MOVE,
            "cannot merge " + forest + " in " + this + " under " + under + ": issue "
              + pathIssue + " on parent path is in the merged forest");
        }
      }
    }

    int size = forest.size();
    int i = 0;
    while (i < size) {
      long nextAfter = forest.getIssues().get(i);
      i = mergeSubforest(forest, i, under, after, previousParents);
      after = nextAfter;
    }

    assert checkInvariants();
  }

  private boolean isMutuallyExclusiveWith(Forest forest) {
    if (this.size() == 0 || forest.size() == 0) return true;
    boolean sw = forest.size() < this.size();
    LongList sortIssues = sw ? forest.getIssues() : this.getIssues();
    LongList checkIssues = sw ? this.getIssues() : forest.getIssues();
    LongSetBuilder builder = new LongSetBuilder();
    builder.addAll(sortIssues);
    LongList sorted = builder.toSortedCollection();
    int size = checkIssues.size();
    for (int i = 0; i < size; i++) {
      if (sorted.binarySearch(checkIssues.get(i)) >= 0) return false;
    }
    return true;
  }

  private int mergeSubforest(Forest forest, int index, long under, long after,
    LongCollector previousParents) throws StructureException
  {
    LongList issues = forest.getIssues();
    long issue = issues.get(index);
    int thisIndex = this.indexOf(issue); // don't confuse thisIndex with index
    if (thisIndex >= 0) {
      if (previousParents != null) {
        int parentIndex = getParentIndex(thisIndex);
        if (parentIndex >= 0) previousParents.add(myIssues.get(parentIndex));
      }
      moveSubtreeAtIndex(thisIndex, under, after, issue);
    } else {
      addForestMutuallyExclusive(new Forest(issue), under, after);
    }
    IntList depths = forest.getDepths();
    int depth = depths.get(index);
    int size = forest.size();
    index++;
    long subAfter = 0;
    while (index < size && depths.get(index) > depth) {
      long nextSubAfter = issues.get(index);
      index = mergeSubforest(forest, index, issue, subAfter, previousParents);
      subAfter = nextSubAfter;
    }
    return index;
  }

  private void addForestMutuallyExclusive(Forest forest, long under, long after) throws StructureException {
    if (forest == null || forest.isEmpty()) return;
    assert checkInvariants();
    assert forest.checkInvariants();
    checkModification();

    int parentIndex = getUnderIndex(under);
    int parentDepth = parentIndex < 0 ? -1 : myDepths.get(parentIndex);

    int insertAtIndex = parentIndex + 1;
    int insertAtDepth = parentDepth + 1;
    if (after > 0) {
      int siblingIndex = myIssues.indexOf(after);
      if (siblingIndex < 0) {
        logger.warn(this + ": ignoring invalid after: issue " + after);
      } else {
        siblingIndex = getPathIndexAtDepth(siblingIndex, insertAtDepth);
        if (siblingIndex < 0) {
          logger.warn(this + ": ignoring invalid after: issue " + after + " not at the right level");
        } else if (parentIndex != getParentIndex(siblingIndex)) {
          logger.warn(this + ": ignoring invalid after: issue " + after + " not under parent " + under);
        } else {
          insertAtIndex = getSubtreeEnd(siblingIndex);
        }
      }
    }
/*
    // the following was attempt to add "after = -1" to mean as "append to the end"
    // the solution is deemed unreliable (could be additional source of conflicts)
    else if (after < 0) {
      insertAtIndex = insertAtDepth == 0 ? myIssues.size() : findSubtreeEnd(parentIndex);
    }
*/
    LongList issues = forest.myIssues;
    IntList depths = forest.myDepths;
    myIssues.insertAll(insertAtIndex, issues);
    myDepths.insertAll(insertAtIndex, depths);
    if (insertAtDepth != 0) {
      for (int i = insertAtIndex + issues.size() - 1; i >= insertAtIndex; i--) {
        myDepths.set(i, myDepths.get(i) + insertAtDepth);
      }
    }
    assert checkInvariants();
  }

  private int getUnderIndex(long under) throws StructureException {
    int parentIndex;
    if (under <= 0) {
      parentIndex = -1;
    } else {
      parentIndex = myIssues.indexOf(under);
      if (parentIndex < 0) {
        throw new StructureException(ISSUE_MISSING_FROM_STRUCTURE, null, under,
          "cannot find under " + under + " in " + this);
      }
    }
    return parentIndex;
  }

  /**
   * Removes a subtree from this forest.
   * <p/>
   * This method will create a new <tt>IssueTree</tt> with <tt>issue</tt> at its root and all subissues, properly
   * outdented.
   *
   * @param issue issue ID of the root of the subtree to be removed
   * @return null if this tree does not contain <tt>issue</tt>, otherwise an IssueTree with the <tt>issue</tt> as the
   *         root
   */
  public Forest removeSubtree(long issue) {
    assert checkInvariants();
    int issueIndex = myIssues.indexOf(issue);
    if (issueIndex < 0) return null;
    return removeSubtreeAtIndex(issueIndex);
  }

  private Forest removeSubtreeAtIndex(int index) {
    assert checkInvariants();
    checkModification();
    int lastIndex = getSubtreeEnd(index);
    Forest r = copySubtree0(index, lastIndex);
    myIssues.removeRange(index, lastIndex);
    myDepths.removeRange(index, lastIndex);
    assert checkInvariants();
    return r;
  }

  private Forest copySubtree0(int from, int to) {
    LongArray newIssues = new LongArray(myIssues.subList(from, to));
    IntArray newDepths = new IntArray(myDepths.subList(from, to));
    // Decrement depths. its separate tree so depths should start from 0
    int issueDepth = newDepths.get(0);
    if (issueDepth > 0) {
      for (int i = 0; i < newDepths.size(); i++) {
        int d = newDepths.get(i) - issueDepth;
        if (d < 0 || d == 0 && i > 0) {
          throw new IllegalStateException("bad depth " + d + " @ " + i);
        }
        newDepths.set(i, d);
      }
    }
    return new Forest(newIssues, newDepths, true);
  }

  public Forest copySubtree(long rootIssue) {
    assert checkInvariants();
    int issueIndex = myIssues.indexOf(rootIssue);
    if (issueIndex < 0) return null;
    int lastIndex = getSubtreeEnd(issueIndex);
    return copySubtree0(issueIndex, lastIndex);
  }

  public int getSubtreeEnd(int index) {
    if (index < 0) return 0;
    int size = myDepths.size();
    if (index > size) return size;
    int d = myDepths.get(index);
    int r = index + 1;
    for (; r < size; r++) {
      if (myDepths.get(r) <= d) break;
    }
    return r;
  }

  /**
   * Method for accessing issue list.
   *
   * @return unmodifiable list of issue IDs
   */
  public LongList getIssues() {
    return myIssues;
  }

  /**
   * Method for accessing depth list.
   *
   * @return unmodifiable list of issue depths
   */
  public IntList getDepths() {
    return myDepths;
  }

  public long getIssue(int index) {
    return myIssues.get(index);
  }

  public int getDepth(int index) {
    return myDepths.get(index);
  }

  /**
   * Gets parent issue.
   *
   * @param issueId issue ID
   * @return null if issue is not in tree or if it's the tree root, otherwise issue ID of the parent issue
   */
  public Long getParent(Long issueId) {
    int index = myIssues.indexOf(issueId);
    if (index < 0) return null;
    int pi = getParentIndex(index);
    return pi < 0 ? null : myIssues.get(pi);
  }

  /**
   * Gets parent issue index.
   *
   * @param index position of the issue
   * @return -1 if issue is the tree root, otherwise position of the parent issue
   */
  public int getParentIndex(int index) {
    int depth = myDepths.get(index);
    if (depth == 0) return -1;
    for (int i = index - 1; i >= 0; i--) {
      if (myDepths.get(i) == depth - 1) return i;
    }
    assert false : index + " " + this;
    return -1;
  }

  public int getPathIndexAtDepth(int index, int depth) {
    if (depth < 0) return -1;
    if (myDepths.get(index) < depth) return -1;
    for (int i = index; i >= 0; i--) {
      if (myDepths.get(i) == depth) return i;
    }
    assert false : index + " " + this;
    return -1;
  }

  /**
   * Gets previous sibling.
   *
   * @param issue issue ID
   * @return null if issue is not in the tree or if there's no previous sibling, otherwise issue ID of the issue that is
   *         under the same parent right before <tt>issue</tt>
   */
  public Long getPreviousSibling(long issue) {
    int index = myIssues.indexOf(issue);
    if (index < 0) return null;
    int depth = myDepths.get(index);
    for (int i = index - 1; i >= 0; i--) {
      int d = myDepths.get(i);
      if (d < depth) return null;
      if (d == depth) return myIssues.get(i);
    }
    return null;
  }

  /**
   * Gets children of an issue.
   *
   * @param issue issue ID
   * @return a list of direct children of that issue, or null if the issue is not in the tree
   */
  public LongArray getChildren(long issue) {
    return getChildrenAtIndex(myIssues.indexOf(issue));
  }

  public LongArray getChildrenAtIndex(int index) {
    if (index < 0) return null;
    LongArray children = new LongArray();
    int depth = myDepths.get(index);
    for (int i = index + 1; i < myDepths.size(); i++) {
      int d = myDepths.get(i);
      if (d <= depth) break;
      if (d == depth + 1) children.add(myIssues.get(i));
    }
    return children;
  }

  public LongArray getRoots() {
    LongArray r = new LongArray();
    for (int i = 0, size = myDepths.size(); i < size; i++) {
      if (myDepths.get(i) == 0) r.add(myIssues.get(i));
    }
    return r;
  }


  /**
   * Filters this forest by hiding issues that do not pass the <tt>filter</tt> condition.
   * <p/>
   * As a result, a number of issue trees and standalone issues are produced, which represent the same hierarchy but
   * with all issues satisfying the condition. Multiple issue trees may be produced by this single tree if the root
   * issue does not pass the filter condition.
   * <p/>
   * This issue tree is not modified by this method.
   *
   * @param filter called once for every issue in this tree; if returns true, the issue is allowed
   * @return a forest of issues in the order of their root appearance in this tree
   */
  public Forest filter(La<Long, ?> filter) {
    if (filter == null) return this;
    int firstFiltered = 0;
    int size = myIssues.size();
    for (; firstFiltered < size; firstFiltered++) {
      if (!filter.accepts(myIssues.get(firstFiltered))) break;
    }
    if (firstFiltered == size) return this;

    LongArray issues = new LongArray(size);
    IntArray depths = new IntArray(size);
    int i = 0;
    while (i < size) {
      i = buildFilteredSubtree(issues, depths, i, 0, firstFiltered, filter);
    }
    return new Forest(issues, depths, true);
  }

  private int buildFilteredSubtree(WritableLongList issues, WritableIntList depths, int index, int targetDepth,
    int firstFiltered, La<Long, ?> filter)
  {
    int size = myIssues.size();
    if (index >= size) return index;
    int rootDepth = myDepths.get(index);
    int delta = targetDepth - rootDepth;
    int i = index;
    while (i < size) {
      long issue = myIssues.get(i);
      int depth = myDepths.get(i);
      if (depth < rootDepth || (depth == rootDepth && i > index)) break;
      boolean accepted = i < firstFiltered || (i > firstFiltered && filter.accepts(issue));
      i++;
      if (accepted) {
        issues.add(issue);
        depths.add(depth + delta);
      } else {
        while (i < size) {
          long nextDepth = myDepths.get(i);
          if (nextDepth <= depth) break;
          assert nextDepth == depth + 1 : i + " " + depth + " " + nextDepth;
          i = buildFilteredSubtree(issues, depths, i, depth + delta, firstFiltered, filter);
        }
      }
    }
    return i;
  }

  public Forest filterSoft(La<Long, ?> filter) {
    if (filter == null) return this;
    int size = myIssues.size();
    int lastFiltered = size - 1;
    for (; lastFiltered >= 0; lastFiltered--) {
      if (!filter.accepts(myIssues.get(lastFiltered))) break;
    }
    if (lastFiltered < 0) return this;
    LongArray revIssues = new LongArray();
    IntArray revDepth = new IntArray();
    int lastDepth = 0;
    if (lastFiltered < size - 1) {
      revIssues.addAll(myIssues.subList(lastFiltered + 1, size));
      revDepth.addAll(myDepths.subList(lastFiltered + 1, size));
      revIssues = StructureUtil.reverse(revIssues);
      revDepth = StructureUtil.reverse(revDepth);
      lastDepth = revDepth.get(revDepth.size() - 1);
    }
    for (int i = lastFiltered; i >= 0; i--) {
      int depth = myDepths.get(i);
      long issue = myIssues.get(i);
      boolean passes = depth < lastDepth || (i < lastFiltered && filter.accepts(issue));
      if (passes) {
        revIssues.add(issue);
        revDepth.add(depth);
        lastDepth = depth;
      }
    }
    assert lastDepth == 0;
    revIssues = StructureUtil.reverse(revIssues);
    revDepth = StructureUtil.reverse(revDepth);
    return new Forest(revIssues, revDepth, true);
  }

  /**
   * Makes this instance non-modifiable
   *
   * @return issue tree with the same data (backed by different collections), which cannot be modified
   */
  public Forest makeImmutable() {
    myImmutable = true;
    return this;
  }

  private void checkModification() {
    if (myImmutable) throw new UnsupportedOperationException();
  }


  /**
   * Gets the number of issues in this tree.
   *
   * @return the number of issues, also the size of lists returned by {@link #getIssues()} and {@link #getDepths()}
   */
  public int size() {
    return myIssues.size();
  }

  public Forest copy() {
    return new Forest(myIssues, myDepths);
  }

  public Forest copyFilteredForRoot(long rootIssue) {
    assert checkInvariants();
    int from = myIssues.indexOf(rootIssue);
    if (from < 0) return new Forest();
    int to = getSubtreeEnd(from);
    LongArray newIssues = new LongArray(myIssues.subList(from, to));
    IntArray newDepths = new IntArray(myDepths.subList(from, to));
    // also insert all parents
    for (int i = getParentIndex(from); i >= 0; i = getParentIndex(i)) {
      newIssues.insert(0, myIssues.get(i));
      newDepths.insert(0, myDepths.get(i));
    }
    return new Forest(newIssues, newDepths, true);
  }

  public boolean moveSubtree(long issue, long under, long after) throws StructureException {
    assert checkInvariants();
    int idx = indexOf(issue);
    if (idx < 0) return false;
    moveSubtreeAtIndex(idx, under, after, issue);
    return true;
  }

  private void moveSubtreeAtIndex(int index, long under, long after, long issue) throws StructureException {
    assert checkInvariants();
    checkModification();
    int parentIndex = getUnderIndex(under);
    if (parentIndex >= 0) {
      int underParentIndex = getPathIndexAtDepth(parentIndex, myDepths.get(index));
      if (underParentIndex == index) {
        throw new StructureException(StructureError.INVALID_MOVE, null, under,
          this + ": cannot move " + issue + " (index " + index + ") under " + under);
      }
    }
    Forest forest = removeSubtreeAtIndex(index);
    assert forest != null;
    addForestMutuallyExclusive(forest, under, after);
    assert checkInvariants();
  }

  public int indexOf(long issue) {
    return myIssues.indexOf(issue);
  }

  public boolean isEmpty() {
    return myIssues.isEmpty();
  }

  public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;

    Forest forest = (Forest) o;

    if (!myDepths.equals(forest.myDepths)) return false;
    if (!myIssues.equals(forest.myIssues)) return false;

    return true;
  }

  public int hashCode() {
    int result = myIssues.hashCode();
    result = 31 * result + myDepths.hashCode();
    return result;
  }

  public String toString() {
    return toStringLimited(20);
  }

  public String toFullString() {
    return toStringLimited(Integer.MAX_VALUE);
  }

  private String toStringLimited(int maxElements) {
    StringBuilder r = new StringBuilder("forest(");
    String prefix = "";
    if (myImmutable) {
      r.append("ro");
      prefix = ",";
    }
    int size = myIssues.size();
    int len = Math.min(maxElements, size);
    for (int i = 0; i < len; i++) {
      r.append(prefix).append(myIssues.get(i)).append(':').append(myDepths.get(i));
      prefix = ",";
    }
    if (len < size) r.append(" ... [").append(size).append(']');
    r.append(')');
    return r.toString();
  }

  public Forest clone() {
    try {
      Forest copy = (Forest) super.clone();
      copy.myIssues = new LongArray(copy.myIssues);
      copy.myDepths = new IntArray(copy.myDepths);
      return copy;
    } catch (CloneNotSupportedException e) {
      throw new AssertionError(e);
    }
  }

  public long getLastChild(long parent) {
    int index = parent == 0 ? -1 : indexOf(parent);
    return getLastChildByIndex(index);
  }

  public long getLastChildByIndex(int parentIndex) {
    int size = myDepths.size();
    if (parentIndex >= size) return 0;
    int depth = parentIndex < 0 ? 0 : myDepths.get(parentIndex) + 1;
    int index = -1;
    for (int i = parentIndex < 0 ? 0 : parentIndex + 1; i < size; i++) {
      int d = myDepths.get(i);
      if (d < depth) break;
      if (d == depth) {
        index = i;
      }
    }
    return index < 0 ? 0 : myIssues.get(index);
  }

  /**
   * <p>Goes over the forest in the backwards direction and reports to the visitor pairs of (parent, direct children).</p>
   * <p>Invariants:</p>
   * <ul>
   * <li>The number of calls to the visitor is equal to the forest size: every issue is reported as a parent once.</li>
   * <li>A child issue is reported (as the parent of its sub-issues) before parent is reported: the iteration goes upwards.</li>
   * <li>A leaf issue is reported as a parent with no children.</li>
   * </ul>
   * <p>Note: it is possible to also report all top-level issues with slight modifications of the method and visitor contract.</p>
   * <p>Note: if the forest is modified during iteration, the results are undefined.</p>
   *
   * @param visitor the visitor to receive pairs of (parent, children)
   */
  public void visitParentChildrenUpwards(ForestParentChildrenVisitor visitor) {
    int index = size();
    if (index == 0) return;
    // arrays for reuse
    List<LongArray> containerStack = new ArrayList<LongArray>();
    while (index > 0) {
      index = visitUpwards0(index, 0, visitor, containerStack);
    }
  }

  /**
   * Processes a subtree that ends at the specified index, and starts somewhere above it at the given depth.
   * Recursively processes nested subtrees first.
   *
   * @param endIndex       equals to {@link #getSubtreeEnd} of the subtree processed.
   * @param targetDepth    the required depth of the root
   * @param visitor        visitor
   * @param containerStack reusable arrays
   * @return the index of the root of the processed subtree, or -1 to abort iteration
   */
  private int visitUpwards0(int endIndex, int targetDepth, ForestParentChildrenVisitor visitor,
    List<LongArray> containerStack)
  {
    if (endIndex <= 0) return endIndex;
    int depth = myDepths.get(endIndex - 1);
    assert depth >= targetDepth : depth + " " + targetDepth;

    // does previous row contain a leaf of the required depth?
    if (depth == targetDepth) {
      boolean proceed = visitor.visit(this, myIssues.get(endIndex - 1), LongList.EMPTY);
      return proceed ? endIndex - 1 : -1;
    }

    // not a leaf - there will be children, allocate array
    while (containerStack.size() <= targetDepth) containerStack.add(new LongArray());
    LongArray children = containerStack.get(targetDepth);
    children.clear();

    // sub-iteration - gather nested sub-trees
    int index = endIndex;
    while (index > 0 && myDepths.get(index - 1) > targetDepth) {
      index = visitUpwards0(index, targetDepth + 1, visitor, containerStack);
      if (index < 0) return index;
      children.add(myIssues.get(index));
    }
    // we should exit by hitting the parent at the specified targetDepth
    assert index > 0 && myDepths.get(index - 1) == targetDepth : index + " " + this;
    index--;
    long parent = myIssues.get(index);
    children = StructureUtil.reverse(children);

    boolean proceed = visitor.visit(this, parent, children);
    return proceed ? index : -1;
  }

  public LongArray getPath(long issue) {
    LongArray r = new LongArray();
    for (int i = indexOf(issue); i >= 0; i = getParentIndex(i)) {
      r.insert(0, myIssues.get(i));
    }
    return r;
  }
}
TOP

Related Classes of com.almworks.jira.structure.api.forest.Forest

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.