Compare commits

...

84 Commits

Author SHA1 Message Date
Spring Builds 3540029931 Release v5.3.26 2023-03-20 09:52:18 +00:00
rstoyanchev eafe3afe11 Polishing and minor refactoring in HandlerMappingIntrospector
Closes gh-30128
2023-03-20 08:38:11 +00:00
Sam Brannen 26e0343c16 Improve diagnostics in SpEL for matches operator
Supplying a large regular expression to the `matches` operator in a
SpEL expression can result in errors that are not very helpful to the
user.

This commit improves the diagnostics in SpEL for the `matches` operator
by throwing a SpelEvaluationException with a meaningful error message
to better assist the user.

Closes gh-30145
2023-03-20 00:07:01 +01:00
Sam Brannen 4d5e7207f2 Improve diagnostics in SpEL for repeated text
Attempting to create repeated text in a SpEL expression using the
repeat operator can result in errors that are not very helpful to the
user.

This commit improves the diagnostics in SpEL for the repeat operator by
throwing a SpelEvaluationException with a meaningful error message in
order to better assist the user.

Closes gh-30143
2023-03-20 00:06:54 +01:00
Sam Brannen 430fc25aca Increase scope of regex pattern cache for the SpEL matches operator
Prior to this commit, the pattern cache for the SpEL `matches` operator
only applied to expressions such as the following where the same
`matches` operator is invoked multiple times with different input:

  "map.keySet().?[#this matches '.+xyz']"

The pattern cache did not apply to expressions such as the following
where the same pattern ('.+xyz') is used in multiple `matches`
operations:

  "foo matches '.+xyz' AND bar matches '.+xyz'"

This commit addresses this by moving the instance of the pattern cache
map from OperatorMatches to InternalSpelExpressionParser so that the
cache can be reused for all `matches` operations for the given parser.

