Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,15 @@
*/
package com.google.api.gax.httpjson;

import com.google.api.client.http.ByteArrayContent;
import com.google.api.client.http.EmptyContent;
import com.google.api.client.http.HttpContent;
import com.google.api.client.http.HttpMethods;
import com.google.api.core.ApiFuture;
import com.google.api.core.InternalApi;
import com.google.api.core.SettableApiFuture;
import com.google.api.gax.resumable.ChunkUploadRequest;
import com.google.api.gax.resumable.ChunkUploadResponse;
import com.google.api.gax.resumable.ResumableUploadClient;
import com.google.api.gax.resumable.ResumableUploadSession;
import com.google.api.gax.resumable.StartUploadRequest;
Expand Down Expand Up @@ -67,8 +72,15 @@

private static final String UPLOAD_PROTOCOL_HEADER = "X-Goog-Upload-Protocol";
private static final String UPLOAD_COMMAND_HEADER = "X-Goog-Upload-Command";
private static final String UPLOAD_OFFSET_HEADER = "X-Goog-Upload-Offset";
private static final String UPLOAD_URL_HEADER = "X-Goog-Upload-URL";
private static final String UPLOAD_GRANULARITY_HEADER = "X-Goog-Upload-Chunk-Granularity";
private static final String UPLOAD_STATUS_HEADER = "X-Goog-Upload-Status";
private static final String UPLOAD_SIZE_RECEIVED_HEADER = "X-Goog-Upload-Size-Received";
private static final String STATUS_FINAL = "final";

/** HTTP status code 308 (Resume Incomplete in Google Scotty resumable upload protocol). */
private static final int HTTP_STATUS_RESUME_INCOMPLETE = 308;

private static final Map<String, List<String>> START_UPLOAD_HEADERS =
ImmutableMap.of(
Expand Down Expand Up @@ -105,6 +117,45 @@
.setResponseParser(StringHttpResponseParser.create())
.build();

private static final ApiMethodDescriptor<ChunkUploadRequest, String> UPLOAD_CHUNK_DESCRIPTOR =
ApiMethodDescriptor.<ChunkUploadRequest, String>newBuilder()
.setFullMethodName("ResumableUpload/UploadChunk")
.setHttpMethod(HttpMethods.POST)
.setType(ApiMethodDescriptor.MethodType.UNARY)
.setRequestFormatter(
new HttpRequestFormatter<ChunkUploadRequest>() {
@Override
public Map<String, List<String>> getQueryParamNames(ChunkUploadRequest request) {
return Collections.emptyMap();
}

@Override
public String getRequestBody(ChunkUploadRequest request) {
return "";
}

@Override
public HttpContent getHttpContent(ChunkUploadRequest request) {
if (!request.getPayload().isEmpty()) {
return new ByteArrayContent(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The null check request.getPayload() != null is redundant because ChunkUploadRequest is annotated with @NullMarked and getPayload() returns a non-nullable ByteString. We can safely simplify this to just check if the payload is empty.

                  if (!request.getPayload().isEmpty()) {
References
  1. Avoid adding defensive null checks for values that are guaranteed to be non-null by design, as this can hide invariant breaks.

"application/octet-stream", request.getPayload().toByteArray());
}
return new EmptyContent();
}

@Override
public String getPath(ChunkUploadRequest request) {
return request.getUploadUrl();
}

@Override
public PathTemplate getPathTemplate() {
return PathTemplate.create("{+path}");
}
})
.setResponseParser(StringHttpResponseParser.create())
.build();

private final ClientContext clientContext;

public static HttpJsonResumableUploadClient create(ClientContext clientContext) {
Expand Down Expand Up @@ -141,6 +192,45 @@
};
}

@Override
public UnaryCallable<ChunkUploadRequest, ChunkUploadResponse> uploadChunkCallable() {
return new UnaryCallable<ChunkUploadRequest, ChunkUploadResponse>() {
@Override
public ApiFuture<ChunkUploadResponse> futureCall(
ChunkUploadRequest request, @Nullable ApiCallContext inputContext) {
Preconditions.checkNotNull(request);
String command;
if (request.isFinal()) {
command = !request.getPayload().isEmpty() ? "upload, finalize" : "finalize";
} else {
command = "upload";
}
Map<String, List<String>> chunkHeaders =
ImmutableMap.of(
UPLOAD_COMMAND_HEADER,
ImmutableList.of(command),
UPLOAD_OFFSET_HEADER,
ImmutableList.of(String.valueOf(request.getOffset())));

HttpJsonCallContext context =
(HttpJsonCallContext)
HttpJsonCallContext.createDefault()
.nullToSelf(clientContext.getDefaultCallContext())
.merge(inputContext)

Check warning on line 219 in sdk-platform-java/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpJsonResumableUploadClient.java

View check run for this annotation

SonarQubeCloud / [gapic-generator-java-root] SonarCloud Code Analysis

Annotate the parameter with @javax.annotation.Nullable in method 'merge' declaration, or make sure that null can not be passed as argument.

See more on https://sonarcloud.io/project/issues?id=googleapis_google-cloud-java_showcase&issues=AaAcXnAClK7i42CvsrYR&open=AaAcXnAClK7i42CvsrYR&pullRequest=14140
.withExtraHeaders(chunkHeaders);
Comment on lines +215 to +220

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The cast to (HttpJsonCallContext) is redundant here. Since HttpJsonCallContext.createDefault().nullToSelf(...).merge(...) already returns HttpJsonCallContext (via covariant return types), and withExtraHeaders is called on it, the resulting expression is already of type HttpJsonCallContext.

        HttpJsonCallContext context =
            HttpJsonCallContext.createDefault()
                .nullToSelf(clientContext.getDefaultCallContext())
                .merge(inputContext)
                .withExtraHeaders(chunkHeaders);


HttpJsonClientCall<ChunkUploadRequest, String> clientCall =
HttpJsonClientCalls.newCall(UPLOAD_CHUNK_DESCRIPTOR, context);

SettableApiFuture<ChunkUploadResponse> future = SettableApiFuture.create();
HttpJsonClientCalls.startUnaryCall(
clientCall, request, context, new ChunkUploadResponseListener(request, future));

return future;
}
};
}

private static class StartUploadResponseListener extends HttpJsonClientCall.Listener<String> {

private final SettableApiFuture<ResumableUploadSession> future;
Expand Down Expand Up @@ -205,4 +295,81 @@
}
}
}

