From 0e3f969aa742445296c8f158f3d95d7f91ad6dc8 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Mon, 16 Sep 2019 13:56:37 +0200 Subject: [PATCH] Allow registration of RSocket metadata extractors Prior to this commit, customizing the extraction of RSocket metadata from frames would require developers to override the default `MetadataExtractor` while configuring `RSocketStrategies`. This touches on many infrastructure parts, whereas the goal is just to configure an extra metadata entry extractor using already configured codecs. This commit adds a way to register metadata entry extractors on the `RSocketStrategies` builder with a `Consumer`-based API. --- .../rsocket/DefaultMetadataExtractor.java | 49 +--------- .../rsocket/DefaultRSocketStrategies.java | 11 +++ .../messaging/rsocket/MetadataExtractor.java | 9 +- .../rsocket/MetadataExtractorRegistry.java | 89 +++++++++++++++++++ .../messaging/rsocket/RSocketStrategies.java | 6 ++ .../DefaultRSocketStrategiesTests.java | 23 +++-- 6 files changed, 131 insertions(+), 56 deletions(-) create mode 100644 spring-messaging/src/main/java/org/springframework/messaging/rsocket/MetadataExtractorRegistry.java diff --git a/spring-messaging/src/main/java/org/springframework/messaging/rsocket/DefaultMetadataExtractor.java b/spring-messaging/src/main/java/org/springframework/messaging/rsocket/DefaultMetadataExtractor.java index 836a7151db1f..028768e8a81e 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/rsocket/DefaultMetadataExtractor.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/rsocket/DefaultMetadataExtractor.java @@ -78,59 +78,14 @@ public List> getDecoders() { return this.decoders; } - - /** - * Decode metadata entries with the given {@link MimeType} to the specified - * target class, and store the decoded value in the output map under the - * given name. - * @param mimeType the mime type of metadata entries to extract - * @param targetType the target value type to decode to - * @param name assign a name for the decoded value; if not provided, then - * the mime type is used as the key - */ - public void metadataToExtract(MimeType mimeType, Class targetType, @Nullable String name) { - String key = name != null ? name : mimeType.toString(); - metadataToExtract(mimeType, targetType, (value, map) -> map.put(key, value)); - } - - /** - * Variant of {@link #metadataToExtract(MimeType, Class, String)} that accepts - * {@link ParameterizedTypeReference} instead of {@link Class} for - * specifying a target type with generic parameters. - * @param mimeType the mime type of metadata entries to extract - * @param targetType the target value type to decode to - */ - public void metadataToExtract( - MimeType mimeType, ParameterizedTypeReference targetType, @Nullable String name) { - - String key = name != null ? name : mimeType.toString(); - metadataToExtract(mimeType, targetType, (value, map) -> map.put(key, value)); - } - - /** - * Variant of {@link #metadataToExtract(MimeType, Class, String)} that allows - * custom logic to be used to map the decoded value to any number of values - * in the output map. - * @param mimeType the mime type of metadata entries to extract - * @param targetType the target value type to decode to - * @param mapper custom logic to add the decoded value to the output map - * @param the target value type - */ + @Override public void metadataToExtract( MimeType mimeType, Class targetType, BiConsumer> mapper) { registerMetadata(mimeType, ResolvableType.forClass(targetType), mapper); } - /** - * Variant of {@link #metadataToExtract(MimeType, Class, BiConsumer)} that - * accepts {@link ParameterizedTypeReference} instead of {@link Class} for - * specifying a target type with generic parameters. - * @param mimeType the mime type of metadata entries to extract - * @param type the target value type to decode to - * @param mapper custom logic to add the decoded value to the output map - * @param the target value type - */ + @Override public void metadataToExtract( MimeType mimeType, ParameterizedTypeReference type, BiConsumer> mapper) { diff --git a/spring-messaging/src/main/java/org/springframework/messaging/rsocket/DefaultRSocketStrategies.java b/spring-messaging/src/main/java/org/springframework/messaging/rsocket/DefaultRSocketStrategies.java index 521be9773595..673e675378aa 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/rsocket/DefaultRSocketStrategies.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/rsocket/DefaultRSocketStrategies.java @@ -46,6 +46,7 @@ * Default implementation of {@link RSocketStrategies}. * * @author Rossen Stoyanchev + * @author Brian Clozel * @since 5.2 */ final class DefaultRSocketStrategies implements RSocketStrategies { @@ -128,6 +129,8 @@ static class DefaultRSocketStrategiesBuilder implements RSocketStrategies.Builde @Nullable private MetadataExtractor metadataExtractor; + private final List> metadataExtractors = new ArrayList<>(); + DefaultRSocketStrategiesBuilder() { this.encoders.add(CharSequenceEncoder.allMimeTypes()); this.encoders.add(new ByteBufferEncoder()); @@ -200,6 +203,12 @@ public Builder metadataExtractor(@Nullable MetadataExtractor metadataExtractor) return this; } + @Override + public Builder metadataExtractors(Consumer consumer) { + this.metadataExtractors.add(consumer); + return this; + } + @Override public RSocketStrategies build() { RouteMatcher matcher = (this.routeMatcher != null ? this.routeMatcher : initRouteMatcher()); @@ -213,6 +222,8 @@ public RSocketStrategies build() { MetadataExtractor extractor = (this.metadataExtractor != null ? this.metadataExtractor : new DefaultMetadataExtractor(this.decoders)); + this.metadataExtractors.forEach(consumer -> consumer.accept(extractor)); + return new DefaultRSocketStrategies( this.encoders, this.decoders, matcher, registry, factory, extractor); } diff --git a/spring-messaging/src/main/java/org/springframework/messaging/rsocket/MetadataExtractor.java b/spring-messaging/src/main/java/org/springframework/messaging/rsocket/MetadataExtractor.java index 23da144de34b..961a6e0b9afd 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/rsocket/MetadataExtractor.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/rsocket/MetadataExtractor.java @@ -24,15 +24,16 @@ /** * Strategy to extract a map of value(s) from {@link Payload} metadata, which - * could be composite metadata with multiple entries. Each metadata entry - * is decoded based on its {@code MimeType} and a name is assigned to the decoded - * value. The resulting name-value pairs can be added to the headers of a + * could be composite metadata with multiple entries. + * The resulting name-value pairs can be added to the headers of a * {@link org.springframework.messaging.Message Message}. + * Strategies to extract individual metadata entries are made available + * through {@link MetadataExtractorRegistry}. * * @author Rossen Stoyanchev * @since 5.2 */ -public interface MetadataExtractor { +public interface MetadataExtractor extends MetadataExtractorRegistry { /** * The key to assign to the extracted "route" of the payload. diff --git a/spring-messaging/src/main/java/org/springframework/messaging/rsocket/MetadataExtractorRegistry.java b/spring-messaging/src/main/java/org/springframework/messaging/rsocket/MetadataExtractorRegistry.java new file mode 100644 index 000000000000..281934762804 --- /dev/null +++ b/spring-messaging/src/main/java/org/springframework/messaging/rsocket/MetadataExtractorRegistry.java @@ -0,0 +1,89 @@ +/* + * Copyright 2002-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 + * + * 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 org.springframework.messaging.rsocket; + +import java.util.Map; +import java.util.function.BiConsumer; + +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.lang.Nullable; +import org.springframework.util.MimeType; + +/** + * Stores registrations of extractors for metadata entries. + * Each metadata entry is decoded based on its {@code MimeType} and a name + * is assigned to the decoded value. + * + * @author Rossen Stoyanchev + * @author Brian Clozel + * @since 5.2 + */ +public interface MetadataExtractorRegistry { + + /** + * Decode metadata entries with the given {@link MimeType} to the specified + * target class, and store the decoded value in the output map under the + * given name. + * @param mimeType the mime type of metadata entries to extract + * @param targetType the target value type to decode to + * @param name assign a name for the decoded value; if not provided, then + * the mime type is used as the key + */ + default void metadataToExtract(MimeType mimeType, Class targetType, @Nullable String name) { + String key = name != null ? name : mimeType.toString(); + metadataToExtract(mimeType, targetType, (value, map) -> map.put(key, value)); + } + + /** + * Variant of {@link #metadataToExtract(MimeType, Class, String)} that accepts + * {@link ParameterizedTypeReference} instead of {@link Class} for + * specifying a target type with generic parameters. + * @param mimeType the mime type of metadata entries to extract + * @param targetType the target value type to decode to + */ + default void metadataToExtract( + MimeType mimeType, ParameterizedTypeReference targetType, @Nullable String name) { + + String key = name != null ? name : mimeType.toString(); + metadataToExtract(mimeType, targetType, (value, map) -> map.put(key, value)); + } + + /** + * Variant of {@link #metadataToExtract(MimeType, Class, String)} that allows + * custom logic to be used to map the decoded value to any number of values + * in the output map. + * @param mimeType the mime type of metadata entries to extract + * @param targetType the target value type to decode to + * @param mapper custom logic to add the decoded value to the output map + * @param the target value type + */ + void metadataToExtract( + MimeType mimeType, Class targetType, BiConsumer> mapper); + + /** + * Variant of {@link #metadataToExtract(MimeType, Class, BiConsumer)} that + * accepts {@link ParameterizedTypeReference} instead of {@link Class} for + * specifying a target type with generic parameters. + * @param mimeType the mime type of metadata entries to extract + * @param type the target value type to decode to + * @param mapper custom logic to add the decoded value to the output map + * @param the target value type + */ + void metadataToExtract( + MimeType mimeType, ParameterizedTypeReference type, BiConsumer> mapper); + +} diff --git a/spring-messaging/src/main/java/org/springframework/messaging/rsocket/RSocketStrategies.java b/spring-messaging/src/main/java/org/springframework/messaging/rsocket/RSocketStrategies.java index 980025a442ae..74848846a8fb 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/rsocket/RSocketStrategies.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/rsocket/RSocketStrategies.java @@ -219,6 +219,12 @@ interface Builder { */ Builder metadataExtractor(@Nullable MetadataExtractor metadataExtractor); + /** + * Apply the consumer to the {@link MetadataExtractorRegistry} in order + * to register extra metadata entry extractors. + */ + Builder metadataExtractors(Consumer extractorRegistry); + /** * Build the {@code RSocketStrategies} instance. */ diff --git a/spring-messaging/src/test/java/org/springframework/messaging/rsocket/DefaultRSocketStrategiesTests.java b/spring-messaging/src/test/java/org/springframework/messaging/rsocket/DefaultRSocketStrategiesTests.java index 6d230e954147..8a4f08103c50 100644 --- a/spring-messaging/src/test/java/org/springframework/messaging/rsocket/DefaultRSocketStrategiesTests.java +++ b/spring-messaging/src/test/java/org/springframework/messaging/rsocket/DefaultRSocketStrategiesTests.java @@ -15,6 +15,8 @@ */ package org.springframework.messaging.rsocket; +import java.util.function.Consumer; + import org.junit.jupiter.api.Test; import org.springframework.core.ReactiveAdapterRegistry; @@ -30,16 +32,20 @@ import org.springframework.util.SimpleRouteMatcher; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; /** * Unit tests for {@link RSocketStrategies}. * @author Rossen Stoyanchev * @since 5.2 */ -public class DefaultRSocketStrategiesTests { +class DefaultRSocketStrategiesTests { @Test - public void defaultSettings() { + void defaultSettings() { RSocketStrategies strategies = RSocketStrategies.create(); assertThat(strategies.encoders()).hasSize(4).hasOnlyElementsOfTypes( @@ -62,8 +68,7 @@ public void defaultSettings() { } @Test - public void explicitValues() { - + void explicitValues() { SimpleRouteMatcher matcher = new SimpleRouteMatcher(new AntPathMatcher()); DefaultMetadataExtractor extractor = new DefaultMetadataExtractor(); ReactiveAdapterRegistry registry = new ReactiveAdapterRegistry(); @@ -90,7 +95,7 @@ public void explicitValues() { } @Test - public void copyConstructor() { + void copyConstructor() { RSocketStrategies strategies1 = RSocketStrategies.create(); RSocketStrategies strategies2 = strategies1.mutate().build(); @@ -101,4 +106,12 @@ public void copyConstructor() { assertThat(strategies1.reactiveAdapterRegistry()).isSameAs(strategies2.reactiveAdapterRegistry()); } + @Test + @SuppressWarnings("unchecked") + void applyMetadataExtractors() { + Consumer consumer = (Consumer) mock(Consumer.class); + RSocketStrategies strategies = RSocketStrategies.builder().metadataExtractors(consumer).build(); + verify(consumer, times(1)).accept(any()); + } + }