Compare commits

...

47 Commits

Author SHA1 Message Date
Brian Clozel 4e35a12209 Release v6.2.17 2026-03-13 08:43:16 +01:00
Sébastien Deleuze 317a1f9909 Leverage ResourceHandlerUtils in ScriptTemplateView
This commit apply extra checks to ScriptTemplateView resource handling
with ResourceHandlerUtils, consistently with what is done with static
resource handling.

Closes gh-36459
2026-03-12 20:02:59 +01:00
Sébastien Deleuze de6601fdac Restore ScriptTemplateViewTests
Restore both WebMVC and WebFlux variants that were deleted
by mistake in commit 4db2f8ea1b.

This commit also removes the empty resource loader path, as it is not
needed for the main WEB-INF/ use case that is typically configured
explicitly by the user, and not needed to pass the restored tests.

Closes gh-36457
2026-03-12 19:53:26 +01:00
Sam Brannen 47dc1c4d93 Fix log message in ConfigurationClassBeanDefinitionReader
The log message now properly generates the fully-qualified method name
and includes the resolved bean name as well.

Closes gh-36453

(cherry picked from commit e634ced56b)
2026-03-12 16:25:03 +01:00
Juergen Hoeller d8c77934ff Upgrade to SnakeYAML 2.6, Protobuf 4.34, H2 2.4.240
(cherry picked from commit d8216d719b)
2026-03-11 15:55:42 +01:00
Juergen Hoeller 99fbce1254 Polishing (aligned with main) 2026-03-11 15:08:12 +01:00
Juergen Hoeller d1e69a9677 Upgrade to Reactor 2024.0.16 and Micrometer 1.15.10
Includes Jetty 12.0.33, Selenium 4.41, Mockito 5.22

Closes gh-36445
Closes gh-36446
2026-03-10 20:26:44 +01:00
Brian Clozel 8dc888e1b8 Guard against invalid id/event values in Server Sent Events
Prior to this commit, our implementation of Server Sent Events (SSE),
`SseEmitter` (MVC) and `ServerSentEvent` (WebFlux), would not guard
against invalid characters if the application mistakenly inserts such
characters in the `id` or `event` types.
Both implementations would also behave differently when it comes
to escaping comment multi-line events.

This commit ensures that both implementations handle multi-line comment
events and reject invalid characters in id/event types.
This commit also optimizes `String` concatenation and memory usage
when writing data.

Fixes gh-36440
2026-03-10 17:56:39 +01:00
Sam Brannen 131f94fbc5 Use link for first reference to @⁠Fallback in @⁠Bean Javadoc
See gh-36439

(cherry picked from commit 37e8aa76e9)
2026-03-10 17:54:05 +01:00
Sam Brannen d4f4c69318 Document @Fallback alongside Primary in the reference docs and @Bean Javadoc
Closes gh-36439

(cherry picked from commit 27686dc2e2)
2026-03-10 17:28:49 +01:00
Sam Brannen 74ab6625ac Document registration recommendations for BeanPostProcessor and BeanFactoryPostProcessor
Closes gh-34964

(cherry picked from commit 3b8efbe5a6)
2026-03-10 13:09:35 +01:00
Sam Brannen 7c27183a4f Polishing
(cherry picked from commit 6f2e59a995)
2026-03-10 13:09:26 +01:00
Sam Brannen 526f67cd83 Polish @⁠Bean Javadoc and reference docs
(cherry picked from commit 644a20ae2a)
2026-03-10 13:09:17 +01:00
Sam Brannen 88f081b1cd Resolve context initializers only once in AbstractTestContextBootstrapper
The internal buildMergedContextConfiguration() method in
AbstractTestContextBootstrapper originally resolved the
ApplicationContextInitializer set only once. However, the changes made
in commit 2244461778 introduced a regression resulting in the
initializers being resolved twice: once for validation and once for
actually building the merged context configuration. In addition, the
resolution for validation does not honor the inheritInitializers flag
in ContextConfigurationAttributes.

To address these issues, buildMergedContextConfiguration() once again
resolves the context initializers once via
ApplicationContextInitializerUtils.resolveInitializerClasses().

See gh-18528
Closes gh-36430

(cherry picked from commit 463138acbc)
2026-03-08 14:50:42 +01:00
Sam Brannen 5806a23bcf Address Checkstyle violation
(cherry picked from commit bbe733def7)
2026-03-05 11:44:13 +01:00
Sam Brannen 507399d9a8 Polish documentation for FrameworkServlet and HttpServletBean
(cherry picked from commit 50c29e64f8)
2026-03-05 11:25:11 +01:00
Juergen Hoeller cd9a430869 Polishing
(cherry picked from commit 4734c15d81)
2026-03-03 23:38:55 +01:00
Sam Brannen 0a933787a6 Exclude legacy @⁠javax.validation.Constraint from attribute override check
Prior to this commit, we only excluded @⁠jakarta.validation.Constraint
from the convention-based annotation attribute override check.

This commit additionally excludes the legacy
@⁠javax.validation.Constraint annotation from the convention-based
annotation attribute override check.

See gh-28760
See gh-28761
Closes gh-36412
2026-03-03 17:02:13 +01:00
cetf 2bbea84830 Fix format string argument count
Fixes: gh-36410
Signed-off-by: cetf <cetf9h@163.com>
2026-03-03 12:15:12 +01:00
dependabot[bot] b9517b8ed7 Upgrade fast-xml-parser to 5.3.8
Closes gh-36402

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

(cherry picked from commit a87b2e0146)
2026-03-01 13:14:49 +01:00
Sam Brannen f41786b6af Fix typo
(cherry picked from commit bd40f0e6bb)
2026-03-01 13:06:56 +01:00
Sam Brannen 2d10d513e1 Consistently refer to URLs and URIs in documentation
(cherry picked from commit 04186fdf0e)
2026-03-01 13:06:56 +01:00
Tran Ngoc Nhan c83fb69d9b Fix links to UriComponentsBuilder and polish examples
Closes gh-36403

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

(cherry picked from commit b2b731b0ba)
2026-03-01 13:06:51 +01:00
Juergen Hoeller 566a42bec9 Remove prefixed FactoryBean name in ApplicationListenerDetector
Closes gh-36404

(cherry picked from commit a3b9098850)
2026-03-01 12:10:44 +01:00
Juergen Hoeller b426ef2d1b Polishing 2026-02-28 19:45:22 +01:00
Juergen Hoeller 728466dce0 Cancel late-executing tasks within revised closed handling
Closes gh-36362

(cherry picked from commit b6833ff31f)
2026-02-28 13:56:34 +01:00
Sam Brannen 455cb766a9 Further emphasize @⁠Configuration classes over XML and Groovy in testing chapter
See gh-36393

(cherry picked from commit 9eea227ab9)
2026-02-25 15:49:19 +01:00
Sam Brannen c77993acac Emphasize @⁠Configuration classes over XML and Groovy in testing chapter
Closes gh-36393

(cherry picked from commit 706c98228d)
2026-02-25 15:27:25 +01:00
Sam Brannen f3d0f9427a Upgrade to JUnit 5.14.3
Closes gh-36388
2026-02-25 11:00:24 +01:00
dependabot[bot] 688975e327 Upgrade fast-xml-parser to 5.3.6
Closes gh-36348

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
(cherry picked from commit ee6a115a18)
2026-02-25 10:29:02 +01:00
Juergen Hoeller b619e69130 Polishing (aligned with main) 2026-02-24 18:04:40 +01:00
Juergen Hoeller 972ba739aa Reject late-executing tasks after termination waiting
Closes gh-36362

(cherry picked from commit cff48fff2d)
2026-02-24 17:58:18 +01:00
Sam Brannen f87b5ee326 Polish contribution
See gh-36367

(cherry picked from commit b1cb9c5052)
2026-02-23 17:33:15 +01:00
Tran Ngoc Nhan 7768cee7a0 Polish SpEL operator examples in reference docs
Closes gh-36367

Signed-off-by: Tran Ngoc Nhan <ngocnhan.tran1996@gmail.com>
(cherry picked from commit 057633edb5)
2026-02-23 17:33:07 +01:00
rstoyanchev 9042682b56 Optimal charset handling in AbstractHttpMessageConverter
Closes gh-36320
2026-02-23 15:44:26 +00:00
Brian Clozel 2d81a9fe9b Optimize MediaType(MediaType, Charset) constructor
Prior to this commit, the `MediaType` and `MimeType` "copy" constructors
would not leverage the fact that the existing instance has been
validated already (types, subtype and parameters have been checked
already for errors) and the entire validation would be performed again.
This would also allocate map instances in the process.

This commit ensures that the already validated information is reused
directly and that we avoid unnessecary operations and allocations for
such constructors.

Closes gh-36318
2026-02-23 15:40:18 +00:00
Brian Clozel a3f7179ab3 Fix InvalidMimeTypeException for compatible media types
The `AbstractMessageConverterMethodProcessor` is in charge of handling
controller method return values and to write those as HTTP response
messages. The content negotiation process is an important part.

The `MimeTypeUtils#sortBySpecificity` is in charge of sorting inbound
"Accept" media types by their specificity and reject them if the list
is too large, in order to protect the application from ddos attacks.

Prior to this commit, the content negotiation process would first get
the sorted "Accept" media types, the producible media types as
advertized by message converters - and collect the intersection of both
in a new list (also sorted by specificity). If the "Accept" list is
large enough (but under the limit), the list of compatible media types
could exceed that limit because duplicates could be introduced in that
list: several converters can produce the same content type.

This commit ensures that compatible media types are collected in a set
to avoid duplicates. Without that, exceeding the limit at this point
will throw an `InvalidMimeTypeException` that's not handled by the
processor and result in a server error.

Fixes gh-36300
2026-02-20 18:43:02 +01:00
rstoyanchev 50ef3b0a29 Update Javadoc of RestClient.Builder defaultStatusHandler
See gh-36248
2026-02-19 11:45:33 +00:00
Juergen Hoeller 1c7e79026f Skip serialization of potentially non-serializable cached state
Closes gh-36346

(cherry picked from commit 22bd8bd704)
2026-02-17 22:19:43 +01:00
Juergen Hoeller c8735efb9d Polishing (aligned with main) 2026-02-17 19:08:15 +01:00
Juergen Hoeller 999bbe3959 Polishing 2026-02-17 18:41:07 +01:00
Juergen Hoeller acab61f9a7 Consistent adaptation of HTTP headers on Servlet responses
Closes gh-36345
2026-02-17 18:40:50 +01:00
Juergen Hoeller bfa81290b3 Polishing 2026-02-17 12:18:31 +01:00
Juergen Hoeller 2c7ed5e5f1 Align validation groups check with InvocableHandlerMethod in spring-web
Closes gh-36337
2026-02-17 12:15:07 +01:00
Brian Clozel 474d520182 Fix MultipartParser & PartGenerator memory leak
Prior to this commit, the reactive `MultipartParser` and `PartGenerator`
types were leaking memory at runtime in specific cases:

* many HTTP clients must send multipart requests to be parsed and close
  the connection while uploading
* the `PartGenerator` must be configured to write file parts to
  temporary files on disk
* concurrency, upload speed must be important to trigger cases where the
  file system is not fast enough to consume incoming buffers

The `MultipartParser` parses and emits `BodyToken` to its sink
(here, the `PartGenerator`). By definition, Reactor's `FluxSink` when
created with `Flux.create(FluxSink)` will use a "buffer" strategy and
will queue emitted elements if they cannot be consumed.

Here, the cancellation signal does dispose internal states in the
`MultiPartParser` and `PartGenerator` but does not clear the internal
queue in `FluxSink`.

This commit ensures that an operation is registered to release buffers
on the discard event.

Fixes gh-36262
2026-02-13 18:45:05 +01:00
Juergen Hoeller 50bffe7ddc Detect all common size exceptions from Tomcat and Commons FileUpload 2.x
Closes gh-36317

