Compare commits

..

28 Commits

Author SHA1 Message Date
Stéphane Nicoll 8f64480c9f Release v6.2.10 2025-08-14 09:15:43 +02:00
Stéphane Nicoll edda4731e1 Build against Java 24
Closes gh-35326
2025-08-14 09:06:18 +02:00
Stéphane Nicoll 9fa2d7d190 Upgrade to Jackson 2.18.4.1
Closes gh-35322
2025-08-14 08:38:58 +02:00
Stéphane Nicoll c30427fd4e Upgrade to Netty 4.1.124.Final
Closes gh-35321
2025-08-14 08:38:34 +02:00
Juergen Hoeller 1d908f1847 Upgrade to Reactor 2024.0.9 and Micrometer 1.14.10
Includes Groovy 4.0.28, JRuby 9.4.13, Jetty 12.0.25, Caffeine 3.2.2, Protobuf 4.31.1, Selenium 4.35, HtmlUnit 4.14

Closes gh-35312
Closes gh-35313
2025-08-13 00:04:31 +02:00
Sam Brannen 37b076be51 Support multiple result sets in ScriptUtils.executeSqlScript()
Prior to this commit, ScriptUtils.executeSqlScript() treated every
statement within the script as if it were a single insert/update/delete
statement. This disregarded the fact that the execution of a JDBC
Statement can result in multiple individual statements, some of which
result in a ResultSet and others that result in an update count.

For example, when executing a stored procedure on Sybase, ScriptUtils
did not execute all statements within the stored procedure.

To address that, this commit revises the implementation of
ScriptUtils.executeSqlScript() so that it handles multiple results and
differentiates between result sets and update counts.

Closes gh-35248
2025-08-11 17:44:39 +03:00
Juergen Hoeller a9453a5959 Polishing 2025-08-11 14:32:45 +02:00
Juergen Hoeller 3781ba223e Optimize NIO path resolution in PathEditor
Closes gh-35304
2025-08-11 14:32:39 +02:00
Sam Brannen f11a1e6f82 Polish tests 2025-08-11 11:27:10 +03:00
rstoyanchev ffc785471b Fix checkstyle error 2025-08-08 12:31:13 +01:00
rstoyanchev 6e2fbfe108 Polishing contribution
Closes gh-35232
2025-08-08 11:50:07 +01:00
秦利斌 968e037503 Add documentation of RequestMapping about SpEL
Signed-off-by: 秦利斌 <68638598+Allan-QLB@users.noreply.github.com>
2025-08-08 11:37:25 +01:00
rstoyanchev f0a9f649c1 Allow null in ProblemDetail#type
See gh-35294
2025-08-08 11:36:53 +01:00
rstoyanchev 600d6c6fc0 Update contribution
Closes gh-34721
2025-08-08 11:31:21 +01:00
giampaolo 7a55ce48a9 Handle CancellationException in JdkClientHttpRequest
Handle CancellationException in order to throw an HttpTimeoutException
when the timeout handler caused the cancellation.

See gh-34721

Signed-off-by: giampaolo <giampaorr@gmail.com>

fix: use timeoutHandler with a flag isTimeout

    Closes gh-33973

    Signed-off-by: giampaolo <giampaorr@gmail.com>
2025-08-08 11:31:21 +01:00
Juergen Hoeller 5df9fd4eff Polishing (aligned with main) 2025-08-06 19:01:18 +02:00
Juergen Hoeller da13a24604 Allow any @Transactional propagation for listener with BEFORE_COMMIT phase
Closes gh-35150
2025-08-01 21:15:56 +02:00
Juergen Hoeller 67e88f3c20 Align task execution tracking and thread interruption on shutdown
Closes gh-35254
2025-08-01 21:15:25 +02:00
Patrick Strawderman 24e66b63d1 Refine StringUtils#uriDecode and update documentation
Refine the StringUtils#uriDecode method in the following ways:
- Use a StringBuilder instead of ByteArrayOutputStream, and only decode
  %-encoded sequences.
- Use HexFormat.fromHexDigits to decode hex sequences.
- Decode to a byte array that is only allocated if encoded sequences are
  encountered.

This commit adds another optimization mainly for the use case where
there is no encoded sequence, and updates the Javadoc of both
StringUtils#uriDecode and UriUtils#decode to match the implementation.

Signed-off-by: Patrick Strawderman <pstrawderman@netflix.com>
Co-Authored-by: Sebastien Deleuze <sebastien.deleuze@broadcom.com>

Closes gh-35253
2025-07-29 21:44:26 +02:00
Juergen Hoeller f3832c7262 Add note on SQL types with SqlBinaryValue/SqlCharacterValue
Closes gh-34786
2025-07-28 22:06:38 +02:00
Juergen Hoeller 16e99f289c Accept support for generated keys column name array on HSQLDB/Derby
Closes gh-34790
2025-07-28 22:04:18 +02:00
Juergen Hoeller 642e554c52 Process PostgreSQL-returned catalog/schema names in given case
Closes gh-35064
2025-07-28 20:28:45 +02:00
Juergen Hoeller 4f6304707d Polishing 2025-07-25 22:40:15 +02:00
Juergen Hoeller 3c112703d9 Introduce useCaches flag on UrlResource (for URLConnection access)
Propagated from PathMatchingResourcePatternResolver's setUseCaches.