Closes gh-30141
2023-03-20 00:06:46 +01:00
Sam Brannen 0882ca57d4 Polishing 2023-03-20 00:06:32 +01:00
Sam Brannen 94bbf85c0e Stop printing to System.out in SpEL tests 2023-03-20 00:06:24 +01:00
Juergen Hoeller 2c2ef12f68 Upgrade to Netty 4.1.90 and Checkstyle 10.9.1 2023-03-17 18:10:02 +01:00
Juergen Hoeller 120d512ff6 Polishing (backported from main) 2023-03-17 18:09:46 +01:00
Sam Brannen 3ddf183922 Update copyright headers 2023-03-17 14:51:13 +01:00
rstoyanchev 4c69bfd32f Upgrade to Reactor 2020.0.30
Closes gh-30116
2023-03-16 08:30:16 +00:00
Sam Brannen 41d71e9a7f Revise contribution
See gh-25316
2023-03-15 14:30:53 +01:00
mrcoffee77 36682b7ad2 Ensure methods declared in Object can be invoked on a JDK proxy in SpEL
This commit ensures that methods declared in java.lang.Object (such as
toString() can be invoked on a JDK proxy instance in a SpEL expression.

Closes gh-25316
2023-03-15 14:30:53 +01:00
Sam Brannen 30601a5014 Polishing 2023-03-15 14:30:53 +01:00
Brian Clozel f2371f5e7d Ignore quality factor when filtering out "*/*"
Prior to this commit, the `RequestedContentTypeResolverBuilder` would
create a `RequestedContentTypeResolver` that internally delegates to a
list of resolvers. Each resolver would either return the list of
requested media types, or a singleton list with the "*/*" media type; in
this case this signals that the resolver cannot find a specific media
type requested and that we should continue with the next resolver in the
list.

Media Types returned by resolvers can contain parameters, such as the
quality factor. If the HTTP client requests "*/*;q=0.8", the
`HeaderContentTypeResolver` will return this as a singleton list. While
this has been resolved from the request, such a media type should not be
selected over other media types that could be returned by other
resolvers.

This commit changes the `RequestedContentTypeResolverBuilder` so that it
does not select "*/*;q=0.8" as the requested media type, but instead
continues delegating to other resolvers in the list. This means we need
to remove the quality factor before comparing it to the "*/*" for
equality check.

Fixes gh-30121
2023-03-15 10:45:31 +01:00
rstoyanchev f0da099b12 Prefer request hostName and hostPort in ReactorServerHttpRequest
Backport of 682a4d53 and 9624ea39

Closes gh-29974
2023-03-14 07:02:21 +00:00
Brian Clozel d00fd4c502 Allow runtime compatibility with SnakeYaml 2.0
This commit ensures that SnakeYaml 2.0 is compatible at runtime with
Spring Framework 5.3.x with the `YamlProcessor` support.
The baseline version for SnakeYaml remains the same.

Closes gh-30097
2023-03-10 12:41:03 +01:00
Sébastien Deleuze 44a6d13cc0 Fix minor spacings in webflux docs
Closes gh-30095
2023-03-09 11:56:55 +01:00
Juergen Hoeller 284657355a Upgrade to Log4J 2.20, Tomcat 9.0.73, Jetty 9.4.51, Undertow 2.2.23 2023-03-08 17:44:10 +01:00
Juergen Hoeller 6a81ed3a50 Polishing 2023-03-08 17:43:35 +01:00
Brian Clozel 0a053cfccb Avoid lock contention in CaffeineCacheManager
Prior to this commit, using a dynamic `CaffeineCacheManager` would rely
on `ConcurrentHashMap#computeIfAbsent` for retrieving and creating cache
instances as needed. It turns out that using this method concurrently
can cause lock contention even when all known cache instances are
instantiated.

This commit avoids using this method if the cache instance already
exists and avoid storing `null` entries in the map. This change reduces
lock contention and the overall HashMap size in the non-dynamic case.

See gh-30066
Fixes gh-30085
2023-03-08 16:27:50 +01:00
Sam Brannen 22bb76d326 Revise documentation for @AspectJ argument name resolution algorithm
Closes gh-30057
2023-03-02 17:22:17 +01:00
Sam Brannen 4dc45d551c Update documentation for @AspectJ argument name resolution algorithm
Closes gh-30057
2023-03-01 17:29:50 +01:00
Sam Brannen e5d05ddfc3 Remove obsolete MetadataAwareAspectInstanceFactory Javadoc
We no longer have any JDK 5 related limitations imposed on us as was
the case when MetadataAwareAspectInstanceFactory was introduced; however,
MetadataAwareAspectInstanceFactory will remain as a specialized
sub-interface of AspectInstanceFactory.
2023-03-01 17:13:55 +01:00
Sam Brannen 44a5f8ec06 Add missing package-info.java file for autoproxy.target package 2023-03-01 17:13:45 +01:00
Sam Brannen 0a8bda40f4 Fix .gitignore pattern for Maven "target" folders
Rationale: changes in org.springframework.aop.framework.autoproxy.target
were previously ignored since the "target" package was ignored by the
previous "eager" pattern for "target" folders.
2023-03-01 17:13:32 +01:00
Radek Kraus 8a879c6fed Protect JMS connection creation against prepareConnection errors
This commit uses a local variable for the creation of a new JMS
Connection so that a rare failure in prepareConnection(...) does not
leave the connection field in a partially initialized state.

If such a JMSException occurs, the intermediary connection is closed.
This commit further defends against close() failures at that point,
by logging the close exception at DEBUG level. As a result, the original
JMSException is always re-thrown.

See gh-29116
Closes gh-30051
2023-02-28 16:35:36 +01:00
Sam Brannen 28d11aaf64 Polish contribution
See gh-30036
2023-02-27 16:47:09 +01:00
1993heqiang 02941127e1 Fix "Configuring a Global Date and Time Format" example
Closes gh-30036
2023-02-27 16:47:03 +01:00
Sam Brannen b1b24458c9 Polishing 2023-02-27 16:46:16 +01:00
Sébastien Deleuze 854b625be2 Add missing @Nullable annotations to LogMessage methods
Closes gh-30009
2023-02-24 17:51:52 +01:00
rstoyanchev a2b7a907ec Prefer local hostAddress in ReactorServerHttpRequest
Closes gh-28601
2023-02-23 16:40:38 +00:00
Brian Clozel 4f0a8911ca Fix CI image resource in pipeline
This commit fixes the CI image resource configuration so that it can be
checked autmatically by the pipeline.
This also updates various resources.
2023-02-23 09:44:09 +01:00
Sam Brannen 40fef7b232 Fix Javadoc for MockRestRequestMatchers 2023-02-22 11:09:03 +01:00
Sam Brannen 423134f64c Revise queryParam() and header() support in MockRestRequestMatchers
See gh-29953
See gh-29964
2023-02-22 11:08:05 +01:00
Sam Brannen 574c10d219 Polishing 2023-02-22 11:08:05 +01:00
Johnny Lim 2b4b947050 Fix Javadoc since for MockRestRequestMatchers queryParam() and header() (#29986)
See gh-29953
See gh-29964
2023-02-22 11:08:05 +01:00
Juergen Hoeller 4993b1090a Polishing 2023-02-22 11:08:05 +01:00
Juergen Hoeller 3adabf391f Consistent ordering of Resource methods (backported from main) 2023-02-15 12:35:41 +01:00
Juergen Hoeller 0026338c00 Consistent @Bean method return type for equivalence with XML example
Closes gh-29970
2023-02-14 16:57:35 +01:00
Juergen Hoeller cdea667e58 Test for request attribute visibility in FreeMarker (backported from main)
See gh-29787
2023-02-14 16:57:22 +01:00
Juergen Hoeller 66b1c0b4b0 Upgrade to Netty 4.1.89 2023-02-14 11:38:32 +01:00
Juergen Hoeller 6e42d36614 ASM upgrade for JDK 20/21 support (backported from main)
Closes gh-29966
2023-02-14 11:38:12 +01:00
Simon Baslé 7f2c93fa1f Allow MockRest to match header/queryParam value list with one Matcher
This commit adds a `header` variant and a `queryParam` variant to the
`MockRestRequestMatchers` API which take a single `Matcher` over the
list of values.

Contrary to the vararg variants, the whole list is evaluated and the
caller can choose the desired semantics using readily-available iterable
matchers like `everyItem`, `hasItems`, `hasSize`, `contains` or
`containsInAnyOrder`...

The fact that the previous variants don't strictly check the size of the
actual list == the number of provided matchers or expected values is
now documented in their respective javadocs.

See gh-29953
Closes gh-29964
2023-02-13 17:53:48 +01:00
Johnny Lim 4c351e811a Polish
See gh-29928
2023-02-09 09:55:19 +01:00
rstoyanchev 5b67dea506 Unwrap session before selecting stats counter
See gh-29375
2023-02-08 18:10:11 +00:00
Sam Brannen b9fe095f60 Clearly document that DataClassRowMapper supports Java records
Closes gh-29814
2023-02-07 19:09:47 +01:00
Sam Brannen 6b17014b5a Polish RowMapper tests 2023-02-07 19:09:32 +01:00
Sam Brannen 878246b09b Avoid confusing terminology in BeanPropertyRowMapper
Prior to this commit, the term "field" was sometimes used to refer to a
database column and sometimes used to refer to a bean property, which
lead to confusion in the Javadoc as well as within the code.

This commit addresses this by avoiding use of the term "field".
2023-02-07 19:07:40 +01:00
Sam Brannen 9067ccab2d Update copyright headers 2023-02-07 16:35:52 +01:00
Manthan Bhatt 4e00aece7a Restrict forwards in MockMvcWebConnection to 100
This change restricts the maximum number of forwards in MockMvcWebConnection to 100,
in case a forward is configured in a way that causes a loop. This is necessary in HtmlUnit
backed tests, unlike in classic MockMvc tests in which the forwards are not actually resolved.

See gh-29557
Closes gh-29866

Co-authored-by: Simon Baslé <sbasle@vmware.com>
2023-02-07 16:24:57 +01:00
Sam Brannen c9841f37b6 Revise Testcontainers examples based on feedback
Closes gh-29940
2023-02-07 15:57:23 +01:00
Johnny Lim 92d513e9fb Add MockMvc.multipart() Kotlin extensions with HttpMethod
See gh-28545
See gh-28631
Closes gh-29941
2023-02-07 15:12:27 +01:00
Sam Brannen 782ee34cb7 Update @DynamicPropertySource examples regarding changes in Testcontainers
Closes gh-29939
2023-02-07 13:38:50 +01:00
Sam Brannen 80c10fad92 Polish RowMapper tests 2023-02-03 18:06:10 +01:00
Sam Brannen 498a0d286f Clarify semantics of primitivesDefaultedForNullValue in BeanPropertyRowMapper
Closes gh-29923
2023-02-03 18:06:10 +01:00
Sam Brannen e9d45d1b49 Polish Javadoc for RowMappers 2023-02-03 18:01:07 +01:00
danu 0851b0f72d Release R2DBC connection when cleanup fails in transaction
When using R2dbcTransactionManager, connection will not be released if
it encounters error while doing `afterCleanup` steps. As `afterCleanup`
can use a database connection when doing `setAutoCommit(true)`, it can
fail under some conditions where the connection is not reliable.

This leads to the Connection not being released.

This commit ensures that inner steps of the `doCleanupAfterCompletion`
are protected against errors, logging the errors and continuing the
cleanup until the last step, which releases the connection.

Backport of commit e050c37
See gh-29703
Closes gh-29925

Co-authored-by: Simon Baslé <sbasle@vmware.com>
2023-02-03 17:43:22 +01:00
Brian Clozel 91d991e86c Rely only on Docker Hub for fetching OCI images 2023-02-02 21:47:19 +01:00
Juergen Hoeller 960f6fb936 Add missing warn level check (backported from main) 2023-02-01 18:28:20 +01:00
Juergen Hoeller 4d3d34528e Upgrade to Tomcat 9.0.71, Netty 4.1.87, AssertJ 3.24.2, HtmlUnit 2.70, Checkstyle 10.7 2023-02-01 18:26:00 +01:00
Juergen Hoeller 78ba946266 Clarify postProcessBeanFactory lifecycle state
Closes gh-29064

(cherry picked from commit b8827d8e11)
2023-02-01 18:21:16 +01:00
Juergen Hoeller 42e7318cbb Polishing 2023-01-31 16:48:36 +01:00
Juergen Hoeller 000383fbff Explicit target ClassLoader for interface-based infrastructure proxies
Includes direct JDK Proxy usage instead of ProxyFactory where possible.

Closes gh-29913

(cherry picked from commit 4d6249811e)
2023-01-31 16:48:27 +01:00
Juergen Hoeller 37cbdc2cf4 Lazily load ContextLoader.properties (and lazily fail if not present)
Closes gh-29905

(cherry picked from commit a74b86e812)
2023-01-31 16:21:45 +01:00
Juergen Hoeller 6d95e7f9d9 Declare no-op close() method in order to avoid container-triggered shutdown call
Closes gh-29892

(cherry picked from commit 7c9dca3d2e)
2023-01-31 16:21:39 +01:00
rstoyanchev 45a7917a36 Backport of cf9fc69d6b
Closes gh-29911
2023-01-31 11:55:32 +00:00
rstoyanchev 25b95b8b98 Avoid ClassLoader issue in Jetty10 WebSocket upgrade
Closes gh-29256
2023-01-31 11:50:22 +00:00
Sam Brannen 6bd7a7321e Revise generated default name for @JmsListener subscription
The previous commit changed the generated default name for a JMS
subscription to <FQCN>#<method name> -- for example:

- org.example.MyListener#myListenerMethod

However, the JMS spec does not guarantee that '#' is a supported
character. This commit therefore changes '#' to '.' as the separator
between the class name and method name -- for example:

- org.example.MyListener.myListenerMethod

This commit also introduces tests and documentation for these changes.

See gh-29902
2023-01-30 19:57:28 +01:00
Sam Brannen 11aaba9877 Polishing 2023-01-30 19:52:24 +01:00
fml2 07fd7606e7 Improve generated default name for a @JmsListener subscription
Prior to this commit, when using durable subscribers with @JmsListener
methods that do not specify a custom subscription name the generated
default subscription name was always
org.springframework.jms.listener.adapter.MessagingMessageListenerAdapter.
Consequently, multiple such @JmsListener methods were assigned the
same subscription name which violates the uniqueness requirement.

To address this, MessagingMessageListenerAdapter now implements
SubscriptionNameProvider and generates the subscription name based on
the following rules.

- if the InvocableHandlerMethod is present, the subscription name will
  take the form of handlerMethod.getBeanType().getName() + "#" +
  handlerMethod.getMethod().getName().
- otherwise, getClass().getName() is used, which is analogous to the
  previous behavior.

Closes gh-29902
2023-01-30 19:52:15 +01:00
Sam Brannen 40d2466334 Fix build due to Jackson Javadoc publication changes
The Jackson project no longer publishes Javadoc at
https://fasterxml.github.io which breaks the `javadoc` and `api` build
tasks due to their dependency on that web site for external Javadoc links.

As a workaround, we now reference Jackson's Javadoc via
https://www.javadoc.io.

See https://github.com/FasterXML/jackson-databind/issues/3440
Closes gh-29895
2023-01-29 12:43:11 +01:00
Sam Brannen 3d6d853bbc Include all Hibernate methods in SharedEntityManagerCreator's queryTerminatingMethods
Prior to this commit, we included Hibernate's Query.list() method in
SharedEntityManagerCreator's queryTerminatingMethods set but did not
include all of Hibernate's query-terminating methods.

To address this, this commit additionally includes the stream(),
uniqueResult(), and uniqueResultOptional() methods from Hibernate's
Query API in SharedEntityManagerCreator's query-terminating methods set.

Closes gh-29888
2023-01-28 20:59:46 +01:00
Sam Brannen 90ea39cc6b Update copyright headers 2023-01-28 20:49:13 +01:00
Arjen Poutsma 21c3d4f4a9 Support Jetty 10 in JettyClientHttpRequest
Though Jetty 10 was previously supported in the JettyClientHttpResponse,
this commit ensures support in the JettyClientHttpRequest.

Closes gh-29867
See gh-26123
2023-01-26 17:27:57 +01:00
Simon Baslé de53d77344 DatabaseClient uses SQL Supplier more lazily
This commit modifies the `DefaultDatabaseClient` implementation in order
to ensure lazier usage of the `Supplier<String>` passed to the sql
method (`DatabaseClient#sql(Supplier)`).

Since technically `DatabaseClient` is an interface that could have 3rd
party implementations, the lazyness expectation is only hinted at in the
`DatabaseClient#sql` javadoc.

Possible caveat: some log statements attempt to reflect the now lazily
resolved SQL string. Similarly, some exceptions can capture the SQL that
caused the issue if known. We expect that these always occur after the
execution of the statement has been attempted (see `ResultFunction`).
At this point the SQL string will be accessible and logs and exceptions
should reflect it as before. Keep an eye out for such strings turning
into `null` after this change, which would indicate the opposite.

Backport of d72df5ace4
See gh-29367
Closes gh-29887
2023-01-26 15:20:38 +01:00
Sébastien Deleuze e4e90bbec0 Polish RouterFunctionDsl KDoc 2023-01-23 13:27:34 +01:00
Sébastien Deleuze d0828be0cd Remove WebClientIntegrationTests#exchangeWithRelativeUrl outdated test
This test can fail when a web server runs on port 80 and
is not relevant anymore due to the removal of related feature
via 6e936a4081.

Closes gh-29863
2023-01-20 12:24:41 +01:00
Sébastien Deleuze 4a4b332709 Refine Jackson2ObjectMapperBuilder#configureFeature exception handling
This commit changes the FatalBeanException previously thrown for
an IllegalArgumentException which seems more suitable for that
use case.

Closes gh-29860
2023-01-20 10:15:49 +01:00
Brian Clozel 33f1c9b614 Upgrade Gradle Enterprise & Conventions plugins
This is required to adapt to the repo.spring.io permission changes.
2023-01-19 11:34:05 +01:00
Arjen Poutsma 53ac812fb0 Updated sdkmanrc 2023-01-19 10:05:36 +01:00
Arjen Poutsma 60c89dd2df Fix IllegalStateException in empty ProducesRequestCondition
When comparing empty ProducesRequestCondition, compareTo would throw an
IllegalStateException if the Accept header was invalid. This commit
fixes that behavior.

See gh-29794
Closes gh-29836
2023-01-18 10:45:12 +01:00
Minsoo Cheong(Merlin) 4bd2531774 Fix R2dbcTransactionManager debug log: don't log a Mono (#29800)
When logging the current connection inside R2dbcTransactionManager
doBegin, the mono object was logged instead of the connection lambda
parameter.

Other similar debug-level logs do use the actual Connection object,
so this commit does the same.

Backport of gh-29800
Closes gh-29824
2023-01-16 11:30:35 +01:00
Spring Builds 4fd5630700 Next development version (v5.3.26-SNAPSHOT) 2023-01-11 11:19:35 +00:00
126 changed files with 2156 additions and 1140 deletions
+1
View File
@@ -28,6 +28,7 @@ buildSrc/build
/integration-tests/build
/src/asciidoc/build
target/
/target/
# Eclipse artifacts, including WTP generated manifests
.classpath
+1 -1
View File
@@ -1,3 +1,3 @@
# Enable auto-env through the sdkman_auto_env config
# Add key=value pairs of SDKs to use below
java=8.0.352-librca
java=8.0.362-librca
+15 -15
View File
@@ -28,18 +28,18 @@ configure(allprojects) { project ->
dependencyManagement {
imports {
mavenBom "com.fasterxml.jackson:jackson-bom:2.12.7"
mavenBom "io.netty:netty-bom:4.1.86.Final"
mavenBom "io.projectreactor:reactor-bom:2020.0.27"
mavenBom "io.netty:netty-bom:4.1.90.Final"
mavenBom "io.projectreactor:reactor-bom:2020.0.30"
mavenBom "io.r2dbc:r2dbc-bom:Arabba-SR13"
mavenBom "io.rsocket:rsocket-bom:1.1.3"
mavenBom "org.eclipse.jetty:jetty-bom:9.4.50.v20221201"
mavenBom "org.eclipse.jetty:jetty-bom:9.4.51.v20230217"
mavenBom "org.jetbrains.kotlin:kotlin-bom:1.5.32"
mavenBom "org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.5.2"
mavenBom "org.jetbrains.kotlinx:kotlinx-serialization-bom:1.2.2"
mavenBom "org.junit:junit-bom:5.8.2"
}
dependencies {
dependencySet(group: 'org.apache.logging.log4j', version: '2.19.0') {
dependencySet(group: 'org.apache.logging.log4j', version: '2.20.0') {
entry 'log4j-api'
entry 'log4j-core'
entry 'log4j-jul'
@@ -92,7 +92,7 @@ configure(allprojects) { project ->
entry 'jibx-run'
}
dependency "org.ogce:xpp3:1.1.6"
dependency "org.yaml:snakeyaml:1.30"
dependency "org.yaml:snakeyaml:1.33"
dependency "com.h2database:h2:2.1.214"
dependency "com.github.ben-manes.caffeine:caffeine:2.9.3"
@@ -128,18 +128,18 @@ configure(allprojects) { project ->
dependency "org.webjars:webjars-locator-core:0.48"
dependency "org.webjars:underscorejs:1.8.3"
dependencySet(group: 'org.apache.tomcat', version: '9.0.70') {
dependencySet(group: 'org.apache.tomcat', version: '9.0.73') {
entry 'tomcat-util'
entry('tomcat-websocket') {
exclude group: "org.apache.tomcat", name: "tomcat-servlet-api"
exclude group: "org.apache.tomcat", name: "tomcat-websocket-api"
}
}
dependencySet(group: 'org.apache.tomcat.embed', version: '9.0.70') {
dependencySet(group: 'org.apache.tomcat.embed', version: '9.0.73') {
entry 'tomcat-embed-core'
entry 'tomcat-embed-websocket'
}
dependencySet(group: 'io.undertow', version: '2.2.22.Final') {
dependencySet(group: 'io.undertow', version: '2.2.23.Final') {
entry 'undertow-core'
entry('undertow-servlet') {
exclude group: "org.jboss.spec.javax.servlet", name: "jboss-servlet-api_4.0_spec"
@@ -191,7 +191,7 @@ configure(allprojects) { project ->
dependency "org.junit.support:testng-engine:1.0.4"
dependency "org.hamcrest:hamcrest:2.2"
dependency "org.awaitility:awaitility:3.1.6"
dependency "org.assertj:assertj-core:3.24.1"
dependency "org.assertj:assertj-core:3.24.2"
dependencySet(group: 'org.xmlunit', version: '2.9.0') {
entry 'xmlunit-assertj'
entry('xmlunit-matchers') {
@@ -206,10 +206,10 @@ configure(allprojects) { project ->
}
dependency "io.mockk:mockk:1.12.1"
dependency("net.sourceforge.htmlunit:htmlunit:2.69.0") {
dependency("net.sourceforge.htmlunit:htmlunit:2.70.0") {
exclude group: "commons-logging", name: "commons-logging"
}
dependency("org.seleniumhq.selenium:htmlunit-driver:2.67.0") {
dependency("org.seleniumhq.selenium:htmlunit-driver:2.70.0") {
exclude group: "commons-logging", name: "commons-logging"
}
dependency("org.seleniumhq.selenium:selenium-java:3.141.59") {
@@ -340,7 +340,7 @@ configure([rootProject] + javaProjects) { project ->
}
checkstyle {
toolVersion = "10.6.0"
toolVersion = "10.9.1"
configDirectory.set(rootProject.file("src/checkstyle"))
}
@@ -377,9 +377,9 @@ configure([rootProject] + javaProjects) { project ->
"https://www.eclipse.org/aspectj/doc/released/aspectj5rt-api/",
"https://www.ehcache.org/apidocs/2.10.4/",
"https://www.quartz-scheduler.org/api/2.3.0/",
"https://fasterxml.github.io/jackson-core/javadoc/2.10/",
"https://fasterxml.github.io/jackson-databind/javadoc/2.10/",
"https://fasterxml.github.io/jackson-dataformat-xml/javadoc/2.10/",
"https://www.javadoc.io/doc/com.fasterxml.jackson.core/jackson-core/2.12.7/",
"https://www.javadoc.io/doc/com.fasterxml.jackson.core/jackson-databind/2.12.7/",
"https://www.javadoc.io/doc/com.fasterxml.jackson.dataformat/jackson-dataformat-xml/2.12.7/",
"https://hc.apache.org/httpcomponents-client-5.1.x/current/httpclient5/apidocs/",
"https://projectreactor.io/docs/test/release/api/",
"https://junit.org/junit4/javadoc/4.13.2/",
-3
View File
@@ -8,7 +8,4 @@ milestone: "5.3.x"
build-name: "spring-framework"
pipeline-name: "spring-framework"
concourse-url: "https://ci.spring.io"
registry-mirror-host: docker.repo.spring.io
registry-mirror-username: ((artifactory-username))
registry-mirror-password: ((artifactory-password))
task-timeout: 1h00m
+11 -9
View File
@@ -23,11 +23,6 @@ anchors:
docker-resource-source: &docker-resource-source
username: ((docker-hub-username))
password: ((docker-hub-password))
tag: ((milestone))
registry-mirror-vars: &registry-mirror-vars
registry-mirror-host: ((registry-mirror-host))
registry-mirror-username: ((registry-mirror-username))
registry-mirror-password: ((registry-mirror-password))
slack-fail-params: &slack-fail-params
text: >
:concourse-failed: <https://ci.spring.io/teams/${BUILD_TEAM_NAME}/pipelines/${BUILD_PIPELINE_NAME}/jobs/${BUILD_JOB_NAME}/builds/${BUILD_NAME}|${BUILD_PIPELINE_NAME} ${BUILD_JOB_NAME} failed!>
@@ -48,31 +43,37 @@ resource_types:
- name: registry-image
type: registry-image
source:
<<: *docker-resource-source
repository: concourse/registry-image-resource
tag: 1.5.0
tag: 1.7.1
- name: artifactory-resource
type: registry-image
source:
<<: *docker-resource-source
repository: springio/artifactory-resource
tag: 0.0.17
tag: 0.0.18
- name: github-release
type: registry-image
source:
<<: *docker-resource-source
repository: concourse/github-release-resource
tag: 1.5.5
tag: 1.8.0
- name: github-status-resource
type: registry-image
source:
<<: *docker-resource-source
repository: dpb587/github-status-resource
tag: master
- name: pull-request
type: registry-image
source:
<<: *docker-resource-source
repository: teliaoss/github-pr-resource
tag: v0.23.0
- name: slack-notification
type: registry-image
source:
<<: *docker-resource-source
repository: cfcommunity/slack-notification-resource
tag: latest
resources:
@@ -101,6 +102,7 @@ resources:
source:
<<: *docker-resource-source
repository: ((docker-hub-organization))/spring-framework-ci
tag: ((milestone))
- name: artifactory-repo
type: artifactory-resource
icon: package-variant
@@ -168,7 +170,7 @@ jobs:
image: ci-image
vars:
ci-image-name: ci-image
<<: *registry-mirror-vars
<<: *docker-resource-source
- put: ci-image
params:
image: ci-image/image.tar
+3 -5
View File
@@ -4,11 +4,9 @@ image_resource:
type: registry-image
source:
repository: concourse/oci-build-task
tag: 0.9.1
registry_mirror:
host: ((registry-mirror-host))
username: ((registry-mirror-username))
password: ((registry-mirror-password))
tag: 0.10.0
username: ((docker-hub-username))
password: ((docker-hub-password))
inputs:
- name: ci-images-git-repo
outputs:
+1 -1
View File
@@ -1,4 +1,4 @@
version=5.3.25-SNAPSHOT
version=5.3.26
org.gradle.jvmargs=-Xmx2048m
org.gradle.caching=true
org.gradle.parallel=true
+2 -2
View File
@@ -7,8 +7,8 @@ pluginManagement {
}
plugins {
id "com.gradle.enterprise" version "3.11.1"
id "io.spring.ge.conventions" version "0.0.11"
id "com.gradle.enterprise" version "3.12.1"
id "io.spring.ge.conventions" version "0.0.13"
}
include "spring-aop"
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2016 the original author or authors.
* Copyright 2002-2023 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.
@@ -23,10 +23,6 @@ import org.springframework.lang.Nullable;
* Subinterface of {@link org.springframework.aop.aspectj.AspectInstanceFactory}
* that returns {@link AspectMetadata} associated with AspectJ-annotated classes.
*
* <p>Ideally, AspectInstanceFactory would include this method itself, but because
* AspectMetadata uses Java-5-only {@link org.aspectj.lang.reflect.AjType},
* we need to split out this subinterface.
*
* @author Rod Johnson
* @since 2.0
* @see AspectMetadata
@@ -35,13 +31,13 @@ import org.springframework.lang.Nullable;
public interface MetadataAwareAspectInstanceFactory extends AspectInstanceFactory {
/**
* Return the AspectJ AspectMetadata for this factory's aspect.
* Get the AspectJ AspectMetadata for this factory's aspect.
* @return the aspect metadata
*/
AspectMetadata getAspectMetadata();
/**
* Return the best possible creation mutex for this factory.
* Get the best possible creation mutex for this factory.
* @return the mutex object (may be {@code null} for no mutex to use)
* @since 4.3
*/
@@ -0,0 +1,10 @@
/**
* Various {@link org.springframework.aop.framework.autoproxy.TargetSourceCreator}
* implementations for use with Spring's AOP auto-proxying support.
*/
@NonNullApi
@NonNullFields
package org.springframework.aop.framework.autoproxy.target;
import org.springframework.lang.NonNullApi;
import org.springframework.lang.NonNullFields;
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2023 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.
@@ -247,7 +247,8 @@ public abstract class BeanUtils {
// A single public constructor
return (Constructor<T>) ctors[0];
}
else if (ctors.length == 0){
else if (ctors.length == 0) {
// No public constructors -> check non-public
ctors = clazz.getDeclaredConstructors();
if (ctors.length == 1) {
// A single non-public constructor, e.g. from a non-public record type
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2023 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.
@@ -25,6 +25,7 @@ import java.util.Set;
import org.springframework.beans.SimpleTypeConverter;
import org.springframework.beans.TypeConverter;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.config.BeanDefinitionHolder;
import org.springframework.beans.factory.config.DependencyDescriptor;
@@ -240,10 +241,11 @@ public class QualifierAnnotationAutowireCandidateResolver extends GenericTypeAwa
}
}
if (targetAnnotation == null) {
BeanFactory beanFactory = getBeanFactory();
// Look for matching annotation on the target class
if (getBeanFactory() != null) {
if (beanFactory != null) {
try {
Class<?> beanType = getBeanFactory().getType(bdHolder.getBeanName());
Class<?> beanType = beanFactory.getType(bdHolder.getBeanName());
if (beanType != null) {
targetAnnotation = AnnotationUtils.getAnnotation(ClassUtils.getUserClass(beanType), type);
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2021 the original author or authors.
* Copyright 2002-2023 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.
@@ -184,8 +184,9 @@ public abstract class YamlProcessor {
protected Yaml createYaml() {
LoaderOptions loaderOptions = new LoaderOptions();
loaderOptions.setAllowDuplicateKeys(false);
return new Yaml(new FilteringConstructor(loaderOptions), new Representer(),
new DumperOptions(), loaderOptions);
DumperOptions dumperOptions = new DumperOptions();
return new Yaml(new FilteringConstructor(loaderOptions), new Representer(dumperOptions),
dumperOptions, loaderOptions);
}
private boolean process(MatchCallback callback, Yaml yaml, Resource resource) {
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2023 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.
@@ -214,12 +214,15 @@ class DisposableBeanAdapter implements DisposableBean, Runnable, Serializable {
}
}
catch (Throwable ex) {
String msg = "Invocation of destroy method failed on bean with name '" + this.beanName + "'";
if (logger.isDebugEnabled()) {
logger.warn(msg, ex);
}
else {
logger.warn(msg + ": " + ex);
if (logger.isWarnEnabled()) {
String msg = "Invocation of destroy method failed on bean with name '" + this.beanName + "'";
if (logger.isDebugEnabled()) {
// Log at warn level like below but add the exception stacktrace only with debug level
logger.warn(msg, ex);
}
else {
logger.warn(msg + ": " + ex);
}
}
}
}
@@ -240,12 +243,15 @@ class DisposableBeanAdapter implements DisposableBean, Runnable, Serializable {
}
}
catch (Throwable ex) {
String msg = "Invocation of close method failed on bean with name '" + this.beanName + "'";
if (logger.isDebugEnabled()) {
logger.warn(msg, ex);
}
else {
logger.warn(msg + ": " + ex);
if (logger.isWarnEnabled()) {
String msg = "Invocation of close method failed on bean with name '" + this.beanName + "'";
if (logger.isDebugEnabled()) {
// Log at warn level like below but add the exception stacktrace only with debug level
logger.warn(msg, ex);
}
else {
logger.warn(msg + ": " + ex);
}
}
}
}
@@ -320,18 +326,23 @@ class DisposableBeanAdapter implements DisposableBean, Runnable, Serializable {
}
}
catch (InvocationTargetException ex) {
String msg = "Custom destroy method '" + this.destroyMethodName + "' on bean with name '" +
this.beanName + "' threw an exception";
if (logger.isDebugEnabled()) {
logger.warn(msg, ex.getTargetException());
}
else {
logger.warn(msg + ": " + ex.getTargetException());
if (logger.isWarnEnabled()) {
String msg = "Custom destroy method '" + this.destroyMethodName + "' on bean with name '" +
this.beanName + "' threw an exception";
if (logger.isDebugEnabled()) {
// Log at warn level like below but add the exception stacktrace only with debug level
logger.warn(msg, ex.getTargetException());
}
else {
logger.warn(msg + ": " + ex.getTargetException());
}
}
}
catch (Throwable ex) {
logger.warn("Failed to invoke custom destroy method '" + this.destroyMethodName +
"' on bean with name '" + this.beanName + "'", ex);
if (logger.isWarnEnabled()) {
logger.warn("Failed to invoke custom destroy method '" + this.destroyMethodName +
"' on bean with name '" + this.beanName + "'", ex);
}
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2023 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.
@@ -547,7 +547,8 @@ public class XmlBeanDefinitionReader extends AbstractBeanDefinitionReader {
* @see DefaultNamespaceHandlerResolver#DefaultNamespaceHandlerResolver(ClassLoader)
*/
protected NamespaceHandlerResolver createDefaultNamespaceHandlerResolver() {
ClassLoader cl = (getResourceLoader() != null ? getResourceLoader().getClassLoader() : getBeanClassLoader());
ResourceLoader resourceLoader = getResourceLoader();
ClassLoader cl = (resourceLoader != null ? resourceLoader.getClassLoader() : getBeanClassLoader());
return new DefaultNamespaceHandlerResolver(cl);
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2023 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.
@@ -395,6 +395,7 @@ class BeanWrapperTests extends AbstractPropertyAccessorTests {
}
@SuppressWarnings("try")
public static class ActiveResource implements AutoCloseable {
public ActiveResource getResource() {
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2023 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.
@@ -436,7 +436,6 @@ class DefaultListableBeanFactoryTests {
@Test
void empty() {
ListableBeanFactory lbf = new DefaultListableBeanFactory();
assertThat(lbf.getBeanDefinitionNames() != null).as("No beans defined --> array != null").isTrue();
assertThat(lbf.getBeanDefinitionNames().length == 0).as("No beans defined after no arg constructor").isTrue();
assertThat(lbf.getBeanDefinitionCount() == 0).as("No beans defined after no arg constructor").isTrue();
@@ -778,21 +777,6 @@ class DefaultListableBeanFactoryTests {
assertThat(factory.getType("child")).isEqualTo(DerivedTestBean.class);
}
@Test
void nameAlreadyBound() {
Properties p = new Properties();
p.setProperty("kerry.(class)", TestBean.class.getName());
p.setProperty("kerry.age", "35");
registerBeanDefinitions(p);
try {
registerBeanDefinitions(p);
}
catch (BeanDefinitionStoreException ex) {
assertThat(ex.getBeanName()).isEqualTo("kerry");
// expected
}
}
private void singleTestBean(ListableBeanFactory lbf) {
assertThat(lbf.getBeanDefinitionCount() == 1).as("1 beans defined").isTrue();
String[] names = lbf.getBeanDefinitionNames();
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2023 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.
@@ -51,6 +51,7 @@ import org.springframework.util.ObjectUtils;
* @author Juergen Hoeller
* @author Stephane Nicoll
* @author Sam Brannen
* @author Brian Clozel
* @since 4.3
* @see CaffeineCache
*/
@@ -188,8 +189,13 @@ public class CaffeineCacheManager implements CacheManager {
@Override
@Nullable
public Cache getCache(String name) {
return this.cacheMap.computeIfAbsent(name, cacheName ->
this.dynamic ? createCaffeineCache(cacheName) : null);
if (this.dynamic) {
Cache cache = this.cacheMap.get(name);
return (cache != null) ? cache : this.cacheMap.computeIfAbsent(name, this::createCaffeineCache);
}
else {
return this.cacheMap.get(name);
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2023 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.
@@ -132,10 +132,10 @@ public class JavaMailSenderImpl implements JavaMailSender {
}
/**
* Allow {code Map} access to the JavaMail properties of this sender,
* Allow {@code Map} access to the JavaMail properties of this sender,
* with the option to add or override specific entries.
* <p>Useful for specifying entries directly, for example via
* {code javaMailProperties[mail.smtp.auth]}.
* {@code javaMailProperties[mail.smtp.auth]}.
*/
public Properties getJavaMailProperties() {
return this.javaMailProperties;
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2021 the original author or authors.
* Copyright 2002-2023 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.
@@ -729,9 +729,11 @@ public abstract class AbstractApplicationContext extends DefaultResourceLoader
/**
* Modify the application context's internal bean factory after its standard
* initialization. All bean definitions will have been loaded, but no beans
* will have been instantiated yet. This allows for registering special
* BeanPostProcessors etc in certain ApplicationContext implementations.
* initialization. The initial definition resources will have been loaded but no
* post-processors will have run and no derived bean definitions will have been
* registered, and most importantly, no beans will have been instantiated yet.
* <p>This template method allows for registering special BeanPostProcessors
* etc in certain AbstractApplicationContext subclasses.
* @param beanFactory the bean factory used by the application context
*/
protected void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {
@@ -82,6 +82,17 @@ public abstract class AnnotationVisitor {
this.av = annotationVisitor;
}
/**
* The annotation visitor to which this visitor must delegate method calls. May be {@literal
* null}.
*
* @return the annotation visitor to which this visitor must delegate method calls, or {@literal
* null}.
*/
public AnnotationVisitor getDelegate() {
return av;
}
/**
* Visits a primitive value of the annotation.
*
@@ -194,7 +194,7 @@ public class ClassReader {
this.b = classFileBuffer;
// Check the class' major_version. This field is after the magic and minor_version fields, which
// use 4 and 2 bytes respectively.
if (checkClassVersion && readShort(classFileOffset + 6) > Opcodes.V19) {
if (checkClassVersion && readShort(classFileOffset + 6) > Opcodes.V21) {
throw new IllegalArgumentException(
"Unsupported class file major version " + readShort(classFileOffset + 6));
}
@@ -308,6 +308,7 @@ public class ClassReader {
* @return the content of the given input stream.
* @throws IOException if a problem occurs during reading.
*/
@SuppressWarnings("PMD.UseTryWithResources")
private static byte[] readStream(final InputStream inputStream, final boolean close)
throws IOException {
if (inputStream == null) {
@@ -376,7 +377,7 @@ public class ClassReader {
}
/**
* Returns the internal of name of the super class (see {@link Type#getInternalName()}). For
* Returns the internal name of the super class (see {@link Type#getInternalName()}). For
* interfaces, the super class is {@link Object}.
*
* @return the internal name of the super class, or {@literal null} for {@link Object} class.
@@ -42,7 +42,8 @@ public final class ClassTooLargeException extends IndexOutOfBoundsException {
/**
* Constructs a new {@link ClassTooLargeException}.
*
* @param className the internal name of the class.
* @param className the internal name of the class (see {@link
* org.objectweb.asm.Type#getInternalName()}).
* @param constantPoolCount the number of constant pool items of the class.
*/
public ClassTooLargeException(final String className, final int constantPoolCount) {
@@ -52,7 +53,7 @@ public final class ClassTooLargeException extends IndexOutOfBoundsException {
}
/**
* Returns the internal name of the class.
* Returns the internal name of the class (see {@link org.objectweb.asm.Type#getInternalName()}).
*
* @return the internal name of the class.
*/
@@ -81,6 +81,15 @@ public abstract class ClassVisitor {
this.cv = classVisitor;
}
/**
* The class visitor to which this visitor must delegate method calls. May be {@literal null}.
*
* @return the class visitor to which this visitor must delegate method calls, or {@literal null}.
*/
public ClassVisitor getDelegate() {
return cv;
}
/**
* Visits the header of the class.
*
@@ -155,7 +164,8 @@ public abstract class ClassVisitor {
* implicitly its own nest, so it's invalid to call this method with the visited class name as
* argument.
*
* @param nestHost the internal name of the host class of the nest.
* @param nestHost the internal name of the host class of the nest (see {@link
* Type#getInternalName()}).
*/
public void visitNestHost(final String nestHost) {
if (api < Opcodes.ASM7) {
@@ -167,14 +177,19 @@ public abstract class ClassVisitor {
}
/**
* Visits the enclosing class of the class. This method must be called only if the class has an
* enclosing class.
* Visits the enclosing class of the class. This method must be called only if this class is a
* local or anonymous class. See the JVMS 4.7.7 section for more details.
*
* @param owner internal name of the enclosing class of the class.
* @param owner internal name of the enclosing class of the class (see {@link
* Type#getInternalName()}).
* @param name the name of the method that contains the class, or {@literal null} if the class is
* not enclosed in a method of its enclosing class.
* not enclosed in a method or constructor of its enclosing class (e.g. if it is enclosed in
* an instance initializer, static initializer, instance variable initializer, or class
* variable initializer).
* @param descriptor the descriptor of the method that contains the class, or {@literal null} if
* the class is not enclosed in a method of its enclosing class.
* the class is not enclosed in a method or constructor of its enclosing class (e.g. if it is
* enclosed in an instance initializer, static initializer, instance variable initializer, or
* class variable initializer).
*/
public void visitOuterClass(final String owner, final String name, final String descriptor) {
if (cv != null) {
@@ -241,7 +256,7 @@ public abstract class ClassVisitor {
* the visited class is the host of a nest. A nest host is implicitly a member of its own nest, so
* it's invalid to call this method with the visited class name as argument.
*
* @param nestMember the internal name of a nest member.
* @param nestMember the internal name of a nest member (see {@link Type#getInternalName()}).
*/
public void visitNestMember(final String nestMember) {
if (api < Opcodes.ASM7) {
@@ -256,7 +271,8 @@ public abstract class ClassVisitor {
* Visits a permitted subclasses. A permitted subclass is one of the allowed subclasses of the
* current class.
*
* @param permittedSubclass the internal name of a permitted subclass.
* @param permittedSubclass the internal name of a permitted subclass (see {@link
* Type#getInternalName()}).
*/
public void visitPermittedSubclass(final String permittedSubclass) {
if (api < Opcodes.ASM9) {
@@ -269,15 +285,18 @@ public abstract class ClassVisitor {
/**
* Visits information about an inner class. This inner class is not necessarily a member of the
* class being visited.
* class being visited. More precisely, every class or interface C which is referenced by this
* class and which is not a package member must be visited with this method. This class must
* reference its nested class or interface members, and its enclosing class, if any. See the JVMS
* 4.7.6 section for more details.
*
* @param name the internal name of an inner class (see {@link Type#getInternalName()}).
* @param outerName the internal name of the class to which the inner class belongs (see {@link
* Type#getInternalName()}). May be {@literal null} for not member classes.
* @param innerName the (simple) name of the inner class inside its enclosing class. May be
* {@literal null} for anonymous inner classes.
* @param access the access flags of the inner class as originally declared in the enclosing
* class.
* @param name the internal name of C (see {@link Type#getInternalName()}).
* @param outerName the internal name of the class or interface C is a member of (see {@link
* Type#getInternalName()}). Must be {@literal null} if C is not the member of a class or
* interface (e.g. for local or anonymous classes).
* @param innerName the (simple) name of C. Must be {@literal null} for anonymous inner classes.
* @param access the access flags of C originally declared in the source code from which this
* class was compiled.
*/
public void visitInnerClass(
final String name, final String outerName, final String innerName, final int access) {
@@ -842,7 +842,7 @@ public class ClassWriter extends ClassVisitor {
* constant pool already contains a similar item. <i>This method is intended for {@link Attribute}
* sub classes, and is normally not needed by class generators or adapters.</i>
*
* @param value the internal name of the class.
* @param value the internal name of the class (see {@link Type#getInternalName()}).
* @return the index of a new or already existing class reference item.
*/
public int newClass(final String value) {
@@ -894,7 +894,8 @@ public class ClassWriter extends ClassVisitor {
* Opcodes#H_GETSTATIC}, {@link Opcodes#H_PUTFIELD}, {@link Opcodes#H_PUTSTATIC}, {@link
* Opcodes#H_INVOKEVIRTUAL}, {@link Opcodes#H_INVOKESTATIC}, {@link Opcodes#H_INVOKESPECIAL},
* {@link Opcodes#H_NEWINVOKESPECIAL} or {@link Opcodes#H_INVOKEINTERFACE}.
* @param owner the internal name of the field or method owner class.
* @param owner the internal name of the field or method owner class (see {@link
* Type#getInternalName()}).
* @param name the name of the field or method.
* @param descriptor the descriptor of the field or method.
* @return the index of a new or already existing method type reference item.
@@ -916,7 +917,8 @@ public class ClassWriter extends ClassVisitor {
* Opcodes#H_GETSTATIC}, {@link Opcodes#H_PUTFIELD}, {@link Opcodes#H_PUTSTATIC}, {@link
* Opcodes#H_INVOKEVIRTUAL}, {@link Opcodes#H_INVOKESTATIC}, {@link Opcodes#H_INVOKESPECIAL},
* {@link Opcodes#H_NEWINVOKESPECIAL} or {@link Opcodes#H_INVOKEINTERFACE}.
* @param owner the internal name of the field or method owner class.
* @param owner the internal name of the field or method owner class (see {@link
* Type#getInternalName()}).
* @param name the name of the field or method.
* @param descriptor the descriptor of the field or method.
* @param isInterface true if the owner is an interface.
@@ -978,7 +980,7 @@ public class ClassWriter extends ClassVisitor {
* constant pool already contains a similar item. <i>This method is intended for {@link Attribute}
* sub classes, and is normally not needed by class generators or adapters.</i>
*
* @param owner the internal name of the field's owner class.
* @param owner the internal name of the field's owner class (see {@link Type#getInternalName()}).
* @param name the field's name.
* @param descriptor the field's descriptor.
* @return the index of a new or already existing field reference item.
@@ -992,7 +994,8 @@ public class ClassWriter extends ClassVisitor {
* constant pool already contains a similar item. <i>This method is intended for {@link Attribute}
* sub classes, and is normally not needed by class generators or adapters.</i>
*
* @param owner the internal name of the method's owner class.
* @param owner the internal name of the method's owner class (see {@link
* Type#getInternalName()}).
* @param name the method's name.
* @param descriptor the method's descriptor.
* @param isInterface {@literal true} if {@code owner} is an interface.
@@ -1028,9 +1031,10 @@ public class ClassWriter extends ClassVisitor {
* currently being generated by this ClassWriter, which can of course not be loaded since it is
* under construction.
*
* @param type1 the internal name of a class.
* @param type2 the internal name of another class.
* @return the internal name of the common super class of the two given classes.
* @param type1 the internal name of a class (see {@link Type#getInternalName()}).
* @param type2 the internal name of another class (see {@link Type#getInternalName()}).
* @return the internal name of the common super class of the two given classes (see {@link
* Type#getInternalName()}).
*/
protected String getCommonSuperClass(final String type1, final String type2) {
ClassLoader classLoader = getClassLoader();
@@ -78,6 +78,15 @@ public abstract class FieldVisitor {
this.fv = fieldVisitor;
}
/**
* The field visitor to which this visitor must delegate method calls. May be {@literal null}.
*
* @return the field visitor to which this visitor must delegate method calls, or {@literal null}.
*/
public FieldVisitor getDelegate() {
return fv;
}
/**
* Visits an annotation of the field.
*
@@ -367,11 +367,12 @@ class Frame {
typeValue = REFERENCE_KIND | symbolTable.addType(internalName);
break;
default:
throw new IllegalArgumentException();
throw new IllegalArgumentException(
"Invalid descriptor fragment: " + buffer.substring(elementDescriptorOffset));
}
return ((elementDescriptorOffset - offset) << DIM_SHIFT) | typeValue;
default:
throw new IllegalArgumentException();
throw new IllegalArgumentException("Invalid descriptor: " + buffer.substring(offset));
}
}
@@ -65,7 +65,7 @@ public final class Handle {
* {@link Opcodes#H_INVOKESPECIAL}, {@link Opcodes#H_NEWINVOKESPECIAL} or {@link
* Opcodes#H_INVOKEINTERFACE}.
* @param owner the internal name of the class that owns the field or method designated by this
* handle.
* handle (see {@link Type#getInternalName()}).
* @param name the name of the field or method designated by this handle.
* @param descriptor the descriptor of the field or method designated by this handle.
* @deprecated this constructor has been superseded by {@link #Handle(int, String, String, String,
@@ -85,7 +85,7 @@ public final class Handle {
* {@link Opcodes#H_INVOKESPECIAL}, {@link Opcodes#H_NEWINVOKESPECIAL} or {@link
* Opcodes#H_INVOKEINTERFACE}.
* @param owner the internal name of the class that owns the field or method designated by this
* handle.
* handle (see {@link Type#getInternalName()}).
* @param name the name of the field or method designated by this handle.
* @param descriptor the descriptor of the field or method designated by this handle.
* @param isInterface whether the owner is an interface or not.
@@ -118,7 +118,8 @@ public final class Handle {
/**
* Returns the internal name of the class that owns the field or method designated by this handle.
*
* @return the internal name of the class that owns the field or method designated by this handle.
* @return the internal name of the class that owns the field or method designated by this handle
* (see {@link Type#getInternalName()}).
*/
public String getOwner() {
return owner;
@@ -44,7 +44,7 @@ public final class MethodTooLargeException extends IndexOutOfBoundsException {
/**
* Constructs a new {@link MethodTooLargeException}.
*
* @param className the internal name of the owner class.
* @param className the internal name of the owner class (see {@link Type#getInternalName()}).
* @param methodName the name of the method.
* @param descriptor the descriptor of the method.
* @param codeSize the size of the method's Code attribute, in bytes.
@@ -64,7 +64,7 @@ public final class MethodTooLargeException extends IndexOutOfBoundsException {
/**
* Returns the internal name of the owner class.
*
* @return the internal name of the owner class.
* @return the internal name of the owner class (see {@link Type#getInternalName()}).
*/
public String getClassName() {
return className;
@@ -94,6 +94,16 @@ public abstract class MethodVisitor {
this.mv = methodVisitor;
}
/**
* The method visitor to which this visitor must delegate method calls. May be {@literal null}.
*
* @return the method visitor to which this visitor must delegate method calls, or {@literal
* null}.
*/
public MethodVisitor getDelegate() {
return mv;
}
// -----------------------------------------------------------------------------------------------
// Parameters, annotations and non standard attributes
// -----------------------------------------------------------------------------------------------
@@ -120,7 +130,7 @@ public abstract class MethodVisitor {
* @return a visitor to the visit the actual default value of this annotation interface method, or
* {@literal null} if this visitor is not interested in visiting this default value. The
* 'name' parameters passed to the methods of this annotation visitor are ignored. Moreover,
* exacly one visit method must be called on this annotation visitor, followed by visitEnd.
* exactly one visit method must be called on this annotation visitor, followed by visitEnd.
*/
public AnnotationVisitor visitAnnotationDefault() {
if (mv != null) {
@@ -273,15 +283,17 @@ public abstract class MethodVisitor {
* @param type the type of this stack map frame. Must be {@link Opcodes#F_NEW} for expanded
* frames, or {@link Opcodes#F_FULL}, {@link Opcodes#F_APPEND}, {@link Opcodes#F_CHOP}, {@link
* Opcodes#F_SAME} or {@link Opcodes#F_APPEND}, {@link Opcodes#F_SAME1} for compressed frames.
* @param numLocal the number of local variables in the visited frame.
* @param numLocal the number of local variables in the visited frame. Long and double values
* count for one variable.
* @param local the local variable types in this frame. This array must not be modified. Primitive
* types are represented by {@link Opcodes#TOP}, {@link Opcodes#INTEGER}, {@link
* Opcodes#FLOAT}, {@link Opcodes#LONG}, {@link Opcodes#DOUBLE}, {@link Opcodes#NULL} or
* {@link Opcodes#UNINITIALIZED_THIS} (long and double are represented by a single element).
* Reference types are represented by String objects (representing internal names), and
* uninitialized types by Label objects (this label designates the NEW instruction that
* created this uninitialized value).
* @param numStack the number of operand stack elements in the visited frame.
* Reference types are represented by String objects (representing internal names, see {@link
* Type#getInternalName()}), and uninitialized types by Label objects (this label designates
* the NEW instruction that created this uninitialized value).
* @param numStack the number of operand stack elements in the visited frame. Long and double
* values count for one stack element.
* @param stack the operand stack types in this frame. This array must not be modified. Its
* content has the same format as the "local" array.
* @throws IllegalStateException if a frame is visited just after another one, without any
@@ -360,7 +372,7 @@ public abstract class MethodVisitor {
/**
* Visits a type instruction. A type instruction is an instruction that takes the internal name of
* a class as parameter.
* a class as parameter (see {@link Type#getInternalName()}).
*
* @param opcode the opcode of the type instruction to be visited. This opcode is either NEW,
* ANEWARRAY, CHECKCAST or INSTANCEOF.
@@ -552,12 +564,12 @@ public abstract class MethodVisitor {
/**
* Visits an IINC instruction.
*
* @param var index of the local variable to be incremented.
* @param varIndex index of the local variable to be incremented.
* @param increment amount to increment the local variable by.
*/
public void visitIincInsn(final int var, final int increment) {
public void visitIincInsn(final int varIndex, final int increment) {
if (mv != null) {
mv.visitIincInsn(var, increment);
mv.visitIincInsn(varIndex, increment);
}
}
@@ -643,8 +655,9 @@ public abstract class MethodVisitor {
* @param start the beginning of the exception handler's scope (inclusive).
* @param end the end of the exception handler's scope (exclusive).
* @param handler the beginning of the exception handler's code.
* @param type the internal name of the type of exceptions handled by the handler, or {@literal
* null} to catch any exceptions (for "finally" blocks).
* @param type the internal name of the type of exceptions handled by the handler (see {@link
* Type#getInternalName()}), or {@literal null} to catch any exceptions (for "finally"
* blocks).
* @throws IllegalArgumentException if one of the labels has already been visited by this visitor
* (by the {@link #visitLabel} method).
*/
@@ -284,6 +284,8 @@ public interface Opcodes {
int V17 = 0 << 16 | 61;
int V18 = 0 << 16 | 62;
int V19 = 0 << 16 | 63;
int V20 = 0 << 16 | 64;
int V21 = 0 << 16 | 65;
/**
* Version flag indicating that the class is using 'preview' features.
@@ -45,7 +45,7 @@ public abstract class RecordComponentVisitor {
/**
* The record visitor to which this visitor must delegate method calls. May be {@literal null}.
*/
/*package-private*/ RecordComponentVisitor delegate;
protected RecordComponentVisitor delegate;
/**
* Constructs a new {@link RecordComponentVisitor}.
@@ -83,7 +83,8 @@ public abstract class RecordComponentVisitor {
/**
* The record visitor to which this visitor must delegate method calls. May be {@literal null}.
*
* @return the record visitor to which this visitor must delegate method calls or {@literal null}.
* @return the record visitor to which this visitor must delegate method calls, or {@literal
* null}.
*/
public RecordComponentVisitor getDelegate() {
return delegate;
@@ -37,7 +37,7 @@ final class RecordComponentWriter extends RecordComponentVisitor {
/** The name_index field of the Record attribute. */
private final int nameIndex;
/** The descriptor_index field of the the Record attribute. */
/** The descriptor_index field of the Record attribute. */
private final int descriptorIndex;
/**
@@ -245,7 +245,7 @@ public final class Type {
/**
* Returns the {@link Type} corresponding to the given internal name.
*
* @param internalName an internal name.
* @param internalName an internal name (see {@link Type#getInternalName()}).
* @return the {@link Type} corresponding to the given internal name.
*/
public static Type getObjectType(final String internalName) {
@@ -708,8 +708,8 @@ public final class Type {
*
* @return the size of the arguments of the method (plus one for the implicit this argument),
* argumentsSize, and the size of its return value, returnSize, packed into a single int i =
* {@code (argumentsSize << 2) | returnSize} (argumentsSize is therefore equal to {@code
* i >> 2}, and returnSize to {@code i & 0x03}).
* {@code (argumentsSize &lt;&lt; 2) | returnSize} (argumentsSize is therefore equal to {@code
* i &gt;&gt; 2}, and returnSize to {@code i &amp; 0x03}).
*/
public int getArgumentsAndReturnSizes() {
return getArgumentsAndReturnSizes(getDescriptor());
@@ -721,8 +721,8 @@ public final class Type {
* @param methodDescriptor a method descriptor.
* @return the size of the arguments of the method (plus one for the implicit this argument),
* argumentsSize, and the size of its return value, returnSize, packed into a single int i =
* {@code (argumentsSize << 2) | returnSize} (argumentsSize is therefore equal to {@code
* i >> 2}, and returnSize to {@code i & 0x03}).
* {@code (argumentsSize &lt;&lt; 2) | returnSize} (argumentsSize is therefore equal to {@code
* i &gt;&gt; 2}, and returnSize to {@code i &amp; 0x03}).
*/
public static int getArgumentsAndReturnSizes(final String methodDescriptor) {
int argumentsSize = 1;
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2023 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.
@@ -22,12 +22,13 @@ import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* A simple log message type for use with Commons Logging, allowing
* for convenient lazy resolution of a given {@link Supplier} instance
* (typically bound to a Java 8 lambda expression) or a printf-style
* format string ({@link String#format}) in its {@link #toString()}.
* A simple log message type for use with Commons Logging, allowing for convenient
* lazy resolution of a given {@link Supplier} instance (typically bound to a lambda
* expression) or a printf-style format string ({@link String#format}) in its
* {@link #toString()}.
*
* @author Juergen Hoeller
* @author Sebastien Deleuze
* @since 5.2
* @see #of(Supplier)
* @see #format(String, Object)
@@ -77,7 +78,7 @@ public abstract class LogMessage implements CharSequence {
/**
* Build a lazily resolving message from the given supplier.
* @param supplier the supplier (typically bound to a Java 8 lambda expression)
* @param supplier the supplier (typically bound to a lambda expression)
* @see #toString()
*/
public static LogMessage of(Supplier<? extends CharSequence> supplier) {
@@ -87,63 +88,68 @@ public abstract class LogMessage implements CharSequence {
/**
* Build a lazily formatted message from the given format string and argument.
* @param format the format string (following {@link String#format} rules)
* @param arg1 the argument
* @param arg1 the argument (can be {@code null})
* @see String#format(String, Object...)
*/
public static LogMessage format(String format, Object arg1) {
public static LogMessage format(String format, @Nullable Object arg1) {
return new FormatMessage1(format, arg1);
}
/**
* Build a lazily formatted message from the given format string and arguments.
* @param format the format string (following {@link String#format} rules)
* @param arg1 the first argument
* @param arg2 the second argument
* @param arg1 the first argument (can be {@code null})
* @param arg2 the second argument (can be {@code null})
* @see String#format(String, Object...)
*/
public static LogMessage format(String format, Object arg1, Object arg2) {
public static LogMessage format(String format, @Nullable Object arg1, @Nullable Object arg2) {
return new FormatMessage2(format, arg1, arg2);
}
/**
* Build a lazily formatted message from the given format string and arguments.
* @param format the format string (following {@link String#format} rules)
* @param arg1 the first argument
* @param arg2 the second argument
* @param arg3 the third argument
* @param arg1 the first argument (can be {@code null})
* @param arg2 the second argument (can be {@code null})
* @param arg3 the third argument (can be {@code null})
* @see String#format(String, Object...)
*/
public static LogMessage format(String format, Object arg1, Object arg2, Object arg3) {
public static LogMessage format(String format, @Nullable Object arg1, @Nullable Object arg2, @Nullable Object arg3) {
return new FormatMessage3(format, arg1, arg2, arg3);
}
/**
* Build a lazily formatted message from the given format string and arguments.
* @param format the format string (following {@link String#format} rules)
* @param arg1 the first argument
* @param arg2 the second argument
* @param arg3 the third argument
* @param arg4 the fourth argument
* @param arg1 the first argument (can be {@code null})
* @param arg2 the second argument (can be {@code null})
* @param arg3 the third argument (can be {@code null})
* @param arg4 the fourth argument (can be {@code null})
* @see String#format(String, Object...)
*/
public static LogMessage format(String format, Object arg1, Object arg2, Object arg3, Object arg4) {
public static LogMessage format(String format, @Nullable Object arg1, @Nullable Object arg2, @Nullable Object arg3,
@Nullable Object arg4) {
return new FormatMessage4(format, arg1, arg2, arg3, arg4);
}
/**
* Build a lazily formatted message from the given format string and varargs.
* <p>This varargs {@code format()} variant may be costly. You should therefore
* use the individual argument variants whenever possible:
* {@link #format(String, Object)}, {@link #format(String, Object, Object)}, etc.
* @param format the format string (following {@link String#format} rules)
* @param args the varargs array (costly, prefer individual arguments)
* @param args the varargs array (can be {@code null} and can contain {@code null}
* elements)
* @see String#format(String, Object...)
*/
public static LogMessage format(String format, Object... args) {
public static LogMessage format(String format, @Nullable Object... args) {
return new FormatMessageX(format, args);
}
private static final class SupplierMessage extends LogMessage {
private Supplier<? extends CharSequence> supplier;
private final Supplier<? extends CharSequence> supplier;
SupplierMessage(Supplier<? extends CharSequence> supplier) {
Assert.notNull(supplier, "Supplier must not be null");
@@ -170,9 +176,10 @@ public abstract class LogMessage implements CharSequence {
private static final class FormatMessage1 extends FormatMessage {
@Nullable
private final Object arg1;
FormatMessage1(String format, Object arg1) {
FormatMessage1(String format, @Nullable Object arg1) {
super(format);
this.arg1 = arg1;
}
@@ -186,11 +193,13 @@ public abstract class LogMessage implements CharSequence {
private static final class FormatMessage2 extends FormatMessage {
@Nullable
private final Object arg1;
@Nullable
private final Object arg2;
FormatMessage2(String format, Object arg1, Object arg2) {
FormatMessage2(String format, @Nullable Object arg1, @Nullable Object arg2) {
super(format);
this.arg1 = arg1;
this.arg2 = arg2;
@@ -205,13 +214,16 @@ public abstract class LogMessage implements CharSequence {
private static final class FormatMessage3 extends FormatMessage {
@Nullable
private final Object arg1;
@Nullable
private final Object arg2;
@Nullable
private final Object arg3;
FormatMessage3(String format, Object arg1, Object arg2, Object arg3) {
FormatMessage3(String format, @Nullable Object arg1, @Nullable Object arg2, @Nullable Object arg3) {
super(format);
this.arg1 = arg1;
this.arg2 = arg2;
@@ -227,15 +239,20 @@ public abstract class LogMessage implements CharSequence {
private static final class FormatMessage4 extends FormatMessage {
@Nullable
private final Object arg1;
@Nullable
private final Object arg2;
@Nullable
private final Object arg3;
@Nullable
private final Object arg4;
FormatMessage4(String format, Object arg1, Object arg2, Object arg3, Object arg4) {
FormatMessage4(String format, @Nullable Object arg1, @Nullable Object arg2, @Nullable Object arg3,
@Nullable Object arg4) {
super(format);
this.arg1 = arg1;
this.arg2 = arg2;
@@ -252,9 +269,10 @@ public abstract class LogMessage implements CharSequence {
private static final class FormatMessageX extends FormatMessage {
@Nullable
private final Object[] args;
FormatMessageX(String format, Object... args) {
FormatMessageX(String format, @Nullable Object... args) {
super(format);
this.args = args;
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2023 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.
@@ -90,4 +90,9 @@ public class ExecutorServiceAdapter extends AbstractExecutorService {
return false;
}
// @Override on JDK 19
public void close() {
// no-op in order to avoid container-triggered shutdown call which would lead to exception logging
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2023 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.
@@ -64,7 +64,7 @@ public class InstanceFilter<T> {
/**
* Determine if the specified {code instance} matches this filter.
* Determine if the specified {@code instance} matches this filter.
*/
public boolean match(T instance) {
Assert.notNull(instance, "Instance to match must not be null");
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2023 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.
@@ -264,7 +264,15 @@ public enum SpelMessage {
/** @since 5.3.17 */
MAX_ARRAY_ELEMENTS_THRESHOLD_EXCEEDED(Kind.ERROR, 1075,
"Array declares too many elements, exceeding the threshold of ''{0}''");
"Array declares too many elements, exceeding the threshold of ''{0}''"),
/** @since 5.3.26 */
MAX_REPEATED_TEXT_SIZE_EXCEEDED(Kind.ERROR, 1076,
"Repeated text results in too many characters, exceeding the threshold of ''{0}''"),
/** @since 5.3.26 */
MAX_REGEX_LENGTH_EXCEEDED(Kind.ERROR, 1077,
"Regular expression contains too many characters, exceeding the threshold of ''{0}''");
private final Kind kind;
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2023 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.
@@ -144,7 +144,7 @@ public class FunctionReference extends SpelNodeImpl {
for (int i = 0; i < getChildCount(); i++) {
sj.add(getChild(i).toStringAST());
}
return '#' + this.name + sj.toString();
return '#' + this.name + sj;
}
/**
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2023 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.
@@ -263,7 +263,7 @@ public class MethodReference extends SpelNodeImpl {
for (int i = 0; i < getChildCount(); i++) {
sj.add(getChild(i).toStringAST());
}
return this.name + sj.toString();
return this.name + sj;
}
/**
@@ -288,12 +288,9 @@ public class MethodReference extends SpelNodeImpl {
if (executor.didArgumentConversionOccur()) {
return false;
}
Class<?> clazz = executor.getMethod().getDeclaringClass();
if (!Modifier.isPublic(clazz.getModifiers()) && executor.getPublicDeclaringClass() == null) {
return false;
}
return true;
Class<?> clazz = executor.getMethod().getDeclaringClass();
return (Modifier.isPublic(clazz.getModifiers()) || executor.getPublicDeclaringClass() != null);
}
@Override
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2023 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.
@@ -25,6 +25,8 @@ import org.springframework.expression.Operation;
import org.springframework.expression.TypedValue;
import org.springframework.expression.spel.CodeFlow;
import org.springframework.expression.spel.ExpressionState;
import org.springframework.expression.spel.SpelEvaluationException;
import org.springframework.expression.spel.SpelMessage;
import org.springframework.util.Assert;
import org.springframework.util.NumberUtils;
@@ -52,6 +54,13 @@ import org.springframework.util.NumberUtils;
*/
public class OpMultiply extends Operator {
/**
* Maximum number of characters permitted in repeated text.
* @since 5.3.26
*/
private static final int MAX_REPEATED_TEXT_SIZE = 256;
public OpMultiply(int startPos, int endPos, SpelNodeImpl... operands) {
super("*", startPos, endPos, operands);
}
@@ -109,10 +118,13 @@ public class OpMultiply extends Operator {
}
if (leftOperand instanceof String && rightOperand instanceof Integer) {
int repeats = (Integer) rightOperand;
StringBuilder result = new StringBuilder();
for (int i = 0; i < repeats; i++) {
result.append(leftOperand);
String text = (String) leftOperand;
int count = (Integer) rightOperand;
int requestedSize = text.length() * count;
checkRepeatedTextSize(requestedSize);
StringBuilder result = new StringBuilder(requestedSize);
for (int i = 0; i < count; i++) {
result.append(text);
}
return new TypedValue(result.toString());
}
@@ -120,6 +132,13 @@ public class OpMultiply extends Operator {
return state.operate(Operation.MULTIPLY, leftOperand, rightOperand);
}
private void checkRepeatedTextSize(int requestedSize) {
if (requestedSize > MAX_REPEATED_TEXT_SIZE) {
throw new SpelEvaluationException(getStartPosition(),
SpelMessage.MAX_REPEATED_TEXT_SIZE_EXCEEDED, MAX_REPEATED_TEXT_SIZE);
}
}
@Override
public boolean isCompilable() {
if (!getLeftOperand().isCompilable()) {
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2023 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.
@@ -36,19 +36,41 @@ import org.springframework.expression.spel.support.BooleanTypedValue;
*
* @author Andy Clement
* @author Juergen Hoeller
* @author Sam Brannen
* @since 3.0
*/
public class OperatorMatches extends Operator {
private static final int PATTERN_ACCESS_THRESHOLD = 1000000;
private final ConcurrentMap<String, Pattern> patternCache = new ConcurrentHashMap<>();
/**
* Maximum number of characters permitted in a regular expression.
* @since 5.3.26
*/
private static final int MAX_REGEX_LENGTH = 256;
private final ConcurrentMap<String, Pattern> patternCache;
/**
* Create a new {@link OperatorMatches} instance.
* @deprecated as of Spring Framework 5.3.26 in favor of invoking
* {@link #OperatorMatches(ConcurrentMap, int, int, SpelNodeImpl...)}
* with a shared pattern cache instead
*/
@Deprecated
public OperatorMatches(int startPos, int endPos, SpelNodeImpl... operands) {
super("matches", startPos, endPos, operands);
this(new ConcurrentHashMap<>(), startPos, endPos, operands);
}
/**
* Create a new {@link OperatorMatches} instance with a shared pattern cache.
* @since 5.3.26
*/
public OperatorMatches(ConcurrentMap<String, Pattern> patternCache, int startPos, int endPos, SpelNodeImpl... operands) {
super("matches", startPos, endPos, operands);
this.patternCache = patternCache;
}
/**
* Check the first operand matches the regex specified as the second operand.
@@ -62,26 +84,28 @@ public class OperatorMatches extends Operator {
public BooleanTypedValue getValueInternal(ExpressionState state) throws EvaluationException {
SpelNodeImpl leftOp = getLeftOperand();
SpelNodeImpl rightOp = getRightOperand();
String left = leftOp.getValue(state, String.class);
Object right = getRightOperand().getValue(state);
if (left == null) {
String input = leftOp.getValue(state, String.class);
if (input == null) {
throw new SpelEvaluationException(leftOp.getStartPosition(),
SpelMessage.INVALID_FIRST_OPERAND_FOR_MATCHES_OPERATOR, (Object) null);
}
Object right = rightOp.getValue(state);
if (!(right instanceof String)) {
throw new SpelEvaluationException(rightOp.getStartPosition(),
SpelMessage.INVALID_SECOND_OPERAND_FOR_MATCHES_OPERATOR, right);
}
String regex = (String) right;
try {
String rightString = (String) right;
Pattern pattern = this.patternCache.get(rightString);
Pattern pattern = this.patternCache.get(regex);
if (pattern == null) {
pattern = Pattern.compile(rightString);
this.patternCache.putIfAbsent(rightString, pattern);
checkRegexLength(regex);
pattern = Pattern.compile(regex);
this.patternCache.putIfAbsent(regex, pattern);
}
Matcher matcher = pattern.matcher(new MatcherInput(left, new AccessCount()));
Matcher matcher = pattern.matcher(new MatcherInput(input, new AccessCount()));
return BooleanTypedValue.forValue(matcher.matches());
}
catch (PatternSyntaxException ex) {
@@ -94,6 +118,13 @@ public class OperatorMatches extends Operator {
}
}
private void checkRegexLength(String regex) {
if (regex.length() > MAX_REGEX_LENGTH) {
throw new SpelEvaluationException(getStartPosition(),
SpelMessage.MAX_REGEX_LENGTH_EXCEEDED, MAX_REGEX_LENGTH);
}
}
private static class AccessCount {
@@ -111,7 +142,7 @@ public class OperatorMatches extends Operator {
private final CharSequence value;
private AccessCount access;
private final AccessCount access;
public MatcherInput(CharSequence value, AccessCount access) {
this.value = value;
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2023 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.
@@ -21,6 +21,8 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.Deque;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.regex.Pattern;
import org.springframework.expression.ParseException;
@@ -83,6 +85,7 @@ import org.springframework.util.StringUtils;
* @author Andy Clement
* @author Juergen Hoeller
* @author Phillip Webb
* @author Sam Brannen
* @since 3.0
*/
class InternalSpelExpressionParser extends TemplateAwareExpressionParser {
@@ -95,6 +98,9 @@ class InternalSpelExpressionParser extends TemplateAwareExpressionParser {
// For rules that build nodes, they are stacked here for return
private final Deque<SpelNodeImpl> constructedNodes = new ArrayDeque<>();
// Shared cache for compiled regex patterns
private final ConcurrentMap<String, Pattern> patternCache = new ConcurrentHashMap<>();
// The expression being parsed
private String expressionString = "";
@@ -248,7 +254,7 @@ class InternalSpelExpressionParser extends TemplateAwareExpressionParser {
}
if (tk == TokenKind.MATCHES) {
return new OperatorMatches(t.startPos, t.endPos, expr, rhExpr);
return new OperatorMatches(this.patternCache, t.startPos, t.endPos, expr, rhExpr);
}
Assert.isTrue(tk == TokenKind.BETWEEN, "Between token expected");
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2023 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.
@@ -48,6 +48,7 @@ import org.springframework.lang.Nullable;
* @author Andy Clement
* @author Juergen Hoeller
* @author Chris Beams
* @author Sam Brannen
* @since 3.0
* @see StandardEvaluationContext#addMethodResolver(MethodResolver)
*/
@@ -225,8 +226,7 @@ public class ReflectiveMethodResolver implements MethodResolver {
if (targetObject instanceof Class) {
Set<Method> result = new LinkedHashSet<>();
// Add these so that static methods are invocable on the type: e.g. Float.valueOf(..)
Method[] methods = getMethods(type);
for (Method method : methods) {
for (Method method : getMethods(type)) {
if (Modifier.isStatic(method.getModifiers())) {
result.add(method);
}
@@ -239,19 +239,23 @@ public class ReflectiveMethodResolver implements MethodResolver {
Set<Method> result = new LinkedHashSet<>();
// Expose interface methods (not proxy-declared overrides) for proper vararg introspection
for (Class<?> ifc : type.getInterfaces()) {
Method[] methods = getMethods(ifc);
for (Method method : methods) {
for (Method method : getMethods(ifc)) {
if (isCandidateForInvocation(method, type)) {
result.add(method);
}
}
}
// Ensure methods defined in java.lang.Object are exposed for JDK proxies.
for (Method method : getMethods(Object.class)) {
if (isCandidateForInvocation(method, type)) {
result.add(method);
}
}
return result;
}
else {
Set<Method> result = new LinkedHashSet<>();
Method[] methods = getMethods(type);
for (Method method : methods) {
for (Method method : getMethods(type)) {
if (isCandidateForInvocation(method, type)) {
result.add(method);
}
@@ -276,7 +280,7 @@ public class ReflectiveMethodResolver implements MethodResolver {
* Determine whether the given {@code Method} is a candidate for method resolution
* on an instance of the given target class.
* <p>The default implementation considers any method as a candidate, even for
* static methods sand non-user-declared methods on the {@link Object} base class.
* static methods and non-user-declared methods on the {@link Object} base class.
* @param method the Method to evaluate
* @param targetClass the concrete target class that is being introspected
* @since 4.3.15
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2021 the original author or authors.
* Copyright 2002-2023 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.
@@ -35,7 +35,7 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
*/
public abstract class AbstractExpressionTests {
private static final boolean DEBUG = false;
protected static final boolean DEBUG = false;
protected static final boolean SHOULD_BE_WRITABLE = true;
@@ -202,7 +202,9 @@ public abstract class AbstractExpressionTests {
protected void parseAndCheckError(String expression, SpelMessage expectedMessage, Object... otherProperties) {
assertThatExceptionOfType(SpelParseException.class).isThrownBy(() -> {
Expression expr = parser.parseExpression(expression);
SpelUtilities.printAbstractSyntaxTree(System.out, expr);
if (DEBUG) {
SpelUtilities.printAbstractSyntaxTree(System.out, expr);
}
}).satisfies(ex -> {
assertThat(ex.getMessageCode()).isEqualTo(expectedMessage);
if (otherProperties != null && otherProperties.length != 0) {
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2023 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.
@@ -127,17 +127,6 @@ class EvaluationTests extends AbstractExpressionTests {
evaluate("null?.null?.null", null, null);
}
@Test // SPR-16731
void matchesWithPatternAccessThreshold() {
String pattern = "^(?=[a-z0-9-]{1,47})([a-z0-9]+[-]{0,1}){1,47}[a-z0-9]{1}$";
String expression = "'abcde-fghijklmn-o42pasdfasdfasdf.qrstuvwxyz10x.xx.yyy.zasdfasfd' matches \'" + pattern + "\'";
Expression expr = parser.parseExpression(expression);
assertThatExceptionOfType(SpelEvaluationException.class)
.isThrownBy(expr::getValue)
.withCauseInstanceOf(IllegalStateException.class)
.satisfies(ex -> assertThat(ex.getMessageCode()).isEqualTo(SpelMessage.FLAWED_PATTERN));
}
// mixing operators
@Test
void mixingOperators() {
@@ -460,28 +449,49 @@ class EvaluationTests extends AbstractExpressionTests {
}
@Test
void relOperatorsMatches01() {
evaluate("'5.0067' matches '^-?\\d+(\\.\\d{2})?$'", "false", Boolean.class);
}
@Test
void relOperatorsMatches02() {
void matchesTrue() {
evaluate("'5.00' matches '^-?\\d+(\\.\\d{2})?$'", "true", Boolean.class);
}
@Test
void relOperatorsMatches03() {
void matchesFalse() {
evaluate("'5.0067' matches '^-?\\d+(\\.\\d{2})?$'", "false", Boolean.class);
}
@Test
void matchesWithInputConversion() {
evaluate("27 matches '^.*2.*$'", true, Boolean.class); // conversion int --> string
}
@Test
void matchesWithNullInput() {
evaluateAndCheckError("null matches '^.*$'", SpelMessage.INVALID_FIRST_OPERAND_FOR_MATCHES_OPERATOR, 0, null);
}
@Test
void relOperatorsMatches04() {
void matchesWithNullPattern() {
evaluateAndCheckError("'abc' matches null", SpelMessage.INVALID_SECOND_OPERAND_FOR_MATCHES_OPERATOR, 14, null);
}
@Test // SPR-16731
void matchesWithPatternAccessThreshold() {
String pattern = "^(?=[a-z0-9-]{1,47})([a-z0-9]+[-]{0,1}){1,47}[a-z0-9]{1}$";
String expression = "'abcde-fghijklmn-o42pasdfasdfasdf.qrstuvwxyz10x.xx.yyy.zasdfasfd' matches '" + pattern + "'";
evaluateAndCheckError(expression, SpelMessage.FLAWED_PATTERN);
}
@Test
void relOperatorsMatches05() {
evaluate("27 matches '^.*2.*$'", true, Boolean.class); // conversion int>string
void matchesWithPatternLengthThreshold() {
String pattern = "(0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789" +
"0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789" +
"01234567890123456789012345678901234567890123456789|abc)";
assertThat(pattern).hasSize(256);
Expression expr = parser.parseExpression("'abc' matches '" + pattern + "'");
assertThat(expr.getValue(context, Boolean.class)).isTrue();
pattern += "?";
assertThat(pattern).hasSize(257);
evaluateAndCheckError("'abc' matches '" + pattern + "'", Boolean.class, SpelMessage.MAX_REGEX_LENGTH_EXCEEDED);
}
}
@@ -1448,7 +1458,9 @@ class EvaluationTests extends AbstractExpressionTests {
private void expectFail(ExpressionParser parser, EvaluationContext eContext, String expressionString, SpelMessage messageCode) {
assertThatExceptionOfType(SpelEvaluationException.class).isThrownBy(() -> {
Expression e = parser.parseExpression(expressionString);
SpelUtilities.printAbstractSyntaxTree(System.out, e);
if (DEBUG) {
SpelUtilities.printAbstractSyntaxTree(System.out, e);
}
e.getValue(eContext);
}).satisfies(ex -> assertThat(ex.getMessageCode()).isEqualTo(messageCode));
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2021 the original author or authors.
* Copyright 2002-2023 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.
@@ -21,22 +21,25 @@ import java.math.BigInteger;
import org.junit.jupiter.api.Test;
import org.springframework.expression.Expression;
import org.springframework.expression.spel.ast.Operator;
import org.springframework.expression.spel.standard.SpelExpression;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.expression.spel.SpelMessage.MAX_REPEATED_TEXT_SIZE_EXCEEDED;
/**
* Tests the evaluation of expressions using relational operators.
* Tests the evaluation of expressions using various operators.
*
* @author Andy Clement
* @author Juergen Hoeller
* @author Giovanni Dall'Oglio Risso
* @author Sam Brannen
*/
class OperatorTests extends AbstractExpressionTests {
@Test
void testEqual() {
void equal() {
evaluate("3 == 5", false, Boolean.class);
evaluate("5 == 3", false, Boolean.class);
evaluate("6 == 6", true, Boolean.class);
@@ -85,7 +88,7 @@ class OperatorTests extends AbstractExpressionTests {
}
@Test
void testNotEqual() {
void notEqual() {
evaluate("3 != 5", true, Boolean.class);
evaluate("5 != 3", true, Boolean.class);
evaluate("6 != 6", false, Boolean.class);
@@ -134,7 +137,7 @@ class OperatorTests extends AbstractExpressionTests {
}
@Test
void testLessThan() {
void lessThan() {
evaluate("5 < 5", false, Boolean.class);
evaluate("3 < 5", true, Boolean.class);
evaluate("5 < 3", false, Boolean.class);
@@ -176,7 +179,7 @@ class OperatorTests extends AbstractExpressionTests {
}
@Test
void testLessThanOrEqual() {
void lessThanOrEqual() {
evaluate("3 <= 5", true, Boolean.class);
evaluate("5 <= 3", false, Boolean.class);
evaluate("6 <= 6", true, Boolean.class);
@@ -225,7 +228,7 @@ class OperatorTests extends AbstractExpressionTests {
}
@Test
void testGreaterThan() {
void greaterThan() {
evaluate("3 > 5", false, Boolean.class);
evaluate("5 > 3", true, Boolean.class);
evaluate("3L > 5L", false, Boolean.class);
@@ -266,7 +269,7 @@ class OperatorTests extends AbstractExpressionTests {
}
@Test
void testGreaterThanOrEqual() {
void greaterThanOrEqual() {
evaluate("3 >= 5", false, Boolean.class);
evaluate("5 >= 3", true, Boolean.class);
evaluate("6 >= 6", true, Boolean.class);
@@ -315,27 +318,22 @@ class OperatorTests extends AbstractExpressionTests {
}
@Test
void testIntegerLiteral() {
void integerLiteral() {
evaluate("3", 3, Integer.class);
}
@Test
void testRealLiteral() {
void realLiteral() {
evaluate("3.5", 3.5d, Double.class);
}
@Test
void testMultiplyStringInt() {
evaluate("'a' * 5", "aaaaa", String.class);
}
@Test
void testMultiplyDoubleDoubleGivesDouble() {
void multiplyDoubleDoubleGivesDouble() {
evaluate("3.0d * 5.0d", 15.0d, Double.class);
}
@Test
void testMixedOperandsBigDecimal() {
void mixedOperandsBigDecimal() {
evaluate("3 * new java.math.BigDecimal('5')", new BigDecimal("15"), BigDecimal.class);
evaluate("3L * new java.math.BigDecimal('5')", new BigDecimal("15"), BigDecimal.class);
evaluate("3.0d * new java.math.BigDecimal('5')", new BigDecimal("15.0"), BigDecimal.class);
@@ -361,19 +359,19 @@ class OperatorTests extends AbstractExpressionTests {
}
@Test
void testMathOperatorAdd02() {
void mathOperatorAdd02() {
evaluate("'hello' + ' ' + 'world'", "hello world", String.class);
}
@Test
void testMathOperatorsInChains() {
void mathOperatorsInChains() {
evaluate("1+2+3",6,Integer.class);
evaluate("2*3*4",24,Integer.class);
evaluate("12-1-2",9,Integer.class);
}
@Test
void testIntegerArithmetic() {
void integerArithmetic() {
evaluate("2 + 4", "6", Integer.class);
evaluate("5 - 4", "1", Integer.class);
evaluate("3 * 5", 15, Integer.class);
@@ -388,7 +386,7 @@ class OperatorTests extends AbstractExpressionTests {
}
@Test
void testPlus() {
void plus() {
evaluate("7 + 2", "9", Integer.class);
evaluate("3.0f + 5.0f", 8.0f, Float.class);
evaluate("3.0d + 5.0d", 8.0d, Double.class);
@@ -400,44 +398,44 @@ class OperatorTests extends AbstractExpressionTests {
evaluate("null + 'ab'", "nullab", String.class);
// AST:
SpelExpression expr = (SpelExpression)parser.parseExpression("+3");
SpelExpression expr = (SpelExpression) parser.parseExpression("+3");
assertThat(expr.toStringAST()).isEqualTo("+3");
expr = (SpelExpression)parser.parseExpression("2+3");
expr = (SpelExpression) parser.parseExpression("2+3");
assertThat(expr.toStringAST()).isEqualTo("(2 + 3)");
// use as a unary operator
evaluate("+5d",5d,Double.class);
evaluate("+5L",5L,Long.class);
evaluate("+5",5,Integer.class);
evaluate("+new java.math.BigDecimal('5')", new BigDecimal("5"),BigDecimal.class);
evaluateAndCheckError("+'abc'",SpelMessage.OPERATOR_NOT_SUPPORTED_BETWEEN_TYPES);
evaluate("+5d", 5d, Double.class);
evaluate("+5L", 5L, Long.class);
evaluate("+5", 5, Integer.class);
evaluate("+new java.math.BigDecimal('5')", new BigDecimal("5"), BigDecimal.class);
evaluateAndCheckError("+'abc'", SpelMessage.OPERATOR_NOT_SUPPORTED_BETWEEN_TYPES);
// string concatenation
evaluate("'abc'+'def'","abcdef",String.class);
evaluate("'abc'+'def'", "abcdef", String.class);
evaluate("5 + new Integer('37')",42,Integer.class);
evaluate("5 + new Integer('37')", 42, Integer.class);
}
@Test
void testMinus() {
void minus() {
evaluate("'c' - 2", "a", String.class);
evaluate("3.0f - 5.0f", -2.0f, Float.class);
evaluateAndCheckError("'ab' - 2", SpelMessage.OPERATOR_NOT_SUPPORTED_BETWEEN_TYPES);
evaluateAndCheckError("2-'ab'", SpelMessage.OPERATOR_NOT_SUPPORTED_BETWEEN_TYPES);
SpelExpression expr = (SpelExpression)parser.parseExpression("-3");
SpelExpression expr = (SpelExpression) parser.parseExpression("-3");
assertThat(expr.toStringAST()).isEqualTo("-3");
expr = (SpelExpression)parser.parseExpression("2-3");
expr = (SpelExpression) parser.parseExpression("2-3");
assertThat(expr.toStringAST()).isEqualTo("(2 - 3)");
evaluate("-5d",-5d,Double.class);
evaluate("-5L",-5L,Long.class);
evaluate("-5d", -5d, Double.class);
evaluate("-5L", -5L, Long.class);
evaluate("-5", -5, Integer.class);
evaluate("-new java.math.BigDecimal('5')", new BigDecimal("-5"),BigDecimal.class);
evaluate("-new java.math.BigDecimal('5')", new BigDecimal("-5"), BigDecimal.class);
evaluateAndCheckError("-'abc'", SpelMessage.OPERATOR_NOT_SUPPORTED_BETWEEN_TYPES);
}
@Test
void testModulus() {
void modulus() {
evaluate("3%2",1,Integer.class);
evaluate("3L%2L",1L,Long.class);
evaluate("3.0f%2.0f",1f,Float.class);
@@ -448,7 +446,7 @@ class OperatorTests extends AbstractExpressionTests {
}
@Test
void testDivide() {
void divide() {
evaluate("3.0f / 5.0f", 0.6f, Float.class);
evaluate("4L/2L",2L,Long.class);
evaluate("3.0f div 5.0f", 0.6f, Float.class);
@@ -461,17 +459,17 @@ class OperatorTests extends AbstractExpressionTests {
}
@Test
void testMathOperatorDivide_ConvertToDouble() {
void mathOperatorDivide_ConvertToDouble() {
evaluateAndAskForReturnType("8/4", 2.0, Double.class);
}
@Test
void testMathOperatorDivide04_ConvertToFloat() {
void mathOperatorDivide04_ConvertToFloat() {
evaluateAndAskForReturnType("8/4", 2.0F, Float.class);
}
@Test
void testDoubles() {
void doubles() {
evaluate("3.0d == 5.0d", false, Boolean.class);
evaluate("3.0d == 3.0d", true, Boolean.class);
evaluate("3.0d != 5.0d", true, Boolean.class);
@@ -484,7 +482,7 @@ class OperatorTests extends AbstractExpressionTests {
}
@Test
void testBigDecimals() {
void bigDecimals() {
evaluate("3 + new java.math.BigDecimal('5')", new BigDecimal("8"), BigDecimal.class);
evaluate("3 - new java.math.BigDecimal('5')", new BigDecimal("-2"), BigDecimal.class);
evaluate("3 * new java.math.BigDecimal('5')", new BigDecimal("15"), BigDecimal.class);
@@ -495,7 +493,7 @@ class OperatorTests extends AbstractExpressionTests {
}
@Test
void testOperatorNames() {
void operatorNames() {
Operator node = getOperatorNode((SpelExpression)parser.parseExpression("1==3"));
assertThat(node.getOperatorName()).isEqualTo("==");
@@ -534,22 +532,22 @@ class OperatorTests extends AbstractExpressionTests {
}
@Test
void testOperatorOverloading() {
void operatorOverloading() {
evaluateAndCheckError("'a' * '2'", SpelMessage.OPERATOR_NOT_SUPPORTED_BETWEEN_TYPES);
evaluateAndCheckError("'a' ^ '2'", SpelMessage.OPERATOR_NOT_SUPPORTED_BETWEEN_TYPES);
}
@Test
void testPower() {
evaluate("3^2",9,Integer.class);
evaluate("3.0d^2.0d",9.0d,Double.class);
evaluate("3L^2L",9L,Long.class);
void power() {
evaluate("3^2", 9, Integer.class);
evaluate("3.0d^2.0d", 9.0d, Double.class);
evaluate("3L^2L", 9L, Long.class);
evaluate("(2^32)^2", 9223372036854775807L, Long.class);
evaluate("new java.math.BigDecimal('5') ^ 3", new BigDecimal("125"), BigDecimal.class);
}
@Test
void testMixedOperands_FloatsAndDoubles() {
void mixedOperands_FloatsAndDoubles() {
evaluate("3.0d + 5.0f", 8.0d, Double.class);
evaluate("3.0D - 5.0f", -2.0d, Double.class);
evaluate("3.0f * 5.0d", 15.0d, Double.class);
@@ -558,7 +556,7 @@ class OperatorTests extends AbstractExpressionTests {
}
@Test
void testMixedOperands_DoublesAndInts() {
void mixedOperands_DoublesAndInts() {
evaluate("3.0d + 5", 8.0d, Double.class);
evaluate("3.0D - 5", -2.0d, Double.class);
evaluate("3.0f * 5", 15.0f, Float.class);
@@ -569,7 +567,7 @@ class OperatorTests extends AbstractExpressionTests {
}
@Test
void testStrings() {
void strings() {
evaluate("'abc' == 'abc'", true, Boolean.class);
evaluate("'abc' == 'def'", false, Boolean.class);
evaluate("'abc' != 'abc'", false, Boolean.class);
@@ -577,7 +575,20 @@ class OperatorTests extends AbstractExpressionTests {
}
@Test
void testLongs() {
void stringRepeat() {
evaluate("'abc' * 0", "", String.class);
evaluate("'abc' * 1", "abc", String.class);
evaluate("'abc' * 2", "abcabc", String.class);
Expression expr = parser.parseExpression("'a' * 256");
assertThat(expr.getValue(context, String.class)).hasSize(256);
// 4 is the position of the '*' (repeat operator)
evaluateAndCheckError("'a' * 257", String.class, MAX_REPEATED_TEXT_SIZE_EXCEEDED, 4);
}
@Test
void longs() {
evaluate("3L == 4L", false, Boolean.class);
evaluate("3L == 3L", true, Boolean.class);
evaluate("3L != 4L", true, Boolean.class);
@@ -588,7 +599,7 @@ class OperatorTests extends AbstractExpressionTests {
}
@Test
void testBigIntegers() {
void bigIntegers() {
evaluate("3 + new java.math.BigInteger('5')", new BigInteger("8"), BigInteger.class);
evaluate("3 - new java.math.BigInteger('5')", new BigInteger("-2"), BigInteger.class);
evaluate("3 * new java.math.BigInteger('5')", new BigInteger("15"), BigInteger.class);
@@ -619,7 +630,7 @@ class OperatorTests extends AbstractExpressionTests {
}
public static class BaseComparable implements Comparable<BaseComparable> {
static class BaseComparable implements Comparable<BaseComparable> {
@Override
public int compareTo(BaseComparable other) {
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2023 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.
@@ -1817,7 +1817,6 @@ class SpelReproTests extends AbstractExpressionTests {
static class CCC {
public boolean method(Object o) {
System.out.println(o);
return false;
}
}
@@ -1889,7 +1888,6 @@ class SpelReproTests extends AbstractExpressionTests {
static class Foo2 {
public void execute(String str) {
System.out.println("Value: " + str);
}
}
@@ -1964,7 +1962,6 @@ class SpelReproTests extends AbstractExpressionTests {
public static class ReflectionUtil<T extends Number> {
public Object methodToCall(T param) {
System.out.println(param + " " + param.getClass());
return "Object methodToCall(T param)";
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2023 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.
@@ -19,13 +19,17 @@ package org.springframework.expression.spel.support;
import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.MethodExecutor;
import org.springframework.expression.MethodResolver;
import org.springframework.expression.ParseException;
import org.springframework.expression.PropertyAccessor;
import org.springframework.expression.TypedValue;
@@ -364,6 +368,20 @@ public class ReflectionHelperTests extends AbstractExpressionTests {
field.write(ctx, tester, "field", null));
}
@Test
void reflectiveMethodResolverForJdkProxies() throws Exception {
Object proxy = Proxy.newProxyInstance(getClass().getClassLoader(), new Class<?>[] { Runnable.class }, (p, m, args) -> null);
MethodResolver resolver = new ReflectiveMethodResolver();
StandardEvaluationContext evaluationContext = new StandardEvaluationContext();
MethodExecutor bogus = resolver.resolve(evaluationContext, proxy, "bogus", Collections.emptyList());
assertThat(bogus).as("MethodExecutor for bogus()").isNull();
MethodExecutor toString = resolver.resolve(evaluationContext, proxy, "toString", Collections.emptyList());
assertThat(toString).as("MethodExecutor for toString()").isNotNull();
MethodExecutor hashCode = resolver.resolve(evaluationContext, proxy, "hashCode", Collections.emptyList());
assertThat(hashCode).as("MethodExecutor for hashCode()").isNotNull();
}
/**
* Used to validate the match returned from a compareArguments call.
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2023 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.
@@ -48,31 +48,42 @@ import org.springframework.util.StringUtils;
/**
* {@link RowMapper} implementation that converts a row into a new instance
* of the specified mapped target class. The mapped target class must be a
* top-level class and it must have a default or no-arg constructor.
* top-level class or {@code static} nested class, and it must have a default or
* no-arg constructor.
*
* <p>Column values are mapped based on matching the column name as obtained from result set
* meta-data to public setters for the corresponding properties. The names are matched either
* directly or by transforming a name separating the parts with underscores to the same name
* using "camel" case.
* <p>Column values are mapped based on matching the column name (as obtained from
* result set meta-data) to public setters in the target class for the corresponding
* properties. The names are matched either directly or by transforming a name
* separating the parts with underscores to the same name using "camel" case.
*
* <p>Mapping is provided for fields in the target class for many common types, e.g.:
* String, boolean, Boolean, byte, Byte, short, Short, int, Integer, long, Long,
* float, Float, double, Double, BigDecimal, {@code java.util.Date}, etc.
* <p>Mapping is provided for properties in the target class for many common types &mdash;
* for example: String, boolean, Boolean, byte, Byte, short, Short, int, Integer,
* long, Long, float, Float, double, Double, BigDecimal, {@code java.util.Date}, etc.
*
* <p>To facilitate mapping between columns and fields that don't have matching names,
* try using column aliases in the SQL statement like "select fname as first_name from customer".
* <p>To facilitate mapping between columns and properties that don't have matching
* names, try using column aliases in the SQL statement like
* {@code "select fname as first_name from customer"}, where {@code first_name}
* can be mapped to a {@code setFirstName(String)} method in the target class.
*
* <p>For 'null' values read from the database, we will attempt to call the setter, but in the case of
* Java primitives, this causes a TypeMismatchException. This class can be configured (using the
* primitivesDefaultedForNullValue property) to trap this exception and use the primitives default value.
* Be aware that if you use the values from the generated bean to update the database the primitive value
* will have been set to the primitive's default value instead of null.
* <p>For a {@code NULL} value read from the database, an attempt will be made to
* call the corresponding setter method with {@code null}, but in the case of
* Java primitives this will result in a {@link TypeMismatchException} by default.
* To ignore {@code NULL} database values for all primitive properties in the
* target class, set the {@code primitivesDefaultedForNullValue} flag to
* {@code true}. See {@link #setPrimitivesDefaultedForNullValue(boolean)} for
* details.
*
* <p>Please note that this class is designed to provide convenience rather than high performance.
* For best performance, consider using a custom {@link RowMapper} implementation.
* <p>If you need to map to a target class which has a <em>data class</em> constructor
* &mdash; for example, a Java {@code record} or a Kotlin {@code data} class &mdash;
* use {@link DataClassRowMapper} instead.
*
* <p>Please note that this class is designed to provide convenience rather than
* high performance. For best performance, consider using a custom {@code RowMapper}
* implementation.
*
* @author Thomas Risberg
* @author Juergen Hoeller
* @author Sam Brannen
* @since 2.5
* @param <T> the result type
* @see DataClassRowMapper
@@ -89,20 +100,24 @@ public class BeanPropertyRowMapper<T> implements RowMapper<T> {
/** Whether we're strictly validating. */
private boolean checkFullyPopulated = false;
/** Whether we're defaulting primitives when mapping a null value. */
/**
* Whether {@code NULL} database values should be ignored for primitive
* properties in the target class.
* @see #setPrimitivesDefaultedForNullValue(boolean)
*/
private boolean primitivesDefaultedForNullValue = false;
/** ConversionService for binding JDBC values to bean properties. */
@Nullable
private ConversionService conversionService = DefaultConversionService.getSharedInstance();
/** Map of the fields we provide mapping for. */
/** Map of the properties we provide mapping for. */
@Nullable
private Map<String, PropertyDescriptor> mappedFields;
private Map<String, PropertyDescriptor> mappedProperties;
/** Set of bean properties we provide mapping for. */
/** Set of bean property names we provide mapping for. */
@Nullable
private Set<String> mappedProperties;
private Set<String> mappedPropertyNames;
/**
@@ -126,7 +141,7 @@ public class BeanPropertyRowMapper<T> implements RowMapper<T> {
* Create a new {@code BeanPropertyRowMapper}.
* @param mappedClass the class that each row should be mapped to
* @param checkFullyPopulated whether we're strictly validating that
* all bean properties have been mapped from corresponding database fields
* all bean properties have been mapped from corresponding database columns
*/
public BeanPropertyRowMapper(Class<T> mappedClass, boolean checkFullyPopulated) {
initialize(mappedClass);
@@ -159,7 +174,7 @@ public class BeanPropertyRowMapper<T> implements RowMapper<T> {
/**
* Set whether we're strictly validating that all bean properties have been mapped
* from corresponding database fields.
* from corresponding database columns.
* <p>Default is {@code false}, accepting unpopulated properties in the target bean.
*/
public void setCheckFullyPopulated(boolean checkFullyPopulated) {
@@ -168,24 +183,33 @@ public class BeanPropertyRowMapper<T> implements RowMapper<T> {
/**
* Return whether we're strictly validating that all bean properties have been
* mapped from corresponding database fields.
* mapped from corresponding database columns.
*/
public boolean isCheckFullyPopulated() {
return this.checkFullyPopulated;
}
/**
* Set whether we're defaulting Java primitives in the case of mapping a null value
* from corresponding database fields.
* <p>Default is {@code false}, throwing an exception when nulls are mapped to Java primitives.
* Set whether a {@code NULL} database column value should be ignored when
* mapping to a corresponding primitive property in the target class.
* <p>Default is {@code false}, throwing an exception when nulls are mapped
* to Java primitives.
* <p>If this flag is set to {@code true} and you use an <em>ignored</em>
* primitive property value from the mapped bean to update the database, the
* value in the database will be changed from {@code NULL} to the current value
* of that primitive property. That value may be the property's initial value
* (potentially Java's default value for the respective primitive type), or
* it may be some other value set for the property in the default constructor
* (or initialization block) or as a side effect of setting some other property
* in the mapped bean.
*/
public void setPrimitivesDefaultedForNullValue(boolean primitivesDefaultedForNullValue) {
this.primitivesDefaultedForNullValue = primitivesDefaultedForNullValue;
}
/**
* Return whether we're defaulting Java primitives in the case of mapping a null value
* from corresponding database fields.
* Get the value of the {@code primitivesDefaultedForNullValue} flag.
* @see #setPrimitivesDefaultedForNullValue(boolean)
*/
public boolean isPrimitivesDefaultedForNullValue() {
return this.primitivesDefaultedForNullValue;
@@ -220,31 +244,31 @@ public class BeanPropertyRowMapper<T> implements RowMapper<T> {
*/
protected void initialize(Class<T> mappedClass) {
this.mappedClass = mappedClass;
this.mappedFields = new HashMap<>();
this.mappedProperties = new HashSet<>();
this.mappedProperties = new HashMap<>();
this.mappedPropertyNames = new HashSet<>();
for (PropertyDescriptor pd : BeanUtils.getPropertyDescriptors(mappedClass)) {
if (pd.getWriteMethod() != null) {
String lowerCaseName = lowerCaseName(pd.getName());
this.mappedFields.put(lowerCaseName, pd);
this.mappedProperties.put(lowerCaseName, pd);
String underscoreName = underscoreName(pd.getName());
if (!lowerCaseName.equals(underscoreName)) {
this.mappedFields.put(underscoreName, pd);
this.mappedProperties.put(underscoreName, pd);
}
this.mappedProperties.add(pd.getName());
this.mappedPropertyNames.add(pd.getName());
}
}
}
/**
* Remove the specified property from the mapped fields.
* Remove the specified property from the mapped properties.
* @param propertyName the property name (as used by property descriptors)
* @since 5.3.9
*/
protected void suppressProperty(String propertyName) {
if (this.mappedFields != null) {
this.mappedFields.remove(lowerCaseName(propertyName));
this.mappedFields.remove(underscoreName(propertyName));
if (this.mappedProperties != null) {
this.mappedProperties.remove(lowerCaseName(propertyName));
this.mappedProperties.remove(underscoreName(propertyName));
}
}
@@ -306,8 +330,8 @@ public class BeanPropertyRowMapper<T> implements RowMapper<T> {
for (int index = 1; index <= columnCount; index++) {
String column = JdbcUtils.lookupColumnName(rsmd, index);
String field = lowerCaseName(StringUtils.delete(column, " "));
PropertyDescriptor pd = (this.mappedFields != null ? this.mappedFields.get(field) : null);
String property = lowerCaseName(StringUtils.delete(column, " "));
PropertyDescriptor pd = (this.mappedProperties != null ? this.mappedProperties.get(property) : null);
if (pd != null) {
try {
Object value = getColumnValue(rs, index, pd);
@@ -321,11 +345,11 @@ public class BeanPropertyRowMapper<T> implements RowMapper<T> {
catch (TypeMismatchException ex) {
if (value == null && this.primitivesDefaultedForNullValue) {
if (logger.isDebugEnabled()) {
logger.debug("Intercepted TypeMismatchException for row " + rowNumber +
" and column '" + column + "' with null value when setting property '" +
pd.getName() + "' of type '" +
ClassUtils.getQualifiedName(pd.getPropertyType()) +
"' on object: " + mappedObject, ex);
String propertyType = ClassUtils.getQualifiedName(pd.getPropertyType());
logger.debug(String.format(
"Ignoring intercepted TypeMismatchException for row %d and column '%s' " +
"with null value when setting property '%s' of type '%s' on object: %s",
rowNumber, column, pd.getName(), propertyType, mappedObject), ex);
}
}
else {
@@ -343,9 +367,9 @@ public class BeanPropertyRowMapper<T> implements RowMapper<T> {
}
}
if (populatedProperties != null && !populatedProperties.equals(this.mappedProperties)) {
throw new InvalidDataAccessApiUsageException("Given ResultSet does not contain all fields " +
"necessary to populate object of " + this.mappedClass + ": " + this.mappedProperties);
if (populatedProperties != null && !populatedProperties.equals(this.mappedPropertyNames)) {
throw new InvalidDataAccessApiUsageException("Given ResultSet does not contain all properties " +
"necessary to populate object of " + this.mappedClass + ": " + this.mappedPropertyNames);
}
return mappedObject;
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2023 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.
@@ -31,15 +31,30 @@ import org.springframework.util.Assert;
/**
* {@link RowMapper} implementation that converts a row into a new instance
* of the specified mapped target class. The mapped target class must be a
* top-level class and may either expose a data class constructor with named
* parameters corresponding to column names or classic bean property setters
* (or even a combination of both).
* top-level class or {@code static} nested class, and it may expose either a
* <em>data class</em> constructor with named parameters corresponding to column
* names or classic bean property setter methods with property names corresponding
* to column names (or even a combination of both).
*
* <p>The term "data class" applies to Java <em>records</em>, Kotlin <em>data
* classes</em>, and any class which has a constructor with named parameters
* that are intended to be mapped to corresponding column names.
*
* <p>When combining a data class constructor with setter methods, any property
* mapped successfully via a constructor argument will not be mapped additionally
* via a corresponding setter method. This means that constructor arguments take
* precedence over property setter methods.
*
* <p>Note that this class extends {@link BeanPropertyRowMapper} and can
* therefore serve as a common choice for any mapped target class, flexibly
* adapting to constructor style versus setter methods in the mapped class.
*
* <p>Please note that this class is designed to provide convenience rather than
* high performance. For best performance, consider using a custom {@code RowMapper}
* implementation.
*
* @author Juergen Hoeller
* @author Sam Brannen
* @since 5.3
* @param <T> the result type
*/
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2023 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.
@@ -24,21 +24,21 @@ import org.springframework.lang.Nullable;
/**
* An interface used by {@link JdbcTemplate} for mapping rows of a
* {@link java.sql.ResultSet} on a per-row basis. Implementations of this
* interface perform the actual work of mapping each row to a result object,
* interface perform the actual work of mapping each row to a result object
* but don't need to worry about exception handling.
* {@link java.sql.SQLException SQLExceptions} will be caught and handled
* by the calling JdbcTemplate.
* by the calling {@code JdbcTemplate}.
*
* <p>Typically used either for {@link JdbcTemplate}'s query methods
* or for out parameters of stored procedures. RowMapper objects are
* <p>Typically used either for {@code JdbcTemplate}'s query methods or for
* {@code out} parameters of stored procedures. {@code RowMapper} objects are
* typically stateless and thus reusable; they are an ideal choice for
* implementing row-mapping logic in a single place.
*
* <p>Alternatively, consider subclassing
* {@link org.springframework.jdbc.object.MappingSqlQuery} from the
* {@code jdbc.object} package: Instead of working with separate
* JdbcTemplate and RowMapper objects, you can build executable query
* objects (containing row-mapping logic) in that style.
* {@code jdbc.object} package: instead of working with separate
* {@code JdbcTemplate} and {@code RowMapper} objects, you can build executable
* query objects (containing row-mapping logic) in that style.
*
* @author Thomas Risberg
* @author Juergen Hoeller
@@ -52,13 +52,13 @@ import org.springframework.lang.Nullable;
public interface RowMapper<T> {
/**
* Implementations must implement this method to map each row of data
* in the ResultSet. This method should not call {@code next()} on
* the ResultSet; it is only supposed to map values of the current row.
* @param rs the ResultSet to map (pre-initialized for the current row)
* Implementations must implement this method to map each row of data in the
* {@code ResultSet}. This method should not call {@code next()} on the
* {@code ResultSet}; it is only supposed to map values of the current row.
* @param rs the {@code ResultSet} to map (pre-initialized for the current row)
* @param rowNum the number of the current row
* @return the result object for the current row (may be {@code null})
* @throws SQLException if an SQLException is encountered getting
* @throws SQLException if an SQLException is encountered while getting
* column values (that is, there's no need to catch SQLException)
*/
@Nullable
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2023 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.
@@ -16,8 +16,6 @@
package org.springframework.jdbc.core;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
@@ -44,12 +42,15 @@ import static org.assertj.core.api.Assertions.assertThatNoException;
*/
class BeanPropertyRowMapperTests extends AbstractRowMapperTests {
private static final String SELECT_NULL_AS_AGE = "select null as age from people";
@Test
@SuppressWarnings({"unchecked", "rawtypes"})
void overridingDifferentClassDefinedForMapping() {
BeanPropertyRowMapper mapper = new BeanPropertyRowMapper(Person.class);
assertThatExceptionOfType(InvalidDataAccessApiUsageException.class).isThrownBy(() ->
mapper.setMappedClass(Long.class));
assertThatExceptionOfType(InvalidDataAccessApiUsageException.class)
.isThrownBy(() -> mapper.setMappedClass(Long.class));
}
@Test
@@ -61,104 +62,106 @@ class BeanPropertyRowMapperTests extends AbstractRowMapperTests {
@Test
void staticQueryWithRowMapper() throws Exception {
Mock mock = new Mock();
List<Person> result = mock.getJdbcTemplate().query(
Person person = mock.getJdbcTemplate().queryForObject(
"select name, age, birth_date, balance from people",
new BeanPropertyRowMapper<>(Person.class));
assertThat(result).hasSize(1);
verifyPerson(result.get(0));
verifyPerson(person);
mock.verifyClosed();
}
@Test
void mappingWithInheritance() throws Exception {
Mock mock = new Mock();
List<ConcretePerson> result = mock.getJdbcTemplate().query(
ConcretePerson person = mock.getJdbcTemplate().queryForObject(
"select name, age, birth_date, balance from people",
new BeanPropertyRowMapper<>(ConcretePerson.class));
assertThat(result).hasSize(1);
verifyPerson(result.get(0));
verifyPerson(person);
mock.verifyClosed();
}
@Test
void mappingWithNoUnpopulatedFieldsFound() throws Exception {
Mock mock = new Mock();
List<ConcretePerson> result = mock.getJdbcTemplate().query(
ConcretePerson person = mock.getJdbcTemplate().queryForObject(
"select name, age, birth_date, balance from people",
new BeanPropertyRowMapper<>(ConcretePerson.class, true));
assertThat(result).hasSize(1);
verifyPerson(result.get(0));
verifyPerson(person);
mock.verifyClosed();
}
@Test
void mappingWithUnpopulatedFieldsNotChecked() throws Exception {
Mock mock = new Mock();
List<ExtendedPerson> result = mock.getJdbcTemplate().query(
ExtendedPerson person = mock.getJdbcTemplate().queryForObject(
"select name, age, birth_date, balance from people",
new BeanPropertyRowMapper<>(ExtendedPerson.class));
assertThat(result).hasSize(1);
verifyPerson(result.get(0));
verifyPerson(person);
mock.verifyClosed();
}
@Test
void mappingWithUnpopulatedFieldsNotAccepted() throws Exception {
BeanPropertyRowMapper<ExtendedPerson> mapper = new BeanPropertyRowMapper<>(ExtendedPerson.class, true);
Mock mock = new Mock();
assertThatExceptionOfType(InvalidDataAccessApiUsageException.class).isThrownBy(() ->
mock.getJdbcTemplate().query("select name, age, birth_date, balance from people",
new BeanPropertyRowMapper<>(ExtendedPerson.class, true)));
assertThatExceptionOfType(InvalidDataAccessApiUsageException.class)
.isThrownBy(() -> mock.getJdbcTemplate().query("select name, age, birth_date, balance from people", mapper));
}
@Test
void mappingNullValue() throws Exception {
BeanPropertyRowMapper<Person> mapper = new BeanPropertyRowMapper<>(Person.class);
Mock mock = new Mock(MockType.TWO);
assertThatExceptionOfType(TypeMismatchException.class).isThrownBy(() ->
mock.getJdbcTemplate().query("select name, null as age, birth_date, balance from people", mapper));
assertThatExceptionOfType(TypeMismatchException.class)
.isThrownBy(() -> mock.getJdbcTemplate().query(SELECT_NULL_AS_AGE, mapper));
}
@Test
void mappingNullValueWithPrimitivesDefaultedForNullValue() throws Exception {
BeanPropertyRowMapper<Person> mapper = new BeanPropertyRowMapper<>(Person.class);
mapper.setPrimitivesDefaultedForNullValue(true);
Mock mock = new Mock(MockType.TWO);
Person person = mock.getJdbcTemplate().queryForObject(SELECT_NULL_AS_AGE, mapper);
assertThat(person).extracting(Person::getAge).isEqualTo(42L);
mock.verifyClosed();
}
@Test
void queryWithSpaceInColumnNameAndLocalDateTime() throws Exception {
Mock mock = new Mock(MockType.THREE);
List<SpacePerson> result = mock.getJdbcTemplate().query(
SpacePerson person = mock.getJdbcTemplate().queryForObject(
"select last_name as \"Last Name\", age, birth_date, balance from people",
new BeanPropertyRowMapper<>(SpacePerson.class));
assertThat(result).hasSize(1);
verifyPerson(result.get(0));
verifyPerson(person);
mock.verifyClosed();
}
@Test
void queryWithSpaceInColumnNameAndLocalDate() throws Exception {
Mock mock = new Mock(MockType.THREE);
List<DatePerson> result = mock.getJdbcTemplate().query(
DatePerson person = mock.getJdbcTemplate().queryForObject(
"select last_name as \"Last Name\", age, birth_date, balance from people",
new BeanPropertyRowMapper<>(DatePerson.class));
assertThat(result).hasSize(1);
verifyPerson(result.get(0));
verifyPerson(person);
mock.verifyClosed();
}
@Test
void queryWithDirectNameMatchOnBirthDate() throws Exception {
Mock mock = new Mock(MockType.FOUR);
List<ConcretePerson> result = mock.getJdbcTemplate().query(
ConcretePerson person = mock.getJdbcTemplate().queryForObject(
"select name, age, birthdate, balance from people",
new BeanPropertyRowMapper<>(ConcretePerson.class));
assertThat(result).hasSize(1);
verifyPerson(result.get(0));
verifyPerson(person);
mock.verifyClosed();
}
@Test
void queryWithUnderscoreInColumnNameAndPersonWithMultipleAdjacentUppercaseLettersInPropertyName() throws Exception {
Mock mock = new Mock();
List<EmailPerson> result = mock.getJdbcTemplate().query(
EmailPerson person = mock.getJdbcTemplate().queryForObject(
"select name, age, birth_date, balance, e_mail from people",
new BeanPropertyRowMapper<>(EmailPerson.class));
assertThat(result).hasSize(1);
verifyPerson(result.get(0));
verifyPerson(person);
mock.verifyClosed();
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2023 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.
@@ -17,9 +17,7 @@
package org.springframework.jdbc.core;
import java.math.BigDecimal;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import org.junit.jupiter.api.Test;
@@ -30,50 +28,48 @@ import org.springframework.jdbc.core.test.ConstructorPersonWithSetters;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link DataClassRowMapper}.
*
* @author Juergen Hoeller
* @author Sam Brannen
* @since 5.3
*/
public class DataClassRowMapperTests extends AbstractRowMapperTests {
class DataClassRowMapperTests extends AbstractRowMapperTests {
@Test
public void testStaticQueryWithDataClass() throws Exception {
void staticQueryWithDataClass() throws Exception {
Mock mock = new Mock();
List<ConstructorPerson> result = mock.getJdbcTemplate().query(
ConstructorPerson person = mock.getJdbcTemplate().queryForObject(
"select name, age, birth_date, balance from people",
new DataClassRowMapper<>(ConstructorPerson.class));
assertThat(result.size()).isEqualTo(1);
verifyPerson(result.get(0));
verifyPerson(person);
mock.verifyClosed();
}
@Test
public void testStaticQueryWithDataClassAndGenerics() throws Exception {
void staticQueryWithDataClassAndGenerics() throws Exception {
Mock mock = new Mock();
List<ConstructorPersonWithGenerics> result = mock.getJdbcTemplate().query(
ConstructorPersonWithGenerics person = mock.getJdbcTemplate().queryForObject(
"select name, age, birth_date, balance from people",
new DataClassRowMapper<>(ConstructorPersonWithGenerics.class));
assertThat(result.size()).isEqualTo(1);
ConstructorPersonWithGenerics person = result.get(0);
assertThat(person.name()).isEqualTo("Bubba");
assertThat(person.age()).isEqualTo(22L);
assertThat(person.birthDate()).usingComparator(Date::compareTo).isEqualTo(new java.util.Date(1221222L));
assertThat(person.balance()).isEqualTo(Collections.singletonList(new BigDecimal("1234.56")));
assertThat(person.birthDate()).usingComparator(Date::compareTo).isEqualTo(new Date(1221222L));
assertThat(person.balance()).containsExactly(new BigDecimal("1234.56"));
mock.verifyClosed();
}
@Test
public void testStaticQueryWithDataClassAndSetters() throws Exception {
void staticQueryWithDataClassAndSetters() throws Exception {
Mock mock = new Mock(MockType.FOUR);
List<ConstructorPersonWithSetters> result = mock.getJdbcTemplate().query(
ConstructorPersonWithSetters person = mock.getJdbcTemplate().queryForObject(
"select name, age, birthdate, balance from people",
new DataClassRowMapper<>(ConstructorPersonWithSetters.class));
assertThat(result.size()).isEqualTo(1);
ConstructorPersonWithSetters person = result.get(0);
assertThat(person.name()).isEqualTo("BUBBA");
assertThat(person.age()).isEqualTo(22L);
assertThat(person.birthDate()).usingComparator(Date::compareTo).isEqualTo(new java.util.Date(1221222L));
assertThat(person.birthDate()).usingComparator(Date::compareTo).isEqualTo(new Date(1221222L));
assertThat(person.balance()).isEqualTo(new BigDecimal("1234.56"));
mock.verifyClosed();
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2023 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.
@@ -31,6 +31,9 @@ public class Person {
private BigDecimal balance;
public Person() {
this.age = 42; // custom "default" value for a primitive
}
public String getName() {
return name;
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2023 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.
@@ -76,6 +76,7 @@ import org.springframework.messaging.handler.annotation.MessageMapping;
* <em>composed annotations</em> with attribute overrides.
*
* @author Stephane Nicoll
* @author Sam Brannen
* @since 4.1
* @see EnableJms
* @see JmsListenerAnnotationBeanPostProcessor
@@ -110,6 +111,12 @@ public @interface JmsListener {
/**
* The name for the durable subscription, if any.
* <p>As of Spring Framework 5.3.26, if an explicit subscription name is not
* specified, a default subscription name will be generated based on the fully
* qualified name of the annotated listener method &mdash; for example,
* {@code "org.example.jms.ProductListener.processRequest"} for a
* {@code processRequest(...)} listener method in the
* {@code org.example.jms.ProductListener} class.
*/
String subscription() default "";
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2023 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.
@@ -347,8 +347,26 @@ public class SingleConnectionFactory implements ConnectionFactory, QueueConnecti
if (this.connection != null) {
closeConnection(this.connection);
}
this.connection = doCreateConnection();
prepareConnection(this.connection);
// Create new (method local) connection, which is later assigned to instance connection
// - prevention to hold instance connection without exception listener, in case when
// some subsequent methods (after creation of connection) throws JMSException
Connection con = doCreateConnection();
try {
prepareConnection(con);
this.connection = con;
}
catch (JMSException ex) {
// Attempt to close new (not used) connection to release possible resources
try {
con.close();
}
catch(Throwable th) {
if (logger.isDebugEnabled()) {
logger.debug("Could not close newly obtained JMS Connection that failed to prepare", th);
}
}
throw ex;
}
if (this.startedCount > 0) {
this.connection.start();
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2021 the original author or authors.
* Copyright 2002-2023 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.
@@ -20,6 +20,7 @@ import javax.jms.JMSException;
import javax.jms.Session;
import org.springframework.core.MethodParameter;
import org.springframework.jms.listener.SubscriptionNameProvider;
import org.springframework.jms.support.JmsHeaderMapper;
import org.springframework.jms.support.converter.MessageConversionException;
import org.springframework.lang.Nullable;
@@ -34,7 +35,7 @@ import org.springframework.util.Assert;
* A {@link javax.jms.MessageListener} adapter that invokes a configurable
* {@link InvocableHandlerMethod}.
*
* <p>Wraps the incoming {@link javax.jms.Message} to Spring's {@link Message}
* <p>Wraps the incoming {@link javax.jms.Message} in Spring's {@link Message}
* abstraction, copying the JMS standard headers using a configurable
* {@link JmsHeaderMapper}.
*
@@ -42,13 +43,19 @@ import org.springframework.util.Assert;
* are provided as additional arguments so that these can be injected as
* method arguments if necessary.
*
* <p>As of Spring Framework 5.3.26, {@code MessagingMessageListenerAdapter} implements
* {@link SubscriptionNameProvider} in order to provide a meaningful default
* subscription name. See {@link #getSubscriptionName()} for details.
*
* @author Stephane Nicoll
* @author Sam Brannen
* @since 4.1
* @see Message
* @see JmsHeaderMapper
* @see InvocableHandlerMethod
*/
public class MessagingMessageListenerAdapter extends AbstractAdaptableMessageListener {
public class MessagingMessageListenerAdapter extends AbstractAdaptableMessageListener
implements SubscriptionNameProvider {
@Nullable
private InvocableHandlerMethod handlerMethod;
@@ -83,17 +90,6 @@ public class MessagingMessageListenerAdapter extends AbstractAdaptableMessageLis
}
}
@Override
protected Object preProcessResponse(Object result) {
MethodParameter returnType = getHandlerMethod().getReturnType();
if (result instanceof Message) {
return MessageBuilder.fromMessage((Message<?>) result)
.setHeader(AbstractMessageSendingTemplate.CONVERSION_HINT_HEADER, returnType).build();
}
return MessageBuilder.withPayload(result).setHeader(
AbstractMessageSendingTemplate.CONVERSION_HINT_HEADER, returnType).build();
}
protected Message<?> toMessagingMessage(javax.jms.Message jmsMessage) {
try {
return (Message<?>) getMessagingMessageConverter().fromMessage(jmsMessage);
@@ -104,7 +100,7 @@ public class MessagingMessageListenerAdapter extends AbstractAdaptableMessageLis
}
/**
* Invoke the handler, wrapping any exception to a {@link ListenerExecutionFailedException}
* Invoke the handler, wrapping any exception in a {@link ListenerExecutionFailedException}
* with a dedicated error message.
*/
@Nullable
@@ -132,4 +128,39 @@ public class MessagingMessageListenerAdapter extends AbstractAdaptableMessageLis
return sb.toString();
}
@Override
protected Object preProcessResponse(Object result) {
MethodParameter returnType = getHandlerMethod().getReturnType();
MessageBuilder<?> messageBuilder = (result instanceof Message ?
MessageBuilder.fromMessage((Message<?>) result) :
MessageBuilder.withPayload(result));
return messageBuilder
.setHeader(AbstractMessageSendingTemplate.CONVERSION_HINT_HEADER, returnType)
.build();
}
/**
* Generate a subscription name for this {@code MessageListener} adapter based
* on the following rules.
* <ul>
* <li>If the {@link #setHandlerMethod(InvocableHandlerMethod) handlerMethod}
* has been set, the generated subscription name takes the form of
* {@code handlerMethod.getBeanType().getName() + "." + handlerMethod.getMethod().getName()}.</li>
* <li>Otherwise, the generated subscription name is the result of invoking
* {@code getClass().getName()}, which aligns with the default behavior of
* {@link org.springframework.jms.listener.AbstractMessageListenerContainer}.</li>
* </ul>
* @since 5.3.26
* @see SubscriptionNameProvider#getSubscriptionName()
*/
@Override
public String getSubscriptionName() {
if (this.handlerMethod != null) {
return this.handlerMethod.getBeanType().getName() + "." + this.handlerMethod.getMethod().getName();
}
else {
return getClass().getName();
}
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2023 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.
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2023 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.
@@ -16,6 +16,9 @@
package org.springframework.jms.connection;
import java.lang.reflect.Field;
import java.util.concurrent.atomic.AtomicInteger;
import javax.jms.Connection;
import javax.jms.ConnectionFactory;
import javax.jms.ExceptionListener;
@@ -30,7 +33,10 @@ import javax.jms.TopicSession;
import org.junit.jupiter.api.Test;
import org.springframework.util.ReflectionUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
@@ -342,6 +348,76 @@ public class SingleConnectionFactoryTests {
assertThat(listener.getCount()).isEqualTo(1);
}
@Test
public void testWithConnectionFactoryAndExceptionListenerAndReconnectOnExceptionWithJMSException() throws Exception {
// Throws JMSException on setExceptionListener() method, but only at the first time
class FailingTestConnection extends TestConnection {
private int setExceptionListenerInvocationCounter;
@Override
public void setExceptionListener(ExceptionListener exceptionListener) throws JMSException {
setExceptionListenerInvocationCounter++;
// Throw JMSException on first invocation
if (setExceptionListenerInvocationCounter == 1) {
throw new JMSException("Test JMSException (setExceptionListener())");
}
super.setExceptionListener(exceptionListener);
}
}
// Prepare base JMS ConnectionFactory
// - createConnection(1st) -> TestConnection,
// - createConnection(2nd and next) -> FailingTestConnection
TestConnection testCon = new TestConnection();
FailingTestConnection failingCon = new FailingTestConnection();
AtomicInteger createConnectionMethodCounter = new AtomicInteger();
ConnectionFactory cf = mock(ConnectionFactory.class);
given(cf.createConnection()).willAnswer(invocation -> {
int methodInvocationCounter = createConnectionMethodCounter.incrementAndGet();
return methodInvocationCounter == 1 ? testCon : failingCon;
});
// Prepare SingleConnectionFactory (setReconnectOnException())
// - internal connection exception listener should be registered
SingleConnectionFactory scf = new SingleConnectionFactory(cf);
scf.setReconnectOnException(true);
Field conField = ReflectionUtils.findField(SingleConnectionFactory.class, "connection");
conField.setAccessible(true);
// Get connection (1st)
Connection con1 = scf.getConnection();
assertThat(createConnectionMethodCounter.get()).isEqualTo(1);
assertThat(con1).isNotNull();
assertThat(con1.getExceptionListener()).isNotNull();
assertThat(con1).isSameAs(testCon);
// Get connection again, the same should be returned (shared connection till some problem)
Connection con2 = scf.getConnection();
assertThat(createConnectionMethodCounter.get()).isEqualTo(1);
assertThat(con2.getExceptionListener()).isNotNull();
assertThat(con2).isSameAs(con1);
// Invoke reset connection to simulate problem with connection
// - SCF exception listener should be invoked -> connection should be set to null
// - next attempt to invoke getConnection() must create new connection
scf.resetConnection();
assertThat(conField.get(scf)).isNull();
// Attempt to get connection again
// - JMSException should be returned from FailingTestConnection
// - connection should be still null (no new connection without exception listener like before fix)
assertThatExceptionOfType(JMSException.class).isThrownBy(() -> scf.getConnection());
assertThat(createConnectionMethodCounter.get()).isEqualTo(2);
assertThat(conField.get(scf)).isNull();
// Attempt to get connection again -> FailingTestConnection should be returned
// - no JMSException is thrown, exception listener should be present
Connection con3 = scf.getConnection();
assertThat(createConnectionMethodCounter.get()).isEqualTo(3);
assertThat(con3).isNotNull();
assertThat(con3).isSameAs(failingCon);
assertThat(con3.getExceptionListener()).isNotNull();
}
@Test
public void testWithConnectionFactoryAndLocalExceptionListenerWithCleanup() throws JMSException {
ConnectionFactory cf = mock(ConnectionFactory.class);
@@ -0,0 +1,104 @@
/*
* Copyright 2002-2023 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.jms.listener.adapter;
import java.lang.reflect.Method;
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 org.springframework.jms.listener.SimpleMessageListenerContainer;
import org.springframework.messaging.handler.invocation.InvocableHandlerMethod;
import org.springframework.util.ReflectionUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Named.named;
import static org.junit.jupiter.params.provider.Arguments.arguments;
/**
* Integration tests for {@link MessagingMessageListenerAdapter}.
*
* <p>These tests are similar to those in {@link MessagingMessageListenerAdapterTests},
* except that these tests have a different scope and do not use mocks.
*
* @author Sam Brannen
* @since 5.3.26
* @see MessagingMessageListenerAdapterTests
*/
class MessagingMessageListenerAdapterIntegrationTests {
@ParameterizedTest
@MethodSource("subscriptionNames")
void defaultSubscriptionName(Method method, String subscriptionName) {
MessagingMessageListenerAdapter messageListenerAdaptor = new MessagingMessageListenerAdapter();
InvocableHandlerMethod handlerMethod = new InvocableHandlerMethod(new CustomListener(), method);
messageListenerAdaptor.setHandlerMethod(handlerMethod);
SimpleMessageListenerContainer listenerContainer = new SimpleMessageListenerContainer();
assertThat(listenerContainer.getSubscriptionName()).isNull();
listenerContainer.setMessageListener(messageListenerAdaptor);
assertThat(listenerContainer.getSubscriptionName()).isEqualTo(subscriptionName);
}
private static Stream<Arguments> subscriptionNames() {
String method1 = "toUpperCase";
String method2 = "toUpperCase(java.lang.String)";
String method3 = "toUpperCase(java.lang.String,int)";
String method4 = "toUpperCase(java.lang.String[])";
String expectedName = CustomListener.class.getName() + ".toUpperCase";
return Stream.of(
arguments(named(method1, findMethod()), expectedName),
arguments(named(method2, findMethod(String.class)), expectedName),
arguments(named(method3, findMethod(String.class, String.class)), expectedName),
arguments(named(method4, findMethod(byte[].class)), expectedName));
}
private static Method findMethod(Class<?>... paramTypes) {
return ReflectionUtils.findMethod(CustomListener.class, "toUpperCase", paramTypes);
}
@SuppressWarnings("unused")
private static class CustomListener {
// @JmsListener(...)
String toUpperCase() {
return "ENIGMA";
}
// @JmsListener(...)
String toUpperCase(String input) {
return "ENIGMA";
}
// @JmsListener(...)
String toUpperCase(String input, String customHeader) {
return "ENIGMA";
}
// @JmsListener(...)
String toUpperCase(byte[] input) {
return "ENIGMA";
}
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2023 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.
@@ -17,8 +17,10 @@
package org.springframework.messaging.handler.invocation;
import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@@ -28,16 +30,14 @@ import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.function.Supplier;
import org.aopalliance.intercept.MethodInterceptor;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.target.EmptyTargetSource;
import org.springframework.cglib.core.SpringNamingPolicy;
import org.springframework.cglib.proxy.Callback;
import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.Factory;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.MethodIntrospector;
@@ -123,6 +123,7 @@ import static java.util.stream.Collectors.joining;
* </pre>
*
* @author Rossen Stoyanchev
* @author Juergen Hoeller
* @since 5.2
*/
public class ResolvableMethod {
@@ -189,7 +190,6 @@ public class ResolvableMethod {
/**
* Filter on method arguments with annotation.
* See {@link org.springframework.web.method.MvcAnnotationPredicates}.
*/
@SafeVarargs
public final ArgResolver annot(Predicate<MethodParameter>... filter) {
@@ -302,7 +302,6 @@ public class ResolvableMethod {
/**
* Filter on annotated methods.
* See {@link org.springframework.web.method.MvcAnnotationPredicates}.
*/
@SafeVarargs
public final Builder<T> annot(Predicate<Method>... filters) {
@@ -313,7 +312,6 @@ public class ResolvableMethod {
/**
* Filter on methods annotated with the given annotation type.
* @see #annot(Predicate[])
* See {@link org.springframework.web.method.MvcAnnotationPredicates}.
*/
@SafeVarargs
public final Builder<T> annotPresent(Class<? extends Annotation>... annotationTypes) {
@@ -530,7 +528,6 @@ public class ResolvableMethod {
/**
* Filter on method arguments with annotations.
* See {@link org.springframework.web.method.MvcAnnotationPredicates}.
*/
@SafeVarargs
public final ArgResolver annot(Predicate<MethodParameter>... filters) {
@@ -542,7 +539,6 @@ public class ResolvableMethod {
* Filter on method arguments that have the given annotations.
* @param annotationTypes the annotation types
* @see #annot(Predicate[])
* See {@link org.springframework.web.method.MvcAnnotationPredicates}.
*/
@SafeVarargs
public final ArgResolver annotPresent(Class<? extends Annotation>... annotationTypes) {
@@ -615,16 +611,10 @@ public class ResolvableMethod {
}
private static class MethodInvocationInterceptor
implements org.springframework.cglib.proxy.MethodInterceptor, MethodInterceptor {
private static class MethodInvocationInterceptor implements MethodInterceptor, InvocationHandler {
private Method invokedMethod;
Method getInvokedMethod() {
return this.invokedMethod;
}
@Override
@Nullable
public Object intercept(Object object, Method method, Object[] args, MethodProxy proxy) {
@@ -639,20 +629,23 @@ public class ResolvableMethod {
@Override
@Nullable
public Object invoke(org.aopalliance.intercept.MethodInvocation inv) throws Throwable {
return intercept(inv.getThis(), inv.getMethod(), inv.getArguments(), null);
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
return intercept(proxy, method, args, null);
}
Method getInvokedMethod() {
return this.invokedMethod;
}
}
@SuppressWarnings("unchecked")
private static <T> T initProxy(Class<?> type, MethodInvocationInterceptor interceptor) {
Assert.notNull(type, "'type' must not be null");
if (type.isInterface()) {
ProxyFactory factory = new ProxyFactory(EmptyTargetSource.INSTANCE);
factory.addInterface(type);
factory.addInterface(Supplier.class);
factory.addAdvice(interceptor);
return (T) factory.getProxy();
return (T) Proxy.newProxyInstance(type.getClassLoader(),
new Class<?>[] {type, Supplier.class},
interceptor);
}
else {
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2023 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.
@@ -62,6 +62,7 @@ import org.springframework.util.ConcurrentReferenceHashMap;
* @author Rod Johnson
* @author Oliver Gierke
* @author Mark Paluch
* @author Sam Brannen
* @since 2.0
* @see javax.persistence.PersistenceContext
* @see javax.persistence.PersistenceContextType#TRANSACTION
@@ -74,9 +75,9 @@ public abstract class SharedEntityManagerCreator {
private static final Map<Class<?>, Class<?>[]> cachedQueryInterfaces = new ConcurrentReferenceHashMap<>(4);
private static final Set<String> transactionRequiringMethods = new HashSet<>(8);
private static final Set<String> transactionRequiringMethods = new HashSet<>(6);
private static final Set<String> queryTerminatingMethods = new HashSet<>(8);
private static final Set<String> queryTerminatingMethods = new HashSet<>(9);
static {
transactionRequiringMethods.add("joinTransaction");
@@ -86,12 +87,15 @@ public abstract class SharedEntityManagerCreator {
transactionRequiringMethods.add("remove");
transactionRequiringMethods.add("refresh");
queryTerminatingMethods.add("execute"); // JPA 2.1 StoredProcedureQuery
queryTerminatingMethods.add("executeUpdate");
queryTerminatingMethods.add("getSingleResult");
queryTerminatingMethods.add("getResultStream");
queryTerminatingMethods.add("getResultList");
queryTerminatingMethods.add("list"); // Hibernate Query.list() method
queryTerminatingMethods.add("execute"); // javax.persistence.StoredProcedureQuery.execute()
queryTerminatingMethods.add("executeUpdate"); // javax.persistence.Query.executeUpdate()
queryTerminatingMethods.add("getSingleResult"); // javax.persistence.Query.getSingleResult()
queryTerminatingMethods.add("getResultStream"); // javax.persistence.Query.getResultStream()
queryTerminatingMethods.add("getResultList"); // javax.persistence.Query.getResultList()
queryTerminatingMethods.add("list"); // org.hibernate.query.Query.list()
queryTerminatingMethods.add("stream"); // org.hibernate.query.Query.stream()
queryTerminatingMethods.add("uniqueResult"); // org.hibernate.query.Query.uniqueResult()
queryTerminatingMethods.add("uniqueResultOptional"); // org.hibernate.query.Query.uniqueResultOptional()
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2023 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.
@@ -193,7 +193,7 @@ public class R2dbcTransactionManager extends AbstractReactiveTransactionManager
Mono<Connection> newCon = Mono.from(obtainConnectionFactory().create());
connectionMono = newCon.doOnNext(connection -> {
if (logger.isDebugEnabled()) {
logger.debug("Acquired Connection [" + newCon + "] for R2DBC transaction");
logger.debug("Acquired Connection [" + connection + "] for R2DBC transaction");
}
txObject.setConnectionHolder(new ConnectionHolder(connection), true);
});
@@ -325,31 +325,48 @@ public class R2dbcTransactionManager extends AbstractReactiveTransactionManager
Mono<Void> afterCleanup = Mono.empty();
if (txObject.isMustRestoreAutoCommit()) {
afterCleanup = afterCleanup.then(Mono.from(con.setAutoCommit(true)));
Mono<Void> restoreAutoCommitStep = safeCleanupStep(
"doCleanupAfterCompletion when restoring autocommit", Mono.from(con.setAutoCommit(true)));
afterCleanup = afterCleanup.then(restoreAutoCommitStep);
}
if (txObject.getPreviousIsolationLevel() != null) {
afterCleanup = afterCleanup
.then(Mono.from(con.setTransactionIsolationLevel(txObject.getPreviousIsolationLevel())));
Mono<Void> restoreIsolationStep = safeCleanupStep(
"doCleanupAfterCompletion when restoring isolation level",
Mono.from(con.setTransactionIsolationLevel(txObject.getPreviousIsolationLevel())));
afterCleanup = afterCleanup.then(restoreIsolationStep);
}
return afterCleanup.then(Mono.defer(() -> {
Mono<Void> releaseConnectionStep = Mono.defer(() -> {
try {
if (txObject.isNewConnectionHolder()) {
if (logger.isDebugEnabled()) {
logger.debug("Releasing R2DBC Connection [" + con + "] after transaction");
}
return ConnectionFactoryUtils.releaseConnection(con, obtainConnectionFactory());
return safeCleanupStep("doCleanupAfterCompletion when releasing R2DBC Connection",
ConnectionFactoryUtils.releaseConnection(con, obtainConnectionFactory()));
}
}
finally {
txObject.getConnectionHolder().clear();
}
return Mono.empty();
}));
});
return afterCleanup.then(releaseConnectionStep);
});
}
private Mono<Void> safeCleanupStep(String stepDescription, Mono<Void> stepMono) {
if (!logger.isDebugEnabled()) {
return stepMono.onErrorComplete();
}
else {
return stepMono.doOnError(e ->
logger.debug(String.format("Error ignored during %s: %s", stepDescription, e)))
.onErrorComplete();
}
}
/**
* Prepare the transactional {@link Connection} right before transaction begin.
* @deprecated in favor of {@link #prepareTransactionalConnection(Connection, TransactionDefinition)}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2023 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.
@@ -20,34 +20,18 @@ import java.util.function.Function;
import io.r2dbc.spi.Connection;
/**
* Union type combining {@link Function} and {@link SqlProvider} to expose the SQL that is
* related to the underlying action.
* related to the underlying action. The SqlProvider can support lazy / generate once semantics,
* in which case {@link #getSql()} can be {@code null} until the {@code #apply(Connection)}
* method is invoked.
*
* @author Mark Paluch
* @author Simon Baslé
* @since 5.3
* @param <R> the type of the result of the function.
*/
class ConnectionFunction<R> implements Function<Connection, R>, SqlProvider {
private final String sql;
private final Function<Connection, R> function;
ConnectionFunction(String sql, Function<Connection, R> function) {
this.sql = sql;
this.function = function;
}
@Override
public R apply(Connection t) {
return this.function.apply(t);
}
@Override
public String getSql() {
return this.sql;
}
interface ConnectionFunction<R> extends Function<Connection, R>, SqlProvider {
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2023 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.
@@ -79,7 +79,10 @@ public interface DatabaseClient extends ConnectionAccessor {
* the execution. The SQL string can contain either native parameter
* bind markers or named parameters (e.g. {@literal :foo, :bar}) when
* {@link NamedParameterExpander} is enabled.
* <p>Accepts {@link PreparedOperation} as SQL and binding {@link Supplier}
* <p>Accepts {@link PreparedOperation} as SQL and binding {@link Supplier}.
* <p>{@code DatabaseClient} implementations should defer the resolution of
* the SQL string as much as possible, ideally up to the point where a
* {@code Subscription} happens. This is the case for the default implementation.
* @param sqlSupplier a supplier for the SQL statement
* @return a new {@link GenericExecuteSpec}
* @see NamedParameterExpander
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2023 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.
@@ -60,6 +60,7 @@ import org.springframework.util.StringUtils;
* @author Mark Paluch
* @author Mingyuan Wu
* @author Bogdan Ilchyshyn
* @author Simon Baslé
* @since 5.3
*/
class DefaultDatabaseClient implements DatabaseClient {
@@ -158,11 +159,9 @@ class DefaultDatabaseClient implements DatabaseClient {
/**
* Release the {@link Connection}.
* @param connection to close.
* @return a {@link Publisher} that completes successfully when the connection is
* closed
* @return a {@link Publisher} that completes successfully when the connection is closed
*/
private Publisher<Void> closeConnection(Connection connection) {
return ConnectionFactoryUtils.currentConnectionFactory(
obtainConnectionFactory()).then().onErrorResume(Exception.class,
e -> Mono.from(connection.close()));
@@ -188,24 +187,22 @@ class DefaultDatabaseClient implements DatabaseClient {
new CloseSuppressingInvocationHandler(con));
}
private static Mono<Integer> sumRowsUpdated(
Function<Connection, Flux<Result>> resultFunction, Connection it) {
private static Mono<Integer> sumRowsUpdated(Function<Connection, Flux<Result>> resultFunction, Connection it) {
return resultFunction.apply(it)
.flatMap(Result::getRowsUpdated)
.collect(Collectors.summingInt(Integer::intValue));
}
/**
* Determine SQL from potential provider object.
* @param sqlProvider object that's potentially a SqlProvider
* Get SQL from a potential provider object.
* @param object an object that is potentially an SqlProvider
* @return the SQL string, or {@code null}
* @see SqlProvider
*/
@Nullable
private static String getSql(Object sqlProvider) {
if (sqlProvider instanceof SqlProvider) {
return ((SqlProvider) sqlProvider).getSql();
private static String getSql(Object object) {
if (object instanceof SqlProvider) {
return ((SqlProvider) object).getSql();
}
else {
return null;
@@ -214,7 +211,7 @@ class DefaultDatabaseClient implements DatabaseClient {
/**
* Base class for {@link DatabaseClient.GenericExecuteSpec} implementations.
* Default {@link DatabaseClient.GenericExecuteSpec} implementation.
*/
class DefaultGenericExecuteSpec implements GenericExecuteSpec {
@@ -322,9 +319,8 @@ class DefaultDatabaseClient implements DatabaseClient {
return fetch().rowsUpdated().then();
}
private <T> FetchSpec<T> execute(Supplier<String> sqlSupplier, BiFunction<Row, RowMetadata, T> mappingFunction) {
String sql = getRequiredSql(sqlSupplier);
Function<Connection, Statement> statementFunction = connection -> {
private ResultFunction getResultFunction(Supplier<String> sqlSupplier) {
BiFunction<Connection, String, Statement> statementFunction = (connection, sql) -> {
if (logger.isDebugEnabled()) {
logger.debug("Executing SQL statement [" + sql + "]");
}
@@ -370,16 +366,16 @@ class DefaultDatabaseClient implements DatabaseClient {
return statement;
};
Function<Connection, Flux<Result>> resultFunction = connection -> {
Statement statement = statementFunction.apply(connection);
return Flux.from(this.filterFunction.filter(statement, DefaultDatabaseClient.this.executeFunction))
.cast(Result.class).checkpoint("SQL \"" + sql + "\" [DatabaseClient]");
};
return new ResultFunction(sqlSupplier, statementFunction, this.filterFunction, DefaultDatabaseClient.this.executeFunction);
}
private <T> FetchSpec<T> execute(Supplier<String> sqlSupplier, BiFunction<Row, RowMetadata, T> mappingFunction) {
ResultFunction resultHandler = getResultFunction(sqlSupplier);
return new DefaultFetchSpec<>(
DefaultDatabaseClient.this, sql,
new ConnectionFunction<>(sql, resultFunction),
new ConnectionFunction<>(sql, connection -> sumRowsUpdated(resultFunction, connection)),
DefaultDatabaseClient.this,
resultHandler,
connection -> sumRowsUpdated(resultHandler, connection),
mappingFunction);
}
@@ -505,12 +501,11 @@ class DefaultDatabaseClient implements DatabaseClient {
private static final long serialVersionUID = -8994138383301201380L;
final Connection connection;
final transient Connection connection;
final Function<Connection, Publisher<Void>> closeFunction;
final transient Function<Connection, Publisher<Void>> closeFunction;
ConnectionCloseHolder(Connection connection,
Function<Connection, Publisher<Void>> closeFunction) {
ConnectionCloseHolder(Connection connection, Function<Connection, Publisher<Void>> closeFunction) {
this.connection = connection;
this.closeFunction = closeFunction;
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2023 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.
@@ -20,7 +20,6 @@ import java.util.function.BiFunction;
import java.util.function.Function;
import io.r2dbc.spi.Connection;
import io.r2dbc.spi.Result;
import io.r2dbc.spi.Row;
import io.r2dbc.spi.RowMetadata;
import reactor.core.publisher.Flux;
@@ -32,6 +31,7 @@ import org.springframework.dao.IncorrectResultSizeDataAccessException;
* Default {@link FetchSpec} implementation.
*
* @author Mark Paluch
* @author Simon Baslé
* @since 5.3
* @param <T> the row result type
*/
@@ -39,24 +39,21 @@ class DefaultFetchSpec<T> implements FetchSpec<T> {
private final ConnectionAccessor connectionAccessor;
private final String sql;
private final Function<Connection, Flux<Result>> resultFunction;
private final ResultFunction resultFunction;
private final Function<Connection, Mono<Integer>> updatedRowsFunction;
private final BiFunction<Row, RowMetadata, T> mappingFunction;
DefaultFetchSpec(ConnectionAccessor connectionAccessor, String sql,
Function<Connection, Flux<Result>> resultFunction,
DefaultFetchSpec(ConnectionAccessor connectionAccessor,
ResultFunction resultFunction,
Function<Connection, Mono<Integer>> updatedRowsFunction,
BiFunction<Row, RowMetadata, T> mappingFunction) {
this.sql = sql;
this.connectionAccessor = connectionAccessor;
this.resultFunction = resultFunction;
this.updatedRowsFunction = updatedRowsFunction;
this.updatedRowsFunction = new DelegateConnectionFunction<>(resultFunction, updatedRowsFunction);
this.mappingFunction = mappingFunction;
}
@@ -70,7 +67,7 @@ class DefaultFetchSpec<T> implements FetchSpec<T> {
}
if (list.size() > 1) {
return Mono.error(new IncorrectResultSizeDataAccessException(
String.format("Query [%s] returned non unique result.", this.sql),
String.format("Query [%s] returned non unique result.", this.resultFunction.getSql()),
1));
}
return Mono.just(list.get(0));
@@ -84,7 +81,7 @@ class DefaultFetchSpec<T> implements FetchSpec<T> {
@Override
public Flux<T> all() {
return this.connectionAccessor.inConnectionMany(new ConnectionFunction<>(this.sql,
return this.connectionAccessor.inConnectionMany(new DelegateConnectionFunction<>(this.resultFunction,
connection -> this.resultFunction.apply(connection)
.flatMap(result -> result.map(this.mappingFunction))));
}
@@ -0,0 +1,56 @@
/*
* Copyright 2002-2023 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.r2dbc.core;
import java.util.function.Function;
import io.r2dbc.spi.Connection;
import org.springframework.lang.Nullable;
/**
* A {@link ConnectionFunction} that delegates to a {@code SqlProvider} and a plain
* {@code Function}.
*
* @author Simon Baslé
* @since 5.3.26
* @param <R> the type of the result of the function.
*/
final class DelegateConnectionFunction<R> implements ConnectionFunction<R> {
private final SqlProvider sql;
private final Function<Connection, R> function;
DelegateConnectionFunction(SqlProvider sql, Function<Connection, R> function) {
this.sql = sql;
this.function = function;
}
@Override
public R apply(Connection t) {
return this.function.apply(t);
}
@Nullable
@Override
public String getSql() {
return this.sql.getSql();
}
}
@@ -0,0 +1,74 @@
/*
* Copyright 2002-2023 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.r2dbc.core;
import java.util.function.BiFunction;
import java.util.function.Supplier;
import io.r2dbc.spi.Connection;
import io.r2dbc.spi.Result;
import io.r2dbc.spi.Statement;
import reactor.core.publisher.Flux;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* A {@link ConnectionFunction} that produces a {@code Flux} of {@link Result} and that
* defers generation of the SQL until the function has been applied.
* Beforehand, the {@code getSql()} method simply returns {@code null}. The sql String is
* also memoized during application, so that subsequent calls to {@link #getSql()} return
* the same {@code String} without further calls to the {@code Supplier}.
*
* @author Mark Paluch
* @author Simon Baslé
* @since 5.3.26
*/
final class ResultFunction implements ConnectionFunction<Flux<Result>> {
final Supplier<String> sqlSupplier;
final BiFunction<Connection, String, Statement> statementFunction;
final StatementFilterFunction filterFunction;
final ExecuteFunction executeFunction;
@Nullable
String resolvedSql = null;
ResultFunction(Supplier<String> sqlSupplier, BiFunction<Connection, String, Statement> statementFunction, StatementFilterFunction filterFunction, ExecuteFunction executeFunction) {
this.sqlSupplier = sqlSupplier;
this.statementFunction = statementFunction;
this.filterFunction = filterFunction;
this.executeFunction = executeFunction;
}
@Override
public Flux<Result> apply(Connection connection) {
String sql = this.sqlSupplier.get();
Assert.state(StringUtils.hasText(sql), "SQL returned by supplier must not be empty");
this.resolvedSql = sql;
Statement statement = this.statementFunction.apply(connection, sql);
return Flux.from(this.filterFunction.filter(statement, this.executeFunction))
.cast(Result.class).checkpoint("SQL \"" + sql + "\" [DatabaseClient]");
}
@Nullable
@Override
public String getSql() {
return this.resolvedSql;
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2023 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.
@@ -22,12 +22,15 @@ import io.r2dbc.spi.Connection;
import io.r2dbc.spi.ConnectionFactory;
import io.r2dbc.spi.IsolationLevel;
import io.r2dbc.spi.R2dbcBadGrammarException;
import io.r2dbc.spi.R2dbcTimeoutException;
import io.r2dbc.spi.Statement;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import org.springframework.r2dbc.BadSqlGrammarException;
import org.springframework.transaction.CannotCreateTransactionException;
import org.springframework.transaction.IllegalTransactionStateException;
import org.springframework.transaction.TransactionDefinition;
@@ -319,6 +322,34 @@ class R2dbcTransactionManagerUnitTests {
verifyNoMoreInteractions(connectionMock);
}
@Test
@SuppressWarnings("unchecked")
void testConnectionReleasedWhenRollbackFails() {
when(connectionMock.rollbackTransaction()).thenReturn(Mono.defer(() -> Mono.error(new R2dbcBadGrammarException("Rollback should fail"))), Mono.empty());
TransactionalOperator operator = TransactionalOperator.create(tm);
when(connectionMock.isAutoCommit()).thenReturn(true);
when(connectionMock.setAutoCommit(true)).thenReturn(Mono.defer(() -> Mono.error(new R2dbcTimeoutException("SET AUTOCOMMIT = 1 timed out"))));
when(connectionMock.setTransactionIsolationLevel(any())).thenReturn(Mono.empty());
when(connectionMock.setAutoCommit(false)).thenReturn(Mono.empty());
operator.execute(reactiveTransaction -> ConnectionFactoryUtils.getConnection(connectionFactoryMock)
.doOnNext(connection -> {
throw new IllegalStateException("Intentional error to trigger rollback");
}).then()).as(StepVerifier::create)
.verifyErrorSatisfies(e -> Assertions.assertThat(e)
.isInstanceOf(BadSqlGrammarException.class)
.hasCause(new R2dbcBadGrammarException("Rollback should fail"))
);
verify(connectionMock).isAutoCommit();
verify(connectionMock).beginTransaction();
verify(connectionMock, never()).commitTransaction();
verify(connectionMock).rollbackTransaction();
verify(connectionMock).close();
}
@Test
void testTransactionSetRollbackOnly() {
when(connectionMock.rollbackTransaction()).thenReturn(Mono.empty());
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2023 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.
@@ -17,6 +17,8 @@
package org.springframework.r2dbc.core;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import io.r2dbc.spi.Connection;
import io.r2dbc.spi.ConnectionFactory;
@@ -64,6 +66,7 @@ import static org.mockito.BDDMockito.when;
* @author Mark Paluch
* @author Ferdinand Jacobs
* @author Jens Schauder
* @author Simon Baslé
*/
@ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.LENIENT)
@@ -397,6 +400,47 @@ class DefaultDatabaseClientUnitTests {
inOrder.verifyNoMoreInteractions();
}
@Test
void sqlSupplierInvocationIsDeferredUntilSubscription() {
// We'll have either 2 or 3 rows, depending on the subscription and the generated SQL
MockRowMetadata metadata = MockRowMetadata.builder().columnMetadata(
MockColumnMetadata.builder().name("id").javaType(Integer.class).build()).build();
final MockRow row1 = MockRow.builder().identified("id", Integer.class, 1).build();
final MockRow row2 = MockRow.builder().identified("id", Integer.class, 2).build();
final MockRow row3 = MockRow.builder().identified("id", Integer.class, 3).build();
// Set up 2 mock statements
mockStatementFor("SELECT id FROM test WHERE id < '3'", MockResult.builder()
.rowMetadata(metadata)
.row(row1, row2).build());
mockStatementFor("SELECT id FROM test WHERE id < '4'", MockResult.builder()
.rowMetadata(metadata)
.row(row1, row2, row3).build());
// Create the client
DatabaseClient databaseClient = this.databaseClientBuilder.build();
AtomicInteger invoked = new AtomicInteger();
// Assemble a publisher, but don't subscribe yet
Mono<List<Integer>> operation = databaseClient
.sql(() -> {
int idMax = 2 + invoked.incrementAndGet();
return String.format("SELECT id FROM test WHERE id < '%s'", idMax);
})
.map(r -> r.get("id", Integer.class))
.all()
.collectList();
assertThat(invoked).as("invoked (before subscription)").hasValue(0);
List<Integer> rows = operation.block();
assertThat(invoked).as("invoked (after 1st subscription)").hasValue(1);
assertThat(rows).containsExactly(1, 2);
rows = operation.block();
assertThat(invoked).as("invoked (after 2nd subscription)").hasValue(2);
assertThat(rows).containsExactly(1, 2, 3);
}
private Statement mockStatement() {
return mockStatementFor(null, null);
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2023 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.
@@ -70,14 +70,15 @@ import java.lang.annotation.Target;
* class ExampleIntegrationTests {
*
* &#064;Container
* static RedisContainer redis = new RedisContainer();
* static GenericContainer redis =
* new GenericContainer("redis:5.0.3-alpine").withExposedPorts(6379);
*
* // ...
*
* &#064;DynamicPropertySource
* static void redisProperties(DynamicPropertyRegistry registry) {
* registry.add("redis.host", redis::getContainerIpAddress);
* registry.add("redis.port", redis::getMappedPort);
* registry.add("redis.host", redis::getHost);
* registry.add("redis.port", redis::getFirstMappedPort);
* }
*
* }</pre>
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2023 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.
@@ -47,6 +47,7 @@ import static org.springframework.test.util.AssertionErrors.fail;
* @author Craig Walls
* @author Rossen Stoyanchev
* @author Sam Brannen
* @author Simon Baslé
* @since 3.2
*/
public abstract class MockRestRequestMatchers {
@@ -112,8 +113,49 @@ public abstract class MockRestRequestMatchers {
return request -> assertEquals("Unexpected request", uri, request.getURI());
}
/**
* Assert request query parameter values with the given Hamcrest matcher,
* matching on the entire {@link List} of values.
* <p>For example, this can be used to check that the list of query parameter
* values has at least one value matching a given Hamcrest matcher (such as
* {@link org.hamcrest.Matchers#hasItem(Matcher)}), that every value in the list
* matches common criteria (such as {@link org.hamcrest.Matchers#everyItem(Matcher)}),
* that each value in the list matches corresponding dedicated criteria
* (such as {@link org.hamcrest.Matchers#contains(Matcher[])}), etc.
* @param name the name of the query parameter whose value(s) will be asserted
* @param matcher the Hamcrest matcher to apply to the entire list of values
* for the given query parameter
* @since 5.3.26
* @see #queryParam(String, Matcher...)
* @see #queryParam(String, String...)
*/
public static RequestMatcher queryParam(String name, Matcher<? super List<String>> matcher) {
return request -> {
MultiValueMap<String, String> params = getQueryParams(request);
List<String> paramValues = params.get(name);
if (paramValues == null) {
fail("Expected query param <" + name + "> to exist but was null");
}
assertThat("Query param [" + name + "] values", paramValues, matcher);
};
}
/**
* Assert request query parameter values with the given Hamcrest matcher(s).
* <p>If the query parameter value list is larger than the number of provided
* {@code matchers}, no matchers will be applied to the extra query parameter
* values, effectively ignoring the additional parameter values. If the number
* of provided {@code matchers} exceeds the number of query parameter values,
* an {@link AssertionError} will be thrown to signal the mismatch.
* <p>See {@link #queryParam(String, Matcher)} for a variant which accepts a
* {@code Matcher} that applies to the entire list of values as opposed to
* applying only to individual values.
* @param name the name of the query parameter whose value(s) will be asserted
* @param matchers the Hamcrest matchers to apply to individual query parameter
* values; the n<sup>th</sup> matcher is applied to the n<sup>th</sup> query
* parameter value
* @see #queryParam(String, Matcher)
* @see #queryParam(String, String...)
*/
@SafeVarargs
public static RequestMatcher queryParam(String name, Matcher<? super String>... matchers) {
@@ -128,6 +170,20 @@ public abstract class MockRestRequestMatchers {
/**
* Assert request query parameter values.
* <p>If the query parameter value list is larger than the number of
* {@code expectedValues}, no assertions will be applied to the extra query
* parameter values, effectively ignoring the additional parameter values. If
* the number of {@code expectedValues} exceeds the number of query parameter
* values, an {@link AssertionError} will be thrown to signal the mismatch.
* <p>See {@link #queryParam(String, Matcher)} for a variant which accepts a
* Hamcrest {@code Matcher} that applies to the entire list of values as opposed
* to asserting only individual values.
* @param name the name of the query parameter whose value(s) will be asserted
* @param expectedValues the expected values of individual query parameter values;
* the n<sup>th</sup> expected value is compared to the n<sup>th</sup> query
* parameter value
* @see #queryParam(String, Matcher)
* @see #queryParam(String, Matcher...)
*/
public static RequestMatcher queryParam(String name, String... expectedValues) {
return request -> {
@@ -143,21 +199,47 @@ public abstract class MockRestRequestMatchers {
return UriComponentsBuilder.fromUri(request.getURI()).build().getQueryParams();
}
private static void assertValueCount(
String valueType, String name, MultiValueMap<String, String> map, int count) {
List<String> values = map.get(name);
String message = "Expected " + valueType + " <" + name + ">";
if (values == null) {
fail(message + " to exist but was null");
}
if (count > values.size()) {
fail(message + " to have at least <" + count + "> values but found " + values);
}
/**
* Assert request header values with the given Hamcrest matcher, matching on
* the entire {@link List} of values.
* <p>For example, this can be used to check that the list of header values
* has at least one value matching a given Hamcrest matcher (such as
* {@link org.hamcrest.Matchers#hasItem(Matcher)}), that every value in the list
* matches common criteria (such as {@link org.hamcrest.Matchers#everyItem(Matcher)}),
* that each value in the list matches corresponding dedicated criteria
* (such as {@link org.hamcrest.Matchers#contains(Matcher[])}), etc.
* @param name the name of the header whose value(s) will be asserted
* @param matcher the Hamcrest matcher to apply to the entire list of values
* for the given header
* @since 5.3.26
* @see #header(String, Matcher...)
* @see #header(String, String...)
*/
public static RequestMatcher header(String name, Matcher<? super List<String>> matcher) {
return request -> {
List<String> headerValues = request.getHeaders().get(name);
if (headerValues == null) {
fail("Expected header <" + name + "> to exist but was null");
}
assertThat("Request header [" + name + "] values", headerValues, matcher);
};
}
/**
* Assert request header values with the given Hamcrest matcher(s).
* <p>If the header value list is larger than the number of provided
* {@code matchers}, no matchers will be applied to the extra header values,
* effectively ignoring the additional header values. If the number of
* provided {@code matchers} exceeds the number of header values, an
* {@link AssertionError} will be thrown to signal the mismatch.
* <p>See {@link #header(String, Matcher)} for a variant which accepts a
* Hamcrest {@code Matcher} that applies to the entire list of values as
* opposed to applying only to individual values.
* @param name the name of the header whose value(s) will be asserted
* @param matchers the Hamcrest matchers to apply to individual header values;
* the n<sup>th</sup> matcher is applied to the n<sup>th</sup> header value
* @see #header(String, Matcher)
* @see #header(String, String...)
*/
@SafeVarargs
public static RequestMatcher header(String name, Matcher<? super String>... matchers) {
@@ -173,6 +255,19 @@ public abstract class MockRestRequestMatchers {
/**
* Assert request header values.
* <p>If the header value list is larger than the number of {@code expectedValues},
* no matchers will be applied to the extra header values, effectively ignoring the
* additional header values. If the number of {@code expectedValues} exceeds the
* number of header values, an {@link AssertionError} will be thrown to signal the
* mismatch.
* <p>See {@link #header(String, Matcher)} for a variant which accepts a
* Hamcrest {@code Matcher} that applies to the entire list of values as
* opposed to applying only to individual values.
* @param name the name of the header whose value(s) will be asserted
* @param expectedValues the expected values of individual header values; the
* n<sup>th</sup> expected value is compared to the n<sup>th</sup> header value
* @see #header(String, Matcher)
* @see #header(String, Matcher...)
*/
public static RequestMatcher header(String name, String... expectedValues) {
return request -> {
@@ -258,4 +353,18 @@ public abstract class MockRestRequestMatchers {
return new XpathRequestMatchers(expression, namespaces, args);
}
private static void assertValueCount(
String valueType, String name, MultiValueMap<String, String> map, int count) {
List<String> values = map.get(name);
String message = "Expected " + valueType + " <" + name + ">";
if (values == null) {
fail(message + " to exist but was null");
}
if (count > values.size()) {
fail(message + " to have at least <" + count + "> values but found " + values);
}
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2023 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.
@@ -68,6 +68,8 @@ public final class MockMvcWebConnection implements WebConnection {
private WebClient webClient;
private static int MAX_FORWARDS = 100;
/**
* Create a new instance that assumes the context path of the application
@@ -133,10 +135,15 @@ public final class MockMvcWebConnection implements WebConnection {
MockHttpServletResponse httpServletResponse = getResponse(requestBuilder);
String forwardedUrl = httpServletResponse.getForwardedUrl();
while (forwardedUrl != null) {
int forwards = 0;
while (forwardedUrl != null && forwards < MAX_FORWARDS) {
requestBuilder.setForwardPostProcessor(new ForwardRequestPostProcessor(forwardedUrl));
httpServletResponse = getResponse(requestBuilder);
forwardedUrl = httpServletResponse.getForwardedUrl();
forwards += 1;
}
if (forwards == MAX_FORWARDS) {
throw new IllegalStateException("Forwarded more than " + forwards + " times in a row, potential infinite forward loop");
}
storeCookies(webRequest, httpServletResponse.getCookies());
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2023 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.
@@ -83,7 +83,7 @@ public class XpathResultMatchers {
}
/**
* Get the response encoding if explicitly defined in the response, {code null} otherwise.
* Get the response encoding if explicitly defined in the response, {@code null} otherwise.
*/
@Nullable
private String getDefinedEncoding(MockHttpServletResponse response) {
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2023 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.
@@ -224,6 +224,18 @@ fun MockMvc.multipart(urlTemplate: String, vararg vars: Any?, dsl: MockMultipart
return MockMultipartHttpServletRequestDsl(requestBuilder).apply(dsl).perform(this)
}
/**
* [MockMvc] extension providing access to [MockMultipartHttpServletRequestDsl] Kotlin DSL.
*
* @see MockMvcRequestBuilders.multipart
* @author Sebastien Deleuze
* @since 5.3.26
*/
fun MockMvc.multipart(httpMethod: HttpMethod, urlTemplate: String, vararg vars: Any?, dsl: MockMultipartHttpServletRequestDsl.() -> Unit = {}): ResultActionsDsl {
val requestBuilder = MockMvcRequestBuilders.multipart(httpMethod, urlTemplate, *vars)
return MockMultipartHttpServletRequestDsl(requestBuilder).apply(dsl).perform(this)
}
/**
* [MockMvc] extension providing access to [MockMultipartHttpServletRequestDsl] Kotlin DSL.
*
@@ -235,3 +247,15 @@ fun MockMvc.multipart(uri: URI, dsl: MockMultipartHttpServletRequestDsl.() -> Un
val requestBuilder = MockMvcRequestBuilders.multipart(uri)
return MockMultipartHttpServletRequestDsl(requestBuilder).apply(dsl).perform(this)
}
/**
* [MockMvc] extension providing access to [MockMultipartHttpServletRequestDsl] Kotlin DSL.
*
* @see MockMvcRequestBuilders.multipart
* @author Sebastien Deleuze
* @since 5.3.26
*/
fun MockMvc.multipart(httpMethod: HttpMethod, uri: URI, dsl: MockMultipartHttpServletRequestDsl.() -> Unit = {}): ResultActionsDsl {
val requestBuilder = MockMvcRequestBuilders.multipart(httpMethod, uri)
return MockMultipartHttpServletRequestDsl(requestBuilder).apply(dsl).perform(this)
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2023 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.
@@ -16,18 +16,33 @@
package org.springframework.test.web.client.match;
import java.io.IOException;
import java.net.URI;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.assertj.core.api.ThrowableTypeAssert;
import org.junit.jupiter.api.Test;
import org.springframework.http.HttpMethod;
import org.springframework.mock.http.client.MockClientHttpRequest;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.any;
import static org.hamcrest.Matchers.anything;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.either;
import static org.hamcrest.Matchers.endsWith;
import static org.hamcrest.Matchers.everyItem;
import static org.hamcrest.Matchers.hasItem;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.Matchers.nullValue;
import static org.hamcrest.Matchers.startsWith;
/**
* Unit tests for {@link MockRestRequestMatchers}.
@@ -35,179 +50,307 @@ import static org.hamcrest.Matchers.containsString;
* @author Craig Walls
* @author Rossen Stoyanchev
* @author Sam Brannen
* @author Simon Baslé
*/
public class MockRestRequestMatchersTests {
class MockRestRequestMatchersTests {
private final MockClientHttpRequest request = new MockClientHttpRequest();
@Test
public void requestTo() throws Exception {
void requestTo() throws Exception {
this.request.setURI(new URI("http://www.foo.example/bar"));
MockRestRequestMatchers.requestTo("http://www.foo.example/bar").match(this.request);
}
@Test // SPR-15819
public void requestToUriTemplate() throws Exception {
void requestToUriTemplate() throws Exception {
this.request.setURI(new URI("http://www.foo.example/bar"));
MockRestRequestMatchers.requestToUriTemplate("http://www.foo.example/{bar}", "bar").match(this.request);
}
@Test
public void requestToNoMatch() throws Exception {
void requestToNoMatch() throws Exception {
this.request.setURI(new URI("http://www.foo.example/bar"));
assertThatThrownBy(
() -> MockRestRequestMatchers.requestTo("http://www.foo.example/wrong").match(this.request))
.isInstanceOf(AssertionError.class);
assertThatAssertionError()
.isThrownBy(() -> MockRestRequestMatchers.requestTo("http://www.foo.example/wrong").match(this.request));
}
@Test
public void requestToContains() throws Exception {
void requestToContains() throws Exception {
this.request.setURI(new URI("http://www.foo.example/bar"));
MockRestRequestMatchers.requestTo(containsString("bar")).match(this.request);
}
@Test
public void method() throws Exception {
void method() throws Exception {
this.request.setMethod(HttpMethod.GET);
MockRestRequestMatchers.method(HttpMethod.GET).match(this.request);
}
@Test
public void methodNoMatch() throws Exception {
void methodNoMatch() throws Exception {
this.request.setMethod(HttpMethod.POST);
assertThatThrownBy(() -> MockRestRequestMatchers.method(HttpMethod.GET).match(this.request))
.isInstanceOf(AssertionError.class)
.hasMessageContaining("expected:<GET> but was:<POST>");
assertThatAssertionError()
.isThrownBy(() -> MockRestRequestMatchers.method(HttpMethod.GET).match(this.request))
.withMessageContaining("expected:<GET> but was:<POST>");
}
@Test
public void header() throws Exception {
void header() throws Exception {
this.request.getHeaders().put("foo", Arrays.asList("bar", "baz"));
MockRestRequestMatchers.header("foo", "bar", "baz").match(this.request);
}
@Test
public void headerDoesNotExist() throws Exception {
void headerDoesNotExist() throws Exception {
MockRestRequestMatchers.headerDoesNotExist(null).match(this.request);
MockRestRequestMatchers.headerDoesNotExist("").match(this.request);
MockRestRequestMatchers.headerDoesNotExist("foo").match(this.request);
List<String> values = Arrays.asList("bar", "baz");
this.request.getHeaders().put("foo", values);
assertThatThrownBy(() -> MockRestRequestMatchers.headerDoesNotExist("foo").match(this.request))
.isInstanceOf(AssertionError.class)
.hasMessage("Expected header <foo> not to exist, but it exists with values: " + values);
assertThatAssertionError()
.isThrownBy(() -> MockRestRequestMatchers.headerDoesNotExist("foo").match(this.request))
.withMessage("Expected header <foo> not to exist, but it exists with values: " + values);
}
@Test
public void headerMissing() throws Exception {
assertThatThrownBy(() -> MockRestRequestMatchers.header("foo", "bar").match(this.request))
.isInstanceOf(AssertionError.class)
.hasMessageContaining("was null");
void headerMissing() {
assertThatAssertionError()
.isThrownBy(() -> MockRestRequestMatchers.header("foo", "bar").match(this.request))
.withMessageContaining("was null");
}
@Test
public void headerMissingValue() throws Exception {
void headerMissingValue() {
this.request.getHeaders().put("foo", Arrays.asList("bar", "baz"));
assertThatThrownBy(() -> MockRestRequestMatchers.header("foo", "bad").match(this.request))
.isInstanceOf(AssertionError.class)
.hasMessageContaining("expected:<bad> but was:<bar>");
assertThatAssertionError()
.isThrownBy(() -> MockRestRequestMatchers.header("foo", "bad").match(this.request))
.withMessageContaining("expected:<bad> but was:<bar>");
}
@Test
public void headerContains() throws Exception {
void headerContains() throws Exception {
this.request.getHeaders().put("foo", Arrays.asList("bar", "baz"));
MockRestRequestMatchers.header("foo", containsString("ba")).match(this.request);
}
@Test
public void headerContainsWithMissingHeader() throws Exception {
assertThatThrownBy(() -> MockRestRequestMatchers.header("foo", containsString("baz")).match(this.request))
.isInstanceOf(AssertionError.class)
.hasMessageContaining("but was null");
void headerContainsWithMissingHeader() {
assertThatAssertionError()
.isThrownBy(() -> MockRestRequestMatchers.header("foo", containsString("baz")).match(this.request))
.withMessage("Expected header <foo> to exist but was null");
}
@Test
public void headerContainsWithMissingValue() throws Exception {
void headerContainsWithMissingValue() {
this.request.getHeaders().put("foo", Arrays.asList("bar", "baz"));
assertThatThrownBy(() -> MockRestRequestMatchers.header("foo", containsString("bx")).match(this.request))
.isInstanceOf(AssertionError.class)
.hasMessageContaining("was \"bar\"");
assertThatAssertionError()
.isThrownBy(() -> MockRestRequestMatchers.header("foo", containsString("bx")).match(this.request))
.withMessageContaining("was \"bar\"");
}
@Test
public void headers() throws Exception {
void headerListMissing() {
assertThatAssertionError()
.isThrownBy(() -> MockRestRequestMatchers.header("foo", hasSize(2)).match(this.request))
.withMessage("Expected header <foo> to exist but was null");
}
@Test
void headerListMatchers() throws IOException {
this.request.getHeaders().put("foo", Arrays.asList("bar", "baz"));
MockRestRequestMatchers.header("foo", containsInAnyOrder(endsWith("baz"), endsWith("bar"))).match(this.request);
MockRestRequestMatchers.header("foo", contains(is("bar"), is("baz"))).match(this.request);
MockRestRequestMatchers.header("foo", contains(is("bar"), anything())).match(this.request);
MockRestRequestMatchers.header("foo", hasItem(endsWith("baz"))).match(this.request);
MockRestRequestMatchers.header("foo", everyItem(startsWith("ba"))).match(this.request);
MockRestRequestMatchers.header("foo", hasSize(2)).match(this.request);
// These can be a bit ambiguous when reading the test (the compiler selects the list matcher):
MockRestRequestMatchers.header("foo", notNullValue()).match(this.request);
MockRestRequestMatchers.header("foo", is(anything())).match(this.request);
MockRestRequestMatchers.header("foo", allOf(notNullValue(), notNullValue())).match(this.request);
// These are not as ambiguous thanks to an inner matcher that is either obviously list-oriented,
// string-oriented, or obviously a vararg of matchers
// list matcher version
MockRestRequestMatchers.header("foo", allOf(notNullValue(), hasSize(2))).match(this.request);
// vararg version
MockRestRequestMatchers.header("foo", allOf(notNullValue(), endsWith("ar"))).match(this.request);
MockRestRequestMatchers.header("foo", is((any(String.class)))).match(this.request);
MockRestRequestMatchers.header("foo", either(is("bar")).or(is(nullValue()))).match(this.request);
MockRestRequestMatchers.header("foo", is(notNullValue()), is(notNullValue())).match(this.request);
}
@Test
void headerListContainsMismatch() {
this.request.getHeaders().put("foo", Arrays.asList("bar", "baz"));
assertThatAssertionError()
.isThrownBy(() -> MockRestRequestMatchers.header("foo", contains(containsString("ba"))).match(this.request))
.withMessageContainingAll(
"Request header [foo] values",
"Expected: iterable containing [a string containing \"ba\"]",
"but: not matched: \"baz\"");
assertThatAssertionError()
.isThrownBy(() -> MockRestRequestMatchers.header("foo", hasItem(endsWith("ba"))).match(this.request))
.withMessageContainingAll(
"Request header [foo] values",
"Expected: a collection containing a string ending with \"ba\"",
"but: mismatches were: [was \"bar\", was \"baz\"]");
assertThatAssertionError()
.isThrownBy(() -> MockRestRequestMatchers.header("foo", everyItem(endsWith("ar"))).match(this.request))
.withMessageContainingAll(
"Request header [foo] values",
"Expected: every item is a string ending with \"ar\"",
"but: an item was \"baz\"");
}
@Test
void headers() throws Exception {
this.request.getHeaders().put("foo", Arrays.asList("bar", "baz"));
MockRestRequestMatchers.header("foo", "bar", "baz").match(this.request);
}
@Test
public void headersWithMissingHeader() throws Exception {
assertThatThrownBy(() -> MockRestRequestMatchers.header("foo", "bar").match(this.request))
.isInstanceOf(AssertionError.class)
.hasMessageContaining("but was null");
void headersWithMissingHeader() {
assertThatAssertionError()
.isThrownBy(() -> MockRestRequestMatchers.header("foo", "bar").match(this.request))
.withMessage("Expected header <foo> to exist but was null");
}
@Test
public void headersWithMissingValue() throws Exception {
this.request.getHeaders().put("foo", Collections.singletonList("bar"));
void headersWithMissingValue() {
this.request.getHeaders().put("foo", Arrays.asList("bar"));
assertThatThrownBy(() -> MockRestRequestMatchers.header("foo", "bar", "baz").match(this.request))
.isInstanceOf(AssertionError.class)
.hasMessageContaining("to have at least <2> values");
assertThatAssertionError()
.isThrownBy(() -> MockRestRequestMatchers.header("foo", "bar", "baz").match(this.request))
.withMessageContaining("to have at least <2> values");
}
@Test
public void queryParam() throws Exception {
void queryParam() throws Exception {
this.request.setURI(new URI("http://www.foo.example/a?foo=bar&foo=baz"));
MockRestRequestMatchers.queryParam("foo", "bar", "baz").match(this.request);
}
@Test
public void queryParamMissing() throws Exception {
void queryParamMissing() throws Exception {
this.request.setURI(new URI("http://www.foo.example/a"));
assertThatThrownBy(() -> MockRestRequestMatchers.queryParam("foo", "bar").match(this.request))
.isInstanceOf(AssertionError.class)
.hasMessageContaining("but was null");
assertThatAssertionError()
.isThrownBy(() -> MockRestRequestMatchers.queryParam("foo", "bar").match(this.request))
.withMessage("Expected query param <foo> to exist but was null");
}
@Test
public void queryParamMissingValue() throws Exception {
void queryParamMissingValue() throws Exception {
this.request.setURI(new URI("http://www.foo.example/a?foo=bar&foo=baz"));
assertThatThrownBy(() -> MockRestRequestMatchers.queryParam("foo", "bad").match(this.request))
.isInstanceOf(AssertionError.class)
.hasMessageContaining("expected:<bad> but was:<bar>");
assertThatAssertionError()
.isThrownBy(() -> MockRestRequestMatchers.queryParam("foo", "bad").match(this.request))
.withMessageContaining("expected:<bad> but was:<bar>");
}
@Test
public void queryParamContains() throws Exception {
void queryParamContains() throws Exception {
this.request.setURI(new URI("http://www.foo.example/a?foo=bar&foo=baz"));
MockRestRequestMatchers.queryParam("foo", containsString("ba")).match(this.request);
}
@Test
public void queryParamContainsWithMissingValue() throws Exception {
void queryParamContainsWithMissingValue() throws Exception {
this.request.setURI(new URI("http://www.foo.example/a?foo=bar&foo=baz"));
assertThatThrownBy(() -> MockRestRequestMatchers.queryParam("foo", containsString("bx")).match(this.request))
.isInstanceOf(AssertionError.class)
.hasMessageContaining("was \"bar\"");
assertThatAssertionError()
.isThrownBy(() -> MockRestRequestMatchers.queryParam("foo", containsString("bx")).match(this.request))
.withMessageContaining("was \"bar\"");
}
@Test
void queryParamListMissing() {
assertThatAssertionError()
.isThrownBy(() -> MockRestRequestMatchers.queryParam("foo", hasSize(2)).match(this.request))
.withMessage("Expected query param <foo> to exist but was null");
}
@Test
void queryParamListMatchers() throws IOException {
this.request.setURI(URI.create("http://www.foo.example/a?foo=bar&foo=baz"));
MockRestRequestMatchers.queryParam("foo", containsInAnyOrder(endsWith("baz"), endsWith("bar"))).match(this.request);
MockRestRequestMatchers.queryParam("foo", contains(is("bar"), is("baz"))).match(this.request);
MockRestRequestMatchers.queryParam("foo", contains(is("bar"), anything())).match(this.request);
MockRestRequestMatchers.queryParam("foo", hasItem(endsWith("baz"))).match(this.request);
MockRestRequestMatchers.queryParam("foo", everyItem(startsWith("ba"))).match(this.request);
MockRestRequestMatchers.queryParam("foo", hasSize(2)).match(this.request);
// These can be a bit ambiguous when reading the test (the compiler selects the list matcher):
MockRestRequestMatchers.queryParam("foo", notNullValue()).match(this.request);
MockRestRequestMatchers.queryParam("foo", is(anything())).match(this.request);
MockRestRequestMatchers.queryParam("foo", allOf(notNullValue(), notNullValue())).match(this.request);
// These are not as ambiguous thanks to an inner matcher that is either obviously list-oriented,
// string-oriented, or obviously a vararg of matchers
// list matcher version
MockRestRequestMatchers.queryParam("foo", allOf(notNullValue(), hasSize(2))).match(this.request);
// vararg version
MockRestRequestMatchers.queryParam("foo", allOf(notNullValue(), endsWith("ar"))).match(this.request);
MockRestRequestMatchers.queryParam("foo", is((any(String.class)))).match(this.request);
MockRestRequestMatchers.queryParam("foo", either(is("bar")).or(is(nullValue()))).match(this.request);
MockRestRequestMatchers.queryParam("foo", is(notNullValue()), is(notNullValue())).match(this.request);
}
@Test
void queryParamListContainsMismatch() {
this.request.setURI(URI.create("http://www.foo.example/a?foo=bar&foo=baz"));
assertThatAssertionError()
.isThrownBy(() -> MockRestRequestMatchers.queryParam("foo", contains(containsString("ba"))).match(this.request))
.withMessageContainingAll(
"Query param [foo] values",
"Expected: iterable containing [a string containing \"ba\"]",
"but: not matched: \"baz\"");
assertThatAssertionError()
.isThrownBy(() -> MockRestRequestMatchers.queryParam("foo", hasItem(endsWith("ba"))).match(this.request))
.withMessageContainingAll(
"Query param [foo] values",
"Expected: a collection containing a string ending with \"ba\"",
"but: mismatches were: [was \"bar\", was \"baz\"]");
assertThatAssertionError()
.isThrownBy(() -> MockRestRequestMatchers.queryParam("foo", everyItem(endsWith("ar"))).match(this.request))
.withMessageContainingAll(
"Query param [foo] values",
"Expected: every item is a string ending with \"ar\"",
"but: an item was \"baz\"");
}
private static ThrowableTypeAssert<AssertionError> assertThatAssertionError() {
return assertThatExceptionOfType(AssertionError.class);
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2023 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.
@@ -31,4 +31,9 @@ public class ForwardController {
return "forward:/a";
}
@RequestMapping("/infiniteForward")
public String infiniteForward() {
return "forward:/infiniteForward";
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2023 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.
@@ -29,6 +29,7 @@ import org.springframework.test.web.servlet.setup.MockMvcBuilders;
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;
/**
@@ -80,6 +81,13 @@ public class MockMvcWebConnectionTests {
assertThat(page.getWebResponse().getContentAsString()).isEqualTo("hello");
}
@Test
public void infiniteForward() {
this.webClient.setWebConnection(new MockMvcWebConnection(this.mockMvc, this.webClient, ""));
assertThatIllegalStateException().isThrownBy(() -> this.webClient.getPage("http://localhost/infiniteForward"))
.withMessage("Forwarded more than 100 times in a row, potential infinite forward loop");
}
@Test
@SuppressWarnings("resource")
public void contextPathDoesNotStartWithSlash() throws IOException {
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2023 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.
@@ -84,11 +84,12 @@ public class GenericMessageEndpointFactory extends AbstractMessageEndpointFactor
@Override
public MessageEndpoint createEndpoint(XAResource xaResource) throws UnavailableException {
GenericMessageEndpoint endpoint = (GenericMessageEndpoint) super.createEndpoint(xaResource);
ProxyFactory proxyFactory = new ProxyFactory(getMessageListener());
Object target = getMessageListener();
ProxyFactory proxyFactory = new ProxyFactory(target);
DelegatingIntroductionInterceptor introduction = new DelegatingIntroductionInterceptor(endpoint);
introduction.suppressInterface(MethodInterceptor.class);
proxyFactory.addAdvice(introduction);
return (MessageEndpoint) proxyFactory.getProxy();
return (MessageEndpoint) proxyFactory.getProxy(target.getClass().getClassLoader());
}
/**
@@ -0,0 +1,93 @@
/*
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.http.client.reactive;
import java.lang.reflect.Method;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.client.api.Response;
import org.springframework.http.HttpHeaders;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ReflectionUtils;
/**
* Used to support Jetty 10.
*
* @author Rossen Stoyanchev
* @author Arjen Poutsma
* @since 5.3.26
*/
abstract class Jetty10HttpFieldsHelper {
private static final boolean jetty10Present;
private static final Method requestGetHeadersMethod;
private static final Method responseGetHeadersMethod;
private static final Method getNameMethod;
private static final Method getValueMethod;
static {
try {
ClassLoader classLoader = JettyClientHttpResponse.class.getClassLoader();
Class<?> httpFieldsClass = classLoader.loadClass("org.eclipse.jetty.http.HttpFields");
jetty10Present = httpFieldsClass.isInterface();
requestGetHeadersMethod = Request.class.getMethod("getHeaders");
responseGetHeadersMethod = Response.class.getMethod("getHeaders");
Class<?> httpFieldClass = classLoader.loadClass("org.eclipse.jetty.http.HttpField");
getNameMethod = httpFieldClass.getMethod("getName");
getValueMethod = httpFieldClass.getMethod("getValue");
}
catch (ClassNotFoundException | NoSuchMethodException ex) {
throw new IllegalStateException("No compatible Jetty version found", ex);
}
}
public static boolean jetty10Present() {
return jetty10Present;
}
public static HttpHeaders getHttpHeaders(Request request) {
Iterable<?> iterator = (Iterable<?>)
ReflectionUtils.invokeMethod(requestGetHeadersMethod, request);
return getHttpHeadersInternal(iterator);
}
public static HttpHeaders getHttpHeaders(Response response) {
Iterable<?> iterator = (Iterable<?>)
ReflectionUtils.invokeMethod(responseGetHeadersMethod, response);
return getHttpHeadersInternal(iterator);
}
private static HttpHeaders getHttpHeadersInternal(@Nullable Iterable<?> iterator) {
Assert.notNull(iterator, "Iterator must not be null");
HttpHeaders headers = new HttpHeaders();
for (Object field : iterator) {
String name = (String) ReflectionUtils.invokeMethod(getNameMethod, field);
Assert.notNull(name, "Header name must not be null");
String value = (String) ReflectionUtils.invokeMethod(getValueMethod, field);
headers.add(name, value);
}
return headers;
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2023 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.
@@ -37,6 +37,7 @@ import org.springframework.core.io.buffer.PooledDataBuffer;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.util.MultiValueMap;
/**
* {@link ClientHttpRequest} implementation for the Jetty ReactiveStreams HTTP client.
@@ -54,7 +55,6 @@ class JettyClientHttpRequest extends AbstractClientHttpRequest {
private final ReactiveRequest.Builder builder;
public JettyClientHttpRequest(Request jettyRequest, DataBufferFactory bufferFactory) {
this.jettyRequest = jettyRequest;
this.bufferFactory = bufferFactory;
@@ -96,8 +96,7 @@ class JettyClientHttpRequest extends AbstractClientHttpRequest {
.as(chunks -> ReactiveRequest.Content.fromPublisher(chunks, getContentType()));
this.builder.content(content);
sink.success();
})
.then(doCommit());
}).then(doCommit());
}
@Override
@@ -144,7 +143,11 @@ class JettyClientHttpRequest extends AbstractClientHttpRequest {
@Override
protected HttpHeaders initReadOnlyHeaders() {
return HttpHeaders.readOnlyHttpHeaders(new JettyHeadersAdapter(this.jettyRequest.getHeaders()));
MultiValueMap<String, String> headers = (Jetty10HttpFieldsHelper.jetty10Present() ?
Jetty10HttpFieldsHelper.getHttpHeaders(this.jettyRequest) :
new JettyHeadersAdapter(this.jettyRequest.getHeaders()));
return HttpHeaders.readOnlyHttpHeaders(headers);
}
public ReactiveRequest toReactiveRequest() {
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2021 the original author or authors.
* Copyright 2002-2023 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.
@@ -16,13 +16,11 @@
package org.springframework.http.client.reactive;
import java.lang.reflect.Method;
import java.net.HttpCookie;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.eclipse.jetty.client.api.Response;
import org.eclipse.jetty.reactive.client.ReactiveResponse;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
@@ -32,11 +30,9 @@ import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseCookie;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.ReflectionUtils;
/**
* {@link ClientHttpResponse} implementation for the Jetty ReactiveStreams HTTP client.
@@ -50,10 +46,6 @@ class JettyClientHttpResponse implements ClientHttpResponse {
private static final Pattern SAMESITE_PATTERN = Pattern.compile("(?i).*SameSite=(Strict|Lax|None).*");
private static final ClassLoader classLoader = JettyClientHttpResponse.class.getClassLoader();
private static final boolean jetty10Present;
private final ReactiveResponse reactiveResponse;
@@ -62,23 +54,12 @@ class JettyClientHttpResponse implements ClientHttpResponse {
private final HttpHeaders headers;
static {
try {
Class<?> httpFieldsClass = classLoader.loadClass("org.eclipse.jetty.http.HttpFields");
jetty10Present = httpFieldsClass.isInterface();
}
catch (ClassNotFoundException ex) {
throw new IllegalStateException("No compatible Jetty version found", ex);
}
}
public JettyClientHttpResponse(ReactiveResponse reactiveResponse, Publisher<DataBuffer> content) {
this.reactiveResponse = reactiveResponse;
this.content = Flux.from(content);
MultiValueMap<String, String> headers = (jetty10Present ?
Jetty10HttpFieldsHelper.getHttpHeaders(reactiveResponse) :
MultiValueMap<String, String> headers = (Jetty10HttpFieldsHelper.jetty10Present() ?
Jetty10HttpFieldsHelper.getHttpHeaders(reactiveResponse.getResponse()) :
new JettyHeadersAdapter(reactiveResponse.getHeaders()));
this.headers = HttpHeaders.readOnlyHttpHeaders(headers);
@@ -132,40 +113,4 @@ class JettyClientHttpResponse implements ClientHttpResponse {
return this.headers;
}
private static class Jetty10HttpFieldsHelper {
private static final Method getHeadersMethod;
private static final Method getNameMethod;
private static final Method getValueMethod;
static {
try {
getHeadersMethod = Response.class.getMethod("getHeaders");
Class<?> type = classLoader.loadClass("org.eclipse.jetty.http.HttpField");
getNameMethod = type.getMethod("getName");
getValueMethod = type.getMethod("getValue");
}
catch (ClassNotFoundException | NoSuchMethodException ex) {
throw new IllegalStateException("No compatible Jetty version found", ex);
}
}
public static HttpHeaders getHttpHeaders(ReactiveResponse response) {
HttpHeaders headers = new HttpHeaders();
Iterable<?> iterator = (Iterable<?>)
ReflectionUtils.invokeMethod(getHeadersMethod, response.getResponse());
Assert.notNull(iterator, "Iterator must not be null");
for (Object field : iterator) {
String name = (String) ReflectionUtils.invokeMethod(getNameMethod, field);
Assert.notNull(name, "Header name must not be null");
String value = (String) ReflectionUtils.invokeMethod(getValueMethod, field);
headers.add(name, value);
}
return headers;
}
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2021 the original author or authors.
* Copyright 2002-2023 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.
@@ -265,7 +265,7 @@ public class ProtobufDecoder extends ProtobufCodecSupport implements Decoder<Mes
* Parse message size as a varint from the input stream, updating {@code messageBytesToRead} and
* {@code offset} fields if needed to allow processing of upcoming chunks.
* Inspired from {@link CodedInputStream#readRawVarint32(int, java.io.InputStream)}
* @return {code true} when the message size is parsed successfully, {code false} when the message size is
* @return {@code true} when the message size is parsed successfully, {@code false} when the message size is
* truncated
* @see <a href="https://developers.google.com/protocol-buffers/docs/encoding#varints">Base 128 Varints</a>
*/
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2023 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.
@@ -57,7 +57,6 @@ import com.fasterxml.jackson.dataformat.xml.XmlFactory;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.FatalBeanException;
import org.springframework.context.ApplicationContext;
import org.springframework.core.KotlinDetector;
import org.springframework.lang.Nullable;
@@ -837,7 +836,7 @@ public class Jackson2ObjectMapperBuilder {
objectMapper.configure((MapperFeature) feature, enabled);
}
else {
throw new FatalBeanException("Unknown feature class: " + feature.getClass().getName());
throw new IllegalArgumentException("Unknown feature class: " + feature.getClass().getName());
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2023 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.
@@ -24,7 +24,6 @@ import java.util.concurrent.atomic.AtomicLong;
import javax.net.ssl.SSLSession;
import io.netty.channel.Channel;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.cookie.Cookie;
import io.netty.handler.ssl.SslHandler;
import org.apache.commons.logging.Log;
@@ -80,40 +79,15 @@ class ReactorServerHttpRequest extends AbstractServerHttpRequest {
return new URI(resolveBaseUrl(request) + resolveRequestUri(request));
}
private static URI resolveBaseUrl(HttpServerRequest request) throws URISyntaxException {
String scheme = getScheme(request);
String header = request.requestHeaders().get(HttpHeaderNames.HOST);
if (header != null) {
final int portIndex;
if (header.startsWith("[")) {
portIndex = header.indexOf(':', header.indexOf(']'));
}
else {
portIndex = header.indexOf(':');
}
if (portIndex != -1) {
try {
return new URI(scheme, null, header.substring(0, portIndex),
Integer.parseInt(header.substring(portIndex + 1)), null, null, null);
}
catch (NumberFormatException ex) {
throw new URISyntaxException(header, "Unable to parse port", portIndex);
}
}
else {
return new URI(scheme, header, null, null);
}
}
else {
InetSocketAddress localAddress = request.hostAddress();
Assert.state(localAddress != null, "No host address available");
return new URI(scheme, null, localAddress.getHostString(),
localAddress.getPort(), null, null, null);
}
private static String resolveBaseUrl(HttpServerRequest request) {
String scheme = request.scheme();
int port = request.hostPort();
return scheme + "://" + request.hostName() + (usePort(scheme, port) ? ":" + port : "");
}
private static String getScheme(HttpServerRequest request) {
return request.scheme();
private static boolean usePort(String scheme, int port) {
return ((scheme.equals("http") || scheme.equals("ws")) && (port != 80)) ||
((scheme.equals("https") || scheme.equals("wss")) && (port != 443));
}
private static String resolveRequestUri(HttpServerRequest request) {
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2023 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.
@@ -818,7 +818,7 @@ public class RestTemplate extends InterceptingHttpAccessor implements RestOperat
logger.debug("Response " + (status != null ? status : code));
}
catch (IOException ex) {
// ignore
logger.debug("Failed to obtain response status code", ex);
}
}
if (hasError) {
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2023 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.
@@ -133,22 +133,6 @@ public class ContextLoader {
private static final String DEFAULT_STRATEGIES_PATH = "ContextLoader.properties";
private static final Properties defaultStrategies;
static {
// Load default strategy implementations from properties file.
// This is currently strictly internal and not meant to be customized
// by application developers.
try {
ClassPathResource resource = new ClassPathResource(DEFAULT_STRATEGIES_PATH, ContextLoader.class);
defaultStrategies = PropertiesLoaderUtils.loadProperties(resource);
}
catch (IOException ex) {
throw new IllegalStateException("Could not load 'ContextLoader.properties': " + ex.getMessage());
}
}
/**
* Map from (thread context) ClassLoader to corresponding 'current' WebApplicationContext.
*/
@@ -162,6 +146,8 @@ public class ContextLoader {
@Nullable
private static volatile WebApplicationContext currentContext;
@Nullable
private static Properties defaultStrategies;
/**
* The root WebApplicationContext instance that this loader manages.
@@ -357,6 +343,18 @@ public class ContextLoader {
}
}
else {
if (defaultStrategies == null) {
// Load default strategy implementations from properties file.
// This is currently strictly internal and not meant to be customized
// by application developers.
try {
ClassPathResource resource = new ClassPathResource(DEFAULT_STRATEGIES_PATH, ContextLoader.class);
defaultStrategies = PropertiesLoaderUtils.loadProperties(resource);
}
catch (IOException ex) {
throw new IllegalStateException("Could not load 'ContextLoader.properties': " + ex.getMessage());
}
}
contextClassName = defaultStrategies.getProperty(WebApplicationContext.class.getName());
try {
return ClassUtils.forName(contextClassName, ContextLoader.class.getClassLoader());
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2023 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.
@@ -66,7 +66,7 @@ public abstract class CorsUtils {
}
/**
* Returns {@code true} if the request is a valid CORS pre-flight one by checking {code OPTIONS} method with
* Returns {@code true} if the request is a valid CORS pre-flight one by checking {@code OPTIONS} method with
* {@code Origin} and {@code Access-Control-Request-Method} headers presence.
*/
public static boolean isPreFlightRequest(HttpServletRequest request) {
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2023 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.
@@ -45,7 +45,7 @@ public abstract class CorsUtils {
}
/**
* Returns {@code true} if the request is a valid CORS pre-flight one by checking {code OPTIONS} method with
* Returns {@code true} if the request is a valid CORS pre-flight one by checking {@code OPTIONS} method with
* {@code Origin} and {@code Access-Control-Request-Method} headers presence.
*/
public static boolean isPreFlightRequest(ServerHttpRequest request) {
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2021 the original author or authors.
* Copyright 2002-2023 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.
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2023 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.
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2021 the original author or authors.
* Copyright 2002-2023 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.
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2021 the original author or authors.
* Copyright 2002-2023 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.
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2023 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.
@@ -83,11 +83,9 @@ import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.junit.jupiter.api.Test;
import org.springframework.beans.FatalBeanException;
import org.springframework.util.StringUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
/**
@@ -106,7 +104,7 @@ class Jackson2ObjectMapperBuilderTests {
@Test
void unknownFeature() {
assertThatExceptionOfType(FatalBeanException.class).isThrownBy(() ->
assertThatIllegalArgumentException().isThrownBy(() ->
Jackson2ObjectMapperBuilder.json().featuresToEnable(Boolean.TRUE).build());
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2023 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.
@@ -62,10 +62,8 @@ import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.junit.jupiter.api.Test;
import org.springframework.beans.FatalBeanException;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
/**
* Test cases for {@link Jackson2ObjectMapperFactoryBean}.
@@ -88,8 +86,7 @@ public class Jackson2ObjectMapperFactoryBeanTests {
@Test
public void unknownFeature() {
this.factory.setFeaturesToEnable(Boolean.TRUE);
assertThatExceptionOfType(FatalBeanException.class).isThrownBy(
this.factory::afterPropertiesSet);
assertThatIllegalArgumentException().isThrownBy(this.factory::afterPropertiesSet);
}
@Test
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2021 the original author or authors.
* Copyright 2002-2023 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.
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2023 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.
@@ -17,8 +17,10 @@
package org.springframework.web.testfixture.method;
import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@@ -28,16 +30,14 @@ import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.function.Supplier;
import org.aopalliance.intercept.MethodInterceptor;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.target.EmptyTargetSource;
import org.springframework.cglib.core.SpringNamingPolicy;
import org.springframework.cglib.proxy.Callback;
import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.Factory;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.MethodIntrospector;
@@ -122,6 +122,7 @@ import static java.util.stream.Collectors.joining;
* </pre>
*
* @author Rossen Stoyanchev
* @author Juergen Hoeller
* @since 5.0
*/
public class ResolvableMethod {
@@ -614,16 +615,10 @@ public class ResolvableMethod {
}
private static class MethodInvocationInterceptor
implements org.springframework.cglib.proxy.MethodInterceptor, MethodInterceptor {
private static class MethodInvocationInterceptor implements MethodInterceptor, InvocationHandler {
private Method invokedMethod;
Method getInvokedMethod() {
return this.invokedMethod;
}
@Override
@Nullable
public Object intercept(Object object, Method method, Object[] args, MethodProxy proxy) {
@@ -638,20 +633,23 @@ public class ResolvableMethod {
@Override
@Nullable
public Object invoke(org.aopalliance.intercept.MethodInvocation inv) throws Throwable {
return intercept(inv.getThis(), inv.getMethod(), inv.getArguments(), null);
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
return intercept(proxy, method, args, null);
}
Method getInvokedMethod() {
return this.invokedMethod;
}
}
@SuppressWarnings("unchecked")
private static <T> T initProxy(Class<?> type, MethodInvocationInterceptor interceptor) {
Assert.notNull(type, "'type' must not be null");
if (type.isInterface()) {
ProxyFactory factory = new ProxyFactory(EmptyTargetSource.INSTANCE);
factory.addInterface(type);
factory.addInterface(Supplier.class);
factory.addAdvice(interceptor);
return (T) factory.getProxy();
return (T) Proxy.newProxyInstance(type.getClassLoader(),
new Class<?>[] {type, Supplier.class},
interceptor);
}
else {
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2023 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.
@@ -94,7 +94,7 @@ public class RequestedContentTypeResolverBuilder {
return exchange -> {
for (RequestedContentTypeResolver resolver : resolvers) {
List<MediaType> mediaTypes = resolver.resolveMediaTypes(exchange);
if (mediaTypes.equals(RequestedContentTypeResolver.MEDIA_TYPE_ALL_LIST)) {
if (isMediaTypeAll(mediaTypes)) {
continue;
}
return mediaTypes;
@@ -103,6 +103,11 @@ public class RequestedContentTypeResolverBuilder {
};
}
private boolean isMediaTypeAll(List<MediaType> mediaTypes) {
return mediaTypes.size() == 1
&& mediaTypes.get(0).removeQualityValue().equals(MediaType.ALL);
}
/**
* Helper to create and configure {@link ParameterContentTypeResolver}.

Some files were not shown because too many files have changed in this diff Show More