private void finalizeMetadataChunk() {
      finalBlobProperties =
          new BlobProperties(getBlobSize(), passedInBlobProperties.getServiceId(), passedInBlobProperties.getOwnerId(),
              passedInBlobProperties.getContentType(), passedInBlobProperties.isPrivate(),
              passedInBlobProperties.getTimeToLiveInSeconds(), passedInBlobProperties.getCreationTimeInMs(),
              passedInBlobProperties.getAccountId(), passedInBlobProperties.getContainerId(),
              passedInBlobProperties.isEncrypted(), passedInBlobProperties.getExternalAssetTag(),
              passedInBlobProperties.getContentEncoding(), passedInBlobProperties.getFilename(), null);

      if (options.skipCompositeChunk()) {
        // close the request as the single blob. we don't generate the composite blob.
        // we set first chunk id to the blobid since it's not supposed to be null. But we return putBlobMetaInfo only.
        blobId = (BlobId) indexToChunkIdsAndChunkSizes.get(0).getFirst();
        state = ChunkState.Complete;
        setOperationCompleted();

        // now generate the PutBlobMetaInfo which includes the data chunk list
        // values returned are in the right order as TreeMap returns them in key-order.
        // .stream.map.collect is supposed to return in order
        List<Pair<String, Long>> orderedChunkList = new ArrayList<>(indexToChunkIdsAndChunkSizes.values()
            .stream()
            .map(p -> new Pair<>(p.getFirst().toString(), p.getSecond()))
            .collect(Collectors.toList()));
        // S3 Part Upload, we don't reserve metadata chunk id for each data chunk. Refer to resolveReservedMetadataId.
        PutBlobMetaInfo putBlobMetaInfoObj = new PutBlobMetaInfo(orderedChunkList, null);
        putBlobMetaInfo = PutBlobMetaInfo.serialize(putBlobMetaInfoObj);
        logger.debug("S3 5MB : finalizeMetadataChunk orderedChunkIdList is {} {}", indexToChunkIdsAndChunkSizes,
            putBlobMetaInfo);
      } else if (isStitchOperation() || getNumDataChunks() > 1) {
        ByteBuffer serialized = null;
        // values returned are in the right order as TreeMap returns them in key-order.
        if (routerConfig.routerMetadataContentVersion == MessageFormatRecord.Metadata_Content_Version_V2) {
          serialized = MetadataContentSerDe.serializeMetadataContentV2(intermediateChunkSize, getBlobSize(),
              getSuccessfullyPutChunkIds());
        } else if (routerConfig.routerMetadataContentVersion == MessageFormatRecord.Metadata_Content_Version_V3) {
          List<Pair<StoreKey, Long>> orderedChunkIdList = new ArrayList<>(indexToChunkIdsAndChunkSizes.values());
          serialized = MetadataContentSerDe.serializeMetadataContentV3(getBlobSize(), orderedChunkIdList);
        } else {
          throw new IllegalStateException(
              "Unexpected metadata content version: " + routerConfig.routerMetadataContentVersion);
        }
        serialized.flip();
        buf = Unpooled.wrappedBuffer(serialized);
        onFillComplete(false);
        checkReservedPartitionState();
      } else {
        // if there is only one chunk
        blobId = (BlobId) indexToChunkIdsAndChunkSizes.get(0).getFirst();
        state = ChunkState.Complete;
        setOperationCompleted();
      }
    }
To be called when chunk filling completes successfully. Finalizing involves preparing the metadata chunk
for sending if this blob is composite, or marking the operation complete if this is a simple blob.
ambry-router/src/main/java/com/github/ambry/router/PutOperation.java