(cherry picked from commit e8e24e65d2)
2026-02-13 16:32:56 +01:00
Brian Clozel 6d849bd9b0 Next development version (v6.2.17-SNAPSHOT) 2026-02-12 10:03:33 +01:00
79 changed files with 1703 additions and 441 deletions
-4
View File
@@ -94,10 +94,6 @@ configure([rootProject] + javaProjects) { project ->
//"https://hc.apache.org/httpcomponents-client-5.5.x/current/httpclient5/apidocs/",
"https://projectreactor.io/docs/test/release/api/",
"https://junit.org/junit4/javadoc/4.13.2/",
// 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.14.2/api/",
"https://www.reactive-streams.org/reactive-streams-1.0.4-javadoc/",
//"https://javadoc.io/static/io.rsocket/rsocket-core/1.1.1/",
"https://r2dbc.io/spec/1.0.0.RELEASE/api/",
+1 -1
View File
@@ -326,9 +326,9 @@
*** xref:testing/testcontext-framework/application-events.adoc[]
*** xref:testing/testcontext-framework/test-execution-events.adoc[]
*** xref:testing/testcontext-framework/ctx-management.adoc[]
**** xref:testing/testcontext-framework/ctx-management/javaconfig.adoc[]
**** xref:testing/testcontext-framework/ctx-management/xml.adoc[]
**** xref:testing/testcontext-framework/ctx-management/groovy.adoc[]
**** xref:testing/testcontext-framework/ctx-management/javaconfig.adoc[]
**** xref:testing/testcontext-framework/ctx-management/mixed-config.adoc[]
**** xref:testing/testcontext-framework/ctx-management/context-customizers.adoc[]
**** xref:testing/testcontext-framework/ctx-management/initializers.adoc[]
@@ -151,17 +151,17 @@ injected into a `Set<MovieCatalog>` annotated with `@Qualifier("action")`.
[TIP]
====
Letting qualifier values select against target bean names, within the type-matching
candidates, does not require a `@Qualifier` annotation at the injection point.
If there is no other resolution indicator (such as a qualifier or a primary marker),
for a non-unique dependency situation, Spring matches the injection point name
(that is, the field name or parameter name) against the target bean names and chooses
the same-named candidate, if any (either by bean name or by associated alias).
candidates, does not require a `@Qualifier` annotation at the injection point. If there
is no other resolution indicator (such as a qualifier, a primary marker, or a fallback
marker), for a non-unique dependency situation, Spring matches the injection point name
(that is, the field name or parameter name) against the target bean names and chooses the
same-named candidate, if any (either by bean name or by associated alias).
Since version 6.1, this requires the `-parameters` Java compiler flag to be present.
As of 6.2, the container applies fast shortcut resolution for bean name matches,
bypassing the full type matching algorithm when the parameter name matches the
bean name and no type, qualifier or primary conditions override the match. It is
therefore recommendable for your parameter names to match the target bean names.
Since version 6.1, this requires the `-parameters` Java compiler flag to be present. As
of 6.2, the container applies fast shortcut resolution for bean name matches, bypassing
the full type matching algorithm when the parameter name matches the bean name and no
type, qualifier, primary, or fallback conditions override the match. It is therefore
recommendable for your parameter names to match the target bean names.
====
As an alternative for injection by name, consider the JSR-250 `@Resource` annotation
@@ -308,7 +308,8 @@ set of multiple matches for the specific bean type (as returned by the factory m
Note that the standard `jakarta.annotation.Priority` annotation is not available at the
`@Bean` level, since it cannot be declared on methods. Its semantics can be modeled
through `@Order` values in combination with `@Primary` on a single bean for each type.
through `@Order` values in combination with `@Primary` or `@Fallback` on a single bean
for each type.
====
Even typed `Map` instances can be autowired as long as the expected key type is `String`.
@@ -27,4 +27,5 @@ with the `CustomAutowireConfigurer`
When multiple beans qualify as autowire candidates, the determination of a "`primary`" is
as follows: If exactly one bean definition among the candidates has a `primary`
attribute set to `true`, it is selected.
attribute set to `true`, it is selected. For annotation-based configuration, see
xref:core/beans/annotation-config/autowired-primary.adoc[Fine-tuning with `@Primary` or `@Fallback`].
@@ -67,6 +67,13 @@ interface, clearly indicating the post-processor nature of that bean. Otherwise,
Since a `BeanPostProcessor` needs to be instantiated early in order to apply to the
initialization of other beans in the context, this early type detection is critical.
Furthermore, when registering a `BeanPostProcessor` via an `@Bean` factory method,
declare the method as `static` and ideally with no dependencies. Doing so avoids eager
initialization of the configuration class and other beans, which would make them
ineligible for full post-processing (such as auto-proxying). See the
"BeanPostProcessor-returning `@Bean` methods" section in the
{spring-framework-api}/context/annotation/Bean.html[`@Bean`] javadoc for details.
[[beans-factory-programmatically-registering-beanpostprocessors]]
.Programmatically registering `BeanPostProcessor` instances
NOTE: While the recommended approach for `BeanPostProcessor` registration is through
@@ -80,7 +87,7 @@ of execution. Note also that `BeanPostProcessor` instances registered programmat
are always processed before those registered through auto-detection, regardless of any
explicit ordering.
.`BeanPostProcessor` instances and AOP auto-proxying
.`BeanPostProcessor` instances and early initialization
[NOTE]
====
Classes that implement the `BeanPostProcessor` interface are special and are treated
@@ -90,17 +97,23 @@ of the `ApplicationContext`. Next, all `BeanPostProcessor` instances are registe
in a sorted fashion and applied to all further beans in the container. Because AOP
auto-proxying is implemented as a `BeanPostProcessor` itself, neither `BeanPostProcessor`
instances nor the beans they directly reference are eligible for auto-proxying and,
thus, do not have aspects woven into them.
thus, do not have aspects woven into them. More generally, any bean that is instantiated
during this early phase is not eligible for full post-processing by all
`BeanPostProcessor` instances.
For any such bean, you should see an informational log message: `Bean someBean is not
eligible for getting processed by all BeanPostProcessor interfaces (for example: not
eligible for auto-proxying)`.
For any such bean, you should see a WARN-level log message similar to the following.
If you have beans wired into your `BeanPostProcessor` by using autowiring or
`@Resource` (which may fall back to autowiring), Spring might access unexpected beans
when searching for type-matching dependency candidates and, therefore, make them
ineligible for auto-proxying or other kinds of bean post-processing. For example, if you
have a dependency annotated with `@Resource` where the field or setter name does not
[quote]
Bean 'someBean' of type [org.example.SomeType] is not eligible for getting processed by
all BeanPostProcessors (for example: not eligible for auto-proxying).
To minimize the number of beans affected, register a `BeanPostProcessor` with a
`static` `@Bean` method that has no dependencies (see the note above). If you have
beans wired into your `BeanPostProcessor` by using autowiring or `@Resource` (which
may fall back to autowiring), Spring might access unexpected beans when searching
for type-matching dependency candidates and, therefore, make them ineligible for
auto-proxying or other kinds of bean post-processing. For example, if you have a
dependency annotated with `@Resource` where the field or setter name does not
directly correspond to the declared name of a bean and no name attribute is used,
Spring accesses other beans for matching them by type.
====
@@ -135,7 +148,7 @@ Java::
}
public Object postProcessAfterInitialization(Object bean, String beanName) {
System.out.println("Bean '" + beanName + "' created : " + bean.toString());
System.out.println("Bean '" + beanName + "' created : " + bean);
return bean;
}
}
@@ -164,7 +177,48 @@ Kotlin::
----
======
The following `beans` element uses the `InstantiationTracingBeanPostProcessor`:
You can register the `InstantiationTracingBeanPostProcessor` with Java configuration
by using a `static` `@Bean` method (recommended to avoid eager initialization of the
configuration class and other beans):
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes",chomp="-packages"]
----
@Configuration
public class AppConfig {
@Bean
public static InstantiationTracingBeanPostProcessor instantiationTracingBeanPostProcessor() {
return new InstantiationTracingBeanPostProcessor();
}
// ... other bean definitions
}
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes",chomp="-packages"]
----
@Configuration
class AppConfig {
@Bean
companion object {
@JvmStatic
fun instantiationTracingBeanPostProcessor() = InstantiationTracingBeanPostProcessor()
}
// ... other bean definitions
}
----
======
Alternatively, the `InstantiationTracingBeanPostProcessor` can be registered via the
`bean` element with XML configuration:
[source,xml,indent=0,subs="verbatim,quotes"]
----
@@ -192,7 +246,7 @@ The following `beans` element uses the `InstantiationTracingBeanPostProcessor`:
----
Notice how the `InstantiationTracingBeanPostProcessor` is merely defined. It does not
even have a name, and, because it is a bean, it can be dependency-injected as you would any
even have a name, and, because it is a bean, it can be dependency-injected as with any
other bean. (The preceding configuration also defines a bean that is backed by a Groovy
script. The Spring dynamic language support is detailed in the chapter entitled
xref:languages/dynamic.adoc[Dynamic Language Support].)
@@ -301,6 +355,23 @@ implement the `BeanFactoryPostProcessor` interface. It uses these beans as bean
post-processors, at the appropriate time. You can deploy these post-processor beans as
you would any other bean.
When registering a `BeanFactoryPostProcessor` via an `@Bean` factory method in a
`@Configuration` class, declare the method as `static` to avoid lifecycle conflicts
with annotation processing (such as `@Autowired`, `@Value`, and `@PostConstruct`) in the
configuration class. See the "BeanFactoryPostProcessor-returning `@Bean` methods"
section in the {spring-framework-api}/context/annotation/Bean.html[`@Bean`] javadoc
for details and an example.
For any non-static `@Bean` factory method with a `BeanFactoryPostProcessor` return type,
you should see an INFO-level log message similar to the following.
[quote]
@Bean method MyConfig.myBfpp is non-static and returns an object assignable to Spring's
BeanFactoryPostProcessor interface. This will result in a failure to process annotations
such as @Autowired, @Resource, and @PostConstruct within the method's declaring
@Configuration class. Add the 'static' modifier to this method to avoid these container
lifecycle issues; see @Bean javadoc for complete details.
NOTE: As with ``BeanPostProcessor``s , you typically do not want to configure
``BeanFactoryPostProcessor``s for lazy initialization. If no other bean references a
`Bean(Factory)PostProcessor`, that post-processor will not get instantiated at all.
@@ -53,7 +53,7 @@ Kotlin::
val trueValue = parser.parseExpression("'black' < 'block'").getValue(Boolean::class.java)
// uses CustomValue:::compareTo
val trueValue = parser.parseExpression("new CustomValue(1) < new CustomValue(2)").getValue(Boolean::class.java);
val trueValue = parser.parseExpression("new CustomValue(1) < new CustomValue(2)").getValue(Boolean::class.java)
----
======
@@ -167,7 +167,7 @@ Kotlin::
[CAUTION]
====
The syntax for the `between` operator is `<input> between {<range_begin>, <range_end>}`,
which is effectively a shortcut for `<input> >= <range_begin> && <input> \<= <range_end>}`.
which is effectively a shortcut for `<input> >= <range_begin> && <input> \<= <range_end>`.
Consequently, `1 between {1, 5}` evaluates to `true`, while `1 between {5, 1}` evaluates
to `false`.
@@ -312,13 +312,13 @@ Kotlin::
// evaluates to 'a'
val ch = parser.parseExpression("'d' - 3")
.getValue(Character::class.java);
.getValue(Char::class.java)
// -- Repeat --
// evaluates to "abcabc"
val repeated = parser.parseExpression("'abc' * 2")
.getValue(String::class.java);
.getValue(String::class.java)
----
======
@@ -485,7 +485,7 @@ Kotlin::
// -- Operator precedence --
val minusTwentyOne = parser.parseExpression("1+2-3*8").getValue(Int::class.java) // -21
val minusTwentyOne = parser.parseExpression("1+2-3*8").getValue(Int::class.java) // -21
----
======
@@ -541,32 +541,7 @@ For example, if we want to overload the `ADD` operator to allow two lists to be
concatenated using the `+` sign, we can implement a custom `OperatorOverloader` as
follows.
[source,java,indent=0,subs="verbatim,quotes"]
----
pubic class ListConcatenation implements OperatorOverloader {
@Override
public boolean overridesOperation(Operation operation, Object left, Object right) {
return (operation == Operation.ADD &&
left instanceof List && right instanceof List);
}
@Override
@SuppressWarnings("unchecked")
public Object operate(Operation operation, Object left, Object right) {
if (operation == Operation.ADD &&
left instanceof List list1 && right instanceof List list2) {
List result = new ArrayList(list1);
result.addAll(list2);
return result;
}
throw new UnsupportedOperationException(
"No overload for operation %s and operands [%s] and [%s]"
.formatted(operation, left, right));
}
}
----
include-code::./ListConcatenation[]
If we register `ListConcatenation` as the `OperatorOverloader` in a
`StandardEvaluationContext`, we can then evaluate expressions like `{1, 2, 3} + {4, 5}`
@@ -589,8 +564,8 @@ Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
StandardEvaluationContext context = StandardEvaluationContext()
context.setOperatorOverloader(ListConcatenation())
val context = StandardEvaluationContext()
context.operatorOverloader = ListConcatenation()
// evaluates to a new list: [1, 2, 3, 4, 5]
parser.parseExpression("{1, 2, 3} + {2 + 2, 5}").getValue(context, List::class.java)
@@ -49,7 +49,6 @@ Kotlin::
<1> Injecting the `ApplicationContext`.
======
Similarly, if your test is configured to load a `WebApplicationContext`, you can inject
the web application context into your test, as follows:
@@ -87,7 +86,6 @@ Kotlin::
<2> Injecting the `WebApplicationContext`.
======
Dependency injection by using `@Autowired` is provided by the
`DependencyInjectionTestExecutionListener`, which is configured by default
(see xref:testing/testcontext-framework/fixture-di.adoc[Dependency Injection of Test Fixtures]).
@@ -96,22 +94,23 @@ Dependency injection by using `@Autowired` is provided by the
Test classes that use the TestContext framework do not need to extend any particular
class or implement a specific interface to configure their application context. Instead,
configuration is achieved by declaring the `@ContextConfiguration` annotation at the
class level. If your test class does not explicitly declare application context resource
locations or component classes, the configured `ContextLoader` determines how to load a
context from a default location or default configuration classes. In addition to context
resource locations and component classes, an application context can also be configured
through application context initializers.
class level. If your test class does not explicitly declare component classes or resource
locations, the configured `ContextLoader` determines how to load a context from _default_
configuration classes or a _default_ location. In addition to component classes and
context resource locations, an application context can also be configured through
xref:testing/testcontext-framework/ctx-management/context-customizers.adoc[context customizers]
or xref:testing/testcontext-framework/ctx-management/initializers.adoc[context initializers].
The following sections explain how to use Spring's `@ContextConfiguration` annotation to
configure a test `ApplicationContext` by using XML configuration files, Groovy scripts,
component classes (typically `@Configuration` classes), or context initializers.
Alternatively, you can implement and configure your own custom `SmartContextLoader` for
advanced use cases.
The following sections explain how to use `@ContextConfiguration` and related annotations
to configure a test `ApplicationContext` by using component classes (typically
`@Configuration` classes), XML configuration files, Groovy scripts, context customizers,
or context initializers. Alternatively, you can implement and configure your own custom
`SmartContextLoader` for advanced use cases.
* xref:testing/testcontext-framework/ctx-management/javaconfig.adoc[Context Configuration with Component Classes]
* xref:testing/testcontext-framework/ctx-management/xml.adoc[Context Configuration with XML resources]
* xref:testing/testcontext-framework/ctx-management/groovy.adoc[Context Configuration with Groovy Scripts]
* xref:testing/testcontext-framework/ctx-management/javaconfig.adoc[Context Configuration with Component Classes]
* xref:testing/testcontext-framework/ctx-management/mixed-config.adoc[Mixing XML, Groovy Scripts, and Component Classes]
* xref:testing/testcontext-framework/ctx-management/mixed-config.adoc[Mixing Component Classes, XML, and Groovy Scripts]
* xref:testing/testcontext-framework/ctx-management/context-customizers.adoc[Context Configuration with Context Customizers]
* xref:testing/testcontext-framework/ctx-management/initializers.adoc[Context Configuration with Context Initializers]
* xref:testing/testcontext-framework/ctx-management/inheritance.adoc[Context Configuration Inheritance]
@@ -1,33 +1,32 @@
[[testcontext-ctx-management-mixed-config]]
= Mixing XML, Groovy Scripts, and Component Classes
= Mixing Component Classes, XML, and Groovy Scripts
It may sometimes be desirable to mix XML configuration files, Groovy scripts, and
component classes (typically `@Configuration` classes) to configure an
`ApplicationContext` for your tests. For example, if you use XML configuration in
production, you may decide that you want to use `@Configuration` classes to configure
It may sometimes be desirable to mix component classes (typically `@Configuration`
classes), XML configuration files, or Groovy scripts to configure an `ApplicationContext`
for your tests. For example, if you use XML configuration in production for legacy
reasons, you may decide that you want to use `@Configuration` classes to configure
specific Spring-managed components for your tests, or vice versa.
Furthermore, some third-party frameworks (such as Spring Boot) provide first-class
support for loading an `ApplicationContext` from different types of resources
simultaneously (for example, XML configuration files, Groovy scripts, and
`@Configuration` classes). The Spring Framework, historically, has not supported this for
standard deployments. Consequently, most of the `SmartContextLoader` implementations that
the Spring Framework delivers in the `spring-test` module support only one resource type
for each test context. However, this does not mean that you cannot use both. One
exception to the general rule is that the `GenericGroovyXmlContextLoader` and
simultaneously (for example, `@Configuration` classes, XML configuration files, and
Groovy scripts). The Spring Framework, historically, has not supported this for standard
deployments. Consequently, most of the `SmartContextLoader` implementations that the
Spring Framework delivers in the `spring-test` module support only one resource type for
each test context. However, this does not mean that you cannot use a mixture of resource
types. One exception to the general rule is that the `GenericGroovyXmlContextLoader` and
`GenericGroovyXmlWebContextLoader` support both XML configuration files and Groovy
scripts simultaneously. Furthermore, third-party frameworks may choose to support the
declaration of both `locations` and `classes` through `@ContextConfiguration`, and, with
declaration of both `classes` and `locations` through `@ContextConfiguration`, and, with
the standard testing support in the TestContext framework, you have the following options.
If you want to use resource locations (for example, XML or Groovy) and `@Configuration`
classes to configure your tests, you must pick one as the entry point, and that one must
include or import the other. For example, in XML or Groovy scripts, you can include
`@Configuration` classes by using component scanning or defining them as normal Spring
beans, whereas, in a `@Configuration` class, you can use `@ImportResource` to import XML
configuration files or Groovy scripts. Note that this behavior is semantically equivalent
If you want to use `@Configuration` classes and resource locations (for example, XML or
Groovy) to configure your tests, you must pick one as the entry point, and that one must
import or include the other. For example, in a `@Configuration` class, you can use
`@ImportResource` to import XML configuration files or Groovy scripts; whereas, in XML or
Groovy scripts, you can include `@Configuration` classes by using component scanning or
defining them as normal Spring beans. Note that this behavior is semantically equivalent
to how you configure your application in production: In production configuration, you
define either a set of XML or Groovy resource locations or a set of `@Configuration`
classes from which your production `ApplicationContext` is loaded, but you still have the
freedom to include or import the other type of configuration.
define either a set of `@Configuration` classes or a set of XML or Groovy resource
locations from which your production `ApplicationContext` is loaded, but you still have
the freedom to import or include the other type of configuration.
@@ -251,7 +251,8 @@ Kotlin::
======
TIP: If you need to have a `LocalValidatorFactoryBean` injected somewhere, create a bean and
mark it with `@Primary` in order to avoid conflict with the one declared in the MVC config.
mark it with `@Primary`, or mark the one declared in the MVC config with `@Fallback`, in
order to avoid conflict.
[[webflux-config-content-negotiation]]
@@ -19,4 +19,5 @@ example shows:
include-code::./MyController[tag=snippet,indent=0]
TIP: If you need to have a `LocalValidatorFactoryBean` injected somewhere, create a bean and
mark it with `@Primary` in order to avoid conflict with the one declared in the MVC configuration.
mark it with `@Primary`, or mark the one declared in the MVC configuration with
`@Fallback`, in order to avoid conflict.
@@ -3,7 +3,7 @@
[.small]#xref:web/webflux/uri-building.adoc[See equivalent in the Reactive stack]#
This section describes various options available in the Spring Framework to work with URI's.
This section describes various options available in the Spring Framework to work with URIs.
include::partial$web/web-uris.adoc[leveloffset=+1]
@@ -2,7 +2,7 @@
= UriComponents
[.small]#Spring MVC and Spring WebFlux#
`UriComponentsBuilder` helps to build URI's from URI templates with variables, as the following example shows:
`UriComponentsBuilder` helps to build URIs from URI templates with variables, as the following example shows:
[tabs]
======
@@ -128,7 +128,7 @@ Kotlin::
= UriBuilder
[.small]#Spring MVC and Spring WebFlux#
<<web-uricomponents, `UriComponentsBuilder`>> implements `UriBuilder`. You can create a
<<uricomponents, `UriComponentsBuilder`>> implements `UriBuilder`. You can create a
`UriBuilder`, in turn, with a `UriBuilderFactory`. Together, `UriBuilderFactory` and
`UriBuilder` provide a pluggable mechanism to build URIs from URI templates, based on
shared configuration, such as a base URL, encoding preferences, and other details.
@@ -247,7 +247,7 @@ and treats deviations from the syntax as illegal.
https://github.com/web-platform-tests/wpt/tree/master/url[URL parsing algorithm] in the
https://url.spec.whatwg.org[WhatWG URL Living standard]. It provides lenient handling of
a wide range of cases of unexpected input. Browsers implement this in order to handle
leniently user typed URL's. For more details, see the URL Living Standard and URL parsing
leniently user typed URLs. For more details, see the URL Living Standard and URL parsing
https://github.com/web-platform-tests/wpt/tree/master/url[test cases].
By default, `RestClient`, `WebClient`, and `RestTemplate` use the RFC parser type, and
@@ -255,10 +255,10 @@ expect applications to provide with URL templates that conform to RFC syntax. To
that you can customize the `UriBuilderFactory` on any of the clients.
Applications and frameworks may further rely on `UriComponentsBuilder` for their own needs
to parse user provided URL's in order to inspect and possibly validated URI components
to parse user provided URLs in order to inspect and possibly validated URI components
such as the scheme, host, port, path, and query. Such components can decide to use the
WhatWG parser type in order to handle URL's more leniently, and to align with the way
browsers parse URI's, in case of a redirect to the input URL or if it is included in a
WhatWG parser type in order to handle URLs more leniently, and to align with the way
browsers parse URIs, in case of a redirect to the input URL or if it is included in a
response to a browser.
@@ -373,14 +373,14 @@ Java::
[source,java,indent=0,subs="verbatim,quotes"]
----
String baseUrl = "https://example.com";
DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(baseUrl)
DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(baseUrl);
factory.setEncodingMode(EncodingMode.TEMPLATE_AND_VALUES);
// Customize the RestTemplate..
// Customize the RestTemplate.
RestTemplate restTemplate = new RestTemplate();
restTemplate.setUriTemplateHandler(factory);
// Customize the WebClient..
// Customize the WebClient.
WebClient client = WebClient.builder().uriBuilderFactory(factory).build();
----
@@ -393,12 +393,12 @@ Kotlin::
encodingMode = EncodingMode.TEMPLATE_AND_VALUES
}
// Customize the RestTemplate..
// Customize the RestTemplate.
val restTemplate = RestTemplate().apply {
uriTemplateHandler = factory
}
// Customize the WebClient..
// Customize the WebClient.
val client = WebClient.builder().uriBuilderFactory(factory).build()
----
======
+1 -1
View File
@@ -5,7 +5,7 @@
"@antora/collector-extension": "1.0.2",
"@asciidoctor/tabs": "1.0.0-beta.6",
"@springio/antora-extensions": "1.14.7",
"fast-xml-parser": "5.3.4",
"fast-xml-parser": "5.3.8",
"@springio/asciidoctor-extensions": "1.0.0-alpha.17"
}
}
@@ -0,0 +1,47 @@
/*
* 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.docs.core.expressions.languageref.expressionsoperatorsoverloaded;
import org.springframework.expression.Operation;
import org.springframework.expression.OperatorOverloader;
import java.util.ArrayList;
import java.util.List;
public class ListConcatenation implements OperatorOverloader {
@Override
public boolean overridesOperation(Operation operation, Object left, Object right) {
return (operation == Operation.ADD && left instanceof List && right instanceof List);
}
@Override
@SuppressWarnings({"rawtypes", "unchecked"})
public Object operate(Operation operation, Object left, Object right) {
if (operation == Operation.ADD &&
left instanceof List list1 && right instanceof List list2) {
List result = new ArrayList(list1);
result.addAll(list2);
return result;
}
throw new UnsupportedOperationException(
"No overload for operation %s and operands [%s] and [%s]"
.formatted(operation, left, right));
}
}
@@ -0,0 +1,37 @@
/*
* 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.docs.core.expressions.languageref.expressionsoperatorsoverloaded
import org.springframework.expression.Operation
import org.springframework.expression.OperatorOverloader
class ListConcatenation: OperatorOverloader {
override fun overridesOperation(operation: Operation, left: Any?, right: Any?): Boolean {
return operation == Operation.ADD && left is List<*> && right is List<*>
}
override fun operate(operation: Operation, left: Any?, right: Any?): Any {
if (operation == Operation.ADD && left is List<*> && right is List<*>) {
return left + right
}
throw UnsupportedOperationException(
"No overload for operation $operation and operands [$left] and [$right]")
}
}
+12 -12
View File
@@ -8,20 +8,20 @@ javaPlatform {
dependencies {
api(platform("com.fasterxml.jackson:jackson-bom:2.18.5"))
api(platform("io.micrometer:micrometer-bom:1.15.9"))
api(platform("io.micrometer:micrometer-bom:1.15.10"))
api(platform("io.netty:netty-bom:4.1.130.Final"))
api(platform("io.netty:netty5-bom:5.0.0.Alpha5"))
api(platform("io.projectreactor:reactor-bom:2024.0.15"))
api(platform("io.projectreactor:reactor-bom:2024.0.16"))
api(platform("io.rsocket:rsocket-bom:1.1.5"))
api(platform("org.apache.groovy:groovy-bom:4.0.30"))
api(platform("org.apache.logging.log4j:log4j-bom:2.21.1"))
api(platform("org.assertj:assertj-bom:3.27.7"))
api(platform("org.eclipse.jetty:jetty-bom:12.0.30"))
api(platform("org.eclipse.jetty.ee10:jetty-ee10-bom:12.0.30"))
api(platform("org.eclipse.jetty:jetty-bom:12.0.33"))
api(platform("org.eclipse.jetty.ee10:jetty-ee10-bom:12.0.33"))
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.14.2"))
api(platform("org.mockito:mockito-bom:5.21.0"))
api(platform("org.junit:junit-bom:5.14.3"))
api(platform("org.mockito:mockito-bom:5.22.0"))
constraints {
api("com.fasterxml:aalto-xml:1.3.4")
@@ -31,8 +31,8 @@ dependencies {
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.2")
api("com.google.protobuf:protobuf-java-util:4.33.4")
api("com.h2database:h2:2.3.232")
api("com.google.protobuf:protobuf-java-util:4.34.0")
api("com.h2database:h2:2.4.240")
api("com.jayway.jsonpath:json-path:2.10.0")
api("com.oracle.database.jdbc:ojdbc11:21.9.0.0")
api("com.rometools:rome:1.19.0")
@@ -50,7 +50,7 @@ dependencies {
api("io.mockk:mockk:1.13.17")
api("io.projectreactor.netty:reactor-netty5-http:2.0.0-M3")
api("io.projectreactor.tools:blockhound:1.0.8.RELEASE")
api("io.r2dbc:r2dbc-h2:1.0.0.RELEASE")
api("io.r2dbc:r2dbc-h2:1.1.0.RELEASE")
api("io.r2dbc:r2dbc-spi-test:1.0.0.RELEASE")
api("io.r2dbc:r2dbc-spi:1.0.0.RELEASE")
api("io.reactivex.rxjava3:rxjava:3.1.12")
@@ -137,8 +137,8 @@ dependencies {
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.40.0")
api("org.seleniumhq.selenium:selenium-java:4.40.0")
api("org.seleniumhq.selenium:htmlunit3-driver:4.41.0")
api("org.seleniumhq.selenium:selenium-java:4.41.0")
api("org.skyscreamer:jsonassert:1.5.3")
api("org.slf4j:slf4j-api:2.0.17")
api("org.testng:testng:7.11.0")
@@ -147,6 +147,6 @@ dependencies {
api("org.webjars:webjars-locator-lite:1.1.0")
api("org.xmlunit:xmlunit-assertj:2.10.4")
api("org.xmlunit:xmlunit-matchers:2.10.4")
api("org.yaml:snakeyaml:2.5")
api("org.yaml:snakeyaml:2.6")
}
}
+1 -1
View File
@@ -1,4 +1,4 @@
version=6.2.16-SNAPSHOT
version=6.2.17
org.gradle.caching=true
org.gradle.jvmargs=-Xmx2048m
@@ -40,6 +40,13 @@ import org.springframework.beans.BeansException;
* A {@code BeanFactoryPostProcessor} may also be registered programmatically
* with a {@code ConfigurableApplicationContext}.
*
* <p>When registering a {@code BeanFactoryPostProcessor} via an {@code @Bean} method
* in a {@code @Configuration} class, use a {@code static} method to avoid eager
* initialization of other beans in the configuration class. See the
* "BeanFactoryPostProcessor-returning {@code @Bean} methods" section in
* {@link org.springframework.context.annotation.Bean @Bean}'s javadoc for details
* and an example.
*
* <h3>Ordering</h3>
* <p>{@code BeanFactoryPostProcessor} beans that are autodetected in an
* {@code ApplicationContext} will be ordered according to
@@ -34,6 +34,14 @@ import org.springframework.lang.Nullable;
* created. A plain {@code BeanFactory} allows for programmatic registration of
* post-processors, applying them to all beans created through the bean factory.
*
* <p>When registering a {@code BeanPostProcessor} via an {@code @Bean} method in
* a {@code @Configuration} class, use a {@code static} method with ideally no
* dependencies in order to avoid eager initialization that can make other beans
* ineligible for full post-processing. See the "BeanPostProcessor-returning
* {@code @Bean} methods" section in
* {@link org.springframework.context.annotation.Bean @Bean}'s javadoc for details
* and an example.
*
* <h3>Ordering</h3>
* <p>{@code BeanPostProcessor} beans that are autodetected in an
* {@code ApplicationContext} will be ordered according to
@@ -35,12 +35,11 @@ import org.springframework.core.annotation.AliasFor;
* example:
*
* <pre class="code">
* &#064;Bean
* public MyBean myBean() {
* // instantiate and configure MyBean obj
* return obj;
* }
* </pre>
* &#064;Bean
* public MyBean myBean() {
* // instantiate and configure MyBean obj
* return obj;
* }</pre>
*
* <h3>Bean Names</h3>
*
@@ -52,31 +51,29 @@ import org.springframework.core.annotation.AliasFor;
* (i.e. a primary bean name plus one or more aliases) for a single bean.
*
* <pre class="code">
* &#064;Bean({"b1", "b2"}) // bean available as 'b1' and 'b2', but not 'myBean'
* public MyBean myBean() {
* // instantiate and configure MyBean obj
* return obj;
* }
* </pre>
* &#064;Bean({"b1", "b2"}) // bean available as 'b1' and 'b2', but not 'myBean'
* public MyBean myBean() {
* // instantiate and configure MyBean obj
* return obj;
* }</pre>
*
* <h3>Profile, Scope, Lazy, DependsOn, Primary, Order</h3>
* <h3>Profile, Scope, Lazy, DependsOn, Primary, Fallback, Order</h3>
*
* <p>Note that the {@code @Bean} annotation does not provide attributes for profile,
* scope, lazy, depends-on or primary. Rather, it should be used in conjunction with
* {@link Scope @Scope}, {@link Lazy @Lazy}, {@link DependsOn @DependsOn} and
* scope, lazy, depends-on, or primary. Rather, it should be used in conjunction with
* {@link Scope @Scope}, {@link Lazy @Lazy}, {@link DependsOn @DependsOn}, and
* {@link Primary @Primary} annotations to declare those semantics. For example:
*
* <pre class="code">
* &#064;Bean
* &#064;Profile("production")
* &#064;Scope("prototype")
* public MyBean myBean() {
* // instantiate and configure MyBean obj
* return obj;
* }
* </pre>
* &#064;Bean
* &#064;Profile("production")
* &#064;Scope("prototype")
* public MyBean myBean() {
* // instantiate and configure MyBean obj
* return obj;
* }</pre>
*
* The semantics of the above-mentioned annotations match their use at the component
* The semantics of the aforementioned annotations match their use at the component
* class level: {@code @Profile} allows for selective inclusion of certain beans.
* {@code @Scope} changes the bean's scope from singleton to the specified scope.
* {@code @Lazy} only has an actual effect in case of the default singleton scope.
@@ -85,6 +82,9 @@ import org.springframework.core.annotation.AliasFor;
* through direct references, which is typically helpful for singleton startup.
* {@code @Primary} is a mechanism to resolve ambiguity at the injection point level
* if a single target component needs to be injected but several beans match by type.
* {@link Fallback @Fallback} marks a bean as a fallback candidate in such scenarios;
* if all beans but one among multiple matching candidates are marked as fallback, the
* remaining bean will be selected.
*
* <p>Additionally, {@code @Bean} methods may also declare qualifier annotations
* and {@link org.springframework.core.annotation.Order @Order} values, to be
@@ -100,8 +100,8 @@ import org.springframework.core.annotation.AliasFor;
* orthogonal concern determined by dependency relationships and {@code @DependsOn}
* declarations as mentioned above. Also, {@link jakarta.annotation.Priority} is not
* available at this level since it cannot be declared on methods; its semantics can
* be modeled through {@code @Order} values in combination with {@code @Primary} on
* a single bean per type.
* be modeled through {@code @Order} values in combination with {@code @Primary} or
* {@code @Fallback} on a single bean per type.
*
* <h3>{@code @Bean} Methods in {@code @Configuration} Classes</h3>
*
@@ -119,17 +119,17 @@ import org.springframework.core.annotation.AliasFor;
* &#064;Configuration
* public class AppConfig {
*
* &#064;Bean
* public FooService fooService() {
* return new FooService(fooRepository());
* }
* &#064;Bean
* public FooService fooService() {
* return new FooService(fooRepository());
* }
*
* &#064;Bean
* public FooRepository fooRepository() {
* return new JdbcFooRepository(dataSource());
* }
* &#064;Bean
* public FooRepository fooRepository() {
* return new JdbcFooRepository(dataSource());
* }
*
* // ...
* // ...
* }</pre>
*
* <h3>{@code @Bean} <em>Lite</em> Mode</h3>
@@ -158,20 +158,21 @@ import org.springframework.core.annotation.AliasFor;
* <pre class="code">
* &#064;Component
* public class Calculator {
* public int sum(int a, int b) {
* return a+b;
* }
* public int sum(int a, int b) {
* return a+b;
* }
*
* &#064;Bean
* public MyBean myBean() {
* return new MyBean();
* }
* &#064;Bean
* public MyBean myBean() {
* return new MyBean();
* }
* }</pre>
*
* <h3>Bootstrapping</h3>
*
* <p>See the @{@link Configuration} javadoc for further details including how to bootstrap
* the container using {@link AnnotationConfigApplicationContext} and friends.
* <p>See the {@link Configuration @Configuration} javadoc for further details
* including how to bootstrap the container using
* {@link AnnotationConfigApplicationContext} and friends.
*
* <h3>{@code BeanFactoryPostProcessor}-returning {@code @Bean} methods</h3>
*
@@ -183,20 +184,44 @@ import org.springframework.core.annotation.AliasFor;
* lifecycle issues, mark {@code BFPP}-returning {@code @Bean} methods as {@code static}. For example:
*
* <pre class="code">
* &#064;Bean
* public static PropertySourcesPlaceholderConfigurer pspc() {
* // instantiate, configure and return pspc ...
* }
* </pre>
* &#064;Bean
* public static PropertySourcesPlaceholderConfigurer pspc() {
* // instantiate, configure and return pspc ...
* }</pre>
*
* By marking this method as {@code static}, it can be invoked without causing instantiation of its
* declaring {@code @Configuration} class, thus avoiding the above-mentioned lifecycle conflicts.
* declaring {@code @Configuration} class, thus avoiding the aforementioned lifecycle conflicts.
* Note however that {@code static} {@code @Bean} methods will not be enhanced for scoping and AOP
* semantics as mentioned above. This works out in {@code BFPP} cases, as they are not typically
* referenced by other {@code @Bean} methods. As a reminder, an INFO-level log message will be
* issued for any non-static {@code @Bean} methods having a return type assignable to
* {@code BeanFactoryPostProcessor}.
*
* <h3>{@code BeanPostProcessor}-returning {@code @Bean} methods</h3>
*
* <p>Similarly, special consideration must be taken for {@code @Bean} methods that return Spring
* {@link org.springframework.beans.factory.config.BeanPostProcessor BeanPostProcessor}
* ({@code BPP}) types. Because {@code BPP} objects must be instantiated early in the container
* lifecycle, a non-static {@code @Bean} method that returns a {@code BPP} will cause eager
* initialization of its declaring {@code @Configuration} class, which can make other beans in the
* {@code @Configuration} class (as well as depencencies of those beans) ineligible for full
* post-processing. To avoid these lifecycle issues, mark {@code BPP}-returning {@code @Bean}
* methods as {@code static}. For example:
*
* <pre class="code">
* &#064;Bean
* public static MyBeanPostProcessor myBeanPostProcessor() {
* return new MyBeanPostProcessor();
* }</pre>
*
* By marking this method as {@code static}, it can be invoked without causing instantiation of its
* declaring {@code @Configuration} class. Furthermore, the method should ideally not declare any
* dependencies so that the container does not need to instantiate other beans to create the
* post-processor, which would make those beans ineligible for post-processing as well. For any such
* bean, you should see a WARN-level log message similar to the following: "Bean 'someBean' of type
* [org.example.SomeType] is not eligible for getting processed by all BeanPostProcessors (for example:
* not eligible for auto-proxying)".
*
* @author Rod Johnson
* @author Costin Leau
* @author Chris Beams
@@ -208,6 +233,7 @@ import org.springframework.core.annotation.AliasFor;
* @see DependsOn
* @see Lazy
* @see Primary
* @see Fallback
* @see org.springframework.stereotype.Component
* @see org.springframework.beans.factory.annotation.Autowired
* @see org.springframework.beans.factory.annotation.Value
@@ -299,8 +299,8 @@ class ConfigurationClassBeanDefinitionReader {
}
if (logger.isTraceEnabled()) {
logger.trace("Registering bean definition for @Bean method %s.%s()"
.formatted(configClass.getMetadata().getClassName(), beanName));
logger.trace("Registering bean definition for @Bean method %s.%s() with bean name '%s'"
.formatted(configClass.getMetadata().getClassName(), methodName, beanName));
}
this.registry.registerBeanDefinition(beanName, beanDefToRegister);
}
@@ -357,8 +357,8 @@ class ConfigurationClassBeanDefinitionReader {
"@Bean definition illegally overridden by existing bean definition: " + existingBeanDef);
}
if (logger.isDebugEnabled()) {
logger.debug("Skipping bean definition for %s: a definition for bean '%s' already exists. " +
"This top-level bean definition is considered as an override.".formatted(beanMethod, beanName));
logger.debug(("Skipping bean definition for %s: a definition for bean '%s' already exists. " +
"This top-level bean definition is considered as an override.").formatted(beanMethod, beanName));
}
return true;
}
@@ -389,13 +389,13 @@ class ConfigurationClassEnhancer {
// create the bean instance.
if (logger.isInfoEnabled() &&
BeanFactoryPostProcessor.class.isAssignableFrom(beanMethod.getReturnType())) {
logger.info(String.format("@Bean method %s.%s is non-static and returns an object " +
"assignable to Spring's BeanFactoryPostProcessor interface. This will " +
"result in a failure to process annotations such as @Autowired, " +
"@Resource and @PostConstruct within the method's declaring " +
"@Configuration class. Add the 'static' modifier to this method to avoid " +
"these container lifecycle issues; see @Bean javadoc for complete details.",
beanMethod.getDeclaringClass().getSimpleName(), beanMethod.getName()));
logger.info("""
@Bean method %s.%s is non-static and returns an object assignable to Spring's \
BeanFactoryPostProcessor interface. This will result in a failure to process \
annotations such as @Autowired, @Resource, and @PostConstruct within the method's \
declaring @Configuration class. Add the 'static' modifier to this method to avoid \
these container lifecycle issues; see @Bean javadoc for complete details."""
.formatted(beanMethod.getDeclaringClass().getSimpleName(), beanMethod.getName()));
}
return cglibMethodProxy.invokeSuper(enhancedConfigInstance, beanMethodArgs);
}
@@ -54,7 +54,7 @@ public abstract class AbstractResourceBasedMessageSource extends AbstractMessage
* Set a single basename, following the basic ResourceBundle convention
* of not specifying file extension or language codes. The resource location
* format is up to the specific {@code MessageSource} implementation.
* <p>Regular and XMl properties files are supported: for example, "messages" will find
* <p>Regular and XML properties files are supported: for example, "messages" will find
* a "messages.properties", "messages_en.properties" etc arrangement as well
* as "messages.xml", "messages_en.xml" etc.
* @param basename the single basename
@@ -23,6 +23,8 @@ import java.util.concurrent.ConcurrentHashMap;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.beans.factory.config.DestructionAwareBeanPostProcessor;
import org.springframework.beans.factory.support.MergedBeanDefinitionPostProcessor;
import org.springframework.beans.factory.support.RootBeanDefinition;
@@ -98,7 +100,8 @@ class ApplicationListenerDetector implements DestructionAwareBeanPostProcessor,
try {
ApplicationEventMulticaster multicaster = this.applicationContext.getApplicationEventMulticaster();
multicaster.removeApplicationListener(applicationListener);
multicaster.removeApplicationListenerBean(beanName);
multicaster.removeApplicationListenerBean(
bean instanceof FactoryBean ? BeanFactory.FACTORY_BEAN_PREFIX + beanName : beanName);
}
catch (IllegalStateException ex) {
// ApplicationEventMulticaster not initialized yet - no need to remove a listener
@@ -92,10 +92,12 @@ import org.springframework.util.StringUtils;
public class ReloadableResourceBundleMessageSource extends AbstractResourceBasedMessageSource
implements ResourceLoaderAware {
private static final String PROPERTIES_EXTENSION = ".properties";
private static final String XML_EXTENSION = ".xml";
private List<String> fileExtensions = List.of(".properties", XML_EXTENSION);
private List<String> fileExtensions = List.of(PROPERTIES_EXTENSION, XML_EXTENSION);
@Nullable
private Properties fileEncodings;
@@ -381,18 +383,18 @@ public class ReloadableResourceBundleMessageSource extends AbstractResourceBased
StringBuilder temp = new StringBuilder(basename);
temp.append('_');
if (language.length() > 0) {
if (!language.isEmpty()) {
temp.append(language);
result.add(0, temp.toString());
}
temp.append('_');
if (country.length() > 0) {
if (!country.isEmpty()) {
temp.append(country);
result.add(0, temp.toString());
}
if (variant.length() > 0 && (language.length() > 0 || country.length() > 0)) {
if (!variant.isEmpty() && (!language.isEmpty() || !country.isEmpty())) {
temp.append('_').append(variant);
result.add(0, temp.toString());
}
@@ -562,13 +564,13 @@ public class ReloadableResourceBundleMessageSource extends AbstractResourceBased
*/
protected Properties loadProperties(Resource resource, String filename) throws IOException {
Properties props = newProperties();
try (InputStream is = resource.getInputStream()) {
try (InputStream inputStream = resource.getInputStream()) {
String resourceFilename = resource.getFilename();
if (resourceFilename != null && resourceFilename.endsWith(XML_EXTENSION)) {
if (logger.isDebugEnabled()) {
logger.debug("Loading properties [" + resource.getFilename() + "]");
}
this.propertiesPersister.loadFromXml(props, is);
this.propertiesPersister.loadFromXml(props, inputStream);
}
else {
String encoding = null;
@@ -582,13 +584,13 @@ public class ReloadableResourceBundleMessageSource extends AbstractResourceBased
if (logger.isDebugEnabled()) {
logger.debug("Loading properties [" + resource.getFilename() + "] with encoding '" + encoding + "'");
}
this.propertiesPersister.load(props, new InputStreamReader(is, encoding));
this.propertiesPersister.load(props, new InputStreamReader(inputStream, encoding));
}
else {
if (logger.isDebugEnabled()) {
logger.debug("Loading properties [" + resource.getFilename() + "]");
}
this.propertiesPersister.load(props, is);
this.propertiesPersister.load(props, inputStream);
}
}
return props;
@@ -242,8 +242,8 @@ public class ThreadPoolTaskExecutor extends ExecutorConfigurationSupport
* @since 6.1.4
* @see #initiateShutdown()
*/
public void setStrictEarlyShutdown(boolean defaultEarlyShutdown) {
this.strictEarlyShutdown = defaultEarlyShutdown;
public void setStrictEarlyShutdown(boolean strictEarlyShutdown) {
this.strictEarlyShutdown = strictEarlyShutdown;
}
/**
@@ -29,6 +29,7 @@ import org.mockito.ArgumentCaptor;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanPostProcessor;
@@ -379,12 +380,15 @@ class ApplicationContextEventTests extends AbstractApplicationEventListenerTests
RootBeanDefinition listener1Def = new RootBeanDefinition(MyOrderedListener1.class);
listener1Def.setDependsOn("nestedChild");
context.registerBeanDefinition("listener1", listener1Def);
context.registerBeanDefinition("listenerFb", new RootBeanDefinition(MyFactoryBeanListener.class));
context.refresh();
MyOrderedListener1 listener1 = context.getBean("listener1", MyOrderedListener1.class);
MyFactoryBeanListener listenerFb = context.getBean("&listenerFb", MyFactoryBeanListener.class);
MyEvent event1 = new MyEvent(context);
context.publishEvent(event1);
assertThat(listener1.seenEvents).contains(event1);
assertThat(listenerFb.seenEvents).contains(event1);
SimpleApplicationEventMulticaster multicaster = context.getBean(SimpleApplicationEventMulticaster.class);
assertThat(multicaster.getApplicationListeners()).isNotEmpty();
@@ -726,6 +730,27 @@ class ApplicationContextEventTests extends AbstractApplicationEventListenerTests
}
public static class MyFactoryBeanListener implements FactoryBean<String>, ApplicationListener<ApplicationEvent> {
public final List<ApplicationEvent> seenEvents = new ArrayList<>();
@Override
public void onApplicationEvent(ApplicationEvent event) {
this.seenEvents.add(event);
}
@Override
public String getObject() {
return "";
}
@Override
public Class<?> getObjectType() {
return String.class;
}
}
public static class EventPublishingBeanPostProcessor implements BeanPostProcessor, ApplicationContextAware {
private ApplicationContext applicationContext;
@@ -128,16 +128,16 @@ public class ResolvableType implements Serializable {
private Class<?> resolved;
@Nullable
private volatile ResolvableType superType;
private transient volatile ResolvableType superType;
@Nullable
private volatile ResolvableType[] interfaces;
private transient volatile ResolvableType[] interfaces;
@Nullable
private volatile ResolvableType[] generics;
private transient volatile ResolvableType[] generics;
@Nullable
private volatile Boolean unresolvableGenerics;
private transient volatile Boolean unresolvableGenerics;
/**
@@ -53,8 +53,10 @@ final class AnnotationTypeMapping {
private static final Log logger = LogFactory.getLog(AnnotationTypeMapping.class);
private static final Predicate<? super Annotation> isBeanValidationConstraint = annotation ->
annotation.annotationType().getName().equals("jakarta.validation.Constraint");
private static final Predicate<? super Annotation> isBeanValidationConstraint = annotation -> {
String name = annotation.annotationType().getName();
return (name.equals("jakarta.validation.Constraint") || name.equals("javax.validation.Constraint"));
};
/**
* Set used to track which convention-based annotation attribute overrides
@@ -19,10 +19,12 @@ package org.springframework.core.task;
import java.io.Serializable;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicBoolean;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
@@ -96,7 +98,9 @@ public class SimpleAsyncTaskExecutor extends CustomizableThreadCreator
private boolean rejectTasksWhenLimitReached = false;
private volatile boolean active = true;
private final AtomicBoolean closed = new AtomicBoolean();
private boolean cancelled = false; // within activeThreads synchronization
/**
@@ -217,7 +221,9 @@ public class SimpleAsyncTaskExecutor extends CustomizableThreadCreator
/**
* Specify whether to reject tasks when the concurrency limit has been reached,
* throwing {@link TaskRejectedException} on any further execution attempts.
* throwing {@link TaskRejectedException} (which extends the common
* {@link java.util.concurrent.RejectedExecutionException})
* on any further execution attempts.
* <p>The default is {@code false}, blocking the caller until the submission can
* be accepted. Switch this to {@code true} for immediate rejection instead.
* @since 6.2.6
@@ -270,7 +276,7 @@ public class SimpleAsyncTaskExecutor extends CustomizableThreadCreator
* @see #close()
*/
public boolean isActive() {
return this.active;
return !this.closed.get();
}
/**
@@ -303,19 +309,20 @@ public class SimpleAsyncTaskExecutor extends CustomizableThreadCreator
* @see #TIMEOUT_IMMEDIATE
* @see #doExecute(Runnable)
*/
@Deprecated
@Deprecated(since = "5.3.16")
@Override
public void execute(Runnable task, long startTimeout) {
Assert.notNull(task, "Runnable must not be null");
if (!isActive()) {
throw new TaskRejectedException(getClass().getSimpleName() + " has been closed already");
throw new TaskRejectedException(getClass().getSimpleName() + " is not active");
}
Runnable taskToUse = (this.taskDecorator != null ? this.taskDecorator.decorate(task) : task);
Future<?> future = (task instanceof Future<?> f ? f : null);
if (isThrottleActive() && startTimeout > TIMEOUT_IMMEDIATE) {
this.concurrencyThrottle.beforeAccess();
try {
doExecute(new TaskTrackingRunnable(taskToUse));
doExecute(new TaskTrackingRunnable(taskToUse, future));
}
catch (Throwable ex) {
// Release concurrency permit if thread creation fails
@@ -325,7 +332,7 @@ public class SimpleAsyncTaskExecutor extends CustomizableThreadCreator
}
}
else if (this.activeThreads != null) {
doExecute(new TaskTrackingRunnable(taskToUse));
doExecute(new TaskTrackingRunnable(taskToUse, future));
}
else {
doExecute(taskToUse);
@@ -401,11 +408,13 @@ public class SimpleAsyncTaskExecutor extends CustomizableThreadCreator
*/
@Override
public void close() {
if (this.active) {
this.active = false;
if (this.closed.compareAndSet(false, true)) {
Set<Thread> threads = this.activeThreads;
if (threads != null) {
if (this.cancelRemainingTasksOnClose) {
synchronized (threads) {
this.cancelled = true;
}
// Early interrupt for remaining tasks on close
threads.forEach(Thread::interrupt);
}
@@ -419,6 +428,7 @@ public class SimpleAsyncTaskExecutor extends CustomizableThreadCreator
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
this.cancelled = true;
}
if (!this.cancelRemainingTasksOnClose) {
// Late interrupt for remaining tasks after timeout
@@ -429,6 +439,15 @@ public class SimpleAsyncTaskExecutor extends CustomizableThreadCreator
}
}
private void checkCancelled(@Nullable Future<?> future) {
if (this.cancelled) { // within synchronization from TaskTrackingRunnable
if (future != null) {
future.cancel(false);
}
throw new CancellationException(getClass().getSimpleName() + " has cancelled all remaining tasks");
}
}
/**
* Subclass of the general ConcurrencyThrottleSupport class,
@@ -465,9 +484,12 @@ public class SimpleAsyncTaskExecutor extends CustomizableThreadCreator
private final Runnable task;
public TaskTrackingRunnable(Runnable task) {
private final @Nullable Future<?> future;
public TaskTrackingRunnable(Runnable task, @Nullable Future<?> future) {
Assert.notNull(task, "Task must not be null");
this.task = task;
this.future = future;
}
@Override
@@ -476,7 +498,10 @@ public class SimpleAsyncTaskExecutor extends CustomizableThreadCreator
Thread thread = null;
if (threads != null) {
thread = Thread.currentThread();
threads.add(thread);
synchronized (threads) {
checkCancelled(this.future);
threads.add(thread);
}
}
try {
this.task.run();
@@ -484,7 +509,7 @@ public class SimpleAsyncTaskExecutor extends CustomizableThreadCreator
finally {
if (threads != null) {
threads.remove(thread);
if (!isActive()) {
if (closed.get()) {
synchronized (threads) {
if (threads.isEmpty()) {
threads.notify();
@@ -120,6 +120,7 @@ class MergedAnnotationReadingVisitor<A extends Annotation> extends AnnotationVis
return new MergedAnnotationReadingVisitor<>(this.classLoader, this.source, type, consumer);
}
@SuppressWarnings("unchecked")
@Nullable
static <A extends Annotation> AnnotationVisitor get(@Nullable ClassLoader classLoader,
@@ -142,31 +142,6 @@ public class MimeType implements Comparable<MimeType>, Serializable {
*/
public MimeType(String type, String subtype, Charset charset) {
this(type, subtype, Collections.singletonMap(PARAM_CHARSET, charset.name()));
this.resolvedCharset = charset;
}
/**
* Copy-constructor that copies the type, subtype, parameters of the given {@code MimeType},
* and allows to set the specified character set.
* @param other the other MimeType
* @param charset the character set
* @throws IllegalArgumentException if any of the parameters contains illegal characters
* @since 4.3
*/
public MimeType(MimeType other, Charset charset) {
this(other.getType(), other.getSubtype(), addCharsetParameter(charset, other.getParameters()));
this.resolvedCharset = charset;
}
/**
* Copy-constructor that copies the type and subtype of the given {@code MimeType},
* and allows for different parameter.
* @param other the other MimeType
* @param parameters the parameters (may be {@code null})
* @throws IllegalArgumentException if any of the parameters contains illegal characters
*/
public MimeType(MimeType other, @Nullable Map<String, String> parameters) {
this(other.getType(), other.getSubtype(), parameters);
}
/**
@@ -183,16 +158,43 @@ public class MimeType implements Comparable<MimeType>, Serializable {
checkToken(subtype);
this.type = type.toLowerCase(Locale.ROOT);
this.subtype = subtype.toLowerCase(Locale.ROOT);
if (!CollectionUtils.isEmpty(parameters)) {
Map<String, String> map = new LinkedCaseInsensitiveMap<>(parameters.size(), Locale.ROOT);
parameters.forEach((parameter, value) -> {
checkParameters(parameter, value);
map.put(parameter, value);
});
this.parameters = Collections.unmodifiableMap(map);
this.parameters = createParametersMap(parameters);
if (this.parameters.containsKey(PARAM_CHARSET)) {
this.resolvedCharset = Charset.forName(unquote(this.parameters.get(PARAM_CHARSET)));
}
else {
this.parameters = Collections.emptyMap();
}
/**
* Copy-constructor that copies the type, subtype, parameters of the given {@code MimeType},
* and allows to set the specified character set.
* @param other the other MimeType
* @param charset the character set
* @throws IllegalArgumentException if any of the parameters contains illegal characters
* @since 4.3
*/
public MimeType(MimeType other, Charset charset) {
this.type = other.type;
this.subtype = other.subtype;
Map<String, String> map = new LinkedCaseInsensitiveMap<>(other.parameters.size() + 1, Locale.ROOT);
map.putAll(other.parameters);
map.put(PARAM_CHARSET, charset.name());
this.parameters = Collections.unmodifiableMap(map);
this.resolvedCharset = charset;
}
/**
* Copy-constructor that copies the type and subtype of the given {@code MimeType},
* and allows for different parameter.
* @param other the other MimeType
* @param parameters the parameters (may be {@code null})
* @throws IllegalArgumentException if any of the parameters contains illegal characters
*/
public MimeType(MimeType other, @Nullable Map<String, String> parameters) {
this.type = other.type;
this.subtype = other.subtype;
this.parameters = createParametersMap(parameters);
if (this.parameters.containsKey(PARAM_CHARSET)) {
this.resolvedCharset = Charset.forName(unquote(this.parameters.get(PARAM_CHARSET)));
}
}
@@ -225,16 +227,25 @@ public class MimeType implements Comparable<MimeType>, Serializable {
}
}
private Map<String, String> createParametersMap(@Nullable Map<String, String> parameters) {
if (!CollectionUtils.isEmpty(parameters)) {
Map<String, String> map = new LinkedCaseInsensitiveMap<>(parameters.size(), Locale.ROOT);
parameters.forEach((parameter, value) -> {
checkParameters(parameter, value);
map.put(parameter, value);
});
return Collections.unmodifiableMap(map);
}
else {
return Collections.emptyMap();
}
}
protected void checkParameters(String parameter, String value) {
Assert.hasLength(parameter, "'parameter' must not be empty");
Assert.hasLength(value, "'value' must not be empty");
checkToken(parameter);
if (PARAM_CHARSET.equals(parameter)) {
if (this.resolvedCharset == null) {
this.resolvedCharset = Charset.forName(unquote(value));
}
}
else if (!isQuotedString(value)) {
if (!isQuotedString(value)) {
checkToken(value);
}
}
@@ -1335,6 +1335,22 @@ class ResolvableTypeTests {
assertThat(deserializedNone).isSameAs(ResolvableType.NONE);
}
@Test
void serializeWithCachedState() throws Exception {
ResolvableType type = ResolvableType.forClass(List.class);
testSerialization(type);
type.getSuperType();
type.getInterfaces();
type.getGenerics();
type.hasUnresolvableGenerics();
testSerialization(type);
type.getSuperType();
type.getInterfaces();
type.getGenerics();
type.hasUnresolvableGenerics();
testSerialization(type);
}
@Test
void canResolveVoid() {
ResolvableType type = ResolvableType.forClass(void.class);
@@ -41,11 +41,13 @@ import static org.assertj.core.api.Assertions.assertThat;
/**
* Test cases for {@link ResourceRegionEncoder} class.
*
* @author Brian Clozel
*/
class ResourceRegionEncoderTests extends AbstractLeakCheckingTests {
private ResourceRegionEncoder encoder = new ResourceRegionEncoder();
private final ResourceRegionEncoder encoder = new ResourceRegionEncoder();
@Test
void canEncode() {
@@ -116,7 +118,7 @@ class ResourceRegionEncoderTests extends AbstractLeakCheckingTests {
.verify();
}
@Test // gh-22107
@Test // gh-22107
void cancelWithoutDemandForMultipleResourceRegions() {
Resource resource = new ClassPathResource("ResourceRegionEncoderTests.txt", getClass());
Flux<ResourceRegion> regions = Flux.just(
@@ -138,7 +140,7 @@ class ResourceRegionEncoderTests extends AbstractLeakCheckingTests {
subscriber.cancel();
}
@Test // gh-22107
@Test // gh-22107
void cancelWithoutDemandForSingleResourceRegion() {
Resource resource = new ClassPathResource("ResourceRegionEncoderTests.txt", getClass());
Mono<ResourceRegion> regions = Mono.just(new ResourceRegion(resource, 0, 6));
@@ -16,8 +16,11 @@
package org.springframework.core.task;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import org.junit.jupiter.api.Test;
@@ -27,6 +30,7 @@ import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
import static org.assertj.core.api.Assertions.assertThatNoException;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.willCallRealMethod;
@@ -86,11 +90,11 @@ class SimpleAsyncTaskExecutorTests {
* <p>This test reproduces a critical bug where OutOfMemoryError from
* Thread.start() causes the executor to permanently deadlock:
* <ol>
* <li>beforeAccess() increments concurrencyCount
* <li>doExecute() throws Error before thread starts
* <li>TaskTrackingRunnable.run() never executes
* <li>afterAccess() in finally block never called
* <li>Subsequent tasks block forever in onLimitReached()
* <li>beforeAccess() increments concurrencyCount
* <li>doExecute() throws Error before thread starts
* <li>TaskTrackingRunnable.run() never executes
* <li>afterAccess() in finally block never called
* <li>Subsequent tasks block forever in onLimitReached()
* </ol>
*
* <p>Test approach: The first execute() should fail with some exception
@@ -131,6 +135,105 @@ class SimpleAsyncTaskExecutorTests {
.isTrue();
}
@Test
void taskTerminationTimeout() throws InterruptedException{
Future<?> future;
try (SimpleAsyncTaskExecutor executor = new SimpleAsyncTaskExecutor()) {
executor.setTaskTerminationTimeout(500);
future = executor.submit(() -> {
try {
Thread.sleep(200);
}
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
throw new IllegalStateException();
}
});
Thread.sleep(100);
}
assertThatNoException().isThrownBy(future::get);
}
@Test
void taskTerminationTimeoutWithImmediateCancel() {
AtomicBoolean finished = new AtomicBoolean();
Future<?> future;
try (SimpleAsyncTaskExecutor executor = new SimpleAsyncTaskExecutor()) {
executor.setTaskTerminationTimeout(100);
future = executor.submit(() -> {
if (finished.get()) {
throw new IllegalStateException();
}
});
}
finished.set(true);
assertThatExceptionOfType(CancellationException.class).isThrownBy(future::get);
}
@Test
void taskTerminationTimeoutWithLateInterrupt() throws InterruptedException {
AtomicBoolean interrupted = new AtomicBoolean();
Future<?> future;
try (SimpleAsyncTaskExecutor executor = new SimpleAsyncTaskExecutor()) {
executor.setTaskTerminationTimeout(200);
future = executor.submit(() -> {
try {
Thread.sleep(500);
}
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
interrupted.set(true);
}
});
Thread.sleep(100);
}
assertThatNoException().isThrownBy(future::get);
assertThat(interrupted).isTrue();
}
@Test
void taskTerminationTimeoutWithEarlyInterrupt() throws InterruptedException {
AtomicBoolean interrupted = new AtomicBoolean();
Future<?> future;
try (SimpleAsyncTaskExecutor executor = new SimpleAsyncTaskExecutor()) {
executor.setTaskTerminationTimeout(500);
executor.setCancelRemainingTasksOnClose(true);
future = executor.submit(() -> {
try {
Thread.sleep(200);
}
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
interrupted.set(true);
}
});
Thread.sleep(100);
}
assertThatNoException().isThrownBy(future::get);
assertThat(interrupted).isTrue();
}
@Test
void cancelRemainingTasksOnClose() throws InterruptedException {
AtomicBoolean interrupted = new AtomicBoolean();
Future<?> future;
try (SimpleAsyncTaskExecutor executor = new SimpleAsyncTaskExecutor()) {
executor.setCancelRemainingTasksOnClose(true);
future = executor.submit(() -> {
try {
Thread.sleep(200);
}
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
interrupted.set(true);
}
});
Thread.sleep(100);
}
assertThatNoException().isThrownBy(future::get);
assertThat(interrupted).isTrue();
}
@Test
void threadNameGetsSetCorrectly() {
String customPrefix = "chankPop#";
@@ -69,6 +69,9 @@ public class DefaultJpaDialect implements JpaDialect, Serializable {
return null;
}
/**
* This implementation simply returns {@code null} for no transaction data.
*/
@Override
@Nullable
public Object prepareTransaction(EntityManager entityManager, boolean readOnly, @Nullable String name)
@@ -81,6 +84,7 @@ public class DefaultJpaDialect implements JpaDialect, Serializable {
* This implementation does nothing, since the default {@code beginTransaction}
* implementation does not require any cleanup.
* @see #beginTransaction
* @see #prepareTransaction
*/
@Override
public void cleanupTransaction(@Nullable Object transactionData) {
@@ -100,7 +100,7 @@ public interface JpaDialect extends PersistenceExceptionTranslator {
* @param readOnly whether the transaction is supposed to be read-only
* @param name the name of the transaction (if any)
* @return an arbitrary object that holds transaction data, if any
* (to be passed into cleanupTransaction)
* (to be passed into {@link #cleanupTransaction})
* @throws jakarta.persistence.PersistenceException if thrown by JPA methods
* @see #cleanupTransaction
*/
@@ -117,6 +117,7 @@ public interface JpaDialect extends PersistenceExceptionTranslator {
* @param transactionData arbitrary object that holds transaction data, if any
* (as returned by beginTransaction or prepareTransaction)
* @see #beginTransaction
* @see #prepareTransaction
* @see org.springframework.jdbc.datasource.DataSourceUtils#resetConnectionAfterTransaction
*/
void cleanupTransaction(@Nullable Object transactionData);
@@ -107,21 +107,12 @@ final class PersistenceUnitReader {
}
/**
* Parse and build all persistence unit infos defined in the specified XML file(s).
* @param persistenceXmlLocation the resource location (can be a pattern)
* @return the resulting PersistenceUnitInfo instances
*/
public SpringPersistenceUnitInfo[] readPersistenceUnitInfos(String persistenceXmlLocation) {
return readPersistenceUnitInfos(new String[] {persistenceXmlLocation});
}
/**
* Parse and build all persistence unit infos defined in the given XML files.
* @param persistenceXmlLocations the resource locations (can be patterns)
* @return the resulting PersistenceUnitInfo instances
*/
public SpringPersistenceUnitInfo[] readPersistenceUnitInfos(String[] persistenceXmlLocations) {
public SpringPersistenceUnitInfo[] readPersistenceUnitInfos(String... persistenceXmlLocations) {
ErrorHandler handler = new SimpleSaxErrorHandler(logger);
List<SpringPersistenceUnitInfo> infos = new ArrayList<>(1);
String resourceLocation = null;
@@ -181,16 +181,18 @@ public class EclipseLinkJpaDialect extends DefaultJpaDialect {
@Override
public Connection getConnection() {
if (this.connection == null) {
Connection con = this.connection;
if (con == null) {
transactionIsolationLock.lock();
try {
this.connection = this.entityManager.unwrap(Connection.class);
con = this.entityManager.unwrap(Connection.class);
}
finally {
transactionIsolationLock.unlock();
}
this.connection = con;
}
return this.connection;
return con;
}
}
@@ -29,6 +29,7 @@ import org.apache.commons.logging.LogFactory;
import org.springframework.beans.BeanInstantiationException;
import org.springframework.beans.BeanUtils;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
import org.springframework.lang.Nullable;
import org.springframework.test.context.BootstrapContext;
@@ -304,8 +305,6 @@ public abstract class AbstractTestContextBootstrapper implements TestContextBoot
ContextLoader contextLoader = resolveContextLoader(testClass, configAttributesList);
List<String> locations = new ArrayList<>();
List<Class<?>> classes = new ArrayList<>();
List<Class<?>> initializers = new ArrayList<>();
for (ContextConfigurationAttributes configAttributes : configAttributesList) {
if (logger.isTraceEnabled()) {
logger.trace(String.format("Processing locations and classes for context configuration attributes %s",
@@ -323,12 +322,13 @@ public abstract class AbstractTestContextBootstrapper implements TestContextBoot
locations.addAll(0, Arrays.asList(processedLocations));
// Legacy ContextLoaders don't know how to process classes
}
initializers.addAll(0, Arrays.asList(configAttributes.getInitializers()));
if (!configAttributes.isInheritLocations()) {
break;
}
}
Set<Class<? extends ApplicationContextInitializer<?>>> initializers =
ApplicationContextInitializerUtils.resolveInitializerClasses(configAttributesList);
Set<ContextCustomizer> contextCustomizers = getContextCustomizers(testClass,
Collections.unmodifiableList(configAttributesList));
@@ -342,8 +342,7 @@ public abstract class AbstractTestContextBootstrapper implements TestContextBoot
TestPropertySourceUtils.buildMergedTestPropertySources(testClass);
MergedContextConfiguration mergedConfig = new MergedContextConfiguration(testClass,
StringUtils.toStringArray(locations), ClassUtils.toClassArray(classes),
ApplicationContextInitializerUtils.resolveInitializerClasses(configAttributesList),
ActiveProfilesUtils.resolveActiveProfiles(testClass),
initializers, ActiveProfilesUtils.resolveActiveProfiles(testClass),
mergedTestPropertySources.getPropertySourceDescriptors(),
mergedTestPropertySources.getProperties(),
contextCustomizers, contextLoader, cacheAwareContextLoaderDelegate, parentConfig);
@@ -150,7 +150,7 @@ public abstract class TransactionAspectSupport implements BeanFactoryAware, Init
* Mainly intended for code that wants to set the current transaction
* rollback-only but not throw an application exception.
* <p>This exposes the locally declared transaction boundary with its declared name
* and characteristics, as managed by the aspect. Ar runtime, the local boundary may
* and characteristics, as managed by the aspect. At runtime, the local boundary may
* participate in an outer transaction: If you need transaction metadata from such
* an outer transaction (the actual resource transaction) instead, consider using
* {@link org.springframework.transaction.support.TransactionSynchronizationManager}.
@@ -2056,9 +2056,8 @@ public class HttpHeaders implements MultiValueMap<String, String>, Serializable
return DATE_FORMATTER.format(time);
}
private static Set<String> toCaseInsensitiveSet(Set<String> originalSet) {
final Set<String> deduplicatedSet = Collections.newSetFromMap(
Set<String> deduplicatedSet = Collections.newSetFromMap(
new LinkedCaseInsensitiveMap<>(originalSet.size(), Locale.ROOT));
// add/addAll (put/putAll in LinkedCaseInsensitiveMap) retain the casing of the last occurrence.
// Here we prefer the first.
@@ -2075,6 +2074,7 @@ public class HttpHeaders implements MultiValueMap<String, String>, Serializable
private static final class CaseInsensitiveEntrySet extends AbstractSet<Entry<String, List<String>>> {
private final MultiValueMap<String, String> headers;
private final Set<String> deduplicatedNames;
public CaseInsensitiveEntrySet(MultiValueMap<String, String> headers) {
@@ -2092,6 +2092,7 @@ public class HttpHeaders implements MultiValueMap<String, String>, Serializable
return this.deduplicatedNames.size();
}
private final class CaseInsensitiveIterator implements Iterator<Map.Entry<String, List<String>>> {
private final Iterator<String> namesIterator;
@@ -2127,6 +2128,7 @@ public class HttpHeaders implements MultiValueMap<String, String>, Serializable
}
}
private final class CaseInsensitiveEntry implements Map.Entry<String, List<String>> {
private final String key;
@@ -2147,10 +2149,9 @@ public class HttpHeaders implements MultiValueMap<String, String>, Serializable
@Override
public List<String> setValue(List<String> value) {
List<String> previousValues = Objects.requireNonNull(
CaseInsensitiveEntrySet.this.headers.get(this.key));
List<String> previous = Objects.requireNonNull(CaseInsensitiveEntrySet.this.headers.get(this.key));
CaseInsensitiveEntrySet.this.headers.put(this.key, value);
return previousValues;
return previous;
}
}
}
@@ -549,7 +549,7 @@ public class MediaType extends MimeType implements Serializable {
* @throws IllegalArgumentException if any of the parameters contain illegal characters
*/
public MediaType(MediaType other, @Nullable Map<String, String> parameters) {
super(other.getType(), other.getSubtype(), parameters);
super(other, parameters);
}
/**
@@ -19,6 +19,7 @@ package org.springframework.http.codec;
import java.time.Duration;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
@@ -253,16 +254,23 @@ public final class ServerSentEvent<T> {
@Override
public Builder<T> id(String id) {
checkEvent(id);
this.id = id;
return this;
}
@Override
public Builder<T> event(String event) {
checkEvent(event);
this.event = event;
return this;
}
private static void checkEvent(String content) {
Assert.isTrue(content.indexOf('\n') == -1 && content.indexOf('\r') == -1,
"illegal character '\\n' or '\\r' in event content");
}
@Override
public Builder<T> retry(Duration retry) {
this.retry = retry;
@@ -40,7 +40,6 @@ import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* {@code HttpMessageWriter} for {@code "text/event-stream"} responses.
@@ -48,6 +47,7 @@ import org.springframework.util.StringUtils;
* @author Sebastien Deleuze
* @author Arjen Poutsma
* @author Rossen Stoyanchev
* @author Brian Clozel
* @since 5.0
*/
public class ServerSentEventHttpMessageWriter implements HttpMessageWriter<Object> {
@@ -131,8 +131,9 @@ public class ServerSentEventHttpMessageWriter implements HttpMessageWriter<Objec
result = Flux.just(encodeText(sseText + "\n", mediaType, factory));
}
else if (data instanceof String text) {
text = StringUtils.replace(text, "\n", "\ndata:");
result = Flux.just(encodeText(sseText + text + "\n\n", mediaType, factory));
StringBuilder sb = new StringBuilder(sseText);
writeStringData(text, sb);
result = Flux.just(encodeText(sb.toString(), mediaType, factory));
}
else {
result = encodeEvent(sseText, data, dataType, mediaType, factory, hints);
@@ -142,6 +143,31 @@ public class ServerSentEventHttpMessageWriter implements HttpMessageWriter<Objec
});
}
private void writeStringData(String input, StringBuilder sb) {
if (input.indexOf('\n') == -1 && input.indexOf('\r') == -1) {
sb.append(input);
}
else {
int length = input.length();
for (int i = 0; i < length; i++) {
char c = input.charAt(i);
if (c == '\r') {
if (i + 1 < length && input.charAt(i + 1) == '\n') {
i++;
}
sb.append("\ndata:");
}
else if (c == '\n') {
sb.append("\ndata:");
}
else {
sb.append(c);
}
}
}
sb.append("\n\n");
}
@SuppressWarnings("unchecked")
private <T> Flux<DataBuffer> encodeEvent(CharSequence sseText, T data, ResolvableType dataType,
MediaType mediaType, DataBufferFactory factory, Map<String, Object> hints) {
@@ -99,7 +99,8 @@ final class PartGenerator extends BaseSubscriber<MultipartParser.Token> {
sink.onCancel(generator);
sink.onRequest(l -> generator.requestToken());
tokens.subscribe(generator);
tokens.doOnDiscard(MultipartParser.BodyToken.class, bodyToken -> DataBufferUtils.release(bodyToken.buffer()))
.subscribe(generator);
});
}
@@ -254,13 +254,14 @@ public abstract class AbstractHttpMessageConverter<T> implements HttpMessageConv
contentTypeToUse = (mediaType != null ? mediaType : contentTypeToUse);
}
if (contentTypeToUse != null) {
String value = contentTypeToUse.toString();
if (contentTypeToUse.getCharset() == null) {
Charset defaultCharset = getDefaultCharset();
if (defaultCharset != null) {
contentTypeToUse = new MediaType(contentTypeToUse, defaultCharset);
value += ";charset=" + defaultCharset.name();
}
}
headers.setContentType(contentTypeToUse);
headers.set(HttpHeaders.CONTENT_TYPE, value);
}
}
if (headers.getContentLength() < 0 && !headers.containsKey(HttpHeaders.TRANSFER_ENCODING)) {
@@ -187,10 +187,10 @@ public class ServletServerHttpRequest implements ServerHttpRequest {
if (contentType != null && contentType.getCharset() == null) {
String requestEncoding = this.servletRequest.getCharacterEncoding();
if (StringUtils.hasLength(requestEncoding)) {
Charset charSet = Charset.forName(requestEncoding);
Charset charset = Charset.forName(requestEncoding);
Map<String, String> params = new LinkedCaseInsensitiveMap<>();
params.putAll(contentType.getParameters());
params.put("charset", charSet.toString());
params.put("charset", charset.toString());
MediaType mediaType = new MediaType(contentType.getType(), contentType.getSubtype(), params);
this.headers.setContentType(mediaType);
}
@@ -207,7 +207,6 @@ public class ServletServerHttpRequest implements ServerHttpRequest {
}
}
}
return this.headers;
}
@@ -18,6 +18,7 @@ package org.springframework.http.server;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
@@ -118,19 +119,31 @@ public class ServletServerHttpResponse implements ServerHttpResponse {
this.servletResponse.addHeader(headerName, headerValue);
}
});
// HttpServletResponse exposes some headers as properties: we should include those if not already present
MediaType contentTypeHeader = this.headers.getContentType();
if (this.servletResponse.getContentType() == null && contentTypeHeader != null) {
this.servletResponse.setContentType(contentTypeHeader.toString());
if (this.servletResponse.getContentType() == null && this.headers.containsKey(HttpHeaders.CONTENT_TYPE)) {
this.servletResponse.setContentType(this.headers.getFirst(HttpHeaders.CONTENT_TYPE));
}
if (this.servletResponse.getCharacterEncoding() == null && contentTypeHeader != null &&
contentTypeHeader.getCharset() != null) {
this.servletResponse.setCharacterEncoding(contentTypeHeader.getCharset().name());
if (this.servletResponse.getCharacterEncoding() == null && this.headers.containsKey(HttpHeaders.CONTENT_TYPE)) {
try {
// Lazy parsing into MediaType
MediaType contentType = this.headers.getContentType();
if (contentType != null) {
Charset charset = contentType.getCharset();
if (charset != null) {
this.servletResponse.setCharacterEncoding(charset.name());
}
}
}
catch (Exception ex) {
// Leave character encoding unspecified
}
}
long contentLength = getHeaders().getContentLength();
long contentLength = this.headers.getContentLength();
if (contentLength != -1) {
this.servletResponse.setContentLengthLong(contentLength);
}
this.headersWritten = true;
}
}
@@ -50,7 +50,9 @@ import org.springframework.util.ReflectionUtils;
*/
class ServletServerHttpResponse extends AbstractListenerServerHttpResponse {
private static final boolean IS_SERVLET61 = ReflectionUtils.findField(HttpServletResponse.class, "SC_PERMANENT_REDIRECT") != null;
private static final boolean SERVLET61 =
(ReflectionUtils.findField(HttpServletResponse.class, "SC_PERMANENT_REDIRECT") != null);
private final HttpServletResponse response;
@@ -138,31 +140,35 @@ class ServletServerHttpResponse extends AbstractListenerServerHttpResponse {
}
protected void adaptHeaders(boolean removeAdaptedHeaders) {
MediaType contentType = null;
try {
contentType = getHeaders().getContentType();
}
catch (Exception ex) {
String rawContentType = getHeaders().getFirst(HttpHeaders.CONTENT_TYPE);
this.response.setContentType(rawContentType);
}
if (this.response.getContentType() == null && contentType != null) {
this.response.setContentType(contentType.toString());
}
HttpHeaders headers = getHeaders();
Charset charset = (contentType != null ? contentType.getCharset() : null);
if (this.response.getCharacterEncoding() == null && charset != null) {
this.response.setCharacterEncoding(charset.name());
// HttpServletResponse exposes some headers as properties: we should include those if not already present
if (this.response.getContentType() == null && headers.containsKey(HttpHeaders.CONTENT_TYPE)) {
this.response.setContentType(headers.getFirst(HttpHeaders.CONTENT_TYPE));
}
long contentLength = getHeaders().getContentLength();
if (this.response.getCharacterEncoding() == null && headers.containsKey(HttpHeaders.CONTENT_TYPE)) {
try {
// Lazy parsing into MediaType
MediaType contentType = headers.getContentType();
if (contentType != null) {
Charset charset = contentType.getCharset();
if (charset != null) {
this.response.setCharacterEncoding(charset.name());
}
}
}
catch (Exception ex) {
// Leave character encoding unspecified
}
}
long contentLength = headers.getContentLength();
if (contentLength != -1) {
this.response.setContentLengthLong(contentLength);
}
if (removeAdaptedHeaders) {
getHeaders().remove(HttpHeaders.CONTENT_TYPE);
getHeaders().remove(HttpHeaders.CONTENT_LENGTH);
headers.remove(HttpHeaders.CONTENT_TYPE);
headers.remove(HttpHeaders.CONTENT_LENGTH);
}
}
@@ -186,7 +192,7 @@ class ServletServerHttpResponse extends AbstractListenerServerHttpResponse {
cookie.setSecure(httpCookie.isSecure());
cookie.setHttpOnly(httpCookie.isHttpOnly());
if (httpCookie.isPartitioned()) {
if (IS_SERVLET61) {
if (SERVLET61) {
cookie.setAttribute("Partitioned", "");
}
else {
@@ -373,7 +379,6 @@ class ServletServerHttpResponse extends AbstractListenerServerHttpResponse {
private class ResponseBodyProcessor extends AbstractListenerWriteProcessor<DataBuffer> {
public ResponseBodyProcessor() {
super(request.getLogPrefix());
}
@@ -353,17 +353,14 @@ public interface RestClient {
ResponseSpec.ErrorHandler errorHandler);
/**
* Register a default
* {@linkplain ResponseSpec#onStatus(ResponseErrorHandler) status handler}
* to apply to every response. Such default handlers are applied in the
* order in which they are registered, and after any others that are
* registered for a specific response.
* <p>The first status handler who claims that a response has an
* error is invoked. If you want to disable other defaults, consider
* using {@link #defaultStatusHandler(Predicate, ResponseSpec.ErrorHandler)}
* with a predicate that matches all status codes.
* @param errorHandler handler that typically, though not necessarily,
* throws an exception
* Variant of {@link #defaultStatusHandler(Predicate, ResponseSpec.ErrorHandler)}
* that allows use of {@code RestTemplate}'s {@code ResponseErrorHandler}
* mechanism. This is provided mainly to assist {@code RestTemplate}
* users to transition to {@link RestClient}. Internally, the given
* handler is adapted to {@link RestClient.ResponseSpec}, which is the
* preferred mechanism to use.
* @param errorHandler the error handler to configure, internally adapted
* and integrated into the {@link ResponseSpec.ErrorHandler} chain.
* @return this builder
*/
Builder defaultStatusHandler(ResponseErrorHandler errorHandler);
@@ -41,8 +41,8 @@ import org.springframework.util.Assert;
* @since 15.03.2004
* @see #setEncoding
* @see #setForceEncoding
* @see jakarta.servlet.http.HttpServletRequest#setCharacterEncoding
* @see jakarta.servlet.http.HttpServletResponse#setCharacterEncoding
* @see jakarta.servlet.http.HttpServletRequest#setCharacterEncoding(String)
* @see jakarta.servlet.http.HttpServletResponse#setCharacterEncoding(String)
*/
public class CharacterEncodingFilter extends OncePerRequestFilter {
@@ -126,7 +126,7 @@ public final class UrlHandlerFilter extends OncePerRequestFilter {
public interface Builder {
/**
* Add a handler for URL's with a trailing slash.
* Add a handler for URLs with a trailing slash.
* @param pathPatterns path patterns to map the handler to, for example,
* <code>"/path/&#42;"</code>, <code>"/path/&#42;&#42;"</code>,
* <code>"/path/foo/"</code>.
@@ -117,7 +117,7 @@ public final class UrlHandlerFilter implements WebFilter {
public interface Builder {
/**
* Add a handler for URL's with a trailing slash.
* Add a handler for URLs with a trailing slash.
* @param pathPatterns path patterns to map the handler to, e.g.
* <code>"/path/&#42;"</code>, <code>"/path/&#42;&#42;"</code>,
* <code>"/path/foo/"</code>.
@@ -117,10 +117,11 @@ public class StandardMultipartHttpServletRequest extends AbstractMultipartHttpSe
// MaxUploadSizeExceededException ?
Throwable cause = ex;
do {
String msg = cause.getMessage();
String msg = cause.toString();
if (msg != null) {
msg = msg.toLowerCase(Locale.ROOT);
if ((msg.contains("exceed") && (msg.contains("size") || msg.contains("length"))) ||
if (((msg.contains("exceed") || msg.contains("limit")) &&
(msg.contains("size") || msg.contains("length") || msg.contains("count"))) ||
(msg.contains("request") && (msg.contains("big") || msg.contains("large")))) {
throw new MaxUploadSizeExceededException(-1, ex);
}
@@ -26,7 +26,7 @@ import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* Parser for URI's based on RFC 3986 syntax.
* Parser for URIs based on RFC 3986 syntax.
*
* @author Rossen Stoyanchev
* @since 6.2
@@ -57,7 +57,7 @@ import org.springframework.web.util.UriComponents.UriTemplateVariables;
* {@link ParserType#WHAT_WG WhatWG parser type}, based on the algorithm from
* the WhatWG <a href="https://url.spec.whatwg.org">URL Living Standard</a>
* provides more lenient handling of a wide range of cases that occur in user
* types URL's.
* typed URLs.
*
* @author Arjen Poutsma
* @author Rossen Stoyanchev
@@ -787,7 +787,7 @@ public class UriComponentsBuilder implements UriBuilder, Cloneable {
public enum ParserType {
/**
* This parser type expects URI's to conform to RFC 3986 syntax.
* This parser type expects URIs to conform to RFC 3986 syntax.
*/
RFC,
@@ -795,7 +795,7 @@ public class UriComponentsBuilder implements UriBuilder, Cloneable {
* This parser follows the
* <a href="https://url.spec.whatwg.org/#url-parsing">URL parsing algorithm</a>
* in the WhatWG URL Living standard that browsers implement to align on
* lenient handling of user typed URL's that may not conform to RFC syntax.
* lenient handling of user typed URLs that may not conform to RFC syntax.
* @see <a href="https://url.spec.whatwg.org">URL Living Standard</a>
* @see <a href="https://github.com/web-platform-tests/wpt/tree/master/url">URL tests</a>
*/
@@ -39,9 +39,9 @@ import org.springframework.util.Assert;
* Implementation of the
* <a href="https://url.spec.whatwg.org/#url-parsing">URL parsing</a> algorithm
* of the WhatWG URL Living standard. Browsers use this algorithm to align on
* lenient parsing of user typed URL's that may deviate from RFC syntax.
* lenient parsing of user typed URLs that may deviate from RFC syntax.
* Use this, via {@link UriComponentsBuilder.ParserType#WHAT_WG}, if you need to
* leniently handle URL's that don't confirm to RFC syntax, or for alignment
* leniently handle URLs that don't confirm to RFC syntax, or for alignment
* with browser behavior.
*
* <p>Comments in this class correlate to the parsing algorithm.
@@ -110,12 +110,13 @@ class ServerSentEventHttpMessageWriterTests extends AbstractDataBufferAllocating
super.bufferFactory = bufferFactory;
MockServerHttpResponse outputMessage = new MockServerHttpResponse(super.bufferFactory);
Flux<String> source = Flux.just("foo\nbar", "foo\nbaz");
Flux<String> source = Flux.just("first\nsecond", "first\rsecond", "first\r\nsecond");
testWrite(source, outputMessage, String.class);
StepVerifier.create(outputMessage.getBody())
.consumeNextWith(stringConsumer("data:foo\ndata:bar\n\n"))
.consumeNextWith(stringConsumer("data:foo\ndata:baz\n\n"))
.consumeNextWith(stringConsumer("data:first\ndata:second\n\n"))
.consumeNextWith(stringConsumer("data:first\ndata:second\n\n"))
.consumeNextWith(stringConsumer("data:first\ndata:second\n\n"))
.expectComplete()
.verify();
}
@@ -0,0 +1,55 @@
/*
* 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.codec;
import java.util.stream.Stream;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
/**
* Tests for {@link ServerSentEvent}.
* @author Brian Clozel
*/
class ServerSentEventTests {
@ParameterizedTest(name = "{1}")
@MethodSource("newLineCharacters")
void rejectsInvalidId(String newLine, String description) {
assertThatIllegalArgumentException().isThrownBy(() ->
ServerSentEvent.<String>builder().id("first" + newLine + "second").build());
}
@ParameterizedTest(name = "{1}")
@MethodSource("newLineCharacters")
void rejectsInvalidEvent(String newLine, String description) {
assertThatIllegalArgumentException().isThrownBy(() ->
ServerSentEvent.<String>builder().event("first" + newLine + "second").build());
}
private static Stream<Arguments> newLineCharacters() {
return Stream.of(
Arguments.of("\n", "LF"),
Arguments.of("\r", "CR"),
Arguments.of("\r\n", "CRLF")
);
}
}
@@ -131,6 +131,38 @@ class StandardMultipartHttpServletRequestTests {
.isThrownBy(() -> requestWithException(ex)).withCause(ex);
}
@Test // gh-36317: Tomcat's Commons FileUpload
void commonsFileSizeLimitExceededException() {
IOException ex = new FileSizeLimitExceededException();
assertThatExceptionOfType(MaxUploadSizeExceededException.class)
.isThrownBy(() -> requestWithException(ex)).withCause(ex);
}
@Test // gh-36317: Tomcat's Commons FileUpload
void commonsFileCountLimitExceededException() {
IOException ex = new FileCountLimitExceededException();
assertThatExceptionOfType(MaxUploadSizeExceededException.class)
.isThrownBy(() -> requestWithException(ex)).withCause(ex);
}
@Test // gh-36317: Commons FileUpload 2.x
void commonsFileUploadByteCountLimitException() {
IOException ex = new FileUploadByteCountLimitException();
assertThatExceptionOfType(MaxUploadSizeExceededException.class)
.isThrownBy(() -> requestWithException(ex)).withCause(ex);
}
@Test // gh-36317: Commons FileUpload 2.x
void commonsFileUploadFileCountLimitException() {
IOException ex = new FileUploadFileCountLimitException();
assertThatExceptionOfType(MaxUploadSizeExceededException.class)
.isThrownBy(() -> requestWithException(ex)).withCause(ex);
}
private static StandardMultipartHttpServletRequest requestWithPart(String name, String disposition, String content) {
MockHttpServletRequest request = new MockHttpServletRequest();
@@ -160,4 +192,21 @@ class StandardMultipartHttpServletRequestTests {
return new StandardMultipartHttpServletRequest(request);
}
@SuppressWarnings("serial")
private static class FileSizeLimitExceededException extends IOException {
}
@SuppressWarnings("serial")
private static class FileCountLimitExceededException extends IOException {
}
@SuppressWarnings("serial")
private static class FileUploadByteCountLimitException extends IOException {
}
@SuppressWarnings("serial")
private static class FileUploadFileCountLimitException extends IOException {
}
}
@@ -149,7 +149,7 @@ public class InvocableHandlerMethod extends HandlerMethod {
/**
* Configure a reactive adapter registry. This is needed for cases where the response is
* fully handled within the controller in combination with an async void return value.
* <p>By default this is a {@link ReactiveAdapterRegistry} with default settings.
* <p>By default, this is a {@link ReactiveAdapterRegistry} with default settings.
*/
public void setReactiveAdapterRegistry(ReactiveAdapterRegistry registry) {
this.reactiveAdapterRegistry = registry;
@@ -163,7 +163,7 @@ public class InvocableHandlerMethod extends HandlerMethod {
*/
public void setMethodValidator(@Nullable MethodValidator methodValidator) {
this.methodValidator = methodValidator;
this.validationGroups = (methodValidator != null ?
this.validationGroups = (methodValidator != null && (shouldValidateArguments() || shouldValidateReturnValue()) ?
methodValidator.determineValidationGroups(getBean(), getBridgedMethod()) : EMPTY_GROUPS);
}
@@ -46,7 +46,7 @@ import org.springframework.web.util.UriComponentsBuilder;
* <li>{@link Locale}
* <li>{@link TimeZone}
* <li>{@link ZoneId}
* <li>{@link UriBuilder} or {@link UriComponentsBuilder} -- for building URL's
* <li>{@link UriBuilder} or {@link UriComponentsBuilder} -- for building URLs
* relative to the current request
* </ul>
*
@@ -47,6 +47,7 @@ import org.springframework.util.Assert;
import org.springframework.util.FileCopyUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.reactive.resource.ResourceHandlerUtils;
import org.springframework.web.reactive.result.view.AbstractUrlBasedView;
import org.springframework.web.server.ServerWebExchange;
@@ -171,14 +172,13 @@ public class ScriptTemplateView extends AbstractUrlBasedView {
*/
public void setResourceLoaderPath(String resourceLoaderPath) {
String[] paths = StringUtils.commaDelimitedListToStringArray(resourceLoaderPath);
this.resourceLoaderPaths = new String[paths.length + 1];
this.resourceLoaderPaths[0] = "";
this.resourceLoaderPaths = new String[paths.length];
for (int i = 0; i < paths.length; i++) {
String path = paths[i];
if (!path.endsWith("/") && !path.endsWith(":")) {
path = path + "/";
}
this.resourceLoaderPaths[i + 1] = path;
this.resourceLoaderPaths[i] = path;
}
}
@@ -307,11 +307,26 @@ public class ScriptTemplateView extends AbstractUrlBasedView {
@Nullable
protected Resource getResource(String location) {
if (this.resourceLoaderPaths != null) {
String normalizedLocation = ResourceHandlerUtils.normalizeInputPath(location);
if (this.resourceLoaderPaths != null && !ResourceHandlerUtils.shouldIgnoreInputPath(normalizedLocation)) {
ApplicationContext context = obtainApplicationContext();
for (String path : this.resourceLoaderPaths) {
Resource resource = obtainApplicationContext().getResource(path + location);
if (resource.exists()) {
return resource;
Resource resource = context.getResource(path + normalizedLocation);
try {
if (resource.exists() && ResourceHandlerUtils.isResourceUnderLocation(context.getResource(path), resource)) {
return resource;
}
}
catch (IOException ex) {
if (logger.isDebugEnabled()) {
String error = "Skip location [" + normalizedLocation + "] due to error";
if (logger.isTraceEnabled()) {
logger.trace(error, ex);
}
else {
logger.debug(error + ": " + ex.getMessage());
}
}
}
}
}
@@ -59,7 +59,6 @@ class WebFluxViewResolutionIntegrationTests {
private static final MediaType TEXT_HTML_ISO_8859_1 = MediaType.parseMediaType("text/html;charset=ISO-8859-1");
@Nested
class FreeMarkerTests {
@@ -115,6 +114,7 @@ class WebFluxViewResolutionIntegrationTests {
}
}
@Configuration(proxyBeanMethods = false)
static class FreeMarkerWebFluxConfig extends AbstractWebFluxConfig {
@@ -131,6 +131,7 @@ class WebFluxViewResolutionIntegrationTests {
}
}
@Configuration(proxyBeanMethods = false)
static class ExplicitDefaultEncodingConfig extends AbstractWebFluxConfig {
@@ -148,6 +149,7 @@ class WebFluxViewResolutionIntegrationTests {
}
}
@Configuration(proxyBeanMethods = false)
static class ExplicitDefaultEncodingAndContentTypeConfig extends AbstractWebFluxConfig {
@@ -196,6 +198,7 @@ class WebFluxViewResolutionIntegrationTests {
}
}
@Controller
static class SampleController {
@@ -0,0 +1,265 @@
/*
* 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.web.reactive.result.view.script;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import javax.script.Invocable;
import javax.script.ScriptEngine;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.DirectFieldAccessor;
import org.springframework.context.ApplicationContextException;
import org.springframework.context.support.StaticApplicationContext;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.assertj.core.api.InstanceOfAssertFactories.BOOLEAN;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/**
* Unit tests for {@link ScriptTemplateView}.
*
* @author Sebastien Deleuze
*/
public class ScriptTemplateViewTests {
private ScriptTemplateView view;
private ScriptTemplateConfigurer configurer;
private StaticApplicationContext context;
@BeforeEach
public void setup() {
this.configurer = new ScriptTemplateConfigurer();
this.context = new StaticApplicationContext();
this.context.getBeanFactory().registerSingleton("scriptTemplateConfigurer", this.configurer);
this.view = new ScriptTemplateView();
}
@Test
public void missingTemplate() throws Exception {
this.context.refresh();
this.view.setResourceLoaderPath("classpath:org/springframework/web/reactive/result/view/script/");
this.view.setUrl("missing.txt");
this.view.setEngine(mock(InvocableScriptEngine.class));
this.configurer.setRenderFunction("render");
this.view.setApplicationContext(this.context);
assertThat(this.view.checkResourceExists(Locale.ENGLISH)).isFalse();
}
@Test
public void missingScriptTemplateConfig() throws Exception {
assertThatExceptionOfType(ApplicationContextException.class).isThrownBy(() ->
this.view.setApplicationContext(new StaticApplicationContext()))
.withMessageContaining("ScriptTemplateConfig");
}
@Test
public void detectScriptTemplateConfigWithEngine() {
InvocableScriptEngine engine = mock(InvocableScriptEngine.class);
this.configurer.setEngine(engine);
this.configurer.setRenderObject("Template");
this.configurer.setRenderFunction("render");
this.configurer.setCharset(StandardCharsets.ISO_8859_1);
this.configurer.setSharedEngine(true);
DirectFieldAccessor accessor = new DirectFieldAccessor(this.view);
this.view.setApplicationContext(this.context);
assertThat(accessor.getPropertyValue("engine")).isEqualTo(engine);
assertThat(accessor.getPropertyValue("renderObject")).isEqualTo("Template");
assertThat(accessor.getPropertyValue("renderFunction")).isEqualTo("render");
assertThat(accessor.getPropertyValue("defaultCharset")).isEqualTo(StandardCharsets.ISO_8859_1);
assertThat(accessor.getPropertyValue("sharedEngine")).asInstanceOf(BOOLEAN).isTrue();
}
@Test
public void detectScriptTemplateConfigWithEngineName() {
this.configurer.setEngineName("jython");
this.configurer.setRenderObject("Template");
this.configurer.setRenderFunction("render");
DirectFieldAccessor accessor = new DirectFieldAccessor(this.view);
this.view.setApplicationContext(this.context);
assertThat(accessor.getPropertyValue("engineName")).isEqualTo("jython");
assertThat(accessor.getPropertyValue("engine")).isNotNull();
assertThat(accessor.getPropertyValue("renderObject")).isEqualTo("Template");
assertThat(accessor.getPropertyValue("renderFunction")).isEqualTo("render");
assertThat(accessor.getPropertyValue("defaultCharset")).isEqualTo(StandardCharsets.UTF_8);
}
@Test
public void customEngineAndRenderFunction() throws Exception {
ScriptEngine engine = mock(InvocableScriptEngine.class);
given(engine.get("key")).willReturn("value");
this.view.setEngine(engine);
this.view.setRenderFunction("render");
this.view.setApplicationContext(this.context);
engine = this.view.getEngine();
assertThat(engine).isNotNull();
assertThat(engine.get("key")).isEqualTo("value");
DirectFieldAccessor accessor = new DirectFieldAccessor(this.view);
assertThat(accessor.getPropertyValue("renderObject")).isNull();
assertThat(accessor.getPropertyValue("renderFunction")).isEqualTo("render");
assertThat(accessor.getPropertyValue("defaultCharset")).isEqualTo(StandardCharsets.UTF_8);
}
@Test
public void nonSharedEngine() throws Exception {
int iterations = 20;
this.view.setEngineName("jython");
this.view.setRenderFunction("render");
this.view.setSharedEngine(false);
this.view.setApplicationContext(this.context);
ExecutorService executor = Executors.newFixedThreadPool(4);
List<Future<Boolean>> results = new ArrayList<>();
for (int i = 0; i < iterations; i++) {
results.add(executor.submit(() -> view.getEngine() != null));
}
assertThat(results.size()).isEqualTo(iterations);
for (int i = 0; i < iterations; i++) {
assertThat((boolean) results.get(i).get()).isTrue();
}
executor.shutdown();
}
@Test
public void nonInvocableScriptEngine() throws Exception {
this.view.setEngine(mock(ScriptEngine.class));
this.view.setApplicationContext(this.context);
}
@Test
public void nonInvocableScriptEngineWithRenderFunction() throws Exception {
this.view.setEngine(mock(ScriptEngine.class));
this.view.setRenderFunction("render");
assertThatIllegalArgumentException().isThrownBy(() ->
this.view.setApplicationContext(this.context));
}
@Test
public void engineAndEngineNameBothDefined() {
this.view.setEngine(mock(InvocableScriptEngine.class));
this.view.setEngineName("test");
this.view.setRenderFunction("render");
assertThatIllegalArgumentException().isThrownBy(() ->
this.view.setApplicationContext(this.context))
.withMessageContaining("You should define either 'engine', 'engineSupplier', or 'engineName'.");
}
@Test // gh-23258
public void engineAndEngineSupplierBothDefined() {
ScriptEngine engine = mock(InvocableScriptEngine.class);
this.view.setEngineSupplier(() -> engine);
this.view.setEngine(engine);
this.view.setRenderFunction("render");
assertThatIllegalArgumentException().isThrownBy(() ->
this.view.setApplicationContext(this.context))
.withMessageContaining("You should define either 'engine', 'engineSupplier', or 'engineName'.");
}
@Test // gh-23258
public void engineNameAndEngineSupplierBothDefined() {
this.view.setEngineSupplier(() -> mock(InvocableScriptEngine.class));
this.view.setEngineName("test");
this.view.setRenderFunction("render");
assertThatIllegalArgumentException().isThrownBy(() ->
this.view.setApplicationContext(this.context))
.withMessageContaining("You should define either 'engine', 'engineSupplier', or 'engineName'.");
}
@Test
public void engineSetterAndNonSharedEngine() {
this.view.setEngine(mock(InvocableScriptEngine.class));
this.view.setRenderFunction("render");
this.view.setSharedEngine(false);
assertThatIllegalArgumentException().isThrownBy(() ->
this.view.setApplicationContext(this.context))
.withMessageContaining("sharedEngine");
}
@Test
public void resourceLoaderPath() {
this.view.setEngine(mock(InvocableScriptEngine.class));
this.view.setApplicationContext(this.context);
DirectFieldAccessor viewAccessor = new DirectFieldAccessor(this.view);
String[] resourceLoaderPaths = (String[]) viewAccessor.getPropertyValue("resourceLoaderPaths");
assertThat(resourceLoaderPaths).containsExactly("classpath:");
this.view.setResourceLoaderPath("classpath:org/springframework/web/reactive/result/view/script/");
resourceLoaderPaths = (String[]) viewAccessor.getPropertyValue("resourceLoaderPaths");
assertThat(resourceLoaderPaths).containsExactly("classpath:org/springframework/web/reactive/result/view/script/");
this.view.setResourceLoaderPath("classpath:org/springframework/web/reactive/result/view/script");
resourceLoaderPaths = (String[]) viewAccessor.getPropertyValue("resourceLoaderPaths");
assertThat(resourceLoaderPaths).containsExactly("classpath:org/springframework/web/reactive/result/view/script/");
}
@Test // gh-23258
public void engineSupplierWithSharedEngine() {
this.configurer.setEngineSupplier(() -> mock(InvocableScriptEngine.class));
this.configurer.setRenderObject("Template");
this.configurer.setRenderFunction("render");
this.configurer.setSharedEngine(true);
DirectFieldAccessor accessor = new DirectFieldAccessor(this.view);
this.view.setApplicationContext(this.context);
ScriptEngine engine1 = this.view.getEngine();
ScriptEngine engine2 = this.view.getEngine();
assertThat(engine1).isNotNull();
assertThat(engine2).isNotNull();
assertThat(accessor.getPropertyValue("renderObject")).isEqualTo("Template");
assertThat(accessor.getPropertyValue("renderFunction")).isEqualTo("render");
assertThat(accessor.getPropertyValue("sharedEngine")).asInstanceOf(BOOLEAN).isTrue();
}
@SuppressWarnings("unchecked")
@Test // gh-23258
public void engineSupplierWithNonSharedEngine() {
this.configurer.setEngineSupplier(() -> mock(InvocableScriptEngine.class));
this.configurer.setRenderObject("Template");
this.configurer.setRenderFunction("render");
this.configurer.setSharedEngine(false);
DirectFieldAccessor accessor = new DirectFieldAccessor(this.view);
this.view.setApplicationContext(this.context);
ScriptEngine engine1 = this.view.getEngine();
ScriptEngine engine2 = this.view.getEngine();
assertThat(engine1).isNotNull();
assertThat(engine2).isNotNull();
assertThat(accessor.getPropertyValue("renderObject")).isEqualTo("Template");
assertThat(accessor.getPropertyValue("renderFunction")).isEqualTo("render");
assertThat(accessor.getPropertyValue("sharedEngine")).asInstanceOf(BOOLEAN).isFalse();
}
private interface InvocableScriptEngine extends ScriptEngine, Invocable {
}
}
@@ -182,7 +182,7 @@ public abstract class FrameworkServlet extends HttpServletBean implements Applic
/** WebApplicationContext implementation class to create. */
private Class<?> contextClass = DEFAULT_CONTEXT_CLASS;
/** WebApplicationContext id to assign. */
/** WebApplicationContext ID to assign. */
@Nullable
private String contextId;
@@ -202,29 +202,29 @@ public abstract class FrameworkServlet extends HttpServletBean implements Applic
@Nullable
private String contextInitializerClasses;
/** Should we publish the context as a ServletContext attribute?. */
/** Whether to publish the context as a ServletContext attribute. */
private boolean publishContext = true;
/** Should we publish a ServletRequestHandledEvent at the end of each request?. */
/** Whether to publish a ServletRequestHandledEvent at the end of each request. */
private boolean publishEvents = true;
/** Expose LocaleContext and RequestAttributes as inheritable for child threads?. */
/** Whether to expose LocaleContext and RequestAttributes as inheritable for child threads. */
private boolean threadContextInheritable = false;
/** Should we dispatch an HTTP OPTIONS request to {@link #doService}?. */
/** Whether to dispatch an HTTP OPTIONS request to {@link #doService}. */
private boolean dispatchOptionsRequest = false;
/** Should we dispatch an HTTP TRACE request to {@link #doService}?. */
/** Whether to dispatch an HTTP TRACE request to {@link #doService}. */
private boolean dispatchTraceRequest = false;
/** Whether to log potentially sensitive info (request params at DEBUG + headers at TRACE). */
/** Whether to log potentially sensitive info (request params at DEBUG and headers at TRACE). */
private boolean enableLoggingRequestDetails = false;
/** WebApplicationContext for this servlet. */
@Nullable
private WebApplicationContext webApplicationContext;
/** If the WebApplicationContext was injected via {@link #setApplicationContext}. */
/** Whether the WebApplicationContext was injected via {@link #setApplicationContext}. */
private boolean webApplicationContextInjected = false;
/** Flag used to detect whether onRefresh has already been called. */
@@ -248,7 +248,7 @@ public abstract class FrameworkServlet extends HttpServletBean implements Applic
* such as {@code AnnotationConfigWebApplicationContext}.
* <p>Calling {@link #setContextInitializerClasses} (init-param 'contextInitializerClasses')
* indicates which {@link ApplicationContextInitializer} classes should be used to
* further configure the internal application context prior to refresh().
* further configure the internal application context prior to refresh.
* @see #FrameworkServlet(WebApplicationContext)
*/
public FrameworkServlet() {
@@ -275,7 +275,7 @@ public abstract class FrameworkServlet extends HttpServletBean implements Applic
* ConfigurableApplicationContext#setParent parent}, the root application context
* will be set as the parent.</li>
* <li>If the given context has not already been assigned an {@linkplain
* ConfigurableApplicationContext#setId id}, one will be assigned to it</li>
* ConfigurableApplicationContext#setId ID}, one will be assigned to it</li>
* <li>{@code ServletContext} and {@code ServletConfig} objects will be delegated to
* the application context</li>
* <li>{@link #postProcessWebApplicationContext} will be called</li>
@@ -337,15 +337,15 @@ public abstract class FrameworkServlet extends HttpServletBean implements Applic
}
/**
* Specify a custom WebApplicationContext id,
* to be used as serialization id for the underlying BeanFactory.
* Specify a custom WebApplicationContext ID,
* to be used as serialization ID for the underlying BeanFactory.
*/
public void setContextId(@Nullable String contextId) {
this.contextId = contextId;
}
/**
* Return the custom WebApplicationContext id, if any.
* Return the custom WebApplicationContext ID, if any.
*/
@Nullable
public String getContextId() {
@@ -472,9 +472,9 @@ public abstract class FrameworkServlet extends HttpServletBean implements Applic
* <p>Default is "false", applying {@link jakarta.servlet.http.HttpServlet}'s
* default behavior (i.e. reflecting the message received back to the client).
* <p>Turn this flag on if you prefer TRACE requests to go through the
* regular dispatching chain, just like other HTTP requests. This usually
* means that your controllers will receive those requests; make sure
* that those endpoints are actually able to handle a TRACE request.
* regular dispatching chain, just like other HTTP requests. This usually means
* that your controllers will receive those requests, in which case you must
* make sure that those endpoints are actually able to handle a TRACE request.
* <p>Note that HttpServlet's default TRACE processing will be applied
* in any case if your controllers happen to not generate a response
* of content type 'message/http' (as required for a TRACE response).
@@ -484,9 +484,9 @@ public abstract class FrameworkServlet extends HttpServletBean implements Applic
}
/**
* Whether to log request params at DEBUG level, and headers at TRACE level.
* Set whether to log request parameters at DEBUG level and headers at TRACE level.
* Both may contain sensitive information.
* <p>By default set to {@code false} so that request details are not shown.
* <p>Defaults to {@code false} so that request details are not shown.
* @param enable whether to enable or not
* @since 5.1
*/
@@ -495,7 +495,7 @@ public abstract class FrameworkServlet extends HttpServletBean implements Applic
}
/**
* Whether logging of potentially sensitive, request details at DEBUG and
* Whether logging of potentially sensitive request details at DEBUG and
* TRACE level is allowed.
* @since 5.1
*/
@@ -574,7 +574,7 @@ public abstract class FrameworkServlet extends HttpServletBean implements Applic
wac = this.webApplicationContext;
if (wac instanceof ConfigurableWebApplicationContext cwac && !cwac.isActive()) {
// The context has not yet been refreshed -> provide services such as
// setting the parent context, setting the application context id, etc
// setting the parent context, setting the application context ID, etc
if (cwac.getParent() == null) {
// The context instance was injected without an explicit parent -> set
// the root application context (if any; may be null) as the parent
@@ -587,7 +587,7 @@ public abstract class FrameworkServlet extends HttpServletBean implements Applic
// No context instance was injected at construction time -> see if one
// has been registered in the servlet context. If one exists, it is assumed
// that the parent context (if any) has already been set and that the
// user has performed any initialization such as setting the context id
// user has performed any initialization such as setting the context ID
wac = findWebApplicationContext();
}
if (wac == null) {
@@ -676,13 +676,13 @@ public abstract class FrameworkServlet extends HttpServletBean implements Applic
protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac) {
if (ObjectUtils.identityToString(wac).equals(wac.getId())) {
// The application context id is still set to its original default value
// -> assign a more useful id based on available information
// The application context ID is still set to its original default value
// -> assign a more useful ID based on available information
if (this.contextId != null) {
wac.setId(this.contextId);
}
else {
// Generate default id...
// Generate default ID...
wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX +
ObjectUtils.getDisplayString(getServletContext().getContextPath()) + '/' + getServletName());
}
@@ -890,7 +890,7 @@ public abstract class FrameworkServlet extends HttpServletBean implements Applic
}
/**
* Delegate GET requests to processRequest/doService.
* Delegate {@code GET} requests to processRequest/doService.
* <p>Will also be invoked by HttpServlet's default implementation of {@code doHead},
* with a {@code NoBodyResponse} that just captures the content length.
* @see #doService
@@ -904,7 +904,7 @@ public abstract class FrameworkServlet extends HttpServletBean implements Applic
}
/**
* Delegate POST requests to {@link #processRequest}.
* Delegate {@code POST} requests to {@link #processRequest}.
* @see #doService
*/
@Override
@@ -915,7 +915,7 @@ public abstract class FrameworkServlet extends HttpServletBean implements Applic
}
/**
* Delegate PUT requests to {@link #processRequest}.
* Delegate {@code PUT} requests to {@link #processRequest}.
* @see #doService
*/
@Override
@@ -926,7 +926,7 @@ public abstract class FrameworkServlet extends HttpServletBean implements Applic
}
/**
* Delegate DELETE requests to {@link #processRequest}.
* Delegate {@code DELETE} requests to {@link #processRequest}.
* @see #doService
*/
@Override
@@ -937,7 +937,7 @@ public abstract class FrameworkServlet extends HttpServletBean implements Applic
}
/**
* Delegate OPTIONS requests to {@link #processRequest}, if desired.
* Delegate {@code OPTIONS} requests to {@link #processRequest}, if desired.
* <p>Applies HttpServlet's standard OPTIONS processing otherwise,
* and also if there is still no 'Allow' header set after dispatching.
* @see #doService
@@ -967,7 +967,7 @@ public abstract class FrameworkServlet extends HttpServletBean implements Applic
}
/**
* Delegate TRACE requests to {@link #processRequest}, if desired.
* Delegate {@code TRACE} requests to {@link #processRequest}, if desired.
* <p>Applies HttpServlet's standard TRACE processing otherwise.
* @see #doService
*/
@@ -1171,7 +1171,9 @@ public abstract class FrameworkServlet extends HttpServletBean implements Applic
/**
* Subclasses must implement this method to do the work of request handling,
* receiving a centralized callback for GET, POST, PUT and DELETE.
* receiving a centralized callback for {@code GET}, {@code POST}, {@code PUT},
* {@code DELETE}, {@code OPTIONS}, and {@code TRACE} requests as well as for
* requests using non-standard HTTP methods (such as WebDAV).
* <p>The contract is essentially the same as that for the commonly overridden
* {@code doGet} or {@code doPost} methods of HttpServlet.
* <p>This class intercepts calls to ensure that exception handling and
@@ -55,7 +55,7 @@ import org.springframework.web.context.support.StandardServletEnvironment;
* parameters is automatic, with the corresponding setter method getting
* invoked with the converted value. It is also possible for subclasses to
* specify required properties. Parameters without matching bean property
* setter will simply be ignored.
* setters will simply be ignored.
*
* <p>This servlet leaves request handling to subclasses, inheriting the default
* behavior of HttpServlet ({@code doGet}, {@code doPost}, etc).
@@ -69,7 +69,7 @@ import org.springframework.web.context.support.StandardServletEnvironment;
*
* <p>The {@link FrameworkServlet} class is a more specific servlet base
* class which loads its own application context. FrameworkServlet serves
* as direct base class of Spring's full-fledged {@link DispatcherServlet}.
* as the direct base class of Spring's full-fledged {@link DispatcherServlet}.
*
* @author Rod Johnson
* @author Juergen Hoeller
@@ -109,6 +109,7 @@ public abstract class HttpServletBean extends HttpServlet implements Environment
* provided by default.
* @throws IllegalArgumentException if environment is not assignable to
* {@code ConfigurableEnvironment}
* @since 3.1
*/
@Override
public void setEnvironment(Environment environment) {
@@ -120,6 +121,7 @@ public abstract class HttpServletBean extends HttpServlet implements Environment
* Return the {@link Environment} associated with this servlet.
* <p>If none specified, a default environment will be initialized via
* {@link #createEnvironment()}.
* @since 3.1
*/
@Override
public ConfigurableEnvironment getEnvironment() {
@@ -133,6 +135,7 @@ public abstract class HttpServletBean extends HttpServlet implements Environment
* Create and return a new {@link StandardServletEnvironment}.
* <p>Subclasses may override this in order to configure the environment or
* specialize the environment type returned.
* @since 3.1
*/
protected ConfigurableEnvironment createEnvironment() {
return new StandardServletEnvironment();
@@ -17,6 +17,7 @@
package org.springframework.web.servlet.function;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.Collection;
import java.util.Set;
@@ -28,6 +29,7 @@ import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.MediaType;
import org.springframework.lang.Nullable;
import org.springframework.util.CollectionUtils;
import org.springframework.util.LinkedMultiValueMap;
@@ -51,15 +53,16 @@ abstract class AbstractServerResponse extends ErrorHandlingServerResponse {
private final MultiValueMap<String, Cookie> cookies;
protected AbstractServerResponse(
HttpStatusCode statusCode, HttpHeaders headers, MultiValueMap<String, Cookie> cookies) {
this.statusCode = statusCode;
this.headers = HttpHeaders.readOnlyHttpHeaders(headers);
this.cookies =
CollectionUtils.unmodifiableMultiValueMap(new LinkedMultiValueMap<>(cookies));
this.cookies = CollectionUtils.unmodifiableMultiValueMap(new LinkedMultiValueMap<>(cookies));
}
@Override
public final HttpStatusCode statusCode() {
return this.statusCode;
@@ -118,14 +121,29 @@ abstract class AbstractServerResponse extends ErrorHandlingServerResponse {
servletResponse.addHeader(headerName, headerValue);
}
});
// HttpServletResponse exposes some headers as properties: we should include those if not already present
if (servletResponse.getContentType() == null && this.headers.getContentType() != null) {
servletResponse.setContentType(this.headers.getContentType().toString());
if (servletResponse.getContentType() == null && this.headers.containsKey(HttpHeaders.CONTENT_TYPE)) {
servletResponse.setContentType(this.headers.getFirst(HttpHeaders.CONTENT_TYPE));
}
if (servletResponse.getCharacterEncoding() == null &&
this.headers.getContentType() != null &&
this.headers.getContentType().getCharset() != null) {
servletResponse.setCharacterEncoding(this.headers.getContentType().getCharset().name());
if (servletResponse.getCharacterEncoding() == null && this.headers.containsKey(HttpHeaders.CONTENT_TYPE)) {
try {
// Lazy parsing into MediaType
MediaType contentType = this.headers.getContentType();
if (contentType != null) {
Charset charset = contentType.getCharset();
if (charset != null) {
servletResponse.setCharacterEncoding(charset.name());
}
}
}
catch (Exception ex) {
// Leave character encoding unspecified
}
}
long contentLength = this.headers.getContentLength();
if (contentLength != -1) {
servletResponse.setContentLengthLong(contentLength);
}
}
@@ -34,6 +34,7 @@ import org.springframework.web.servlet.ModelAndView;
/**
* Base class for {@link ServerResponse} implementations with error handling.
*
* @author Arjen Poutsma
* @since 5.3
*/
@@ -83,6 +84,7 @@ abstract class ErrorHandlingServerResponse implements ServerResponse {
return null;
}
private static class ErrorHandler<T extends ServerResponse> {
private final Predicate<Throwable> predicate;
@@ -275,12 +275,11 @@ public abstract class AbstractMessageConverterMethodProcessor extends AbstractMe
"No converter found for return value of type: " + valueType);
}
List<MediaType> compatibleMediaTypes = new ArrayList<>();
determineCompatibleMediaTypes(acceptableTypes, producibleTypes, compatibleMediaTypes);
List<MediaType> compatibleMediaTypes = determineCompatibleMediaTypes(acceptableTypes, producibleTypes);
// For ProblemDetail, fall back on RFC 9457 format
if (compatibleMediaTypes.isEmpty() && ProblemDetail.class.isAssignableFrom(valueType)) {
determineCompatibleMediaTypes(this.problemMediaTypes, producibleTypes, compatibleMediaTypes);
compatibleMediaTypes = determineCompatibleMediaTypes(this.problemMediaTypes, producibleTypes);
}
if (compatibleMediaTypes.isEmpty()) {
@@ -453,16 +452,18 @@ public abstract class AbstractMessageConverterMethodProcessor extends AbstractMe
return this.contentNegotiationManager.resolveMediaTypes(new ServletWebRequest(request));
}
private void determineCompatibleMediaTypes(
List<MediaType> acceptableTypes, List<MediaType> producibleTypes, List<MediaType> mediaTypesToUse) {
private List<MediaType> determineCompatibleMediaTypes(
List<MediaType> acceptableTypes, List<MediaType> producibleTypes) {
Set<MediaType> compatibleTypes = new LinkedHashSet<>();
for (MediaType requestedType : acceptableTypes) {
for (MediaType producibleType : producibleTypes) {
if (requestedType.isCompatibleWith(producibleType)) {
mediaTypesToUse.add(getMostSpecificMediaType(requestedType, producibleType));
compatibleTypes.add(getMostSpecificMediaType(requestedType, producibleType));
}
}
}
return new ArrayList<>(compatibleTypes);
}
/**
@@ -459,6 +459,11 @@ public class ResponseBodyEmitterReturnValueHandler implements HandlerMethodRetur
// ignore
}
// @Override - on Servlet 6.1
public void setCharacterEncoding(Charset encoding) {
// ignore
}
@Override
public void setContentLength(int len) {
// ignore
@@ -26,6 +26,7 @@ import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.ModelAndView;
@@ -195,19 +196,20 @@ public class SseEmitter extends ResponseBodyEmitter {
private final Set<DataWithMediaType> dataToSend = new LinkedHashSet<>(4);
@Nullable
private StringBuilder sb;
private final StringBuilder sb = new StringBuilder();
private boolean hasName;
@Override
public SseEventBuilder id(String id) {
checkEvent(id);
append("id:").append(id).append('\n');
return this;
}
@Override
public SseEventBuilder name(String name) {
checkEvent(name);
this.hasName = true;
append("event:").append(name).append('\n');
return this;
@@ -221,7 +223,7 @@ public class SseEmitter extends ResponseBodyEmitter {
@Override
public SseEventBuilder comment(String comment) {
append(':').append(comment).append('\n');
append(':').append(StringUtils.replace(comment, "\n", "\n:")).append('\n');
return this;
}
@@ -236,27 +238,53 @@ public class SseEmitter extends ResponseBodyEmitter {
name(mav.getViewName());
}
append("data:");
saveAppendedText();
saveAppendedText(TEXT_PLAIN);
if (object instanceof String text) {
object = StringUtils.replace(text, "\n", "\ndata:");
writeStringData(text, mediaType);
}
else {
this.dataToSend.add(new DataWithMediaType(object, mediaType));
}
this.dataToSend.add(new DataWithMediaType(object, mediaType));
append('\n');
return this;
}
SseEventBuilderImpl append(String text) {
if (this.sb == null) {
this.sb = new StringBuilder();
private static void checkEvent(String content) {
Assert.isTrue(content.indexOf('\n') == -1 && content.indexOf('\r') == -1,
"illegal character '\\n' or '\\r' in event content");
}
private void writeStringData(String input, @Nullable MediaType mediaType) {
if (input.indexOf('\n') == -1 && input.indexOf('\r') == -1) {
this.dataToSend.add(new DataWithMediaType(input, mediaType));
}
else {
int length = input.length();
for (int i = 0; i < length; i++) {
char c = input.charAt(i);
if (c == '\r') {
if (i + 1 < length && input.charAt(i + 1) == '\n') {
i++;
}
this.sb.append("\ndata:");
}
else if (c == '\n') {
this.sb.append("\ndata:");
}
else {
this.sb.append(c);
}
}
saveAppendedText(mediaType);
}
}
SseEventBuilderImpl append(String text) {
this.sb.append(text);
return this;
}
SseEventBuilderImpl append(char ch) {
if (this.sb == null) {
this.sb = new StringBuilder();
}
this.sb.append(ch);
return this;
}
@@ -267,14 +295,14 @@ public class SseEmitter extends ResponseBodyEmitter {
return Collections.emptySet();
}
append('\n');
saveAppendedText();
saveAppendedText(TEXT_PLAIN);
return this.dataToSend;
}
private void saveAppendedText() {
if (this.sb != null) {
this.dataToSend.add(new DataWithMediaType(this.sb.toString(), TEXT_PLAIN));
this.sb = null;
private void saveAppendedText(@Nullable MediaType mediaType) {
if (StringUtils.hasLength(this.sb)) {
this.dataToSend.add(new DataWithMediaType(this.sb.toString(), mediaType));
this.sb.setLength(0);
}
}
}
@@ -51,6 +51,7 @@ import org.springframework.util.Assert;
import org.springframework.util.FileCopyUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.resource.ResourceHandlerUtils;
import org.springframework.web.servlet.support.RequestContextUtils;
import org.springframework.web.servlet.view.AbstractUrlBasedView;
@@ -199,14 +200,13 @@ public class ScriptTemplateView extends AbstractUrlBasedView {
*/
public void setResourceLoaderPath(String resourceLoaderPath) {
String[] paths = StringUtils.commaDelimitedListToStringArray(resourceLoaderPath);
this.resourceLoaderPaths = new String[paths.length + 1];
this.resourceLoaderPaths[0] = "";
this.resourceLoaderPaths = new String[paths.length];
for (int i = 0; i < paths.length; i++) {
String path = paths[i];
if (!path.endsWith("/") && !path.endsWith(":")) {
path = path + "/";
}
this.resourceLoaderPaths[i + 1] = path;
this.resourceLoaderPaths[i] = path;
}
}
@@ -352,11 +352,26 @@ public class ScriptTemplateView extends AbstractUrlBasedView {
@Nullable
protected Resource getResource(String location) {
if (this.resourceLoaderPaths != null) {
String normalizedLocation = ResourceHandlerUtils.normalizeInputPath(location);
if (this.resourceLoaderPaths != null && !ResourceHandlerUtils.shouldIgnoreInputPath(normalizedLocation)) {
ApplicationContext context = obtainApplicationContext();
for (String path : this.resourceLoaderPaths) {
Resource resource = obtainApplicationContext().getResource(path + location);
if (resource.exists()) {
return resource;
Resource resource = context.getResource(path + normalizedLocation);
try {
if (resource.exists() && ResourceHandlerUtils.isResourceUnderLocation(context.getResource(path), resource)) {
return resource;
}
}
catch (IOException ex) {
if (logger.isDebugEnabled()) {
String error = "Skip location [" + normalizedLocation + "] due to error";
if (logger.isTraceEnabled()) {
logger.trace(error, ex);
}
else {
logger.debug(error + ": " + ex.getMessage());
}
}
}
}
}
@@ -402,7 +402,7 @@ public class XsltView extends AbstractUrlBasedView {
* Configure the supplied {@link HttpServletResponse}.
* <p>The default implementation of this method sets the
* {@link HttpServletResponse#setContentType content type} and
* {@link HttpServletResponse#setCharacterEncoding encoding}
* {@link HttpServletResponse#setCharacterEncoding(String) encoding}
* from the "media-type" and "encoding" output properties
* specified in the {@link Transformer}.
* @param model merged output Map (never {@code null})
@@ -24,6 +24,8 @@ import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.JsonTypeName;
@@ -783,6 +785,24 @@ class RequestResponseBodyMethodProcessorTests {
assertThat(value).isEqualTo("foo");
}
@Test // gh-36300
void shouldNotDuplicateInCompatibleMediaTypes() throws Exception {
Method method = TestRestController.class.getMethod("handle");
MethodParameter returnType = new MethodParameter(method, -1);
List<HttpMessageConverter<?>> converters = List.of(new StringHttpMessageConverter(), new MappingJackson2HttpMessageConverter());
RequestResponseBodyMethodProcessor processor = new RequestResponseBodyMethodProcessor(converters);
String accept = Stream.iterate(1, i -> i + 1)
.limit(48).map(i -> "application/" + i)
.collect(Collectors.joining(","));
accept = accept + ", application/json";
this.servletRequest.addHeader("Accept", accept);
processor.writeWithMessageConverters("spring framework", returnType, this.request);
}
private void assertContentDisposition(RequestResponseBodyMethodProcessor processor,
boolean expectContentDisposition, String requestURI, String comment) throws Exception {
@@ -22,14 +22,19 @@ import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.function.Consumer;
import java.util.stream.Stream;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.springframework.http.MediaType;
import org.springframework.lang.Nullable;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.springframework.web.servlet.mvc.method.annotation.SseEmitter.event;
@@ -105,9 +110,10 @@ class SseEmitterTests {
this.handler.assertWriteCount(1);
}
@Test
void sendEventWithMultiline() throws Exception {
this.emitter.send(event().data("foo\nbar\nbaz"));
@ParameterizedTest(name = "{1}")
@MethodSource("newLineCharacters")
void sendEventWithMultiline(String newLineChars, String description) throws Exception {
this.emitter.send(event().data("foo" + newLineChars + "bar" + newLineChars + "baz"));
this.handler.assertSentObjectCount(3);
this.handler.assertObject(0, "data:", TEXT_PLAIN_UTF8);
this.handler.assertObject(1, "foo\ndata:bar\ndata:baz");
@@ -115,6 +121,17 @@ class SseEmitterTests {
this.handler.assertWriteCount(1);
}
@ParameterizedTest(name = "{1}")
@MethodSource("newLineCharacters")
void sendEventWithMultilineWithMediaType(String newLineChars, String description) throws Exception {
this.emitter.send(event().data("foo" + newLineChars + "bar" + newLineChars + "baz", MediaType.TEXT_PLAIN));
this.handler.assertSentObjectCount(3);
this.handler.assertObject(0, "data:", TEXT_PLAIN_UTF8);
this.handler.assertObject(1, "foo\ndata:bar\ndata:baz", MediaType.TEXT_PLAIN);
this.handler.assertObject(2, "\n\n", TEXT_PLAIN_UTF8);
this.handler.assertWriteCount(1);
}
@Test
void sendEventFull() throws Exception {
this.emitter.send(event().comment("blah").name("test").reconnectTime(5000L).id("1").data("foo"));
@@ -137,6 +154,28 @@ class SseEmitterTests {
this.handler.assertWriteCount(1);
}
@ParameterizedTest(name = "{1}")
@MethodSource("newLineCharacters")
void rejectInvalidId(String newLineChars, String description) {
assertThatIllegalArgumentException().isThrownBy(() -> this.emitter
.send(event().id("first" + newLineChars + "second")));
}
@ParameterizedTest(name = "{1}")
@MethodSource("newLineCharacters")
void rejectInvalidName(String newLineChars, String description) {
assertThatIllegalArgumentException().isThrownBy(() -> this.emitter
.send(event().name("first" + newLineChars + "second")));
}
private static Stream<Arguments> newLineCharacters() {
return Stream.of(
Arguments.of("\n", "LF"),
Arguments.of("\r", "CR"),
Arguments.of("\r\n", "CRLF")
);
}
private static class TestHandler implements ResponseBodyEmitter.Handler {
@@ -0,0 +1,333 @@
/*
* 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.web.servlet.view.script;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import javax.script.Invocable;
import javax.script.ScriptEngine;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.DirectFieldAccessor;
import org.springframework.context.ApplicationContextException;
import org.springframework.context.support.StaticApplicationContext;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.web.context.support.StaticWebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.testfixture.servlet.MockHttpServletRequest;
import org.springframework.web.testfixture.servlet.MockHttpServletResponse;
import org.springframework.web.testfixture.servlet.MockServletContext;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.assertj.core.api.InstanceOfAssertFactories.BOOLEAN;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/**
* Unit tests for {@link ScriptTemplateView}.
*
* @author Sebastien Deleuze
*/
public class ScriptTemplateViewTests {
private ScriptTemplateView view;
private ScriptTemplateConfigurer configurer;
private StaticWebApplicationContext wac;
@BeforeEach
public void setup() {
this.configurer = new ScriptTemplateConfigurer();
this.wac = new StaticWebApplicationContext();
this.wac.getBeanFactory().registerSingleton("scriptTemplateConfigurer", this.configurer);
this.view = new ScriptTemplateView();
}
@Test
public void missingTemplate() throws Exception {
MockServletContext servletContext = new MockServletContext();
this.wac.setServletContext(servletContext);
this.wac.refresh();
this.view.setResourceLoaderPath("classpath:org/springframework/web/servlet/view/script/");
this.view.setUrl("missing.txt");
this.view.setEngine(mock(InvocableScriptEngine.class));
this.configurer.setRenderFunction("render");
this.view.setApplicationContext(this.wac);
assertThat(this.view.checkResource(Locale.ENGLISH)).isFalse();
}
@Test
public void missingScriptTemplateConfig() {
assertThatExceptionOfType(ApplicationContextException.class).isThrownBy(() ->
this.view.setApplicationContext(new StaticApplicationContext()))
.withMessageContaining("ScriptTemplateConfig");
}
@Test
public void detectScriptTemplateConfigWithEngine() {
InvocableScriptEngine engine = mock(InvocableScriptEngine.class);
this.configurer.setEngine(engine);
this.configurer.setRenderObject("Template");
this.configurer.setRenderFunction("render");
this.configurer.setContentType(MediaType.TEXT_PLAIN_VALUE);
this.configurer.setCharset(StandardCharsets.ISO_8859_1);
this.configurer.setSharedEngine(true);
DirectFieldAccessor accessor = new DirectFieldAccessor(this.view);
this.view.setApplicationContext(this.wac);
assertThat(accessor.getPropertyValue("engine")).isEqualTo(engine);
assertThat(accessor.getPropertyValue("renderObject")).isEqualTo("Template");
assertThat(accessor.getPropertyValue("renderFunction")).isEqualTo("render");
assertThat(accessor.getPropertyValue("contentType")).isEqualTo(MediaType.TEXT_PLAIN_VALUE);
assertThat(accessor.getPropertyValue("charset")).isEqualTo(StandardCharsets.ISO_8859_1);
assertThat(accessor.getPropertyValue("sharedEngine")).asInstanceOf(BOOLEAN).isTrue();
}
@Test
public void detectScriptTemplateConfigWithEngineName() {
this.configurer.setEngineName("jython");
this.configurer.setRenderObject("Template");
this.configurer.setRenderFunction("render");
DirectFieldAccessor accessor = new DirectFieldAccessor(this.view);
this.view.setApplicationContext(this.wac);
assertThat(accessor.getPropertyValue("engineName")).isEqualTo("jython");
assertThat(accessor.getPropertyValue("engine")).isNotNull();
assertThat(accessor.getPropertyValue("renderObject")).isEqualTo("Template");
assertThat(accessor.getPropertyValue("renderFunction")).isEqualTo("render");
assertThat(accessor.getPropertyValue("contentType")).isEqualTo(MediaType.TEXT_HTML_VALUE);
assertThat(accessor.getPropertyValue("charset")).isEqualTo(StandardCharsets.UTF_8);
}
@Test
public void customEngineAndRenderFunction() {
ScriptEngine engine = mock(InvocableScriptEngine.class);
given(engine.get("key")).willReturn("value");
this.view.setEngine(engine);
this.view.setRenderFunction("render");
this.view.setApplicationContext(this.wac);
engine = this.view.getEngine();
assertThat(engine).isNotNull();
assertThat(engine.get("key")).isEqualTo("value");
DirectFieldAccessor accessor = new DirectFieldAccessor(this.view);
assertThat(accessor.getPropertyValue("renderObject")).isNull();
assertThat(accessor.getPropertyValue("renderFunction")).isEqualTo("render");
assertThat(accessor.getPropertyValue("charset")).isEqualTo(StandardCharsets.UTF_8);
}
@Test
public void nonSharedEngine() throws Exception {
int iterations = 20;
this.view.setEngineName("jython");
this.view.setRenderFunction("render");
this.view.setSharedEngine(false);
this.view.setApplicationContext(this.wac);
ExecutorService executor = Executors.newFixedThreadPool(4);
List<Future<Boolean>> results = new ArrayList<>();
for (int i = 0; i < iterations; i++) {
results.add(executor.submit(() -> view.getEngine() != null));
}
assertThat(results.size()).isEqualTo(iterations);
for (int i = 0; i < iterations; i++) {
assertThat((boolean) results.get(i).get()).isTrue();
}
executor.shutdown();
}
@Test
public void nonInvocableScriptEngine() {
this.view.setEngine(mock(ScriptEngine.class));
this.view.setApplicationContext(this.wac);
}
@Test
public void nonInvocableScriptEngineWithRenderFunction() {
this.view.setEngine(mock(ScriptEngine.class));
this.view.setRenderFunction("render");
assertThatIllegalArgumentException().isThrownBy(() ->
this.view.setApplicationContext(this.wac));
}
@Test
public void engineAndEngineNameBothDefined() {
this.view.setEngine(mock(InvocableScriptEngine.class));
this.view.setEngineName("test");
this.view.setRenderFunction("render");
assertThatIllegalArgumentException().isThrownBy(() ->
this.view.setApplicationContext(this.wac))
.withMessageContaining("You should define either 'engine', 'engineSupplier', or 'engineName'.");
}
@Test // gh-23258
public void engineAndEngineSupplierBothDefined() {
ScriptEngine engine = mock(InvocableScriptEngine.class);
this.view.setEngineSupplier(() -> engine);
this.view.setEngine(engine);
this.view.setRenderFunction("render");
assertThatIllegalArgumentException().isThrownBy(() ->
this.view.setApplicationContext(this.wac))
.withMessageContaining("You should define either 'engine', 'engineSupplier', or 'engineName'.");
}
@Test // gh-23258
public void engineNameAndEngineSupplierBothDefined() {
this.view.setEngineSupplier(() -> mock(InvocableScriptEngine.class));
this.view.setEngineName("test");
this.view.setRenderFunction("render");
assertThatIllegalArgumentException().isThrownBy(() ->
this.view.setApplicationContext(this.wac))
.withMessageContaining("You should define either 'engine', 'engineSupplier', or 'engineName'.");
}
@Test
public void engineSetterAndNonSharedEngine() {
this.view.setEngine(mock(InvocableScriptEngine.class));
this.view.setRenderFunction("render");
this.view.setSharedEngine(false);
assertThatIllegalArgumentException().isThrownBy(() ->
this.view.setApplicationContext(this.wac))
.withMessageContaining("sharedEngine");
}
@Test // SPR-14210
public void resourceLoaderPath() throws Exception {
MockServletContext servletContext = new MockServletContext();
this.wac.setServletContext(servletContext);
this.wac.refresh();
MockHttpServletRequest request = new MockHttpServletRequest();
request.setAttribute(DispatcherServlet.WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.wac);
MockHttpServletResponse response = new MockHttpServletResponse();
Map<String, Object> model = new HashMap<>();
InvocableScriptEngine engine = mock(InvocableScriptEngine.class);
given(engine.invokeFunction(any(), any(), any(), any())).willReturn("foo");
this.view.setEngine(engine);
this.view.setRenderFunction("render");
this.view.setApplicationContext(this.wac);
this.view.setUrl("org/springframework/web/servlet/view/script/empty.txt");
this.view.render(model, request, response);
assertThat(response.getContentAsString()).isEqualTo("foo");
DirectFieldAccessor viewAccessor = new DirectFieldAccessor(this.view);
String[] resourceLoaderPaths = (String[]) viewAccessor.getPropertyValue("resourceLoaderPaths");
assertThat(resourceLoaderPaths).containsExactly("classpath:");
response = new MockHttpServletResponse();
this.view.setResourceLoaderPath("classpath:org/springframework/web/servlet/view/script/");
this.view.setUrl("empty.txt");
this.view.render(model, request, response);
assertThat(response.getContentAsString()).isEqualTo("foo");
resourceLoaderPaths = (String[]) viewAccessor.getPropertyValue("resourceLoaderPaths");
assertThat(resourceLoaderPaths).containsExactly("classpath:org/springframework/web/servlet/view/script/");
response = new MockHttpServletResponse();
this.view.setResourceLoaderPath("classpath:org/springframework/web/servlet/view/script");
this.view.setUrl("empty.txt");
this.view.render(model, request, response);
assertThat(response.getContentAsString()).isEqualTo("foo");
resourceLoaderPaths = (String[]) viewAccessor.getPropertyValue("resourceLoaderPaths");
assertThat(resourceLoaderPaths).containsExactly("classpath:org/springframework/web/servlet/view/script/");
}
@Test // SPR-13379
public void contentType() throws Exception {
MockServletContext servletContext = new MockServletContext();
this.wac.setServletContext(servletContext);
this.wac.refresh();
MockHttpServletRequest request = new MockHttpServletRequest();
request.setAttribute(DispatcherServlet.WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.wac);
MockHttpServletResponse response = new MockHttpServletResponse();
Map<String, Object> model = new HashMap<>();
this.view.setEngine(mock(InvocableScriptEngine.class));
this.view.setRenderFunction("render");
this.view.setResourceLoaderPath("classpath:org/springframework/web/servlet/view/script/");
this.view.setUrl("empty.txt");
this.view.setApplicationContext(this.wac);
this.view.render(model, request, response);
assertThat(response.getHeader(HttpHeaders.CONTENT_TYPE)).isEqualTo((MediaType.TEXT_HTML_VALUE + ";charset=" +
StandardCharsets.UTF_8));
response = new MockHttpServletResponse();
this.view.setContentType(MediaType.TEXT_PLAIN_VALUE);
this.view.render(model, request, response);
assertThat(response.getHeader(HttpHeaders.CONTENT_TYPE)).isEqualTo((MediaType.TEXT_PLAIN_VALUE + ";charset=" +
StandardCharsets.UTF_8));
response = new MockHttpServletResponse();
this.view.setCharset(StandardCharsets.ISO_8859_1);
this.view.render(model, request, response);
assertThat(response.getHeader(HttpHeaders.CONTENT_TYPE)).isEqualTo((MediaType.TEXT_PLAIN_VALUE + ";charset=" +
StandardCharsets.ISO_8859_1));
}
@Test // gh-23258
public void engineSupplierWithSharedEngine() {
this.configurer.setEngineSupplier(() -> mock(InvocableScriptEngine.class));
this.configurer.setRenderObject("Template");
this.configurer.setRenderFunction("render");
this.configurer.setSharedEngine(true);
DirectFieldAccessor accessor = new DirectFieldAccessor(this.view);
this.view.setApplicationContext(this.wac);
ScriptEngine engine1 = this.view.getEngine();
ScriptEngine engine2 = this.view.getEngine();
assertThat(engine1).isNotNull();
assertThat(engine2).isNotNull();
assertThat(accessor.getPropertyValue("renderObject")).isEqualTo("Template");
assertThat(accessor.getPropertyValue("renderFunction")).isEqualTo("render");
assertThat(accessor.getPropertyValue("sharedEngine")).asInstanceOf(BOOLEAN).isTrue();
}
@Test // gh-23258
public void engineSupplierWithNonSharedEngine() {
this.configurer.setEngineSupplier(() -> mock(InvocableScriptEngine.class));
this.configurer.setRenderObject("Template");
this.configurer.setRenderFunction("render");
this.configurer.setSharedEngine(false);
DirectFieldAccessor accessor = new DirectFieldAccessor(this.view);
this.view.setApplicationContext(this.wac);
ScriptEngine engine1 = this.view.getEngine();
ScriptEngine engine2 = this.view.getEngine();
assertThat(engine1).isNotNull();
assertThat(engine2).isNotNull();
assertThat(accessor.getPropertyValue("renderObject")).isEqualTo("Template");
assertThat(accessor.getPropertyValue("renderFunction")).isEqualTo("render");
assertThat(accessor.getPropertyValue("sharedEngine")).asInstanceOf(BOOLEAN).isFalse();
}
private interface InvocableScriptEngine extends ScriptEngine, Invocable {
}
}