/**
   * Does a 'breadth first' search of ancestors, caching as it goes
   *
   * @param nodeIds initial list of nodes to visit
   * @return all visited nodes, in no particular order
   */
  private List<Long> cacheAncestors(List<Long> nodeIds) {
    final LinkedList<Long> toVisit = new LinkedList<Long>(nodeIds);
    Set<Long> visited = new TreeSet<Long>();
    Long nodeId;
    nodeDAO.cacheNodesById(toVisit);
    Long lastCached = toVisit.peekLast();
    while ((nodeId = toVisit.pollFirst()) != null) {
      if (visited.add(nodeId)
          && (nodeDAO.getNodeIdStatus(nodeId) != null)
          && (false == nodeDAO.getNodeIdStatus(nodeId).isDeleted())) {
        nodeDAO.getParentAssocs(
            nodeId,
            null,
            null,
            null,
            new ChildAssocRefQueryCallback() {
              @Override
              public boolean preLoadNodes() {
                return false;
              }

              @Override
              public boolean orderResults() {
                return false;
              }

              @Override
              public boolean handle(
                  Pair<Long, ChildAssociationRef> childAssocPair,
                  Pair<Long, NodeRef> parentNodePair,
                  Pair<Long, NodeRef> childNodePair) {
                toVisit.add(parentNodePair.getFirst());
                return true;
              }

              @Override
              public void done() {}
            });
      }
      final boolean nodeIdEqualsLastCached =
          (nodeId == null && lastCached == null) || (nodeId != null && nodeId.equals(lastCached));
      if (nodeIdEqualsLastCached && !toVisit.isEmpty()) {
        nodeDAO.cacheNodesById(toVisit);
        lastCached = toVisit.peekLast();
      }
    }
    return new ArrayList<Long>(visited);
  }
  private List<Long> preCacheNodes(NodeMetaDataParameters nodeMetaDataParameters) {
    int maxResults = nodeMetaDataParameters.getMaxResults();
    boolean isLimitSet = (maxResults != 0 && maxResults != Integer.MAX_VALUE);

    List<Long> nodeIds = null;
    Iterable<Long> iterable = null;
    List<Long> allNodeIds = nodeMetaDataParameters.getNodeIds();
    if (allNodeIds != null) {
      int toIndex = (maxResults > allNodeIds.size() ? allNodeIds.size() : maxResults);
      nodeIds = isLimitSet ? allNodeIds.subList(0, toIndex) : nodeMetaDataParameters.getNodeIds();
      iterable = nodeMetaDataParameters.getNodeIds();
    } else {
      Long fromNodeId = nodeMetaDataParameters.getFromNodeId();
      Long toNodeId = nodeMetaDataParameters.getToNodeId();
      nodeIds = new ArrayList<Long>(isLimitSet ? maxResults : 100); // TODO better default here?
      iterable = new SequenceIterator(fromNodeId, toNodeId, maxResults);
      int counter = 1;
      for (Long nodeId : iterable) {
        if (isLimitSet && counter++ > maxResults) {
          break;
        }
        nodeIds.add(nodeId);
      }
    }

    // Pre-evaluate ancestors so we can bulk load them
    List<Long> ancestors;
    if (cacheAncestors) {
      ancestors = cacheAncestors(nodeIds);
    } else {
      ancestors = nodeIds;
    }
    // Ensure that we get fresh node references
    nodeDAO.setCheckNodeConsistency();
    // bulk load nodes and their ancestors
    nodeDAO.cacheNodesById(ancestors);

    return nodeIds;
  }