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