/* (non-Javadoc)
   * @see org.alfresco.repo.tenant.TenantService#getRootNode(org.alfresco.service.cmr.repository.NodeService, org.alfresco.service.cmr.search.SearchService, org.alfresco.service.namespace.NamespaceService, java.lang.String, org.alfresco.service.cmr.repository.NodeRef)
   */
  public NodeRef getRootNode(
      NodeService nodeService,
      SearchService searchService,
      NamespaceService namespaceService,
      String rootPath,
      NodeRef rootNodeRef) {
    // Check that all the passed values are not null
    ParameterCheck.mandatory("NodeService", nodeService);
    ParameterCheck.mandatory("SearchService", searchService);
    ParameterCheck.mandatory("NamespaceService", namespaceService);
    ParameterCheck.mandatory("RootPath", rootPath);
    ParameterCheck.mandatory("RootNodeRef", rootNodeRef);

    String username = AuthenticationUtil.getFullyAuthenticatedUser();
    StoreRef storeRef = getName(username, rootNodeRef.getStoreRef());

    AuthenticationUtil.RunAsWork<NodeRef> action =
        new GetRootNode(
            nodeService, searchService, namespaceService, rootPath, rootNodeRef, storeRef);
    return getBaseName(AuthenticationUtil.runAs(action, AuthenticationUtil.getSystemUserName()));
  }
  /* (non-Javadoc)
   * @see org.alfresco.repo.tenant.TenantService#getName(org.alfresco.service.cmr.repository.NodeRef)
   */
  public NodeRef getName(NodeRef nodeRef) {
    if (nodeRef == null) {
      return null;
    }

    return new NodeRef(
        nodeRef.getStoreRef().getProtocol(),
        getName(nodeRef.getStoreRef().getIdentifier()),
        nodeRef.getId());
  }
  /* (non-Javadoc)
   * @see org.alfresco.repo.tenant.TenantService#getName(org.alfresco.service.cmr.repository.NodeRef, org.alfresco.service.namespace.QName)
   */
  public QName getName(NodeRef inNodeRef, QName name) {
    // Check that all the passed values are not null
    ParameterCheck.mandatory("InNodeRef", inNodeRef);
    ParameterCheck.mandatory("Name", name);

    int idx = inNodeRef.getStoreRef().getIdentifier().lastIndexOf(SEPARATOR);
    if (idx != -1) {
      String tenantDomain = inNodeRef.getStoreRef().getIdentifier().substring(1, idx);
      checkTenantEnabled(tenantDomain);
      return getName(name, tenantDomain);
    }

    return name;
  }
  /* (non-Javadoc)
   * @see org.alfresco.repo.tenant.TenantService#getName(org.alfresco.service.cmr.repository.NodeRef, org.alfresco.service.cmr.repository.NodeRef)
   */
  public NodeRef getName(NodeRef inNodeRef, NodeRef nodeRef) {
    if (inNodeRef == null || nodeRef == null) {
      return null;
    }

    int idx = inNodeRef.getStoreRef().getIdentifier().lastIndexOf(SEPARATOR);
    if (idx != -1) {
      String tenantDomain = inNodeRef.getStoreRef().getIdentifier().substring(1, idx);
      return new NodeRef(
          nodeRef.getStoreRef().getProtocol(),
          getName(nodeRef.getStoreRef().getIdentifier(), tenantDomain),
          nodeRef.getId());
    }

    return nodeRef;
  }
  /** @return Returns true if the pattern is present, otherwise false. */
  public boolean like(
      NodeRef nodeRef, QName propertyQName, String sqlLikePattern, boolean includeFTS) {
    if (propertyQName == null) {
      throw new IllegalArgumentException("Property QName is mandatory for the like expression");
    }

    StringBuilder sb = new StringBuilder(sqlLikePattern.length() * 3);

    if (includeFTS) {
      // convert the SQL-like pattern into a Lucene-compatible string
      String pattern =
          SearchLanguageConversion.convertXPathLikeToLucene(sqlLikePattern.toLowerCase());

      // build Lucene search string specific to the node
      sb = new StringBuilder();
      sb.append("+ID:\"").append(nodeRef.toString()).append("\" +(");
      // FTS or attribute matches
      if (includeFTS) {
        sb.append("TEXT:(").append(pattern).append(") ");
      }
      if (propertyQName != null) {
        sb.append(" @")
            .append(
                SearchLanguageConversion.escapeLuceneQuery(
                    QName.createQName(
                            propertyQName.getNamespaceURI(),
                            ISO9075.encode(propertyQName.getLocalName()))
                        .toString()))
            .append(":(")
            .append(pattern)
            .append(")");
      }
      sb.append(")");

      ResultSet resultSet = null;
      try {
        resultSet = this.query(nodeRef.getStoreRef(), "lucene", sb.toString());
        boolean answer = resultSet.length() > 0;
        return answer;
      } finally {
        if (resultSet != null) {
          resultSet.close();
        }
      }
    } else {
      // convert the SQL-like pattern into a Lucene-compatible string
      String pattern =
          SearchLanguageConversion.convertXPathLikeToRegex(sqlLikePattern.toLowerCase());

      Serializable property = nodeService.getProperty(nodeRef, propertyQName);
      if (property == null) {
        return false;
      } else {
        String propertyString =
            DefaultTypeConverter.INSTANCE.convert(
                String.class, nodeService.getProperty(nodeRef, propertyQName));
        return propertyString.toLowerCase().matches(pattern);
      }
    }
  }
  /**
   * @see
   *     org.alfresco.service.cmr.coci.CheckOutCheckInService#getWorkingCopy(org.alfresco.service.cmr.repository.NodeRef)
   */
  public NodeRef getWorkingCopy(NodeRef nodeRef) {
    NodeRef workingCopy = null;

    // Do a search to find the working copy document
    ResultSet resultSet = null;

    try {
      resultSet =
          this.searchService.query(
              nodeRef.getStoreRef(),
              SearchService.LANGUAGE_LUCENE,
              "+ASPECT:\""
                  + ContentModel.ASPECT_WORKING_COPY.toString()
                  + "\" +@\\{http\\://www.alfresco.org/model/content/1.0\\}"
                  + ContentModel.PROP_COPY_REFERENCE.getLocalName()
                  + ":\""
                  + nodeRef.toString()
                  + "\"");
      if (resultSet.getNodeRefs().size() != 0) {
        workingCopy = resultSet.getNodeRef(0);
      }
    } finally {
      if (resultSet != null) {
        resultSet.close();
      }
    }

    return workingCopy;
  }
  private AuditMode onApplicationAudit(
      AuditMode auditMode,
      AuditState auditInfo,
      String source,
      String description,
      NodeRef key,
      Object... args) {
    AuditMode effectiveAuditMode =
        auditModel.beforeExecution(auditMode, source, description, key, args);
    auditModel.getAuditRecordOptions(source);
    if (auditMode != AuditMode.NONE) {
      if (source.equals(SYSTEM_APPLICATION)) {
        throw new AuditException(
            "Application audit can not use the reserved identifier " + SYSTEM_APPLICATION);
      }

      auditInfo.setAuditApplication(source);
      auditInfo.setAuditConfiguration(auditConfiguration);
      auditInfo.setAuditMethod(null);
      auditInfo.setAuditService(null);
      auditInfo.setClientAddress(null);
      auditInfo.setDate(new Date());
      auditInfo.setFail(false);
      auditInfo.setFiltered(false);
      auditInfo.setHostAddress(auditHost);
      auditInfo.setPath(null);
      if (key != null) {
        auditInfo.setKeyStore(key.getStoreRef());
        auditInfo.setKeyGUID(key.getId());
        RecordOptions recordOptions = auditModel.getAuditRecordOptions(source);
        if (recordOptions != null && recordOptions.getRecordPath() == TrueFalseUnset.TRUE) {
          auditInfo.setPath(getNodePath(key));
        }
      }
      auditInfo.setKeyPropertiesAfter(null);
      auditInfo.setKeyPropertiesBefore(null);
      auditInfo.setMessage(description);
      if (args != null) {
        Serializable[] serArgs = new Serializable[args.length];
        for (int i = 0; i < args.length; i++) {
          if (args[i] == null) {
            serArgs[i] = null;
          } else if (args[i] instanceof Serializable) {
            serArgs[i] = (Serializable) args[i];
          } else {
            serArgs[i] = args[i].toString();
          }
        }
        auditInfo.setMethodArguments(serArgs);
      }
      auditInfo.setReturnObject(null);
      auditInfo.setSessionId(null);
      auditInfo.setThrowable(null);
      auditInfo.setTxId(AlfrescoTransactionSupport.getTransactionId());
      auditInfo.setUserIdentifier(AuthenticationUtil.getFullyAuthenticatedUser());
    }

    return effectiveAuditMode;
  }
  /**
   * Helper method to set audited information after method invocation and to determine if auditing
   * should take place based on the method return value.
   *
   * @param auditMode
   * @param auditInfo
   * @param mi
   * @param returnObject
   * @return - the audit mode.
   */
  private AuditMode postInvocation(
      AuditMode auditMode, AuditState auditInfo, MethodInvocation mi, Object returnObject) {
    if (returnObject == null) {
      auditInfo.setReturnObject(null);
    } else if (returnObject instanceof Serializable) {
      auditInfo.setReturnObject((Serializable) returnObject);
    } else {
      auditInfo.setReturnObject(returnObject.toString());
    }

    Auditable auditable = mi.getMethod().getAnnotation(Auditable.class);
    if (auditable.key() == Auditable.Key.RETURN) {
      if (returnObject != null) {
        if (returnObject instanceof NodeRef) {
          NodeRef key = (NodeRef) returnObject;
          auditInfo.setKeyStore(key.getStoreRef());
          auditInfo.setKeyGUID(key.getId());
          RecordOptions recordOptions = auditModel.getAuditRecordOptions(mi);
          if (recordOptions != null && recordOptions.getRecordPath() == TrueFalseUnset.TRUE) {
            auditInfo.setPath(getNodePath(key));
          }
        } else if (returnObject instanceof StoreRef) {
          auditInfo.setKeyStore((StoreRef) returnObject);
        } else if (returnObject instanceof ChildAssociationRef) {
          ChildAssociationRef car = (ChildAssociationRef) returnObject;
          auditInfo.setKeyStore(car.getChildRef().getStoreRef());
          auditInfo.setKeyGUID(car.getChildRef().getId());
          RecordOptions recordOptions = auditModel.getAuditRecordOptions(mi);
          if (recordOptions != null && recordOptions.getRecordPath() == TrueFalseUnset.TRUE) {
            auditInfo.setPath(nodeService.getPath(car.getChildRef()).toString());
          }
        } else {
          logger.warn(
              "Key argument is not a node, store or child assoc ref for return object on "
                  + publicServiceIdentifier.getPublicServiceName(mi)
                  + "."
                  + mi.getMethod().getName()
                  + " it is "
                  + returnObject.getClass().getName());
        }
      }
    }

    // If the user name is not set, try and set it after the method call.
    // This covers authentication when the user is only known after the call.

    if (auditInfo.getUserIdentifier() == null) {
      auditInfo.setUserIdentifier(AuthenticationUtil.getFullyAuthenticatedUser());
    }

    return auditMode;
  }
  /** @return Returns true if the pattern is present, otherwise false. */
  public boolean contains(
      NodeRef nodeRef,
      QName propertyQName,
      String googleLikePattern,
      SearchParameters.Operator defaultOperator) {
    ResultSet resultSet = null;
    try {
      // build Lucene search string specific to the node
      StringBuilder sb = new StringBuilder();
      sb.append("+ID:\"")
          .append(nodeRef.toString())
          .append("\" +(TEXT:(")
          .append(googleLikePattern.toLowerCase())
          .append(") ");
      if (propertyQName != null) {
        sb.append(" OR @")
            .append(
                SearchLanguageConversion.escapeLuceneQuery(
                    QName.createQName(
                            propertyQName.getNamespaceURI(),
                            ISO9075.encode(propertyQName.getLocalName()))
                        .toString()));
        sb.append(":(").append(googleLikePattern.toLowerCase()).append(")");
      } else {
        for (QName key : nodeService.getProperties(nodeRef).keySet()) {
          sb.append(" OR @")
              .append(
                  SearchLanguageConversion.escapeLuceneQuery(
                      QName.createQName(key.getNamespaceURI(), ISO9075.encode(key.getLocalName()))
                          .toString()));
          sb.append(":(").append(googleLikePattern.toLowerCase()).append(")");
        }
      }
      sb.append(")");

      SearchParameters sp = new SearchParameters();
      sp.setLanguage(SearchService.LANGUAGE_LUCENE);
      sp.setQuery(sb.toString());
      sp.setDefaultOperator(defaultOperator);
      sp.addStore(nodeRef.getStoreRef());

      resultSet = this.query(sp);
      boolean answer = resultSet.length() > 0;
      return answer;
    } finally {
      if (resultSet != null) {
        resultSet.close();
      }
    }
  }
  /* (non-Javadoc)
   * @see org.alfresco.service.cmr.view.Exporter#startReference(org.alfresco.service.cmr.repository.NodeRef, org.alfresco.service.namespace.QName)
   */
  public void startReference(NodeRef nodeRef, QName childName) {
    try {
      // determine format of reference e.g. node or path based
      ReferenceType referenceFormat = referenceType;
      if (nodeRef.equals(nodeService.getRootNode(nodeRef.getStoreRef()))) {
        referenceFormat = ReferenceType.PATHREF;
      }

      // output reference
      AttributesImpl attrs = new AttributesImpl();
      if (referenceFormat.equals(ReferenceType.PATHREF)) {
        Path path = createPath(context.getExportParent(), context.getExportParent(), nodeRef);
        attrs.addAttribute(
            NamespaceService.REPOSITORY_VIEW_1_0_URI,
            PATHREF_LOCALNAME,
            PATHREF_QNAME.toPrefixString(),
            null,
            path.toPrefixString(namespaceService));
      } else {
        attrs.addAttribute(
            NamespaceService.REPOSITORY_VIEW_1_0_URI,
            NODEREF_LOCALNAME,
            NODEREF_QNAME.toPrefixString(),
            null,
            nodeRef.toString());
      }
      if (childName != null) {
        attrs.addAttribute(
            NamespaceService.REPOSITORY_VIEW_1_0_URI,
            CHILDNAME_LOCALNAME,
            CHILDNAME_QNAME.toPrefixString(),
            null,
            childName.toPrefixString(namespaceService));
      }
      contentHandler.startElement(
          REFERENCE_QNAME.getNamespaceURI(),
          REFERENCE_LOCALNAME,
          toPrefixString(REFERENCE_QNAME),
          attrs);
    } catch (SAXException e) {
      throw new ExporterException("Failed to process start reference", e);
    }
  }
  /**
   * Return relative path between from and to references within export root
   *
   * @param fromRef from reference
   * @param toRef to reference
   * @return path
   */
  private Path createPath(NodeRef rootRef, NodeRef fromRef, NodeRef toRef) {
    // Check that item exists first
    if (!nodeService.exists(toRef)) {
      // return null path
      return null;
    }

    // Check whether item is the root node of the store
    // If so, always return absolute path
    if (toRef.equals(nodeService.getRootNode(toRef.getStoreRef()))) {
      return nodeService.getPath(toRef);
    }

    // construct relative path
    Path rootPath = createIndexedPath(rootRef, nodeService.getPath(rootRef));
    Path fromPath = createIndexedPath(fromRef, nodeService.getPath(fromRef));
    Path toPath = createIndexedPath(toRef, nodeService.getPath(toRef));
    Path relativePath = null;

    try {
      // Determine if 'to path' is a category
      // TODO: This needs to be resolved in a more appropriate manner - special support is
      //       required for categories.
      for (int i = 0; i < toPath.size(); i++) {
        Path.Element pathElement = toPath.get(i);
        if (pathElement.getPrefixedString(namespaceService).equals("cm:categoryRoot")) {
          Path.ChildAssocElement childPath = (Path.ChildAssocElement) pathElement;
          relativePath = new Path();
          relativePath.append(
              new Path.ChildAssocElement(
                  new ChildAssociationRef(null, null, null, childPath.getRef().getParentRef())));
          relativePath.append(toPath.subPath(i + 1, toPath.size() - 1));
          break;
        }
      }

      if (relativePath == null) {
        // Determine if from node is relative to export tree
        int i = 0;
        while (i < rootPath.size()
            && i < fromPath.size()
            && rootPath.get(i).equals(fromPath.get(i))) {
          i++;
        }
        if (i == rootPath.size()) {
          // Determine if to node is relative to export tree
          i = 0;
          while (i < rootPath.size()
              && i < toPath.size()
              && rootPath.get(i).equals(toPath.get(i))) {
            i++;
          }
          if (i == rootPath.size()) {
            // build relative path between from and to
            relativePath = new Path();
            for (int p = 0; p < fromPath.size() - i; p++) {
              relativePath.append(new Path.ParentElement());
            }
            if (i < toPath.size()) {
              relativePath.append(toPath.subPath(i, toPath.size() - 1));
            }
          }
        }
      }

      if (relativePath == null) {
        // default to absolute path
        relativePath = toPath;
      }
    } catch (Throwable e) {
      String msg =
          "Failed to determine relative path: root path="
              + rootPath
              + "; from path="
              + fromPath
              + "; to path="
              + toPath;
      throw new ExporterException(msg, e);
    }

    return relativePath;
  }
  /* (non-Javadoc)
   * @see org.alfresco.service.cmr.view.Exporter#value(org.alfresco.service.cmr.repository.NodeRef, org.alfresco.service.namespace.QName, java.io.Serializable)
   */
  public void value(NodeRef nodeRef, QName property, Object value, int index) {
    try {
      // determine data type of value
      QName valueDataType = null;
      PropertyDefinition propDef = dictionaryService.getProperty(property);
      DataTypeDefinition dataTypeDef = (propDef == null) ? null : propDef.getDataType();
      if (dataTypeDef == null || dataTypeDef.getName().equals(DataTypeDefinition.ANY)) {
        dataTypeDef = (value == null) ? null : dictionaryService.getDataType(value.getClass());
        if (dataTypeDef != null) {
          valueDataType = dataTypeDef.getName();
        }
      }

      // convert node references to paths
      if (value instanceof NodeRef && referenceType.equals(ReferenceType.PATHREF)) {
        NodeRef valueNodeRef = (NodeRef) value;
        if (nodeRef.getStoreRef().equals(valueNodeRef.getStoreRef())) {
          Path nodeRefPath = createPath(context.getExportOf(), nodeRef, valueNodeRef);
          value = (nodeRefPath == null) ? null : nodeRefPath.toPrefixString(namespaceService);
        }
      }

      // output value wrapper if value is null or property data type is ANY or value is part of
      // collection
      if (value == null || valueDataType != null || index != -1) {
        AttributesImpl attrs = new AttributesImpl();
        if (value == null) {
          attrs.addAttribute(
              NamespaceService.REPOSITORY_VIEW_PREFIX,
              ISNULL_LOCALNAME,
              ISNULL_QNAME.toPrefixString(),
              null,
              "true");
        }
        if (valueDataType != null) {
          attrs.addAttribute(
              NamespaceService.REPOSITORY_VIEW_PREFIX,
              DATATYPE_LOCALNAME,
              DATATYPE_QNAME.toPrefixString(),
              null,
              toPrefixString(valueDataType));
        }
        contentHandler.startElement(
            NamespaceService.REPOSITORY_VIEW_PREFIX,
            VALUE_LOCALNAME,
            toPrefixString(VALUE_QNAME),
            attrs);
      }

      // output value
      String strValue = (String) DefaultTypeConverter.INSTANCE.convert(String.class, value);
      if (strValue != null) {
        for (int i = 0; i < strValue.length(); i++) {
          char[] temp = new char[] {strValue.charAt(i)};
          contentHandler.characters(temp, 0, 1);
        }
      }

      // output value wrapper if property data type is any
      if (value == null || valueDataType != null || index != -1) {
        contentHandler.endElement(
            NamespaceService.REPOSITORY_VIEW_PREFIX, VALUE_LOCALNAME, toPrefixString(VALUE_QNAME));
      }
    } catch (SAXException e) {
      throw new ExporterException(
          "Failed to process value event - nodeRef "
              + nodeRef
              + "; property "
              + toPrefixString(property)
              + "; value "
              + value,
          e);
    }
  }
  /** {@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);
    }
  }