/**
   * 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);
  }
  /** {@inheritDoc} */
  public void getNodesMetadata(
      NodeMetaDataParameters nodeMetaDataParameters,
      MetaDataResultsFilter resultFilter,
      NodeMetaDataQueryCallback callback) {
    if (false == enabled) {
      return;
    }

    NodeMetaDataQueryRowHandler rowHandler = new NodeMetaDataQueryRowHandler(callback);
    boolean includeType = (resultFilter == null ? true : resultFilter.getIncludeType());
    boolean includeProperties = (resultFilter == null ? true : resultFilter.getIncludeProperties());
    boolean includeAspects = (resultFilter == null ? true : resultFilter.getIncludeAspects());
    boolean includePaths = (resultFilter == null ? true : resultFilter.getIncludePaths());
    boolean includeNodeRef = (resultFilter == null ? true : resultFilter.getIncludeNodeRef());
    boolean includeParentAssociations =
        (resultFilter == null ? true : resultFilter.getIncludeParentAssociations());
    boolean includeChildAssociations =
        (resultFilter == null ? true : resultFilter.getIncludeChildAssociations());
    boolean includeOwner = (resultFilter == null ? true : resultFilter.getIncludeOwner());
    boolean includeChildIds = (resultFilter == null ? true : resultFilter.getIncludeChildIds());
    boolean includeTxnId = (resultFilter == null ? true : resultFilter.getIncludeTxnId());

    List<Long> nodeIds = preCacheNodes(nodeMetaDataParameters);

    for (Long nodeId : nodeIds) {
      Status status = nodeDAO.getNodeIdStatus(nodeId);
      if (status == null) {
        // We've been called with the ID of a purged node, probably due to processing a transaction
        // with a
        // cascading delete. Fine to skip and assume it will be processed in a transaction.
        // See org.alfresco.solr.tracker.CoreTracker.updateDescendantAuxDocs(NodeMetaData, boolean,
        // SolrIndexSearcher)
        continue;
      }
      NodeRef nodeRef = status.getNodeRef();

      NodeMetaData nodeMetaData = new NodeMetaData();
      nodeMetaData.setNodeId(nodeId);

      if (includeNodeRef) {
        nodeMetaData.setNodeRef(tenantService.getBaseName(nodeRef, true));
      }

      if (includeTxnId) {
        nodeMetaData.setTxnId(status.getDbTxnId());
      }

      if (status.isDeleted()) {
        rowHandler.processResult(nodeMetaData);
        continue;
      }

      Map<QName, Serializable> props = null;
      Set<QName> aspects = null;

      nodeMetaData.setAclId(nodeDAO.getNodeAclId(nodeId));

      if (includeType) {
        QName nodeType = getNodeType(nodeId);
        if (nodeType != null) {
          nodeMetaData.setNodeType(nodeType);
        } else {
          throw new AlfrescoRuntimeException("Nodes with no type are ignored by SOLR");
        }
      }

      if (includeProperties) {
        if (props == null) {
          props = getProperties(nodeId);
        }
        nodeMetaData.setProperties(props);
      } else {
        nodeMetaData.setProperties(Collections.<QName, Serializable>emptyMap());
      }

      if (includeAspects || includePaths || includeParentAssociations) {
        aspects = getNodeAspects(nodeId);
      }
      nodeMetaData.setAspects(aspects);

      boolean ignoreLargeMetadata =
          (typeIndexFilter.shouldBeIgnored(getNodeType(nodeId))
              || aspectIndexFilter.shouldBeIgnored(getNodeAspects(nodeId)));
      if (!ignoreLargeMetadata
          && (typeIndexFilter.isIgnorePathsForSpecificTypes()
              || aspectIndexFilter.isIgnorePathsForSpecificAspects())) {
        // check if parent should be ignored
        final List<Long> parentIds = new LinkedList<Long>();
        nodeDAO.getParentAssocs(
            nodeId,
            null,
            null,
            true,
            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) {
                parentIds.add(parentNodePair.getFirst());
                return false;
              }

              @Override
              public void done() {}
            });

        if (!parentIds.isEmpty()) {
          Long parentId = parentIds.iterator().next();
          if (typeIndexFilter.isIgnorePathsForSpecificTypes()) {
            QName parentType = getNodeType(parentId);
            ignoreLargeMetadata = typeIndexFilter.shouldBeIgnored(parentType);
          }
          if (!ignoreLargeMetadata && aspectIndexFilter.isIgnorePathsForSpecificAspects()) {
            ignoreLargeMetadata = aspectIndexFilter.shouldBeIgnored(getNodeAspects(parentId));
          }
        }
      }

      CategoryPaths categoryPaths =
          new CategoryPaths(
              new ArrayList<Pair<Path, QName>>(), new ArrayList<ChildAssociationRef>());
      if (!ignoreLargeMetadata && (includePaths || includeParentAssociations)) {
        if (props == null) {
          props = getProperties(nodeId);
        }
        categoryPaths = getCategoryPaths(status.getNodeRef(), aspects, props);
      }

      if (includePaths && !ignoreLargeMetadata) {
        if (props == null) {
          props = getProperties(nodeId);
        }

        List<Path> directPaths =
            nodeDAO.getPaths(new Pair<Long, NodeRef>(nodeId, status.getNodeRef()), false);

        Collection<Pair<Path, QName>> paths =
            new ArrayList<Pair<Path, QName>>(directPaths.size() + categoryPaths.getPaths().size());
        for (Path path : directPaths) {
          paths.add(new Pair<Path, QName>(path.getBaseNamePath(tenantService), null));
        }
        for (Pair<Path, QName> catPair : categoryPaths.getPaths()) {
          paths.add(
              new Pair<Path, QName>(
                  catPair.getFirst().getBaseNamePath(tenantService), catPair.getSecond()));
        }

        nodeMetaData.setPaths(paths);

        // Calculate name path
        Collection<Collection<String>> namePaths = new ArrayList<Collection<String>>(2);
        nodeMetaData.setNamePaths(namePaths);
        for (Pair<Path, QName> catPair : paths) {
          Path path = catPair.getFirst();

          boolean added = false;
          List<String> namePath = new ArrayList<String>(path.size());
          NEXT_ELEMENT:
          for (Path.Element pathElement : path) {
            if (!(pathElement instanceof ChildAssocElement)) {
              // This is some path element that is terminal to a cm:name path
              break;
            }
            ChildAssocElement pathChildAssocElement = (ChildAssocElement) pathElement;
            NodeRef childNodeRef = pathChildAssocElement.getRef().getChildRef();
            Pair<Long, NodeRef> childNodePair = nodeDAO.getNodePair(childNodeRef);
            if (childNodePair == null) {
              // Gone
              break;
            }
            Long childNodeId = childNodePair.getFirst();
            String childNodeName =
                (String) nodeDAO.getNodeProperty(childNodeId, ContentModel.PROP_NAME);
            if (childNodeName == null) {
              // We have hit a non-name node, which acts as a root for cm:name
              // DH: There is no particular constraint here.  This is just a decision made.
              namePath.clear();
              // We have to continue down the path as there could be a name path lower down
              continue NEXT_ELEMENT;
            }
            // We can finally add the name to the path
            namePath.add(childNodeName);
            // Add the path if this is the first entry in the name path
            if (!added) {
              namePaths.add(namePath);
              added = true;
            }
          }
        }
      }

      nodeMetaData.setTenantDomain(tenantService.getDomain(nodeRef.getStoreRef().getIdentifier()));

      if (includeChildAssociations) {
        final List<ChildAssociationRef> childAssocs = new ArrayList<ChildAssociationRef>(100);
        nodeDAO.getChildAssocs(
            nodeId,
            null,
            null,
            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) {
                boolean addCurrentChildAssoc = true;
                if (typeIndexFilter.isIgnorePathsForSpecificTypes()) {
                  QName nodeType = nodeDAO.getNodeType(childNodePair.getFirst());
                  addCurrentChildAssoc = !typeIndexFilter.shouldBeIgnored(nodeType);
                }
                if (!addCurrentChildAssoc && aspectIndexFilter.isIgnorePathsForSpecificAspects()) {
                  addCurrentChildAssoc =
                      !aspectIndexFilter.shouldBeIgnored(getNodeAspects(childNodePair.getFirst()));
                }
                if (addCurrentChildAssoc) {
                  childAssocs.add(tenantService.getBaseName(childAssocPair.getSecond(), true));
                }
                return true;
              }

              @Override
              public void done() {}
            });
        nodeMetaData.setChildAssocs(childAssocs);
      }

      if (includeChildIds) {
        final List<Long> childIds = new ArrayList<Long>(100);
        nodeDAO.getChildAssocs(
            nodeId,
            null,
            null,
            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) {
                boolean addCurrentId = true;
                if (typeIndexFilter.isIgnorePathsForSpecificTypes()) {
                  QName nodeType = nodeDAO.getNodeType(childNodePair.getFirst());
                  addCurrentId = !typeIndexFilter.shouldBeIgnored(nodeType);
                }
                if (!addCurrentId) {
                  addCurrentId =
                      !aspectIndexFilter.shouldBeIgnored(getNodeAspects(childNodePair.getFirst()));
                }
                if (addCurrentId) {
                  childIds.add(childNodePair.getFirst());
                }
                return true;
              }

              @Override
              public void done() {}
            });
        nodeMetaData.setChildIds(childIds);
      }

      if (includeParentAssociations && !ignoreLargeMetadata) {
        final List<ChildAssociationRef> parentAssocs = new ArrayList<ChildAssociationRef>(100);
        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) {
                parentAssocs.add(tenantService.getBaseName(childAssocPair.getSecond(), true));
                return true;
              }

              @Override
              public void done() {}
            });
        for (ChildAssociationRef ref : categoryPaths.getCategoryParents()) {
          parentAssocs.add(tenantService.getBaseName(ref, true));
        }

        CRC32 crc = new CRC32();
        for (ChildAssociationRef car : parentAssocs) {
          try {
            crc.update(car.toString().getBytes("UTF-8"));
          } catch (UnsupportedEncodingException e) {
            throw new RuntimeException("UTF-8 encoding is not supported");
          }
        }
        nodeMetaData.setParentAssocs(parentAssocs, crc.getValue());

        // TODO non-child associations
        //                Collection<Pair<Long, AssociationRef>> sourceAssocs =
        // nodeDAO.getSourceNodeAssocs(nodeId);
        //                Collection<Pair<Long, AssociationRef>> targetAssocs =
        // nodeDAO.getTargetNodeAssocs(nodeId);
        //
        //                nodeMetaData.setAssocs();
      }

      if (includeOwner) {
        // cached in OwnableService
        nodeMetaData.setOwner(ownableService.getOwner(status.getNodeRef()));
      }

      rowHandler.processResult(nodeMetaData);
    }
  }