Compare commits

..

39 Commits

Author SHA1 Message Date
Brian Clozel b038beb854 Release v7.0.1 2025-11-20 09:57:36 +01:00
Sam Brannen abec289e9f Stop mentioning non-existent NestedServletException
NestedServletException has been removed from the framework.
2025-11-19 17:32:19 +01:00
Juergen Hoeller 3026f0a49b Lazily initialize ProblemDetail for picking up actual status code
Closes gh-35829
2025-11-19 17:21:32 +01:00
Sam Brannen 9fe4e7798d Fix link to MockMvc test in HtmlUnit section
See gh-35853
2025-11-19 17:17:50 +01:00
Sam Brannen d178930186 Polishing 2025-11-19 17:11:17 +01:00
Tran Ngoc Nhan 91d2a51f3f Fix cross-reference links in HtmlUnit sections
Closes gh-35853

Signed-off-by: Tran Ngoc Nhan <ngocnhan.tran1996@gmail.com>
2025-11-19 16:53:49 +01:00
Juergen Hoeller f456674529 Polishing 2025-11-19 16:19:14 +01:00
Juergen Hoeller 35b8fbf901 Remove javadoc references to deprecated PropertiesBeanDefinitionReader
Closes gh-35836
2025-11-19 16:18:56 +01:00
Sébastien Deleuze 1d1851f48a Refine RestOperations Kotlin extensions nullability
This commit aligns RestOperationsExtensions.kt nullability with the
Java APIs one, like what has been done in gh-35846 for JdbcOperations.

Closes gh-35852
2025-11-19 16:08:51 +01:00
Sébastien Deleuze 23f0cfb925 Fix JdbcOperations Kotlin extensions
This commit updates JdbcOperationsExtensions.kt to:
 - Properly use the spread operator for invoking Java methods with
   a varargs parameter
 - Align JdbcOperationsExtensions return values nullability
   with the Java API (breaking change)
 - Use varargs where Java counterpart does (breaking change, undo some
   changes from gh-34668)
 - Use nullable args instead of non-nullable ones

 Closes gh-35846
2025-11-19 15:55:00 +01:00
Brian Clozel 2cca56064e Restrict "validateMultiReleaseJar" task to Java 25+
This commit configures the "validateMultiReleaseJar" Gradle task only if
the current Java runtime for Gradle is 25+.

Closes gh-35850
2025-11-19 13:55:27 +01:00
Yanming Zhou e0353fe75f Fix buildSrc:test failing on JDKs before JDK 25 with Non-English locale
See https://github.com/spring-projects/spring-framework/issues/35777#issuecomment-3551591336

Signed-off-by: Yanming Zhou <zhouyanming@gmail.com>
2025-11-19 17:43:12 +08:00
rstoyanchev f80b79bc45 Configure JsonPath in RestTestClient with MappingProvider
Closes gh-35793
2025-11-18 15:46:57 +00:00
rstoyanchev adffd3dcf6 Fix JsonConverterDelegate initialization in RestTestClient
If the RestClient was built with default message converters, then
in mutate, the saved builder also has 0 converters, and adding a
interferes with default registrations.

We need to check if there are no converters at all, and if so
use the default registrations.

See gh-35793
2025-11-18 15:46:57 +00:00
rstoyanchev 284a28659d Add isEmpty() to HttpMessageConverters
See gh-35793
2025-11-18 15:46:57 +00:00
Juergen Hoeller 57a1d4007b Fix getCacheNames() concurrent access in NoOpCacheManager
Closes gh-35842
2025-11-18 13:37:39 +01:00
Juergen Hoeller bc3431f435 Add resetCaches() method to Caffeine/ConcurrentMapCacheManager
Closes gh-35840
2025-11-18 13:37:14 +01:00
Juergen Hoeller de5b9aab55 Narrow Aware interface exclusion check to BeanFactoryAware only
Closes gh-35835
2025-11-18 13:36:29 +01:00
Sébastien Deleuze f72891c24d Upgrade to Gradle 9.2.1
Closes gh-35839
2025-11-18 10:13:56 +01:00
Sam Brannen fac8708857 Migrate CronTriggerTests to @⁠ParameterizedClass
This commit migrates CronTriggerTests to @⁠ParameterizedClass as a proof
of concept for how this can be applied across the code base.

See gh-35833
2025-11-17 16:36:57 +01:00
Sam Brannen 0342cd0904 Merge HtmlCharacterEntityDecoderTests into HtmlUtilsTests
See gh-35711
2025-11-17 15:27:51 +01:00
Brian Clozel e0fd42981a Fix compilation warnings
See gh-35711
2025-11-17 15:24:44 +01:00
Brian Clozel 87d95dc30a Polishing contribution
Closes gh-35477
2025-11-17 15:07:48 +01:00
potato 47de8b05e6 Fix HtmlUtils unescape for supplementary chars
See gh-35477

Signed-off-by: potato <65760583+juntae6942@users.noreply.github.com>
2025-11-17 15:07:42 +01:00
Patrick Strawderman 3b6be3d4d3 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>
2025-11-17 15:02:53 +01:00
github-actions[bot] 6115c3966c Update Antora Spring UI to v0.4.20
Closes gh-35813
2025-11-17 14:23:26 +01:00
Sam Brannen 09a8bbc0c7 Polish contribution
See gh-35817
2025-11-17 12:20:12 +01:00
Patrick Strawderman ed75906834 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>
2025-11-17 11:55:38 +01:00
Sébastien Deleuze 4756a4be23 Polishing
See gh-35820
2025-11-17 11:33:56 +01:00
Tran Ngoc Nhan 40c5c5d5f0 Add Kotlin code samples for KT-22208
Closes gh-35820
Signed-off-by: Tran Ngoc Nhan <ngocnhan.tran1996@gmail.com>
2025-11-17 11:33:56 +01:00
Sébastien Deleuze 6b8552b4c1 Refine ParameterizedPreparedStatementSetter nullability
This commit refines ParameterizedPreparedStatementSetter nullability
to ensure consistency with JdbcTemplate#batchUpdate.

Closes gh-35749
2025-11-17 10:31:56 +01:00
Brian Clozel f42eea183e Polishing tests
Closes gh-35678
2025-11-17 09:39:50 +01:00
Sam Brannen ebcfe113d1 Disable flaky failWhileSendingMultipartRequest() test
See gh-35678
2025-11-15 15:55:10 +01:00
Sam Brannen b133c2b346 Document semantics of SpringExtension.getApplicationContext()
Since getApplicationContext() was originally not intended to be part of
the public API, its Javadoc is intentionally sparse. However, since it
is actually a public API used by third parties, this commit improves the
documentation for getApplicationContext() by pointing out that invoking
the method actually results in the context being eagerly loaded, which
may not be desired.

This commit also updates the Javadoc for supportsParameter() along the
same lines.

Closes gh-35764
2025-11-15 15:44:21 +01:00
Sam Brannen 648629fab2 Polish Javadoc for SpringExtension 2025-11-15 15:42:32 +01:00
Sam Brannen f2bf227cd0 Upgrade to testng-engine 1.1.0 2025-11-15 12:42:24 +01:00
Brian Clozel 1260081436 Add WebClient integration test for multipart
This commit adds an integration test for `WebClient`, specifically
testing that a failure happening while pulishing the request body is
reported on the main reactive pipeline.

See gh-35678
2025-11-14 16:34:15 +01:00
Tran Ngoc Nhan 18d8d458e3 Fix typos in tests
Closes gh-35815

