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-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..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 @@ -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,24 @@ 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}. + */ + 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())); + } + } 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); } 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/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); } 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/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(); + } + +} 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-core/src/test/java/org/springframework/util/CollectionUtilsTests.java b/spring-core/src/test/java/org/springframework/util/CollectionUtilsTests.java index 45efbeea5e37..ab3a57da3edb 100644 --- a/spring-core/src/test/java/org/springframework/util/CollectionUtilsTests.java +++ b/spring-core/src/test/java/org/springframework/util/CollectionUtilsTests.java @@ -18,6 +18,7 @@ import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.Enumeration; import java.util.HashMap; import java.util.HashSet; @@ -28,8 +29,11 @@ import java.util.Map; import java.util.Properties; import java.util.Set; +import java.util.stream.Stream; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; import static org.assertj.core.api.Assertions.assertThat; @@ -211,17 +215,21 @@ void hasUniqueObject() { assertThat(CollectionUtils.hasUniqueObject(list)).isFalse(); } - @Test - void conversionOfEmptyMap() { - MultiValueMap> asMultiValueMap = CollectionUtils.toMultiValueMap(new HashMap<>()); + @ParameterizedTest + @MethodSource("emptyMaps") + void conversionOfEmptyMap(Map> emptyMap) { + MultiValueMap asMultiValueMap = CollectionUtils.toMultiValueMap(emptyMap); assertThat(asMultiValueMap.isEmpty()).isTrue(); assertThat(asMultiValueMap).isEmpty(); } + static Stream>> emptyMaps() { + return Stream.of(Collections.emptyMap(), new HashMap<>()); + } + @Test void conversionOfNonEmptyMap() { - Map> wrapped = new HashMap<>(); - wrapped.put("key", Arrays.asList("first", "second")); + Map> wrapped = Collections.singletonMap("key", Arrays.asList("first", "second")); MultiValueMap asMultiValueMap = CollectionUtils.toMultiValueMap(wrapped); assertThat(asMultiValueMap).containsAllEntriesOf(wrapped); } @@ -231,8 +239,8 @@ void changesValueByReference() { Map> wrapped = new HashMap<>(); MultiValueMap asMultiValueMap = CollectionUtils.toMultiValueMap(wrapped); assertThat(asMultiValueMap).doesNotContainKeys("key"); - wrapped.put("key", new ArrayList<>()); + wrapped.put("key", new ArrayList<>()); assertThat(asMultiValueMap).containsKey("key"); } diff --git a/spring-core/src/test/java/org/springframework/util/MultiValueMapRelatedTests.java b/spring-core/src/test/java/org/springframework/util/MultiValueMapAdaptersTests.java similarity index 63% rename from spring-core/src/test/java/org/springframework/util/MultiValueMapRelatedTests.java rename to spring-core/src/test/java/org/springframework/util/MultiValueMapAdaptersTests.java index 686d77a1a2d5..32aef33bcd65 100644 --- a/spring-core/src/test/java/org/springframework/util/MultiValueMapRelatedTests.java +++ b/spring-core/src/test/java/org/springframework/util/MultiValueMapAdaptersTests.java @@ -16,13 +16,6 @@ package org.springframework.util; -import org.jetbrains.annotations.NotNull; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.MethodSource; - -import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; @@ -31,6 +24,10 @@ import java.util.Map; import java.util.stream.Stream; +import org.assertj.core.api.SoftAssertions; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + import static org.assertj.core.api.Assertions.assertThat; /** @@ -38,7 +35,7 @@ * @author Arjen Poutsma * @author Juergen Hoeller */ -class MultiValueMapRelatedTests { +class MultiValueMapAdaptersTests { @ParameterizedTest @MethodSource("objectsUnderTest") @@ -76,7 +73,7 @@ void set(MultiValueMap objectUnderTest) { @ParameterizedTest @MethodSource("objectsUnderTest") void addAll(MultiValueMap objectUnderTest) { - int startingSize = objectUnderTest.size(); + int startingSize = objectUnderTest.size(); objectUnderTest.add("key", "value1"); objectUnderTest.addAll("key", Arrays.asList("value2", "value3")); @@ -86,10 +83,11 @@ void addAll(MultiValueMap objectUnderTest) { @ParameterizedTest @MethodSource("objectsUnderTest") - @Disabled("to be fixed in gh-25140") void addAllWithEmptyList(MultiValueMap objectUnderTest) { + int startingSize = objectUnderTest.size(); + objectUnderTest.addAll("key", Collections.emptyList()); - assertThat(objectUnderTest).hasSize(1); + assertThat(objectUnderTest).hasSize(startingSize + 1); assertThat(objectUnderTest.get("key")).isEmpty(); assertThat(objectUnderTest.getFirst("key")).isNull(); } @@ -97,10 +95,7 @@ void addAllWithEmptyList(MultiValueMap objectUnderTest) { @ParameterizedTest @MethodSource("objectsUnderTest") void getFirst(MultiValueMap objectUnderTest) { - List values = new ArrayList<>(2); - values.add("value1"); - values.add("value2"); - objectUnderTest.put("key", values); + objectUnderTest.put("key", Arrays.asList("value1", "value2")); assertThat(objectUnderTest.getFirst("key")).isEqualTo("value1"); assertThat(objectUnderTest.getFirst("other")).isNull(); } @@ -111,18 +106,14 @@ void toSingleValueMap(MultiValueMap objectUnderTest) { int startingSize = objectUnderTest.size(); - List values = new ArrayList<>(2); - values.add("value1"); - values.add("value2"); - objectUnderTest.put("key", values); + objectUnderTest.put("key", Arrays.asList("value1", "value2")); Map singleValueMap = objectUnderTest.toSingleValueMap(); assertThat(singleValueMap).hasSize(startingSize + 1); assertThat(singleValueMap.get("key")).isEqualTo("value1"); } @ParameterizedTest - @MethodSource("objectsUnderTest") - @Disabled("to be fixed in gh-25140") + @MethodSource("emptyObjectsUnderTest") void toSingleValueMapWithEmptyList(MultiValueMap objectUnderTest) { objectUnderTest.put("key", Collections.emptyList()); Map singleValueMap = objectUnderTest.toSingleValueMap(); @@ -131,24 +122,21 @@ void toSingleValueMapWithEmptyList(MultiValueMap objectUnderTest } @ParameterizedTest - @MethodSource("objectsUnderTest") + @MethodSource("emptyObjectsUnderTest") void equalsOnExistingValues(MultiValueMap objectUnderTest) { - objectUnderTest.clear(); objectUnderTest.set("key1", "value1"); assertThat(objectUnderTest).isEqualTo(objectUnderTest); } @ParameterizedTest - @MethodSource("objectsUnderTest") + @MethodSource("emptyObjectsUnderTest") void equalsOnEmpty(MultiValueMap objectUnderTest) { - objectUnderTest.clear(); objectUnderTest.set("key1", "value1"); MultiValueMap o1 = new LinkedMultiValueMap<>(); o1.set("key1", "value1"); assertThat(o1).isEqualTo(objectUnderTest); assertThat(objectUnderTest).isEqualTo(o1); - Map> o2 = new HashMap<>(); - o2.put("key1", Collections.singletonList("value1")); + Map> o2 = Collections.singletonMap("key1", Collections.singletonList("value1")); assertThat(o2).isEqualTo(objectUnderTest); assertThat(objectUnderTest).isEqualTo(o2); } @@ -157,58 +145,53 @@ void equalsOnEmpty(MultiValueMap objectUnderTest) { @MethodSource("objectsUnderTest") void canNotChangeAnUnmodifiableMultiValueMap(MultiValueMap objectUnderTest) { MultiValueMap asUnmodifiableMultiValueMap = CollectionUtils.unmodifiableMultiValueMap(objectUnderTest); - Assertions.assertAll( - () -> Assertions.assertThrows(UnsupportedOperationException.class, - () -> asUnmodifiableMultiValueMap.add("key", "value")), - () -> Assertions.assertThrows(UnsupportedOperationException.class, - () -> asUnmodifiableMultiValueMap.addIfAbsent("key", "value")), - () -> Assertions.assertThrows(UnsupportedOperationException.class, - () -> asUnmodifiableMultiValueMap.addAll("key", exampleListOfValues())), - () -> Assertions.assertThrows(UnsupportedOperationException.class, - () -> asUnmodifiableMultiValueMap.addAll(exampleMultiValueMap())), - () -> Assertions.assertThrows(UnsupportedOperationException.class, - () -> asUnmodifiableMultiValueMap.set("key", "value")), - () -> Assertions.assertThrows(UnsupportedOperationException.class, - () -> asUnmodifiableMultiValueMap.setAll(exampleHashMap())), - () -> Assertions.assertThrows(UnsupportedOperationException.class, - () -> asUnmodifiableMultiValueMap.put("key", exampleListOfValues())), - () -> Assertions.assertThrows(UnsupportedOperationException.class, - () -> asUnmodifiableMultiValueMap.putIfAbsent("key", exampleListOfValues())), - () -> Assertions.assertThrows(UnsupportedOperationException.class, - () -> asUnmodifiableMultiValueMap.putAll(exampleMultiValueMap())), - () -> Assertions.assertThrows(UnsupportedOperationException.class, - () -> asUnmodifiableMultiValueMap.remove("key1")) - ); - - } - @NotNull - private List exampleListOfValues() { - return Arrays.asList("value1", "value2"); + SoftAssertions bundle = new SoftAssertions(); + + bundle.assertThatThrownBy(() -> asUnmodifiableMultiValueMap.add("key", "value")) + .isExactlyInstanceOf(UnsupportedOperationException.class); + bundle.assertThatThrownBy(() -> asUnmodifiableMultiValueMap.addIfAbsent("key", "value")) + .isExactlyInstanceOf(UnsupportedOperationException.class); + bundle.assertThatThrownBy(() -> asUnmodifiableMultiValueMap.addAll("key", Arrays.asList("value1", "value2"))) + .isExactlyInstanceOf(UnsupportedOperationException.class); + bundle.assertThatThrownBy(() -> asUnmodifiableMultiValueMap.addAll( + new LinkedMultiValueMap() {{ + put("key1", Arrays.asList("key1.value1", "key1.value2")); + }})).isExactlyInstanceOf(UnsupportedOperationException.class); + bundle.assertThatThrownBy(() -> asUnmodifiableMultiValueMap.set("key", "value")) + .isExactlyInstanceOf(UnsupportedOperationException.class); + bundle.assertThatThrownBy(() -> asUnmodifiableMultiValueMap.setAll(Collections.singletonMap("key2", "key2.value"))) + .isExactlyInstanceOf(UnsupportedOperationException.class); + bundle.assertThatThrownBy(() -> asUnmodifiableMultiValueMap.put("key", Arrays.asList("value1", "value2"))) + .isExactlyInstanceOf(UnsupportedOperationException.class); + bundle.assertThatThrownBy(() -> asUnmodifiableMultiValueMap.putIfAbsent("key", Arrays.asList("value1", "value2"))) + .isExactlyInstanceOf(UnsupportedOperationException.class); + bundle.assertThatThrownBy(() -> asUnmodifiableMultiValueMap.putAll(Collections.singletonMap("key", Arrays.asList("value1", "value2")))) + .isExactlyInstanceOf(UnsupportedOperationException.class); + bundle.assertThatThrownBy(() -> asUnmodifiableMultiValueMap.remove("key1")) + .isExactlyInstanceOf(UnsupportedOperationException.class); + + bundle.assertAll(); } - @NotNull - private HashMap exampleHashMap() { - return new HashMap() {{ - put("key2", "key2.value1"); + static Stream> objectsUnderTest() { + HashMap> wrappedHashMap = new HashMap>() {{ + put("existingkey", Arrays.asList("existingvalue1", "existingvalue2")); }}; - } - private MultiValueMap exampleMultiValueMap() { - return new LinkedMultiValueMap() {{ - put("key1", Arrays.asList("key1.value1", "key1.value2")); - }}; + return Stream.concat( + emptyObjectsUnderTest(), + Stream.of( + new LinkedMultiValueMap<>(wrappedHashMap), + CollectionUtils.toMultiValueMap(wrappedHashMap) + )); } - static Stream> objectsUnderTest() { + static Stream> emptyObjectsUnderTest() { return Stream.of( new LinkedMultiValueMap<>(), new LinkedMultiValueMap<>(new HashMap<>()), new LinkedMultiValueMap<>(new LinkedHashMap<>()), - new LinkedMultiValueMap<>(new HashMap>(){{ - put("existingkey", Arrays.asList("existingvalue1", "existingvalue2")); - }}), CollectionUtils.toMultiValueMap(new HashMap<>())); } - } 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 f38fb2c8510d..16151ded6910 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 @@ -965,6 +965,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; @@ -974,6 +976,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; @@ -985,20 +989,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 2f123d327f8d..5d22b535e612 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 @@ -530,6 +530,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); @@ -537,8 +539,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); 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-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) */ 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/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; 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();