private static class ChunkUploadResponseListener extends HttpJsonClientCall.Listener<String> {

private final ChunkUploadRequest request;
private final SettableApiFuture<ChunkUploadResponse> future;
private boolean isComplete = false;
private long committedOffset = -1L;
private String responseBody = "";

ChunkUploadResponseListener(
ChunkUploadRequest request, SettableApiFuture<ChunkUploadResponse> future) {
this.request = request;
this.future = future;
}

@Override
public void onHeaders(HttpJsonMetadata responseHeaders) {
Map<String, Object> headers = responseHeaders.getHeaders();

String statusStr = HttpHeadersUtils.getFirstHeader(headers, UPLOAD_STATUS_HEADER);
if (STATUS_FINAL.equalsIgnoreCase(statusStr)) {
this.isComplete = true;
}

String sizeReceivedStr =
HttpHeadersUtils.getFirstHeader(headers, UPLOAD_SIZE_RECEIVED_HEADER);
if (!Strings.isNullOrEmpty(sizeReceivedStr)) {
try {
this.committedOffset = Long.parseLong(sizeReceivedStr);
} catch (NumberFormatException ignored) {

Check warning on line 327 in sdk-platform-java/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpJsonResumableUploadClient.java

View check run for this annotation

SonarQubeCloud / [gapic-generator-java-root] SonarCloud Code Analysis

Remove this block of code, fill it in, or add a comment explaining why it is empty.

See more on https://sonarcloud.io/project/issues?id=googleapis_google-cloud-java_showcase&issues=AaAcXnAClK7i42CvsrYS&open=AaAcXnAClK7i42CvsrYS&pullRequest=14140
}
}
}

@Override
public void onMessage(@Nullable String message) {
if (message != null) {
this.responseBody = message;
}
}

@Override
public void onClose(int statusCode, HttpJsonMetadata trailers) {
if ((statusCode >= 200 && statusCode < 300)
|| statusCode == HTTP_STATUS_RESUME_INCOMPLETE) {
if (statusCode == HTTP_STATUS_RESUME_INCOMPLETE && committedOffset < 0) {
future.setException(
ApiExceptionFactory.createException(
"Server returned 308 Resume Incomplete but the "
+ UPLOAD_SIZE_RECEIVED_HEADER
+ " header was missing or invalid",
/* cause= */ null,
HttpJsonStatusCode.of(statusCode),
/* retryable= */ false));
return;
}
long confirmedOffset =
committedOffset >= 0
? committedOffset
: request.getOffset() + request.getPayload().size();
future.set(
ChunkUploadResponse.create(
confirmedOffset, isComplete, isComplete ? responseBody : ""));
} else {
Throwable cause = trailers.getException();
ApiException apiException =
cause != null
? API_EXCEPTION_FACTORY.create(cause)
: ApiExceptionFactory.createException(
"Failed to upload chunk with status code: " + statusCode,
/* cause= */ null,
HttpJsonStatusCode.of(statusCode),
/* retryable= */ false);
future.setException(apiException);
}
}
}
}
Loading
Loading