private ReadableStreamChannel serializeAsXml(RestRequest restRequest, Page<NamedBlobListEntry> namedBlobRecordPage)
      throws IOException, RestServiceException {
    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
    Container container = (Container) restRequest.getArgs().get(InternalKeys.TARGET_CONTAINER_KEY);
    String containerName = container.getName();
    String prefix = getHeader(restRequest.getArgs(), PREFIX_PARAM_NAME, false);
    String delimiter = getHeader(restRequest.getArgs(), DELIMITER_PARAM_NAME, false);
    String encodingType = getHeader(restRequest.getArgs(), ENCODING_TYPE_PARAM_NAME, false);
    String maxKeys = getHeader(restRequest.getArgs(), MAXKEYS_PARAM_NAME, false);
    String marker = getHeader(restRequest.getArgs(), MARKER, false);
    String continuationToken = getHeader(restRequest.getArgs(), CONTINUATION_TOKEN, false);
    //By default S3 list returns up to 1000 key names.
    int maxKeysValue = maxKeys == null ? DEFAULT_MAX_KEY_VALUE : Integer.parseInt(maxKeys);
    int keyCount = namedBlobRecordPage.getEntries().size();
    // Iterate through list of blob names.
    List<Contents> contentsList = new ArrayList<>();
    // Use LinkedHashSet to maintain order of directories.
    Set<String> dirSet = new LinkedHashSet<>();
    for (NamedBlobListEntry namedBlobRecord : namedBlobRecordPage.getEntries()) {
      if (namedBlobRecord.isDirectory()) {
        dirSet.add(namedBlobRecord.getBlobName());
        continue;
      }
      String blobName = namedBlobRecord.getBlobName();
      long blobSize = namedBlobRecord.getBlobSize();
      long modifiedTimeMs = namedBlobRecord.getModifiedTimeMs();
      if (modifiedTimeMs == -1) {
        modifiedTimeMs = System.currentTimeMillis();
      }
      ZonedDateTime zonedDateTime = Instant.ofEpochMilli(modifiedTimeMs).atZone(ZoneId.of("UTC"));
      String formattedDate = zonedDateTime.format(TIMESTAMP_FORMATTER);
      contentsList.add(new Contents(blobName, formattedDate, blobSize));
    }

    List<Prefix> commonPrefixes = new ArrayList<>();
    for (String dir : dirSet) {
      commonPrefixes.add(new Prefix(dir));
    }

    boolean enableDelimiter = delimiter != null && delimiter.equals(DELIMITER) && frontendConfig.enableDelimiter;
    if (LIST_TYPE_VERSION_2.equals(getHeader(restRequest.getArgs(), LIST_TYPE, false))) {
      ListBucketResultV2 resultV2 =
          new ListBucketResultV2(containerName, prefix, maxKeysValue, keyCount, delimiter, contentsList, encodingType,
              continuationToken, namedBlobRecordPage.getNextPageToken(), namedBlobRecordPage.getNextPageToken() != null,
              enableDelimiter ? commonPrefixes : null);
      LOGGER.debug("Sending response for S3 ListObjects {}", resultV2);
      // Serialize xml
      xmlMapper.writeValue(outputStream, resultV2);
    } else {
      ListBucketResult result =
          new ListBucketResult(containerName, prefix, maxKeysValue, keyCount, delimiter, contentsList, encodingType,
              marker, namedBlobRecordPage.getNextPageToken(), namedBlobRecordPage.getNextPageToken() != null,
              enableDelimiter ? commonPrefixes : null);
      LOGGER.debug("Sending response for S3 ListObjects {}", result);
      // Serialize xml
      xmlMapper.writeValue(outputStream, result);
    }
    return new ByteBufferReadableStreamChannel(ByteBuffer.wrap(outputStream.toByteArray()));
  }
Asynchronously get account metadata.
@param restRequest the {@link RestRequest} that contains the request parameters and body.
@param restResponseChannel the {@link RestResponseChannel} where headers should be set.
@param callback the {@link Callback} to invoke when the response is ready (or if there is an exception).
ambry-frontend/src/main/java/com/github/ambry/frontend/s3/S3ListHandler.java