Skip to content
Merged
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
changeKind: fix
packages:
- "@typespec/http-client-java"
---

Fix XML serialization to only apply for `azure-v1` data-plane clients, and to skip the XML `ObjectSerializer` for raw `byte[]`/`BinaryData` payloads that are not structured XML models. This avoids emitting a reference to a non-generated `XmlSerializerProviders` helper (which caused a build break) for operations that return raw XML bytes.
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,8 @@ protected void writeInvocationAndConversion(ClientMethod convenienceMethod, Clie

ClientMethodType methodType = protocolMethod.getType();

IType responseBodyType = getResponseBodyType(convenienceMethod);
IType protocolResponseBodyType = getResponseBodyType(protocolMethod);
IType responseBodyType = getConvenienceResponseBodyType(convenienceMethod);
IType protocolResponseBodyType = getConvenienceResponseBodyType(protocolMethod);
IType rawResponseBodyType = convenienceMethod.getProxyMethod().getRawResponseBodyType();

if (methodType == ClientMethodType.PagingAsync) {
Expand Down Expand Up @@ -120,17 +120,6 @@ protected void writeThrowException(ClientMethodType methodType, String exception
}
}

private IType getResponseBodyType(ClientMethod method) {
// no need to care about LRO
// Mono<T> / PagedFlux<T>
IType type = ((GenericType) method.getReturnValue().getType()).getTypeArguments()[0];
if (type instanceof GenericType && ClassType.RESPONSE.getName().equals(((GenericType) type).getName())) {
// Mono<Response<T>>
type = ((GenericType) type).getTypeArguments()[0];
}
return type;
}

private String expressionConvertFromBinaryData(IType responseBodyType, IType rawType, Set<String> mediaTypes,
Set<GenericType> typeReferenceStaticClasses) {
String expressionMapFromBinaryData
Expand All @@ -146,7 +135,7 @@ private String expressionConvertFromBinaryData(IType responseBodyType, IType raw
private String expressionMapFromBinaryData(IType responseBodyType, IType rawType, Set<String> mediaTypes,
Set<GenericType> typeReferenceStaticClasses) {
SupportedMimeType mimeType = SupportedMimeType.getResponseKnownMimeType(mediaTypes);
String serializerArgument = xmlSerializerArgument(mimeType);
String serializerArgument = xmlSerializerArgument(mimeType, responseBodyType);
switch (mimeType) {
case TEXT:
String baseHandling = "protocolMethodData.toString()";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.RequestParameterLocation;
import com.microsoft.typespec.http.client.generator.core.extension.plugin.JavaSettings;
import com.microsoft.typespec.http.client.generator.core.model.clientmodel.Annotation;
import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ArrayType;
import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClassType;
import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientMethod;
import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientMethodParameter;
Expand All @@ -32,6 +33,7 @@
import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ParameterTransformation;
import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ParameterTransformations;
import com.microsoft.typespec.http.client.generator.core.model.clientmodel.PrimitiveType;
import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ProxyMethod;
import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ProxyMethodParameter;
import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaBlock;
import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaClass;
Expand Down Expand Up @@ -63,65 +65,155 @@
abstract class ConvenienceMethodTemplateBase {

// Name of the static ObjectSerializer member used for XML serialization on the convenience client.
static final String XML_SERIALIZER_MEMBER_NAME = "SERIALIZER";
static final String XML_SERIALIZER_MEMBER_NAME = "XML_SERIALIZER";

protected ConvenienceMethodTemplateBase() {
}

/**
* Whether XML serialization via an explicit {@link com.azure.core.util.serializer.ObjectSerializer} is supported.
* It is only required for the azure-core (v1) data-plane flavor; management (Fluent) and vanilla clients, as well
* as
* the azure-core v2 / clientcore flavor, are excluded.
*
* @return whether XML serialization via an explicit serializer is supported.
*/
static boolean isXmlSerializationSupported() {
JavaSettings settings = JavaSettings.getInstance();
return settings.isAzureV1() && settings.isDataPlaneClient();
}

/**
* Whether the given payload type is serialized/deserialized as a model when the payload is XML. Raw binary payloads
* ({@code byte[]}, {@code Base64Url}, {@link com.azure.core.util.BinaryData}) are passed through as-is and do not
* involve model serialization, so the XML {@link com.azure.core.util.serializer.ObjectSerializer} must not be used
* for them.
*
* @param type the payload (request body or response body) type.
* @return whether the type is serialized as a model.
*/
static boolean isXmlSerializableType(IType type) {
return type != null
&& type != ClassType.BINARY_DATA
&& type != ArrayType.BYTE_ARRAY
&& type != ClassType.BASE_64_URL;
}

/**
* Whether the XML {@link com.azure.core.util.serializer.ObjectSerializer} overload should be used for the given
* MIME type. XML serialization via an explicit serializer is only required for the azure-core (v1) flavor.
* MIME type and payload type. XML serialization via an explicit serializer is only required for the azure-core (v1)
* data-plane flavor, and only when the payload is an actual model (not a raw binary payload).
*
* @param mimeType the MIME type.
* @param type the payload (request body or response body) type.
* @return whether to use the XML serializer overload of {@code toObject}/{@code fromObject}.
*/
static boolean useXmlObjectSerializer(SupportedMimeType mimeType) {
return mimeType == SupportedMimeType.XML && JavaSettings.getInstance().isAzureV1();
static boolean useXmlObjectSerializer(SupportedMimeType mimeType, IType type) {
return mimeType == SupportedMimeType.XML && isXmlSerializationSupported() && isXmlSerializableType(type);
}

/**
* The additional argument (e.g. {@code ", SERIALIZER"}) to append to {@code toObject}/{@code fromObject} calls when
* the XML serializer overload should be used, or an empty string otherwise.
*
* @param mimeType the MIME type.
* @param type the payload (request body or response body) type.
* @return the serializer argument, possibly empty.
*/
static String xmlSerializerArgument(SupportedMimeType mimeType) {
return useXmlObjectSerializer(mimeType) ? ", " + XML_SERIALIZER_MEMBER_NAME : "";
static String xmlSerializerArgument(SupportedMimeType mimeType, IType type) {
return useXmlObjectSerializer(mimeType, type) ? ", " + XML_SERIALIZER_MEMBER_NAME : "";
}

/**
* Whether any of the convenience methods requires XML serialization (request or response). Used to decide whether
* the convenience client needs a static XML serializer member. Only applicable to the azure-core (v1) flavor.
* the convenience client needs a static XML serializer member. Only applicable to the azure-core (v1) data-plane
* flavor.
*
* @param convenienceMethods the convenience methods on the client.
* @return whether a static XML serializer member is required.
*/
public boolean useXmlSerializerMember(Collection<ConvenienceMethod> convenienceMethods) {
if (!JavaSettings.getInstance().isAzureV1() || convenienceMethods == null) {
if (!isXmlSerializationSupported() || convenienceMethods == null) {
return false;
}
for (ConvenienceMethod convenienceMethod : convenienceMethods) {
if (!isMethodIncluded(convenienceMethod)) {
continue;
}
ProxyMethod proxyMethod = convenienceMethod.getProtocolMethod().getProxyMethod();
// getResponseKnownMimeType simply parses a MIME string, so it is reused here for the request content type.
String requestContentType = convenienceMethod.getProtocolMethod().getProxyMethod().getRequestContentType();
if (requestContentType != null
&& SupportedMimeType.getResponseKnownMimeType(List.of(requestContentType)) == SupportedMimeType.XML) {
return true;
}
Set<String> responseContentTypes
= convenienceMethod.getProtocolMethod().getProxyMethod().getResponseContentTypes();
if (responseContentTypes != null
String requestContentType = proxyMethod.getRequestContentType();
boolean xmlRequest = requestContentType != null
&& SupportedMimeType.getResponseKnownMimeType(List.of(requestContentType)) == SupportedMimeType.XML;
Set<String> responseContentTypes = proxyMethod.getResponseContentTypes();
boolean xmlResponse = responseContentTypes != null
&& !responseContentTypes.isEmpty()
&& SupportedMimeType.getResponseKnownMimeType(responseContentTypes) == SupportedMimeType.XML) {
return true;
&& SupportedMimeType.getResponseKnownMimeType(responseContentTypes) == SupportedMimeType.XML;
if (!xmlRequest && !xmlResponse) {
continue;
}
// Inspect the convenience method body types (models), not the protocol method (which uses BinaryData). The
// XML serializer is only needed when an actual model is serialized/deserialized as XML.
for (ClientMethod method : convenienceMethod.getConvenienceMethods()) {
if (!isMethodIncluded(method)) {
continue;
}
if (xmlResponse && isXmlSerializableType(getConvenienceResponseBodyType(method))) {
return true;
}
if (xmlRequest && isXmlSerializableType(getConvenienceRequestBodyType(method))) {
return true;
}
}
}
return false;
}

/**
* Gets the (unwrapped) response body type of a convenience method, peeling reactive and response wrappers such as
* {@code Mono}, {@code Response}, {@code ResponseBase}, {@code PagedIterable} and {@code PagedFlux}.
*
* @param method the convenience method.
* @return the response body type.
*/
protected static IType getConvenienceResponseBodyType(ClientMethod method) {
IType type = method.getReturnValue().getType();
while (type instanceof GenericType) {
GenericType genericType = (GenericType) type;
String name = genericType.getName();
IType[] typeArguments = genericType.getTypeArguments();
if ((ClassType.MONO.getName().equals(name)
|| ClassType.FLUX.getName().equals(name)
|| ClassType.RESPONSE.getName().equals(name)
|| ClassType.PAGED_ITERABLE.getName().equals(name)
|| ClassType.PAGED_FLUX.getName().equals(name)) && typeArguments.length >= 1) {
type = typeArguments[0];
} else if ((ClassType.RESPONSE_BASE.getName().equals(name)
|| ClassType.PAGED_RESPONSE_BASE.getName().equals(name)) && typeArguments.length >= 2) {
type = typeArguments[1];
} else {
break;
}
}
return type;
}

/**
* Gets the client type of the request body (BODY location) parameter of a convenience method, or {@code null} if
* the method has no request body.
*
* @param method the convenience method.
* @return the request body type, or {@code null}.
*/
private static IType getConvenienceRequestBodyType(ClientMethod method) {
return method.getMethodParameters()
.stream()
.filter(p -> p.getRequestParameterLocation() == RequestParameterLocation.BODY)
.map(ClientMethodParameter::getClientType)
.findFirst()
.orElse(null);
}

public void write(ConvenienceMethod convenienceMethodObj, JavaClass classBlock,
Set<GenericType> typeReferenceStaticClasses) {
if (!isMethodIncluded(convenienceMethodObj)) {
Expand Down Expand Up @@ -627,7 +719,7 @@ private static String expressionConvertToBinaryData(String name, IType type, Str

default:
// JSON, XML etc.
String serializerArgument = xmlSerializerArgument(mimeType);
String serializerArgument = xmlSerializerArgument(mimeType, type);
if (type == ClassType.BINARY_DATA) {
return name;
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,8 @@ protected void writeMethodImplementation(ClientMethod protocolMethod, ClientMeth
protected void writeInvocationAndConversion(ClientMethod convenienceMethod, ClientMethod protocolMethod,
String invocationExpression, JavaBlock methodBlock, Set<GenericType> typeReferenceStaticClasses) {

IType responseBodyType = getResponseBodyType(convenienceMethod);
IType protocolResponseBodyType = getResponseBodyType(protocolMethod);
IType responseBodyType = getConvenienceResponseBodyType(convenienceMethod);
IType protocolResponseBodyType = getConvenienceResponseBodyType(protocolMethod);
IType rawResponseBodyType = convenienceMethod.getProxyMethod().getRawResponseBodyType();

String convertFromResponse
Expand Down Expand Up @@ -184,28 +184,14 @@ private String getProtocolMethodResponseStatement(ClientMethod protocolMethod, S
statement);
}

private IType getResponseBodyType(ClientMethod method) {
// no need to care about LRO
IType type = method.getReturnValue().getType();
if (type instanceof GenericType
&& (ClassType.RESPONSE.getName().equals(((GenericType) type).getName())
|| (ClassType.PAGED_ITERABLE.getName().equals(((GenericType) type).getName())))) {
type = ((GenericType) type).getTypeArguments()[0];
} else if (isResponseBase(type)) {
// TODO: ResponseBase is not in use, hence it may have bug
type = ((GenericType) type).getTypeArguments()[1];
}
return type;
}

private boolean isResponseBase(IType type) {
return type instanceof GenericType && ClassType.RESPONSE_BASE.getName().equals(((GenericType) type).getName());
}

private String expressionConvertFromBinaryData(IType responseBodyType, IType rawType, String invocationExpression,
Set<String> mediaTypes, Set<GenericType> typeReferenceStaticClasses) {
SupportedMimeType mimeType = SupportedMimeType.getResponseKnownMimeType(mediaTypes);
String serializerArgument = xmlSerializerArgument(mimeType);
String serializerArgument = xmlSerializerArgument(mimeType, responseBodyType);
switch (mimeType) {
case TEXT:
String basicText = invocationExpression + ".toString()";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

package com.microsoft.typespec.http.client.generator.core.template;

import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ArrayType;
import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClassType;
import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientMethod;
import com.microsoft.typespec.http.client.generator.core.model.clientmodel.GenericType;
import com.microsoft.typespec.http.client.generator.core.model.clientmodel.IType;
import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ReturnValue;
import java.util.List;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

public class ConvenienceMethodTemplateBaseTests {

@Test
public void unwrapsMonoResponse() {
IType leaf = ClassType.STRING;
IType returnType = GenericType.mono(GenericType.response(leaf));

IType result = ConvenienceMethodTemplateBase.getConvenienceResponseBodyType(methodWithReturnType(returnType));

Assertions.assertSame(leaf, result);
}

@Test
public void unwrapsPagedFlux() {
IType leaf = ClassType.BINARY_DATA;
IType returnType = GenericType.pagedFlux(leaf);

IType result = ConvenienceMethodTemplateBase.getConvenienceResponseBodyType(methodWithReturnType(returnType));

Assertions.assertSame(leaf, result);
}

@Test
public void unwrapsResponseBaseSecondTypeArgument() {
IType leaf = ClassType.INTEGER;
IType returnType = GenericType.restResponse(ClassType.BINARY_DATA, leaf);

IType result = ConvenienceMethodTemplateBase.getConvenienceResponseBodyType(methodWithReturnType(returnType));

Assertions.assertSame(leaf, result);
}

@Test
public void unwrapsNestedMonoPagedResponseBase() {
IType leaf = ArrayType.BYTE_ARRAY;
IType pagedResponseBase = new GenericType(ClassType.PAGED_RESPONSE_BASE.getPackage(),
ClassType.PAGED_RESPONSE_BASE.getName(), ClassType.STRING, leaf);
IType returnType = GenericType.mono(pagedResponseBase);

IType result = ConvenienceMethodTemplateBase.getConvenienceResponseBodyType(methodWithReturnType(returnType));

Assertions.assertSame(leaf, result);
}

@Test
public void keepsNonGenericTypeUnchanged() {
IType returnType = ClassType.BASE_64_URL;

IType result = ConvenienceMethodTemplateBase.getConvenienceResponseBodyType(methodWithReturnType(returnType));

Assertions.assertSame(returnType, result);
}

private static ClientMethod methodWithReturnType(IType returnType) {
return new ClientMethod.Builder().name("test")
.description("test")
.parameters(List.of())
.returnValue(new ReturnValue("test", returnType))
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
@ServiceClient(builder = XmlClientBuilder.class, isAsync = true)
public final class ModelWithArrayOfModelValueAsyncClient {
@Generated
private static final ObjectSerializer SERIALIZER = XmlSerializerProviders.createInstance();
private static final ObjectSerializer XML_SERIALIZER = XmlSerializerProviders.createInstance();

@Generated
private final ModelWithArrayOfModelValuesImpl serviceClient;
Expand Down Expand Up @@ -120,7 +120,7 @@ public Mono<ModelWithArrayOfModel> get() {
// Generated convenience method for getWithResponse
RequestOptions requestOptions = new RequestOptions();
return getWithResponse(requestOptions).flatMap(FluxUtil::toMono)
.map(protocolMethodData -> protocolMethodData.toObject(ModelWithArrayOfModel.class, SERIALIZER));
.map(protocolMethodData -> protocolMethodData.toObject(ModelWithArrayOfModel.class, XML_SERIALIZER));
}

/**
Expand All @@ -140,6 +140,6 @@ public Mono<ModelWithArrayOfModel> get() {
public Mono<Void> put(ModelWithArrayOfModel input) {
// Generated convenience method for putWithResponse
RequestOptions requestOptions = new RequestOptions();
return putWithResponse(BinaryData.fromObject(input, SERIALIZER), requestOptions).flatMap(FluxUtil::toMono);
return putWithResponse(BinaryData.fromObject(input, XML_SERIALIZER), requestOptions).flatMap(FluxUtil::toMono);
}
}
Loading
Loading