Browse Source

Support for RSocket composite metadata

Closes gh-22798
pull/23128/head
Rossen Stoyanchev 6 years ago
parent
commit
14e2c6803e
  1. 142
      spring-messaging/src/main/java/org/springframework/messaging/rsocket/DefaultRSocketRequester.java
  2. 18
      spring-messaging/src/main/java/org/springframework/messaging/rsocket/DefaultRSocketRequesterBuilder.java
  3. 46
      spring-messaging/src/main/java/org/springframework/messaging/rsocket/MessageHandlerAcceptor.java
  4. 43
      spring-messaging/src/main/java/org/springframework/messaging/rsocket/MessagingRSocket.java
  5. 98
      spring-messaging/src/main/java/org/springframework/messaging/rsocket/RSocketRequester.java
  6. 101
      spring-messaging/src/test/java/org/springframework/messaging/rsocket/DefaultRSocketRequesterTests.java
  7. 4
      spring-messaging/src/test/java/org/springframework/messaging/rsocket/RSocketClientToServerIntegrationTests.java
  8. 3
      spring-messaging/src/test/java/org/springframework/messaging/rsocket/RSocketServerToClientIntegrationTests.java
  9. 9
      spring-web/src/test/java/org/springframework/http/ContentDispositionTests.java

142
spring-messaging/src/main/java/org/springframework/messaging/rsocket/DefaultRSocketRequester.java

