Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions eng/code-quality-reports/src/main/resources/revapi/revapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -633,6 +633,20 @@
"old": "class com.azure.resourcemanager.sql.models.UserIdentity",
"new": "class com.azure.resourcemanager.sql.models.UserIdentity",
"justification": "Customer unlikely to subclass this class."
},
{
"ignore": true,
"code": "java.method.parameterTypeChanged",
"old": "parameter void com.azure.storage.blob.models.BlobStorageException::<init>(java.lang.String, com.azure.core.http.HttpResponse, ===java.lang.Object===)",
"new": "parameter void com.azure.storage.blob.models.BlobStorageException::<init>(java.lang.String, com.azure.core.http.HttpResponse, ===com.azure.storage.blob.models.BlobStorageError===)",
"justification": "Previous parameter conveyed no meaning to the customer and this is a type customers aren't expected to instantiate."
},
{
"ignore": true,
"code": "java.method.parameterTypeChanged",
"old": "parameter void com.azure.storage.file.datalake.models.DataLakeStorageException::<init>(java.lang.String, com.azure.core.http.HttpResponse, ===java.lang.Object===)",
"new": "parameter void com.azure.storage.file.datalake.models.DataLakeStorageException::<init>(java.lang.String, com.azure.core.http.HttpResponse, ===com.azure.storage.file.datalake.models.DataLakeStorageError===)",
"justification": "Previous parameter conveyed no meaning to the customer and this is a type customers aren't expected to instantiate."
}
]
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

package com.azure.storage.blob.batch;

import com.azure.core.http.HttpHeaderName;
import com.azure.core.http.HttpHeaders;
import com.azure.core.http.HttpRequest;
import com.azure.core.http.HttpResponse;
Expand All @@ -11,11 +12,14 @@
import com.azure.core.util.CoreUtils;
import com.azure.core.util.FluxUtil;
import com.azure.core.util.logging.ClientLogger;
import com.azure.storage.blob.models.BlobStorageError;
import com.azure.storage.blob.models.BlobStorageException;
import com.azure.storage.common.implementation.Constants;
import com.azure.xml.XmlReader;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import javax.xml.stream.XMLStreamException;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
Expand Down Expand Up @@ -61,7 +65,7 @@ static Mono<SimpleResponse<Void>> mapBatchResponse(BlobBatchOperationInfo batchO
* Content-Type will contain the boundary for each batch response. The expected format is:
* "Content-Type: multipart/mixed; boundary=batchresponse_66925647-d0cb-4109-b6d3-28efe3e1e5ed"
*/
String contentType = rawResponse.getHeaders().getValue(Constants.HeaderConstants.CONTENT_TYPE);
String contentType = rawResponse.getHeaders().getValue(HttpHeaderName.CONTENT_TYPE);

// Split on the boundary [ "multipart/mixed; boundary", "batchresponse_66925647-d0cb-4109-b6d3-28efe3e1e5ed"]
String[] boundaryPieces = contentType.split("=", 2);
Expand Down Expand Up @@ -93,9 +97,14 @@ static Mono<SimpleResponse<Void>> mapBatchResponse(BlobBatchOperationInfo batchO
int statusCode = getStatusCode(exceptionSections[1], logger);
HttpHeaders headers = getHttpHeaders(exceptionSections[1]);

sink.error(logger.logExceptionAsError(new BlobStorageException(
headers.getValue(Constants.HeaderConstants.ERROR_CODE),
createHttpResponse(rawResponse.getRequest(), statusCode, headers, body), body)));
try (XmlReader xmlReader = XmlReader.fromString(exceptionSections[2])) {
sink.error(logger.logExceptionAsError(
new BlobStorageException(headers.getValue(Constants.HeaderConstants.ERROR_CODE),
createHttpResponse(rawResponse.getRequest(), statusCode, headers, body),
BlobStorageError.fromXml(xmlReader))));
} catch (XMLStreamException ex) {
sink.error(ex);
}
}

// Split the batch response body into batch operation responses.
Expand Down Expand Up @@ -123,7 +132,7 @@ static Mono<SimpleResponse<Void>> mapBatchResponse(BlobBatchOperationInfo batchO
}
}

