com.rabbitmq.http.client.ReactorNettyClient Maven / Gradle / Ivy
Show all versions of http-client Show documentation
/*
* Copyright 2018-2022 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
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.rabbitmq.http.client;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.rabbitmq.http.client.domain.*;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufOutputStream;
import io.netty.buffer.PooledByteBufAllocator;
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.http.HttpMethod;
import org.reactivestreams.Publisher;
import reactor.core.Exceptions;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.netty.ByteBufFlux;
import reactor.netty.http.client.HttpClient;
import reactor.netty.http.client.HttpClientResponse;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Array;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import static com.rabbitmq.http.client.PercentEncoder.encodeParameter;
import static com.rabbitmq.http.client.PercentEncoder.encodePathSegment;
/**
* Reactive client based on Reactor Netty.
* Use the {@link ReactorNettyClientOptions} constructors for
* advanced settings, e.g. TLS, authentication other than HTTP basic, etc.
* The default settings for this class are the following:
*
* - {@link HttpClient}: created with the {@link HttpClient#baseUrl(String)}.
*
* -
* {@link ObjectMapper}:
DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES
and
* MapperFeature.DEFAULT_VIEW_INCLUSION
are disabled.
* {@link JsonUtils#CURRENT_USER_DETAILS_DESERIALIZER_INSTANCE}, {@link JsonUtils#USER_INFO_DESERIALIZER_INSTANCE},
* {@link JsonUtils#VHOST_LIMITS_DESERIALIZER_INSTANCE},
* and {@link JsonUtils#CHANNEL_DETAILS_DESERIALIZER_INSTANCE} set up.
*
* Mono<String> token
: basic HTTP authentication used for the
* authorization
header.
*
* BiConsumer<? super HttpRequest, ? super HttpResponse> responseCallback
:
* 4xx and 5xx responses on GET requests throw {@link HttpClientException} and {@link HttpServerException}
* respectively.
*
*
*
* @see ReactorNettyClientOptions
* @since 2.1.0
*/
public class ReactorNettyClient {
private static final Consumer JSON_HEADER = headers ->
headers.set(HttpHeaderNames.CONTENT_TYPE, HttpHeaderValues.APPLICATION_JSON);
private final ObjectMapper objectMapper;
private final HttpClient client;
private final Mono token;
private final Supplier byteBufSupplier;
private final Consumer responseCallback;
public ReactorNettyClient(String url, ReactorNettyClientOptions options) {
this(Utils.urlWithoutCredentials(url),
Utils.extractUsernamePassword(url)[0],
Utils.extractUsernamePassword(url)[1], options);
}
public ReactorNettyClient(String url) {
this(url, new ReactorNettyClientOptions());
}
public ReactorNettyClient(String url, String username, String password) {
this(url, username, password, new ReactorNettyClientOptions());
}
public ReactorNettyClient(String url, String username, String password, ReactorNettyClientOptions options) {
objectMapper = options.objectMapper() == null ? createDefaultObjectMapper() : options.objectMapper().get();
client = options.client() == null ?
HttpClient.create().baseUrl(url) : options.client().get();
this.token = options.token() == null ? createBasicAuthenticationToken(username, password) : options.token();
if (options.onResponseCallback() == null) {
this.responseCallback = response -> {
if (response.method() == HttpMethod.GET) {
if (response.status().code() >= 500) {
throw new HttpServerException(response.status().code(), response.status().reasonPhrase());
} else if (response.status().code() >= 400) {
throw new HttpClientException(response.status().code(), response.status().reasonPhrase());
}
}
};
} else {
this.responseCallback = response ->
options.onResponseCallback().accept(new HttpEndpoint(response.uri(), response.method().name()), toHttpResponse(response));
}
this.byteBufSupplier = options.byteBufSupplier() == null ?
() -> PooledByteBufAllocator.DEFAULT.buffer() :
options.byteBufSupplier();
}
private static HttpResponse toHttpResponse(HttpClientResponse response) {
Map headers = new LinkedHashMap<>();
for (Map.Entry headerEntry : response.responseHeaders().entries()) {
headers.put(headerEntry.getKey(), headerEntry.getValue());
}
return new HttpResponse(response.status().code(), response.status().reasonPhrase(), headers);
}
public static ObjectMapper createDefaultObjectMapper() {
return JsonUtils.createDefaultObjectMapper();
}
public static Mono createBasicAuthenticationToken(String username, String password) {
return Mono.fromSupplier(() -> basicAuthentication(username, password)).cache();
}
public static String basicAuthentication(String username, String password) {
String credentials = username + ":" + password;
byte[] credentialsAsBytes = credentials.getBytes(StandardCharsets.ISO_8859_1);
byte[] encodedBytes = Base64.getEncoder().encode(credentialsAsBytes);
String encodedCredentials = new String(encodedBytes, StandardCharsets.ISO_8859_1);
return "Basic " + encodedCredentials;
}
public Mono getOverview() {
return doGetMono(OverviewResponse.class, "overview");
}
public Flux getNodes() {
return doGetFlux(NodeInfo.class, "nodes");
}
public Mono getNode(String name) {
return doGetMono(NodeInfo.class, "nodes", encodePathSegment(name));
}
public Flux getConnections() {
return doGetFlux(ConnectionInfo.class, "connections");
}
public Mono getConnection(String name) {
return doGetMono(ConnectionInfo.class, "connections", encodePathSegment(name));
}
public Mono closeConnection(String name) {
return doDelete("connections", encodePathSegment(name));
}
public Mono closeConnection(String name, String reason) {
return doDelete(headers -> headers.set("X-Reason", reason), "connections", encodePathSegment(name));
}
public Flux getConsumers() {
return doGetFlux(ConsumerDetails.class, "consumers");
}
public Flux getConsumers(String vhost) {
return doGetFlux(ConsumerDetails.class, "consumers", encodePathSegment(vhost));
}
public Mono declarePolicy(String vhost, String name, PolicyInfo info) {
return doPut(info, "policies", encodePathSegment(vhost), encodePathSegment(name));
}
public Mono declareOperatorPolicy(String vhost, String name, PolicyInfo info) {
return doPut(info, "operator-policies", encodePathSegment(vhost), encodePathSegment(name));
}
public Flux getPolicies() {
return doGetFlux(PolicyInfo.class, "policies");
}
public Flux getPolicies(String vhost) {
return doGetFlux(PolicyInfo.class, "policies", encodePathSegment(vhost));
}
public Flux getOperatorPolicies() {
return doGetFlux(PolicyInfo.class, "operator-policies");
}
public Flux getOperatorPolicies(String vhost) {
return doGetFlux(PolicyInfo.class, "operator-policies", encodePathSegment(vhost));
}
public Mono deletePolicy(String vhost, String name) {
return doDelete("policies", encodePathSegment(vhost), encodePathSegment(name));
}
public Mono deleteOperatorPolicy(String vhost, String name) {
return doDelete("operator-policies", encodePathSegment(vhost), encodePathSegment(name));
}
public Flux getChannels() {
return doGetFlux(ChannelInfo.class, "channels");
}
public Flux getChannels(String connectionName) {
return doGetFlux(ChannelInfo.class, "connections", encodePathSegment(connectionName), "channels");
}
public Mono getChannel(String name) {
return doGetMono(ChannelInfo.class, "channels", encodePathSegment(name));
}
public Flux getVhosts() {
return doGetFlux(VhostInfo.class, "vhosts");
}
public Mono getVhost(String name) {
return doGetMono(VhostInfo.class, "vhosts", encodePathSegment(name));
}
/**
* Create a virtual host with name, tracing flag, and metadata.
* Note metadata (description and tags) are supported as of RabbitMQ 3.8.
*
* @param name name of the virtual host
* @param tracing whether tracing is enabled or not
* @param description virtual host description (requires RabbitMQ 3.8 or more)
* @param tags virtual host tags (requires RabbitMQ 3.8 or more)
* @return response wrapped in {@link Mono}
* @since 3.4.0
*/
public Mono createVhost(String name, boolean tracing, String description, String... tags) {
Map body = new HashMap();
body.put("tracing", tracing);
if (description != null && !description.isEmpty()) {
body.put("description", description);
}
if (tags != null && tags.length > 0) {
body.put("tags", String.join(",", tags));
}
return doPut(body, "vhosts", encodePathSegment(name));
}
/**
* Create a virtual host with name and metadata.
* Note metadata (description and tags) are supported as of RabbitMQ 3.8.
*
* @param name name of the virtual host
* @param description virtual host description (requires RabbitMQ 3.8 or more)
* @param tags virtual host tags (requires RabbitMQ 3.8 or more)
* @return response wrapped in {@link Mono}
* @since 3.4.0
*/
public Mono createVhost(String name, String description, String... tags) {
return createVhost(name, false, description, tags);
}
/**
* Create a virtual host with name and tracing flag.
*
* @param name name of the virtual host
* @param tracing whether tracing is enabled or not
* @return response wrapped in {@link Mono}
* @since 3.4.0
*/
public Mono createVhost(String name, boolean tracing) {
return createVhost(name, tracing, null);
}
public Mono createVhost(String name) {
return doPut("vhosts", encodePathSegment(name));
}
public Mono deleteVhost(String name) {
return doDelete("vhosts", encodePathSegment(name));
}
public Flux getPermissionsIn(String vhost) {
return doGetFlux(UserPermissions.class, "vhosts", encodePathSegment(vhost), "permissions");
}
public Mono updatePermissions(String vhost, String username, UserPermissions permissions) {
return doPut(permissions, "permissions", encodePathSegment(vhost), encodePathSegment(username));
}
public Flux getTopicPermissionsIn(String vhost) {
return doGetFlux(TopicPermissions.class, "vhosts", encodePathSegment(vhost), "topic-permissions");
}
public Mono updateTopicPermissions(String vhost, String username, TopicPermissions permissions) {
return doPut(permissions, "topic-permissions", encodePathSegment(vhost), encodePathSegment(username));
}
public Flux getUsers() {
return doGetFlux(UserInfo.class, "users");
}
public Mono getUser(String username) {
return doGetMono(UserInfo.class, "users", encodePathSegment(username));
}
public Mono deleteUser(String username) {
return doDelete("users", encodePathSegment(username));
}
public Mono createUser(String username, char[] password, List tags) {
if (username == null) {
throw new IllegalArgumentException("username cannot be null");
}
if (password == null) {
throw new IllegalArgumentException("password cannot be null or empty. If you need to create a user that "
+ "will only authenticate using an x509 certificate, use createUserWithPasswordHash with a blank hash.");
}
Map body = new HashMap();
body.put("password", new String(password));
if (tags == null || tags.isEmpty()) {
body.put("tags", "");
} else {
body.put("tags", String.join(",", tags));
}
return doPut(body, "users", encodePathSegment(username));
}
public Mono updateUser(String username, char[] password, List tags) {
if (username == null) {
throw new IllegalArgumentException("username cannot be null");
}
Map body = new HashMap();
// only update password if provided
if (password != null) {
body.put("password", new String(password));
}
if (tags == null || tags.isEmpty()) {
body.put("tags", "");
} else {
body.put("tags", String.join(",", tags));
}
return doPut(body, "users", encodePathSegment(username));
}
public Flux getPermissionsOf(String username) {
return doGetFlux(UserPermissions.class, "users", encodePathSegment(username), "permissions");
}
public Flux getTopicPermissionsOf(String username) {
return doGetFlux(TopicPermissions.class, "users", encodePathSegment(username), "topic-permissions");
}
public Mono createUserWithPasswordHash(String username, char[] passwordHash, List tags) {
if (username == null) {
throw new IllegalArgumentException("username cannot be null");
}
// passwordless authentication is a thing. See
// https://github.com/rabbitmq/hop/issues/94 and https://www.rabbitmq.com/authentication.html. MK.
if (passwordHash == null) {
passwordHash = "".toCharArray();
}
Map body = new HashMap();
body.put("password_hash", String.valueOf(passwordHash));
if (tags == null || tags.isEmpty()) {
body.put("tags", "");
} else {
body.put("tags", String.join(",", tags));
}
return doPut(body, "users", encodePathSegment(username));
}
public Mono whoAmI() {
return doGetMono(CurrentUserDetails.class, "whoami");
}
public Flux getPermissions() {
return doGetFlux(UserPermissions.class, "permissions");
}
public Mono getPermissions(String vhost, String username) {
return doGetMono(UserPermissions.class, "permissions", encodePathSegment(vhost), encodePathSegment(username));
}
public Mono clearPermissions(String vhost, String username) {
return doDelete("permissions", encodePathSegment(vhost), encodePathSegment(username));
}
public Flux getTopicPermissions() {
return doGetFlux(TopicPermissions.class, "topic-permissions");
}
public Flux getTopicPermissions(String vhost, String username) {
return doGetFlux(TopicPermissions.class, "topic-permissions", encodePathSegment(vhost), encodePathSegment(username));
}
public Mono clearTopicPermissions(String vhost, String username) {
return doDelete("topic-permissions", encodePathSegment(vhost), encodePathSegment(username));
}
public Flux getExchanges() {
return doGetFlux(ExchangeInfo.class, "exchanges");
}
public Flux getExchanges(String vhost) {
return doGetFlux(ExchangeInfo.class, "exchanges", encodePathSegment(vhost));
}
public Mono getExchange(String vhost, String name) {
return doGetMono(ExchangeInfo.class, "exchanges", encodePathSegment(vhost), encodePathSegment(name));
}
public Mono declareExchange(String vhost, String name, ExchangeInfo info) {
return doPut(info, "exchanges", encodePathSegment(vhost), encodePathSegment(name));
}
public Mono deleteExchange(String vhost, String name) {
return doDelete("exchanges", encodePathSegment(vhost), encodePathSegment(name));
}
/**
* Publishes a message to an exchange.
*
* DO NOT USE THIS METHOD IN PRODUCTION. The HTTP API has to create a new TCP
* connection for each message published, which is highly suboptimal.
*
* Use this method for test or development code only.
* In production, use AMQP 0-9-1 or any other messaging protocol that uses a long-lived connection.
*
* @param vhost the virtual host to use
* @param exchange the target exchange
* @param routingKey the routing key to use
* @param outboundMessage the message to publish
* @return true if message has been routed to at least a queue, false otherwise
* @since 3.4.0
*/
public Mono publish(String vhost, String exchange, String routingKey, OutboundMessage outboundMessage) {
if (vhost == null || vhost.isEmpty()) {
throw new IllegalArgumentException("vhost cannot be null or blank");
}
if (exchange == null || exchange.isEmpty()) {
throw new IllegalArgumentException("exchange cannot be null or blank");
}
Map body = Utils.bodyForPublish(routingKey, outboundMessage);
return doPostMono(body, Map.class, "exchanges", encodePathSegment(vhost), encodePathSegment(exchange), "publish").map(response -> {
Boolean routed = (Boolean) response.get("routed");
if (routed == null) {
return Boolean.FALSE;
} else {
return routed;
}
});
}
public Mono alivenessTest(String vhost) {
return doGetMono(AlivenessTestResult.class, "aliveness-test", encodePathSegment(vhost));
}
public Mono getClusterName() {
return doGetMono(ClusterId.class, "cluster-name");
}
public Mono setClusterName(String name) {
if (name == null || name.isEmpty()) {
throw new IllegalArgumentException("name cannot be null or blank");
}
return doPut(Collections.singletonMap("name", name), "cluster-name");
}
@SuppressWarnings({"unchecked", "rawtypes"})
public Flux