From 768257567d75b3f0142770d0a81bffdd33f062f1 Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Fri, 29 May 2020 13:32:52 +0200 Subject: [PATCH 1/8] Make use of custom types configurable in YamlProcessor Prior to this commit, there was no easy way to restrict what types could be loaded from a YAML document in subclasses of YamlProcessor such as YamlPropertiesFactoryBean and YamlMapFactoryBean. This commit introduces a setSupportedTypes(Class...) method in YamlProcessor in order to address this. If no supported types are configured, all types encountered in YAML documents will be supported. If an unsupported type is encountered, an IllegalStateException will be thrown when the corresponding YAML node is processed. Closes gh-25152 --- .../beans/factory/config/YamlProcessor.java | 70 ++++++++++++++++- .../factory/config/YamlProcessorTests.java | 78 ++++++++++++++----- 2 files changed, 126 insertions(+), 22 deletions(-) diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/YamlProcessor.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/YamlProcessor.java index 340dee2dcac6..1b4e2bdeeaa1 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/config/YamlProcessor.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/YamlProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2020 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. @@ -25,17 +25,23 @@ import java.util.List; import java.util.Map; import java.util.Properties; +import java.util.Set; +import java.util.stream.Collectors; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.yaml.snakeyaml.DumperOptions; import org.yaml.snakeyaml.LoaderOptions; import org.yaml.snakeyaml.Yaml; +import org.yaml.snakeyaml.constructor.Constructor; import org.yaml.snakeyaml.reader.UnicodeReader; +import org.yaml.snakeyaml.representer.Representer; import org.springframework.core.CollectionFactory; import org.springframework.core.io.Resource; import org.springframework.lang.Nullable; import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; /** @@ -45,6 +51,7 @@ * * @author Dave Syer * @author Juergen Hoeller + * @author Sam Brannen * @since 4.1 */ public abstract class YamlProcessor { @@ -59,6 +66,8 @@ public abstract class YamlProcessor { private boolean matchDefault = true; + private Set supportedTypes = Collections.emptySet(); + /** * A map of document matchers allowing callers to selectively use only @@ -117,6 +126,27 @@ public void setResources(Resource... resources) { this.resources = resources; } + /** + * Set the supported types that can be loaded from YAML documents. + *

If no supported types are configured, all types encountered in YAML + * documents will be supported. If an unsupported type is encountered, an + * {@link IllegalStateException} will be thrown when the corresponding YAML + * node is processed. + * @param supportedTypes the supported types, or an empty array to clear the + * supported types + * @since 5.1.16 + * @see #createYaml() + */ + public void setSupportedTypes(Class... supportedTypes) { + if (ObjectUtils.isEmpty(supportedTypes)) { + this.supportedTypes = Collections.emptySet(); + } + else { + Assert.noNullElements(supportedTypes, "'supportedTypes' must not contain null elements"); + this.supportedTypes = Arrays.stream(supportedTypes).map(Class::getName) + .collect(Collectors.collectingAndThen(Collectors.toSet(), Collections::unmodifiableSet)); + } + } /** * Provide an opportunity for subclasses to process the Yaml parsed from the supplied @@ -142,12 +172,22 @@ protected void process(MatchCallback callback) { * Create the {@link Yaml} instance to use. *

The default implementation sets the "allowDuplicateKeys" flag to {@code false}, * enabling built-in duplicate key handling in SnakeYAML 1.18+. + *

As of Spring Framework 5.1.16, if custom {@linkplain #setSupportedTypes + * supported types} have been configured, the default implementation creates + * a {@code Yaml} instance that filters out unsupported types encountered in + * YAML documents. If an unsupported type is encountered, an + * {@link IllegalStateException} will be thrown when the node is processed. * @see LoaderOptions#setAllowDuplicateKeys(boolean) */ protected Yaml createYaml() { - LoaderOptions options = new LoaderOptions(); - options.setAllowDuplicateKeys(false); - return new Yaml(options); + LoaderOptions loaderOptions = new LoaderOptions(); + loaderOptions.setAllowDuplicateKeys(false); + + if (!this.supportedTypes.isEmpty()) { + return new Yaml(new FilteringConstructor(loaderOptions), new Representer(), + new DumperOptions(), loaderOptions); + } + return new Yaml(loaderOptions); } private boolean process(MatchCallback callback, Yaml yaml, Resource resource) { @@ -388,4 +428,26 @@ public enum ResolutionMethod { FIRST_FOUND } + + /** + * {@link Constructor} that supports filtering of unsupported types. + *

If an unsupported type is encountered in a YAML document, an + * {@link IllegalStateException} will be thrown from {@link #getClassForName(String)}. + * @since 5.1.16 + */ + private class FilteringConstructor extends Constructor { + + FilteringConstructor(LoaderOptions loaderOptions) { + super(loaderOptions); + } + + + @Override + protected Class getClassForName(String name) throws ClassNotFoundException { + Assert.state(YamlProcessor.this.supportedTypes.contains(name), + () -> "Unsupported type encountered in YAML document: " + name); + return super.getClassForName(name); + } + } + } diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/config/YamlProcessorTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/config/YamlProcessorTests.java index 0bacf2162582..60fbd272ce2c 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/config/YamlProcessorTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/config/YamlProcessorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2020 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. @@ -16,11 +16,13 @@ package org.springframework.beans.factory.config; +import java.net.URL; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import org.junit.jupiter.api.Test; +import org.yaml.snakeyaml.constructor.ConstructorException; import org.yaml.snakeyaml.parser.ParserException; import org.yaml.snakeyaml.scanner.ScannerException; @@ -29,6 +31,7 @@ import static java.util.stream.Collectors.toList; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.entry; /** * Tests for {@link YamlProcessor}. @@ -37,14 +40,14 @@ * @author Juergen Hoeller * @author Sam Brannen */ -public class YamlProcessorTests { +class YamlProcessorTests { private final YamlProcessor processor = new YamlProcessor() {}; @Test - public void arrayConvertedToIndexedBeanReference() { - this.processor.setResources(new ByteArrayResource("foo: bar\nbar: [1,2,3]".getBytes())); + void arrayConvertedToIndexedBeanReference() { + setYaml("foo: bar\nbar: [1,2,3]"); this.processor.process((properties, map) -> { assertThat(properties.size()).isEqualTo(4); assertThat(properties.get("foo")).isEqualTo("bar"); @@ -59,30 +62,30 @@ public void arrayConvertedToIndexedBeanReference() { } @Test - public void stringResource() { - this.processor.setResources(new ByteArrayResource("foo # a document that is a literal".getBytes())); + void stringResource() { + setYaml("foo # a document that is a literal"); this.processor.process((properties, map) -> assertThat(map.get("document")).isEqualTo("foo")); } @Test - public void badDocumentStart() { - this.processor.setResources(new ByteArrayResource("foo # a document\nbar: baz".getBytes())); + void badDocumentStart() { + setYaml("foo # a document\nbar: baz"); assertThatExceptionOfType(ParserException.class) .isThrownBy(() -> this.processor.process((properties, map) -> {})) .withMessageContaining("line 2, column 1"); } @Test - public void badResource() { - this.processor.setResources(new ByteArrayResource("foo: bar\ncd\nspam:\n foo: baz".getBytes())); + void badResource() { + setYaml("foo: bar\ncd\nspam:\n foo: baz"); assertThatExceptionOfType(ScannerException.class) .isThrownBy(() -> this.processor.process((properties, map) -> {})) .withMessageContaining("line 3, column 1"); } @Test - public void mapConvertedToIndexedBeanReference() { - this.processor.setResources(new ByteArrayResource("foo: bar\nbar:\n spam: bucket".getBytes())); + void mapConvertedToIndexedBeanReference() { + setYaml("foo: bar\nbar:\n spam: bucket"); this.processor.process((properties, map) -> { assertThat(properties.get("bar.spam")).isEqualTo("bucket"); assertThat(properties).hasSize(2); @@ -90,8 +93,8 @@ public void mapConvertedToIndexedBeanReference() { } @Test - public void integerKeyBehaves() { - this.processor.setResources(new ByteArrayResource("foo: bar\n1: bar".getBytes())); + void integerKeyBehaves() { + setYaml("foo: bar\n1: bar"); this.processor.process((properties, map) -> { assertThat(properties.get("[1]")).isEqualTo("bar"); assertThat(properties).hasSize(2); @@ -99,8 +102,8 @@ public void integerKeyBehaves() { } @Test - public void integerDeepKeyBehaves() { - this.processor.setResources(new ByteArrayResource("foo:\n 1: bar".getBytes())); + void integerDeepKeyBehaves() { + setYaml("foo:\n 1: bar"); this.processor.process((properties, map) -> { assertThat(properties.get("foo[1]")).isEqualTo("bar"); assertThat(properties).hasSize(1); @@ -109,8 +112,8 @@ public void integerDeepKeyBehaves() { @Test @SuppressWarnings("unchecked") - public void flattenedMapIsSameAsPropertiesButOrdered() { - this.processor.setResources(new ByteArrayResource("cat: dog\nfoo: bar\nbar:\n spam: bucket".getBytes())); + void flattenedMapIsSameAsPropertiesButOrdered() { + setYaml("cat: dog\nfoo: bar\nbar:\n spam: bucket"); this.processor.process((properties, map) -> { Map flattenedMap = processor.getFlattenedMap(map); assertThat(flattenedMap).isInstanceOf(LinkedHashMap.class); @@ -134,4 +137,43 @@ public void flattenedMapIsSameAsPropertiesButOrdered() { }); } + @Test + void customTypeSupportedByDefault() throws Exception { + URL url = new URL("https://localhost:9000/"); + setYaml("value: !!java.net.URL [\"" + url + "\"]"); + + this.processor.process((properties, map) -> { + assertThat(properties).containsExactly(entry("value", url)); + assertThat(map).containsExactly(entry("value", url)); + }); + } + + @Test + void customTypesSupportedDueToExplicitConfiguration() throws Exception { + this.processor.setSupportedTypes(URL.class, String.class); + + URL url = new URL("https://localhost:9000/"); + setYaml("value: !!java.net.URL [!!java.lang.String [\"" + url + "\"]]"); + + this.processor.process((properties, map) -> { + assertThat(properties).containsExactly(entry("value", url)); + assertThat(map).containsExactly(entry("value", url)); + }); + } + + @Test + void customTypeNotSupportedDueToExplicitConfiguration() { + this.processor.setSupportedTypes(List.class); + + setYaml("value: !!java.net.URL [\"https://localhost:9000/\"]"); + + assertThatExceptionOfType(ConstructorException.class) + .isThrownBy(() -> this.processor.process((properties, map) -> {})) + .withMessageContaining("Unsupported type encountered in YAML document: java.net.URL"); + } + + private void setYaml(String yaml) { + this.processor.setResources(new ByteArrayResource(yaml.getBytes())); + } + } From bec89dba4c6051eb7e1df4ac46ff0e2d23b07583 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Fri, 29 May 2020 15:48:19 +0200 Subject: [PATCH 2/8] Consistent MultiValueMap behavior for empty list values This commit extracts MultiValueMapAdapter from CollectionUtils and reuses its implementation as base class of LinkedMultiValueMap. Closes gh-25140 --- .../springframework/util/CollectionUtils.java | 164 ++-------------- .../util/LinkedCaseInsensitiveMap.java | 6 +- .../util/LinkedMultiValueMap.java | 153 +-------------- .../util/MultiValueMapAdapter.java | 178 ++++++++++++++++++ 4 files changed, 202 insertions(+), 299 deletions(-) create mode 100644 spring-core/src/main/java/org/springframework/util/MultiValueMapAdapter.java diff --git a/spring-core/src/main/java/org/springframework/util/CollectionUtils.java b/spring-core/src/main/java/org/springframework/util/CollectionUtils.java index 1cd16243fd31..1ec05104673b 100644 --- a/spring-core/src/main/java/org/springframework/util/CollectionUtils.java +++ b/spring-core/src/main/java/org/springframework/util/CollectionUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2020 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. @@ -16,7 +16,6 @@ package org.springframework.util; -import java.io.Serializable; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -24,7 +23,6 @@ import java.util.Enumeration; import java.util.Iterator; import java.util.LinkedHashMap; -import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Properties; @@ -412,25 +410,28 @@ public static Iterator toIterator(@Nullable Enumeration enumeration) { /** * Adapt a {@code Map>} to an {@code MultiValueMap}. - * @param map the original map - * @return the multi-value map + * @param targetMap the original map + * @return the adapted multi-value map (wrapping the original map) * @since 3.1 */ - public static MultiValueMap toMultiValueMap(Map> map) { - return new MultiValueMapAdapter<>(map); + public static MultiValueMap toMultiValueMap(Map> targetMap) { + Assert.notNull(targetMap, "'targetMap' must not be null"); + return new MultiValueMapAdapter<>(targetMap); } /** * Return an unmodifiable view of the specified multi-value map. - * @param map the map for which an unmodifiable view is to be returned. - * @return an unmodifiable view of the specified multi-value map. + * @param targetMap the map for which an unmodifiable view is to be returned. + * @return an unmodifiable view of the specified multi-value map * @since 3.1 */ @SuppressWarnings("unchecked") - public static MultiValueMap unmodifiableMultiValueMap(MultiValueMap map) { - Assert.notNull(map, "'map' must not be null"); - Map> result = new LinkedHashMap<>(map.size()); - map.forEach((key, value) -> { + public static MultiValueMap unmodifiableMultiValueMap( + MultiValueMap targetMap) { + + Assert.notNull(targetMap, "'targetMap' must not be null"); + Map> result = new LinkedHashMap<>(targetMap.size()); + targetMap.forEach((key, value) -> { List values = Collections.unmodifiableList(value); result.put(key, (List) values); }); @@ -467,141 +468,4 @@ public void remove() throws UnsupportedOperationException { } - /** - * Adapts a Map to the MultiValueMap contract. - */ - @SuppressWarnings("serial") - private static class MultiValueMapAdapter implements MultiValueMap, Serializable { - - private final Map> map; - - public MultiValueMapAdapter(Map> map) { - Assert.notNull(map, "'map' must not be null"); - this.map = map; - } - - @Override - @Nullable - public V getFirst(K key) { - List values = this.map.get(key); - return (values != null ? values.get(0) : null); - } - - @Override - public void add(K key, @Nullable V value) { - List values = this.map.computeIfAbsent(key, k -> new LinkedList<>()); - values.add(value); - } - - @Override - public void addAll(K key, List values) { - List currentValues = this.map.computeIfAbsent(key, k -> new LinkedList<>()); - currentValues.addAll(values); - } - - @Override - public void addAll(MultiValueMap values) { - for (Entry> entry : values.entrySet()) { - addAll(entry.getKey(), entry.getValue()); - } - } - - @Override - public void set(K key, @Nullable V value) { - List values = new LinkedList<>(); - values.add(value); - this.map.put(key, values); - } - - @Override - public void setAll(Map values) { - values.forEach(this::set); - } - - @Override - public Map toSingleValueMap() { - LinkedHashMap singleValueMap = new LinkedHashMap<>(this.map.size()); - this.map.forEach((key, value) -> singleValueMap.put(key, value.get(0))); - return singleValueMap; - } - - @Override - public int size() { - return this.map.size(); - } - - @Override - public boolean isEmpty() { - return this.map.isEmpty(); - } - - @Override - public boolean containsKey(Object key) { - return this.map.containsKey(key); - } - - @Override - public boolean containsValue(Object value) { - return this.map.containsValue(value); - } - - @Override - public List get(Object key) { - return this.map.get(key); - } - - @Override - public List put(K key, List value) { - return this.map.put(key, value); - } - - @Override - public List remove(Object key) { - return this.map.remove(key); - } - - @Override - public void putAll(Map> map) { - this.map.putAll(map); - } - - @Override - public void clear() { - this.map.clear(); - } - - @Override - public Set keySet() { - return this.map.keySet(); - } - - @Override - public Collection> values() { - return this.map.values(); - } - - @Override - public Set>> entrySet() { - return this.map.entrySet(); - } - - @Override - public boolean equals(@Nullable Object other) { - if (this == other) { - return true; - } - return this.map.equals(other); - } - - @Override - public int hashCode() { - return this.map.hashCode(); - } - - @Override - public String toString() { - return this.map.toString(); - } - } - } diff --git a/spring-core/src/main/java/org/springframework/util/LinkedCaseInsensitiveMap.java b/spring-core/src/main/java/org/springframework/util/LinkedCaseInsensitiveMap.java index 1b70fa555093..a7e24a762070 100644 --- a/spring-core/src/main/java/org/springframework/util/LinkedCaseInsensitiveMap.java +++ b/spring-core/src/main/java/org/springframework/util/LinkedCaseInsensitiveMap.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2020 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. @@ -273,8 +273,8 @@ public LinkedCaseInsensitiveMap clone() { } @Override - public boolean equals(@Nullable Object obj) { - return this.targetMap.equals(obj); + public boolean equals(@Nullable Object other) { + return (this == other || this.targetMap.equals(other)); } @Override diff --git a/spring-core/src/main/java/org/springframework/util/LinkedMultiValueMap.java b/spring-core/src/main/java/org/springframework/util/LinkedMultiValueMap.java index c3edb979718e..00ef6b4d6bd6 100644 --- a/spring-core/src/main/java/org/springframework/util/LinkedMultiValueMap.java +++ b/spring-core/src/main/java/org/springframework/util/LinkedMultiValueMap.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2020 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. @@ -17,14 +17,10 @@ package org.springframework.util; import java.io.Serializable; -import java.util.Collection; import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; -import java.util.Set; - -import org.springframework.lang.Nullable; /** * Simple implementation of {@link MultiValueMap} that wraps a {@link LinkedHashMap}, @@ -39,18 +35,16 @@ * @param the key type * @param the value element type */ -public class LinkedMultiValueMap implements MultiValueMap, Serializable, Cloneable { +public class LinkedMultiValueMap extends MultiValueMapAdapter implements Serializable, Cloneable { private static final long serialVersionUID = 3801124242820219131L; - private final Map> targetMap; - /** * Create a new LinkedMultiValueMap that wraps a {@link LinkedHashMap}. */ public LinkedMultiValueMap() { - this.targetMap = new LinkedHashMap<>(); + super(new LinkedHashMap<>()); } /** @@ -59,7 +53,7 @@ public LinkedMultiValueMap() { * @param initialCapacity the initial capacity */ public LinkedMultiValueMap(int initialCapacity) { - this.targetMap = new LinkedHashMap<>(initialCapacity); + super(new LinkedHashMap<>(initialCapacity)); } /** @@ -71,125 +65,7 @@ public LinkedMultiValueMap(int initialCapacity) { * @see #deepCopy() */ public LinkedMultiValueMap(Map> otherMap) { - this.targetMap = new LinkedHashMap<>(otherMap); - } - - - // MultiValueMap implementation - - @Override - @Nullable - public V getFirst(K key) { - List values = this.targetMap.get(key); - return (values != null && !values.isEmpty() ? values.get(0) : null); - } - - @Override - public void add(K key, @Nullable V value) { - List values = this.targetMap.computeIfAbsent(key, k -> new LinkedList<>()); - values.add(value); - } - - @Override - public void addAll(K key, List values) { - List currentValues = this.targetMap.computeIfAbsent(key, k -> new LinkedList<>()); - currentValues.addAll(values); - } - - @Override - public void addAll(MultiValueMap values) { - for (Entry> entry : values.entrySet()) { - addAll(entry.getKey(), entry.getValue()); - } - } - - @Override - public void set(K key, @Nullable V value) { - List values = new LinkedList<>(); - values.add(value); - this.targetMap.put(key, values); - } - - @Override - public void setAll(Map values) { - values.forEach(this::set); - } - - @Override - public Map toSingleValueMap() { - LinkedHashMap singleValueMap = new LinkedHashMap<>(this.targetMap.size()); - this.targetMap.forEach((key, values) -> { - if (values != null && !values.isEmpty()) { - singleValueMap.put(key, values.get(0)); - } - }); - return singleValueMap; - } - - - // Map implementation - - @Override - public int size() { - return this.targetMap.size(); - } - - @Override - public boolean isEmpty() { - return this.targetMap.isEmpty(); - } - - @Override - public boolean containsKey(Object key) { - return this.targetMap.containsKey(key); - } - - @Override - public boolean containsValue(Object value) { - return this.targetMap.containsValue(value); - } - - @Override - @Nullable - public List get(Object key) { - return this.targetMap.get(key); - } - - @Override - @Nullable - public List put(K key, List value) { - return this.targetMap.put(key, value); - } - - @Override - @Nullable - public List remove(Object key) { - return this.targetMap.remove(key); - } - - @Override - public void putAll(Map> map) { - this.targetMap.putAll(map); - } - - @Override - public void clear() { - this.targetMap.clear(); - } - - @Override - public Set keySet() { - return this.targetMap.keySet(); - } - - @Override - public Collection> values() { - return this.targetMap.values(); - } - - @Override - public Set>> entrySet() { - return this.targetMap.entrySet(); + super(new LinkedHashMap<>(otherMap)); } @@ -203,8 +79,8 @@ public Set>> entrySet() { * @see #clone() */ public LinkedMultiValueMap deepCopy() { - LinkedMultiValueMap copy = new LinkedMultiValueMap<>(this.targetMap.size()); - this.targetMap.forEach((key, value) -> copy.put(key, new LinkedList<>(value))); + LinkedMultiValueMap copy = new LinkedMultiValueMap<>(size()); + forEach((key, values) -> copy.put(key, new LinkedList<>(values))); return copy; } @@ -224,19 +100,4 @@ public LinkedMultiValueMap clone() { return new LinkedMultiValueMap<>(this); } - @Override - public boolean equals(@Nullable Object obj) { - return this.targetMap.equals(obj); - } - - @Override - public int hashCode() { - return this.targetMap.hashCode(); - } - - @Override - public String toString() { - return this.targetMap.toString(); - } - } diff --git a/spring-core/src/main/java/org/springframework/util/MultiValueMapAdapter.java b/spring-core/src/main/java/org/springframework/util/MultiValueMapAdapter.java new file mode 100644 index 000000000000..cfbf79cb3b7b --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/MultiValueMapAdapter.java @@ -0,0 +1,178 @@ +/* + * Copyright 2002-2020 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.util; + +import java.io.Serializable; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.springframework.lang.Nullable; + +/** + * Adapts a given {@link Map} to the {@link MultiValueMap} contract. + * + * @author Arjen Poutsma + * @author Juergen Hoeller + * @since 3.1 + * @param the key type + * @param the value element type + * @see CollectionUtils#toMultiValueMap + * @see LinkedMultiValueMap + */ +@SuppressWarnings("serial") +class MultiValueMapAdapter implements MultiValueMap, Serializable { + + private final Map> targetMap; + + + MultiValueMapAdapter(Map> targetMap) { + this.targetMap = targetMap; + } + + + @Override + @Nullable + public V getFirst(K key) { + List values = this.targetMap.get(key); + return (values != null && !values.isEmpty() ? values.get(0) : null); + } + + @Override + public void add(K key, @Nullable V value) { + List values = this.targetMap.computeIfAbsent(key, k -> new LinkedList<>()); + values.add(value); + } + + @Override + public void addAll(K key, List values) { + List currentValues = this.targetMap.computeIfAbsent(key, k -> new LinkedList<>()); + currentValues.addAll(values); + } + + @Override + public void addAll(MultiValueMap values) { + for (Entry> entry : values.entrySet()) { + addAll(entry.getKey(), entry.getValue()); + } + } + + @Override + public void set(K key, @Nullable V value) { + List values = new LinkedList<>(); + values.add(value); + this.targetMap.put(key, values); + } + + @Override + public void setAll(Map values) { + values.forEach(this::set); + } + + @Override + public Map toSingleValueMap() { + Map singleValueMap = new LinkedHashMap<>(this.targetMap.size()); + this.targetMap.forEach((key, values) -> { + if (values != null && !values.isEmpty()) { + singleValueMap.put(key, values.get(0)); + } + }); + return singleValueMap; + } + + @Override + public int size() { + return this.targetMap.size(); + } + + @Override + public boolean isEmpty() { + return this.targetMap.isEmpty(); + } + + @Override + public boolean containsKey(Object key) { + return this.targetMap.containsKey(key); + } + + @Override + public boolean containsValue(Object value) { + return this.targetMap.containsValue(value); + } + + @Override + @Nullable + public List get(Object key) { + return this.targetMap.get(key); + } + + @Override + @Nullable + public List put(K key, List value) { + return this.targetMap.put(key, value); + } + + @Override + @Nullable + public List remove(Object key) { + return this.targetMap.remove(key); + } + + @Override + public void putAll(Map> map) { + this.targetMap.putAll(map); + } + + @Override + public void clear() { + this.targetMap.clear(); + } + + @Override + public Set keySet() { + return this.targetMap.keySet(); + } + + @Override + public Collection> values() { + return this.targetMap.values(); + } + + @Override + public Set>> entrySet() { + return this.targetMap.entrySet(); + } + + @Override + public boolean equals(@Nullable Object other) { + return (this == other || this.targetMap.equals(other)); + } + + @Override + public int hashCode() { + return this.targetMap.hashCode(); + } + + @Override + public String toString() { + return this.targetMap.toString(); + } + +} From ef626e992d05190111a1540d8eb5b8ba082ff96f Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Fri, 29 May 2020 15:49:36 +0200 Subject: [PATCH 3/8] Document that MapPropertySource should not contain null values Closes gh-25142 --- .../springframework/core/env/MapPropertySource.java | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/spring-core/src/main/java/org/springframework/core/env/MapPropertySource.java b/spring-core/src/main/java/org/springframework/core/env/MapPropertySource.java index d08c6fb27844..36597a5b24a5 100644 --- a/spring-core/src/main/java/org/springframework/core/env/MapPropertySource.java +++ b/spring-core/src/main/java/org/springframework/core/env/MapPropertySource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2020 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. @@ -23,6 +23,8 @@ /** * {@link PropertySource} that reads keys and values from a {@code Map} object. + * The underlying map should not contain any {@code null} values in order to + * comply with {@link #getProperty} and {@link #containsProperty} semantics. * * @author Chris Beams * @author Juergen Hoeller @@ -31,6 +33,12 @@ */ public class MapPropertySource extends EnumerablePropertySource> { + /** + * Create a new {@code MapPropertySource} with the given name and {@code Map}. + * @param name the associated name + * @param source the Map source (without {@code null} values in order to get + * consistent {@link #getProperty} and {@link #containsProperty} behavior) + */ public MapPropertySource(String name, Map source) { super(name, source); } From 2ff22510d92986ebabd0f7f4667d7ede3f83e01d Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Fri, 29 May 2020 15:50:10 +0200 Subject: [PATCH 4/8] Avoid earlyApplicationEvents iteration in case of empty Set Closes gh-25161 --- .../context/support/AbstractApplicationContext.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/spring-context/src/main/java/org/springframework/context/support/AbstractApplicationContext.java b/spring-context/src/main/java/org/springframework/context/support/AbstractApplicationContext.java index 6b4e5b6a4fac..933fc7359cb8 100644 --- a/spring-context/src/main/java/org/springframework/context/support/AbstractApplicationContext.java +++ b/spring-context/src/main/java/org/springframework/context/support/AbstractApplicationContext.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2020 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. @@ -79,6 +79,7 @@ import org.springframework.core.io.support.ResourcePatternResolver; import org.springframework.lang.Nullable; import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; import org.springframework.util.ObjectUtils; import org.springframework.util.ReflectionUtils; @@ -836,7 +837,7 @@ protected void registerListeners() { // Publish early application events now that we finally have a multicaster... Set earlyEventsToProcess = this.earlyApplicationEvents; this.earlyApplicationEvents = null; - if (earlyEventsToProcess != null) { + if (!CollectionUtils.isEmpty(earlyEventsToProcess)) { for (ApplicationEvent earlyEvent : earlyEventsToProcess) { getApplicationEventMulticaster().multicastEvent(earlyEvent); } From 08474aa921b0c57e34ef25607994fe58bef91e09 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Fri, 29 May 2020 15:51:19 +0200 Subject: [PATCH 5/8] Clarify JDBC-defined negative values returned from batchUpdate Closes gh-25138 --- .../jdbc/core/JdbcOperations.java | 16 ++++++++++++---- .../namedparam/NamedParameterJdbcOperations.java | 7 ++++++- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/JdbcOperations.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/JdbcOperations.java index 67ff03c8d766..3c3bd254f01b 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/JdbcOperations.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/JdbcOperations.java @@ -896,6 +896,8 @@ List queryForList(String sql, Object[] args, int[] argTypes, Class ele * @param pss object to set parameters on the PreparedStatement * created by this method * @return an array of the number of rows affected by each statement + * (may also contain special JDBC-defined negative values for affected rows such as + * {@link java.sql.Statement#SUCCESS_NO_INFO}/{@link java.sql.Statement#EXECUTE_FAILED}) * @throws DataAccessException if there is any problem issuing the update */ int[] batchUpdate(String sql, BatchPreparedStatementSetter pss) throws DataAccessException; @@ -905,6 +907,8 @@ List queryForList(String sql, Object[] args, int[] argTypes, Class ele * @param sql the SQL statement to execute * @param batchArgs the List of Object arrays containing the batch of arguments for the query * @return an array containing the numbers of rows affected by each update in the batch + * (may also contain special JDBC-defined negative values for affected rows such as + * {@link java.sql.Statement#SUCCESS_NO_INFO}/{@link java.sql.Statement#EXECUTE_FAILED}) * @throws DataAccessException if there is any problem issuing the update */ int[] batchUpdate(String sql, List batchArgs) throws DataAccessException; @@ -916,20 +920,24 @@ List queryForList(String sql, Object[] args, int[] argTypes, Class ele * @param argTypes the SQL types of the arguments * (constants from {@code java.sql.Types}) * @return an array containing the numbers of rows affected by each update in the batch + * (may also contain special JDBC-defined negative values for affected rows such as + * {@link java.sql.Statement#SUCCESS_NO_INFO}/{@link java.sql.Statement#EXECUTE_FAILED}) * @throws DataAccessException if there is any problem issuing the update */ int[] batchUpdate(String sql, List batchArgs, int[] argTypes) throws DataAccessException; /** - * Execute multiple batches using the supplied SQL statement with the collect of supplied arguments. - * The arguments' values will be set using the ParameterizedPreparedStatementSetter. + * Execute multiple batches using the supplied SQL statement with the collect of supplied + * arguments. The arguments' values will be set using the ParameterizedPreparedStatementSetter. * Each batch should be of size indicated in 'batchSize'. * @param sql the SQL statement to execute. * @param batchArgs the List of Object arrays containing the batch of arguments for the query * @param batchSize batch size * @param pss the ParameterizedPreparedStatementSetter to use - * @return an array containing for each batch another array containing the numbers of rows affected - * by each update in the batch + * @return an array containing for each batch another array containing the numbers of + * rows affected by each update in the batch + * (may also contain special JDBC-defined negative values for affected rows such as + * {@link java.sql.Statement#SUCCESS_NO_INFO}/{@link java.sql.Statement#EXECUTE_FAILED}) * @throws DataAccessException if there is any problem issuing the update * @since 3.1 */ diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/NamedParameterJdbcOperations.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/NamedParameterJdbcOperations.java index ad5c627eeb85..62f5934137bf 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/NamedParameterJdbcOperations.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/NamedParameterJdbcOperations.java @@ -498,6 +498,8 @@ int update(String sql, SqlParameterSource paramSource, KeyHolder generatedKeyHol * @param sql the SQL statement to execute * @param batchValues the array of Maps containing the batch of arguments for the query * @return an array containing the numbers of rows affected by each update in the batch + * (may also contain special JDBC-defined negative values for affected rows such as + * {@link java.sql.Statement#SUCCESS_NO_INFO}/{@link java.sql.Statement#EXECUTE_FAILED}) * @throws DataAccessException if there is any problem issuing the update */ int[] batchUpdate(String sql, Map[] batchValues); @@ -505,8 +507,11 @@ int update(String sql, SqlParameterSource paramSource, KeyHolder generatedKeyHol /** * Execute a batch using the supplied SQL statement with the batch of supplied arguments. * @param sql the SQL statement to execute - * @param batchArgs the array of {@link SqlParameterSource} containing the batch of arguments for the query + * @param batchArgs the array of {@link SqlParameterSource} containing the batch of + * arguments for the query * @return an array containing the numbers of rows affected by each update in the batch + * (may also contain special JDBC-defined negative values for affected rows such as + * {@link java.sql.Statement#SUCCESS_NO_INFO}/{@link java.sql.Statement#EXECUTE_FAILED}) * @throws DataAccessException if there is any problem issuing the update */ int[] batchUpdate(String sql, SqlParameterSource[] batchArgs); From cd4ef6f781a5a4df6e8fef44811a54b410fdeab6 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Fri, 29 May 2020 15:51:54 +0200 Subject: [PATCH 6/8] Consistently refer to FlushMode.MANUAL instead of outdated NEVER Closes gh-25158 --- .../orm/hibernate5/support/OpenSessionInViewFilter.java | 6 +++--- .../support/TransactionSynchronizationManager.java | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/spring-orm/src/main/java/org/springframework/orm/hibernate5/support/OpenSessionInViewFilter.java b/spring-orm/src/main/java/org/springframework/orm/hibernate5/support/OpenSessionInViewFilter.java index a68af6f7786e..ec23f51c025b 100644 --- a/spring-orm/src/main/java/org/springframework/orm/hibernate5/support/OpenSessionInViewFilter.java +++ b/spring-orm/src/main/java/org/springframework/orm/hibernate5/support/OpenSessionInViewFilter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2020 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. @@ -51,11 +51,11 @@ * as well as for non-transactional execution (if configured appropriately). * *

NOTE: This filter will by default not flush the Hibernate Session, - * with the flush mode set to {@code FlushMode.NEVER}. It assumes to be used + * with the flush mode set to {@code FlushMode.MANUAL}. It assumes to be used * in combination with service layer transactions that care for the flushing: The * active transaction manager will temporarily change the flush mode to * {@code FlushMode.AUTO} during a read-write transaction, with the flush - * mode reset to {@code FlushMode.NEVER} at the end of each transaction. + * mode reset to {@code FlushMode.MANUAL} at the end of each transaction. * *

WARNING: Applying this filter to existing logic can cause issues that * have not appeared before, through the use of a single Hibernate Session for the diff --git a/spring-tx/src/main/java/org/springframework/transaction/support/TransactionSynchronizationManager.java b/spring-tx/src/main/java/org/springframework/transaction/support/TransactionSynchronizationManager.java index 19f20f8571e5..df9132d13d51 100644 --- a/spring-tx/src/main/java/org/springframework/transaction/support/TransactionSynchronizationManager.java +++ b/spring-tx/src/main/java/org/springframework/transaction/support/TransactionSynchronizationManager.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2020 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. @@ -383,7 +383,7 @@ public static void setCurrentTransactionReadOnly(boolean readOnly) { * as argument for the {@code beforeCommit} callback, to be able * to suppress change detection on commit. The present method is meant * to be used for earlier read-only checks, for example to set the - * flush mode of a Hibernate Session to "FlushMode.NEVER" upfront. + * flush mode of a Hibernate Session to "FlushMode.MANUAL" upfront. * @see org.springframework.transaction.TransactionDefinition#isReadOnly() * @see TransactionSynchronization#beforeCommit(boolean) */ From 914425eefaee7c80a1654c1598844b69607d14e1 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Fri, 29 May 2020 15:52:39 +0200 Subject: [PATCH 7/8] Polishing --- .../beans/factory/config/YamlProcessor.java | 4 +--- .../core/env/AbstractPropertyResolver.java | 5 ++++- .../core/env/EnumerablePropertySource.java | 12 +++++++++++- .../springframework/core/env/PropertyResolver.java | 4 +--- .../springframework/core/env/PropertySource.java | 4 +++- .../springframework/util/SystemPropertyUtils.java | 5 ++++- .../web/util/ServletContextPropertyUtils.java | 13 ++++++++----- 7 files changed, 32 insertions(+), 15 deletions(-) diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/YamlProcessor.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/YamlProcessor.java index 1b4e2bdeeaa1..6d929f2ba5db 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/config/YamlProcessor.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/YamlProcessor.java @@ -432,8 +432,7 @@ public enum ResolutionMethod { /** * {@link Constructor} that supports filtering of unsupported types. *

If an unsupported type is encountered in a YAML document, an - * {@link IllegalStateException} will be thrown from {@link #getClassForName(String)}. - * @since 5.1.16 + * {@link IllegalStateException} will be thrown from {@link #getClassForName}. */ private class FilteringConstructor extends Constructor { @@ -441,7 +440,6 @@ private class FilteringConstructor extends Constructor { super(loaderOptions); } - @Override protected Class getClassForName(String name) throws ClassNotFoundException { Assert.state(YamlProcessor.this.supportedTypes.contains(name), diff --git a/spring-core/src/main/java/org/springframework/core/env/AbstractPropertyResolver.java b/spring-core/src/main/java/org/springframework/core/env/AbstractPropertyResolver.java index d5f7fd1110ca..c3f29e106ad9 100644 --- a/spring-core/src/main/java/org/springframework/core/env/AbstractPropertyResolver.java +++ b/spring-core/src/main/java/org/springframework/core/env/AbstractPropertyResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2020 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. @@ -223,6 +223,9 @@ public String resolveRequiredPlaceholders(String text) throws IllegalArgumentExc * @see #setIgnoreUnresolvableNestedPlaceholders */ protected String resolveNestedPlaceholders(String value) { + if (value.isEmpty()) { + return value; + } return (this.ignoreUnresolvableNestedPlaceholders ? resolvePlaceholders(value) : resolveRequiredPlaceholders(value)); } diff --git a/spring-core/src/main/java/org/springframework/core/env/EnumerablePropertySource.java b/spring-core/src/main/java/org/springframework/core/env/EnumerablePropertySource.java index 0ef6ce4a7797..2c1386d31258 100644 --- a/spring-core/src/main/java/org/springframework/core/env/EnumerablePropertySource.java +++ b/spring-core/src/main/java/org/springframework/core/env/EnumerablePropertySource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2020 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. @@ -44,10 +44,20 @@ */ public abstract class EnumerablePropertySource extends PropertySource { + /** + * Create a new {@code EnumerablePropertySource} with the given name and source object. + * @param name the associated name + * @param source the source object + */ public EnumerablePropertySource(String name, T source) { super(name, source); } + /** + * Create a new {@code EnumerablePropertySource} with the given name and with a new + * {@code Object} instance as the underlying source. + * @param name the associated name + */ protected EnumerablePropertySource(String name) { super(name); } diff --git a/spring-core/src/main/java/org/springframework/core/env/PropertyResolver.java b/spring-core/src/main/java/org/springframework/core/env/PropertyResolver.java index 5554463c4490..173a1a33784d 100644 --- a/spring-core/src/main/java/org/springframework/core/env/PropertyResolver.java +++ b/spring-core/src/main/java/org/springframework/core/env/PropertyResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-2020 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. @@ -98,7 +98,6 @@ public interface PropertyResolver { * @return the resolved String (never {@code null}) * @throws IllegalArgumentException if given text is {@code null} * @see #resolveRequiredPlaceholders - * @see org.springframework.util.SystemPropertyUtils#resolvePlaceholders(String) */ String resolvePlaceholders(String text); @@ -109,7 +108,6 @@ public interface PropertyResolver { * @return the resolved String (never {@code null}) * @throws IllegalArgumentException if given text is {@code null} * or if any placeholders are unresolvable - * @see org.springframework.util.SystemPropertyUtils#resolvePlaceholders(String, boolean) */ String resolveRequiredPlaceholders(String text) throws IllegalArgumentException; diff --git a/spring-core/src/main/java/org/springframework/core/env/PropertySource.java b/spring-core/src/main/java/org/springframework/core/env/PropertySource.java index 2cb6313064cc..c8a1da6bb82f 100644 --- a/spring-core/src/main/java/org/springframework/core/env/PropertySource.java +++ b/spring-core/src/main/java/org/springframework/core/env/PropertySource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2020 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. @@ -68,6 +68,8 @@ public abstract class PropertySource { /** * Create a new {@code PropertySource} with the given name and source object. + * @param name the associated name + * @param source the source object */ public PropertySource(String name, T source) { Assert.hasText(name, "Property source name must contain at least one character"); diff --git a/spring-core/src/main/java/org/springframework/util/SystemPropertyUtils.java b/spring-core/src/main/java/org/springframework/util/SystemPropertyUtils.java index c1769c6b6319..7c5ac6bdf425 100644 --- a/spring-core/src/main/java/org/springframework/util/SystemPropertyUtils.java +++ b/spring-core/src/main/java/org/springframework/util/SystemPropertyUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2020 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. @@ -78,6 +78,9 @@ public static String resolvePlaceholders(String text) { * and the "ignoreUnresolvablePlaceholders" flag is {@code false} */ public static String resolvePlaceholders(String text, boolean ignoreUnresolvablePlaceholders) { + if (text.isEmpty()) { + return text; + } PropertyPlaceholderHelper helper = (ignoreUnresolvablePlaceholders ? nonStrictHelper : strictHelper); return helper.replacePlaceholders(text, new SystemPropertyPlaceholderResolver(text)); } diff --git a/spring-web/src/main/java/org/springframework/web/util/ServletContextPropertyUtils.java b/spring-web/src/main/java/org/springframework/web/util/ServletContextPropertyUtils.java index 18709788aa27..ffaefa75b4be 100644 --- a/spring-web/src/main/java/org/springframework/web/util/ServletContextPropertyUtils.java +++ b/spring-web/src/main/java/org/springframework/web/util/ServletContextPropertyUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2020 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. @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.web.util; import javax.servlet.ServletContext; @@ -73,16 +74,18 @@ public static String resolvePlaceholders(String text, ServletContext servletCont * @see SystemPropertyUtils#PLACEHOLDER_SUFFIX * @see SystemPropertyUtils#resolvePlaceholders(String, boolean) */ - public static String resolvePlaceholders(String text, ServletContext servletContext, - boolean ignoreUnresolvablePlaceholders) { + public static String resolvePlaceholders( + String text, ServletContext servletContext, boolean ignoreUnresolvablePlaceholders) { + if (text.isEmpty()) { + return text; + } PropertyPlaceholderHelper helper = (ignoreUnresolvablePlaceholders ? nonStrictHelper : strictHelper); return helper.replacePlaceholders(text, new ServletContextPlaceholderResolver(text, servletContext)); } - private static class ServletContextPlaceholderResolver - implements PropertyPlaceholderHelper.PlaceholderResolver { + private static class ServletContextPlaceholderResolver implements PropertyPlaceholderHelper.PlaceholderResolver { private final String text; From 6d6269f1eee53b1ebccbccc97d84e8617bf257d1 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Fri, 29 May 2020 15:07:07 +0100 Subject: [PATCH 8/8] Switch to Reactor 2020.0.0 snapshots A switch to RSocket 1.0.1 snapshots is also required to pick up a for froward compatibility with Reactor Netty 1.0. See gh-25085 --- build.gradle | 6 +- .../tcp/reactor/ReactorNettyTcpClient.java | 29 +++++----- .../reactor/ReactorNettyTcpConnection.java | 4 +- .../DefaultRSocketRequesterBuilderTests.java | 5 +- .../reactive/ReactorClientHttpConnector.java | 2 +- .../reactive/ReactorResourceFactory.java | 3 +- .../reactive/bootstrap/ReactorHttpServer.java | 8 +-- .../bootstrap/ReactorHttpsServer.java | 5 +- .../client/DefaultWebClientBuilder.java | 2 +- .../resource/CssLinkResourceTransformer.java | 2 +- .../client/ReactorNettyWebSocketClient.java | 58 ++++++++++++++++--- .../socket/server/RequestUpgradeStrategy.java | 1 - .../WebClientDataBufferAllocatingTests.java | 4 +- 13 files changed, 84 insertions(+), 45 deletions(-) diff --git a/build.gradle b/build.gradle index 62b8abf36ce7..dfc389ecd432 100644 --- a/build.gradle +++ b/build.gradle @@ -25,8 +25,8 @@ configure(allprojects) { project -> imports { mavenBom "com.fasterxml.jackson:jackson-bom:2.11.0" mavenBom "io.netty:netty-bom:4.1.50.Final" - mavenBom "io.projectreactor:reactor-bom:Dysprosium-SR7" - mavenBom "io.rsocket:rsocket-bom:1.0.0" + mavenBom "io.projectreactor:reactor-bom:2020.0.0-SNAPSHOT" + mavenBom "io.rsocket:rsocket-bom:1.0.1-SNAPSHOT" mavenBom "org.eclipse.jetty:jetty-bom:9.4.29.v20200521" mavenBom "org.jetbrains.kotlin:kotlin-bom:1.3.72" mavenBom "org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.3.5" @@ -281,6 +281,8 @@ configure(allprojects) { project -> repositories { mavenCentral() maven { url "https://repo.spring.io/libs-spring-framework-build" } + maven { url "https://repo.spring.io/snapshot" } // Reactor + maven { url "https://oss.jfrog.org/artifactory/oss-snapshot-local" } // RSocket } } configurations.all { diff --git a/spring-messaging/src/main/java/org/springframework/messaging/tcp/reactor/ReactorNettyTcpClient.java b/spring-messaging/src/main/java/org/springframework/messaging/tcp/reactor/ReactorNettyTcpClient.java index 8e327bcd818f..f7f303f39963 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/tcp/reactor/ReactorNettyTcpClient.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/tcp/reactor/ReactorNettyTcpClient.java @@ -19,7 +19,6 @@ import java.time.Duration; import java.util.Collection; import java.util.List; -import java.util.Optional; import java.util.function.BiFunction; import java.util.function.Consumer; import java.util.function.Function; @@ -34,7 +33,6 @@ import org.apache.commons.logging.LogFactory; import org.reactivestreams.Publisher; import reactor.core.publisher.DirectProcessor; -import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.core.publisher.MonoProcessor; import reactor.core.scheduler.Scheduler; @@ -46,6 +44,7 @@ import reactor.netty.resources.ConnectionProvider; import reactor.netty.resources.LoopResources; import reactor.netty.tcp.TcpClient; +import reactor.util.retry.Retry; import org.springframework.lang.Nullable; import org.springframework.messaging.Message; @@ -103,14 +102,13 @@ public class ReactorNettyTcpClient

implements TcpOperations

{ * @param codec for encoding and decoding the input/output byte streams * @see org.springframework.messaging.simp.stomp.StompReactorNettyCodec */ - @SuppressWarnings("deprecation") public ReactorNettyTcpClient(String host, int port, ReactorNettyCodec

codec) { Assert.notNull(host, "host is required"); Assert.notNull(codec, "ReactorNettyCodec is required"); this.channelGroup = new DefaultChannelGroup(ImmediateEventExecutor.INSTANCE); this.loopResources = LoopResources.create("tcp-client-loop"); - this.poolResources = ConnectionProvider.fixed("tcp-client-pool", 10000); + this.poolResources = ConnectionProvider.create("tcp-client-pool", 10000); this.codec = codec; this.tcpClient = TcpClient.create(this.poolResources) @@ -129,13 +127,12 @@ public ReactorNettyTcpClient(String host, int port, ReactorNettyCodec

codec) * @since 5.1.3 * @see org.springframework.messaging.simp.stomp.StompReactorNettyCodec */ - @SuppressWarnings("deprecation") public ReactorNettyTcpClient(Function clientConfigurer, ReactorNettyCodec

codec) { Assert.notNull(codec, "ReactorNettyCodec is required"); this.channelGroup = new DefaultChannelGroup(ImmediateEventExecutor.INSTANCE); this.loopResources = LoopResources.create("tcp-client-loop"); - this.poolResources = ConnectionProvider.fixed("tcp-client-pool", 10000); + this.poolResources = ConnectionProvider.create("tcp-client-pool", 10000); this.codec = codec; this.tcpClient = clientConfigurer.apply(TcpClient @@ -199,7 +196,6 @@ public ListenableFuture connect(final TcpConnectionHandler

handler) { } @Override - @SuppressWarnings("deprecation") public ListenableFuture connect(TcpConnectionHandler

handler, ReconnectStrategy strategy) { Assert.notNull(handler, "TcpConnectionHandler is required"); Assert.notNull(strategy, "ReconnectStrategy is required"); @@ -218,8 +214,12 @@ public ListenableFuture connect(TcpConnectionHandler

handler, Reconnect .doOnError(updateConnectMono(connectMono)) .doOnError(handler::afterConnectFailure) // report all connect failures to the handler .flatMap(Connection::onDispose) // post-connect issues - .retryWhen(reconnectFunction(strategy)) - .repeatWhen(reconnectFunction(strategy)) + .retryWhen(Retry.from(signals -> signals + .map(retrySignal -> (int) retrySignal.totalRetriesInARow()) + .flatMap(attempt -> reconnect(attempt, strategy)))) + .repeatWhen(flux -> flux + .scan(1, (count, element) -> count++) + .flatMap(attempt -> reconnect(attempt, strategy))) .subscribe(); return new MonoToListenableFutureAdapter<>(connectMono); @@ -244,12 +244,9 @@ private Consumer updateConnectMono(MonoProcessor connectMono) { }; } - private Function, Publisher> reconnectFunction(ReconnectStrategy reconnectStrategy) { - return flux -> flux - .scan(1, (count, element) -> count++) - .flatMap(attempt -> Optional.ofNullable(reconnectStrategy.getTimeToNextAttempt(attempt)) - .map(time -> Mono.delay(Duration.ofMillis(time), this.scheduler)) - .orElse(Mono.empty())); + private Publisher reconnect(Integer attempt, ReconnectStrategy reconnectStrategy) { + Long time = reconnectStrategy.getTimeToNextAttempt(attempt); + return (time != null ? Mono.delay(Duration.ofMillis(time), this.scheduler) : Mono.empty()); } @Override @@ -342,7 +339,7 @@ private static class StompMessageDecoder

extends ByteToMessageDecoder { private final ReactorNettyCodec

codec; - public StompMessageDecoder(ReactorNettyCodec

codec) { + StompMessageDecoder(ReactorNettyCodec

codec) { this.codec = codec; } diff --git a/spring-messaging/src/main/java/org/springframework/messaging/tcp/reactor/ReactorNettyTcpConnection.java b/spring-messaging/src/main/java/org/springframework/messaging/tcp/reactor/ReactorNettyTcpConnection.java index dfed717aafaa..9665057011b7 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/tcp/reactor/ReactorNettyTcpConnection.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/tcp/reactor/ReactorNettyTcpConnection.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2020 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. @@ -64,13 +64,11 @@ public ListenableFuture send(Message

message) { } @Override - @SuppressWarnings("deprecation") public void onReadInactivity(Runnable runnable, long inactivityDuration) { this.inbound.withConnection(conn -> conn.onReadIdle(inactivityDuration, runnable)); } @Override - @SuppressWarnings("deprecation") public void onWriteInactivity(Runnable runnable, long inactivityDuration) { this.inbound.withConnection(conn -> conn.onWriteIdle(inactivityDuration, runnable)); } diff --git a/spring-messaging/src/test/java/org/springframework/messaging/rsocket/DefaultRSocketRequesterBuilderTests.java b/spring-messaging/src/test/java/org/springframework/messaging/rsocket/DefaultRSocketRequesterBuilderTests.java index f695228117f4..a622d878ceea 100644 --- a/spring-messaging/src/test/java/org/springframework/messaging/rsocket/DefaultRSocketRequesterBuilderTests.java +++ b/spring-messaging/src/test/java/org/springframework/messaging/rsocket/DefaultRSocketRequesterBuilderTests.java @@ -52,7 +52,6 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; @@ -75,7 +74,7 @@ public class DefaultRSocketRequesterBuilderTests { @BeforeEach public void setup() { this.transport = mock(ClientTransport.class); - given(this.transport.connect(anyInt())).willReturn(Mono.just(this.connection)); + given(this.transport.connect()).willReturn(Mono.just(this.connection)); } @@ -106,7 +105,7 @@ public void rsocketConnectorConfigurer() { // RSocketStrategies and RSocketConnector configurers should have been called - verify(this.transport).connect(anyInt()); + verify(this.transport).connect(); verify(strategiesConfigurer).accept(any(RSocketStrategies.Builder.class)); verify(factoryConfigurer).configure(any(io.rsocket.RSocketFactory.ClientRSocketFactory.class)); assertThat(this.connectorConfigurer.connector()).isNotNull(); diff --git a/spring-web/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpConnector.java b/spring-web/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpConnector.java index 98dd9dd8babb..3e2e93adbedf 100644 --- a/spring-web/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpConnector.java +++ b/spring-web/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpConnector.java @@ -82,7 +82,7 @@ private static HttpClient initHttpClient(ReactorResourceFactory resourceFactory) LoopResources resources = resourceFactory.getLoopResources(); Assert.notNull(provider, "No ConnectionProvider: is ReactorResourceFactory not initialized yet?"); Assert.notNull(resources, "No LoopResources: is ReactorResourceFactory not initialized yet?"); - return HttpClient.create(provider).tcpConfiguration(tcpClient -> tcpClient.runOn(resources)); + return HttpClient.create(provider).runOn(resources); } /** diff --git a/spring-web/src/main/java/org/springframework/http/client/reactive/ReactorResourceFactory.java b/spring-web/src/main/java/org/springframework/http/client/reactive/ReactorResourceFactory.java index 87af75f09d22..556427435fb6 100644 --- a/spring-web/src/main/java/org/springframework/http/client/reactive/ReactorResourceFactory.java +++ b/spring-web/src/main/java/org/springframework/http/client/reactive/ReactorResourceFactory.java @@ -47,8 +47,7 @@ public class ReactorResourceFactory implements InitializingBean, DisposableBean @Nullable private Consumer globalResourcesConsumer; - @SuppressWarnings("deprecation") - private Supplier connectionProviderSupplier = () -> ConnectionProvider.fixed("webflux", 500); + private Supplier connectionProviderSupplier = () -> ConnectionProvider.create("webflux", 500); @Nullable private ConnectionProvider connectionProvider; diff --git a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/bootstrap/ReactorHttpServer.java b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/bootstrap/ReactorHttpServer.java index 4df1070f61ff..d9a12fc4c3e7 100644 --- a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/bootstrap/ReactorHttpServer.java +++ b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/bootstrap/ReactorHttpServer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2020 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. @@ -16,6 +16,7 @@ package org.springframework.web.testfixture.http.server.reactive.bootstrap; +import java.net.InetSocketAddress; import java.util.concurrent.atomic.AtomicReference; import reactor.netty.DisposableServer; @@ -38,8 +39,7 @@ public class ReactorHttpServer extends AbstractHttpServer { protected void initServer() { this.reactorHandler = createHttpHandlerAdapter(); this.reactorServer = reactor.netty.http.server.HttpServer.create() - .tcpConfiguration(server -> server.host(getHost())) - .port(getPort()); + .host(getHost()).port(getPort()); } private ReactorHttpHandlerAdapter createHttpHandlerAdapter() { @@ -49,7 +49,7 @@ private ReactorHttpHandlerAdapter createHttpHandlerAdapter() { @Override protected void startInternal() { DisposableServer server = this.reactorServer.handle(this.reactorHandler).bind().block(); - setPort(server.address().getPort()); + setPort(((InetSocketAddress) server.address()).getPort()); this.serverRef.set(server); } diff --git a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/bootstrap/ReactorHttpsServer.java b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/bootstrap/ReactorHttpsServer.java index 004e27832a9e..ad6c90a68985 100644 --- a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/bootstrap/ReactorHttpsServer.java +++ b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/bootstrap/ReactorHttpsServer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2020 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. @@ -16,6 +16,7 @@ package org.springframework.web.testfixture.http.server.reactive.bootstrap; +import java.net.InetSocketAddress; import java.util.concurrent.atomic.AtomicReference; import io.netty.handler.ssl.SslContextBuilder; @@ -57,7 +58,7 @@ private ReactorHttpHandlerAdapter createHttpHandlerAdapter() { @Override protected void startInternal() { DisposableServer server = this.reactorServer.handle(this.reactorHandler).bind().block(); - setPort(server.address().getPort()); + setPort(((InetSocketAddress) server.address()).getPort()); this.serverRef.set(server); } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClientBuilder.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClientBuilder.java index 0fa80f70dc09..f7ba51ed10ce 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClientBuilder.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClientBuilder.java @@ -228,8 +228,8 @@ public WebClient.Builder exchangeStrategies(ExchangeStrategies strategies) { return this; } - @SuppressWarnings("deprecation") @Override + @SuppressWarnings("deprecation") public WebClient.Builder exchangeStrategies(Consumer configurer) { if (this.strategiesConfigurers == null) { this.strategiesConfigurers = new ArrayList<>(4); diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/resource/CssLinkResourceTransformer.java b/spring-webflux/src/main/java/org/springframework/web/reactive/resource/CssLinkResourceTransformer.java index e794c6f6a5c7..3d96ff0b571b 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/resource/CssLinkResourceTransformer.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/resource/CssLinkResourceTransformer.java @@ -70,8 +70,8 @@ public CssLinkResourceTransformer() { } - @SuppressWarnings("deprecation") @Override + @SuppressWarnings("deprecation") public Mono transform(ServerWebExchange exchange, Resource inputResource, ResourceTransformerChain transformerChain) { diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/client/ReactorNettyWebSocketClient.java b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/client/ReactorNettyWebSocketClient.java index 5b9453297fb2..392af3e894bc 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/client/ReactorNettyWebSocketClient.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/client/ReactorNettyWebSocketClient.java @@ -17,15 +17,18 @@ package org.springframework.web.reactive.socket.client; import java.net.URI; +import java.util.function.Supplier; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import reactor.core.publisher.Mono; import reactor.netty.http.client.HttpClient; +import reactor.netty.http.client.WebsocketClientSpec; import reactor.netty.http.websocket.WebsocketInbound; import org.springframework.core.io.buffer.NettyDataBufferFactory; import org.springframework.http.HttpHeaders; +import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.StringUtils; import org.springframework.web.reactive.socket.HandshakeInfo; @@ -47,9 +50,13 @@ public class ReactorNettyWebSocketClient implements WebSocketClient { private final HttpClient httpClient; - private int maxFramePayloadLength = NettyWebSocketSessionSupport.DEFAULT_FRAME_MAX_SIZE; + private final Supplier specBuilderSupplier; - private boolean handlePing; + @Nullable + private Integer maxFramePayloadLength = NettyWebSocketSessionSupport.DEFAULT_FRAME_MAX_SIZE; + + @Nullable + private Boolean handlePing; /** @@ -60,12 +67,25 @@ public ReactorNettyWebSocketClient() { } /** - * Constructor that accepts an existing {@link HttpClient} builder. + * Constructor that accepts an existing {@link HttpClient}. * @since 5.1 */ public ReactorNettyWebSocketClient(HttpClient httpClient) { + this(httpClient, WebsocketClientSpec.builder()); + } + + /** + * Constructor with an {@link HttpClient} and a supplier for the + * {@link WebsocketClientSpec.Builder} to use. + * @since 5.3 + */ + public ReactorNettyWebSocketClient( + HttpClient httpClient, Supplier builderSupplier) { + Assert.notNull(httpClient, "HttpClient is required"); + Assert.notNull(builderSupplier, "WebsocketClientSpec.Builder is required"); this.httpClient = httpClient; + this.specBuilderSupplier = builderSupplier; } @@ -76,6 +96,31 @@ public HttpClient getHttpClient() { return this.httpClient; } + /** + * Build an instance of {@code WebsocketClientSpec} that reflects the current + * configuration. This can be used to check the configured parameters except + * for sub-protocols which depend on the {@link WebSocketHandler} that is used + * for a given upgrade. + * @since 5.3 + */ + public WebsocketClientSpec getWebsocketClientSpec() { + return buildSpec(null); + } + + private WebsocketClientSpec buildSpec(@Nullable String protocols) { + WebsocketClientSpec.Builder builder = this.specBuilderSupplier.get(); + if (StringUtils.hasText(protocols)) { + builder.protocols(protocols); + } + if (this.maxFramePayloadLength != null) { + builder.maxFramePayloadLength(this.maxFramePayloadLength); + } + if (this.handlePing != null) { + builder.handlePing(this.handlePing); + } + return builder.build(); + } + /** * Configure the maximum allowable frame payload length. Setting this value * to your application's requirement may reduce denial of service attacks @@ -96,7 +141,7 @@ public void setMaxFramePayloadLength(int maxFramePayloadLength) { * @since 5.2 */ public int getMaxFramePayloadLength() { - return this.maxFramePayloadLength; + return getWebsocketClientSpec().maxFramePayloadLength(); } /** @@ -119,7 +164,7 @@ public void setHandlePing(boolean handlePing) { * @since 5.2.4 */ public boolean getHandlePing() { - return this.handlePing; + return getWebsocketClientSpec().handlePing(); } @Override @@ -128,12 +173,11 @@ public Mono execute(URI url, WebSocketHandler handler) { } @Override - @SuppressWarnings("deprecation") public Mono execute(URI url, HttpHeaders requestHeaders, WebSocketHandler handler) { String protocols = StringUtils.collectionToCommaDelimitedString(handler.getSubProtocols()); return getHttpClient() .headers(nettyHeaders -> setNettyHeaders(requestHeaders, nettyHeaders)) - .websocket(protocols, getMaxFramePayloadLength(), this.handlePing) + .websocket(buildSpec(protocols)) .uri(url.toString()) .handle((inbound, outbound) -> { HttpHeaders responseHeaders = toHttpHeaders(inbound); diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/server/RequestUpgradeStrategy.java b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/server/RequestUpgradeStrategy.java index 4ac711ca795a..a50fe279ecb8 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/server/RequestUpgradeStrategy.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/server/RequestUpgradeStrategy.java @@ -68,7 +68,6 @@ default Mono upgrade(ServerWebExchange exchange, WebSocketHandler webSocke * WebSocket session handling. * @since 5.1 */ - @SuppressWarnings("deprecation") default Mono upgrade(ServerWebExchange exchange, WebSocketHandler webSocketHandler, @Nullable String subProtocol, Supplier handshakeInfoFactory) { diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/WebClientDataBufferAllocatingTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/WebClientDataBufferAllocatingTests.java index 5dd289cba61a..450198d08d57 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/WebClientDataBufferAllocatingTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/WebClientDataBufferAllocatingTests.java @@ -87,8 +87,8 @@ private ReactorClientHttpConnector initConnector() { if (super.bufferFactory instanceof NettyDataBufferFactory) { ByteBufAllocator allocator = ((NettyDataBufferFactory) super.bufferFactory).getByteBufAllocator(); - return new ReactorClientHttpConnector(this.factory, httpClient -> - httpClient.tcpConfiguration(tcpClient -> tcpClient.option(ChannelOption.ALLOCATOR, allocator))); + return new ReactorClientHttpConnector(this.factory, + client -> client.option(ChannelOption.ALLOCATOR, allocator)); } else { return new ReactorClientHttpConnector();