Signed-off-by: Tran Ngoc Nhan <ngocnhan.tran1996@gmail.com>
2025-11-13 13:43:54 +01:00
Brian Clozel 1aa520162b Next development version (v7.0.1-SNAPSHOT) 2025-11-13 11:33:12 +01:00
54 changed files with 1005 additions and 545 deletions
@@ -18,6 +18,7 @@ package org.springframework.build.multirelease;
import javax.inject.Inject;
import org.gradle.api.JavaVersion;
import org.gradle.api.Plugin;
import org.gradle.api.Project;
import org.gradle.api.artifacts.ConfigurationContainer;
@@ -64,10 +65,12 @@ public class MultiReleaseJarPlugin implements Plugin<Project> {
dependencies,
objects);
TaskProvider<MultiReleaseJarValidateTask> validateJarTask = tasks.register(VALIDATE_JAR_TASK_NAME, MultiReleaseJarValidateTask.class, (task) -> {
task.getJar().set(tasks.named("jar", Jar.class).flatMap(AbstractArchiveTask::getArchiveFile));
task.getJavaLauncher().set(task.getJavaToolchainService().launcherFor(spec -> spec.getLanguageVersion().set(JavaLanguageVersion.of(25))));
});
tasks.named("check", task -> task.dependsOn(validateJarTask));
if (JavaVersion.current().isCompatibleWith(JavaVersion.VERSION_25)) {
TaskProvider<MultiReleaseJarValidateTask> validateJarTask = tasks.register(VALIDATE_JAR_TASK_NAME, MultiReleaseJarValidateTask.class, (task) -> {
task.getJar().set(tasks.named("jar", Jar.class).flatMap(AbstractArchiveTask::getArchiveFile));
task.getJavaLauncher().set(task.getJavaToolchainService().launcherFor(spec -> spec.getLanguageVersion().set(JavaLanguageVersion.of(25))));
});
tasks.named("check", task -> task.dependsOn(validateJarTask));
}
}
}
@@ -29,6 +29,8 @@ import org.gradle.testkit.runner.GradleRunner;
import org.gradle.testkit.runner.UnexpectedBuildFailure;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.DisabledForJreRange;
import org.junit.jupiter.api.condition.JRE;
import org.junit.jupiter.api.io.TempDir;
import static org.assertj.core.api.Assertions.assertThat;
@@ -43,10 +45,13 @@ public class MultiReleaseJarPluginTests {
private File buildFile;
private File propertiesFile;
@BeforeEach
void setup(@TempDir File projectDir) {
this.projectDir = projectDir;
this.buildFile = new File(this.projectDir, "build.gradle");
this.propertiesFile = new File(this.projectDir, "gradle.properties");
}
@Test
@@ -119,6 +124,7 @@ public class MultiReleaseJarPluginTests {
}
@Test
@DisabledForJreRange(max = JRE.JAVA_24, disabledReason = "'jar --validate' is available as of Java 25")
void validateJar() throws IOException {
writeBuildFile("""
plugins {
@@ -131,6 +137,9 @@ public class MultiReleaseJarPluginTests {
}
multiRelease { releaseVersions 17 }
""");
writeGradleProperties("""
org.gradle.jvmargs=-Duser.language=en
""");
writeClass("src/main/java17", "Main.java", """
public class Main {
@@ -152,6 +161,12 @@ public class MultiReleaseJarPluginTests {
}
}
private void writeGradleProperties(String properties) throws IOException {
try (PrintWriter out = new PrintWriter(new FileWriter(this.propertiesFile))) {
out.print(properties);
}
}
private void writeClass(String path, String fileName, String fileContent) throws IOException {
Path folder = this.projectDir.toPath().resolve(path);
Files.createDirectories(folder);
+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.19/ui-bundle.zip
url: https://github.com/spring-io/antora-ui-spring/releases/download/v0.4.20/ui-bundle.zip
@@ -184,7 +184,10 @@ Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
// Not possible in Kotlin until {kotlin-issues}/KT-22208 is fixed
standaloneSetup(SimpleController())
.alwaysExpect<StandaloneMockMvcBuilder>(status().isOk())
.alwaysExpect<StandaloneMockMvcBuilder>(content().contentType("application/json;charset=UTF-8"))
.build()
----
======
@@ -18,7 +18,7 @@ Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
// Not possible in Kotlin until {kotlin-issues}/KT-22208 is fixed
mockMvc = standaloneSetup(PersonController()).addFilters<StandaloneMockMvcBuilder>(CharacterEncodingFilter()).build()
----
======
@@ -159,7 +159,18 @@ Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
// Not possible in Kotlin until {kotlin-issues}/KT-22208 is fixed
class MyWebTests {
lateinit var mockMvc: MockMvc
@BeforeEach
fun setup() {
mockMvc = standaloneSetup(AccountController())
.defaultRequest<StandaloneMockMvcBuilder>(get("/")
.contextPath("/app").servletPath("/main")
.accept(MediaType.APPLICATION_JSON)).build()
}
}
----
======
@@ -25,7 +25,13 @@ Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
// Not possible in Kotlin until {kotlin-issues}/KT-22208 is fixed
// static import of MockMvcBuilders.standaloneSetup
val mockMvc = standaloneSetup(MusicController())
.defaultRequest<StandaloneMockMvcBuilder>(get("/").accept(MediaType.APPLICATION_JSON))
.alwaysExpect<StandaloneMockMvcBuilder>(status().isOk())
.alwaysExpect<StandaloneMockMvcBuilder>(content().contentType("application/json;charset=UTF-8"))
.build()
----
======
@@ -53,7 +59,13 @@ Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
// Not possible in Kotlin until {kotlin-issues}/KT-22208 is fixed
// static import of SharedHttpSessionConfigurer.sharedHttpSession
val mockMvc = MockMvcBuilders.standaloneSetup(TestController())
.apply<StandaloneMockMvcBuilder>(sharedHttpSession())
.build()
// Use mockMvc to perform requests...
----
======
@@ -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
@@ -267,7 +266,19 @@ Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
// Not possible in Kotlin until {kotlin-issues}/KT-22208 is fixed
val mockMvc = MockMvcBuilders
.webAppContextSetup(context)
.apply<DefaultMockMvcBuilder>(springSecurity())
.build()
webClient = MockMvcWebClientBuilder
.mockMvcSetup(mockMvc)
// for illustration only - defaults to ""
.contextPath("")
// By default MockMvc is used for localhost only;
// the following will use MockMvc for example.com and example.org as well
.useMockMvcForHosts("example.com", "example.org")
.build()
----
======
@@ -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:
--
@@ -562,7 +563,19 @@ Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
// Not possible in Kotlin until {kotlin-issues}/KT-22208 is fixed
val mockMvc: MockMvc = MockMvcBuilders
.webAppContextSetup(context)
.apply<DefaultMockMvcBuilder>(springSecurity())
.build()
driver = MockMvcHtmlUnitDriverBuilder
.mockMvcSetup(mockMvc)
// for illustration only - defaults to ""
.contextPath("")
// By default MockMvc is used for localhost only;
// the following will use MockMvc for example.com and example.org as well
.useMockMvcForHosts("example.com", "example.org")
.build()
----
======
@@ -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
@@ -129,7 +129,7 @@ dependencies {
api("org.jboss.logging:jboss-logging:3.6.1.Final")
api("org.jruby:jruby:10.0.2.0")
api("org.jspecify:jspecify:1.0.0")
api("org.junit.support:testng-engine:1.0.5")
api("org.junit.support:testng-engine:1.1.0")
api("org.mozilla:rhino:1.7.15")
api("org.ogce:xpp3:1.1.6")
api("org.python:jython-standalone:2.7.4")
+1 -1
View File
@@ -1,4 +1,4 @@
version=7.0.0
version=7.0.1
org.gradle.caching=true
org.gradle.jvmargs=-Xmx2048m
+1 -1
View File
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.0-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
@@ -33,7 +33,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 {
@@ -76,7 +76,7 @@ import org.springframework.util.StringUtils;
* @since 26.11.2003
* @see DefaultListableBeanFactory
* @deprecated in favor of Spring's common bean definition formats and/or
* custom reader implementations
* custom BeanDefinitionReader implementations
*/
@Deprecated(since = "5.3")
public class PropertiesBeanDefinitionReader extends AbstractBeanDefinitionReader {
@@ -46,14 +46,14 @@ class YamlMapFactoryBeanTests {
@Test
void testSetIgnoreResourceNotFound() {
this.factory.setResolutionMethod(YamlMapFactoryBean.ResolutionMethod.OVERRIDE_AND_IGNORE);
this.factory.setResources(new FileSystemResource("non-exsitent-file.yml"));
this.factory.setResources(new FileSystemResource("non-existent-file.yml"));
assertThat(this.factory.getObject()).isEmpty();
}
@Test
void testSetBarfOnResourceNotFound() {
assertThatIllegalStateException().isThrownBy(() -> {
this.factory.setResources(new FileSystemResource("non-exsitent-file.yml"));
this.factory.setResources(new FileSystemResource("non-existent-file.yml"));
this.factory.getObject().size();
});
}
@@ -51,7 +51,7 @@ class MetadataAttachmentTests {
void metadataIsInherited() {
BeanDefinition beanDefinition = this.beanFactory.getMergedBeanDefinition("testBean2");
assertThat(beanDefinition.getAttribute("foo")).as("Metadata not inherited").isEqualTo("bar");
assertThat(beanDefinition.getAttribute("abc")).as("Child metdata not attached").isEqualTo("123");
assertThat(beanDefinition.getAttribute("abc")).as("Child metadata not attached").isEqualTo("123");
}
@Test
@@ -76,7 +76,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);
@@ -101,10 +101,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) {
@@ -244,11 +249,6 @@ public class CaffeineCacheManager implements CacheManager {
}
@Override
public Collection<String> getCacheNames() {
return Collections.unmodifiableSet(this.cacheMap.keySet());
}
@Override
public @Nullable Cache getCache(String name) {
Cache cache = this.cacheMap.get(name);
@@ -258,6 +258,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,
@@ -301,16 +328,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}
@@ -26,7 +26,7 @@ import org.apache.commons.logging.LogFactory;
import org.jspecify.annotations.Nullable;
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.util.ReflectionUtils;
@@ -95,8 +95,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
@@ -55,7 +55,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
public @Nullable Cache getCache(String name) {
Cache cache = this.cacheMap.get(name);
@@ -174,6 +174,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
@@ -28,7 +28,7 @@ import org.apache.commons.logging.LogFactory;
import org.jspecify.annotations.Nullable;
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.util.ClassUtils;
import org.springframework.util.CollectionUtils;
@@ -137,8 +137,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;
@@ -33,44 +31,28 @@ import org.springframework.cache.CacheManager;
* 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
public @Nullable 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());
}
}
@@ -225,7 +225,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)
@@ -81,8 +81,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");
@@ -104,7 +102,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
@@ -92,7 +92,7 @@ class CommonAnnotationBeanRegistrationAotContributionTests {
@Test
@CompileWithForkedClassLoader
void contributeWhenPackagePrivateFieldInjectionInjectsUsingFieldAssignement() {
void contributeWhenPackagePrivateFieldInjectionInjectsUsingFieldAssignment() {
this.beanFactory.registerSingleton("one", "1");
this.beanFactory.registerSingleton("two", "2");
RegisteredBean registeredBean = getAndApplyContribution(
@@ -16,19 +16,16 @@
package org.springframework.scheduling.support;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.List;
import java.util.TimeZone;
import java.util.stream.Stream;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedClass;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.params.provider.FieldSource;
import org.springframework.scheduling.TriggerContext;
@@ -44,51 +41,55 @@ import static org.junit.jupiter.params.provider.Arguments.arguments;
* @author Juergen Hoeller
* @author Sam Brannen
*/
@ParameterizedClass
@FieldSource("parameters")
@SuppressWarnings("deprecation")
class CronTriggerTests {
private final Calendar calendar = new GregorianCalendar();
static List<Arguments> parameters = List.of(
arguments(new Date(), TimeZone.getTimeZone("PST")),
arguments(new Date(), TimeZone.getTimeZone("CET")));
private void setup(Date localDateTime, TimeZone timeZone) {
private final Calendar calendar = new GregorianCalendar();
private final Date localDateTime;
private final TimeZone timeZone;
CronTriggerTests(Date localDateTime, TimeZone timeZone) {
this.calendar.setTime(localDateTime);
this.calendar.setTimeZone(timeZone);
roundup(this.calendar);
this.localDateTime = localDateTime;
this.timeZone = timeZone;
}
@ParameterizedCronTriggerTest
void matchAll(Date localDateTime, TimeZone timeZone) {
setup(localDateTime, timeZone);
@Test
void matchAll() {
CronTrigger trigger = new CronTrigger("* * * * * *", timeZone);
TriggerContext context = getTriggerContext(localDateTime);
assertThat(trigger.nextExecutionTime(context)).isEqualTo(this.calendar.getTime());
}
@ParameterizedCronTriggerTest
void matchLastSecond(Date localDateTime, TimeZone timeZone) {
setup(localDateTime, timeZone);
@Test
void matchLastSecond() {
CronTrigger trigger = new CronTrigger("* * * * * *", timeZone);
GregorianCalendar calendar = new GregorianCalendar();
calendar.set(Calendar.SECOND, 58);
assertMatchesNextSecond(trigger, calendar);
}
@ParameterizedCronTriggerTest
void matchSpecificSecond(Date localDateTime, TimeZone timeZone) {
setup(localDateTime, timeZone);
@Test
void matchSpecificSecond() {
CronTrigger trigger = new CronTrigger("10 * * * * *", timeZone);
GregorianCalendar calendar = new GregorianCalendar();
calendar.set(Calendar.SECOND, 9);
assertMatchesNextSecond(trigger, calendar);
}
@ParameterizedCronTriggerTest
void incrementSecondByOne(Date localDateTime, TimeZone timeZone) {
setup(localDateTime, timeZone);
@Test
void incrementSecondByOne() {
CronTrigger trigger = new CronTrigger("11 * * * * *", timeZone);
this.calendar.set(Calendar.SECOND, 10);
Date localDate = this.calendar.getTime();
@@ -97,10 +98,8 @@ class CronTriggerTests {
assertThat(trigger.nextExecutionTime(context)).isEqualTo(this.calendar.getTime());
}
@ParameterizedCronTriggerTest
void incrementSecondWithPreviousExecutionTooEarly(Date localDateTime, TimeZone timeZone) {
setup(localDateTime, timeZone);
@Test
void incrementSecondWithPreviousExecutionTooEarly() {
CronTrigger trigger = new CronTrigger("11 * * * * *", timeZone);
this.calendar.set(Calendar.SECOND, 11);
SimpleTriggerContext context = new SimpleTriggerContext();
@@ -110,10 +109,8 @@ class CronTriggerTests {
assertThat(trigger.nextExecutionTime(context)).isEqualTo(this.calendar.getTime());
}
@ParameterizedCronTriggerTest
void incrementSecondAndRollover(Date localDateTime, TimeZone timeZone) {
setup(localDateTime, timeZone);
@Test
void incrementSecondAndRollover() {
CronTrigger trigger = new CronTrigger("10 * * * * *", timeZone);
this.calendar.set(Calendar.SECOND, 11);
Date localDate = this.calendar.getTime();
@@ -122,10 +119,8 @@ class CronTriggerTests {
assertThat(trigger.nextExecutionTime(context)).isEqualTo(this.calendar.getTime());
}
@ParameterizedCronTriggerTest
void secondRange(Date localDateTime, TimeZone timeZone) {
setup(localDateTime, timeZone);
@Test
void secondRange() {
CronTrigger trigger = new CronTrigger("10-15 * * * * *", timeZone);
this.calendar.set(Calendar.SECOND, 9);
assertMatchesNextSecond(trigger, this.calendar);
@@ -133,10 +128,8 @@ class CronTriggerTests {
assertMatchesNextSecond(trigger, this.calendar);
}
@ParameterizedCronTriggerTest
void incrementMinute(Date localDateTime, TimeZone timeZone) {
setup(localDateTime, timeZone);
@Test
void incrementMinute() {
CronTrigger trigger = new CronTrigger("0 * * * * *", timeZone);
this.calendar.set(Calendar.MINUTE, 10);
Date localDate = this.calendar.getTime();
@@ -151,10 +144,8 @@ class CronTriggerTests {
assertThat(localDate).isEqualTo(this.calendar.getTime());
}
@ParameterizedCronTriggerTest
void incrementMinuteByOne(Date localDateTime, TimeZone timeZone) {
setup(localDateTime, timeZone);
@Test
void incrementMinuteByOne() {
CronTrigger trigger = new CronTrigger("0 11 * * * *", timeZone);
this.calendar.set(Calendar.MINUTE, 10);
TriggerContext context = getTriggerContext(this.calendar.getTime());
@@ -163,10 +154,8 @@ class CronTriggerTests {
assertThat(trigger.nextExecutionTime(context)).isEqualTo(this.calendar.getTime());
}
@ParameterizedCronTriggerTest
void incrementMinuteAndRollover(Date localDateTime, TimeZone timeZone) {
setup(localDateTime, timeZone);
@Test
void incrementMinuteAndRollover() {
CronTrigger trigger = new CronTrigger("0 10 * * * *", timeZone);
this.calendar.set(Calendar.MINUTE, 11);
this.calendar.set(Calendar.SECOND, 0);
@@ -176,10 +165,8 @@ class CronTriggerTests {
assertThat(trigger.nextExecutionTime(context)).isEqualTo(this.calendar.getTime());
}
@ParameterizedCronTriggerTest
void incrementHour(Date localDateTime, TimeZone timeZone) {
setup(localDateTime, timeZone);
@Test
void incrementHour() {
CronTrigger trigger = new CronTrigger("0 0 * * * *", timeZone);
this.calendar.set(Calendar.MONTH, 9);
this.calendar.set(Calendar.DAY_OF_MONTH, 30);
@@ -197,10 +184,8 @@ class CronTriggerTests {
assertThat(trigger.nextExecutionTime(context2)).isEqualTo(this.calendar.getTime());
}
@ParameterizedCronTriggerTest
void incrementHourAndRollover(Date localDateTime, TimeZone timeZone) {
setup(localDateTime, timeZone);
@Test
void incrementHourAndRollover() {
CronTrigger trigger = new CronTrigger("0 0 * * * *", timeZone);
this.calendar.set(Calendar.MONTH, 9);
this.calendar.set(Calendar.DAY_OF_MONTH, 10);
@@ -219,10 +204,8 @@ class CronTriggerTests {
assertThat(trigger.nextExecutionTime(context2)).isEqualTo(this.calendar.getTime());
}
@ParameterizedCronTriggerTest
void incrementDayOfMonth(Date localDateTime, TimeZone timeZone) {
setup(localDateTime, timeZone);
@Test
void incrementDayOfMonth() {
CronTrigger trigger = new CronTrigger("0 0 0 * * *", timeZone);
this.calendar.set(Calendar.DAY_OF_MONTH, 1);
Date localDate = this.calendar.getTime();
@@ -241,10 +224,8 @@ class CronTriggerTests {
assertThat(this.calendar.get(Calendar.DAY_OF_MONTH)).isEqualTo(3);
}
@ParameterizedCronTriggerTest
void incrementDayOfMonthByOne(Date localDateTime, TimeZone timeZone) {
setup(localDateTime, timeZone);
@Test
void incrementDayOfMonthByOne() {
CronTrigger trigger = new CronTrigger("* * * 10 * *", timeZone);
this.calendar.set(Calendar.DAY_OF_MONTH, 9);
Date localDate = this.calendar.getTime();
@@ -256,10 +237,8 @@ class CronTriggerTests {
assertThat(trigger.nextExecutionTime(context)).isEqualTo(this.calendar.getTime());
}
@ParameterizedCronTriggerTest
void incrementDayOfMonthAndRollover(Date localDateTime, TimeZone timeZone) {
setup(localDateTime, timeZone);
@Test
void incrementDayOfMonthAndRollover() {
CronTrigger trigger = new CronTrigger("* * * 10 * *", timeZone);
this.calendar.set(Calendar.DAY_OF_MONTH, 11);
Date localDate = this.calendar.getTime();
@@ -272,10 +251,8 @@ class CronTriggerTests {
assertThat(trigger.nextExecutionTime(context)).isEqualTo(this.calendar.getTime());
}
@ParameterizedCronTriggerTest
void dailyTriggerInShortMonth(Date localDateTime, TimeZone timeZone) {
setup(localDateTime, timeZone);
@Test
void dailyTriggerInShortMonth() {
CronTrigger trigger = new CronTrigger("0 0 0 * * *", timeZone);
this.calendar.set(Calendar.MONTH, 8); // September: 30 days
this.calendar.set(Calendar.DAY_OF_MONTH, 30);
@@ -293,10 +270,8 @@ class CronTriggerTests {
assertThat(trigger.nextExecutionTime(context2)).isEqualTo(this.calendar.getTime());
}
@ParameterizedCronTriggerTest
void dailyTriggerInLongMonth(Date localDateTime, TimeZone timeZone) {
setup(localDateTime, timeZone);
@Test
void dailyTriggerInLongMonth() {
CronTrigger trigger = new CronTrigger("0 0 0 * * *", timeZone);
this.calendar.set(Calendar.MONTH, 7); // August: 31 days and not a daylight saving boundary
this.calendar.set(Calendar.DAY_OF_MONTH, 30);
@@ -314,10 +289,8 @@ class CronTriggerTests {
assertThat(trigger.nextExecutionTime(context2)).isEqualTo(this.calendar.getTime());
}
@ParameterizedCronTriggerTest
void dailyTriggerOnDaylightSavingBoundary(Date localDateTime, TimeZone timeZone) {
setup(localDateTime, timeZone);
@Test
void dailyTriggerOnDaylightSavingBoundary() {
CronTrigger trigger = new CronTrigger("0 0 0 * * *", timeZone);
this.calendar.set(Calendar.MONTH, 9); // October: 31 days and a daylight saving boundary in CET
this.calendar.set(Calendar.DAY_OF_MONTH, 30);
@@ -335,10 +308,8 @@ class CronTriggerTests {
assertThat(trigger.nextExecutionTime(context2)).isEqualTo(this.calendar.getTime());
}
@ParameterizedCronTriggerTest
void incrementMonth(Date localDateTime, TimeZone timeZone) {
setup(localDateTime, timeZone);
@Test
void incrementMonth() {
CronTrigger trigger = new CronTrigger("0 0 0 1 * *", timeZone);
this.calendar.set(Calendar.MONTH, 9);
this.calendar.set(Calendar.DAY_OF_MONTH, 30);
@@ -356,10 +327,8 @@ class CronTriggerTests {
assertThat(trigger.nextExecutionTime(context2)).isEqualTo(this.calendar.getTime());
}
@ParameterizedCronTriggerTest
void incrementMonthAndRollover(Date localDateTime, TimeZone timeZone) {
setup(localDateTime, timeZone);
@Test
void incrementMonthAndRollover() {
CronTrigger trigger = new CronTrigger("0 0 0 1 * *", timeZone);
this.calendar.set(Calendar.MONTH, 11);
this.calendar.set(Calendar.DAY_OF_MONTH, 31);
@@ -379,10 +348,8 @@ class CronTriggerTests {
assertThat(trigger.nextExecutionTime(context2)).isEqualTo(this.calendar.getTime());
}
@ParameterizedCronTriggerTest
void monthlyTriggerInLongMonth(Date localDateTime, TimeZone timeZone) {
setup(localDateTime, timeZone);
@Test
void monthlyTriggerInLongMonth() {
CronTrigger trigger = new CronTrigger("0 0 0 31 * *", timeZone);
this.calendar.set(Calendar.MONTH, 9);
this.calendar.set(Calendar.DAY_OF_MONTH, 30);
@@ -395,10 +362,8 @@ class CronTriggerTests {
assertThat(trigger.nextExecutionTime(context)).isEqualTo(this.calendar.getTime());
}
@ParameterizedCronTriggerTest
void monthlyTriggerInShortMonth(Date localDateTime, TimeZone timeZone) {
setup(localDateTime, timeZone);
@Test
void monthlyTriggerInShortMonth() {
CronTrigger trigger = new CronTrigger("0 0 0 1 * *", timeZone);
this.calendar.set(Calendar.MONTH, 9);
this.calendar.set(Calendar.DAY_OF_MONTH, 30);
@@ -412,10 +377,8 @@ class CronTriggerTests {
assertThat(trigger.nextExecutionTime(context)).isEqualTo(this.calendar.getTime());
}
@ParameterizedCronTriggerTest
void incrementDayOfWeekByOne(Date localDateTime, TimeZone timeZone) {
setup(localDateTime, timeZone);
@Test
void incrementDayOfWeekByOne() {
CronTrigger trigger = new CronTrigger("* * * * * 2", timeZone);
this.calendar.set(Calendar.DAY_OF_WEEK, 2);
Date localDate = this.calendar.getTime();
@@ -428,10 +391,8 @@ class CronTriggerTests {
assertThat(this.calendar.get(Calendar.DAY_OF_WEEK)).isEqualTo(Calendar.TUESDAY);
}
@ParameterizedCronTriggerTest
void incrementDayOfWeekAndRollover(Date localDateTime, TimeZone timeZone) {
setup(localDateTime, timeZone);
@Test
void incrementDayOfWeekAndRollover() {
CronTrigger trigger = new CronTrigger("* * * * * 2", timeZone);
this.calendar.set(Calendar.DAY_OF_WEEK, 4);
Date localDate = this.calendar.getTime();
@@ -444,10 +405,8 @@ class CronTriggerTests {
assertThat(this.calendar.get(Calendar.DAY_OF_WEEK)).isEqualTo(Calendar.TUESDAY);
}
@ParameterizedCronTriggerTest
void specificMinuteSecond(Date localDateTime, TimeZone timeZone) {
setup(localDateTime, timeZone);
@Test
void specificMinuteSecond() {
CronTrigger trigger = new CronTrigger("55 5 * * * *", timeZone);
this.calendar.set(Calendar.MINUTE, 4);
this.calendar.set(Calendar.SECOND, 54);
@@ -463,10 +422,8 @@ class CronTriggerTests {
assertThat(actual).isEqualTo(this.calendar.getTime());
}
@ParameterizedCronTriggerTest
void specificHourSecond(Date localDateTime, TimeZone timeZone) {
setup(localDateTime, timeZone);
@Test
void specificHourSecond() {
CronTrigger trigger = new CronTrigger("55 * 10 * * *", timeZone);
this.calendar.set(Calendar.HOUR_OF_DAY, 9);
this.calendar.set(Calendar.SECOND, 54);
@@ -483,10 +440,8 @@ class CronTriggerTests {
assertThat(actual).isEqualTo(this.calendar.getTime());
}
@ParameterizedCronTriggerTest
void specificMinuteHour(Date localDateTime, TimeZone timeZone) {
setup(localDateTime, timeZone);
@Test
void specificMinuteHour() {
CronTrigger trigger = new CronTrigger("* 5 10 * * *", timeZone);
this.calendar.set(Calendar.MINUTE, 4);
this.calendar.set(Calendar.HOUR_OF_DAY, 9);
@@ -504,10 +459,8 @@ class CronTriggerTests {
assertThat(actual).isEqualTo(this.calendar.getTime());
}
@ParameterizedCronTriggerTest
void specificDayOfMonthSecond(Date localDateTime, TimeZone timeZone) {
setup(localDateTime, timeZone);
@Test
void specificDayOfMonthSecond() {
CronTrigger trigger = new CronTrigger("55 * * 3 * *", timeZone);
this.calendar.set(Calendar.DAY_OF_MONTH, 2);
this.calendar.set(Calendar.SECOND, 54);
@@ -525,10 +478,8 @@ class CronTriggerTests {
assertThat(actual).isEqualTo(this.calendar.getTime());
}
@ParameterizedCronTriggerTest
void specificDate(Date localDateTime, TimeZone timeZone) {
setup(localDateTime, timeZone);
@Test
void specificDate() {
CronTrigger trigger = new CronTrigger("* * * 3 11 *", timeZone);
this.calendar.set(Calendar.DAY_OF_MONTH, 2);
this.calendar.set(Calendar.MONTH, 9);
@@ -547,10 +498,8 @@ class CronTriggerTests {
assertThat(actual).isEqualTo(this.calendar.getTime());
}
@ParameterizedCronTriggerTest
void nonExistentSpecificDate(Date localDateTime, TimeZone timeZone) {
setup(localDateTime, timeZone);
@Test
void nonExistentSpecificDate() {
// TODO: maybe try and detect this as a special case in parser?
CronTrigger trigger = new CronTrigger("0 0 0 31 6 *", timeZone);
this.calendar.set(Calendar.DAY_OF_MONTH, 10);
@@ -560,10 +509,8 @@ class CronTriggerTests {
assertThat(trigger.nextExecutionTime(context1)).isNull();
}
@ParameterizedCronTriggerTest
void leapYearSpecificDate(Date localDateTime, TimeZone timeZone) {
setup(localDateTime, timeZone);
@Test
void leapYearSpecificDate() {
CronTrigger trigger = new CronTrigger("0 0 0 29 2 *", timeZone);
this.calendar.set(Calendar.YEAR, 2007);
this.calendar.set(Calendar.DAY_OF_MONTH, 10);
@@ -583,10 +530,8 @@ class CronTriggerTests {
assertThat(actual).isEqualTo(this.calendar.getTime());
}
@ParameterizedCronTriggerTest
void weekDaySequence(Date localDateTime, TimeZone timeZone) {
setup(localDateTime, timeZone);
@Test
void weekDaySequence() {
CronTrigger trigger = new CronTrigger("0 0 7 ? * MON-FRI", timeZone);
// This is a Saturday
this.calendar.set(2009, 8, 26);
@@ -611,184 +556,138 @@ class CronTriggerTests {
assertThat(actual).isEqualTo(this.calendar.getTime());
}
@ParameterizedCronTriggerTest
void dayOfWeekIndifferent(Date localDateTime, TimeZone timeZone) {
setup(localDateTime, timeZone);
@Test
void dayOfWeekIndifferent() {
CronTrigger trigger1 = new CronTrigger("* * * 2 * *", timeZone);
CronTrigger trigger2 = new CronTrigger("* * * 2 * ?", timeZone);
assertThat(trigger2).isEqualTo(trigger1);
}
@ParameterizedCronTriggerTest
void secondIncrementer(Date localDateTime, TimeZone timeZone) {
setup(localDateTime, timeZone);
@Test
void secondIncrementer() {
CronTrigger trigger1 = new CronTrigger("57,59 * * * * *", timeZone);
CronTrigger trigger2 = new CronTrigger("57/2 * * * * *", timeZone);
assertThat(trigger2).isEqualTo(trigger1);
}
@ParameterizedCronTriggerTest
void secondIncrementerWithRange(Date localDateTime, TimeZone timeZone) {
setup(localDateTime, timeZone);
@Test
void secondIncrementerWithRange() {
CronTrigger trigger1 = new CronTrigger("1,3,5 * * * * *", timeZone);
CronTrigger trigger2 = new CronTrigger("1-6/2 * * * * *", timeZone);
assertThat(trigger2).isEqualTo(trigger1);
}
@ParameterizedCronTriggerTest
void hourIncrementer(Date localDateTime, TimeZone timeZone) {
setup(localDateTime, timeZone);
@Test
void hourIncrementer() {
CronTrigger trigger1 = new CronTrigger("* * 4,8,12,16,20 * * *", timeZone);
CronTrigger trigger2 = new CronTrigger("* * 4/4 * * *", timeZone);
assertThat(trigger2).isEqualTo(trigger1);
}
@ParameterizedCronTriggerTest
void dayNames(Date localDateTime, TimeZone timeZone) {
setup(localDateTime, timeZone);
@Test
void dayNames() {
CronTrigger trigger1 = new CronTrigger("* * * * * 0-6", timeZone);
CronTrigger trigger2 = new CronTrigger("* * * * * TUE,WED,THU,FRI,SAT,SUN,MON", timeZone);
assertThat(trigger2).isEqualTo(trigger1);
}
@ParameterizedCronTriggerTest
void sundayIsZero(Date localDateTime, TimeZone timeZone) {
setup(localDateTime, timeZone);
@Test
void sundayIsZero() {
CronTrigger trigger1 = new CronTrigger("* * * * * 0", timeZone);
CronTrigger trigger2 = new CronTrigger("* * * * * SUN", timeZone);
assertThat(trigger2).isEqualTo(trigger1);
}
@ParameterizedCronTriggerTest
void sundaySynonym(Date localDateTime, TimeZone timeZone) {
setup(localDateTime, timeZone);
@Test
void sundaySynonym() {
CronTrigger trigger1 = new CronTrigger("* * * * * 0", timeZone);
CronTrigger trigger2 = new CronTrigger("* * * * * 7", timeZone);
assertThat(trigger2).isEqualTo(trigger1);
}
@ParameterizedCronTriggerTest
void monthNames(Date localDateTime, TimeZone timeZone) {
setup(localDateTime, timeZone);
@Test
void monthNames() {
CronTrigger trigger1 = new CronTrigger("* * * * 1-12 *", timeZone);
CronTrigger trigger2 = new CronTrigger("* * * * FEB,JAN,MAR,APR,MAY,JUN,JUL,AUG,SEP,OCT,NOV,DEC *", timeZone);
assertThat(trigger2).isEqualTo(trigger1);
}
@ParameterizedCronTriggerTest
void monthNamesMixedCase(Date localDateTime, TimeZone timeZone) {
setup(localDateTime, timeZone);
@Test
void monthNamesMixedCase() {
CronTrigger trigger1 = new CronTrigger("* * * * 2 *", timeZone);
CronTrigger trigger2 = new CronTrigger("* * * * Feb *", timeZone);
assertThat(trigger2).isEqualTo(trigger1);
}
@ParameterizedCronTriggerTest
void secondInvalid(Date localDateTime, TimeZone timeZone) {
setup(localDateTime, timeZone);
@Test
void secondInvalid() {
assertThatIllegalArgumentException().isThrownBy(() -> new CronTrigger("77 * * * * *", timeZone));
}
@ParameterizedCronTriggerTest
void secondRangeInvalid(Date localDateTime, TimeZone timeZone) {
setup(localDateTime, timeZone);
@Test
void secondRangeInvalid() {
assertThatIllegalArgumentException().isThrownBy(() -> new CronTrigger("44-77 * * * * *", timeZone));
}
@ParameterizedCronTriggerTest
void minuteInvalid(Date localDateTime, TimeZone timeZone) {
setup(localDateTime, timeZone);
@Test
void minuteInvalid() {
assertThatIllegalArgumentException().isThrownBy(() -> new CronTrigger("* 77 * * * *", timeZone));
}
@ParameterizedCronTriggerTest
void minuteRangeInvalid(Date localDateTime, TimeZone timeZone) {
setup(localDateTime, timeZone);
@Test
void minuteRangeInvalid() {
assertThatIllegalArgumentException().isThrownBy(() -> new CronTrigger("* 44-77 * * * *", timeZone));
}
@ParameterizedCronTriggerTest
void hourInvalid(Date localDateTime, TimeZone timeZone) {
setup(localDateTime, timeZone);
@Test
void hourInvalid() {
assertThatIllegalArgumentException().isThrownBy(() -> new CronTrigger("* * 27 * * *", timeZone));
}
@ParameterizedCronTriggerTest
void hourRangeInvalid(Date localDateTime, TimeZone timeZone) {
setup(localDateTime, timeZone);
@Test
void hourRangeInvalid() {
assertThatIllegalArgumentException().isThrownBy(() -> new CronTrigger("* * 23-28 * * *", timeZone));
}
@ParameterizedCronTriggerTest
void dayInvalid(Date localDateTime, TimeZone timeZone) {
setup(localDateTime, timeZone);
@Test
void dayInvalid() {
assertThatIllegalArgumentException().isThrownBy(() -> new CronTrigger("* * * 45 * *", timeZone));
}
@ParameterizedCronTriggerTest
void dayRangeInvalid(Date localDateTime, TimeZone timeZone) {
setup(localDateTime, timeZone);
@Test
void dayRangeInvalid() {
assertThatIllegalArgumentException().isThrownBy(() -> new CronTrigger("* * * 28-45 * *", timeZone));
}
@ParameterizedCronTriggerTest
void monthInvalid(Date localDateTime, TimeZone timeZone) {
setup(localDateTime, timeZone);
@Test
void monthInvalid() {
assertThatIllegalArgumentException().isThrownBy(() -> new CronTrigger("0 0 0 25 13 ?", timeZone));
}
@ParameterizedCronTriggerTest
void monthInvalidTooSmall(Date localDateTime, TimeZone timeZone) {
setup(localDateTime, timeZone);
@Test
void monthInvalidTooSmall() {
assertThatIllegalArgumentException().isThrownBy(() -> new CronTrigger("0 0 0 25 0 ?", timeZone));
}
@ParameterizedCronTriggerTest
void dayOfMonthInvalid(Date localDateTime, TimeZone timeZone) {
setup(localDateTime, timeZone);
@Test
void dayOfMonthInvalid() {
assertThatIllegalArgumentException().isThrownBy(() -> new CronTrigger("0 0 0 32 12 ?", timeZone));
}
@ParameterizedCronTriggerTest
void monthRangeInvalid(Date localDateTime, TimeZone timeZone) {
setup(localDateTime, timeZone);
@Test
void monthRangeInvalid() {
assertThatIllegalArgumentException().isThrownBy(() -> new CronTrigger("* * * * 11-13 *", timeZone));
}
@ParameterizedCronTriggerTest
void whitespace(Date localDateTime, TimeZone timeZone) {
setup(localDateTime, timeZone);
@Test
void whitespace() {
CronTrigger trigger1 = new CronTrigger("* * * * 1 *", timeZone);
CronTrigger trigger2 = new CronTrigger("* * * * 1 *", timeZone);
assertThat(trigger2).isEqualTo(trigger1);
}
@ParameterizedCronTriggerTest
void monthSequence(Date localDateTime, TimeZone timeZone) {
setup(localDateTime, timeZone);
@Test
void monthSequence() {
CronTrigger trigger = new CronTrigger("0 30 23 30 1/3 ?", timeZone);
this.calendar.set(2010, 11, 30);
Date localDate = this.calendar.getTime();
@@ -812,10 +711,8 @@ class CronTriggerTests {
assertThat(actual).isEqualTo(this.calendar.getTime());
}
@ParameterizedCronTriggerTest
void daylightSavingMissingHour(Date localDateTime, TimeZone timeZone) {
setup(localDateTime, timeZone);
@Test
void daylightSavingMissingHour() {
// This trigger has to be somewhere between 2:00 AM and 3:00 AM, so we
// use a cron expression for 2:10 AM every day.
CronTrigger trigger = new CronTrigger("0 10 2 * * *", timeZone);
@@ -865,19 +762,4 @@ class CronTriggerTests {
return new SimpleTriggerContext(null, null, lastCompletionTime);
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@ParameterizedTest(name = "[{index}] localDateTime[{0}], time zone[{1}]")
@MethodSource("parameters")
@interface ParameterizedCronTriggerTest {
}
static Stream<Arguments> parameters() {
return Stream.of(
arguments(new Date(), TimeZone.getTimeZone("PST")),
arguments(new Date(), TimeZone.getTimeZone("CET"))
);
}
}
@@ -154,7 +154,7 @@ class InstrumentedMethodTests {
}
@Test
void classGetConstructorsShouldNotMatchWhemMissingTypeHint() {
void classGetConstructorsShouldNotMatchWhenMissingTypeHint() {
assertThatInvocationDoesNotMatch(InstrumentedMethod.CLASS_GETCONSTRUCTORS, this.stringGetConstructors);
}
@@ -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;
@@ -101,7 +105,17 @@ public class ConcurrentReferenceHashMap<K, V> extends AbstractMap<K, V> implemen
/**
* Late binding entry set.
*/
private volatile @Nullable Set<Map.Entry<K, V>> entrySet;
private @Nullable Set<Map.Entry<K, V>> entrySet;
/**
* Late binding key set.
*/
private @Nullable Set<K> keySet;
/**
* Late binding values collection.
*/
private @Nullable Collection<V> values;
/**
@@ -512,6 +526,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;
}
private <T> @Nullable T doTask(@Nullable Object key, Task<T> task) {
int hash = getHash(key);
return getSegmentForHash(hash).doTask(hash, key, task);
@@ -940,7 +974,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() {
@@ -976,13 +1010,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;
@@ -141,26 +141,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.jspecify.annotations.Nullable;
import org.junit.jupiter.api.Test;
@@ -32,12 +33,13 @@ import org.junit.jupiter.api.Test;
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
@@ -81,7 +81,7 @@ class MapTests extends AbstractExpressionTests {
}
@Test
void inelineMapIsInstanceOfMap() {
void inlineMapIsInstanceOfMap() {
evaluate("{a:1, b:2} instanceof T(java.util.Map)", "true", Boolean.class);
}
@@ -19,8 +19,6 @@ package org.springframework.jdbc.core;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import org.jspecify.annotations.Nullable;
/**
* Parameterized callback interface used by the {@link JdbcTemplate} class for
* batch updates.
@@ -49,6 +47,6 @@ public interface ParameterizedPreparedStatementSetter<T> {
* @param argument the object containing the values to be set
* @throws SQLException if an SQLException is encountered (i.e. there is no need to catch SQLException)
*/
void setValues(PreparedStatement ps, @Nullable T argument) throws SQLException;
void setValues(PreparedStatement ps, T argument) throws SQLException;
}
@@ -41,9 +41,8 @@ import org.springframework.util.Assert;
* @author Rod Johnson
* @author Juergen Hoeller
* @see #loadBeanDefinitions
* @see org.springframework.beans.factory.support.PropertiesBeanDefinitionReader
* @deprecated 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(since = "5.3")
public class JdbcBeanDefinitionReader {
@@ -24,8 +24,8 @@ import java.sql.ResultSet
* @author Mario Arias
* @since 5.0
*/
inline fun <reified T> JdbcOperations.queryForObject(sql: String): T =
queryForObject(sql, T::class.java as Class<*>) as T
inline fun <reified T : Any> JdbcOperations.queryForObject(sql: String): T? =
queryForObject(sql, T::class.java)
/**
* Extensions for [JdbcOperations.queryForObject] providing a RowMapper-like function
@@ -34,7 +34,7 @@ inline fun <reified T> JdbcOperations.queryForObject(sql: String): T =
* @author Mario Arias
* @since 5.0
*/
inline fun <reified T> JdbcOperations.queryForObject(sql: String, vararg args: Any, crossinline function: (ResultSet, Int) -> T): T =
inline fun <reified T> JdbcOperations.queryForObject(sql: String, vararg args: Any?, crossinline function: (ResultSet, Int) -> T): T =
queryForObject(sql, { resultSet, i -> function(resultSet, i) }, *args)
/**
@@ -44,18 +44,18 @@ inline fun <reified T> JdbcOperations.queryForObject(sql: String, vararg args: A
* @author Mario Arias
* @since 5.0
*/
inline fun <reified T> JdbcOperations.queryForObject(sql: String, args: Array<out Any>, argTypes: IntArray): T =
queryForObject(sql, args, argTypes, T::class.java as Class<*>) as T
inline fun <reified T : Any> JdbcOperations.queryForObject(sql: String, args: Array<out Any?>, argTypes: IntArray): T? =
queryForObject(sql, args, argTypes, T::class.java)
/**
* Extension for [JdbcOperations.queryForObject] providing a
* `queryForObject<Foo>("...", arrayOf(arg1, argN))` variant.
* `queryForObject<Foo>("...", arg1, argN)` variant.
*
* @author Mario Arias
* @since 5.0
* @author Sébastien Deleuze
* @since 7.0
*/
inline fun <reified T> JdbcOperations.queryForObject(sql: String, args: Array<out Any>): T =
queryForObject(sql, T::class.java as Class<*>, args) as T
inline fun <reified T : Any> JdbcOperations.queryForObject(sql: String, vararg args: Any?): T? =
queryForObject(sql, T::class.java, *args)
/**
* Extension for [JdbcOperations.queryForList] providing a `queryForList<Foo>("...")` variant.
@@ -63,9 +63,8 @@ inline fun <reified T> JdbcOperations.queryForObject(sql: String, args: Array<ou
* @author Mario Arias
* @since 5.0
*/
@Suppress("UNCHECKED_CAST")
inline fun <reified T> JdbcOperations.queryForList(sql: String): List<T> =
queryForList(sql, T::class.java) as List<T>
inline fun <reified T : Any> JdbcOperations.queryForList(sql: String): List<T?> =
queryForList(sql, T::class.java)
/**
* Extension for [JdbcOperations.queryForList] providing a
@@ -74,21 +73,19 @@ inline fun <reified T> JdbcOperations.queryForList(sql: String): List<T> =
* @author Mario Arias
* @since 5.0
*/
@Suppress("UNCHECKED_CAST")
inline fun <reified T> JdbcOperations.queryForList(sql: String, args: Array<out Any>,
argTypes: IntArray): List<T> =
queryForList(sql, args, argTypes, T::class.java) as List<T>
inline fun <reified T : Any> JdbcOperations.queryForList(sql: String, args: Array<out Any?>,
argTypes: IntArray): List<T?> =
queryForList(sql, args, argTypes, T::class.java)
/**
* Extension for [JdbcOperations.queryForList] providing a
* `queryForList<Foo>("...", arrayOf(arg1, argN))` variant.
* `queryForList<Foo>("...", arg1, argN)` variant.
*
* @author Mario Arias
* @since 5.0
* @author Sebastien Deleuze
* @since 7.0
*/
@Suppress("UNCHECKED_CAST")
inline fun <reified T> JdbcOperations.queryForList(sql: String, args: Array<out Any>): List<T> =
queryForList(sql, T::class.java, args) as List<T>
inline fun <reified T : Any> JdbcOperations.queryForList(sql: String, vararg args: Any?): List<T?> =
queryForList(sql, T::class.java, *args)
/**
@@ -98,7 +95,7 @@ inline fun <reified T> JdbcOperations.queryForList(sql: String, args: Array<out
* @author Mario Arias
* @since 5.0
*/
fun <T> JdbcOperations.query(sql: String, vararg args: Any,
fun <T> JdbcOperations.query(sql: String, vararg args: Any?,
function: (ResultSet) -> T): T =
query(sql, ResultSetExtractor { function(it) }, *args)
@@ -109,7 +106,7 @@ fun <T> JdbcOperations.query(sql: String, vararg args: Any,
* @author Mario Arias
* @since 5.0
*/
fun JdbcOperations.query(sql: String, vararg args: Any, function: (ResultSet) -> Unit): Unit =
fun JdbcOperations.query(sql: String, vararg args: Any?, function: (ResultSet) -> Unit): Unit =
query(sql, { function(it) }, *args)
/**
@@ -119,5 +116,5 @@ fun JdbcOperations.query(sql: String, vararg args: Any, function: (ResultSet) ->
* @author Mario Arias
* @since 5.0
*/
fun <T> JdbcOperations.query(sql: String, vararg args: Any, function: (ResultSet, Int) -> T): List<T> =
fun <T> JdbcOperations.query(sql: String, vararg args: Any?, function: (ResultSet, Int) -> T): List<T> =
query(sql, { rs, i -> function(rs, i) }, *args)
@@ -46,7 +46,7 @@ class JdbcOperationsExtensionsTests {
@Test
fun `queryForObject with nullable reified type parameters`() {
every { template.queryForObject(sql, any<Class<Int>>()) } returns null
assertThat(template.queryForObject<Int?>(sql)).isNull()
assertThat(template.queryForObject<Int>(sql)).isNull()
verify { template.queryForObject(sql, any<Class<Int>>()) }
}
@@ -65,8 +65,17 @@ class JdbcOperationsExtensionsTests {
}
@Test
fun `queryForObject with reified type parameters and argTypes`() {
val args = arrayOf(3)
fun `queryForObject with reified type parameters, non-null args array and argTypes`() {
val args = arrayOf(3, 4)
val argTypes = intArrayOf(JDBCType.INTEGER.vendorTypeNumber)
every { template.queryForObject(sql, args, argTypes, any<Class<Int>>()) } returns 2
assertThat(template.queryForObject<Int>(sql, args, argTypes)).isEqualTo(2)
verify { template.queryForObject(sql, args, argTypes, any<Class<Int>>()) }
}
@Test
fun `queryForObject with reified type parameters, nullable args array and argTypes`() {
val args = arrayOf(3, null)
val argTypes = intArrayOf(JDBCType.INTEGER.vendorTypeNumber)
every { template.queryForObject(sql, args, argTypes, any<Class<Int>>()) } returns 2
assertThat(template.queryForObject<Int>(sql, args, argTypes)).isEqualTo(2)
@@ -78,24 +87,22 @@ class JdbcOperationsExtensionsTests {
val args = arrayOf(3)
val argTypes = intArrayOf(JDBCType.INTEGER.vendorTypeNumber)
every { template.queryForObject(sql, args, argTypes, any<Class<Int>>()) } returns null
assertThat(template.queryForObject<Int?>(sql, args, argTypes)).isNull()
assertThat(template.queryForObject<Int>(sql, args, argTypes)).isNull()
verify { template.queryForObject(sql, args, argTypes, any<Class<Int>>()) }
}
@Test
fun `queryForObject with reified type parameters and args`() {
val args = arrayOf(3, 4)
every { template.queryForObject(sql, any<Class<Int>>(), args) } returns 2
assertThat(template.queryForObject<Int>(sql, args)).isEqualTo(2)
verify { template.queryForObject(sql, any<Class<Int>>(), args) }
every { template.queryForObject(sql, any<Class<Int>>(), 3, null) } returns 2
assertThat(template.queryForObject<Int>(sql, 3, null)).isEqualTo(2)
verify { template.queryForObject(sql, any<Class<Int>>(), 3, null) }
}
@Test
fun `queryForObject with nullable reified type parameters and args`() {
val args = arrayOf(3, 4)
every { template.queryForObject(sql, any<Class<Int>>(), args) } returns null
assertThat(template.queryForObject<Int?>(sql, args)).isNull()
verify { template.queryForObject(sql, any<Class<Int>>(), args) }
every { template.queryForObject(sql, any<Class<Int>>(), 3, 4) } returns null
assertThat(template.queryForObject<Int>(sql, 3, 4)).isNull()
verify { template.queryForObject(sql, any<Class<Int>>(), 3, 4) }
}
@Test
@@ -110,12 +117,12 @@ class JdbcOperationsExtensionsTests {
fun `queryForList with nullable reified type parameters`() {
val list = listOf(1, null, 3)
every { template.queryForList(sql, any<Class<Int>>()) } returns list
assertThat(template.queryForList<Int?>(sql)).isEqualTo(list)
assertThat(template.queryForList<Int>(sql)).isEqualTo(list)
verify { template.queryForList(sql, any<Class<Int>>()) }
}
@Test
fun `queryForList with reified type parameters and argTypes`() {
fun `queryForList with reified type parameters, non-null args and argTypes`() {
val list = listOf(1, 2, 3)
val args = arrayOf(3)
val argTypes = intArrayOf(JDBCType.INTEGER.vendorTypeNumber)
@@ -125,31 +132,49 @@ class JdbcOperationsExtensionsTests {
}
@Test
fun `queryForList with nullable reified type parameters and argTypes`() {
fun `queryForList with reified type parameters, nullable args and argTypes`() {
val list = listOf(1, 2, 3)
val args = arrayOf("foo", null)
val argTypes = intArrayOf(JDBCType.VARCHAR.vendorTypeNumber)
every { template.queryForList(sql, args, argTypes, any<Class<Int>>()) } returns list
assertThat(template.queryForList<Int>(sql, args, argTypes)).isEqualTo(list)
verify { template.queryForList(sql, args, argTypes, any<Class<Int>>()) }
}
@Test
fun `queryForList with nullable reified type parameters, non-null args and argTypes`() {
val list = listOf(1, null, 3)
val args = arrayOf(3)
val argTypes = intArrayOf(JDBCType.INTEGER.vendorTypeNumber)
every { template.queryForList(sql, args, argTypes, any<Class<Int>>()) } returns list
assertThat(template.queryForList<Int?>(sql, args, argTypes)).isEqualTo(list)
assertThat(template.queryForList<Int>(sql, args, argTypes)).isEqualTo(list)
verify { template.queryForList(sql, args, argTypes, any<Class<Int>>()) }
}
@Test
fun `queryForList with nullable reified type parameters, nullable args and argTypes`() {
val list = listOf(1, null, 3)
val args = arrayOf("foo", null)
val argTypes = intArrayOf(JDBCType.VARCHAR.vendorTypeNumber)
every { template.queryForList(sql, args, argTypes, any<Class<Int>>()) } returns list
assertThat(template.queryForList<Int>(sql, args, argTypes)).isEqualTo(list)
verify { template.queryForList(sql, args, argTypes, any<Class<Int>>()) }
}
@Test
fun `queryForList with reified type parameters and args`() {
val list = listOf(1, 2, 3)
val args = arrayOf(3, 4)
every { template.queryForList(sql, any<Class<Int>>(), args) } returns list
template.queryForList<Int>(sql, args)
verify { template.queryForList(sql, any<Class<Int>>(), args) }
every { template.queryForList(sql, any<Class<Int>>(), 3, null) } returns list
template.queryForList<Int>(sql, 3, null)
verify { template.queryForList(sql, any<Class<Int>>(), 3, null) }
}
@Test
fun `queryForList with nullable reified type parameters and args`() {
val list = listOf(1, null, 3)
val args = arrayOf(3, 4)
every { template.queryForList(sql, any<Class<Int>>(), args) } returns list
template.queryForList<Int?>(sql, args)
verify { template.queryForList(sql, any<Class<Int>>(), args) }
every { template.queryForList(sql, any<Class<Int>>(), 3, null) } returns list
template.queryForList<Int>(sql, 3, null)
verify { template.queryForList(sql, any<Class<Int>>(), 3, null) }
}
@Test
@@ -105,14 +105,10 @@ class PersistenceManagedTypesBeanRegistrationAotProcessorTests {
.withMemberCategories(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS)).accepts(hints);
assertThat(RuntimeHintsPredicates.reflection().onType(EmployeeLocation.class)
.withMemberCategories(MemberCategory.ACCESS_DECLARED_FIELDS)).accepts(hints);
assertThat(RuntimeHintsPredicates.reflection().onMethod(Car.class, "setId")
.invoke()).accepts(hints);
assertThat(RuntimeHintsPredicates.reflection().onMethod(Car.class, "getId")
.invoke()).accepts(hints);
assertThat(RuntimeHintsPredicates.reflection().onMethod(Car.class, "setModel")
.invoke()).accepts(hints);
assertThat(RuntimeHintsPredicates.reflection().onMethod(Car.class, "getModel")
.invoke()).accepts(hints);
assertThat(RuntimeHintsPredicates.reflection().onMethodInvocation(Car.class, "setId")).accepts(hints);
assertThat(RuntimeHintsPredicates.reflection().onMethodInvocation(Car.class, "getId")).accepts(hints);
assertThat(RuntimeHintsPredicates.reflection().onMethodInvocation(Car.class, "setModel")).accepts(hints);
assertThat(RuntimeHintsPredicates.reflection().onMethodInvocation(Car.class, "getModel")).accepts(hints);
});
}
@@ -153,7 +153,7 @@ public class SpringExtension implements BeforeAllCallback, AfterAllCallback, Tes
* Returns {@link ExtensionContextScope#TEST_METHOD ExtensionContextScope.TEST_METHOD}.
* <p>This can be effectively overridden by annotating a test class with
* {@code @SpringExtensionConfig(useTestClassScopedExtensionContext = true)}.
* See the {@link SpringExtension class-level Javadoc} for further details.
* See the {@linkplain SpringExtension class-level Javadoc} for further details.
* @since 7.0
* @see SpringExtensionConfig#useTestClassScopedExtensionContext()
*/
@@ -334,6 +334,10 @@ public class SpringExtension implements BeforeAllCallback, AfterAllCallback, Tes
* <li>The parameter is of type {@link ApplicationEvents} or a sub-type thereof.</li>
* <li>{@link ParameterResolutionDelegate#isAutowirable} returns {@code true}.</li>
* </ol>
* <p>This method does not {@linkplain #getApplicationContext(ExtensionContext)
* load} the {@code ApplicationContext} or verify that the application context
* actually contains a matching candidate bean, since doing so would potentially
* load an application context too early or unnecessarily.
* <p><strong>WARNING</strong>: If a test class {@code Constructor} is annotated
* with {@code @Autowired} or automatically autowirable (see
* {@link org.springframework.test.context.TestConstructor @TestConstructor}),
@@ -391,7 +395,18 @@ public class SpringExtension implements BeforeAllCallback, AfterAllCallback, Tes
/**
* Get the {@link ApplicationContext} associated with the supplied {@code ExtensionContext}.
* Get the {@link ApplicationContext} associated with the supplied {@link ExtensionContext}.
* <p><strong>NOTE</strong>: As of Spring Framework 7.0, the supplied
* {@code ExtensionContext} may not be properly <em>scoped</em>. See the
* {@linkplain SpringExtension class-level Javadoc} for further details.
* <p><strong>WARNING</strong>: Invoking this method ensures that the
* corresponding {@code ApplicationContext} is
* {@linkplain org.springframework.test.context.TestContext#getApplicationContext()
* loaded}. Consequently, this method should not be used if eager loading of
* the application context is undesired. For example,
* {@link #supportsParameter(ParameterContext, ExtensionContext)} intentionally
* does not invoke this method, since doing so would potentially load an
* application context too early or unnecessarily.
* @param context the current {@code ExtensionContext} (never {@code null})
* @return the application context
* @throws IllegalStateException if an error occurs while retrieving the application context
@@ -402,7 +417,7 @@ public class SpringExtension implements BeforeAllCallback, AfterAllCallback, Tes
}
/**
* Get the {@link TestContextManager} associated with the supplied {@code ExtensionContext}.
* Get the {@link TestContextManager} associated with the supplied {@link ExtensionContext}.
* @return the {@code TestContextManager} (never {@code null})
*/
static TestContextManager getTestContextManager(ExtensionContext context) {
@@ -28,9 +28,13 @@ import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Consumer;
import java.util.function.Function;
import com.jayway.jsonpath.Configuration;
import com.jayway.jsonpath.TypeRef;
import com.jayway.jsonpath.spi.mapper.MappingProvider;
import org.jspecify.annotations.Nullable;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.core.ResolvableType;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpRequest;
@@ -144,9 +148,10 @@ class DefaultRestTestClient implements RestTestClient {
client.mutate()
.configureMessageConverters(convertersBuilder -> {
HttpMessageConverters converters = convertersBuilder.build();
if (converters.iterator().hasNext()) {
this.converter = JsonConverterDelegate.of(converters);
if (converters.isEmpty()) {
converters = HttpMessageConverters.forClient().registerDefaults().build();
}
this.converter = JsonConverterDelegate.of(converters);
})
.build();
}
@@ -485,7 +490,8 @@ class DefaultRestTestClient implements RestTestClient {
@Override
public JsonPathAssertions jsonPath(String expression) {
return new JsonPathAssertions(this, getBodyAsString(), expression, null);
Configuration config = JsonPathConfigurationProvider.getConfiguration(this.result);
return new JsonPathAssertions(this, getBodyAsString(), expression, config);
}
@Override
@@ -539,4 +545,37 @@ class DefaultRestTestClient implements RestTestClient {
}
}
private static class JsonPathConfigurationProvider {
static Configuration getConfiguration(EntityExchangeResult<?> result) {
Configuration config = Configuration.defaultConfiguration();
JsonConverterDelegate delegate = result.getJsonConverterDelegate();
return (delegate != null ? config.mappingProvider(new MessageConverterMappingProvider(delegate)) : config);
}
}
private record MessageConverterMappingProvider(JsonConverterDelegate delegate) implements MappingProvider {
@Override
public <T> T map(Object value, Class<T> targetType, Configuration configuration) {
return mapToTargetType(value, ResolvableType.forClass(targetType));
}
@Override
public <T> T map(Object value, TypeRef<T> targetType, Configuration configuration) {
return mapToTargetType(value, ResolvableType.forType(targetType.getType()));
}
private <T> T mapToTargetType(Object value, ResolvableType targetType) {
try {
return delegate().map(value, targetType);
}
catch (IOException ex) {
throw new IllegalStateException("Failed to map " + value + " to " + targetType, ex);
}
}
}
}
@@ -23,6 +23,7 @@ import java.util.Map;
import org.hamcrest.MatcherAssert;
import org.junit.jupiter.api.Test;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.test.web.Person;
@@ -106,6 +107,19 @@ class JsonPathAssertionTests {
.jsonPath("$.performers[1].name").value(v -> MatcherAssert.assertThat(v, equalTo("Yehudi Menuhin")));
}
@Test
void valueConsumer() {
client.get().uri("/music/people")
.exchange()
.expectBody()
.jsonPath("$.composers[0].name").value(
String.class,
name -> assertThat(name).isEqualTo("Johann Sebastian Bach"))
.jsonPath("$.composers[0].name").value(
ParameterizedTypeReference.forType(String.class),
name -> assertThat(name).isEqualTo("Johann Sebastian Bach"));
}
@Test
void hamcrestMatcher() {
client.get().uri("/music/people")
@@ -26,7 +26,7 @@ import org.apache.commons.logging.LogFactory;
import org.jspecify.annotations.Nullable;
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.util.ClassUtils;
@@ -163,8 +163,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 {
@@ -57,10 +57,17 @@ class DefaultHttpMessageConverters implements HttpMessageConverters {
private final List<HttpMessageConverter<?>> messageConverters;
DefaultHttpMessageConverters(List<HttpMessageConverter<?>> messageConverters) {
this.messageConverters = messageConverters;
}
@Override
public boolean isEmpty() {
return this.messageConverters.isEmpty();
}
@Override
public Iterator<HttpMessageConverter<?>> iterator() {
return this.messageConverters.iterator();
@@ -32,6 +32,11 @@ import java.util.function.Consumer;
*/
public interface HttpMessageConverters extends Iterable<HttpMessageConverter<?>> {
/**
* Return true if this instance does not contain any message converters.
*/
boolean isEmpty();
/**
* Create a builder instance, tailored for HTTP client usage.
* <p>The following HTTP message converters can be detected and registered if available, in order:
@@ -25,12 +25,11 @@ import org.springframework.http.ProblemDetail;
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
* (as the plain ServletException doesn't expose its root cause at all).
* <p>Extends {@link ServletException} for convenient throwing in any Servlet
* resource (such as a Filter).
*
* @author Rod Johnson
* @author Juergen Hoeller
@@ -38,12 +37,12 @@ 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;
private final Object @Nullable [] messageDetailArguments;
private @Nullable ProblemDetail body;
/**
* Constructor with a message only.
@@ -95,6 +94,7 @@ public class ServletRequestBindingException extends ServletException implements
this.messageDetailArguments = messageDetailArguments;
}
private String initMessageDetailCode(@Nullable String messageDetailCode) {
return (messageDetailCode != null ?
messageDetailCode : ErrorResponse.getDefaultDetailMessageCode(getClass(), null));
@@ -107,7 +107,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) {
@@ -21,9 +21,7 @@ import org.springframework.http.HttpEntity
import org.springframework.http.HttpMethod
import org.springframework.http.RequestEntity
import org.springframework.http.ResponseEntity
import java.lang.Class
import java.net.URI
import kotlin.reflect.KClass
/**
* Extension for [RestOperations.getForObject] providing a `getForObject<Foo>(...)`
@@ -36,8 +34,8 @@ import kotlin.reflect.KClass
* @since 5.0
*/
@Throws(RestClientException::class)
inline fun <reified T> RestOperations.getForObject(url: String, vararg uriVariables: Any?): T =
getForObject(url, T::class.java as Class<*>, *uriVariables) as T
inline fun <reified T : Any> RestOperations.getForObject(url: String, vararg uriVariables: Any?): T? =
getForObject(url, T::class.java, *uriVariables)
/**
* Extension for [RestOperations.getForObject] providing a `getForObject<Foo>(...)`
@@ -50,8 +48,8 @@ inline fun <reified T> RestOperations.getForObject(url: String, vararg uriVariab
* @since 5.0
*/
@Throws(RestClientException::class)
inline fun <reified T> RestOperations.getForObject(url: String, uriVariables: Map<String, Any?>): T =
getForObject(url, T::class.java as Class<*>, uriVariables) as T
inline fun <reified T : Any> RestOperations.getForObject(url: String, uriVariables: Map<String, Any?>): T? =
getForObject(url, T::class.java, uriVariables)
/**
* Extension for [RestOperations.getForObject] providing a `getForObject<Foo>(...)`
@@ -64,8 +62,8 @@ inline fun <reified T> RestOperations.getForObject(url: String, uriVariables: Ma
* @since 5.0
*/
@Throws(RestClientException::class)
inline fun <reified T> RestOperations.getForObject(url: URI): T =
getForObject(url, T::class.java as Class<*>) as T
inline fun <reified T : Any> RestOperations.getForObject(url: URI): T? =
getForObject(url, T::class.java)
/**
* Extension for [RestOperations.getForEntity] providing a `getForEntity<Foo>(...)`
@@ -118,9 +116,9 @@ inline fun <reified T: Any> RestOperations.getForEntity(url: String, uriVariable
* @since 5.0.2
*/
@Throws(RestClientException::class)
inline fun <reified T> RestOperations.patchForObject(url: String, request: Any? = null,
vararg uriVariables: Any?): T =
patchForObject(url, request, T::class.java as Class<*>, *uriVariables) as T
inline fun <reified T : Any> RestOperations.patchForObject(url: String, request: Any? = null,
vararg uriVariables: Any?): T? =
patchForObject(url, request, T::class.java, *uriVariables)
/**
* Extension for [RestOperations.patchForObject] providing a `patchForObject<Foo>(...)`
@@ -132,9 +130,9 @@ inline fun <reified T> RestOperations.patchForObject(url: String, request: Any?
* @since 5.0.2
*/
@Throws(RestClientException::class)
inline fun <reified T> RestOperations.patchForObject(url: String, request: Any? = null,
uriVariables: Map<String, *>): T =
patchForObject(url, request, T::class.java as Class<*>, uriVariables) as T
inline fun <reified T : Any> RestOperations.patchForObject(url: String, request: Any? = null,
uriVariables: Map<String, *>): T? =
patchForObject(url, request, T::class.java, uriVariables)
/**
* Extension for [RestOperations.patchForObject] providing a `patchForObject<Foo>(...)`
@@ -146,8 +144,8 @@ inline fun <reified T> RestOperations.patchForObject(url: String, request: Any?
* @since 5.0.2
*/
@Throws(RestClientException::class)
inline fun <reified T> RestOperations.patchForObject(url: URI, request: Any? = null): T =
patchForObject(url, request, T::class.java as Class<*>) as T
inline fun <reified T : Any> RestOperations.patchForObject(url: URI, request: Any? = null): T? =
patchForObject(url, request, T::class.java)
/**
* Extension for [RestOperations.postForObject] providing a `postForObject<Foo>(...)`
@@ -160,9 +158,9 @@ inline fun <reified T> RestOperations.patchForObject(url: URI, request: Any? = n
* @since 5.0
*/
@Throws(RestClientException::class)
inline fun <reified T> RestOperations.postForObject(url: String, request: Any? = null,
vararg uriVariables: Any?): T =
postForObject(url, request, T::class.java as Class<*>, *uriVariables) as T
inline fun <reified T : Any> RestOperations.postForObject(url: String, request: Any? = null,
vararg uriVariables: Any?): T? =
postForObject(url, request, T::class.java, *uriVariables)
/**
* Extension for [RestOperations.postForObject] providing a `postForObject<Foo>(...)`
@@ -175,9 +173,9 @@ inline fun <reified T> RestOperations.postForObject(url: String, request: Any? =
* @since 5.0
*/
@Throws(RestClientException::class)
inline fun <reified T> RestOperations.postForObject(url: String, request: Any? = null,
uriVariables: Map<String, *>): T =
postForObject(url, request, T::class.java as Class<*>, uriVariables) as T
inline fun <reified T : Any> RestOperations.postForObject(url: String, request: Any? = null,
uriVariables: Map<String, *>): T? =
postForObject(url, request, T::class.java, uriVariables)
/**
* Extension for [RestOperations.postForObject] providing a `postForObject<Foo>(...)`
@@ -190,8 +188,8 @@ inline fun <reified T> RestOperations.postForObject(url: String, request: Any? =
* @since 5.0
*/
@Throws(RestClientException::class)
inline fun <reified T> RestOperations.postForObject(url: URI, request: Any? = null): T =
postForObject(url, request, T::class.java as Class<*>) as T
inline fun <reified T : Any> RestOperations.postForObject(url: URI, request: Any? = null): T? =
postForObject(url, request, T::class.java)
/**
* Extension for [RestOperations.postForEntity] providing a `postForEntity<Foo>(...)`
@@ -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()).isTrue();
}
@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.");
@@ -270,7 +270,6 @@ class RestOperationsExtensionsTests {
}
@Test
@Disabled("May require Kotlin 2") // TODO Enable after Kotlin 2 upgrade
fun `RestOperations are available`() {
val extensions = Class.forName("org.springframework.web.client.RestOperationsExtensionsKt")
ReflectionUtils.doWithMethods(RestOperations::class.java) { method ->
@@ -281,7 +280,7 @@ class RestOperationsExtensionsTests {
assertThat(f.typeParameters.size).isEqualTo(1)
val type = f.typeParameters[0].upperBounds.first()
assertThat(type.classifier).isEqualTo(Any::class)
assertThat(type.isMarkedNullable).isTrue()
assertThat(type.isMarkedNullable).isFalse()
}
}
}
@@ -113,6 +113,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"/>