Compare commits

..

1 Commits

Author SHA1 Message Date
Brian Clozel 053d8e25f4 Release v6.2.16 2026-02-12 09:33:48 +01:00
79 changed files with 441 additions and 1703 deletions
+4
View File
@@ -94,6 +94,10 @@ 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, 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).
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).
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.
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.
====
As an alternative for injection by name, consider the JSR-250 `@Resource` annotation
@@ -308,8 +308,7 @@ 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` or `@Fallback` on a single bean
for each type.
through `@Order` values in combination with `@Primary` on a single bean for each type.
====
Even typed `Map` instances can be autowired as long as the expected key type is `String`.
@@ -27,5 +27,4 @@ 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. For annotation-based configuration, see
xref:core/beans/annotation-config/autowired-primary.adoc[Fine-tuning with `@Primary` or `@Fallback`].
attribute set to `true`, it is selected.
@@ -67,13 +67,6 @@ 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
@@ -87,7 +80,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 early initialization
.`BeanPostProcessor` instances and AOP auto-proxying
[NOTE]
====
Classes that implement the `BeanPostProcessor` interface are special and are treated
@@ -97,23 +90,17 @@ 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. More generally, any bean that is instantiated
during this early phase is not eligible for full post-processing by all
`BeanPostProcessor` instances.
thus, do not have aspects woven into them.
For any such bean, you should see a WARN-level log message similar to the following.
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)`.
[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
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.
====
@@ -148,7 +135,7 @@ Java::
}
public Object postProcessAfterInitialization(Object bean, String beanName) {
System.out.println("Bean '" + beanName + "' created : " + bean);
System.out.println("Bean '" + beanName + "' created : " + bean.toString());
return bean;
}
}
@@ -177,48 +164,7 @@ Kotlin::
----
======
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:
The following `beans` element uses the `InstantiationTracingBeanPostProcessor`:
[source,xml,indent=0,subs="verbatim,quotes"]
----
@@ -246,7 +192,7 @@ Alternatively, the `InstantiationTracingBeanPostProcessor` can be registered via
----
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 with any
even have a name, and, because it is a bean, it can be dependency-injected as you would 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].)
@@ -355,23 +301,6 @@ 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(Char::class.java)
.getValue(Character::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,7 +541,32 @@ 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.
include-code::./ListConcatenation[]
[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));
}
}
----
If we register `ListConcatenation` as the `OperatorOverloader` in a
`StandardEvaluationContext`, we can then evaluate expressions like `{1, 2, 3} + {4, 5}`
@@ -564,8 +589,8 @@ Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
val context = StandardEvaluationContext()
context.operatorOverloader = ListConcatenation()
StandardEvaluationContext context = StandardEvaluationContext()
context.setOperatorOverloader(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,6 +49,7 @@ 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:
@@ -86,6 +87,7 @@ 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]).
@@ -94,23 +96,22 @@ 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 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].
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.
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.
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.
* 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/mixed-config.adoc[Mixing Component Classes, XML, and 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/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,32 +1,33 @@
[[testcontext-ctx-management-mixed-config]]
= Mixing Component Classes, XML, and Groovy Scripts
= Mixing XML, Groovy Scripts, and Component Classes
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
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
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, `@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
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
`GenericGroovyXmlWebContextLoader` support both XML configuration files and Groovy
scripts simultaneously. Furthermore, third-party frameworks may choose to support the
declaration of both `classes` and `locations` through `@ContextConfiguration`, and, with
declaration of both `locations` and `classes` through `@ContextConfiguration`, and, with
the standard testing support in the TestContext framework, you have the following options.
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
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
to how you configure your application in production: In production configuration, you
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.
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.
@@ -251,8 +251,7 @@ Kotlin::
======
TIP: If you need to have a `LocalValidatorFactoryBean` injected somewhere, create a bean and
mark it with `@Primary`, or mark the one declared in the MVC config with `@Fallback`, in
order to avoid conflict.
mark it with `@Primary` in order to avoid conflict with the one declared in the MVC config.
[[webflux-config-content-negotiation]]
@@ -19,5 +19,4 @@ 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`, or mark the one declared in the MVC configuration with
`@Fallback`, in order to avoid conflict.
mark it with `@Primary` in order to avoid conflict with the one declared in the MVC configuration.
@@ -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 URIs.
This section describes various options available in the Spring Framework to work with URI's.
include::partial$web/web-uris.adoc[leveloffset=+1]
@@ -2,7 +2,7 @@
= UriComponents
[.small]#Spring MVC and Spring WebFlux#
`UriComponentsBuilder` helps to build URIs from URI templates with variables, as the following example shows:
`UriComponentsBuilder` helps to build URI's from URI templates with variables, as the following example shows:
[tabs]
======
@@ -128,7 +128,7 @@ Kotlin::
= UriBuilder
[.small]#Spring MVC and Spring WebFlux#
<<uricomponents, `UriComponentsBuilder`>> implements `UriBuilder`. You can create a
<<web-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 URLs. For more details, see the URL Living Standard and URL parsing
leniently user typed URL's. 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 URLs in order to inspect and possibly validated URI components
to parse user provided URL's 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 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
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
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.8",
"fast-xml-parser": "5.3.4",
"@springio/asciidoctor-extensions": "1.0.0-alpha.17"
}
}
@@ -1,47 +0,0 @@
/*
* 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));
}
}
@@ -1,37 +0,0 @@
/*
* 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.10"))
api(platform("io.micrometer:micrometer-bom:1.15.9"))
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.16"))
api(platform("io.projectreactor:reactor-bom:2024.0.15"))
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.33"))
api(platform("org.eclipse.jetty.ee10:jetty-ee10-bom:12.0.33"))
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.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.3"))
api(platform("org.mockito:mockito-bom:5.22.0"))
api(platform("org.junit:junit-bom:5.14.2"))
api(platform("org.mockito:mockito-bom:5.21.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.34.0")
api("com.h2database:h2:2.4.240")
api("com.google.protobuf:protobuf-java-util:4.33.4")
api("com.h2database:h2:2.3.232")
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.1.0.RELEASE")
api("io.r2dbc:r2dbc-h2:1.0.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.41.0")
api("org.seleniumhq.selenium:selenium-java:4.41.0")
api("org.seleniumhq.selenium:htmlunit3-driver:4.40.0")
api("org.seleniumhq.selenium:selenium-java:4.40.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.6")
api("org.yaml:snakeyaml:2.5")
}
}
+1 -1
View File
@@ -1,4 +1,4 @@
version=6.2.17
version=6.2.16
org.gradle.caching=true
org.gradle.jvmargs=-Xmx2048m
@@ -40,13 +40,6 @@ 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,14 +34,6 @@ 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,11 +35,12 @@ 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>
*
@@ -51,29 +52,31 @@ 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, Fallback, Order</h3>
* <h3>Profile, Scope, Lazy, DependsOn, Primary, 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 aforementioned annotations match their use at the component
* The semantics of the above-mentioned 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.
@@ -82,9 +85,6 @@ 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} or
* {@code @Fallback} on a single bean per type.
* be modeled through {@code @Order} values in combination with {@code @Primary} 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,21 +158,20 @@ 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 @Configuration} javadoc for further details
* including how to bootstrap the container using
* {@link AnnotationConfigApplicationContext} and friends.
* <p>See the @{@link Configuration} javadoc for further details including how to bootstrap
* the container using {@link AnnotationConfigApplicationContext} and friends.
*
* <h3>{@code BeanFactoryPostProcessor}-returning {@code @Bean} methods</h3>
*
@@ -184,44 +183,20 @@ 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 aforementioned lifecycle conflicts.
* declaring {@code @Configuration} class, thus avoiding the above-mentioned 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
@@ -233,7 +208,6 @@ 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() with bean name '%s'"
.formatted(configClass.getMetadata().getClassName(), methodName, beanName));
logger.trace("Registering bean definition for @Bean method %s.%s()"
.formatted(configClass.getMetadata().getClassName(), 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("""
@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()));
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()));
}
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,8 +23,6 @@ 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;
@@ -100,8 +98,7 @@ class ApplicationListenerDetector implements DestructionAwareBeanPostProcessor,
try {
ApplicationEventMulticaster multicaster = this.applicationContext.getApplicationEventMulticaster();
multicaster.removeApplicationListener(applicationListener);
multicaster.removeApplicationListenerBean(
bean instanceof FactoryBean ? BeanFactory.FACTORY_BEAN_PREFIX + beanName : beanName);
multicaster.removeApplicationListenerBean(beanName);
}
catch (IllegalStateException ex) {
// ApplicationEventMulticaster not initialized yet - no need to remove a listener
@@ -92,12 +92,10 @@ 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_EXTENSION, XML_EXTENSION);
private List<String> fileExtensions = List.of(".properties", XML_EXTENSION);
@Nullable
private Properties fileEncodings;
@@ -383,18 +381,18 @@ public class ReloadableResourceBundleMessageSource extends AbstractResourceBased
StringBuilder temp = new StringBuilder(basename);
temp.append('_');
if (!language.isEmpty()) {
if (language.length() > 0) {
temp.append(language);
result.add(0, temp.toString());
}
temp.append('_');
if (!country.isEmpty()) {
if (country.length() > 0) {
temp.append(country);
result.add(0, temp.toString());
}
if (!variant.isEmpty() && (!language.isEmpty() || !country.isEmpty())) {
if (variant.length() > 0 && (language.length() > 0 || country.length() > 0)) {
temp.append('_').append(variant);
result.add(0, temp.toString());
}
@@ -564,13 +562,13 @@ public class ReloadableResourceBundleMessageSource extends AbstractResourceBased
*/
protected Properties loadProperties(Resource resource, String filename) throws IOException {
Properties props = newProperties();
try (InputStream inputStream = resource.getInputStream()) {
try (InputStream is = 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, inputStream);
this.propertiesPersister.loadFromXml(props, is);
}
else {
String encoding = null;
@@ -584,13 +582,13 @@ public class ReloadableResourceBundleMessageSource extends AbstractResourceBased
if (logger.isDebugEnabled()) {
logger.debug("Loading properties [" + resource.getFilename() + "] with encoding '" + encoding + "'");
}
this.propertiesPersister.load(props, new InputStreamReader(inputStream, encoding));
this.propertiesPersister.load(props, new InputStreamReader(is, encoding));
}
else {
if (logger.isDebugEnabled()) {
logger.debug("Loading properties [" + resource.getFilename() + "]");
}
this.propertiesPersister.load(props, inputStream);
this.propertiesPersister.load(props, is);
}
}
return props;
@@ -242,8 +242,8 @@ public class ThreadPoolTaskExecutor extends ExecutorConfigurationSupport
* @since 6.1.4
* @see #initiateShutdown()
*/
public void setStrictEarlyShutdown(boolean strictEarlyShutdown) {
this.strictEarlyShutdown = strictEarlyShutdown;
public void setStrictEarlyShutdown(boolean defaultEarlyShutdown) {
this.strictEarlyShutdown = defaultEarlyShutdown;
}
/**
@@ -29,7 +29,6 @@ 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;
@@ -380,15 +379,12 @@ 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();
@@ -730,27 +726,6 @@ 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 transient volatile ResolvableType superType;
private volatile ResolvableType superType;
@Nullable
private transient volatile ResolvableType[] interfaces;
private volatile ResolvableType[] interfaces;
@Nullable
private transient volatile ResolvableType[] generics;
private volatile ResolvableType[] generics;
@Nullable
private transient volatile Boolean unresolvableGenerics;
private volatile Boolean unresolvableGenerics;
/**
@@ -53,10 +53,8 @@ final class AnnotationTypeMapping {
private static final Log logger = LogFactory.getLog(AnnotationTypeMapping.class);
private static final Predicate<? super Annotation> isBeanValidationConstraint = annotation -> {
String name = annotation.annotationType().getName();
return (name.equals("jakarta.validation.Constraint") || name.equals("javax.validation.Constraint"));
};
private static final Predicate<? super Annotation> isBeanValidationConstraint = annotation ->
annotation.annotationType().getName().equals("jakarta.validation.Constraint");
/**
* Set used to track which convention-based annotation attribute overrides
@@ -19,12 +19,10 @@ 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;
@@ -98,9 +96,7 @@ public class SimpleAsyncTaskExecutor extends CustomizableThreadCreator
private boolean rejectTasksWhenLimitReached = false;
private final AtomicBoolean closed = new AtomicBoolean();
private boolean cancelled = false; // within activeThreads synchronization
private volatile boolean active = true;
/**
@@ -221,9 +217,7 @@ public class SimpleAsyncTaskExecutor extends CustomizableThreadCreator
/**
* Specify whether to reject tasks when the concurrency limit has been reached,
* throwing {@link TaskRejectedException} (which extends the common
* {@link java.util.concurrent.RejectedExecutionException})
* on any further execution attempts.
* throwing {@link TaskRejectedException} 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
@@ -276,7 +270,7 @@ public class SimpleAsyncTaskExecutor extends CustomizableThreadCreator
* @see #close()
*/
public boolean isActive() {
return !this.closed.get();
return this.active;
}
/**
@@ -309,20 +303,19 @@ public class SimpleAsyncTaskExecutor extends CustomizableThreadCreator
* @see #TIMEOUT_IMMEDIATE
* @see #doExecute(Runnable)
*/
@Deprecated(since = "5.3.16")
@Deprecated
@Override
public void execute(Runnable task, long startTimeout) {
Assert.notNull(task, "Runnable must not be null");
if (!isActive()) {
throw new TaskRejectedException(getClass().getSimpleName() + " is not active");
throw new TaskRejectedException(getClass().getSimpleName() + " has been closed already");
}
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, future));
doExecute(new TaskTrackingRunnable(taskToUse));
}
catch (Throwable ex) {
// Release concurrency permit if thread creation fails
@@ -332,7 +325,7 @@ public class SimpleAsyncTaskExecutor extends CustomizableThreadCreator
}
}
else if (this.activeThreads != null) {
doExecute(new TaskTrackingRunnable(taskToUse, future));
doExecute(new TaskTrackingRunnable(taskToUse));
}
else {
doExecute(taskToUse);
@@ -408,13 +401,11 @@ public class SimpleAsyncTaskExecutor extends CustomizableThreadCreator
*/
@Override
public void close() {
if (this.closed.compareAndSet(false, true)) {
if (this.active) {
this.active = false;
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);
}
@@ -428,7 +419,6 @@ public class SimpleAsyncTaskExecutor extends CustomizableThreadCreator
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
this.cancelled = true;
}
if (!this.cancelRemainingTasksOnClose) {
// Late interrupt for remaining tasks after timeout
@@ -439,15 +429,6 @@ 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,
@@ -484,12 +465,9 @@ public class SimpleAsyncTaskExecutor extends CustomizableThreadCreator
private final Runnable task;
private final @Nullable Future<?> future;
public TaskTrackingRunnable(Runnable task, @Nullable Future<?> future) {
public TaskTrackingRunnable(Runnable task) {
Assert.notNull(task, "Task must not be null");
this.task = task;
this.future = future;
}
@Override
@@ -498,10 +476,7 @@ public class SimpleAsyncTaskExecutor extends CustomizableThreadCreator
Thread thread = null;
if (threads != null) {
thread = Thread.currentThread();
synchronized (threads) {
checkCancelled(this.future);
threads.add(thread);
}
threads.add(thread);
}
try {
this.task.run();
@@ -509,7 +484,7 @@ public class SimpleAsyncTaskExecutor extends CustomizableThreadCreator
finally {
if (threads != null) {
threads.remove(thread);
if (closed.get()) {
if (!isActive()) {
synchronized (threads) {
if (threads.isEmpty()) {
threads.notify();
@@ -120,7 +120,6 @@ 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,6 +142,31 @@ 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);
}
/**
@@ -158,43 +183,16 @@ public class MimeType implements Comparable<MimeType>, Serializable {
checkToken(subtype);
this.type = type.toLowerCase(Locale.ROOT);
this.subtype = subtype.toLowerCase(Locale.ROOT);
this.parameters = createParametersMap(parameters);
if (this.parameters.containsKey(PARAM_CHARSET)) {
this.resolvedCharset = Charset.forName(unquote(this.parameters.get(PARAM_CHARSET)));
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);
}
}
/**
* 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)));
else {
this.parameters = Collections.emptyMap();
}
}
@@ -227,25 +225,16 @@ 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 (!isQuotedString(value)) {
if (PARAM_CHARSET.equals(parameter)) {
if (this.resolvedCharset == null) {
this.resolvedCharset = Charset.forName(unquote(value));
}
}
else if (!isQuotedString(value)) {
checkToken(value);
}
}
@@ -1335,22 +1335,6 @@ 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,13 +41,11 @@ import static org.assertj.core.api.Assertions.assertThat;
/**
* Test cases for {@link ResourceRegionEncoder} class.
*
* @author Brian Clozel
*/
class ResourceRegionEncoderTests extends AbstractLeakCheckingTests {
private final ResourceRegionEncoder encoder = new ResourceRegionEncoder();
private ResourceRegionEncoder encoder = new ResourceRegionEncoder();
@Test
void canEncode() {
@@ -118,7 +116,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(
@@ -140,7 +138,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,11 +16,8 @@
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;
@@ -30,7 +27,6 @@ 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;
@@ -90,11 +86,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
@@ -135,105 +131,6 @@ 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,9 +69,6 @@ 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)
@@ -84,7 +81,6 @@ 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 {@link #cleanupTransaction})
* (to be passed into cleanupTransaction)
* @throws jakarta.persistence.PersistenceException if thrown by JPA methods
* @see #cleanupTransaction
*/
@@ -117,7 +117,6 @@ 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,12 +107,21 @@ 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,18 +181,16 @@ public class EclipseLinkJpaDialect extends DefaultJpaDialect {
@Override
public Connection getConnection() {
Connection con = this.connection;
if (con == null) {
if (this.connection == null) {
transactionIsolationLock.lock();
try {
con = this.entityManager.unwrap(Connection.class);
this.connection = this.entityManager.unwrap(Connection.class);
}
finally {
transactionIsolationLock.unlock();
}
this.connection = con;
}
return con;
return this.connection;
}
}
@@ -29,7 +29,6 @@ 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;
@@ -305,6 +304,8 @@ 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",
@@ -322,13 +323,12 @@ 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,7 +342,8 @@ public abstract class AbstractTestContextBootstrapper implements TestContextBoot
TestPropertySourceUtils.buildMergedTestPropertySources(testClass);
MergedContextConfiguration mergedConfig = new MergedContextConfiguration(testClass,
StringUtils.toStringArray(locations), ClassUtils.toClassArray(classes),
initializers, ActiveProfilesUtils.resolveActiveProfiles(testClass),
ApplicationContextInitializerUtils.resolveInitializerClasses(configAttributesList),
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. At runtime, the local boundary may
* and characteristics, as managed by the aspect. Ar 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,8 +2056,9 @@ public class HttpHeaders implements MultiValueMap<String, String>, Serializable
return DATE_FORMATTER.format(time);
}
private static Set<String> toCaseInsensitiveSet(Set<String> originalSet) {
Set<String> deduplicatedSet = Collections.newSetFromMap(
final 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.
@@ -2074,7 +2075,6 @@ 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,7 +2092,6 @@ 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;
@@ -2128,7 +2127,6 @@ public class HttpHeaders implements MultiValueMap<String, String>, Serializable
}
}
private final class CaseInsensitiveEntry implements Map.Entry<String, List<String>> {
private final String key;
@@ -2149,9 +2147,10 @@ public class HttpHeaders implements MultiValueMap<String, String>, Serializable
@Override
public List<String> setValue(List<String> value) {
List<String> previous = Objects.requireNonNull(CaseInsensitiveEntrySet.this.headers.get(this.key));
List<String> previousValues = Objects.requireNonNull(
CaseInsensitiveEntrySet.this.headers.get(this.key));
CaseInsensitiveEntrySet.this.headers.put(this.key, value);
return previous;
return previousValues;
}
}
}
@@ -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, parameters);
super(other.getType(), other.getSubtype(), parameters);
}
/**
@@ -19,7 +19,6 @@ 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;
@@ -254,23 +253,16 @@ 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,6 +40,7 @@ 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.
@@ -47,7 +48,6 @@ import org.springframework.util.Assert;
* @author Sebastien Deleuze
* @author Arjen Poutsma
* @author Rossen Stoyanchev
* @author Brian Clozel
* @since 5.0
*/
public class ServerSentEventHttpMessageWriter implements HttpMessageWriter<Object> {
@@ -131,9 +131,8 @@ public class ServerSentEventHttpMessageWriter implements HttpMessageWriter<Objec
result = Flux.just(encodeText(sseText + "\n", mediaType, factory));
}
else if (data instanceof String text) {
StringBuilder sb = new StringBuilder(sseText);
writeStringData(text, sb);
result = Flux.just(encodeText(sb.toString(), mediaType, factory));
text = StringUtils.replace(text, "\n", "\ndata:");
result = Flux.just(encodeText(sseText + text + "\n\n", mediaType, factory));
}
else {
result = encodeEvent(sseText, data, dataType, mediaType, factory, hints);
@@ -143,31 +142,6 @@ 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,8 +99,7 @@ final class PartGenerator extends BaseSubscriber<MultipartParser.Token> {
sink.onCancel(generator);
sink.onRequest(l -> generator.requestToken());
tokens.doOnDiscard(MultipartParser.BodyToken.class, bodyToken -> DataBufferUtils.release(bodyToken.buffer()))
.subscribe(generator);
tokens.subscribe(generator);
});
}
@@ -254,14 +254,13 @@ 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) {
value += ";charset=" + defaultCharset.name();
contentTypeToUse = new MediaType(contentTypeToUse, defaultCharset);
}
}
headers.set(HttpHeaders.CONTENT_TYPE, value);
headers.setContentType(contentTypeToUse);
}
}
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,6 +207,7 @@ public class ServletServerHttpRequest implements ServerHttpRequest {
}
}
}
return this.headers;
}
@@ -18,7 +18,6 @@ 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;
@@ -119,31 +118,19 @@ public class ServletServerHttpResponse implements ServerHttpResponse {
this.servletResponse.addHeader(headerName, headerValue);
}
});
// HttpServletResponse exposes some headers as properties: we should include those if not already present
if (this.servletResponse.getContentType() == null && this.headers.containsKey(HttpHeaders.CONTENT_TYPE)) {
this.servletResponse.setContentType(this.headers.getFirst(HttpHeaders.CONTENT_TYPE));
MediaType contentTypeHeader = this.headers.getContentType();
if (this.servletResponse.getContentType() == null && contentTypeHeader != null) {
this.servletResponse.setContentType(contentTypeHeader.toString());
}
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
}
if (this.servletResponse.getCharacterEncoding() == null && contentTypeHeader != null &&
contentTypeHeader.getCharset() != null) {
this.servletResponse.setCharacterEncoding(contentTypeHeader.getCharset().name());
}
long contentLength = this.headers.getContentLength();
long contentLength = getHeaders().getContentLength();
if (contentLength != -1) {
this.servletResponse.setContentLengthLong(contentLength);
}
this.headersWritten = true;
}
}
@@ -50,9 +50,7 @@ import org.springframework.util.ReflectionUtils;
*/
class ServletServerHttpResponse extends AbstractListenerServerHttpResponse {
private static final boolean SERVLET61 =
(ReflectionUtils.findField(HttpServletResponse.class, "SC_PERMANENT_REDIRECT") != null);
private static final boolean IS_SERVLET61 = ReflectionUtils.findField(HttpServletResponse.class, "SC_PERMANENT_REDIRECT") != null;
private final HttpServletResponse response;
@@ -140,35 +138,31 @@ class ServletServerHttpResponse extends AbstractListenerServerHttpResponse {
}
protected void adaptHeaders(boolean removeAdaptedHeaders) {
HttpHeaders headers = getHeaders();
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());
}
// 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));
Charset charset = (contentType != null ? contentType.getCharset() : null);
if (this.response.getCharacterEncoding() == null && charset != null) {
this.response.setCharacterEncoding(charset.name());
}
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();
long contentLength = getHeaders().getContentLength();
if (contentLength != -1) {
this.response.setContentLengthLong(contentLength);
}
if (removeAdaptedHeaders) {
headers.remove(HttpHeaders.CONTENT_TYPE);
headers.remove(HttpHeaders.CONTENT_LENGTH);
getHeaders().remove(HttpHeaders.CONTENT_TYPE);
getHeaders().remove(HttpHeaders.CONTENT_LENGTH);
}
}
@@ -192,7 +186,7 @@ class ServletServerHttpResponse extends AbstractListenerServerHttpResponse {
cookie.setSecure(httpCookie.isSecure());
cookie.setHttpOnly(httpCookie.isHttpOnly());
if (httpCookie.isPartitioned()) {
if (SERVLET61) {
if (IS_SERVLET61) {
cookie.setAttribute("Partitioned", "");
}
else {
@@ -379,6 +373,7 @@ class ServletServerHttpResponse extends AbstractListenerServerHttpResponse {
private class ResponseBodyProcessor extends AbstractListenerWriteProcessor<DataBuffer> {
public ResponseBodyProcessor() {
super(request.getLogPrefix());
}
@@ -353,14 +353,17 @@ public interface RestClient {
ResponseSpec.ErrorHandler errorHandler);
/**
* 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.
* 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
* @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(String)
* @see jakarta.servlet.http.HttpServletResponse#setCharacterEncoding(String)
* @see jakarta.servlet.http.HttpServletRequest#setCharacterEncoding
* @see jakarta.servlet.http.HttpServletResponse#setCharacterEncoding
*/
public class CharacterEncodingFilter extends OncePerRequestFilter {
@@ -126,7 +126,7 @@ public final class UrlHandlerFilter extends OncePerRequestFilter {
public interface Builder {
/**
* Add a handler for URLs with a trailing slash.
* Add a handler for URL's 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 URLs with a trailing slash.
* Add a handler for URL's 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,11 +117,10 @@ public class StandardMultipartHttpServletRequest extends AbstractMultipartHttpSe
// MaxUploadSizeExceededException ?
Throwable cause = ex;
do {
String msg = cause.toString();
String msg = cause.getMessage();
if (msg != null) {
msg = msg.toLowerCase(Locale.ROOT);
if (((msg.contains("exceed") || msg.contains("limit")) &&
(msg.contains("size") || msg.contains("length") || msg.contains("count"))) ||
if ((msg.contains("exceed") && (msg.contains("size") || msg.contains("length"))) ||
(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 URIs based on RFC 3986 syntax.
* Parser for URI's 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
* typed URLs.
* types URL's.
*
* @author Arjen Poutsma
* @author Rossen Stoyanchev
@@ -787,7 +787,7 @@ public class UriComponentsBuilder implements UriBuilder, Cloneable {
public enum ParserType {
/**
* This parser type expects URIs to conform to RFC 3986 syntax.
* This parser type expects URI's 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 URLs that may not conform to RFC syntax.
* lenient handling of user typed URL's 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 URLs that may deviate from RFC syntax.
* lenient parsing of user typed URL's that may deviate from RFC syntax.
* Use this, via {@link UriComponentsBuilder.ParserType#WHAT_WG}, if you need to
* leniently handle URLs that don't confirm to RFC syntax, or for alignment
* leniently handle URL's that don't confirm to RFC syntax, or for alignment
* with browser behavior.
*
* <p>Comments in this class correlate to the parsing algorithm.
@@ -110,13 +110,12 @@ class ServerSentEventHttpMessageWriterTests extends AbstractDataBufferAllocating
super.bufferFactory = bufferFactory;
MockServerHttpResponse outputMessage = new MockServerHttpResponse(super.bufferFactory);
Flux<String> source = Flux.just("first\nsecond", "first\rsecond", "first\r\nsecond");
Flux<String> source = Flux.just("foo\nbar", "foo\nbaz");
testWrite(source, outputMessage, String.class);
StepVerifier.create(outputMessage.getBody())
.consumeNextWith(stringConsumer("data:first\ndata:second\n\n"))
.consumeNextWith(stringConsumer("data:first\ndata:second\n\n"))
.consumeNextWith(stringConsumer("data:first\ndata:second\n\n"))
.consumeNextWith(stringConsumer("data:foo\ndata:bar\n\n"))
.consumeNextWith(stringConsumer("data:foo\ndata:baz\n\n"))
.expectComplete()
.verify();
}
@@ -1,55 +0,0 @@
/*
* 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,38 +131,6 @@ 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();
@@ -192,21 +160,4 @@ 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 && (shouldValidateArguments() || shouldValidateReturnValue()) ?
this.validationGroups = (methodValidator != null ?
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 URLs
* <li>{@link UriBuilder} or {@link UriComponentsBuilder} -- for building URL's
* relative to the current request
* </ul>
*
@@ -47,7 +47,6 @@ 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;
@@ -172,13 +171,14 @@ public class ScriptTemplateView extends AbstractUrlBasedView {
*/
public void setResourceLoaderPath(String resourceLoaderPath) {
String[] paths = StringUtils.commaDelimitedListToStringArray(resourceLoaderPath);
this.resourceLoaderPaths = new String[paths.length];
this.resourceLoaderPaths = new String[paths.length + 1];
this.resourceLoaderPaths[0] = "";
for (int i = 0; i < paths.length; i++) {
String path = paths[i];
if (!path.endsWith("/") && !path.endsWith(":")) {
path = path + "/";
}
this.resourceLoaderPaths[i] = path;
this.resourceLoaderPaths[i + 1] = path;
}
}
@@ -307,26 +307,11 @@ public class ScriptTemplateView extends AbstractUrlBasedView {
@Nullable
protected Resource getResource(String location) {
String normalizedLocation = ResourceHandlerUtils.normalizeInputPath(location);
if (this.resourceLoaderPaths != null && !ResourceHandlerUtils.shouldIgnoreInputPath(normalizedLocation)) {
ApplicationContext context = obtainApplicationContext();
if (this.resourceLoaderPaths != null) {
for (String path : this.resourceLoaderPaths) {
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());
}
}
Resource resource = obtainApplicationContext().getResource(path + location);
if (resource.exists()) {
return resource;
}
}
}
@@ -59,6 +59,7 @@ class WebFluxViewResolutionIntegrationTests {
private static final MediaType TEXT_HTML_ISO_8859_1 = MediaType.parseMediaType("text/html;charset=ISO-8859-1");
@Nested
class FreeMarkerTests {
@@ -114,7 +115,6 @@ class WebFluxViewResolutionIntegrationTests {
}
}
@Configuration(proxyBeanMethods = false)
static class FreeMarkerWebFluxConfig extends AbstractWebFluxConfig {
@@ -131,7 +131,6 @@ class WebFluxViewResolutionIntegrationTests {
}
}
@Configuration(proxyBeanMethods = false)
static class ExplicitDefaultEncodingConfig extends AbstractWebFluxConfig {
@@ -149,7 +148,6 @@ class WebFluxViewResolutionIntegrationTests {
}
}
@Configuration(proxyBeanMethods = false)
static class ExplicitDefaultEncodingAndContentTypeConfig extends AbstractWebFluxConfig {
@@ -198,7 +196,6 @@ class WebFluxViewResolutionIntegrationTests {
}
}
@Controller
static class SampleController {
@@ -1,265 +0,0 @@
/*
* 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;
/** Whether to publish the context as a ServletContext attribute. */
/** Should we publish the context as a ServletContext attribute?. */
private boolean publishContext = true;
/** Whether to publish a ServletRequestHandledEvent at the end of each request. */
/** Should we publish a ServletRequestHandledEvent at the end of each request?. */
private boolean publishEvents = true;
/** Whether to expose LocaleContext and RequestAttributes as inheritable for child threads. */
/** Expose LocaleContext and RequestAttributes as inheritable for child threads?. */
private boolean threadContextInheritable = false;
/** Whether to dispatch an HTTP OPTIONS request to {@link #doService}. */
/** Should we dispatch an HTTP OPTIONS request to {@link #doService}?. */
private boolean dispatchOptionsRequest = false;
/** Whether to dispatch an HTTP TRACE request to {@link #doService}. */
/** Should we dispatch an HTTP TRACE request to {@link #doService}?. */
private boolean dispatchTraceRequest = false;
/** Whether to log potentially sensitive info (request params at DEBUG and headers at TRACE). */
/** Whether to log potentially sensitive info (request params at DEBUG + headers at TRACE). */
private boolean enableLoggingRequestDetails = false;
/** WebApplicationContext for this servlet. */
@Nullable
private WebApplicationContext webApplicationContext;
/** Whether the WebApplicationContext was injected via {@link #setApplicationContext}. */
/** If 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, in which case you must
* 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; 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
}
/**
* Set whether to log request parameters at DEBUG level and headers at TRACE level.
* Whether to log request params at DEBUG level, and headers at TRACE level.
* Both may contain sensitive information.
* <p>Defaults to {@code false} so that request details are not shown.
* <p>By default set 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 {@code GET} requests to processRequest/doService.
* Delegate 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 {@code POST} requests to {@link #processRequest}.
* Delegate POST requests to {@link #processRequest}.
* @see #doService
*/
@Override
@@ -915,7 +915,7 @@ public abstract class FrameworkServlet extends HttpServletBean implements Applic
}
/**
* Delegate {@code PUT} requests to {@link #processRequest}.
* Delegate PUT requests to {@link #processRequest}.
* @see #doService
*/
@Override
@@ -926,7 +926,7 @@ public abstract class FrameworkServlet extends HttpServletBean implements Applic
}
/**
* Delegate {@code DELETE} requests to {@link #processRequest}.
* Delegate DELETE requests to {@link #processRequest}.
* @see #doService
*/
@Override
@@ -937,7 +937,7 @@ public abstract class FrameworkServlet extends HttpServletBean implements Applic
}
/**
* Delegate {@code OPTIONS} requests to {@link #processRequest}, if desired.
* Delegate 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 {@code TRACE} requests to {@link #processRequest}, if desired.
* Delegate TRACE requests to {@link #processRequest}, if desired.
* <p>Applies HttpServlet's standard TRACE processing otherwise.
* @see #doService
*/
@@ -1171,9 +1171,7 @@ 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 {@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).
* receiving a centralized callback for GET, POST, PUT and DELETE.
* <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
* setters will simply be ignored.
* setter 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 the direct base class of Spring's full-fledged {@link DispatcherServlet}.
* as direct base class of Spring's full-fledged {@link DispatcherServlet}.
*
* @author Rod Johnson
* @author Juergen Hoeller
@@ -109,7 +109,6 @@ 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) {
@@ -121,7 +120,6 @@ 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() {
@@ -135,7 +133,6 @@ 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,7 +17,6 @@
package org.springframework.web.servlet.function;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.Collection;
import java.util.Set;
@@ -29,7 +28,6 @@ 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;
@@ -53,16 +51,15 @@ 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;
@@ -121,29 +118,14 @@ 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.containsKey(HttpHeaders.CONTENT_TYPE)) {
servletResponse.setContentType(this.headers.getFirst(HttpHeaders.CONTENT_TYPE));
if (servletResponse.getContentType() == null && this.headers.getContentType() != null) {
servletResponse.setContentType(this.headers.getContentType().toString());
}
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);
if (servletResponse.getCharacterEncoding() == null &&
this.headers.getContentType() != null &&
this.headers.getContentType().getCharset() != null) {
servletResponse.setCharacterEncoding(this.headers.getContentType().getCharset().name());
}
}
@@ -34,7 +34,6 @@ import org.springframework.web.servlet.ModelAndView;
/**
* Base class for {@link ServerResponse} implementations with error handling.
*
* @author Arjen Poutsma
* @since 5.3
*/
@@ -84,7 +83,6 @@ abstract class ErrorHandlingServerResponse implements ServerResponse {
return null;
}
private static class ErrorHandler<T extends ServerResponse> {
private final Predicate<Throwable> predicate;
@@ -275,11 +275,12 @@ public abstract class AbstractMessageConverterMethodProcessor extends AbstractMe
"No converter found for return value of type: " + valueType);
}
List<MediaType> compatibleMediaTypes = determineCompatibleMediaTypes(acceptableTypes, producibleTypes);
List<MediaType> compatibleMediaTypes = new ArrayList<>();
determineCompatibleMediaTypes(acceptableTypes, producibleTypes, compatibleMediaTypes);
// For ProblemDetail, fall back on RFC 9457 format
if (compatibleMediaTypes.isEmpty() && ProblemDetail.class.isAssignableFrom(valueType)) {
compatibleMediaTypes = determineCompatibleMediaTypes(this.problemMediaTypes, producibleTypes);
determineCompatibleMediaTypes(this.problemMediaTypes, producibleTypes, compatibleMediaTypes);
}
if (compatibleMediaTypes.isEmpty()) {
@@ -452,18 +453,16 @@ public abstract class AbstractMessageConverterMethodProcessor extends AbstractMe
return this.contentNegotiationManager.resolveMediaTypes(new ServletWebRequest(request));
}
private List<MediaType> determineCompatibleMediaTypes(
List<MediaType> acceptableTypes, List<MediaType> producibleTypes) {
private void determineCompatibleMediaTypes(
List<MediaType> acceptableTypes, List<MediaType> producibleTypes, List<MediaType> mediaTypesToUse) {
Set<MediaType> compatibleTypes = new LinkedHashSet<>();
for (MediaType requestedType : acceptableTypes) {
for (MediaType producibleType : producibleTypes) {
if (requestedType.isCompatibleWith(producibleType)) {
compatibleTypes.add(getMostSpecificMediaType(requestedType, producibleType));
mediaTypesToUse.add(getMostSpecificMediaType(requestedType, producibleType));
}
}
}
return new ArrayList<>(compatibleTypes);
}
/**
@@ -459,11 +459,6 @@ 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,7 +26,6 @@ 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;
@@ -196,20 +195,19 @@ public class SseEmitter extends ResponseBodyEmitter {
private final Set<DataWithMediaType> dataToSend = new LinkedHashSet<>(4);
private final StringBuilder sb = new StringBuilder();
@Nullable
private StringBuilder sb;
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;
@@ -223,7 +221,7 @@ public class SseEmitter extends ResponseBodyEmitter {
@Override
public SseEventBuilder comment(String comment) {
append(':').append(StringUtils.replace(comment, "\n", "\n:")).append('\n');
append(':').append(comment).append('\n');
return this;
}
@@ -238,53 +236,27 @@ public class SseEmitter extends ResponseBodyEmitter {
name(mav.getViewName());
}
append("data:");
saveAppendedText(TEXT_PLAIN);
saveAppendedText();
if (object instanceof String text) {
writeStringData(text, mediaType);
}
else {
this.dataToSend.add(new DataWithMediaType(object, mediaType));
object = StringUtils.replace(text, "\n", "\ndata:");
}
this.dataToSend.add(new DataWithMediaType(object, mediaType));
append('\n');
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");
}
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) {
if (this.sb == null) {
this.sb = new StringBuilder();
}
this.sb.append(text);
return this;
}
SseEventBuilderImpl append(char ch) {
if (this.sb == null) {
this.sb = new StringBuilder();
}
this.sb.append(ch);
return this;
}
@@ -295,14 +267,14 @@ public class SseEmitter extends ResponseBodyEmitter {
return Collections.emptySet();
}
append('\n');
saveAppendedText(TEXT_PLAIN);
saveAppendedText();
return this.dataToSend;
}
private void saveAppendedText(@Nullable MediaType mediaType) {
if (StringUtils.hasLength(this.sb)) {
this.dataToSend.add(new DataWithMediaType(this.sb.toString(), mediaType));
this.sb.setLength(0);
private void saveAppendedText() {
if (this.sb != null) {
this.dataToSend.add(new DataWithMediaType(this.sb.toString(), TEXT_PLAIN));
this.sb = null;
}
}
}
@@ -51,7 +51,6 @@ 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;
@@ -200,13 +199,14 @@ public class ScriptTemplateView extends AbstractUrlBasedView {
*/
public void setResourceLoaderPath(String resourceLoaderPath) {
String[] paths = StringUtils.commaDelimitedListToStringArray(resourceLoaderPath);
this.resourceLoaderPaths = new String[paths.length];
this.resourceLoaderPaths = new String[paths.length + 1];
this.resourceLoaderPaths[0] = "";
for (int i = 0; i < paths.length; i++) {
String path = paths[i];
if (!path.endsWith("/") && !path.endsWith(":")) {
path = path + "/";
}
this.resourceLoaderPaths[i] = path;
this.resourceLoaderPaths[i + 1] = path;
}
}
@@ -352,26 +352,11 @@ public class ScriptTemplateView extends AbstractUrlBasedView {
@Nullable
protected Resource getResource(String location) {
String normalizedLocation = ResourceHandlerUtils.normalizeInputPath(location);
if (this.resourceLoaderPaths != null && !ResourceHandlerUtils.shouldIgnoreInputPath(normalizedLocation)) {
ApplicationContext context = obtainApplicationContext();
if (this.resourceLoaderPaths != null) {
for (String path : this.resourceLoaderPaths) {
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());
}
}
Resource resource = obtainApplicationContext().getResource(path + location);
if (resource.exists()) {
return resource;
}
}
}
@@ -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(String) encoding}
* {@link HttpServletResponse#setCharacterEncoding encoding}
* from the "media-type" and "encoding" output properties
* specified in the {@link Transformer}.
* @param model merged output Map (never {@code null})
@@ -24,8 +24,6 @@ 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;
@@ -785,24 +783,6 @@ 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,19 +22,14 @@ 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;
@@ -110,10 +105,9 @@ class SseEmitterTests {
this.handler.assertWriteCount(1);
}
@ParameterizedTest(name = "{1}")
@MethodSource("newLineCharacters")
void sendEventWithMultiline(String newLineChars, String description) throws Exception {
this.emitter.send(event().data("foo" + newLineChars + "bar" + newLineChars + "baz"));
@Test
void sendEventWithMultiline() throws Exception {
this.emitter.send(event().data("foo\nbar\nbaz"));
this.handler.assertSentObjectCount(3);
this.handler.assertObject(0, "data:", TEXT_PLAIN_UTF8);
this.handler.assertObject(1, "foo\ndata:bar\ndata:baz");
@@ -121,17 +115,6 @@ 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"));
@@ -154,28 +137,6 @@ 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 {
@@ -1,333 +0,0 @@
/*
* 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 {
}
}