mirror of
https://github.com/spring-projects/spring-framework
synced 2026-06-08 17:33:33 +00:00
Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8f64480c9f | |||
| edda4731e1 | |||
| 9fa2d7d190 | |||
| c30427fd4e | |||
| 1d908f1847 | |||
| 37b076be51 | |||
| a9453a5959 | |||
| 3781ba223e | |||
| f11a1e6f82 | |||
| ffc785471b | |||
| 6e2fbfe108 | |||
| 968e037503 | |||
| f0a9f649c1 | |||
| 600d6c6fc0 | |||
| 7a55ce48a9 | |||
| 5df9fd4eff | |||
| da13a24604 | |||
| 67e88f3c20 | |||
| 24e66b63d1 | |||
| f3832c7262 | |||
| 16e99f289c | |||
| 642e554c52 | |||
| 4f6304707d | |||
| 3c112703d9 | |||
| 8c44a61033 | |||
| 5e338ef1b8 | |||
| 445da24631 | |||
| 6d97791f5e |
@@ -21,7 +21,7 @@ jobs:
|
||||
toolchain: false
|
||||
- version: 21
|
||||
toolchain: true
|
||||
- version: 23
|
||||
- version: 24
|
||||
toolchain: true
|
||||
exclude:
|
||||
- os:
|
||||
|
||||
+1
-1
@@ -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]]
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
+1
@@ -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();
|
||||
|
||||
+3
-3
@@ -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}.
|
||||
|
||||
+8
-1
@@ -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
|
||||
|
||||
+1
-1
@@ -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 {
|
||||
|
||||
+26
-3
@@ -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;
|
||||
|
||||
}
|
||||
|
||||
+1
-1
@@ -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)
|
||||
|
||||
+3
-3
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
+18
-7
@@ -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);
|
||||
}
|
||||
|
||||
+15
-5
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+4
-4
@@ -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]);
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+11
-1
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+46
-21
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -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();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+11
-44
@@ -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) {
|
||||
|
||||
+17
-1
@@ -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
|
||||
|
||||
+8
-3
@@ -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
|
||||
|
||||
+31
-10
@@ -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.
|
||||
|
||||
+5
-5
@@ -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) {}
|
||||
|
||||
+1
-1
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
+67
-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) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+1
-1
@@ -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
-1
@@ -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
|
||||
);
|
||||
|
||||
+3
-3
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
+16
-12
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+12
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+7
-3
@@ -105,16 +105,20 @@ public @interface RequestMapping {
|
||||
|
||||
/**
|
||||
* The path mapping URIs — 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);
|
||||
|
||||
+78
@@ -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
|
||||
|
||||
+1
-1
@@ -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")) {
|
||||
|
||||
+4
-6
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user