From 6ef619afe4081792a6136dc67882ca1febe06377 Mon Sep 17 00:00:00 2001 From: Alexis Le Dantec Date: Fri, 17 Nov 2023 15:15:10 +0100 Subject: [PATCH] Add native support for InputStream responses to dialogue annotations processor --- .../palantir/myservice/example/MyService.java | 4 + .../processor/data/ReturnType.java | 13 +++ .../processor/data/ReturnTypesResolver.java | 1 + .../ServiceImplementationGenerator.java | 28 ++++-- .../CustomInputStreamDeserializer.java | 33 +++++++ .../palantir/myservice/service/MyService.java | 7 ++ ...rviceDialogueServiceFactory.java.generated | 88 +++++++++++++++++++ .../ErrorHandlingDeserializer.java | 49 +++++++++++ .../ErrorHandlingVoidDeserializer.java | 27 +----- 9 files changed, 218 insertions(+), 32 deletions(-) create mode 100644 dialogue-annotations-processor/src/test/java/com/palantir/myservice/service/CustomInputStreamDeserializer.java create mode 100644 dialogue-annotations/src/main/java/com/palantir/dialogue/annotations/ErrorHandlingDeserializer.java diff --git a/dialogue-annotations-example/src/main/java/com/palantir/myservice/example/MyService.java b/dialogue-annotations-example/src/main/java/com/palantir/myservice/example/MyService.java index f9fa05d93..0626f6756 100644 --- a/dialogue-annotations-example/src/main/java/com/palantir/myservice/example/MyService.java +++ b/dialogue-annotations-example/src/main/java/com/palantir/myservice/example/MyService.java @@ -27,6 +27,7 @@ import com.palantir.dialogue.annotations.MapToMultimapParamEncoder; import com.palantir.dialogue.annotations.Request; import com.palantir.myservice.example.PutFileRequest.PutFileRequestSerializer; +import java.io.InputStream; import java.util.List; import java.util.Map; import java.util.Optional; @@ -47,6 +48,9 @@ String greet( @Request(method = HttpMethod.GET, path = "/greeting", accept = CustomStringDeserializer.class) ListenableFuture getGreetingAsync(); + @Request(method = HttpMethod.GET, path = "/input-stream") + InputStream inputStream(); + // No decoders allowed (void method) // No encoders allowed (RequestBody is pre-encoded) @Request(method = HttpMethod.PUT, path = "/custom/request") diff --git a/dialogue-annotations-processor/src/main/java/com/palantir/dialogue/annotations/processor/data/ReturnType.java b/dialogue-annotations-processor/src/main/java/com/palantir/dialogue/annotations/processor/data/ReturnType.java index c050ba017..5c79fabe0 100644 --- a/dialogue-annotations-processor/src/main/java/com/palantir/dialogue/annotations/processor/data/ReturnType.java +++ b/dialogue-annotations-processor/src/main/java/com/palantir/dialogue/annotations/processor/data/ReturnType.java @@ -16,7 +16,9 @@ package com.palantir.dialogue.annotations.processor.data; +import com.squareup.javapoet.ClassName; import com.squareup.javapoet.TypeName; +import java.io.InputStream; import java.util.Optional; import org.immutables.value.Value; @@ -27,6 +29,11 @@ public interface ReturnType { TypeName deserializerFactory(); + @Value.Default + default boolean isUsingCustomDeserializer() { + return false; + } + TypeName errorDecoder(); String deserializerFieldName(); @@ -38,4 +45,10 @@ default boolean isVoid() { TypeName type = asyncInnerType().orElseGet(this::returnType); return type.box().equals(TypeName.VOID.box()); } + + @Value.Derived + default boolean isInputStream() { + TypeName type = asyncInnerType().orElseGet(this::returnType); + return type.equals(ClassName.get(InputStream.class)); + } } diff --git a/dialogue-annotations-processor/src/main/java/com/palantir/dialogue/annotations/processor/data/ReturnTypesResolver.java b/dialogue-annotations-processor/src/main/java/com/palantir/dialogue/annotations/processor/data/ReturnTypesResolver.java index 70ecc519b..b4a52f26c 100644 --- a/dialogue-annotations-processor/src/main/java/com/palantir/dialogue/annotations/processor/data/ReturnTypesResolver.java +++ b/dialogue-annotations-processor/src/main/java/com/palantir/dialogue/annotations/processor/data/ReturnTypesResolver.java @@ -67,6 +67,7 @@ public Optional getReturnType( .orElseGet(() -> context.getTypeName(ConjureErrorDecoder.class))) .deserializerFieldName(InstanceVariables.joinCamelCase(endpointName.get(), "Deserializer")) .asyncInnerType(maybeListenableFutureInnerType.map(TypeName::get)) + .isUsingCustomDeserializer(maybeAcceptDeserializerFactory.isPresent()) .build()); } diff --git a/dialogue-annotations-processor/src/main/java/com/palantir/dialogue/annotations/processor/generate/ServiceImplementationGenerator.java b/dialogue-annotations-processor/src/main/java/com/palantir/dialogue/annotations/processor/generate/ServiceImplementationGenerator.java index a76afe501..d9510adce 100644 --- a/dialogue-annotations-processor/src/main/java/com/palantir/dialogue/annotations/processor/generate/ServiceImplementationGenerator.java +++ b/dialogue-annotations-processor/src/main/java/com/palantir/dialogue/annotations/processor/generate/ServiceImplementationGenerator.java @@ -22,6 +22,7 @@ import com.palantir.dialogue.Serializer; import com.palantir.dialogue.TypeMarker; import com.palantir.dialogue.annotations.DefaultParameterSerializer; +import com.palantir.dialogue.annotations.ErrorHandlingDeserializer; import com.palantir.dialogue.annotations.ErrorHandlingDeserializerFactory; import com.palantir.dialogue.annotations.ErrorHandlingVoidDeserializer; import com.palantir.dialogue.annotations.ParameterSerializer; @@ -166,21 +167,34 @@ private Optional deserializer(ReturnType type) { ParameterizedTypeName deserializerType = ParameterizedTypeName.get(ClassName.get(Deserializer.class), innerType); - CodeBlock realDeserializer = CodeBlock.of( + CodeBlock deserializer = CodeBlock.of( "new $T<>(new $T(), new $T()).deserializerFor(new $T<$T>() {})", ErrorHandlingDeserializerFactory.class, deserializerFactoryType, errorDecoderType, TypeMarker.class, innerType); - CodeBlock voidDeserializer = CodeBlock.of( - "new $T($L.bodySerDe().emptyBodyDeserializer(), new $T())", - ErrorHandlingVoidDeserializer.class, - serviceDefinition.conjureRuntimeArgName(), - errorDecoderType); + + // If no custom deserializer is used, use the runtime-provided deserializers for specific types + if (!type.isUsingCustomDeserializer()) { + if (type.isVoid()) { + deserializer = CodeBlock.of( + "new $T($L.bodySerDe().emptyBodyDeserializer(), new $T())", + ErrorHandlingVoidDeserializer.class, + serviceDefinition.conjureRuntimeArgName(), + errorDecoderType); + } else if (type.isInputStream()) { + deserializer = CodeBlock.of( + "new $T<>($L.bodySerDe().inputStreamDeserializer(), new $T())", + ErrorHandlingDeserializer.class, + serviceDefinition.conjureRuntimeArgName(), + errorDecoderType); + } + } + return Optional.of(FieldSpec.builder(deserializerType, type.deserializerFieldName()) .addModifiers(Modifier.PRIVATE, Modifier.FINAL) - .initializer(type.isVoid() ? voidDeserializer : realDeserializer) + .initializer(deserializer) .build()); } diff --git a/dialogue-annotations-processor/src/test/java/com/palantir/myservice/service/CustomInputStreamDeserializer.java b/dialogue-annotations-processor/src/test/java/com/palantir/myservice/service/CustomInputStreamDeserializer.java new file mode 100644 index 000000000..460a58411 --- /dev/null +++ b/dialogue-annotations-processor/src/test/java/com/palantir/myservice/service/CustomInputStreamDeserializer.java @@ -0,0 +1,33 @@ +/* + * (c) Copyright 2021 Palantir Technologies Inc. All rights reserved. + * + * 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.palantir.myservice.service; + +import com.palantir.dialogue.Response; +import com.palantir.dialogue.annotations.StdDeserializer; +import java.io.InputStream; + +public final class CustomInputStreamDeserializer extends StdDeserializer { + + public CustomInputStreamDeserializer() { + super("application/octet-stream"); + } + + @Override + public InputStream deserialize(Response response) { + return response.body(); + } +} diff --git a/dialogue-annotations-processor/src/test/java/com/palantir/myservice/service/MyService.java b/dialogue-annotations-processor/src/test/java/com/palantir/myservice/service/MyService.java index f74cddb65..56bf0b748 100644 --- a/dialogue-annotations-processor/src/test/java/com/palantir/myservice/service/MyService.java +++ b/dialogue-annotations-processor/src/test/java/com/palantir/myservice/service/MyService.java @@ -22,6 +22,7 @@ import com.palantir.dialogue.Response; import com.palantir.dialogue.annotations.Request; import com.palantir.tokens.auth.AuthHeader; +import java.io.InputStream; import java.util.List; import java.util.Optional; import java.util.OptionalDouble; @@ -47,6 +48,12 @@ String greet( @Request(method = HttpMethod.GET, path = "/greeting", accept = CustomStringDeserializer.class) ListenableFuture getGreetingAsync(); + @Request(method = HttpMethod.GET, path = "/input-stream") + InputStream inputStream(); + + @Request(method = HttpMethod.GET, path = "/input-stream-custom", accept = CustomInputStreamDeserializer.class) + InputStream customInputStream(); + // No decoders allowed (void method) // No encoders allowed (RequestBody is pre-encoded) @Request(method = HttpMethod.PUT, path = "/custom/request") diff --git a/dialogue-annotations-processor/src/test/resources/com/palantir/myservice/service/MyServiceDialogueServiceFactory.java.generated b/dialogue-annotations-processor/src/test/resources/com/palantir/myservice/service/MyServiceDialogueServiceFactory.java.generated index 40d089cdc..883b20857 100644 --- a/dialogue-annotations-processor/src/test/resources/com/palantir/myservice/service/MyServiceDialogueServiceFactory.java.generated +++ b/dialogue-annotations-processor/src/test/resources/com/palantir/myservice/service/MyServiceDialogueServiceFactory.java.generated @@ -17,12 +17,14 @@ import com.palantir.dialogue.TypeMarker; import com.palantir.dialogue.UrlBuilder; import com.palantir.dialogue.annotations.ConjureErrorDecoder; import com.palantir.dialogue.annotations.DefaultParameterSerializer; +import com.palantir.dialogue.annotations.ErrorHandlingDeserializer; import com.palantir.dialogue.annotations.ErrorHandlingDeserializerFactory; import com.palantir.dialogue.annotations.ErrorHandlingVoidDeserializer; import com.palantir.dialogue.annotations.Json; import com.palantir.dialogue.annotations.ParameterSerializer; import com.palantir.dialogue.annotations.ResponseDeserializer; import com.palantir.tokens.auth.AuthHeader; +import java.io.InputStream; import java.lang.Override; import java.lang.String; import java.lang.Void; @@ -57,6 +59,19 @@ public final class MyServiceDialogueServiceFactory implements DialogueServiceFac new CustomStringDeserializer(), new ConjureErrorDecoder()) .deserializerFor(new TypeMarker() {}); + private final EndpointChannel inputStreamChannel = endpointChannelFactory.endpoint(Endpoints.inputStream); + + private final Deserializer inputStreamDeserializer = new ErrorHandlingDeserializer<>( + runtime.bodySerDe().inputStreamDeserializer(), new ConjureErrorDecoder()); + + private final EndpointChannel customInputStreamChannel = + endpointChannelFactory.endpoint(Endpoints.customInputStream); + + private final Deserializer customInputStreamDeserializer = + new ErrorHandlingDeserializerFactory<>( + new CustomInputStreamDeserializer(), new ConjureErrorDecoder()) + .deserializerFor(new TypeMarker() {}); + private final EndpointChannel customRequestChannel = endpointChannelFactory.endpoint(Endpoints.customRequest); @@ -110,6 +125,19 @@ public final class MyServiceDialogueServiceFactory implements DialogueServiceFac return runtime.clients().call(getGreetingAsyncChannel, _request.build(), getGreetingAsyncDeserializer); } + @Override + public InputStream inputStream() { + Request.Builder _request = Request.builder(); + return runtime.clients().callBlocking(inputStreamChannel, _request.build(), inputStreamDeserializer); + } + + @Override + public InputStream customInputStream() { + Request.Builder _request = Request.builder(); + return runtime.clients() + .callBlocking(customInputStreamChannel, _request.build(), customInputStreamDeserializer); + } + @Override public void customRequest(EmptyRequestBody requestBody) { Request.Builder _request = Request.builder(); @@ -275,6 +303,66 @@ public final class MyServiceDialogueServiceFactory implements DialogueServiceFac } }, + inputStream { + private final PathTemplate pathTemplate = + PathTemplate.builder().fixed("input-stream").build(); + + @Override + public void renderPath(ListMultimap params, UrlBuilder url) { + pathTemplate.fill(params, url); + } + + @Override + public HttpMethod httpMethod() { + return HttpMethod.GET; + } + + @Override + public String serviceName() { + return "MyService"; + } + + @Override + public String endpointName() { + return "inputStream"; + } + + @Override + public String version() { + return VERSION; + } + }, + + customInputStream { + private final PathTemplate pathTemplate = + PathTemplate.builder().fixed("input-stream-custom").build(); + + @Override + public void renderPath(ListMultimap params, UrlBuilder url) { + pathTemplate.fill(params, url); + } + + @Override + public HttpMethod httpMethod() { + return HttpMethod.GET; + } + + @Override + public String serviceName() { + return "MyService"; + } + + @Override + public String endpointName() { + return "customInputStream"; + } + + @Override + public String version() { + return VERSION; + } + }, + customRequest { private final PathTemplate pathTemplate = PathTemplate.builder().fixed("custom").fixed("request").build(); diff --git a/dialogue-annotations/src/main/java/com/palantir/dialogue/annotations/ErrorHandlingDeserializer.java b/dialogue-annotations/src/main/java/com/palantir/dialogue/annotations/ErrorHandlingDeserializer.java new file mode 100644 index 000000000..e48fa4fd4 --- /dev/null +++ b/dialogue-annotations/src/main/java/com/palantir/dialogue/annotations/ErrorHandlingDeserializer.java @@ -0,0 +1,49 @@ +/* + * (c) Copyright 2021 Palantir Technologies Inc. All rights reserved. + * + * 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.palantir.dialogue.annotations; + +import com.palantir.dialogue.Deserializer; +import com.palantir.dialogue.Response; +import com.palantir.logsafe.Preconditions; +import java.util.Optional; + +public class ErrorHandlingDeserializer implements Deserializer { + + private final Deserializer delegate; + private final ErrorDecoder errorDecoder; + + public ErrorHandlingDeserializer(Deserializer delegate, ErrorDecoder errorDecoder) { + this.delegate = Preconditions.checkNotNull(delegate, "delegate"); + this.errorDecoder = Preconditions.checkNotNull(errorDecoder, "errorDecoder"); + } + + @Override + public final T deserialize(Response response) { + try (response) { + if (errorDecoder.isError(response)) { + throw errorDecoder.decode(response); + } else { + return delegate.deserialize(response); + } + } + } + + @Override + public final Optional accepts() { + return delegate.accepts(); + } +} diff --git a/dialogue-annotations/src/main/java/com/palantir/dialogue/annotations/ErrorHandlingVoidDeserializer.java b/dialogue-annotations/src/main/java/com/palantir/dialogue/annotations/ErrorHandlingVoidDeserializer.java index 96fc52c6d..7e74ad976 100644 --- a/dialogue-annotations/src/main/java/com/palantir/dialogue/annotations/ErrorHandlingVoidDeserializer.java +++ b/dialogue-annotations/src/main/java/com/palantir/dialogue/annotations/ErrorHandlingVoidDeserializer.java @@ -17,33 +17,10 @@ package com.palantir.dialogue.annotations; import com.palantir.dialogue.Deserializer; -import com.palantir.dialogue.Response; -import com.palantir.logsafe.Preconditions; -import java.util.Optional; -public final class ErrorHandlingVoidDeserializer implements Deserializer { - - private final Deserializer delegate; - private final ErrorDecoder errorDecoder; +public final class ErrorHandlingVoidDeserializer extends ErrorHandlingDeserializer { public ErrorHandlingVoidDeserializer(Deserializer delegate, ErrorDecoder errorDecoder) { - this.delegate = Preconditions.checkNotNull(delegate, "delegate"); - this.errorDecoder = Preconditions.checkNotNull(errorDecoder, "errorDecoder"); - } - - @Override - public Void deserialize(Response response) { - try (response) { - if (errorDecoder.isError(response)) { - throw errorDecoder.decode(response); - } else { - return delegate.deserialize(response); - } - } - } - - @Override - public Optional accepts() { - return delegate.accepts(); + super(delegate, errorDecoder); } }