Compare commits

...

17 Commits

Author SHA1 Message Date
Brian Clozel 1ab9f7adbc Release v6.2.14 2025-11-20 09:16:52 +01:00
Juergen Hoeller 59025c5b96 Lazily initialize ProblemDetail for picking up actual status code
Closes gh-35829

(cherry picked from commit 3026f0a49b)
2025-11-19 17:31:27 +01:00
Sam Brannen e5de8b9bc6 Fix link to MockMvc test in HtmlUnit section
See gh-35853

(cherry picked from commit 9fe4e7798d)
2025-11-19 17:19:34 +01:00
Sam Brannen e146e809e5 Polishing
(cherry picked from commit d178930186)
2025-11-19 17:19:34 +01:00
Tran Ngoc Nhan ff62e7355f Fix cross-reference links in HtmlUnit sections
Closes gh-35853

Signed-off-by: Tran Ngoc Nhan <ngocnhan.tran1996@gmail.com>

(cherry picked from commit 91d2a51f3f)
2025-11-19 17:19:30 +01:00
Juergen Hoeller 4e97013595 Polishing
(cherry picked from commit f456674529)
2025-11-19 16:21:38 +01:00
Juergen Hoeller bd10b7ae06 Remove javadoc references to deprecated PropertiesBeanDefinitionReader
Closes gh-35836

(cherry picked from commit 35b8fbf901)
2025-11-19 16:21:33 +01:00
Juergen Hoeller e4288170c8 Fix getCacheNames() concurrent access in NoOpCacheManager
Closes gh-35842

(cherry picked from commit 57a1d4007b)
2025-11-18 13:50:13 +01:00
Juergen Hoeller 8545a759a7 Add resetCaches() method to Caffeine/ConcurrentMapCacheManager
Closes gh-35840

(cherry picked from commit bc3431f435)
2025-11-18 13:49:20 +01:00
Juergen Hoeller f94645de17 Narrow Aware interface exclusion check to BeanFactoryAware only
Closes gh-35835

(cherry picked from commit de5b9aab55)
2025-11-18 13:48:32 +01:00
Sam Brannen 8553f97df1 Merge HtmlCharacterEntityDecoderTests into HtmlUtilsTests
See gh-35711

(cherry picked from commit 0342cd0904)
2025-11-17 15:29:52 +01:00
Brian Clozel 030dace2be Polishing contribution
Closes gh-35477
2025-11-17 15:15:39 +01:00
potato 5af1c9b487 Fix HtmlUtils unescape for supplementary chars
See gh-35477

Signed-off-by: potato <65760583+juntae6942@users.noreply.github.com>
2025-11-17 15:15:39 +01:00
Patrick Strawderman f3ed04c9d7 Fix single-check idiom in UnmodifiableMultiValueMap
Read the respective fields only once in the values(), entrySet(), and
keySet() methods.

Closes gh-35822

Signed-off-by: Patrick Strawderman <pstrawderman@netflix.com>

(cherry picked from commit 3b6be3d4d3)
2025-11-17 15:04:49 +01:00
github-actions[bot] 46ee944acc Update Antora Spring UI to v0.4.20
Closes gh-35814
2025-11-17 14:22:35 +01:00
Sam Brannen 6be1c29fda Polish contribution
See gh-35817

(cherry picked from commit 09a8bbc0c7)
2025-11-17 12:28:03 +01:00
Patrick Strawderman 3fa56db88b Fix Spliterator characteristics in ConcurrentReferenceHashMap
The Spliterators returned by values, entrySet, and keySet incorrectly
reported the SIZED characteristic, instead of CONCURRENT. This could
lead to bugs when the map is concurrently modified during a stream
operation.

For keySet and values, the incorrect characteristics are inherited from
AbstractMap, so to rectify that the respective methods are overridden,
and custom collections are provided that report the correct Spliterator
characteristics.

Closes gh-35817

Signed-off-by: Patrick Strawderman <pstrawderman@netflix.com>

