From 4d9fb440de8ed346d28b5f57db1a0fc32a3d2125 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 | 38 +-- ...ractTransportServiceStubClassComposer.java | 220 ++++++++++++++++-- .../grpc/GrpcServiceStubClassComposer.java | 169 +++++++++++++- ...umableUploadTransportStubComposerTest.java | 205 ++++++++++++++++ .../GrpcServiceStubClassComposerTest.java | 10 + .../grpc/ServiceStubClassComposerTest.java | 8 +- .../GrpcResumableUploadServiceStub.golden | 149 ++++++++++++ .../goldens/ResumableUploadServiceStub.golden | 28 +++ ...eResumableUploadStubClassComposerTest.java | 17 ++ .../HttpJsonServiceStubClassComposerTest.java | 10 + .../HttpJsonResumableUploadServiceStub.golden | 134 +++++++++++ 12 files changed, 956 insertions(+), 39 deletions(-) create mode 100644 sdk-platform-java/gapic-generator-java/src/test/java/com/google/api/generator/gapic/composer/common/ResumableUploadTransportStubComposerTest.java 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..5d6da88b5f1b 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; @@ -52,6 +53,7 @@ import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.stream.Collectors; import javax.annotation.Generated; import org.jspecify.annotations.NullMarked; @@ -162,22 +164,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 +257,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 +316,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..2a7d5bc861b8 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, @@ -851,6 +883,17 @@ protected List createConstructorMethods( secondCtorExprs.clear(); secondCtorStatements.add(EMPTY_LINE_STATEMENT); + if (service.methods().stream().anyMatch(Method::isResumableUpload)) { + secondCtorStatements.addAll( + createResumableUploadStubInitStatements( + service, + typeStore, + thisExpr, + classMemberVarExprs.get(RESUMABLE_UPLOAD_STUB_MEMBER_NAME), + clientContextVarExpr, + settingsVarExpr)); + } + secondCtorStatements.addAll(createLongRunningClient(service, typeStore)); // Instantiate backgroundResources. @@ -860,20 +903,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(); + + if (isResumableUploadStubNullable()) { + Statement nonNullBackgroundResourceStmt = + ExprStatement.withExpr( + AssignmentExpr.builder() + .setVariableExpr(thisBackgroundResourcesVarExpr) + .setValueExpr(aggregatedBackgroundResourceExpr) + .build()); + Statement nullBackgroundResourceStmt = + ExprStatement.withExpr( + AssignmentExpr.builder() + .setVariableExpr(thisBackgroundResourcesVarExpr) + .setValueExpr(standardBackgroundResourceAggregationExpr) + .build()); + secondCtorStatements.add( + IfStatement.builder() + .setConditionExpr( + RelationalOperationExpr.notEqualToWithExprs( + classMemberVarExprs.get(RESUMABLE_UPLOAD_STUB_MEMBER_NAME), + ValueExpr.createNullExpr())) + .setBody(Arrays.asList(nonNullBackgroundResourceStmt)) + .setElseBody(Arrays.asList(nullBackgroundResourceStmt)) + .build()); + } else { + secondCtorStatements.add( + ExprStatement.withExpr( + AssignmentExpr.builder() + .setVariableExpr(thisBackgroundResourcesVarExpr) + .setValueExpr(aggregatedBackgroundResourceExpr) + .build())); + } + } else { + secondCtorStatements.add( + ExprStatement.withExpr( + AssignmentExpr.builder() + .setVariableExpr(thisBackgroundResourcesVarExpr) + .setValueExpr(standardBackgroundResourceAggregationExpr) + .build())); + } // Second constructor method. MethodDefinition secondCtor = @@ -906,6 +1015,36 @@ protected List createOperationsStubInitExpr( .build()); } + 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 Arrays.asList( + ExprStatement.withExpr( + AssignmentExpr.builder() + .setVariableExpr( + resumableUploadStubVarExpr.toBuilder().setExprReferenceExpr(thisExpr).build()) + .setValueExpr(createStubExpr) + .build()), + EMPTY_LINE_STATEMENT); + } + + protected boolean isResumableUploadStubNullable() { + return false; + } + protected List createLongRunningClient(Service service, TypeStore typeStore) { return ImmutableList.of(); } @@ -1092,8 +1231,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 +1321,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..bc1f6f87f338 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,8 +41,10 @@ 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.longrunning.stub.GrpcOperationsStub; import io.grpc.MethodDescriptor; +import io.grpc.Status; import io.grpc.protobuf.ProtoUtils; import java.util.Arrays; import java.util.HashSet; @@ -44,6 +53,7 @@ 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 +68,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 +84,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 +104,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 +171,7 @@ protected Statement createMethodDescriptorVariableDecl( methodDescriptorMaker = MethodInvocationExpr.builder() - .setMethodName("build") + .setMethodName(BUILD_METHOD_NAME) .setExprReferenceExpr(methodDescriptorMaker) .setReturnType(methodDescriptorVarExpr.type()) .build(); @@ -213,4 +229,151 @@ 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 createResumableUploadStubInitStatements( + Service service, + TypeStore typeStore, + Expr thisExpr, + VariableExpr resumableUploadStubVarExpr, + VariableExpr clientContextVarExpr, + VariableExpr settingsVarExpr) { + // Resumable uploads execute over HTTP/JSON and require credentials, which cannot be extracted + // from a pre-constructed gRPC Channel -- hence the null guard. The upload stub owns the + // derivation of its own HTTP/JSON ClientContext (see + // HttpJsonResumableUploadStub.createFrom), so all this stub does is hand over its + // context and settings. + TypeNode uploadStubType = + typeStore.get(String.format(RESUMABLE_UPLOAD_STUB_PATTERN, service.name())); + Expr getCredentialsExpr = + MethodInvocationExpr.builder() + .setExprReferenceExpr(clientContextVarExpr) + .setMethodName("getCredentials") + .setReturnType(TypeNode.OBJECT) + .build(); + VariableExpr assignTargetVarExpr = + resumableUploadStubVarExpr.toBuilder().setExprReferenceExpr(thisExpr).build(); + Statement assignResumableUploadStub = + ExprStatement.withExpr( + AssignmentExpr.builder() + .setVariableExpr(assignTargetVarExpr) + .setValueExpr( + MethodInvocationExpr.builder() + .setStaticReferenceType(uploadStubType) + .setMethodName("createFrom") + .setArguments(Arrays.asList(clientContextVarExpr, settingsVarExpr)) + .setReturnType(uploadStubType) + .build()) + .build()); + Statement assignNullStmt = + ExprStatement.withExpr( + AssignmentExpr.builder() + .setVariableExpr(assignTargetVarExpr) + .setValueExpr(ValueExpr.createNullExpr()) + .build()); + Statement ifStmt = + IfStatement.builder() + .setConditionExpr( + RelationalOperationExpr.notEqualToWithExprs( + getCredentialsExpr, ValueExpr.createNullExpr())) + .setBody(Arrays.asList(assignResumableUploadStub)) + .setElseBody(Arrays.asList(assignNullStmt)) + .build(); + return Arrays.asList(ifStmt, EMPTY_LINE_STATEMENT); + } + + @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/common/ResumableUploadTransportStubComposerTest.java b/sdk-platform-java/gapic-generator-java/src/test/java/com/google/api/generator/gapic/composer/common/ResumableUploadTransportStubComposerTest.java new file mode 100644 index 000000000000..9471d5f5c0fc --- /dev/null +++ b/sdk-platform-java/gapic-generator-java/src/test/java/com/google/api/generator/gapic/composer/common/ResumableUploadTransportStubComposerTest.java @@ -0,0 +1,205 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.api.generator.gapic.composer.common; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.api.generator.engine.writer.JavaWriterVisitor; +import com.google.api.generator.gapic.composer.grpc.GrpcServiceStubClassComposer; +import com.google.api.generator.gapic.composer.rest.HttpJsonServiceStubClassComposer; +import com.google.api.generator.gapic.model.GapicClass; +import com.google.api.generator.gapic.model.GapicClass.Kind; +import com.google.api.generator.gapic.model.GapicContext; +import com.google.api.generator.gapic.model.Service; +import com.google.api.generator.test.protoloader.GrpcTestProtoLoader; +import com.google.api.generator.test.protoloader.RestTestProtoLoader; +import org.junit.jupiter.api.Test; + +class ResumableUploadTransportStubComposerTest { + + private static String generateGrpcStubCode() { + GapicContext context = GrpcTestProtoLoader.instance().parseShowcaseResumableUpload(); + Service service = context.services().get(0); + + GapicClass clazz = GrpcServiceStubClassComposer.instance().generate(context, service); + assertThat(clazz.kind()).isEqualTo(Kind.STUB); + + JavaWriterVisitor visitor = new JavaWriterVisitor(); + clazz.classDefinition().accept(visitor); + return visitor.write(); + } + + private static String generateRestStubCode() { + GapicContext context = RestTestProtoLoader.instance().parseShowcaseResumableUpload(); + Service service = context.services().get(0); + + GapicClass clazz = HttpJsonServiceStubClassComposer.instance().generate(context, service); + assertThat(clazz.kind()).isEqualTo(Kind.STUB); + + JavaWriterVisitor visitor = new JavaWriterVisitor(); + clazz.classDefinition().accept(visitor); + return visitor.write(); + } + + @Test + void generate_grpcStub_withCredentials_initializesHttpStub() { + String code = generateGrpcStubCode(); + + // Verify key imports + assertThat(code).contains("import com.google.api.gax.grpc.GrpcStatusCode;"); + assertThat(code).contains("import com.google.api.gax.rpc.FailedPreconditionException;"); + assertThat(code).contains("import com.google.api.gax.rpc.ResumableUploadCallable;"); + assertThat(code).contains("import com.google.common.collect.ImmutableList;"); + assertThat(code).contains("import io.grpc.Status;"); + + // Verify class declaration and nullable stub field + assertThat(code) + .contains("public class GrpcResumableUploadServiceStub extends ResumableUploadServiceStub"); + assertThat(code) + .contains( + "private final @Nullable HttpJsonResumableUploadServiceResumableUploadStub" + + " resumableUploadStub;"); + + // The constructor guards on credentials and hands its own context and settings to the upload + // stub; deriving the HTTP/JSON ClientContext is the upload stub's job, not this stub's. + assertThat(code).contains("if (clientContext.getCredentials() != null) {"); + assertThat(code).contains("this.resumableUploadStub ="); + assertThat(code) + .contains( + "HttpJsonResumableUploadServiceResumableUploadStub.createFrom(clientContext," + + " settings);"); + assertThat(code).doesNotContain("ManagedHttpJsonChannel"); + assertThat(code).doesNotContain("HttpJsonTransportChannel"); + assertThat(code).doesNotContain("HttpJsonCallContext"); + assertThat(code).doesNotContain("uploadHeaders"); + } + + @Test + void generate_grpcStub_withoutCredentials_assignsNull() { + String code = generateGrpcStubCode(); + + assertThat(code).contains("} else {"); + assertThat(code).contains("this.resumableUploadStub = null;"); + } + + @Test + void generate_grpcStub_uploadMediaCallable_guardsNullWithFailedPrecondition() { + String code = generateGrpcStubCode(); + + // Verify callable getter throwing FailedPreconditionException when resumableUploadStub == null + assertThat(code) + .contains( + "public ResumableUploadCallable" + + " uploadMediaCallable() {"); + assertThat(code).contains("if (resumableUploadStub == null) {"); + assertThat(code).contains("throw new FailedPreconditionException("); + assertThat(code) + .contains("\"Resumable uploads execute over HTTP/REST and require credentials."); + assertThat(code).contains("GrpcStatusCode.of(Status.Code.FAILED_PRECONDITION)"); + assertThat(code).contains("return resumableUploadStub.uploadMediaCallable();"); + } + + @Test + void generate_grpcStub_implementsBackgroundResource() { + String code = generateGrpcStubCode(); + + // Verify BackgroundResource aggregation with fallback + assertThat(code).contains("if (resumableUploadStub != null) {"); + assertThat(code).contains("this.backgroundResources ="); + assertThat(code).contains("new BackgroundResourceAggregation("); + assertThat(code).contains("ImmutableList.builder()"); + assertThat(code).contains(".addAll(clientContext.getBackgroundResources())"); + assertThat(code).contains(".add(resumableUploadStub)"); + assertThat(code).contains(".build());"); + assertThat(code) + .contains( + "this.backgroundResources =\n" + + " new" + + " BackgroundResourceAggregation(clientContext.getBackgroundResources());"); + + // Verify close() delegates only to backgroundResources + assertThat(code).contains("backgroundResources.close();"); + assertThat(code).doesNotContain("resumableUploadStub.close();"); + } + + @Test + void generate_restStub_delegatesToResumableUploadStub() { + String code = generateRestStubCode(); + + // Verify class declaration and non-null stub field + assertThat(code) + .contains( + "public class HttpJsonResumableUploadServiceStub extends ResumableUploadServiceStub"); + assertThat(code) + .contains( + "private final HttpJsonResumableUploadServiceResumableUploadStub resumableUploadStub;"); + assertThat(code) + .doesNotContain( + "private final @Nullable HttpJsonResumableUploadServiceResumableUploadStub" + + " resumableUploadStub;"); + + // Verify REST constructor directly creates the upload stub + assertThat(code).contains("this.resumableUploadStub ="); + assertThat(code) + .contains( + "HttpJsonResumableUploadServiceResumableUploadStub.create(clientContext, settings);"); + + // Verify callable getter delegates directly + assertThat(code) + .contains( + "public ResumableUploadCallable" + + " uploadMediaCallable() {"); + assertThat(code).contains("return resumableUploadStub.uploadMediaCallable();"); + + // Verify BackgroundResource aggregation + assertThat(code).contains("ImmutableList.builder()"); + assertThat(code).contains(".addAll(clientContext.getBackgroundResources())"); + assertThat(code).contains(".add(resumableUploadStub)"); + assertThat(code).contains("backgroundResources.close();"); + assertThat(code).doesNotContain("resumableUploadStub.close();"); + } + + @Test + void generateGrpcServiceStubClass_serviceWithoutResumableUpload_noUploadStub() { + GapicContext context = GrpcTestProtoLoader.instance().parseShowcaseEcho(); + Service service = context.services().get(0); + + GapicClass clazz = GrpcServiceStubClassComposer.instance().generate(context, service); + assertThat(clazz.kind()).isEqualTo(Kind.STUB); + + JavaWriterVisitor visitor = new JavaWriterVisitor(); + clazz.classDefinition().accept(visitor); + String code = visitor.write(); + + assertThat(code).doesNotContain("resumableUploadStub"); + assertThat(code).doesNotContain("HttpJsonCallContext"); + assertThat(code).doesNotContain("FailedPreconditionException"); + } + + @Test + void generateHttpJsonServiceStubClass_serviceWithoutResumableUpload_noUploadStub() { + GapicContext context = RestTestProtoLoader.instance().parseCompliance(); + Service service = context.services().get(0); + + GapicClass clazz = HttpJsonServiceStubClassComposer.instance().generate(context, service); + assertThat(clazz.kind()).isEqualTo(Kind.STUB); + + JavaWriterVisitor visitor = new JavaWriterVisitor(); + clazz.classDefinition().accept(visitor); + String code = visitor.write(); + + assertThat(code).doesNotContain("resumableUploadStub"); + } +} 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..69408dbb5614 --- /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,149 @@ +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.createFrom(clientContext, settings); + } else { + this.resumableUploadStub = null; + } + + if (resumableUploadStub != null) { + this.backgroundResources = + new BackgroundResourceAggregation( + ImmutableList.builder() + .addAll(clientContext.getBackgroundResources()) + .add(resumableUploadStub) + .build()); + } else { + 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/HttpJsonServiceResumableUploadStubClassComposerTest.java b/sdk-platform-java/gapic-generator-java/src/test/java/com/google/api/generator/gapic/composer/rest/HttpJsonServiceResumableUploadStubClassComposerTest.java index d9b332d77746..52613cb2aba0 100644 --- a/sdk-platform-java/gapic-generator-java/src/test/java/com/google/api/generator/gapic/composer/rest/HttpJsonServiceResumableUploadStubClassComposerTest.java +++ b/sdk-platform-java/gapic-generator-java/src/test/java/com/google/api/generator/gapic/composer/rest/HttpJsonServiceResumableUploadStubClassComposerTest.java @@ -17,12 +17,14 @@ import static com.google.common.truth.Truth.assertThat; import com.google.api.generator.engine.writer.JavaWriterVisitor; +import com.google.api.generator.gapic.composer.Composer; import com.google.api.generator.gapic.model.GapicClass; import com.google.api.generator.gapic.model.GapicClass.Kind; import com.google.api.generator.gapic.model.GapicContext; import com.google.api.generator.gapic.model.Service; import com.google.api.generator.test.framework.Assert; import com.google.api.generator.test.protoloader.RestTestProtoLoader; +import java.util.List; import org.junit.jupiter.api.Test; class HttpJsonServiceResumableUploadStubClassComposerTest { @@ -189,6 +191,21 @@ void generate_serviceWithoutResumableUpload_returnsNonGeneratedClass() { assertThat(clazz.kind()).isEqualTo(GapicClass.Kind.NON_GENERATED); } + @Test + void composer_generateStubClasses_registersUploadStubWhenResumableUploadPresent() { + GapicContext context = RestTestProtoLoader.instance().parseShowcaseResumableUpload(); + List stubClasses = Composer.generateStubClasses(context); + assertThat( + stubClasses.stream() + .anyMatch( + c -> + c.classDefinition() + .classIdentifier() + .name() + .equals("HttpJsonResumableUploadServiceResumableUploadStub"))) + .isTrue(); + } + @Test void generate_matchesGolden() { GapicContext context = RestTestProtoLoader.instance().parseShowcaseResumableUpload(); 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 8f0281c28aea..4b372e73ccb7 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 @@ -178,4 +178,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); + } +}