From 87db8af470648f9cb9f96b70e523f95ccf728a9b Mon Sep 17 00:00:00 2001 From: Alexander Tsvetkov Date: Fri, 13 Sep 2019 17:04:07 +0300 Subject: [PATCH] Adopt reactor-netty 0.9.0.RELEASE --- cloudfoundry-client-reactor/pom.xml | 2 +- .../reactor/AbstractRootProvider.java | 39 ++- .../reactor/ConnectionContext.java | 2 +- .../reactor/_DefaultConnectionContext.java | 175 ++++++---- .../reactor/_HttpClientResponseWithBody.java | 17 + .../reactor/_InfoPayloadRootProvider.java | 45 ++- .../reactor/_ProxyConfiguration.java | 24 +- .../reactor/_RootPayloadRootProvider.java | 53 ++- .../client/v2/AbstractClientV2Operations.java | 130 ++++---- .../applications/ReactorApplicationsV2.java | 167 +++++----- .../v2/buildpacks/ReactorBuildpacks.java | 82 ++--- .../client/v3/AbstractClientV3Operations.java | 167 +++++----- .../client/v3/packages/ReactorPackages.java | 89 +++-- .../doppler/AbstractDopplerOperations.java | 27 +- .../reactor/doppler/MultipartCodec.java | 28 +- .../doppler/ReactorDopplerEndpoints.java | 25 +- .../AbstractNetworkingOperations.java | 45 +-- .../v1/AbstractRoutingV1Operations.java | 43 ++- .../v1/tcproutes/EventStreamCodec.java | 9 +- .../v1/tcproutes/ReactorTcpRoutes.java | 27 +- .../AbstractUaaTokenProvider.java | 205 ++++++------ .../_ClientCredentialsGrantTokenProvider.java | 21 +- .../_OneTimePasscodeTokenProvider.java | 21 +- .../_PasswordGrantTokenProvider.java | 25 +- .../_RefreshTokenGrantTokenProvider.java | 21 +- .../reactor/uaa/AbstractUaaOperations.java | 187 +++++------ .../reactor/uaa/IdentityZoneBuilder.java | 13 +- .../reactor/uaa/VersionBuilder.java | 10 +- .../authorizations/ReactorAuthorizations.java | 81 ++--- .../ReactorServerInformation.java | 10 +- .../reactor/uaa/tokens/ReactorTokens.java | 135 ++++---- .../util/AbstractReactorOperations.java | 276 ++-------------- .../util/DefaultSslCertificateTruster.java | 90 +++--- .../reactor/util/ErrorPayloadMapper.java | 92 +----- .../reactor/util/ErrorPayloadMappers.java | 127 ++++++++ .../cloudfoundry/reactor/util/JsonCodec.java | 79 ++--- .../util/MultipartHttpClientRequest.java | 188 +++-------- .../reactor/util/NetworkLogging.java | 97 ------ .../cloudfoundry/reactor/util/Operator.java | 304 ++++++++++++++++++ .../reactor/util/RequestLogger.java | 72 +++++ .../cloudfoundry/reactor/util/UserAgent.java | 15 +- .../reactor/util/_OperatorContext.java | 22 ++ .../ReactorApplicationsV2Test.java | 14 +- .../v2/buildpacks/ReactorBuildpacksTest.java | 5 +- .../v3/packages/ReactorPackagesTest.java | 7 +- .../v1/tcproutes/EventStreamCodecTest.java | 70 ++-- .../reactor/uaa/IdentityZoneBuilderTest.java | 15 +- .../reactor/uaa/VersionBuilderTest.java | 13 +- .../reactor/uaa/users/ReactorUsersTest.java | 32 +- ...Test.java => ErrorPayloadMappersTest.java} | 172 +++++----- .../src/test/resources/logback-test.xml | 2 +- .../_DeleteServiceInstanceResponse.java | 2 - .../v3/servicebindings/ServiceBinding.java | 2 +- .../org/cloudfoundry/uaa/clients/Action.java | 2 +- .../applications/DefaultApplications.java | 2 +- .../serviceadmin/DefaultServiceAdmin.java | 2 +- .../cloudfoundry/util/PaginationUtils.java | 1 - .../src/test/resources/logback-test.xml | 2 +- pom.xml | 6 +- 59 files changed, 1930 insertions(+), 1706 deletions(-) create mode 100644 cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/_HttpClientResponseWithBody.java create mode 100644 cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/util/ErrorPayloadMappers.java delete mode 100644 cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/util/NetworkLogging.java create mode 100644 cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/util/Operator.java create mode 100644 cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/util/RequestLogger.java create mode 100644 cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/util/_OperatorContext.java rename cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/util/{ErrorPayloadMapperTest.java => ErrorPayloadMappersTest.java} (54%) diff --git a/cloudfoundry-client-reactor/pom.xml b/cloudfoundry-client-reactor/pom.xml index 92c1dd4ca6e..2aebe8fa6be 100644 --- a/cloudfoundry-client-reactor/pom.xml +++ b/cloudfoundry-client-reactor/pom.xml @@ -66,7 +66,7 @@ test - io.projectreactor.ipc + io.projectreactor.netty reactor-netty diff --git a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/AbstractRootProvider.java b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/AbstractRootProvider.java index ce54ca4f32d..b5b8998e441 100644 --- a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/AbstractRootProvider.java +++ b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/AbstractRootProvider.java @@ -16,10 +16,17 @@ package org.cloudfoundry.reactor; +import org.cloudfoundry.reactor.util.JsonCodec; +import org.cloudfoundry.reactor.util.Operator; +import org.cloudfoundry.reactor.util.OperatorContext; +import org.cloudfoundry.reactor.util.UserAgent; import org.immutables.value.Value; import org.springframework.web.util.UriComponents; import org.springframework.web.util.UriComponentsBuilder; + +import io.netty.handler.codec.http.HttpHeaders; import reactor.core.publisher.Mono; +import reactor.netty.http.client.HttpClient; import java.util.Optional; import java.util.regex.Matcher; @@ -41,19 +48,19 @@ public final void checkForValidApiHost() { Matcher matcher = HOSTNAME_PATTERN.matcher(getApiHost()); if (!matcher.matches()) { - throw new IllegalArgumentException(String.format("API hostname %s is not correctly formatted (e.g. 'api.local.pcfdev.io')", getApiHost())); + throw new IllegalArgumentException(String.format("API hostname %s is not correctly formatted (e.g. 'api.local.pcfdev.io')", + getApiHost())); } } /** - * The hostname of the API root. Typically something like {@code api.run.pivotal.io}. + * The hostname of the API root. Typically something like {@code api.run.pivotal.io}. */ public abstract String getApiHost(); @Override public final Mono getRoot(ConnectionContext connectionContext) { - Mono cached = doGetRoot(connectionContext) - .delayUntil(uri -> trust(uri.getHost(), uri.getPort(), connectionContext)) + Mono cached = doGetRoot(connectionContext).delayUntil(uri -> trust(uri.getHost(), uri.getPort(), connectionContext)) .map(UriComponents::toUriString); return connectionContext.getCacheDuration() @@ -63,8 +70,7 @@ public final Mono getRoot(ConnectionContext connectionContext) { @Override public final Mono getRoot(String key, ConnectionContext connectionContext) { - Mono cached = doGetRoot(key, connectionContext) - .delayUntil(uri -> trust(uri.getHost(), uri.getPort(), connectionContext)) + Mono cached = doGetRoot(key, connectionContext).delayUntil(uri -> trust(uri.getHost(), uri.getPort(), connectionContext)) .map(UriComponents::toUriString); return connectionContext.getCacheDuration() @@ -77,7 +83,9 @@ public final Mono getRoot(String key, ConnectionContext connectionContex protected abstract Mono doGetRoot(String key, ConnectionContext connectionContext); protected final UriComponents getRoot() { - UriComponentsBuilder builder = UriComponentsBuilder.newInstance().scheme("https").host(getApiHost()); + UriComponentsBuilder builder = UriComponentsBuilder.newInstance() + .scheme("https") + .host(getApiHost()); getPort().ifPresent(builder::port); return normalize(builder); @@ -92,7 +100,8 @@ protected final UriComponents normalize(UriComponentsBuilder builder) { builder.port(getPort().orElse(DEFAULT_PORT)); } - return builder.build().encode(); + return builder.build() + .encode(); } /** @@ -101,7 +110,7 @@ protected final UriComponents normalize(UriComponentsBuilder builder) { abstract Optional getPort(); /** - * Whether the connection to the root API should be secure (i.e. using HTTPS). Defaults to {@code true}. + * Whether the connection to the root API should be secure (i.e. using HTTPS). Defaults to {@code true}. */ abstract Optional getSecure(); @@ -117,4 +126,16 @@ private Mono trust(String host, int port, ConnectionContext connectionCont return connectionContext.trust(host, port); } + public Mono createOperator(ConnectionContext connectionContext) { + HttpClient httpClient = connectionContext.getHttpClient(); + return getRoot(connectionContext).map(root -> OperatorContext.of(connectionContext, root)) + .map(operatorContext -> new Operator(operatorContext, httpClient)) + .map(operator -> operator.headers(this::addHeaders)); + } + + private void addHeaders(HttpHeaders httpHeaders) { + UserAgent.setUserAgent(httpHeaders); + JsonCodec.setDecodeHeaders(httpHeaders); + } + } diff --git a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/ConnectionContext.java b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/ConnectionContext.java index 1ad135f75fc..ee1acfba99c 100644 --- a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/ConnectionContext.java +++ b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/ConnectionContext.java @@ -18,7 +18,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import reactor.core.publisher.Mono; -import reactor.ipc.netty.http.client.HttpClient; +import reactor.netty.http.client.HttpClient; import java.time.Duration; import java.util.Optional; diff --git a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/_DefaultConnectionContext.java b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/_DefaultConnectionContext.java index 586ff82d6e2..40c8c9b544c 100644 --- a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/_DefaultConnectionContext.java +++ b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/_DefaultConnectionContext.java @@ -16,11 +16,24 @@ package org.cloudfoundry.reactor; -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.deser.DeserializationProblemHandler; -import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; -import io.netty.buffer.PooledByteBufAllocator; +import static com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL; +import static io.netty.channel.ChannelOption.CONNECT_TIMEOUT_MILLIS; +import static io.netty.channel.ChannelOption.SO_KEEPALIVE; +import static io.netty.channel.ChannelOption.SO_RCVBUF; +import static io.netty.channel.ChannelOption.SO_SNDBUF; + +import java.lang.management.ManagementFactory; +import java.time.Duration; +import java.util.List; +import java.util.Optional; + +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; +import javax.management.JMException; +import javax.management.MalformedObjectNameException; +import javax.management.ObjectName; +import javax.net.ssl.TrustManagerFactory; + import org.cloudfoundry.Nullable; import org.cloudfoundry.reactor.util.ByteBufAllocatorMetricProviderWrapper; import org.cloudfoundry.reactor.util.DefaultSslCertificateTruster; @@ -29,38 +42,32 @@ import org.immutables.value.Value; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import reactor.core.publisher.Mono; -import reactor.ipc.netty.http.client.HttpClient; -import reactor.ipc.netty.resources.LoopResources; -import reactor.ipc.netty.resources.PoolResources; -import javax.annotation.PostConstruct; -import javax.annotation.PreDestroy; -import javax.management.JMException; -import javax.management.MalformedObjectNameException; -import javax.management.ObjectName; -import java.lang.management.ManagementFactory; -import java.time.Duration; -import java.util.List; -import java.util.Optional; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.deser.DeserializationProblemHandler; +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; -import static com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL; -import static io.netty.channel.ChannelOption.CONNECT_TIMEOUT_MILLIS; -import static io.netty.channel.ChannelOption.SO_KEEPALIVE; -import static io.netty.channel.ChannelOption.SO_RCVBUF; -import static io.netty.channel.ChannelOption.SO_SNDBUF; +import io.netty.buffer.PooledByteBufAllocator; +import io.netty.handler.ssl.SslContextBuilder; +import reactor.core.publisher.Mono; +import reactor.netty.http.client.HttpClient; +import reactor.netty.resources.ConnectionProvider; +import reactor.netty.resources.LoopResources; +import reactor.netty.tcp.SslProvider; +import reactor.netty.tcp.SslProvider.DefaultConfigurationType; +import reactor.netty.tcp.TcpClient; /** - * The default implementation of the {@link ConnectionContext} interface. This is the implementation that should be used for most non-testing cases. + * The default implementation of the {@link ConnectionContext} interface. This is the implementation that should be used for most + * non-testing cases. */ @Value.Immutable abstract class _DefaultConnectionContext implements ConnectionContext { private static final int DEFAULT_PORT = 443; - private static final int RECEIVE_BUFFER_SIZE = 10 * 1024 * 1024; - - private static final int SEND_BUFFER_SIZE = 10 * 1024 * 1024; + private static final int SEND_RECEIVE_BUFFER_SIZE = 10 * 1024 * 1024; private final Logger logger = LoggerFactory.getLogger("cloudfoundry-client"); @@ -69,14 +76,16 @@ abstract class _DefaultConnectionContext implements ConnectionContext { */ @PreDestroy public final void dispose() { - getConnectionPool().ifPresent(PoolResources::dispose); + getConnectionProvider().ifPresent(ConnectionProvider::dispose); getThreadPool().dispose(); try { ObjectName name = getByteBufAllocatorObjectName(); - if (ManagementFactory.getPlatformMBeanServer().isRegistered(name)) { - ManagementFactory.getPlatformMBeanServer().unregisterMBean(name); + if (ManagementFactory.getPlatformMBeanServer() + .isRegistered(name)) { + ManagementFactory.getPlatformMBeanServer() + .unregisterMBean(name); } } catch (JMException e) { this.logger.error("Unable to register ByteBufAllocator MBean", e); @@ -87,42 +96,74 @@ public final void dispose() { public abstract Optional getCacheDuration(); /** - * The number of connections to use when processing requests and responses. Setting this to {@code null} disables connection pooling. + * The number of connections to use when processing requests and responses. Setting this to {@code null} disables connection pooling. */ @Nullable @Value.Default public Integer getConnectionPoolSize() { - return PoolResources.DEFAULT_POOL_MAX_CONNECTION; + return ConnectionProvider.DEFAULT_POOL_MAX_CONNECTIONS; } @Override @Value.Default public HttpClient getHttpClient() { - return HttpClient.create(options -> { - options - .compression(true) - .loopResources(getThreadPool()) - .option(SO_SNDBUF, SEND_BUFFER_SIZE) - .option(SO_RCVBUF, RECEIVE_BUFFER_SIZE) - .disablePool(); - - options.sslSupport(ssl -> getSslCertificateTruster().ifPresent(trustManager -> ssl.trustManager(new StaticTrustManagerFactory(trustManager)))); - - getConnectionPool().ifPresent(options::poolResources); - getConnectTimeout().ifPresent(socketTimeout -> options.option(CONNECT_TIMEOUT_MILLIS, (int) socketTimeout.toMillis())); - getKeepAlive().ifPresent(keepAlive -> options.option(SO_KEEPALIVE, keepAlive)); - getSslHandshakeTimeout().ifPresent(options::sslHandshakeTimeout); - getSslCloseNotifyFlushTimeout().ifPresent(options::sslCloseNotifyFlushTimeout); - getSslCloseNotifyReadTimeout().ifPresent(options::sslCloseNotifyReadTimeout); - getProxyConfiguration().ifPresent(c -> c.configure(options)); - }); + return createHttpClient().compress(true) + .tcpConfiguration(this::configureTcpClient) + .secure(this::configureSsl); + } + + private HttpClient createHttpClient() { + return getConnectionProvider().map(HttpClient::create) + .orElse(HttpClient.create()); + } + + private TcpClient configureTcpClient(TcpClient tcpClient) { + tcpClient = configureProxy(tcpClient); + tcpClient = tcpClient.runOn(getThreadPool()) + .option(SO_SNDBUF, SEND_RECEIVE_BUFFER_SIZE) + .option(SO_RCVBUF, SEND_RECEIVE_BUFFER_SIZE); + tcpClient = configureKeepAlive(tcpClient); + return configureConnectTimeout(tcpClient); + } + + private TcpClient configureProxy(TcpClient tcpClient) { + return getProxyConfiguration().map(proxyConfiguration -> proxyConfiguration.configure(tcpClient)) + .orElse(tcpClient); + } + + private TcpClient configureKeepAlive(TcpClient tcpClient) { + return getKeepAlive().map(keepAlive -> tcpClient.option(SO_KEEPALIVE, keepAlive)) + .orElse(tcpClient); + } + + private TcpClient configureConnectTimeout(TcpClient tcpClient) { + return getConnectTimeout().map(connectTimeout -> tcpClient.option(CONNECT_TIMEOUT_MILLIS, (int) connectTimeout.toMillis())) + .orElse(tcpClient); + } + + private void configureSsl(SslProvider.SslContextSpec ssl) { + SslProvider.Builder builder = ssl.sslContext(createSslContextBuilder()) + .defaultConfiguration(DefaultConfigurationType.TCP); + getSslCloseNotifyReadTimeout().ifPresent(builder::closeNotifyReadTimeout); + getSslHandshakeTimeout().ifPresent(builder::handshakeTimeout); + getSslCloseNotifyFlushTimeout().ifPresent(builder::closeNotifyFlushTimeout); + } + + private SslContextBuilder createSslContextBuilder() { + SslContextBuilder sslContextBuilder = SslContextBuilder.forClient(); + getSslCertificateTruster().map(this::createTrustManagerFactory) + .ifPresent(sslContextBuilder::trustManager); + return sslContextBuilder; + } + + private TrustManagerFactory createTrustManagerFactory(SslCertificateTruster sslCertificateTruster) { + return new StaticTrustManagerFactory(sslCertificateTruster); } @Override @Value.Default public ObjectMapper getObjectMapper() { - ObjectMapper objectMapper = new ObjectMapper() - .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) + ObjectMapper objectMapper = new ObjectMapper().disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) .enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY) .registerModule(new Jdk8Module()) .setSerializationInclusion(NON_NULL); @@ -153,13 +194,12 @@ public Integer getThreadPoolSize() { @Override public Mono trust(String host, int port) { - return getSslCertificateTruster() - .map(t -> t.trust(host, port, Duration.ofSeconds(30))) + return getSslCertificateTruster().map(t -> t.trust(host, port, Duration.ofSeconds(30))) .orElse(Mono.empty()); } /** - * The hostname of the API root. Typically something like {@code api.run.pivotal.io}. + * The hostname of the API root. Typically something like {@code api.run.pivotal.io}. */ abstract String getApiHost(); @@ -169,9 +209,9 @@ public Mono trust(String host, int port) { abstract Optional getConnectTimeout(); @Value.Derived - Optional getConnectionPool() { + Optional getConnectionProvider() { return Optional.ofNullable(getConnectionPoolSize()) - .map(connectionPoolSize -> PoolResources.fixed("cloudfoundry-client", connectionPoolSize)); + .map(connectionPoolSize -> ConnectionProvider.fixed("cloudfoundry-client", connectionPoolSize)); } /** @@ -185,7 +225,7 @@ Optional getConnectionPool() { abstract Optional getPort(); /** - * Jackson deserialization problem handlers. Typically only used for testing. + * Jackson deserialization problem handlers. Typically only used for testing. */ abstract List getProblemHandlers(); @@ -195,12 +235,12 @@ Optional getConnectionPool() { abstract Optional getProxyConfiguration(); /** - * Whether the connection to the root API should be secure (i.e. using HTTPS). Defaults to {@code true}. + * Whether the connection to the root API should be secure (i.e. using HTTPS). Defaults to {@code true}. */ abstract Optional getSecure(); /** - * Whether to skip SSL certificate validation for all hosts reachable from the API host. Defaults to {@code false}. + * Whether to skip SSL certificate validation for all hosts reachable from the API host. Defaults to {@code false}. */ abstract Optional getSkipSslValidation(); @@ -238,19 +278,24 @@ void monitorByteBufAllocator() { try { ObjectName name = getByteBufAllocatorObjectName(); - if (ManagementFactory.getPlatformMBeanServer().isRegistered(name)) { - this.logger.warn("MBean '{}' is already registered and will be removed. You should only have a single DefaultConnectionContext per endpoint.", name); - ManagementFactory.getPlatformMBeanServer().unregisterMBean(name); + if (ManagementFactory.getPlatformMBeanServer() + .isRegistered(name)) { + this.logger.warn("MBean '{}' is already registered and will be removed. You should only have a single DefaultConnectionContext per endpoint.", + name); + ManagementFactory.getPlatformMBeanServer() + .unregisterMBean(name); } - ManagementFactory.getPlatformMBeanServer().registerMBean(new ByteBufAllocatorMetricProviderWrapper(PooledByteBufAllocator.DEFAULT), name); + ManagementFactory.getPlatformMBeanServer() + .registerMBean(new ByteBufAllocatorMetricProviderWrapper(PooledByteBufAllocator.DEFAULT), name); } catch (JMException e) { this.logger.error("Unable to register ByteBufAllocator MBean", e); } } private ObjectName getByteBufAllocatorObjectName() throws MalformedObjectNameException { - return ObjectName.getInstance(String.format("org.cloudfoundry.reactor:type=ByteBufAllocator,endpoint=%s/%d", getApiHost(), getPort().orElse(DEFAULT_PORT))); + return ObjectName.getInstance(String.format("org.cloudfoundry.reactor:type=ByteBufAllocator,endpoint=%s/%d", getApiHost(), + getPort().orElse(DEFAULT_PORT))); } } diff --git a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/_HttpClientResponseWithBody.java b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/_HttpClientResponseWithBody.java new file mode 100644 index 00000000000..903f7c1ef9e --- /dev/null +++ b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/_HttpClientResponseWithBody.java @@ -0,0 +1,17 @@ +package org.cloudfoundry.reactor; + +import org.immutables.value.Value; + +import reactor.netty.ByteBufFlux; +import reactor.netty.http.client.HttpClientResponse; + +@Value.Immutable +public interface _HttpClientResponseWithBody { + + @Value.Parameter + HttpClientResponse getResponse(); + + @Value.Parameter + ByteBufFlux getBody(); + +} diff --git a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/_InfoPayloadRootProvider.java b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/_InfoPayloadRootProvider.java index bbe524aeb09..5da7e152721 100644 --- a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/_InfoPayloadRootProvider.java +++ b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/_InfoPayloadRootProvider.java @@ -16,17 +16,15 @@ package org.cloudfoundry.reactor; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.cloudfoundry.reactor.util.JsonCodec; -import org.cloudfoundry.reactor.util.NetworkLogging; -import org.cloudfoundry.reactor.util.UserAgent; +import java.util.Map; + import org.immutables.value.Value; import org.springframework.web.util.UriComponents; import org.springframework.web.util.UriComponentsBuilder; -import reactor.core.publisher.Mono; -import reactor.ipc.netty.http.client.HttpClientRequest; -import java.util.Map; +import com.fasterxml.jackson.databind.ObjectMapper; + +import reactor.core.publisher.Mono; /** * A {@link RootProvider} that returns endpoints extracted from the `/v2/info` API for the configured endpoint. @@ -39,14 +37,13 @@ protected Mono doGetRoot(ConnectionContext connectionContext) { } protected Mono doGetRoot(String key, ConnectionContext connectionContext) { - return getInfo(connectionContext) - .map(info -> { - if (!info.containsKey(key)) { - throw new IllegalArgumentException(String.format("Info payload does not contain key '%s'", key)); - } + return getInfo(connectionContext).map(info -> { + if (!info.containsKey(key)) { + throw new IllegalArgumentException(String.format("Info payload does not contain key '%s'", key)); + } - return normalize(UriComponentsBuilder.fromUriString(info.get(key))); - }); + return normalize(UriComponentsBuilder.fromUriString(info.get(key))); + }); } abstract ObjectMapper getObjectMapper(); @@ -54,19 +51,17 @@ protected Mono doGetRoot(String key, ConnectionContext connection @SuppressWarnings("unchecked") @Value.Derived private Mono> getInfo(ConnectionContext connectionContext) { - return getRoot(connectionContext) - .map(uri -> UriComponentsBuilder.fromUriString(uri).pathSegment("v2", "info").build().encode().toUriString()) - .flatMap(uri -> connectionContext.getHttpClient() - .get(uri, request -> Mono.just(request) - .map(UserAgent::addUserAgent) - .map(JsonCodec::addDecodeHeaders) - .flatMapMany(HttpClientRequest::send)) - .doOnSubscribe(NetworkLogging.get(uri)) - .transform(NetworkLogging.response(uri))) - .transform(JsonCodec.decode(getObjectMapper(), Map.class)) + return createOperator(connectionContext).flatMap(operator -> operator.get() + .uri(this::buildInfoUri) + .response() + .parseBody(Map.class)) + .map(payload -> (Map) payload) .switchIfEmpty(Mono.error(new IllegalArgumentException("Info endpoint does not contain a payload"))) - .map(m -> (Map) m) .checkpoint(); } + private UriComponentsBuilder buildInfoUri(UriComponentsBuilder root) { + return root.pathSegment("v2", "info"); + } + } diff --git a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/_ProxyConfiguration.java b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/_ProxyConfiguration.java index 13a8bd142c6..39319c8892e 100644 --- a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/_ProxyConfiguration.java +++ b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/_ProxyConfiguration.java @@ -16,30 +16,29 @@ package org.cloudfoundry.reactor; -import org.immutables.value.Value; -import reactor.ipc.netty.options.ClientOptions; -import reactor.ipc.netty.options.ClientProxyOptions; - import java.util.Optional; import java.util.function.Function; +import org.immutables.value.Value; + +import reactor.netty.tcp.ProxyProvider.Builder; +import reactor.netty.tcp.ProxyProvider.Proxy; +import reactor.netty.tcp.TcpClient; + /** * Proxy configuration */ @Value.Immutable abstract class _ProxyConfiguration { - public void configure(ClientOptions.Builder options) { - options.proxy(typeSpec -> { - ClientProxyOptions.Builder builder = typeSpec - .type(ClientProxyOptions.Proxy.HTTP) + public TcpClient configure(TcpClient tcpClient) { + return tcpClient.proxy(proxyOptions -> { + Builder builder = proxyOptions.type(Proxy.HTTP) .host(getHost()); - getPort().ifPresent(builder::port); getUsername().ifPresent(builder::username); - getPassword().map(password -> (Function) s -> password).ifPresent(builder::password); - - return builder; + getPassword().map(password -> (Function) s -> password) + .ifPresent(builder::password); }); } @@ -63,5 +62,4 @@ public void configure(ClientOptions.Builder options) { */ abstract Optional getUsername(); - } diff --git a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/_RootPayloadRootProvider.java b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/_RootPayloadRootProvider.java index c6b1b7e98af..596f1cf0a70 100644 --- a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/_RootPayloadRootProvider.java +++ b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/_RootPayloadRootProvider.java @@ -16,18 +16,17 @@ package org.cloudfoundry.reactor; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.cloudfoundry.reactor.util.JsonCodec; -import org.cloudfoundry.reactor.util.NetworkLogging; -import org.cloudfoundry.reactor.util.UserAgent; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + import org.immutables.value.Value; import org.springframework.web.util.UriComponents; import org.springframework.web.util.UriComponentsBuilder; -import reactor.core.publisher.Mono; -import reactor.ipc.netty.http.client.HttpClientRequest; -import java.util.Map; -import java.util.stream.Collectors; +import com.fasterxml.jackson.databind.ObjectMapper; + +import reactor.core.publisher.Mono; /** * A {@link RootProvider} that returns endpoints extracted from the `/` API for the configured endpoint. @@ -42,14 +41,13 @@ protected Mono doGetRoot(ConnectionContext connectionContext) { @Override protected Mono doGetRoot(String key, ConnectionContext connectionContext) { - return getPayload(connectionContext) - .map(payload -> { - if (!payload.containsKey(key)) { - throw new IllegalArgumentException(String.format("Root payload does not contain key '%s'", key)); - } + return getPayload(connectionContext).map(payload -> { + if (!payload.containsKey(key)) { + throw new IllegalArgumentException(String.format("Root payload does not contain key '%s'", key)); + } - return normalize(UriComponentsBuilder.fromUriString(payload.get(key))); - }); + return normalize(UriComponentsBuilder.fromUriString(payload.get(key))); + }); } abstract ObjectMapper getObjectMapper(); @@ -57,24 +55,23 @@ protected Mono doGetRoot(String key, ConnectionContext connection @SuppressWarnings("unchecked") @Value.Derived private Mono> getPayload(ConnectionContext connectionContext) { - return getRoot(connectionContext) - .flatMap(uri -> connectionContext.getHttpClient() - .get(uri, request -> Mono.just(request) - .map(UserAgent::addUserAgent) - .map(JsonCodec::addDecodeHeaders) - .flatMapMany(HttpClientRequest::send)) - .doOnSubscribe(NetworkLogging.get(uri)) - .transform(NetworkLogging.response(uri))) - .transform(JsonCodec.decode(getObjectMapper(), Map.class)) + return createOperator(connectionContext).flatMap(operator -> operator.get() + .uri(Function.identity()) + .response() + .parseBody(Map.class)) + .map(payload -> (Map>>) payload) + .map(this::processPayload) .switchIfEmpty(Mono.error(new IllegalArgumentException("Root endpoint does not contain a payload"))) - .map(this::parsePayload) .checkpoint(); } - private Map parsePayload(Map>> payload) { - return payload.get("links").entrySet().stream() + private Map processPayload(Map>> payload) { + return payload.get("links") + .entrySet() + .stream() .filter(item -> null != item.getValue()) - .collect(Collectors.toMap(Map.Entry::getKey, entry -> entry.getValue().get("href"))); + .collect(Collectors.toMap(Map.Entry::getKey, entry -> entry.getValue() + .get("href"))); } } diff --git a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/client/v2/AbstractClientV2Operations.java b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/client/v2/AbstractClientV2Operations.java index cafae7bd4ed..e02927f9d42 100644 --- a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/client/v2/AbstractClientV2Operations.java +++ b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/client/v2/AbstractClientV2Operations.java @@ -16,96 +16,104 @@ package org.cloudfoundry.reactor.client.v2; -import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.Function; + import org.cloudfoundry.reactor.ConnectionContext; import org.cloudfoundry.reactor.TokenProvider; import org.cloudfoundry.reactor.client.QueryBuilder; import org.cloudfoundry.reactor.util.AbstractReactorOperations; -import org.cloudfoundry.reactor.util.ErrorPayloadMapper; +import org.cloudfoundry.reactor.util.ErrorPayloadMappers; import org.cloudfoundry.reactor.util.MultipartHttpClientRequest; -import org.reactivestreams.Publisher; +import org.cloudfoundry.reactor.util.Operator; import org.springframework.web.util.UriComponentsBuilder; -import reactor.core.publisher.Mono; -import reactor.ipc.netty.http.client.HttpClientRequest; -import reactor.ipc.netty.http.client.HttpClientResponse; -import java.util.function.Function; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.netty.ByteBufFlux; +import reactor.netty.http.client.HttpClientForm; +import reactor.netty.http.client.HttpClientRequest; public abstract class AbstractClientV2Operations extends AbstractReactorOperations { - private final ConnectionContext connectionContext; - protected AbstractClientV2Operations(ConnectionContext connectionContext, Mono root, TokenProvider tokenProvider) { super(connectionContext, root, tokenProvider); - this.connectionContext = connectionContext; } - protected final Mono delete(Object requestPayload, Class responseType, Function uriTransformer) { - return doDelete(requestPayload, responseType, - queryTransformer(requestPayload) - .andThen(uriTransformer), - outbound -> outbound, - ErrorPayloadMapper.clientV2(this.connectionContext.getObjectMapper())); + protected final Mono delete(Object requestPayload, Class responseType, + Function uriTransformer) { + return createOperator().flatMap(operator -> operator.delete() + .uri(queryTransformer(requestPayload).andThen(uriTransformer)) + .send(requestPayload) + .response() + .parseBody(responseType)); } - protected final Mono get(Object requestPayload, Class responseType, Function uriTransformer) { - return doGet(responseType, - queryTransformer(requestPayload) - .andThen(uriTransformer), - outbound -> outbound, - ErrorPayloadMapper.clientV2(this.connectionContext.getObjectMapper())); + protected final Mono get(Object requestPayload, Class responseType, + Function uriTransformer) { + return createOperator().flatMap(operator -> operator.get() + .uri(queryTransformer(requestPayload).andThen(uriTransformer)) + .response() + .parseBody(responseType)); } - protected final Mono get(Object requestPayload, Function uriTransformer, - Function, Mono> requestTransformer) { + protected final Flux get(Object requestPayload, Function uriTransformer, + Function> bodyTransformer) { + return createOperator().flatMapMany(operator -> operator.followRedirects() + .get() + .uri(queryTransformer(requestPayload).andThen(uriTransformer)) + .response() + .parseBodyToFlux(responseWithBody -> bodyTransformer.apply(responseWithBody.getBody()))); + } - return doGet(queryTransformer(requestPayload) - .andThen(uriTransformer), - outbound -> outbound - .transform(requestTransformer), - ErrorPayloadMapper.clientV2(this.connectionContext.getObjectMapper())); + protected final Mono post(Object requestPayload, Class responseType, + Function uriTransformer) { + return createOperator().flatMap(operator -> operator.post() + .uri(queryTransformer(requestPayload).andThen(uriTransformer)) + .send(requestPayload) + .response() + .parseBody(responseType)); } - protected final Mono post(Object requestPayload, Class responseType, Function uriTransformer) { - return doPost(requestPayload, responseType, - queryTransformer(requestPayload) - .andThen(uriTransformer), - outbound -> outbound, - ErrorPayloadMapper.clientV2(this.connectionContext.getObjectMapper())); + protected final Mono put(Object requestPayload, Class responseType, + Function uriTransformer) { + return createOperator().flatMap(operator -> operator.put() + .uri(queryTransformer(requestPayload).andThen(uriTransformer)) + .send(requestPayload) + .response() + .parseBody(responseType)); } - protected final Mono post(Object requestPayload, Class responseType, Function uriTransformer, - Function, Publisher> requestTransformer) { - return doPost(responseType, - queryTransformer(requestPayload) - .andThen(uriTransformer), - outbound -> outbound - .map(multipartRequest(this.connectionContext.getObjectMapper())) - .transform(requestTransformer), - ErrorPayloadMapper.clientV2(this.connectionContext.getObjectMapper())); + protected final Mono put(Object requestPayload, Class responseType, + Function uriTransformer, + Consumer requestTransformer, Runnable onTerminate) { + return createOperator().flatMap(operator -> operator.put() + .uri(queryTransformer(requestPayload).andThen(uriTransformer)) + .sendForm(multipartRequest(requestTransformer)) + .response() + .parseBody(responseType)) + .doFinally(signalType -> onTerminate.run()); } - protected final Mono put(Object requestPayload, Class responseType, Function uriTransformer) { - return doPut(requestPayload, responseType, - queryTransformer(requestPayload) - .andThen(uriTransformer), - outbound -> outbound, - ErrorPayloadMapper.clientV2(this.connectionContext.getObjectMapper())); + @Override + protected Mono createOperator() { + return super.createOperator().map(this::attachErrorPayloadMapper); } - protected final Mono put(Object requestPayload, Class responseType, Function uriTransformer, - Function, Publisher> requestTransformer) { - return doPut(responseType, - queryTransformer(requestPayload) - .andThen(uriTransformer), - outbound -> outbound - .map(multipartRequest(this.connectionContext.getObjectMapper())) - .transform(requestTransformer), - ErrorPayloadMapper.clientV2(this.connectionContext.getObjectMapper())); + private Operator attachErrorPayloadMapper(Operator operator) { + return operator.withErrorPayloadMapper(ErrorPayloadMappers.clientV2(this.connectionContext.getObjectMapper())); + } + + private BiConsumer multipartRequest(Consumer requestTransformer) { + return (request, form) -> { + MultipartHttpClientRequest multipartRequest = createMultipartRequest(request, form); + requestTransformer.accept(multipartRequest); + }; } - private static Function multipartRequest(ObjectMapper objectMapper) { - return request -> new MultipartHttpClientRequest(objectMapper, request); + private MultipartHttpClientRequest createMultipartRequest(HttpClientRequest request, HttpClientForm form) { + return new MultipartHttpClientRequest(this.connectionContext.getObjectMapper(), request, form); } private static Function queryTransformer(Object requestPayload) { diff --git a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/client/v2/applications/ReactorApplicationsV2.java b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/client/v2/applications/ReactorApplicationsV2.java index 6f4df0deb1d..8442997eea7 100644 --- a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/client/v2/applications/ReactorApplicationsV2.java +++ b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/client/v2/applications/ReactorApplicationsV2.java @@ -16,6 +16,12 @@ package org.cloudfoundry.reactor.client.v2.applications; +import static io.netty.handler.codec.http.HttpHeaderValues.APPLICATION_JSON; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + import org.cloudfoundry.client.v2.applications.ApplicationEnvironmentRequest; import org.cloudfoundry.client.v2.applications.ApplicationEnvironmentResponse; import org.cloudfoundry.client.v2.applications.ApplicationInstancesRequest; @@ -60,17 +66,11 @@ import org.cloudfoundry.reactor.client.v2.AbstractClientV2Operations; import org.cloudfoundry.reactor.util.MultipartHttpClientRequest; import org.cloudfoundry.util.FileUtils; + import reactor.core.Exceptions; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import reactor.ipc.netty.http.client.HttpClientRequest; - -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; - -import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_TYPE; -import static io.netty.handler.codec.http.HttpHeaderValues.APPLICATION_JSON; +import reactor.netty.ByteBufFlux; /** * The Reactor-based implementation of {@link ApplicationsV2} @@ -81,7 +81,7 @@ public final class ReactorApplicationsV2 extends AbstractClientV2Operations impl * Creates an instance * * @param connectionContext the {@link ConnectionContext} to use when communicating with the server - * @param root the root URI of the server. Typically something like {@code https://api.run.pivotal.io}. + * @param root the root URI of the server. Typically something like {@code https://api.run.pivotal.io}. * @param tokenProvider the {@link TokenProvider} to use when communicating with the server */ public ReactorApplicationsV2(ConnectionContext connectionContext, Mono root, TokenProvider tokenProvider) { @@ -90,175 +90,168 @@ public ReactorApplicationsV2(ConnectionContext connectionContext, Mono r @Override public Mono associateRoute(AssociateApplicationRouteRequest request) { - return put(request, AssociateApplicationRouteResponse.class, builder -> builder.pathSegment("apps", request.getApplicationId(), "routes", request.getRouteId())) - .checkpoint(); + return put(request, AssociateApplicationRouteResponse.class, + builder -> builder.pathSegment("apps", request.getApplicationId(), "routes", request.getRouteId())).checkpoint(); } @Override public Mono copy(CopyApplicationRequest request) { - return post(request, CopyApplicationResponse.class, builder -> builder.pathSegment("apps", request.getApplicationId(), "copy_bits")) - .checkpoint(); + return post(request, CopyApplicationResponse.class, + builder -> builder.pathSegment("apps", request.getApplicationId(), "copy_bits")).checkpoint(); } @Override public Mono create(CreateApplicationRequest request) { - return post(request, CreateApplicationResponse.class, builder -> builder.pathSegment("apps")) - .checkpoint(); + return post(request, CreateApplicationResponse.class, builder -> builder.pathSegment("apps")).checkpoint(); } @Override public Mono delete(DeleteApplicationRequest request) { - return delete(request, Void.class, builder -> builder.pathSegment("apps", request.getApplicationId())) - .checkpoint(); + return delete(request, Void.class, builder -> builder.pathSegment("apps", request.getApplicationId())).checkpoint(); } @Override public Flux download(DownloadApplicationRequest request) { - return get(request, builder -> builder.pathSegment("apps", request.getApplicationId(), "download"), outbound -> outbound.map(HttpClientRequest::followRedirect)) - .flatMapMany(response -> response.receive().asByteArray()) - .checkpoint(); + return get(request, builder -> builder.pathSegment("apps", request.getApplicationId(), "download"), + ByteBufFlux::asByteArray).checkpoint(); } @Override public Flux downloadDroplet(DownloadApplicationDropletRequest request) { - return get(request, builder -> builder.pathSegment("apps", request.getApplicationId(), "droplet", "download"), outbound -> outbound.map(HttpClientRequest::followRedirect)) - .flatMapMany(response -> response.receive().asByteArray()) - .checkpoint(); + return get(request, builder -> builder.pathSegment("apps", request.getApplicationId(), "droplet", "download"), + ByteBufFlux::asByteArray).checkpoint(); } @Override public Mono environment(ApplicationEnvironmentRequest request) { - return get(request, ApplicationEnvironmentResponse.class, builder -> builder.pathSegment("apps", request.getApplicationId(), "env")) - .checkpoint(); + return get(request, ApplicationEnvironmentResponse.class, + builder -> builder.pathSegment("apps", request.getApplicationId(), "env")).checkpoint(); } @Override public Mono get(GetApplicationRequest request) { - return get(request, GetApplicationResponse.class, builder -> builder.pathSegment("apps", request.getApplicationId())) - .checkpoint(); + return get(request, GetApplicationResponse.class, builder -> builder.pathSegment("apps", request.getApplicationId())).checkpoint(); } @Override public Mono getPermissions(GetApplicationPermissionsRequest request) { - return get(request, GetApplicationPermissionsResponse.class, builder -> builder.pathSegment("apps", request.getApplicationId(), "permissions")) - .checkpoint(); + return get(request, GetApplicationPermissionsResponse.class, + builder -> builder.pathSegment("apps", request.getApplicationId(), "permissions")).checkpoint(); } @Override public Mono instances(ApplicationInstancesRequest request) { - return get(request, ApplicationInstancesResponse.class, builder -> builder.pathSegment("apps", request.getApplicationId(), "instances")) - .checkpoint(); + return get(request, ApplicationInstancesResponse.class, + builder -> builder.pathSegment("apps", request.getApplicationId(), "instances")).checkpoint(); } @Override public Mono list(ListApplicationsRequest request) { - return get(request, ListApplicationsResponse.class, builder -> builder.pathSegment("apps")) - .checkpoint(); + return get(request, ListApplicationsResponse.class, builder -> builder.pathSegment("apps")).checkpoint(); } @Override public Mono listRoutes(ListApplicationRoutesRequest request) { - return get(request, ListApplicationRoutesResponse.class, builder -> builder.pathSegment("apps", request.getApplicationId(), "routes")) - .checkpoint(); + return get(request, ListApplicationRoutesResponse.class, + builder -> builder.pathSegment("apps", request.getApplicationId(), "routes")).checkpoint(); } @Override public Mono listServiceBindings(ListApplicationServiceBindingsRequest request) { - return get(request, ListApplicationServiceBindingsResponse.class, builder -> builder.pathSegment("apps", request.getApplicationId(), "service_bindings")) - .checkpoint(); + return get(request, ListApplicationServiceBindingsResponse.class, + builder -> builder.pathSegment("apps", request.getApplicationId(), "service_bindings")).checkpoint(); } @Override public Mono removeRoute(RemoveApplicationRouteRequest request) { - return delete(request, Void.class, builder -> builder.pathSegment("apps", request.getApplicationId(), "routes", request.getRouteId())) - .checkpoint(); + return delete(request, Void.class, + builder -> builder.pathSegment("apps", request.getApplicationId(), "routes", request.getRouteId())).checkpoint(); } @Override public Mono removeServiceBinding(RemoveApplicationServiceBindingRequest request) { - return delete(request, Void.class, builder -> builder.pathSegment("apps", request.getApplicationId(), "service_bindings", request.getServiceBindingId())) - .checkpoint(); + return delete(request, Void.class, builder -> builder.pathSegment("apps", request.getApplicationId(), "service_bindings", + request.getServiceBindingId())).checkpoint(); } @Override public Mono restage(RestageApplicationRequest request) { - return post(request, RestageApplicationResponse.class, builder -> builder.pathSegment("apps", request.getApplicationId(), "restage")) - .checkpoint(); + return post(request, RestageApplicationResponse.class, + builder -> builder.pathSegment("apps", request.getApplicationId(), "restage")).checkpoint(); } @Override public Mono statistics(ApplicationStatisticsRequest request) { - return get(request, ApplicationStatisticsResponse.class, builder -> builder.pathSegment("apps", request.getApplicationId(), "stats")) - .checkpoint(); + return get(request, ApplicationStatisticsResponse.class, + builder -> builder.pathSegment("apps", request.getApplicationId(), "stats")).checkpoint(); } @Override public Mono summary(SummaryApplicationRequest request) { - return get(request, SummaryApplicationResponse.class, builder -> builder.pathSegment("apps", request.getApplicationId(), "summary")) - .checkpoint(); + return get(request, SummaryApplicationResponse.class, + builder -> builder.pathSegment("apps", request.getApplicationId(), "summary")).checkpoint(); } @Override public Mono terminateInstance(TerminateApplicationInstanceRequest request) { - return delete(request, Void.class, builder -> builder.pathSegment("apps", request.getApplicationId(), "instances", request.getIndex())) - .checkpoint(); + return delete(request, Void.class, + builder -> builder.pathSegment("apps", request.getApplicationId(), "instances", request.getIndex())).checkpoint(); } @Override public Mono update(UpdateApplicationRequest request) { - return put(request, UpdateApplicationResponse.class, builder -> builder.pathSegment("apps", request.getApplicationId())) - .checkpoint(); + return put(request, UpdateApplicationResponse.class, + builder -> builder.pathSegment("apps", request.getApplicationId())).checkpoint(); } - @SuppressWarnings("unchecked") @Override public Mono upload(UploadApplicationRequest request) { - return put(request, UploadApplicationResponse.class, builder -> builder.pathSegment("apps", request.getApplicationId(), "bits"), - outbound -> outbound - .flatMap(r -> { - if (Files.isDirectory(request.getApplication())) { - return FileUtils.compress(request.getApplication()) - .flatMap(application -> upload(application, r, request) - .doOnTerminate(() -> { - try { - Files.delete(application); - } catch (IOException e) { - throw Exceptions.propagate(e); - } - })); - } else { - return upload(request.getApplication(), r, request); + Path application = request.getApplication(); + if (application.toFile().isDirectory()) { + return FileUtils.compress(application) + .map(temporaryFile -> UploadApplicationRequest.builder() + .from(request) + .application(temporaryFile) + .build()) + .flatMap(requestWithTemporaryFile -> upload(requestWithTemporaryFile, () -> { + try { + Files.delete(requestWithTemporaryFile.getApplication()); + } catch (IOException e) { + throw Exceptions.propagate(e); } - })) - .checkpoint(); + })); + } else { + return upload(request, () -> { + }); + } + } + + private Mono upload(UploadApplicationRequest request, Runnable onTerminate) { + return put(request, UploadApplicationResponse.class, builder -> builder.pathSegment("apps", request.getApplicationId(), "bits"), + multipartRequest -> upload(request.getApplication(), multipartRequest, request), onTerminate).checkpoint(); } @Override public Mono uploadDroplet(UploadApplicationDropletRequest request) { - return put(request, UploadApplicationDropletResponse.class, builder -> builder.pathSegment("apps", request.getApplicationId(), "droplet", "upload"), - outbound -> outbound - .flatMap(r -> upload(r, request))) - .checkpoint(); + return put(request, UploadApplicationDropletResponse.class, + builder -> builder.pathSegment("apps", request.getApplicationId(), "droplet", "upload"), + multipartRequest -> upload(multipartRequest, request), () -> { + }).checkpoint(); } - private Mono upload(Path application, MultipartHttpClientRequest r, UploadApplicationRequest request) { - return r - .addPart(part -> part - .setContentDispositionFormData("resources") - .setHeader(CONTENT_TYPE, APPLICATION_JSON) - .send(request.getResources())) - .addPart(part -> part - .setContentDispositionFormData("application", "application.zip") - .setHeader(CONTENT_TYPE, APPLICATION_ZIP) + private void upload(Path application, MultipartHttpClientRequest multipartRequest, UploadApplicationRequest request) { + multipartRequest.addPart(part -> part.setName("resources") + .setContentType(APPLICATION_JSON.toString()) + .send(request.getResources())) + .addPart(part -> part.setName("application") + .setContentType(APPLICATION_ZIP.toString()) .sendFile(application)) .done(); } - private Mono upload(MultipartHttpClientRequest r, UploadApplicationDropletRequest request) { - return r - .addPart(part -> part - .setContentDispositionFormData("droplet", request.getDroplet().getFileName().toString()) - .sendFile(request.getDroplet())) + private void upload(MultipartHttpClientRequest multipartRequest, UploadApplicationDropletRequest request) { + multipartRequest.addPart(part -> part.setName("droplet") + .sendFile(request.getDroplet())) .done(); } diff --git a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/client/v2/buildpacks/ReactorBuildpacks.java b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/client/v2/buildpacks/ReactorBuildpacks.java index 2341b0239ec..772a052d64c 100644 --- a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/client/v2/buildpacks/ReactorBuildpacks.java +++ b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/client/v2/buildpacks/ReactorBuildpacks.java @@ -16,6 +16,10 @@ package org.cloudfoundry.reactor.client.v2.buildpacks; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + import org.cloudfoundry.client.v2.buildpacks.Buildpacks; import org.cloudfoundry.client.v2.buildpacks.CreateBuildpackRequest; import org.cloudfoundry.client.v2.buildpacks.CreateBuildpackResponse; @@ -34,15 +38,10 @@ import org.cloudfoundry.reactor.client.v2.AbstractClientV2Operations; import org.cloudfoundry.reactor.util.MultipartHttpClientRequest; import org.cloudfoundry.util.FileUtils; + import reactor.core.Exceptions; import reactor.core.publisher.Mono; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; - -import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_TYPE; - /** * The Reactor-based implementation of {@link Buildpacks} */ @@ -52,7 +51,7 @@ public final class ReactorBuildpacks extends AbstractClientV2Operations implemen * Creates an instance * * @param connectionContext the {@link ConnectionContext} to use when communicating with the server - * @param root the root URI of the server. Typically something like {@code https://api.run.pivotal.io}. + * @param root the root URI of the server. Typically something like {@code https://api.run.pivotal.io}. * @param tokenProvider the {@link TokenProvider} to use when communicating with the server */ public ReactorBuildpacks(ConnectionContext connectionContext, Mono root, TokenProvider tokenProvider) { @@ -61,63 +60,64 @@ public ReactorBuildpacks(ConnectionContext connectionContext, Mono root, @Override public Mono create(CreateBuildpackRequest request) { - return post(request, CreateBuildpackResponse.class, builder -> builder.pathSegment("buildpacks")) - .checkpoint(); + return post(request, CreateBuildpackResponse.class, builder -> builder.pathSegment("buildpacks")).checkpoint(); } @Override public Mono delete(DeleteBuildpackRequest request) { - return delete(request, DeleteBuildpackResponse.class, builder -> builder.pathSegment("buildpacks", request.getBuildpackId())) - .checkpoint(); + return delete(request, DeleteBuildpackResponse.class, + builder -> builder.pathSegment("buildpacks", request.getBuildpackId())).checkpoint(); } @Override public Mono get(GetBuildpackRequest request) { - return get(request, GetBuildpackResponse.class, builder -> builder.pathSegment("buildpacks", request.getBuildpackId())) - .checkpoint(); + return get(request, GetBuildpackResponse.class, + builder -> builder.pathSegment("buildpacks", request.getBuildpackId())).checkpoint(); } @Override public Mono list(ListBuildpacksRequest request) { - return get(request, ListBuildpacksResponse.class, builder -> builder.pathSegment("buildpacks")) - .checkpoint(); + return get(request, ListBuildpacksResponse.class, builder -> builder.pathSegment("buildpacks")).checkpoint(); } @Override public Mono update(UpdateBuildpackRequest request) { - return put(request, UpdateBuildpackResponse.class, builder -> builder.pathSegment("buildpacks", request.getBuildpackId())) - .checkpoint(); + return put(request, UpdateBuildpackResponse.class, + builder -> builder.pathSegment("buildpacks", request.getBuildpackId())).checkpoint(); } - @SuppressWarnings("unchecked") @Override public Mono upload(UploadBuildpackRequest request) { - return put(request, UploadBuildpackResponse.class, builder -> builder.pathSegment("buildpacks", request.getBuildpackId(), "bits"), - outbound -> outbound - .flatMap(r -> { - if (Files.isDirectory(request.getBuildpack())) { - return FileUtils.compress(request.getBuildpack()) - .flatMap(buildpack -> upload(buildpack, r, request.getFilename() + ".zip") - .doOnTerminate(() -> { - try { - Files.delete(buildpack); - } catch (IOException e) { - throw Exceptions.propagate(e); - } - })); - } else { - return upload(request.getBuildpack(), r, request.getFilename()); + Path buildpack = request.getBuildpack(); + if (buildpack.toFile().isDirectory()) { + return FileUtils.compress(buildpack) + .map(temporaryFile -> UploadBuildpackRequest.builder() + .from(request) + .buildpack(temporaryFile) + .build()) + .flatMap(requestWithTemporaryFile -> upload(requestWithTemporaryFile, () -> { + try { + Files.delete(requestWithTemporaryFile.getBuildpack()); + } catch (IOException e) { + throw Exceptions.propagate(e); } - })) - .checkpoint(); + })); + } else { + return upload(request, () -> { + }); + } + } + + private Mono upload(UploadBuildpackRequest request, Runnable onTerminate) { + return put(request, UploadBuildpackResponse.class, builder -> builder.pathSegment("buildpacks", request.getBuildpackId(), "bits"), + multipartRequest -> upload(request.getBuildpack(), multipartRequest, request.getFilename()), onTerminate).checkpoint(); } - private Mono upload(Path buildpack, MultipartHttpClientRequest r, String filename) { - return r - .addPart(part -> part - .setContentDispositionFormData("buildpack", filename) - .setHeader(CONTENT_TYPE, APPLICATION_ZIP) - .sendFile(buildpack)) + private void upload(Path buildpack, MultipartHttpClientRequest multipartRequest, String filename) { + multipartRequest.addPart(part -> part.setName("buildpack") + .setFilename(filename) + .setContentType(APPLICATION_ZIP.toString()) + .sendFile(buildpack)) .done(); } diff --git a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/client/v3/AbstractClientV3Operations.java b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/client/v3/AbstractClientV3Operations.java index 675fc8d6955..28501c263e6 100644 --- a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/client/v3/AbstractClientV3Operations.java +++ b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/client/v3/AbstractClientV3Operations.java @@ -16,117 +16,134 @@ package org.cloudfoundry.reactor.client.v3; -import com.fasterxml.jackson.databind.ObjectMapper; -import io.netty.handler.codec.http.HttpHeaderNames; +import java.util.List; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.Function; + import org.cloudfoundry.reactor.ConnectionContext; import org.cloudfoundry.reactor.TokenProvider; import org.cloudfoundry.reactor.client.QueryBuilder; import org.cloudfoundry.reactor.util.AbstractReactorOperations; -import org.cloudfoundry.reactor.util.ErrorPayloadMapper; +import org.cloudfoundry.reactor.util.ErrorPayloadMappers; import org.cloudfoundry.reactor.util.MultipartHttpClientRequest; -import org.reactivestreams.Publisher; +import org.cloudfoundry.reactor.util.Operator; import org.springframework.web.util.UriComponentsBuilder; -import reactor.core.publisher.Mono; -import reactor.ipc.netty.http.client.HttpClientRequest; -import reactor.ipc.netty.http.client.HttpClientResponse; -import java.util.List; -import java.util.function.Function; +import io.netty.handler.codec.http.HttpHeaderNames; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.netty.ByteBufFlux; +import reactor.netty.http.client.HttpClientForm; +import reactor.netty.http.client.HttpClientRequest; +import reactor.netty.http.client.HttpClientResponse; public abstract class AbstractClientV3Operations extends AbstractReactorOperations { - private final ConnectionContext connectionContext; - protected AbstractClientV3Operations(ConnectionContext connectionContext, Mono root, TokenProvider tokenProvider) { super(connectionContext, root, tokenProvider); - this.connectionContext = connectionContext; } - protected final Mono delete(Object requestPayload, Class responseType, Function uriTransformer) { - return doDelete(requestPayload, responseType, - queryTransformer(requestPayload) - .andThen(uriTransformer), - outbound -> outbound, - ErrorPayloadMapper.clientV3(this.connectionContext.getObjectMapper())); + protected final Mono delete(Object requestPayload, Class responseType, + Function uriTransformer) { + return createOperator().flatMap(operator -> operator.delete() + .uri(queryTransformer(requestPayload).andThen(uriTransformer)) + .send(requestPayload) + .response() + .parseBody(responseType)); } protected final Mono delete(Object requestPayload, Function uriTransformer) { - return doDelete(requestPayload, - queryTransformer(requestPayload) - .andThen(uriTransformer), - outbound -> outbound, - inbound -> inbound) + return createOperator().flatMap(operator -> operator.delete() + .uri(queryTransformer(requestPayload).andThen(uriTransformer)) + .send(requestPayload) + .response() + .get()) .map(AbstractClientV3Operations::extractJobId); } - protected final Mono get(Object requestPayload, Class responseType, Function uriTransformer) { - return doGet(responseType, - queryTransformer(requestPayload) - .andThen(uriTransformer), - outbound -> outbound, - ErrorPayloadMapper.clientV3(this.connectionContext.getObjectMapper())); + protected final Mono get(Object requestPayload, Class responseType, + Function uriTransformer) { + return createOperator().flatMap(operator -> operator.get() + .uri(queryTransformer(requestPayload).andThen(uriTransformer)) + .response() + .parseBody(responseType)); } - protected final Mono get(Object requestPayload, Function uriTransformer) { - return doGet(queryTransformer(requestPayload) - .andThen(uriTransformer), - outbound -> outbound, - ErrorPayloadMapper.clientV3(this.connectionContext.getObjectMapper())); + protected final Flux get(Object requestPayload, Function uriTransformer, + Function> bodyTransformer) { + return createOperator().flatMapMany(operator -> operator.followRedirects() + .get() + .uri(queryTransformer(requestPayload).andThen(uriTransformer)) + .response() + .parseBodyToFlux(responseWithBody -> bodyTransformer.apply(responseWithBody.getBody()))); } - protected final Mono patch(Object requestPayload, Class responseType, Function uriTransformer) { - return doPatch(requestPayload, responseType, - queryTransformer(requestPayload) - .andThen(uriTransformer), - outbound -> outbound, - ErrorPayloadMapper.clientV3(this.connectionContext.getObjectMapper())); + protected final Mono patch(Object requestPayload, Class responseType, + Function uriTransformer) { + return createOperator().flatMap(operator -> operator.patch() + .uri(queryTransformer(requestPayload).andThen(uriTransformer)) + .send(requestPayload) + .response() + .parseBody(responseType)); } - protected Mono post(Object requestPayload, Class responseType, Function uriTransformer) { - return doPost(requestPayload, responseType, - queryTransformer(requestPayload) - .andThen(uriTransformer), - outbound -> outbound, - ErrorPayloadMapper.clientV3(this.connectionContext.getObjectMapper())); + protected Mono post(Object requestPayload, Class responseType, + Function uriTransformer) { + return createOperator().flatMap(operator -> operator.post() + .uri(queryTransformer(requestPayload).andThen(uriTransformer)) + .send(requestPayload) + .response() + .parseBody(responseType)); } - protected final Mono post(Object requestPayload, Class responseType, Function uriTransformer, - Function, Publisher> requestTransformer) { - return doPost(responseType, - queryTransformer(requestPayload) - .andThen(uriTransformer), - outbound -> outbound - .map(multipartRequest(this.connectionContext.getObjectMapper())) - .transform(requestTransformer), - ErrorPayloadMapper.clientV3(this.connectionContext.getObjectMapper())); + protected final Mono post(Object requestPayload, Class responseType, + Function uriTransformer, + Consumer requestTransformer, Runnable onTerminate) { + return createOperator().flatMap(operator -> operator.post() + .uri(queryTransformer(requestPayload).andThen(uriTransformer)) + .sendForm(multipartRequest(requestTransformer)) + .response() + .parseBody(responseType)) + .doFinally(signalType -> onTerminate.run()); } - protected final Mono put(Object requestPayload, Class responseType, Function uriTransformer) { - return doPut(requestPayload, responseType, - queryTransformer(requestPayload) - .andThen(uriTransformer), - outbound -> outbound, - ErrorPayloadMapper.clientV3(this.connectionContext.getObjectMapper())); + protected final Mono put(Object requestPayload, Class responseType, + Function uriTransformer) { + return createOperator().flatMap(operator -> operator.put() + .uri(queryTransformer(requestPayload).andThen(uriTransformer)) + .send(requestPayload) + .response() + .parseBody(responseType)); } - protected final Mono put(Object requestPayload, Class responseType, Function uriTransformer, - Function, Publisher> requestTransformer) { - return doPut(responseType, - queryTransformer(requestPayload) - .andThen(uriTransformer), - outbound -> outbound - .map(multipartRequest(this.connectionContext.getObjectMapper())) - .transform(requestTransformer), - ErrorPayloadMapper.clientV3(this.connectionContext.getObjectMapper())); + @Override + protected Mono createOperator() { + return super.createOperator().map(this::attachErrorPayloadMapper); } - private static String extractJobId(HttpClientResponse response) { - List pathSegments = UriComponentsBuilder.fromUriString(response.responseHeaders().get(HttpHeaderNames.LOCATION)).build().getPathSegments(); - return pathSegments.get(pathSegments.size() - 1); + private Operator attachErrorPayloadMapper(Operator operator) { + return operator.withErrorPayloadMapper(ErrorPayloadMappers.clientV3(this.connectionContext.getObjectMapper())); + } + + private BiConsumer + multipartRequest(Consumer requestTransformer) { + return (request, outbound) -> { + MultipartHttpClientRequest multipartRequest = createMultipartRequest(request, outbound); + requestTransformer.accept(multipartRequest); + }; } - private static Function multipartRequest(ObjectMapper objectMapper) { - return request -> new MultipartHttpClientRequest(objectMapper, request); + private MultipartHttpClientRequest createMultipartRequest(HttpClientRequest request, HttpClientForm form) { + return new MultipartHttpClientRequest(this.connectionContext.getObjectMapper(), request, form); + } + + private static String extractJobId(HttpClientResponse response) { + List pathSegments = UriComponentsBuilder.fromUriString(response.responseHeaders() + .get(HttpHeaderNames.LOCATION)) + .build() + .getPathSegments(); + return pathSegments.get(pathSegments.size() - 1); } private static Function queryTransformer(Object requestPayload) { diff --git a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/client/v3/packages/ReactorPackages.java b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/client/v3/packages/ReactorPackages.java index 67651ac4e24..7db3f30db77 100644 --- a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/client/v3/packages/ReactorPackages.java +++ b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/client/v3/packages/ReactorPackages.java @@ -16,6 +16,10 @@ package org.cloudfoundry.reactor.client.v3.packages; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + import org.cloudfoundry.client.v3.packages.CopyPackageRequest; import org.cloudfoundry.client.v3.packages.CopyPackageResponse; import org.cloudfoundry.client.v3.packages.CreatePackageRequest; @@ -36,15 +40,11 @@ import org.cloudfoundry.reactor.client.v3.AbstractClientV3Operations; import org.cloudfoundry.reactor.util.MultipartHttpClientRequest; import org.cloudfoundry.util.FileUtils; + import reactor.core.Exceptions; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; - -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; - -import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_TYPE; +import reactor.netty.ByteBufFlux; /** * The Reactor-based implementation of {@link Packages} @@ -55,7 +55,7 @@ public final class ReactorPackages extends AbstractClientV3Operations implements * Creates an instance * * @param connectionContext the {@link ConnectionContext} to use when communicating with the server - * @param root the root URI of the server. Typically something like {@code https://api.run.pivotal.io}. + * @param root the root URI of the server. Typically something like {@code https://api.run.pivotal.io}. * @param tokenProvider the {@link TokenProvider} to use when communicating with the server */ public ReactorPackages(ConnectionContext connectionContext, Mono root, TokenProvider tokenProvider) { @@ -64,76 +64,73 @@ public ReactorPackages(ConnectionContext connectionContext, Mono root, T @Override public Mono copy(CopyPackageRequest request) { - return post(request, CopyPackageResponse.class, builder -> builder.pathSegment("packages")) - .checkpoint(); + return post(request, CopyPackageResponse.class, builder -> builder.pathSegment("packages")).checkpoint(); } @Override public Mono create(CreatePackageRequest request) { - return post(request, CreatePackageResponse.class, builder -> builder.pathSegment("packages")) - .checkpoint(); + return post(request, CreatePackageResponse.class, builder -> builder.pathSegment("packages")).checkpoint(); } @Override public Mono delete(DeletePackageRequest request) { - return delete(request, builder -> builder.pathSegment("packages", request.getPackageId())) - .checkpoint(); + return delete(request, builder -> builder.pathSegment("packages", request.getPackageId())).checkpoint(); } @Override public Flux download(DownloadPackageRequest request) { - return get(request, builder -> builder.pathSegment("packages", request.getPackageId(), "download")) - .flatMapMany(response -> response.receive().aggregate().asByteArray()) - .checkpoint(); + return get(request, builder -> builder.pathSegment("packages", request.getPackageId(), "download"), + ByteBufFlux::asByteArray).checkpoint(); } @Override public Mono get(GetPackageRequest request) { - return get(request, GetPackageResponse.class, builder -> builder.pathSegment("packages", request.getPackageId())) - .checkpoint(); + return get(request, GetPackageResponse.class, builder -> builder.pathSegment("packages", request.getPackageId())).checkpoint(); } @Override public Mono list(ListPackagesRequest request) { - return get(request, ListPackagesResponse.class, builder -> builder.pathSegment("packages")) - .checkpoint(); + return get(request, ListPackagesResponse.class, builder -> builder.pathSegment("packages")).checkpoint(); } @Override public Mono listDroplets(ListPackageDropletsRequest request) { - return get(request, ListPackageDropletsResponse.class, builder -> builder.pathSegment("packages", request.getPackageId(), "droplets")) - .checkpoint(); + return get(request, ListPackageDropletsResponse.class, + builder -> builder.pathSegment("packages", request.getPackageId(), "droplets")).checkpoint(); } @Override public Mono upload(UploadPackageRequest request) { - return post(request, UploadPackageResponse.class, builder -> builder.pathSegment("packages", request.getPackageId(), "upload"), - outbound -> outbound - .flatMap(r -> { - if (Files.isDirectory(request.getBits())) { - return FileUtils.compress(request.getBits()) - .flatMap(bits -> upload(bits, r) - .doOnTerminate(() -> { - try { - Files.delete(bits); - } catch (IOException e) { - throw Exceptions.propagate(e); - } - }) - ); - } else { - return upload(request.getBits(), r); + Path bits = request.getBits(); + if (bits.toFile() + .isDirectory()) { + return FileUtils.compress(bits) + .map(temporaryFile -> UploadPackageRequest.builder() + .from(request) + .bits(temporaryFile) + .build()) + .flatMap(requestWithTemporaryFile -> upload(requestWithTemporaryFile, () -> { + try { + Files.delete(requestWithTemporaryFile.getBits()); + } catch (IOException e) { + throw Exceptions.propagate(e); } - })) - .checkpoint(); + })); + } else { + return upload(request, () -> { + }); + } + } + + private Mono upload(UploadPackageRequest request, Runnable onTerminate) { + return post(request, UploadPackageResponse.class, builder -> builder.pathSegment("packages", request.getPackageId(), "upload"), + outbound -> upload(request.getBits(), outbound), onTerminate).checkpoint(); } - private Mono upload(Path bits, MultipartHttpClientRequest r) { - return r - .addPart(part -> part - .setContentDispositionFormData("bits", "application.zip") - .setHeader(CONTENT_TYPE, APPLICATION_ZIP) - .sendFile(bits)) + private void upload(Path bits, MultipartHttpClientRequest r) { + r.addPart(part -> part.setName("bits") + .setContentType(APPLICATION_ZIP.toString()) + .sendFile(bits)) .done(); } diff --git a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/doppler/AbstractDopplerOperations.java b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/doppler/AbstractDopplerOperations.java index 42c55360b51..a0b4b53d386 100644 --- a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/doppler/AbstractDopplerOperations.java +++ b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/doppler/AbstractDopplerOperations.java @@ -16,14 +16,19 @@ package org.cloudfoundry.reactor.doppler; +import java.io.InputStream; +import java.util.function.Function; + import org.cloudfoundry.reactor.ConnectionContext; import org.cloudfoundry.reactor.TokenProvider; import org.cloudfoundry.reactor.util.AbstractReactorOperations; import org.springframework.web.util.UriComponentsBuilder; -import reactor.core.publisher.Mono; -import reactor.ipc.netty.http.client.HttpClientResponse; -import java.util.function.Function; +import io.netty.channel.ChannelHandler; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.netty.ByteBufFlux; +import reactor.netty.http.client.HttpClientResponse; abstract class AbstractDopplerOperations extends AbstractReactorOperations { @@ -31,12 +36,20 @@ abstract class AbstractDopplerOperations extends AbstractReactorOperations { super(connectionContext, root, tokenProvider); } - final Mono get(Function uriTransformer) { - return doGet(uriTransformer, outbound -> outbound, inbound -> inbound); + final Flux get(Function uriTransformer, + Function channelHandlerBuilder, + Function> bodyTransformer) { + return createOperator().flatMapMany(operator -> operator.get() + .uri(uriTransformer) + .response() + .addChannelHandler(channelHandlerBuilder) + .parseBodyToFlux(responseWithBody -> bodyTransformer.apply(responseWithBody.getBody()))); } - final Mono ws(Function uriTransformer) { - return doWs(uriTransformer, outbound -> outbound, inbound -> inbound); + final Flux ws(Function uriTransformer) { + return createOperator().flatMapMany(operator -> operator.websocket() + .uri(uriTransformer) + .get()); } } diff --git a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/doppler/MultipartCodec.java b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/doppler/MultipartCodec.java index 6ea3afc144b..d3aa859a1f5 100644 --- a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/doppler/MultipartCodec.java +++ b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/doppler/MultipartCodec.java @@ -16,17 +16,18 @@ package org.cloudfoundry.reactor.doppler; -import io.netty.buffer.Unpooled; -import io.netty.handler.codec.DelimiterBasedFrameDecoder; -import io.netty.handler.codec.http.HttpHeaderNames; -import reactor.core.publisher.Flux; -import reactor.ipc.netty.http.client.HttpClientResponse; - import java.io.InputStream; import java.nio.charset.Charset; import java.util.regex.Matcher; import java.util.regex.Pattern; +import io.netty.buffer.Unpooled; +import io.netty.handler.codec.DelimiterBasedFrameDecoder; +import io.netty.handler.codec.http.HttpHeaderNames; +import reactor.core.publisher.Flux; +import reactor.netty.ByteBufFlux; +import reactor.netty.http.client.HttpClientResponse; + final class MultipartCodec { private static final Pattern BOUNDARY_PATTERN = Pattern.compile("multipart/.+; boundary=(.*)"); @@ -36,27 +37,24 @@ final class MultipartCodec { private MultipartCodec() { } - static Flux decode(HttpClientResponse response) { - return response - .addHandler(createDecoder(response)) - .receive() - .asInputStream() + static Flux decode(ByteBufFlux body) { + return body.asInputStream() .skip(1); } - private static DelimiterBasedFrameDecoder createDecoder(HttpClientResponse response) { + static DelimiterBasedFrameDecoder createDecoder(HttpClientResponse response) { String boundary = extractMultipartBoundary(response); return new DelimiterBasedFrameDecoder(MAX_PAYLOAD_SIZE, Unpooled.copiedBuffer(String.format("--%s\r\n\r\n", boundary), Charset.defaultCharset()), Unpooled.copiedBuffer(String.format("\r\n--%s\r\n\r\n", boundary), Charset.defaultCharset()), Unpooled.copiedBuffer(String.format("\r\n--%s--", boundary), Charset.defaultCharset()), - Unpooled.copiedBuffer(String.format("\r\n--%s--\r\n", boundary), Charset.defaultCharset()) - ); + Unpooled.copiedBuffer(String.format("\r\n--%s--\r\n", boundary), Charset.defaultCharset())); } private static String extractMultipartBoundary(HttpClientResponse response) { - String contentType = response.responseHeaders().get(HttpHeaderNames.CONTENT_TYPE); + String contentType = response.responseHeaders() + .get(HttpHeaderNames.CONTENT_TYPE); Matcher matcher = BOUNDARY_PATTERN.matcher(contentType); if (matcher.matches()) { return matcher.group(1); diff --git a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/doppler/ReactorDopplerEndpoints.java b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/doppler/ReactorDopplerEndpoints.java index d186951c452..c59cb1592aa 100644 --- a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/doppler/ReactorDopplerEndpoints.java +++ b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/doppler/ReactorDopplerEndpoints.java @@ -16,6 +16,9 @@ package org.cloudfoundry.reactor.doppler; +import java.io.IOException; +import java.io.InputStream; + import org.cloudfoundry.doppler.ContainerMetricsRequest; import org.cloudfoundry.doppler.Envelope; import org.cloudfoundry.doppler.FirehoseRequest; @@ -23,13 +26,11 @@ import org.cloudfoundry.doppler.StreamRequest; import org.cloudfoundry.reactor.ConnectionContext; import org.cloudfoundry.reactor.TokenProvider; + import reactor.core.Exceptions; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import java.io.IOException; -import java.io.InputStream; - final class ReactorDopplerEndpoints extends AbstractDopplerOperations { ReactorDopplerEndpoints(ConnectionContext connectionContext, Mono root, TokenProvider tokenProvider) { @@ -37,30 +38,24 @@ final class ReactorDopplerEndpoints extends AbstractDopplerOperations { } Flux containerMetrics(ContainerMetricsRequest request) { - return get(builder -> builder.pathSegment("apps", request.getApplicationId(), "containermetrics")) - .flatMapMany(response -> MultipartCodec.decode(response) - .map(ReactorDopplerEndpoints::toEnvelope)) + return get(builder -> builder.pathSegment("apps", request.getApplicationId(), "containermetrics"), MultipartCodec::createDecoder, + MultipartCodec::decode).map(ReactorDopplerEndpoints::toEnvelope) .checkpoint(); } Flux firehose(FirehoseRequest request) { - return ws(builder -> builder.pathSegment("firehose", request.getSubscriptionId())) - .flatMapMany(response -> response.receiveWebsocket().aggregateFrames().receive().asInputStream() - .map(ReactorDopplerEndpoints::toEnvelope)) + return ws(builder -> builder.pathSegment("firehose", request.getSubscriptionId())).map(ReactorDopplerEndpoints::toEnvelope) .checkpoint(); } Flux recentLogs(RecentLogsRequest request) { - return get(builder -> builder.pathSegment("apps", request.getApplicationId(), "recentlogs")) - .flatMapMany(response -> MultipartCodec.decode(response) - .map(ReactorDopplerEndpoints::toEnvelope)) + return get(builder -> builder.pathSegment("apps", request.getApplicationId(), "recentlogs"), MultipartCodec::createDecoder, + MultipartCodec::decode).map(ReactorDopplerEndpoints::toEnvelope) .checkpoint(); } Flux stream(StreamRequest request) { - return ws(builder -> builder.pathSegment("apps", request.getApplicationId(), "stream")) - .flatMapMany(response -> response.receiveWebsocket().aggregateFrames().receive().asInputStream() - .map(ReactorDopplerEndpoints::toEnvelope)) + return ws(builder -> builder.pathSegment("apps", request.getApplicationId(), "stream")).map(ReactorDopplerEndpoints::toEnvelope) .checkpoint(); } diff --git a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/networking/AbstractNetworkingOperations.java b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/networking/AbstractNetworkingOperations.java index 9a691c34b51..bffae127a82 100644 --- a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/networking/AbstractNetworkingOperations.java +++ b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/networking/AbstractNetworkingOperations.java @@ -16,15 +16,15 @@ package org.cloudfoundry.reactor.networking; +import java.util.function.Function; + import org.cloudfoundry.reactor.ConnectionContext; import org.cloudfoundry.reactor.TokenProvider; import org.cloudfoundry.reactor.client.QueryBuilder; import org.cloudfoundry.reactor.util.AbstractReactorOperations; import org.springframework.web.util.UriComponentsBuilder; -import reactor.core.publisher.Mono; -import reactor.ipc.netty.http.client.HttpClientResponse; -import java.util.function.Function; +import reactor.core.publisher.Mono; public abstract class AbstractNetworkingOperations extends AbstractReactorOperations { @@ -33,27 +33,36 @@ protected AbstractNetworkingOperations(ConnectionContext connectionContext, Mono } protected final Mono get(Class responseType, Function uriTransformer) { - return doGet(responseType, uriTransformer, outbound -> outbound, inbound -> inbound); - } - - protected final Mono get(Object requestPayload, Class responseType, Function uriTransformer) { - return doGet(responseType, - queryTransformer(requestPayload) - .andThen(uriTransformer), - outbound -> outbound, - inbound -> inbound); + return createOperator().flatMap(operator -> operator.get() + .uri(uriTransformer) + .response() + .parseBody(responseType)); } - protected final Mono get(Function uriTransformer) { - return doGet(uriTransformer, outbound -> outbound, inbound -> inbound); + protected final Mono get(Object requestPayload, Class responseType, + Function uriTransformer) { + return createOperator().flatMap(operator -> operator.get() + .uri(queryTransformer(requestPayload).andThen(uriTransformer)) + .response() + .parseBody(responseType)); } - protected final Mono post(Object request, Class responseType, Function uriTransformer) { - return doPost(request, responseType, uriTransformer, outbound -> outbound, inbound -> inbound); + protected final Mono post(Object request, Class responseType, + Function uriTransformer) { + return createOperator().flatMap(operator -> operator.post() + .uri(uriTransformer) + .send(request) + .response() + .parseBody(responseType)); } - protected final Mono put(Object requestPayload, Class responseType, Function uriTransformer) { - return doPut(requestPayload, responseType, uriTransformer, outbound -> outbound, inbound -> inbound); + protected final Mono put(Object requestPayload, Class responseType, + Function uriTransformer) { + return createOperator().flatMap(operator -> operator.put() + .uri(uriTransformer) + .send(requestPayload) + .response() + .parseBody(responseType)); } private static Function queryTransformer(Object requestPayload) { diff --git a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/routing/v1/AbstractRoutingV1Operations.java b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/routing/v1/AbstractRoutingV1Operations.java index 1a173e62607..4a9909cefa9 100644 --- a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/routing/v1/AbstractRoutingV1Operations.java +++ b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/routing/v1/AbstractRoutingV1Operations.java @@ -16,14 +16,18 @@ package org.cloudfoundry.reactor.routing.v1; +import java.util.function.Function; + import org.cloudfoundry.reactor.ConnectionContext; import org.cloudfoundry.reactor.TokenProvider; import org.cloudfoundry.reactor.util.AbstractReactorOperations; import org.springframework.web.util.UriComponentsBuilder; -import reactor.core.publisher.Mono; -import reactor.ipc.netty.http.client.HttpClientResponse; -import java.util.function.Function; +import io.netty.channel.ChannelHandler; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.netty.ByteBufFlux; +import reactor.netty.http.client.HttpClientResponse; public abstract class AbstractRoutingV1Operations extends AbstractReactorOperations { @@ -32,19 +36,38 @@ protected AbstractRoutingV1Operations(ConnectionContext connectionContext, Mono< } protected final Mono get(Class responseType, Function uriTransformer) { - return doGet(responseType, uriTransformer, outbound -> outbound, inbound -> inbound); + return createOperator().flatMap(operator -> operator.get() + .uri(uriTransformer) + .response() + .parseBody(responseType)); } - protected final Mono get(Function uriTransformer) { - return doGet(uriTransformer, outbound -> outbound, inbound -> inbound); + protected final Flux get(Function handlerBuilder, + Function uriTransformer, + Function> bodyTransformer) { + return createOperator().flatMapMany(operator -> operator.get() + .uri(uriTransformer) + .response() + .addChannelHandler(handlerBuilder) + .parseBodyToFlux(responseWithBody -> bodyTransformer.apply(responseWithBody.getBody()))); } - protected final Mono post(Object request, Class responseType, Function uriTransformer) { - return doPost(request, responseType, uriTransformer, outbound -> outbound, inbound -> inbound); + protected final Mono post(Object request, Class responseType, + Function uriTransformer) { + return createOperator().flatMap(operator -> operator.post() + .uri(uriTransformer) + .send(request) + .response() + .parseBody(responseType)); } - protected final Mono put(Object requestPayload, Class responseType, Function uriTransformer) { - return doPut(requestPayload, responseType, uriTransformer, outbound -> outbound, inbound -> inbound); + protected final Mono put(Object requestPayload, Class responseType, + Function uriTransformer) { + return createOperator().flatMap(operator -> operator.put() + .uri(uriTransformer) + .send(requestPayload) + .response() + .parseBody(responseType)); } } diff --git a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/routing/v1/tcproutes/EventStreamCodec.java b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/routing/v1/tcproutes/EventStreamCodec.java index e1227538445..7e321e98f05 100644 --- a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/routing/v1/tcproutes/EventStreamCodec.java +++ b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/routing/v1/tcproutes/EventStreamCodec.java @@ -18,7 +18,8 @@ import io.netty.handler.codec.LineBasedFrameDecoder; import reactor.core.publisher.Flux; -import reactor.ipc.netty.http.client.HttpClientResponse; +import reactor.netty.ByteBufFlux; +import reactor.netty.http.client.HttpClientResponse; final class EventStreamCodec { @@ -27,8 +28,8 @@ final class EventStreamCodec { private EventStreamCodec() { } - static Flux decode(HttpClientResponse response) { - return response.addHandler(createDecoder()).receive().asString() + static Flux decode(ByteBufFlux body) { + return body.asString() .windowWhile(s -> !s.isEmpty()) .concatMap(window -> window .reduce(ServerSentEvent.builder(), EventStreamCodec::parseLine)) @@ -36,7 +37,7 @@ static Flux decode(HttpClientResponse response) { .filter(sse -> sse.getData() != null || sse.getEventType() != null || sse.getId() != null || sse.getRetry() != null); } - private static LineBasedFrameDecoder createDecoder() { + static LineBasedFrameDecoder createDecoder(HttpClientResponse response) { return new LineBasedFrameDecoder(MAX_PAYLOAD_SIZE); } diff --git a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/routing/v1/tcproutes/ReactorTcpRoutes.java b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/routing/v1/tcproutes/ReactorTcpRoutes.java index 123290b6784..a0280ca053a 100644 --- a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/routing/v1/tcproutes/ReactorTcpRoutes.java +++ b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/routing/v1/tcproutes/ReactorTcpRoutes.java @@ -16,6 +16,8 @@ package org.cloudfoundry.reactor.routing.v1.tcproutes; +import java.io.IOException; + import org.cloudfoundry.reactor.ConnectionContext; import org.cloudfoundry.reactor.TokenProvider; import org.cloudfoundry.reactor.routing.v1.AbstractRoutingV1Operations; @@ -28,50 +30,46 @@ import org.cloudfoundry.routing.v1.tcproutes.ListTcpRoutesResponse; import org.cloudfoundry.routing.v1.tcproutes.TcpRouteEvent; import org.cloudfoundry.routing.v1.tcproutes.TcpRoutes; + import reactor.core.Exceptions; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import java.io.IOException; - /** * The Reactor-based implementation of {@link TcpRoutes} */ public class ReactorTcpRoutes extends AbstractRoutingV1Operations implements TcpRoutes { - private final ConnectionContext connectionContext; - /** * Creates an instance * * @param connectionContext the {@link ConnectionContext} to use when communicating with the server - * @param root the root URI of the server. Typically something like {@code https://api.run.pivotal.io}. + * @param root the root URI of the server. Typically something like {@code https://api.run.pivotal.io}. * @param tokenProvider the {@link TokenProvider} to use when communicating with the server */ public ReactorTcpRoutes(ConnectionContext connectionContext, Mono root, TokenProvider tokenProvider) { super(connectionContext, root, tokenProvider); - this.connectionContext = connectionContext; } @Override public Mono create(CreateTcpRoutesRequest request) { - return post(request, CreateTcpRoutesResponse.class, builder -> builder.pathSegment("v1", "tcp_routes", "create")) - .checkpoint(); + return post(request, CreateTcpRoutesResponse.class, builder -> builder.pathSegment("v1", "tcp_routes", "create")).checkpoint(); } @Override public Mono delete(DeleteTcpRoutesRequest request) { - return post(request, Void.class, builder -> builder.pathSegment("v1", "tcp_routes", "delete")) - .checkpoint(); + return post(request, Void.class, builder -> builder.pathSegment("v1", "tcp_routes", "delete")).checkpoint(); } @Override public Flux events(EventsRequest request) { - return get(builder -> builder.pathSegment("v1", "tcp_routes", "events")) - .flatMapMany(EventStreamCodec::decode) + return get(EventStreamCodec::createDecoder, + builder -> builder.pathSegment("v1", "tcp_routes", "events"), EventStreamCodec::decode) .map(event -> { try { - return this.connectionContext.getObjectMapper().readValue(event.getData(), TcpRouteEvent.Builder.class) + return this.connectionContext.getObjectMapper() + .readValue(event.getData(), + TcpRouteEvent.Builder.class) .eventType(EventType.from(event.getEventType())) .build(); } catch (IOException e) { @@ -83,8 +81,7 @@ public Flux events(EventsRequest request) { @Override public Mono list(ListTcpRoutesRequest request) { - return get(ListTcpRoutesResponse.class, builder -> builder.pathSegment("v1", "tcp_routes")) - .checkpoint(); + return get(ListTcpRoutesResponse.class, builder -> builder.pathSegment("v1", "tcp_routes")).checkpoint(); } } diff --git a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/tokenprovider/AbstractUaaTokenProvider.java b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/tokenprovider/AbstractUaaTokenProvider.java index 330e9b92f25..995c104c8b3 100644 --- a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/tokenprovider/AbstractUaaTokenProvider.java +++ b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/tokenprovider/AbstractUaaTokenProvider.java @@ -16,46 +16,52 @@ package org.cloudfoundry.reactor.tokenprovider; -import io.jsonwebtoken.Claims; -import io.jsonwebtoken.Jwts; -import io.netty.util.AsciiString; +import static io.netty.handler.codec.http.HttpHeaderNames.AUTHORIZATION; +import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_TYPE; +import static io.netty.handler.codec.http.HttpHeaderValues.APPLICATION_X_WWW_FORM_URLENCODED; + +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.Base64; +import java.util.Date; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.Function; + import org.cloudfoundry.Nullable; import org.cloudfoundry.reactor.ConnectionContext; import org.cloudfoundry.reactor.TokenProvider; -import org.cloudfoundry.reactor.util.ErrorPayloadMapper; +import org.cloudfoundry.reactor.util.ErrorPayloadMappers; import org.cloudfoundry.reactor.util.JsonCodec; -import org.cloudfoundry.reactor.util.NetworkLogging; +import org.cloudfoundry.reactor.util.Operator; +import org.cloudfoundry.reactor.util.OperatorContext; import org.cloudfoundry.reactor.util.UserAgent; import org.cloudfoundry.uaa.UaaException; import org.immutables.value.Value; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.web.util.UriComponentsBuilder; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.netty.handler.codec.http.HttpHeaders; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.netty.util.AsciiString; import reactor.core.publisher.Flux; import reactor.core.publisher.FluxSink; import reactor.core.publisher.Mono; import reactor.core.publisher.ReplayProcessor; -import reactor.ipc.netty.http.client.HttpClientRequest; -import reactor.ipc.netty.http.client.HttpClientResponse; - -import java.time.LocalDateTime; -import java.time.ZoneId; -import java.util.Base64; -import java.util.Date; -import java.util.Map; -import java.util.Optional; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; -import java.util.function.Consumer; -import java.util.function.Function; - -import static io.netty.handler.codec.http.HttpHeaderNames.AUTHORIZATION; -import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_TYPE; -import static io.netty.handler.codec.http.HttpHeaderValues.APPLICATION_X_WWW_FORM_URLENCODED; -import static io.netty.handler.codec.http.HttpResponseStatus.UNAUTHORIZED; +import reactor.netty.ByteBufFlux; +import reactor.netty.http.client.HttpClientForm; +import reactor.netty.http.client.HttpClientRequest; /** - * An abstract base class for all token providers that interact with the UAA. It encapsulates the logic to refresh the token before expiration. + * An abstract base class for all token providers that interact with the UAA. It encapsulates the logic to refresh the token before + * expiration. */ public abstract class AbstractUaaTokenProvider implements TokenProvider { @@ -120,25 +126,15 @@ public void invalidate(ConnectionContext connectionContext) { abstract String getIdentityZoneSubdomain(); /** - * Transforms a {@code Mono} in order to make a request to negotiate an access token + * Transforms an {@code HttpClientRequest} and an {@code HttpClientForm} in order to make a request that negotiates an access token. * - * @param outbound the {@link Mono} to transform to perform the token request + * @param request the {@link HttpClientRequest} to transform to perform the token request + * @param form the {@link HttpClientForm} to transform to perform the token request */ - abstract Mono tokenRequestTransformer(Mono outbound); + abstract void tokenRequestTransformer(HttpClientRequest request, HttpClientForm form); - private static HttpClientRequest addContentType(HttpClientRequest request) { - return request - .header(CONTENT_TYPE, APPLICATION_X_WWW_FORM_URLENCODED); - } - - private static HttpClientRequest disableChunkedTransfer(HttpClientRequest request) { - return request.chunkedTransfer(false); - } - - private static HttpClientRequest disableFailOnError(HttpClientRequest request) { - return request - .failOnClientError(false) - .failOnServerError(false); + private static void setContentType(HttpHeaders httpHeaders) { + httpHeaders.set(CONTENT_TYPE, APPLICATION_X_WWW_FORM_URLENCODED); } private static String extractAccessToken(Map payload) { @@ -147,44 +143,47 @@ private static String extractAccessToken(Map payload) { if (LOGGER.isDebugEnabled()) { LOGGER.debug("Access Token: {}", accessToken); - parseToken(accessToken) - .ifPresent(claims -> { - LOGGER.debug("Access Token Issued At: {} UTC", toLocalDateTime(claims.getIssuedAt())); - LOGGER.debug("Access Token Expires At: {} UTC", toLocalDateTime(claims.getExpiration())); - }); + parseToken(accessToken).ifPresent(claims -> { + LOGGER.debug("Access Token Issued At: {} UTC", toLocalDateTime(claims.getIssuedAt())); + LOGGER.debug("Access Token Expires At: {} UTC", toLocalDateTime(claims.getExpiration())); + }); } return String.format("%s %s", payload.get(TOKEN_TYPE), accessToken); } - private static String getTokenUri(String root, String identityZoneId) { - UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(root); - - if (identityZoneId != null) { - builder.host(String.format("%s.%s", identityZoneId, builder.build().getHost())); - } - - return builder - .pathSegment("oauth", "token") - .build().encode().toUriString(); + private static Function tokenUriTransformer(String identityZoneId) { + return root -> { + if (identityZoneId != null) { + root.host(String.format("%s.%s", identityZoneId, root.build() + .getHost())); + } + return root.pathSegment("oauth", "token"); + }; } private static Optional parseToken(String token) { try { String jws = token.substring(0, token.lastIndexOf('.') + 1); - return Optional.of(Jwts.parser().parseClaimsJwt(jws).getBody()); + return Optional.of(Jwts.parser() + .parseClaimsJwt(jws) + .getBody()); } catch (Exception e) { return Optional.empty(); } } private static LocalDateTime toLocalDateTime(Date date) { - return LocalDateTime.from(date.toInstant().atZone(UTC)); + return LocalDateTime.from(date.toInstant() + .atZone(UTC)); } - private HttpClientRequest addAuthorization(HttpClientRequest request) { - String encoded = Base64.getEncoder().encodeToString(new AsciiString(getClientId()).concat(":").concat(getClientSecret()).toByteArray()); - return request.header(AUTHORIZATION, String.format("Basic %s", encoded)); + private void setAuthorization(HttpHeaders headers) { + String encoded = Base64.getEncoder() + .encodeToString(new AsciiString(getClientId()).concat(":") + .concat(getClientSecret()) + .toByteArray()); + headers.set(AUTHORIZATION, String.format("Basic %s", encoded)); } private Consumer> extractRefreshToken(ConnectionContext connectionContext) { @@ -193,11 +192,10 @@ private Consumer> extractRefreshToken(ConnectionContext conn if (LOGGER.isDebugEnabled()) { LOGGER.debug("Refresh Token: {}", refreshToken); - parseToken(refreshToken) - .ifPresent(claims -> { - LOGGER.debug("Refresh Token Issued At: {} UTC", toLocalDateTime(claims.getIssuedAt())); - LOGGER.debug("Refresh Token Expires At: {} UTC", toLocalDateTime(claims.getExpiration())); - }); + parseToken(refreshToken).ifPresent(claims -> { + LOGGER.debug("Refresh Token Issued At: {} UTC", toLocalDateTime(claims.getIssuedAt())); + LOGGER.debug("Refresh Token Expires At: {} UTC", toLocalDateTime(claims.getExpiration())); + }); } this.refreshTokens.put(connectionContext, Mono.just(refreshToken)); @@ -206,9 +204,8 @@ private Consumer> extractRefreshToken(ConnectionContext conn } @SuppressWarnings("unchecked") - private Function, Mono> extractTokens(ConnectionContext connectionContext) { - return inbound -> inbound - .transform(JsonCodec.decode(connectionContext.getObjectMapper(), Map.class)) + private Function> tokensExtractor(ConnectionContext connectionContext) { + return body -> JsonCodec.decode(connectionContext.getObjectMapper(), body, Map.class) .map(payload -> (Map) payload) .doOnNext(extractRefreshToken(connectionContext)) .map(AbstractUaaTokenProvider::extractAccessToken); @@ -218,53 +215,55 @@ private RefreshToken getRefreshTokenStream(ConnectionContext connectionContext) return this.refreshTokenStreams.computeIfAbsent(connectionContext, c -> new RefreshToken()); } - private Mono primaryToken(ConnectionContext connectionContext) { - return requestToken(connectionContext, this::tokenRequestTransformer); + private Mono primaryToken(ConnectionContext connectionContext) { + return requestToken(connectionContext, this::tokenRequestTransformer, tokensExtractor(connectionContext)); } - private Mono refreshToken(ConnectionContext connectionContext, String refreshToken) { - return requestToken(connectionContext, refreshTokenGrantTokenRequestTransformer(refreshToken)) - .onErrorResume(t -> t instanceof UaaException && ((UaaException) t).getStatusCode() == UNAUTHORIZED.code(), t -> Mono.empty()); + private Mono refreshToken(ConnectionContext connectionContext, String refreshToken) { + return requestToken(connectionContext, refreshTokenGrantTokenRequestTransformer(refreshToken), + tokensExtractor(connectionContext)).onErrorResume(t -> t instanceof UaaException && ((UaaException) t).getStatusCode() == HttpResponseStatus.UNAUTHORIZED.code(), t -> Mono.empty()); } - private Function, Mono> refreshTokenGrantTokenRequestTransformer(String refreshToken) { - return outbound -> outbound - .flatMap(request -> request - .sendForm(form -> form - .multipart(false) - .attr("client_id", getClientId()) - .attr("client_secret", getClientSecret()) - .attr("grant_type", "refresh_token") - .attr("refresh_token", refreshToken)) - .then()); + private BiConsumer refreshTokenGrantTokenRequestTransformer(String refreshToken) { + return (request, form) -> form.multipart(false) + .attr("client_id", getClientId()) + .attr("client_secret", getClientSecret()) + .attr("grant_type", "refresh_token") + .attr("refresh_token", refreshToken); } - private Mono requestToken(ConnectionContext connectionContext, Function, Mono> tokenRequestTransformer) { + private Mono requestToken(ConnectionContext connectionContext, + BiConsumer tokenRequestTransformer, + Function> tokenExtractor) { return connectionContext.getRootProvider() .getRoot(AUTHORIZATION_ENDPOINT, connectionContext) - .map(root -> getTokenUri(root, getIdentityZoneSubdomain())) - .flatMap(uri -> connectionContext.getHttpClient() - .post(uri, request -> Mono.just(request) - .map(AbstractUaaTokenProvider::disableChunkedTransfer) - .map(AbstractUaaTokenProvider::disableFailOnError) - .map(this::addAuthorization) - .map(UserAgent::addUserAgent) - .map(AbstractUaaTokenProvider::addContentType) - .map(JsonCodec::addDecodeHeaders) - .transform(tokenRequestTransformer)) - .doOnSubscribe(NetworkLogging.post(uri)) - .transform(NetworkLogging.response(uri))) - .transform(ErrorPayloadMapper.uaa(connectionContext.getObjectMapper())); + .map(root -> createOperator(connectionContext, root)) + .flatMap(operator -> operator.headers(this::addHeaders) + .post() + .uri(tokenUriTransformer(getIdentityZoneSubdomain())) + .sendForm(tokenRequestTransformer) + .response() + .parseBodyToMono(responseWithBody -> tokenExtractor.apply(responseWithBody.getBody()))); + } + + private Operator createOperator(ConnectionContext connectionContext, String root) { + OperatorContext context = OperatorContext.of(connectionContext, root); + return new Operator(context, + connectionContext.getHttpClient()).withErrorPayloadMapper(ErrorPayloadMappers.uaa(connectionContext.getObjectMapper())); + } + + private void addHeaders(HttpHeaders httpHeaders) { + setContentType(httpHeaders); + setAuthorization(httpHeaders); + UserAgent.setUserAgent(httpHeaders); + JsonCodec.setDecodeHeaders(httpHeaders); } private Mono token(ConnectionContext connectionContext) { Mono cached = this.refreshTokens.getOrDefault(connectionContext, Mono.empty()) - .flatMap(refreshToken -> refreshToken(connectionContext, refreshToken) - .doOnSubscribe(s -> LOGGER.debug("Negotiating using refresh token"))) - .switchIfEmpty(primaryToken(connectionContext) - .doOnSubscribe(s -> LOGGER.debug("Negotiating using token provider"))) - .transform(ErrorPayloadMapper.fallback()) - .transform(extractTokens(connectionContext)); + .flatMap(refreshToken -> refreshToken(connectionContext, + refreshToken).doOnSubscribe(s -> LOGGER.debug("Negotiating using refresh token"))) + .switchIfEmpty(primaryToken(connectionContext).doOnSubscribe(s -> LOGGER.debug("Negotiating using token provider"))); return connectionContext.getCacheDuration() .map(cached::cache) diff --git a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/tokenprovider/_ClientCredentialsGrantTokenProvider.java b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/tokenprovider/_ClientCredentialsGrantTokenProvider.java index 2a665fbcb66..c42c99c8bc8 100644 --- a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/tokenprovider/_ClientCredentialsGrantTokenProvider.java +++ b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/tokenprovider/_ClientCredentialsGrantTokenProvider.java @@ -18,8 +18,9 @@ import org.cloudfoundry.reactor.TokenProvider; import org.immutables.value.Value; -import reactor.core.publisher.Mono; -import reactor.ipc.netty.http.client.HttpClientRequest; + +import reactor.netty.http.client.HttpClientForm; +import reactor.netty.http.client.HttpClientRequest; /** * The Client Credentials Grant implementation of {@link TokenProvider} @@ -28,16 +29,12 @@ abstract class _ClientCredentialsGrantTokenProvider extends AbstractUaaTokenProvider { @Override - Mono tokenRequestTransformer(Mono outbound) { - return outbound - .flatMap(request -> request - .sendForm(form -> form - .multipart(false) - .attr("client_id", getClientId()) - .attr("client_secret", getClientSecret()) - .attr("grant_type", "client_credentials") - .attr("response_type", "token")) - .then()); + void tokenRequestTransformer(HttpClientRequest request, HttpClientForm form) { + form.multipart(false) + .attr("client_id", getClientId()) + .attr("client_secret", getClientSecret()) + .attr("grant_type", "client_credentials") + .attr("response_type", "token"); } } diff --git a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/tokenprovider/_OneTimePasscodeTokenProvider.java b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/tokenprovider/_OneTimePasscodeTokenProvider.java index a1a8b029d6f..6a9fbb162d5 100644 --- a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/tokenprovider/_OneTimePasscodeTokenProvider.java +++ b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/tokenprovider/_OneTimePasscodeTokenProvider.java @@ -18,8 +18,9 @@ import org.cloudfoundry.reactor.TokenProvider; import org.immutables.value.Value; -import reactor.core.publisher.Mono; -import reactor.ipc.netty.http.client.HttpClientRequest; + +import reactor.netty.http.client.HttpClientForm; +import reactor.netty.http.client.HttpClientRequest; /** * The One-time Passcode Password Grant implementation of {@link TokenProvider} @@ -33,16 +34,12 @@ abstract class _OneTimePasscodeTokenProvider extends AbstractUaaTokenProvider { abstract String getPasscode(); @Override - Mono tokenRequestTransformer(Mono outbound) { - return outbound - .flatMap(request -> request - .sendForm(form -> form - .multipart(false) - .attr("client_id", getClientId()) - .attr("client_secret", getClientSecret()) - .attr("grant_type", "password") - .attr("passcode", getPasscode())) - .then()); + void tokenRequestTransformer(HttpClientRequest request, HttpClientForm form) { + form.multipart(false) + .attr("client_id", getClientId()) + .attr("client_secret", getClientSecret()) + .attr("grant_type", "password") + .attr("passcode", getPasscode()); } } diff --git a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/tokenprovider/_PasswordGrantTokenProvider.java b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/tokenprovider/_PasswordGrantTokenProvider.java index cb3cebc5d9b..2f318b49559 100644 --- a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/tokenprovider/_PasswordGrantTokenProvider.java +++ b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/tokenprovider/_PasswordGrantTokenProvider.java @@ -19,8 +19,9 @@ import org.cloudfoundry.Nullable; import org.cloudfoundry.reactor.TokenProvider; import org.immutables.value.Value; -import reactor.core.publisher.Mono; -import reactor.ipc.netty.http.client.HttpClientRequest; + +import reactor.netty.http.client.HttpClientForm; +import reactor.netty.http.client.HttpClientRequest; /** * The OAuth Password Grant implementation of {@link TokenProvider} @@ -45,18 +46,14 @@ abstract class _PasswordGrantTokenProvider extends AbstractUaaTokenProvider { abstract String getUsername(); @Override - Mono tokenRequestTransformer(Mono outbound) { - return outbound - .flatMap(request -> request - .sendForm(form -> form - .multipart(false) - .attr("client_id", getClientId()) - .attr("client_secret", getClientSecret()) - .attr("grant_type", "password") - .attr("password", getPassword()) - .attr("username", getUsername()) - .attr("login_hint", getLoginHint())) - .then()); + void tokenRequestTransformer(HttpClientRequest request, HttpClientForm form) { + form.multipart(false) + .attr("client_id", getClientId()) + .attr("client_secret", getClientSecret()) + .attr("grant_type", "password") + .attr("password", getPassword()) + .attr("username", getUsername()) + .attr("login_hint", getLoginHint()); } } diff --git a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/tokenprovider/_RefreshTokenGrantTokenProvider.java b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/tokenprovider/_RefreshTokenGrantTokenProvider.java index ff1fceb8268..60e7fa527de 100644 --- a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/tokenprovider/_RefreshTokenGrantTokenProvider.java +++ b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/tokenprovider/_RefreshTokenGrantTokenProvider.java @@ -18,8 +18,9 @@ import org.cloudfoundry.reactor.TokenProvider; import org.immutables.value.Value; -import reactor.core.publisher.Mono; -import reactor.ipc.netty.http.client.HttpClientRequest; + +import reactor.netty.http.client.HttpClientForm; +import reactor.netty.http.client.HttpClientRequest; /** * The OAuth Refresh Token Grant implementation of {@link TokenProvider} @@ -33,16 +34,12 @@ abstract class _RefreshTokenGrantTokenProvider extends AbstractUaaTokenProvider abstract String getToken(); @Override - Mono tokenRequestTransformer(Mono outbound) { - return outbound - .flatMap(request -> request - .sendForm(form -> form - .multipart(false) - .attr("grant_type", "refresh_token") - .attr("client_id", getClientId()) - .attr("client_secret", getClientSecret()) - .attr("refresh_token", getToken())) - .then()); + void tokenRequestTransformer(HttpClientRequest request, HttpClientForm form) { + form.multipart(false) + .attr("grant_type", "refresh_token") + .attr("client_id", getClientId()) + .attr("client_secret", getClientSecret()) + .attr("refresh_token", getToken()); } } diff --git a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/uaa/AbstractUaaOperations.java b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/uaa/AbstractUaaOperations.java index 719e15c7519..3a5aa082e6c 100644 --- a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/uaa/AbstractUaaOperations.java +++ b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/uaa/AbstractUaaOperations.java @@ -16,133 +16,136 @@ package org.cloudfoundry.reactor.uaa; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.UnaryOperator; + import org.cloudfoundry.reactor.ConnectionContext; import org.cloudfoundry.reactor.TokenProvider; import org.cloudfoundry.reactor.client.QueryBuilder; import org.cloudfoundry.reactor.util.AbstractReactorOperations; -import org.cloudfoundry.reactor.util.ErrorPayloadMapper; +import org.cloudfoundry.reactor.util.ErrorPayloadMappers; +import org.cloudfoundry.reactor.util.Operator; import org.springframework.web.util.UriComponentsBuilder; -import reactor.core.publisher.Mono; -import reactor.ipc.netty.http.client.HttpClientRequest; -import reactor.ipc.netty.http.client.HttpClientResponse; -import java.util.function.Function; +import io.netty.handler.codec.http.HttpHeaders; +import reactor.core.publisher.Mono; +import reactor.netty.http.client.HttpClientResponse; public abstract class AbstractUaaOperations extends AbstractReactorOperations { - private final ConnectionContext connectionContext; - protected AbstractUaaOperations(ConnectionContext connectionContext, Mono root, TokenProvider tokenProvider) { super(connectionContext, root, tokenProvider); - this.connectionContext = connectionContext; } - protected final Mono delete(Object requestPayload, Class responseType, Function uriTransformer) { - return doDelete(requestPayload, responseType, - queryTransformer(requestPayload) - .andThen(uriTransformer), - outbound -> outbound - .transform(headerTransformer(requestPayload)), - ErrorPayloadMapper.uaa(this.connectionContext.getObjectMapper())); + protected final Mono delete(Object requestPayload, Class responseType, + Function uriTransformer) { + return createOperator().flatMap(operator -> operator.headers(headers -> addHeaders(headers, requestPayload)) + .delete() + .uri(queryTransformer(requestPayload).andThen(uriTransformer)) + .send(requestPayload) + .response() + .parseBody(responseType)); } - protected final Mono delete(Object requestPayload, Class responseType, Function uriTransformer, - Function, Mono> requestTransformer) { - return doDelete(requestPayload, responseType, - queryTransformer(requestPayload) - .andThen(uriTransformer), - outbound -> outbound - .transform(headerTransformer(requestPayload)) - .transform(requestTransformer), - ErrorPayloadMapper.uaa(this.connectionContext.getObjectMapper())); + protected final Mono get(Object requestPayload, + Function uriTransformer) { + return createOperator().flatMap(operator -> operator.headers(headers -> addHeaders(headers, requestPayload)) + .get() + .uri(queryTransformer(requestPayload).andThen(uriTransformer)) + .response() + .get()); } - protected final Mono get(Object requestPayload, Function uriTransformer) { - return doGet(queryTransformer(requestPayload) - .andThen(uriTransformer), - outbound -> outbound - .transform(headerTransformer(requestPayload)), - ErrorPayloadMapper.uaa(this.connectionContext.getObjectMapper())); + protected final Mono get(Object requestPayload, Function uriTransformer, + Consumer headersTransformer) { + return createOperator().flatMap(operator -> operator.headers(headers -> addHeaders(headers, requestPayload, headersTransformer)) + .get() + .uri(queryTransformer(requestPayload).andThen(uriTransformer)) + .response() + .get()); } - protected final Mono get(Object requestPayload, Function uriTransformer, - Function, Mono> requestTransformer) { - return doGet(queryTransformer(requestPayload) - .andThen(uriTransformer), - outbound -> outbound - .transform(headerTransformer(requestPayload)) - .transform(requestTransformer), - ErrorPayloadMapper.uaa(this.connectionContext.getObjectMapper())); + protected final Mono get(Object requestPayload, Class responseType, + Function uriTransformer) { + return createOperator().flatMap(operator -> operator.headers(headers -> addHeaders(headers, requestPayload)) + .get() + .uri(queryTransformer(requestPayload).andThen(uriTransformer)) + .response() + .parseBody(responseType)); + } + + protected final Mono get(Object requestPayload, Class responseType, + Function uriTransformer, + Consumer headersTransformer) { + return createOperator().flatMap(operator -> operator.headers(headers -> addHeaders(headers, requestPayload, headersTransformer)) + .get() + .uri(queryTransformer(requestPayload).andThen(uriTransformer)) + .response() + .parseBody(responseType)); + } + + protected final Mono patch(Object requestPayload, Class responseType, + Function uriTransformer) { + return createOperator().flatMap(operator -> operator.headers(headers -> addHeaders(headers, requestPayload)) + .patch() + .uri(queryTransformer(requestPayload).andThen(uriTransformer)) + .send(requestPayload) + .response() + .parseBody(responseType)); } - protected final Mono get(Object requestPayload, Class responseType, Function uriTransformer) { - return doGet(responseType, - queryTransformer(requestPayload) - .andThen(uriTransformer), - outbound -> outbound - .transform(headerTransformer(requestPayload)), - ErrorPayloadMapper.uaa(this.connectionContext.getObjectMapper())); + protected final Mono post(Object requestPayload, Class responseType, + Function uriTransformer, + Consumer headersTransformer) { + return createOperator().flatMap(operator -> operator.headers(headers -> addHeaders(headers, requestPayload, headersTransformer)) + .post() + .uri(queryTransformer(requestPayload).andThen(uriTransformer)) + .send(requestPayload) + .response() + .parseBody(responseType)); } - protected final Mono get(Object requestPayload, Class responseType, Function uriTransformer, - Function, Mono> requestTransformer) { - return doGet(responseType, - queryTransformer(requestPayload) - .andThen(uriTransformer), - outbound -> outbound - .transform(headerTransformer(requestPayload)) - .transform(requestTransformer), - ErrorPayloadMapper.uaa(this.connectionContext.getObjectMapper())); + protected final Mono post(Object requestPayload, Class responseType, + Function uriTransformer) { + return createOperator().flatMap(operator -> operator.headers(headers -> addHeaders(headers, requestPayload)) + .post() + .uri(queryTransformer(requestPayload).andThen(uriTransformer)) + .send(requestPayload) + .response() + .parseBody(responseType)); } - protected final Mono patch(Object requestPayload, Class responseType, Function uriTransformer) { - return doPatch(requestPayload, responseType, - queryTransformer(requestPayload) - .andThen(uriTransformer), - outbound -> outbound - .transform(headerTransformer(requestPayload)), - ErrorPayloadMapper.uaa(this.connectionContext.getObjectMapper())); + protected final Mono put(Object requestPayload, Class responseType, + Function uriTransformer) { + return createOperator().flatMap(operator -> operator.headers(headers -> addHeaders(headers, requestPayload)) + .put() + .uri(queryTransformer(requestPayload).andThen(uriTransformer)) + .send(requestPayload) + .response() + .parseBody(responseType)); } - protected final Mono post(Object requestPayload, Class responseType, Function uriTransformer, - Function, Mono> requestTransformer) { - return doPost(requestPayload, responseType, - queryTransformer(requestPayload) - .andThen(uriTransformer), - outbound -> outbound - .transform(headerTransformer(requestPayload)) - .transform(requestTransformer), - ErrorPayloadMapper.uaa(this.connectionContext.getObjectMapper())); + @Override + protected Mono createOperator() { + return super.createOperator().map(this::attachErrorPayloadMapper); } - protected final Mono post(Object requestPayload, Class responseType, Function uriTransformer) { - return doPost(requestPayload, responseType, - queryTransformer(requestPayload) - .andThen(uriTransformer), - outbound -> outbound - .transform(headerTransformer(requestPayload)), - ErrorPayloadMapper.uaa(this.connectionContext.getObjectMapper())); + private Operator attachErrorPayloadMapper(Operator operator) { + return operator.withErrorPayloadMapper(ErrorPayloadMappers.uaa(this.connectionContext.getObjectMapper())); } - protected final Mono put(Object requestPayload, Class responseType, Function uriTransformer) { - return doPut(requestPayload, responseType, - queryTransformer(requestPayload) - .andThen(uriTransformer), - outbound -> outbound - .transform(headerTransformer(requestPayload)), - ErrorPayloadMapper.uaa(this.connectionContext.getObjectMapper())); + private static void addHeaders(HttpHeaders httpHeaders, Object requestPayload, Consumer headersTransformer) { + addHeaders(httpHeaders, requestPayload); + headersTransformer.accept(httpHeaders); } - private static Function, Mono> headerTransformer(Object requestPayload) { - return outbound -> outbound - .map(request -> { - IdentityZoneBuilder.augment(request, requestPayload); - VersionBuilder.augment(request, requestPayload); - return request; - }); + private static void addHeaders(HttpHeaders httpHeaders, Object requestPayload) { + IdentityZoneBuilder.augment(httpHeaders, requestPayload); + VersionBuilder.augment(httpHeaders, requestPayload); } - private static Function queryTransformer(Object requestPayload) { + private static UnaryOperator queryTransformer(Object requestPayload) { return builder -> { QueryBuilder.augment(builder, requestPayload); return builder; diff --git a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/uaa/IdentityZoneBuilder.java b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/uaa/IdentityZoneBuilder.java index 27e566080ba..b28371923af 100644 --- a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/uaa/IdentityZoneBuilder.java +++ b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/uaa/IdentityZoneBuilder.java @@ -16,21 +16,24 @@ package org.cloudfoundry.reactor.uaa; +import java.util.Optional; + import org.cloudfoundry.uaa.IdentityZoned; -import reactor.ipc.netty.http.client.HttpClientRequest; -import java.util.Optional; +import io.netty.handler.codec.http.HttpHeaders; final class IdentityZoneBuilder { private IdentityZoneBuilder() { } - static void augment(HttpClientRequest outbound, Object request) { + static void augment(HttpHeaders httpHeaders, Object request) { if (request instanceof IdentityZoned) { IdentityZoned identityZoned = (IdentityZoned) request; - Optional.ofNullable(identityZoned.getIdentityZoneId()).ifPresent(identityZoneId -> outbound.header("X-Identity-Zone-Id", identityZoneId)); - Optional.ofNullable(identityZoned.getIdentityZoneSubdomain()).ifPresent(identityZoneSubdomain -> outbound.header("X-Identity-Zone-Subdomain", identityZoneSubdomain)); + Optional.ofNullable(identityZoned.getIdentityZoneId()) + .ifPresent(identityZoneId -> httpHeaders.set("X-Identity-Zone-Id", identityZoneId)); + Optional.ofNullable(identityZoned.getIdentityZoneSubdomain()) + .ifPresent(identityZoneSubdomain -> httpHeaders.set("X-Identity-Zone-Subdomain", identityZoneSubdomain)); } } diff --git a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/uaa/VersionBuilder.java b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/uaa/VersionBuilder.java index 777e1ae5155..ce34c3cfb7c 100644 --- a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/uaa/VersionBuilder.java +++ b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/uaa/VersionBuilder.java @@ -16,20 +16,22 @@ package org.cloudfoundry.reactor.uaa; +import java.util.Optional; + import org.cloudfoundry.uaa.Versioned; -import reactor.ipc.netty.http.client.HttpClientRequest; -import java.util.Optional; +import io.netty.handler.codec.http.HttpHeaders; final class VersionBuilder { private VersionBuilder() { } - static void augment(HttpClientRequest outbound, Object request) { + static void augment(HttpHeaders httpHeaders, Object request) { if (request instanceof Versioned) { Versioned versioned = (Versioned) request; - Optional.ofNullable(versioned.getVersion()).ifPresent(version -> outbound.header("If-Match", version)); + Optional.ofNullable(versioned.getVersion()) + .ifPresent(version -> httpHeaders.set("If-Match", version)); } } diff --git a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/uaa/authorizations/ReactorAuthorizations.java b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/uaa/authorizations/ReactorAuthorizations.java index 0c38d92fb36..f0b92329af6 100644 --- a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/uaa/authorizations/ReactorAuthorizations.java +++ b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/uaa/authorizations/ReactorAuthorizations.java @@ -16,7 +16,10 @@ package org.cloudfoundry.reactor.uaa.authorizations; -import io.netty.util.AsciiString; +import static io.netty.handler.codec.http.HttpHeaderNames.AUTHORIZATION; + +import java.util.Optional; + import org.cloudfoundry.reactor.ConnectionContext; import org.cloudfoundry.reactor.TokenProvider; import org.cloudfoundry.reactor.uaa.AbstractUaaOperations; @@ -33,12 +36,10 @@ import org.cloudfoundry.uaa.authorizations.GetOpenIdProviderConfigurationResponse; import org.cloudfoundry.util.ExceptionUtils; import org.springframework.web.util.UriComponentsBuilder; -import reactor.core.publisher.Mono; -import reactor.ipc.netty.http.client.HttpClientRequest; -import java.util.Optional; - -import static io.netty.handler.codec.http.HttpHeaderNames.AUTHORIZATION; +import io.netty.handler.codec.http.HttpHeaders; +import io.netty.util.AsciiString; +import reactor.core.publisher.Mono; /** * The Reactor-based implementation of {@link Authorizations} @@ -51,7 +52,7 @@ public final class ReactorAuthorizations extends AbstractUaaOperations implement * Creates an instance * * @param connectionContext the {@link ConnectionContext} to use when communicating with the server - * @param root the root URI of the server. Typically something like {@code https://uaa.run.pivotal.io}. + * @param root the root URI of the server. Typically something like {@code https://uaa.run.pivotal.io}. * @param tokenProvider the {@link TokenProvider} to use when communicating with the server */ public ReactorAuthorizations(ConnectionContext connectionContext, Mono root, TokenProvider tokenProvider) { @@ -60,77 +61,85 @@ public ReactorAuthorizations(ConnectionContext connectionContext, Mono r @Override public Mono authorizationCodeGrantApi(AuthorizeByAuthorizationCodeGrantApiRequest request) { - return get(request, builder -> builder.pathSegment("oauth", "authorize").queryParam("response_type", ResponseType.CODE)) - .map(inbound -> inbound.responseHeaders().get(LOCATION)) + return get(request, builder -> builder.pathSegment("oauth", "authorize") + .queryParam("response_type", ResponseType.CODE)).map(inbound -> inbound.responseHeaders() + .get(LOCATION)) .flatMap(location -> { - String candidate = UriComponentsBuilder.fromUriString(location).build().getQueryParams().getFirst("code"); + String candidate = UriComponentsBuilder.fromUriString(location) + .build() + .getQueryParams() + .getFirst("code"); return Optional.ofNullable(candidate) .map(Mono::just) - .orElse(ExceptionUtils.illegalState(String.format("Parameter %s not in URI %s", "code", location))); + .orElse(ExceptionUtils.illegalState(String.format("Parameter %s not in URI %s", + "code", + location))); }) .checkpoint(); } @Override public Mono authorizationCodeGrantBrowser(AuthorizeByAuthorizationCodeGrantBrowserRequest request) { - return get(request, builder -> builder.pathSegment("oauth", "authorize").queryParam("response_type", ResponseType.CODE), - outbound -> outbound - .map(ReactorAuthorizations::removeAuthorization)) - .map(inbound -> inbound.responseHeaders().get(LOCATION)) + return get(request, builder -> builder.pathSegment("oauth", "authorize") + .queryParam("response_type", ResponseType.CODE), + ReactorAuthorizations::removeAuthorization).map(inbound -> inbound.responseHeaders() + .get(LOCATION)) .checkpoint(); } @Override public Mono authorizationCodeGrantHybrid(AuthorizeByAuthorizationCodeGrantHybridRequest request) { - return get(request, builder -> builder.pathSegment("oauth", "authorize").queryParam("response_type", ResponseType.CODE_AND_ID_TOKEN), - outbound -> outbound - .map(ReactorAuthorizations::removeAuthorization)) - .map(inbound -> inbound.responseHeaders().get(LOCATION)) + return get(request, builder -> builder.pathSegment("oauth", "authorize") + .queryParam("response_type", ResponseType.CODE_AND_ID_TOKEN), + ReactorAuthorizations::removeAuthorization).map(inbound -> inbound.responseHeaders() + .get(LOCATION)) .checkpoint(); } @Override public Mono getOpenIdProviderConfiguration(GetOpenIdProviderConfigurationRequest request) { - return get(request, GetOpenIdProviderConfigurationResponse.class, builder -> builder.pathSegment(".well-known", "openid-configuration")) - .checkpoint(); + return get(request, GetOpenIdProviderConfigurationResponse.class, + builder -> builder.pathSegment(".well-known", "openid-configuration")).checkpoint(); } @Override public Mono implicitGrantBrowser(AuthorizeByImplicitGrantBrowserRequest request) { - return get(request, builder -> builder.pathSegment("oauth", "authorize").queryParam("response_type", ResponseType.TOKEN), - outbound -> outbound - .map(ReactorAuthorizations::removeAuthorization)) - .map(inbound -> inbound.responseHeaders().get(LOCATION)) + return get(request, builder -> builder.pathSegment("oauth", "authorize") + .queryParam("response_type", ResponseType.TOKEN), + ReactorAuthorizations::removeAuthorization).map(inbound -> inbound.responseHeaders() + .get(LOCATION)) .checkpoint(); } @Override public Mono openIdWithAuthorizationCodeAndIdToken(AuthorizeByOpenIdWithAuthorizationCodeGrantRequest request) { - return get(request, builder -> builder.pathSegment("oauth", "authorize").queryParam("response_type", ResponseType.CODE_AND_ID_TOKEN), - outbound -> outbound - .map(ReactorAuthorizations::removeAuthorization)) - .map(inbound -> inbound.responseHeaders().get(LOCATION)) + return get(request, builder -> builder.pathSegment("oauth", "authorize") + .queryParam("response_type", ResponseType.CODE_AND_ID_TOKEN), + ReactorAuthorizations::removeAuthorization).map(inbound -> inbound.responseHeaders() + .get(LOCATION)) .checkpoint(); } @Override public Mono openIdWithIdToken(AuthorizeByOpenIdWithIdTokenRequest request) { - return get(request, builder -> builder.pathSegment("oauth", "authorize").queryParam("response_type", ResponseType.ID_TOKEN)) - .map(inbound -> inbound.responseHeaders().get(LOCATION)) + return get(request, builder -> builder.pathSegment("oauth", "authorize") + .queryParam("response_type", ResponseType.ID_TOKEN)).map(inbound -> inbound.responseHeaders() + .get(LOCATION)) .checkpoint(); } @Override public Mono openIdWithTokenAndIdToken(AuthorizeByOpenIdWithImplicitGrantRequest request) { - return get(request, builder -> builder.pathSegment("oauth", "authorize").queryParam("response_type", ResponseType.TOKEN_AND_ID_TOKEN)) - .map(inbound -> inbound.responseHeaders().get(LOCATION)) + return get(request, builder -> builder.pathSegment("oauth", "authorize") + .queryParam("response_type", ResponseType.TOKEN_AND_ID_TOKEN)) + .map(inbound -> inbound.responseHeaders() + .get(LOCATION)) .checkpoint(); } - private static HttpClientRequest removeAuthorization(HttpClientRequest request) { - request.requestHeaders().remove(AUTHORIZATION); - return request; + private static void removeAuthorization(HttpHeaders httpHeaders) { + httpHeaders.remove(AUTHORIZATION); } } diff --git a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/uaa/serverinformation/ReactorServerInformation.java b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/uaa/serverinformation/ReactorServerInformation.java index cbdba9b10a9..d32727dc853 100644 --- a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/uaa/serverinformation/ReactorServerInformation.java +++ b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/uaa/serverinformation/ReactorServerInformation.java @@ -60,12 +60,10 @@ public Mono autoLogin(AutoLoginRequest request) { @Override public Mono getAuthenticationCode(GetAutoLoginAuthenticationCodeRequest request) { return post(request, GetAutoLoginAuthenticationCodeResponse.class, builder -> builder.pathSegment("autologin"), - outbound -> outbound - .map(r -> { - String encoded = Base64.getEncoder().encodeToString(new AsciiString(request.getClientId()).concat(":").concat(request.getClientSecret()).toByteArray()); - r.requestHeaders().set(AUTHORIZATION, BASIC_PREAMBLE + encoded); - return r; - })) + outbound -> { + String encoded = Base64.getEncoder().encodeToString(new AsciiString(request.getClientId()).concat(":").concat(request.getClientSecret()).toByteArray()); + outbound.set(AUTHORIZATION, BASIC_PREAMBLE + encoded); + }) .checkpoint(); } diff --git a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/uaa/tokens/ReactorTokens.java b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/uaa/tokens/ReactorTokens.java index 5e8a9600d18..cb15559d2c1 100644 --- a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/uaa/tokens/ReactorTokens.java +++ b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/uaa/tokens/ReactorTokens.java @@ -16,7 +16,16 @@ package org.cloudfoundry.reactor.uaa.tokens; -import io.netty.util.AsciiString; +import static io.netty.handler.codec.http.HttpHeaderNames.AUTHORIZATION; +import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_TYPE; +import static io.netty.handler.codec.http.HttpHeaderValues.APPLICATION_X_WWW_FORM_URLENCODED; +import static org.cloudfoundry.uaa.tokens.GrantType.AUTHORIZATION_CODE; +import static org.cloudfoundry.uaa.tokens.GrantType.CLIENT_CREDENTIALS; +import static org.cloudfoundry.uaa.tokens.GrantType.PASSWORD; +import static org.cloudfoundry.uaa.tokens.GrantType.REFRESH_TOKEN; + +import java.util.Base64; + import org.cloudfoundry.reactor.ConnectionContext; import org.cloudfoundry.reactor.TokenProvider; import org.cloudfoundry.reactor.uaa.AbstractUaaOperations; @@ -40,19 +49,10 @@ import org.cloudfoundry.uaa.tokens.RefreshTokenRequest; import org.cloudfoundry.uaa.tokens.RefreshTokenResponse; import org.cloudfoundry.uaa.tokens.Tokens; -import reactor.core.publisher.Mono; -import reactor.ipc.netty.http.client.HttpClientRequest; - -import java.util.Base64; - -import static io.netty.handler.codec.http.HttpHeaderNames.AUTHORIZATION; -import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_TYPE; -import static io.netty.handler.codec.http.HttpHeaderValues.APPLICATION_X_WWW_FORM_URLENCODED; -import static org.cloudfoundry.uaa.tokens.GrantType.AUTHORIZATION_CODE; -import static org.cloudfoundry.uaa.tokens.GrantType.CLIENT_CREDENTIALS; -import static org.cloudfoundry.uaa.tokens.GrantType.PASSWORD; -import static org.cloudfoundry.uaa.tokens.GrantType.REFRESH_TOKEN; +import io.netty.handler.codec.http.HttpHeaders; +import io.netty.util.AsciiString; +import reactor.core.publisher.Mono; public final class ReactorTokens extends AbstractUaaOperations implements Tokens { @@ -62,7 +62,7 @@ public final class ReactorTokens extends AbstractUaaOperations implements Tokens * Creates an instance * * @param connectionContext the {@link ConnectionContext} to use when communicating with the server - * @param root the root URI of the server. Typically something like {@code https://uaa.run.pivotal.io}. + * @param root the root URI of the server. Typically something like {@code https://uaa.run.pivotal.io}. * @param tokenProvider the {@link TokenProvider} to use when communicating with the server */ public ReactorTokens(ConnectionContext connectionContext, Mono root, TokenProvider tokenProvider) { @@ -71,95 +71,96 @@ public ReactorTokens(ConnectionContext connectionContext, Mono root, Tok @Override public Mono check(CheckTokenRequest request) { - return post(request, CheckTokenResponse.class, builder -> builder.pathSegment("check_token"), - outbound -> outbound - .map(r -> { - String encoded = Base64.getEncoder().encodeToString(new AsciiString(request.getClientId()).concat(":").concat(request.getClientSecret()).toByteArray()); - r.requestHeaders().set(AUTHORIZATION, BASIC_PREAMBLE + encoded); - return r; - })) - .checkpoint(); + return post(request, CheckTokenResponse.class, builder -> builder.pathSegment("check_token"), outbound -> { + String encoded = Base64.getEncoder() + .encodeToString(new AsciiString(request.getClientId()).concat(":") + .concat(request.getClientSecret()) + .toByteArray()); + outbound.set(AUTHORIZATION, BASIC_PREAMBLE + encoded); + }).checkpoint(); } @Override public Mono getByAuthorizationCode(GetTokenByAuthorizationCodeRequest request) { - return post(request, GetTokenByAuthorizationCodeResponse.class, - builder -> builder.pathSegment("oauth", "token").queryParam("grant_type", AUTHORIZATION_CODE).queryParam("response_type", ResponseType.TOKEN), - outbound -> outbound - .map(ReactorTokens::removeAuthorization) - .map(ReactorTokens::setUrlEncoded)) - .checkpoint(); + return post(request, GetTokenByAuthorizationCodeResponse.class, builder -> builder.pathSegment("oauth", "token") + .queryParam("grant_type", AUTHORIZATION_CODE) + .queryParam("response_type", ResponseType.TOKEN), + outbound -> { + ReactorTokens.removeAuthorization(outbound); + ReactorTokens.setUrlEncoded(outbound); + }).checkpoint(); } @Override public Mono getByClientCredentials(GetTokenByClientCredentialsRequest request) { - return post(request, GetTokenByClientCredentialsResponse.class, - builder -> builder.pathSegment("oauth", "token").queryParam("grant_type", CLIENT_CREDENTIALS).queryParam("response_type", ResponseType.TOKEN), - outbound -> outbound - .map(ReactorTokens::removeAuthorization) - .map(ReactorTokens::setUrlEncoded)) - .checkpoint(); + return post(request, GetTokenByClientCredentialsResponse.class, builder -> builder.pathSegment("oauth", "token") + .queryParam("grant_type", CLIENT_CREDENTIALS) + .queryParam("response_type", ResponseType.TOKEN), + outbound -> { + ReactorTokens.removeAuthorization(outbound); + ReactorTokens.setUrlEncoded(outbound); + }).checkpoint(); } @Override public Mono getByOneTimePasscode(GetTokenByOneTimePasscodeRequest request) { - return post(request, GetTokenByOneTimePasscodeResponse.class, - builder -> builder.pathSegment("oauth", "token").queryParam("grant_type", PASSWORD).queryParam("response_type", ResponseType.TOKEN), - outbound -> outbound - .map(ReactorTokens::removeAuthorization) - .map(ReactorTokens::setUrlEncoded)) - .checkpoint(); + return post(request, GetTokenByOneTimePasscodeResponse.class, builder -> builder.pathSegment("oauth", "token") + .queryParam("grant_type", PASSWORD) + .queryParam("response_type", ResponseType.TOKEN), + outbound -> { + ReactorTokens.removeAuthorization(outbound); + ReactorTokens.setUrlEncoded(outbound); + }).checkpoint(); } @Override public Mono getByOpenId(GetTokenByOpenIdRequest request) { - return post(request, GetTokenByOpenIdResponse.class, - builder -> builder.pathSegment("oauth", "token").queryParam("grant_type", AUTHORIZATION_CODE).queryParam("response_type", ResponseType.ID_TOKEN), - outbound -> outbound - .map(ReactorTokens::removeAuthorization) - .map(ReactorTokens::setUrlEncoded)) - .checkpoint(); + return post(request, GetTokenByOpenIdResponse.class, builder -> builder.pathSegment("oauth", "token") + .queryParam("grant_type", AUTHORIZATION_CODE) + .queryParam("response_type", ResponseType.ID_TOKEN), + outbound -> { + ReactorTokens.removeAuthorization(outbound); + ReactorTokens.setUrlEncoded(outbound); + }).checkpoint(); } @Override public Mono getByPassword(GetTokenByPasswordRequest request) { - return post(request, GetTokenByPasswordResponse.class, builder -> builder.pathSegment("oauth", "token").queryParam("grant_type", PASSWORD).queryParam("response_type", ResponseType.TOKEN), - outbound -> outbound - .map(ReactorTokens::removeAuthorization) - .map(ReactorTokens::setUrlEncoded)) - .checkpoint(); + return post(request, GetTokenByPasswordResponse.class, builder -> builder.pathSegment("oauth", "token") + .queryParam("grant_type", PASSWORD) + .queryParam("response_type", ResponseType.TOKEN), + outbound -> { + ReactorTokens.removeAuthorization(outbound); + ReactorTokens.setUrlEncoded(outbound); + }).checkpoint(); } @Override public Mono getKey(GetTokenKeyRequest request) { - return get(request, GetTokenKeyResponse.class, builder -> builder.pathSegment("token_key")) - .checkpoint(); + return get(request, GetTokenKeyResponse.class, builder -> builder.pathSegment("token_key")).checkpoint(); } @Override public Mono listKeys(ListTokenKeysRequest request) { - return get(request, ListTokenKeysResponse.class, builder -> builder.pathSegment("token_keys")) - .checkpoint(); + return get(request, ListTokenKeysResponse.class, builder -> builder.pathSegment("token_keys")).checkpoint(); } @Override public Mono refresh(RefreshTokenRequest request) { - return post(request, RefreshTokenResponse.class, builder -> builder.pathSegment("oauth", "token").queryParam("grant_type", REFRESH_TOKEN), - outbound -> outbound - .map(ReactorTokens::removeAuthorization) - .map(ReactorTokens::setUrlEncoded)) - .checkpoint(); + return post(request, RefreshTokenResponse.class, builder -> builder.pathSegment("oauth", "token") + .queryParam("grant_type", REFRESH_TOKEN), + outbound -> { + ReactorTokens.removeAuthorization(outbound); + ReactorTokens.setUrlEncoded(outbound); + }).checkpoint(); } - private static HttpClientRequest removeAuthorization(HttpClientRequest request) { - request.requestHeaders().remove(AUTHORIZATION); - return request; + private static void removeAuthorization(HttpHeaders request) { + request.remove(AUTHORIZATION); } - private static HttpClientRequest setUrlEncoded(HttpClientRequest request) { - return request - .chunkedTransfer(false) - .header(CONTENT_TYPE, APPLICATION_X_WWW_FORM_URLENCODED); + private static void setUrlEncoded(HttpHeaders request) { + request.set(CONTENT_TYPE, APPLICATION_X_WWW_FORM_URLENCODED); } } diff --git a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/util/AbstractReactorOperations.java b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/util/AbstractReactorOperations.java index 43fc3c4441f..7835186d510 100644 --- a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/util/AbstractReactorOperations.java +++ b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/util/AbstractReactorOperations.java @@ -16,31 +16,24 @@ package org.cloudfoundry.reactor.util; +import static io.netty.handler.codec.http.HttpHeaderNames.AUTHORIZATION; -import io.netty.handler.codec.http.HttpResponseStatus; -import io.netty.util.AsciiString; import org.cloudfoundry.reactor.ConnectionContext; import org.cloudfoundry.reactor.TokenProvider; -import org.reactivestreams.Publisher; -import org.springframework.web.util.UriComponentsBuilder; -import reactor.core.publisher.Mono; -import reactor.ipc.netty.http.client.HttpClientRequest; -import reactor.ipc.netty.http.client.HttpClientResponse; -import java.util.function.Function; - -import static io.netty.handler.codec.http.HttpHeaderNames.AUTHORIZATION; -import static org.cloudfoundry.util.tuple.TupleUtils.function; +import io.netty.util.AsciiString; +import reactor.core.publisher.Mono; +import reactor.netty.http.client.HttpClient; public abstract class AbstractReactorOperations { protected static final AsciiString APPLICATION_ZIP = new AsciiString("application/zip"); - private final ConnectionContext connectionContext; + protected final ConnectionContext connectionContext; - private final Mono root; + protected final Mono root; - private final TokenProvider tokenProvider; + protected final TokenProvider tokenProvider; protected AbstractReactorOperations(ConnectionContext connectionContext, Mono root, TokenProvider tokenProvider) { this.connectionContext = connectionContext; @@ -48,247 +41,28 @@ protected AbstractReactorOperations(ConnectionContext connectionContext, Mono Mono doDelete(Object requestPayload, Class responseType, - Function uriTransformer, - Function, Mono> requestTransformer, - Function, Mono> responseTransformer) { - - return doDelete(requestPayload, uriTransformer, requestTransformer, responseTransformer) - .transform(deserializedResponse(responseType)); - } - - protected final Mono doDelete(Object requestPayload, - Function uriTransformer, - Function, Mono> requestTransformer, - Function, Mono> responseTransformer) { - return this.root - .transform(transformUri(uriTransformer)) - .flatMap(uri -> this.connectionContext.getHttpClient() - .delete(uri, request -> Mono.just(request) - .map(AbstractReactorOperations::disableFailOnError) - .transform(this::addAuthorization) - .map(UserAgent::addUserAgent) - .map(JsonCodec::addDecodeHeaders) - .transform(requestTransformer) - .transform(serializedRequest(requestPayload))) - .doOnSubscribe(NetworkLogging.delete(uri)) - .transform(NetworkLogging.response(uri))) - .transform(this::invalidateToken) - .transform(responseTransformer) - .transform(ErrorPayloadMapper.fallback()); - } - - protected final Mono doGet(Class responseType, - Function uriTransformer, - Function, Mono> requestTransformer, - Function, Mono> responseTransformer) { - - return doGet(uriTransformer, - outbound -> outbound - .map(JsonCodec::addDecodeHeaders) - .transform(requestTransformer), - inbound -> inbound - .transform(responseTransformer)) - .transform(deserializedResponse(responseType)); - } - - protected final Mono doGet(Function uriTransformer, - Function, Mono> requestTransformer, - Function, Mono> responseTransformer) { - return this.root - .transform(transformUri(uriTransformer)) - .flatMap(uri -> this.connectionContext.getHttpClient() - .get(uri, request -> Mono.just(request) - .map(AbstractReactorOperations::disableFailOnError) - .transform(this::addAuthorization) - .map(UserAgent::addUserAgent) - .transform(requestTransformer) - .flatMap(HttpClientRequest::send)) - .doOnSubscribe(NetworkLogging.get(uri)) - .transform(NetworkLogging.response(uri))) - .transform(this::invalidateToken) - .transform(responseTransformer) - .transform(ErrorPayloadMapper.fallback()); - } - - protected final Mono doPatch(Object requestPayload, Class responseType, - Function uriTransformer, - Function, Mono> requestTransformer, - Function, Mono> responseTransformer) { - - return doPatch(responseType, uriTransformer, - outbound -> outbound - .transform(requestTransformer) - .transform(serializedRequest(requestPayload)), - responseTransformer); - } - - protected final Mono doPatch(Class responseType, - Function uriTransformer, - Function, Publisher> requestTransformer, - Function, Mono> responseTransformer) { - return this.root - .transform(transformUri(uriTransformer)) - .flatMap(uri -> this.connectionContext.getHttpClient() - .patch(uri, request -> Mono.just(request) - .map(AbstractReactorOperations::disableChunkedTransfer) - .map(AbstractReactorOperations::disableFailOnError) - .transform(this::addAuthorization) - .map(UserAgent::addUserAgent) - .map(JsonCodec::addDecodeHeaders) - .transform(requestTransformer)) - .doOnSubscribe(NetworkLogging.patch(uri)) - .transform(NetworkLogging.response(uri))) - .transform(this::invalidateToken) - .transform(responseTransformer) - .transform(ErrorPayloadMapper.fallback()) - .transform(deserializedResponse(responseType)); - } - - protected final Mono doPost(Object requestPayload, Class responseType, - Function uriTransformer, - Function, Mono> requestTransformer, - Function, Mono> responseTransformer) { - - return doPost(responseType, uriTransformer, - outbound -> outbound - .transform(requestTransformer) - .transform(serializedRequest(requestPayload)), - responseTransformer); - } - - protected final Mono doPost(Class responseType, - Function uriTransformer, - Function, Publisher> requestTransformer, - Function, Mono> responseTransformer) { - return this.root - .transform(transformUri(uriTransformer)) - .flatMap(uri -> this.connectionContext.getHttpClient() - .post(uri, request -> Mono.just(request) - .map(AbstractReactorOperations::disableChunkedTransfer) - .map(AbstractReactorOperations::disableFailOnError) - .transform(this::addAuthorization) - .map(UserAgent::addUserAgent) - .map(JsonCodec::addDecodeHeaders) - .transform(requestTransformer)) - .doOnSubscribe(NetworkLogging.post(uri)) - .transform(NetworkLogging.response(uri))) - .transform(this::invalidateToken) - .transform(responseTransformer) - .transform(ErrorPayloadMapper.fallback()) - .transform(deserializedResponse(responseType)); - } - - protected final Mono doPut(Object requestPayload, Class responseType, - Function uriTransformer, - Function, Mono> requestTransformer, - Function, Mono> responseTransformer) { - - return doPut(responseType, uriTransformer, - outbound -> outbound - .transform(requestTransformer) - .transform(serializedRequest(requestPayload)), - responseTransformer); - } - - protected final Mono doPut(Class responseType, - Function uriTransformer, - Function, Publisher> requestTransformer, - Function, Mono> responseTransformer) { - return this.root - .transform(transformUri(uriTransformer)) - .flatMap(uri -> this.connectionContext.getHttpClient() - .put(uri, request -> Mono.just(request) - .map(AbstractReactorOperations::disableChunkedTransfer) - .map(AbstractReactorOperations::disableFailOnError) - .transform(this::addAuthorization) - .map(UserAgent::addUserAgent) - .map(JsonCodec::addDecodeHeaders) - .transform(requestTransformer)) - .doOnSubscribe(NetworkLogging.put(uri)) - .transform(NetworkLogging.response(uri))) - .transform(this::invalidateToken) - .transform(responseTransformer) - .transform(ErrorPayloadMapper.fallback()) - .transform(deserializedResponse(responseType)); - } - - protected final Mono doWs(Function uriTransformer, - Function, Mono> requestTransformer, - Function, Mono> responseTransformer) { - return this.root - .transform(transformUri(uriTransformer)) - .flatMap(uri -> this.connectionContext.getHttpClient() - .get(uri, request -> Mono.just(request) - .map(AbstractReactorOperations::disableFailOnError) - .transform(this::addAuthorization) - .map(UserAgent::addUserAgent) - .transform(requestTransformer) - .flatMapMany(HttpClientRequest::sendWebsocket)) - .doOnSubscribe(NetworkLogging.ws(uri)) - .transform(NetworkLogging.response(uri))) - .transform(this::invalidateToken) - .transform(responseTransformer) - .transform(ErrorPayloadMapper.fallback()); - } - - private static HttpClientRequest disableChunkedTransfer(HttpClientRequest request) { - return request.chunkedTransfer(false); - } - - private static HttpClientRequest disableFailOnError(HttpClientRequest request) { - return request - .failOnClientError(false) - .failOnServerError(false); - } - - private static boolean isUnauthorized(HttpClientResponse response) { - return response.status() == HttpResponseStatus.UNAUTHORIZED; - } - - private static Function, Mono> transformUri(Function uriTransformer) { - return uri -> uri - .map(UriComponentsBuilder::fromUriString) - .map(uriTransformer) - .map(builder -> builder.build().encode().toUriString()); - } - - private Mono addAuthorization(Mono outbound) { - return Mono - .zip(outbound, this.tokenProvider.getToken(this.connectionContext)) - .map(function((request, token) -> { - if (request.redirectedFrom().length == 0) { - request.header(AUTHORIZATION, token); - } - - return request; - })); - } - - private Function, Mono> deserializedResponse(Class responseType) { - return inbound -> inbound - .transform(JsonCodec.decode(this.connectionContext.getObjectMapper(), responseType)) - .doOnNext(response -> NetworkLogging.RESPONSE_LOGGER.trace(" {}", response)) - .doOnError(JsonParsingException.class, e -> NetworkLogging.RESPONSE_LOGGER.error("{}\n{}", e.getCause().getMessage(), e.getPayload())); + protected Mono createOperator() { + HttpClient httpClient = connectionContext.getHttpClient(); + return root.map(this::buildOperatorContext) + .map(context -> new Operator(context, httpClient)) + .flatMap(operator -> tokenProvider.getToken(connectionContext) + .map(token -> setHeaders(operator, token))); } - private Mono invalidateToken(Mono inbound) { - return inbound - .flatMap(response -> { - if (isUnauthorized(response)) { - this.tokenProvider.invalidate(this.connectionContext); - return inbound - .transform(this::invalidateToken); - } else { - return Mono.just(response); - } - }); + private OperatorContext buildOperatorContext(String root) { + return OperatorContext.builder() + .connectionContext(connectionContext) + .root(root) + .tokenProvider(tokenProvider) + .build(); } - private Function, Publisher> serializedRequest(Object requestPayload) { - return outbound -> outbound - .doOnNext(request -> NetworkLogging.REQUEST_LOGGER.trace(" {}", requestPayload)) - .transform(JsonCodec.encode(this.connectionContext.getObjectMapper(), requestPayload)); + private Operator setHeaders(Operator operator, String token) { + return operator.headers(httpHeaders -> { + httpHeaders.set(AUTHORIZATION, token); + UserAgent.setUserAgent(httpHeaders); + JsonCodec.setDecodeHeaders(httpHeaders); + }); } } diff --git a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/util/DefaultSslCertificateTruster.java b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/util/DefaultSslCertificateTruster.java index bb74d050669..78128342fcf 100644 --- a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/util/DefaultSslCertificateTruster.java +++ b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/util/DefaultSslCertificateTruster.java @@ -16,18 +16,6 @@ package org.cloudfoundry.reactor.util; -import org.cloudfoundry.reactor.ProxyConfiguration; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import reactor.core.publisher.Mono; -import reactor.ipc.netty.resources.LoopResources; -import reactor.ipc.netty.tcp.TcpClient; -import reactor.util.function.Tuple2; -import reactor.util.function.Tuples; - -import javax.net.ssl.TrustManager; -import javax.net.ssl.TrustManagerFactory; -import javax.net.ssl.X509TrustManager; import java.io.IOException; import java.security.KeyStore; import java.security.KeyStoreException; @@ -41,6 +29,22 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicReference; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509TrustManager; + +import org.cloudfoundry.reactor.ProxyConfiguration; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.netty.handler.ssl.SslContextBuilder; +import reactor.core.publisher.Mono; +import reactor.netty.resources.LoopResources; +import reactor.netty.tcp.SslProvider.SslContextSpec; +import reactor.netty.tcp.TcpClient; +import reactor.util.function.Tuple2; +import reactor.util.function.Tuples; + public final class DefaultSslCertificateTruster implements SslCertificateTruster { private final Logger logger = LoggerFactory.getLogger("cloudfoundry-client.trust"); @@ -62,17 +66,20 @@ public DefaultSslCertificateTruster(Optional proxyConfigurat @Override public void checkClientTrusted(X509Certificate[] x509Certificates, String authType) throws CertificateException { - this.delegate.get().checkClientTrusted(x509Certificates, authType); + this.delegate.get() + .checkClientTrusted(x509Certificates, authType); } @Override public void checkServerTrusted(X509Certificate[] x509Certificates, String authType) throws CertificateException { - this.delegate.get().checkServerTrusted(x509Certificates, authType); + this.delegate.get() + .checkServerTrusted(x509Certificates, authType); } @Override public X509Certificate[] getAcceptedIssuers() { - return this.delegate.get().getAcceptedIssuers(); + return this.delegate.get() + .getAcceptedIssuers(); } @Override @@ -86,11 +93,11 @@ public Mono trust(String host, int port, Duration duration) { X509TrustManager trustManager = this.delegate.get(); - return getUntrustedCertificates(duration, host, port, this.proxyConfiguration, this.threadPool, trustManager) - .doOnNext(untrustedCertificates -> { - KeyStore trustStore = addToTrustStore(untrustedCertificates, trustManager); - this.delegate.set(getTrustManager(getTrustManagerFactory(trustStore))); - }) + return getUntrustedCertificates(duration, host, port, this.proxyConfiguration, this.threadPool, + trustManager).doOnNext(untrustedCertificates -> { + KeyStore trustStore = addToTrustStore(untrustedCertificates, trustManager); + this.delegate.set(getTrustManager(getTrustManagerFactory(trustStore))); + }) .doOnSuccess(untrustedCertificates -> { this.trustedHostsAndPorts.add(hostAndPort); this.logger.debug("Trusted SSL Certificate for {}:{}", host, port); @@ -117,17 +124,20 @@ private static KeyStore addToTrustStore(X509Certificate[] untrustedCertificates, } } - private static TcpClient getTcpClient(Optional proxyConfiguration, LoopResources threadPool, CertificateCollectingTrustManager collector, String host, int port) { - return TcpClient.create(options -> { - options - .host(host) - .port(port) - .loopResources(threadPool) - .disablePool() - .sslSupport(ssl -> ssl.trustManager(new StaticTrustManagerFactory(collector))); - - proxyConfiguration.ifPresent(c -> c.configure(options)); - }); + private static TcpClient getTcpClient(Optional proxyConfiguration, LoopResources threadPool, + CertificateCollectingTrustManager collector, String host, int port) { + TcpClient tcpClient = TcpClient.create() + .runOn(threadPool) + .host(host) + .port(port) + .secure(spec -> configureSsl(spec, collector)); + return proxyConfiguration.map(configuration -> configuration.configure(tcpClient)) + .orElse(tcpClient); + } + + private static void configureSsl(SslContextSpec sslContextSpec, CertificateCollectingTrustManager collector) { + sslContextSpec.sslContext(SslContextBuilder.forClient() + .trustManager(new StaticTrustManagerFactory(collector))); } private static X509TrustManager getTrustManager(TrustManagerFactory trustManagerFactory) { @@ -151,15 +161,18 @@ private static TrustManagerFactory getTrustManagerFactory(KeyStore trustStore) { } } - private static Mono getUntrustedCertificates(Duration duration, String host, int port, Optional proxyConfiguration, LoopResources threadPool, - X509TrustManager delegate) { + private static Mono getUntrustedCertificates(Duration duration, String host, int port, + Optional proxyConfiguration, + LoopResources threadPool, X509TrustManager delegate) { CertificateCollectingTrustManager collector = new CertificateCollectingTrustManager(delegate); - - return getTcpClient(proxyConfiguration, threadPool, collector, host, port) - .newHandler((inbound, outbound) -> inbound.receive().then()) + TcpClient tcpClient = getTcpClient(proxyConfiguration, threadPool, collector, host, port); + return tcpClient.handle((inbound, outbound) -> inbound.receive() + .then()) + .connect() .timeout(duration) - .handle((c, sink) -> { + .handle((connection, sink) -> { + X509Certificate[] chain = collector.getCollectedCertificateChain(); if (chain == null) { @@ -171,6 +184,9 @@ private static Mono getUntrustedCertificates(Duration duratio } else { sink.next(chain); } + + connection.dispose(); + }); } diff --git a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/util/ErrorPayloadMapper.java b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/util/ErrorPayloadMapper.java index 9afe2860f36..19804fcb747 100644 --- a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/util/ErrorPayloadMapper.java +++ b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/util/ErrorPayloadMapper.java @@ -16,98 +16,12 @@ package org.cloudfoundry.reactor.util; -import com.fasterxml.jackson.databind.ObjectMapper; -import io.netty.handler.codec.http.HttpStatusClass; -import org.cloudfoundry.UnknownCloudFoundryException; -import org.cloudfoundry.client.v2.ClientV2Exception; -import org.cloudfoundry.client.v3.ClientV3Exception; -import org.cloudfoundry.client.v3.Errors; -import org.cloudfoundry.uaa.UaaException; -import reactor.core.publisher.Mono; -import reactor.ipc.netty.http.client.HttpClientResponse; - -import java.util.Map; import java.util.function.Function; -import static io.netty.handler.codec.http.HttpStatusClass.CLIENT_ERROR; -import static io.netty.handler.codec.http.HttpStatusClass.SERVER_ERROR; - -public final class ErrorPayloadMapper { - - @SuppressWarnings("unchecked") - public static Function, Mono> clientV2(ObjectMapper objectMapper) { - return inbound -> inbound - .flatMap(mapToError((statusCode, payload) -> { - Map map = objectMapper.readValue(payload, Map.class); - Integer code = (Integer) map.get("code"); - String description = (String) map.get("description"); - String errorCode = (String) map.get("error_code"); - - return new ClientV2Exception(statusCode, code, description, errorCode); - })); - } - - @SuppressWarnings("unchecked") - public static Function, Mono> clientV3(ObjectMapper objectMapper) { - return inbound -> inbound - .flatMap(mapToError((statusCode, payload) -> { - Errors errors = objectMapper.readValue(payload, Errors.class); - return new ClientV3Exception(statusCode, errors.getErrors()); - })); - } - - public static Function, Mono> fallback() { - return inbound -> inbound - .flatMap(response -> { - if (!isError(response)) { - return Mono.just(response); - } - - return response.receive().aggregate().asString() - .flatMap(payload -> Mono.error(new UnknownCloudFoundryException(response.status().code(), payload))); - }); - } +import org.cloudfoundry.reactor.HttpClientResponseWithBody; - @SuppressWarnings("unchecked") - public static Function, Mono> uaa(ObjectMapper objectMapper) { - return inbound -> inbound - .flatMap(mapToError((statusCode, payload) -> { - Map map = objectMapper.readValue(payload, Map.class); - String error = (String) map.get("error"); - String errorDescription = (String) map.get("error_description"); - - return new UaaException(statusCode, error, errorDescription); - })); - } - - private static boolean isError(HttpClientResponse response) { - HttpStatusClass statusClass = response.status().codeClass(); - return statusClass == CLIENT_ERROR || statusClass == SERVER_ERROR; - } - - private static Function> mapToError(ExceptionGenerator exceptionGenerator) { - return response -> { - if (!isError(response)) { - return Mono.just(response); - } - - return response.receive().aggregate().asString() - .switchIfEmpty(Mono.error(new UnknownCloudFoundryException(response.status().code()))) - .flatMap(payload -> { - try { - return Mono.error(exceptionGenerator.apply(response.status().code(), payload)); - } catch (Exception e) { - return Mono.error(new UnknownCloudFoundryException(response.status().code(), payload)); - } - }); - }; - } - - @FunctionalInterface - private interface ExceptionGenerator { - - RuntimeException apply(Integer statusCode, String payload) throws Exception; +import reactor.core.publisher.Mono; - } +public interface ErrorPayloadMapper extends Function, Mono> { } diff --git a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/util/ErrorPayloadMappers.java b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/util/ErrorPayloadMappers.java new file mode 100644 index 00000000000..5cf915740a0 --- /dev/null +++ b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/util/ErrorPayloadMappers.java @@ -0,0 +1,127 @@ +/* + * Copyright 2013-2019 the original author or authors. + * + * 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 org.cloudfoundry.reactor.util; + +import static io.netty.handler.codec.http.HttpStatusClass.CLIENT_ERROR; +import static io.netty.handler.codec.http.HttpStatusClass.SERVER_ERROR; + +import java.util.Map; +import java.util.function.Function; + +import org.cloudfoundry.UnknownCloudFoundryException; +import org.cloudfoundry.client.v2.ClientV2Exception; +import org.cloudfoundry.client.v3.ClientV3Exception; +import org.cloudfoundry.client.v3.Errors; +import org.cloudfoundry.reactor.HttpClientResponseWithBody; +import org.cloudfoundry.uaa.UaaException; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import io.netty.handler.codec.http.HttpStatusClass; +import reactor.core.publisher.Mono; +import reactor.netty.http.client.HttpClientResponse; + +public final class ErrorPayloadMappers { + + public static ErrorPayloadMapper fallback() { + return inbound -> inbound.flatMap(responseWithBody -> { + HttpClientResponse response = responseWithBody.getResponse(); + if (isError(response)) { + return responseWithBody.getBody() + .aggregate() + .asString() + .flatMap(payload -> Mono.error(new UnknownCloudFoundryException(response.status() + .code(), + payload))); + } + return Mono.just(responseWithBody); + }); + } + + @SuppressWarnings("unchecked") + public static ErrorPayloadMapper clientV2(ObjectMapper objectMapper) { + return inbound -> inbound.flatMap(mapToError((statusCode, payload) -> { + Map map = objectMapper.readValue(payload, Map.class); + Integer code = (Integer) map.get("code"); + String description = (String) map.get("description"); + String errorCode = (String) map.get("error_code"); + + return new ClientV2Exception(statusCode, code, description, errorCode); + })); + } + + public static ErrorPayloadMapper clientV3(ObjectMapper objectMapper) { + return inbound -> inbound.flatMap(mapToError((statusCode, payload) -> { + Errors errors = objectMapper.readValue(payload, Errors.class); + return new ClientV3Exception(statusCode, errors.getErrors()); + })); + } + + @SuppressWarnings("unchecked") + public static ErrorPayloadMapper uaa(ObjectMapper objectMapper) { + return inbound -> inbound.flatMap(mapToError((statusCode, payload) -> { + Map map = objectMapper.readValue(payload, Map.class); + String error = (String) map.get("error"); + String errorDescription = (String) map.get("error_description"); + + return new UaaException(statusCode, error, errorDescription); + })); + } + + private static boolean isError(HttpClientResponse response) { + HttpStatusClass statusClass = response.status() + .codeClass(); + return statusClass == CLIENT_ERROR || statusClass == SERVER_ERROR; + } + + private static Function> + mapToError(ExceptionGenerator exceptionGenerator) { + return response -> { + if (!isError(response.getResponse())) { + return Mono.just(response); + } + + return response.getBody() + .aggregate() + .asString() + .switchIfEmpty(Mono.error(new UnknownCloudFoundryException(response.getResponse() + .status() + .code()))) + .flatMap(payload -> { + try { + return Mono.error(exceptionGenerator.apply(response.getResponse() + .status() + .code(), + payload)); + } catch (Exception e) { + return Mono.error(new UnknownCloudFoundryException(response.getResponse() + .status() + .code(), + payload)); + } + }); + }; + } + + @FunctionalInterface + private interface ExceptionGenerator { + + RuntimeException apply(Integer statusCode, String payload) throws Exception; + + } + +} diff --git a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/util/JsonCodec.java b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/util/JsonCodec.java index a2481dd7fda..30bb950a84b 100644 --- a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/util/JsonCodec.java +++ b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/util/JsonCodec.java @@ -16,62 +16,69 @@ package org.cloudfoundry.reactor.util; +import java.nio.charset.Charset; +import java.util.function.BiFunction; + +import org.reactivestreams.Publisher; + import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.annotation.JsonSerialize; + import io.netty.handler.codec.http.HttpHeaderNames; import io.netty.handler.codec.http.HttpHeaderValues; +import io.netty.handler.codec.http.HttpHeaders; import io.netty.handler.codec.json.JsonObjectDecoder; -import org.reactivestreams.Publisher; import reactor.core.Exceptions; -import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import reactor.ipc.netty.http.client.HttpClientRequest; -import reactor.ipc.netty.http.client.HttpClientResponse; - -import java.nio.charset.Charset; -import java.util.function.Function; +import reactor.netty.ByteBufFlux; +import reactor.netty.NettyOutbound; +import reactor.netty.http.client.HttpClientRequest; +import reactor.netty.http.client.HttpClientResponse; public final class JsonCodec { private static final int MAX_PAYLOAD_SIZE = 100 * 1024 * 1024; - public static HttpClientRequest addDecodeHeaders(HttpClientRequest request) { - return request - .header(HttpHeaderNames.ACCEPT, HttpHeaderValues.APPLICATION_JSON); + public static void setDecodeHeaders(HttpHeaders httpHeaders) { + httpHeaders.set(HttpHeaderNames.ACCEPT, HttpHeaderValues.APPLICATION_JSON); } - public static Function, Flux> decode(ObjectMapper objectMapper, Class responseType) { - return inbound -> inbound - .flatMapMany(response -> response.addHandler(new JsonObjectDecoder(MAX_PAYLOAD_SIZE)).receive().asByteArray() - .map(payload -> { - try { - return objectMapper.readValue(payload, responseType); - } catch (Throwable t) { - throw new JsonParsingException(t.getMessage(), t, new String(payload, Charset.defaultCharset())); - } - })); + static JsonObjectDecoder createDecoder(HttpClientResponse response) { + return new JsonObjectDecoder(MAX_PAYLOAD_SIZE); } - static Function, Publisher> encode(ObjectMapper objectMapper, Object requestPayload) { - if (!AnnotationUtils.findAnnotation(requestPayload.getClass(), JsonSerialize.class).isPresent()) { - return outbound -> outbound - .flatMap(HttpClientRequest::send); - } - - return outbound -> outbound - .flatMapMany(request -> { + public static Mono decode(ObjectMapper objectMapper, ByteBufFlux responseBody, Class responseType) { + return responseBody.aggregate() + .asByteArray() + .map(payload -> { try { - byte[] bytes = objectMapper.writeValueAsBytes(requestPayload); - - return request - .header(HttpHeaderNames.CONTENT_TYPE, HttpHeaderValues.APPLICATION_JSON) - .header(HttpHeaderNames.CONTENT_LENGTH, String.valueOf(bytes.length)) - .sendByteArray(Mono.just(bytes)); - } catch (JsonProcessingException e) { - throw Exceptions.propagate(e); + return objectMapper.readValue(payload, responseType); + } catch (Throwable t) { + throw new JsonParsingException(t.getMessage(), t, new String(payload, Charset.defaultCharset())); } }); } + static BiFunction> encode(ObjectMapper objectMapper, Object requestPayload) { + if (!AnnotationUtils.findAnnotation(requestPayload.getClass(), JsonSerialize.class) + .isPresent()) { + return (request, outbound) -> Mono.empty(); + } + + return (request, outbound) -> { + try { + byte[] bytes = objectMapper.writeValueAsBytes(requestPayload); + String contentLength = String.valueOf(bytes.length); + + Mono body = Mono.just(bytes); + request.header(HttpHeaderNames.CONTENT_LENGTH, contentLength); + request.header(HttpHeaderNames.CONTENT_TYPE, HttpHeaderValues.APPLICATION_JSON); + return outbound.sendByteArray(body); + } catch (JsonProcessingException e) { + throw Exceptions.propagate(e); + } + }; + } + } diff --git a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/util/MultipartHttpClientRequest.java b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/util/MultipartHttpClientRequest.java index 68cdc608711..96be6aa1877 100644 --- a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/util/MultipartHttpClientRequest.java +++ b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/util/MultipartHttpClientRequest.java @@ -16,46 +16,22 @@ package org.cloudfoundry.reactor.util; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import io.netty.buffer.ByteBuf; -import io.netty.buffer.Unpooled; -import io.netty.handler.codec.http.DefaultHttpHeaders; -import io.netty.handler.codec.http.HttpHeaders; -import io.netty.util.AsciiString; -import reactor.core.Exceptions; -import reactor.core.publisher.Mono; -import reactor.ipc.netty.NettyOutbound; -import reactor.ipc.netty.http.client.HttpClientRequest; - -import java.io.IOException; -import java.nio.file.Files; +import java.io.ByteArrayInputStream; import java.nio.file.Path; import java.util.ArrayList; -import java.util.Comparator; import java.util.List; -import java.util.Map; -import java.util.Random; import java.util.function.Consumer; import java.util.stream.Collectors; -import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_DISPOSITION; -import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_LENGTH; -import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_TYPE; -import static io.netty.handler.codec.http.HttpHeaderValues.MULTIPART_FORM_DATA; - -public final class MultipartHttpClientRequest { - - private static final byte[] BOUNDARY_CHARS = new byte[]{'-', '_', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', - 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'}; - - private static final AsciiString BOUNDARY_PREAMBLE = MULTIPART_FORM_DATA.concat("; boundary="); - - private static final AsciiString CRLF = new AsciiString("\r\n"); +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; - private static final AsciiString DOUBLE_DASH = new AsciiString("--"); +import io.netty.handler.codec.http.HttpHeaderNames; +import reactor.core.Exceptions; +import reactor.netty.http.client.HttpClientForm; +import reactor.netty.http.client.HttpClientRequest; - private static final Random RND = new Random(); +public final class MultipartHttpClientRequest { private final ObjectMapper objectMapper; @@ -63,9 +39,12 @@ public final class MultipartHttpClientRequest { private final HttpClientRequest request; - public MultipartHttpClientRequest(ObjectMapper objectMapper, HttpClientRequest request) { + private final HttpClientForm form; + + public MultipartHttpClientRequest(ObjectMapper objectMapper, HttpClientRequest request, HttpClientForm form) { this.objectMapper = objectMapper; this.request = request; + this.form = form; } public MultipartHttpClientRequest addPart(Consumer partConsumer) { @@ -73,11 +52,7 @@ public MultipartHttpClientRequest addPart(Consumer partCo return this; } - public Mono done() { - AsciiString boundary = generateMultipartBoundary(); - AsciiString delimiter = getDelimiter(boundary); - AsciiString closeDelimiter = getCloseDelimiter(boundary); - + public void done() { List parts = this.partConsumers.stream() .map(partConsumer -> { PartHttpClientRequest part = new PartHttpClientRequest(this.objectMapper); @@ -85,142 +60,75 @@ public Mono done() { return part; }) .collect(Collectors.toList()); + + this.request.requestHeaders() + .remove(HttpHeaderNames.TRANSFER_ENCODING); - Long contentLength = parts.stream() - .mapToLong(part -> delimiter.length() + CRLF.length() + part.getLength()) - .sum() + closeDelimiter.length(); - - NettyOutbound intermediateRequest = this.request - .chunkedTransfer(false) - .header(CONTENT_TYPE, BOUNDARY_PREAMBLE.concat(boundary)) - .header(CONTENT_LENGTH, String.valueOf(contentLength)); - + form.multipart(true); for (PartHttpClientRequest part : parts) { - intermediateRequest = intermediateRequest.sendObject(Unpooled.wrappedBuffer(delimiter.toByteArray())); - intermediateRequest = intermediateRequest.sendObject(Unpooled.wrappedBuffer(CRLF.toByteArray())); - intermediateRequest = intermediateRequest.sendObject(part.renderedHeaders); - intermediateRequest = part.sendPayload(intermediateRequest); - } - - intermediateRequest = intermediateRequest.sendObject(Unpooled.wrappedBuffer(closeDelimiter.toByteArray())); - - return intermediateRequest - .then(); - } - - private static AsciiString generateMultipartBoundary() { - byte[] boundary = new byte[RND.nextInt(11) + 30]; - for (int i = 0; i < boundary.length; i++) { - boundary[i] = BOUNDARY_CHARS[RND.nextInt(BOUNDARY_CHARS.length)]; + part.send(form); } - return new AsciiString(boundary); - } - - private static AsciiString getCloseDelimiter(AsciiString boundary) { - return CRLF.concat(DOUBLE_DASH).concat(boundary).concat(DOUBLE_DASH); - } - - private static AsciiString getDelimiter(AsciiString boundary) { - return CRLF.concat(DOUBLE_DASH).concat(boundary); } public static final class PartHttpClientRequest { - private static final AsciiString HEADER_DELIMITER = new AsciiString(": "); - - private final HttpHeaders headers = new DefaultHttpHeaders(true); - private final ObjectMapper objectMapper; private Path file; - private byte[] payload; + private ByteArrayInputStream payload; + + private String name; + + private String filename; - private ByteBuf renderedHeaders; + private String contentType; private PartHttpClientRequest(ObjectMapper objectMapper) { this.objectMapper = objectMapper; } - public void send(Object source) { - try { - byte[] bytes = this.objectMapper.writeValueAsBytes(source); - this.headers.set(CONTENT_LENGTH, bytes.length); - this.renderedHeaders = renderHeaders(); - this.payload = bytes; - } catch (JsonProcessingException e) { - throw Exceptions.propagate(e); - } - } - - public void sendFile(Path file) { - try { - this.headers.set(CONTENT_LENGTH, Files.size(file)); - this.renderedHeaders = renderHeaders(); - this.file = file; - } catch (IOException e) { - throw Exceptions.propagate(e); - } + public PartHttpClientRequest setName(String name) { + this.name = name; + return this; } - public PartHttpClientRequest setContentDispositionFormData(String name) { - return setContentDispositionFormData(name, null); + public PartHttpClientRequest setFilename(String filename) { + this.filename = filename; + return this; } - public PartHttpClientRequest setContentDispositionFormData(String name, String filename) { - AsciiString s = new AsciiString("form-data; name=\"").concat(name).concat("\""); - - if (filename != null) { - s = s.concat("; filename=\"").concat(filename).concat("\""); - } - - this.headers.set(CONTENT_DISPOSITION, s); + public PartHttpClientRequest setContentType(String contentType) { + this.contentType = contentType; return this; } - public PartHttpClientRequest setHeader(CharSequence name, CharSequence value) { - this.headers.set(name, value); - return this; + public void send(Object source) { + try { + byte[] bytes = this.objectMapper.writeValueAsBytes(source); + this.payload = new ByteArrayInputStream(bytes); + } catch (JsonProcessingException e) { + throw Exceptions.propagate(e); + } } - private long getLength() { - return this.renderedHeaders.readableBytes() + getPayloadLength(); + public void sendFile(Path file) { + this.file = file; } - private long getPayloadLength() { + private HttpClientForm send(HttpClientForm form) { if (this.file != null) { - try { - return Files.size(this.file); - } catch (IOException e) { - throw Exceptions.propagate(e); - } + return form.file(name, getFilenameOrDefault(), this.file.toFile(), contentType); } else if (this.payload != null) { - return this.payload.length; - } else { - return 0; + return form.file(name, this.payload, contentType); } + return form; } - private ByteBuf renderHeaders() { - AsciiString s = this.headers.entries().stream() - .sorted(Comparator.comparing(Map.Entry::getKey)) - .map(entry -> new AsciiString(entry.getKey()).concat(HEADER_DELIMITER).concat(entry.getValue()).concat(CRLF)) - .reduce(AsciiString::concat) - .orElse(AsciiString.EMPTY_STRING) - .concat(CRLF); - - return Unpooled.wrappedBuffer(s.toByteArray()); + private String getFilenameOrDefault() { + return filename != null ? filename : this.file.getFileName().toString(); } - private NettyOutbound sendPayload(NettyOutbound request) { - if (this.file != null) { - return request.sendFile(this.file); - } else if (this.payload != null) { - return request.sendByteArray(Mono.just(this.payload)); - } else { - return request; - } - } } -} +} \ No newline at end of file diff --git a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/util/NetworkLogging.java b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/util/NetworkLogging.java deleted file mode 100644 index 36b855db8d1..00000000000 --- a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/util/NetworkLogging.java +++ /dev/null @@ -1,97 +0,0 @@ -/* - * Copyright 2013-2019 the original author or authors. - * - * 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 org.cloudfoundry.reactor.util; - -import org.cloudfoundry.util.TimeUtils; -import org.reactivestreams.Subscription; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import reactor.core.publisher.Mono; -import reactor.ipc.netty.http.client.HttpClientResponse; - -import java.util.List; -import java.util.Optional; -import java.util.concurrent.atomic.AtomicLong; -import java.util.concurrent.atomic.AtomicReference; -import java.util.function.Consumer; -import java.util.function.Function; -import java.util.stream.Collectors; - -public final class NetworkLogging { - - static final Logger REQUEST_LOGGER = LoggerFactory.getLogger("cloudfoundry-client.request"); - - static final Logger RESPONSE_LOGGER = LoggerFactory.getLogger("cloudfoundry-client.response"); - - private static final String CF_WARNINGS = "X-Cf-Warnings"; - - private NetworkLogging() { - } - - public static Consumer delete(String uri) { - return s -> REQUEST_LOGGER.debug("DELETE {}", uri); - } - - public static Consumer get(String uri) { - return s -> REQUEST_LOGGER.debug("GET {}", uri); - } - - public static Consumer patch(String uri) { - return s -> REQUEST_LOGGER.debug("PATCH {}", uri); - } - - public static Consumer post(String uri) { - return s -> REQUEST_LOGGER.debug("POST {}", uri); - } - - public static Consumer put(String uri) { - return s -> REQUEST_LOGGER.debug("PUT {}", uri); - } - - public static Function, Mono> response(String uri) { - if (!RESPONSE_LOGGER.isDebugEnabled()) { - return inbound -> inbound; - } - - AtomicLong startTimeHolder = new AtomicLong(); - AtomicReference responseHolder = new AtomicReference<>(); - - return inbound -> inbound - .doOnSubscribe(s -> startTimeHolder.set(System.currentTimeMillis())) - .doOnNext(responseHolder::set) - .doFinally(signalType -> { - String elapsed = TimeUtils.asTime(System.currentTimeMillis() - startTimeHolder.get()); - - Optional.ofNullable(responseHolder.get()) - .ifPresent(response -> { - List warnings = response.responseHeaders().getAll(CF_WARNINGS); - - if (warnings.isEmpty()) { - RESPONSE_LOGGER.debug("{} {} ({})", response.status().code(), uri, elapsed); - } else { - RESPONSE_LOGGER.warn("{} {} ({}) [{}]", response.status().code(), uri, elapsed, warnings.stream().collect(Collectors.joining(", "))); - } - }); - }); - } - - public static Consumer ws(String uri) { - return s -> REQUEST_LOGGER.debug("WS {}", uri); - } - - -} diff --git a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/util/Operator.java b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/util/Operator.java new file mode 100644 index 00000000000..e8bc9327255 --- /dev/null +++ b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/util/Operator.java @@ -0,0 +1,304 @@ +/* + * Copyright 2013-2019 the original author or authors. + * + * 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 org.cloudfoundry.reactor.util; + +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.function.BiConsumer; +import java.util.function.BiFunction; +import java.util.function.Consumer; +import java.util.function.Function; + +import org.cloudfoundry.reactor.HttpClientResponseWithBody; +import org.reactivestreams.Publisher; +import org.springframework.web.util.UriComponentsBuilder; + +import io.netty.channel.ChannelHandler; +import io.netty.handler.codec.http.HttpHeaders; +import io.netty.handler.codec.http.HttpMethod; +import io.netty.handler.codec.http.HttpResponseStatus; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.netty.ByteBufFlux; +import reactor.netty.Connection; +import reactor.netty.NettyOutbound; +import reactor.netty.http.client.HttpClient; +import reactor.netty.http.client.HttpClientForm; +import reactor.netty.http.client.HttpClientRequest; +import reactor.netty.http.client.HttpClientResponse; +import reactor.netty.http.websocket.WebsocketInbound; +import reactor.netty.http.websocket.WebsocketOutbound; + +public class Operator extends OperatorContextAware { + + private final HttpClient httpClient; + + public Operator(OperatorContext context, HttpClient httpClient) { + super(context); + this.httpClient = httpClient; + } + + public Operator followRedirects() { + return new Operator(context, httpClient.followRedirect(true)); + } + + public Operator headers(Consumer headersTransformer) { + return new Operator(context, httpClient.headers(headersTransformer)); + } + + public Operator withErrorPayloadMapper(ErrorPayloadMapper errorPayloadMapper) { + return new Operator(context.withErrorPayloadMapper(errorPayloadMapper), httpClient); + } + + public UriConfiguration get() { + return request(HttpMethod.GET); + } + + public UriConfiguration put() { + return request(HttpMethod.PUT); + } + + public UriConfiguration post() { + return request(HttpMethod.POST); + } + + public UriConfiguration patch() { + return request(HttpMethod.PATCH); + } + + public UriConfiguration delete() { + return request(HttpMethod.DELETE); + } + + public UriConfiguration request(HttpMethod method) { + return new UriConfiguration(context, attachRequestLogger(httpClient).request(method)); + } + + public WebsocketUriConfiguration websocket() { + return new WebsocketUriConfiguration(context, httpClient.websocket()); + } + + private static HttpClient attachRequestLogger(HttpClient httpClient) { + RequestLogger requestLogger = new RequestLogger(); + return httpClient.doAfterRequest((request, connection) -> requestLogger.request(request)) + .doAfterResponse((response, connection) -> requestLogger.response(response)); + } + + public static class UriConfiguration extends OperatorContextAware { + + private final HttpClient.RequestSender requestSender; + + public UriConfiguration(OperatorContext context, HttpClient.RequestSender requestSender) { + super(context); + this.requestSender = requestSender; + } + + public PayloadConfiguration uri(Function uriTransformer) { + String uri = transformRoot(uriTransformer); + return new PayloadConfiguration(context, requestSender.uri(uri)); + } + + } + + public static class PayloadConfiguration extends OperatorContextAware { + + private final HttpClient.RequestSender requestSender; + + public PayloadConfiguration(OperatorContext context, HttpClient.RequestSender requestSender) { + super(context); + this.requestSender = requestSender; + } + + public ResponseReceiver response() { + return new ResponseReceiver(context, requestSender); + } + + public ResponseReceiverConstructor send(Object payload) { + return send(serialized(payload)); + } + + public ResponseReceiverConstructor send(BiFunction> requestTransformer) { + HttpClient.ResponseReceiver responseReceiver = requestSender.send(requestTransformer); + return new ResponseReceiverConstructor(context, responseReceiver); + } + + public ResponseReceiverConstructor sendForm(BiConsumer requestTransformer) { + HttpClient.ResponseReceiver responseReceiver = requestSender.sendForm(requestTransformer); + return new ResponseReceiverConstructor(context, responseReceiver); + } + + private BiFunction> serialized(Object payload) { + return JsonCodec.encode(context.getConnectionContext() + .getObjectMapper(), + payload); + } + + } + + public static class ResponseReceiverConstructor extends OperatorContextAware { + + private final HttpClient.ResponseReceiver responseReceiver; + + public ResponseReceiverConstructor(OperatorContext context, HttpClient.ResponseReceiver responseReceiver) { + super(context); + this.responseReceiver = responseReceiver; + } + + public ResponseReceiver response() { + return new ResponseReceiver(context, responseReceiver); + } + + } + + public static class ResponseReceiver extends OperatorContextAware { + + private final HttpClient.ResponseReceiver responseReceiver; + + private final List> channelHandlerBuilders = new ArrayList<>(); + + public ResponseReceiver(OperatorContext context, HttpClient.ResponseReceiver responseReceiver) { + super(context); + this.responseReceiver = responseReceiver; + } + + public ResponseReceiver addChannelHandler(Function channelHandlerBuilder) { + channelHandlerBuilders.add(channelHandlerBuilder); + return this; + } + + public Mono get() { + return responseReceiver.response((response, + body) -> processResponse(response, body).map(HttpClientResponseWithBody::getResponse)) + .singleOrEmpty(); + } + + public Mono parseBody(Class bodyType) { + addChannelHandler(JsonCodec::createDecoder); + return parseBodyToMono(responseWithBody -> deserialized(responseWithBody.getBody(), bodyType)); + } + + private Mono deserialized(ByteBufFlux body, Class bodyType) { + return JsonCodec.decode(context.getConnectionContext() + .getObjectMapper(), + body, bodyType); + } + + public Mono parseBodyToMono(Function> responseTransformer) { + return parseBodyToFlux(responseTransformer).singleOrEmpty(); + } + + public Flux parseBodyToFlux(Function> responseTransformer) { + return responseReceiver.responseConnection((response, connection) -> { + attachChannelHandlers(response, connection); + ByteBufFlux body = connection.inbound() + .receive(); + return processResponse(response, body).flatMapMany(responseTransformer) + .doOnTerminate(connection::dispose); + }); + } + + private void attachChannelHandlers(HttpClientResponse response, Connection connection) { + for (Function handlerBuilder : channelHandlerBuilders) { + connection.addHandler(handlerBuilder.apply(response)); + } + } + + private Mono processResponse(HttpClientResponse response, ByteBufFlux body) { + HttpClientResponseWithBody responseWithBody = HttpClientResponseWithBody.of(response, body); + + return Mono.just(responseWithBody) + .map(this::invalidateToken) + .transform(context.getErrorPayloadMapper() + .orElse(ErrorPayloadMappers.fallback())); + } + + private HttpClientResponseWithBody invalidateToken(HttpClientResponseWithBody response) { + if (isUnauthorized(response.getResponse())) { + context.getTokenProvider() + .ifPresent(tokenProvider -> tokenProvider.invalidate(context.getConnectionContext())); + } + return response; + } + + private static boolean isUnauthorized(HttpClientResponse response) { + return response.status() == HttpResponseStatus.UNAUTHORIZED; + } + + } + + public static class WebsocketUriConfiguration extends OperatorContextAware { + + private final HttpClient.WebsocketSender sender; + + public WebsocketUriConfiguration(OperatorContext context, HttpClient.WebsocketSender sender) { + super(context); + this.sender = sender; + } + + public WebsocketResponseReceiver uri(Function uriTransformer) { + String uri = transformRoot(uriTransformer); + logWebsocketRequest(uri); + return new WebsocketResponseReceiver(sender.uri(uri)); + } + + private static void logWebsocketRequest(String uri) { + new RequestLogger().websocketRequest(uri); + } + + } + + public static class WebsocketResponseReceiver { + + private final HttpClient.WebsocketSender sender; + + public WebsocketResponseReceiver(HttpClient.WebsocketSender sender) { + this.sender = sender; + } + + public Flux get() { + return sender.handle(this::handleWebsocketCommunication); + } + + private Publisher handleWebsocketCommunication(WebsocketInbound inbound, WebsocketOutbound outbound) { + return inbound.aggregateFrames() + .receive() + .asInputStream() + .doOnTerminate(outbound::sendClose); + } + + } + +} + +class OperatorContextAware { + + protected final OperatorContext context; + + protected OperatorContextAware(OperatorContext context) { + this.context = context; + } + + protected String transformRoot(Function uriTransformer) { + UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromUriString(context.getRoot()); + return uriTransformer.apply(uriComponentsBuilder) + .build() + .encode() + .toUriString(); + } + +} diff --git a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/util/RequestLogger.java b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/util/RequestLogger.java new file mode 100644 index 00000000000..1f82bb21c11 --- /dev/null +++ b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/util/RequestLogger.java @@ -0,0 +1,72 @@ +/* + * Copyright 2013-2019 the original author or authors. + * + * 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 org.cloudfoundry.reactor.util; + +import java.util.List; +import java.util.stream.Collectors; + +import org.cloudfoundry.util.TimeUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import reactor.netty.http.client.HttpClientRequest; +import reactor.netty.http.client.HttpClientResponse; + +public class RequestLogger { + + static final Logger REQUEST_LOGGER = LoggerFactory.getLogger("cloudfoundry-client.request"); + + static final Logger RESPONSE_LOGGER = LoggerFactory.getLogger("cloudfoundry-client.response"); + + private static final String CF_WARNINGS = "X-Cf-Warnings"; + + private long requestSentTime; + + public void request(HttpClientRequest request) { + request(String.format("%-5s {}", request.method()), request.uri()); + } + + public void websocketRequest(String uri) { + request("WS {}", uri); + } + + public void response(HttpClientResponse response) { + if (!RESPONSE_LOGGER.isDebugEnabled()) { + return; + } + String elapsed = TimeUtils.asTime(System.currentTimeMillis() - requestSentTime); + List warnings = response.responseHeaders() + .getAll(CF_WARNINGS); + + if (warnings.isEmpty()) { + RESPONSE_LOGGER.debug("{} {} ({})", response.status() + .code(), + response.uri(), elapsed); + } else { + RESPONSE_LOGGER.warn("{} {} ({}) [{}]", response.status() + .code(), + response.uri(), elapsed, warnings.stream() + .collect(Collectors.joining(", "))); + } + } + + private void request(String message, String uri) { + REQUEST_LOGGER.debug(message, uri); + this.requestSentTime = System.currentTimeMillis(); + } + +} diff --git a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/util/UserAgent.java b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/util/UserAgent.java index 139ee7da361..3c749b466c9 100644 --- a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/util/UserAgent.java +++ b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/util/UserAgent.java @@ -16,12 +16,12 @@ package org.cloudfoundry.reactor.util; +import java.util.Optional; + import io.netty.bootstrap.Bootstrap; import io.netty.handler.codec.http.HttpHeaderNames; -import reactor.ipc.netty.http.client.HttpClient; -import reactor.ipc.netty.http.client.HttpClientRequest; - -import java.util.Optional; +import io.netty.handler.codec.http.HttpHeaders; +import reactor.netty.http.client.HttpClient; /** * Utilities for working with the {@Code User-Agent} @@ -40,11 +40,10 @@ private UserAgent() { /** * Add the {@code User-Agent} to a request. Typically used with `.map` * - * @param request The request to transform - * @return the transformed request + * @param httpHeaders The headers to transform */ - public static HttpClientRequest addUserAgent(HttpClientRequest request) { - return request.header(HttpHeaderNames.USER_AGENT, USER_AGENT); + public static void setUserAgent(HttpHeaders httpHeaders) { + httpHeaders.set(HttpHeaderNames.USER_AGENT, USER_AGENT); } private static String javaClientVersion() { diff --git a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/util/_OperatorContext.java b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/util/_OperatorContext.java new file mode 100644 index 00000000000..932d2b3a559 --- /dev/null +++ b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/util/_OperatorContext.java @@ -0,0 +1,22 @@ +package org.cloudfoundry.reactor.util; + +import java.util.Optional; + +import org.cloudfoundry.reactor.ConnectionContext; +import org.cloudfoundry.reactor.TokenProvider; +import org.immutables.value.Value; + +@Value.Immutable(copy = true) +public interface _OperatorContext { + + @Value.Parameter + ConnectionContext getConnectionContext(); + + @Value.Parameter + String getRoot(); + + Optional getErrorPayloadMapper(); + + Optional getTokenProvider(); + +} diff --git a/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/v2/applications/ReactorApplicationsV2Test.java b/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/v2/applications/ReactorApplicationsV2Test.java index 92c84167917..b4b1f13c527 100644 --- a/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/v2/applications/ReactorApplicationsV2Test.java +++ b/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/v2/applications/ReactorApplicationsV2Test.java @@ -993,21 +993,23 @@ public void upload() throws IOException { String boundary = extractBoundary(headers); assertThat(body.readString(Charset.defaultCharset())) - .isEqualTo("\r\n--" + boundary + "\r\n" + + .isEqualTo("--" + boundary + "\r\n" + "content-disposition: form-data; name=\"resources\"\r\n" + "content-length: 178\r\n" + "content-type: application/json\r\n" + + "content-transfer-encoding: binary\r\n" + "\r\n" + "[{\"sha1\":\"b907173290db6a155949ab4dc9b2d019dea0c901\",\"fn\":\"path/to/content.txt\",\"size\":123}," + "{\"sha1\":\"ff84f89760317996b9dd180ab996b079f418396f\",\"fn\":\"path/to/code.jar\",\"size\":123}]" + "\r\n" + "--" + boundary + "\r\n" + - "content-disposition: form-data; name=\"application\"; filename=\"application.zip\"\r\n" + + "content-disposition: form-data; name=\"application\"; filename=\"test-application.zip\"\r\n" + "content-length: 12\r\n" + "content-type: application/zip\r\n" + + "content-transfer-encoding: binary\r\n" + "\r\n" + "test-content" + "\r\n" + - "--" + boundary + "--"); + "--" + boundary + "--\r\n"); })) .build()) .response(TestResponse.builder() @@ -1056,13 +1058,15 @@ public void uploadDroplet() throws IOException { String boundary = extractBoundary(headers); assertThat(body.readString(Charset.defaultCharset())) - .isEqualTo("\r\n" + "--" + boundary + "\r\n" + + .isEqualTo("--" + boundary + "\r\n" + "content-disposition: form-data; name=\"droplet\"; filename=\"test-droplet.tgz\"\r\n" + "content-length: 12\r\n" + + "content-type: application/octet-stream\r\n" + + "content-transfer-encoding: binary\r\n" + "\r\n" + "test-content" + "\r\n" + - "--" + boundary + "--"); + "--" + boundary + "--\r\n"); })) .build()) .response(TestResponse.builder() diff --git a/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/v2/buildpacks/ReactorBuildpacksTest.java b/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/v2/buildpacks/ReactorBuildpacksTest.java index 6b023777fc9..0e1f2882e7a 100644 --- a/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/v2/buildpacks/ReactorBuildpacksTest.java +++ b/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/v2/buildpacks/ReactorBuildpacksTest.java @@ -278,14 +278,15 @@ public void upload() throws IOException { String boundary = extractBoundary(headers); assertThat(body.readString(Charset.defaultCharset())) - .isEqualTo("\r\n--" + boundary + "\r\n" + + .isEqualTo("--" + boundary + "\r\n" + "content-disposition: form-data; name=\"buildpack\"; filename=\"test-filename\"\r\n" + "content-length: 12\r\n" + "content-type: application/zip\r\n" + + "content-transfer-encoding: binary\r\n" + "\r\n" + "test-content" + "\r\n" + - "--" + boundary + "--"); + "--" + boundary + "--\r\n"); })) .build()) .response(TestResponse.builder() diff --git a/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/v3/packages/ReactorPackagesTest.java b/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/v3/packages/ReactorPackagesTest.java index 45b44470f46..c84453a9851 100644 --- a/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/v3/packages/ReactorPackagesTest.java +++ b/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/v3/packages/ReactorPackagesTest.java @@ -469,14 +469,15 @@ public void upload() throws IOException { String boundary = extractBoundary(headers); assertThat(body.readString(Charset.defaultCharset())) - .isEqualTo("\r\n--" + boundary + "\r\n" + - "content-disposition: form-data; name=\"bits\"; filename=\"application.zip\"\r\n" + + .isEqualTo("--" + boundary + "\r\n" + + "content-disposition: form-data; name=\"bits\"; filename=\"test-package.zip\"\r\n" + "content-length: 12\r\n" + "content-type: application/zip\r\n" + + "content-transfer-encoding: binary\r\n" + "\r\n" + "test-content" + "\r\n" + - "--" + boundary + "--"); + "--" + boundary + "--\r\n"); })) .build()) .response(TestResponse.builder() diff --git a/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/routing/v1/tcproutes/EventStreamCodecTest.java b/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/routing/v1/tcproutes/EventStreamCodecTest.java index 7121eacfe3f..34dfd611f85 100644 --- a/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/routing/v1/tcproutes/EventStreamCodecTest.java +++ b/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/routing/v1/tcproutes/EventStreamCodecTest.java @@ -16,17 +16,22 @@ package org.cloudfoundry.reactor.routing.v1.tcproutes; +import static io.netty.handler.codec.http.HttpMethod.GET; +import static io.netty.handler.codec.http.HttpResponseStatus.OK; + +import java.time.Duration; + import org.cloudfoundry.reactor.AbstractRestTest; import org.cloudfoundry.reactor.InteractionContext; import org.cloudfoundry.reactor.TestRequest; import org.cloudfoundry.reactor.TestResponse; import org.junit.Test; -import reactor.test.StepVerifier; -import java.time.Duration; - -import static io.netty.handler.codec.http.HttpMethod.GET; -import static io.netty.handler.codec.http.HttpResponseStatus.OK; +import reactor.core.publisher.Flux; +import reactor.netty.ByteBufFlux; +import reactor.netty.Connection; +import reactor.netty.http.client.HttpClientResponse; +import reactor.test.StepVerifier; public final class EventStreamCodecTest extends AbstractRestTest { @@ -34,7 +39,8 @@ public final class EventStreamCodecTest extends AbstractRestTest { public void allData() { mockRequest(InteractionContext.builder() .request(TestRequest.builder() - .method(GET).path("/") + .method(GET) + .path("/") .build()) .response(TestResponse.builder() .status(OK) @@ -43,8 +49,9 @@ public void allData() { .build()); CONNECTION_CONTEXT.getHttpClient() - .get(this.root.block()) - .flatMapMany(EventStreamCodec::decode) + .get() + .uri(this.root.block()) + .responseConnection(EventStreamCodecTest::toEventsFlux) .as(StepVerifier::create) .expectNext(ServerSentEvent.builder() .data("This is the first message.") @@ -64,7 +71,8 @@ public void allData() { public void colonSpacing() { mockRequest(InteractionContext.builder() .request(TestRequest.builder() - .method(GET).path("/") + .method(GET) + .path("/") .build()) .response(TestResponse.builder() .status(OK) @@ -73,8 +81,9 @@ public void colonSpacing() { .build()); CONNECTION_CONTEXT.getHttpClient() - .get(this.root.block()) - .flatMapMany(EventStreamCodec::decode) + .get() + .uri(this.root.block()) + .responseConnection(EventStreamCodecTest::toEventsFlux) .as(StepVerifier::create) .expectNext(ServerSentEvent.builder() .data("test") @@ -90,7 +99,8 @@ public void colonSpacing() { public void randomColons() { mockRequest(InteractionContext.builder() .request(TestRequest.builder() - .method(GET).path("/") + .method(GET) + .path("/") .build()) .response(TestResponse.builder() .status(OK) @@ -99,8 +109,9 @@ public void randomColons() { .build()); CONNECTION_CONTEXT.getHttpClient() - .get(this.root.block()) - .flatMapMany(EventStreamCodec::decode) + .get() + .uri(this.root.block()) + .responseConnection(EventStreamCodecTest::toEventsFlux) .as(StepVerifier::create) .expectNext(ServerSentEvent.builder() .data("") @@ -117,7 +128,8 @@ public void randomColons() { public void threeLines() { mockRequest(InteractionContext.builder() .request(TestRequest.builder() - .method(GET).path("/") + .method(GET) + .path("/") .build()) .response(TestResponse.builder() .status(OK) @@ -126,8 +138,9 @@ public void threeLines() { .build()); CONNECTION_CONTEXT.getHttpClient() - .get(this.root.block()) - .flatMapMany(EventStreamCodec::decode) + .get() + .uri(this.root.block()) + .responseConnection(EventStreamCodecTest::toEventsFlux) .as(StepVerifier::create) .expectNext(ServerSentEvent.builder() .data("YHOO") @@ -142,7 +155,8 @@ public void threeLines() { public void withComment() { mockRequest(InteractionContext.builder() .request(TestRequest.builder() - .method(GET).path("/") + .method(GET) + .path("/") .build()) .response(TestResponse.builder() .status(OK) @@ -151,8 +165,9 @@ public void withComment() { .build()); CONNECTION_CONTEXT.getHttpClient() - .get(this.root.block()) - .flatMapMany(EventStreamCodec::decode) + .get() + .uri(this.root.block()) + .responseConnection(EventStreamCodecTest::toEventsFlux) .as(StepVerifier::create) .expectNext(ServerSentEvent.builder() .id("1") @@ -173,7 +188,8 @@ public void withComment() { public void withEventTypes() { mockRequest(InteractionContext.builder() .request(TestRequest.builder() - .method(GET).path("/") + .method(GET) + .path("/") .build()) .response(TestResponse.builder() .status(OK) @@ -182,8 +198,9 @@ public void withEventTypes() { .build()); CONNECTION_CONTEXT.getHttpClient() - .get(this.root.block()) - .flatMapMany(EventStreamCodec::decode) + .get() + .uri(this.root.block()) + .responseConnection(EventStreamCodecTest::toEventsFlux) .as(StepVerifier::create) .expectNext(ServerSentEvent.builder() .eventType("add") @@ -201,4 +218,11 @@ public void withEventTypes() { .verify(Duration.ofSeconds(5)); } + private static Flux toEventsFlux(HttpClientResponse response, Connection connection) { + connection.addHandler(EventStreamCodec.createDecoder(response)); + ByteBufFlux body = connection.inbound() + .receive(); + return EventStreamCodec.decode(body); + } + } diff --git a/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/uaa/IdentityZoneBuilderTest.java b/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/uaa/IdentityZoneBuilderTest.java index 6b028579634..13171a5f1b2 100644 --- a/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/uaa/IdentityZoneBuilderTest.java +++ b/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/uaa/IdentityZoneBuilderTest.java @@ -16,24 +16,25 @@ package org.cloudfoundry.reactor.uaa; -import org.cloudfoundry.uaa.IdentityZoned; -import org.junit.Test; -import reactor.ipc.netty.http.client.HttpClientRequest; - import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyZeroInteractions; +import org.cloudfoundry.uaa.IdentityZoned; +import org.junit.Test; + +import io.netty.handler.codec.http.HttpHeaders; + public final class IdentityZoneBuilderTest { - private final HttpClientRequest outbound = mock(HttpClientRequest.class); + private final HttpHeaders outbound = mock(HttpHeaders.class); @Test public void augment() { IdentityZoneBuilder.augment(this.outbound, new StubIdentityZoned()); - verify(this.outbound).header("X-Identity-Zone-Id", "test-identity-zone-id"); - verify(this.outbound).header("X-Identity-Zone-Subdomain", "test-identity-zone-subdomain"); + verify(this.outbound).set("X-Identity-Zone-Id", "test-identity-zone-id"); + verify(this.outbound).set("X-Identity-Zone-Subdomain", "test-identity-zone-subdomain"); } @Test diff --git a/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/uaa/VersionBuilderTest.java b/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/uaa/VersionBuilderTest.java index 507b15467fd..8e95b28e678 100644 --- a/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/uaa/VersionBuilderTest.java +++ b/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/uaa/VersionBuilderTest.java @@ -16,23 +16,24 @@ package org.cloudfoundry.reactor.uaa; -import org.cloudfoundry.uaa.Versioned; -import org.junit.Test; -import reactor.ipc.netty.http.client.HttpClientRequest; - import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyZeroInteractions; +import org.cloudfoundry.uaa.Versioned; +import org.junit.Test; + +import io.netty.handler.codec.http.HttpHeaders; + public final class VersionBuilderTest { - private final HttpClientRequest outbound = mock(HttpClientRequest.class); + private final HttpHeaders outbound = mock(HttpHeaders.class); @Test public void augment() { VersionBuilder.augment(this.outbound, new StubVersioned("test-version")); - verify(this.outbound).header("If-Match", "test-version"); + verify(this.outbound).set("If-Match", "test-version"); } @Test diff --git a/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/uaa/users/ReactorUsersTest.java b/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/uaa/users/ReactorUsersTest.java index a50fe0ef214..cd29962ea0c 100644 --- a/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/uaa/users/ReactorUsersTest.java +++ b/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/uaa/users/ReactorUsersTest.java @@ -16,6 +16,21 @@ package org.cloudfoundry.reactor.uaa.users; +import static io.netty.handler.codec.http.HttpMethod.DELETE; +import static io.netty.handler.codec.http.HttpMethod.GET; +import static io.netty.handler.codec.http.HttpMethod.PATCH; +import static io.netty.handler.codec.http.HttpMethod.POST; +import static io.netty.handler.codec.http.HttpMethod.PUT; +import static io.netty.handler.codec.http.HttpResponseStatus.OK; +import static org.cloudfoundry.uaa.SortOrder.ASCENDING; +import static org.cloudfoundry.uaa.SortOrder.DESCENDING; +import static org.cloudfoundry.uaa.users.ApprovalStatus.APPROVED; +import static org.cloudfoundry.uaa.users.ApprovalStatus.DENIED; +import static org.cloudfoundry.uaa.users.MembershipType.DIRECT; + +import java.time.Duration; +import java.util.Collections; + import org.cloudfoundry.reactor.InteractionContext; import org.cloudfoundry.reactor.TestRequest; import org.cloudfoundry.reactor.TestResponse; @@ -52,22 +67,8 @@ import org.cloudfoundry.uaa.users.VerifyUserRequest; import org.cloudfoundry.uaa.users.VerifyUserResponse; import org.junit.Test; -import reactor.test.StepVerifier; -import java.time.Duration; -import java.util.Collections; - -import static io.netty.handler.codec.http.HttpMethod.DELETE; -import static io.netty.handler.codec.http.HttpMethod.GET; -import static io.netty.handler.codec.http.HttpMethod.PATCH; -import static io.netty.handler.codec.http.HttpMethod.POST; -import static io.netty.handler.codec.http.HttpMethod.PUT; -import static io.netty.handler.codec.http.HttpResponseStatus.OK; -import static org.cloudfoundry.uaa.SortOrder.ASCENDING; -import static org.cloudfoundry.uaa.SortOrder.DESCENDING; -import static org.cloudfoundry.uaa.users.ApprovalStatus.APPROVED; -import static org.cloudfoundry.uaa.users.ApprovalStatus.DENIED; -import static org.cloudfoundry.uaa.users.MembershipType.DIRECT; +import reactor.test.StepVerifier; public final class ReactorUsersTest extends AbstractUaaApiTest { @@ -244,6 +245,7 @@ public void delete() { .build()) .build()); + this.users .delete(DeleteUserRequest.builder() .userId("421225f4-318e-4a4d-9219-4b6a0ed3678a") diff --git a/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/util/ErrorPayloadMapperTest.java b/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/util/ErrorPayloadMappersTest.java similarity index 54% rename from cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/util/ErrorPayloadMapperTest.java rename to cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/util/ErrorPayloadMappersTest.java index 1b58153abe7..aba6b44b270 100644 --- a/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/util/ErrorPayloadMapperTest.java +++ b/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/util/ErrorPayloadMappersTest.java @@ -16,30 +16,34 @@ package org.cloudfoundry.reactor.util; -import com.fasterxml.jackson.databind.ObjectMapper; +import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST; +import static io.netty.handler.codec.http.HttpResponseStatus.INTERNAL_SERVER_ERROR; +import static io.netty.handler.codec.http.HttpResponseStatus.OK; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.RETURNS_SMART_NULLS; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.time.Duration; + import org.cloudfoundry.UnknownCloudFoundryException; import org.cloudfoundry.client.v2.ClientV2Exception; import org.cloudfoundry.client.v3.ClientV3Exception; +import org.cloudfoundry.reactor.HttpClientResponseWithBody; import org.cloudfoundry.uaa.UaaException; import org.junit.Test; import org.springframework.core.io.ClassPathResource; -import reactor.core.publisher.Mono; -import reactor.ipc.netty.ByteBufFlux; -import reactor.ipc.netty.http.client.HttpClientResponse; -import reactor.test.StepVerifier; -import java.io.IOException; -import java.time.Duration; +import com.fasterxml.jackson.databind.ObjectMapper; -import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST; -import static io.netty.handler.codec.http.HttpResponseStatus.INTERNAL_SERVER_ERROR; -import static io.netty.handler.codec.http.HttpResponseStatus.OK; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.RETURNS_SMART_NULLS; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.netty.ByteBufFlux; +import reactor.netty.http.client.HttpClientResponse; +import reactor.test.StepVerifier; -public final class ErrorPayloadMapperTest { +public final class ErrorPayloadMappersTest { private final ObjectMapper objectMapper = new ObjectMapper(); @@ -48,13 +52,13 @@ public final class ErrorPayloadMapperTest { @Test public void clientV2BadPayload() throws IOException { when(this.response.status()).thenReturn(BAD_REQUEST); - when(this.response.receive()).thenReturn(ByteBufFlux.fromPath(new ClassPathResource("fixtures/invalid_error_response.json").getFile().toPath())); + HttpClientResponseWithBody responseWithBody = buildResponseWithBody(ByteBufFlux.fromPath(new ClassPathResource("fixtures/invalid_error_response.json").getFile() + .toPath())); - Mono.just(this.response) - .transform(ErrorPayloadMapper.clientV2(this.objectMapper)) + Mono.just(responseWithBody) + .transform(ErrorPayloadMappers.clientV2(this.objectMapper)) .as(StepVerifier::create) - .consumeErrorWith(t -> assertThat(t) - .isInstanceOf(UnknownCloudFoundryException.class) + .consumeErrorWith(t -> assertThat(t).isInstanceOf(UnknownCloudFoundryException.class) .hasMessage("Unknown Cloud Foundry Exception") .extracting("statusCode", "payload") .containsExactly(BAD_REQUEST.code(), "Invalid Error Response")) @@ -64,27 +68,30 @@ public void clientV2BadPayload() throws IOException { @Test public void clientV2ClientError() throws IOException { when(this.response.status()).thenReturn(BAD_REQUEST); - when(this.response.receive()).thenReturn(ByteBufFlux.fromPath(new ClassPathResource("fixtures/client/v2/error_response.json").getFile().toPath())); + HttpClientResponseWithBody responseWithBody = buildResponseWithBody(ByteBufFlux.fromPath(new ClassPathResource("fixtures/client/v2/error_response.json").getFile() + .toPath())); - Mono.just(this.response) - .transform(ErrorPayloadMapper.clientV2(this.objectMapper)) + Mono.just(responseWithBody) + .transform(ErrorPayloadMappers.clientV2(this.objectMapper)) .as(StepVerifier::create) - .consumeErrorWith(t -> assertThat(t) - .isInstanceOf(ClientV2Exception.class) + .consumeErrorWith(t -> assertThat(t).isInstanceOf(ClientV2Exception.class) .hasMessage("CF-UnprocessableEntity(10008): The request is semantically invalid: space_guid and name unique") .extracting("statusCode", "code", "description", "errorCode") - .containsExactly(BAD_REQUEST.code(), 10008, "The request is semantically invalid: space_guid and name unique", "CF-UnprocessableEntity")) + .containsExactly(BAD_REQUEST.code(), 10008, + "The request is semantically invalid: space_guid and name unique", + "CF-UnprocessableEntity")) .verify(Duration.ofSeconds(1)); } @Test public void clientV2NoError() { when(this.response.status()).thenReturn(OK); + HttpClientResponseWithBody responseWithBody = buildResponseWithBody(); - Mono.just(this.response) - .transform(ErrorPayloadMapper.clientV2(this.objectMapper)) + Mono.just(responseWithBody) + .transform(ErrorPayloadMappers.clientV2(this.objectMapper)) .as(StepVerifier::create) - .expectNext(this.response) + .expectNext(responseWithBody) .expectComplete() .verify(Duration.ofSeconds(1)); } @@ -92,29 +99,31 @@ public void clientV2NoError() { @Test public void clientV2ServerError() throws IOException { when(this.response.status()).thenReturn(INTERNAL_SERVER_ERROR); - when(this.response.receive()).thenReturn(ByteBufFlux.fromPath(new ClassPathResource("fixtures/client/v2/error_response.json").getFile().toPath())); + HttpClientResponseWithBody responseWithBody = buildResponseWithBody(ByteBufFlux.fromPath(new ClassPathResource("fixtures/client/v2/error_response.json").getFile() + .toPath())); - Mono.just(this.response) - .transform(ErrorPayloadMapper.clientV2(this.objectMapper)) + Mono.just(responseWithBody) + .transform(ErrorPayloadMappers.clientV2(this.objectMapper)) .as(StepVerifier::create) - .consumeErrorWith(t -> assertThat(t) - .isInstanceOf(ClientV2Exception.class) + .consumeErrorWith(t -> assertThat(t).isInstanceOf(ClientV2Exception.class) .hasMessage("CF-UnprocessableEntity(10008): The request is semantically invalid: space_guid and name unique") .extracting("statusCode", "code", "description", "errorCode") - .containsExactly(INTERNAL_SERVER_ERROR.code(), 10008, "The request is semantically invalid: space_guid and name unique", "CF-UnprocessableEntity")) + .containsExactly(INTERNAL_SERVER_ERROR.code(), 10008, + "The request is semantically invalid: space_guid and name unique", + "CF-UnprocessableEntity")) .verify(Duration.ofSeconds(1)); } @Test public void clientV3BadPayload() throws IOException { when(this.response.status()).thenReturn(BAD_REQUEST); - when(this.response.receive()).thenReturn(ByteBufFlux.fromPath(new ClassPathResource("fixtures/invalid_error_response.json").getFile().toPath())); + HttpClientResponseWithBody responseWithBody = buildResponseWithBody(ByteBufFlux.fromPath(new ClassPathResource("fixtures/invalid_error_response.json").getFile() + .toPath())); - Mono.just(this.response) - .transform(ErrorPayloadMapper.clientV3(this.objectMapper)) + Mono.just(responseWithBody) + .transform(ErrorPayloadMappers.clientV3(this.objectMapper)) .as(StepVerifier::create) - .consumeErrorWith(t -> assertThat(t) - .isInstanceOf(UnknownCloudFoundryException.class) + .consumeErrorWith(t -> assertThat(t).isInstanceOf(UnknownCloudFoundryException.class) .hasMessage("Unknown Cloud Foundry Exception") .extracting("statusCode", "payload") .containsExactly(BAD_REQUEST.code(), "Invalid Error Response")) @@ -124,20 +133,21 @@ public void clientV3BadPayload() throws IOException { @Test public void clientV3ClientError() throws IOException { when(this.response.status()).thenReturn(BAD_REQUEST); - when(this.response.receive()).thenReturn(ByteBufFlux.fromPath(new ClassPathResource("fixtures/client/v3/error_response.json").getFile().toPath())); + HttpClientResponseWithBody responseWithBody = buildResponseWithBody(ByteBufFlux.fromPath(new ClassPathResource("fixtures/client/v3/error_response.json").getFile() + .toPath())); - Mono.just(this.response) - .transform(ErrorPayloadMapper.clientV3(this.objectMapper)) + Mono.just(responseWithBody) + .transform(ErrorPayloadMappers.clientV3(this.objectMapper)) .as(StepVerifier::create) .consumeErrorWith(t -> { - assertThat(t) - .isInstanceOf(ClientV3Exception.class) + assertThat(t).isInstanceOf(ClientV3Exception.class) .hasMessage("CF-UnprocessableEntity(10008): something went wrong") .extracting("statusCode") .containsExactly(BAD_REQUEST.code()); - assertThat(((ClientV3Exception) t).getErrors()) - .flatExtracting(org.cloudfoundry.client.v3.Error::getCode, org.cloudfoundry.client.v3.Error::getDetail, org.cloudfoundry.client.v3.Error::getTitle) + assertThat(((ClientV3Exception) t).getErrors()).flatExtracting(org.cloudfoundry.client.v3.Error::getCode, + org.cloudfoundry.client.v3.Error::getDetail, + org.cloudfoundry.client.v3.Error::getTitle) .containsExactly(10008, "something went wrong", "CF-UnprocessableEntity"); }) .verify(Duration.ofSeconds(1)); @@ -146,11 +156,12 @@ public void clientV3ClientError() throws IOException { @Test public void clientV3NoError() { when(this.response.status()).thenReturn(OK); + HttpClientResponseWithBody responseWithBody = buildResponseWithBody(); - Mono.just(this.response) - .transform(ErrorPayloadMapper.clientV3(this.objectMapper)) + Mono.just(responseWithBody) + .transform(ErrorPayloadMappers.clientV3(this.objectMapper)) .as(StepVerifier::create) - .expectNext(this.response) + .expectNext(responseWithBody) .expectComplete() .verify(Duration.ofSeconds(1)); } @@ -158,36 +169,36 @@ public void clientV3NoError() { @Test public void clientV3ServerError() throws IOException { when(this.response.status()).thenReturn(INTERNAL_SERVER_ERROR); - when(this.response.receive()).thenReturn(ByteBufFlux.fromPath(new ClassPathResource("fixtures/client/v3/error_response.json").getFile().toPath())); + HttpClientResponseWithBody responseWithBody = buildResponseWithBody(ByteBufFlux.fromPath(new ClassPathResource("fixtures/client/v3/error_response.json").getFile() + .toPath())); - Mono.just(this.response) - .transform(ErrorPayloadMapper.clientV3(this.objectMapper)) + Mono.just(responseWithBody) + .transform(ErrorPayloadMappers.clientV3(this.objectMapper)) .as(StepVerifier::create) .consumeErrorWith(t -> { - assertThat(t) - .isInstanceOf(ClientV3Exception.class) + assertThat(t).isInstanceOf(ClientV3Exception.class) .hasMessage("CF-UnprocessableEntity(10008): something went wrong") .extracting("statusCode") .containsExactly(INTERNAL_SERVER_ERROR.code()); - assertThat(((ClientV3Exception) t).getErrors()) - .flatExtracting(org.cloudfoundry.client.v3.Error::getCode, org.cloudfoundry.client.v3.Error::getDetail, org.cloudfoundry.client.v3.Error::getTitle) + assertThat(((ClientV3Exception) t).getErrors()).flatExtracting(org.cloudfoundry.client.v3.Error::getCode, + org.cloudfoundry.client.v3.Error::getDetail, + org.cloudfoundry.client.v3.Error::getTitle) .containsExactly(10008, "something went wrong", "CF-UnprocessableEntity"); }) .verify(Duration.ofSeconds(1)); } - @Test public void uaaBadPayload() throws IOException { when(this.response.status()).thenReturn(BAD_REQUEST); - when(this.response.receive()).thenReturn(ByteBufFlux.fromPath(new ClassPathResource("fixtures/invalid_error_response.json").getFile().toPath())); + HttpClientResponseWithBody responseWithBody = buildResponseWithBody(ByteBufFlux.fromPath(new ClassPathResource("fixtures/invalid_error_response.json").getFile() + .toPath())); - Mono.just(this.response) - .transform(ErrorPayloadMapper.uaa(this.objectMapper)) + Mono.just(responseWithBody) + .transform(ErrorPayloadMappers.uaa(this.objectMapper)) .as(StepVerifier::create) - .consumeErrorWith(t -> assertThat(t) - .isInstanceOf(UnknownCloudFoundryException.class) + .consumeErrorWith(t -> assertThat(t).isInstanceOf(UnknownCloudFoundryException.class) .hasMessage("Unknown Cloud Foundry Exception") .extracting("statusCode", "payload") .containsExactly(BAD_REQUEST.code(), "Invalid Error Response")) @@ -197,13 +208,13 @@ public void uaaBadPayload() throws IOException { @Test public void uaaClientError() throws IOException { when(this.response.status()).thenReturn(BAD_REQUEST); - when(this.response.receive()).thenReturn(ByteBufFlux.fromPath(new ClassPathResource("fixtures/uaa/error_response.json").getFile().toPath())); + HttpClientResponseWithBody responseWithBody = buildResponseWithBody(ByteBufFlux.fromPath(new ClassPathResource("fixtures/uaa/error_response.json").getFile() + .toPath())); - Mono.just(this.response) - .transform(ErrorPayloadMapper.uaa(this.objectMapper)) + Mono.just(responseWithBody) + .transform(ErrorPayloadMappers.uaa(this.objectMapper)) .as(StepVerifier::create) - .consumeErrorWith(t -> assertThat(t) - .isInstanceOf(UaaException.class) + .consumeErrorWith(t -> assertThat(t).isInstanceOf(UaaException.class) .hasMessage("unauthorized: Bad credentials") .extracting("statusCode", "error", "errorDescription") .containsExactly(BAD_REQUEST.code(), "unauthorized", "Bad credentials")) @@ -213,11 +224,12 @@ public void uaaClientError() throws IOException { @Test public void uaaNoError() { when(this.response.status()).thenReturn(OK); + HttpClientResponseWithBody responseWithBody = buildResponseWithBody(); - Mono.just(this.response) - .transform(ErrorPayloadMapper.uaa(this.objectMapper)) + Mono.just(responseWithBody) + .transform(ErrorPayloadMappers.uaa(this.objectMapper)) .as(StepVerifier::create) - .expectNext(this.response) + .expectNext(responseWithBody) .expectComplete() .verify(Duration.ofSeconds(1)); } @@ -225,17 +237,25 @@ public void uaaNoError() { @Test public void uaaServerError() throws IOException { when(this.response.status()).thenReturn(INTERNAL_SERVER_ERROR); - when(this.response.receive()).thenReturn(ByteBufFlux.fromPath(new ClassPathResource("fixtures/uaa/error_response.json").getFile().toPath())); + HttpClientResponseWithBody responseWithBody = buildResponseWithBody(ByteBufFlux.fromPath(new ClassPathResource("fixtures/uaa/error_response.json").getFile() + .toPath())); - Mono.just(this.response) - .transform(ErrorPayloadMapper.uaa(this.objectMapper)) + Mono.just(responseWithBody) + .transform(ErrorPayloadMappers.uaa(this.objectMapper)) .as(StepVerifier::create) - .consumeErrorWith(t -> assertThat(t) - .isInstanceOf(UaaException.class) + .consumeErrorWith(t -> assertThat(t).isInstanceOf(UaaException.class) .hasMessage("unauthorized: Bad credentials") .extracting("statusCode", "error", "errorDescription") .containsExactly(INTERNAL_SERVER_ERROR.code(), "unauthorized", "Bad credentials")) .verify(Duration.ofSeconds(1)); } + private HttpClientResponseWithBody buildResponseWithBody() { + return buildResponseWithBody(ByteBufFlux.fromInbound(Flux.empty())); + } + + private HttpClientResponseWithBody buildResponseWithBody(ByteBufFlux body) { + return HttpClientResponseWithBody.of(this.response, body); + } + } diff --git a/cloudfoundry-client-reactor/src/test/resources/logback-test.xml b/cloudfoundry-client-reactor/src/test/resources/logback-test.xml index ec27c488f48..f2003df3359 100644 --- a/cloudfoundry-client-reactor/src/test/resources/logback-test.xml +++ b/cloudfoundry-client-reactor/src/test/resources/logback-test.xml @@ -26,7 +26,7 @@ - + diff --git a/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v2/serviceinstances/_DeleteServiceInstanceResponse.java b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v2/serviceinstances/_DeleteServiceInstanceResponse.java index da063048746..3a7e22ffb56 100644 --- a/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v2/serviceinstances/_DeleteServiceInstanceResponse.java +++ b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v2/serviceinstances/_DeleteServiceInstanceResponse.java @@ -28,8 +28,6 @@ import org.cloudfoundry.client.v2.jobs.JobEntity; import org.immutables.value.Value; -import java.io.IOException; - /** * The response for the Delete Service Instance operation */ diff --git a/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/servicebindings/ServiceBinding.java b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/servicebindings/ServiceBinding.java index d6315ff9ff1..c6211f57a9c 100644 --- a/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/servicebindings/ServiceBinding.java +++ b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/servicebindings/ServiceBinding.java @@ -38,5 +38,5 @@ public abstract class ServiceBinding extends Resource { @JsonProperty("type") @Nullable public abstract String getType(); - + } diff --git a/cloudfoundry-client/src/main/java/org/cloudfoundry/uaa/clients/Action.java b/cloudfoundry-client/src/main/java/org/cloudfoundry/uaa/clients/Action.java index 7f9fb19a6a4..58781161875 100644 --- a/cloudfoundry-client/src/main/java/org/cloudfoundry/uaa/clients/Action.java +++ b/cloudfoundry-client/src/main/java/org/cloudfoundry/uaa/clients/Action.java @@ -17,7 +17,7 @@ package org.cloudfoundry.uaa.clients; /* -* An interface that indicates the type of a UAA mixed action + * An interface that indicates the type of a UAA mixed action */ public interface Action { diff --git a/cloudfoundry-operations/src/main/java/org/cloudfoundry/operations/applications/DefaultApplications.java b/cloudfoundry-operations/src/main/java/org/cloudfoundry/operations/applications/DefaultApplications.java index d03fb52efdd..716c139f022 100644 --- a/cloudfoundry-operations/src/main/java/org/cloudfoundry/operations/applications/DefaultApplications.java +++ b/cloudfoundry-operations/src/main/java/org/cloudfoundry/operations/applications/DefaultApplications.java @@ -887,7 +887,7 @@ private static Flux getLogs(Mono dopplerClient, Strin return requestLogsStream(dopplerClient, applicationId) .filter(e -> EventType.LOG_MESSAGE == e.getEventType()) .map(Envelope::getLogMessage) - .compose(SortingUtils.timespan(LOG_MESSAGE_COMPARATOR, LOG_MESSAGE_TIMESPAN)); + .transformDeferred(SortingUtils.timespan(LOG_MESSAGE_COMPARATOR, LOG_MESSAGE_TIMESPAN)); } } diff --git a/cloudfoundry-operations/src/main/java/org/cloudfoundry/operations/serviceadmin/DefaultServiceAdmin.java b/cloudfoundry-operations/src/main/java/org/cloudfoundry/operations/serviceadmin/DefaultServiceAdmin.java index ceb4ee91928..14cb2218adf 100644 --- a/cloudfoundry-operations/src/main/java/org/cloudfoundry/operations/serviceadmin/DefaultServiceAdmin.java +++ b/cloudfoundry-operations/src/main/java/org/cloudfoundry/operations/serviceadmin/DefaultServiceAdmin.java @@ -159,7 +159,7 @@ public Mono update(UpdateServiceBrokerRequest request) { Mono.just(cloudFoundryClient), getServiceBrokerId(cloudFoundryClient, request.getName()) )) - .flatMap( function((cloudFoundryClient, serviceBrokerId) -> requestUpdateServiceBroker(cloudFoundryClient, request, serviceBrokerId))) + .flatMap(function((cloudFoundryClient, serviceBrokerId) -> requestUpdateServiceBroker(cloudFoundryClient, request, serviceBrokerId))) .then() .transform(OperationsLogging.log("Update Service Broker")) .checkpoint(); diff --git a/cloudfoundry-util/src/main/java/org/cloudfoundry/util/PaginationUtils.java b/cloudfoundry-util/src/main/java/org/cloudfoundry/util/PaginationUtils.java index e355e96f934..17843270f72 100644 --- a/cloudfoundry-util/src/main/java/org/cloudfoundry/util/PaginationUtils.java +++ b/cloudfoundry-util/src/main/java/org/cloudfoundry/util/PaginationUtils.java @@ -24,7 +24,6 @@ /** * A utility class to provide functions for handling PaginatedResponse and those containing lists of Resources. - * */ public final class PaginationUtils { diff --git a/integration-test/src/test/resources/logback-test.xml b/integration-test/src/test/resources/logback-test.xml index b0e60b9d098..dca05a27366 100644 --- a/integration-test/src/test/resources/logback-test.xml +++ b/integration-test/src/test/resources/logback-test.xml @@ -33,7 +33,7 @@ - + diff --git a/pom.xml b/pom.xml index 58eb44330ff..214e15b9da4 100644 --- a/pom.xml +++ b/pom.xml @@ -51,8 +51,8 @@ 0.9.1 3.12.0 UTF-8 - 3.2.3.RELEASE - 0.7.12.RELEASE + 3.3.0.RELEASE + 0.9.0.RELEASE 2.2.0 @@ -89,7 +89,7 @@ ${reactor-core.version} - io.projectreactor + io.projectreactor.netty reactor-netty ${reactor-netty.version}