private void handleCompleteMultipartUpload(HttpServletRequest request,
            HttpServletResponse response, InputStream is,
            final BlobStore blobStore, String containerName, String blobName,
            String uploadId) throws IOException, S3Exception {
        BlobMetadata metadata;
        PutOptions options;
        if (Quirks.MULTIPART_REQUIRES_STUB.contains(getBlobStoreType(
                blobStore))) {
            metadata = blobStore.blobMetadata(containerName, uploadId);
            BlobAccess access = blobStore.getBlobAccess(containerName,
                    uploadId);
            options = new PutOptions().setBlobAccess(access);
        } else {
            metadata = new MutableBlobMetadataImpl();
            options = new PutOptions();
        }
        final MultipartUpload mpu = MultipartUpload.create(containerName,
                blobName, uploadId, metadata, options);


        final List<MultipartPart> parts = new ArrayList<>();
        String blobStoreType = getBlobStoreType(blobStore);
        if (blobStoreType.equals("azureblob") ||
                blobStoreType.equals("azureblob-sdk")) {
            // TODO: how to sanity check parts?
            for (MultipartPart part : blobStore.listMultipartUpload(mpu)) {
                parts.add(part);
            }
        } else if (blobStoreType.equals("google-cloud-storage")) {
            // GCS only supports 32 parts but we can support up to 1024 by
            // recursively combining objects.
            for (int partNumber = 1;; ++partNumber) {
                MultipartUpload mpu2 = MultipartUpload.create(
                        containerName,
                        String.format("%s_%08d", mpu.id(), partNumber),
                        String.format("%s_%08d", mpu.id(), partNumber),
                        metadata, options);
                List<MultipartPart> subParts = blobStore.listMultipartUpload(
                        mpu2);
                if (subParts.isEmpty()) {
                    break;
                }
                long partSize = 0;
                for (MultipartPart part : subParts) {
                    partSize += part.partSize();
                }
                String eTag = blobStore.completeMultipartUpload(mpu2, subParts);
                parts.add(MultipartPart.create(
                        partNumber, partSize, eTag, /*lastModified=*/ null));
            }
        } else {
            // List parts to get part sizes and to map multiple Azure parts
            // into single parts.
            var partsByListing =
                blobStore.listMultipartUpload(mpu).stream().collect(
                        Collectors.toMap(
                                part -> part.partNumber(),
                                part -> part));
            CompleteMultipartUploadRequest cmu;
            try {
                cmu = mapper.readValue(
                        is, CompleteMultipartUploadRequest.class);
            } catch (JsonParseException jpe) {
                throw new S3Exception(S3ErrorCode.MALFORMED_X_M_L, jpe);
            }

            // use TreeMap to allow runt last part
            SortedMap<Integer, String> requestParts = new TreeMap<>();
            if (cmu.parts != null) {
                for (CompleteMultipartUploadRequest.Part part : cmu.parts) {
                    requestParts.put(part.partNumber, part.eTag);
                }
            }

            for (var it = requestParts.entrySet().iterator(); it.hasNext();) {
                var entry = it.next();
                MultipartPart part = partsByListing.get(entry.getKey());
                if (part == null) {
                    throw new S3Exception(S3ErrorCode.INVALID_PART);
                }
                long partSize = part.partSize();
                if (it.hasNext() && partSize != -1 &&
                        (partSize < 5 * 1024 * 1024 || partSize <
                                blobStore.getMinimumMultipartPartSize())) {
                    throw new S3Exception(S3ErrorCode.ENTITY_TOO_SMALL);
                }
                if (part.partETag() != null &&
                        !equalsIgnoringSurroundingQuotes(part.partETag(),
                                entry.getValue())) {
                    throw new S3Exception(S3ErrorCode.INVALID_PART);
                }
                parts.add(MultipartPart.create(entry.getKey(),
                        partSize, part.partETag(), part.lastModified()));
            }
        }

        if (parts.isEmpty()) {
            // Amazon requires at least one part
            throw new S3Exception(S3ErrorCode.MALFORMED_X_M_L);
        }

        response.setCharacterEncoding(UTF_8);
        addCorsResponseHeader(request, response);
        try (PrintWriter writer = response.getWriter()) {
            response.setStatus(HttpServletResponse.SC_OK);
            response.setContentType(XML_CONTENT_TYPE);

            // Launch async thread to allow main thread to emit newlines to
            // the client while completeMultipartUpload processes.
            final AtomicReference<String> eTag = new AtomicReference<>();
            final AtomicReference<RuntimeException> exception =
                    new AtomicReference<>();
            var thread = new Thread() {
                @Override
                public void run() {
                    try {
                        eTag.set(blobStore.completeMultipartUpload(mpu, parts));
                    } catch (RuntimeException re) {
                        exception.set(re);
                    }
                }
            };
            thread.start();

            XMLStreamWriter xml = xmlOutputFactory.createXMLStreamWriter(
                    writer);
            xml.writeStartDocument();
            xml.writeStartElement("CompleteMultipartUploadResult");
            xml.writeDefaultNamespace(AWS_XMLNS);
            xml.flush();

            while (thread.isAlive()) {
                try {
                    thread.join(1000);
                } catch (InterruptedException ie) {
                    // ignore
                }
                writer.write("\n");
                writer.flush();
            }

            if (exception.get() != null) {
                throw exception.get();
            }

            if (Quirks.MULTIPART_REQUIRES_STUB.contains(getBlobStoreType(
                    blobStore))) {
                blobStore.removeBlob(containerName, uploadId);
            }

            // TODO: bogus value
            writeSimpleElement(xml, "Location",
                    "http://Example-Bucket.s3.amazonaws.com/" + blobName);

            writeSimpleElement(xml, "Bucket", containerName);
            writeSimpleElement(xml, "Key", blobName);

            if (eTag.get() != null) {
                writeSimpleElement(xml, "ETag", maybeQuoteETag(eTag.get()));
            }

            xml.writeEndElement();
            xml.flush();
        } catch (XMLStreamException xse) {
            throw new IOException(xse);
        }
    }
Map XML ACLs to a canned policy if an exact tranformation exists.
src/main/java/org/gaul/s3proxy/S3ProxyHandler.java