Compare commits

...
112 Commits
Author SHA1 Message Date
Spring Builds 08bc1a050e Release v5.3.27 2023-04-13 08:43:55 +00:00
Sam Brannen 6bfb94a563 Disable variable assignment in SimpleEvaluationContext
This commit introduces infrastructure to differentiate between
programmatic setting of a variable in an EvaluationContext versus the
assignment of a variable within a SpEL expression using the assignment
operator (=). In addition, this commit disables variable assignment
within expressions when using the SimpleEvaluationContext.

Closes gh-30327
2023-04-13 10:20:06 +02:00
Sam Brannen ebc8265428 Limit SpEL expression length
This commit enforces a limit of the maximum size of a single SpEL
expression.

Closes gh-30329
2023-04-13 10:15:02 +02:00
Sam Brannen 86457464d7 Limit string concatenation in SpEL expressions
This commit introduces support for limiting the maximum length of a
string resulting from the concatenation operator (+) in SpEL
expressions.

Closes gh-30331
2023-04-13 10:14:18 +02:00
Sam Brannen be129dc171 Change max regex length in SpEL expressions to 1000
This commit changes the max regex length in SpEL expressions from 1024
to 1000 in order to consistently use "round" numbers for recently
introduced limits.

See gh-30265
2023-04-13 10:14:15 +02:00
Juergen Hoeller 8bb1b3eb44 Upgrade to Netty 4.1.91 and Checkstyle 10.9.3 2023-04-12 13:53:17 +02:00
Stephane Nicoll 6abd822e77 Upgrade to Reactor 2020.0.31
Closes gh-30315
2023-04-11 17:35:25 +02:00
rstoyanchev 1c43a4c7ab Fix regression in ReactorServerHttpRequest
Instead of a backport for cef916, this change simply undoes the
optimization that led to the regression.

Closes gh-30314
2023-04-11 15:12:54 +01:00
Sam Brannen 423f2215c2 Remove flaky assertion to fix build on JDK 17
Sometime between JDK 8 and JDK 17, the behavior for List::toArray()
changed. Specifically, the type returned for List<String> changed from
String[] to Object[].

This commit therefore removes an assertion against this particular
JDK-specific behavior. The affected test method retains additional
assertions along the same lines but which are not flaky.
2023-04-10 17:34:04 +02:00
Sébastien Deleuze 0bad69d5fb Fix SSE with indenting serializer in WebMvc.fn
This commit ensures that HTTP headers like "text/event-stream"
are correctly forwarded to the converter used in
SseServerResponse for proper pretty print handling.

Close gh-30302
2023-04-07 11:56:47 +02:00
Sam Brannen 6b19642256 Increase max regex length in SpEL expressions
This commit increases the max regex length in SpEL expressions from 256
to 1024 in order to support use cases where a regex may be rather long
without necessarily increasing the complexity of the regex.

Closes gh-30298
2023-04-06 18:02:36 +02:00
Sam Brannen 19bb4e96f2 Improve Javadoc for ObjectUtils.nullSafeConciseToString() 2023-04-06 17:36:32 +02:00
Sam Brannen 7a2594acda Add tests for corner cases
See gh-30290
See gh-30286
2023-04-05 15:28:45 +02:00
Sam Brannen 91c58af7af Introduce ObjectUtils.nullSafeConciseToString()
ObjectUtils.nullSafeToString(Object) exists for generating a string
representation of various objects in a "null-safe" manner, including
support for object graphs, collections, etc.

However, there are times when we would like to generate a "concise",
null-safe string representation that does not include an entire object
graph (or potentially a collection of object graphs).

This commit introduces ObjectUtils.nullSafeConciseToString(Object) to
address this need and makes use of the new feature in FieldError and
ConversionFailedException.

Closes gh-30286
2023-04-05 15:25:59 +02:00
Sam Brannen 0a1aeafe08 Introduce StringUtils.truncate()
StringUtils.truncate() serves as central, consistent way for truncating
strings used in log messages and exception failure messages, for
immediate use in LogFormatUtils and ObjectUtils.

See gh-30286
Closes gh-30290
2023-04-05 15:15:53 +02:00
Simon Baslé 0e69bac7b0 Polish 0cfbf60: fix a test for Java 8 compatibility 2023-04-04 15:25:27 +02:00
Simon Baslé 0cfbf6036c Rename MockMVC matcher methods to prevent regression in user tests
This commit changes the name of two recently introduced methods in the
`MockRestRequestMatchers` class for header and queryParam. These have
been found to cause false negatives in user tests, due to the new
overload taking precedence in some cases.

Namely, using a `Matcher` factory method which can apply to both `List`
and `String` will cause the compiler to select the newest list overload,
by instantiating a `Matcher<Object>`.

This can cause false negatives in user tests, failing tests that used
to pass because the Matcher previously applied to the first String in
the header or queryParam value list. For instance, `equalsTo("a")`.

The new overloads are recent enough and this has enough potential to
cause an arbitrary number of user tests to fail that we break the API
to eliminate the ambiguity, by renaming the methods with a `*List`
suffix.

See gh-30220
See gh-30238
Closes gh-30235
2023-04-04 15:07:41 +02:00
Sébastien Deleuze e931fdc5c2 Make HttpComponentsHeadersAdapter#getFirst nullable
Backport of gh-30267

Closes gh-30269
2023-04-03 09:06:32 +02:00
Sébastien Deleuze 3a1681357c Fix PathVariable reference documentation code snippets
Closes gh-30258
2023-03-31 15:51:45 +02:00
Juergen Hoeller a93382dbbf Propagate HttpStreamResetException itself if cause is null
Closes gh-30245

(cherry picked from commit 8fca258207)
2023-03-30 19:32:29 +02:00
Sébastien Deleuze 79c5ef88f5 Refine generic type management in AbstractMessageWriterResultHandler
This commit updates AbstractMessageWriterResultHandler#writeBody in
order to use the declared bodyParameter instead of
ResolvableType.forInstance(body) when the former has unresolvable
generics.

Closes gh-30215
2023-03-30 18:26:15 +02:00
Juergen Hoeller 0c80e5f9e6 Use JdkDynamicAopProxy class loader instead of JDK bootstrap/platform loader
Closes gh-30115

(cherry picked from commit 7e905e3e00)
2023-03-29 14:07:50 +02:00
Juergen Hoeller 7ad01a94d6 Use MethodInvocationInfo class loader in case of JDK platform loader as well
Closes gh-30210

(cherry picked from commit 491ae1e3be)
2023-03-29 13:56:23 +02:00
Juergen Hoeller 5d6d653cbd Use MethodInvocationInfo class loader in case of core JDK interface type
Closes gh-30210

(cherry picked from commit ce2689eead)
2023-03-28 13:56:02 +02:00
Johnny Lim 9868e888a5 Update versions in Javadoc
Closes gh-30191
2023-03-25 17:17:28 +01:00
ghostg00 6f6eb3d996 Fix example in Javadoc for @EnableWebSocket
The `echoWebSocketHandler()` method is not defined in the
`WebSocketConfigurer` interface and should therefore be annotated with
`@Bean` instead of `@Override`.

Closes gh-30187
2023-03-24 17:55:24 +01:00
Giuseppe a94c50e294 Handle all exceptions for stored proc out param retrieval in SharedEntityManagerCreator
Prior to this commit, the EntityManager was not closed in
SharedEntityManagerCreator.DeferredQueryInvocationHandler's
invoke(Object, Method, Object[]) method if an invocation of
getOutputParameterValue(*) threw an exception other than
IllegalArgumentException, which could lead to a connection leak.