Closes gh-35218
2025-07-25 22:40:09 +02:00
Juergen Hoeller 8c44a61033 Invalidate cache entries for matching types after singleton creation
Closes gh-35239
2025-07-25 22:38:57 +02:00
Sébastien Deleuze 5e338ef1b8 Make MessageSource locale parameter nullable
Closes gh-35230
2025-07-23 16:02:38 +02:00
Sam Brannen 445da24631 Upgrade to JUnit 5.13.4
Closes gh-35229
2025-07-21 13:17:38 +03:00
Brian Clozel 6d97791f5e Next development version (v6.2.10-SNAPSHOT) 2025-07-17 09:55:21 +02:00
49 changed files with 604 additions and 250 deletions
+1 -1
View File
@@ -21,7 +21,7 @@ jobs:
toolchain: false
- version: 21
toolchain: true
- version: 23
- version: 24
toolchain: true
exclude:
- os:
+1 -1
View File
@@ -97,7 +97,7 @@ configure([rootProject] + javaProjects) { project ->
// TODO Uncomment link to JUnit 5 docs once we execute Gradle with Java 18+.
// See https://github.com/spring-projects/spring-framework/issues/27497
//
// "https://junit.org/junit5/docs/5.13.3/api/",
// "https://junit.org/junit5/docs/5.13.4/api/",
"https://www.reactive-streams.org/reactive-streams-1.0.3-javadoc/",
//"https://javadoc.io/static/io.rsocket/rsocket-core/1.1.1/",
"https://r2dbc.io/spec/1.0.0.RELEASE/api/",
@@ -1,24 +1,26 @@
[[beans-classpath-scanning]]
= Classpath Scanning and Managed Components
Most examples in this chapter use XML to specify the configuration metadata that produces
each `BeanDefinition` within the Spring container. The previous section
(xref:core/beans/annotation-config.adoc[Annotation-based Container Configuration]) demonstrates how to provide a lot of the configuration
metadata through source-level annotations. Even in those examples, however, the "base"
bean definitions are explicitly defined in the XML file, while the annotations drive only
the dependency injection. This section describes an option for implicitly detecting the
candidate components by scanning the classpath. Candidate components are classes that
match against a filter criteria and have a corresponding bean definition registered with
the container. This removes the need to use XML to perform bean registration. Instead, you
can use annotations (for example, `@Component`), AspectJ type expressions, or your own
Most examples in this chapter use XML to specify the configuration metadata that
produces each `BeanDefinition` within the Spring container. The previous section
(xref:core/beans/annotation-config.adoc[Annotation-based Container Configuration])
demonstrates how to provide a lot of the configuration metadata through source-level
annotations. Even in those examples, however, the "base" bean definitions are explicitly
defined in the XML file, while the annotations drive only the dependency injection.
This section describes an option for implicitly detecting the candidate components by
scanning the classpath. Candidate components are classes that match against a filter
criteria and have a corresponding bean definition registered with the container.
This removes the need to use XML to perform bean registration. Instead, you can use
annotations (for example, `@Component`), AspectJ type expressions, or your own
custom filter criteria to select which classes have bean definitions registered with
the container.
[NOTE]
====
You can define beans using Java rather than using XML files. Take a look at the
`@Configuration`, `@Bean`, `@Import`, and `@DependsOn` annotations for examples of how to
use these features.
`@Configuration`, `@Bean`, `@Import`, and `@DependsOn` annotations for examples
of how to use these features.
====
@@ -830,10 +832,10 @@ definitions, there is no notion of bean definition inheritance, and inheritance
hierarchies at the class level are irrelevant for metadata purposes.
For details on web-specific scopes such as "`request`" or "`session`" in a Spring context,
see xref:core/beans/factory-scopes.adoc#beans-factory-scopes-other[Request, Session, Application, and WebSocket Scopes]. As with the pre-built annotations for those scopes,
you may also compose your own scoping annotations by using Spring's meta-annotation
approach: for example, a custom annotation meta-annotated with `@Scope("prototype")`,
possibly also declaring a custom scoped-proxy mode.
see xref:core/beans/factory-scopes.adoc#beans-factory-scopes-other[Request, Session, Application, and WebSocket Scopes].
As with the pre-built annotations for those scopes, you may also compose your own scoping
annotations by using Spring's meta-annotation approach: for example, a custom annotation
meta-annotated with `@Scope("prototype")`, possibly also declaring a custom scoped-proxy mode.
NOTE: To provide a custom strategy for scope resolution rather than relying on the
annotation-based approach, you can implement the
@@ -875,7 +877,8 @@ Kotlin::
----
When using certain non-singleton scopes, it may be necessary to generate proxies for the
scoped objects. The reasoning is described in xref:core/beans/factory-scopes.adoc#beans-factory-scopes-other-injection[Scoped Beans as Dependencies].
scoped objects. The reasoning is described in
xref:core/beans/factory-scopes.adoc#beans-factory-scopes-other-injection[Scoped Beans as Dependencies].
For this purpose, a scoped-proxy attribute is available on the component-scan
element. The three possible values are: `no`, `interfaces`, and `targetClass`. For example,
the following configuration results in standard JDK dynamic proxies:
@@ -234,10 +234,13 @@ Kotlin::
======
--
URI path patterns can also have embedded `${...}` placeholders that are resolved on startup
by using `PropertySourcesPlaceholderConfigurer` against local, system, environment, and
other property sources. You can use this, for example, to parameterize a base URL based on
some external configuration.
URI path patterns can also have:
- Embedded `${...}` placeholders that are resolved on startup via
`PropertySourcesPlaceholderConfigurer` against local, system, environment, and
other property sources. This is useful, for example, to parameterize a base URL based on
external configuration.
- SpEL expressions `#{...}`.
NOTE: Spring WebFlux uses `PathPattern` and the `PathPatternParser` for URI path matching support.
Both classes are located in `spring-web` and are expressly designed for use with HTTP URL
@@ -217,10 +217,13 @@ Kotlin::
----
======
URI path patterns can also have embedded `${...}` placeholders that are resolved on startup
by using `PropertySourcesPlaceholderConfigurer` against local, system, environment, and
other property sources. You can use this, for example, to parameterize a base URL based on
some external configuration.
URI path patterns can also have:
- Embedded `${...}` placeholders that are resolved on startup via
`PropertySourcesPlaceholderConfigurer` against local, system, environment, and
other property sources. This is useful, for example, to parameterize a base URL based on
external configuration.
- SpEL expression `#{...}`.
[[mvc-ann-requestmapping-pattern-comparison]]
+14 -14
View File
@@ -7,31 +7,31 @@ javaPlatform {
}
dependencies {
api(platform("com.fasterxml.jackson:jackson-bom:2.18.4"))
api(platform("io.micrometer:micrometer-bom:1.14.9"))
api(platform("io.netty:netty-bom:4.1.123.Final"))
api(platform("com.fasterxml.jackson:jackson-bom:2.18.4.1"))
api(platform("io.micrometer:micrometer-bom:1.14.10"))
api(platform("io.netty:netty-bom:4.1.124.Final"))
api(platform("io.netty:netty5-bom:5.0.0.Alpha5"))
api(platform("io.projectreactor:reactor-bom:2024.0.8"))
api(platform("io.projectreactor:reactor-bom:2024.0.9"))
api(platform("io.rsocket:rsocket-bom:1.1.5"))
api(platform("org.apache.groovy:groovy-bom:4.0.27"))
api(platform("org.apache.groovy:groovy-bom:4.0.28"))
api(platform("org.apache.logging.log4j:log4j-bom:2.21.1"))
api(platform("org.assertj:assertj-bom:3.27.3"))
api(platform("org.eclipse.jetty:jetty-bom:12.0.23"))
api(platform("org.eclipse.jetty.ee10:jetty-ee10-bom:12.0.23"))
api(platform("org.eclipse.jetty:jetty-bom:12.0.25"))
api(platform("org.eclipse.jetty.ee10:jetty-ee10-bom:12.0.25"))
api(platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.8.1"))
api(platform("org.jetbrains.kotlinx:kotlinx-serialization-bom:1.6.3"))
api(platform("org.junit:junit-bom:5.13.3"))
api(platform("org.junit:junit-bom:5.13.4"))
api(platform("org.mockito:mockito-bom:5.18.0"))
constraints {
api("com.fasterxml:aalto-xml:1.3.2")
api("com.fasterxml.woodstox:woodstox-core:6.7.0")
api("com.github.ben-manes.caffeine:caffeine:3.2.1")
api("com.github.ben-manes.caffeine:caffeine:3.2.2")
api("com.github.librepdf:openpdf:1.3.43")
api("com.google.code.findbugs:findbugs:3.0.1")
api("com.google.code.findbugs:jsr305:3.0.2")
api("com.google.code.gson:gson:2.13.1")
api("com.google.protobuf:protobuf-java-util:4.30.2")
api("com.google.protobuf:protobuf-java-util:4.31.1")
api("com.h2database:h2:2.3.232")
api("com.jayway.jsonpath:json-path:2.9.0")
api("com.oracle.database.jdbc:ojdbc11:21.9.0.0")
@@ -129,16 +129,16 @@ dependencies {
api("org.hibernate:hibernate-core-jakarta:5.6.15.Final")
api("org.hibernate:hibernate-validator:7.0.5.Final")
api("org.hsqldb:hsqldb:2.7.4")
api("org.htmlunit:htmlunit:4.13.0")
api("org.htmlunit:htmlunit:4.14.0")
api("org.javamoney:moneta:1.4.4")
api("org.jruby:jruby:9.4.12.0")
api("org.jruby:jruby:9.4.13.0")
api("org.junit.support:testng-engine:1.0.5")
api("org.mozilla:rhino:1.7.15")
api("org.ogce:xpp3:1.1.6")
api("org.python:jython-standalone:2.7.4")
api("org.quartz-scheduler:quartz:2.3.2")
api("org.seleniumhq.selenium:htmlunit3-driver:4.33.0")
api("org.seleniumhq.selenium:selenium-java:4.34.0")
api("org.seleniumhq.selenium:htmlunit3-driver:4.34.0")
api("org.seleniumhq.selenium:selenium-java:4.35.0")
api("org.skyscreamer:jsonassert:1.5.3")
api("org.slf4j:slf4j-api:2.0.17")
api("org.testng:testng:7.11.0")
+1 -1
View File
@@ -1,4 +1,4 @@
version=6.2.9
version=6.2.10
org.gradle.caching=true
org.gradle.jvmargs=-Xmx2048m
@@ -121,12 +121,12 @@ public abstract class AopConfigUtils {
Assert.notNull(registry, "BeanDefinitionRegistry must not be null");
if (registry.containsBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME)) {
BeanDefinition apcDefinition = registry.getBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME);
if (!cls.getName().equals(apcDefinition.getBeanClassName())) {
int currentPriority = findPriorityForClass(apcDefinition.getBeanClassName());
BeanDefinition beanDefinition = registry.getBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME);
if (!cls.getName().equals(beanDefinition.getBeanClassName())) {
int currentPriority = findPriorityForClass(beanDefinition.getBeanClassName());
int requiredPriority = findPriorityForClass(cls);
if (currentPriority < requiredPriority) {
apcDefinition.setBeanClassName(cls.getName());
beanDefinition.setBeanClassName(cls.getName());
}
}
return null;
@@ -134,8 +134,8 @@ public abstract class AopConfigUtils {
RootBeanDefinition beanDefinition = new RootBeanDefinition(cls);
beanDefinition.setSource(source);
beanDefinition.getPropertyValues().add("order", Ordered.HIGHEST_PRECEDENCE);
beanDefinition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
beanDefinition.getPropertyValues().add("order", Ordered.HIGHEST_PRECEDENCE);
registry.registerBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME, beanDefinition);
return beanDefinition;
}
@@ -117,6 +117,7 @@ public abstract class AbstractAdvisingBeanPostProcessor extends ProxyProcessorSu
}
proxyFactory.addAdvisor(this.advisor);
customizeProxyFactory(proxyFactory);
proxyFactory.setPreFiltered(true);
// Use original ClassLoader if bean class not locally loaded in overriding class loader
ClassLoader classLoader = getProxyClassLoader();
@@ -24,9 +24,9 @@ import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.lang.Nullable;
/**
* Extension of {@link AbstractAutoProxyCreator} which implements {@link BeanFactoryAware},
* adds exposure of the original target class for each proxied bean
* ({@link AutoProxyUtils#ORIGINAL_TARGET_CLASS_ATTRIBUTE}),
* Extension of {@link AbstractAdvisingBeanPostProcessor} which implements
* {@link BeanFactoryAware}, adds exposure of the original target class for each
* proxied bean ({@link AutoProxyUtils#ORIGINAL_TARGET_CLASS_ATTRIBUTE}),
* and participates in an externally enforced target-class mode for any given bean
* ({@link AutoProxyUtils#PRESERVE_TARGET_CLASS_ATTRIBUTE}).
* This post-processor is therefore aligned with {@link AbstractAutoProxyCreator}.
@@ -1455,11 +1455,18 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
}
}
@Override
protected void addSingleton(String beanName, Object singletonObject) {
super.addSingleton(beanName, singletonObject);
Predicate<Class<?>> filter = (beanType -> beanType != Object.class && beanType.isInstance(singletonObject));
this.allBeanNamesByType.keySet().removeIf(filter);
this.singletonBeanNamesByType.keySet().removeIf(filter);
}
@Override
public void registerSingleton(String beanName, Object singletonObject) throws IllegalStateException {
super.registerSingleton(beanName, singletonObject);
updateManualSingletonNames(set -> set.add(beanName), set -> !this.beanDefinitionMap.containsKey(beanName));
clearByTypeCache();
}
@Override
@@ -103,7 +103,7 @@ public class PathEditor extends PropertyEditorSupport {
if (resource == null) {
setValue(null);
}
else if (nioPathCandidate && !resource.exists()) {
else if (nioPathCandidate && (!resource.isFile() || !resource.exists())) {
setValue(Paths.get(text).normalize());
}
else {
@@ -3202,6 +3202,29 @@ class DefaultListableBeanFactoryTests {
assertThat(holder.getNonPublicEnum()).isEqualTo(NonPublicEnum.VALUE_1);
}
@Test
void mostSpecificCacheEntryForTypeMatching() {
RootBeanDefinition bd1 = new RootBeanDefinition();
bd1.setFactoryBeanName("config");
bd1.setFactoryMethodName("create");
lbf.registerBeanDefinition("config", new RootBeanDefinition(BeanWithFactoryMethod.class));
lbf.registerBeanDefinition("bd1", bd1);
lbf.registerBeanDefinition("bd2", new RootBeanDefinition(NestedTestBean.class));
lbf.freezeConfiguration();
String[] allBeanNames = lbf.getBeanNamesForType(Object.class);
String[] nestedBeanNames = lbf.getBeanNamesForType(NestedTestBean.class);
assertThat(lbf.getType("bd1")).isEqualTo(TestBean.class);
assertThat(lbf.getBeanNamesForType(TestBean.class)).containsExactly("bd1");
assertThat(lbf.getBeanNamesForType(DerivedTestBean.class)).isEmpty();
lbf.getBean("bd1");
assertThat(lbf.getType("bd1")).isEqualTo(DerivedTestBean.class);
assertThat(lbf.getBeanNamesForType(TestBean.class)).containsExactly("bd1");
assertThat(lbf.getBeanNamesForType(DerivedTestBean.class)).containsExactly("bd1");
assertThat(lbf.getBeanNamesForType(NestedTestBean.class)).isSameAs(nestedBeanNames);
assertThat(lbf.getBeanNamesForType(Object.class)).isSameAs(allBeanNames);
}
private int registerBeanDefinitions(Properties p) {
return registerBeanDefinitions(p, null);
@@ -3418,7 +3441,7 @@ class DefaultListableBeanFactoryTests {
}
public TestBean create() {
TestBean tb = new TestBean();
DerivedTestBean tb = new DerivedTestBean();
tb.setName(this.name);
return tb;
}
@@ -3646,11 +3669,11 @@ class DefaultListableBeanFactoryTests {
private FactoryBean<?> factoryBean;
public final FactoryBean<?> getFactoryBean() {
public FactoryBean<?> getFactoryBean() {
return this.factoryBean;
}
public final void setFactoryBean(final FactoryBean<?> factoryBean) {
public void setFactoryBean(FactoryBean<?> factoryBean) {
this.factoryBean = factoryBean;
}
}
@@ -55,7 +55,7 @@ public interface MessageSource {
* @see java.text.MessageFormat
*/
@Nullable
String getMessage(String code, @Nullable Object[] args, @Nullable String defaultMessage, Locale locale);
String getMessage(String code, @Nullable Object[] args, @Nullable String defaultMessage, @Nullable Locale locale);
/**
* Try to resolve the message. Treat as an error if the message can't be found.
@@ -71,7 +71,7 @@ public interface MessageSource {
* @see #getMessage(MessageSourceResolvable, Locale)
* @see java.text.MessageFormat
*/
String getMessage(String code, @Nullable Object[] args, Locale locale) throws NoSuchMessageException;
String getMessage(String code, @Nullable Object[] args, @Nullable Locale locale) throws NoSuchMessageException;
/**
* Try to resolve the message using all the attributes contained within the
@@ -91,6 +91,6 @@ public interface MessageSource {
* @see MessageSourceResolvable#getDefaultMessage()
* @see java.text.MessageFormat
*/
String getMessage(MessageSourceResolvable resolvable, Locale locale) throws NoSuchMessageException;
String getMessage(MessageSourceResolvable resolvable, @Nullable Locale locale) throws NoSuchMessageException;
}
@@ -51,9 +51,9 @@ import org.springframework.core.annotation.AliasFor;
* ignored.
*
* @author Stephane Nicoll
* @since 6.2
* @see Reflective @Reflective
* @see RegisterReflection @RegisterReflection
* @since 6.2
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@@ -1502,17 +1502,17 @@ public abstract class AbstractApplicationContext extends DefaultResourceLoader
@Override
@Nullable
public String getMessage(String code, @Nullable Object[] args, @Nullable String defaultMessage, Locale locale) {
public String getMessage(String code, @Nullable Object[] args, @Nullable String defaultMessage, @Nullable Locale locale) {
return getMessageSource().getMessage(code, args, defaultMessage, locale);
}
@Override
public String getMessage(String code, @Nullable Object[] args, Locale locale) throws NoSuchMessageException {
public String getMessage(String code, @Nullable Object[] args, @Nullable Locale locale) throws NoSuchMessageException {
return getMessageSource().getMessage(code, args, locale);
}
@Override
public String getMessage(MessageSourceResolvable resolvable, Locale locale) throws NoSuchMessageException {
public String getMessage(MessageSourceResolvable resolvable, @Nullable Locale locale) throws NoSuchMessageException {
return getMessageSource().getMessage(resolvable, locale);
}
@@ -138,7 +138,7 @@ public abstract class AbstractMessageSource extends MessageSourceSupport impleme
@Override
@Nullable
public final String getMessage(String code, @Nullable Object[] args, @Nullable String defaultMessage, Locale locale) {
public final String getMessage(String code, @Nullable Object[] args, @Nullable String defaultMessage, @Nullable Locale locale) {
String msg = getMessageInternal(code, args, locale);
if (msg != null) {
return msg;
@@ -150,7 +150,7 @@ public abstract class AbstractMessageSource extends MessageSourceSupport impleme
}
@Override
public final String getMessage(String code, @Nullable Object[] args, Locale locale) throws NoSuchMessageException {
public final String getMessage(String code, @Nullable Object[] args, @Nullable Locale locale) throws NoSuchMessageException {
String msg = getMessageInternal(code, args, locale);
if (msg != null) {
return msg;
@@ -159,11 +159,16 @@ public abstract class AbstractMessageSource extends MessageSourceSupport impleme
if (fallback != null) {
return fallback;
}
throw new NoSuchMessageException(code, locale);
if (locale == null ) {
throw new NoSuchMessageException(code);
}
else {
throw new NoSuchMessageException(code, locale);
}
}
@Override
public final String getMessage(MessageSourceResolvable resolvable, Locale locale) throws NoSuchMessageException {
public final String getMessage(MessageSourceResolvable resolvable, @Nullable Locale locale) throws NoSuchMessageException {
String[] codes = resolvable.getCodes();
if (codes != null) {
for (String code : codes) {
@@ -177,7 +182,13 @@ public abstract class AbstractMessageSource extends MessageSourceSupport impleme
if (defaultMessage != null) {
return defaultMessage;
}
throw new NoSuchMessageException(!ObjectUtils.isEmpty(codes) ? codes[codes.length - 1] : "", locale);
String code = !ObjectUtils.isEmpty(codes) ? codes[codes.length - 1] : "";
if (locale == null ) {
throw new NoSuchMessageException(code);
}
else {
throw new NoSuchMessageException(code, locale);
}
}
@@ -284,7 +295,7 @@ public abstract class AbstractMessageSource extends MessageSourceSupport impleme
* @see #getDefaultMessage(String)
*/
@Nullable
protected String getDefaultMessage(MessageSourceResolvable resolvable, Locale locale) {
protected String getDefaultMessage(MessageSourceResolvable resolvable, @Nullable Locale locale) {
String defaultMessage = resolvable.getDefaultMessage();
String[] codes = resolvable.getCodes();
if (defaultMessage != null) {
@@ -331,7 +342,7 @@ public abstract class AbstractMessageSource extends MessageSourceSupport impleme
* @return an array of arguments with any MessageSourceResolvables resolved
*/
@Override
protected Object[] resolveArguments(@Nullable Object[] args, Locale locale) {
protected Object[] resolveArguments(@Nullable Object[] args, @Nullable Locale locale) {
if (ObjectUtils.isEmpty(args)) {
return super.resolveArguments(args, locale);
}
@@ -55,7 +55,7 @@ public class DelegatingMessageSource extends MessageSourceSupport implements Hie
@Override
@Nullable
public String getMessage(String code, @Nullable Object[] args, @Nullable String defaultMessage, Locale locale) {
public String getMessage(String code, @Nullable Object[] args, @Nullable String defaultMessage, @Nullable Locale locale) {
if (this.parentMessageSource != null) {
return this.parentMessageSource.getMessage(code, args, defaultMessage, locale);
}
@@ -68,17 +68,22 @@ public class DelegatingMessageSource extends MessageSourceSupport implements Hie
}
@Override
public String getMessage(String code, @Nullable Object[] args, Locale locale) throws NoSuchMessageException {
public String getMessage(String code, @Nullable Object[] args, @Nullable Locale locale) throws NoSuchMessageException {
if (this.parentMessageSource != null) {
return this.parentMessageSource.getMessage(code, args, locale);
}
else {
throw new NoSuchMessageException(code, locale);
if (locale == null) {
throw new NoSuchMessageException(code);
}
else {
throw new NoSuchMessageException(code, locale);
}
}
}
@Override
public String getMessage(MessageSourceResolvable resolvable, Locale locale) throws NoSuchMessageException {
public String getMessage(MessageSourceResolvable resolvable, @Nullable Locale locale) throws NoSuchMessageException {
if (this.parentMessageSource != null) {
return this.parentMessageSource.getMessage(resolvable, locale);
}
@@ -88,7 +93,12 @@ public class DelegatingMessageSource extends MessageSourceSupport implements Hie
}
String[] codes = resolvable.getCodes();
String code = (codes != null && codes.length > 0 ? codes[0] : "");
throw new NoSuchMessageException(code, locale);
if (locale == null) {
throw new NoSuchMessageException(code);
}
else {
throw new NoSuchMessageException(code, locale);
}
}
}
@@ -98,7 +98,7 @@ public abstract class MessageSourceSupport {
* @return the rendered default message (with resolved arguments)
* @see #formatMessage(String, Object[], java.util.Locale)
*/
protected String renderDefaultMessage(String defaultMessage, @Nullable Object[] args, Locale locale) {
protected String renderDefaultMessage(String defaultMessage, @Nullable Object[] args, @Nullable Locale locale) {
return formatMessage(defaultMessage, args, locale);
}
@@ -112,7 +112,7 @@ public abstract class MessageSourceSupport {
* @param locale the Locale used for formatting
* @return the formatted message (with resolved arguments)
*/
protected String formatMessage(String msg, @Nullable Object[] args, Locale locale) {
protected String formatMessage(String msg, @Nullable Object[] args, @Nullable Locale locale) {
if (!isAlwaysUseMessageFormat() && ObjectUtils.isEmpty(args)) {
return msg;
}
@@ -146,7 +146,7 @@ public abstract class MessageSourceSupport {
* @param locale the Locale to create a {@code MessageFormat} for
* @return the {@code MessageFormat} instance
*/
protected MessageFormat createMessageFormat(String msg, Locale locale) {
protected MessageFormat createMessageFormat(String msg, @Nullable Locale locale) {
return new MessageFormat(msg, locale);
}
@@ -158,7 +158,7 @@ public abstract class MessageSourceSupport {
* @param locale the Locale to resolve against
* @return the resolved argument array
*/
protected Object[] resolveArguments(@Nullable Object[] args, Locale locale) {
protected Object[] resolveArguments(@Nullable Object[] args, @Nullable Locale locale) {
return (args != null ? args : new Object[0]);
}
@@ -376,7 +376,7 @@ public class SimpleAsyncTaskScheduler extends SimpleAsyncTaskExecutor implements
@Override
public boolean isRunning() {
return this.triggerLifecycle.isRunning();
return (this.triggerLifecycle.isRunning() || this.fixedDelayLifecycle.isRunning());
}
@Override
@@ -361,12 +361,22 @@ public abstract class AbstractFileResolvingResource extends AbstractResource {
* @throws IOException if thrown from URLConnection methods
*/
protected void customizeConnection(URLConnection con) throws IOException {
ResourceUtils.useCachesIfNecessary(con);
useCachesIfNecessary(con);
if (con instanceof HttpURLConnection httpCon) {
customizeConnection(httpCon);
}
}
/**
* Apply {@link URLConnection#setUseCaches useCaches} if necessary.
* @param con the URLConnection to customize
* @since 6.2.10
* @see ResourceUtils#useCachesIfNecessary(URLConnection)
*/
void useCachesIfNecessary(URLConnection con) {
ResourceUtils.useCachesIfNecessary(con);
}
/**
* Customize the given {@link HttpURLConnection} before fetching the resource.
* <p>Can be overridden in subclasses for configuring request headers and timeouts.
@@ -109,7 +109,9 @@ public class FileUrlResource extends UrlResource implements WritableResource {
@Override
public Resource createRelative(String relativePath) throws MalformedURLException {
return new FileUrlResource(createRelativeURL(relativePath));
FileUrlResource resource = new FileUrlResource(createRelativeURL(relativePath));
resource.useCaches = this.useCaches;
return resource;
}
}
@@ -67,6 +67,12 @@ public class UrlResource extends AbstractFileResolvingResource {
@Nullable
private volatile String cleanedUrl;
/**
* Whether to use URLConnection caches ({@code null} means default).
*/
@Nullable
volatile Boolean useCaches;
/**
* Create a new {@code UrlResource} based on the given URL object.
@@ -216,11 +222,22 @@ public class UrlResource extends AbstractFileResolvingResource {
return cleanedUrl;
}
/**
* Set an explicit flag for {@link URLConnection#setUseCaches},
* to be applied for any {@link URLConnection} operation in this resource.
* <p>By default, caching will be applied only to jar resources.
* An explicit {@code true} flag applies caching to all resources, whereas an
* explicit {@code false} flag turns off caching for jar resources as well.
* @since 6.2.10
* @see ResourceUtils#useCachesIfNecessary
*/
public void setUseCaches(boolean useCaches) {
this.useCaches = useCaches;
}
/**
* This implementation opens an InputStream for the given URL.
* <p>It sets the {@code useCaches} flag to {@code false},
* mainly to avoid jar file locking on Windows.
* @see java.net.URL#openConnection()
* @see java.net.URLConnection#setUseCaches(boolean)
* @see java.net.URLConnection#getInputStream()
@@ -251,6 +268,17 @@ public class UrlResource extends AbstractFileResolvingResource {
}
}
@Override
void useCachesIfNecessary(URLConnection con) {
Boolean useCaches = this.useCaches;
if (useCaches != null) {
con.setUseCaches(useCaches);
}
else {
super.useCachesIfNecessary(con);
}
}
/**
* This implementation returns the underlying URL reference.
*/
@@ -305,7 +333,9 @@ public class UrlResource extends AbstractFileResolvingResource {
*/
@Override
public Resource createRelative(String relativePath) throws MalformedURLException {
return new UrlResource(createRelativeURL(relativePath));
UrlResource resource = new UrlResource(createRelativeURL(relativePath));
resource.useCaches = this.useCaches;
return resource;
}
/**
@@ -260,7 +260,8 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol
private PathMatcher pathMatcher = new AntPathMatcher();
private boolean useCaches = true;
@Nullable
private Boolean useCaches;
private final Map<String, Resource[]> rootDirCache = new ConcurrentHashMap<>();
@@ -342,10 +343,12 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol
* the {@link JarURLConnection} level as well as within this resolver instance.
* <p>Note that {@link JarURLConnection#setDefaultUseCaches} can be turned off
* independently. This resolver-level setting is designed to only enforce
* {@code JarURLConnection#setUseCaches(false)} if necessary but otherwise
* leaves the JVM-level default in place.
* {@code JarURLConnection#setUseCaches(true/false)} if necessary but otherwise
* leaves the JVM-level default in place (if this setter has not been called).
* <p>As of 6.2.10, this setting propagates to {@link UrlResource#setUseCaches}.
* @since 6.1.19
* @see JarURLConnection#setUseCaches
* @see UrlResource#setUseCaches
* @see #clearCache()
*/
public void setUseCaches(boolean useCaches) {
@@ -355,7 +358,11 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol
@Override
public Resource getResource(String location) {
return getResourceLoader().getResource(location);
Resource resource = getResourceLoader().getResource(location);
if (this.useCaches != null && resource instanceof UrlResource urlResource) {
urlResource.setUseCaches(this.useCaches);
}
return resource;
}
@Override
@@ -473,20 +480,27 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol
}
}
else {
UrlResource resource = null;
String urlString = url.toString();
String cleanedPath = StringUtils.cleanPath(urlString);
if (!cleanedPath.equals(urlString)) {
// Prefer cleaned URL, aligned with UrlResource#createRelative(String)
try {
// Retain original URL instance, potentially including custom URLStreamHandler.
return new UrlResource(new URL(url, cleanedPath));
resource = new UrlResource(new URL(url, cleanedPath));
}
catch (MalformedURLException ex) {
// Fallback to regular URL construction below...
}
}
// Retain original URL instance, potentially including custom URLStreamHandler.
return new UrlResource(url);
if (resource == null) {
resource = new UrlResource(url);
}
if (this.useCaches != null) {
resource.setUseCaches(this.useCaches);
}
return resource;
}
}
@@ -505,6 +519,9 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol
UrlResource jarResource = (ResourceUtils.URL_PROTOCOL_JAR.equals(url.getProtocol()) ?
new UrlResource(url) :
new UrlResource(ResourceUtils.JAR_URL_PREFIX + url + ResourceUtils.JAR_URL_SEPARATOR));
if (this.useCaches != null) {
jarResource.setUseCaches(this.useCaches);
}
if (jarResource.exists()) {
result.add(jarResource);
}
@@ -556,7 +573,7 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol
Set<ClassPathManifestEntry> entries = this.manifestEntriesCache;
if (entries == null) {
entries = getClassPathManifestEntries();
if (this.useCaches) {
if (this.useCaches == null || this.useCaches) {
this.manifestEntriesCache = entries;
}
}
@@ -577,7 +594,7 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol
try {
File jar = new File(path).getAbsoluteFile();
if (jar.isFile() && seen.add(jar)) {
manifestEntries.add(ClassPathManifestEntry.of(jar));
manifestEntries.add(ClassPathManifestEntry.of(jar, this.useCaches));
manifestEntries.addAll(getClassPathManifestEntriesFromJar(jar));
}
}
@@ -616,7 +633,7 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol
}
File candidate = new File(parent, path);
if (candidate.isFile() && candidate.getCanonicalPath().contains(parent.getCanonicalPath())) {
manifestEntries.add(ClassPathManifestEntry.of(candidate));
manifestEntries.add(ClassPathManifestEntry.of(candidate, this.useCaches));
}
}
}
@@ -710,7 +727,7 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol
if (rootDirResources == null) {
// Lookup for specific directory, creating a cache entry for it.
rootDirResources = getResources(rootDirPath);
if (this.useCaches) {
if (this.useCaches == null || this.useCaches) {
this.rootDirCache.put(rootDirPath, rootDirResources);
}
}
@@ -729,7 +746,11 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol
if (resolvedUrl != null) {
rootDirUrl = resolvedUrl;
}
rootDirResource = new UrlResource(rootDirUrl);
UrlResource urlResource = new UrlResource(rootDirUrl);
if (this.useCaches != null) {
urlResource.setUseCaches(this.useCaches);
}
rootDirResource = urlResource;
}
if (rootDirUrl.getProtocol().startsWith(ResourceUtils.URL_PROTOCOL_VFS)) {
result.addAll(VfsResourceMatchingDelegate.findMatchingResources(rootDirUrl, subPattern, getPathMatcher()));
@@ -865,8 +886,8 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol
if (con instanceof JarURLConnection jarCon) {
// Should usually be the case for traditional JAR files.
if (!this.useCaches) {
jarCon.setUseCaches(false);
if (this.useCaches != null) {
jarCon.setUseCaches(this.useCaches);
}
try {
jarFile = jarCon.getJarFile();
@@ -931,7 +952,7 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol
}
}
}
if (this.useCaches) {
if (this.useCaches == null || this.useCaches) {
// Cache jar entries in TreeSet for efficient searching on re-encounter.
this.jarEntriesCache.put(jarFileUrl, entriesCache);
}
@@ -1257,10 +1278,10 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol
private static final String JARFILE_URL_PREFIX = ResourceUtils.JAR_URL_PREFIX + ResourceUtils.FILE_URL_PREFIX;
static ClassPathManifestEntry of(File file) throws MalformedURLException {
static ClassPathManifestEntry of(File file, @Nullable Boolean useCaches) throws MalformedURLException {
String path = fixPath(file.getAbsolutePath());
Resource resource = asJarFileResource(path);
Resource alternative = createAlternative(path);
Resource resource = asJarFileResource(path, useCaches);
Resource alternative = createAlternative(path, useCaches);
return new ClassPathManifestEntry(resource, alternative);
}
@@ -1281,18 +1302,22 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol
* @return the alternative form or {@code null}
*/
@Nullable
private static Resource createAlternative(String path) {
private static Resource createAlternative(String path, @Nullable Boolean useCaches) {
try {
String alternativePath = path.startsWith("/") ? path.substring(1) : "/" + path;
return asJarFileResource(alternativePath);
return asJarFileResource(alternativePath, useCaches);
}
catch (MalformedURLException ex) {
return null;
}
}
private static Resource asJarFileResource(String path) throws MalformedURLException {
return new UrlResource(JARFILE_URL_PREFIX + path + ResourceUtils.JAR_URL_SEPARATOR);
private static Resource asJarFileResource(String path, @Nullable Boolean useCaches) throws MalformedURLException {
UrlResource resource = new UrlResource(JARFILE_URL_PREFIX + path + ResourceUtils.JAR_URL_SEPARATOR);
if (useCaches != null) {
resource.setUseCaches(useCaches);
}
return resource;
}
}
@@ -364,7 +364,6 @@ public class SimpleAsyncTaskExecutor extends CustomizableThreadCreator
this.active = false;
Set<Thread> threads = this.activeThreads;
if (threads != null) {
threads.forEach(Thread::interrupt);
synchronized (threads) {
try {
if (!threads.isEmpty()) {
@@ -375,6 +374,7 @@ public class SimpleAsyncTaskExecutor extends CustomizableThreadCreator
Thread.currentThread().interrupt();
}
}
threads.forEach(Thread::interrupt);
}
}
}
@@ -16,7 +16,6 @@
package org.springframework.util;
import java.io.ByteArrayOutputStream;
import java.nio.charset.Charset;
import java.util.ArrayDeque;
import java.util.ArrayList;
@@ -25,6 +24,7 @@ import java.util.Collection;
import java.util.Collections;
import java.util.Deque;
import java.util.Enumeration;
import java.util.HexFormat;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
@@ -803,54 +803,60 @@ public abstract class StringUtils {
}
/**
* Decode the given encoded URI component value. Based on the following rules:
* <ul>
* <li>Alphanumeric characters {@code "a"} through {@code "z"}, {@code "A"} through {@code "Z"},
* and {@code "0"} through {@code "9"} stay the same.</li>
* <li>Special characters {@code "-"}, {@code "_"}, {@code "."}, and {@code "*"} stay the same.</li>
* <li>A sequence "<i>{@code %xy}</i>" is interpreted as a hexadecimal representation of the character.</li>
* <li>For all other characters (including those already decoded), the output is undefined.</li>
* </ul>
* @param source the encoded String
* @param charset the character set
* Decode the given encoded URI component value by replacing "<i>{@code %xy}</i>" sequences
* by an hexadecimal representation of the character in the specified charset, letting other
* characters unchanged.
* @param source the encoded {@code String}
* @param charset the character encoding to use to decode the "<i>{@code %xy}</i>" sequences
* @return the decoded value
* @throws IllegalArgumentException when the given source contains invalid encoded sequences
* @since 5.0
* @see java.net.URLDecoder#decode(String, String)
* @see java.net.URLDecoder#decode(String, String) java.net.URLDecoder#decode for HTML form decoding
*/
public static String uriDecode(String source, Charset charset) {
int length = source.length();
if (length == 0) {
int firstPercentIndex = source.indexOf('%');
if (length == 0 || firstPercentIndex < 0) {
return source;
}
Assert.notNull(charset, "Charset must not be null");
ByteArrayOutputStream baos = new ByteArrayOutputStream(length);
boolean changed = false;
for (int i = 0; i < length; i++) {
int ch = source.charAt(i);
StringBuilder output = new StringBuilder(length);
output.append(source, 0, firstPercentIndex);
byte[] bytes = null;
int i = firstPercentIndex;
while (i < length) {
char ch = source.charAt(i);
if (ch == '%') {
if (i + 2 < length) {
char hex1 = source.charAt(i + 1);
char hex2 = source.charAt(i + 2);
int u = Character.digit(hex1, 16);
int l = Character.digit(hex2, 16);
if (u == -1 || l == -1) {
throw new IllegalArgumentException("Invalid encoded sequence \"" + source.substring(i) + "\"");
try {
if (bytes == null) {
bytes = new byte[(length - i) / 3];
}
baos.write((char) ((u << 4) + l));
i += 2;
changed = true;
int pos = 0;
while (i + 2 < length && ch == '%') {
bytes[pos++] = (byte) HexFormat.fromHexDigits(source, i + 1, i + 3);
i += 3;
if (i < length) {
ch = source.charAt(i);
}
}
if (i < length && ch == '%') {
throw new IllegalArgumentException("Incomplete trailing escape (%) pattern");
}
output.append(new String(bytes, 0, pos, charset));
}
else {
catch (NumberFormatException ex) {
throw new IllegalArgumentException("Invalid encoded sequence \"" + source.substring(i) + "\"");
}
}
else {
baos.write(ch);
output.append(ch);
i++;
}
}
return (changed ? StreamUtils.copyToString(baos, charset) : source);
return output.toString();
}
/**
@@ -21,7 +21,6 @@ import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Types;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
@@ -48,11 +47,6 @@ public class GenericTableMetaDataProvider implements TableMetaDataProvider {
/** Logger available to subclasses. */
protected static final Log logger = LogFactory.getLog(TableMetaDataProvider.class);
/** Database products we know not supporting the use of a String[] for generated keys. */
private static final List<String> productsNotSupportingGeneratedKeysColumnNameArray =
Arrays.asList("Apache Derby", "HSQL Database Engine");
/** The name of the user currently connected. */
@Nullable
private final String userName;
@@ -95,45 +89,14 @@ public class GenericTableMetaDataProvider implements TableMetaDataProvider {
@Override
public void initializeWithMetaData(DatabaseMetaData databaseMetaData) throws SQLException {
try {
if (databaseMetaData.supportsGetGeneratedKeys()) {
logger.debug("GetGeneratedKeys is supported");
setGetGeneratedKeysSupported(true);
}
else {
logger.debug("GetGeneratedKeys is not supported");
setGetGeneratedKeysSupported(false);
}
setGetGeneratedKeysSupported(databaseMetaData.supportsGetGeneratedKeys());
setGeneratedKeysColumnNameArraySupported(isGetGeneratedKeysSupported());
}
catch (SQLException ex) {
if (logger.isWarnEnabled()) {
logger.warn("Error retrieving 'DatabaseMetaData.supportsGetGeneratedKeys': " + ex.getMessage());
}
}
try {
String databaseProductName = databaseMetaData.getDatabaseProductName();
if (productsNotSupportingGeneratedKeysColumnNameArray.contains(databaseProductName)) {
if (logger.isDebugEnabled()) {
logger.debug("GeneratedKeysColumnNameArray is not supported for " + databaseProductName);
}
setGeneratedKeysColumnNameArraySupported(false);
}
else {
if (isGetGeneratedKeysSupported()) {
if (logger.isDebugEnabled()) {
logger.debug("GeneratedKeysColumnNameArray is supported for " + databaseProductName);
}
setGeneratedKeysColumnNameArraySupported(true);
}
else {
setGeneratedKeysColumnNameArraySupported(false);
}
}
}
catch (SQLException ex) {
if (logger.isWarnEnabled()) {
logger.warn("Error retrieving 'DatabaseMetaData.getDatabaseProductName': " + ex.getMessage());
}
}
try {
this.databaseVersion = databaseMetaData.getDatabaseProductVersion();
@@ -225,19 +188,23 @@ public class GenericTableMetaDataProvider implements TableMetaDataProvider {
}
}
/**
* This implementation delegates to {@link #catalogNameToUse}.
*/
@Override
@Nullable
public String metaDataCatalogNameToUse(@Nullable String catalogName) {
return catalogNameToUse(catalogName);
}
/**
* This implementation delegates to {@link #schemaNameToUse}.
* @see #getDefaultSchema()
*/
@Override
@Nullable
public String metaDataSchemaNameToUse(@Nullable String schemaName) {
if (schemaName == null) {
return schemaNameToUse(getDefaultSchema());
}
return schemaNameToUse(schemaName);
return schemaNameToUse(schemaName != null ? schemaName : getDefaultSchema());
}
/**
@@ -401,7 +368,7 @@ public class GenericTableMetaDataProvider implements TableMetaDataProvider {
try {
tableColumns = databaseMetaData.getColumns(
metaDataCatalogName, metaDataSchemaName, metaDataTableName, null);
while (tableColumns.next()) {
while (tableColumns != null && tableColumns.next()) {
String columnName = tableColumns.getString("COLUMN_NAME");
int dataType = tableColumns.getInt("DATA_TYPE");
if (dataType == Types.DECIMAL) {
@@ -19,12 +19,16 @@ package org.springframework.jdbc.core.metadata;
import java.sql.DatabaseMetaData;
import java.sql.SQLException;
import org.springframework.lang.Nullable;
/**
* The PostgreSQL specific implementation of {@link TableMetaDataProvider}.
* Supports a feature for retrieving generated keys without the JDBC 3.0
* {@code getGeneratedKeys} support.
* {@code getGeneratedKeys} support. Also, it processes PostgreSQL-returned
* catalog and schema names from {@code DatabaseMetaData} in the given case.
*
* @author Thomas Risberg
* @author Juergen Hoeller
* @since 2.5
*/
public class PostgresTableMetaDataProvider extends GenericTableMetaDataProvider {
@@ -34,6 +38,18 @@ public class PostgresTableMetaDataProvider extends GenericTableMetaDataProvider
}
@Override
@Nullable
public String metaDataCatalogNameToUse(@Nullable String catalogName) {
return catalogName;
}
@Override
@Nullable
public String metaDataSchemaNameToUse(@Nullable String schemaName) {
return (schemaName != null ? schemaName : getDefaultSchema());
}
@Override
public boolean isGetGeneratedKeysSimulated() {
return true;
@@ -34,12 +34,18 @@ import org.springframework.lang.Nullable;
*
* <p>Designed for use with {@link org.springframework.jdbc.core.JdbcTemplate}
* as well as {@link org.springframework.jdbc.core.simple.JdbcClient}, to be
* passed in as a parameter value wrapping the target content value. Can be
* combined with {@link org.springframework.jdbc.core.SqlParameterValue} for
* specifying a SQL type, for example,
* passed in as a parameter value wrapping the target content value.
*
* <p>Can be combined with {@link org.springframework.jdbc.core.SqlParameterValue}
* for specifying a SQL type, for example,
* {@code new SqlParameterValue(Types.BLOB, new SqlBinaryValue(myContent))}.
* With most database drivers, the type hint is not actually necessary.
*
* <p>Note: Only specify {@code Types.BLOB} in case of an actual BLOB, preferring
* {@code Types.LONGVARBINARY} otherwise. With PostgreSQL, {@code Types.ARRAY}
* has to be specified for BYTEA columns, rather than {@code Types.BLOB}. This
* is in contrast to {@link SqlLobValue} where byte array handling was lenient.
*
* @author Juergen Hoeller
* @since 6.1.4
* @see SqlCharacterValue
@@ -33,12 +33,17 @@ import org.springframework.lang.Nullable;
*
* <p>Designed for use with {@link org.springframework.jdbc.core.JdbcTemplate}
* as well as {@link org.springframework.jdbc.core.simple.JdbcClient}, to be
* passed in as a parameter value wrapping the target content value. Can be
* combined with {@link org.springframework.jdbc.core.SqlParameterValue} for
* specifying a SQL type, for example,
* passed in as a parameter value wrapping the target content value.
*
* <p>Can be combined with {@link org.springframework.jdbc.core.SqlParameterValue}
* for specifying a SQL type, for example,
* {@code new SqlParameterValue(Types.CLOB, new SqlCharacterValue(myContent))}.
* With most database drivers, the type hint is not actually necessary.
*
* <p>Note: Only specify {@code Types.CLOB} in case of an actual CLOB, preferring
* {@code Types.LONGVARCHAR} otherwise. This is in contrast to {@link SqlLobValue}
* where char sequence handling was lenient.
*
* @author Juergen Hoeller
* @since 6.1.4
* @see SqlBinaryValue
@@ -258,18 +258,29 @@ public abstract class ScriptUtils {
for (String statement : statements) {
stmtNumber++;
try {
stmt.execute(statement);
int rowsAffected = stmt.getUpdateCount();
boolean hasResultSet = stmt.execute(statement);
int updateCount = -1;
if (logger.isDebugEnabled()) {
logger.debug(rowsAffected + " returned as update count for SQL: " + statement);
SQLWarning warningToLog = stmt.getWarnings();
while (warningToLog != null) {
logger.debug("SQLWarning ignored: SQL state '" + warningToLog.getSQLState() +
"', error code '" + warningToLog.getErrorCode() +
"', message [" + warningToLog.getMessage() + "]");
warningToLog = warningToLog.getNextWarning();
}
logSqlWarnings(stmt);
}
do {
if (hasResultSet) {
// We invoke getResultSet() to ensure the JDBC driver processes
// it, but we intentionally ignore the returned ResultSet since
// we cannot do anything meaningful with it here.
stmt.getResultSet();
if (logger.isDebugEnabled()) {
logger.debug("ResultSet returned for SQL: " + statement);
}
}
else {
updateCount = stmt.getUpdateCount();
if (updateCount >= 0 && logger.isDebugEnabled()) {
logger.debug(updateCount + " returned as update count for SQL: " + statement);
}
}
hasResultSet = stmt.getMoreResults();
} while (hasResultSet || updateCount != -1);
}
catch (SQLException ex) {
boolean dropStatement = StringUtils.startsWithIgnoreCase(statement.trim(), "drop");
@@ -307,6 +318,16 @@ public abstract class ScriptUtils {
}
}
private static void logSqlWarnings(Statement stmt) throws SQLException {
SQLWarning warningToLog = stmt.getWarnings();
while (warningToLog != null) {
logger.debug("SQLWarning ignored: SQL state '" + warningToLog.getSQLState() +
"', error code '" + warningToLog.getErrorCode() +
"', message [" + warningToLog.getMessage() + "]");
warningToLog = warningToLog.getNextWarning();
}
}
/**
* Read a script from the provided resource, using the supplied comment prefixes
* and statement separator, and build a {@code String} containing the lines.
@@ -74,7 +74,7 @@ class JdbcClientIntegrationTests {
@Test
void updateWithGeneratedKeys() {
int expectedId = 2;
int expectedId = 1;
String firstName = "Jane";
String lastName = "Smith";
@@ -92,7 +92,7 @@ class JdbcClientIntegrationTests {
@Test
void updateWithGeneratedKeysAndKeyColumnNames() {
int expectedId = 2;
int expectedId = 1;
String firstName = "Jane";
String lastName = "Smith";
@@ -110,7 +110,7 @@ class JdbcClientIntegrationTests {
@Test
void updateWithGeneratedKeysUsingNamedParameters() {
int expectedId = 2;
int expectedId = 1;
String firstName = "Jane";
String lastName = "Smith";
@@ -129,7 +129,7 @@ class JdbcClientIntegrationTests {
@Test
void updateWithGeneratedKeysAndKeyColumnNamesUsingNamedParameters() {
int expectedId = 2;
int expectedId = 1;
String firstName = "Jane";
String lastName = "Smith";
@@ -217,7 +217,7 @@ class JdbcClientIntegrationTests {
private static void assertResults(List<User> users) {
assertThat(users).containsExactly(new User(2, "John", "John"), new User(3, "John", "Smith"));
assertThat(users).containsExactly(new User(1, "John", "John"), new User(2, "John", "Smith"));
}
record Name(String name) {}
@@ -323,7 +323,7 @@ class SimpleJdbcInsertIntegrationTests {
protected void insertJaneSmith(SimpleJdbcInsert insert) {
Number id = insert.executeAndReturnKey(Map.of("first_name", "Jane", "last_name", "Smith"));
assertThat(id.intValue()).isEqualTo(2);
assertThat(id.intValue()).isEqualTo(1);
assertNumRows(2);
}
@@ -16,13 +16,24 @@
package org.springframework.jdbc.datasource.init;
import java.nio.charset.StandardCharsets;
import java.sql.SQLException;
import java.util.List;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.Parameter;
import org.junit.jupiter.params.ParameterizedClass;
import org.junit.jupiter.params.provider.EnumSource;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.EncodedResource;
import org.springframework.jdbc.core.DataClassRowMapper;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assumptions.assumeThat;
import static org.springframework.jdbc.datasource.init.ScriptUtils.executeSqlScript;
/**
@@ -32,16 +43,22 @@ import static org.springframework.jdbc.datasource.init.ScriptUtils.executeSqlScr
* @since 4.0.3
* @see ScriptUtilsTests
*/
@ParameterizedClass
@EnumSource(EmbeddedDatabaseType.class)
class ScriptUtilsIntegrationTests extends AbstractDatabaseInitializationTests {
@Parameter
EmbeddedDatabaseType databaseType;
@Override
protected EmbeddedDatabaseType getEmbeddedDatabaseType() {
return EmbeddedDatabaseType.HSQL;
return this.databaseType;
}
@BeforeEach
void setUpSchema() throws SQLException {
executeSqlScript(db.getConnection(), usersSchema());
executeSqlScript(db.getConnection(), encodedResource(usersSchema()), false, true, "--", null, "/*", "*/");
}
@Test
@@ -59,4 +76,52 @@ class ScriptUtilsIntegrationTests extends AbstractDatabaseInitializationTests {
assertUsersDatabaseCreated("Hoeller", "Brannen");
}
@Test
@SuppressWarnings("unchecked")
void statementWithMultipleResultSets() throws SQLException {
// Derby does not support multiple statements/ResultSets within a single Statement.
assumeThat(this.databaseType).isNotSameAs(EmbeddedDatabaseType.DERBY);
EncodedResource resource = encodedResource(resource("users-data.sql"));
executeSqlScript(db.getConnection(), resource, false, true, "--", null, "/*", "*/");
assertUsersInDatabase(user("Sam", "Brannen"));
resource = encodedResource(inlineResource("""
SELECT last_name FROM users WHERE id = 0;
UPDATE users SET first_name = 'Jane' WHERE id = 0;
UPDATE users SET last_name = 'Smith' WHERE id = 0;
SELECT last_name FROM users WHERE id = 0;
GO
"""));
String separator = "GO\n";
executeSqlScript(db.getConnection(), resource, false, true, "--", separator, "/*", "*/");
assertUsersInDatabase(user("Jane", "Smith"));
}
private void assertUsersInDatabase(User... expectedUsers) {
List<User> users = jdbcTemplate.query("SELECT * FROM users WHERE id = 0",
new DataClassRowMapper<>(User.class));
assertThat(users).containsExactly(expectedUsers);
}
private static EncodedResource encodedResource(Resource resource) {
return new EncodedResource(resource);
}
private static Resource inlineResource(String sql) {
byte[] bytes = sql.getBytes(StandardCharsets.UTF_8);
return new ByteArrayResource(bytes, "inline SQL");
}
private static User user(String firstName, String lastName) {
return new User(0, firstName, lastName);
}
record User(int id, String firstName, String lastName) {
}
}
@@ -5,7 +5,7 @@ SET SCHEMA my_schema;
DROP TABLE users IF EXISTS;
CREATE TABLE users (
id INTEGER GENERATED BY DEFAULT AS IDENTITY,
id INTEGER GENERATED BY DEFAULT AS IDENTITY(START WITH 0) PRIMARY KEY,
first_name VARCHAR(50) NOT NULL,
last_name VARCHAR(50) NOT NULL
);
@@ -1,7 +1,7 @@
DROP TABLE users IF EXISTS;
CREATE TABLE users (
id INTEGER GENERATED BY DEFAULT AS IDENTITY,
id INTEGER GENERATED BY DEFAULT AS IDENTITY(START WITH 0) PRIMARY KEY,
first_name VARCHAR(50) NOT NULL,
last_name VARCHAR(50) NOT NULL
);
@@ -357,17 +357,17 @@ class StubWebApplicationContext implements WebApplicationContext {
@Override
@Nullable
public String getMessage(String code, @Nullable Object[] args, @Nullable String defaultMessage, Locale locale) {
public String getMessage(String code, @Nullable Object[] args, @Nullable String defaultMessage, @Nullable Locale locale) {
return this.messageSource.getMessage(code, args, defaultMessage, locale);
}
@Override
public String getMessage(String code, @Nullable Object[] args, Locale locale) throws NoSuchMessageException {
public String getMessage(String code, @Nullable Object[] args, @Nullable Locale locale) throws NoSuchMessageException {
return this.messageSource.getMessage(code, args, locale);
}
@Override
public String getMessage(MessageSourceResolvable resolvable, Locale locale) throws NoSuchMessageException {
public String getMessage(MessageSourceResolvable resolvable, @Nullable Locale locale) throws NoSuchMessageException {
return this.messageSource.getMessage(resolvable, locale);
}
@@ -20,6 +20,8 @@ import java.lang.reflect.Method;
import org.springframework.context.ApplicationListener;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.transaction.event.TransactionPhase;
import org.springframework.transaction.event.TransactionalApplicationListenerMethodAdapter;
import org.springframework.transaction.event.TransactionalEventListenerFactory;
/**
@@ -37,20 +39,22 @@ public class RestrictedTransactionalEventListenerFactory extends TransactionalEv
@Override
public ApplicationListener<?> createApplicationListener(String beanName, Class<?> type, Method method) {
Transactional txAnn = AnnotatedElementUtils.findMergedAnnotation(method, Transactional.class);
if (txAnn == null) {
txAnn = AnnotatedElementUtils.findMergedAnnotation(type, Transactional.class);
}
if (txAnn != null) {
Propagation propagation = txAnn.propagation();
if (propagation != Propagation.REQUIRES_NEW && propagation != Propagation.NOT_SUPPORTED) {
throw new IllegalStateException("@TransactionalEventListener method must not be annotated with " +
"@Transactional unless when declared as REQUIRES_NEW or NOT_SUPPORTED: " + method);
TransactionalApplicationListenerMethodAdapter adapter =
new TransactionalApplicationListenerMethodAdapter(beanName, type, method);
if (adapter.getTransactionPhase() != TransactionPhase.BEFORE_COMMIT) {
Transactional txAnn = AnnotatedElementUtils.findMergedAnnotation(method, Transactional.class);
if (txAnn == null) {
txAnn = AnnotatedElementUtils.findMergedAnnotation(type, Transactional.class);
}
if (txAnn != null) {
Propagation propagation = txAnn.propagation();
if (propagation != Propagation.REQUIRES_NEW && propagation != Propagation.NOT_SUPPORTED) {
throw new IllegalStateException("@TransactionalEventListener method must not be annotated with " +
"@Transactional unless when declared as REQUIRES_NEW or NOT_SUPPORTED: " + method);
}
}
}
return super.createApplicationListener(beanName, type, method);
return adapter;
}
}
@@ -157,6 +157,13 @@ class TransactionalApplicationListenerMethodAdapterTests {
assertThatNoException().isThrownBy(() -> factory.createApplicationListener("test", SampleEvents.class, m));
}
@Test
void withTransactionalAnnotationBeforeCommit() {
RestrictedTransactionalEventListenerFactory factory = new RestrictedTransactionalEventListenerFactory();
Method m = ReflectionUtils.findMethod(SampleEvents.class, "withTransactionalAnnotationBeforeCommit", String.class);
assertThatNoException().isThrownBy(() -> factory.createApplicationListener("test", SampleEvents.class, m));
}
@Test
void withTransactionalAnnotationOnEnclosingClass() {
RestrictedTransactionalEventListenerFactory factory = new RestrictedTransactionalEventListenerFactory();
@@ -277,6 +284,11 @@ class TransactionalApplicationListenerMethodAdapterTests {
public void withAsyncTransactionalAnnotation(String data) {
}
@TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT)
@Transactional
public void withTransactionalAnnotationBeforeCommit(String data) {
}
@Transactional
static class SampleEventsWithTransactionalAnnotation {
@@ -110,7 +110,6 @@ public class ProblemDetail implements Serializable {
* @param type the problem type
*/
public void setType(URI type) {
Assert.notNull(type, "'type' is required");
this.type = type;
}
@@ -251,7 +250,7 @@ public class ProblemDetail implements Serializable {
@Override
public boolean equals(@Nullable Object other) {
return (this == other || (other instanceof ProblemDetail that &&
getType().equals(that.getType()) &&
ObjectUtils.nullSafeEquals(getType(), that.getType()) &&
ObjectUtils.nullSafeEquals(getTitle(), that.getTitle()) &&
this.status == that.status &&
ObjectUtils.nullSafeEquals(this.detail, that.detail) &&
@@ -37,6 +37,7 @@ import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.Flow;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
@@ -97,12 +98,13 @@ class JdkClientHttpRequest extends AbstractStreamingClientHttpRequest {
@SuppressWarnings("NullAway")
protected ClientHttpResponse executeInternal(HttpHeaders headers, @Nullable Body body) throws IOException {
CompletableFuture<HttpResponse<InputStream>> responseFuture = null;
TimeoutHandler timeoutHandler = null;
try {
HttpRequest request = buildRequest(headers, body);
responseFuture = this.httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofInputStream());
if (this.timeout != null) {
TimeoutHandler timeoutHandler = new TimeoutHandler(responseFuture, this.timeout);
timeoutHandler = new TimeoutHandler(responseFuture, this.timeout);
HttpResponse<InputStream> response = responseFuture.get();
InputStream inputStream = timeoutHandler.wrapInputStream(response);
return new JdkClientHttpResponse(response, inputStream);
@@ -120,8 +122,11 @@ class JdkClientHttpRequest extends AbstractStreamingClientHttpRequest {
catch (ExecutionException ex) {
Throwable cause = ex.getCause();
if (cause instanceof CancellationException) {
throw new HttpTimeoutException("Request timed out");
if (cause instanceof CancellationException ce) {
if (timeoutHandler != null) {
timeoutHandler.handleCancellationException(ce);
}
throw new IOException("Request cancelled", cause);
}
if (cause instanceof UncheckedIOException uioEx) {
throw uioEx.getCause();
@@ -136,6 +141,12 @@ class JdkClientHttpRequest extends AbstractStreamingClientHttpRequest {
throw new IOException(cause.getMessage(), cause);
}
}
catch (CancellationException ex) {
if (timeoutHandler != null) {
timeoutHandler.handleCancellationException(ex);
}
throw new IOException("Request cancelled", ex);
}
}
private HttpRequest buildRequest(HttpHeaders headers, @Nullable Body body) {
@@ -234,12 +245,15 @@ class JdkClientHttpRequest extends AbstractStreamingClientHttpRequest {
private final CompletableFuture<Void> timeoutFuture;
private final AtomicBoolean timeout = new AtomicBoolean(false);
private TimeoutHandler(CompletableFuture<HttpResponse<InputStream>> future, Duration timeout) {
this.timeoutFuture = new CompletableFuture<Void>()
.completeOnTimeout(null, timeout.toMillis(), TimeUnit.MILLISECONDS);
this.timeoutFuture.thenRun(() -> {
this.timeout.set(true);
if (future.cancel(true) || future.isCompletedExceptionally() || !future.isDone()) {
return;
}
@@ -250,7 +264,6 @@ class JdkClientHttpRequest extends AbstractStreamingClientHttpRequest {
// ignore
}
});
}
@Nullable
@@ -268,6 +281,12 @@ class JdkClientHttpRequest extends AbstractStreamingClientHttpRequest {
}
};
}
public void handleCancellationException(CancellationException ex) throws HttpTimeoutException {
if (this.timeout.get()) {
throw new HttpTimeoutException(ex.getMessage());
}
}
}
}
@@ -105,16 +105,20 @@ public @interface RequestMapping {
/**
* The path mapping URIs &mdash; for example, {@code "/profile"}.
* <p>Ant-style path patterns are also supported (for example, {@code "/profile/**"}).
* At the method level, relative paths (for example, {@code "edit"}) are supported
* <p>Ant-style path patterns are also supported, e.g. {@code "/profile/**"}.
* At the method level, relative paths, e.g., {@code "edit"} are supported
* within the primary mapping expressed at the type level.
* Path mapping URIs may contain placeholders (for example, <code>"/${profile_path}"</code>).
* Path mapping URIs may contain property placeholders, e.g. <code>"/${profile_path}"</code>,
* and SpEL expressions, e.g. {@code "/profile/#{@bean.property}"}.
* <p><b>Supported at the type level as well as at the method level!</b>
* When used at the type level, all method-level mappings inherit
* this primary mapping, narrowing it for a specific handler method.
* <p><strong>NOTE</strong>: A handler method that is not mapped to any path
* explicitly is effectively mapped to an empty path.
* @since 4.2
* @see org.springframework.beans.factory.config.EmbeddedValueResolver
* @see org.springframework.context.expression.StandardBeanExpressionResolver
* @see org.springframework.context.support.AbstractApplicationContext
*/
@AliasFor("value")
String[] path() default {};
@@ -116,7 +116,7 @@ public abstract class BindErrorUtils {
@Override
@Nullable
protected String getDefaultMessage(MessageSourceResolvable resolvable, Locale locale) {
protected String getDefaultMessage(MessageSourceResolvable resolvable, @Nullable Locale locale) {
String message = super.getDefaultMessage(resolvable, locale);
return (resolvable instanceof FieldError error ? error.getField() + ": " + message : message);
}
@@ -30,7 +30,6 @@ import org.springframework.util.Assert;
*
* @author Rossen Stoyanchev
* @since 6.2
*
* @see <a href="https://www.rfc-editor.org/info/rfc3986">RFC 3986</a>
*/
abstract class RfcUriParser {
@@ -78,10 +77,10 @@ abstract class RfcUriParser {
* @param query the query, if present
* @param fragment the fragment, if present
*/
record UriRecord(@Nullable String scheme, boolean isOpaque,
@Nullable String user, @Nullable String host, @Nullable String port,
@Nullable String path, @Nullable String query, @Nullable String fragment) {
record UriRecord(
@Nullable String scheme, boolean isOpaque,
@Nullable String user, @Nullable String host, @Nullable String port,
@Nullable String path, @Nullable String query, @Nullable String fragment) {
}
@@ -130,6 +129,7 @@ abstract class RfcUriParser {
}
},
HOST_OR_PATH {
@Override
@@ -158,6 +158,7 @@ abstract class RfcUriParser {
}
},
SCHEME_OR_PATH {
@Override
@@ -188,6 +189,7 @@ abstract class RfcUriParser {
}
},
HOST {
@Override
@@ -229,6 +231,7 @@ abstract class RfcUriParser {
}
},
IPV6 {
@Override
@@ -259,6 +262,7 @@ abstract class RfcUriParser {
}
},
PORT {
@Override
@@ -291,6 +295,7 @@ abstract class RfcUriParser {
}
},
PATH {
@Override
@@ -319,6 +324,7 @@ abstract class RfcUriParser {
}
},
QUERY {
@Override
@@ -334,7 +340,9 @@ abstract class RfcUriParser {
}
},
FRAGMENT {
@Override
public void handleNext(InternalParser parser, char c, int i) {
}
@@ -345,6 +353,7 @@ abstract class RfcUriParser {
}
},
WILDCARD {
@Override
@@ -358,6 +367,7 @@ abstract class RfcUriParser {
}
};
/**
* Method to handle each character from the input string.
* @param parser provides access to parsing state, and helper methods
@@ -429,6 +439,7 @@ abstract class RfcUriParser {
this.uri = uri;
}
// Check internal state
public boolean hasScheme() {
@@ -451,6 +462,7 @@ abstract class RfcUriParser {
return (this.index == this.componentIndex);
}
// Top-level parse loop, iterate over chars and delegate to states
public UriRecord parse() {
@@ -475,6 +487,7 @@ abstract class RfcUriParser {
return this.uri.charAt(this.index);
}
// Transitions and index updates
public void advanceTo(State state) {
@@ -500,6 +513,7 @@ abstract class RfcUriParser {
this.index = index;
}
// Component capture
public InternalParser resolveIfOpaque() {
@@ -593,6 +607,7 @@ abstract class RfcUriParser {
return this;
}
// Encoding and curly bracket handling
/**
@@ -643,6 +658,7 @@ abstract class RfcUriParser {
return (this.openCurlyBracketCount > 0);
}
@Override
public String toString() {
return "[State=" + this.state + ", index=" + this.index + ", componentIndex=" + this.componentIndex +
@@ -373,15 +373,16 @@ public abstract class UriUtils {
}
/**
* Decode the given encoded URI component.
* <p>See {@link StringUtils#uriDecode(String, Charset)} for the decoding rules.
* @param source the encoded String
* @param charset the character encoding to use
* Decode the given encoded URI component value by replacing "<i>{@code %xy}</i>" sequences
* by an hexadecimal representation of the character in the specified charset, letting other
* characters unchanged.
* @param source the encoded {@code String}
* @param charset the character encoding to use to decode the "<i>{@code %xy}</i>" sequences
* @return the decoded value
* @throws IllegalArgumentException when the given source contains invalid encoded sequences
* @since 5.0
* @see StringUtils#uriDecode(String, Charset)
* @see java.net.URLDecoder#decode(String, String)
* @see java.net.URLDecoder#decode(String, String) java.net.URLDecoder#decode for HTML form decoding
*/
public static String decode(String source, Charset charset) {
return StringUtils.uriDecode(source, charset);
@@ -0,0 +1,78 @@
/*
* Copyright 2002-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.http.client;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.http.HttpTimeoutException;
import java.time.Duration;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import org.junit.jupiter.api.AutoClose;
import org.junit.jupiter.api.Test;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
/**
* Unit tests for {@link JdkClientHttpRequest}.
*/
class JdkClientHttpRequestTests {
private final HttpClient client = mock(HttpClient.class);
@AutoClose("shutdownNow")
private final ExecutorService executor = Executors.newSingleThreadExecutor();
@Test
@SuppressWarnings("unchecked")
void futureCancelledAfterTimeout() {
CompletableFuture<HttpResponse<InputStream>> future = new CompletableFuture<>();
when(client.sendAsync(any(HttpRequest.class), any(HttpResponse.BodyHandler.class))).thenReturn(future);
assertThatThrownBy(() -> createRequest(Duration.ofMillis(10)).executeInternal(new HttpHeaders(), null))
.isExactlyInstanceOf(HttpTimeoutException.class);
}
@Test
@SuppressWarnings("unchecked")
void futureCancelled() {
CompletableFuture<HttpResponse<InputStream>> future = new CompletableFuture<>();
future.cancel(true);
when(client.sendAsync(any(HttpRequest.class), any(HttpResponse.BodyHandler.class))).thenReturn(future);
assertThatThrownBy(() -> createRequest(null).executeInternal(new HttpHeaders(), null))
.isExactlyInstanceOf(IOException.class);
}
private JdkClientHttpRequest createRequest(Duration timeout) {
return new JdkClientHttpRequest(client, URI.create("https://abc.com"), HttpMethod.GET, executor, timeout);
}
}
@@ -107,12 +107,21 @@ class UriUtilsTests {
assertThat(UriUtils.decode("T%C5%8Dky%C5%8D", CHARSET)).as("Invalid encoded result").isEqualTo("T\u014dky\u014d");
assertThat(UriUtils.decode("/Z%C3%BCrich", CHARSET)).as("Invalid encoded result").isEqualTo("/Z\u00fcrich");
assertThat(UriUtils.decode("T\u014dky\u014d", CHARSET)).as("Invalid encoded result").isEqualTo("T\u014dky\u014d");
assertThat(UriUtils.decode("%20\u2019", CHARSET)).as("Invalid encoded result").isEqualTo(" \u2019");
assertThat(UriUtils.decode("\u015bp\u0159\u00ec\u0144\u0121", CHARSET)).as("Invalid encoded result").isEqualTo("śpřìńġ");
assertThat(UriUtils.decode("%20\u015bp\u0159\u00ec\u0144\u0121", CHARSET)).as("Invalid encoded result").isEqualTo(" śpřìńġ");
}
@Test
void decodeInvalidSequence() {
assertThatIllegalArgumentException().isThrownBy(() ->
UriUtils.decode("foo%2", CHARSET));
assertThatIllegalArgumentException().isThrownBy(() ->
UriUtils.decode("foo%", CHARSET));
assertThatIllegalArgumentException().isThrownBy(() ->
UriUtils.decode("%", CHARSET));
assertThatIllegalArgumentException().isThrownBy(() ->
UriUtils.decode("%zz", CHARSET));
}
@Test
@@ -393,7 +393,7 @@ class RequestMappingHandlerAdapterTests {
}
private static class SseController {
static class SseController {
public ResponseEntity<?> handle(@RequestParam String q) throws IOException {
if (q.equals("sse")) {
@@ -183,21 +183,19 @@ class PathResourceResolverTests {
private String relativePath;
public TestUrlResource(String path) throws MalformedURLException {
super(path);
}
public String getSavedRelativePath() {
return this.relativePath;
}
@Override
public Resource createRelative(String relativePath) {
this.relativePath = relativePath;
return this;
}
public String getSavedRelativePath() {
return this.relativePath;
}
}
}