if (throwOnAnyFailure && exceptions.size() != 0) {
if (throwOnAnyFailure && !exceptions.isEmpty()) {
sink.error(logger.logExceptionAsError(new BlobBatchStorageException("Batch had operation failures.",
createHttpResponse(rawResponse), exceptions)));
}
Expand Down Expand Up @@ -190,11 +199,15 @@ private static void setBodyOrAddException(BlobBatchOperationResponse<?> batchOpe
* Currently, no batching operations will return a success body, they will only return a body on an exception.
* For now this will only construct the exception and throw if it should throw on an error.
*/
BlobStorageException exception = new BlobStorageException(responseBody,
batchOperationResponse.asHttpResponse(responseBody), responseBody);
logger.logExceptionAsError(exception);
batchOperationResponse.setException(exception);
exceptions.add(exception);
try (XmlReader xmlReader = XmlReader.fromString(responseBody)) {
BlobStorageException exception = new BlobStorageException(responseBody,
batchOperationResponse.asHttpResponse(responseBody), BlobStorageError.fromXml(xmlReader));
logger.logExceptionAsError(exception);
batchOperationResponse.setException(exception);
exceptions.add(exception);
} catch (XMLStreamException ex) {
throw new RuntimeException(ex);
}
}

static HttpResponse createHttpResponse(HttpRequest request, int statusCode, HttpHeaders headers, String body) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package com.azure.storage.blob.models;

import com.azure.core.util.CoreUtils;
import com.azure.json.JsonReader;
import com.azure.json.JsonSerializable;
import com.azure.json.JsonToken;
import com.azure.json.JsonWriter;
import com.azure.xml.XmlReader;
import com.azure.xml.XmlSerializable;
import com.azure.xml.XmlToken;
import com.azure.xml.XmlWriter;

import javax.xml.namespace.QName;
import javax.xml.stream.XMLStreamException;
import java.io.IOException;

/**
* Represents an error response returned by the Azure Storage Blob service.
*/
public final class BlobStorageError implements JsonSerializable<BlobStorageError>, XmlSerializable<BlobStorageError> {
private String errorCode;
private String message;
private String queryParameterName;
private String queryParameterValue;
private String reason;
private String extendedErrorDetail;

/**
* Creates an instance of BlobStorageError.
*/
public BlobStorageError() {
}

/**
* Gets the error code returned by the Azure Storage Blob service.
*
* @return The error code.
*/
public String getErrorCode() {
return errorCode;
}

/**
* Sets the error code returned by the Azure Storage Blob service.
*
* @param errorCode The error code.
* @return The updated BlobStorageError object.
*/
public BlobStorageError setErrorCode(String errorCode) {
this.errorCode = errorCode;
return this;
}

/**
* Gets the error message returned by the Azure Storage Blob service.
*
* @return The error message.
*/
public String getMessage() {
return message;
}

/**
* Sets the error message returned by the Azure Storage Blob service.
*
* @param message The error message.
* @return The updated BlobStorageError object.
*/
public BlobStorageError setMessage(String message) {
this.message = message;
return this;
}

@Override
public JsonWriter toJson(JsonWriter jsonWriter) throws IOException {
return jsonWriter.writeStartObject("Error")
.writeStringField("Code", errorCode)
.writeStringField("Message", this.message)
.writeStringField("QueryParameterName", this.queryParameterName)
.writeStringField("QueryParameterValue", this.queryParameterValue)
.writeStringField("Reason", this.reason)
.writeStringField("ExtendedErrorDetail", this.extendedErrorDetail)
.writeEndObject();
}

/**
* Reads an instance of BlobStorageError from the JsonReader.
*
* @param jsonReader The JsonReader being read.
* @return An instance of BlobStorageError if the JsonReader was pointing to an instance of it, or null if it
* was pointing to JSON null.
* @throws IOException If an error occurs while reading the BlobStorageError.
*/
public static BlobStorageError fromJson(JsonReader jsonReader) throws IOException {
return jsonReader.readObject(reader -> {
// Buffer the next JSON object as ResponseError can take two forms:
//
// - A DataLakeStorageError object
// - A DataLakeStorageError object wrapped in an "error" node.
JsonReader bufferedReader = reader.bufferObject();
bufferedReader.nextToken(); // Get to the START_OBJECT token.
while (bufferedReader.nextToken() != JsonToken.END_OBJECT) {
String fieldName = bufferedReader.getFieldName();
bufferedReader.nextToken();

if ("error".equals(fieldName)) {
// If the DataLakeStorageError was wrapped in the "error" node begin reading it now.
return readDataLakeError(bufferedReader);
} else {
bufferedReader.skipChildren();
}
}

// Otherwise reset the JsonReader and read the whole JSON object.
return readDataLakeError(bufferedReader.reset());
});
}

private static BlobStorageError readDataLakeError(JsonReader jsonReader) throws IOException {
return jsonReader.readObject(reader -> {
BlobStorageError deserializedStorageError = new BlobStorageError();

while (reader.nextToken() != JsonToken.END_OBJECT) {
String fieldName = reader.getFieldName();
reader.nextToken();

if ("code".equals(fieldName)) {
deserializedStorageError.errorCode = reader.getString();
} else if ("message".equals(fieldName)) {
deserializedStorageError.message = reader.getString();
} else if ("queryParameterName".equals(fieldName)) {
deserializedStorageError.queryParameterName = reader.getString();
} else if ("queryParameterValue".equals(fieldName)) {
deserializedStorageError.queryParameterValue = reader.getString();
} else if ("reason".equals(fieldName)) {
deserializedStorageError.reason = reader.getString();
} else if ("extendedErrorDetail".equals(fieldName)) {
deserializedStorageError.extendedErrorDetail = reader.getString();
} else {
reader.skipChildren();
}
}

return deserializedStorageError;
});
}

@Override
public XmlWriter toXml(XmlWriter xmlWriter, String rootElementName) throws XMLStreamException {
rootElementName = CoreUtils.isNullOrEmpty(rootElementName) ? "Error" : rootElementName;
xmlWriter.writeStartElement(rootElementName);
xmlWriter.writeStringElement("Code", errorCode);
xmlWriter.writeStringElement("Message", this.message);
xmlWriter.writeStringElement("QueryParameterName", this.queryParameterName);
xmlWriter.writeStringElement("QueryParameterValue", this.queryParameterValue);
xmlWriter.writeStringElement("Reason", this.reason);
xmlWriter.writeStringElement("ExtendedErrorDetail", this.extendedErrorDetail);
return xmlWriter.writeEndElement();
}

/**
* Reads an instance of BlobStorageError from the XmlReader.
*
* @param xmlReader The XmlReader being read.
* @return An instance of BlobStorageError if the XmlReader was pointing to an instance of it, or null if it was
* pointing to XML null.
* @throws XMLStreamException If an error occurs while reading the BlobStorageError.
*/
public static BlobStorageError fromXml(XmlReader xmlReader) throws XMLStreamException {
return fromXml(xmlReader, null);
}

/**
* Reads an instance of BlobStorageError from the XmlReader.
*
* @param xmlReader The XmlReader being read.
* @param rootElementName Optional root element name to override the default defined by the model. Used to support
* cases where the model can deserialize from different root element names.
* @return An instance of BlobStorageError if the XmlReader was pointing to an instance of it, or null if it was
* pointing to XML null.
* @throws XMLStreamException If an error occurs while reading the BlobStorageError.
*/
public static BlobStorageError fromXml(XmlReader xmlReader, String rootElementName) throws XMLStreamException {
String finalRootElementName = CoreUtils.isNullOrEmpty(rootElementName) ? "Error" : rootElementName;
return xmlReader.readObject(finalRootElementName, reader -> {
BlobStorageError deserializedStorageError = new BlobStorageError();
while (reader.nextElement() != XmlToken.END_ELEMENT) {
QName elementName = reader.getElementName();

if ("Code".equals(elementName.getLocalPart())) {
deserializedStorageError.errorCode = reader.getStringElement();
} else if ("Message".equals(elementName.getLocalPart())) {
deserializedStorageError.message = reader.getStringElement();
} else if ("QueryParameterName".equals(elementName.getLocalPart())) {
deserializedStorageError.queryParameterName = reader.getStringElement();
} else if ("QueryParameterValue".equals(elementName.getLocalPart())) {
deserializedStorageError.queryParameterValue = reader.getStringElement();
} else if ("Reason".equals(elementName.getLocalPart())) {
deserializedStorageError.reason = reader.getStringElement();
} else if ("ExtendedErrorDetail".equals(elementName.getLocalPart())) {
deserializedStorageError.extendedErrorDetail = reader.getStringElement();
}
}

return deserializedStorageError;
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,19 @@ public final class BlobStorageException extends HttpResponseException {
* @param response the HTTP response.
* @param value the error code of the exception.
*/
public BlobStorageException(String message, HttpResponse response, Object value) {
public BlobStorageException(String message, HttpResponse response, BlobStorageError value) {
super(StorageImplUtils.convertStorageExceptionMessage(message, response), response, value);
}

/**
* Gets the decoded error response returned by the service.
*
* @return The decoded error response returned by the service.
*/
public BlobStorageError getValue() {
return (BlobStorageError) super.getValue();
}

/**
* @return The error code returned by the service.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package com.azure.storage.blob;

import com.azure.core.http.HttpHeaderName;
import com.azure.core.http.HttpHeaders;
import com.azure.core.http.HttpPipeline;
import com.azure.core.http.HttpPipelineBuilder;
import com.azure.core.test.http.MockHttpResponse;
import com.azure.core.test.utils.MockTokenCredential;
import com.azure.storage.blob.models.BlobStorageException;
import org.junit.jupiter.api.Test;
import reactor.core.publisher.Mono;

import java.nio.charset.StandardCharsets;

import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

/**
* Tests cases where the service returns a response that would result in a {@link BlobStorageException} being thrown
* with a response body that needs to be deserialized.
*/
public class BlobErrorDeserializationTests {
@Test
public void errorResponseBody() {
String errorResponse = "<?xml version=\"1.0\" encoding=\"utf-8\"?><Error><Code>ContainerAlreadyExists</Code>"
+ "<Message>The specified container already exists.</Message></Error>";
HttpPipeline httpPipeline = new HttpPipelineBuilder()
.httpClient(request -> Mono.just(new MockHttpResponse(request, 409,
new HttpHeaders().set(HttpHeaderName.CONTENT_TYPE, "application/xml"),
errorResponse.getBytes(StandardCharsets.UTF_8))))
.build();
BlobContainerClient containerClient = new BlobContainerClientBuilder()
.endpoint("https://account.blob.core.windows.net/container")
.credential(new MockTokenCredential())
.pipeline(httpPipeline)
.buildClient();

BlobStorageException exception = assertThrows(BlobStorageException.class, containerClient::create);
assertTrue(exception.getMessage().contains("The specified container already exists."));
// assertEquals(BlobErrorCode.CONTAINER_ALREADY_EXISTS, exception.getErrorCode());
}
}
Loading