From 627610b2833f7d3b2df5bf40f37248f394393b65 Mon Sep 17 00:00:00 2001 From: whowes Date: Tue, 8 Sep 2026 17:10:19 +0000 Subject: [PATCH] feat(generator): wire transport stub delegation to resumable upload stubs Wires the generated transport stubs (GrpcServiceStub and HttpJsonServiceStub) to delegate resumable upload methods to the internal HTTP upload stub. In GrpcServiceStub, credentials, headers, and clocks are forwarded to the underlying HTTP client context. Clients initialize successfully without credentials; an exception results only if use of the HTTP upload stub is attempted. Also declares the stub-side contract that the transports implement: - In AbstractServiceStubClassComposer: emit public ResumableUploadCallable [method]Callable() throwing UnsupportedOperationException. --- .../generator/gapic/composer/Composer.java | 7 + .../AbstractServiceStubClassComposer.java | 37 +-- ...ractTransportServiceStubClassComposer.java | 235 ++++++++++++++++-- .../grpc/GrpcServiceStubClassComposer.java | 157 +++++++++++- .../GrpcServiceStubClassComposerTest.java | 10 + .../grpc/ServiceStubClassComposerTest.java | 8 +- .../GrpcResumableUploadServiceStub.golden | 145 +++++++++++ .../goldens/ResumableUploadServiceStub.golden | 28 +++ .../HttpJsonServiceStubClassComposerTest.java | 10 + .../HttpJsonResumableUploadServiceStub.golden | 134 ++++++++++ 10 files changed, 732 insertions(+), 39 deletions(-) create mode 100644 sdk-platform-java/gapic-generator-java/src/test/java/com/google/api/generator/gapic/composer/grpc/goldens/GrpcResumableUploadServiceStub.golden create mode 100644 sdk-platform-java/gapic-generator-java/src/test/java/com/google/api/generator/gapic/composer/grpc/goldens/ResumableUploadServiceStub.golden create mode 100644 sdk-platform-java/gapic-generator-java/src/test/java/com/google/api/generator/gapic/composer/rest/goldens/HttpJsonResumableUploadServiceStub.golden diff --git a/sdk-platform-java/gapic-generator-java/src/main/java/com/google/api/generator/gapic/composer/Composer.java b/sdk-platform-java/gapic-generator-java/src/main/java/com/google/api/generator/gapic/composer/Composer.java index edb180676c82..eaf34fff9f70 100644 --- a/sdk-platform-java/gapic-generator-java/src/main/java/com/google/api/generator/gapic/composer/Composer.java +++ b/sdk-platform-java/gapic-generator-java/src/main/java/com/google/api/generator/gapic/composer/Composer.java @@ -29,10 +29,12 @@ import com.google.api.generator.gapic.composer.grpcrest.HttpJsonServiceClientTestClassComposer; import com.google.api.generator.gapic.composer.resourcename.ResourceNameHelperClassComposer; import com.google.api.generator.gapic.composer.rest.HttpJsonServiceCallableFactoryClassComposer; +import com.google.api.generator.gapic.composer.rest.HttpJsonServiceResumableUploadStubClassComposer; import com.google.api.generator.gapic.composer.rest.HttpJsonServiceStubClassComposer; import com.google.api.generator.gapic.model.GapicClass; import com.google.api.generator.gapic.model.GapicContext; import com.google.api.generator.gapic.model.GapicPackageInfo; +import com.google.api.generator.gapic.model.Method; import com.google.api.generator.gapic.model.ReflectConfig; import com.google.api.generator.gapic.model.Sample; import com.google.api.generator.gapic.model.Service; @@ -91,6 +93,11 @@ public static List generateStubClasses(GapicContext context) { .services() .forEach( s -> { + if (s.methods().stream().anyMatch(Method::isResumableUpload)) { + clazzes.add( + HttpJsonServiceResumableUploadStubClassComposer.instance() + .generate(context, s)); + } if (context.transport() == Transport.REST) { clazzes.add( com.google.api.generator.gapic.composer.rest.ServiceStubClassComposer.instance() diff --git a/sdk-platform-java/gapic-generator-java/src/main/java/com/google/api/generator/gapic/composer/common/AbstractServiceStubClassComposer.java b/sdk-platform-java/gapic-generator-java/src/main/java/com/google/api/generator/gapic/composer/common/AbstractServiceStubClassComposer.java index 51e6f410c079..1b42e299adc0 100644 --- a/sdk-platform-java/gapic-generator-java/src/main/java/com/google/api/generator/gapic/composer/common/AbstractServiceStubClassComposer.java +++ b/sdk-platform-java/gapic-generator-java/src/main/java/com/google/api/generator/gapic/composer/common/AbstractServiceStubClassComposer.java @@ -21,6 +21,7 @@ import com.google.api.gax.rpc.ClientStreamingCallable; import com.google.api.gax.rpc.LongRunningClient; import com.google.api.gax.rpc.OperationCallable; +import com.google.api.gax.rpc.ResumableUploadCallable; import com.google.api.gax.rpc.ServerStreamingCallable; import com.google.api.gax.rpc.UnaryCallable; import com.google.api.generator.engine.ast.AnnotationNode; @@ -162,22 +163,7 @@ private MethodDefinition createCallableGetter(Method method, TypeStore typeStore private MethodDefinition createCallableGetterHelper( Method method, TypeStore typeStore, boolean isLroCallable, boolean isPaged) { - TypeNode returnType; - switch (method.stream()) { - case CLIENT: - returnType = typeStore.get("ClientStreamingCallable"); - break; - case SERVER: - returnType = typeStore.get("ServerStreamingCallable"); - break; - case BIDI: - returnType = typeStore.get("BidiStreamingCallable"); - break; - case NONE: - // Fall through. - default: - returnType = typeStore.get(isLroCallable ? "OperationCallable" : "UnaryCallable"); - } + TypeNode returnType = getCallableType(method, typeStore, isLroCallable); String methodName = String.format( @@ -270,6 +256,7 @@ private static TypeStore createTypes(Service service, Map messa Generated.class, Operation.class, OperationCallable.class, + ResumableUploadCallable.class, ServerStreamingCallable.class, UnaryCallable.class, UnsupportedOperationException.class, @@ -328,4 +315,22 @@ protected MethodDefinition createOperationsStubGetterMethodDefinition( .build()))) .build(); } + + private static TypeNode getCallableType( + Method method, TypeStore typeStore, boolean isLroCallable) { + if (method.isResumableUpload()) { + return typeStore.get("ResumableUploadCallable"); + } + switch (method.stream()) { + case CLIENT: + return typeStore.get("ClientStreamingCallable"); + case SERVER: + return typeStore.get("ServerStreamingCallable"); + case BIDI: + return typeStore.get("BidiStreamingCallable"); + case NONE: + default: + return typeStore.get(isLroCallable ? "OperationCallable" : "UnaryCallable"); + } + } } diff --git a/sdk-platform-java/gapic-generator-java/src/main/java/com/google/api/generator/gapic/composer/common/AbstractTransportServiceStubClassComposer.java b/sdk-platform-java/gapic-generator-java/src/main/java/com/google/api/generator/gapic/composer/common/AbstractTransportServiceStubClassComposer.java index c025c3c207f8..26fa3bb8a30f 100644 --- a/sdk-platform-java/gapic-generator-java/src/main/java/com/google/api/generator/gapic/composer/common/AbstractTransportServiceStubClassComposer.java +++ b/sdk-platform-java/gapic-generator-java/src/main/java/com/google/api/generator/gapic/composer/common/AbstractTransportServiceStubClassComposer.java @@ -25,6 +25,7 @@ import com.google.api.gax.rpc.OperationCallable; import com.google.api.gax.rpc.RequestParamsBuilder; import com.google.api.gax.rpc.RequestParamsExtractor; +import com.google.api.gax.rpc.ResumableUploadCallable; import com.google.api.gax.rpc.ServerStreamingCallable; import com.google.api.gax.rpc.UnaryCallable; import com.google.api.generator.engine.ast.AnnotationNode; @@ -104,7 +105,7 @@ public abstract class AbstractTransportServiceStubClassComposer implements Class private static final List AIP_STANDARDS_METHODS = ImmutableList.of( "Get", "List", "Create", "Delete", "Update", "Patch", "Insert", "AggregatedList"); - private static final Statement EMPTY_LINE_STATEMENT = EmptyLineStatement.create(); + protected static final Statement EMPTY_LINE_STATEMENT = EmptyLineStatement.create(); private static final String METHOD_DESCRIPTOR_NAME_PATTERN = "%sMethodDescriptor"; private static final String PAGED_CALLABLE_CLASS_MEMBER_PATTERN = "%sPagedCallable"; @@ -113,6 +114,8 @@ public abstract class AbstractTransportServiceStubClassComposer implements Class private static final String CALLABLE_FACTORY_MEMBER_NAME = "callableFactory"; protected static final String CALLABLE_CLASS_MEMBER_PATTERN = "%sCallable"; private static final String OPERATION_CALLABLE_CLASS_MEMBER_PATTERN = "%sOperationCallable"; + protected static final String RESUMABLE_UPLOAD_STUB_MEMBER_NAME = "resumableUploadStub"; + protected static final String RESUMABLE_UPLOAD_STUB_PATTERN = "HttpJson%sResumableUploadStub"; private static final ImmutableList HEURISTIC_ENABLED_PACKAGES = ImmutableList.of("google.cloud.compute", "google.cloud.sql", "google.cloud.bigquery"); @@ -139,6 +142,7 @@ private static TypeStore createStaticTypes() { ClientContext.class, ClientStreamingCallable.class, Generated.class, + ImmutableList.class, ImmutableMap.class, InterruptedException.class, IOException.class, @@ -151,6 +155,7 @@ private static TypeStore createStaticTypes() { TimeUnit.class, TypeRegistry.class, UnaryCallable.class, + ResumableUploadCallable.class, UnsupportedOperationException.class, NullMarked.class); return new TypeStore(concreteClazzes); @@ -211,6 +216,22 @@ public GapicClass generate(GapicContext context, Service service) { } } + if (service.methods().stream().anyMatch(Method::isResumableUpload)) { + TypeNode resumableUploadStubType = + typeStore.get(String.format(RESUMABLE_UPLOAD_STUB_PATTERN, service.name())); + if (isResumableUploadStubNullable()) { + resumableUploadStubType = + TypeNode.withReference(resumableUploadStubType.reference().copyAndSetNullable(true)); + } + classMemberVarExprs.put( + RESUMABLE_UPLOAD_STUB_MEMBER_NAME, + VariableExpr.withVariable( + Variable.builder() + .setName(RESUMABLE_UPLOAD_STUB_MEMBER_NAME) + .setType(resumableUploadStubType) + .build())); + } + classMemberVarExprs.put( CALLABLE_FACTORY_MEMBER_NAME, VariableExpr.withVariable( @@ -238,9 +259,7 @@ public GapicClass generate(GapicContext context, Service service) { callableClassMemberVarExprs, protoMethodNameToDescriptorVarExprs, classStatements); - methodDefinitions.addAll( - createStubOverrideMethods( - classMemberVarExprs.get(BACKGROUND_RESOURCES_MEMBER_NAME), service)); + methodDefinitions.addAll(createStubOverrideMethods(classMemberVarExprs, service)); StubCommentComposer commentComposer = new StubCommentComposer(getTransportContext().transportNames().get(0)); @@ -423,6 +442,7 @@ protected List createMethodDescriptorVariableDecls( boolean restNumericEnumsEnabled) { return service.methods().stream() .filter(x -> x.isSupportedByTransport(getTransportContext().transport())) + .filter(x -> !x.isResumableUpload()) .map( m -> createMethodDescriptorVariableDecl( @@ -452,6 +472,7 @@ protected Map createProtoMethodNameToDescriptorClassMember Service service, Class descriptorClass) { return service.methods().stream() .filter(x -> x.isSupportedByTransport(getTransportContext().transport())) + .filter(x -> !x.isResumableUpload()) .collect( Collectors.toMap( Method::name, @@ -486,6 +507,9 @@ private Map createCallableClassMembers( if (!protoMethod.isSupportedByTransport(getTransportContext().transport())) { continue; } + if (protoMethod.isResumableUpload()) { + continue; + } String javaStyleProtoMethodName = JavaStyle.toLowerCamelCase(protoMethod.name()); String callableName = String.format(CALLABLE_CLASS_MEMBER_PATTERN, javaStyleProtoMethodName); callableClassMembers.put(callableName, getCallableExpr(protoMethod, callableName)); @@ -590,6 +614,9 @@ protected List createClassMethods( service, classMemberVarExprs.get(getTransportContext().transportOperationsStubNames().get(0)))); javaMethods.addAll(createCallableGetterMethods(callableClassMemberVarExprs)); + if (service.methods().stream().anyMatch(Method::isResumableUpload)) { + javaMethods.addAll(createResumableUploadCallableGetterMethods(service, classMemberVarExprs)); + } return javaMethods; } @@ -776,6 +803,7 @@ protected List createConstructorMethods( Map javaStyleMethodNameToTransportSettingsVarExprs = service.methods().stream() .filter(x -> x.isSupportedByTransport(getTransportContext().transport())) + .filter(x -> !x.isResumableUpload()) .collect( Collectors.toMap( m -> JavaStyle.toLowerCamelCase(m.name()), @@ -801,6 +829,7 @@ protected List createConstructorMethods( secondCtorExprs.addAll( service.methods().stream() .filter(x -> x.isSupportedByTransport(getTransportContext().transport())) + .filter(x -> !x.isResumableUpload()) .map( m -> createTransportSettingsInitExpr( @@ -833,6 +862,9 @@ protected List createConstructorMethods( if (!method.isSupportedByTransport(getTransportContext().transport())) { continue; } + if (method.isResumableUpload()) { + continue; + } secondCtorExprs.addAll( createCallableInitExprs( context, @@ -860,20 +892,86 @@ protected List createConstructorMethods( .setMethodName("getBackgroundResources") .build(); - VariableExpr backgroundResourcesVarExpr = classMemberVarExprs.get("backgroundResources"); - secondCtorExprs.add( - AssignmentExpr.builder() - .setVariableExpr( - backgroundResourcesVarExpr.toBuilder().setExprReferenceExpr(thisExpr).build()) - .setValueExpr( - NewObjectExpr.builder() - .setType(FIXED_TYPESTORE.get("BackgroundResourceAggregation")) - .setArguments(Arrays.asList(getBackgroundResourcesMethodExpr)) - .build()) - .build()); - secondCtorStatements.addAll( - secondCtorExprs.stream().map(ExprStatement::withExpr).collect(Collectors.toList())); - secondCtorExprs.clear(); + VariableExpr backgroundResourcesVarExpr = + classMemberVarExprs.get(BACKGROUND_RESOURCES_MEMBER_NAME); + VariableExpr thisBackgroundResourcesVarExpr = + backgroundResourcesVarExpr.toBuilder().setExprReferenceExpr(thisExpr).build(); + + Expr standardBackgroundResourceAggregationExpr = + NewObjectExpr.builder() + .setType(FIXED_TYPESTORE.get("BackgroundResourceAggregation")) + .setArguments(Arrays.asList(getBackgroundResourcesMethodExpr)) + .build(); + + if (service.methods().stream().anyMatch(Method::isResumableUpload)) { + MethodInvocationExpr listBuilderExpr = + MethodInvocationExpr.builder() + .setStaticReferenceType(FIXED_TYPESTORE.get("ImmutableList")) + .setGenerics(Arrays.asList(FIXED_TYPESTORE.get("BackgroundResource").reference())) + .setMethodName("builder") + .build(); + listBuilderExpr = + MethodInvocationExpr.builder() + .setExprReferenceExpr(listBuilderExpr) + .setMethodName("addAll") + .setArguments(Arrays.asList(getBackgroundResourcesMethodExpr)) + .build(); + listBuilderExpr = + MethodInvocationExpr.builder() + .setExprReferenceExpr(listBuilderExpr) + .setMethodName("add") + .setArguments( + Arrays.asList(classMemberVarExprs.get(RESUMABLE_UPLOAD_STUB_MEMBER_NAME))) + .build(); + listBuilderExpr = + MethodInvocationExpr.builder() + .setExprReferenceExpr(listBuilderExpr) + .setMethodName("build") + .build(); + Expr aggregatedBackgroundResourceExpr = + NewObjectExpr.builder() + .setType(FIXED_TYPESTORE.get("BackgroundResourceAggregation")) + .setArguments(Arrays.asList(listBuilderExpr)) + .build(); + + VariableExpr resumableUploadStubVarExpr = + classMemberVarExprs.get(RESUMABLE_UPLOAD_STUB_MEMBER_NAME); + + // The upload stub is a background resource, so it belongs in the aggregation when it exists. + Statement uploadStubPresentBackgroundResourceStmt = + ExprStatement.withExpr( + AssignmentExpr.builder() + .setVariableExpr(thisBackgroundResourcesVarExpr) + .setValueExpr(aggregatedBackgroundResourceExpr) + .build()); + Statement uploadStubAbsentBackgroundResourceStmt = + ExprStatement.withExpr( + AssignmentExpr.builder() + .setVariableExpr(thisBackgroundResourcesVarExpr) + .setValueExpr(standardBackgroundResourceAggregationExpr) + .build()); + + secondCtorStatements.addAll( + createResumableUploadStubBackgroundResourceStatements( + clientContextVarExpr, + resumableUploadStubVarExpr.toBuilder().setExprReferenceExpr(thisExpr).build(), + createResumableUploadStubInitStatements( + service, + typeStore, + thisExpr, + resumableUploadStubVarExpr, + clientContextVarExpr, + settingsVarExpr), + uploadStubPresentBackgroundResourceStmt, + uploadStubAbsentBackgroundResourceStmt)); + } else { + secondCtorStatements.add( + ExprStatement.withExpr( + AssignmentExpr.builder() + .setVariableExpr(thisBackgroundResourcesVarExpr) + .setValueExpr(standardBackgroundResourceAggregationExpr) + .build())); + } // Second constructor method. MethodDefinition secondCtor = @@ -906,6 +1004,62 @@ protected List createOperationsStubInitExpr( .build()); } + /** + * Emits {@code this. = HttpJsonResumableUploadStub.create(clientContext, + * settings);}. Transports that cannot always create the stub guard this in {@link + * #createResumableUploadStubBackgroundResourceStatements} rather than overriding here. + */ + protected List createResumableUploadStubInitStatements( + Service service, + TypeStore typeStore, + Expr thisExpr, + VariableExpr resumableUploadStubVarExpr, + VariableExpr clientContextVarExpr, + VariableExpr settingsVarExpr) { + TypeNode stubType = typeStore.get(String.format(RESUMABLE_UPLOAD_STUB_PATTERN, service.name())); + Expr createStubExpr = + MethodInvocationExpr.builder() + .setStaticReferenceType(stubType) + .setMethodName("create") + .setArguments(Arrays.asList(clientContextVarExpr, settingsVarExpr)) + .setReturnType(stubType) + .build(); + return Collections.singletonList( + ExprStatement.withExpr( + AssignmentExpr.builder() + .setVariableExpr( + resumableUploadStubVarExpr.toBuilder().setExprReferenceExpr(thisExpr).build()) + .setValueExpr(createStubExpr) + .build())); + } + + /** + * Joins the upload stub initialization to the {@code this.backgroundResources} assignment. + * + *

Both assignments are supplied because the two cases need different aggregations: {@code + * uploadStubPresentBackgroundResourceStmt} includes the upload stub, {@code + * uploadStubAbsentBackgroundResourceStmt} does not. This default implementation always creates + * the stub — see {@link #isResumableUploadStubNullable()} — so the absent case is unreachable and + * is deliberately not emitted. The gRPC transport overrides this to emit both branches under a + * credentials guard, because credentials cannot be extracted from a pre-constructed Channel. + */ + protected List createResumableUploadStubBackgroundResourceStatements( + VariableExpr clientContextVarExpr, + VariableExpr thisResumableUploadStubVarExpr, + List resumableUploadStubInitStatements, + Statement uploadStubPresentBackgroundResourceStmt, + Statement uploadStubAbsentBackgroundResourceStmt) { + return ImmutableList.builder() + .addAll(resumableUploadStubInitStatements) + .add(EMPTY_LINE_STATEMENT) + .add(uploadStubPresentBackgroundResourceStmt) + .build(); + } + + protected boolean isResumableUploadStubNullable() { + return false; + } + protected List createLongRunningClient(Service service, TypeStore typeStore) { return ImmutableList.of(); } @@ -1092,8 +1246,39 @@ private static List createCallableGetterMethods( .collect(Collectors.toList()); } + protected List createResumableUploadCallableGetterMethods( + Service service, Map classMemberVarExprs) { + VariableExpr resumableUploadStubVarExpr = + classMemberVarExprs.get(RESUMABLE_UPLOAD_STUB_MEMBER_NAME); + return service.methods().stream() + .filter(Method::isResumableUpload) + .map( + m -> { + String javaStyleMethodName = JavaStyle.toLowerCamelCase(m.name()); + String callableMethodName = + String.format(CALLABLE_CLASS_MEMBER_PATTERN, javaStyleMethodName); + TypeNode callableType = getCallableType(m); + Expr returnExpr = + MethodInvocationExpr.builder() + .setExprReferenceExpr(resumableUploadStubVarExpr) + .setMethodName(callableMethodName) + .setReturnType(callableType) + .build(); + return MethodDefinition.builder() + .setIsOverride(true) + .setScope(ScopeNode.PUBLIC) + .setReturnType(callableType) + .setName(callableMethodName) + .setReturnExpr(returnExpr) + .build(); + }) + .collect(Collectors.toList()); + } + private List createStubOverrideMethods( - VariableExpr backgroundResourcesVarExpr, Service service) { + Map classMemberVarExprs, Service service) { + VariableExpr backgroundResourcesVarExpr = + classMemberVarExprs.get(BACKGROUND_RESOURCES_MEMBER_NAME); List javaMethods = new ArrayList<>(); if (service.operationPollingMethod() != null) { javaMethods.addAll(createLongRunningClientGetters()); @@ -1151,10 +1336,22 @@ private TypeStore createDynamicTypes(Service service, String stubPakkage) { .collect(Collectors.toList()), true, getTransportContext().classNames().getServiceClientClassName(service)); + if (service.methods().stream().anyMatch(Method::isResumableUpload)) { + typeStore.put(stubPakkage, String.format(RESUMABLE_UPLOAD_STUB_PATTERN, service.name())); + } return typeStore; } protected static TypeNode getCallableType(Method protoMethod) { + if (protoMethod.isResumableUpload()) { + return TypeNode.withReference( + ConcreteReference.builder() + .setClazz(ResumableUploadCallable.class) + .setGenerics( + Arrays.asList( + protoMethod.inputType().reference(), protoMethod.outputType().reference())) + .build()); + } TypeNode callableType = FIXED_TYPESTORE.get("UnaryCallable"); switch (protoMethod.stream()) { case CLIENT: diff --git a/sdk-platform-java/gapic-generator-java/src/main/java/com/google/api/generator/gapic/composer/grpc/GrpcServiceStubClassComposer.java b/sdk-platform-java/gapic-generator-java/src/main/java/com/google/api/generator/gapic/composer/grpc/GrpcServiceStubClassComposer.java index 5cbff693c162..217290287ebc 100644 --- a/sdk-platform-java/gapic-generator-java/src/main/java/com/google/api/generator/gapic/composer/grpc/GrpcServiceStubClassComposer.java +++ b/sdk-platform-java/gapic-generator-java/src/main/java/com/google/api/generator/gapic/composer/grpc/GrpcServiceStubClassComposer.java @@ -15,17 +15,24 @@ package com.google.api.generator.gapic.composer.grpc; import com.google.api.gax.grpc.GrpcCallSettings; +import com.google.api.gax.grpc.GrpcStatusCode; import com.google.api.gax.grpc.GrpcStubCallableFactory; +import com.google.api.gax.rpc.FailedPreconditionException; import com.google.api.generator.engine.ast.AssignmentExpr; import com.google.api.generator.engine.ast.ConcreteReference; import com.google.api.generator.engine.ast.EnumRefExpr; import com.google.api.generator.engine.ast.Expr; import com.google.api.generator.engine.ast.ExprStatement; +import com.google.api.generator.engine.ast.IfStatement; +import com.google.api.generator.engine.ast.MethodDefinition; import com.google.api.generator.engine.ast.MethodInvocationExpr; +import com.google.api.generator.engine.ast.NewObjectExpr; import com.google.api.generator.engine.ast.PrimitiveValue; +import com.google.api.generator.engine.ast.RelationalOperationExpr; import com.google.api.generator.engine.ast.ScopeNode; import com.google.api.generator.engine.ast.Statement; import com.google.api.generator.engine.ast.StringObjectValue; +import com.google.api.generator.engine.ast.ThrowExpr; import com.google.api.generator.engine.ast.TypeNode; import com.google.api.generator.engine.ast.ValueExpr; import com.google.api.generator.engine.ast.VariableExpr; @@ -34,16 +41,21 @@ import com.google.api.generator.gapic.model.Message; import com.google.api.generator.gapic.model.Method; import com.google.api.generator.gapic.model.Service; +import com.google.api.generator.gapic.utils.JavaStyle; +import com.google.common.collect.ImmutableList; import com.google.longrunning.stub.GrpcOperationsStub; import io.grpc.MethodDescriptor; +import io.grpc.Status; import io.grpc.protobuf.ProtoUtils; import java.util.Arrays; +import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.function.BiFunction; import java.util.function.Function; +import java.util.stream.Collectors; import org.jspecify.annotations.NullMarked; @NullMarked @@ -58,6 +70,9 @@ public class GrpcServiceStubClassComposer extends AbstractTransportServiceStubCl private static final Set REROUTE_TO_GRPC_INTERFACE_IAM_METHOD_ALLOWLIST = new HashSet<>(Arrays.asList("SetIamPolicy", "GetIamPolicy", "TestIamPermissions")); + private static final String BUILD_METHOD_NAME = "build"; + private static final String NEW_BUILDER_METHOD_NAME = "newBuilder"; + private static final TypeStore FIXED_GRPC_TYPE_STORE = createStaticTypes(); protected GrpcServiceStubClassComposer() { @@ -71,11 +86,14 @@ public static GrpcServiceStubClassComposer instance() { private static TypeStore createStaticTypes() { List> concreteClazzes = Arrays.asList( + FailedPreconditionException.class, GrpcCallSettings.class, GrpcOperationsStub.class, + GrpcStatusCode.class, GrpcStubCallableFactory.class, MethodDescriptor.class, - ProtoUtils.class); + ProtoUtils.class, + Status.class); return new TypeStore(concreteClazzes); } @@ -88,7 +106,7 @@ protected Statement createMethodDescriptorVariableDecl( boolean restNumericEnumsEnabled) { MethodInvocationExpr methodDescriptorMaker = MethodInvocationExpr.builder() - .setMethodName("newBuilder") + .setMethodName(NEW_BUILDER_METHOD_NAME) .setStaticReferenceType(FIXED_GRPC_TYPE_STORE.get("MethodDescriptor")) .setGenerics(methodDescriptorVarExpr.variable().type().reference().generics()) .build(); @@ -155,7 +173,7 @@ protected Statement createMethodDescriptorVariableDecl( methodDescriptorMaker = MethodInvocationExpr.builder() - .setMethodName("build") + .setMethodName(BUILD_METHOD_NAME) .setExprReferenceExpr(methodDescriptorMaker) .setReturnType(methodDescriptorVarExpr.type()) .build(); @@ -213,4 +231,137 @@ protected String getProtoRpcFullMethodName(Service protoService, Method protoMet // long-term solution. return String.format("google.iam.v1.IAMPolicy/%s", protoMethod.name()); } + + @Override + protected boolean isResumableUploadStubNullable() { + return true; + } + + @Override + protected List createResumableUploadStubBackgroundResourceStatements( + VariableExpr clientContextVarExpr, + VariableExpr thisResumableUploadStubVarExpr, + List resumableUploadStubInitStatements, + Statement uploadStubPresentBackgroundResourceStmt, + Statement uploadStubAbsentBackgroundResourceStmt) { + // Credentials cannot be extracted from a pre-constructed gRPC Channel, hence the null guard. + // The upload stub derives its own HTTP/JSON ClientContext from the one passed here. + Expr getCredentialsExpr = + MethodInvocationExpr.builder() + .setExprReferenceExpr(clientContextVarExpr) + .setMethodName("getCredentials") + .setReturnType(TypeNode.OBJECT) + .build(); + List uploadStubPresentStmts = + ImmutableList.builder() + .addAll(resumableUploadStubInitStatements) + .add(uploadStubPresentBackgroundResourceStmt) + .build(); + List uploadStubAbsentStmts = + Arrays.asList( + ExprStatement.withExpr( + AssignmentExpr.builder() + .setVariableExpr(thisResumableUploadStubVarExpr) + .setValueExpr(ValueExpr.createNullExpr()) + .build()), + uploadStubAbsentBackgroundResourceStmt); + return Collections.singletonList( + IfStatement.builder() + .setConditionExpr( + RelationalOperationExpr.notEqualToWithExprs( + getCredentialsExpr, ValueExpr.createNullExpr())) + .setBody(uploadStubPresentStmts) + .setElseBody(uploadStubAbsentStmts) + .build()); + } + + @Override + protected List createResumableUploadCallableGetterMethods( + Service service, Map classMemberVarExprs) { + VariableExpr resumableUploadStubVarExpr = + classMemberVarExprs.get(RESUMABLE_UPLOAD_STUB_MEMBER_NAME); + return service.methods().stream() + .filter(Method::isResumableUpload) + .map( + m -> { + String javaStyleMethodName = JavaStyle.toLowerCamelCase(m.name()); + String callableMethodName = + String.format(CALLABLE_CLASS_MEMBER_PATTERN, javaStyleMethodName); + TypeNode callableType = getCallableType(m); + + Statement nullCheckIfStmt = + IfStatement.builder() + .setConditionExpr( + RelationalOperationExpr.equalToWithExprs( + resumableUploadStubVarExpr, ValueExpr.createNullExpr())) + .setBody( + Arrays.asList( + ExprStatement.withExpr( + ThrowExpr.builder() + .setThrowExpr( + NewObjectExpr.builder() + .setType( + FIXED_GRPC_TYPE_STORE.get( + "FailedPreconditionException")) + .setArguments( + Arrays.asList( + ValueExpr.withValue( + StringObjectValue.withValue( + "Resumable uploads execute over" + + " HTTP/REST and require" + + " credentials. The client was" + + " initialized with a" + + " pre-constructed gRPC Channel," + + " from which credentials cannot" + + " be extracted. Please" + + " configure a" + + " CredentialsProvider" + + " instead.")), + ValueExpr.createNullExpr(), + MethodInvocationExpr.builder() + .setStaticReferenceType( + FIXED_GRPC_TYPE_STORE.get( + "GrpcStatusCode")) + .setMethodName("of") + .setArguments( + Arrays.asList( + EnumRefExpr.builder() + .setName( + "FAILED_PRECONDITION") + .setType( + TypeNode.withReference( + ConcreteReference + .builder() + .setClazz( + Status.Code + .class) + .build())) + .build())) + .build(), + ValueExpr.withValue( + PrimitiveValue.builder() + .setType(TypeNode.BOOLEAN) + .setValue("false") + .build()))) + .build()) + .build()))) + .build(); + + Expr returnExpr = + MethodInvocationExpr.builder() + .setExprReferenceExpr(resumableUploadStubVarExpr) + .setMethodName(callableMethodName) + .setReturnType(callableType) + .build(); + return MethodDefinition.builder() + .setIsOverride(true) + .setScope(ScopeNode.PUBLIC) + .setReturnType(callableType) + .setName(callableMethodName) + .setBody(Arrays.asList(nullCheckIfStmt)) + .setReturnExpr(returnExpr) + .build(); + }) + .collect(Collectors.toList()); + } } diff --git a/sdk-platform-java/gapic-generator-java/src/test/java/com/google/api/generator/gapic/composer/grpc/GrpcServiceStubClassComposerTest.java b/sdk-platform-java/gapic-generator-java/src/test/java/com/google/api/generator/gapic/composer/grpc/GrpcServiceStubClassComposerTest.java index eb54b80091b9..423eca5430a0 100644 --- a/sdk-platform-java/gapic-generator-java/src/test/java/com/google/api/generator/gapic/composer/grpc/GrpcServiceStubClassComposerTest.java +++ b/sdk-platform-java/gapic-generator-java/src/test/java/com/google/api/generator/gapic/composer/grpc/GrpcServiceStubClassComposerTest.java @@ -122,4 +122,14 @@ void generateGrpcServiceStubClass_clientResourceNameExtractor() { Assert.assertGoldenClass(this.getClass(), clazz, "GrpcResourceNameExtractorStub.golden"); Assert.assertEmptySamples(clazz.samples()); } + + @Test + void generateGrpcServiceStubClass_resumableUpload() { + GapicContext context = GrpcTestProtoLoader.instance().parseShowcaseResumableUpload(); + Service service = context.services().get(0); + GapicClass clazz = GrpcServiceStubClassComposer.instance().generate(context, service); + + Assert.assertGoldenClass(this.getClass(), clazz, "GrpcResumableUploadServiceStub.golden"); + Assert.assertEmptySamples(clazz.samples()); + } } diff --git a/sdk-platform-java/gapic-generator-java/src/test/java/com/google/api/generator/gapic/composer/grpc/ServiceStubClassComposerTest.java b/sdk-platform-java/gapic-generator-java/src/test/java/com/google/api/generator/gapic/composer/grpc/ServiceStubClassComposerTest.java index 21d57f02b8a3..5c8d522ec286 100644 --- a/sdk-platform-java/gapic-generator-java/src/test/java/com/google/api/generator/gapic/composer/grpc/ServiceStubClassComposerTest.java +++ b/sdk-platform-java/gapic-generator-java/src/test/java/com/google/api/generator/gapic/composer/grpc/ServiceStubClassComposerTest.java @@ -39,7 +39,13 @@ static Stream data() { TestProtoLoader.instance().parseSelectiveGenerationTesting(), "", "", - 1)); + 1), + Arguments.of( + "ResumableUploadServiceStub", + TestProtoLoader.instance().parseShowcaseResumableUpload(), + "", + "", + 0)); } @ParameterizedTest diff --git a/sdk-platform-java/gapic-generator-java/src/test/java/com/google/api/generator/gapic/composer/grpc/goldens/GrpcResumableUploadServiceStub.golden b/sdk-platform-java/gapic-generator-java/src/test/java/com/google/api/generator/gapic/composer/grpc/goldens/GrpcResumableUploadServiceStub.golden new file mode 100644 index 000000000000..2ffeee116552 --- /dev/null +++ b/sdk-platform-java/gapic-generator-java/src/test/java/com/google/api/generator/gapic/composer/grpc/goldens/GrpcResumableUploadServiceStub.golden @@ -0,0 +1,145 @@ +package com.google.showcase.v1beta1.stub; + +import com.google.api.core.BetaApi; +import com.google.api.gax.core.BackgroundResource; +import com.google.api.gax.core.BackgroundResourceAggregation; +import com.google.api.gax.grpc.GrpcStatusCode; +import com.google.api.gax.grpc.GrpcStubCallableFactory; +import com.google.api.gax.rpc.ClientContext; +import com.google.api.gax.rpc.FailedPreconditionException; +import com.google.api.gax.rpc.ResumableUploadCallable; +import com.google.common.collect.ImmutableList; +import com.google.longrunning.stub.GrpcOperationsStub; +import com.google.showcase.v1beta1.UploadMediaRequest; +import com.google.showcase.v1beta1.UploadMediaResponse; +import io.grpc.Status; +import java.io.IOException; +import java.util.concurrent.TimeUnit; +import javax.annotation.Generated; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +// AUTO-GENERATED DOCUMENTATION AND CLASS. +/** + * gRPC stub implementation for the ResumableUploadService service API. + * + *

This class is for advanced usage and reflects the underlying API directly. + */ +@NullMarked +@BetaApi +@Generated("by gapic-generator-java") +public class GrpcResumableUploadServiceStub extends ResumableUploadServiceStub { + + private final BackgroundResource backgroundResources; + private final GrpcOperationsStub operationsStub; + private final @Nullable HttpJsonResumableUploadServiceResumableUploadStub resumableUploadStub; + private final GrpcStubCallableFactory callableFactory; + + public static final GrpcResumableUploadServiceStub create( + ResumableUploadServiceStubSettings settings) throws IOException { + return new GrpcResumableUploadServiceStub(settings, ClientContext.create(settings)); + } + + public static final GrpcResumableUploadServiceStub create(ClientContext clientContext) + throws IOException { + return new GrpcResumableUploadServiceStub( + ResumableUploadServiceStubSettings.newBuilder().build(), clientContext); + } + + public static final GrpcResumableUploadServiceStub create( + ClientContext clientContext, GrpcStubCallableFactory callableFactory) throws IOException { + return new GrpcResumableUploadServiceStub( + ResumableUploadServiceStubSettings.newBuilder().build(), clientContext, callableFactory); + } + + /** + * Constructs an instance of GrpcResumableUploadServiceStub, using the given settings. This is + * protected so that it is easy to make a subclass, but otherwise, the static factory methods + * should be preferred. + */ + protected GrpcResumableUploadServiceStub( + ResumableUploadServiceStubSettings settings, ClientContext clientContext) throws IOException { + this(settings, clientContext, new GrpcResumableUploadServiceCallableFactory()); + } + + /** + * Constructs an instance of GrpcResumableUploadServiceStub, using the given settings. This is + * protected so that it is easy to make a subclass, but otherwise, the static factory methods + * should be preferred. + */ + protected GrpcResumableUploadServiceStub( + ResumableUploadServiceStubSettings settings, + ClientContext clientContext, + GrpcStubCallableFactory callableFactory) + throws IOException { + this.callableFactory = callableFactory; + this.operationsStub = GrpcOperationsStub.create(clientContext, callableFactory); + + if (clientContext.getCredentials() != null) { + this.resumableUploadStub = + HttpJsonResumableUploadServiceResumableUploadStub.create(clientContext, settings); + this.backgroundResources = + new BackgroundResourceAggregation( + ImmutableList.builder() + .addAll(clientContext.getBackgroundResources()) + .add(resumableUploadStub) + .build()); + } else { + this.resumableUploadStub = null; + this.backgroundResources = + new BackgroundResourceAggregation(clientContext.getBackgroundResources()); + } + } + + public GrpcOperationsStub getOperationsStub() { + return operationsStub; + } + + @Override + public ResumableUploadCallable uploadMediaCallable() { + if (resumableUploadStub == null) { + throw new FailedPreconditionException( + "Resumable uploads execute over HTTP/REST and require credentials. The client was initialized with a pre-constructed gRPC Channel, from which credentials cannot be extracted. Please configure a CredentialsProvider instead.", + null, + GrpcStatusCode.of(Status.Code.FAILED_PRECONDITION), + false); + } + return resumableUploadStub.uploadMediaCallable(); + } + + @Override + public final void close() { + try { + backgroundResources.close(); + } catch (RuntimeException e) { + throw e; + } catch (Exception e) { + throw new IllegalStateException("Failed to close resource", e); + } + } + + @Override + public void shutdown() { + backgroundResources.shutdown(); + } + + @Override + public boolean isShutdown() { + return backgroundResources.isShutdown(); + } + + @Override + public boolean isTerminated() { + return backgroundResources.isTerminated(); + } + + @Override + public void shutdownNow() { + backgroundResources.shutdownNow(); + } + + @Override + public boolean awaitTermination(long duration, TimeUnit unit) throws InterruptedException { + return backgroundResources.awaitTermination(duration, unit); + } +} diff --git a/sdk-platform-java/gapic-generator-java/src/test/java/com/google/api/generator/gapic/composer/grpc/goldens/ResumableUploadServiceStub.golden b/sdk-platform-java/gapic-generator-java/src/test/java/com/google/api/generator/gapic/composer/grpc/goldens/ResumableUploadServiceStub.golden new file mode 100644 index 000000000000..02423b36738e --- /dev/null +++ b/sdk-platform-java/gapic-generator-java/src/test/java/com/google/api/generator/gapic/composer/grpc/goldens/ResumableUploadServiceStub.golden @@ -0,0 +1,28 @@ +package com.google.showcase.v1beta1.stub; + +import com.google.api.core.BetaApi; +import com.google.api.gax.core.BackgroundResource; +import com.google.api.gax.rpc.ResumableUploadCallable; +import com.google.showcase.v1beta1.UploadMediaRequest; +import com.google.showcase.v1beta1.UploadMediaResponse; +import javax.annotation.Generated; +import org.jspecify.annotations.NullMarked; + +// AUTO-GENERATED DOCUMENTATION AND CLASS. +/** + * Base stub class for the ResumableUploadService service API. + * + *

This class is for advanced usage and reflects the underlying API directly. + */ +@NullMarked +@BetaApi +@Generated("by gapic-generator-java") +public abstract class ResumableUploadServiceStub implements BackgroundResource { + + public ResumableUploadCallable uploadMediaCallable() { + throw new UnsupportedOperationException("Not implemented: uploadMediaCallable()"); + } + + @Override + public abstract void close(); +} diff --git a/sdk-platform-java/gapic-generator-java/src/test/java/com/google/api/generator/gapic/composer/rest/HttpJsonServiceStubClassComposerTest.java b/sdk-platform-java/gapic-generator-java/src/test/java/com/google/api/generator/gapic/composer/rest/HttpJsonServiceStubClassComposerTest.java index 76c07f0e9181..e195d2172665 100644 --- a/sdk-platform-java/gapic-generator-java/src/test/java/com/google/api/generator/gapic/composer/rest/HttpJsonServiceStubClassComposerTest.java +++ b/sdk-platform-java/gapic-generator-java/src/test/java/com/google/api/generator/gapic/composer/rest/HttpJsonServiceStubClassComposerTest.java @@ -227,4 +227,14 @@ void generateHttpJsonServiceStubClass_clientResourceNameExtractor() { Assert.assertGoldenClass(this.getClass(), clazz, "HttpJsonResourceNameExtractorStub.golden"); Assert.assertEmptySamples(clazz.samples()); } + + @Test + void generateHttpJsonServiceStubClass_resumableUpload() { + GapicContext context = RestTestProtoLoader.instance().parseShowcaseResumableUpload(); + Service service = context.services().get(0); + GapicClass clazz = composer.generate(context, service); + + Assert.assertGoldenClass(this.getClass(), clazz, "HttpJsonResumableUploadServiceStub.golden"); + Assert.assertEmptySamples(clazz.samples()); + } } diff --git a/sdk-platform-java/gapic-generator-java/src/test/java/com/google/api/generator/gapic/composer/rest/goldens/HttpJsonResumableUploadServiceStub.golden b/sdk-platform-java/gapic-generator-java/src/test/java/com/google/api/generator/gapic/composer/rest/goldens/HttpJsonResumableUploadServiceStub.golden new file mode 100644 index 000000000000..1a6b8b67ecd4 --- /dev/null +++ b/sdk-platform-java/gapic-generator-java/src/test/java/com/google/api/generator/gapic/composer/rest/goldens/HttpJsonResumableUploadServiceStub.golden @@ -0,0 +1,134 @@ +package com.google.showcase.v1beta1.stub; + +import com.google.api.core.BetaApi; +import com.google.api.core.InternalApi; +import com.google.api.gax.core.BackgroundResource; +import com.google.api.gax.core.BackgroundResourceAggregation; +import com.google.api.gax.httpjson.ApiMethodDescriptor; +import com.google.api.gax.httpjson.HttpJsonStubCallableFactory; +import com.google.api.gax.rpc.ClientContext; +import com.google.api.gax.rpc.ResumableUploadCallable; +import com.google.common.collect.ImmutableList; +import com.google.protobuf.TypeRegistry; +import com.google.showcase.v1beta1.UploadMediaRequest; +import com.google.showcase.v1beta1.UploadMediaResponse; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; +import javax.annotation.Generated; +import org.jspecify.annotations.NullMarked; + +// AUTO-GENERATED DOCUMENTATION AND CLASS. +/** + * REST stub implementation for the ResumableUploadService service API. + * + *

This class is for advanced usage and reflects the underlying API directly. + */ +@NullMarked +@BetaApi +@Generated("by gapic-generator-java") +public class HttpJsonResumableUploadServiceStub extends ResumableUploadServiceStub { + private static final TypeRegistry typeRegistry = TypeRegistry.newBuilder().build(); + + private final BackgroundResource backgroundResources; + private final HttpJsonResumableUploadServiceResumableUploadStub resumableUploadStub; + private final HttpJsonStubCallableFactory callableFactory; + + public static final HttpJsonResumableUploadServiceStub create( + ResumableUploadServiceStubSettings settings) throws IOException { + return new HttpJsonResumableUploadServiceStub(settings, ClientContext.create(settings)); + } + + public static final HttpJsonResumableUploadServiceStub create(ClientContext clientContext) + throws IOException { + return new HttpJsonResumableUploadServiceStub( + ResumableUploadServiceStubSettings.newBuilder().build(), clientContext); + } + + public static final HttpJsonResumableUploadServiceStub create( + ClientContext clientContext, HttpJsonStubCallableFactory callableFactory) throws IOException { + return new HttpJsonResumableUploadServiceStub( + ResumableUploadServiceStubSettings.newBuilder().build(), clientContext, callableFactory); + } + + /** + * Constructs an instance of HttpJsonResumableUploadServiceStub, using the given settings. This is + * protected so that it is easy to make a subclass, but otherwise, the static factory methods + * should be preferred. + */ + protected HttpJsonResumableUploadServiceStub( + ResumableUploadServiceStubSettings settings, ClientContext clientContext) throws IOException { + this(settings, clientContext, new HttpJsonResumableUploadServiceCallableFactory()); + } + + /** + * Constructs an instance of HttpJsonResumableUploadServiceStub, using the given settings. This is + * protected so that it is easy to make a subclass, but otherwise, the static factory methods + * should be preferred. + */ + protected HttpJsonResumableUploadServiceStub( + ResumableUploadServiceStubSettings settings, + ClientContext clientContext, + HttpJsonStubCallableFactory callableFactory) + throws IOException { + this.callableFactory = callableFactory; + + this.resumableUploadStub = + HttpJsonResumableUploadServiceResumableUploadStub.create(clientContext, settings); + + this.backgroundResources = + new BackgroundResourceAggregation( + ImmutableList.builder() + .addAll(clientContext.getBackgroundResources()) + .add(resumableUploadStub) + .build()); + } + + @InternalApi + public static List getMethodDescriptors() { + List methodDescriptors = new ArrayList<>(); + return methodDescriptors; + } + + @Override + public ResumableUploadCallable uploadMediaCallable() { + return resumableUploadStub.uploadMediaCallable(); + } + + @Override + public final void close() { + try { + backgroundResources.close(); + } catch (RuntimeException e) { + throw e; + } catch (Exception e) { + throw new IllegalStateException("Failed to close resource", e); + } + } + + @Override + public void shutdown() { + backgroundResources.shutdown(); + } + + @Override + public boolean isShutdown() { + return backgroundResources.isShutdown(); + } + + @Override + public boolean isTerminated() { + return backgroundResources.isTerminated(); + } + + @Override + public void shutdownNow() { + backgroundResources.shutdownNow(); + } + + @Override + public boolean awaitTermination(long duration, TimeUnit unit) throws InterruptedException { + return backgroundResources.awaitTermination(duration, unit); + } +}