This commit addresses this by catching RuntimeException instead of
IllegalArgumentException.

Closes gh-30164
2023-03-22 15:44:06 +01:00
Kukri b8c1255ee0 Fix anchor in link to "Web on Reactive Stack" chapter
Closes gh-30163
2023-03-22 11:35:12 +01:00
Spring Builds 129062034b Next development version (v5.3.27-SNAPSHOT) 2023-03-20 10:05:15 +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 BhattandSimon Baslé 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
danuandSimon Baslé 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
162 changed files with 3123 additions and 1261 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.91.Final"
mavenBom "io.projectreactor:reactor-bom:2020.0.31"
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.3"
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.27
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
*/
@@ -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.
@@ -123,6 +123,11 @@ final class JdkDynamicAopProxy implements AopProxy, InvocationHandler, Serializa
if (logger.isTraceEnabled()) {
logger.trace("Creating JDK dynamic proxy: " + this.advised.getTargetSource());
}
if (classLoader == null || classLoader.getParent() == null) {
// JDK bootstrap loader or platform loader suggested ->
// use higher-level loader which can see Spring infrastructure classes
classLoader = getClass().getClassLoader();
}
return Proxy.newProxyInstance(classLoader, this.proxiedInterfaces, this);
}
@@ -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.
@@ -16,7 +16,10 @@
package org.springframework.aop.framework;
import java.sql.SQLException;
import java.sql.Savepoint;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import javax.accessibility.Accessible;
@@ -380,6 +383,40 @@ public class ProxyFactoryTests {
assertThat(proxy.getName()).isEqualTo("tb");
}
@Test
public void testCharSequenceProxy() {
CharSequence target = "test";
ProxyFactory pf = new ProxyFactory(target);
ClassLoader cl = target.getClass().getClassLoader();
assertThat(((CharSequence) pf.getProxy(cl)).toString()).isEqualTo(target);
}
@Test
public void testDateProxy() {
Date target = new Date();
ProxyFactory pf = new ProxyFactory(target);
pf.setProxyTargetClass(true);
ClassLoader cl = target.getClass().getClassLoader();
assertThat(((Date) pf.getProxy(cl)).getTime()).isEqualTo(target.getTime());
}
@Test
public void testJdbcSavepointProxy() throws SQLException {
Savepoint target = new Savepoint() {
@Override
public int getSavepointId() throws SQLException {
return 1;
}
@Override
public String getSavepointName() throws SQLException {
return "sp";
}
};
ProxyFactory pf = new ProxyFactory(target);
ClassLoader cl = Savepoint.class.getClassLoader();
assertThat(((Savepoint) pf.getProxy(cl)).getSavepointName()).isEqualTo("sp");
}
@Order(2)
public static class A implements Runnable {
@@ -391,7 +428,7 @@ public class ProxyFactoryTests {
@Order(1)
public static class B implements Runnable{
public static class B implements Runnable {
@Override
public void run() {
@@ -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) {
@@ -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.
@@ -125,7 +125,7 @@ public class FieldError extends ObjectError {
@Override
public String toString() {
return "Field error in object '" + getObjectName() + "' on field '" + this.field +
"': rejected value [" + ObjectUtils.nullSafeToString(this.rejectedValue) + "]; " +
"': rejected value [" + ObjectUtils.nullSafeConciseToString(this.rejectedValue) + "]; " +
resolvableToString();
}
@@ -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.
@@ -133,7 +133,7 @@ public class DateFormattingTests {
assertThat(exception)
.hasMessageContaining("for property 'styleDate'")
.hasCauseInstanceOf(ConversionFailedException.class).cause()
.hasMessageContaining("for value '99/01/01'")
.hasMessageContaining("for value [99/01/01]")
.hasCauseInstanceOf(IllegalArgumentException.class).cause()
.hasMessageContaining("Parse attempt failed for value [99/01/01]")
.hasCauseInstanceOf(ParseException.class).cause()
@@ -353,7 +353,7 @@ public class DateFormattingTests {
assertThat(fieldError.unwrap(TypeMismatchException.class))
.hasMessageContaining("for property 'patternDateWithFallbackPatterns'")
.hasCauseInstanceOf(ConversionFailedException.class).cause()
.hasMessageContaining("for value '210302'")
.hasMessageContaining("for value [210302]")
.hasCauseInstanceOf(IllegalArgumentException.class).cause()
.hasMessageContaining("Parse attempt failed for value [210302]")
.hasCauseInstanceOf(ParseException.class).cause()
@@ -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.
@@ -333,7 +333,7 @@ class DateTimeFormattingTests {
assertThat(fieldError.unwrap(TypeMismatchException.class))
.hasMessageContaining("for property 'isoLocalDate'")
.hasCauseInstanceOf(ConversionFailedException.class).cause()
.hasMessageContaining("for value '2009-31-10'")
.hasMessageContaining("for value [2009-31-10]")
.hasCauseInstanceOf(IllegalArgumentException.class).cause()
.hasMessageContaining("Parse attempt failed for value [2009-31-10]")
.hasCauseInstanceOf(DateTimeParseException.class).cause()
@@ -540,7 +540,7 @@ class DateTimeFormattingTests {
assertThat(fieldError.unwrap(TypeMismatchException.class))
.hasMessageContaining("for property 'patternLocalDateWithFallbackPatterns'")
.hasCauseInstanceOf(ConversionFailedException.class).cause()
.hasMessageContaining("for value '210302'")
.hasMessageContaining("for value [210302]")
.hasCauseInstanceOf(IllegalArgumentException.class).cause()
.hasMessageContaining("Parse attempt failed for value [210302]")
.hasCauseInstanceOf(DateTimeParseException.class).cause()
@@ -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-2017 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.
@@ -49,7 +49,7 @@ public class ConversionFailedException extends ConversionException {
@Nullable Object value, Throwable cause) {
super("Failed to convert from type [" + sourceType + "] to type [" + targetType +
"] for value '" + ObjectUtils.nullSafeToString(value) + "'", cause);
"] for value [" + ObjectUtils.nullSafeConciseToString(value) + "]", cause);
this.sourceType = sourceType;
this.targetType = targetType;
this.value = value;
@@ -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.
@@ -23,6 +23,7 @@ import org.apache.commons.logging.Log;
import org.springframework.lang.Nullable;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
/**
* Utility methods for formatting and logging messages.
@@ -78,7 +79,7 @@ public abstract class LogFormatUtils {
result = ObjectUtils.nullSafeToString(ex);
}
if (maxLength != -1) {
result = (result.length() > maxLength ? result.substring(0, maxLength) + " (truncated)..." : result);
result = StringUtils.truncate(result, maxLength);
}
if (replaceNewlinesAndControlCharacters) {
result = NEWLINE_PATTERN.matcher(result).replaceAll("<EOL>");
@@ -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-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.
@@ -17,8 +17,13 @@
package org.springframework.util;
import java.lang.reflect.Array;
import java.net.URI;
import java.net.URL;
import java.time.temporal.Temporal;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.StringJoiner;
@@ -653,6 +658,7 @@ public abstract class ObjectUtils {
* Returns a {@code "null"} String if {@code obj} is {@code null}.
* @param obj the object to build a String representation for
* @return a String representation of {@code obj}
* @see #nullSafeConciseToString(Object)
*/
public static String nullSafeToString(@Nullable Object obj) {
if (obj == null) {
@@ -908,4 +914,73 @@ public abstract class ObjectUtils {
return stringJoiner.toString();
}
/**
* Generate a null-safe, concise string representation of the supplied object
* as described below.
* <p>Favor this method over {@link #nullSafeToString(Object)} when you need
* the length of the generated string to be limited.
* <p>Returns:
* <ul>
* <li>{@code "null"} if {@code obj} is {@code null}</li>
* <li>{@linkplain Class#getName() Class name} if {@code obj} is a {@link Class}</li>
* <li>Potentially {@linkplain StringUtils#truncate(CharSequence) truncated string}
* if {@code obj} is a {@link String} or {@link CharSequence}</li>
* <li>Potentially {@linkplain StringUtils#truncate(CharSequence) truncated string}
* if {@code obj} is a <em>simple type</em> whose {@code toString()} method returns
* a non-null value.</li>
* <li>Otherwise, a string representation of the object's type name concatenated
* with {@code @} and a hex string form of the object's identity hash code</li>
* </ul>
* <p>In the context of this method, a <em>simple type</em> is any of the following:
* a primitive wrapper (excluding {@link Void}), an {@link Enum}, a {@link Number},
* a {@link Date}, a {@link Temporal}, a {@link URI}, a {@link URL}, or a {@link Locale}.
* @param obj the object to build a string representation for
* @return a concise string representation of the supplied object
* @since 5.3.27
* @see #nullSafeToString(Object)
* @see StringUtils#truncate(CharSequence)
*/
public static String nullSafeConciseToString(@Nullable Object obj) {
if (obj == null) {
return "null";
}
if (obj instanceof Class<?>) {
return ((Class<?>) obj).getName();
}
if (obj instanceof CharSequence) {
return StringUtils.truncate((CharSequence) obj);
}
Class<?> type = obj.getClass();
if (isSimpleValueType(type)) {
String str = obj.toString();
if (str != null) {
return StringUtils.truncate(str);
}
}
return type.getTypeName() + "@" + getIdentityHexString(obj);
}
/**
* Copy of {@link org.springframework.beans.BeanUtils#isSimpleValueType(Class)}.
* <p>Check if the given type represents a "simple" value type: a primitive or
* primitive wrapper, an enum, a String or other CharSequence, a Number, a
* Date, a Temporal, a URI, a URL, a Locale, or a Class.
* <p>{@code Void} and {@code void} are not considered simple value types.
* @param type the type to check
* @return whether the given type represents a "simple" value type
*/
private static boolean isSimpleValueType(Class<?> type) {
return (Void.class != type && void.class != type &&
(ClassUtils.isPrimitiveOrWrapper(type) ||
Enum.class.isAssignableFrom(type) ||
CharSequence.class.isAssignableFrom(type) ||
Number.class.isAssignableFrom(type) ||
Date.class.isAssignableFrom(type) ||
Temporal.class.isAssignableFrom(type) ||
URI.class == type ||
URL.class == type ||
Locale.class == type ||
Class.class == type));
}
}
@@ -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.
@@ -75,6 +75,10 @@ public abstract class StringUtils {
private static final char EXTENSION_SEPARATOR = '.';
private static final int DEFAULT_TRUNCATION_THRESHOLD = 100;
private static final String TRUNCATION_SUFFIX = " (truncated)...";
//---------------------------------------------------------------------
// General convenience methods for working with Strings
@@ -1388,4 +1392,40 @@ public abstract class StringUtils {
return arrayToDelimitedString(arr, ",");
}
/**
* Truncate the supplied {@link CharSequence}.
* <p>Delegates to {@link #truncate(CharSequence, int)}, supplying {@code 100}
* as the threshold.
* @param charSequence the {@code CharSequence} to truncate
* @return a truncated string, or a string representation of the original
* {@code CharSequence} if its length does not exceed the threshold
* @since 5.3.27
*/
public static String truncate(CharSequence charSequence) {
return truncate(charSequence, DEFAULT_TRUNCATION_THRESHOLD);
}
/**
* Truncate the supplied {@link CharSequence}.
* <p>If the length of the {@code CharSequence} is greater than the threshold,
* this method returns a {@linkplain CharSequence#subSequence(int, int)
* subsequence} of the {@code CharSequence} (up to the threshold) appended
* with the suffix {@code " (truncated)..."}. Otherwise, this method returns
* {@code charSequence.toString()}.
* @param charSequence the {@code CharSequence} to truncate
* @param threshold the maximum length after which to truncate; must be a
* positive number
* @return a truncated string, or a string representation of the original
* {@code CharSequence} if its length does not exceed the threshold
* @since 5.3.27
*/
public static String truncate(CharSequence charSequence, int threshold) {
Assert.isTrue(threshold > 0,
() -> "Truncation threshold must be a positive number: " + threshold);
if (charSequence.length() > threshold) {
return charSequence.subSequence(0, threshold) + TRUNCATION_SUFFIX;
}
return charSequence.toString();
}
}
@@ -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,15 +17,25 @@
package org.springframework.util;
import java.io.IOException;
import java.net.URI;
import java.net.URL;
import java.sql.SQLException;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
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.springframework.util.ObjectUtils.isEmpty;
@@ -816,7 +826,144 @@ class ObjectUtilsTests {
.withMessage("Constant [bogus] does not exist in enum type org.springframework.util.ObjectUtilsTests$Tropes");
}
private void assertEqualHashCodes(int expected, Object array) {
@Nested
class NullSafeConciseToStringTests {
private final String truncated = " (truncated)...";
private final int truncatedLength = 100 + truncated.length();
@Test
void nullSafeConciseToStringForNull() {
assertThat(ObjectUtils.nullSafeConciseToString(null)).isEqualTo("null");
}
@Test
void nullSafeConciseToStringForClass() {
assertThat(ObjectUtils.nullSafeConciseToString(String.class)).isEqualTo("java.lang.String");
}
@Test
void nullSafeConciseToStringForStrings() {
String repeat100 = repeat("X", 100);
String repeat101 = repeat("X", 101);
assertThat(ObjectUtils.nullSafeConciseToString("")).isEqualTo("");
assertThat(ObjectUtils.nullSafeConciseToString("foo")).isEqualTo("foo");
assertThat(ObjectUtils.nullSafeConciseToString(repeat100)).isEqualTo(repeat100);
assertThat(ObjectUtils.nullSafeConciseToString(repeat101)).hasSize(truncatedLength).endsWith(truncated);
}
@Test
void nullSafeConciseToStringForStringBuilders() {
String repeat100 = repeat("X", 100);
String repeat101 = repeat("X", 101);
assertThat(ObjectUtils.nullSafeConciseToString(new StringBuilder("foo"))).isEqualTo("foo");
assertThat(ObjectUtils.nullSafeConciseToString(new StringBuilder(repeat100))).isEqualTo(repeat100);
assertThat(ObjectUtils.nullSafeConciseToString(new StringBuilder(repeat101))).hasSize(truncatedLength).endsWith(truncated);
}
@Test
void nullSafeConciseToStringForEnum() {
assertThat(ObjectUtils.nullSafeConciseToString(Tropes.FOO)).isEqualTo("FOO");
}
@Test
void nullSafeConciseToStringForNumber() {
assertThat(ObjectUtils.nullSafeConciseToString(42L)).isEqualTo("42");
assertThat(ObjectUtils.nullSafeConciseToString(99.1234D)).isEqualTo("99.1234");
}
@Test
void nullSafeConciseToStringForDate() {
Date date = new Date();
assertThat(ObjectUtils.nullSafeConciseToString(date)).isEqualTo(date.toString());
}
@Test
void nullSafeConciseToStringForTemporal() {
LocalDate localDate = LocalDate.now();
assertThat(ObjectUtils.nullSafeConciseToString(localDate)).isEqualTo(localDate.toString());
}
@Test
void nullSafeConciseToStringForUri() {
String uri = "https://www.example.com/?foo=1&bar=2&baz=3";
assertThat(ObjectUtils.nullSafeConciseToString(URI.create(uri))).isEqualTo(uri);
uri += "&qux=" + repeat("4", 60);
assertThat(ObjectUtils.nullSafeConciseToString(URI.create(uri)))
.hasSize(truncatedLength)
.startsWith(uri.subSequence(0, 100))
.endsWith(truncated);
}
@Test
void nullSafeConciseToStringForUrl() throws Exception {
String url = "https://www.example.com/?foo=1&bar=2&baz=3";
assertThat(ObjectUtils.nullSafeConciseToString(new URL(url))).isEqualTo(url);
url += "&qux=" + repeat("4", 60);
assertThat(ObjectUtils.nullSafeConciseToString(new URL(url)))
.hasSize(truncatedLength)
.startsWith(url.subSequence(0, 100))
.endsWith(truncated);
}
@Test
void nullSafeConciseToStringForLocale() {
assertThat(ObjectUtils.nullSafeConciseToString(Locale.GERMANY)).isEqualTo("de_DE");
}
@Test
void nullSafeConciseToStringForArraysAndCollections() {
List<String> list = Arrays.asList("a", "b", "c");
assertThat(ObjectUtils.nullSafeConciseToString(new int[][] {{1, 2}, {3, 4}})).startsWith(prefix(int[][].class));
assertThat(ObjectUtils.nullSafeConciseToString(list.toArray(new Object[0]))).startsWith(prefix(Object[].class));
assertThat(ObjectUtils.nullSafeConciseToString(list.toArray(new String[0]))).startsWith(prefix(String[].class));
assertThat(ObjectUtils.nullSafeConciseToString(new ArrayList<>(list))).startsWith(prefix(ArrayList.class));
assertThat(ObjectUtils.nullSafeConciseToString(new HashSet<>(list))).startsWith(prefix(HashSet.class));
}
@Test
void nullSafeConciseToStringForCustomTypes() {
class ExplosiveType {
@Override
public String toString() {
throw new UnsupportedOperationException("no-go");
}
}
ExplosiveType explosiveType = new ExplosiveType();
assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(explosiveType::toString);
assertThat(ObjectUtils.nullSafeConciseToString(explosiveType)).startsWith(prefix(ExplosiveType.class));
class WordyType {
@Override
public String toString() {
return repeat("blah blah", 20);
}
}
WordyType wordyType = new WordyType();
assertThat(wordyType).asString().hasSizeGreaterThanOrEqualTo(180 /* 9x20 */);
assertThat(ObjectUtils.nullSafeConciseToString(wordyType)).startsWith(prefix(WordyType.class));
}
private String repeat(String str, int count) {
String result = "";
for (int i = 0; i < count; i++) {
result += str;
}
return result;
}
private String prefix(Class<?> clazz) {
return clazz.getTypeName() + "@";
}
}
private static void assertEqualHashCodes(int expected, Object array) {
int actual = ObjectUtils.nullSafeHashCode(array);
assertThat(actual).isEqualTo(expected);
assertThat(array.hashCode() != actual).isTrue();
@@ -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.
@@ -22,6 +22,8 @@ import java.util.Locale;
import java.util.Properties;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
@@ -785,4 +787,26 @@ class StringUtilsTests {
assertThat(StringUtils.collectionToCommaDelimitedString(Collections.singletonList(null))).isEqualTo("null");
}
@Test
void truncatePreconditions() {
assertThatIllegalArgumentException()
.isThrownBy(() -> StringUtils.truncate("foo", 0))
.withMessage("Truncation threshold must be a positive number: 0");
assertThatIllegalArgumentException()
.isThrownBy(() -> StringUtils.truncate("foo", -99))
.withMessage("Truncation threshold must be a positive number: -99");
}
@ParameterizedTest
@CsvSource(delimiterString = "-->", value = {
"'' --> ''",
"aardvark --> aardvark",
"aardvark12 --> aardvark12",
"aardvark123 --> aardvark12 (truncated)...",
"aardvark, bird, cat --> aardvark, (truncated)..."
})
void truncate(String text, String truncated) {
assertThat(StringUtils.truncate(text, 10)).isEqualTo(truncated);
}
}
@@ -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.
@@ -17,6 +17,7 @@
package org.springframework.expression;
import java.util.List;
import java.util.function.Supplier;
import org.springframework.lang.Nullable;
@@ -24,12 +25,21 @@ import org.springframework.lang.Nullable;
* Expressions are executed in an evaluation context. It is in this context that
* references are resolved when encountered during expression evaluation.
*
* <p>There is a default implementation of this EvaluationContext interface:
* {@link org.springframework.expression.spel.support.StandardEvaluationContext}
* which can be extended, rather than having to implement everything manually.
* <p>There are two default implementations of this interface.
* <ul>
* <li>{@link org.springframework.expression.spel.support.SimpleEvaluationContext
* SimpleEvaluationContext}: a simpler builder-style {@code EvaluationContext}
* variant for data-binding purposes, which allows for opting into several SpEL
* features as needed.</li>
* <li>{@link org.springframework.expression.spel.support.StandardEvaluationContext
* StandardEvaluationContext}: a powerful and highly configurable {@code EvaluationContext}
* implementation, which can be extended, rather than having to implement everything
* manually.</li>
* </ul>
*
* @author Andy Clement
* @author Juergen Hoeller
* @author Sam Brannen
* @since 3.0
*/
public interface EvaluationContext {
@@ -85,7 +95,30 @@ public interface EvaluationContext {
OperatorOverloader getOperatorOverloader();
/**
* Set a named variable within this evaluation context to a specified value.
* Assign the value created by the specified {@link Supplier} to a named variable
* within this evaluation context.
* <p>In contrast to {@link #setVariable(String, Object)}, this method should only
* be invoked to support the assignment operator ({@code =}) within an expression.
* <p>By default, this method delegates to {@code setVariable(String, Object)},
* providing the value created by the {@code valueSupplier}. Concrete implementations
* may override this <em>default</em> method to provide different semantics.
* @param name the name of the variable to assign
* @param valueSupplier the supplier of the value to be assigned to the variable
* @return a {@link TypedValue} wrapping the assigned value
* @since 5.2.24
*/
default TypedValue assignVariable(String name, Supplier<TypedValue> valueSupplier) {
TypedValue typedValue = valueSupplier.get();
setVariable(name, typedValue.getValue());
return typedValue;
}
/**
* Set a named variable in this evaluation context to a specified value.
* <p>In contrast to {@link #assignVariable(String, Supplier)}, this method
* should only be invoked programmatically when interacting directly with the
* {@code EvaluationContext} &mdash; for example, to provide initial
* configuration for the context.
* @param name the name of the variable to set
* @param value the value to be placed in the variable
*/
@@ -93,7 +126,7 @@ public interface EvaluationContext {
/**
* Look up a named variable within this evaluation context.
* @param name variable to lookup
* @param name the name of the variable to look up
* @return the value of the variable, or {@code null} if not found
*/
@Nullable
@@ -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.
@@ -23,6 +23,7 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.function.Supplier;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.expression.EvaluationContext;
@@ -38,18 +39,19 @@ import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
/**
* An ExpressionState is for maintaining per-expression-evaluation state, any changes to
* it are not seen by other expressions but it gives a place to hold local variables and
* ExpressionState is for maintaining per-expression-evaluation state: any changes to
* it are not seen by other expressions, but it gives a place to hold local variables and
* for component expressions in a compound expression to communicate state. This is in
* contrast to the EvaluationContext, which is shared amongst expression evaluations, and
* any changes to it will be seen by other expressions or any code that chooses to ask
* questions of the context.
*
* <p>It also acts as a place for to define common utility routines that the various AST
* <p>It also acts as a place to define common utility routines that the various AST
* nodes might need.
*
* @author Andy Clement
* @author Juergen Hoeller
* @author Sam Brannen
* @since 3.0
*/
public class ExpressionState {
@@ -138,6 +140,29 @@ public class ExpressionState {
return this.scopeRootObjects.element();
}
/**
* Assign the value created by the specified {@link Supplier} to a named variable
* within the evaluation context.
* <p>In contrast to {@link #setVariable(String, Object)}, this method should
* only be invoked to support assignment within an expression.
* @param name the name of the variable to assign
* @param valueSupplier the supplier of the value to be assigned to the variable
* @return a {@link TypedValue} wrapping the assigned value
* @since 5.2.24
* @see EvaluationContext#assignVariable(String, Supplier)
*/
public TypedValue assignVariable(String name, Supplier<TypedValue> valueSupplier) {
return this.relatedContext.assignVariable(name, valueSupplier);
}
/**
* Set a named variable in the evaluation context to a specified value.
* <p>In contrast to {@link #assignVariable(String, Supplier)}, this method
* should only be invoked programmatically.
* @param name the name of the variable to set
* @param value the value to be placed in the variable
* @see EvaluationContext#setVariable(String, Object)
*/
public void setVariable(String name, @Nullable Object value) {
this.relatedContext.setVariable(name, value);
}
@@ -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,27 @@ 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.2.23 */
MAX_REPEATED_TEXT_SIZE_EXCEEDED(Kind.ERROR, 1076,
"Repeated text is too long, exceeding the threshold of ''{0}'' characters"),
/** @since 5.2.23 */
MAX_REGEX_LENGTH_EXCEEDED(Kind.ERROR, 1077,
"Regular expression is too long, exceeding the threshold of ''{0}'' characters"),
/** @since 5.2.24 */
MAX_CONCATENATED_STRING_LENGTH_EXCEEDED(Kind.ERROR, 1078,
"Concatenated string is too long, exceeding the threshold of ''{0}'' characters"),
/** @since 5.2.24 */
MAX_EXPRESSION_LENGTH_EXCEEDED(Kind.ERROR, 1079,
"SpEL expression is too long, exceeding the threshold of ''{0}'' characters"),
/** @since 5.2.24 */
VARIABLE_ASSIGNMENT_NOT_SUPPORTED(Kind.ERROR, 1080,
"Assignment to variable ''{0}'' is not supported");
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.
@@ -27,6 +27,7 @@ import org.springframework.expression.spel.ExpressionState;
* <p>Example: 'someNumberProperty=42'
*
* @author Andy Clement
* @author Sam Brannen
* @since 3.0
*/
public class Assign extends SpelNodeImpl {
@@ -38,9 +39,7 @@ public class Assign extends SpelNodeImpl {
@Override
public TypedValue getValueInternal(ExpressionState state) throws EvaluationException {
TypedValue newValue = this.children[1].getValueInternal(state);
getChild(0).setValue(state, newValue.getValue());
return newValue;
return this.children[0].setValueInternal(state, () -> this.children[1].getValueInternal(state));
}
@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.
@@ -17,6 +17,7 @@
package org.springframework.expression.spel.ast;
import java.util.StringJoiner;
import java.util.function.Supplier;
import org.springframework.asm.MethodVisitor;
import org.springframework.expression.EvaluationException;
@@ -24,13 +25,13 @@ 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.lang.Nullable;
/**
* Represents a DOT separated expression sequence, such as
* {@code 'property1.property2.methodOne()'}.
*
* @author Andy Clement
* @author Sam Brannen
* @since 3.0
*/
public class CompoundExpression extends SpelNodeImpl {
@@ -95,8 +96,12 @@ public class CompoundExpression extends SpelNodeImpl {
}
@Override
public void setValue(ExpressionState state, @Nullable Object value) throws EvaluationException {
getValueRef(state).setValue(value);
public TypedValue setValueInternal(ExpressionState state, Supplier<TypedValue> valueSupplier)
throws EvaluationException {
TypedValue typedValue = valueSupplier.get();
getValueRef(state).setValue(typedValue.getValue());
return typedValue;
}
@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.
@@ -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.
@@ -25,6 +25,7 @@ import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.StringJoiner;
import java.util.function.Supplier;
import org.springframework.asm.MethodVisitor;
import org.springframework.core.convert.TypeDescriptor;
@@ -45,11 +46,12 @@ import org.springframework.util.ReflectionUtils;
/**
* An Indexer can index into some proceeding structure to access a particular piece of it.
* Supported structures are: strings / collections (lists/sets) / arrays.
* <p>Supported structures are: strings / collections (lists/sets) / arrays.
*
* @author Andy Clement
* @author Phillip Webb
* @author Stephane Nicoll
* @author Sam Brannen
* @since 3.0
*/
// TODO support multidimensional arrays
@@ -102,8 +104,12 @@ public class Indexer extends SpelNodeImpl {
}
@Override
public void setValue(ExpressionState state, @Nullable Object newValue) throws EvaluationException {
getValueRef(state).setValue(newValue);
public TypedValue setValueInternal(ExpressionState state, Supplier<TypedValue> valueSupplier)
throws EvaluationException {
TypedValue typedValue = valueSupplier.get();
getValueRef(state).setValue(typedValue.getValue());
return typedValue;
}
@Override
@@ -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.2.23
*/
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.
@@ -27,6 +27,8 @@ import org.springframework.expression.TypeConverter;
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.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.NumberUtils;
@@ -46,10 +48,18 @@ import org.springframework.util.NumberUtils;
* @author Juergen Hoeller
* @author Ivo Smid
* @author Giovanni Dall'Oglio Risso
* @author Sam Brannen
* @since 3.0
*/
public class OpPlus extends Operator {
/**
* Maximum number of characters permitted in a concatenated string.
* @since 5.2.24
*/
private static final int MAX_CONCATENATED_STRING_LENGTH = 100_000;
public OpPlus(int startPos, int endPos, SpelNodeImpl... operands) {
super("+", startPos, endPos, operands);
Assert.notEmpty(operands, "Operands must not be empty");
@@ -123,22 +133,45 @@ public class OpPlus extends Operator {
if (leftOperand instanceof String && rightOperand instanceof String) {
this.exitTypeDescriptor = "Ljava/lang/String";
return new TypedValue((String) leftOperand + rightOperand);
String leftString = (String) leftOperand;
String rightString = (String) rightOperand;
checkStringLength(leftString);
checkStringLength(rightString);
return concatenate(leftString, rightString);
}
if (leftOperand instanceof String) {
return new TypedValue(
leftOperand + (rightOperand == null ? "null" : convertTypedValueToString(operandTwoValue, state)));
String leftString = (String) leftOperand;
checkStringLength(leftString);
String rightString = (rightOperand == null ? "null" : convertTypedValueToString(operandTwoValue, state));
checkStringLength(rightString);
return concatenate(leftString, rightString);
}
if (rightOperand instanceof String) {
return new TypedValue(
(leftOperand == null ? "null" : convertTypedValueToString(operandOneValue, state)) + rightOperand);
String rightString = (String) rightOperand;
checkStringLength(rightString);
String leftString = (leftOperand == null ? "null" : convertTypedValueToString(operandOneValue, state));
checkStringLength(leftString);
return concatenate(leftString, rightString);
}
return state.operate(Operation.ADD, leftOperand, rightOperand);
}
private void checkStringLength(String string) {
if (string.length() > MAX_CONCATENATED_STRING_LENGTH) {
throw new SpelEvaluationException(getStartPosition(),
SpelMessage.MAX_CONCATENATED_STRING_LENGTH_EXCEEDED, MAX_CONCATENATED_STRING_LENGTH);
}
}
private TypedValue concatenate(String leftString, String rightString) {
String result = leftString + rightString;
checkStringLength(result);
return new TypedValue(result);
}
@Override
public String toStringAST() {
if (this.children.length < 2) { // unary plus
@@ -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.2.23
*/
private static final int MAX_REGEX_LENGTH = 1000;
private final ConcurrentMap<String, Pattern> patternCache;
/**
* Create a new {@link OperatorMatches} instance.
* @deprecated as of Spring Framework 5.2.23 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.2.23
*/
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,7 @@ import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Supplier;
import org.springframework.asm.Label;
import org.springframework.asm.MethodVisitor;
@@ -46,6 +47,7 @@ import org.springframework.util.ReflectionUtils;
* @author Andy Clement
* @author Juergen Hoeller
* @author Clark Duplichien
* @author Sam Brannen
* @since 3.0
*/
public class PropertyOrFieldReference extends SpelNodeImpl {
@@ -147,8 +149,12 @@ public class PropertyOrFieldReference extends SpelNodeImpl {
}
@Override
public void setValue(ExpressionState state, @Nullable Object newValue) throws EvaluationException {
writeProperty(state.getActiveContextObject(), state.getEvaluationContext(), this.name, newValue);
public TypedValue setValueInternal(ExpressionState state, Supplier<TypedValue> valueSupplier)
throws EvaluationException {
TypedValue typedValue = valueSupplier.get();
writeProperty(state.getActiveContextObject(), state.getEvaluationContext(), this.name, typedValue.getValue());
return typedValue;
}
@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.
@@ -19,6 +19,7 @@ package org.springframework.expression.spel.ast;
import java.lang.reflect.Constructor;
import java.lang.reflect.Member;
import java.lang.reflect.Method;
import java.util.function.Supplier;
import org.springframework.asm.MethodVisitor;
import org.springframework.asm.Opcodes;
@@ -40,6 +41,7 @@ import org.springframework.util.ObjectUtils;
*
* @author Andy Clement
* @author Juergen Hoeller
* @author Sam Brannen
* @since 3.0
*/
public abstract class SpelNodeImpl implements SpelNode, Opcodes {
@@ -64,7 +66,7 @@ public abstract class SpelNodeImpl implements SpelNode, Opcodes {
* <p>The descriptor is like the bytecode form but is slightly easier to work with.
* It does not include the trailing semicolon (for non array reference types).
* Some examples: Ljava/lang/String, I, [I
*/
*/
@Nullable
protected volatile String exitTypeDescriptor;
@@ -83,8 +85,8 @@ public abstract class SpelNodeImpl implements SpelNode, Opcodes {
/**
* Return {@code true} if the next child is one of the specified classes.
*/
* Return {@code true} if the next child is one of the specified classes.
*/
protected boolean nextChildIs(Class<?>... classes) {
if (this.parent != null) {
SpelNodeImpl[] peers = this.parent.children;
@@ -125,6 +127,28 @@ public abstract class SpelNodeImpl implements SpelNode, Opcodes {
@Override
public void setValue(ExpressionState expressionState, @Nullable Object newValue) throws EvaluationException {
setValueInternal(expressionState, () -> new TypedValue(newValue));
}
/**
* Evaluate the expression to a node and then set the new value created by the
* specified {@link Supplier} on that node.
* <p>For example, if the expression evaluates to a property reference, then the
* property will be set to the new value.
* <p>Favor this method over {@link #setValue(ExpressionState, Object)} when
* the value should be lazily computed.
* <p>By default, this method throws a {@link SpelEvaluationException},
* effectively disabling this feature. Subclasses may override this method to
* provide an actual implementation.
* @param expressionState the current expression state (includes the context)
* @param valueSupplier a supplier of the new value
* @throws EvaluationException if any problem occurs evaluating the expression or
* setting the new value
* @since 5.2.24
*/
public TypedValue setValueInternal(ExpressionState expressionState, Supplier<TypedValue> valueSupplier)
throws EvaluationException {
throw new SpelEvaluationException(getStartPosition(), SpelMessage.SETVALUE_NOT_SUPPORTED, getClass());
}
@@ -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.
@@ -17,9 +17,11 @@
package org.springframework.expression.spel.ast;
import java.lang.reflect.Modifier;
import java.util.function.Supplier;
import org.springframework.asm.MethodVisitor;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.EvaluationException;
import org.springframework.expression.TypedValue;
import org.springframework.expression.spel.CodeFlow;
import org.springframework.expression.spel.ExpressionState;
@@ -27,10 +29,11 @@ import org.springframework.expression.spel.SpelEvaluationException;
import org.springframework.lang.Nullable;
/**
* Represents a variable reference, eg. #someVar. Note this is different to a *local*
* variable like $someVar
* Represents a variable reference &mdash; for example, {@code #someVar}. Note
* that this is different than a <em>local</em> variable like {@code $someVar}.
*
* @author Andy Clement
* @author Sam Brannen
* @since 3.0
*/
public class VariableReference extends SpelNodeImpl {
@@ -53,14 +56,14 @@ public class VariableReference extends SpelNodeImpl {
@Override
public ValueRef getValueRef(ExpressionState state) throws SpelEvaluationException {
if (this.name.equals(THIS)) {
return new ValueRef.TypedValueHolderValueRef(state.getActiveContextObject(),this);
return new ValueRef.TypedValueHolderValueRef(state.getActiveContextObject(), this);
}
if (this.name.equals(ROOT)) {
return new ValueRef.TypedValueHolderValueRef(state.getRootContextObject(),this);
return new ValueRef.TypedValueHolderValueRef(state.getRootContextObject(), this);
}
TypedValue result = state.lookupVariable(this.name);
// a null value will mean either the value was null or the variable was not found
return new VariableRef(this.name,result,state.getEvaluationContext());
return new VariableRef(this.name, result, state.getEvaluationContext());
}
@Override
@@ -90,8 +93,10 @@ public class VariableReference extends SpelNodeImpl {
}
@Override
public void setValue(ExpressionState state, @Nullable Object value) throws SpelEvaluationException {
state.setVariable(this.name, value);
public TypedValue setValueInternal(ExpressionState state, Supplier<TypedValue> valueSupplier)
throws EvaluationException {
return state.assignVariable(this.name, valueSupplier);
}
@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.
@@ -21,12 +21,15 @@ 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;
import org.springframework.expression.ParserContext;
import org.springframework.expression.common.TemplateAwareExpressionParser;
import org.springframework.expression.spel.InternalParseException;
import org.springframework.expression.spel.SpelEvaluationException;
import org.springframework.expression.spel.SpelMessage;
import org.springframework.expression.spel.SpelParseException;
import org.springframework.expression.spel.SpelParserConfiguration;
@@ -83,18 +86,28 @@ import org.springframework.util.StringUtils;
* @author Andy Clement
* @author Juergen Hoeller
* @author Phillip Webb
* @author Sam Brannen
* @since 3.0
*/
class InternalSpelExpressionParser extends TemplateAwareExpressionParser {
private static final Pattern VALID_QUALIFIED_ID_PATTERN = Pattern.compile("[\\p{L}\\p{N}_$]+");
/**
* Maximum length permitted for a SpEL expression.
* @since 5.2.24
*/
private static final int MAX_EXPRESSION_LENGTH = 10_000;
private final SpelParserConfiguration configuration;
// 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 = "";
@@ -121,6 +134,8 @@ class InternalSpelExpressionParser extends TemplateAwareExpressionParser {
protected SpelExpression doParseExpression(String expressionString, @Nullable ParserContext context)
throws ParseException {
checkExpressionLength(expressionString);
try {
this.expressionString = expressionString;
Tokenizer tokenizer = new Tokenizer(expressionString);
@@ -142,6 +157,12 @@ class InternalSpelExpressionParser extends TemplateAwareExpressionParser {
}
}
private void checkExpressionLength(String string) {
if (string.length() > MAX_EXPRESSION_LENGTH) {
throw new SpelEvaluationException(SpelMessage.MAX_EXPRESSION_LENGTH_EXCEEDED, MAX_EXPRESSION_LENGTH);
}
}
// expression
// : logicalOrExpression
// ( (ASSIGN^ logicalOrExpression)
@@ -248,7 +269,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-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.
@@ -21,6 +21,7 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Supplier;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.TypeDescriptor;
@@ -78,6 +79,7 @@ import org.springframework.lang.Nullable;
*
* @author Rossen Stoyanchev
* @author Juergen Hoeller
* @author Sam Brannen
* @since 4.3.15
* @see #forPropertyAccessors
* @see #forReadOnlyDataBinding()
@@ -200,6 +202,17 @@ public final class SimpleEvaluationContext implements EvaluationContext {
return this.operatorOverloader;
}
/**
* {@code SimpleEvaluationContext} does not support variable assignment within
* expressions.
* @throws SpelEvaluationException with {@link SpelMessage#VARIABLE_ASSIGNMENT_NOT_SUPPORTED}
* @since 5.2.24
*/
@Override
public TypedValue assignVariable(String name, Supplier<TypedValue> valueSupplier) {
throw new SpelEvaluationException(SpelMessage.VARIABLE_ASSIGNMENT_NOT_SUPPORTED, "#" + name);
}
@Override
public void setVariable(String name, @Nullable Object value) {
this.variables.put(name, value);
@@ -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.
@@ -25,6 +25,8 @@ import java.util.Map;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.springframework.expression.AccessException;
import org.springframework.expression.BeanResolver;
@@ -36,6 +38,7 @@ import org.springframework.expression.MethodFilter;
import org.springframework.expression.ParseException;
import org.springframework.expression.spel.standard.SpelExpression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.SimpleEvaluationContext;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.expression.spel.support.StandardTypeLocator;
import org.springframework.expression.spel.testresources.TestPerson;
@@ -60,6 +63,20 @@ class EvaluationTests extends AbstractExpressionTests {
@Nested
class MiscellaneousTests {
@Test
void expressionLength() {
String expression = String.format("'X' + '%s'", repeat(" ", 9_992));
assertThat(expression).hasSize(10_000);
Expression expr = parser.parseExpression(expression);
String result = expr.getValue(context, String.class);
assertThat(result).hasSize(9_993);
assertThat(result.trim()).isEqualTo("X");
expression = String.format("'X' + '%s'", repeat(" ", 9_993));
assertThat(expression).hasSize(10_001);
evaluateAndCheckError(expression, String.class, SpelMessage.MAX_EXPRESSION_LENGTH_EXCEEDED);
}
@Test
void createListsOnAttemptToIndexNull01() throws EvaluationException, ParseException {
ExpressionParser parser = new SpelExpressionParser(new SpelParserConfiguration(true, true));
@@ -127,17 +144,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() {
@@ -146,8 +152,24 @@ class EvaluationTests extends AbstractExpressionTests {
// assignment
@Test
void assignmentToVariables() {
evaluate("#var1='value1'", "value1", String.class);
void assignmentToVariableWithStandardEvaluationContext() {
evaluate("#var1 = 'value1'", "value1", String.class);
}
@ParameterizedTest
@CsvSource(delimiterString = "->", value = {
"'#var1 = \"value1\"' -> #var1",
"'true ? #myVar = 4 : 0' -> #myVar"
})
void assignmentToVariableWithSimpleEvaluationContext(String expression, String varName) {
EvaluationContext context = SimpleEvaluationContext.forReadWriteDataBinding().build();
Expression expr = parser.parseExpression(expression);
assertThatExceptionOfType(SpelEvaluationException.class)
.isThrownBy(() -> expr.getValue(context))
.satisfies(ex -> {
assertThat(ex.getMessageCode()).isEqualTo(SpelMessage.VARIABLE_ASSIGNMENT_NOT_SUPPORTED);
assertThat(ex.getInserts()).as("inserts").containsExactly(varName);
});
}
@Test
@@ -460,28 +482,47 @@ 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 = String.format("^(%s|X)", repeat("12345", 199));
assertThat(pattern).hasSize(1000);
Expression expr = parser.parseExpression("'X' matches '" + pattern + "'");
assertThat(expr.getValue(context, Boolean.class)).isTrue();
pattern += "?";
assertThat(pattern).hasSize(1001);
evaluateAndCheckError("'abc' matches '" + pattern + "'", Boolean.class, SpelMessage.MAX_REGEX_LENGTH_EXCEEDED);
}
}
@@ -1448,7 +1489,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));
}
@@ -1456,6 +1499,15 @@ class EvaluationTests extends AbstractExpressionTests {
}
private static String repeat(String str, int count) {
String result = "";
for (int i = 0; i < count; i++) {
result += str;
}
return result;
}
@SuppressWarnings("rawtypes")
static class TestClass {
@@ -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,26 @@ 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_CONCATENATED_STRING_LENGTH_EXCEEDED;
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 +89,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 +138,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 +180,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 +229,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 +270,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 +319,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 +360,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,56 +387,47 @@ 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);
evaluate("3 + new java.math.BigDecimal('5')", new BigDecimal("8"), BigDecimal.class);
evaluate("'ab' + 2", "ab2", String.class);
evaluate("2 + 'a'", "2a", String.class);
evaluate("'ab' + null", "abnull", String.class);
evaluate("null + 'ab'", "nullab", String.class);
evaluate("5 + new Integer('37')", 42, Integer.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);
// string concatenation
evaluate("'abc'+'def'","abcdef",String.class);
evaluate("5 + new Integer('37')",42,Integer.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);
evaluateAndCheckError("+'abc'", SpelMessage.OPERATOR_NOT_SUPPORTED_BETWEEN_TYPES);
}
@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 +438,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 +451,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 +474,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 +485,7 @@ class OperatorTests extends AbstractExpressionTests {
}
@Test
void testOperatorNames() {
void operatorNames() {
Operator node = getOperatorNode((SpelExpression)parser.parseExpression("1==3"));
assertThat(node.getOperatorName()).isEqualTo("==");
@@ -534,22 +524,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 +548,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 +559,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 +567,73 @@ 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 stringConcatenation() {
evaluate("'' + ''", "", String.class);
evaluate("'' + null", "null", String.class);
evaluate("null + ''", "null", String.class);
evaluate("'ab' + null", "abnull", String.class);
evaluate("null + 'ab'", "nullab", String.class);
evaluate("'ab' + 2", "ab2", String.class);
evaluate("2 + 'ab'", "2ab", String.class);
evaluate("'abc' + 'def'", "abcdef", String.class);
// Text is big but not too big
final int maxSize = 100_000;
context.setVariable("text1", createString(maxSize));
Expression expr = parser.parseExpression("#text1 + ''");
assertThat(expr.getValue(context, String.class)).hasSize(maxSize);
expr = parser.parseExpression("'' + #text1");
assertThat(expr.getValue(context, String.class)).hasSize(maxSize);
context.setVariable("text1", createString(maxSize / 2));
expr = parser.parseExpression("#text1 + #text1");
assertThat(expr.getValue(context, String.class)).hasSize(maxSize);
// Text is too big
context.setVariable("text1", createString(maxSize + 1));
evaluateAndCheckError("#text1 + ''", String.class, MAX_CONCATENATED_STRING_LENGTH_EXCEEDED, 7);
evaluateAndCheckError("#text1 + true", String.class, MAX_CONCATENATED_STRING_LENGTH_EXCEEDED, 7);
evaluateAndCheckError("'' + #text1", String.class, MAX_CONCATENATED_STRING_LENGTH_EXCEEDED, 3);
evaluateAndCheckError("true + #text1", String.class, MAX_CONCATENATED_STRING_LENGTH_EXCEEDED, 5);
context.setVariable("text1", createString(maxSize / 2));
context.setVariable("text2", createString((maxSize / 2) + 1));
evaluateAndCheckError("#text1 + #text2", String.class, MAX_CONCATENATED_STRING_LENGTH_EXCEEDED, 7);
evaluateAndCheckError("#text1 + #text2 + true", String.class, MAX_CONCATENATED_STRING_LENGTH_EXCEEDED, 7);
evaluateAndCheckError("#text1 + true + #text2", String.class, MAX_CONCATENATED_STRING_LENGTH_EXCEEDED, 14);
evaluateAndCheckError("true + #text1 + #text2", String.class, MAX_CONCATENATED_STRING_LENGTH_EXCEEDED, 14);
evaluateAndCheckError("#text2 + #text1", String.class, MAX_CONCATENATED_STRING_LENGTH_EXCEEDED, 7);
evaluateAndCheckError("#text2 + #text1 + true", String.class, MAX_CONCATENATED_STRING_LENGTH_EXCEEDED, 7);
evaluateAndCheckError("#text2 + true + #text1", String.class, MAX_CONCATENATED_STRING_LENGTH_EXCEEDED, 14);
evaluateAndCheckError("true + #text2 + #text1", String.class, MAX_CONCATENATED_STRING_LENGTH_EXCEEDED, 14);
context.setVariable("text1", createString((maxSize / 3) + 1));
evaluateAndCheckError("#text1 + #text1 + #text1", String.class, MAX_CONCATENATED_STRING_LENGTH_EXCEEDED, 16);
evaluateAndCheckError("(#text1 + #text1) + #text1", String.class, MAX_CONCATENATED_STRING_LENGTH_EXCEEDED, 18);
evaluateAndCheckError("#text1 + (#text1 + #text1)", String.class, MAX_CONCATENATED_STRING_LENGTH_EXCEEDED, 7);
}
private static String createString(int size) {
return new String(new char[size]);
}
@Test
void longs() {
evaluate("3L == 4L", false, Boolean.class);
evaluate("3L == 3L", true, Boolean.class);
evaluate("3L != 4L", true, Boolean.class);
@@ -588,7 +644,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 +675,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()
}
@@ -432,7 +436,7 @@ public abstract class SharedEntityManagerCreator {
entry.setValue(storedProc.getOutputParameterValue(key.toString()));
}
}
catch (IllegalArgumentException ex) {
catch (RuntimeException ex) {
entry.setValue(ex);
}
}
@@ -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.27
* @see #queryParam(String, Matcher...)
* @see #queryParam(String, String...)
*/
public static RequestMatcher queryParamList(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 #queryParamList(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 #queryParamList(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 #queryParamList(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 #queryParamList(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.27
* @see #header(String, Matcher...)
* @see #header(String, String...)
*/
public static RequestMatcher headerList(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 #headerList(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 #headerList(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 #headerList(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 #headerList(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,31 @@
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.anything;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.endsWith;
import static org.hamcrest.Matchers.equalTo;
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.startsWith;
/**
* Unit tests for {@link MockRestRequestMatchers}.
@@ -35,179 +48,313 @@ 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.headerList("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.headerList("foo", containsInAnyOrder(endsWith("baz"), endsWith("bar"))).match(this.request);
MockRestRequestMatchers.headerList("foo", contains(is("bar"), is("baz"))).match(this.request);
MockRestRequestMatchers.headerList("foo", contains(is("bar"), anything())).match(this.request);
MockRestRequestMatchers.headerList("foo", hasItem(endsWith("baz"))).match(this.request);
MockRestRequestMatchers.headerList("foo", everyItem(startsWith("ba"))).match(this.request);
MockRestRequestMatchers.headerList("foo", hasSize(2)).match(this.request);
MockRestRequestMatchers.headerList("foo", notNullValue()).match(this.request);
MockRestRequestMatchers.headerList("foo", is(anything())).match(this.request);
MockRestRequestMatchers.headerList("foo", allOf(notNullValue(), notNullValue())).match(this.request);
MockRestRequestMatchers.headerList("foo", allOf(notNullValue(), hasSize(2))).match(this.request);
}
@Test
void headerListContainsMismatch() {
this.request.getHeaders().put("foo", Arrays.asList("bar", "baz"));
assertThatAssertionError()
.isThrownBy(() -> MockRestRequestMatchers.headerList("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.headerList("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.headerList("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 headerListDoesntHideHeaderWithSingleMatcher() throws IOException {
this.request.getHeaders().put("foo", Arrays.asList("bar", "baz"));
MockRestRequestMatchers.header("foo", equalTo("bar")).match(this.request);
assertThatAssertionError()
.isThrownBy(() -> MockRestRequestMatchers.headerList("foo", equalTo("bar")).match(this.request))
.withMessageContainingAll(
"Request header [foo] values",
"Expected: \"bar\"",
"but: was <[bar, 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.queryParamList("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.queryParamList("foo", containsInAnyOrder(endsWith("baz"), endsWith("bar"))).match(this.request);
MockRestRequestMatchers.queryParamList("foo", contains(is("bar"), is("baz"))).match(this.request);
MockRestRequestMatchers.queryParamList("foo", contains(is("bar"), anything())).match(this.request);
MockRestRequestMatchers.queryParamList("foo", hasItem(endsWith("baz"))).match(this.request);
MockRestRequestMatchers.queryParamList("foo", everyItem(startsWith("ba"))).match(this.request);
MockRestRequestMatchers.queryParamList("foo", hasSize(2)).match(this.request);
MockRestRequestMatchers.queryParamList("foo", notNullValue()).match(this.request);
MockRestRequestMatchers.queryParamList("foo", is(anything())).match(this.request);
MockRestRequestMatchers.queryParamList("foo", allOf(notNullValue(), notNullValue())).match(this.request);
MockRestRequestMatchers.queryParamList("foo", allOf(notNullValue(), hasSize(2))).match(this.request);
}
@Test
void queryParamListContainsMismatch() {
this.request.setURI(URI.create("http://www.foo.example/a?foo=bar&foo=baz"));
assertThatAssertionError()
.isThrownBy(() -> MockRestRequestMatchers.queryParamList("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.queryParamList("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.queryParamList("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\"");
}
@Test
void queryParamListDoesntHideQueryParamWithSingleMatcher() throws IOException {
this.request.setURI(URI.create("http://www.foo.example/a?foo=bar&foo=baz"));
MockRestRequestMatchers.queryParam("foo", equalTo("bar")).match(this.request);
assertThatAssertionError()
.isThrownBy(() -> MockRestRequestMatchers.queryParamList("foo", equalTo("bar")).match(this.request))
.withMessageContainingAll(
"Query param [foo] values",
"Expected: \"bar\"",
"but: was <[bar, 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";
}
}

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