diff --git a/gax-httpjson/src/main/java/com/google/api/gax/httpjson/ApiMessageOperationTransformers.java b/gax-httpjson/src/main/java/com/google/api/gax/httpjson/ApiMessageOperationTransformers.java new file mode 100644 index 000000000..fffeb8c07 --- /dev/null +++ b/gax-httpjson/src/main/java/com/google/api/gax/httpjson/ApiMessageOperationTransformers.java @@ -0,0 +1,122 @@ +/* + * Copyright 2019 Google LLC + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google LLC nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package com.google.api.gax.httpjson; + +import com.google.api.core.ApiFunction; +import com.google.api.core.BetaApi; +import com.google.api.gax.longrunning.OperationSnapshot; +import com.google.api.gax.rpc.ApiExceptionFactory; +import com.google.api.gax.rpc.StatusCode.Code; + +/** + * Transformers from OperationSnapshot wrappers to the underlying native ApiMessage objects. Public + * for technical reasons; intended for use by generated code. + */ +@BetaApi("The surface for use by generated code is not stable yet and may change in the future.") +public class ApiMessageOperationTransformers { + private ApiMessageOperationTransformers() {} + + public static class ResponseTransformer + implements ApiFunction { + private final Class responseTClass; + + private ResponseTransformer(Class responseTClass) { + this.responseTClass = responseTClass; + } + + /** Unwraps an OperationSnapshot and returns the contained method response message. */ + public ResponseT apply(OperationSnapshot operationSnapshot) { + if (!operationSnapshot.getErrorCode().getCode().equals(Code.OK)) { + // We potentially need to handle 2xx codes that are also successful. + throw ApiExceptionFactory.createException( + String.format( + "Operation with name \"%s\" failed with status = %s and message = %s", + operationSnapshot.getName(), + operationSnapshot.getErrorCode(), + operationSnapshot.getErrorMessage()), + null, + operationSnapshot.getErrorCode(), + false); + } + return transformEntityFromOperationSnapshot( + operationSnapshot, responseTClass, operationSnapshot.getResponse(), "response"); + } + + public static + ApiMessageOperationTransformers.ResponseTransformer create( + Class packedClass) { + return new ApiMessageOperationTransformers.ResponseTransformer<>(packedClass); + } + } + + public static class MetadataTransformer + implements ApiFunction { + private final Class metadataTClass; + + private MetadataTransformer(Class metadataTClass) { + this.metadataTClass = metadataTClass; + } + + /** Unwraps an OperationSnapshot and returns the contained operation metadata message. */ + @Override + public MetadataT apply(OperationSnapshot operationSnapshot) { + return transformEntityFromOperationSnapshot( + operationSnapshot, metadataTClass, operationSnapshot.getMetadata(), "metadata"); + } + + public static + ApiMessageOperationTransformers.MetadataTransformer create( + Class packedClass) { + return new ApiMessageOperationTransformers.MetadataTransformer<>(packedClass); + } + } + + private static T transformEntityFromOperationSnapshot( + OperationSnapshot operationSnapshot, + Class clazz, + Object operationEntity, + String entityName) { + if (!clazz.isAssignableFrom(operationEntity.getClass())) { + throw ApiExceptionFactory.createException( + new Throwable( + String.format( + "Operation with name \"%s\" succeeded, but its %s type %s cannot be cast to %s.", + operationSnapshot.getName(), + entityName, + operationEntity.getClass().getCanonicalName(), + clazz.getCanonicalName())), + operationSnapshot.getErrorCode(), + false); + } + @SuppressWarnings("unchecked") + T typedEntity = (T) operationEntity; + return typedEntity; + } +} diff --git a/gax-httpjson/src/test/java/com/google/api/gax/httpjson/ApiMessageOperationTransformersTest.java b/gax-httpjson/src/test/java/com/google/api/gax/httpjson/ApiMessageOperationTransformersTest.java new file mode 100644 index 000000000..783408632 --- /dev/null +++ b/gax-httpjson/src/test/java/com/google/api/gax/httpjson/ApiMessageOperationTransformersTest.java @@ -0,0 +1,229 @@ +/* + * Copyright 2019 Google LLC + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google LLC nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package com.google.api.gax.httpjson; + +import com.google.api.gax.httpjson.ApiMessageOperationTransformers.MetadataTransformer; +import com.google.api.gax.httpjson.ApiMessageOperationTransformers.ResponseTransformer; +import com.google.api.gax.httpjson.testing.FakeApiMessage; +import com.google.api.gax.longrunning.OperationSnapshot; +import com.google.api.gax.rpc.ApiException; +import com.google.api.gax.rpc.StatusCode; +import com.google.api.gax.rpc.StatusCode.Code; +import com.google.api.gax.rpc.UnavailableException; +import com.google.common.collect.ImmutableMap; +import com.google.common.truth.Truth; +import java.util.List; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Tests for ApiMessageOperationTransformers. */ +@RunWith(JUnit4.class) +public class ApiMessageOperationTransformersTest { + @Rule public ExpectedException thrown = ExpectedException.none(); + + @Test + public void testResponseTransformer() { + ResponseTransformer transformer = ResponseTransformer.create(EmptyMessage.class); + EmptyMessage emptyResponse = EmptyMessage.getDefaultInstance(); + + FakeMetadataMessage metadata = new FakeMetadataMessage(Status.PENDING, Code.OK); + OperationSnapshot operationSnapshot = + new OperationSnapshotImpl( + new FakeOperationMessage<>("Pending; no response method", emptyResponse, metadata)); + + Truth.assertThat(transformer.apply(operationSnapshot)).isEqualTo(emptyResponse); + } + + @Test + public void testResponseTransformer_exception() { + thrown.expect(UnavailableException.class); + ResponseTransformer transformer = ResponseTransformer.create(EmptyMessage.class); + EmptyMessage emptyResponse = EmptyMessage.getDefaultInstance(); + FakeMetadataMessage metadata = new FakeMetadataMessage(Status.PENDING, Code.UNAVAILABLE); + OperationSnapshot operationSnapshot = + new OperationSnapshotImpl( + new FakeOperationMessage<>("Unavailable; no response method", emptyResponse, metadata)); + + Truth.assertThat(transformer.apply(operationSnapshot)).isEqualTo(emptyResponse); + } + + @Test + public void testResponseTransformer_mismatchedTypes() { + thrown.expect(ApiException.class); + thrown.expectMessage("cannot be cast"); + ResponseTransformer transformer = ResponseTransformer.create(EmptyMessage.class); + FakeMetadataMessage metadata = new FakeMetadataMessage(Status.PENDING, Code.OK); + ApiMessage bananaResponse = + new FakeApiMessage(ImmutableMap.of("name", "banana"), null, null); + EmptyMessage emptyResponse = EmptyMessage.getDefaultInstance(); + OperationSnapshot operationSnapshot = + new OperationSnapshotImpl( + new FakeOperationMessage<>("No response method", bananaResponse, metadata)); + Truth.assertThat(transformer.apply(operationSnapshot)).isEqualTo(emptyResponse); + } + + @Test + public void testMetadataTransformer() { + MetadataTransformer transformer = + MetadataTransformer.create(FakeMetadataMessage.class); + EmptyMessage returnType = EmptyMessage.getDefaultInstance(); + FakeMetadataMessage metadataMessage = new FakeMetadataMessage(Status.PENDING, Code.OK); + FakeOperationMessage operation = new FakeOperationMessage<>("foo", returnType, metadataMessage); + OperationSnapshot operationSnapshot = new OperationSnapshotImpl(operation); + Truth.assertThat(transformer.apply(operationSnapshot)).isEqualTo(metadataMessage); + } + + @Test + public void testMetadataTransformer_mismatchedTypes() { + thrown.expect(ApiException.class); + thrown.expectMessage("cannot be cast"); + MetadataTransformer transformer = + MetadataTransformer.create(FakeOperationMessage.class); + FakeMetadataMessage metadataMessage = new FakeMetadataMessage(Status.PENDING, Code.OK); + ApiMessage bananaResponse = + new FakeApiMessage(ImmutableMap.of("name", "banana"), null, null); + FakeOperationMessage metadata = + new FakeOperationMessage<>("No response method", bananaResponse, metadataMessage); + OperationSnapshot operationSnapshot = new OperationSnapshotImpl(metadata); + Truth.assertThat(transformer.apply(operationSnapshot)).isEqualTo(bananaResponse); + } + + private enum Status { + PENDING, + DONE + } + + private static class FakeMetadataMessage implements ApiMessage { + + private final Status status; + private final Code code; + + public FakeMetadataMessage(Status status, Code code) { + this.status = status; + this.code = code; + } + + public Object getFieldValue(String fieldName) { + if ("status".equals(fieldName)) { + return status; + } + if ("code".equals(fieldName)) { + return code; + } + return null; + } + + public List getFieldMask() { + return null; + } + + public ApiMessage getApiMessageRequestBody() { + return null; + } + } + + private static class FakeOperationMessage< + ResponseT extends ApiMessage, MetadataT extends ApiMessage> + implements ApiMessage { + + private final String name; + private final ResponseT responseT; + private final MetadataT metadata; + + public FakeOperationMessage(String name, ResponseT responseT, MetadataT metadata) { + this.name = name; + this.responseT = responseT; + this.metadata = metadata; + } + + public Object getFieldValue(String fieldName) { + if ("name".equals(fieldName)) { + return name; + } + if ("responseT".equals(fieldName)) { + return responseT; + } + if ("metadata".equals(fieldName)) { + return metadata; + } + return null; + } + + public List getFieldMask() { + return null; + } + + public ResponseT getApiMessageRequestBody() { + return responseT; + } + } + + private static class OperationSnapshotImpl implements OperationSnapshot { + + private final FakeOperationMessage operation; + + public OperationSnapshotImpl(FakeOperationMessage operation) { + this.operation = operation; + } + + @Override + public String getName() { + return (String) operation.getFieldValue("name"); + } + + @Override + public Object getMetadata() { + return operation.metadata; + } + + @Override + public boolean isDone() { + return operation.metadata.getFieldValue("status") != Status.PENDING; + } + + @Override + public Object getResponse() { + return operation.getApiMessageRequestBody(); + } + + @Override + public StatusCode getErrorCode() { + return HttpJsonStatusCode.of((Code) operation.metadata.getFieldValue("code")); + } + + @Override + public String getErrorMessage() { + return ((Code) operation.metadata.getFieldValue("code")).name(); + } + } +}