All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.microsoft.azure.storage.BatchOperation Maven / Gradle / Ivy

There is a newer version: 8.6.6
Show newest version
package com.microsoft.azure.storage;

import com.microsoft.azure.storage.core.BaseRequest;
import com.microsoft.azure.storage.core.StorageRequest;
import com.microsoft.azure.storage.core.Utility;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.nio.charset.StandardCharsets;
import java.util.*;

/**
 * A collection of operations to be sent as a batch request. Maintains the order of requests as added to the batch.
 *
 * @param 
 *     The ServiceClient type of the Storage service this batch targets.
 * @param 

* The type of the parent object making the REST call. * @param * The return type of the individual requests on the batch. */ public abstract class BatchOperation implements Iterable, P>> { private static final String HTTP_LINE_ENDING = "\r\n"; private static final String HTTP_MIXED_MULTIPART_CONTENT_TYPE = "multipart/mixed"; private static final String HTTP_MIXED_MULTIPART_DELIMITER_DEFINITION = "boundary="; private static final String STORAGE_BATCH_DELIMITER_PREFIX = "batch_"; private final Map, P> subOperations = new LinkedHashMap<>(); // maintains iteration order private final UUID batchId = UUID.randomUUID(); /** * Adds an operation to the subOperations collection. * * @param request * The request to add. * * @throws IllegalArgumentException * Throws if this batch is already at max subOperations size. See {@link Constants#BATCH_MAX_REQUESTS}. */ protected final void addSubOperation(final StorageRequest request, final P parent) { Utility.assertInBounds("subOperationCount", this.subOperations.size(), 0, Constants.BATCH_MAX_REQUESTS - 1); subOperations.put(request, parent); } public UUID getBatchId() { return batchId; } /** * Converts a batch sub-response from it's basic HTTP form to the response type of the operation being batched. * * @param response * Object model of the HTTP response. * * @return * Parsed response. */ protected abstract R convertResponse(BatchSubResponse response); /** * Creates a {@link StorageRequest} for a batch operation based on this object's collected requests to make. * * @param client * The {@link ServiceClient} making this request. * @param requestOptions * Request options for this request. * * @return * The built request. */ protected StorageRequest, Map> batchImpl( C client, final RequestOptions requestOptions) { return new StorageRequest, Map>(requestOptions, client.getStorageUri()) { @Override public HttpURLConnection buildRequest(C client, BatchOperation parentObject, OperationContext context) throws Exception { byte[] body = BaseRequest.buildBatchBody(client, parentObject, context); this.setSendStream(new ByteArrayInputStream(body)); this.setLength((long)body.length); return BaseRequest.batch( client.getEndpoint(), requestOptions, context, null /* accessCondition */); } @Override public void setHeaders(HttpURLConnection connection, BatchOperation parentObject, OperationContext context) { connection.setRequestProperty( Constants.HeaderConstants.CONTENT_TYPE, Utility.stringJoin("; ", HTTP_MIXED_MULTIPART_CONTENT_TYPE, HTTP_MIXED_MULTIPART_DELIMITER_DEFINITION + STORAGE_BATCH_DELIMITER_PREFIX + parentObject.getBatchId())); } @Override public void signRequest(HttpURLConnection connection, C client, OperationContext context) throws Exception { StorageRequest.signBlobQueueAndFileRequest(connection, client, this.getLength(), context); } @Override public Map preProcessResponse(BatchOperation parentObject, C client, OperationContext context) throws Exception { if (this.getResult().getStatusCode() != HttpURLConnection.HTTP_ACCEPTED) { this.setNonExceptionedRetryableFailure(true); } return null; } @Override public Map postProcessResponse(HttpURLConnection connection, BatchOperation parentObject, C client, OperationContext context, Map storageObject) throws Exception { ByteArrayOutputStream responseBuffer = new ByteArrayOutputStream(); int bytesRead; byte[] readBuffer = new byte[Constants.KB]; while ((bytesRead = connection.getInputStream().read(readBuffer, 0, readBuffer.length)) != -1) { responseBuffer.write(readBuffer, 0, bytesRead); } responseBuffer.flush(); final List parsedResponses = parseBatchBody( responseBuffer.toByteArray(), connection.getHeaderField(Constants.HeaderConstants.CONTENT_TYPE).split(HTTP_MIXED_MULTIPART_DELIMITER_DEFINITION)[1]); throwIfUberRequestFails(parsedResponses); final Map successfulResponses = new HashMap<>(); final Map failedResponses = new HashMap<>(); sortResponses(parsedResponses, successfulResponses, failedResponses); if (!failedResponses.isEmpty()) { throw new BatchException(successfulResponses, failedResponses, context); } return successfulResponses; } }; } private P findParent(BatchSubResponse subResponse) { //iterate through requests, keeping track of index (Content-ID) and find the parent that matches int i = 0; for (Map.Entry, P> op : subOperations.entrySet()) { if (i == Integer.parseInt(subResponse.getHeaders().get(Constants.HeaderConstants.CONTENT_ID))) { return op.getValue(); } i++; } /* * If parent isn't present, it's because batch returns a success code when the overall batch fails, and creates * a single sub-response that contains the actual failure response of the batch. This failure has already been * checked where we already declare a StorageException. We will not hit this state. */ throw new IllegalStateException(); } private void sortResponses( List responses, Map successfulBucket, Map failedBucket) { Iterator it = responses.iterator(); while (it.hasNext()) { BatchSubResponse response = it.next(); if (response.getStatusCode() / 100 != 2) { failedBucket.put(findParent(response), response); it.remove(); } else { successfulBucket.put(findParent(response), convertResponse(response)); } } } /** * Throws if the batch as a whole failed. * * @param parsedResponses * The parsed batch body. * @throws StorageException * If the body contained a single response detailing that the batch failed. */ private void throwIfUberRequestFails(List parsedResponses) throws StorageException { // Failure results in 1 and only 1 response. If this is not the case, we're successful. if (parsedResponses.size() != 1) { return; } BatchSubResponse subResponse = parsedResponses.get(0); // If single response is successful, we successfully batched one request. if (subResponse.getStatusCode() / 100 == 2) { return; } /* If the user batched one request and it failed, we do not have a method of determining whether the overall * batch failed, or if the single subrequest failed. Either way, there was not a single success, so throw as if * the batch failed. */ ByteArrayOutputStream responseBuffer = new ByteArrayOutputStream(); try { int bytesRead; byte[] readBuffer = new byte[Constants.KB]; while ((bytesRead = subResponse.getBody().read(readBuffer, 0, readBuffer.length)) != -1) { responseBuffer.write(readBuffer, 0, bytesRead); } responseBuffer.flush(); } catch (IOException e) { throw new RuntimeException(e); } throw new StorageException(subResponse.getStatusMessage(), responseBuffer.toString(), subResponse.getStatusCode(), null, null); } ///// Response parsing ///// /** * Splits the batch body into it's sub-responses based on an HTTP mixed/multipart delimiter. * * @param body * The body to split. * @param responseDelimiter * The delimiter to split on. * * @return * The sub-responses. */ private List parseBatchBody(byte[] body, String responseDelimiter) { List subResponses = Utility.splitOnPattern( body, (HTTP_LINE_ENDING + "--" + responseDelimiter + HTTP_LINE_ENDING).getBytes(StandardCharsets.UTF_8)); // strip first, slightly different delim "--\r\n" off first entry subResponses.set( 0, Utility.splitOnPattern( subResponses.get(0), ("--" + responseDelimiter + HTTP_LINE_ENDING).getBytes(StandardCharsets.UTF_8)) .get(0)); // strip final, slightly different delim "\r\n----" off last entry subResponses.set( subResponses.size() - 1, Utility.splitOnPattern( subResponses.get(subResponses.size() - 1), (HTTP_LINE_ENDING + "--" + responseDelimiter + "--").getBytes(StandardCharsets.UTF_8)) .get(0)); List responses = new ArrayList<>(); for (byte[] subResponse : subResponses) { responses.add(parseResponse(subResponse)); } return responses; } /** * Parses a serialized sub-response into an object model. * * @param response * The serialized sub-response. * * @return * The parsed sub-response. */ private BatchSubResponse parseResponse(byte[] response) { /* * Header: Value (1 or more times) * * HTTP/ * Header: Value (1 or more times) * * body (if any) */ int parsedStatusCode = 0; String parsedStatusMessage = ""; final Map parsedHeaders = new HashMap<>(); InputStream body = null; int lineStart = 0; // inclusive int lineEnd; // exclusive int numBlankLinesFound = 0; byte[] newlinePattern = HTTP_LINE_ENDING.getBytes(); /* * Java's only built in line-reading tool on a byte array is BufferedReader.readLine(). * This forces everything to be treated as chars, leading to potential interpretation * issues when working with a response body that could be raw binary. To prevent this, * we must read raw bytes, looking for newlines, etc.. We can safely convert anything * not the HTTP body into strings. */ while (numBlankLinesFound < 2 && lineStart < response.length) { lineEnd = (lineEnd = Utility.findPattern(response, newlinePattern, lineStart)) == -1 ? response.length : lineEnd; String line = new String(Arrays.copyOfRange(response, lineStart, lineEnd)); if (line.equals("")) { numBlankLinesFound++; } else if (line.startsWith("HTTP")) { // HTTP response String[] lineTokens = line.split(" "); parsedStatusCode = Integer.parseInt(lineTokens[1]); if (lineTokens.length > 2) { StringBuilder builder = new StringBuilder(); for (int i = 2; i < lineTokens.length; i++) { builder.append(lineTokens[i]).append(' '); } builder.deleteCharAt(builder.length() - 1); parsedStatusMessage = builder.toString(); } } else { // a header line; anything else would be against protocol String[] tokens = line.split(":"); parsedHeaders.put(tokens[0], tokens[1].trim()); } lineStart = lineEnd + newlinePattern.length; } // response body (if any) if (lineStart < response.length) { // cannot read line by line, we need to preserve all bytes, as data can be just binary body = new ByteArrayInputStream(response, lineStart, response.length - lineStart); } BatchSubResponse parsedResponse = new BatchSubResponse(); parsedResponse.setStatusCode(parsedStatusCode); parsedResponse.setStatusMessage(parsedStatusMessage); parsedResponse.setHeaders(parsedHeaders); parsedResponse.setBody(body); return parsedResponse; } @Override public Iterator, P>> iterator() { return new Iterator, P>>() { final Iterator, P>> baseIt = subOperations.entrySet().iterator(); @Override public void remove() { throw new UnsupportedOperationException(); } @Override public boolean hasNext() { return baseIt.hasNext(); } @Override public Map.Entry, P> next() { return baseIt.next(); } }; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy