From 5c5fbcab46175d2e28537431a2b342aa930f3253 Mon Sep 17 00:00:00 2001 From: Spencer Gibb Date: Mon, 16 Jan 2017 14:33:06 -0700 Subject: [PATCH] Adds AddRequestParameter --- .../config/GatewayAutoConfiguration.java | 6 + .../route/AddRequestParameterRouteFilter.java | 138 ++++++++++++++++++ .../gateway/test/GatewayIntegrationTests.java | 53 +++++-- src/test/resources/application.yml | 9 ++ 4 files changed, 195 insertions(+), 11 deletions(-) create mode 100644 src/main/java/org/springframework/cloud/gateway/filter/route/AddRequestParameterRouteFilter.java diff --git a/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java b/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java index e3d46b332..84bffcc0d 100644 --- a/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java +++ b/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java @@ -13,6 +13,7 @@ import org.springframework.cloud.gateway.filter.GlobalFilter; import org.springframework.cloud.gateway.filter.RouteToRequestUrlFilter; import org.springframework.cloud.gateway.filter.WriteResponseFilter; import org.springframework.cloud.gateway.filter.route.AddRequestHeaderRouteFilter; +import org.springframework.cloud.gateway.filter.route.AddRequestParameterRouteFilter; import org.springframework.cloud.gateway.filter.route.AddResponseHeaderRouteFilter; import org.springframework.cloud.gateway.filter.route.RemoveRequestHeaderRouteFilter; import org.springframework.cloud.gateway.filter.route.RemoveResponseHeaderRouteFilter; @@ -130,6 +131,11 @@ public class GatewayAutoConfiguration { return new AddRequestHeaderRouteFilter(); } + @Bean(name = "AddRequestParameterRouteFilter") + public AddRequestParameterRouteFilter addRequestParameterRouteFilter() { + return new AddRequestParameterRouteFilter(); + } + @Bean(name = "AddResponseHeaderRouteFilter") public AddResponseHeaderRouteFilter addResponseHeaderRouteFilter() { return new AddResponseHeaderRouteFilter(); diff --git a/src/main/java/org/springframework/cloud/gateway/filter/route/AddRequestParameterRouteFilter.java b/src/main/java/org/springframework/cloud/gateway/filter/route/AddRequestParameterRouteFilter.java new file mode 100644 index 000000000..2d7c807d0 --- /dev/null +++ b/src/main/java/org/springframework/cloud/gateway/filter/route/AddRequestParameterRouteFilter.java @@ -0,0 +1,138 @@ +package org.springframework.cloud.gateway.filter.route; + +import java.net.URI; +import java.net.URISyntaxException; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.http.server.reactive.ServerHttpRequestDecorator; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; +import org.springframework.web.server.WebFilter; + +/** + * @author Spencer Gibb + */ +public class AddRequestParameterRouteFilter implements RouteFilter { + + @Override + public WebFilter apply(String parameter, String[] args) { + validate(args, 1); + + //TODO: caching can happen here + return (exchange, chain) -> { + + URI uri = exchange.getRequest().getURI(); + StringBuilder query = new StringBuilder(); + String originalQuery = uri.getQuery(); + + if (StringUtils.hasText(originalQuery)) { + query.append(originalQuery); + if (originalQuery.charAt(originalQuery.length() - 1) != '&') { + query.append('&'); + } + } + + //TODO urlencode? + query.append(parameter); + query.append('='); + query.append(args[0]); + + ServerHttpRequest request = new QueryParamServerHttpRequestBuilder(exchange.getRequest()) + .query(query.toString()) + .build(); + + return chain.filter(exchange.mutate().request(request).build()); + }; + } + + class QueryParamServerHttpRequestBuilder implements ServerHttpRequest.Builder { + + private final ServerHttpRequest delegate; + private String query; + + public QueryParamServerHttpRequestBuilder(ServerHttpRequest delegate) { + Assert.notNull(delegate, "ServerHttpRequest delegate is required"); + this.delegate = delegate; + } + + + @Override + public ServerHttpRequest.Builder method(HttpMethod httpMethod) { + throw new UnsupportedOperationException(); + } + + @Override + public ServerHttpRequest.Builder path(String path) { + throw new UnsupportedOperationException(); + } + + public ServerHttpRequest.Builder query(String query) { + this.query = query; + return this; + } + + @Override + public ServerHttpRequest.Builder contextPath(String contextPath) { + throw new UnsupportedOperationException(); + } + + @Override + public ServerHttpRequest.Builder header(String key, String value) { + throw new UnsupportedOperationException(); + } + + @Override + public ServerHttpRequest build() { + URI uri = null; + if (this.query != null) { + uri = this.delegate.getURI(); + try { + uri = new URI(uri.getScheme(), uri.getUserInfo(), uri.getHost(), uri.getPort(), + uri.getPath(), this.query, uri.getFragment()); + } catch (URISyntaxException ex) { + throw new IllegalStateException("Invalid URI query: \"" + this.query + "\""); + } + } + return new MutativeDecorator(this.delegate, uri); + } + + + /** + * An immutable wrapper of a request returning property overrides -- given + * to the constructor -- or original values otherwise. + */ + private class MutativeDecorator extends ServerHttpRequestDecorator { + + private final URI uri; + + public MutativeDecorator(ServerHttpRequest delegate, URI uri) { + super(delegate); + + this.uri = uri; + } + + @Override + public HttpMethod getMethod() { + return super.getMethod(); + } + + @Override + public URI getURI() { + return (this.uri != null ? this.uri : super.getURI()); + } + + @Override + public String getContextPath() { + return super.getContextPath(); + } + + @Override + public HttpHeaders getHeaders() { + return super.getHeaders(); + } + } + + } +} diff --git a/src/test/java/org/springframework/cloud/gateway/test/GatewayIntegrationTests.java b/src/test/java/org/springframework/cloud/gateway/test/GatewayIntegrationTests.java index 637821101..d7ec2dbe5 100644 --- a/src/test/java/org/springframework/cloud/gateway/test/GatewayIntegrationTests.java +++ b/src/test/java/org/springframework/cloud/gateway/test/GatewayIntegrationTests.java @@ -45,6 +45,7 @@ public class GatewayIntegrationTests { private static final String HANDLER_MAPPER_HEADER = "X-Gateway-Handler-Mapper-Class"; private static final String ROUTE_ID_HEADER = "X-Gateway-Route-Id"; + public static final Duration DURATION = Duration.ofSeconds(3); @LocalServerPort private int port; @@ -100,7 +101,37 @@ public class GatewayIntegrationTests { assertThat(headers).containsEntry("X-Request-Foo", "Bar"); }) .expectComplete() - .verify(Duration.ofSeconds(3)) + .verify(DURATION) + ); + } + + @Test + public void addRequestParameterFilterWorksBlankQuery() { + testRequestParameterFilter(""); + } + + @Test + public void addRequestParameterFilterWorksNonBlankQuery() { + testRequestParameterFilter("?baz=bam"); + } + + private void testRequestParameterFilter(String query) { + Mono result = webClient.exchange( + GET("http://localhost:" + port + "/get" + query) + .header("Host", "www.addrequestparameter.org") + .build() + ).then(response -> response.body(toMono(Map.class))); + + verify( () -> + StepVerifier.create(result) + .consumeNextWith( + response -> { + assertThat(response).containsKey("args").isInstanceOf(Map.class); + Map args = (Map) response.get("args"); + assertThat(args).containsEntry("foo", "bar"); + }) + .expectComplete() + .verify(DURATION) ); } @@ -121,7 +152,7 @@ public class GatewayIntegrationTests { .isEqualTo("Bar"); }) .expectComplete() - .verify(Duration.ofSeconds(3)) + .verify(DURATION) ); } @@ -175,7 +206,7 @@ public class GatewayIntegrationTests { assertThat(statusCode).isEqualTo(HttpStatus.OK); }) .expectComplete() - .verify(Duration.ofSeconds(3)) + .verify(DURATION) ); } @@ -192,7 +223,7 @@ public class GatewayIntegrationTests { StepVerifier.create(result) .consumeNextWith(map -> assertThat(map).containsEntry("data", "testdata")) .expectComplete() - .verify(Duration.ofSeconds(3)) + .verify(DURATION) ); } @@ -215,7 +246,7 @@ public class GatewayIntegrationTests { assertThat(headers).doesNotContainKey("X-Request-Foo"); }) .expectComplete() - .verify(Duration.ofSeconds(3)) + .verify(DURATION) ); } @@ -235,7 +266,7 @@ public class GatewayIntegrationTests { assertThat(httpHeaders).doesNotContainKey("X-Request-Foo"); }) .expectComplete() - .verify(Duration.ofSeconds(3)) + .verify(DURATION) ); } @@ -255,7 +286,7 @@ public class GatewayIntegrationTests { assertThat(statusCode).isEqualTo(HttpStatus.OK); }) .expectComplete() - .verify(Duration.ofSeconds(3)) + .verify(DURATION) ); } @@ -275,7 +306,7 @@ public class GatewayIntegrationTests { assertThat(statusCode).isEqualTo(HttpStatus.OK); }) .expectComplete() - .verify(Duration.ofSeconds(3)) + .verify(DURATION) ); } @@ -296,7 +327,7 @@ public class GatewayIntegrationTests { assertThat(httpHeaders.get("X-Request-Foo")).containsExactly("Bar"); }) .expectComplete() - .verify(Duration.ofSeconds(3)) + .verify(DURATION) ); } @@ -325,7 +356,7 @@ public class GatewayIntegrationTests { assertThat(statusCode).isEqualTo(status); }) .expectComplete() - .verify(Duration.ofSeconds(3)) + .verify(DURATION) ); } @@ -348,7 +379,7 @@ public class GatewayIntegrationTests { assertThat(statusCode).isEqualTo(HttpStatus.OK); }) .expectComplete() - .verify(Duration.ofSeconds(3)) + .verify(DURATION) ); } diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml index a381593e1..bae7ca765 100644 --- a/src/test/resources/application.yml +++ b/src/test/resources/application.yml @@ -29,6 +29,15 @@ spring: filters: - AddRequestHeader=X-Request-Foo, Bar + # ===================================== + - id: add_request_parameter_test + uri: http://httpbin.org:80 + predicates: + - Host=**.addrequestparameter.org + - Url=/get + filters: + - AddRequestParameter=foo, bar + # ===================================== - id: add_response_header_test uri: http://httpbin.org:80