diff --git a/build.gradle b/build.gradle index 6624d52805..64d37b8892 100644 --- a/build.gradle +++ b/build.gradle @@ -160,6 +160,7 @@ subprojects { hpack: 'com.twitter:hpack:0.10.1', jsr305: 'com.google.code.findbugs:jsr305:3.0.0', oauth_client: 'com.google.auth:google-auth-library-oauth2-http:0.4.0', + google_api_protos: 'com.google.api.grpc:grpc-google-common-protos:0.1.6', google_auth_credentials: 'com.google.auth:google-auth-library-credentials:0.4.0', okhttp: 'com.squareup.okhttp:okhttp:2.5.0', okio: 'com.squareup.okio:okio:1.6.0', diff --git a/protobuf/build.gradle b/protobuf/build.gradle index d0c5f7229d..e18cecedc7 100644 --- a/protobuf/build.gradle +++ b/protobuf/build.gradle @@ -1,14 +1,39 @@ description = 'gRPC: Protobuf' +buildscript { + repositories { + mavenCentral() + } + dependencies { + classpath libraries.protobuf_plugin + } +} + dependencies { compile project(':grpc-core'), libraries.protobuf, libraries.guava, libraries.protobuf_util + compile (libraries.google_api_protos) { + exclude group: 'io.grpc' + // 'com.google.api' transitively depends on 'com.google.auto.value:auto-value:1.1', which + // breaks our annotations. + exclude group: 'com.google.api' + } + compile (project(':grpc-protobuf-lite')) { exclude group: 'com.google.protobuf', module: 'protobuf-lite' } signature "org.codehaus.mojo.signature:java16:+@signature" } + +configureProtoCompilation() + +idea { + module { + sourceDirs += file("${projectDir}/src/generated/main/grpc"); + sourceDirs += file("${projectDir}/src/generated/main/java"); + } +} diff --git a/protobuf/src/main/java/io/grpc/protobuf/StatusProto.java b/protobuf/src/main/java/io/grpc/protobuf/StatusProto.java new file mode 100644 index 0000000000..798c039420 --- /dev/null +++ b/protobuf/src/main/java/io/grpc/protobuf/StatusProto.java @@ -0,0 +1,170 @@ +/* + * Copyright 2017, Google Inc. All rights reserved. + * + * 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 Inc. 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 io.grpc.protobuf; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +import io.grpc.ExperimentalApi; +import io.grpc.Metadata; +import io.grpc.Status; +import io.grpc.StatusException; +import io.grpc.StatusRuntimeException; +import io.grpc.protobuf.lite.ProtoLiteUtils; +import javax.annotation.Nullable; + +/** Utility methods for working with {@link com.google.rpc.Status}. */ +@ExperimentalApi +public final class StatusProto { + private StatusProto() {} + + private static final Metadata.Key STATUS_DETAILS_KEY = + Metadata.Key.of( + "grpc-status-details-bin", + ProtoLiteUtils.metadataMarshaller(com.google.rpc.Status.getDefaultInstance())); + + /** + * Convert a {@link com.google.rpc.Status} instance to a {@link StatusRuntimeException}. + * + *

The returned {@link StatusRuntimeException} will wrap a {@link Status} whose code and + * description are set from the code and message in {@code statusProto}. {@code statusProto} will + * be serialized and placed into the metadata of the returned {@link StatusRuntimeException}. + * + * @throws IllegalArgumentException if the value of {@code statusProto.getCode()} is not a valid + * gRPC status code. + */ + public static StatusRuntimeException toStatusRuntimeException(com.google.rpc.Status statusProto) { + return toStatus(statusProto).asRuntimeException(toMetadata(statusProto)); + } + + /** + * Convert a {@link com.google.rpc.Status} instance to a {@link StatusRuntimeException} with + * additional metadata. + * + *

The returned {@link StatusRuntimeException} will wrap a {@link Status} whose code and + * description are set from the code and message in {@code statusProto}. {@code statusProto} will + * be serialized and added to {@code metadata}. {@code metadata} will be set as the metadata of + * the returned {@link StatusRuntimeException}. + * + * @throws IllegalArgumentException if the value of {@code statusProto.getCode()} is not a valid + * gRPC status code. + */ + public static StatusRuntimeException toStatusRuntimeException( + com.google.rpc.Status statusProto, Metadata metadata) { + return toStatus(statusProto).asRuntimeException(toMetadata(statusProto, metadata)); + } + + /** + * Convert a {@link com.google.rpc.Status} instance to a {@link StatusException}. + * + *

The returned {@link StatusException} will wrap a {@link Status} whose code and description + * are set from the code and message in {@code statusProto}. {@code statusProto} will be + * serialized and placed into the metadata of the returned {@link StatusException}. + * + * @throws IllegalArgumentException if the value of {@code statusProto.getCode()} is not a valid + * gRPC status code. + */ + public static StatusException toStatusException(com.google.rpc.Status statusProto) { + return toStatus(statusProto).asException(toMetadata(statusProto)); + } + + /** + * Convert a {@link com.google.rpc.Status} instance to a {@link StatusException} with additional + * metadata. + * + *

The returned {@link StatusException} will wrap a {@link Status} whose code and description + * are set from the code and message in {@code statusProto}. {@code statusProto} will be + * serialized and added to {@code metadata}. {@code metadata} will be set as the metadata of the + * returned {@link StatusException}. + * + * @throws IllegalArgumentException if the value of {@code statusProto.getCode()} is not a valid + * gRPC status code. + */ + public static StatusException toStatusException( + com.google.rpc.Status statusProto, Metadata metadata) { + return toStatus(statusProto).asException(toMetadata(statusProto, metadata)); + } + + private static Status toStatus(com.google.rpc.Status statusProto) { + Status status = Status.fromCodeValue(statusProto.getCode()); + checkArgument(status.getCode().value() == statusProto.getCode(), "invalid status code"); + return status.withDescription(statusProto.getMessage()); + } + + private static Metadata toMetadata(com.google.rpc.Status statusProto) { + Metadata metadata = new Metadata(); + metadata.put(STATUS_DETAILS_KEY, statusProto); + return metadata; + } + + private static Metadata toMetadata(com.google.rpc.Status statusProto, Metadata metadata) { + checkNotNull(metadata, "metadata must not be null"); + metadata.discardAll(STATUS_DETAILS_KEY); + metadata.put(STATUS_DETAILS_KEY, statusProto); + return metadata; + } + + /** + * Extract a {@link com.google.rpc.Status} instance from the causal chain of a {@link Throwable}. + * + * @return the extracted {@link com.google.rpc.Status} instance, or {@code null} if none exists. + * @throws IllegalArgumentException if an embedded {@link com.google.rpc.Status} is found and its + * code does not match the gRPC {@link Status} code. + */ + @Nullable + public static com.google.rpc.Status fromThrowable(Throwable t) { + Throwable cause = checkNotNull(t, "t"); + while (cause != null) { + if (cause instanceof StatusException) { + StatusException e = (StatusException) cause; + return toStatusProto(e.getStatus(), e.getTrailers()); + } else if (cause instanceof StatusRuntimeException) { + StatusRuntimeException e = (StatusRuntimeException) cause; + return toStatusProto(e.getStatus(), e.getTrailers()); + } + cause = cause.getCause(); + } + return null; + } + + @Nullable + private static com.google.rpc.Status toStatusProto(Status status, Metadata trailers) { + if (trailers != null) { + com.google.rpc.Status statusProto = trailers.get(STATUS_DETAILS_KEY); + checkArgument( + status.getCode().value() == statusProto.getCode(), + "com.google.rpc.Status code must match gRPC status code"); + return statusProto; + } + return null; + } +} diff --git a/protobuf/src/test/java/io/grpc/protobuf/StatusProtoTest.java b/protobuf/src/test/java/io/grpc/protobuf/StatusProtoTest.java new file mode 100644 index 0000000000..8a085cbc56 --- /dev/null +++ b/protobuf/src/test/java/io/grpc/protobuf/StatusProtoTest.java @@ -0,0 +1,196 @@ +/* + * Copyright 2017, Google Inc. All rights reserved. + * + * 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 Inc. 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 io.grpc.protobuf; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.fail; + +import io.grpc.Metadata; +import io.grpc.Status; +import io.grpc.StatusException; +import io.grpc.StatusRuntimeException; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Unit tests for {@link StatusProto}. */ +@RunWith(JUnit4.class) +public class StatusProtoTest { + private Metadata metadata; + + @Before + public void setup() { + metadata = new Metadata(); + metadata.put(METADATA_KEY, METADATA_VALUE); + } + + @Test + public void toStatusRuntimeException() throws Exception { + StatusRuntimeException sre = StatusProto.toStatusRuntimeException(STATUS_PROTO); + com.google.rpc.Status extractedStatusProto = StatusProto.fromThrowable(sre); + + assertEquals(STATUS_PROTO.getCode(), sre.getStatus().getCode().value()); + assertEquals(STATUS_PROTO.getMessage(), sre.getStatus().getDescription()); + assertEquals(STATUS_PROTO, extractedStatusProto); + } + + @Test + public void toStatusRuntimeExceptionWithMetadata_shouldIncludeMetadata() throws Exception { + StatusRuntimeException sre = StatusProto.toStatusRuntimeException(STATUS_PROTO, metadata); + com.google.rpc.Status extractedStatusProto = StatusProto.fromThrowable(sre); + + assertEquals(STATUS_PROTO.getCode(), sre.getStatus().getCode().value()); + assertEquals(STATUS_PROTO.getMessage(), sre.getStatus().getDescription()); + assertEquals(STATUS_PROTO, extractedStatusProto); + assertNotNull(sre.getTrailers()); + assertEquals(METADATA_VALUE, sre.getTrailers().get(METADATA_KEY)); + } + + @Test + public void toStatusRuntimeExceptionWithMetadata_shouldThrowIfMetadataIsNull() throws Exception { + try { + StatusProto.toStatusRuntimeException(STATUS_PROTO, null); + fail("NullPointerException expected"); + } catch (NullPointerException npe) { + assertEquals("metadata must not be null", npe.getMessage()); + } + } + + @Test + public void toStatusRuntimeException_shouldThrowIfStatusCodeInvalid() throws Exception { + try { + StatusProto.toStatusRuntimeException(INVALID_STATUS_PROTO); + fail("IllegalArgumentException expected"); + } catch (IllegalArgumentException expectedException) { + assertEquals("invalid status code", expectedException.getMessage()); + } + } + + @Test + public void toStatusException() throws Exception { + StatusException se = StatusProto.toStatusException(STATUS_PROTO); + com.google.rpc.Status extractedStatusProto = StatusProto.fromThrowable(se); + + assertEquals(STATUS_PROTO.getCode(), se.getStatus().getCode().value()); + assertEquals(STATUS_PROTO.getMessage(), se.getStatus().getDescription()); + assertEquals(STATUS_PROTO, extractedStatusProto); + } + + @Test + public void toStatusExceptionWithMetadata_shouldIncludeMetadata() throws Exception { + StatusException se = StatusProto.toStatusException(STATUS_PROTO, metadata); + com.google.rpc.Status extractedStatusProto = StatusProto.fromThrowable(se); + + assertEquals(STATUS_PROTO.getCode(), se.getStatus().getCode().value()); + assertEquals(STATUS_PROTO.getMessage(), se.getStatus().getDescription()); + assertEquals(STATUS_PROTO, extractedStatusProto); + assertNotNull(se.getTrailers()); + assertEquals(METADATA_VALUE, se.getTrailers().get(METADATA_KEY)); + } + + @Test + public void toStatusExceptionWithMetadata_shouldThrowIfMetadataIsNull() throws Exception { + try { + StatusProto.toStatusException(STATUS_PROTO, null); + fail("NullPointerException expected"); + } catch (NullPointerException npe) { + assertEquals("metadata must not be null", npe.getMessage()); + } + } + + @Test + public void toStatusException_shouldThrowIfStatusCodeInvalid() throws Exception { + try { + StatusProto.toStatusException(INVALID_STATUS_PROTO); + fail("IllegalArgumentException expected"); + } catch (IllegalArgumentException expectedException) { + assertEquals("invalid status code", expectedException.getMessage()); + } + } + + @Test + public void fromThrowable_shouldReturnNullIfTrailersAreNull() { + Status status = Status.fromCodeValue(0); + + assertNull(StatusProto.fromThrowable(status.asRuntimeException())); + assertNull(StatusProto.fromThrowable(status.asException())); + } + + @Test + public void fromThrowableWithNestedStatusRuntimeException() { + StatusRuntimeException sre = StatusProto.toStatusRuntimeException(STATUS_PROTO); + Throwable nestedSre = new Throwable(sre); + + com.google.rpc.Status extractedStatusProto = StatusProto.fromThrowable(sre); + com.google.rpc.Status extractedStatusProtoFromNestedSre = StatusProto.fromThrowable(nestedSre); + + assertEquals(extractedStatusProto, extractedStatusProtoFromNestedSre); + } + + @Test + public void fromThrowableWithNestedStatusException() { + StatusException se = StatusProto.toStatusException(STATUS_PROTO); + Throwable nestedSe = new Throwable(se); + + com.google.rpc.Status extractedStatusProto = StatusProto.fromThrowable(se); + com.google.rpc.Status extractedStatusProtoFromNestedSe = StatusProto.fromThrowable(nestedSe); + + assertEquals(extractedStatusProto, extractedStatusProtoFromNestedSe); + } + + @Test + public void fromThrowable_shouldReturnNullIfNoEmbeddedStatus() { + Throwable nestedSe = new Throwable(new Throwable("no status found")); + + assertNull(StatusProto.fromThrowable(nestedSe)); + } + + private static final Metadata.Key METADATA_KEY = + Metadata.Key.of("test-metadata", Metadata.ASCII_STRING_MARSHALLER); + private static final String METADATA_VALUE = "test metadata value"; + private static final com.google.rpc.Status STATUS_PROTO = + com.google.rpc.Status.newBuilder() + .setCode(2) + .setMessage("status message") + .addDetails( + com.google.protobuf.Any.pack( + com.google.rpc.Status.newBuilder() + .setCode(13) + .setMessage("nested message") + .build())) + .build(); + private static final com.google.rpc.Status INVALID_STATUS_PROTO = + com.google.rpc.Status.newBuilder().setCode(-1).build(); +}