diff --git a/conjure-java-core/src/main/java/com/palantir/conjure/java/services/UndertowServiceHandlerGenerator.java b/conjure-java-core/src/main/java/com/palantir/conjure/java/services/UndertowServiceHandlerGenerator.java index 0d753a7bf..9469ceb74 100644 --- a/conjure-java-core/src/main/java/com/palantir/conjure/java/services/UndertowServiceHandlerGenerator.java +++ b/conjure-java-core/src/main/java/com/palantir/conjure/java/services/UndertowServiceHandlerGenerator.java @@ -289,17 +289,19 @@ private CodeBlock endpointInvocation(EndpointDefinition endpointDefinition, List // body parameter getBodyParamTypeArgument(endpointDefinition.getArgs()).ifPresent(bodyParam -> { + String paramName = bodyParam.getArgName().get(); if (bodyParam.getType().accept(TypeVisitor.IS_BINARY)) { // TODO(ckozak): Support aliased and optional binary types code.addStatement("$1T $2N = $3N.bodySerDe().deserializeInputStream($4N)", - InputStream.class, bodyParam.getArgName().get(), RUNTIME_VAR_NAME, EXCHANGE_VAR_NAME); + InputStream.class, paramName, RUNTIME_VAR_NAME, EXCHANGE_VAR_NAME); } else { code.addStatement("$1T $2N = $3N.deserialize($4N)", typeMapper.getClassName(bodyParam.getType()).box(), - bodyParam.getArgName().get(), + paramName, DESERIALIZER_VAR_NAME, EXCHANGE_VAR_NAME); } + code.add(generateParamMarkers(bodyParam.getMarkers(), paramName, typeMapper)); }); // path parameters @@ -368,6 +370,19 @@ private CodeBlock endpointInvocation(EndpointDefinition endpointDefinition, List return code.build(); } + private CodeBlock generateParamMarkers( + List markers, + String paramName, + TypeMapper typeMapper) { + return CodeBlocks.of(markers.stream().map(marker -> + CodeBlock.of("$1N.markers().param($2S, $3S, $4N, $5N);", + RUNTIME_VAR_NAME, + typeMapper.getClassName(marker).box(), paramName, + paramName, + EXCHANGE_VAR_NAME)) + .collect(Collectors.toList())); + } + // Adds code for authorization. Returns an optional that contains the name of the variable that contains the // deserialized optional parameter. private Optional addAuthCode( @@ -504,27 +519,32 @@ private CodeBlock generateParameterCodeBlock(Stream params, arg -> { Type normalizedType = UndertowTypeFunctions.toConjureTypeWithoutAliases(arg.getType(), typeDefinitions); + String paramName = arg.getArgName().get(); + final CodeBlock retrieveParam; if (normalizedType.equals(arg.getType())) { // type does not contain any aliases - return decodePlainParameterCodeBlock(normalizedType, typeMapper, arg.getArgName().get(), + retrieveParam = decodePlainParameterCodeBlock(normalizedType, typeMapper, paramName, paramsVarName, toParamId.apply(arg)); } else { // type contains aliases: decode raw value and then construct real value from raw one String rawVarName = arg.getArgName().get() + "Raw"; - return CodeBlocks.of( + retrieveParam = CodeBlocks.of( decodePlainParameterCodeBlock(normalizedType, typeMapper, rawVarName, paramsVarName, toParamId.apply(arg)), CodeBlocks.statement( "$1T $2N = $3L", typeMapper.getClassName(arg.getType()), - arg.getArgName().get(), + paramName, createConstructorForTypeWithReference(arg.getType(), rawVarName, typeDefinitions, typeMapper) ) ); } + return CodeBlocks.of( + retrieveParam, + generateParamMarkers(arg.getMarkers(), paramName, typeMapper)); }).collect(Collectors.toList())); } diff --git a/conjure-java-core/src/test/resources/example-service.yml b/conjure-java-core/src/test/resources/example-service.yml index ce92f9b0a..b6aacadfa 100644 --- a/conjure-java-core/src/test/resources/example-service.yml +++ b/conjure-java-core/src/test/resources/example-service.yml @@ -68,6 +68,8 @@ services: param-id: Test-Header param-type: header type: string + markers: + - Safe returns: Dataset getDataset: @@ -104,6 +106,7 @@ services: type: rid markers: - Safe + - Nonnull returns: optional getAliasedString: @@ -121,6 +124,8 @@ services: input: type: binary param-type: body + markers: + - Safe uploadAliasedRawData: http: POST /datasets/upload-raw-aliased @@ -170,6 +175,8 @@ services: type: rid param-id: datasetRid param-type: path + markers: + - Safe returns: optional testQueryParams: @@ -180,6 +187,8 @@ services: type: rid param-id: different param-type: query + markers: + - Safe optionalMiddle: type: optional param-type: query diff --git a/conjure-java-core/src/test/resources/test/api/TestService.java.jersey b/conjure-java-core/src/test/resources/test/api/TestService.java.jersey index 49927daa1..8d4921944 100644 --- a/conjure-java-core/src/test/resources/test/api/TestService.java.jersey +++ b/conjure-java-core/src/test/resources/test/api/TestService.java.jersey @@ -16,6 +16,7 @@ import java.util.OptionalDouble; import java.util.OptionalInt; import java.util.Set; import javax.annotation.Generated; +import javax.annotation.Nonnull; import javax.validation.constraints.NotNull; import javax.ws.rs.Consumes; import javax.ws.rs.GET; @@ -44,7 +45,7 @@ public interface TestService { @Path("catalog/datasets") Dataset createDataset( @HeaderParam("Authorization") @NotNull AuthHeader authHeader, - @HeaderParam("Test-Header") String testHeaderArg, + @HeaderParam("Test-Header") @Safe String testHeaderArg, @NotNull CreateDatasetRequest request); @GET @@ -72,7 +73,7 @@ public interface TestService { @Produces(MediaType.APPLICATION_OCTET_STREAM) Optional maybeGetRawData( @HeaderParam("Authorization") @NotNull AuthHeader authHeader, - @PathParam("datasetRid") @Safe ResourceIdentifier datasetRid); + @PathParam("datasetRid") @Safe @Nonnull ResourceIdentifier datasetRid); @GET @Path("catalog/datasets/{datasetRid}/string-aliased") @@ -85,7 +86,7 @@ public interface TestService { @Consumes(MediaType.APPLICATION_OCTET_STREAM) void uploadRawData( @HeaderParam("Authorization") @NotNull AuthHeader authHeader, - @NotNull InputStream input); + @NotNull @Safe InputStream input); @POST @Path("catalog/datasets/upload-raw-aliased") @@ -122,13 +123,13 @@ public interface TestService { @Path("catalog/datasets/{datasetRid}/testParam") Optional testParam( @HeaderParam("Authorization") @NotNull AuthHeader authHeader, - @PathParam("datasetRid") ResourceIdentifier datasetRid); + @PathParam("datasetRid") @Safe ResourceIdentifier datasetRid); @POST @Path("catalog/test-query-params") int testQueryParams( @HeaderParam("Authorization") @NotNull AuthHeader authHeader, - @QueryParam("different") ResourceIdentifier something, + @QueryParam("different") @Safe ResourceIdentifier something, @QueryParam("implicit") ResourceIdentifier implicit, @QueryParam("optionalMiddle") Optional optionalMiddle, @QueryParam("setEnd") Set setEnd, diff --git a/conjure-java-core/src/test/resources/test/api/TestService.java.jersey_require_not_null b/conjure-java-core/src/test/resources/test/api/TestService.java.jersey_require_not_null index 49927daa1..8d4921944 100644 --- a/conjure-java-core/src/test/resources/test/api/TestService.java.jersey_require_not_null +++ b/conjure-java-core/src/test/resources/test/api/TestService.java.jersey_require_not_null @@ -16,6 +16,7 @@ import java.util.OptionalDouble; import java.util.OptionalInt; import java.util.Set; import javax.annotation.Generated; +import javax.annotation.Nonnull; import javax.validation.constraints.NotNull; import javax.ws.rs.Consumes; import javax.ws.rs.GET; @@ -44,7 +45,7 @@ public interface TestService { @Path("catalog/datasets") Dataset createDataset( @HeaderParam("Authorization") @NotNull AuthHeader authHeader, - @HeaderParam("Test-Header") String testHeaderArg, + @HeaderParam("Test-Header") @Safe String testHeaderArg, @NotNull CreateDatasetRequest request); @GET @@ -72,7 +73,7 @@ public interface TestService { @Produces(MediaType.APPLICATION_OCTET_STREAM) Optional maybeGetRawData( @HeaderParam("Authorization") @NotNull AuthHeader authHeader, - @PathParam("datasetRid") @Safe ResourceIdentifier datasetRid); + @PathParam("datasetRid") @Safe @Nonnull ResourceIdentifier datasetRid); @GET @Path("catalog/datasets/{datasetRid}/string-aliased") @@ -85,7 +86,7 @@ public interface TestService { @Consumes(MediaType.APPLICATION_OCTET_STREAM) void uploadRawData( @HeaderParam("Authorization") @NotNull AuthHeader authHeader, - @NotNull InputStream input); + @NotNull @Safe InputStream input); @POST @Path("catalog/datasets/upload-raw-aliased") @@ -122,13 +123,13 @@ public interface TestService { @Path("catalog/datasets/{datasetRid}/testParam") Optional testParam( @HeaderParam("Authorization") @NotNull AuthHeader authHeader, - @PathParam("datasetRid") ResourceIdentifier datasetRid); + @PathParam("datasetRid") @Safe ResourceIdentifier datasetRid); @POST @Path("catalog/test-query-params") int testQueryParams( @HeaderParam("Authorization") @NotNull AuthHeader authHeader, - @QueryParam("different") ResourceIdentifier something, + @QueryParam("different") @Safe ResourceIdentifier something, @QueryParam("implicit") ResourceIdentifier implicit, @QueryParam("optionalMiddle") Optional optionalMiddle, @QueryParam("setEnd") Set setEnd, diff --git a/conjure-java-core/src/test/resources/test/api/TestServiceEndpoints.java.undertow b/conjure-java-core/src/test/resources/test/api/TestServiceEndpoints.java.undertow index 928af237f..5e3914cf4 100644 --- a/conjure-java-core/src/test/resources/test/api/TestServiceEndpoints.java.undertow +++ b/conjure-java-core/src/test/resources/test/api/TestServiceEndpoints.java.undertow @@ -144,6 +144,8 @@ public final class TestServiceEndpoints implements UndertowService { HeaderMap headerParams = exchange.getRequestHeaders(); String testHeaderArg = runtime.plainSerDe().deserializeString(headerParams.get("Test-Header")); + runtime.markers() + .param("com.palantir.redaction.Safe", "testHeaderArg", testHeaderArg, exchange); Dataset result = delegate.createDataset(authHeader, testHeaderArg, request); serializer.serialize(result, exchange); } @@ -195,6 +197,8 @@ public final class TestServiceEndpoints implements UndertowService { exchange.getAttachment(PathTemplateMatch.ATTACHMENT_KEY).getParameters(); ResourceIdentifier datasetRid = runtime.plainSerDe().deserializeRid(pathParams.get("datasetRid")); + runtime.markers() + .param("com.palantir.redaction.Safe", "datasetRid", datasetRid, exchange); Optional result = delegate.getDataset(authHeader, datasetRid); if (result.isPresent()) { serializer.serialize(result, exchange); @@ -246,6 +250,8 @@ public final class TestServiceEndpoints implements UndertowService { exchange.getAttachment(PathTemplateMatch.ATTACHMENT_KEY).getParameters(); ResourceIdentifier datasetRid = runtime.plainSerDe().deserializeRid(pathParams.get("datasetRid")); + runtime.markers() + .param("com.palantir.redaction.Safe", "datasetRid", datasetRid, exchange); BinaryResponseBody result = delegate.getRawData(authHeader, datasetRid); runtime.bodySerDe().serialize(result, exchange); } @@ -297,6 +303,8 @@ public final class TestServiceEndpoints implements UndertowService { exchange.getAttachment(PathTemplateMatch.ATTACHMENT_KEY).getParameters(); ResourceIdentifier datasetRid = runtime.plainSerDe().deserializeRid(pathParams.get("datasetRid")); + runtime.markers() + .param("com.palantir.redaction.Safe", "datasetRid", datasetRid, exchange); NestedAliasedBinary result = delegate.getAliasedRawData(authHeader, datasetRid); serializer.serialize(result, exchange); } @@ -344,6 +352,9 @@ public final class TestServiceEndpoints implements UndertowService { exchange.getAttachment(PathTemplateMatch.ATTACHMENT_KEY).getParameters(); ResourceIdentifier datasetRid = runtime.plainSerDe().deserializeRid(pathParams.get("datasetRid")); + runtime.markers() + .param("com.palantir.redaction.Safe", "datasetRid", datasetRid, exchange); + runtime.markers().param("javax.annotation.Nonnull", "datasetRid", datasetRid, exchange); Optional result = delegate.maybeGetRawData(authHeader, datasetRid); if (result.isPresent()) { runtime.bodySerDe().serialize(result.get(), exchange); @@ -398,6 +409,8 @@ public final class TestServiceEndpoints implements UndertowService { exchange.getAttachment(PathTemplateMatch.ATTACHMENT_KEY).getParameters(); ResourceIdentifier datasetRid = runtime.plainSerDe().deserializeRid(pathParams.get("datasetRid")); + runtime.markers() + .param("com.palantir.redaction.Safe", "datasetRid", datasetRid, exchange); AliasedString result = delegate.getAliasedString(authHeader, datasetRid); serializer.serialize(result, exchange); } @@ -442,6 +455,7 @@ public final class TestServiceEndpoints implements UndertowService { public void handleRequest(HttpServerExchange exchange) throws IOException { AuthHeader authHeader = runtime.auth().header(exchange); InputStream input = runtime.bodySerDe().deserializeInputStream(exchange); + runtime.markers().param("com.palantir.redaction.Safe", "input", input, exchange); delegate.uploadRawData(authHeader, input); exchange.setStatusCode(StatusCodes.NO_CONTENT); } @@ -540,6 +554,8 @@ public final class TestServiceEndpoints implements UndertowService { exchange.getAttachment(PathTemplateMatch.ATTACHMENT_KEY).getParameters(); ResourceIdentifier datasetRid = runtime.plainSerDe().deserializeRid(pathParams.get("datasetRid")); + runtime.markers() + .param("com.palantir.redaction.Safe", "datasetRid", datasetRid, exchange); Set result = delegate.getBranches(authHeader, datasetRid); serializer.serialize(result, exchange); } @@ -591,6 +607,8 @@ public final class TestServiceEndpoints implements UndertowService { exchange.getAttachment(PathTemplateMatch.ATTACHMENT_KEY).getParameters(); ResourceIdentifier datasetRid = runtime.plainSerDe().deserializeRid(pathParams.get("datasetRid")); + runtime.markers() + .param("com.palantir.redaction.Safe", "datasetRid", datasetRid, exchange); Set result = delegate.getBranchesDeprecated(authHeader, datasetRid); serializer.serialize(result, exchange); } @@ -641,6 +659,8 @@ public final class TestServiceEndpoints implements UndertowService { exchange.getAttachment(PathTemplateMatch.ATTACHMENT_KEY).getParameters(); ResourceIdentifier datasetRid = runtime.plainSerDe().deserializeRid(pathParams.get("datasetRid")); + runtime.markers() + .param("com.palantir.redaction.Safe", "datasetRid", datasetRid, exchange); String branch = runtime.plainSerDe().deserializeString(pathParams.get("branch")); Optional result = delegate.resolveBranch(authHeader, datasetRid, branch); if (result.isPresent()) { @@ -696,6 +716,8 @@ public final class TestServiceEndpoints implements UndertowService { exchange.getAttachment(PathTemplateMatch.ATTACHMENT_KEY).getParameters(); ResourceIdentifier datasetRid = runtime.plainSerDe().deserializeRid(pathParams.get("datasetRid")); + runtime.markers() + .param("com.palantir.redaction.Safe", "datasetRid", datasetRid, exchange); Optional result = delegate.testParam(authHeader, datasetRid); if (result.isPresent()) { serializer.serialize(result, exchange); @@ -753,6 +775,8 @@ public final class TestServiceEndpoints implements UndertowService { Map> queryParams = exchange.getQueryParameters(); ResourceIdentifier something = runtime.plainSerDe().deserializeRid(queryParams.get("different")); + runtime.markers() + .param("com.palantir.redaction.Safe", "something", something, exchange); Optional optionalMiddle = runtime.plainSerDe().deserializeOptionalRid(queryParams.get("optionalMiddle")); ResourceIdentifier implicit = diff --git a/conjure-java-undertow-runtime/src/main/java/com/palantir/conjure/java/undertow/runtime/ConjureUndertowRuntime.java b/conjure-java-undertow-runtime/src/main/java/com/palantir/conjure/java/undertow/runtime/ConjureUndertowRuntime.java index f9670609c..259023a2f 100644 --- a/conjure-java-undertow-runtime/src/main/java/com/palantir/conjure/java/undertow/runtime/ConjureUndertowRuntime.java +++ b/conjure-java-undertow-runtime/src/main/java/com/palantir/conjure/java/undertow/runtime/ConjureUndertowRuntime.java @@ -21,6 +21,7 @@ import com.google.errorprone.annotations.CanIgnoreReturnValue; import com.palantir.conjure.java.undertow.lib.AuthorizationExtractor; import com.palantir.conjure.java.undertow.lib.BodySerDe; +import com.palantir.conjure.java.undertow.lib.MarkerCallback; import com.palantir.conjure.java.undertow.lib.PlainSerDe; import com.palantir.conjure.java.undertow.lib.UndertowRuntime; import com.palantir.logsafe.Preconditions; @@ -33,11 +34,16 @@ public final class ConjureUndertowRuntime implements UndertowRuntime { private final BodySerDe bodySerDe; private final AuthorizationExtractor auth; + private final MarkerCallback markerCallback; private ConjureUndertowRuntime(Builder builder) { this.bodySerDe = new ConjureBodySerDe(builder.encodings.isEmpty() ? ImmutableList.of(Encodings.json(), Encodings.cbor()) : builder.encodings); this.auth = new ConjureAuthorizationExtractor(plainSerDe()); + List paramMarkers = ImmutableList.copyOf(builder.paramMarkers); + this.markerCallback = (markerClass, parameterName, parameterValue, exchange) -> + paramMarkers.forEach(marked -> marked.mark(markerClass, parameterName, parameterValue, exchange)); + } public static Builder builder() { @@ -54,6 +60,12 @@ public PlainSerDe plainSerDe() { return ConjurePlainSerDe.INSTANCE; } + + @Override + public MarkerCallback markers() { + return markerCallback; + } + @Override public AuthorizationExtractor auth() { return auth; @@ -62,6 +74,7 @@ public AuthorizationExtractor auth() { public static final class Builder { private final List encodings = Lists.newArrayList(); + private final List paramMarkers = Lists.newArrayList(); private Builder() {} @@ -71,6 +84,12 @@ public Builder encodings(Encoding value) { return this; } + @CanIgnoreReturnValue + public Builder paramMarker(ParamMarker value) { + paramMarkers.add(Preconditions.checkNotNull(value, "Value is required")); + return this; + } + public ConjureUndertowRuntime build() { return new ConjureUndertowRuntime(this); } diff --git a/conjure-java-undertow-runtime/src/main/java/com/palantir/conjure/java/undertow/runtime/ParamMarker.java b/conjure-java-undertow-runtime/src/main/java/com/palantir/conjure/java/undertow/runtime/ParamMarker.java new file mode 100644 index 000000000..4daf5f800 --- /dev/null +++ b/conjure-java-undertow-runtime/src/main/java/com/palantir/conjure/java/undertow/runtime/ParamMarker.java @@ -0,0 +1,27 @@ +/* + * (c) Copyright 2019 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.conjure.java.undertow.runtime; + +import io.undertow.server.HttpServerExchange; + +/** + * execute arbitrary actions given a marker on a param. + */ +public interface ParamMarker { + + void mark(String markerClass, String parameterName, Object parameterValue, HttpServerExchange exchange); +} diff --git a/conjure-java-undertow-runtime/src/test/java/com/palantir/conjure/java/undertow/runtime/MarkerCallbackTest.java b/conjure-java-undertow-runtime/src/test/java/com/palantir/conjure/java/undertow/runtime/MarkerCallbackTest.java new file mode 100644 index 000000000..e5739aeff --- /dev/null +++ b/conjure-java-undertow-runtime/src/test/java/com/palantir/conjure/java/undertow/runtime/MarkerCallbackTest.java @@ -0,0 +1,50 @@ +/* + * (c) Copyright 2019 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.conjure.java.undertow.runtime; + +import com.palantir.conjure.java.undertow.HttpServerExchanges; +import com.palantir.conjure.java.undertow.lib.UndertowRuntime; +import io.undertow.server.HttpServerExchange; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class MarkerCallbackTest { + + @Mock + private ParamMarker paramMarker; + private HttpServerExchange exchange; + private UndertowRuntime runtime; + + @Before + public void before() { + exchange = HttpServerExchanges.createStub(); + runtime = ConjureUndertowRuntime.builder() + .paramMarker(paramMarker) + .build(); + } + + @Test + public void testMarkersIsCalled() { + runtime.markers().param("fully.qualified.Path", "foo", "bar", exchange); + Mockito.verify(paramMarker).mark("fully.qualified.Path", "foo", "bar", exchange); + } +} diff --git a/conjure-undertow-lib/src/main/java/com/palantir/conjure/java/undertow/lib/MarkerCallback.java b/conjure-undertow-lib/src/main/java/com/palantir/conjure/java/undertow/lib/MarkerCallback.java new file mode 100644 index 000000000..bee71d46c --- /dev/null +++ b/conjure-undertow-lib/src/main/java/com/palantir/conjure/java/undertow/lib/MarkerCallback.java @@ -0,0 +1,24 @@ +/* + * (c) Copyright 2019 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.conjure.java.undertow.lib; + +import io.undertow.server.HttpServerExchange; + +public interface MarkerCallback { + + void param(String markerClass, String parameterName, Object parameterValue, HttpServerExchange exchange); +} diff --git a/conjure-undertow-lib/src/main/java/com/palantir/conjure/java/undertow/lib/UndertowRuntime.java b/conjure-undertow-lib/src/main/java/com/palantir/conjure/java/undertow/lib/UndertowRuntime.java index 9aad4ba3a..784e79947 100644 --- a/conjure-undertow-lib/src/main/java/com/palantir/conjure/java/undertow/lib/UndertowRuntime.java +++ b/conjure-undertow-lib/src/main/java/com/palantir/conjure/java/undertow/lib/UndertowRuntime.java @@ -32,4 +32,7 @@ public interface UndertowRuntime { /** Provides the {@link AuthorizationExtractor} used to read auth tokens from request headers. */ AuthorizationExtractor auth(); + + /** Provides the {@link MarkerCallback} to execute arbitrary actions given a marker on a param. */ + MarkerCallback markers(); }