(cherry picked from commit ed75906834)
2025-11-17 12:27:43 +01:00
29 changed files with 603 additions and 183 deletions
+1 -1
View File
@@ -36,4 +36,4 @@ runtime:
failure_level: warn
ui:
bundle:
url: https://github.com/spring-io/antora-ui-spring/releases/download/v0.4.18/ui-bundle.zip
url: https://github.com/spring-io/antora-ui-spring/releases/download/v0.4.20/ui-bundle.zip
@@ -8,9 +8,9 @@ use https://www.gebish.org/[Geb] to make our tests even Groovy-er.
== Why Geb and MockMvc?
Geb is backed by WebDriver, so it offers many of the
xref:testing/mockmvc/htmlunit/webdriver.adoc#spring-mvc-test-server-htmlunit-webdriver-why[same benefits] that we get from
WebDriver. However, Geb makes things even easier by taking care of some of the
boilerplate code for us.
xref:testing/mockmvc/htmlunit/webdriver.adoc#mockmvc-server-htmlunit-webdriver-why[same benefits]
that we get from WebDriver. However, Geb makes things even easier by taking care of some
of the boilerplate code for us.
[[mockmvc-server-htmlunit-geb-setup]]
== MockMvc and Geb Setup
@@ -28,7 +28,8 @@ def setup() {
----
NOTE: This is a simple example of using `MockMvcHtmlUnitDriverBuilder`. For more advanced
usage, see xref:testing/mockmvc/htmlunit/webdriver.adoc#spring-mvc-test-server-htmlunit-webdriver-advanced-builder[Advanced `MockMvcHtmlUnitDriverBuilder`].
usage, see
xref:testing/mockmvc/htmlunit/webdriver.adoc#mockmvc-server-htmlunit-webdriver-advanced-builder[Advanced `MockMvcHtmlUnitDriverBuilder`].
This ensures that any URL referencing `localhost` as the server is directed to our
`MockMvc` instance without the need for a real HTTP connection. Any other URL is
@@ -62,10 +63,10 @@ forwarded to the current page object. This removes a lot of the boilerplate code
needed when using WebDriver directly.
As with direct WebDriver usage, this improves on the design of our
xref:testing/mockmvc/htmlunit/mah.adoc#spring-mvc-test-server-htmlunit-mah-usage[HtmlUnit test] by using the Page Object
Pattern. As mentioned previously, we can use the Page Object Pattern with HtmlUnit and
WebDriver, but it is even easier with Geb. Consider our new Groovy-based
`CreateMessagePage` implementation:
xref:testing/mockmvc/htmlunit/mah.adoc#mockmvc-server-htmlunit-mah-usage[HtmlUnit test]
by using the Page Object Pattern. As mentioned previously, we can use the Page Object
Pattern with HtmlUnit and WebDriver, but it is even easier with Geb. Consider our new
Groovy-based `CreateMessagePage` implementation:
[source,groovy]
----
@@ -7,8 +7,7 @@ to use the raw HtmlUnit libraries.
[[mockmvc-server-htmlunit-mah-setup]]
== MockMvc and HtmlUnit Setup
First, make sure that you have included a test dependency on
`org.htmlunit:htmlunit`.
First, make sure that you have included a test dependency on `org.htmlunit:htmlunit`.
We can easily create an HtmlUnit `WebClient` that integrates with MockMvc by using the
`MockMvcWebClientBuilder`, as follows:
@@ -45,7 +44,7 @@ Kotlin::
======
NOTE: This is a simple example of using `MockMvcWebClientBuilder`. For advanced usage,
see xref:testing/mockmvc/htmlunit/mah.adoc#spring-mvc-test-server-htmlunit-mah-advanced-builder[Advanced `MockMvcWebClientBuilder`].
see <<mockmvc-server-htmlunit-mah-advanced-builder>>.
This ensures that any URL that references `localhost` as the server is directed to our
`MockMvc` instance without the need for a real HTTP connection. Any other URL is
@@ -77,7 +76,7 @@ Kotlin::
======
NOTE: The default context path is `""`. Alternatively, we can specify the context path,
as described in xref:testing/mockmvc/htmlunit/mah.adoc#spring-mvc-test-server-htmlunit-mah-advanced-builder[Advanced `MockMvcWebClientBuilder`].
as described in <<mockmvc-server-htmlunit-mah-advanced-builder>>.
Once we have a reference to the `HtmlPage`, we can then fill out the form and submit it
to create a message, as the following example shows:
@@ -144,10 +143,10 @@ Kotlin::
======
The preceding code improves on our
xref:testing/mockmvc/htmlunit/why.adoc#spring-mvc-test-server-htmlunit-mock-mvc-test[MockMvc test] in a number of ways.
First, we no longer have to explicitly verify our form and then create a request that
looks like the form. Instead, we request the form, fill it out, and submit it, thereby
significantly reducing the overhead.
xref:testing/mockmvc/htmlunit/why.adoc#mockmvc-server-htmlunit-why[MockMvc test] in a
number of ways. First, we no longer have to explicitly verify our form and then create a
request that looks like the form. Instead, we request the form, fill it out, and submit
it, thereby significantly reducing the overhead.
Another important factor is that https://htmlunit.sourceforge.io/javascript.html[HtmlUnit
uses the Mozilla Rhino engine] to evaluate JavaScript. This means that we can also test
@@ -203,7 +203,7 @@ Kotlin::
======
NOTE: This is a simple example of using `MockMvcHtmlUnitDriverBuilder`. For more advanced
usage, see xref:testing/mockmvc/htmlunit/webdriver.adoc#spring-mvc-test-server-htmlunit-webdriver-advanced-builder[Advanced `MockMvcHtmlUnitDriverBuilder`].
usage, see <<mockmvc-server-htmlunit-webdriver-advanced-builder>>.
The preceding example ensures that any URL that references `localhost` as the server is
directed to our `MockMvc` instance without the need for a real HTTP connection. Any other
@@ -259,10 +259,11 @@ Kotlin::
======
--
This improves on the design of our xref:testing/mockmvc/htmlunit/mah.adoc#spring-mvc-test-server-htmlunit-mah-usage[HtmlUnit test]
This improves on the design of our
xref:testing/mockmvc/htmlunit/mah.adoc#mockmvc-server-htmlunit-mah-usage[HtmlUnit test]
by leveraging the Page Object Pattern. As we mentioned in
xref:testing/mockmvc/htmlunit/webdriver.adoc#mockmvc-server-htmlunit-webdriver-why[Why WebDriver and MockMvc?], we can use the Page Object Pattern
with HtmlUnit, but it is much easier with WebDriver. Consider the following
<<mockmvc-server-htmlunit-webdriver-why>>, we can use the Page Object Pattern with
HtmlUnit, but it is much easier with WebDriver. Consider the following
`CreateMessagePage` implementation:
--
@@ -60,7 +60,7 @@ assume our form looks like the following snippet:
</form>
----
How do we ensure that our form produce the correct request to create a new message? A
How do we ensure that our form produces the correct request to create a new message? A
naive attempt might resemble the following:
[tabs]
@@ -154,7 +154,7 @@ validation.
[[mockmvc-server-htmlunit-why-integration]]
== Integration Testing to the Rescue?
To resolve the issues mentioned earlier, we could perform end-to-end integration testing,
To resolve the issues mentioned above, we could perform end-to-end integration testing,
but this has some drawbacks. Consider testing the view that lets us page through the
messages. We might need the following tests:
@@ -171,7 +171,7 @@ leads to a number of additional challenges:
* Testing can become slow, since each test would need to ensure that the database is in
the correct state.
* Since our database needs to be in a specific state, we cannot run tests in parallel.
* Performing assertions on such items as auto-generated IDs, timestamps, and others can
* Performing assertions on items such as auto-generated IDs, timestamps, and others can
be difficult.
These challenges do not mean that we should abandon end-to-end integration testing
+1 -1
View File
@@ -1,4 +1,4 @@
version=6.2.14-SNAPSHOT
version=6.2.14
org.gradle.caching=true
org.gradle.jvmargs=-Xmx2048m
@@ -32,7 +32,6 @@ import org.springframework.util.StringUtils;
* @author Juergen Hoeller
* @author Rob Harrop
* @since 1.1
* @see PropertiesBeanDefinitionReader
* @see org.springframework.beans.factory.xml.DefaultBeanDefinitionDocumentReader
*/
public abstract class BeanDefinitionReaderUtils {
@@ -43,7 +43,6 @@ import org.springframework.core.AliasRegistry;
* @see DefaultListableBeanFactory
* @see org.springframework.context.support.GenericApplicationContext
* @see org.springframework.beans.factory.xml.XmlBeanDefinitionReader
* @see PropertiesBeanDefinitionReader
*/
public interface BeanDefinitionRegistry extends AliasRegistry {
@@ -74,10 +74,10 @@ import org.springframework.util.StringUtils;
* @author Rob Harrop
* @since 26.11.2003
* @see DefaultListableBeanFactory
* @deprecated as of 5.3, in favor of Spring's common bean definition formats
* and/or custom reader implementations
* @deprecated in favor of Spring's common bean definition formats and/or
* custom BeanDefinitionReader implementations
*/
@Deprecated
@Deprecated(since = "5.3")
public class PropertiesBeanDefinitionReader extends AbstractBeanDefinitionReader {
/**
@@ -77,7 +77,7 @@ public class CaffeineCacheManager implements CacheManager {
private boolean allowNullValues = true;
private boolean dynamic = true;
private volatile boolean dynamic = true;
private final Map<String, Cache> cacheMap = new ConcurrentHashMap<>(16);
@@ -102,10 +102,15 @@ public class CaffeineCacheManager implements CacheManager {
/**
* Specify the set of cache names for this CacheManager's 'static' mode.
* <p>The number of caches and their names will be fixed after a call to this method,
* with no creation of further cache regions at runtime.
* <p>Calling this with a {@code null} collection argument resets the
* mode to 'dynamic', allowing for further creation of caches again.
* <p>The number of caches and their names will be fixed after a call
* to this method, with no creation of further cache regions at runtime.
* <p>Note that this method replaces existing caches of the given names
* and prevents the creation of further cache regions from here on - but
* does <i>not</i> remove unrelated existing caches. For a full reset,
* consider calling {@link #resetCaches()} before calling this method.
* <p>Calling this method with a {@code null} collection argument resets
* the mode to 'dynamic', allowing for further creation of caches again.
* @see #resetCaches()
*/
public void setCacheNames(@Nullable Collection<String> cacheNames) {
if (cacheNames != null) {
@@ -245,11 +250,6 @@ public class CaffeineCacheManager implements CacheManager {
}
@Override
public Collection<String> getCacheNames() {
return Collections.unmodifiableSet(this.cacheMap.keySet());
}
@Override
@Nullable
public Cache getCache(String name) {
@@ -260,6 +260,33 @@ public class CaffeineCacheManager implements CacheManager {
return cache;
}
@Override
public Collection<String> getCacheNames() {
return Collections.unmodifiableSet(this.cacheMap.keySet());
}
/**
* Reset this cache manager's caches, removing them completely for on-demand
* re-creation in 'dynamic' mode, or simply clearing their entries otherwise.
* @since 6.2.14
*/
public void resetCaches() {
this.cacheMap.values().forEach(Cache::clear);
if (this.dynamic) {
this.cacheMap.keySet().retainAll(this.customCacheNames);
}
}
/**
* Remove the specified cache from this cache manager, applying to
* custom caches as well as dynamically registered caches at runtime.
* @param name the name of the cache
* @since 6.1.15
*/
public void removeCache(String name) {
this.customCacheNames.remove(name);
this.cacheMap.remove(name);
}
/**
* Register the given native Caffeine Cache instance with this cache manager,
@@ -303,16 +330,6 @@ public class CaffeineCacheManager implements CacheManager {
this.cacheMap.put(name, adaptCaffeineCache(name, cache));
}
/**
* Remove the specified cache from this cache manager, applying to
* custom caches as well as dynamically registered caches at runtime.
* @param name the name of the cache
* @since 6.1.15
*/
public void removeCache(String name) {
this.customCacheNames.remove(name);
this.cacheMap.remove(name);
}
/**
* Adapt the given new native Caffeine Cache instance to Spring's {@link Cache}
@@ -25,7 +25,7 @@ import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.aop.support.AopUtils;
import org.springframework.beans.factory.Aware;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.core.MethodClassKey;
import org.springframework.lang.Nullable;
import org.springframework.util.ReflectionUtils;
@@ -98,8 +98,8 @@ public abstract class AbstractFallbackJCacheOperationSource implements JCacheOpe
if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
return null;
}
// Skip methods declared on BeanFactoryAware and co.
if (method.getDeclaringClass().isInterface() && Aware.class.isAssignableFrom(method.getDeclaringClass())) {
// Skip setBeanFactory method on BeanFactoryAware.
if (method.getDeclaringClass() == BeanFactoryAware.class) {
return null;
}
@@ -24,7 +24,6 @@ import com.github.benmanes.caffeine.cache.CaffeineSpec;
import org.junit.jupiter.api.Test;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.cache.support.SimpleValueWrapper;
import static org.assertj.core.api.Assertions.assertThat;
@@ -42,7 +41,7 @@ class CaffeineCacheManagerTests {
@Test
@SuppressWarnings("cast")
void dynamicMode() {
CacheManager cm = new CaffeineCacheManager();
CaffeineCacheManager cm = new CaffeineCacheManager();
Cache cache1 = cm.getCache("c1");
assertThat(cache1).isInstanceOf(CaffeineCache.class);
@@ -76,6 +75,14 @@ class CaffeineCacheManagerTests {
cache1.evict("key3");
assertThat(cache1.get("key3", () -> (String) null)).isNull();
assertThat(cache1.get("key3", () -> (String) null)).isNull();
cm.removeCache("c1");
assertThat(cm.getCache("c1")).isNotSameAs(cache1);
assertThat(cm.getCache("c2")).isSameAs(cache2);
cm.resetCaches();
assertThat(cm.getCache("c1")).isNotSameAs(cache1);
assertThat(cm.getCache("c2")).isNotSameAs(cache2);
}
@Test
@@ -131,11 +138,24 @@ class CaffeineCacheManagerTests {
cm.setAllowNullValues(true);
Cache cache1y = cm.getCache("c1");
Cache cache2y = cm.getCache("c2");
cache1y.put("key3", null);
assertThat(cache1y.get("key3").get()).isNull();
cache1y.evict("key3");
assertThat(cache1y.get("key3")).isNull();
cache2y.put("key4", "value4");
assertThat(cache2y.get("key4").get()).isEqualTo("value4");
cm.removeCache("c1");
assertThat(cm.getCache("c1")).isNull();
assertThat(cm.getCache("c2")).isSameAs(cache2y);
assertThat(cache2y.get("key4").get()).isEqualTo("value4");
cm.resetCaches();
assertThat(cm.getCache("c1")).isNull();
assertThat(cm.getCache("c2")).isSameAs(cache2y);
assertThat(cache2y.get("key4")).isNull();
}
@Test
@@ -54,7 +54,7 @@ public class ConcurrentMapCacheManager implements CacheManager, BeanClassLoaderA
private final ConcurrentMap<String, Cache> cacheMap = new ConcurrentHashMap<>(16);
private boolean dynamic = true;
private volatile boolean dynamic = true;
private boolean allowNullValues = true;
@@ -82,10 +82,15 @@ public class ConcurrentMapCacheManager implements CacheManager, BeanClassLoaderA
/**
* Specify the set of cache names for this CacheManager's 'static' mode.
* <p>The number of caches and their names will be fixed after a call to this method,
* with no creation of further cache regions at runtime.
* <p>Calling this with a {@code null} collection argument resets the
* mode to 'dynamic', allowing for further creation of caches again.
* <p>The number of caches and their names will be fixed after a call
* to this method, with no creation of further cache regions at runtime.
* <p>Note that this method replaces existing caches of the given names
* and prevents the creation of further cache regions from here on - but
* does <i>not</i> remove unrelated existing caches. For a full reset,
* consider calling {@link #resetCaches()} before calling this method.
* <p>Calling this method with a {@code null} collection argument resets
* the mode to 'dynamic', allowing for further creation of caches again.
* @see #resetCaches()
*/
public void setCacheNames(@Nullable Collection<String> cacheNames) {
if (cacheNames != null) {
@@ -160,11 +165,6 @@ public class ConcurrentMapCacheManager implements CacheManager, BeanClassLoaderA
}
@Override
public Collection<String> getCacheNames() {
return Collections.unmodifiableSet(this.cacheMap.keySet());
}
@Override
@Nullable
public Cache getCache(String name) {
@@ -175,6 +175,23 @@ public class ConcurrentMapCacheManager implements CacheManager, BeanClassLoaderA
return cache;
}
@Override
public Collection<String> getCacheNames() {
return Collections.unmodifiableSet(this.cacheMap.keySet());
}
/**
* Reset this cache manager's caches, removing them completely for on-demand
* re-creation in 'dynamic' mode, or simply clearing their entries otherwise.
* @since 6.2.14
*/
public void resetCaches() {
this.cacheMap.values().forEach(Cache::clear);
if (this.dynamic) {
this.cacheMap.clear();
}
}
/**
* Remove the specified cache from this cache manager.
* @param name the name of the cache
@@ -27,7 +27,7 @@ import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.aop.support.AopUtils;
import org.springframework.beans.factory.Aware;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.core.MethodClassKey;
import org.springframework.lang.Nullable;
import org.springframework.util.ClassUtils;
@@ -140,8 +140,8 @@ public abstract class AbstractFallbackCacheOperationSource implements CacheOpera
if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
return null;
}
// Skip methods declared on BeanFactoryAware and co.
if (method.getDeclaringClass().isInterface() && Aware.class.isAssignableFrom(method.getDeclaringClass())) {
// Skip setBeanFactory method on BeanFactoryAware.
if (method.getDeclaringClass() == BeanFactoryAware.class) {
return null;
}
@@ -18,8 +18,6 @@ package org.springframework.cache.support;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
@@ -32,45 +30,29 @@ import org.springframework.lang.Nullable;
* for disabling caching, typically used for backing cache declarations
* without an actual backing store.
*
* <p>Will simply accept any items into the cache not actually storing them.
* <p>This implementation will simply accept any items into the cache,
* not actually storing them.
*
* @author Costin Leau
* @author Stephane Nicoll
* @author Juergen Hoeller
* @since 3.1
* @see NoOpCache
*/
public class NoOpCacheManager implements CacheManager {
private final ConcurrentMap<String, Cache> caches = new ConcurrentHashMap<>(16);
private final Set<String> cacheNames = new LinkedHashSet<>(16);
private final ConcurrentMap<String, Cache> cacheMap = new ConcurrentHashMap<>(16);
/**
* This implementation always returns a {@link Cache} implementation that will not store items.
* Additionally, the request cache will be remembered by the manager for consistency.
*/
@Override
@Nullable
public Cache getCache(String name) {
Cache cache = this.caches.get(name);
if (cache == null) {
this.caches.computeIfAbsent(name, NoOpCache::new);
synchronized (this.cacheNames) {
this.cacheNames.add(name);
}
}
return this.caches.get(name);
return this.cacheMap.computeIfAbsent(name, NoOpCache::new);
}
/**
* This implementation returns the name of the caches previously requested.
*/
@Override
public Collection<String> getCacheNames() {
synchronized (this.cacheNames) {
return Collections.unmodifiableSet(this.cacheNames);
}
return Collections.unmodifiableSet(this.cacheMap.keySet());
}
}
@@ -227,7 +227,6 @@ public abstract class AbstractRefreshableApplicationContext extends AbstractAppl
* @param beanFactory the bean factory to load bean definitions into
* @throws BeansException if parsing of the bean definitions failed
* @throws IOException if loading of bean definition files failed
* @see org.springframework.beans.factory.support.PropertiesBeanDefinitionReader
* @see org.springframework.beans.factory.xml.XmlBeanDefinitionReader
*/
protected abstract void loadBeanDefinitions(DefaultListableBeanFactory beanFactory)
@@ -78,8 +78,6 @@ import org.springframework.util.Assert;
* GenericApplicationContext ctx = new GenericApplicationContext();
* XmlBeanDefinitionReader xmlReader = new XmlBeanDefinitionReader(ctx);
* xmlReader.loadBeanDefinitions(new ClassPathResource("applicationContext.xml"));
* PropertiesBeanDefinitionReader propReader = new PropertiesBeanDefinitionReader(ctx);
* propReader.loadBeanDefinitions(new ClassPathResource("otherBeans.properties"));
* ctx.refresh();
*
* MyBean myBean = (MyBean) ctx.getBean("myBean");
@@ -101,7 +99,6 @@ import org.springframework.util.Assert;
* @see #registerBeanDefinition
* @see #refresh()
* @see org.springframework.beans.factory.xml.XmlBeanDefinitionReader
* @see org.springframework.beans.factory.support.PropertiesBeanDefinitionReader
*/
public class GenericApplicationContext extends AbstractApplicationContext implements BeanDefinitionRegistry {
@@ -19,7 +19,6 @@ package org.springframework.cache.concurrent;
import org.junit.jupiter.api.Test;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import static org.assertj.core.api.Assertions.assertThat;
@@ -31,7 +30,7 @@ class ConcurrentMapCacheManagerTests {
@Test
void testDynamicMode() {
CacheManager cm = new ConcurrentMapCacheManager();
ConcurrentMapCacheManager cm = new ConcurrentMapCacheManager();
Cache cache1 = cm.getCache("c1");
assertThat(cache1).isInstanceOf(ConcurrentMapCache.class);
Cache cache1again = cm.getCache("c1");
@@ -65,6 +64,14 @@ class ConcurrentMapCacheManagerTests {
assertThat(cache1.get("key3").get()).isNull();
cache1.evict("key3");
assertThat(cache1.get("key3")).isNull();
cm.removeCache("c1");
assertThat(cm.getCache("c1")).isNotSameAs(cache1);
assertThat(cm.getCache("c2")).isSameAs(cache2);
cm.resetCaches();
assertThat(cm.getCache("c1")).isNotSameAs(cache1);
assertThat(cm.getCache("c2")).isNotSameAs(cache2);
}
@Test
@@ -107,11 +114,24 @@ class ConcurrentMapCacheManagerTests {
cm.setAllowNullValues(true);
Cache cache1y = cm.getCache("c1");
Cache cache2y = cm.getCache("c2");
cache1y.put("key3", null);
assertThat(cache1y.get("key3").get()).isNull();
cache1y.evict("key3");
assertThat(cache1y.get("key3")).isNull();
cache2y.put("key4", "value4");
assertThat(cache2y.get("key4").get()).isEqualTo("value4");
cm.removeCache("c1");
assertThat(cm.getCache("c1")).isNull();
assertThat(cm.getCache("c2")).isSameAs(cache2y);
assertThat(cache2y.get("key4").get()).isEqualTo("value4");
cm.resetCaches();
assertThat(cm.getCache("c1")).isNull();
assertThat(cm.getCache("c2")).isSameAs(cache2y);
assertThat(cache2y.get("key4")).isNull();
}
@Test
@@ -20,8 +20,10 @@ import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;
import java.lang.ref.WeakReference;
import java.lang.reflect.Array;
import java.util.AbstractCollection;
import java.util.AbstractMap;
import java.util.AbstractSet;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashSet;
@@ -29,6 +31,8 @@ import java.util.Iterator;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.Spliterator;
import java.util.Spliterators;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicInteger;
@@ -104,6 +108,18 @@ public class ConcurrentReferenceHashMap<K, V> extends AbstractMap<K, V> implemen
@Nullable
private volatile Set<Map.Entry<K, V>> entrySet;
/**
* Late binding key set.
*/
@Nullable
private Set<K> keySet;
/**
* Late binding values collection.
*/
@Nullable
private Collection<V> values;
/**
* Create a new {@code ConcurrentReferenceHashMap} instance.
@@ -528,6 +544,26 @@ public class ConcurrentReferenceHashMap<K, V> extends AbstractMap<K, V> implemen
return entrySet;
}
@Override
public Set<K> keySet() {
Set<K> keySet = this.keySet;
if (keySet == null) {
keySet = new KeySet();
this.keySet = keySet;
}
return keySet;
}
@Override
public Collection<V> values() {
Collection<V> values = this.values;
if (values == null) {
values = new Values();
this.values = values;
}
return values;
}
@Nullable
private <T> T doTask(@Nullable Object key, Task<T> task) {
int hash = getHash(key);
@@ -969,7 +1005,7 @@ public class ConcurrentReferenceHashMap<K, V> extends AbstractMap<K, V> implemen
/**
* Internal entry-set implementation.
*/
private class EntrySet extends AbstractSet<Map.Entry<K, V>> {
private final class EntrySet extends AbstractSet<Map.Entry<K, V>> {
@Override
public Iterator<Map.Entry<K, V>> iterator() {
@@ -1005,13 +1041,140 @@ public class ConcurrentReferenceHashMap<K, V> extends AbstractMap<K, V> implemen
public void clear() {
ConcurrentReferenceHashMap.this.clear();
}
@Override
public Spliterator<Map.Entry<K, V>> spliterator() {
return Spliterators.spliterator(this, Spliterator.DISTINCT | Spliterator.CONCURRENT);
}
}
/**
* Internal key-set implementation.
*/
private final class KeySet extends AbstractSet<K> {
@Override
public Iterator<K> iterator() {
return new KeyIterator();
}
@Override
public int size() {
return ConcurrentReferenceHashMap.this.size();
}
@Override
public boolean isEmpty() {
return ConcurrentReferenceHashMap.this.isEmpty();
}
@Override
public void clear() {
ConcurrentReferenceHashMap.this.clear();
}
@Override
public boolean contains(Object k) {
return ConcurrentReferenceHashMap.this.containsKey(k);
}
@Override
public Spliterator<K> spliterator() {
return Spliterators.spliterator(this, Spliterator.DISTINCT | Spliterator.CONCURRENT);
}
}
/**
* Internal key iterator implementation.
*/
private final class KeyIterator implements Iterator<K> {
private final Iterator<Map.Entry<K, V>> iterator = entrySet().iterator();
@Override
public boolean hasNext() {
return this.iterator.hasNext();
}
@Override
public void remove() {
this.iterator.remove();
}
@Override
public K next() {
return this.iterator.next().getKey();
}
}
/**
* Internal values collection implementation.
*/
private final class Values extends AbstractCollection<V> {
@Override
public Iterator<V> iterator() {
return new ValueIterator();
}
@Override
public int size() {
return ConcurrentReferenceHashMap.this.size();
}
@Override
public boolean isEmpty() {
return ConcurrentReferenceHashMap.this.isEmpty();
}
@Override
public void clear() {
ConcurrentReferenceHashMap.this.clear();
}
@Override
public boolean contains(Object v) {
return ConcurrentReferenceHashMap.this.containsValue(v);
}
@Override
public Spliterator<V> spliterator() {
return Spliterators.spliterator(this, Spliterator.CONCURRENT);
}
}
/**
* Internal value iterator implementation.
*/
private final class ValueIterator implements Iterator<V> {
private final Iterator<Map.Entry<K, V>> iterator = entrySet().iterator();
@Override
public boolean hasNext() {
return this.iterator.hasNext();
}
@Override
public void remove() {
this.iterator.remove();
}
@Override
public V next() {
return this.iterator.next().getValue();
}
}
/**
* Internal entry iterator implementation.
*/
private class EntryIterator implements Iterator<Map.Entry<K, V>> {
private final class EntryIterator implements Iterator<Map.Entry<K, V>> {
private int segmentIndex;
@@ -145,26 +145,32 @@ final class UnmodifiableMultiValueMap<K,V> implements MultiValueMap<K,V>, Serial
@Override
public Set<K> keySet() {
if (this.keySet == null) {
this.keySet = Collections.unmodifiableSet(this.delegate.keySet());
Set<K> keySet = this.keySet;
if (keySet == null) {
keySet = Collections.unmodifiableSet(this.delegate.keySet());
this.keySet = keySet;
}
return this.keySet;
return keySet;
}
@Override
public Set<Entry<K, List<V>>> entrySet() {
if (this.entrySet == null) {
this.entrySet = new UnmodifiableEntrySet<>(this.delegate.entrySet());
Set<Entry<K, List<V>>> entrySet = this.entrySet;
if (entrySet == null) {
entrySet = new UnmodifiableEntrySet<>(this.delegate.entrySet());
this.entrySet = entrySet;
}
return this.entrySet;
return entrySet;
}
@Override
public Collection<List<V>> values() {
if (this.values == null) {
this.values = new UnmodifiableValueCollection<>(this.delegate.values());
Collection<List<V>> values = this.values;
if (values == null) {
values = new UnmodifiableValueCollection<>(this.delegate.values());
this.values = values;
}
return this.values;
return values;
}
// unsupported
@@ -16,8 +16,7 @@
package org.springframework.util;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
@@ -25,6 +24,8 @@ import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Spliterator;
import java.util.stream.Collectors;
import org.junit.jupiter.api.Test;
@@ -32,12 +33,13 @@ import org.springframework.lang.Nullable;
import org.springframework.util.ConcurrentReferenceHashMap.Entry;
import org.springframework.util.ConcurrentReferenceHashMap.Reference;
import org.springframework.util.ConcurrentReferenceHashMap.Restructure;
import org.springframework.util.comparator.Comparators;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
import static org.assertj.core.api.Assertions.assertThatNoException;
import static org.assertj.core.api.Assertions.entry;
/**
* Tests for {@link ConcurrentReferenceHashMap}.
@@ -47,8 +49,6 @@ import static org.assertj.core.api.Assertions.assertThatNoException;
*/
class ConcurrentReferenceHashMapTests {
private static final Comparator<? super String> NULL_SAFE_STRING_SORT = Comparators.nullsLow();
private TestWeakConcurrentCache<Integer, String> map = new TestWeakConcurrentCache<>();
@@ -450,19 +450,174 @@ class ConcurrentReferenceHashMapTests {
assertThat(this.map.keySet()).isEqualTo(expected);
}
@Test // gh-35817
void keySetContains() {
this.map.put(123, "123");
this.map.put(456, null);
this.map.put(null, "789");
assertThat(this.map.keySet()).containsExactlyInAnyOrder(123, 456, null);
}
@Test // gh-35817
void keySetRemove() {
this.map.put(123, "123");
this.map.put(456, null);
this.map.put(null, "789");
assertThat(this.map.keySet().remove(123)).isTrue();
assertThat(this.map).doesNotContainKey(123);
assertThat(this.map.keySet().remove(123)).isFalse();
}
@Test // gh-35817
void keySetIterator() {
this.map.put(123, "123");
this.map.put(456, null);
this.map.put(null, "789");
Iterator<Integer> it = this.map.keySet().iterator();
assertThat(it).toIterable().containsExactlyInAnyOrder(123, 456, null);
assertThat(it).isExhausted();
}
@Test // gh-35817
void keySetIteratorRemove() {
this.map.put(123, "123");
this.map.put(456, null);
this.map.put(null, "789");
Iterator<Integer> keySetIterator = this.map.keySet().iterator();
while (keySetIterator.hasNext()) {
Integer key = keySetIterator.next();
if (key != null && key.equals(456)) {
keySetIterator.remove();
}
}
assertThat(this.map).containsOnlyKeys(123, null);
}
@Test // gh-35817
void keySetClear() {
this.map.put(123, "123");
this.map.put(456, null);
this.map.put(null, "789");
this.map.keySet().clear();
assertThat(this.map).isEmpty();
assertThat(this.map.keySet()).isEmpty();
}
@Test // gh-35817
void keySetAdd() {
assertThatExceptionOfType(UnsupportedOperationException.class)
.isThrownBy(() -> this.map.keySet().add(12345));
}
@Test // gh-35817
void keySetStream() {
this.map.put(123, "123");
this.map.put(456, null);
this.map.put(null, "789");
Set<Integer> keys = this.map.keySet().stream().collect(Collectors.toSet());
assertThat(keys).containsExactlyInAnyOrder(123, 456, null);
}
@Test // gh-35817
void keySetSpliteratorCharacteristics() {
this.map.put(123, "123");
this.map.put(456, null);
this.map.put(null, "789");
Spliterator<Integer> spliterator = this.map.keySet().spliterator();
assertThat(spliterator).hasOnlyCharacteristics(Spliterator.CONCURRENT, Spliterator.DISTINCT);
assertThat(spliterator.estimateSize()).isEqualTo(3L);
assertThat(spliterator.getExactSizeIfKnown()).isEqualTo(-1L);
}
@Test
void valuesCollection() {
this.map.put(123, "123");
this.map.put(456, null);
this.map.put(null, "789");
List<String> actual = new ArrayList<>(this.map.values());
List<String> expected = new ArrayList<>();
expected.add("123");
expected.add(null);
expected.add("789");
actual.sort(NULL_SAFE_STRING_SORT);
expected.sort(NULL_SAFE_STRING_SORT);
assertThat(actual).isEqualTo(expected);
assertThat(this.map.values()).containsExactlyInAnyOrder("123", null, "789");
}
@Test // gh-35817
void valuesCollectionAdd() {
assertThatExceptionOfType(UnsupportedOperationException.class)
.isThrownBy(() -> this.map.values().add("12345"));
}
@Test // gh-35817
void valuesCollectionClear() {
Collection<String> values = this.map.values();
this.map.put(123, "123");
this.map.put(456, null);
this.map.put(null, "789");
assertThat(values).hasSize(3);
values.clear();
assertThat(values).isEmpty();
assertThat(this.map).isEmpty();
}
@Test // gh-35817
void valuesCollectionRemoval() {
Collection<String> values = this.map.values();
assertThat(values).isEmpty();
this.map.put(123, "123");
this.map.put(456, null);
this.map.put(null, "789");
assertThat(values).containsExactlyInAnyOrder("123", null, "789");
values.remove(null);
assertThat(values).containsExactlyInAnyOrder("123", "789");
assertThat(map).containsOnly(entry(123, "123"), entry(null, "789"));
values.remove("123");
values.remove("789");
assertThat(values).isEmpty();
assertThat(map).isEmpty();
}
@Test // gh-35817
void valuesCollectionIterator() {
Iterator<String> iterator = this.map.values().iterator();
assertThat(iterator).isExhausted();
this.map.put(123, "123");
this.map.put(456, null);
this.map.put(null, "789");
iterator = this.map.values().iterator();
assertThat(iterator).toIterable().containsExactlyInAnyOrder("123", null, "789");
}
@Test // gh-35817
void valuesCollectionIteratorRemoval() {
this.map.put(123, "123");
this.map.put(456, null);
this.map.put(null, "789");
Iterator<String> iterator = this.map.values().iterator();
while (iterator.hasNext()) {
String value = iterator.next();
if (value != null && value.equals("789")) {
iterator.remove();
}
}
assertThat(iterator).isExhausted();
assertThat(this.map.values()).containsExactlyInAnyOrder("123", null);
assertThat(this.map).containsOnlyKeys(123, 456);
}
@Test // gh-35817
void valuesCollectionStream() {
this.map.put(123, "123");
this.map.put(456, null);
this.map.put(null, "789");
List<String> values = this.map.values().stream().toList();
assertThat(values).containsExactlyInAnyOrder("123", null, "789");
}
@Test // gh-35817
void valuesCollectionSpliteratorCharacteristics() {
this.map.put(123, "123");
this.map.put(456, null);
this.map.put(null, "789");
Spliterator<String> spliterator = this.map.values().spliterator();
assertThat(spliterator).hasOnlyCharacteristics(Spliterator.CONCURRENT);
assertThat(spliterator.estimateSize()).isEqualTo(3L);
assertThat(spliterator.getExactSizeIfKnown()).isEqualTo(-1L);
}
@Test
@@ -541,6 +696,17 @@ class ConcurrentReferenceHashMapTests {
copy.forEach(entry -> assertThat(entrySet).doesNotContain(entry));
}
@Test // gh-35817
void entrySetSpliteratorCharacteristics() {
this.map.put(1, "1");
this.map.put(2, "2");
this.map.put(3, "3");
Spliterator<Map.Entry<Integer, String>> spliterator = this.map.entrySet().spliterator();
assertThat(spliterator).hasOnlyCharacteristics(Spliterator.CONCURRENT, Spliterator.DISTINCT);
assertThat(spliterator.estimateSize()).isEqualTo(3L);
assertThat(spliterator.getExactSizeIfKnown()).isEqualTo(-1L);
}
@Test
void supportNullReference() {
// GC could happen during restructure so we must be able to create a reference for a null entry
@@ -40,11 +40,10 @@ import org.springframework.util.Assert;
* @author Rod Johnson
* @author Juergen Hoeller
* @see #loadBeanDefinitions
* @see org.springframework.beans.factory.support.PropertiesBeanDefinitionReader
* @deprecated as of 5.3, in favor of Spring's common bean definition formats
* and/or custom reader implementations
* @deprecated in favor of Spring's common bean definition formats and/or
* custom BeanDefinitionReader implementations
*/
@Deprecated
@Deprecated(since = "5.3")
public class JdbcBeanDefinitionReader {
private final org.springframework.beans.factory.support.PropertiesBeanDefinitionReader propReader;
@@ -25,7 +25,7 @@ import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.aop.support.AopUtils;
import org.springframework.beans.factory.Aware;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.context.EmbeddedValueResolverAware;
import org.springframework.core.MethodClassKey;
import org.springframework.lang.Nullable;
@@ -167,8 +167,8 @@ public abstract class AbstractFallbackTransactionAttributeSource
if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
return null;
}
// Skip methods declared on BeanFactoryAware and co.
if (method.getDeclaringClass().isInterface() && Aware.class.isAssignableFrom(method.getDeclaringClass())) {
// Skip setBeanFactory method on BeanFactoryAware.
if (method.getDeclaringClass() == BeanFactoryAware.class) {
return null;
}
@@ -32,6 +32,7 @@ import org.junit.jupiter.api.Test;
import org.springframework.aop.framework.Advised;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.beans.factory.BeanNameAware;
import org.springframework.core.annotation.AliasFor;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.testfixture.io.SerializationTestUtils;
@@ -59,6 +60,7 @@ class AnnotationTransactionAttributeSourceTests {
private final AnnotationTransactionAttributeSource attributeSource = new AnnotationTransactionAttributeSource();
@Test
void serializable() throws Exception {
TestBean1 tb = new TestBean1();
@@ -123,6 +125,10 @@ class AnnotationTransactionAttributeSourceTests {
void transactionAttributeDeclaredOnInterfaceMethodOnly() {
TransactionAttribute actual = getTransactionAttribute(TestBean2.class, ITestBean2.class, "getAge");
assertThat(actual).satisfies(hasNoRollbackRule());
actual = getTransactionAttribute(TestBean2.class, ITestBean2X.class, "getAge");
assertThat(actual).satisfies(hasNoRollbackRule());
actual = getTransactionAttribute(ITestBean2X.class, ITestBean2X.class, "getAge");
assertThat(actual).satisfies(hasNoRollbackRule());
}
/**
@@ -249,6 +255,7 @@ class AnnotationTransactionAttributeSourceTests {
assertThat(actual.isReadOnly()).isTrue();
}
@Nested
class JtaAttributeTests {
@@ -276,6 +283,7 @@ class AnnotationTransactionAttributeSourceTests {
assertThat(getNameAttr.getPropagationBehavior()).isEqualTo(TransactionAttribute.PROPAGATION_SUPPORTS);
}
static class JtaAnnotatedBean1 implements ITestBean1 {
private String name;
@@ -305,7 +313,6 @@ class AnnotationTransactionAttributeSourceTests {
}
}
@jakarta.transaction.Transactional(jakarta.transaction.Transactional.TxType.SUPPORTS)
static class JtaAnnotatedBean2 implements ITestBean1 {
@@ -362,7 +369,6 @@ class AnnotationTransactionAttributeSourceTests {
}
}
@jakarta.transaction.Transactional(jakarta.transaction.Transactional.TxType.SUPPORTS)
interface ITestJta {
@@ -375,9 +381,9 @@ class AnnotationTransactionAttributeSourceTests {
void setName(String name);
}
}
@Nested
class Ejb3AttributeTests {
@@ -448,7 +454,6 @@ class AnnotationTransactionAttributeSourceTests {
}
}
@jakarta.ejb.TransactionAttribute(TransactionAttributeType.SUPPORTS)
static class Ejb3AnnotatedBean2 implements ITestBean1 {
@@ -506,6 +511,7 @@ class AnnotationTransactionAttributeSourceTests {
}
}
@Nested
class GroovyTests {
@@ -519,6 +525,7 @@ class AnnotationTransactionAttributeSourceTests {
assertThat(attributeSource.getTransactionAttribute(getMetaClassMethod, GroovyTestBean.class)).isNull();
}
@Transactional
static class GroovyTestBean implements ITestBean1, GroovyObject {
@@ -571,6 +578,7 @@ class AnnotationTransactionAttributeSourceTests {
}
}
private Consumer<TransactionAttribute> hasRollbackRules(RollbackRuleAttribute... rollbackRuleAttributes) {
return transactionAttribute -> {
RuleBasedTransactionAttribute rbta = new RuleBasedTransactionAttribute();
@@ -626,7 +634,12 @@ class AnnotationTransactionAttributeSourceTests {
}
interface ITestBean2X extends ITestBean2 {
interface ITestBean2X extends ITestBean2, BeanNameAware {
@Transactional
int getAge();
void setAge(int age);
String getName();
@@ -735,6 +748,10 @@ class AnnotationTransactionAttributeSourceTests {
this.age = age;
}
@Override
public void setBeanName(String name) {
}
@Override
public String getName() {
return name;
@@ -917,6 +934,7 @@ class AnnotationTransactionAttributeSourceTests {
}
}
@Transactional(label = {"retryable", "long-running"})
static class TestBean11 {
@@ -25,8 +25,8 @@ import org.springframework.lang.Nullable;
import org.springframework.web.ErrorResponse;
/**
* Fatal binding exception, thrown when we want to
* treat binding exceptions as unrecoverable.
* Fatal binding exception, thrown when we want to treat binding exceptions
* as unrecoverable.
*
* <p>Extends ServletException for convenient throwing in any Servlet resource
* (such as a Filter), and NestedServletException for proper root cause handling
@@ -38,13 +38,14 @@ import org.springframework.web.ErrorResponse;
@SuppressWarnings("serial")
public class ServletRequestBindingException extends ServletException implements ErrorResponse {
private final ProblemDetail body = ProblemDetail.forStatus(getStatusCode());
private final String messageDetailCode;
@Nullable
private final Object[] messageDetailArguments;
@Nullable
private ProblemDetail body;
/**
* Constructor with a message only.
@@ -108,7 +109,10 @@ public class ServletRequestBindingException extends ServletException implements
}
@Override
public ProblemDetail getBody() {
public synchronized ProblemDetail getBody() {
if (this.body == null) {
this.body = ProblemDetail.forStatus(getStatusCode());
}
return this.body;
}
@@ -124,7 +124,10 @@ class HtmlCharacterEntityDecoder {
int value = (!isHexNumberedReference ?
Integer.parseInt(getReferenceSubstring(2)) :
Integer.parseInt(getReferenceSubstring(3), 16));
this.decodedMessage.append((char) value);
if (value > Character.MAX_CODE_POINT) {
return false;
}
this.decodedMessage.appendCodePoint(value);
return true;
}
catch (NumberFormatException ex) {
@@ -78,7 +78,6 @@ class ErrorResponseExceptionTests {
@Test
void httpMediaTypeNotSupportedException() {
List<MediaType> mediaTypes =
Arrays.asList(MediaType.APPLICATION_JSON, MediaType.APPLICATION_CBOR);
@@ -96,7 +95,6 @@ class ErrorResponseExceptionTests {
@Test
void httpMediaTypeNotSupportedExceptionWithParseError() {
ErrorResponse ex = new HttpMediaTypeNotSupportedException(
"Could not parse Accept header: Invalid mime type \"foo\": does not contain '/'");
@@ -109,7 +107,6 @@ class ErrorResponseExceptionTests {
@Test
void httpMediaTypeNotAcceptableException() {
List<MediaType> mediaTypes = Arrays.asList(MediaType.APPLICATION_JSON, MediaType.APPLICATION_CBOR);
HttpMediaTypeNotAcceptableException ex = new HttpMediaTypeNotAcceptableException(mediaTypes);
@@ -123,7 +120,6 @@ class ErrorResponseExceptionTests {
@Test
void httpMediaTypeNotAcceptableExceptionWithParseError() {
ErrorResponse ex = new HttpMediaTypeNotAcceptableException(
"Could not parse Accept header: Invalid mime type \"foo\": does not contain '/'");
@@ -136,7 +132,6 @@ class ErrorResponseExceptionTests {
@Test
void asyncRequestTimeoutException() {
ErrorResponse ex = new AsyncRequestTimeoutException();
assertDetailMessageCode(ex, null, null);
@@ -148,7 +143,6 @@ class ErrorResponseExceptionTests {
@Test
void httpRequestMethodNotSupportedException() {
HttpRequestMethodNotSupportedException ex =
new HttpRequestMethodNotSupportedException("PUT", Arrays.asList("GET", "POST"));
@@ -162,7 +156,6 @@ class ErrorResponseExceptionTests {
@Test
void missingRequestHeaderException() {
MissingRequestHeaderException ex = new MissingRequestHeaderException("Authorization", this.methodParameter);
assertStatus(ex, HttpStatus.BAD_REQUEST);
@@ -174,7 +167,6 @@ class ErrorResponseExceptionTests {
@Test
void missingServletRequestParameterException() {
MissingServletRequestParameterException ex = new MissingServletRequestParameterException("query", "String");
assertStatus(ex, HttpStatus.BAD_REQUEST);
@@ -186,10 +178,8 @@ class ErrorResponseExceptionTests {
@Test
void missingMatrixVariableException() {
MissingMatrixVariableException ex = new MissingMatrixVariableException("region", this.methodParameter);
assertStatus(ex, HttpStatus.BAD_REQUEST);
assertDetail(ex, "Required path parameter 'region' is not present.");
assertDetailMessageCode(ex, null, new Object[] {ex.getVariableName()});
@@ -199,7 +189,6 @@ class ErrorResponseExceptionTests {
@Test
void missingPathVariableException() {
MissingPathVariableException ex = new MissingPathVariableException("id", this.methodParameter);
assertStatus(ex, HttpStatus.INTERNAL_SERVER_ERROR);
@@ -210,8 +199,18 @@ class ErrorResponseExceptionTests {
}
@Test
void missingRequestCookieException() {
void missingPathVariableExceptionAfterConversion() {
MissingPathVariableException ex = new MissingPathVariableException("id", this.methodParameter, true);
assertStatus(ex, HttpStatus.BAD_REQUEST);
assertDetail(ex, "Required path variable 'id' is not present.");
assertDetailMessageCode(ex, null, new Object[] {ex.getVariableName()});
assertThat(ex.getHeaders().isEmpty()).isTrue();
}
@Test
void missingRequestCookieException() {
MissingRequestCookieException ex = new MissingRequestCookieException("oreo", this.methodParameter);
assertStatus(ex, HttpStatus.BAD_REQUEST);
@@ -223,7 +222,6 @@ class ErrorResponseExceptionTests {
@Test
void unsatisfiedServletRequestParameterException() {
UnsatisfiedServletRequestParameterException ex = new UnsatisfiedServletRequestParameterException(
new String[] { "foo=bar", "bar=baz" }, Collections.singletonMap("q", new String[] {"1"}));
@@ -236,7 +234,6 @@ class ErrorResponseExceptionTests {
@Test
void missingServletRequestPartException() {
MissingServletRequestPartException ex = new MissingServletRequestPartException("file");
assertStatus(ex, HttpStatus.BAD_REQUEST);
@@ -248,7 +245,6 @@ class ErrorResponseExceptionTests {
@Test
void methodArgumentNotValidException() {
ValidationTestHelper testHelper = new ValidationTestHelper(MethodArgumentNotValidException.class);
BindingResult result = testHelper.bindingResult();
@@ -280,7 +276,6 @@ class ErrorResponseExceptionTests {
@Test
void unsupportedMediaTypeStatusException() {
List<MediaType> mediaTypes =
Arrays.asList(MediaType.APPLICATION_JSON, MediaType.APPLICATION_CBOR);
@@ -298,7 +293,6 @@ class ErrorResponseExceptionTests {
@Test
void unsupportedMediaTypeStatusExceptionWithParseError() {
ErrorResponse ex = new UnsupportedMediaTypeStatusException(
"Could not parse Accept header: Invalid mime type \"foo\": does not contain '/'");
@@ -311,7 +305,6 @@ class ErrorResponseExceptionTests {
@Test
void notAcceptableStatusException() {
List<MediaType> mediaTypes = Arrays.asList(MediaType.APPLICATION_JSON, MediaType.APPLICATION_CBOR);
NotAcceptableStatusException ex = new NotAcceptableStatusException(mediaTypes);
@@ -325,7 +318,6 @@ class ErrorResponseExceptionTests {
@Test
void notAcceptableStatusExceptionWithParseError() {
ErrorResponse ex = new NotAcceptableStatusException(
"Could not parse Accept header: Invalid mime type \"foo\": does not contain '/'");
@@ -338,7 +330,6 @@ class ErrorResponseExceptionTests {
@Test
void serverErrorException() {
ServerErrorException ex = new ServerErrorException("Failure", null);
assertStatus(ex, HttpStatus.INTERNAL_SERVER_ERROR);
@@ -350,7 +341,6 @@ class ErrorResponseExceptionTests {
@Test
void missingRequestValueException() {
MissingRequestValueException ex =
new MissingRequestValueException("foo", String.class, "header", this.methodParameter);
@@ -363,7 +353,6 @@ class ErrorResponseExceptionTests {
@Test
void unsatisfiedRequestParameterException() {
UnsatisfiedRequestParameterException ex =
new UnsatisfiedRequestParameterException(
Arrays.asList("foo=bar", "bar=baz"),
@@ -378,7 +367,6 @@ class ErrorResponseExceptionTests {
@Test
void webExchangeBindException() {
ValidationTestHelper testHelper = new ValidationTestHelper(WebExchangeBindException.class);
BindingResult result = testHelper.bindingResult();
@@ -393,7 +381,6 @@ class ErrorResponseExceptionTests {
@Test
void methodNotAllowedException() {
List<HttpMethod> supportedMethods = Arrays.asList(HttpMethod.GET, HttpMethod.POST);
MethodNotAllowedException ex = new MethodNotAllowedException(HttpMethod.PUT, supportedMethods);
@@ -407,7 +394,6 @@ class ErrorResponseExceptionTests {
@Test
void methodNotAllowedExceptionWithoutSupportedMethods() {
MethodNotAllowedException ex = new MethodNotAllowedException(HttpMethod.PUT, Collections.emptyList());
assertStatus(ex, HttpStatus.METHOD_NOT_ALLOWED);
@@ -417,9 +403,8 @@ class ErrorResponseExceptionTests {
assertThat(ex.getHeaders()).isEmpty();
}
@Test // gh-30300
@Test // gh-30300
void responseStatusException() {
Locale locale = Locale.UK;
LocaleContextHolder.setLocale(locale);
@@ -519,7 +504,6 @@ class ErrorResponseExceptionTests {
assertThat(BindErrorUtils.resolve(errors, this.messageSource, Locale.UK)).hasSize(4)
.containsValues("Bean A message", "Bean B message", "name is required", "age is below minimum");
}
}
}
@@ -21,6 +21,8 @@ import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link HtmlUtils}.
*
* @author Alef Arendsen
* @author Martin Kersten
* @author Rick Evans
@@ -28,7 +30,7 @@ import static org.assertj.core.api.Assertions.assertThat;
class HtmlUtilsTests {
@Test
void testHtmlEscape() {
void htmlEscape() {
String unescaped = "\"This is a quote'";
String escaped = HtmlUtils.htmlEscape(unescaped);
assertThat(escaped).isEqualTo("&quot;This is a quote&#39;");
@@ -39,14 +41,7 @@ class HtmlUtilsTests {
}
@Test
void testHtmlUnescape() {
String escaped = "&quot;This is a quote&#39;";
String unescaped = HtmlUtils.htmlUnescape(escaped);
assertThat(unescaped).isEqualTo("\"This is a quote'");
}
@Test
void testEncodeIntoHtmlCharacterSet() {
void htmlEscapeIntoHtmlCharacterSet() {
assertThat(HtmlUtils.htmlEscape("")).as("An empty string should be converted to an empty string").isEmpty();
assertThat(HtmlUtils.htmlEscape("A sentence containing no special characters.")).as("A string containing no special characters should not be affected").isEqualTo("A sentence containing no special characters.");
@@ -60,12 +55,11 @@ class HtmlUtilsTests {
assertThat(HtmlUtils.htmlEscapeDecimal("" + (char) 977)).as("The special character 977 should be encoded to '&#977;'").isEqualTo("&#977;");
}
// SPR-9293
@Test
void testEncodeIntoHtmlCharacterSetFromUtf8() {
@Test // SPR-9293
void htmlEscapeIntoHtmlCharacterSetFromUtf8() {
String utf8 = ("UTF-8");
assertThat(HtmlUtils.htmlEscape("", utf8)).as("An empty string should be converted to an empty string")
.isEmpty();
assertThat(HtmlUtils.htmlEscape("", utf8)).as("An empty string should be converted to an empty string").isEmpty();
assertThat(HtmlUtils.htmlEscape("A sentence containing no special characters.")).as("A string containing no special characters should not be affected").isEqualTo("A sentence containing no special characters.");
assertThat(HtmlUtils.htmlEscape("< >", utf8)).as("'< >' should be encoded to '&lt; &gt;'").isEqualTo("&lt; &gt;");
@@ -75,7 +69,38 @@ class HtmlUtilsTests {
}
@Test
void testDecodeFromHtmlCharacterSet() {
void htmlUnescape() {
String escaped = "&quot;This is a quote&#39;";
String unescaped = HtmlUtils.htmlUnescape(escaped);
assertThat(unescaped).isEqualTo("\"This is a quote'");
}
@Test
void htmlUnescapeHandlesSupplementaryCharactersAsDecimal() {
String expectedCharacter = "😀";
String decimalEntity = "&#128512;";
String actualResultFromDecimal = HtmlUtils.htmlUnescape(decimalEntity);
assertThat(actualResultFromDecimal).as("Decimal entity was not converted correctly.").isEqualTo(expectedCharacter);
}
@Test
void htmlUnescapeHandlesSupplementaryCharactersAsHexadecimal() {
String expectedCharacter = "😀";
String hexEntity = "&#x1F600;";
String actualResultFromHex = HtmlUtils.htmlUnescape(hexEntity);
assertThat(actualResultFromHex).as("Hexadecimal entity was not converted correctly.").isEqualTo(expectedCharacter);
}
@Test
void htmlUnescapeHandlesBasicEntities() {
String input = "&lt;p&gt;Tom &amp; Jerry&#39;s &quot;Show&quot;&lt;/p&gt;";
String expectedOutput = "<p>Tom & Jerry's \"Show\"</p>";
String actualOutput = HtmlUtils.htmlUnescape(input);
assertThat(actualOutput).as("Basic HTML entities were not unescaped correctly.").isEqualTo(expectedOutput);
}
@Test
void htmlUnescapeFromHtmlCharacterSet() {
assertThat(HtmlUtils.htmlUnescape("")).as("An empty string should be converted to an empty string").isEmpty();
assertThat(HtmlUtils.htmlUnescape("This is a sentence containing no special characters.")).as("A string containing no special characters should not be affected").isEqualTo("This is a sentence containing no special characters.");
@@ -128,6 +128,7 @@
<suppress files="PatternParseException" checks="JavadocVariable"/>
<suppress files="web[\\/]reactive[\\/]socket[\\/]CloseStatus" checks="JavadocStyle"/>
<suppress files="RestClientResponseException" checks="MutableException"/>
<suppress files="ServletRequestBindingException" checks="MutableException"/>
<!-- spring-webflux -->
<suppress files="src[\\/]test[\\/]java[\\/]org[\\/]springframework[\\/]web[\\/]reactive[\\/]resource[\\/]GzipSupport" checks="IllegalImport" id="bannedJUnitJupiterImports"/>