protected NexusNGArtifactLink createNexusNGArtifactLink(
      final Request request,
      final String repositoryId,
      final String groupId,
      final String artifactId,
      final String version,
      final String extension,
      final String classifier) {
    NexusNGArtifactLink link = new NexusNGArtifactLink();

    link.setExtension(extension);

    link.setClassifier(classifier);

    return link;
  }
  protected void repackIteratorSearchResponse(
      Request request,
      Map<String, String> terms,
      SearchNGResponse response,
      boolean collapsed,
      Integer from,
      int count,
      IteratorSearchResponse iterator,
      SystemWideLatestVersionCollector systemWideCollector,
      RepositoryWideLatestVersionCollector repositoryWideCollector)
      throws NoSuchRepositoryException, IOException {
    response.setCollapsed(collapsed);

    response.setTotalCount(iterator.getTotalHitsCount());

    response.setFrom(from == null ? -1 : from.intValue());

    response.setCount(count == LUCENE_HIT_LIMIT ? -1 : count);

    // System.out.println( "** Query is \"" + iterator.getQuery().toString() + "\"." );

    try {
      if (!response.isTooManyResults()) {
        // 1st pass, collect results
        LinkedHashMap<String, NexusNGArtifact> hits = new LinkedHashMap<String, NexusNGArtifact>();

        NexusNGArtifact artifact;

        float firstDocumentScore = -1f;

        float lastDocumentScore = -1f;

        final long startedAtMillis = System.currentTimeMillis();

        // 1sd pass, build first two level (no links), and actually consume the iterator and
        // collectors will be
        // set
        for (ArtifactInfo ai : iterator) {
          final String key = ai.groupId + ":" + ai.artifactId + ":" + ai.version;

          artifact = hits.get(key);

          // System.out.println( "* " + ai.context + " : " + ai.toString() + " -- " +
          // ai.getLuceneScore() +
          // " -- "
          // + ( artifact != null ? "F" : "N" ) );

          if (artifact == null) {
            if (System.currentTimeMillis() - startedAtMillis > FIRST_LOOP_EXECUTION_TIME_LIMIT) {
              getSearchDiagnosticLogger()
                  .debug(
                      "Stopping delivering search results since we spent more than "
                          + FIRST_LOOP_EXECUTION_TIME_LIMIT
                          + " millis in 1st loop processing results.");

              break;
            }

            // we stop if we delivered "most important" hits (change of relevance from 1st document
            // we got)
            if (hits.size() > 10
                && (firstDocumentScore - ai.getLuceneScore())
                    > DOCUMENT_TOP_RELEVANCE_HIT_CHANGE_THRESHOLD) {
              getSearchDiagnosticLogger()
                  .debug(
                      "Stopping delivering search results since we span "
                          + DOCUMENT_TOP_RELEVANCE_HIT_CHANGE_THRESHOLD
                          + " of score change (firstDocScore="
                          + firstDocumentScore
                          + ", currentDocScore="
                          + ai.getLuceneScore()
                          + ").");

              break;
            }

            // we stop if we detect a "big drop" in relevance in relation to previous document's
            // score
            if (hits.size() > 10 && lastDocumentScore > 0) {
              if ((lastDocumentScore - ai.getLuceneScore())
                  > DOCUMENT_RELEVANCE_HIT_CHANGE_THRESHOLD) {
                getSearchDiagnosticLogger()
                    .debug(
                        "Stopping delivering search results since we hit a relevance drop bigger than "
                            + DOCUMENT_RELEVANCE_HIT_CHANGE_THRESHOLD
                            + " (lastDocScore="
                            + lastDocumentScore
                            + ", currentDocScore="
                            + ai.getLuceneScore()
                            + ").");

                // the relevance change was big, so we stepped over "trash" results that are
                // probably not relevant at all, just stop here then
                break;
              }
            }

            // we stop if we hit the GA limit
            if ((hits.size() + 1) > GA_HIT_LIMIT) {
              getSearchDiagnosticLogger()
                  .debug(
                      "Stopping delivering search results since we hit a GA hit limit of "
                          + GA_HIT_LIMIT
                          + ".");

              // check for HIT_LIMIT: if we are stepping it over, stop here
              break;
            } else {
              artifact = new NexusNGArtifact();

              artifact.setGroupId(ai.groupId);

              artifact.setArtifactId(ai.artifactId);

              artifact.setVersion(ai.version);

              artifact.setHighlightedFragment(getMatchHighlightHtmlSnippet(ai));

              hits.put(key, artifact);
            }
          }

          Repository repository = getUnprotectedRepositoryRegistry().getRepository(ai.repository);

          addRepositoryDetails(request, response, repository);

          NexusNGArtifactHit hit = null;

          for (NexusNGArtifactHit artifactHit : artifact.getArtifactHits()) {
            if (repository.getId().equals(artifactHit.getRepositoryId())) {
              hit = artifactHit;

              break;
            }
          }

          if (hit == null) {
            hit = new NexusNGArtifactHit();

            hit.setRepositoryId(repository.getId());

            // if collapsed, we add links in 2nd pass, otherwise here
            if (!collapsed) {
              // we are adding the POM link "blindly", unless packaging is POM,
              // since the it will be added below the "usual" way
              if (!"pom".equals(ai.packaging)) {
                NexusNGArtifactLink link =
                    createNexusNGArtifactLink(
                        request, ai.repository, ai.groupId, ai.artifactId, ai.version, "pom", null);

                // add the POM link
                hit.addArtifactLink(link);
              }
            }

            // we just created it, add it
            artifact.addArtifactHit(hit);
          }

          if (!collapsed) {
            boolean needsToBeAdded = true;

            for (NexusNGArtifactLink link : hit.getArtifactLinks()) {
              if (StringUtils.equals(link.getClassifier(), ai.classifier)
                  && StringUtils.equals(link.getExtension(), ai.fextension)) {
                needsToBeAdded = false;

                break;
              }
            }

            if (needsToBeAdded) {
              NexusNGArtifactLink link =
                  createNexusNGArtifactLink(
                      request,
                      ai.repository,
                      ai.groupId,
                      ai.artifactId,
                      ai.version,
                      ai.fextension,
                      ai.classifier);

              hit.addArtifactLink(link);
            }
          }

          if (firstDocumentScore < 0) {
            firstDocumentScore = ai.getLuceneScore();
          }

          lastDocumentScore = ai.getLuceneScore();
        }

        // summary:
        getSearchDiagnosticLogger()
            .debug(
                "Query terms \""
                    + terms
                    + "\" (LQL \""
                    + iterator.getQuery()
                    + "\") matched total of "
                    + iterator.getTotalHitsCount()
                    + " records, "
                    + iterator.getTotalProcessedArtifactInfoCount()
                    + " records were processed out of those, resulting in "
                    + hits.size()
                    + " unique GA records. Lucene scored documents first="
                    + firstDocumentScore
                    + ", last="
                    + lastDocumentScore
                    + ". Main processing loop took "
                    + (System.currentTimeMillis() - startedAtMillis)
                    + " ms.");

        // 2nd pass, set versions
        for (NexusNGArtifact artifactNg : hits.values()) {
          final String systemWideCollectorKey =
              systemWideCollector.getKey(artifactNg.getGroupId(), artifactNg.getArtifactId());

          LatestVersionHolder systemWideHolder =
              systemWideCollector.getLVHForKey(systemWideCollectorKey);

          if (systemWideHolder != null) {
            if (systemWideHolder.getLatestSnapshot() != null) {
              artifactNg.setLatestSnapshot(systemWideHolder.getLatestSnapshot().toString());

              artifactNg.setLatestSnapshotRepositoryId(
                  systemWideHolder.getLatestSnapshotRepositoryId());
            }

            if (systemWideHolder.getLatestRelease() != null) {
              artifactNg.setLatestRelease(systemWideHolder.getLatestRelease().toString());

              artifactNg.setLatestReleaseRepositoryId(
                  systemWideHolder.getLatestReleaseRepositoryId());
            }
          }

          // add some "touche" on 1st level
          if (collapsed) {
            // set the top level version to one of the latest ones
            if (artifactNg.getLatestRelease() != null) {
              artifactNg.setVersion(artifactNg.getLatestRelease());
            } else {
              artifactNg.setVersion(artifactNg.getLatestSnapshot());
            }

            // "create" the links now
            for (NexusNGArtifactHit hit : artifactNg.getArtifactHits()) {
              final String repositoryWideCollectorKey =
                  repositoryWideCollector.getKey(
                      hit.getRepositoryId(), artifactNg.getGroupId(), artifactNg.getArtifactId());

              LatestECVersionHolder repositoryWideHolder =
                  repositoryWideCollector.getLVHForKey(repositoryWideCollectorKey);

              if (repositoryWideHolder != null) {
                String versionToSet = null;

                // do we have a "latest release" version?
                if (repositoryWideHolder.getLatestRelease() != null) {
                  versionToSet = repositoryWideHolder.getLatestRelease().toString();
                } else {
                  versionToSet = repositoryWideHolder.getLatestSnapshot().toString();
                }

                // add POM link
                NexusNGArtifactLink pomLink =
                    createNexusNGArtifactLink(
                        request,
                        hit.getRepositoryId(),
                        artifactNg.getGroupId(),
                        artifactNg.getArtifactId(),
                        versionToSet,
                        "pom",
                        null);

                hit.addArtifactLink(pomLink);

                // TODO: order!
                // add main artifact link
                // add everything else

                // make the list by joining two collections
                // rationale: in case of reposes, only one of these will be populated, other will be
                // empty
                // but in case of mixed policy (like group), probably both will exist
                // TODO: this will not work like it in groups, since then the versions will
                // mismatch!
                ArrayList<ECHolder> ecHolders =
                    new ArrayList<ECHolder>(repositoryWideHolder.getReleaseECHolders());
                ecHolders.addAll(repositoryWideHolder.getSnapshotECHolders());

                for (ECHolder holder : ecHolders) {
                  // add non-poms only, since we added POMs above
                  if (!"pom".equals(holder.getExtension())) {
                    NexusNGArtifactLink link =
                        createNexusNGArtifactLink(
                            request,
                            hit.getRepositoryId(),
                            artifactNg.getGroupId(),
                            artifactNg.getArtifactId(),
                            versionToSet,
                            holder.getExtension(),
                            holder.getClassifier());

                    hit.addArtifactLink(link);
                  }
                }
              }
            }
          }
        }

        response.setData(new ArrayList<NexusNGArtifact>(hits.values()));
      }
    } finally {
      iterator.close();
    }

    response.setTooManyResults(iterator.getTotalHitsCount() > count);
  }