diff --git a/.chronus/changes/http-client-java-azure-continuation-token.md b/.chronus/changes/http-client-java-azure-continuation-token.md new file mode 100644 index 00000000000..1196fe24b9b --- /dev/null +++ b/.chronus/changes/http-client-java-azure-continuation-token.md @@ -0,0 +1,11 @@ +--- +changeKind: feature +packages: + - "@typespec/http-client-java" +--- + +Support continuation-token pagination in Azure Java clients. Tokens are hidden from client method signatures; resume through the paging API: + +```java +client.list().streamByPage(token); +``` diff --git a/.github/instructions/http-client-java.instructions.md b/.github/instructions/http-client-java.instructions.md index 98d1338a792..1e4c64acd43 100644 --- a/.github/instructions/http-client-java.instructions.md +++ b/.github/instructions/http-client-java.instructions.md @@ -5,6 +5,7 @@ applyTo: "packages/http-client-java/**/*" - The directories mentioned in this instruction are relative to `/packages/http-client-java`. For example, the root is the folder `/packages/http-client-java`; the directory `generator/http-client-generator-test` refers to `/packages/http-client-java/generator/http-client-generator-test`. - Always use absolute paths when changing directories or running scripts. - Whenever calling `npm`, use `--registry=https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/`. +- `README.md` is generated by `tspd`; do not edit it directly. Update `.tspd/docs/` or emitter option documentation, then run `npm run regen-docs`. # Update Node.js packages for latest dependencies diff --git a/packages/http-client-java/emitter/src/code-model-builder.ts b/packages/http-client-java/emitter/src/code-model-builder.ts index ac0abb3e57e..48b0879a002 100644 --- a/packages/http-client-java/emitter/src/code-model-builder.ts +++ b/packages/http-client-java/emitter/src/code-model-builder.ts @@ -1304,63 +1304,61 @@ export class CodeModelBuilder { let continuationTokenParameter: Parameter | undefined; let continuationTokenResponseProperty: Property[] | undefined; let continuationTokenResponseHeader: HttpHeader | undefined; - if (!this.isAzureV1()) { - // parameter would either be query or header parameter, so taking the last segment would be enough - const continuationTokenParameterSegment = - sdkMethod.pagingMetadata.continuationTokenParameterSegments?.at(-1); - // response could be response header, where the last segment would do; or it be json path in the response body, where we use "findResponsePropertySegments" to find them - const continuationTokenResponseSegment = - sdkMethod.pagingMetadata.continuationTokenResponseSegments?.at(-1); - if (continuationTokenParameterSegment && op.parameters) { - // for now, continuationToken is either request query or header parameter - const parameter = getHttpOperationParameter(sdkMethod, continuationTokenParameterSegment); - if (parameter) { - for (const param of op.parameters) { - if (param.protocol.http?.in === parameter.kind) { - if ( - parameter.kind === "header" && - param.language.default.serializedName?.toLowerCase() === - parameter.serializedName.toLowerCase() - ) { - continuationTokenParameter = param; - break; - } else if ( - parameter.kind === "query" && - param.language.default.serializedName === parameter.serializedName - ) { - continuationTokenParameter = param; - break; - } + // parameter would either be query or header parameter, so taking the last segment would be enough + const continuationTokenParameterSegment = + sdkMethod.pagingMetadata.continuationTokenParameterSegments?.at(-1); + // response could be response header, where the last segment would do; or it be json path in the response body, where we use "findResponsePropertySegments" to find them + const continuationTokenResponseSegment = + sdkMethod.pagingMetadata.continuationTokenResponseSegments?.at(-1); + if (continuationTokenParameterSegment && op.parameters) { + // for now, continuationToken is either request query or header parameter + const parameter = getHttpOperationParameter(sdkMethod, continuationTokenParameterSegment); + if (parameter) { + for (const param of op.parameters) { + if (param.protocol.http?.in === parameter.kind) { + if ( + parameter.kind === "header" && + param.language.default.serializedName?.toLowerCase() === + parameter.serializedName.toLowerCase() + ) { + continuationTokenParameter = param; + break; + } else if ( + parameter.kind === "query" && + param.language.default.serializedName === parameter.serializedName + ) { + continuationTokenParameter = param; + break; } } } } - if (continuationTokenResponseSegment && op.responses) { - if (continuationTokenResponseSegment?.kind === "responseheader") { - // continuationToken is response header - for (const response of op.responses) { - if (isSchemaResponse(response) && response.protocol.http) { - for (const header of response.protocol.http.headers ?? []) { - if ( - header.header.toLowerCase() === - continuationTokenResponseSegment.serializedName.toLowerCase() - ) { - continuationTokenResponseHeader = header; - break; - } + } + if (continuationTokenResponseSegment && op.responses) { + if (continuationTokenResponseSegment?.kind === "responseheader") { + // continuationToken is response header + for (const response of op.responses) { + if (isSchemaResponse(response) && response.protocol.http) { + for (const header of response.protocol.http.headers ?? []) { + if ( + header.header.toLowerCase() === + continuationTokenResponseSegment.serializedName.toLowerCase() + ) { + continuationTokenResponseHeader = header; + break; } } - if (continuationTokenResponseHeader) { - break; - } } - } else if (continuationTokenResponseSegment?.kind === "property") { - // continuationToken is response body property - continuationTokenResponseProperty = findResponsePropertySegments( - op, - sdkMethod.pagingMetadata.continuationTokenResponseSegments, - ); + if (continuationTokenResponseHeader) { + break; + } } + } else if (continuationTokenResponseSegment?.kind === "property") { + // continuationToken is response body property + continuationTokenResponseProperty = findResponsePropertySegments( + op, + sdkMethod.pagingMetadata.continuationTokenResponseSegments, + ); } } diff --git a/packages/http-client-java/emitter/test/code-model.test.ts b/packages/http-client-java/emitter/test/code-model.test.ts index 97db49dc1bd..ab87ee650c6 100644 --- a/packages/http-client-java/emitter/test/code-model.test.ts +++ b/packages/http-client-java/emitter/test/code-model.test.ts @@ -4,6 +4,7 @@ import { fileURLToPath } from "node:url"; import { assert, describe, expect, it } from "vitest"; import { parse, stringify } from "yaml"; import { CodeModelBuilder } from "../src/code-model-builder.js"; +import type { PageableContinuationToken } from "../src/common/client.js"; import { addSubClient, createClient } from "../src/common/client.js"; import { addGlobalParameter, createCodeModel } from "../src/common/code-model.js"; import { @@ -40,6 +41,57 @@ import { createResponseErrorSchema } from "../src/external-schemas.js"; import { cloneOperationParameter } from "../src/operation-utils.js"; describe("code model characterization", () => { + it.each(["azure", "standard"] as const)( + "preserves continuation-token metadata for %s", + async (flavor) => { + const program = await compile( + NodeHost, + fileURLToPath(new URL("./fixtures/continuation-token.tsp", import.meta.url)), + { noEmit: true }, + ); + expectDiagnosticEmpty(program.diagnostics); + const model = await new CodeModelBuilder(program, { + program, + emitterOutputDir: "/code-model-test-output", + options: { flavor, "dev-options": {} }, + perf: { + startTimer: () => ({ end: () => 0 }), + time: (_label, callback) => callback(), + timeAsync: (_label, callback) => callback(), + report: () => {}, + }, + }).build(); + expectDiagnosticEmpty(program.diagnostics); + const operations = model.clients.flatMap((client) => + client.operationGroups.flatMap((group) => group.operations ?? []), + ); + expect(operations).toHaveLength(4); + for (const operation of operations) { + const name = operation.language.default.name; + const pageable = operation.extensions?.["x-ms-pageable"] as + { continuationToken?: PageableContinuationToken; nextLinkName?: string } | undefined; + assert.isDefined(pageable?.continuationToken); + const token = pageable.continuationToken; + expect(token.parameter).toBe( + operation.parameters?.find((p) => p.language.default.serializedName === "token"), + ); + expect(token.parameter.protocol.http?.in).toBe( + name.startsWith("query") ? "query" : "header", + ); + expect(pageable.nextLinkName).toBeUndefined(); + if (name.endsWith("Header")) { + expect(token.responseHeader?.header).toBe("next-token"); + expect(token.responseProperty).toBeUndefined(); + } else { + expect(token.responseProperty?.map((p) => p.serializedName)).toEqual( + name === "queryBody" ? ["next", "token"] : ["nextToken"], + ); + expect(token.responseHeader).toBeUndefined(); + } + } + }, + ); + it("preserves metadata defaults and supplied schema references", () => { const schema = createStringSchema("string", "A string"); const property = createProperty("value", "A value", schema, { diff --git a/packages/http-client-java/emitter/test/fixtures/continuation-token.tsp b/packages/http-client-java/emitter/test/fixtures/continuation-token.tsp new file mode 100644 index 00000000000..34934da1552 --- /dev/null +++ b/packages/http-client-java/emitter/test/fixtures/continuation-token.tsp @@ -0,0 +1,40 @@ +import "@typespec/http"; + +using TypeSpec.Http; + +@service +namespace ContinuationToken; + +model Item { + id: string; +} + +@list +@route("/query-body") +op queryBody(@query @continuationToken token?: string): { + @pageItems items: Item[]; + next?: { + @continuationToken token?: string; + }; +}; + +@list +@route("/header-body") +op headerBody(@header @continuationToken token?: string): { + @pageItems items: Item[]; + @continuationToken nextToken?: string; +}; + +@list +@route("/query-header") +op queryHeader(@query @continuationToken token?: string): { + @pageItems items: Item[]; + @header("next-token") @continuationToken nextToken?: string; +}; + +@list +@route("/header-header") +op headerHeader(@header @continuationToken token?: string): { + @pageItems items: Item[]; + @header("next-token") @continuationToken nextToken?: string; +}; diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/mapper/PagingMetadata.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/mapper/PagingMetadata.java index c843858b03a..0b658001e41 100644 --- a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/mapper/PagingMetadata.java +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/mapper/PagingMetadata.java @@ -89,8 +89,9 @@ static PagingMetadata create(Operation operation, ProxyMethod proxyMethod, nextMethods = List.of(); } - final MethodPageDetails.ContinuationToken continuationToken - = getContinuationToken(xmsPageable, responseType, parametersDetails); + final MethodPageDetails.ContinuationToken continuationToken = CoreUtils.isNullOrEmpty(nextLinkPropertyReference) + ? getContinuationToken(xmsPageable, responseType, parametersDetails) + : null; final ClientMethodParameter maxPageSizeParameter = getPageSizeClientMethodParameter(parametersDetails); MethodPageDetails.NextLinkReInjection nextLinkReInjection = null; diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/ClientMethodTemplate.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/ClientMethodTemplate.java index ac07ae179e3..d4af96d58f8 100644 --- a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/ClientMethodTemplate.java +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/ClientMethodTemplate.java @@ -38,6 +38,7 @@ import io.clientcore.core.annotations.ReturnType; import io.clientcore.core.http.models.HttpHeaderName; import io.clientcore.core.utils.CoreUtils; +import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; @@ -99,6 +100,13 @@ private static String xmlPropertyPath(List propertyReferen return xmlPropertyPath(propertyReference) + ", " + ClassType.STRING.defaultValueExpression(itemElementName); } + private static String xmlNextLinkExpression(List properties) { + ModelPropertySegment terminal = properties.get(properties.size() - 1); + return "getXmlNextLink(res.getValue(), " + terminal.getProperty().isXmlAttribute() + ", " + + ClassType.STRING.defaultValueExpression(terminal.getProperty().getXmlNamespace()) + ", " + + xmlPropertyPath(properties) + ")"; + } + private static String serializedPropertyPath(List propertyReference) { return propertyReference.stream() .map(segment -> ClassType.STRING.defaultValueExpression(segment.getProperty().getSerializedName())) @@ -171,6 +179,11 @@ protected static void addOptionalVariables(JavaBlock function, ClientMethod clie // Parameter is required and will be part of the method signature. continue; } + if (clientMethod.isPageStreamingType() + && clientMethod.getMethodPageDetails().getContinuationToken() != null + && parameter == clientMethod.getMethodPageDetails().getContinuationToken().getClientMethodParameter()) { + continue; + } final String defaultValue = parameterDefaultValueExpression(parameter); function.line("final %s %s = %s;", parameter.getClientType(), parameter.getName(), defaultValue == null ? "null" : defaultValue); @@ -859,20 +872,26 @@ protected void generateProtocolPagingSync(ClientMethod clientMethod, JavaType ty protected void generateProtocolPagingPlainSync(ClientMethod clientMethod, JavaType typeBlock, JavaSettings settings) { + if (settings.isAzureV1() && hasContinuationToken(clientMethod)) { + generateContinuationTokenPaging(clientMethod, typeBlock, settings, true); + return; + } addServiceMethodAnnotation(typeBlock, ReturnType.COLLECTION); if (clientMethod.getMethodPageDetails().nonNullNextLink()) { writeMethod(typeBlock, clientMethod.getMethodVisibility(), clientMethod.getDeclaration(), function -> { addOptionalVariables(function, clientMethod); - function.line("RequestOptions requestOptionsForNextPage = new RequestOptions();"); - function.line( - "requestOptionsForNextPage.setContext(requestOptions != null && requestOptions.getContext() != null ? requestOptions.getContext() : " - + TemplateUtil.getContextNone() + ");"); + String nextOptions + = availablePagingVariableName("requestOptionsForNextPage", pagingVariableNames(clientMethod)); + function.line("RequestOptions %s = new RequestOptions();", nextOptions); + function.line(nextOptions + + ".setContext(requestOptions != null && requestOptions.getContext() != null ? requestOptions.getContext() : " + + TemplateUtil.getContextNone() + ");"); MethodPageDetails.NextLinkReInjection nextLinkReInjection = clientMethod.getMethodPageDetails().getNextLinkReInjection(); if (nextLinkReInjection != null) { - addQueryParameterReInjectionLogic(nextLinkReInjection, function); + addQueryParameterReInjectionLogic(nextLinkReInjection, clientMethod, function); } function.line("return new PagedIterable<>("); @@ -1043,14 +1062,16 @@ protected void pagedSinglePageResponseConversion(ProxyMethod restAPIMethod, Clie } if (clientMethod.getMethodPageDetails().nonNullNextLink()) { if (isXmlPagingResponse(clientMethod, settings)) { - function.line("getXmlNextLink(res.getValue(), %s),", - xmlPropertyPath(clientMethod.getMethodPageDetails().getNextLinkPropertyReference())); + function.line("%s,", + xmlNextLinkExpression(clientMethod.getMethodPageDetails().getNextLinkPropertyReference())); } else if (settings.isDataPlaneClient()) { function.line("getNextLink(res.getValue(), %s),", serializedPropertyPath(clientMethod.getMethodPageDetails().getNextLinkPropertyReference())); } else { function.line(nextLinkLine(clientMethod)); } + } else if (clientMethod.getMethodPageDetails().getContinuationToken() != null) { + function.line(continuationTokenExpression(clientMethod, settings) + ","); } else { function.line("null,"); } @@ -1066,29 +1087,37 @@ protected void generatePagingSync(ClientMethod clientMethod, JavaType typeBlock, addServiceMethodAnnotation(typeBlock, ReturnType.COLLECTION); writeMethod(typeBlock, clientMethod.getMethodVisibility(), clientMethod.getDeclaration(), function -> { addOptionalVariables(function, clientMethod); + String arguments = clientMethod.getMethodParameters() + .stream() + .filter(p -> !clientMethod.getMethodPageDetails().shouldHideParameter(p)) + .map(ClientMethodParameter::getName) + .collect(Collectors.joining(", ")); function.methodReturn(String.format("new PagedIterable<>(%s(%s))", - MethodNamer.getSimpleAsyncMethodName(clientMethod.getName()), clientMethod.getArgumentList())); + MethodNamer.getSimpleAsyncMethodName(clientMethod.getName()), arguments)); }); } protected void generatePagingPlainSync(ClientMethod clientMethod, JavaType typeBlock, JavaSettings settings) { + if (settings.isAzureV1() && hasContinuationToken(clientMethod)) { + generateContinuationTokenPaging(clientMethod, typeBlock, settings, true); + return; + } addServiceMethodAnnotation(typeBlock, ReturnType.COLLECTION); if (clientMethod.getMethodPageDetails().nonNullNextLink()) { writeMethod(typeBlock, clientMethod.getMethodVisibility(), clientMethod.getDeclaration(), function -> { addOptionalVariables(function, clientMethod); if (settings.isDataPlaneClient()) { - function.line("RequestOptions requestOptionsForNextPage = new RequestOptions();"); - function.line( - "requestOptionsForNextPage.setContext(requestOptions != null && requestOptions.getContext() != null ? requestOptions.getContext() : " - + TemplateUtil.getContextNone() + ");"); + String nextOptions + = availablePagingVariableName("requestOptionsForNextPage", pagingVariableNames(clientMethod)); + function.line("RequestOptions %s = new RequestOptions();", nextOptions); + function.line(nextOptions + + ".setContext(requestOptions != null && requestOptions.getContext() != null ? requestOptions.getContext() : " + + TemplateUtil.getContextNone() + ");"); } function.line("return new PagedIterable<>("); - String nextMethodArgs = clientMethod.getMethodPageDetails() - .getNextMethod() - .getArgumentList() - .replace("requestOptions", "requestOptionsForNextPage"); + String nextMethodArgs = clientMethod.getMethodPageDetails().getNextMethod().getArgumentList(); String firstPageArgs = clientMethod.getArgumentList(); function.indent(() -> { function.line("%s,", this.getPagingSinglePageExpression(clientMethod, @@ -1116,21 +1145,27 @@ protected void generatePagingPlainSync(ClientMethod clientMethod, JavaType typeB } protected void generatePagingAsync(ClientMethod clientMethod, JavaType typeBlock, JavaSettings settings) { + if (settings.isAzureV1() && hasContinuationToken(clientMethod)) { + generateContinuationTokenPaging(clientMethod, typeBlock, settings, false); + return; + } addServiceMethodAnnotation(typeBlock, ReturnType.COLLECTION); if (clientMethod.getMethodPageDetails().nonNullNextLink()) { writeMethod(typeBlock, clientMethod.getMethodVisibility(), clientMethod.getDeclaration(), function -> { addOptionalVariables(function, clientMethod); if (settings.isDataPlaneClient()) { - function.line("RequestOptions requestOptionsForNextPage = new RequestOptions();"); - function.line( - "requestOptionsForNextPage.setContext(requestOptions != null && requestOptions.getContext() != null ? requestOptions.getContext() : " - + TemplateUtil.getContextNone() + ");"); + String nextOptions + = availablePagingVariableName("requestOptionsForNextPage", pagingVariableNames(clientMethod)); + function.line("RequestOptions %s = new RequestOptions();", nextOptions); + function.line(nextOptions + + ".setContext(requestOptions != null && requestOptions.getContext() != null ? requestOptions.getContext() : " + + TemplateUtil.getContextNone() + ");"); } MethodPageDetails.NextLinkReInjection reinjectedParams = clientMethod.getMethodPageDetails().getNextLinkReInjection(); if (reinjectedParams != null) { - addQueryParameterReInjectionLogic(reinjectedParams, function); + addQueryParameterReInjectionLogic(reinjectedParams, clientMethod, function); } function.line("return new PagedFlux<>("); @@ -1161,19 +1196,128 @@ private static void addServiceMethodAnnotation(JavaType typeBlock, ReturnType re typeBlock.annotation("ServiceMethod(returns = ReturnType." + returnType.name() + ")"); } + private static boolean hasContinuationToken(ClientMethod clientMethod) { + return !clientMethod.getMethodPageDetails().nonNullNextLink() + && clientMethod.getMethodPageDetails().getContinuationToken() != null; + } + + private static String continuationTokenParameterName(ClientMethod clientMethod) { + MethodPageDetails.ContinuationToken token = clientMethod.getMethodPageDetails().getContinuationToken(); + Set usedNames + = new HashSet<>(List.of("requestOptions", "requestOptionsLocal", "requestLocal", "urlBuilder", "pageSize")); + clientMethod.getParameters() + .stream() + .filter(parameter -> parameter != token.getClientMethodParameter()) + .map(ClientMethodParameter::getName) + .forEach(usedNames::add); + String baseName = token.getRequestParameter().getName(); + return availablePagingVariableName(baseName, usedNames); + } + + private static Set pagingVariableNames(ClientMethod clientMethod) { + return clientMethod.getParameters() + .stream() + .map(ClientMethodParameter::getName) + .collect(Collectors.toCollection(HashSet::new)); + } + + private static String availablePagingVariableName(String baseName, Set usedNames) { + String name = baseName; + int suffix = 1; + while (!usedNames.add(name)) { + name = baseName + suffix++; + } + return name; + } + + private static String replacePagingArgument(String arguments, String name, String replacement) { + return List.of(arguments.split(", ")) + .stream() + .map(argument -> argument.equals(name) ? replacement : argument) + .collect(Collectors.joining(", ")); + } + + private static String pagingQueryCallback(String options, String request, String builder, String name, + String value) { + return options + ".addRequestCallback(" + request + " -> {" + "UrlBuilder " + builder + " = UrlBuilder.parse(" + + request + ".getUrl());" + builder + ".setQueryParameter(" + ClassType.STRING.defaultValueExpression(name) + + ", " + value + ");" + request + ".setUrl(" + builder + ".toString());});"; + } + + private void generateContinuationTokenPaging(ClientMethod clientMethod, JavaType typeBlock, JavaSettings settings, + boolean sync) { + addServiceMethodAnnotation(typeBlock, ReturnType.COLLECTION); + writeMethod(typeBlock, clientMethod.getMethodVisibility(), clientMethod.getDeclaration(), function -> { + addOptionalVariables(function, clientMethod); + ClientMethodParameter tokenParameter + = clientMethod.getMethodPageDetails().getContinuationToken().getClientMethodParameter(); + String firstArguments = clientMethod.getMethodParameters() + .stream() + .map(p -> p == tokenParameter ? "null" : p.getName()) + .collect(Collectors.joining(", ")); + String methodName = sync + ? clientMethod.getProxyMethod().getPagingSinglePageMethodName() + : clientMethod.getProxyMethod().getPagingAsyncSinglePageMethodName(); + String firstPage = getPagingSinglePageExpression(clientMethod, methodName, firstArguments, settings); + String tokenName = continuationTokenParameterName(clientMethod); + String nextArguments = settings.isDataPlaneClient() + ? firstArguments + : clientMethod.getMethodParameters() + .stream() + .map(p -> p == tokenParameter ? tokenName : p.getName()) + .collect(Collectors.joining(", ")); + String nextPage + = getPagingSinglePageExpression(clientMethod, methodName, nextArguments, settings, tokenName); + function.line("return new %s<>(", sync ? "PagedIterable" : "PagedFlux"); + function.indent(() -> { + function.line(firstPage + ","); + function.line(nextPage + ");"); + }); + }); + } + + private static String continuationTokenExpression(ClientMethod clientMethod, JavaSettings settings) { + MethodPageDetails.ContinuationToken token = clientMethod.getMethodPageDetails().getContinuationToken(); + if (token.getResponseHeaderSerializedName() != null) { + return "res.getHeaders().getValue(HttpHeaderName.fromString(" + + ClassType.STRING.defaultValueExpression(token.getResponseHeaderSerializedName()) + "))"; + } + List properties = token.getResponsePropertyReference(); + if (CoreUtils.isNullOrEmpty(properties)) { + throw new IllegalStateException("Continuation-token paging requires a response token property or header."); + } + if (isXmlPagingResponse(clientMethod, settings)) { + return xmlNextLinkExpression(properties); + } else if (settings.isDataPlaneClient()) { + return "getNextLink(res.getValue(), " + serializedPropertyPath(properties) + ")"; + } + String expression = "res.getValue()"; + StringBuilder nullChecks = new StringBuilder(expression + " == null"); + for (ModelPropertySegment property : properties) { + expression += "." + CodeNamer.getModelNamer().modelPropertyGetterName(property.getProperty()) + "()"; + nullChecks.append(" || ").append(expression).append(" == null"); + } + return nullChecks + " ? null : " + expression; + } + protected void addQueryParameterReInjectionLogic(MethodPageDetails.NextLinkReInjection nextLinkReInjection, - JavaBlock javaBlock) { + ClientMethod clientMethod, JavaBlock javaBlock) { + Set usedNames = pagingVariableNames(clientMethod); + String nextOptions = availablePagingVariableName("requestOptionsForNextPage", usedNames); + String httpRequest = availablePagingVariableName("httpRequest", usedNames); + String urlBuilder = availablePagingVariableName("urlBuilder", usedNames); + String queryParams = availablePagingVariableName("queryParams", usedNames); javaBlock.line("if (requestOptions != null) {"); javaBlock.indent(() -> { - javaBlock.line("requestOptions.addRequestCallback(httpRequest -> {"); + javaBlock.line("requestOptions.addRequestCallback(%s -> {", httpRequest); javaBlock.indent(() -> { - javaBlock.line("UrlBuilder urlBuilder = UrlBuilder.parse(httpRequest.getUrl().toString());"); - javaBlock.line("Map queryParams = urlBuilder.getQuery();"); + javaBlock.line("UrlBuilder %s = UrlBuilder.parse(%s.getUrl().toString());", urlBuilder, httpRequest); + javaBlock.line("Map %s = %s.getQuery();", queryParams, urlBuilder); for (String paramSerializedName : nextLinkReInjection.getQueryParameterSerializedNames()) { - javaBlock.line("if (queryParams.containsKey(\"" + paramSerializedName + "\")) {"); + String name = ClassType.STRING.defaultValueExpression(paramSerializedName); + javaBlock.line("if (%s.containsKey(%s)) {", queryParams, name); javaBlock.indent(() -> { - javaBlock.line("requestOptionsForNextPage.addQueryParam(\"" + paramSerializedName - + "\", queryParams.get(\"" + paramSerializedName + "\"));"); + javaBlock.line("%s.addQueryParam(%s, %s.get(%s));", nextOptions, name, queryParams, name); }); javaBlock.line("}"); } @@ -1508,14 +1652,16 @@ protected void generatePagedAsyncSinglePage(ClientMethod clientMethod, JavaType } if (clientMethod.getMethodPageDetails().nonNullNextLink()) { if (isXmlPagingResponse(clientMethod, settings)) { - function.line("getXmlNextLink(res.getValue(), %s),", - xmlPropertyPath(clientMethod.getMethodPageDetails().getNextLinkPropertyReference())); + function.line("%s,", xmlNextLinkExpression( + clientMethod.getMethodPageDetails().getNextLinkPropertyReference())); } else if (settings.isDataPlaneClient() && settings.isAzureV1()) { function.line("getNextLink(res.getValue(), %s),", serializedPropertyPath( clientMethod.getMethodPageDetails().getNextLinkPropertyReference())); } else { function.line(nextLinkLine(clientMethod)); } + } else if (clientMethod.getMethodPageDetails().getContinuationToken() != null) { + function.line(continuationTokenExpression(clientMethod, settings) + ","); } else { function.line("null,"); } @@ -1842,32 +1988,92 @@ protected void generateProtocolLongRunningBeginAsync(ClientMethod clientMethod, private String getPagingSinglePageExpression(ClientMethod clientMethod, String methodName, String argumentLine, JavaSettings settings) { - if (settings.isDataPlaneClient() && settings.isPageSizeEnabled()) { - Optional maxPageSizeSerializedName - = MethodUtil.serializedNameOfMaxPageSizeParameter(clientMethod.getProxyMethod()); + return getPagingSinglePageExpression(clientMethod, methodName, argumentLine, settings, null); + } + + private String getPagingSinglePageExpression(ClientMethod clientMethod, String methodName, String argumentLine, + JavaSettings settings, String continuationParameter) { + Optional maxPageSizeSerializedName = settings.isDataPlaneClient() && settings.isPageSizeEnabled() + ? MethodUtil.serializedNameOfMaxPageSizeParameter(clientMethod.getProxyMethod()) + : Optional.empty(); + boolean continuationToken + = settings.isAzureV1() && settings.isDataPlaneClient() && hasContinuationToken(clientMethod); + if (continuationToken || maxPageSizeSerializedName.isPresent()) { + Set usedNames = pagingVariableNames(clientMethod); + if (continuationParameter != null) { + usedNames.add(continuationParameter); + } + String requestOptionsLocal = availablePagingVariableName("requestOptionsLocal", usedNames); + String requestLocal = availablePagingVariableName("requestLocal", usedNames); + String urlBuilder = availablePagingVariableName("urlBuilder", usedNames); + String pageSize = availablePagingVariableName("pageSize", usedNames); + argumentLine = replacePagingArgument(argumentLine, "requestOptions", requestOptionsLocal); + StringBuilder expression = new StringBuilder("("); + if (continuationParameter != null) { + expression.append(continuationParameter); + } if (maxPageSizeSerializedName.isPresent()) { - argumentLine = argumentLine.replace("requestOptions", "requestOptionsLocal"); - StringBuilder expression = new StringBuilder(); - expression.append("(pageSize) -> {"); - expression.append( - "RequestOptions requestOptionsLocal = requestOptions == null ? new RequestOptions() : requestOptions;") - .append("if (pageSize != null) {") - .append(" requestOptionsLocal.addRequestCallback(requestLocal -> {") - .append(" UrlBuilder urlBuilder = UrlBuilder.parse(requestLocal.getUrl());") - .append(" urlBuilder.setQueryParameter(\"") - .append(maxPageSizeSerializedName.get()) - .append("\", String.valueOf(pageSize));") - .append(" requestLocal.setUrl(urlBuilder.toString());") - .append(" });") - .append("}") - .append(String.format("return %s(%s);", methodName, argumentLine)); - expression.append("}"); - return expression.toString(); + if (continuationParameter != null) { + expression.append(", "); + } + expression.append(pageSize); + } + expression.append(") -> {") + .append("RequestOptions ") + .append(requestOptionsLocal) + .append(" = requestOptions == null ? new RequestOptions() : requestOptions;"); + if (continuationToken) { + // Clear tokens from earlier callbacks before applying the current page's token. + ProxyMethodParameter token + = clientMethod.getMethodPageDetails().getContinuationToken().getRequestParameter(); + String name = ClassType.STRING.defaultValueExpression(token.getRequestParameterName()); + if (token.getRequestParameterLocation() == RequestParameterLocation.QUERY) { + expression.append(pagingQueryCallback(requestOptionsLocal, requestLocal, urlBuilder, + token.getRequestParameterName(), "null")); + if (continuationParameter != null) { + expression.append(requestOptionsLocal) + .append(".addQueryParam(") + .append(name) + .append(", ") + .append(continuationParameter) + .append(", ") + .append(token.getAlreadyEncoded()) + .append(");"); + } + } else { + expression.append(requestOptionsLocal) + .append(".addRequestCallback(") + .append(requestLocal) + .append(" -> ") + .append(requestLocal) + .append(".getHeaders().remove(") + .append(name) + .append("));"); + if (continuationParameter != null) { + expression.append(requestOptionsLocal) + .append(".setHeader(") + .append(name) + .append(", ") + .append(continuationParameter) + .append(");"); + } + } + } + if (maxPageSizeSerializedName.isPresent()) { + expression.append("if (") + .append(pageSize) + .append(" != null) {") + .append(pagingQueryCallback(requestOptionsLocal, requestLocal, urlBuilder, + maxPageSizeSerializedName.get(), "String.valueOf(" + pageSize + ")")) + .append("}"); } + expression.append(String.format("return %s(%s);", methodName, argumentLine)).append("}"); + return expression.toString(); } if (settings.isAzureV1()) { - return String.format("() -> %s(%s)", methodName, argumentLine); + return String.format("%s -> %s(%s)", continuationParameter == null ? "()" : continuationParameter, + methodName, argumentLine); } else { return String.format("pagingOptions -> { %s return %s(%s); }", getLogExceptionExpressionForPagingOptions(clientMethod), methodName, argumentLine); @@ -1876,23 +2082,33 @@ private String getPagingSinglePageExpression(ClientMethod clientMethod, String m private String getPagingNextPageExpression(ClientMethod clientMethod, String methodName, String argumentLine, JavaSettings settings) { + Set usedNames = pagingVariableNames(clientMethod); + String nextOptions = availablePagingVariableName("requestOptionsForNextPage", usedNames); + String nextLink = availablePagingVariableName("nextLink", usedNames); + argumentLine = replacePagingArgument(argumentLine, "nextLink", nextLink); if (settings.isDataPlaneClient() && settings.isPageSizeEnabled()) { Optional maxPageSizeSerializedName = MethodUtil.serializedNameOfMaxPageSizeParameter(clientMethod.getProxyMethod()); if (maxPageSizeSerializedName.isPresent()) { - argumentLine = argumentLine.replace("requestOptions", "requestOptionsLocal"); + String requestOptionsLocal = availablePagingVariableName("requestOptionsLocal", usedNames); + String requestLocal = availablePagingVariableName("requestLocal", usedNames); + String urlBuilder = availablePagingVariableName("urlBuilder", usedNames); + String pageSize = availablePagingVariableName("pageSize", usedNames); + argumentLine = replacePagingArgument(argumentLine, "requestOptions", requestOptionsLocal); StringBuilder expression = new StringBuilder(); - expression.append("(nextLink, pageSize) -> {"); - expression.append("RequestOptions requestOptionsLocal = new RequestOptions();") - .append("requestOptionsLocal.setContext(requestOptionsForNextPage.getContext());") - .append("if (pageSize != null) {") - .append(" requestOptionsLocal.addRequestCallback(requestLocal -> {") - .append(" UrlBuilder urlBuilder = UrlBuilder.parse(requestLocal.getUrl());") - .append(" urlBuilder.setQueryParameter(\"") - .append(maxPageSizeSerializedName.get()) - .append("\", String.valueOf(pageSize));") - .append(" requestLocal.setUrl(urlBuilder.toString());") - .append(" });") + expression.append("(").append(nextLink).append(", ").append(pageSize).append(") -> {"); + expression.append("RequestOptions ") + .append(requestOptionsLocal) + .append(" = new RequestOptions();") + .append(requestOptionsLocal) + .append(".setContext(") + .append(nextOptions) + .append(".getContext());") + .append("if (") + .append(pageSize) + .append(" != null) {") + .append(pagingQueryCallback(requestOptionsLocal, requestLocal, urlBuilder, + maxPageSizeSerializedName.get(), "String.valueOf(" + pageSize + ")")) .append("}") .append(String.format("return %s(%s);", methodName, argumentLine)); expression.append("}"); @@ -1901,13 +2117,13 @@ private String getPagingNextPageExpression(ClientMethod clientMethod, String met } if (settings.isDataPlaneClient()) { - argumentLine = argumentLine.replace("requestOptions", "requestOptionsForNextPage"); + argumentLine = replacePagingArgument(argumentLine, "requestOptions", nextOptions); } if (settings.isAzureV1()) { - return String.format("nextLink -> %s(%s)", methodName, argumentLine); + return String.format("%s -> %s(%s)", nextLink, methodName, argumentLine); } else { - return String.format("(pagingOptions, nextLink) -> { %s return %s(%s); }", + return String.format("(pagingOptions, %s) -> { %s return %s(%s); }", nextLink, getLogExceptionExpressionForPagingOptions(clientMethod), methodName, argumentLine); } } diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/ClientMethodTemplateBase.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/ClientMethodTemplateBase.java index 3dc7497a9f0..650ce00018a 100644 --- a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/ClientMethodTemplateBase.java +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/ClientMethodTemplateBase.java @@ -43,6 +43,7 @@ protected static void generateProtocolMethodJavadoc(ClientMethod clientMethod, J List queryParameters = clientMethod.getProxyMethod() .getAllParameters() .stream() + .filter(p -> !isHiddenContinuationToken(clientMethod, p)) .filter(p -> RequestParameterLocation.QUERY.equals(p.getRequestParameterLocation()) // ignore if synthesized by modelerfour, i.e. api-version && p.getOrigin() == ParameterSynthesizedOrigin.NONE) @@ -55,6 +56,7 @@ protected static void generateProtocolMethodJavadoc(ClientMethod clientMethod, J List headerParameters = clientMethod.getProxyMethod() .getAllParameters() .stream() + .filter(p -> !isHiddenContinuationToken(clientMethod, p)) .filter(p -> RequestParameterLocation.HEADER.equals(p.getRequestParameterLocation())) // ignore if synthesized by modelerfour and is constant // we would want user to provide a correct "content-type" if it is not a constant @@ -125,6 +127,9 @@ protected static void generateProtocolMethodJavadoc(ClientMethod clientMethod, J } clientMethod.getParameters() + .stream() + .filter( + p -> !clientMethod.isPageStreamingType() || !clientMethod.getMethodPageDetails().shouldHideParameter(p)) .forEach(p -> commentBlock.param(p.getName(), MethodUtil.methodParameterDescriptionOrDefault(p))); if (clientMethod.getProxyMethod() != null) { generateJavadocExceptions(clientMethod, commentBlock, false); @@ -138,6 +143,17 @@ protected static void generateProtocolMethodJavadoc(ClientMethod clientMethod, J } } + private static boolean isHiddenContinuationToken(ClientMethod clientMethod, ProxyMethodParameter parameter) { + if (!clientMethod.isPageStreamingType() || clientMethod.getMethodPageDetails().getContinuationToken() == null) { + return false; + } + ProxyMethodParameter token = clientMethod.getMethodPageDetails().getContinuationToken().getRequestParameter(); + return token.getRequestParameterLocation() == parameter.getRequestParameterLocation() + && (token.getRequestParameterLocation() == RequestParameterLocation.HEADER + ? token.getRequestParameterName().equalsIgnoreCase(parameter.getRequestParameterName()) + : token.getRequestParameterName().equals(parameter.getRequestParameterName())); + } + protected static void generateJavadocExceptions(ClientMethod clientMethod, JavaJavadocComment commentBlock, boolean useFullClassName) { ProxyMethod restAPIMethod = clientMethod.getProxyMethod(); diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/ConvenienceMethodTemplateBase.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/ConvenienceMethodTemplateBase.java index e72c75ea5b3..1af99ed6801 100644 --- a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/ConvenienceMethodTemplateBase.java +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/ConvenienceMethodTemplateBase.java @@ -430,27 +430,32 @@ protected void writeMethodImplementation(ClientMethod protocolMethod, ClientMeth } // invocation with protocol method parameters and RequestOptions - String invocationExpression = protocolMethod.getMethodInputParameters().stream().map(p -> { - String parameterName = p.getName(); - String expression = parameterExpressionsMap.get(parameterName); - IType parameterRawType = p.getRawType(); - if (isJsonMergePatchOperation - && ClientModelUtil.isClientModel(parameterRawType) - && RequestParameterLocation.BODY.equals(p.getRequestParameterLocation()) - && ClientModelUtil.isJsonMergePatchModel( - ClientModelUtil.getClientModel(((ClassType) parameterRawType).getName()), - JavaSettings.getInstance())) { - ClientModel clientModel = ClientModelUtil.getClientModel(((ClassType) parameterRawType).getName()); - // If it is polymorphic model, we need to enable json merge patch through root parent model - ClientModel rootParentModel = ClientModelUtil.getRootParent(clientModel); - IType rootParentModelType = rootParentModel.getType(); - - return writeParameterConversionExpressionWithJsonMergePatchEnabled(methodBlock, - rootParentModelType.toString(), parameterName, expression); - } else { - return expression == null ? parameterName : expression; - } - }).collect(Collectors.joining(", ")); + String invocationExpression = protocolMethod.getMethodInputParameters() + .stream() + .filter(p -> !protocolMethod.isPageStreamingType() + || !protocolMethod.getMethodPageDetails().shouldHideParameter(p)) + .map(p -> { + String parameterName = p.getName(); + String expression = parameterExpressionsMap.get(parameterName); + IType parameterRawType = p.getRawType(); + if (isJsonMergePatchOperation + && ClientModelUtil.isClientModel(parameterRawType) + && RequestParameterLocation.BODY.equals(p.getRequestParameterLocation()) + && ClientModelUtil.isJsonMergePatchModel( + ClientModelUtil.getClientModel(((ClassType) parameterRawType).getName()), + JavaSettings.getInstance())) { + ClientModel clientModel = ClientModelUtil.getClientModel(((ClassType) parameterRawType).getName()); + // If it is polymorphic model, we need to enable json merge patch through root parent model + ClientModel rootParentModel = ClientModelUtil.getRootParent(clientModel); + IType rootParentModelType = rootParentModel.getType(); + + return writeParameterConversionExpressionWithJsonMergePatchEnabled(methodBlock, + rootParentModelType.toString(), parameterName, expression); + } else { + return expression == null ? parameterName : expression; + } + }) + .collect(Collectors.joining(", ")); // write the invocation of protocol method, and related type conversion writeInvocationAndConversion(convenienceMethod, protocolMethod, invocationExpression, methodBlock, diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/ConvenienceSyncMethodTemplate.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/ConvenienceSyncMethodTemplate.java index d06633dbdbf..5d2bad78102 100644 --- a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/ConvenienceSyncMethodTemplate.java +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/ConvenienceSyncMethodTemplate.java @@ -193,6 +193,8 @@ protected void writeThrowException(ClientMethodType methodType, String exception private static String getMethodInvokeViaAsyncClient(ClientMethod convenienceMethod) { List parameterNames = convenienceMethod.getMethodInputParameters() .stream() + .filter(p -> !convenienceMethod.isPageStreamingType() + || !convenienceMethod.getMethodPageDetails().shouldHideParameter(p)) .map(ClientMethodParameter::getName) .collect(Collectors.toList()); diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/ServiceSyncClientWrapAsyncClientTemplate.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/ServiceSyncClientWrapAsyncClientTemplate.java index 85a1c3069c8..6a13a6c1171 100644 --- a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/ServiceSyncClientWrapAsyncClientTemplate.java +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/ServiceSyncClientWrapAsyncClientTemplate.java @@ -68,6 +68,8 @@ private String clientReference() { protected void writeMethodInvocation(ClientMethod clientMethod, JavaBlock function, boolean shouldReturn) { List parameterNames = clientMethod.getMethodInputParameters() .stream() + .filter(p -> !clientMethod.isPageStreamingType() + || !clientMethod.getMethodPageDetails().shouldHideParameter(p)) .map(ClientMethodParameter::getName) .collect(Collectors.toList()); diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/WrapperClientMethodTemplate.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/WrapperClientMethodTemplate.java index 37a162525c3..f6cba187c1f 100644 --- a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/WrapperClientMethodTemplate.java +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/WrapperClientMethodTemplate.java @@ -104,7 +104,11 @@ public void write(ClientMethod clientMethod, JavaType typeBlock) { protected void writeMethodInvocation(ClientMethod clientMethod, JavaBlock function, boolean shouldReturn) { List parameters = clientMethod.getMethodInputParameters(); function.line((shouldReturn ? "return " : "") + "this.serviceClient.%1$s(%2$s);", clientMethod.getName(), - parameters.stream().map(ClientMethodParameter::getName).collect(Collectors.joining(", "))); + parameters.stream() + .filter(p -> !clientMethod.isPageStreamingType() + || !clientMethod.getMethodPageDetails().shouldHideParameter(p)) + .map(ClientMethodParameter::getName) + .collect(Collectors.joining(", "))); } protected void generateJavadoc(ClientMethod clientMethod, JavaType typeBlock, ProxyMethod restAPIMethod) { @@ -112,6 +116,10 @@ protected void generateJavadoc(ClientMethod clientMethod, JavaType typeBlock, Pr comment.description(clientMethod.getDescription()); List methodParameters = clientMethod.getMethodInputParameters(); for (ClientMethodParameter parameter : methodParameters) { + if (clientMethod.isPageStreamingType() + && clientMethod.getMethodPageDetails().shouldHideParameter(parameter)) { + continue; + } comment.param(parameter.getName(), parameter.getDescription()); } if (clientMethod.hasParameterDeclaration()) { diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/clientcore/ClientCoreClientMethodTemplate.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/clientcore/ClientCoreClientMethodTemplate.java index f4dd1e6195c..d03a6480618 100644 --- a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/clientcore/ClientCoreClientMethodTemplate.java +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/clientcore/ClientCoreClientMethodTemplate.java @@ -1327,7 +1327,7 @@ protected String getLogExpression(String propertyName, String methodName) { @Override protected void addQueryParameterReInjectionLogic(MethodPageDetails.NextLinkReInjection nextLinkReInjection, - JavaBlock javaBlock) { + ClientMethod clientMethod, JavaBlock javaBlock) { } /** diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/util/TemplateUtil.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/util/TemplateUtil.java index 93d77a9479f..c218215cbc3 100644 --- a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/util/TemplateUtil.java +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/util/TemplateUtil.java @@ -311,28 +311,36 @@ private static void writeXmlPagingHelperMethods(JavaClass classBlock) { block.line("}"); block.line("return values;"); }); - classBlock.privateMethod("String getXmlNextLink(BinaryData binaryData, String... path)", block -> { - block.line( - "try (com.azure.xml.XmlReader reader = com.azure.xml.XmlReader.fromStream(binaryData.toStream())) {"); - block.indent(() -> { - block.line("reader.nextElement();"); - block.line("return getXmlNextLink(reader, path, 0);"); + classBlock.privateMethod( + "String getXmlNextLink(BinaryData binaryData, boolean attribute, String namespace, String... path)", + block -> { + block.line( + "try (com.azure.xml.XmlReader reader = com.azure.xml.XmlReader.fromStream(binaryData.toStream())) {"); + block.indent(() -> { + block.line("reader.nextElement();"); + block.line("return getXmlNextLink(reader, attribute, namespace, path, 0);"); + }); + block.line("} catch (javax.xml.stream.XMLStreamException e) {"); + block.indent( + () -> block.line("throw new IllegalStateException(\"Failed to read XML pageable response.\", e);")); + block.line("}"); }); - block.line("} catch (javax.xml.stream.XMLStreamException e) {"); - block.indent( - () -> block.line("throw new IllegalStateException(\"Failed to read XML pageable response.\", e);")); - block.line("}"); - }); - classBlock.privateMethod("String getXmlNextLink(com.azure.xml.XmlReader reader, String[] path, int pathIndex) " - + "throws javax.xml.stream.XMLStreamException", block -> { + classBlock.privateMethod( + "String getXmlNextLink(com.azure.xml.XmlReader reader, boolean attribute, String namespace, String[] path, int pathIndex) " + + "throws javax.xml.stream.XMLStreamException", + block -> { + block.ifBlock("attribute && pathIndex == path.length - 1", attributeBlock -> attributeBlock + .line("return reader.getStringAttribute(namespace, path[pathIndex]);")); block.line("while (reader.nextElement() != com.azure.xml.XmlToken.END_ELEMENT) {"); block.indent(() -> { - block.line("if (!reader.elementNameMatches(path[pathIndex])) {"); + block.line( + "if (!reader.elementNameMatches(pathIndex == path.length - 1 ? namespace : null, path[pathIndex])) {"); block.indent(() -> block.line("reader.skipElement();")); block.line("} else if (pathIndex == path.length - 1) {"); block.indent(() -> block.line("return reader.getStringElement();")); block.line("} else {"); - block.indent(() -> block.line("return getXmlNextLink(reader, path, pathIndex + 1);")); + block.indent( + () -> block.line("return getXmlNextLink(reader, attribute, namespace, path, pathIndex + 1);")); block.line("}"); }); block.line("}"); diff --git a/packages/http-client-java/generator/http-client-generator-core/src/test/java/com/microsoft/typespec/http/client/generator/core/mapper/PagingMetadataTests.java b/packages/http-client-java/generator/http-client-generator-core/src/test/java/com/microsoft/typespec/http/client/generator/core/mapper/PagingMetadataTests.java new file mode 100644 index 00000000000..a08fd17b11f --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/test/java/com/microsoft/typespec/http/client/generator/core/mapper/PagingMetadataTests.java @@ -0,0 +1,124 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.mapper; + +import com.microsoft.typespec.http.client.generator.core.MockUnitJavagen; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Header; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Language; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Languages; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Operation; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Parameter; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Protocol; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Protocols; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.RequestParameterLocation; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Schema; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.StringSchema; +import com.microsoft.typespec.http.client.generator.core.extension.model.extensionmodel.PageableContinuationToken; +import com.microsoft.typespec.http.client.generator.core.extension.model.extensionmodel.XmsExtensions; +import com.microsoft.typespec.http.client.generator.core.extension.model.extensionmodel.XmsPageable; +import com.microsoft.typespec.http.client.generator.core.extension.plugin.JavaSettings; +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; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientMethodType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientModel; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientModelProperty; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientModels; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.GenericType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ListType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.MethodPageDetails; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ProxyMethod; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ReturnValue; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.parallel.Isolated; + +@Isolated +public class PagingMetadataTests { + @AfterEach + public void restoreSettings() { + ClientModels.getInstance().clear(); + new MockUnitJavagen(); + JavaSettings.clear(); + } + + @Test + public void prefersNextLinkOverContinuationToken() { + new MockUnitJavagen(); + JavaSettings.clear(); + ClientModel model = new ClientModel.Builder().name("PagingMetadataResponse") + .packageName("com.azure.test") + .properties(List.of( + new ClientModelProperty.Builder().name("items") + .serializedName("items") + .wireType(new ListType(ClassType.STRING)) + .clientType(new ListType(ClassType.STRING)) + .build(), + new ClientModelProperty.Builder().name("nextLink") + .serializedName("nextLink") + .wireType(ClassType.STRING) + .clientType(ClassType.STRING) + .build())) + .build(); + ClientModels.getInstance().addModel(model); + StringSchema schema = new StringSchema(); + schema.setType(Schema.AllSchemaTypes.STRING); + Language language = new Language(); + language.setName("token"); + language.setSerializedName("token"); + Languages languages = new Languages(); + languages.setJava(language); + languages.setDefault(language); + Protocol http = new Protocol(); + http.setIn(RequestParameterLocation.QUERY); + Protocols protocols = new Protocols(); + protocols.setHttp(http); + Parameter parameter = new Parameter(); + parameter.setSchema(schema); + parameter.setLanguage(languages); + parameter.setProtocol(protocols); + Header header = new Header(); + header.setHeader("next-token"); + PageableContinuationToken continuationToken = new PageableContinuationToken(); + continuationToken.setParameter(parameter); + continuationToken.setResponseHeader(header); + XmsPageable pageable = new XmsPageable(); + pageable.setItemName("items"); + pageable.setContinuationToken(continuationToken); + XmsExtensions extensions = new XmsExtensions(); + extensions.setXmsPageable(pageable); + Operation operation = new Operation(); + operation.setExtensions(extensions); + ClientMethodParameter token = new ClientMethodParameter.Builder().name("token") + .annotations(List.of()) + .wireType(ClassType.STRING) + .build(); + ClientMethodParametersDetails parameters = new ClientMethodParametersDetails( + List.of(new ParametersTuple(parameter, token)), List.of(), Map.of(), null); + ProxyMethod proxy = new ProxyMethod.Builder().name("list") + .responseBodyType(new ClassType.Builder().name(model.getName()).packageName(model.getPackage()).build()) + .build(); + for (boolean nextLink : List.of(false, true)) { + pageable.setNextLinkName(nextLink ? "nextLink" : null); + PagingMetadata metadata = PagingMetadata.create(operation, proxy, parameters, JavaSettings.getInstance()); + Assertions.assertNotNull(metadata); + for (boolean sync : List.of(false, true)) { + MethodPageDetails details = metadata.asMethodPageDetails(sync); + Assertions.assertEquals(nextLink, details.nonNullNextLink()); + Assertions.assertEquals(nextLink, details.getContinuationToken() == null); + ClientMethod method = new ClientMethod.Builder().name("list") + .parameters(List.of(token, ClientMethodParameter.CONTEXT_PARAMETER)) + .methodPageDetails(details) + .type(sync ? ClientMethodType.PagingSync : ClientMethodType.PagingAsync) + .returnValue(new ReturnValue("The items.", + sync ? GenericType.pagedIterable(ClassType.STRING) : GenericType.pagedFlux(ClassType.STRING))) + .build(); + Assertions.assertEquals(nextLink, method.getParametersDeclaration().contains("String token")); + } + } + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/test/java/com/microsoft/typespec/http/client/generator/core/template/ContinuationTokenPagingTests.java b/packages/http-client-java/generator/http-client-generator-core/src/test/java/com/microsoft/typespec/http/client/generator/core/template/ContinuationTokenPagingTests.java new file mode 100644 index 00000000000..6d306d71c26 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/test/java/com/microsoft/typespec/http/client/generator/core/template/ContinuationTokenPagingTests.java @@ -0,0 +1,299 @@ +// 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.MockUnitJavagen; +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.ClassType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientMethod; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientMethodParameter; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientMethodType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientModelProperty; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.GenericType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.MethodPageDetails; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ModelPropertySegment; +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.clientmodel.ReturnValue; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaClass; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaFileContents; +import io.clientcore.core.utils.IOExceptionCheckedFunction; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import javax.tools.ToolProvider; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.junit.jupiter.api.parallel.Isolated; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +@Isolated +public class ContinuationTokenPagingTests { + @TempDir + Path compilationDirectory; + + @AfterEach + public void restoreSettings() { + new MockUnitJavagen(); + JavaSettings.clear(); + } + + @ParameterizedTest + @ValueSource(strings = { "requestOptionsLocal", "requestLocal", "urlBuilder", "pageSize" }) + public void avoidsTokenLambdaNameCollisions(String tokenName) throws IOException { + for (boolean pageSize : List.of(false, true)) { + String output = renderPaging(RequestParameterLocation.QUERY, false, true, tokenName, pageSize); + Assertions.assertTrue(output.contains("(" + tokenName + "1" + (pageSize ? ", pageSize" : "") + ") ->"), + output); + assertCompiles(output, "RequestOptions requestOptions"); + } + } + + @ParameterizedTest + @ValueSource(strings = { "requestOptionsFilter", "requestOptionsLocal", "requestLocal", "urlBuilder", "pageSize" }) + public void preservesNonTokenParameterNames(String parameterName) throws IOException { + ClientMethodParameter parameter = new ClientMethodParameter.Builder().name(parameterName) + .annotations(List.of()) + .wireType(ClassType.STRING) + .required(true) + .build(); + for (boolean sync : List.of(false, true)) { + for (boolean pageSize : List.of(false, true)) { + for (RequestParameterLocation location : List.of(RequestParameterLocation.QUERY, + RequestParameterLocation.HEADER)) { + String output = renderPaging(location, sync, true, "token", pageSize, parameter); + assertCompiles(output, "String " + parameterName + ", RequestOptions requestOptions"); + } + } + } + } + + @ParameterizedTest + @ValueSource( + strings = { + "requestOptionsFilter", + "requestOptionsLocal", + "requestOptionsForNextPage", + "requestLocal", + "urlBuilder", + "pageSize", + "nextLink", + "httpRequest", + "queryParams" }) + public void preservesNextLinkParameterNames(String parameterName) throws IOException { + ClientMethodParameter parameter = new ClientMethodParameter.Builder().name(parameterName) + .annotations(List.of()) + .wireType(ClassType.STRING) + .required(true) + .build(); + for (boolean sync : List.of(false, true)) { + for (boolean pageSize : List.of(false, true)) { + String output + = renderPaging(RequestParameterLocation.QUERY, sync, true, "token", pageSize, true, parameter); + assertCompiles(output, "String " + parameterName + ", RequestOptions requestOptions"); + } + } + } + + @Test + public void keepsTypedTokenAsOrdinaryParameterWithNextLink() throws IOException { + for (boolean sync : List.of(false, true)) { + String output = renderPaging(RequestParameterLocation.QUERY, sync, false, "token", false, true); + Assertions.assertTrue(output.contains("list(String token, Context context)"), output); + assertCompiles(output, "String token, Context context"); + } + } + + @Test + public void passesTypedTokenDirectlyWithContext() throws IOException { + for (boolean sync : List.of(false, true)) { + String output = renderPaging(RequestParameterLocation.QUERY, sync, false, "token", false); + String singlePage = sync ? "listSinglePage" : "listSinglePageAsync"; + Assertions.assertTrue(output.contains(singlePage + "(null, context)"), output); + Assertions.assertTrue(output.contains(singlePage + "(token, context)"), output); + Assertions.assertFalse(output.contains("requestOptions"), output); + assertCompiles(output, "String token, Context context"); + } + } + + @Test + public void passesQueryTokenThroughRequestOptions() { + String output = renderProtocolPaging(RequestParameterLocation.QUERY, false); + Assertions.assertTrue(output.contains("requestOptions == null ? new RequestOptions() : requestOptions")); + Assertions.assertTrue(output.contains("setQueryParameter(\"token\", null)")); + Assertions.assertTrue(output.contains("requestOptionsLocal.addQueryParam(\"token\", token, false)")); + Assertions.assertTrue(output.contains("listSinglePageAsync(requestOptionsLocal)")); + Assertions.assertFalse(output.contains("listSinglePageAsync(token,")); + } + + @Test + public void passesHeaderTokenThroughRequestOptions() { + String output = renderProtocolPaging(RequestParameterLocation.HEADER, true); + Assertions.assertTrue(output.contains("requestOptions == null ? new RequestOptions() : requestOptions")); + Assertions.assertTrue(output.contains("getHeaders().remove(\"token\")")); + Assertions.assertTrue(output.contains("requestOptionsLocal.setHeader(\"token\", token)")); + Assertions.assertTrue(output.contains("listSinglePage(requestOptionsLocal)")); + } + + private static String renderProtocolPaging(RequestParameterLocation location, boolean sync) { + return renderPaging(location, sync, true, "token", false); + } + + private static String renderPaging(RequestParameterLocation location, boolean sync, boolean dataPlane, + String tokenName, boolean pageSize, ClientMethodParameter... additionalParameters) { + return renderPaging(location, sync, dataPlane, tokenName, pageSize, false, additionalParameters); + } + + private static String renderPaging(RequestParameterLocation location, boolean sync, boolean dataPlane, + String tokenName, boolean pageSize, boolean nextLink, ClientMethodParameter... additionalParameters) { + new MockUnitJavagen() { + @Override + @SuppressWarnings("unchecked") + public T getValue(String key, IOExceptionCheckedFunction converter) { + if ("data-plane".equals(key)) { + return (T) Boolean.valueOf(dataPlane); + } + if ("enable-page-size".equals(key)) { + return (T) Boolean.valueOf(pageSize); + } + return super.getValue(key, converter); + } + }; + JavaSettings.clear(); + ProxyMethodParameter token = new ProxyMethodParameter.Builder().name(tokenName) + .requestParameterName(tokenName) + .requestParameterLocation(location) + .wireType(ClassType.STRING) + .build(); + ClientMethodParameter typedToken = new ClientMethodParameter.Builder().name(tokenName) + .annotations(List.of()) + .wireType(ClassType.STRING) + .build(); + ProxyMethodParameter maxPageSize = new ProxyMethodParameter.Builder().name("maxPageSize") + .requestParameterName("maxpagesize") + .requestParameterLocation(RequestParameterLocation.QUERY) + .wireType(ClassType.INTEGER) + .build(); + List parameters = new ArrayList<>(List.of(additionalParameters)); + parameters.addAll(dataPlane + ? List.of(ClientMethodParameter.REQUEST_OPTIONS_PARAMETER) + : List.of(typedToken, ClientMethodParameter.CONTEXT_PARAMETER)); + ClientMethod nextMethod = new ClientMethod.Builder().name("listNext") + .parameters(List.of( + new ClientMethodParameter.Builder().name("nextLink") + .annotations(List.of()) + .wireType(ClassType.STRING) + .required(true) + .build(), + dataPlane ? ClientMethodParameter.REQUEST_OPTIONS_PARAMETER : ClientMethodParameter.CONTEXT_PARAMETER)) + .proxyMethod(new ProxyMethod.Builder().name("listNext").build()) + .build(); + ClientMethod method = pagingMethod().newBuilder() + .type(sync ? ClientMethodType.PagingSync : ClientMethodType.PagingAsync) + .returnValue(new ReturnValue("The items.", + sync ? GenericType.pagedIterable(ClassType.STRING) : GenericType.pagedFlux(ClassType.STRING))) + .parameters(parameters) + .proxyMethod(new ProxyMethod.Builder().name("list") + .parameters(List.of()) + .allParameters(pageSize ? List.of(maxPageSize) : List.of()) + .build()) + .methodPageDetails(new MethodPageDetails(List.of(), + nextLink + ? List.of(new ModelPropertySegment(null, + new ClientModelProperty.Builder().name("nextLink") + .serializedName("nextLink") + .wireType(ClassType.STRING) + .clientType(ClassType.STRING) + .build())) + : null, + nextLink ? nextMethod : null, null, + nextLink + ? null + : new MethodPageDetails.ContinuationToken(token, dataPlane ? null : typedToken, "next-token", null), + null, nextLink && dataPlane ? new MethodPageDetails.NextLinkReInjection(List.of("filter")) : null)) + .build(); + JavaFileContents contents = new JavaFileContents(); + if (sync) { + ClientMethodTemplate.getInstance() + .generatePagingPlainSync(method, new JavaClass(contents), JavaSettings.getInstance()); + } else { + ClientMethodTemplate.getInstance() + .generatePagingAsync(method, new JavaClass(contents), JavaSettings.getInstance()); + } + return contents.toString(); + } + + private void assertCompiles(String method, String singlePageParameters) throws IOException { + String source = "import com.azure.core.annotation.*;\n" + "import com.azure.core.http.rest.*;\n" + + "import com.azure.core.util.*;\n" + "import reactor.core.publisher.Mono;\n" + "import java.util.Map;\n" + + "public class PagingCompilation {\n" + method + + "\nprivate Mono> listSinglePageAsync(" + singlePageParameters + + ") { return Mono.empty(); }\n" + "private PagedResponse listSinglePage(" + singlePageParameters + + ") { return null; }\n" + + "private Mono> listNextSinglePageAsync(String nextLink, RequestOptions options) { return Mono.empty(); }\n" + + "private PagedResponse listNextSinglePage(String nextLink, RequestOptions options) { return null; }\n" + + "private Mono> listNextSinglePageAsync(String nextLink, Context context) { return Mono.empty(); }\n" + + "private PagedResponse listNextSinglePage(String nextLink, Context context) { return null; }\n" + + "}\n"; + Path file = compilationDirectory.resolve("PagingCompilation.java"); + Files.writeString(file, source); + ByteArrayOutputStream diagnostics = new ByteArrayOutputStream(); + int result = ToolProvider.getSystemJavaCompiler() + .run(null, diagnostics, diagnostics, "--release", "11", "-classpath", + System.getProperty("surefire.test.class.path", System.getProperty("java.class.path")), "-d", + compilationDirectory.toString(), file.toString()); + Assertions.assertEquals(0, result, () -> diagnostics.toString(StandardCharsets.UTF_8) + "\n" + source); + } + + @Test + public void hidesTokenInWrapperDelegation() { + JavaFileContents contents = new JavaFileContents(); + WrapperClientMethodTemplate.getInstance().write(pagingMethod(), new JavaClass(contents)); + String output = contents.toString(); + Assertions.assertTrue(output.contains("list(String filter)")); + Assertions.assertTrue(output.contains("return this.serviceClient.list(filter);")); + Assertions.assertFalse(output.contains("@param token")); + } + + @Test + public void hidesTokenInSyncOverAsyncDelegation() { + JavaFileContents contents = new JavaFileContents(); + ServiceSyncClientWrapAsyncClientTemplate.getInstance().writeMethod(pagingMethod(), new JavaClass(contents)); + String output = contents.toString(); + Assertions.assertTrue(output.contains("list(String filter)")); + Assertions.assertTrue(output.contains("return new PagedIterable<>(this.client.list(filter));")); + Assertions.assertFalse(output.contains("@param token")); + } + + private static ClientMethod pagingMethod() { + ClientMethodParameter token = new ClientMethodParameter.Builder().name("token") + .annotations(List.of()) + .description("The continuation token.") + .wireType(ClassType.STRING) + .build(); + ClientMethodParameter filter = new ClientMethodParameter.Builder().name("filter") + .annotations(List.of()) + .description("The filter.") + .wireType(ClassType.STRING) + .required(true) + .build(); + MethodPageDetails details = new MethodPageDetails(List.of(), null, null, null, + new MethodPageDetails.ContinuationToken(null, token, "next-token", null), null, null); + return new ClientMethod.Builder().name("list") + .description("List items.") + .parameters(List.of(filter, token)) + .type(ClientMethodType.PagingSync) + .methodPageDetails(details) + .returnValue(new ReturnValue("The items.", GenericType.pagedIterable(ClassType.STRING))) + .build(); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/test/java/com/microsoft/typespec/http/client/generator/core/template/XmlContinuationTokenTests.java b/packages/http-client-java/generator/http-client-generator-core/src/test/java/com/microsoft/typespec/http/client/generator/core/template/XmlContinuationTokenTests.java new file mode 100644 index 00000000000..a00ead4e5f0 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/test/java/com/microsoft/typespec/http/client/generator/core/template/XmlContinuationTokenTests.java @@ -0,0 +1,181 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.template; + +import com.azure.core.http.HttpHeaders; +import com.azure.core.http.rest.PagedResponse; +import com.azure.core.http.rest.Response; +import com.azure.core.http.rest.SimpleResponse; +import com.azure.core.util.BinaryData; +import com.microsoft.typespec.http.client.generator.core.MockUnitJavagen; +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.ClassType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientMethod; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientMethodParameter; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientMethodType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientModelProperty; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.GenericType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ListType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.MethodPageDetails; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ModelPropertySegment; +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.clientmodel.ReturnValue; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaClass; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaFileContents; +import com.microsoft.typespec.http.client.generator.core.util.TemplateUtil; +import io.clientcore.core.utils.IOExceptionCheckedFunction; +import java.io.ByteArrayOutputStream; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import javax.tools.ToolProvider; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.io.TempDir; +import org.junit.jupiter.api.parallel.Isolated; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +@Isolated +public class XmlContinuationTokenTests { + @TempDir + Path compilationDirectory; + + @AfterEach + public void restoreSettings() { + new MockUnitJavagen(); + JavaSettings.clear(); + } + + @ParameterizedTest + @CsvSource({ + "true,false,false", + "true,true,false", + "true,false,true", + "true,true,true", + "false,false,false", + "false,true,false", + "false,false,true", + "false,true,true" }) + public void readsXmlToken(boolean attribute, boolean nested, boolean namespaced) throws Exception { + new MockUnitJavagen() { + @Override + @SuppressWarnings("unchecked") + public T getValue(String key, IOExceptionCheckedFunction converter) { + return "data-plane".equals(key) ? (T) Boolean.TRUE : super.getValue(key, converter); + } + }; + JavaSettings.clear(); + ClassType xmlType = new ClassType.Builder().name("XmlPage").packageName("test").usedInXml(true).build(); + ClassType itemType = new ClassType.Builder().name("Item").packageName("test").build(); + List tokenPath = new ArrayList<>(); + if (nested) { + tokenPath.add(new ModelPropertySegment(null, + new ClientModelProperty.Builder().name("paging").xmlName("Paging").wireType(xmlType).build())); + } + tokenPath.add(new ModelPropertySegment(null, + new ClientModelProperty.Builder().name("nextMarker") + .xmlName("NextMarker") + .xmlAttribute(attribute) + .xmlNamespace(namespaced ? "urn:paging" : null) + .wireType(ClassType.STRING) + .build())); + ProxyMethod proxy = new ProxyMethod.Builder().name("list") + .parameters(List.of()) + .allParameters(List.of()) + .rawResponseBodyType(xmlType) + .returnType(GenericType.response(ClassType.BINARY_DATA)) + .responseExpectedStatusCodes(List.of(200)) + .build(); + ClientMethod method = new ClientMethod.Builder().name("list") + .description("List items.") + .type(ClientMethodType.PagingSync) + .parameters(List.of(ClientMethodParameter.REQUEST_OPTIONS_PARAMETER)) + .returnValue(new ReturnValue("The items.", GenericType.pagedIterable(ClassType.BINARY_DATA))) + .proxyMethod(proxy) + .validateExpressions(Map.of()) + .requiredNullableParameterExpressions(List.of()) + .methodPageDetails(new MethodPageDetails( + List.of(new ModelPropertySegment(null, + new ClientModelProperty.Builder().name("items") + .xmlName("Items") + .xmlListElementName("Item") + .clientType(new ListType(itemType)) + .build())), + null, null, null, + new MethodPageDetails.ContinuationToken(new ProxyMethodParameter.Builder().name("token") + .requestParameterName("token") + .requestParameterLocation(RequestParameterLocation.QUERY) + .build(), null, null, tokenPath), + null, null)) + .build(); + JavaFileContents contents = new JavaFileContents(); + JavaClass classBlock = new JavaClass(contents); + TemplateUtil.writeClientMethodsAndHelpers(classBlock, List.of(method)); + classBlock.publicMethod("PagedResponse read(Response res)", + block -> ClientMethodTemplate.getInstance() + .pagedSinglePageResponseConversion(proxy, method, JavaSettings.getInstance(), block)); + ClientMethod nextLinkMethod = method.newBuilder() + .methodPageDetails(new MethodPageDetails(method.getMethodPageDetails().getPageItemsPropertyReference(), + tokenPath, null, null, null, null, null)) + .build(); + classBlock.publicMethod("PagedResponse readNextLink(Response res)", + block -> ClientMethodTemplate.getInstance() + .pagedSinglePageResponseConversion(proxy, nextLinkMethod, JavaSettings.getInstance(), block)); + String source = "package test;\nimport com.azure.core.annotation.*;\nimport com.azure.core.http.rest.*;\n" + + "import com.azure.core.util.*;\nimport com.azure.core.util.serializer.*;\n" + + "import java.util.*;\nimport java.util.stream.Collectors;\n" + "public class XmlPagingCompilation {\n" + + contents + "\nprivate PagedResponse listSinglePage(RequestOptions options) { return null; }\n" + + "private PagedFlux listAsync(RequestOptions options) { return null; }\n}\n" + + "class Item { static Item fromXml(com.azure.xml.XmlReader reader, String root)" + + " throws javax.xml.stream.XMLStreamException { reader.skipElement(); return new Item(); } }\n"; + Path file = compilationDirectory.resolve("XmlPagingCompilation.java"); + Files.writeString(file, source); + Path provider = compilationDirectory.resolve("XmlSerializerProviders.java"); + Path serializer = compilationDirectory.resolve("XmlSerializer.java"); + Files.writeString(provider, + "package test;\n" + TemplateUtil.loadTextFromResource("XmlSerializerProviders.java")); + Files.writeString(serializer, "package test;\n" + TemplateUtil.loadTextFromResource("XmlSerializer.java")); + ByteArrayOutputStream diagnostics = new ByteArrayOutputStream(); + int result = ToolProvider.getSystemJavaCompiler() + .run(null, diagnostics, diagnostics, "--release", "11", "-classpath", + System.getProperty("surefire.test.class.path", System.getProperty("java.class.path")), "-d", + compilationDirectory.toString(), file.toString(), provider.toString(), serializer.toString()); + Assertions.assertEquals(0, result, () -> diagnostics.toString(StandardCharsets.UTF_8) + "\n" + source); + String marker = namespaced ? "p:NextMarker" : "NextMarker"; + String token = attribute ? " " + marker + "=\"page2\"" : "<" + marker + ">page2"; + String decoys = namespaced && !attribute + ? "wrongNamespaceunqualified" + : ""; + String element = nested ? "Paging" : "PetListResult"; + String xml = attribute + ? "<" + element + token + ">" + : "<" + element + ">" + decoys + token + ""; + if (nested) { + xml = "" + xml + ""; + } + xml = xml.replaceFirst(" generated = loader.loadClass("test.XmlPagingCompilation"); + Object client = generated.getConstructor().newInstance(); + for (String methodName : List.of("read", "readNextLink")) { + var read = generated.getMethod(methodName, Response.class); + PagedResponse page = (PagedResponse) read.invoke(client, + new SimpleResponse<>(null, 200, new HttpHeaders(), BinaryData.fromString(xml))); + Assertions.assertEquals("page2", page.getContinuationToken()); + PagedResponse lastPage = (PagedResponse) read.invoke(client, + new SimpleResponse<>(null, 200, new HttpHeaders(), BinaryData.fromString(xml.replace(token, "")))); + Assertions.assertNull(lastPage.getContinuationToken()); + } + } + } +} diff --git a/packages/http-client-java/generator/http-client-generator-test/src/main/java/payload/pageable/ServerDrivenPaginationContinuationTokenAsyncClient.java b/packages/http-client-java/generator/http-client-generator-test/src/main/java/payload/pageable/ServerDrivenPaginationContinuationTokenAsyncClient.java index 975886ae606..5f4f6345552 100644 --- a/packages/http-client-java/generator/http-client-generator-test/src/main/java/payload/pageable/ServerDrivenPaginationContinuationTokenAsyncClient.java +++ b/packages/http-client-java/generator/http-client-generator-test/src/main/java/payload/pageable/ServerDrivenPaginationContinuationTokenAsyncClient.java @@ -47,7 +47,6 @@ public final class ServerDrivenPaginationContinuationTokenAsyncClient { * * * - * * *
Query Parameters
NameTypeRequiredDescription
tokenStringNoThe token parameter
barStringNoThe bar parameter
* You can add these to a request with {@link RequestOptions#addQueryParam} @@ -95,7 +94,6 @@ public PagedFlux requestQueryResponseBody(RequestOptions requestOpti * * * - * * *
Header Parameters
NameTypeRequiredDescription
tokenStringNoThe token parameter
fooStringNoThe foo parameter
* You can add these to a request with {@link RequestOptions#addHeader} @@ -129,7 +127,6 @@ public PagedFlux requestHeaderResponseBody(RequestOptions requestOpt * * * - * * *
Query Parameters
NameTypeRequiredDescription
tokenStringNoThe token parameter
barStringNoThe bar parameter
* You can add these to a request with {@link RequestOptions#addQueryParam} @@ -177,7 +174,6 @@ public PagedFlux requestQueryResponseHeader(RequestOptions requestOp * * * - * * *
Header Parameters
NameTypeRequiredDescription
tokenStringNoThe token parameter
fooStringNoThe foo parameter
* You can add these to a request with {@link RequestOptions#addHeader} @@ -211,7 +207,6 @@ public PagedFlux requestHeaderResponseHeader(RequestOptions requestO * * * - * * *
Query Parameters
NameTypeRequiredDescription
tokenStringNoThe token parameter
barStringNoThe bar parameter
* You can add these to a request with {@link RequestOptions#addQueryParam} @@ -259,7 +254,6 @@ public PagedFlux requestQueryNestedResponseBody(RequestOptions reque * * * - * * *
Header Parameters
NameTypeRequiredDescription
tokenStringNoThe token parameter
fooStringNoThe foo parameter
* You can add these to a request with {@link RequestOptions#addHeader} @@ -290,7 +284,6 @@ public PagedFlux requestHeaderNestedResponseBody(RequestOptions requ /** * The requestQueryResponseBody operation. * - * @param token The token parameter. * @param foo The foo parameter. * @param bar The bar parameter. * @throws IllegalArgumentException thrown if parameters fail the validation. @@ -303,12 +296,9 @@ public PagedFlux requestHeaderNestedResponseBody(RequestOptions requ */ @Generated @ServiceMethod(returns = ReturnType.COLLECTION) - public PagedFlux requestQueryResponseBody(String token, String foo, String bar) { + public PagedFlux requestQueryResponseBody(String foo, String bar) { // Generated convenience method for requestQueryResponseBody RequestOptions requestOptions = new RequestOptions(); - if (token != null) { - requestOptions.addQueryParam("token", token, false); - } if (foo != null) { requestOptions.setHeader(HttpHeaderName.fromString("foo"), foo); } @@ -377,7 +367,6 @@ public PagedFlux requestQueryResponseBody() { /** * The requestHeaderResponseBody operation. * - * @param token The token parameter. * @param foo The foo parameter. * @param bar The bar parameter. * @throws IllegalArgumentException thrown if parameters fail the validation. @@ -390,12 +379,9 @@ public PagedFlux requestQueryResponseBody() { */ @Generated @ServiceMethod(returns = ReturnType.COLLECTION) - public PagedFlux requestHeaderResponseBody(String token, String foo, String bar) { + public PagedFlux requestHeaderResponseBody(String foo, String bar) { // Generated convenience method for requestHeaderResponseBody RequestOptions requestOptions = new RequestOptions(); - if (token != null) { - requestOptions.setHeader(HttpHeaderName.fromString("token"), token); - } if (foo != null) { requestOptions.setHeader(HttpHeaderName.fromString("foo"), foo); } @@ -464,7 +450,6 @@ public PagedFlux requestHeaderResponseBody() { /** * The requestQueryResponseHeader operation. * - * @param token The token parameter. * @param foo The foo parameter. * @param bar The bar parameter. * @throws IllegalArgumentException thrown if parameters fail the validation. @@ -477,12 +462,9 @@ public PagedFlux requestHeaderResponseBody() { */ @Generated @ServiceMethod(returns = ReturnType.COLLECTION) - public PagedFlux requestQueryResponseHeader(String token, String foo, String bar) { + public PagedFlux requestQueryResponseHeader(String foo, String bar) { // Generated convenience method for requestQueryResponseHeader RequestOptions requestOptions = new RequestOptions(); - if (token != null) { - requestOptions.addQueryParam("token", token, false); - } if (foo != null) { requestOptions.setHeader(HttpHeaderName.fromString("foo"), foo); } @@ -551,7 +533,6 @@ public PagedFlux requestQueryResponseHeader() { /** * The requestHeaderResponseHeader operation. * - * @param token The token parameter. * @param foo The foo parameter. * @param bar The bar parameter. * @throws IllegalArgumentException thrown if parameters fail the validation. @@ -564,12 +545,9 @@ public PagedFlux requestQueryResponseHeader() { */ @Generated @ServiceMethod(returns = ReturnType.COLLECTION) - public PagedFlux requestHeaderResponseHeader(String token, String foo, String bar) { + public PagedFlux requestHeaderResponseHeader(String foo, String bar) { // Generated convenience method for requestHeaderResponseHeader RequestOptions requestOptions = new RequestOptions(); - if (token != null) { - requestOptions.setHeader(HttpHeaderName.fromString("token"), token); - } if (foo != null) { requestOptions.setHeader(HttpHeaderName.fromString("foo"), foo); } @@ -638,7 +616,6 @@ public PagedFlux requestHeaderResponseHeader() { /** * The requestQueryNestedResponseBody operation. * - * @param token The token parameter. * @param foo The foo parameter. * @param bar The bar parameter. * @throws IllegalArgumentException thrown if parameters fail the validation. @@ -651,12 +628,9 @@ public PagedFlux requestHeaderResponseHeader() { */ @Generated @ServiceMethod(returns = ReturnType.COLLECTION) - public PagedFlux requestQueryNestedResponseBody(String token, String foo, String bar) { + public PagedFlux requestQueryNestedResponseBody(String foo, String bar) { // Generated convenience method for requestQueryNestedResponseBody RequestOptions requestOptions = new RequestOptions(); - if (token != null) { - requestOptions.addQueryParam("token", token, false); - } if (foo != null) { requestOptions.setHeader(HttpHeaderName.fromString("foo"), foo); } @@ -725,7 +699,6 @@ public PagedFlux requestQueryNestedResponseBody() { /** * The requestHeaderNestedResponseBody operation. * - * @param token The token parameter. * @param foo The foo parameter. * @param bar The bar parameter. * @throws IllegalArgumentException thrown if parameters fail the validation. @@ -738,12 +711,9 @@ public PagedFlux requestQueryNestedResponseBody() { */ @Generated @ServiceMethod(returns = ReturnType.COLLECTION) - public PagedFlux requestHeaderNestedResponseBody(String token, String foo, String bar) { + public PagedFlux requestHeaderNestedResponseBody(String foo, String bar) { // Generated convenience method for requestHeaderNestedResponseBody RequestOptions requestOptions = new RequestOptions(); - if (token != null) { - requestOptions.setHeader(HttpHeaderName.fromString("token"), token); - } if (foo != null) { requestOptions.setHeader(HttpHeaderName.fromString("foo"), foo); } diff --git a/packages/http-client-java/generator/http-client-generator-test/src/main/java/payload/pageable/ServerDrivenPaginationContinuationTokenClient.java b/packages/http-client-java/generator/http-client-generator-test/src/main/java/payload/pageable/ServerDrivenPaginationContinuationTokenClient.java index 545b4d29e28..8137c2bd88e 100644 --- a/packages/http-client-java/generator/http-client-generator-test/src/main/java/payload/pageable/ServerDrivenPaginationContinuationTokenClient.java +++ b/packages/http-client-java/generator/http-client-generator-test/src/main/java/payload/pageable/ServerDrivenPaginationContinuationTokenClient.java @@ -43,7 +43,6 @@ public final class ServerDrivenPaginationContinuationTokenClient { * * * - * * *
Query Parameters
NameTypeRequiredDescription
tokenStringNoThe token parameter
barStringNoThe bar parameter
* You can add these to a request with {@link RequestOptions#addQueryParam} @@ -91,7 +90,6 @@ public PagedIterable requestQueryResponseBody(RequestOptions request * * * - * * *
Header Parameters
NameTypeRequiredDescription
tokenStringNoThe token parameter
fooStringNoThe foo parameter
* You can add these to a request with {@link RequestOptions#addHeader} @@ -125,7 +123,6 @@ public PagedIterable requestHeaderResponseBody(RequestOptions reques * * * - * * *
Query Parameters
NameTypeRequiredDescription
tokenStringNoThe token parameter
barStringNoThe bar parameter
* You can add these to a request with {@link RequestOptions#addQueryParam} @@ -173,7 +170,6 @@ public PagedIterable requestQueryResponseHeader(RequestOptions reque * * * - * * *
Header Parameters
NameTypeRequiredDescription
tokenStringNoThe token parameter
fooStringNoThe foo parameter
* You can add these to a request with {@link RequestOptions#addHeader} @@ -207,7 +203,6 @@ public PagedIterable requestHeaderResponseHeader(RequestOptions requ * * * - * * *
Query Parameters
NameTypeRequiredDescription
tokenStringNoThe token parameter
barStringNoThe bar parameter
* You can add these to a request with {@link RequestOptions#addQueryParam} @@ -255,7 +250,6 @@ public PagedIterable requestQueryNestedResponseBody(RequestOptions r * * * - * * *
Header Parameters
NameTypeRequiredDescription
tokenStringNoThe token parameter
fooStringNoThe foo parameter
* You can add these to a request with {@link RequestOptions#addHeader} @@ -286,7 +280,6 @@ public PagedIterable requestHeaderNestedResponseBody(RequestOptions /** * The requestQueryResponseBody operation. * - * @param token The token parameter. * @param foo The foo parameter. * @param bar The bar parameter. * @throws IllegalArgumentException thrown if parameters fail the validation. @@ -299,12 +292,9 @@ public PagedIterable requestHeaderNestedResponseBody(RequestOptions */ @Generated @ServiceMethod(returns = ReturnType.COLLECTION) - public PagedIterable requestQueryResponseBody(String token, String foo, String bar) { + public PagedIterable requestQueryResponseBody(String foo, String bar) { // Generated convenience method for requestQueryResponseBody RequestOptions requestOptions = new RequestOptions(); - if (token != null) { - requestOptions.addQueryParam("token", token, false); - } if (foo != null) { requestOptions.setHeader(HttpHeaderName.fromString("foo"), foo); } @@ -337,7 +327,6 @@ public PagedIterable requestQueryResponseBody() { /** * The requestHeaderResponseBody operation. * - * @param token The token parameter. * @param foo The foo parameter. * @param bar The bar parameter. * @throws IllegalArgumentException thrown if parameters fail the validation. @@ -350,12 +339,9 @@ public PagedIterable requestQueryResponseBody() { */ @Generated @ServiceMethod(returns = ReturnType.COLLECTION) - public PagedIterable requestHeaderResponseBody(String token, String foo, String bar) { + public PagedIterable requestHeaderResponseBody(String foo, String bar) { // Generated convenience method for requestHeaderResponseBody RequestOptions requestOptions = new RequestOptions(); - if (token != null) { - requestOptions.setHeader(HttpHeaderName.fromString("token"), token); - } if (foo != null) { requestOptions.setHeader(HttpHeaderName.fromString("foo"), foo); } @@ -388,7 +374,6 @@ public PagedIterable requestHeaderResponseBody() { /** * The requestQueryResponseHeader operation. * - * @param token The token parameter. * @param foo The foo parameter. * @param bar The bar parameter. * @throws IllegalArgumentException thrown if parameters fail the validation. @@ -401,12 +386,9 @@ public PagedIterable requestHeaderResponseBody() { */ @Generated @ServiceMethod(returns = ReturnType.COLLECTION) - public PagedIterable requestQueryResponseHeader(String token, String foo, String bar) { + public PagedIterable requestQueryResponseHeader(String foo, String bar) { // Generated convenience method for requestQueryResponseHeader RequestOptions requestOptions = new RequestOptions(); - if (token != null) { - requestOptions.addQueryParam("token", token, false); - } if (foo != null) { requestOptions.setHeader(HttpHeaderName.fromString("foo"), foo); } @@ -439,7 +421,6 @@ public PagedIterable requestQueryResponseHeader() { /** * The requestHeaderResponseHeader operation. * - * @param token The token parameter. * @param foo The foo parameter. * @param bar The bar parameter. * @throws IllegalArgumentException thrown if parameters fail the validation. @@ -452,12 +433,9 @@ public PagedIterable requestQueryResponseHeader() { */ @Generated @ServiceMethod(returns = ReturnType.COLLECTION) - public PagedIterable requestHeaderResponseHeader(String token, String foo, String bar) { + public PagedIterable requestHeaderResponseHeader(String foo, String bar) { // Generated convenience method for requestHeaderResponseHeader RequestOptions requestOptions = new RequestOptions(); - if (token != null) { - requestOptions.setHeader(HttpHeaderName.fromString("token"), token); - } if (foo != null) { requestOptions.setHeader(HttpHeaderName.fromString("foo"), foo); } @@ -490,7 +468,6 @@ public PagedIterable requestHeaderResponseHeader() { /** * The requestQueryNestedResponseBody operation. * - * @param token The token parameter. * @param foo The foo parameter. * @param bar The bar parameter. * @throws IllegalArgumentException thrown if parameters fail the validation. @@ -503,12 +480,9 @@ public PagedIterable requestHeaderResponseHeader() { */ @Generated @ServiceMethod(returns = ReturnType.COLLECTION) - public PagedIterable requestQueryNestedResponseBody(String token, String foo, String bar) { + public PagedIterable requestQueryNestedResponseBody(String foo, String bar) { // Generated convenience method for requestQueryNestedResponseBody RequestOptions requestOptions = new RequestOptions(); - if (token != null) { - requestOptions.addQueryParam("token", token, false); - } if (foo != null) { requestOptions.setHeader(HttpHeaderName.fromString("foo"), foo); } @@ -541,7 +515,6 @@ public PagedIterable requestQueryNestedResponseBody() { /** * The requestHeaderNestedResponseBody operation. * - * @param token The token parameter. * @param foo The foo parameter. * @param bar The bar parameter. * @throws IllegalArgumentException thrown if parameters fail the validation. @@ -554,12 +527,9 @@ public PagedIterable requestQueryNestedResponseBody() { */ @Generated @ServiceMethod(returns = ReturnType.COLLECTION) - public PagedIterable requestHeaderNestedResponseBody(String token, String foo, String bar) { + public PagedIterable requestHeaderNestedResponseBody(String foo, String bar) { // Generated convenience method for requestHeaderNestedResponseBody RequestOptions requestOptions = new RequestOptions(); - if (token != null) { - requestOptions.setHeader(HttpHeaderName.fromString("token"), token); - } if (foo != null) { requestOptions.setHeader(HttpHeaderName.fromString("foo"), foo); } diff --git a/packages/http-client-java/generator/http-client-generator-test/src/main/java/payload/pageable/XmlPaginationAsyncClient.java b/packages/http-client-java/generator/http-client-generator-test/src/main/java/payload/pageable/XmlPaginationAsyncClient.java index 199b858542a..a94f6044b80 100644 --- a/packages/http-client-java/generator/http-client-generator-test/src/main/java/payload/pageable/XmlPaginationAsyncClient.java +++ b/packages/http-client-java/generator/http-client-generator-test/src/main/java/payload/pageable/XmlPaginationAsyncClient.java @@ -47,13 +47,6 @@ public final class XmlPaginationAsyncClient { /** * The listWithContinuation operation. - *

Query Parameters

- * - * - * - * - *
Query Parameters
NameTypeRequiredDescription
markerStringNoThe marker parameter
- * You can add these to a request with {@link RequestOptions#addQueryParam} *

Response Body Schema

* *
@@ -104,48 +97,6 @@ public PagedFlux listWithNextLink(RequestOptions requestOptions) {
         return this.serviceClient.listWithNextLinkAsync(requestOptions);
     }
 
-    /**
-     * The listWithContinuation operation.
-     * 
-     * @param marker The marker parameter.
-     * @throws IllegalArgumentException thrown if parameters fail the validation.
-     * @throws HttpResponseException thrown if the request is rejected by server.
-     * @throws ClientAuthenticationException thrown if the request is rejected by server on status code 401.
-     * @throws ResourceNotFoundException thrown if the request is rejected by server on status code 404.
-     * @throws ResourceModifiedException thrown if the request is rejected by server on status code 409.
-     * @throws RuntimeException all other wrapped checked exceptions if the request fails to be sent.
-     * @return the XML response for listing pets as paginated response with {@link PagedFlux}.
-     */
-    @Generated
-    @ServiceMethod(returns = ReturnType.COLLECTION)
-    public PagedFlux listWithContinuation(String marker) {
-        // Generated convenience method for listWithContinuation
-        RequestOptions requestOptions = new RequestOptions();
-        if (marker != null) {
-            requestOptions.addQueryParam("marker", marker, false);
-        }
-        PagedFlux pagedFluxResponse = listWithContinuation(requestOptions);
-        return PagedFlux.create(() -> (continuationTokenParam, pageSizeParam) -> {
-            Flux> flux;
-            if (pageSizeParam == null) {
-                flux = (continuationTokenParam == null)
-                    ? pagedFluxResponse.byPage().take(1)
-                    : pagedFluxResponse.byPage(continuationTokenParam).take(1);
-            } else {
-                flux = (continuationTokenParam == null)
-                    ? pagedFluxResponse.byPage(pageSizeParam).take(1)
-                    : pagedFluxResponse.byPage(continuationTokenParam, pageSizeParam).take(1);
-            }
-            return flux.map(pagedResponse -> new PagedResponseBase(pagedResponse.getRequest(),
-                pagedResponse.getStatusCode(), pagedResponse.getHeaders(),
-                pagedResponse.getValue()
-                    .stream()
-                    .map(protocolMethodData -> protocolMethodData.toObject(XmlPet.class, XML_SERIALIZER))
-                    .collect(Collectors.toList()),
-                pagedResponse.getContinuationToken(), null));
-        });
-    }
-
     /**
      * The listWithContinuation operation.
      * 
diff --git a/packages/http-client-java/generator/http-client-generator-test/src/main/java/payload/pageable/XmlPaginationClient.java b/packages/http-client-java/generator/http-client-generator-test/src/main/java/payload/pageable/XmlPaginationClient.java
index c1d5f6fd3d5..f11694e747a 100644
--- a/packages/http-client-java/generator/http-client-generator-test/src/main/java/payload/pageable/XmlPaginationClient.java
+++ b/packages/http-client-java/generator/http-client-generator-test/src/main/java/payload/pageable/XmlPaginationClient.java
@@ -43,13 +43,6 @@ public final class XmlPaginationClient {
 
     /**
      * The listWithContinuation operation.
-     * 

Query Parameters

- * - * - * - * - *
Query Parameters
NameTypeRequiredDescription
markerStringNoThe marker parameter
- * You can add these to a request with {@link RequestOptions#addQueryParam} *

Response Body Schema

* *
@@ -100,30 +93,6 @@ public PagedIterable listWithNextLink(RequestOptions requestOptions)
         return this.serviceClient.listWithNextLink(requestOptions);
     }
 
-    /**
-     * The listWithContinuation operation.
-     * 
-     * @param marker The marker parameter.
-     * @throws IllegalArgumentException thrown if parameters fail the validation.
-     * @throws HttpResponseException thrown if the request is rejected by server.
-     * @throws ClientAuthenticationException thrown if the request is rejected by server on status code 401.
-     * @throws ResourceNotFoundException thrown if the request is rejected by server on status code 404.
-     * @throws ResourceModifiedException thrown if the request is rejected by server on status code 409.
-     * @throws RuntimeException all other wrapped checked exceptions if the request fails to be sent.
-     * @return the XML response for listing pets as paginated response with {@link PagedIterable}.
-     */
-    @Generated
-    @ServiceMethod(returns = ReturnType.COLLECTION)
-    public PagedIterable listWithContinuation(String marker) {
-        // Generated convenience method for listWithContinuation
-        RequestOptions requestOptions = new RequestOptions();
-        if (marker != null) {
-            requestOptions.addQueryParam("marker", marker, false);
-        }
-        return serviceClient.listWithContinuation(requestOptions)
-            .mapPage(bodyItemValue -> bodyItemValue.toObject(XmlPet.class, XML_SERIALIZER));
-    }
-
     /**
      * The listWithContinuation operation.
      * 
diff --git a/packages/http-client-java/generator/http-client-generator-test/src/main/java/payload/pageable/implementation/ServerDrivenPaginationContinuationTokensImpl.java b/packages/http-client-java/generator/http-client-generator-test/src/main/java/payload/pageable/implementation/ServerDrivenPaginationContinuationTokensImpl.java
index 3594168cfdd..2ad7738a619 100644
--- a/packages/http-client-java/generator/http-client-generator-test/src/main/java/payload/pageable/implementation/ServerDrivenPaginationContinuationTokensImpl.java
+++ b/packages/http-client-java/generator/http-client-generator-test/src/main/java/payload/pageable/implementation/ServerDrivenPaginationContinuationTokensImpl.java
@@ -17,6 +17,7 @@
 import com.azure.core.exception.HttpResponseException;
 import com.azure.core.exception.ResourceModifiedException;
 import com.azure.core.exception.ResourceNotFoundException;
+import com.azure.core.http.HttpHeaderName;
 import com.azure.core.http.rest.PagedFlux;
 import com.azure.core.http.rest.PagedIterable;
 import com.azure.core.http.rest.PagedResponse;
@@ -27,6 +28,7 @@
 import com.azure.core.util.BinaryData;
 import com.azure.core.util.Context;
 import com.azure.core.util.FluxUtil;
+import com.azure.core.util.UrlBuilder;
 import java.util.List;
 import java.util.Map;
 import java.util.stream.Collectors;
@@ -215,7 +217,7 @@ private Mono> requestQueryResponseBodySinglePageAsync(
             .withContext(
                 context -> service.requestQueryResponseBody(this.client.getEndpoint(), accept, requestOptions, context))
             .map(res -> new PagedResponseBase<>(res.getRequest(), res.getStatusCode(), res.getHeaders(),
-                getValues(res.getValue(), "pets"), null, null));
+                getValues(res.getValue(), "pets"), getNextLink(res.getValue(), "nextToken"), null));
     }
 
     /**
@@ -224,7 +226,6 @@ private Mono> requestQueryResponseBodySinglePageAsync(
      * 
      * 
      * 
-     * 
      * 
      * 
Query Parameters
NameTypeRequiredDescription
tokenStringNoThe token parameter
barStringNoThe bar parameter
* You can add these to a request with {@link RequestOptions#addQueryParam} @@ -255,7 +256,24 @@ private Mono> requestQueryResponseBodySinglePageAsync( */ @ServiceMethod(returns = ReturnType.COLLECTION) public PagedFlux requestQueryResponseBodyAsync(RequestOptions requestOptions) { - return new PagedFlux<>(() -> requestQueryResponseBodySinglePageAsync(requestOptions)); + return new PagedFlux<>(() -> { + RequestOptions requestOptionsLocal = requestOptions == null ? new RequestOptions() : requestOptions; + requestOptionsLocal.addRequestCallback(requestLocal -> { + UrlBuilder urlBuilder = UrlBuilder.parse(requestLocal.getUrl()); + urlBuilder.setQueryParameter("token", null); + requestLocal.setUrl(urlBuilder.toString()); + }); + return requestQueryResponseBodySinglePageAsync(requestOptionsLocal); + }, (token) -> { + RequestOptions requestOptionsLocal = requestOptions == null ? new RequestOptions() : requestOptions; + requestOptionsLocal.addRequestCallback(requestLocal -> { + UrlBuilder urlBuilder = UrlBuilder.parse(requestLocal.getUrl()); + urlBuilder.setQueryParameter("token", null); + requestLocal.setUrl(urlBuilder.toString()); + }); + requestOptionsLocal.addQueryParam("token", token, false); + return requestQueryResponseBodySinglePageAsync(requestOptionsLocal); + }); } /** @@ -299,7 +317,7 @@ private PagedResponse requestQueryResponseBodySinglePage(RequestOpti Response res = service.requestQueryResponseBodySync(this.client.getEndpoint(), accept, requestOptions, Context.NONE); return new PagedResponseBase<>(res.getRequest(), res.getStatusCode(), res.getHeaders(), - getValues(res.getValue(), "pets"), null, null); + getValues(res.getValue(), "pets"), getNextLink(res.getValue(), "nextToken"), null); } /** @@ -308,7 +326,6 @@ private PagedResponse requestQueryResponseBodySinglePage(RequestOpti * * * - * * *
Query Parameters
NameTypeRequiredDescription
tokenStringNoThe token parameter
barStringNoThe bar parameter
* You can add these to a request with {@link RequestOptions#addQueryParam} @@ -339,7 +356,24 @@ private PagedResponse requestQueryResponseBodySinglePage(RequestOpti */ @ServiceMethod(returns = ReturnType.COLLECTION) public PagedIterable requestQueryResponseBody(RequestOptions requestOptions) { - return new PagedIterable<>(() -> requestQueryResponseBodySinglePage(requestOptions)); + return new PagedIterable<>(() -> { + RequestOptions requestOptionsLocal = requestOptions == null ? new RequestOptions() : requestOptions; + requestOptionsLocal.addRequestCallback(requestLocal -> { + UrlBuilder urlBuilder = UrlBuilder.parse(requestLocal.getUrl()); + urlBuilder.setQueryParameter("token", null); + requestLocal.setUrl(urlBuilder.toString()); + }); + return requestQueryResponseBodySinglePage(requestOptionsLocal); + }, (token) -> { + RequestOptions requestOptionsLocal = requestOptions == null ? new RequestOptions() : requestOptions; + requestOptionsLocal.addRequestCallback(requestLocal -> { + UrlBuilder urlBuilder = UrlBuilder.parse(requestLocal.getUrl()); + urlBuilder.setQueryParameter("token", null); + requestLocal.setUrl(urlBuilder.toString()); + }); + requestOptionsLocal.addQueryParam("token", token, false); + return requestQueryResponseBodySinglePage(requestOptionsLocal); + }); } /** @@ -383,7 +417,7 @@ private Mono> requestHeaderResponseBodySinglePageAsync return FluxUtil.withContext( context -> service.requestHeaderResponseBody(this.client.getEndpoint(), accept, requestOptions, context)) .map(res -> new PagedResponseBase<>(res.getRequest(), res.getStatusCode(), res.getHeaders(), - getValues(res.getValue(), "pets"), null, null)); + getValues(res.getValue(), "pets"), getNextLink(res.getValue(), "nextToken"), null)); } /** @@ -399,7 +433,6 @@ private Mono> requestHeaderResponseBodySinglePageAsync * * * - * * *
Header Parameters
NameTypeRequiredDescription
tokenStringNoThe token parameter
fooStringNoThe foo parameter
* You can add these to a request with {@link RequestOptions#addHeader} @@ -423,7 +456,16 @@ private Mono> requestHeaderResponseBodySinglePageAsync */ @ServiceMethod(returns = ReturnType.COLLECTION) public PagedFlux requestHeaderResponseBodyAsync(RequestOptions requestOptions) { - return new PagedFlux<>(() -> requestHeaderResponseBodySinglePageAsync(requestOptions)); + return new PagedFlux<>(() -> { + RequestOptions requestOptionsLocal = requestOptions == null ? new RequestOptions() : requestOptions; + requestOptionsLocal.addRequestCallback(requestLocal -> requestLocal.getHeaders().remove("token")); + return requestHeaderResponseBodySinglePageAsync(requestOptionsLocal); + }, (token) -> { + RequestOptions requestOptionsLocal = requestOptions == null ? new RequestOptions() : requestOptions; + requestOptionsLocal.addRequestCallback(requestLocal -> requestLocal.getHeaders().remove("token")); + requestOptionsLocal.setHeader("token", token); + return requestHeaderResponseBodySinglePageAsync(requestOptionsLocal); + }); } /** @@ -467,7 +509,7 @@ private PagedResponse requestHeaderResponseBodySinglePage(RequestOpt Response res = service.requestHeaderResponseBodySync(this.client.getEndpoint(), accept, requestOptions, Context.NONE); return new PagedResponseBase<>(res.getRequest(), res.getStatusCode(), res.getHeaders(), - getValues(res.getValue(), "pets"), null, null); + getValues(res.getValue(), "pets"), getNextLink(res.getValue(), "nextToken"), null); } /** @@ -483,7 +525,6 @@ private PagedResponse requestHeaderResponseBodySinglePage(RequestOpt * * * - * * *
Header Parameters
NameTypeRequiredDescription
tokenStringNoThe token parameter
fooStringNoThe foo parameter
* You can add these to a request with {@link RequestOptions#addHeader} @@ -507,7 +548,16 @@ private PagedResponse requestHeaderResponseBodySinglePage(RequestOpt */ @ServiceMethod(returns = ReturnType.COLLECTION) public PagedIterable requestHeaderResponseBody(RequestOptions requestOptions) { - return new PagedIterable<>(() -> requestHeaderResponseBodySinglePage(requestOptions)); + return new PagedIterable<>(() -> { + RequestOptions requestOptionsLocal = requestOptions == null ? new RequestOptions() : requestOptions; + requestOptionsLocal.addRequestCallback(requestLocal -> requestLocal.getHeaders().remove("token")); + return requestHeaderResponseBodySinglePage(requestOptionsLocal); + }, (token) -> { + RequestOptions requestOptionsLocal = requestOptions == null ? new RequestOptions() : requestOptions; + requestOptionsLocal.addRequestCallback(requestLocal -> requestLocal.getHeaders().remove("token")); + requestOptionsLocal.setHeader("token", token); + return requestHeaderResponseBodySinglePage(requestOptionsLocal); + }); } /** @@ -551,7 +601,8 @@ private Mono> requestQueryResponseHeaderSinglePageAsyn return FluxUtil.withContext( context -> service.requestQueryResponseHeader(this.client.getEndpoint(), accept, requestOptions, context)) .map(res -> new PagedResponseBase<>(res.getRequest(), res.getStatusCode(), res.getHeaders(), - getValues(res.getValue(), "pets"), null, null)); + getValues(res.getValue(), "pets"), res.getHeaders().getValue(HttpHeaderName.fromString("next-token")), + null)); } /** @@ -560,7 +611,6 @@ private Mono> requestQueryResponseHeaderSinglePageAsyn * * * - * * *
Query Parameters
NameTypeRequiredDescription
tokenStringNoThe token parameter
barStringNoThe bar parameter
* You can add these to a request with {@link RequestOptions#addQueryParam} @@ -591,7 +641,24 @@ private Mono> requestQueryResponseHeaderSinglePageAsyn */ @ServiceMethod(returns = ReturnType.COLLECTION) public PagedFlux requestQueryResponseHeaderAsync(RequestOptions requestOptions) { - return new PagedFlux<>(() -> requestQueryResponseHeaderSinglePageAsync(requestOptions)); + return new PagedFlux<>(() -> { + RequestOptions requestOptionsLocal = requestOptions == null ? new RequestOptions() : requestOptions; + requestOptionsLocal.addRequestCallback(requestLocal -> { + UrlBuilder urlBuilder = UrlBuilder.parse(requestLocal.getUrl()); + urlBuilder.setQueryParameter("token", null); + requestLocal.setUrl(urlBuilder.toString()); + }); + return requestQueryResponseHeaderSinglePageAsync(requestOptionsLocal); + }, (token) -> { + RequestOptions requestOptionsLocal = requestOptions == null ? new RequestOptions() : requestOptions; + requestOptionsLocal.addRequestCallback(requestLocal -> { + UrlBuilder urlBuilder = UrlBuilder.parse(requestLocal.getUrl()); + urlBuilder.setQueryParameter("token", null); + requestLocal.setUrl(urlBuilder.toString()); + }); + requestOptionsLocal.addQueryParam("token", token, false); + return requestQueryResponseHeaderSinglePageAsync(requestOptionsLocal); + }); } /** @@ -635,7 +702,8 @@ private PagedResponse requestQueryResponseHeaderSinglePage(RequestOp Response res = service.requestQueryResponseHeaderSync(this.client.getEndpoint(), accept, requestOptions, Context.NONE); return new PagedResponseBase<>(res.getRequest(), res.getStatusCode(), res.getHeaders(), - getValues(res.getValue(), "pets"), null, null); + getValues(res.getValue(), "pets"), res.getHeaders().getValue(HttpHeaderName.fromString("next-token")), + null); } /** @@ -644,7 +712,6 @@ private PagedResponse requestQueryResponseHeaderSinglePage(RequestOp * * * - * * *
Query Parameters
NameTypeRequiredDescription
tokenStringNoThe token parameter
barStringNoThe bar parameter
* You can add these to a request with {@link RequestOptions#addQueryParam} @@ -675,7 +742,24 @@ private PagedResponse requestQueryResponseHeaderSinglePage(RequestOp */ @ServiceMethod(returns = ReturnType.COLLECTION) public PagedIterable requestQueryResponseHeader(RequestOptions requestOptions) { - return new PagedIterable<>(() -> requestQueryResponseHeaderSinglePage(requestOptions)); + return new PagedIterable<>(() -> { + RequestOptions requestOptionsLocal = requestOptions == null ? new RequestOptions() : requestOptions; + requestOptionsLocal.addRequestCallback(requestLocal -> { + UrlBuilder urlBuilder = UrlBuilder.parse(requestLocal.getUrl()); + urlBuilder.setQueryParameter("token", null); + requestLocal.setUrl(urlBuilder.toString()); + }); + return requestQueryResponseHeaderSinglePage(requestOptionsLocal); + }, (token) -> { + RequestOptions requestOptionsLocal = requestOptions == null ? new RequestOptions() : requestOptions; + requestOptionsLocal.addRequestCallback(requestLocal -> { + UrlBuilder urlBuilder = UrlBuilder.parse(requestLocal.getUrl()); + urlBuilder.setQueryParameter("token", null); + requestLocal.setUrl(urlBuilder.toString()); + }); + requestOptionsLocal.addQueryParam("token", token, false); + return requestQueryResponseHeaderSinglePage(requestOptionsLocal); + }); } /** @@ -719,7 +803,8 @@ private Mono> requestHeaderResponseHeaderSinglePageAsy return FluxUtil.withContext( context -> service.requestHeaderResponseHeader(this.client.getEndpoint(), accept, requestOptions, context)) .map(res -> new PagedResponseBase<>(res.getRequest(), res.getStatusCode(), res.getHeaders(), - getValues(res.getValue(), "pets"), null, null)); + getValues(res.getValue(), "pets"), res.getHeaders().getValue(HttpHeaderName.fromString("next-token")), + null)); } /** @@ -735,7 +820,6 @@ private Mono> requestHeaderResponseHeaderSinglePageAsy * * * - * * *
Header Parameters
NameTypeRequiredDescription
tokenStringNoThe token parameter
fooStringNoThe foo parameter
* You can add these to a request with {@link RequestOptions#addHeader} @@ -759,7 +843,16 @@ private Mono> requestHeaderResponseHeaderSinglePageAsy */ @ServiceMethod(returns = ReturnType.COLLECTION) public PagedFlux requestHeaderResponseHeaderAsync(RequestOptions requestOptions) { - return new PagedFlux<>(() -> requestHeaderResponseHeaderSinglePageAsync(requestOptions)); + return new PagedFlux<>(() -> { + RequestOptions requestOptionsLocal = requestOptions == null ? new RequestOptions() : requestOptions; + requestOptionsLocal.addRequestCallback(requestLocal -> requestLocal.getHeaders().remove("token")); + return requestHeaderResponseHeaderSinglePageAsync(requestOptionsLocal); + }, (token) -> { + RequestOptions requestOptionsLocal = requestOptions == null ? new RequestOptions() : requestOptions; + requestOptionsLocal.addRequestCallback(requestLocal -> requestLocal.getHeaders().remove("token")); + requestOptionsLocal.setHeader("token", token); + return requestHeaderResponseHeaderSinglePageAsync(requestOptionsLocal); + }); } /** @@ -803,7 +896,8 @@ private PagedResponse requestHeaderResponseHeaderSinglePage(RequestO Response res = service.requestHeaderResponseHeaderSync(this.client.getEndpoint(), accept, requestOptions, Context.NONE); return new PagedResponseBase<>(res.getRequest(), res.getStatusCode(), res.getHeaders(), - getValues(res.getValue(), "pets"), null, null); + getValues(res.getValue(), "pets"), res.getHeaders().getValue(HttpHeaderName.fromString("next-token")), + null); } /** @@ -819,7 +913,6 @@ private PagedResponse requestHeaderResponseHeaderSinglePage(RequestO * * * - * * *
Header Parameters
NameTypeRequiredDescription
tokenStringNoThe token parameter
fooStringNoThe foo parameter
* You can add these to a request with {@link RequestOptions#addHeader} @@ -843,7 +936,16 @@ private PagedResponse requestHeaderResponseHeaderSinglePage(RequestO */ @ServiceMethod(returns = ReturnType.COLLECTION) public PagedIterable requestHeaderResponseHeader(RequestOptions requestOptions) { - return new PagedIterable<>(() -> requestHeaderResponseHeaderSinglePage(requestOptions)); + return new PagedIterable<>(() -> { + RequestOptions requestOptionsLocal = requestOptions == null ? new RequestOptions() : requestOptions; + requestOptionsLocal.addRequestCallback(requestLocal -> requestLocal.getHeaders().remove("token")); + return requestHeaderResponseHeaderSinglePage(requestOptionsLocal); + }, (token) -> { + RequestOptions requestOptionsLocal = requestOptions == null ? new RequestOptions() : requestOptions; + requestOptionsLocal.addRequestCallback(requestLocal -> requestLocal.getHeaders().remove("token")); + requestOptionsLocal.setHeader("token", token); + return requestHeaderResponseHeaderSinglePage(requestOptionsLocal); + }); } /** @@ -889,7 +991,8 @@ public PagedIterable requestHeaderResponseHeader(RequestOptions requ .withContext(context -> service.requestQueryNestedResponseBody(this.client.getEndpoint(), accept, requestOptions, context)) .map(res -> new PagedResponseBase<>(res.getRequest(), res.getStatusCode(), res.getHeaders(), - getValues(res.getValue(), "nestedItems", "pets"), null, null)); + getValues(res.getValue(), "nestedItems", "pets"), + getNextLink(res.getValue(), "nestedNext", "nextToken"), null)); } /** @@ -898,7 +1001,6 @@ public PagedIterable requestHeaderResponseHeader(RequestOptions requ * * * - * * *
Query Parameters
NameTypeRequiredDescription
tokenStringNoThe token parameter
barStringNoThe bar parameter
* You can add these to a request with {@link RequestOptions#addQueryParam} @@ -929,7 +1031,24 @@ public PagedIterable requestHeaderResponseHeader(RequestOptions requ */ @ServiceMethod(returns = ReturnType.COLLECTION) public PagedFlux requestQueryNestedResponseBodyAsync(RequestOptions requestOptions) { - return new PagedFlux<>(() -> requestQueryNestedResponseBodySinglePageAsync(requestOptions)); + return new PagedFlux<>(() -> { + RequestOptions requestOptionsLocal = requestOptions == null ? new RequestOptions() : requestOptions; + requestOptionsLocal.addRequestCallback(requestLocal -> { + UrlBuilder urlBuilder = UrlBuilder.parse(requestLocal.getUrl()); + urlBuilder.setQueryParameter("token", null); + requestLocal.setUrl(urlBuilder.toString()); + }); + return requestQueryNestedResponseBodySinglePageAsync(requestOptionsLocal); + }, (token) -> { + RequestOptions requestOptionsLocal = requestOptions == null ? new RequestOptions() : requestOptions; + requestOptionsLocal.addRequestCallback(requestLocal -> { + UrlBuilder urlBuilder = UrlBuilder.parse(requestLocal.getUrl()); + urlBuilder.setQueryParameter("token", null); + requestLocal.setUrl(urlBuilder.toString()); + }); + requestOptionsLocal.addQueryParam("token", token, false); + return requestQueryNestedResponseBodySinglePageAsync(requestOptionsLocal); + }); } /** @@ -973,7 +1092,8 @@ private PagedResponse requestQueryNestedResponseBodySinglePage(Reque Response res = service.requestQueryNestedResponseBodySync(this.client.getEndpoint(), accept, requestOptions, Context.NONE); return new PagedResponseBase<>(res.getRequest(), res.getStatusCode(), res.getHeaders(), - getValues(res.getValue(), "nestedItems", "pets"), null, null); + getValues(res.getValue(), "nestedItems", "pets"), getNextLink(res.getValue(), "nestedNext", "nextToken"), + null); } /** @@ -982,7 +1102,6 @@ private PagedResponse requestQueryNestedResponseBodySinglePage(Reque * * * - * * *
Query Parameters
NameTypeRequiredDescription
tokenStringNoThe token parameter
barStringNoThe bar parameter
* You can add these to a request with {@link RequestOptions#addQueryParam} @@ -1013,7 +1132,24 @@ private PagedResponse requestQueryNestedResponseBodySinglePage(Reque */ @ServiceMethod(returns = ReturnType.COLLECTION) public PagedIterable requestQueryNestedResponseBody(RequestOptions requestOptions) { - return new PagedIterable<>(() -> requestQueryNestedResponseBodySinglePage(requestOptions)); + return new PagedIterable<>(() -> { + RequestOptions requestOptionsLocal = requestOptions == null ? new RequestOptions() : requestOptions; + requestOptionsLocal.addRequestCallback(requestLocal -> { + UrlBuilder urlBuilder = UrlBuilder.parse(requestLocal.getUrl()); + urlBuilder.setQueryParameter("token", null); + requestLocal.setUrl(urlBuilder.toString()); + }); + return requestQueryNestedResponseBodySinglePage(requestOptionsLocal); + }, (token) -> { + RequestOptions requestOptionsLocal = requestOptions == null ? new RequestOptions() : requestOptions; + requestOptionsLocal.addRequestCallback(requestLocal -> { + UrlBuilder urlBuilder = UrlBuilder.parse(requestLocal.getUrl()); + urlBuilder.setQueryParameter("token", null); + requestLocal.setUrl(urlBuilder.toString()); + }); + requestOptionsLocal.addQueryParam("token", token, false); + return requestQueryNestedResponseBodySinglePage(requestOptionsLocal); + }); } /** @@ -1059,7 +1195,8 @@ public PagedIterable requestQueryNestedResponseBody(RequestOptions r .withContext(context -> service.requestHeaderNestedResponseBody(this.client.getEndpoint(), accept, requestOptions, context)) .map(res -> new PagedResponseBase<>(res.getRequest(), res.getStatusCode(), res.getHeaders(), - getValues(res.getValue(), "nestedItems", "pets"), null, null)); + getValues(res.getValue(), "nestedItems", "pets"), + getNextLink(res.getValue(), "nestedNext", "nextToken"), null)); } /** @@ -1075,7 +1212,6 @@ public PagedIterable requestQueryNestedResponseBody(RequestOptions r * * * - * * *
Header Parameters
NameTypeRequiredDescription
tokenStringNoThe token parameter
fooStringNoThe foo parameter
* You can add these to a request with {@link RequestOptions#addHeader} @@ -1099,7 +1235,16 @@ public PagedIterable requestQueryNestedResponseBody(RequestOptions r */ @ServiceMethod(returns = ReturnType.COLLECTION) public PagedFlux requestHeaderNestedResponseBodyAsync(RequestOptions requestOptions) { - return new PagedFlux<>(() -> requestHeaderNestedResponseBodySinglePageAsync(requestOptions)); + return new PagedFlux<>(() -> { + RequestOptions requestOptionsLocal = requestOptions == null ? new RequestOptions() : requestOptions; + requestOptionsLocal.addRequestCallback(requestLocal -> requestLocal.getHeaders().remove("token")); + return requestHeaderNestedResponseBodySinglePageAsync(requestOptionsLocal); + }, (token) -> { + RequestOptions requestOptionsLocal = requestOptions == null ? new RequestOptions() : requestOptions; + requestOptionsLocal.addRequestCallback(requestLocal -> requestLocal.getHeaders().remove("token")); + requestOptionsLocal.setHeader("token", token); + return requestHeaderNestedResponseBodySinglePageAsync(requestOptionsLocal); + }); } /** @@ -1143,7 +1288,8 @@ private PagedResponse requestHeaderNestedResponseBodySinglePage(Requ Response res = service.requestHeaderNestedResponseBodySync(this.client.getEndpoint(), accept, requestOptions, Context.NONE); return new PagedResponseBase<>(res.getRequest(), res.getStatusCode(), res.getHeaders(), - getValues(res.getValue(), "nestedItems", "pets"), null, null); + getValues(res.getValue(), "nestedItems", "pets"), getNextLink(res.getValue(), "nestedNext", "nextToken"), + null); } /** @@ -1159,7 +1305,6 @@ private PagedResponse requestHeaderNestedResponseBodySinglePage(Requ * * * - * * *
Header Parameters
NameTypeRequiredDescription
tokenStringNoThe token parameter
fooStringNoThe foo parameter
* You can add these to a request with {@link RequestOptions#addHeader} @@ -1183,7 +1328,16 @@ private PagedResponse requestHeaderNestedResponseBodySinglePage(Requ */ @ServiceMethod(returns = ReturnType.COLLECTION) public PagedIterable requestHeaderNestedResponseBody(RequestOptions requestOptions) { - return new PagedIterable<>(() -> requestHeaderNestedResponseBodySinglePage(requestOptions)); + return new PagedIterable<>(() -> { + RequestOptions requestOptionsLocal = requestOptions == null ? new RequestOptions() : requestOptions; + requestOptionsLocal.addRequestCallback(requestLocal -> requestLocal.getHeaders().remove("token")); + return requestHeaderNestedResponseBodySinglePage(requestOptionsLocal); + }, (token) -> { + RequestOptions requestOptionsLocal = requestOptions == null ? new RequestOptions() : requestOptions; + requestOptionsLocal.addRequestCallback(requestLocal -> requestLocal.getHeaders().remove("token")); + requestOptionsLocal.setHeader("token", token); + return requestHeaderNestedResponseBodySinglePage(requestOptionsLocal); + }); } private List getValues(BinaryData binaryData, String... path) { diff --git a/packages/http-client-java/generator/http-client-generator-test/src/main/java/payload/pageable/implementation/XmlPaginationsImpl.java b/packages/http-client-java/generator/http-client-generator-test/src/main/java/payload/pageable/implementation/XmlPaginationsImpl.java index 8986bca9518..52310f9aa7a 100644 --- a/packages/http-client-java/generator/http-client-generator-test/src/main/java/payload/pageable/implementation/XmlPaginationsImpl.java +++ b/packages/http-client-java/generator/http-client-generator-test/src/main/java/payload/pageable/implementation/XmlPaginationsImpl.java @@ -28,6 +28,7 @@ import com.azure.core.util.BinaryData; import com.azure.core.util.Context; import com.azure.core.util.FluxUtil; +import com.azure.core.util.UrlBuilder; import java.util.List; import java.util.Map; import java.util.stream.Collectors; @@ -164,18 +165,11 @@ private Mono> listWithContinuationSinglePageAsync(Requ } catch (javax.xml.stream.XMLStreamException e) { throw new IllegalStateException(e); } - }, "Pets", "Pet"), null, null)); + }, "Pets", "Pet"), getXmlNextLink(res.getValue(), false, null, "NextMarker"), null)); } /** * The listWithContinuation operation. - *

Query Parameters

- * - * - * - * - *
Query Parameters
NameTypeRequiredDescription
markerStringNoThe marker parameter
- * You can add these to a request with {@link RequestOptions#addQueryParam} *

Response Body Schema

* *
@@ -196,7 +190,24 @@ private Mono> listWithContinuationSinglePageAsync(Requ
      */
     @ServiceMethod(returns = ReturnType.COLLECTION)
     public PagedFlux listWithContinuationAsync(RequestOptions requestOptions) {
-        return new PagedFlux<>(() -> listWithContinuationSinglePageAsync(requestOptions));
+        return new PagedFlux<>(() -> {
+            RequestOptions requestOptionsLocal = requestOptions == null ? new RequestOptions() : requestOptions;
+            requestOptionsLocal.addRequestCallback(requestLocal -> {
+                UrlBuilder urlBuilder = UrlBuilder.parse(requestLocal.getUrl());
+                urlBuilder.setQueryParameter("marker", null);
+                requestLocal.setUrl(urlBuilder.toString());
+            });
+            return listWithContinuationSinglePageAsync(requestOptionsLocal);
+        }, (marker) -> {
+            RequestOptions requestOptionsLocal = requestOptions == null ? new RequestOptions() : requestOptions;
+            requestOptionsLocal.addRequestCallback(requestLocal -> {
+                UrlBuilder urlBuilder = UrlBuilder.parse(requestLocal.getUrl());
+                urlBuilder.setQueryParameter("marker", null);
+                requestLocal.setUrl(urlBuilder.toString());
+            });
+            requestOptionsLocal.addQueryParam("marker", marker, false);
+            return listWithContinuationSinglePageAsync(requestOptionsLocal);
+        });
     }
 
     /**
@@ -238,18 +249,11 @@ private PagedResponse listWithContinuationSinglePage(RequestOptions
                 } catch (javax.xml.stream.XMLStreamException e) {
                     throw new IllegalStateException(e);
                 }
-            }, "Pets", "Pet"), null, null);
+            }, "Pets", "Pet"), getXmlNextLink(res.getValue(), false, null, "NextMarker"), null);
     }
 
     /**
      * The listWithContinuation operation.
-     * 

Query Parameters

- * - * - * - * - *
Query Parameters
NameTypeRequiredDescription
markerStringNoThe marker parameter
- * You can add these to a request with {@link RequestOptions#addQueryParam} *

Response Body Schema

* *
@@ -270,7 +274,24 @@ private PagedResponse listWithContinuationSinglePage(RequestOptions
      */
     @ServiceMethod(returns = ReturnType.COLLECTION)
     public PagedIterable listWithContinuation(RequestOptions requestOptions) {
-        return new PagedIterable<>(() -> listWithContinuationSinglePage(requestOptions));
+        return new PagedIterable<>(() -> {
+            RequestOptions requestOptionsLocal = requestOptions == null ? new RequestOptions() : requestOptions;
+            requestOptionsLocal.addRequestCallback(requestLocal -> {
+                UrlBuilder urlBuilder = UrlBuilder.parse(requestLocal.getUrl());
+                urlBuilder.setQueryParameter("marker", null);
+                requestLocal.setUrl(urlBuilder.toString());
+            });
+            return listWithContinuationSinglePage(requestOptionsLocal);
+        }, (marker) -> {
+            RequestOptions requestOptionsLocal = requestOptions == null ? new RequestOptions() : requestOptions;
+            requestOptionsLocal.addRequestCallback(requestLocal -> {
+                UrlBuilder urlBuilder = UrlBuilder.parse(requestLocal.getUrl());
+                urlBuilder.setQueryParameter("marker", null);
+                requestLocal.setUrl(urlBuilder.toString());
+            });
+            requestOptionsLocal.addQueryParam("marker", marker, false);
+            return listWithContinuationSinglePage(requestOptionsLocal);
+        });
     }
 
     /**
@@ -308,7 +329,7 @@ private Mono> listWithNextLinkSinglePageAsync(RequestO
                     } catch (javax.xml.stream.XMLStreamException e) {
                         throw new IllegalStateException(e);
                     }
-                }, "Pets", "Pet"), getXmlNextLink(res.getValue(), "NextLink"), null));
+                }, "Pets", "Pet"), getXmlNextLink(res.getValue(), false, null, "NextLink"), null));
     }
 
     /**
@@ -372,7 +393,7 @@ private PagedResponse listWithNextLinkSinglePage(RequestOptions requ
                 } catch (javax.xml.stream.XMLStreamException e) {
                     throw new IllegalStateException(e);
                 }
-            }, "Pets", "Pet"), getXmlNextLink(res.getValue(), "NextLink"), null);
+            }, "Pets", "Pet"), getXmlNextLink(res.getValue(), false, null, "NextLink"), null);
     }
 
     /**
@@ -441,7 +462,7 @@ private Mono> listWithNextLinkNextSinglePageAsync(Stri
                     } catch (javax.xml.stream.XMLStreamException e) {
                         throw new IllegalStateException(e);
                     }
-                }, "Pets", "Pet"), getXmlNextLink(res.getValue(), "NextLink"), null));
+                }, "Pets", "Pet"), getXmlNextLink(res.getValue(), false, null, "NextLink"), null));
     }
 
     /**
@@ -477,7 +498,7 @@ private PagedResponse listWithNextLinkNextSinglePage(String nextLink
                 } catch (javax.xml.stream.XMLStreamException e) {
                     throw new IllegalStateException(e);
                 }
-            }, "Pets", "Pet"), getXmlNextLink(res.getValue(), "NextLink"), null);
+            }, "Pets", "Pet"), getXmlNextLink(res.getValue(), false, null, "NextLink"), null);
     }
 
     private List getValues(BinaryData binaryData, String... path) {
@@ -534,24 +555,27 @@ private List getXmlValues(com.azure.xml.XmlReader reader,
         return values;
     }
 
-    private String getXmlNextLink(BinaryData binaryData, String... path) {
+    private String getXmlNextLink(BinaryData binaryData, boolean attribute, String namespace, String... path) {
         try (com.azure.xml.XmlReader reader = com.azure.xml.XmlReader.fromStream(binaryData.toStream())) {
             reader.nextElement();
-            return getXmlNextLink(reader, path, 0);
+            return getXmlNextLink(reader, attribute, namespace, path, 0);
         } catch (javax.xml.stream.XMLStreamException e) {
             throw new IllegalStateException("Failed to read XML pageable response.", e);
         }
     }
 
-    private String getXmlNextLink(com.azure.xml.XmlReader reader, String[] path, int pathIndex)
-        throws javax.xml.stream.XMLStreamException {
+    private String getXmlNextLink(com.azure.xml.XmlReader reader, boolean attribute, String namespace, String[] path,
+        int pathIndex) throws javax.xml.stream.XMLStreamException {
+        if (attribute && pathIndex == path.length - 1) {
+            return reader.getStringAttribute(namespace, path[pathIndex]);
+        }
         while (reader.nextElement() != com.azure.xml.XmlToken.END_ELEMENT) {
-            if (!reader.elementNameMatches(path[pathIndex])) {
+            if (!reader.elementNameMatches(pathIndex == path.length - 1 ? namespace : null, path[pathIndex])) {
                 reader.skipElement();
             } else if (pathIndex == path.length - 1) {
                 return reader.getStringElement();
             } else {
-                return getXmlNextLink(reader, path, pathIndex + 1);
+                return getXmlNextLink(reader, attribute, namespace, path, pathIndex + 1);
             }
         }
         return null;
diff --git a/packages/http-client-java/generator/http-client-generator-test/src/test/java/payload/pageable/PageableTests.java b/packages/http-client-java/generator/http-client-generator-test/src/test/java/payload/pageable/PageableTests.java
index 10fa38a72db..5d6f0a471c4 100644
--- a/packages/http-client-java/generator/http-client-generator-test/src/test/java/payload/pageable/PageableTests.java
+++ b/packages/http-client-java/generator/http-client-generator-test/src/test/java/payload/pageable/PageableTests.java
@@ -4,6 +4,8 @@
 package payload.pageable;
 
 import com.azure.core.http.rest.PagedIterable;
+import com.azure.core.http.rest.RequestOptions;
+import java.time.Duration;
 import java.util.List;
 import java.util.stream.Collectors;
 import org.junit.jupiter.api.Assertions;
@@ -11,6 +13,7 @@
 import payload.pageable.models.Pet;
 import payload.pageable.models.XmlPet;
 import payload.pageable.serverdrivenpagination.alternateinitialverb.models.Filter;
+import reactor.core.publisher.Flux;
 
 public class PageableTests {
 
@@ -54,13 +57,86 @@ public void testXmlListWithNextLink() {
             pagedIterable.stream().map(XmlPet::getId).collect(Collectors.toList()));
     }
 
-    /*
-     * Continuation-token scenarios are intentionally not covered here. Azure V1 currently emits a single-page
-     * PagedIterable for them because it does not propagate a response continuation token into the next request.
-     *
-     * XML continuation-token paging is also intentionally not covered because it has the same token propagation
-     * limitation.
-     */
+    @Test
+    public void testContinuationToken() {
+        ServerDrivenPaginationContinuationTokenClient client
+            = builder.buildServerDrivenPaginationContinuationTokenClient();
+        assertPetIds(client.requestQueryResponseBody("foo", "bar"));
+        assertPetIds(client.requestHeaderResponseBody("foo", "bar"));
+        assertPetIds(client.requestQueryResponseHeader("foo", "bar"));
+        assertPetIds(client.requestHeaderResponseHeader("foo", "bar"));
+        assertPetIds(client.requestQueryNestedResponseBody("foo", "bar"));
+        assertPetIds(client.requestHeaderNestedResponseBody("foo", "bar"));
+    }
+
+    @Test
+    public void testContinuationTokenAsync() {
+        ServerDrivenPaginationContinuationTokenAsyncClient client
+            = builder.buildServerDrivenPaginationContinuationTokenAsyncClient();
+        List> results = List.of(client.requestQueryResponseBody("foo", "bar"),
+            client.requestHeaderResponseBody("foo", "bar"), client.requestQueryResponseHeader("foo", "bar"),
+            client.requestHeaderResponseHeader("foo", "bar"), client.requestQueryNestedResponseBody("foo", "bar"),
+            client.requestHeaderNestedResponseBody("foo", "bar"));
+        for (Flux result : results) {
+            Assertions.assertEquals(List.of("1", "2", "3", "4"),
+                result.map(Pet::getId).collectList().block(Duration.ofSeconds(10)));
+        }
+    }
+
+    @Test
+    public void testResumeWithContinuationToken() {
+        ServerDrivenPaginationContinuationTokenClient client
+            = builder.buildServerDrivenPaginationContinuationTokenClient();
+        Assertions.assertEquals(List.of("3", "4"),
+            client.requestQueryResponseBody("foo", "bar")
+                .streamByPage("page2")
+                .flatMap(page -> page.getValue().stream())
+                .map(Pet::getId)
+                .collect(Collectors.toList()));
+        Assertions.assertEquals(List.of("3", "4"),
+            client.requestHeaderResponseHeader("foo", "bar")
+                .streamByPage("page2")
+                .flatMap(page -> page.getValue().stream())
+                .map(Pet::getId)
+                .collect(Collectors.toList()));
+        Assertions.assertEquals(List.of("3", "4"),
+            builder.buildServerDrivenPaginationContinuationTokenAsyncClient()
+                .requestQueryNestedResponseBody("foo", "bar")
+                .byPage("page2")
+                .flatMapIterable(page -> page.getValue())
+                .map(Pet::getId)
+                .collectList()
+                .block(Duration.ofSeconds(10)));
+    }
+
+    @Test
+    public void testProtocolContinuationTokenPreservesOptions() {
+        RequestOptions options = new RequestOptions().addHeader("foo", "foo").addQueryParam("bar", "bar");
+        ServerDrivenPaginationContinuationTokenClient client
+            = builder.buildServerDrivenPaginationContinuationTokenClient();
+        for (int i = 0; i < 2; i++) {
+            Assertions.assertEquals(4, client.requestQueryResponseBody(options).stream().count());
+            Assertions.assertEquals(4, client.requestHeaderResponseHeader(options).stream().count());
+        }
+    }
+
+    @Test
+    public void testXmlContinuationToken() {
+        Assertions.assertEquals(List.of("1", "2", "3", "4"),
+            builder.buildXmlPaginationClient()
+                .listWithContinuation()
+                .stream()
+                .map(XmlPet::getId)
+                .collect(Collectors.toList()));
+        Assertions.assertEquals(List.of("3", "4"),
+            builder.buildXmlPaginationAsyncClient()
+                .listWithContinuation()
+                .byPage("page2")
+                .flatMapIterable(page -> page.getValue())
+                .map(XmlPet::getId)
+                .collectList()
+                .block(Duration.ofSeconds(10)));
+    }
 
     private static void assertPetIds(PagedIterable pagedIterable) {
         assertPetIds(pagedIterable, "1", "2", "3", "4");