@ -16,12 +16,19 @@
package org.springframework.messaging.rsocket; package org.springframework.messaging.rsocket;
import java.nio.charset.StandardCharsets; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator;
import io.netty.buffer.CompositeByteBuf;
import io.netty.buffer.Unpooled;
import io.rsocket.Payload; import io.rsocket.Payload;
import io.rsocket.RSocket; import io.rsocket.RSocket;
import io.rsocket.metadata.CompositeMetadataFlyweight;
import org.reactivestreams.Publisher; import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux; import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
@ -32,6 +39,10 @@ import org.springframework.core.ResolvableType;
import org.springframework.core.codec.Decoder; import org.springframework.core.codec.Decoder;
import org.springframework.core.codec.Encoder; import org.springframework.core.codec.Encoder;
import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.core.io.buffer.NettyDataBuffer;
import org.springframework.core.io.buffer.NettyDataBufferFactory;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.util.MimeType; import org.springframework.util.MimeType;
@ -44,24 +55,42 @@ import org.springframework.util.MimeType;
*/ */
final class DefaultRSocketRequester implements RSocketRequester { final class DefaultRSocketRequester implements RSocketRequester {
static final MimeType COMPOSITE_METADATA = new MimeType("message", "x.rsocket.composite-metadata.v0");
static final MimeType ROUTING = new MimeType("message", "x.rsocket.routing.v0");
static final List<MimeType> METADATA_MIME_TYPES = Arrays.asList(COMPOSITE_METADATA, ROUTING);
private static final Map<String, Object> EMPTY_HINTS = Collections.emptyMap(); private static final Map<String, Object> EMPTY_HINTS = Collections.emptyMap();
private final RSocket rsocket; private final RSocket rsocket;
@Nullable
private final MimeType dataMimeType; private final MimeType dataMimeType;
private final MimeType metadataMimeType;
private final RSocketStrategies strategies; private final RSocketStrategies strategies;
private DataBuffer emptyDataBuffer; private final DataBuffer emptyDataBuffer;
DefaultRSocketRequester(RSocket rsocket, @Nullable MimeType dataMimeType, RSocketStrategies strategies) { DefaultRSocketRequester(
RSocket rsocket, MimeType dataMimeType, MimeType metadataMimeType,
RSocketStrategies strategies) {
Assert.notNull(rsocket, "RSocket is required"); Assert.notNull(rsocket, "RSocket is required");
Assert.notNull(dataMimeType, "'dataMimeType' is required");
Assert.notNull(metadataMimeType, "'metadataMimeType' is required");
Assert.notNull(strategies, "RSocketStrategies is required"); Assert.notNull(strategies, "RSocketStrategies is required");
Assert.isTrue(METADATA_MIME_TYPES.contains(metadataMimeType),
() -> "Unexpected metadatata mime type: '" + metadataMimeType + "'");
this.rsocket = rsocket; this.rsocket = rsocket;
this.dataMimeType = dataMimeType; this.dataMimeType = dataMimeType;
this.metadataMimeType = metadataMimeType;
this.strategies = strategies; this.strategies = strategies;
this.emptyDataBuffer = this.strategies.dataBufferFactory().wrap(new byte[0]); this.emptyDataBuffer = this.strategies.dataBufferFactory().wrap(new byte[0]);
} }
@ -72,6 +101,16 @@ final class DefaultRSocketRequester implements RSocketRequester {
return this.rsocket; return this.rsocket;
} }
@Override
public MimeType dataMimeType() {
return this.dataMimeType;
}
@Override
public MimeType metadataMimeType() {
return this.metadataMimeType;
}
@Override @Override
public RequestSpec route(String route) { public RequestSpec route(String route) {
return new DefaultRequestSpec(route); return new DefaultRequestSpec(route);
@ -82,13 +121,28 @@ final class DefaultRSocketRequester implements RSocketRequester {
return (Void.class.equals(elementType.resolve()) || void.class.equals(elementType.resolve())); return (Void.class.equals(elementType.resolve()) || void.class.equals(elementType.resolve()));
} }
private DataBufferFactory bufferFactory() {
return this.strategies.dataBufferFactory();
}
private class DefaultRequestSpec implements RequestSpec { private class DefaultRequestSpec implements RequestSpec {
private final String route; private final Map<Object, MimeType> metadata = new LinkedHashMap<>(4);
public DefaultRequestSpec(String route) {
Assert.notNull(route, "'route' is required");
metadata(route, ROUTING);
}
DefaultRequestSpec(String route) { @Override
this.route = route; public RequestSpec metadata(Object metadata, MimeType mimeType) {
Assert.isTrue(this.metadata.isEmpty() || metadataMimeType().equals(COMPOSITE_METADATA),
"Additional metadata entries supported only with composite metadata");
this.metadata.put(metadata, mimeType);
return this;
} }
@Override @Override
@ -122,7 +176,7 @@ final class DefaultRSocketRequester implements RSocketRequester {
} }
else { else {
Mono<Payload> payloadMono = Mono Mono<Payload> payloadMono = Mono
.fromCallable(() -> encodeValue(input, ResolvableType.forInstance(input), null)) .fromCallable(() -> encodeData(input, ResolvableType.forInstance(input), null))
.map(this::firstPayload) .map(this::firstPayload)
.doOnDiscard(Payload.class, Payload::release) .doOnDiscard(Payload.class, Payload::release)
.switchIfEmpty(emptyPayload()); .switchIfEmpty(emptyPayload());
@ -139,14 +193,14 @@ final class DefaultRSocketRequester implements RSocketRequester {
if (adapter != null && !adapter.isMultiValue()) { if (adapter != null && !adapter.isMultiValue()) {
Mono<Payload> payloadMono = Mono.from(publisher) Mono<Payload> payloadMono = Mono.from(publisher)
.map(value -> encodeValue(value, dataType, encoder)) .map(value -> encodeData(value, dataType, encoder))
.map(this::firstPayload) .map(this::firstPayload)
.switchIfEmpty(emptyPayload()); .switchIfEmpty(emptyPayload());
return new DefaultResponseSpec(payloadMono); return new DefaultResponseSpec(payloadMono);
} }
Flux<Payload> payloadFlux = Flux.from(publisher) Flux<Payload> payloadFlux = Flux.from(publisher)
.map(value -> encodeValue(value, dataType, encoder)) .map(value -> encodeData(value, dataType, encoder))
.switchOnFirst((signal, inner) -> { .switchOnFirst((signal, inner) -> {
DataBuffer data = signal.get(); DataBuffer data = signal.get();
if (data != null) { if (data != null) {
@ -163,16 +217,28 @@ final class DefaultRSocketRequester implements RSocketRequester {
} }
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
private <T> DataBuffer encodeValue(T value, ResolvableType valueType, @Nullable Encoder<?> encoder) { private <T> DataBuffer encodeData(T value, ResolvableType valueType, @Nullable Encoder<?> encoder) {
if (value instanceof DataBuffer) {
return (DataBuffer) value;
}
if (encoder == null) { if (encoder == null) {
encoder = strategies.encoder(ResolvableType.forInstance(value), dataMimeType); valueType = ResolvableType.forInstance(value);
encoder = strategies.encoder(valueType, dataMimeType);
} }
return ((Encoder<T>) encoder).encodeValue( return ((Encoder<T>) encoder).encodeValue(
value, strategies.dataBufferFactory(), valueType, dataMimeType, EMPTY_HINTS); value, bufferFactory(), valueType, dataMimeType, EMPTY_HINTS);
} }
private Payload firstPayload(DataBuffer data) { private Payload firstPayload(DataBuffer data) {
return PayloadUtils.createPayload(getMetadata(), data); DataBuffer metadata;
try {
metadata = getMetadata();
return PayloadUtils.createPayload(metadata, data);
}
catch (Throwable ex) {
DataBufferUtils.release(data);
throw ex;
}
} }
private Mono<Payload> emptyPayload() { private Mono<Payload> emptyPayload() {
@ -180,7 +246,51 @@ final class DefaultRSocketRequester implements RSocketRequester {
} }
private DataBuffer getMetadata() { private DataBuffer getMetadata() {
return strategies.dataBufferFactory().wrap(this.route.getBytes(StandardCharsets.UTF_8)); if (metadataMimeType().equals(COMPOSITE_METADATA)) {
CompositeByteBuf metadata = getAllocator().compositeBuffer();
this.metadata.forEach((key, value) -> {
DataBuffer dataBuffer = encodeMetadata(key, value);
CompositeMetadataFlyweight.encodeAndAddMetadata(metadata, getAllocator(), value.toString(),
dataBuffer instanceof NettyDataBuffer ?
((NettyDataBuffer) dataBuffer).getNativeBuffer() :
Unpooled.wrappedBuffer(dataBuffer.asByteBuffer()));
});
return asDataBuffer(metadata);
}
Assert.isTrue(this.metadata.size() < 2, "Composite metadata required for multiple entries");
Map.Entry<Object, MimeType> entry = this.metadata.entrySet().iterator().next();
Assert.isTrue(metadataMimeType().equals(entry.getValue()),
() -> "Expected metadata MimeType '" + metadataMimeType() + "', actual " + this.metadata);
return encodeMetadata(entry.getKey(), entry.getValue());
}
@SuppressWarnings("unchecked")
private <T> DataBuffer encodeMetadata(Object metadata, MimeType mimeType) {
if (metadata instanceof DataBuffer) {
return (DataBuffer) metadata;
}
ResolvableType type = ResolvableType.forInstance(metadata);
Encoder<T> encoder = strategies.encoder(type, mimeType);
Assert.notNull(encoder, () -> "No encoder for metadata " + metadata + ", mimeType '" + mimeType + "'");
return encoder.encodeValue((T) metadata, bufferFactory(), type, mimeType, EMPTY_HINTS);
}
private ByteBufAllocator getAllocator() {
return bufferFactory() instanceof NettyDataBufferFactory ?
((NettyDataBufferFactory) bufferFactory()).getByteBufAllocator() :
ByteBufAllocator.DEFAULT;
}
private DataBuffer asDataBuffer(ByteBuf byteBuf) {
if (bufferFactory() instanceof NettyDataBufferFactory) {
return ((NettyDataBufferFactory) bufferFactory()).wrap(byteBuf);
}
else {
DataBuffer dataBuffer = bufferFactory().wrap(byteBuf.nioBuffer());
byteBuf.release();
return dataBuffer;
}
} }
} }
@ -259,7 +369,7 @@ final class DefaultRSocketRequester implements RSocketRequester {
} }
private DataBuffer retainDataAndReleasePayload(Payload payload) { private DataBuffer retainDataAndReleasePayload(Payload payload) {
return PayloadUtils.retainDataAndReleasePayload(payload, strategies.dataBufferFactory()); return PayloadUtils.retainDataAndReleasePayload(payload, bufferFactory());
} }
} }

18
spring-messaging/src/main/java/org/springframework/messaging/rsocket/DefaultRSocketRequesterBuilder.java

@ -44,6 +44,8 @@ final class DefaultRSocketRequesterBuilder implements RSocketRequester.Builder {
@Nullable @Nullable
private MimeType dataMimeType; private MimeType dataMimeType;
private MimeType metadataMimeType = DefaultRSocketRequester.COMPOSITE_METADATA;
private List<Consumer<RSocketFactory.ClientRSocketFactory>> factoryConfigurers = new ArrayList<>(); private List<Consumer<RSocketFactory.ClientRSocketFactory>> factoryConfigurers = new ArrayList<>();
@Nullable @Nullable
@ -53,11 +55,18 @@ final class DefaultRSocketRequesterBuilder implements RSocketRequester.Builder {
@Override @Override
public RSocketRequester.Builder dataMimeType(MimeType mimeType) { public RSocketRequester.Builder dataMimeType(@Nullable MimeType mimeType) {
this.dataMimeType = mimeType; this.dataMimeType = mimeType;
return this; return this;
} }
@Override
public RSocketRequester.Builder metadataMimeType(MimeType mimeType) {
Assert.notNull(mimeType, "`metadataMimeType` is required");
this.metadataMimeType = mimeType;
return this;
}
@Override @Override
public RSocketRequester.Builder rsocketFactory(Consumer<RSocketFactory.ClientRSocketFactory> configurer) { public RSocketRequester.Builder rsocketFactory(Consumer<RSocketFactory.ClientRSocketFactory> configurer) {
this.factoryConfigurers.add(configurer); this.factoryConfigurers.add(configurer);
@ -100,10 +109,13 @@ final class DefaultRSocketRequesterBuilder implements RSocketRequester.Builder {
RSocketFactory.ClientRSocketFactory rsocketFactory = RSocketFactory.connect(); RSocketFactory.ClientRSocketFactory rsocketFactory = RSocketFactory.connect();
MimeType dataMimeType = getDataMimeType(rsocketStrategies); MimeType dataMimeType = getDataMimeType(rsocketStrategies);
rsocketFactory.dataMimeType(dataMimeType.toString()); rsocketFactory.dataMimeType(dataMimeType.toString());
rsocketFactory.metadataMimeType(this.metadataMimeType.toString());
this.factoryConfigurers.forEach(consumer -> consumer.accept(rsocketFactory)); this.factoryConfigurers.forEach(consumer -> consumer.accept(rsocketFactory));
return rsocketFactory.transport(transport).start() return rsocketFactory.transport(transport)
.map(rsocket -> new DefaultRSocketRequester(rsocket, dataMimeType, rsocketStrategies)); .start()
.map(rsocket -> new DefaultRSocketRequester(
rsocket, dataMimeType, this.metadataMimeType, rsocketStrategies));
} }
private RSocketStrategies getRSocketStrategies() { private RSocketStrategies getRSocketStrategies() {

46
spring-messaging/src/main/java/org/springframework/messaging/rsocket/MessageHandlerAcceptor.java

@ -24,9 +24,9 @@ import io.rsocket.RSocket;
import io.rsocket.SocketAcceptor; import io.rsocket.SocketAcceptor;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
import org.springframework.messaging.Message; import org.springframework.messaging.Message;
import org.springframework.util.Assert;
import org.springframework.util.MimeType; import org.springframework.util.MimeType;
import org.springframework.util.MimeTypeUtils; import org.springframework.util.MimeTypeUtils;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
@ -47,16 +47,28 @@ public final class MessageHandlerAcceptor extends RSocketMessageHandler
@Nullable @Nullable
private MimeType defaultDataMimeType; private MimeType defaultDataMimeType;
private MimeType defaultMetadataMimeType = DefaultRSocketRequester.COMPOSITE_METADATA;
/**
* Configure the default content type to use for data payloads if the
* {@code SETUP} frame did not specify one.
* <p>By default this is not set.
* @param mimeType the MimeType to use
*/
public void setDefaultDataMimeType(@Nullable MimeType mimeType) {
this.defaultDataMimeType = mimeType;
}
/** /**
* Configure the default content type to use for data payloads. * Configure the default {@code MimeType} for payload data if the
* <p>By default this is not set. However a server acceptor will use the * {@code SETUP} frame did not specify one.
* content type from the {@link ConnectionSetupPayload}, so this is typically * <p>By default this is set to {@code "message/x.rsocket.composite-metadata.v0"}
* required for clients but can also be used on servers as a fallback. * @param mimeType the MimeType to use
* @param defaultDataMimeType the MimeType to use
*/ */
public void setDefaultDataMimeType(@Nullable MimeType defaultDataMimeType) { public void setDefaultMetadataMimeType(MimeType mimeType) {
this.defaultDataMimeType = defaultDataMimeType; Assert.notNull(mimeType, "'metadataMimeType' is required");
this.defaultMetadataMimeType = mimeType;
} }
@ -76,12 +88,24 @@ public final class MessageHandlerAcceptor extends RSocketMessageHandler
} }
private MessagingRSocket createRSocket(ConnectionSetupPayload setupPayload, RSocket rsocket) { private MessagingRSocket createRSocket(ConnectionSetupPayload setupPayload, RSocket rsocket) {
MimeType dataMimeType = StringUtils.hasText(setupPayload.dataMimeType()) ? MimeType dataMimeType = StringUtils.hasText(setupPayload.dataMimeType()) ?
MimeTypeUtils.parseMimeType(setupPayload.dataMimeType()) : MimeTypeUtils.parseMimeType(setupPayload.dataMimeType()) :
this.defaultDataMimeType; this.defaultDataMimeType;
RSocketRequester requester = RSocketRequester.wrap(rsocket, dataMimeType, getRSocketStrategies()); Assert.notNull(dataMimeType,
DataBufferFactory bufferFactory = getRSocketStrategies().dataBufferFactory(); "No `dataMimeType` in the ConnectionSetupPayload and no default value");
return new MessagingRSocket(this, getRouteMatcher(), requester, dataMimeType, bufferFactory);
MimeType metadataMimeType = StringUtils.hasText(setupPayload.dataMimeType()) ?
MimeTypeUtils.parseMimeType(setupPayload.metadataMimeType()) :
this.defaultMetadataMimeType;
Assert.notNull(dataMimeType,
"No `metadataMimeType` in the ConnectionSetupPayload and no default value");
RSocketRequester requester = RSocketRequester.wrap(
rsocket, dataMimeType, metadataMimeType, getRSocketStrategies());
return new MessagingRSocket(this, getRouteMatcher(), requester,
dataMimeType, metadataMimeType, getRSocketStrategies().dataBufferFactory());
} }
} }

43
spring-messaging/src/main/java/org/springframework/messaging/rsocket/MessagingRSocket.java

@ -16,6 +16,7 @@
package org.springframework.messaging.rsocket; package org.springframework.messaging.rsocket;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Function; import java.util.function.Function;
@ -23,6 +24,7 @@ import io.rsocket.AbstractRSocket;
import io.rsocket.ConnectionSetupPayload; import io.rsocket.ConnectionSetupPayload;
import io.rsocket.Payload; import io.rsocket.Payload;
import io.rsocket.RSocket; import io.rsocket.RSocket;
import io.rsocket.metadata.CompositeMetadata;
import org.reactivestreams.Publisher; import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux; import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
@ -61,24 +63,31 @@ class MessagingRSocket extends AbstractRSocket {
private final RSocketRequester requester; private final RSocketRequester requester;
@Nullable private final MimeType dataMimeType;
private MimeType dataMimeType;
private final MimeType metadataMimeType;
private final DataBufferFactory bufferFactory; private final DataBufferFactory bufferFactory;
MessagingRSocket(RSocketMessageHandler messageHandler, RouteMatcher routeMatcher, MessagingRSocket(RSocketMessageHandler messageHandler, RouteMatcher routeMatcher,
RSocketRequester requester, @Nullable MimeType defaultDataMimeType, RSocketRequester requester, MimeType dataMimeType, MimeType metadataMimeType,
DataBufferFactory bufferFactory) { DataBufferFactory bufferFactory) {
Assert.notNull(messageHandler, "'messageHandler' is required"); Assert.notNull(messageHandler, "'messageHandler' is required");
Assert.notNull(routeMatcher, "'routeMatcher' is required"); Assert.notNull(routeMatcher, "'routeMatcher' is required");
Assert.notNull(requester, "'requester' is required"); Assert.notNull(requester, "'requester' is required");
Assert.notNull(requester, "'dataMimeType' is required");
Assert.notNull(requester, "'metadataMimeType' is required");
Assert.isTrue(DefaultRSocketRequester.METADATA_MIME_TYPES.contains(metadataMimeType),
() -> "Unexpected metadatata mime type: '" + metadataMimeType + "'");
this.messageHandler = messageHandler; this.messageHandler = messageHandler;
this.routeMatcher = routeMatcher; this.routeMatcher = routeMatcher;
this.requester = requester; this.requester = requester;
this.dataMimeType = defaultDataMimeType; this.dataMimeType = dataMimeType;
this.metadataMimeType = metadataMimeType;
this.bufferFactory = bufferFactory; this.bufferFactory = bufferFactory;
} }
@ -169,13 +178,21 @@ class MessagingRSocket extends AbstractRSocket {
} }
private String getDestination(Payload payload) { private String getDestination(Payload payload) {
if (this.metadataMimeType.equals(DefaultRSocketRequester.COMPOSITE_METADATA)) {
// TODO: CompositeMetadata metadata = new CompositeMetadata(payload.metadata(), false);
// For now treat the metadata as a simple string with routing information. for (CompositeMetadata.Entry entry : metadata) {
// We'll have to get more sophisticated once the routing extension is completed. String mimeType = entry.getMimeType();
// https://github.com/rsocket/rsocket-java/issues/568 if (DefaultRSocketRequester.ROUTING.toString().equals(mimeType)) {
return entry.getContent().toString(StandardCharsets.UTF_8);
return payload.getMetadataUtf8(); }
}
return "";
}
else if (this.metadataMimeType.equals(DefaultRSocketRequester.ROUTING)) {
return payload.getMetadataUtf8();
}
// Should not happen (given constructor assertions)
throw new IllegalArgumentException("Unexpected metadata MimeType");
} }
private DataBuffer retainDataAndReleasePayload(Payload payload) { private DataBuffer retainDataAndReleasePayload(Payload payload) {
@ -187,9 +204,7 @@ class MessagingRSocket extends AbstractRSocket {
headers.setLeaveMutable(true); headers.setLeaveMutable(true);
RouteMatcher.Route route = this.routeMatcher.parseRoute(destination); RouteMatcher.Route route = this.routeMatcher.parseRoute(destination);
headers.setHeader(DestinationPatternsMessageCondition.LOOKUP_DESTINATION_HEADER, route); headers.setHeader(DestinationPatternsMessageCondition.LOOKUP_DESTINATION_HEADER, route);
if (this.dataMimeType != null) { headers.setContentType(this.dataMimeType);
headers.setContentType(this.dataMimeType);
}
headers.setHeader(RSocketRequesterMethodArgumentResolver.RSOCKET_REQUESTER_HEADER, this.requester); headers.setHeader(RSocketRequesterMethodArgumentResolver.RSOCKET_REQUESTER_HEADER, this.requester);
if (replyMono != null) { if (replyMono != null) {
headers.setHeader(RSocketPayloadReturnValueHandler.RESPONSE_HEADER, replyMono); headers.setHeader(RSocketPayloadReturnValueHandler.RESPONSE_HEADER, replyMono);

98
spring-messaging/src/main/java/org/springframework/messaging/rsocket/RSocketRequester.java

@ -19,6 +19,7 @@ package org.springframework.messaging.rsocket;
import java.net.URI; import java.net.URI;
import java.util.function.Consumer; import java.util.function.Consumer;
import io.rsocket.ConnectionSetupPayload;
import io.rsocket.RSocket; import io.rsocket.RSocket;
import io.rsocket.RSocketFactory; import io.rsocket.RSocketFactory;
import io.rsocket.transport.ClientTransport; import io.rsocket.transport.ClientTransport;
@ -47,15 +48,28 @@ public interface RSocketRequester {
*/ */
RSocket rsocket(); RSocket rsocket();
// For now we treat metadata as a simple string that is the route. /**
// This will change after the resolution of: * Return the data {@code MimeType} selected for the underlying RSocket
// https://github.com/rsocket/rsocket-java/issues/568 * at connection time. On the client side this is configured via
* {@link RSocketRequester.Builder#dataMimeType(MimeType)} while on the
* server side it's obtained from the {@link ConnectionSetupPayload}.
*/
MimeType dataMimeType();
/** /**
* Entry point to prepare a new request to the given route. * Return the metadata {@code MimeType} selected for the underlying RSocket
* <p>For requestChannel interactions, i.e. Flux-to-Flux the metadata is * at connection time. On the client side this is configured via
* attached to the first request payload. * {@link RSocketRequester.Builder#metadataMimeType(MimeType)} while on the
* @param route the routing destination * server side it's obtained from the {@link ConnectionSetupPayload}.
*/
MimeType metadataMimeType();
/**
* Begin to specify a new request with the given route to a handler on the
* remote side. The route will be encoded in the metadata of the first
* payload.
* @param route the route to a handler
* @return a spec for further defining and executing the request * @return a spec for further defining and executing the request
*/ */
RequestSpec route(String route); RequestSpec route(String route);
@ -72,31 +86,19 @@ public interface RSocketRequester {
} }
/** /**
* Wrap an existing {@link RSocket}. Typically used in a client or server * Wrap an existing {@link RSocket}. This is typically used in a responder,
* responder to wrap the remote {@code RSocket}. * client or server, to wrap the remote/sending {@code RSocket}.
* @param rsocket the RSocket to wrap * @param rsocket the RSocket to wrap
* @param dataMimeType the data MimeType, obtained from the * @param dataMimeType the data MimeType from the {@code ConnectionSetupPayload}
* {@link io.rsocket.ConnectionSetupPayload} (server) or the * @param metadataMimeType the metadata MimeType from the {@code ConnectionSetupPayload}
* {@link io.rsocket.RSocketFactory.ClientRSocketFactory} (client)
* @param strategies the strategies to use * @param strategies the strategies to use
* @return the created RSocketRequester * @return the created RSocketRequester
*/ */
static RSocketRequester wrap(RSocket rsocket, @Nullable MimeType dataMimeType, RSocketStrategies strategies) { static RSocketRequester wrap(
return new DefaultRSocketRequester(rsocket, dataMimeType, strategies); RSocket rsocket, MimeType dataMimeType, MimeType metadataMimeType,
} RSocketStrategies strategies) {
/** return new DefaultRSocketRequester(rsocket, dataMimeType, metadataMimeType, strategies);
* Create a new {@code RSocketRequester} from the given {@link RSocket} and
* strategies for encoding and decoding request and response payloads.
* @param rsocket the sending RSocket to use
* @param dataMimeType the MimeType for data (from the SETUP frame)
* @param strategies encoders, decoders, and others
* @return the created RSocketRequester wrapper
* @deprecated use {@link #wrap(RSocket, MimeType, RSocketStrategies)} instead
*/
@Deprecated
static RSocketRequester create(RSocket rsocket, @Nullable MimeType dataMimeType, RSocketStrategies strategies) {
return new DefaultRSocketRequester(rsocket, dataMimeType, strategies);
} }
@ -107,20 +109,37 @@ public interface RSocketRequester {
interface Builder { interface Builder {
/** /**
* Configure the MimeType to use for payload data. This is set on the * Configure the MimeType to use for payload data. This is then
* {@code SETUP} frame for the whole connection. * specified on the {@code SETUP} frame for the whole connection.
* <p>By default this is set to the first concrete MimeType supported * <p>By default this is set to the first concrete MimeType supported
* by the configured encoders and decoders. * by the configured encoders and decoders.
* @param mimeType the data MimeType to use * @param mimeType the data MimeType to use
*/ */
RSocketRequester.Builder dataMimeType(MimeType mimeType); RSocketRequester.Builder dataMimeType(@Nullable MimeType mimeType);
/**
* Configure the MimeType to use for payload metadata. This is then
* specified on the {@code SETUP} frame for the whole connection.
* <p>At present the metadata MimeType must be
* {@code "message/x.rsocket.routing.v0"} to allow the request
* {@link RSocketRequester#route(String) route} to be encoded, or it
* could also be {@code "message/x.rsocket.composite-metadata.v0"} in
* which case the route can be encoded along with other metadata entries.
* <p>By default this is set to
* {@code "message/x.rsocket.composite-metadata.v0"}.
* @param mimeType the data MimeType to use
*/
RSocketRequester.Builder metadataMimeType(MimeType mimeType);
/** /**
* Configure the {@code ClientRSocketFactory}. * Configure the {@code ClientRSocketFactory}.
* <p><strong>Note:</strong> Please, do not set the {@code dataMimeType} * <p><strong>Note:</strong> This builder provides shortcuts for certain
* directly on the underlying {@code RSocketFactory.ClientRSocketFactory}, * {@code ClientRSocketFactory} options it needs to know about such as
* and use {@link #dataMimeType(MimeType)} instead. * {@link #dataMimeType(MimeType)} and {@link #metadataMimeType(MimeType)}.
* @param configurer the configurer to apply * Please, use these shortcuts vs configuring them directly on the
* {@code ClientRSocketFactory} so that the resulting
* {@code RSocketRequester} is aware of those changes.
* @param configurer consumer to customize the factory
*/ */
RSocketRequester.Builder rsocketFactory(Consumer<RSocketFactory.ClientRSocketFactory> configurer); RSocketRequester.Builder rsocketFactory(Consumer<RSocketFactory.ClientRSocketFactory> configurer);
@ -169,6 +188,17 @@ public interface RSocketRequester {
*/ */
interface RequestSpec { interface RequestSpec {
/**
* Use this to append additional metadata entries if the RSocket
* connection is configured to use composite metadata. If not, an
* {@link IllegalArgumentException} will be raised.
* @param metadata an Object, to be encoded with a suitable
* {@link org.springframework.core.codec.Encoder Encoder}, or a
* {@link org.springframework.core.io.buffer.DataBuffer DataBuffer}
* @param mimeType the mime type that describes the metadata
*/
RequestSpec metadata(Object metadata, MimeType mimeType);
/** /**
* Provide request payload data. The given Object may be a synchronous * Provide request payload data. The given Object may be a synchronous
* value, or a {@link Publisher} of values, or another async type that's * value, or a {@link Publisher} of values, or another async type that's

101
spring-messaging/src/test/java/org/springframework/messaging/rsocket/DefaultRSocketRequesterTests.java

@ -19,6 +19,7 @@ package org.springframework.messaging.rsocket;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.time.Duration; import java.time.Duration;
import java.util.Arrays; import java.util.Arrays;
import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Function; import java.util.function.Function;
@ -28,6 +29,7 @@ import io.reactivex.Observable;
import io.reactivex.Single; import io.reactivex.Single;
import io.rsocket.AbstractRSocket; import io.rsocket.AbstractRSocket;
import io.rsocket.Payload; import io.rsocket.Payload;
import io.rsocket.metadata.CompositeMetadata;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.reactivestreams.Publisher; import org.reactivestreams.Publisher;
@ -61,37 +63,41 @@ public class DefaultRSocketRequesterTests {
private RSocketRequester requester; private RSocketRequester requester;
private RSocketStrategies strategies;
private final DefaultDataBufferFactory bufferFactory = new DefaultDataBufferFactory(); private final DefaultDataBufferFactory bufferFactory = new DefaultDataBufferFactory();
@Before @Before
public void setUp() { public void setUp() {
RSocketStrategies strategies = RSocketStrategies.builder() this.strategies = RSocketStrategies.builder()
.decoder(StringDecoder.allMimeTypes()) .decoder(StringDecoder.allMimeTypes())
.encoder(CharSequenceEncoder.allMimeTypes()) .encoder(CharSequenceEncoder.allMimeTypes())
.build(); .build();
this.rsocket = new TestRSocket(); this.rsocket = new TestRSocket();
this.requester = RSocketRequester.wrap(this.rsocket, MimeTypeUtils.TEXT_PLAIN, strategies); this.requester = RSocketRequester.wrap(this.rsocket,
MimeTypeUtils.TEXT_PLAIN, DefaultRSocketRequester.ROUTING,
this.strategies);
} }
@Test @Test
public void singlePayload() { public void sendMono() {
// data(Object) // data(Object)
testSinglePayload(spec -> spec.data("bodyA"), "bodyA"); testSendMono(spec -> spec.data("bodyA"), "bodyA");
testSinglePayload(spec -> spec.data(Mono.delay(MILLIS_10).map(l -> "bodyA")), "bodyA"); testSendMono(spec -> spec.data(Mono.delay(MILLIS_10).map(l -> "bodyA")), "bodyA");
testSinglePayload(spec -> spec.data(Mono.delay(MILLIS_10).then()), ""); testSendMono(spec -> spec.data(Mono.delay(MILLIS_10).then()), "");
testSinglePayload(spec -> spec.data(Single.timer(10, MILLISECONDS).map(l -> "bodyA")), "bodyA"); testSendMono(spec -> spec.data(Single.timer(10, MILLISECONDS).map(l -> "bodyA")), "bodyA");
testSinglePayload(spec -> spec.data(Completable.complete()), ""); testSendMono(spec -> spec.data(Completable.complete()), "");
// data(Publisher<T>, Class<T>) // data(Publisher<T>, Class<T>)
testSinglePayload(spec -> spec.data(Mono.delay(MILLIS_10).map(l -> "bodyA"), String.class), "bodyA"); testSendMono(spec -> spec.data(Mono.delay(MILLIS_10).map(l -> "bodyA"), String.class), "bodyA");
testSinglePayload(spec -> spec.data(Mono.delay(MILLIS_10).map(l -> "bodyA"), Object.class), "bodyA"); testSendMono(spec -> spec.data(Mono.delay(MILLIS_10).map(l -> "bodyA"), Object.class), "bodyA");
testSinglePayload(spec -> spec.data(Mono.delay(MILLIS_10).then(), Void.class), ""); testSendMono(spec -> spec.data(Mono.delay(MILLIS_10).then(), Void.class), "");
} }
private void testSinglePayload(Function<RequestSpec, ResponseSpec> mapper, String expectedValue) { private void testSendMono(Function<RequestSpec, ResponseSpec> mapper, String expectedValue) {
mapper.apply(this.requester.route("toA")).send().block(Duration.ofSeconds(5)); mapper.apply(this.requester.route("toA")).send().block(Duration.ofSeconds(5));
assertThat(this.rsocket.getSavedMethodName()).isEqualTo("fireAndForget"); assertThat(this.rsocket.getSavedMethodName()).isEqualTo("fireAndForget");
@ -100,22 +106,22 @@ public class DefaultRSocketRequesterTests {
} }
@Test @Test
public void multiPayload() { public void sendFlux() {
String[] values = new String[] {"bodyA", "bodyB", "bodyC"}; String[] values = new String[] {"bodyA", "bodyB", "bodyC"};
Flux<String> stringFlux = Flux.fromArray(values).delayElements(MILLIS_10); Flux<String> stringFlux = Flux.fromArray(values).delayElements(MILLIS_10);
// data(Object) // data(Object)
testMultiPayload(spec -> spec.data(stringFlux), values); testSendFlux(spec -> spec.data(stringFlux), values);
testMultiPayload(spec -> spec.data(Flux.empty()), ""); testSendFlux(spec -> spec.data(Flux.empty()), "");
testMultiPayload(spec -> spec.data(Observable.fromArray(values).delay(10, MILLISECONDS)), values); testSendFlux(spec -> spec.data(Observable.fromArray(values).delay(10, MILLISECONDS)), values);
testMultiPayload(spec -> spec.data(Observable.empty()), ""); testSendFlux(spec -> spec.data(Observable.empty()), "");
// data(Publisher<T>, Class<T>) // data(Publisher<T>, Class<T>)
testMultiPayload(spec -> spec.data(stringFlux, String.class), values); testSendFlux(spec -> spec.data(stringFlux, String.class), values);
testMultiPayload(spec -> spec.data(stringFlux.cast(Object.class), Object.class), values); testSendFlux(spec -> spec.data(stringFlux.cast(Object.class), Object.class), values);
} }
private void testMultiPayload(Function<RequestSpec, ResponseSpec> mapper, String... expectedValues) { private void testSendFlux(Function<RequestSpec, ResponseSpec> mapper, String... expectedValues) {
this.rsocket.reset(); this.rsocket.reset();
mapper.apply(this.requester.route("toA")).retrieveFlux(String.class).blockLast(Duration.ofSeconds(5)); mapper.apply(this.requester.route("toA")).retrieveFlux(String.class).blockLast(Duration.ofSeconds(5));
@ -129,19 +135,50 @@ public class DefaultRSocketRequesterTests {
assertThat(payloads.get(0).getDataUtf8()).isEqualTo(""); assertThat(payloads.get(0).getDataUtf8()).isEqualTo("");
} }
else { else {
assertThat(payloads.stream().map(Payload::getMetadataUtf8).toArray(String[]::new)).isEqualTo(new String[] {"toA", "", ""}); assertThat(payloads.stream().map(Payload::getMetadataUtf8).toArray(String[]::new))
assertThat(payloads.stream().map(Payload::getDataUtf8).toArray(String[]::new)).isEqualTo(expectedValues); .isEqualTo(new String[] {"toA", "", ""});
assertThat(payloads.stream().map(Payload::getDataUtf8).toArray(String[]::new))
.isEqualTo(expectedValues);
} }
} }
@Test @Test
public void send() { public void sendCompositeMetadata() {
String value = "bodyA"; RSocketRequester requester = RSocketRequester.wrap(this.rsocket,
this.requester.route("toA").data(value).send().block(Duration.ofSeconds(5)); MimeTypeUtils.TEXT_PLAIN, DefaultRSocketRequester.COMPOSITE_METADATA,
this.strategies);
requester.route("toA")
.metadata("My metadata", MimeTypeUtils.TEXT_PLAIN).data("bodyA")
.send()
.block(Duration.ofSeconds(5));
CompositeMetadata entries = new CompositeMetadata(this.rsocket.getSavedPayload().metadata(), false);
Iterator<CompositeMetadata.Entry> iterator = entries.iterator();
assertThat(iterator.hasNext()).isTrue();
CompositeMetadata.Entry entry = iterator.next();
assertThat(entry.getMimeType()).isEqualTo(DefaultRSocketRequester.ROUTING.toString());
assertThat(entry.getContent().toString(StandardCharsets.UTF_8)).isEqualTo("toA");
assertThat(iterator.hasNext()).isTrue();
entry = iterator.next();
assertThat(entry.getMimeType()).isEqualTo(MimeTypeUtils.TEXT_PLAIN.toString());
assertThat(entry.getContent().toString(StandardCharsets.UTF_8)).isEqualTo("My metadata");
assertThat(iterator.hasNext()).isFalse();
}
assertThat(this.rsocket.getSavedMethodName()).isEqualTo("fireAndForget"); @Test
assertThat(this.rsocket.getSavedPayload().getMetadataUtf8()).isEqualTo("toA"); public void supportedMetadataMimeTypes() {
assertThat(this.rsocket.getSavedPayload().getDataUtf8()).isEqualTo("bodyA"); RSocketRequester.wrap(this.rsocket, MimeTypeUtils.TEXT_PLAIN,
DefaultRSocketRequester.COMPOSITE_METADATA, this.strategies);
RSocketRequester.wrap(this.rsocket, MimeTypeUtils.TEXT_PLAIN,
DefaultRSocketRequester.ROUTING, this.strategies);
assertThatIllegalArgumentException().isThrownBy(() -> RSocketRequester.wrap(
this.rsocket, MimeTypeUtils.TEXT_PLAIN, MimeTypeUtils.TEXT_PLAIN, this.strategies));
} }
@Test @Test
@ -188,10 +225,10 @@ public class DefaultRSocketRequesterTests {
} }
@Test @Test
public void rejectFluxToMono() { public void fluxToMonoIsRejected() {
assertThatIllegalArgumentException().isThrownBy(() -> assertThatIllegalArgumentException()
this.requester.route("").data(Flux.just("a", "b")).retrieveMono(String.class)) .isThrownBy(() -> this.requester.route("").data(Flux.just("a", "b")).retrieveMono(String.class))
.withMessage("No RSocket interaction model for Flux request to Mono response."); .withMessage("No RSocket interaction model for Flux request to Mono response.");
} }
private Payload toPayload(String value) { private Payload toPayload(String value) {

4
spring-messaging/src/test/java/org/springframework/messaging/rsocket/RSocketClientToServerIntegrationTests.java

@ -101,7 +101,9 @@ public class RSocketClientToServerIntegrationTests {
.verify(Duration.ofSeconds(5)); .verify(Duration.ofSeconds(5));
assertThat(interceptor.getRSocketCount()).isEqualTo(1); assertThat(interceptor.getRSocketCount()).isEqualTo(1);
assertThat(interceptor.getFireAndForgetCount(0)).as("Fire and forget requests did not actually complete handling on the server side").isEqualTo(3); assertThat(interceptor.getFireAndForgetCount(0))
.as("Fire and forget requests did not actually complete handling on the server side")
.isEqualTo(3);
} }
@Test @Test

3
spring-messaging/src/test/java/org/springframework/messaging/rsocket/RSocketServerToClientIntegrationTests.java

@ -106,8 +106,9 @@ public class RSocketServerToClientIntegrationTests {
RSocket rsocket = null; RSocket rsocket = null;
try { try {
rsocket = RSocketFactory.connect() rsocket = RSocketFactory.connect()
.setupPayload(DefaultPayload.create("", destination)) .metadataMimeType("message/x.rsocket.routing.v0")
.dataMimeType("text/plain") .dataMimeType("text/plain")
.setupPayload(DefaultPayload.create("", destination))
.frameDecoder(PayloadDecoder.ZERO_COPY) .frameDecoder(PayloadDecoder.ZERO_COPY)
.acceptor(context.getBean("clientAcceptor", MessageHandlerAcceptor.class)) .acceptor(context.getBean("clientAcceptor", MessageHandlerAcceptor.class))
.transport(TcpClientTransport.create("localhost", 7000)) .transport(TcpClientTransport.create("localhost", 7000))

9
spring-web/src/test/java/org/springframework/http/ContentDispositionTests.java

@ -36,6 +36,15 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException
*/ */
public class ContentDispositionTests { public class ContentDispositionTests {
@Test
public void parseTest() {
ContentDisposition disposition = ContentDisposition
.parse("form-data; name=\"foo\"; filename=\"foo.txt\"; size=123");
assertThat(disposition).isEqualTo(ContentDisposition.builder("form-data")
.name("foo").filename("foo.txt").size(123L).build());
}
@Test @Test
public void parse() { public void parse() {
ContentDisposition disposition = ContentDisposition ContentDisposition disposition = ContentDisposition

Loading…
Cancel
Save