response
* @return The exception returned by the public methods.
*/
public static Throwable mapException(Throwable exception) {
- AcrErrorsException acrException = null;
+ HttpResponseException acrException = null;
- if (exception instanceof AcrErrorsException) {
- acrException = ((AcrErrorsException) exception);
+ if (exception instanceof HttpResponseException) {
+ acrException = ((HttpResponseException) exception);
} else if (exception instanceof RuntimeException) {
RuntimeException runtimeException = (RuntimeException) exception;
Throwable throwable = runtimeException.getCause();
- if (throwable instanceof AcrErrorsException) {
- acrException = (AcrErrorsException) throwable;
+ if (throwable instanceof HttpResponseException) {
+ acrException = (HttpResponseException) throwable;
}
}
@@ -265,25 +260,10 @@ public static Throwable mapException(Throwable exception) {
return exception;
}
- final HttpResponse errorHttpResponse = acrException.getResponse();
- final int statusCode = errorHttpResponse.getStatusCode();
- final String errorDetail = acrException.getMessage();
-
- switch (statusCode) {
- case 401:
- return new ClientAuthenticationException(errorDetail, acrException.getResponse(), exception);
- case 404:
- return new ResourceNotFoundException(errorDetail, acrException.getResponse(), exception);
- case 409:
- return new ResourceExistsException(errorDetail, acrException.getResponse(), exception);
- case 412:
- return new ResourceModifiedException(errorDetail, acrException.getResponse(), exception);
- default:
- return new HttpResponseException(errorDetail, acrException.getResponse(), exception);
- }
+ return mapAcrErrorsException(acrException);
}
- public static HttpResponseException mapAcrErrorsException(AcrErrorsException acrException) {
+ public static HttpResponseException mapAcrErrorsException(HttpResponseException acrException) {
final HttpResponse errorHttpResponse = acrException.getResponse();
final int statusCode = errorHttpResponse.getStatusCode();
final String errorDetail = acrException.getMessage();
diff --git a/sdk/core/azure-core/src/main/java/com/azure/core/implementation/AccessibleByteArrayOutputStream.java b/sdk/core/azure-core/src/main/java/com/azure/core/implementation/AccessibleByteArrayOutputStream.java
index 40e0d2823592e..7131faff85659 100644
--- a/sdk/core/azure-core/src/main/java/com/azure/core/implementation/AccessibleByteArrayOutputStream.java
+++ b/sdk/core/azure-core/src/main/java/com/azure/core/implementation/AccessibleByteArrayOutputStream.java
@@ -4,12 +4,17 @@
package com.azure.core.implementation;
import java.io.ByteArrayOutputStream;
+import java.nio.ByteBuffer;
import java.nio.charset.Charset;
+import java.util.Arrays;
/**
* This class is an extension of {@link ByteArrayOutputStream} which allows access to the backing {@code byte[]} without
* requiring a copying of the data. The only use of this class is for internal purposes where we know it is safe to
* directly access the {@code byte[]} without copying.
+ *
+ * This class isn't meant to be thread-safe as usage should be internal to azure-core and should be guarded
+ * appropriately when used.
*/
public class AccessibleByteArrayOutputStream extends ByteArrayOutputStream {
/**
@@ -30,8 +35,38 @@ public AccessibleByteArrayOutputStream(int initialCapacity) {
}
@Override
- public synchronized byte[] toByteArray() {
- return buf;
+ public byte[] toByteArray() {
+ return Arrays.copyOf(buf, count);
+ }
+
+ // Commenting out as this isn't used but may be added back in the future.
+// /**
+// * Returns the internal {@code byte[]} without copying.
+// *
+// * This will be the full {@code byte[]}, so if writing required it to be resized to 8192 bytes but only 6000 bytes
+// * were written the final 2192 bytes will be undefined data. If this is used in an API where a {@code byte[]} is
+// * accepted you must use the range based overload with {@link #count()}, if a range based overload isn't available
+// * use {@link #toByteArray()} which will copy the range of bytes written.
+// *
+// * @return A direct reference to the internal {@code byte[]} where data is being written.
+// */
+// public byte[] toByteArrayUnsafe() {
+// return buf;
+// }
+
+ /**
+ * Returns a {@link ByteBuffer} representation of the content written to this stream.
+ *
+ * The {@link ByteBuffer} will use a direct reference to the internal {@code byte[]} being written, so any
+ * modifications to the content already written will be reflected in the {@link ByteBuffer}. Given the direct
+ * reference to the internal {@code byte[]} the {@link ByteBuffer} returned by the API will be read-only. Further
+ * writing to this stream won't be reflected in the {@link ByteBuffer} as the ByteBuffer will be created using
+ * {@code ByteBuffer.wrap(bytes, 0, count())}.
+ *
+ * @return A read-only {@link ByteBuffer} represented by the internal buffer being written to.
+ */
+ public ByteBuffer toByteBuffer() {
+ return ByteBuffer.wrap(buf, 0, count).asReadOnlyBuffer();
}
/**
@@ -52,4 +87,17 @@ public int count() {
public String toString(Charset charset) {
return new String(buf, 0, count, charset);
}
+
+ /**
+ * Gets a BOM aware string representation of the stream.
+ *
+ * This method is the equivalent of calling
+ * {@code ImplUtils.bomAwareToString(toByteBufferUnsafe(), 0, count(), contentType)}.
+ *
+ * @param contentType The {@code Content-Type} header value.
+ * @return A string representation of the stream encoded to the found encoding.
+ */
+ public String bomAwareToString(String contentType) {
+ return ImplUtils.bomAwareToString(buf, 0, count, contentType);
+ }
}
diff --git a/sdk/core/azure-core/src/main/java/com/azure/core/implementation/ImplUtils.java b/sdk/core/azure-core/src/main/java/com/azure/core/implementation/ImplUtils.java
index 658e153b98d7e..66cb915b23fa7 100644
--- a/sdk/core/azure-core/src/main/java/com/azure/core/implementation/ImplUtils.java
+++ b/sdk/core/azure-core/src/main/java/com/azure/core/implementation/ImplUtils.java
@@ -15,6 +15,10 @@
import java.io.OutputStream;
import java.net.URL;
import java.nio.ByteBuffer;
+import java.nio.charset.Charset;
+import java.nio.charset.IllegalCharsetNameException;
+import java.nio.charset.StandardCharsets;
+import java.nio.charset.UnsupportedCharsetException;
import java.time.DateTimeException;
import java.time.Duration;
import java.time.OffsetDateTime;
@@ -26,6 +30,8 @@
import java.util.NoSuchElementException;
import java.util.function.Function;
import java.util.function.Supplier;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
/**
* Utility class containing implementation specific methods.
@@ -37,6 +43,16 @@ public final class ImplUtils {
// future improvement - make this configurable
public static final int MAX_CACHE_SIZE = 10000;
+ private static final Charset UTF_32BE = Charset.forName("UTF-32BE");
+ private static final Charset UTF_32LE = Charset.forName("UTF-32LE");
+ private static final byte ZERO = (byte) 0x00;
+ private static final byte BB = (byte) 0xBB;
+ private static final byte BF = (byte) 0xBF;
+ private static final byte EF = (byte) 0xEF;
+ private static final byte FE = (byte) 0xFE;
+ private static final byte FF = (byte) 0xFF;
+ private static final Pattern CHARSET_PATTERN = Pattern.compile("charset=(\\S+)\\b", Pattern.CASE_INSENSITIVE);
+
/**
* Attempts to extract a retry after duration from a given set of {@link HttpHeaders}.
*
@@ -247,6 +263,61 @@ public Map.Entry next() {
}
}
+ /**
+ * Attempts to convert a byte stream into the properly encoded String.
+ *
+ * This utility method will attempt to find the encoding for the String in this order.
+ *
+ * - Find the byte order mark in the byte array.
+ * - Find the charset in the {@code contentType} header.
+ * - Default to {@code UTF-8}.
+ *
+ *
+ * @param bytes The byte array.
+ * @param offset The starting offset in the byte array.
+ * @param count The number of bytes to process in the byte array.
+ * @param contentType The {@code Content-Type} header value.
+ * @return A string representation of the byte encoded to the found encoding, or null if {@code bytes} is null.
+ */
+ public static String bomAwareToString(byte[] bytes, int offset, int count, String contentType) {
+ if (bytes == null) {
+ return null;
+ }
+
+ if (count >= 3 && bytes[offset] == EF && bytes[offset + 1] == BB && bytes[offset + 2] == BF) {
+ return new String(bytes, 3, bytes.length - 3, StandardCharsets.UTF_8);
+ } else if (count >= 4 && bytes[offset] == ZERO && bytes[offset + 1] == ZERO
+ && bytes[offset + 2] == FE && bytes[offset + 3] == FF) {
+ return new String(bytes, 4, bytes.length - 4, UTF_32BE);
+ } else if (count >= 4 && bytes[offset] == FF && bytes[offset + 1] == FE
+ && bytes[offset + 2] == ZERO && bytes[offset + 3] == ZERO) {
+ return new String(bytes, 4, bytes.length - 4, UTF_32LE);
+ } else if (count >= 2 && bytes[offset] == FE && bytes[offset + 1] == FF) {
+ return new String(bytes, 2, bytes.length - 2, StandardCharsets.UTF_16BE);
+ } else if (count >= 2 && bytes[offset] == FF && bytes[offset + 1] == FE) {
+ return new String(bytes, 2, bytes.length - 2, StandardCharsets.UTF_16LE);
+ } else {
+ /*
+ * Attempt to retrieve the default charset from the 'Content-Encoding' header, if the value isn't
+ * present or invalid fallback to 'UTF-8' for the default charset.
+ */
+ if (!CoreUtils.isNullOrEmpty(contentType)) {
+ try {
+ Matcher charsetMatcher = CHARSET_PATTERN.matcher(contentType);
+ if (charsetMatcher.find()) {
+ return new String(bytes, offset, count, Charset.forName(charsetMatcher.group(1)));
+ } else {
+ return new String(bytes, offset, count, StandardCharsets.UTF_8);
+ }
+ } catch (IllegalCharsetNameException | UnsupportedCharsetException ex) {
+ return new String(bytes, offset, count, StandardCharsets.UTF_8);
+ }
+ } else {
+ return new String(bytes, offset, count, StandardCharsets.UTF_8);
+ }
+ }
+ }
+
private ImplUtils() {
}
}
diff --git a/sdk/core/azure-core/src/main/java/com/azure/core/implementation/http/rest/ReflectionSerializable.java b/sdk/core/azure-core/src/main/java/com/azure/core/implementation/http/rest/ReflectionSerializable.java
index f144fb623d8bb..371da8ad140bf 100644
--- a/sdk/core/azure-core/src/main/java/com/azure/core/implementation/http/rest/ReflectionSerializable.java
+++ b/sdk/core/azure-core/src/main/java/com/azure/core/implementation/http/rest/ReflectionSerializable.java
@@ -202,7 +202,7 @@ static ByteBuffer serializeAsJsonSerializable(Object jsonSerializable) throws IO
JSON_WRITER_WRITE_JSON_SERIALIZABLE.writeJson(jsonWriter, jsonSerializable);
JSON_WRITER_FLUSH.flush(jsonWriter);
- return ByteBuffer.wrap(outputStream.toByteArray(), 0, outputStream.count());
+ return outputStream.toByteBuffer();
}
}
@@ -289,7 +289,7 @@ static ByteBuffer serializeAsXmlSerializable(Object bodyContent) throws IOExcept
XML_WRITER_WRITE_XML_SERIALIZABLE.writeXml(xmlWriter, bodyContent);
XML_WRITER_FLUSH.flush(xmlWriter);
- return ByteBuffer.wrap(outputStream.toByteArray(), 0, outputStream.count());
+ return outputStream.toByteBuffer();
} catch (IOException ex) {
throw ex;
} catch (Exception ex) {
diff --git a/sdk/core/azure-core/src/main/java/com/azure/core/implementation/http/rest/RestProxyBase.java b/sdk/core/azure-core/src/main/java/com/azure/core/implementation/http/rest/RestProxyBase.java
index fa8d715f55471..1251e812deab0 100644
--- a/sdk/core/azure-core/src/main/java/com/azure/core/implementation/http/rest/RestProxyBase.java
+++ b/sdk/core/azure-core/src/main/java/com/azure/core/implementation/http/rest/RestProxyBase.java
@@ -25,6 +25,7 @@
import com.azure.core.implementation.TypeUtil;
import com.azure.core.implementation.http.UnexpectedExceptionInformation;
import com.azure.core.implementation.serializer.HttpResponseDecoder;
+import com.azure.core.implementation.serializer.MalformedValueException;
import com.azure.core.util.BinaryData;
import com.azure.core.util.Context;
import com.azure.core.util.UrlBuilder;
@@ -352,6 +353,16 @@ public Exception instantiateUnexpectedException(final UnexpectedExceptionInforma
exceptionMessage.append("\"").append(new String(responseContent, StandardCharsets.UTF_8)).append("\"");
}
+ // If the decoded response content is on of these exception types there was a failure in creating the actual
+ // exception body type. In this case return an HttpResponseException to maintain the exception having a
+ // reference to the HttpResponse and information about what caused the deserialization failure.
+ if (responseDecodedContent instanceof IOException
+ || responseDecodedContent instanceof MalformedValueException
+ || responseDecodedContent instanceof IllegalStateException) {
+ return new HttpResponseException(exceptionMessage.toString(), httpResponse,
+ (Throwable) responseDecodedContent);
+ }
+
// For HttpResponseException types that exist in azure-core, call the constructor directly.
Class extends HttpResponseException> exceptionType = exception.getExceptionType();
if (exceptionType == HttpResponseException.class) {
diff --git a/sdk/core/azure-core/src/main/java/com/azure/core/implementation/serializer/HttpResponseBodyDecoder.java b/sdk/core/azure-core/src/main/java/com/azure/core/implementation/serializer/HttpResponseBodyDecoder.java
index b4a75c54ba0b3..9eca628ff7aa5 100644
--- a/sdk/core/azure-core/src/main/java/com/azure/core/implementation/serializer/HttpResponseBodyDecoder.java
+++ b/sdk/core/azure-core/src/main/java/com/azure/core/implementation/serializer/HttpResponseBodyDecoder.java
@@ -61,11 +61,16 @@ static Object decodeByteArray(byte[] body, HttpResponse httpResponse, Serializer
return deserializeBody(body,
decodeData.getUnexpectedException(httpResponse.getStatusCode()).getExceptionBodyType(),
null, serializer, SerializerEncoding.fromHeaders(httpResponse.getHeaders()));
- } catch (IOException | MalformedValueException ex) {
- // This translates in RestProxy as a RestException with no deserialized body.
- // The response content will still be accessible via the .response() member.
+ } catch (IOException | MalformedValueException | IllegalStateException ex) {
+ // MalformedValueException is thrown by Jackson, IllegalStateException is thrown by the TEXT
+ // serialization encoding handler, and IOException can be thrown by both Jackson and TEXT.
+ //
+ // There has been an issue deserializing the error response body. This may be an error in the service
+ // return.
+ //
+ // Return the exception as the body type, RestProxyBase will handle this later.
LOGGER.warning("Failed to deserialize the error entity.", ex);
- return null;
+ return ex;
}
} else {
if (!decodeData.isReturnTypeDecodeable()) {
diff --git a/sdk/core/azure-core/src/main/java/com/azure/core/util/CoreUtils.java b/sdk/core/azure-core/src/main/java/com/azure/core/util/CoreUtils.java
index a53ede2ea7639..7229c529cfa0c 100644
--- a/sdk/core/azure-core/src/main/java/com/azure/core/util/CoreUtils.java
+++ b/sdk/core/azure-core/src/main/java/com/azure/core/util/CoreUtils.java
@@ -6,16 +6,13 @@
import com.azure.core.http.HttpHeaders;
import com.azure.core.http.policy.HttpLogOptions;
import com.azure.core.http.rest.PagedResponse;
+import com.azure.core.implementation.ImplUtils;
import com.azure.core.util.logging.ClientLogger;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
import java.io.IOException;
import java.io.InputStream;
-import java.nio.charset.Charset;
-import java.nio.charset.IllegalCharsetNameException;
-import java.nio.charset.StandardCharsets;
-import java.nio.charset.UnsupportedCharsetException;
import java.time.Duration;
import java.util.Arrays;
import java.util.Collection;
@@ -27,8 +24,6 @@
import java.util.Properties;
import java.util.function.BiFunction;
import java.util.function.Function;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
import java.util.stream.Collectors;
/**
@@ -37,15 +32,6 @@
public final class CoreUtils {
// CoreUtils is a commonly used utility, use a static logger.
private static final ClientLogger LOGGER = new ClientLogger(CoreUtils.class);
- private static final Charset UTF_32BE = Charset.forName("UTF-32BE");
- private static final Charset UTF_32LE = Charset.forName("UTF-32LE");
- private static final byte ZERO = (byte) 0x00;
- private static final byte BB = (byte) 0xBB;
- private static final byte BF = (byte) 0xBF;
- private static final byte EF = (byte) 0xEF;
- private static final byte FE = (byte) 0xFE;
- private static final byte FF = (byte) 0xFF;
- private static final Pattern CHARSET_PATTERN = Pattern.compile("charset=([\\S]+)\\b", Pattern.CASE_INSENSITIVE);
private CoreUtils() {
// Exists only to defeat instantiation.
@@ -235,36 +221,7 @@ public static String bomAwareToString(byte[] bytes, String contentType) {
return null;
}
- if (bytes.length >= 3 && bytes[0] == EF && bytes[1] == BB && bytes[2] == BF) {
- return new String(bytes, 3, bytes.length - 3, StandardCharsets.UTF_8);
- } else if (bytes.length >= 4 && bytes[0] == ZERO && bytes[1] == ZERO && bytes[2] == FE && bytes[3] == FF) {
- return new String(bytes, 4, bytes.length - 4, UTF_32BE);
- } else if (bytes.length >= 4 && bytes[0] == FF && bytes[1] == FE && bytes[2] == ZERO && bytes[3] == ZERO) {
- return new String(bytes, 4, bytes.length - 4, UTF_32LE);
- } else if (bytes.length >= 2 && bytes[0] == FE && bytes[1] == FF) {
- return new String(bytes, 2, bytes.length - 2, StandardCharsets.UTF_16BE);
- } else if (bytes.length >= 2 && bytes[0] == FF && bytes[1] == FE) {
- return new String(bytes, 2, bytes.length - 2, StandardCharsets.UTF_16LE);
- } else {
- /*
- * Attempt to retrieve the default charset from the 'Content-Encoding' header, if the value isn't
- * present or invalid fallback to 'UTF-8' for the default charset.
- */
- if (!isNullOrEmpty(contentType)) {
- try {
- Matcher charsetMatcher = CHARSET_PATTERN.matcher(contentType);
- if (charsetMatcher.find()) {
- return new String(bytes, Charset.forName(charsetMatcher.group(1)));
- } else {
- return new String(bytes, StandardCharsets.UTF_8);
- }
- } catch (IllegalCharsetNameException | UnsupportedCharsetException ex) {
- return new String(bytes, StandardCharsets.UTF_8);
- }
- } else {
- return new String(bytes, StandardCharsets.UTF_8);
- }
- }
+ return ImplUtils.bomAwareToString(bytes, 0, bytes.length, contentType);
}
/**
diff --git a/sdk/core/azure-core/src/main/java/com/azure/core/util/serializer/JacksonAdapter.java b/sdk/core/azure-core/src/main/java/com/azure/core/util/serializer/JacksonAdapter.java
index 413606a7c57a4..cb248cb420a09 100644
--- a/sdk/core/azure-core/src/main/java/com/azure/core/util/serializer/JacksonAdapter.java
+++ b/sdk/core/azure-core/src/main/java/com/azure/core/util/serializer/JacksonAdapter.java
@@ -4,9 +4,12 @@
package com.azure.core.util.serializer;
import com.azure.core.http.HttpHeaders;
+import com.azure.core.implementation.AccessibleByteArrayOutputStream;
import com.azure.core.implementation.jackson.ObjectMapperShim;
import com.azure.core.util.Configuration;
import com.azure.core.util.CoreUtils;
+import com.azure.core.util.DateTimeRfc1123;
+import com.azure.core.util.ExpandableStringEnum;
import com.azure.core.util.Header;
import com.azure.core.util.logging.ClientLogger;
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -16,10 +19,17 @@
import java.io.OutputStream;
import java.io.UncheckedIOException;
import java.lang.reflect.Type;
+import java.net.MalformedURLException;
+import java.net.URI;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
+import java.time.LocalDate;
+import java.time.OffsetDateTime;
import java.util.List;
import java.util.Objects;
+import java.util.UUID;
import java.util.function.BiConsumer;
/**
@@ -156,9 +166,15 @@ public String serialize(Object object, SerializerEncoding encoding) throws IOExc
return null;
}
- return (String) useAccessHelper(() -> (encoding == SerializerEncoding.XML)
- ? getXmlMapper().writeValueAsString(object)
- : mapper.writeValueAsString(object));
+ return (String) useAccessHelper(() -> {
+ if (encoding == SerializerEncoding.XML) {
+ return getXmlMapper().writeValueAsString(object);
+ } else if (encoding == SerializerEncoding.TEXT) {
+ return object.toString();
+ } else {
+ return mapper.writeValueAsString(object);
+ }
+ });
}
@Override
@@ -167,9 +183,15 @@ public byte[] serializeToBytes(Object object, SerializerEncoding encoding) throw
return null;
}
- return (byte[]) useAccessHelper(() -> (encoding == SerializerEncoding.XML)
- ? getXmlMapper().writeValueAsBytes(object)
- : mapper.writeValueAsBytes(object));
+ return (byte[]) useAccessHelper(() -> {
+ if (encoding == SerializerEncoding.XML) {
+ return getXmlMapper().writeValueAsBytes(object);
+ } else if (encoding == SerializerEncoding.TEXT) {
+ return object.toString().getBytes(StandardCharsets.UTF_8);
+ } else {
+ return mapper.writeValueAsBytes(object);
+ }
+ });
}
@Override
@@ -181,6 +203,8 @@ public void serialize(Object object, SerializerEncoding encoding, OutputStream o
useAccessHelper(() -> {
if (encoding == SerializerEncoding.XML) {
getXmlMapper().writeValue(outputStream, object);
+ } else if (encoding == SerializerEncoding.TEXT) {
+ outputStream.write(object.toString().getBytes(StandardCharsets.UTF_8));
} else {
mapper.writeValue(outputStream, object);
}
@@ -261,9 +285,15 @@ public T deserialize(String value, Type type, SerializerEncoding encoding) t
return null;
}
- return (T) useAccessHelper(() -> (encoding == SerializerEncoding.XML)
- ? getXmlMapper().readValue(value, type)
- : mapper.readValue(value, type));
+ return (T) useAccessHelper(() -> {
+ if (encoding == SerializerEncoding.XML) {
+ return getXmlMapper().readValue(value, type);
+ } else if (encoding == SerializerEncoding.TEXT) {
+ return deserializeText(value, type);
+ } else {
+ return mapper.readValue(value, type);
+ }
+ });
}
@SuppressWarnings("unchecked")
@@ -273,9 +303,15 @@ public T deserialize(byte[] bytes, Type type, SerializerEncoding encoding) t
return null;
}
- return (T) useAccessHelper(() -> (encoding == SerializerEncoding.XML)
- ? getXmlMapper().readValue(bytes, type)
- : mapper.readValue(bytes, type));
+ return (T) useAccessHelper(() -> {
+ if (encoding == SerializerEncoding.XML) {
+ return getXmlMapper().readValue(bytes, type);
+ } else if (encoding == SerializerEncoding.TEXT) {
+ return deserializeText(CoreUtils.bomAwareToString(bytes, null), type);
+ } else {
+ return mapper.readValue(bytes, type);
+ }
+ });
}
@SuppressWarnings("unchecked")
@@ -286,9 +322,73 @@ public T deserialize(InputStream inputStream, final Type type, SerializerEnc
return null;
}
- return (T) useAccessHelper(() -> (encoding == SerializerEncoding.XML)
- ? getXmlMapper().readValue(inputStream, type)
- : mapper.readValue(inputStream, type));
+ return (T) useAccessHelper(() -> {
+ if (encoding == SerializerEncoding.XML) {
+ return getXmlMapper().readValue(inputStream, type);
+ } else if (encoding == SerializerEncoding.TEXT) {
+ AccessibleByteArrayOutputStream outputStream = new AccessibleByteArrayOutputStream();
+ byte[] buffer = new byte[8192];
+ int readCount;
+ while ((readCount = inputStream.read(buffer)) != -1) {
+ outputStream.write(buffer, 0, readCount);
+ }
+
+ return deserializeText(outputStream.bomAwareToString(null), type);
+ } else {
+ return mapper.readValue(inputStream, type);
+ }
+ });
+ }
+
+ @SuppressWarnings({"unchecked", "rawtypes"})
+ private static Object deserializeText(String value, Type type) throws IOException {
+ if (type == String.class || type == CharSequence.class) {
+ return value;
+ } else if (type == int.class || type == Integer.class) {
+ return Integer.parseInt(value);
+ } else if (type == char.class || type == Character.class) {
+ return CoreUtils.isNullOrEmpty(value) ? null : value.charAt(0);
+ } else if (type == byte.class || type == Byte.class) {
+ return CoreUtils.isNullOrEmpty(value) ? null : (byte) value.charAt(0);
+ } else if (type == byte[].class) {
+ return CoreUtils.isNullOrEmpty(value) ? null : value.getBytes(StandardCharsets.UTF_8);
+ } else if (type == long.class || type == Long.class) {
+ return Long.parseLong(value);
+ } else if (type == short.class || type == Short.class) {
+ return Short.parseShort(value);
+ } else if (type == float.class || type == Float.class) {
+ return Float.parseFloat(value);
+ } else if (type == double.class || type == Double.class) {
+ return Double.parseDouble(value);
+ } else if (type == boolean.class || type == Boolean.class) {
+ return Boolean.parseBoolean(value);
+ } else if (type == OffsetDateTime.class) {
+ return OffsetDateTime.parse(value);
+ } else if (type == DateTimeRfc1123.class) {
+ return new DateTimeRfc1123(value);
+ } else if (type == URL.class) {
+ try {
+ return new URL(value);
+ } catch (MalformedURLException ex) {
+ throw new IOException(ex);
+ }
+ } else if (type == URI.class) {
+ return URI.create(value);
+ } else if (type == UUID.class) {
+ return UUID.fromString(value);
+ } else if (type == LocalDate.class) {
+ return LocalDate.parse(value);
+ } else if (Enum.class.isAssignableFrom((Class>) type)) {
+ return Enum.valueOf((Class) type, value);
+ } else if (ExpandableStringEnum.class.isAssignableFrom((Class>) type)) {
+ try {
+ return ((Class>) type).getDeclaredMethod("fromString", String.class).invoke(null, value);
+ } catch (ReflectiveOperationException ex) {
+ throw new IOException(ex);
+ }
+ } else {
+ throw new IllegalStateException("Unsupported text Content-Type Type: " + type);
+ }
}
@SuppressWarnings("unchecked")
diff --git a/sdk/core/azure-core/src/main/java/com/azure/core/util/serializer/SerializerEncoding.java b/sdk/core/azure-core/src/main/java/com/azure/core/util/serializer/SerializerEncoding.java
index f916db0eac6af..efddb7e2bd41a 100644
--- a/sdk/core/azure-core/src/main/java/com/azure/core/util/serializer/SerializerEncoding.java
+++ b/sdk/core/azure-core/src/main/java/com/azure/core/util/serializer/SerializerEncoding.java
@@ -5,6 +5,7 @@
import com.azure.core.http.HttpHeaderName;
import com.azure.core.http.HttpHeaders;
+import com.azure.core.util.CoreUtils;
import com.azure.core.util.logging.ClientLogger;
import java.util.Map;
@@ -22,12 +23,16 @@ public enum SerializerEncoding {
/**
* Extensible Markup Language.
*/
- XML;
+ XML,
+
+ /**
+ * Text.
+ */
+ TEXT;
private static final ClientLogger LOGGER = new ClientLogger(SerializerEncoding.class);
private static final String CONTENT_TYPE = "Content-Type";
private static final Map SUPPORTED_MIME_TYPES;
- private static final TreeMap SUPPORTED_SUFFIXES;
private static final SerializerEncoding DEFAULT_ENCODING = JSON;
@@ -37,10 +42,11 @@ public enum SerializerEncoding {
SUPPORTED_MIME_TYPES.put("text/xml", XML);
SUPPORTED_MIME_TYPES.put("application/xml", XML);
SUPPORTED_MIME_TYPES.put("application/json", JSON);
-
- SUPPORTED_SUFFIXES = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
- SUPPORTED_SUFFIXES.put("xml", XML);
- SUPPORTED_SUFFIXES.put("json", JSON);
+ SUPPORTED_MIME_TYPES.put("text/css", TEXT);
+ SUPPORTED_MIME_TYPES.put("text/csv", TEXT);
+ SUPPORTED_MIME_TYPES.put("text/html", TEXT);
+ SUPPORTED_MIME_TYPES.put("text/javascript", TEXT);
+ SUPPORTED_MIME_TYPES.put("text/plain", TEXT);
}
/**
@@ -52,35 +58,40 @@ public enum SerializerEncoding {
*/
public static SerializerEncoding fromHeaders(HttpHeaders headers) {
final String mimeContentType = headers.getValue(HttpHeaderName.CONTENT_TYPE);
- if (mimeContentType == null || mimeContentType.isEmpty()) {
+ if (CoreUtils.isNullOrEmpty(mimeContentType)) {
LOGGER.warning("'{}' not found. Returning default encoding: {}", CONTENT_TYPE, DEFAULT_ENCODING);
return DEFAULT_ENCODING;
}
- final String[] parts = mimeContentType.split(";");
- final SerializerEncoding encoding = SUPPORTED_MIME_TYPES.get(parts[0]);
+ int contentTypeEnd = mimeContentType.indexOf(';');
+ String contentType = (contentTypeEnd == -1) ? mimeContentType : mimeContentType.substring(0, contentTypeEnd);
+ final SerializerEncoding encoding = SUPPORTED_MIME_TYPES.get(contentType);
if (encoding != null) {
return encoding;
}
- final String[] mimeTypeParts = parts[0].split("/");
- if (mimeTypeParts.length != 2) {
+ int contentTypeTypeSplit = contentType.indexOf('/');
+ if (contentTypeTypeSplit == -1) {
LOGGER.warning("Content-Type '{}' does not match mime-type formatting 'type'/'subtype'. "
- + "Returning default: {}", parts[0], DEFAULT_ENCODING);
+ + "Returning default: {}", contentType, DEFAULT_ENCODING);
return DEFAULT_ENCODING;
}
// Check the suffix if it does not match the full types.
- final String subtype = mimeTypeParts[1];
- final int lastIndex = subtype.lastIndexOf("+");
+ // Suffixes are defined by the Structured Syntax Suffix Registry
+ // https://www.rfc-editor.org/rfc/rfc6839
+ final String subtype = contentType.substring(contentTypeTypeSplit + 1);
+ final int lastIndex = subtype.lastIndexOf('+');
if (lastIndex == -1) {
return DEFAULT_ENCODING;
}
+ // Only XML and JSON are supported suffixes, there is no suffix for TEXT.
final String mimeTypeSuffix = subtype.substring(lastIndex + 1);
- final SerializerEncoding serializerEncoding = SUPPORTED_SUFFIXES.get(mimeTypeSuffix);
- if (serializerEncoding != null) {
- return serializerEncoding;
+ if ("xml".equalsIgnoreCase(mimeTypeSuffix)) {
+ return XML;
+ } else if ("json".equalsIgnoreCase(mimeTypeSuffix)) {
+ return JSON;
}
LOGGER.warning("Content-Type '{}' does not match any supported one. Returning default: {}",
diff --git a/sdk/core/azure-core/src/test/java/com/azure/core/util/serializer/JacksonAdapterTests.java b/sdk/core/azure-core/src/test/java/com/azure/core/util/serializer/JacksonAdapterTests.java
index 608311db7f61a..10de78cd16400 100644
--- a/sdk/core/azure-core/src/test/java/com/azure/core/util/serializer/JacksonAdapterTests.java
+++ b/sdk/core/azure-core/src/test/java/com/azure/core/util/serializer/JacksonAdapterTests.java
@@ -4,6 +4,10 @@
package com.azure.core.util.serializer;
import com.azure.core.http.HttpHeaders;
+import com.azure.core.http.HttpMethod;
+import com.azure.core.implementation.AccessibleByteArrayOutputStream;
+import com.azure.core.models.GeoObjectType;
+import com.azure.core.models.JsonPatchDocument;
import com.azure.core.util.DateTimeRfc1123;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
@@ -14,35 +18,47 @@
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
+import java.io.ByteArrayInputStream;
import java.io.IOException;
+import java.io.InputStream;
+import java.net.MalformedURLException;
+import java.net.URI;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.time.LocalDate;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeParseException;
+import java.time.temporal.ChronoUnit;
import java.util.Arrays;
+import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.UUID;
import java.util.stream.Stream;
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
public class JacksonAdapterTests {
+ private static final JacksonAdapter ADAPTER = new JacksonAdapter();
+
@Test
public void emptyMap() throws IOException {
final Map map = new HashMap<>();
- final JacksonAdapter serializer = new JacksonAdapter();
- assertEquals("{}", serializer.serialize(map, SerializerEncoding.JSON));
+ assertEquals("{}", ADAPTER.serialize(map, SerializerEncoding.JSON));
}
@Test
public void mapWithNullKey() throws IOException {
final Map map = new HashMap<>();
map.put(null, null);
- final JacksonAdapter serializer = new JacksonAdapter();
- assertEquals("{}", serializer.serialize(map, SerializerEncoding.JSON));
+ assertEquals("{}", ADAPTER.serialize(map, SerializerEncoding.JSON));
}
@Test
@@ -51,8 +67,7 @@ public void mapWithEmptyKeyAndNullValue() throws IOException {
mapHolder.map(new HashMap<>());
mapHolder.map().put("", null);
- final JacksonAdapter serializer = new JacksonAdapter();
- assertEquals("{\"map\":{\"\":null}}", serializer.serialize(mapHolder, SerializerEncoding.JSON));
+ assertEquals("{\"map\":{\"\":null}}", ADAPTER.serialize(mapHolder, SerializerEncoding.JSON));
}
@Test
@@ -60,16 +75,16 @@ public void mapWithEmptyKeyAndEmptyValue() throws IOException {
final MapHolder mapHolder = new MapHolder();
mapHolder.map = new HashMap<>();
mapHolder.map.put("", "");
- final JacksonAdapter serializer = new JacksonAdapter();
- assertEquals("{\"map\":{\"\":\"\"}}", serializer.serialize(mapHolder, SerializerEncoding.JSON));
+
+ assertEquals("{\"map\":{\"\":\"\"}}", ADAPTER.serialize(mapHolder, SerializerEncoding.JSON));
}
@Test
public void mapWithEmptyKeyAndNonEmptyValue() throws IOException {
final Map map = new HashMap<>();
map.put("", "test");
- final JacksonAdapter serializer = new JacksonAdapter();
- assertEquals("{\"\":\"test\"}", serializer.serialize(map, SerializerEncoding.JSON));
+
+ assertEquals("{\"\":\"test\"}", ADAPTER.serialize(map, SerializerEncoding.JSON));
}
private static class MapHolder {
@@ -235,6 +250,149 @@ private static Stream quoteRemovalSupplier() {
);
}
+ @ParameterizedTest
+ @MethodSource("textSerializationSupplier")
+ public void textToStringSerialization(Object value, String expected) throws IOException {
+ assertEquals(expected, ADAPTER.serialize(value, SerializerEncoding.TEXT));
+ }
+
+ @ParameterizedTest
+ @MethodSource("textSerializationSupplier")
+ public void textToBytesSerialization(Object value, String expected) throws IOException {
+ byte[] actual = ADAPTER.serializeToBytes(value, SerializerEncoding.TEXT);
+
+ if (expected == null) {
+ assertNull(actual);
+ } else {
+ assertEquals(expected, new String(actual, StandardCharsets.UTF_8));
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource("textSerializationSupplier")
+ public void textToOutputStreamSerialization(Object value, String expected) throws IOException {
+ AccessibleByteArrayOutputStream outputStream = new AccessibleByteArrayOutputStream();
+ ADAPTER.serialize(value, SerializerEncoding.TEXT, outputStream);
+
+ if (expected == null) {
+ assertEquals(0, outputStream.count());
+ } else {
+ assertEquals(expected, outputStream.toString(StandardCharsets.UTF_8));
+ }
+ }
+
+ private static Stream textSerializationSupplier() {
+ Map map = Collections.singletonMap("key", "value");
+
+ return Stream.of(
+ Arguments.of(1, "1"),
+ Arguments.of(1L, "1"),
+ Arguments.of(1.0F, "1.0"),
+ Arguments.of(1.0D, "1.0"),
+ Arguments.of("1", "1"),
+ Arguments.of(HttpMethod.GET, "GET"),
+ Arguments.of(GeoObjectType.POINT, "Point"),
+ Arguments.of(map, String.valueOf(map)),
+ Arguments.of(null, null)
+ );
+ }
+
+ @ParameterizedTest
+ @MethodSource("textDeserializationSupplier")
+ public void stringToTextDeserialization(byte[] stringBytes, Class> type, Object expected) throws IOException {
+ Object actual = ADAPTER.deserialize(new String(stringBytes, StandardCharsets.UTF_8), type,
+ SerializerEncoding.TEXT);
+
+ if (type == byte[].class) {
+ assertArrayEquals((byte[]) expected, (byte[]) actual);
+ } else {
+ assertEquals(expected, actual);
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource("textDeserializationSupplier")
+ public void bytesToTextDeserialization(byte[] bytes, Class> type, Object expected) throws IOException {
+ Object actual = ADAPTER.deserialize(bytes, type, SerializerEncoding.TEXT);
+
+ if (type == byte[].class) {
+ assertArrayEquals((byte[]) expected, (byte[]) actual);
+ } else {
+ assertEquals(expected, actual);
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource("textDeserializationSupplier")
+ public void inputStreamToTextDeserialization(byte[] inputStreamBytes, Class> type, Object expected)
+ throws IOException {
+ Object actual = ADAPTER.deserialize(new ByteArrayInputStream(inputStreamBytes), type, SerializerEncoding.TEXT);
+
+ if (type == byte[].class) {
+ assertArrayEquals((byte[]) expected, (byte[]) actual);
+ } else {
+ assertEquals(expected, actual);
+ }
+ }
+
+ private static Stream textDeserializationSupplier() throws MalformedURLException {
+ byte[] helloBytes = "hello".getBytes(StandardCharsets.UTF_8);
+ String urlUri = "https://azure.com";
+ byte[] urlUriBytes = urlUri.getBytes(StandardCharsets.UTF_8);
+ OffsetDateTime offsetDateTime = OffsetDateTime.now(ZoneOffset.UTC).truncatedTo(ChronoUnit.SECONDS);
+ DateTimeRfc1123 dateTimeRfc1123 = new DateTimeRfc1123(offsetDateTime);
+ LocalDate localDate = LocalDate.now(ZoneOffset.UTC);
+ UUID uuid = UUID.randomUUID();
+ HttpMethod httpMethod = HttpMethod.GET;
+ GeoObjectType geoObjectType = GeoObjectType.POINT;
+
+ return Stream.of(
+ Arguments.of(helloBytes, String.class, "hello"),
+ Arguments.of(helloBytes, CharSequence.class, "hello"),
+ Arguments.of("1".getBytes(StandardCharsets.UTF_8), int.class, 1),
+ Arguments.of("1".getBytes(StandardCharsets.UTF_8), Integer.class, 1),
+ Arguments.of("1".getBytes(StandardCharsets.UTF_8), byte.class, (byte) 49),
+ Arguments.of("1".getBytes(StandardCharsets.UTF_8), Byte.class, (byte) 49),
+ Arguments.of("1".getBytes(StandardCharsets.UTF_8), long.class, 1L),
+ Arguments.of("1".getBytes(StandardCharsets.UTF_8), Long.class, 1L),
+ Arguments.of("1".getBytes(StandardCharsets.UTF_8), short.class, (short) 1),
+ Arguments.of("1".getBytes(StandardCharsets.UTF_8), Short.class, (short) 1),
+ Arguments.of("1.0".getBytes(StandardCharsets.UTF_8), double.class, 1.0D),
+ Arguments.of("1.0".getBytes(StandardCharsets.UTF_8), Double.class, 1.0D),
+ Arguments.of("1.0".getBytes(StandardCharsets.UTF_8), float.class, 1.0F),
+ Arguments.of("1.0".getBytes(StandardCharsets.UTF_8), Float.class, 1.0F),
+ Arguments.of("1".getBytes(StandardCharsets.UTF_8), char.class, '1'),
+ Arguments.of("1".getBytes(StandardCharsets.UTF_8), Character.class, '1'),
+ Arguments.of("1".getBytes(StandardCharsets.UTF_8), byte[].class, "1".getBytes(StandardCharsets.UTF_8)),
+ Arguments.of("true".getBytes(StandardCharsets.UTF_8), boolean.class, true),
+ Arguments.of("true".getBytes(StandardCharsets.UTF_8), Boolean.class, true),
+ Arguments.of(urlUriBytes, URL.class, new URL(urlUri)),
+ Arguments.of(urlUriBytes, URI.class, URI.create(urlUri)),
+ Arguments.of(getObjectBytes(offsetDateTime), OffsetDateTime.class, offsetDateTime),
+ Arguments.of(getObjectBytes(dateTimeRfc1123), DateTimeRfc1123.class, dateTimeRfc1123),
+ Arguments.of(getObjectBytes(localDate), LocalDate.class, localDate),
+ Arguments.of(getObjectBytes(uuid), UUID.class, uuid),
+ Arguments.of(getObjectBytes(httpMethod), HttpMethod.class, httpMethod),
+ Arguments.of(getObjectBytes(geoObjectType), GeoObjectType.class, geoObjectType)
+ );
+ }
+
+ @ParameterizedTest
+ @MethodSource("textUnsupportedDeserializationSupplier")
+ public void unsupportedTextTypesDeserialization(Class> unsupportedType,
+ Class extends Throwable> exceptionType) {
+ assertThrows(exceptionType, () -> ADAPTER.deserialize(":////", unsupportedType, SerializerEncoding.TEXT));
+ }
+
+ private static Stream textUnsupportedDeserializationSupplier() {
+ return Stream.of(
+ Arguments.of(InputStream.class, IllegalStateException.class),
+ Arguments.of(JsonPatchDocument.class, IllegalStateException.class),
+ Arguments.of(URL.class, IOException.class), // Thrown when the String isn't a valid URL
+ Arguments.of(URI.class, IllegalArgumentException.class) // Thrown when the String isn't a valid URI
+ );
+ }
+
public static final class StronglyTypedHeaders {
private final DateTimeRfc1123 date;
@@ -253,4 +411,8 @@ public InvalidStronglyTypedHeaders(HttpHeaders httpHeaders) throws Exception {
throw new Exception();
}
}
+
+ private static byte[] getObjectBytes(Object value) {
+ return String.valueOf(value).getBytes(StandardCharsets.UTF_8);
+ }
}
diff --git a/sdk/core/azure-core/src/test/java/com/azure/core/util/serializer/SerializerEncodingTests.java b/sdk/core/azure-core/src/test/java/com/azure/core/util/serializer/SerializerEncodingTests.java
index bcf76b4d1cca1..b5edeb9b3a058 100644
--- a/sdk/core/azure-core/src/test/java/com/azure/core/util/serializer/SerializerEncodingTests.java
+++ b/sdk/core/azure-core/src/test/java/com/azure/core/util/serializer/SerializerEncodingTests.java
@@ -17,8 +17,10 @@ class SerializerEncodingTests {
private static final String CONTENT_TYPE = "Content-Type";
@ParameterizedTest
- @ValueSource(strings = {"application/xml", "application/atom+xml", "text/xml", "application/foo+XML", "TEXT/XML",
- "application/xml;charset=utf-8", "application/atom+xml; charset=utf-32"})
+ @ValueSource(strings = {
+ "application/xml", "application/atom+xml", "text/xml", "application/foo+XML", "TEXT/XML",
+ "application/xml;charset=utf-8", "application/atom+xml; charset=utf-32"
+ })
void recognizeXml(String mimeType) {
// Arrange
HttpHeaders headers = new HttpHeaders(Collections.singletonMap(CONTENT_TYPE, mimeType));
@@ -28,8 +30,10 @@ void recognizeXml(String mimeType) {
}
@ParameterizedTest
- @ValueSource(strings = {"application/json", "application/kv+json", "APPLICATION/JSON", "application/FOO+JSON",
- "application/json;charset=utf-8", "application/config+json; charset=utf-32"})
+ @ValueSource(strings = {
+ "application/json", "application/kv+json", "APPLICATION/JSON", "application/FOO+JSON",
+ "application/json;charset=utf-8", "application/config+json; charset=utf-32"
+ })
void recognizeJson(String mimeType) {
// Arrange
HttpHeaders headers = new HttpHeaders(Collections.singletonMap(CONTENT_TYPE, mimeType));
@@ -38,6 +42,18 @@ void recognizeJson(String mimeType) {
Assertions.assertEquals(SerializerEncoding.JSON, SerializerEncoding.fromHeaders(headers));
}
+ @ParameterizedTest
+ @ValueSource(strings = {
+ "text/css", "text/csv", "text/html", "text/javascript", "text/plain", "TEXT/PLAIN", "text/plain; charset=utf-8"
+ })
+ void recognizeText(String mimeType) {
+ // Arrange
+ HttpHeaders headers = new HttpHeaders(Collections.singletonMap(CONTENT_TYPE, mimeType));
+
+ // Act & Assert
+ Assertions.assertEquals(SerializerEncoding.TEXT, SerializerEncoding.fromHeaders(headers));
+ }
+
@Test
void defaultNoContentType() {
// Arrange