Compare commits

...

101 Commits

Author SHA1 Message Date
Spring Buildmaster e3069d4e55 Release version 5.1.13.RELEASE 2020-01-14 07:37:49 +00:00
Rossen Stoyanchev 9969cb6d83 Improve limit handling in StringDecoder
The case of one data buffer containing multiple lines can could cause
a buffer leak due to a suspected issue in concatMapIterable. This
commit adds workarounds for that until the underlying issue is
addressed.

Closes gh-24346
2020-01-13 20:46:51 +00:00
Rossen Stoyanchev 04b3f5a247 Upgrade to Reactor Californium SR15
Closes gh-24345
2020-01-13 17:09:38 +00:00
Sam Brannen c91b47fded Document Objenesis license in license.txt
Closes gh-24340
2020-01-13 16:08:14 +01:00
Sam Brannen f2c364b491 Update ASM and CBLIB versions in license.txt 2020-01-13 13:22:19 +01:00
Juergen Hoeller 170adccf80 Upgrade to RxJava 2.2.17, OkHttp 3.14.6, Hibernate ORM 5.3.15 2020-01-13 10:58:51 +01:00
Brian Clozel 98390e1e5e Copy strategiesConfigurers when cloning WebClient.Builder
This commit fixes the missing `strategiesConfigurers` copy when the
`WebClient.Builder` is cloned.

Fixes gh-24330
See gh-24329
2020-01-10 16:08:04 +01:00
Juergen Hoeller 6df8c2678b Upgrade to SLF4J 1.7.30 and Checkstyle 8.28 2020-01-09 17:13:16 +01:00
Juergen Hoeller 7f49abb6d1 Upgrade to Netty 4.1.44, Jetty 9.4.25, Undertow 2.0.29, OkHttp 3.14.5 2020-01-09 16:17:33 +01:00
Juergen Hoeller b9bf56e41d Polishing 2020-01-09 16:16:36 +01:00
Juergen Hoeller d0e3e2acfc Thread-safe compiled expression evaluation in SpelExpression
Closes gh-24265
2020-01-09 16:16:01 +01:00
Rossen Stoyanchev 18d983c686 Multi-value headers in ResponseStatusException
Closes gh-24284
2020-01-06 22:05:29 +00:00
Rossen Stoyanchev d8abbc501e CorsInterceptor skips async dispatch
Closes gh-24223
2020-01-06 21:24:02 +00:00
Rossen Stoyanchev 6ce19ff861 Escape quotes in filename
Also sync up with master on refactorings in ContentDisposition and
ContentDispositionTests.

Closes gh-24224
2020-01-06 18:18:56 +00:00
Juergen Hoeller c8ef49cc8e Upgrade to RxJava 2.2.16 2019-12-16 17:06:08 +01:00
Juergen Hoeller f61983d908 Polishing 2019-12-16 17:05:48 +01:00
Juergen Hoeller 7615e0b036 Un-deprecate PathResource (for NIO Path resolution in createRelative)
Includes aligned createRelative signature and dedicated java.io.File test.

Closes gh-24211
2019-12-16 17:05:28 +01:00
Juergen Hoeller afe22b84c2 ConcurrentReferenceHashMap cache for getInterfaceMethodIfPossible results
Closes gh-24206
2019-12-16 16:55:10 +01:00
Rossen Stoyanchev 634aba4ab6 Fix cloning issue in CodecConfigurer for multipart writers
Backport of b23617637d

Closes gh-24194
2019-12-13 06:38:49 +00:00
Rossen Stoyanchev 26a2d3875f Expose ClientCodecConfigurer in WebClient.Builder
Using Consumer<ClientCodecConfigurer> instead of
Consumer<ExchangeStrategies> eliminates one level of nesting that is
also unnecessary since codecs are the only strategy at present.

Backport of dd9b6287b4

Closes gh-24201
2019-12-13 06:38:49 +00:00
Rossen Stoyanchev 802d083df7 Add register methods to CodecConfigurer.CustomCodecs
The new register methods replace the now deprecated
encoder, decoder, reader, and writer methods, and also offer a choice
to opt into default properties such maxInMemorySize, if configured.

Backport of 11e321b8e7

See gh-24201
2019-12-13 06:38:43 +00:00
Rossen Stoyanchev f5b43a264a CodecConfigurer implementation refactoring
Backport of 9d65830133

See gh-24201
2019-12-13 06:23:40 +00:00
Rossen Stoyanchev 59e4755562 Correct WebFlux docs on BindingResult with @RequestBody
Backport of 70a0c93d69

Closes gh-22997
2019-12-13 05:54:22 +00:00
Rossen Stoyanchev 2576aa4063 ContentDisposition trims charset in filename
Backport of c8bce9686f

Closes gh-24112
2019-12-13 05:46:09 +00:00
Juergen Hoeller 3fbe762832 Consistent use of annotation-api dependency instead of tomcat-embed-core 2019-12-12 17:12:27 +01:00
Juergen Hoeller 611bb0b92e Explicit test dependency on javax.annotation.Priority 2019-12-12 17:01:18 +01:00
Juergen Hoeller 32a8b9b25a Polishing 2019-12-12 16:48:25 +01:00
Juergen Hoeller 97cad6ca8e Upgrade to Tomcat 9.0.30 2019-12-12 16:48:11 +01:00
Juergen Hoeller 015f7d8ce1 Polishing 2019-12-11 17:13:02 +01:00
Juergen Hoeller da4e2710b4 Upgrade to Joda-Time 2.10.5 and Commons Pool 2.6.2 2019-12-10 00:38:21 +01:00
Juergen Hoeller a368040fd6 Polishing 2019-12-09 15:54:40 +01:00
Juergen Hoeller 5dbd3b0bbf Avoid ByteArrayOutputStream for source values without the need to be encoded
Closes gh-24154
2019-12-09 13:56:50 +01:00
Juergen Hoeller 197dbffe20 Support variable resolution of wildcard types
Includes cleanup of "varaible" typos in ResolvableTypeTests.

Closes gh-24150
2019-12-09 13:56:25 +01:00
Sam Brannen 5341e77796 Introduce regression tests for prototype/request-scoped @ControllerAdvice
See gh-24157
2019-12-07 19:02:06 +01:00
Sam Brannen f049a6ec16 Add integration test for gh-24110 2019-12-03 17:12:14 +01:00
Spring Buildmaster 7203da7133 Next Development Version 2019-12-03 08:06:03 +00:00
Brian Clozel a21df0cc6d Provide default codecs config callback to custom codecs
As a follow-up of gh-23961, this change provides a way for custom codecs
to align with the default codecs' behavior on common features like
buffer size limits and logging request details.

Closes gh-24119
Co-authored-by: Rossen Stoyanchev <rstoyanchev@pivotal.io>
2019-12-02 23:16:49 +01:00
Brian Clozel 83683a13bb Allow ExchangeStrategies customizations in WebClient
Backport of d4209392 and acfeb77d

Closes gh-23961
2019-12-02 17:10:29 +00:00
Juergen Hoeller 59165dd526 Upgrade to AspectJ 1.9.5 and Checkstyle 8.27 2019-12-02 13:10:07 +01:00
Brian Clozel 25554d0b21 Revert "Allow ExchangeStrategies customizations in WebClient"
This reverts commit 43e047c523.
2019-12-02 10:29:24 +01:00
Juergen Hoeller 4f86282b14 Polishing 2019-12-01 01:55:26 +01:00
Juergen Hoeller b3237f3eb6 Polishing 2019-12-01 00:41:53 +01:00
Juergen Hoeller fd68fb115d Backport of recent ExtendedBeanInfo refinements from master
Closes gh-24095
2019-12-01 00:40:57 +01:00
Brian Clozel 43e047c523 Allow ExchangeStrategies customizations in WebClient
Prior to this commit, developers could configure their WebClient to use
their custom `ExchangeStrategies`, by providing it in the
`WebClient.Builder` chain.
Once created, an `ExchangeStrategies` instance is not mutable, which
makes it hard for further customizations by other components. In the
case of the reported issue, other components would override the default
configuration for the codecs maxInMemorySize.

This commit makes the `ExchangeStrategies` mutable and uses that fact to
further customize them with a new `WebClient.Builder#exchangeStrategies`
`Consumer` variant. This commit is also deprecating those mutating
variants in favor of a new `WebClient.Builder#exchangeStrategies` that
takes a `ExchangeStrategies#Builder` directly and avoids mutation issues
altogether.

Closes gh-24106
2019-11-29 23:35:20 +01:00
Sam Brannen 7fdf775394 Test status quo for @Inherited annotations in AnnotationMetadata
See gh-24077
2019-11-28 18:19:44 +01:00
Sam Brannen 59084c6b73 Test status quo for AnnotatedTypeMetadata.getAnnotationAttributes()
See gh-24077
2019-11-28 13:46:35 +01:00
Stephane Nicoll 4fcc09a02c Upgrade to Reactor Californium-SR14
Closes gh-24038
2019-11-27 11:51:27 +01:00
Arjen Poutsma f5b082d3b3 Remove println 2019-11-25 18:27:13 +01:00
Arjen Poutsma c164759c41 Fix NullPointerException in Jackson2SmileDecoder
Fix uncommon case in Jackson2SmileDecoder, where a null token,
incicating a document separator in streaming mode, is followed by
NOT_AVAILABLE.

Closes gh-24009

(cherry picked from commit 5f3c7ca559)
2019-11-25 16:37:36 +01:00
Juergen Hoeller 2179b67706 Upgrade to Tomcat 9.0.29, Jetty 9.4.24, RxJava 2.2.15 2019-11-24 21:47:25 +01:00
Rossen Stoyanchev fbde98f36e Add missing verify() in Jackson2TokenizerTests
Closes gh-24056
2019-11-22 16:39:14 +00:00
Rossen Stoyanchev 529f8ba786 Extra isReady-onWritePossible after last write
Closes gh-24050
2019-11-22 15:58:25 +00:00
Juergen Hoeller f4676bb41c Restore short-circuiting in equals implementation
Closes gh-24048
2019-11-21 18:25:37 +01:00
Juergen Hoeller 262332a7c9 Upgrade to Jetty 9.4.23 and Woodstox 5.3 2019-11-21 13:48:55 +01:00
Juergen Hoeller 8b1709b8f3 Upgrade to Hibernate Validator 6.0.18 and Undertow 2.0.28 2019-11-20 17:45:18 +01:00
Juergen Hoeller aee33d8b4b Mark SqlRowSet accessor methods as nullable (for alignment with JDBC)
Closes gh-24042
2019-11-20 17:42:11 +01:00
Stephane Nicoll 83f03976ea Start building against Reactor Californium-SR14 snapshots
See gh-24038
2019-11-20 11:02:08 +01:00
Sam Brannen ffcd83e3a8 Ignore scoped proxy targets for @ControllerAdvice beans
Prior to this commit, methods in a @ControllerAdvice bean were
registered and invoked twice if the advice was a scoped bean (e.g.,
request or session scoped). In other words, both the proxy bean and the
target bean were wrapped in ControllerAdviceBean instances.

This commit fixes this bug by modifying the findAnnotatedBeans() method
in ControllerAdviceBean so that it filters out targets of scoped
proxies.

Closes gh-24017
2019-11-19 15:21:48 +01:00
Sam Brannen 9a522946a5 Backport tests for gh-23985 2019-11-19 15:21:48 +01:00
Juergen Hoeller 268d029584 Avoid substring allocation in StringUtils.replace
Closes gh-24023
2019-11-19 15:04:15 +01:00
Juergen Hoeller 85471d0587 Revise getElementTypeDescriptor javadoc (no IllegalStateException)
Closes gh-23996
2019-11-14 18:34:19 +01:00
Sam Brannen 96a1a0dec5 Convert InstantFormatterTests to JUnit 4
See gh-23895
2019-11-14 12:01:11 +01:00
Sam Brannen 40ac055d11 Polish contribution
See gh-23895
2019-11-14 12:01:11 +01:00
monosoul c9a6f4282a Inverse condition to fix ISO-formatted Instant parsing
Prior to this commit, InstantFormatter was able to properly serialize
an Instant that is far in the future (or in the past), but it could not
properly deserialize it, because in such scenarios an ISO-formatted
Instant starts with a +/- sign.

This commit fixes this issue, while maintaining the previous contract,
and also introduces tests for InstantFormatter.

Closes gh-23895
2019-11-14 10:10:43 +01:00
Juergen Hoeller 6ed6c08ace Polishing 2019-11-13 23:49:33 +01:00
Juergen Hoeller e4c57a9b61 Minor internal refinements (backported from master) 2019-11-13 16:53:23 +01:00
Juergen Hoeller b0d8a667a1 Upgrade to Hibernate ORM 5.3.14 2019-11-13 16:51:47 +01:00
Juergen Hoeller 9c9ee22c2b Note on injecting results from local @Bean methods (self references)
Closes gh-23934
2019-11-13 16:50:47 +01:00
Juergen Hoeller 639dce6282 Support for new MySQL 8 error code 3572
Closes gh-23972
2019-11-13 16:25:30 +01:00
YuDongYing 8e65834c44 Fix schemaZip Gradle task on MS Windows
Prior to this commit, the schemaZip Gradle task failed to find Spring
schema files on MS Windows due to path separators hard coded to forward
slashes that are not compatible with the Windows operating system.

Consequently, a full build failed on Windows since the distZip task was
not able to locate the zipped schema archive that the schemaZip task
failed to create.

This commit fixes this by updating the schemaZip task to search for
schema files using backslashes as well as forward slashes.

Closes gh-23933
2019-11-13 15:02:49 +01:00
Rossen Stoyanchev 64db939e4a Javadoc update for content negotiation
Closes gh-23409
2019-11-11 12:08:05 +00:00
Sam Brannen 3d83f869d9 Fix Javadoc for SmartLifecycle.DEFAULT_PHASE regarding ordering
Closes gh-23956
2019-11-09 18:03:43 +01:00
Juergen Hoeller 088a653318 Upgrade to RxJava 2.2.14, OkHttp 3.14.4, Awaitility 3.1.6 2019-11-07 22:35:30 +01:00
Juergen Hoeller bdb1a81a39 Use Method.getParameterCount() when full type array is never needed 2019-11-07 22:35:21 +01:00
Arjen Poutsma c2e7b6341c Polishing 2019-11-07 15:03:28 +01:00
Arjen Poutsma e75556bc7b Polishing 2019-11-07 15:02:34 +01:00
Arjen Poutsma d3d40983d6 Corrupt multipart should not hang SynchronossPartHttpMessageReader
This commit notifies the Synchronoss listener that the buffer stream
has ended.

Closes gh-23768

(cherry picked from commit bf36f49dc5)
2019-11-07 14:48:18 +01:00
Arjen Poutsma 32adf77b22 Corrupt multipart should not hang SynchronossPartHttpMessageReader
This commit notifies the Synchronoss listener that the buffer stream
has ended.

See gh-23768
2019-11-07 09:42:36 +01:00
Rossen Stoyanchev 1301c7e95d Update WebMvcConfigurer#addInterceptors Javadoc
Closes gh-23908
2019-11-06 21:38:32 +00:00
Rossen Stoyanchev 1a057654b2 Defer ExchangeFilterFunction to subscription time
Previously fixed in 5.2 via d46359. Now also backported to 5.1.x.

Closes gh-23909
2019-11-06 21:38:32 +00:00
Sam Brannen ca3440cb42 Re-enable support for invalid Expires attributes in MockCookie
Changes introduced in commit 9b2087618b
caused a regression for Cookie support in MockHttpServletResponse.
Specifically, an Expires attribute that cannot be parsed using
`ZonedDateTime.parse()` now results in an exception; whereas,
previously an entry such as `Expires=0` was allowed.

This commit fixes this issue in MockCookie by catching and ignoring any
DateTimeException thrown while attempting to parse an Expires attribute.

Closes gh-23911
2019-11-06 22:23:20 +01:00
Rossen Stoyanchev 48b22292ff Fix issue with path matching options
Closes gh-23907
2019-11-06 18:13:10 +00:00
Rossen Stoyanchev e0faaa4807 Relax domain name checks in ResponseCookie
Closes gh-23924
2019-11-06 18:05:44 +00:00
Rossen Stoyanchev 2e4944198d Fix condition in ServletInvocableHandlerMethod
Closes gh-23775
2019-11-06 18:02:28 +00:00
Spring Buildmaster 38a1caefb8 Next Development Version 2019-11-02 07:17:14 +00:00
Rossen Stoyanchev e731a0a164 Use int for maxParts instead of long 2019-10-31 14:16:48 +00:00
Brian Clozel 0f2efdbe97 Polish
See gh-23884
2019-10-30 19:58:06 +01:00
Brian Clozel feeeab1761 Reorder date formatting converter in registrar
Prior to this commit, the `DateFormatterRegistrar` would register the
annotation-based formatter before the pattern-based formatter. This
would create an issue when an application tries to convert a `String` to
an annotated `@DateTimeFormat Date`: since the converters are considered
in reversed order of registration in
`GenericConversionServicei#ConvertersForPair`, the pattern-based variant
would always be considered before the annotation-based variant,
overriding the developer's opinion.

This commit aligns the `DateFormatterRegistrar` with the
`DateTimeFormatterRegistrar` and registers the annotation-based variant
last.

Closes gh-23896
2019-10-30 17:34:49 +01:00
Juergen Hoeller b4cf471021 Polishing 2019-10-30 16:45:35 +01:00
Juergen Hoeller 82751141ac Revise concurrent JAXBContext creation towards computeIfAbsent
Closes gh-23879
2019-10-30 16:26:13 +01:00
Sam Brannen 9f43ee3304 Treat InvalidPathException like an IOException in MockServletContext
Prior to this commit, if MockServletContext was configured with a
FileSystemResourceLoader, invocations of the following methods on a
Microsoft Windows operating system resulted in an InvalidPathException
if the supplied path contained a colon (such as "C:\\temp"). This is
inconsistent with the behavior on non-Windows operating systems. In
addition, for comparable errors resulting in an IOException, those
methods (except getRealPath()) return null instead of throwing the
exception.

- getResourcePaths()
- getResource()
- getResourceAsStream()
- getRealPath()

This commit makes handling of InvalidPathException and IOException
consistent for these methods: both exceptions now result in null be
returned by these methods.

Closes gh-23717
2019-10-30 16:19:40 +01:00
Rossen Stoyanchev 614c7b0f8e ResponseStatusException associated headers
A ResponseStatus exception now exposes extra method to return headers
for the response. This is used in ResponseStatusExceptionHandler to
apply the headers to the response.

Closes gh-23741
2019-10-30 12:34:24 +00:00
Rossen Stoyanchev cc84533d85 Logging decorator for WebSocketStompClient handler
Closes gh-23793
2019-10-30 12:16:45 +00:00
Juergen Hoeller 6faf61ba40 Latest applicable dependency updates (Tomcat 9.0.27, Undertow 2.0.27, RxJava 2.2.13, Hibernate ORM 5.3.13, POI 4.1.1, Checkstyle 8.26) 2019-10-30 11:07:39 +01:00
Juergen Hoeller 80a5019534 Verify read-only propagation in DataSourceTransactionManagerTests
See gh-23747
2019-10-30 11:07:07 +01:00
Juergen Hoeller 03bd02a627 Expose primary flag on BeanDefinitionBuilder
Closes gh-23794
2019-10-30 10:30:43 +01:00
Juergen Hoeller f16aa4a9b5 Nullability refinements 2019-10-30 10:30:36 +01:00
Sam Brannen a3c9e8d4fc Polish contribution
See gh-23769
2019-10-29 13:50:38 +01:00
Vedran Pavic 3814f12b67 Preserve expires attribute in MockCookie
At present, MockCookie doesn't preserve expires attribute. This has a
consequence that a cookie value set using
MockHttpServletResponse#addHeader containing an expires attribute will
not match the cookie value obtained from
MockHttpServletResponse#getHeader, since the expires attribute will get
calculated based on current time.

This commit enhances MockCookie to preserve the expires attribute.

Closes gh-23769
2019-10-29 13:50:38 +01:00
Sam Brannen ceb881ab05 Ignore projects not in this branch 2019-10-29 13:50:38 +01:00
Rossen Stoyanchev 871464811c Limits on input stream in codecs
- Add maxInMemorySize property to Decoder and HttpMessageReader
  implementations that aggregate input to trigger
  DataBufferLimitException when reached.

- For codecs that call DataBufferUtils#join, there is now an overloaded
  variant with a maxInMemorySize extra argument. Internally, a custom
  LimitedDataBufferList is used to count and enforce the limit.

- Jackson2Tokenizer and XmlEventDecoder support those limits per
  streamed JSON object.

- Configurable limits for multipart requests with Synchronoss NIO.

- Centralized maxInMemorySize exposed via CodecConfigurer along with
  ability to plug in an instance of MultipartHttpMessageWrite.

Closes gh-23884
2019-10-29 12:36:36 +00:00
156 changed files with 4278 additions and 1070 deletions
+4
View File
@@ -24,6 +24,10 @@ buildSrc/build
/src/asciidoc/build
target/
# Projects not in this branch
integration-tests/
spring-core/kotlin-coroutines/
# Eclipse artifacts, including WTP generated manifests
.classpath
.project
+10 -10
View File
@@ -28,24 +28,24 @@ ext {
!it.name.equals("spring-build-src") && !it.name.equals("spring-framework-bom")
}
aspectjVersion = "1.9.4"
aspectjVersion = "1.9.5"
freemarkerVersion = "2.3.28"
groovyVersion = "2.5.8"
hsqldbVersion = "2.4.1"
jackson2Version = "2.9.9"
jettyVersion = "9.4.21.v20190926"
jettyVersion = "9.4.25.v20191220"
junit5Version = "5.3.2"
kotlinVersion = "1.2.71"
log4jVersion = "2.11.2"
nettyVersion = "4.1.43.Final"
reactorVersion = "Californium-SR13"
nettyVersion = "4.1.44.Final"
reactorVersion = "Californium-SR15"
rxjavaVersion = "1.3.8"
rxjavaAdapterVersion = "1.2.1"
rxjava2Version = "2.2.12"
slf4jVersion = "1.7.28" // spring-jcl + consistent 3rd party deps
rxjava2Version = "2.2.17"
slf4jVersion = "1.7.30" // spring-jcl + consistent 3rd party deps
tiles3Version = "3.0.8"
tomcatVersion = "9.0.26"
undertowVersion = "2.0.26.Final"
tomcatVersion = "9.0.30"
undertowVersion = "2.0.29.Final"
gradleScriptDir = "${rootProject.projectDir}/gradle"
withoutJclOverSlf4j = {
@@ -142,7 +142,7 @@ configure(allprojects) { project ->
}
checkstyle {
toolVersion = "8.24"
toolVersion = "8.28"
configDir = rootProject.file("src/checkstyle")
}
@@ -184,7 +184,7 @@ configure(allprojects) { project ->
"https://tiles.apache.org/tiles-request/apidocs/",
"https://tiles.apache.org/framework/apidocs/",
"https://www.eclipse.org/aspectj/doc/released/aspectj5rt-api/",
"https://www.ehcache.org/apidocs/2.10.4",
"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.9/",
"https://fasterxml.github.io/jackson-databind/javadoc/2.9/",
+1 -1
View File
@@ -1 +1 @@
version=5.1.11.BUILD-SNAPSHOT
version=5.1.13.RELEASE
+2 -2
View File
@@ -149,14 +149,14 @@ task schemaZip(type: Zip) {
def Properties schemas = new Properties();
subproject.sourceSets.main.resources.find {
it.path.endsWith("META-INF/spring.schemas")
(it.path.endsWith("META-INF/spring.schemas") || it.path.endsWith("META-INF\\spring.schemas"))
}?.withInputStream { schemas.load(it) }
for (def key : schemas.keySet()) {
def shortName = key.replaceAll(/http.*schema.(.*).spring-.*/, '$1')
assert shortName != key
File xsdFile = subproject.sourceSets.main.resources.find {
it.path.endsWith(schemas.get(key))
(it.path.endsWith(schemas.get(key)) || it.path.endsWith(schemas.get(key).replaceAll('\\/','\\\\')))
}
assert xsdFile != null
into (shortName) {
+1 -1
View File
@@ -4,6 +4,6 @@ dependencies {
compile(project(":spring-beans"))
compile(project(":spring-core"))
optional("org.aspectj:aspectjweaver:${aspectjVersion}")
optional("org.apache.commons:commons-pool2:2.6.0")
optional("org.apache.commons:commons-pool2:2.6.2")
optional("com.jamonapi:jamon:2.81")
}
+1 -1
View File
@@ -9,7 +9,7 @@ dependencies {
optional("org.codehaus.groovy:groovy-xml:${groovyVersion}")
optional("org.jetbrains.kotlin:kotlin-reflect:${kotlinVersion}")
optional("org.jetbrains.kotlin:kotlin-stdlib:${kotlinVersion}")
testCompile("org.apache.tomcat.embed:tomcat-embed-core:${tomcatVersion}")
testCompile("javax.annotation:javax.annotation-api:1.3.2")
}
// This module does joint compilation for Java and Groovy code with the compileGroovy task.
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2019 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.
@@ -43,9 +43,9 @@ import org.springframework.util.StringUtils;
* Internal class that caches JavaBeans {@link java.beans.PropertyDescriptor}
* information for a Java class. Not intended for direct use by application code.
*
* <p>Necessary for own caching of descriptors within the application's
* ClassLoader, rather than rely on the JDK's system-wide BeanInfo cache
* (in order to avoid leaks on ClassLoader shutdown).
* <p>Necessary for Spring's own caching of bean descriptors within the application
* {@link ClassLoader}, rather than relying on the JDK's system-wide {@link BeanInfo}
* cache (in order to avoid leaks on individual application shutdown in a shared JVM).
*
* <p>Information is cached statically, so we don't need to create new
* objects of this class for every JavaBean we manipulate. Hence, this class
@@ -163,7 +163,6 @@ public final class CachedIntrospectionResults {
* @return the corresponding CachedIntrospectionResults
* @throws BeansException in case of introspection failure
*/
@SuppressWarnings("unchecked")
static CachedIntrospectionResults forClass(Class<?> beanClass) throws BeansException {
CachedIntrospectionResults results = strongClassCache.get(beanClass);
if (results != null) {
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2019 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.
@@ -41,8 +41,8 @@ import org.springframework.util.ObjectUtils;
/**
* Decorator for a standard {@link BeanInfo} object, e.g. as created by
* {@link Introspector#getBeanInfo(Class)}, designed to discover and register static
* and/or non-void returning setter methods. For example:
* {@link Introspector#getBeanInfo(Class)}, designed to discover and register
* static and/or non-void returning setter methods. For example:
*
* <pre class="code">
* public class Bean {
@@ -145,11 +145,10 @@ class ExtendedBeanInfo implements BeanInfo {
public static boolean isCandidateWriteMethod(Method method) {
String methodName = method.getName();
Class<?>[] parameterTypes = method.getParameterTypes();
int nParams = parameterTypes.length;
int nParams = method.getParameterCount();
return (methodName.length() > 3 && methodName.startsWith("set") && Modifier.isPublic(method.getModifiers()) &&
(!void.class.isAssignableFrom(method.getReturnType()) || Modifier.isStatic(method.getModifiers())) &&
(nParams == 1 || (nParams == 2 && int.class == parameterTypes[0])));
(nParams == 1 || (nParams == 2 && int.class == method.getParameterTypes()[0])));
}
private void handleCandidateWriteMethod(Method method) throws IntrospectionException {
@@ -209,7 +208,7 @@ class ExtendedBeanInfo implements BeanInfo {
}
private String propertyNameFor(Method method) {
return Introspector.decapitalize(method.getName().substring(3, method.getName().length()));
return Introspector.decapitalize(method.getName().substring(3));
}
@@ -488,7 +487,7 @@ class ExtendedBeanInfo implements BeanInfo {
}
/*
* See java.beans.IndexedPropertyDescriptor#equals(java.lang.Object)
* See java.beans.IndexedPropertyDescriptor#equals
*/
@Override
public boolean equals(Object other) {
@@ -535,11 +534,13 @@ class ExtendedBeanInfo implements BeanInfo {
public int compare(PropertyDescriptor desc1, PropertyDescriptor desc2) {
String left = desc1.getName();
String right = desc2.getName();
byte[] leftBytes = left.getBytes();
byte[] rightBytes = right.getBytes();
for (int i = 0; i < left.length(); i++) {
if (right.length() == i) {
return 1;
}
int result = left.getBytes()[i] - right.getBytes()[i];
int result = leftBytes[i] - rightBytes[i];
if (result != 0) {
return result;
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -66,8 +66,7 @@ abstract class PropertyDescriptorUtils {
Class<?> propertyType = null;
if (readMethod != null) {
Class<?>[] params = readMethod.getParameterTypes();
if (params.length != 0) {
if (readMethod.getParameterCount() != 0) {
throw new IntrospectionException("Bad read method arg count: " + readMethod);
}
propertyType = readMethod.getReturnType();
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2019 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.
@@ -182,10 +182,12 @@ public class GroovyBeanDefinitionReader extends AbstractBeanDefinitionReader imp
}
@Override
public void setMetaClass(MetaClass metaClass) {
this.metaClass = metaClass;
}
@Override
public MetaClass getMetaClass() {
return this.metaClass;
}
@@ -216,6 +218,7 @@ public class GroovyBeanDefinitionReader extends AbstractBeanDefinitionReader imp
* @return the number of bean definitions found
* @throws BeanDefinitionStoreException in case of loading or parsing errors
*/
@Override
public int loadBeanDefinitions(Resource resource) throws BeanDefinitionStoreException {
return loadBeanDefinitions(new EncodedResource(resource));
}
@@ -240,10 +243,11 @@ public class GroovyBeanDefinitionReader extends AbstractBeanDefinitionReader imp
logger.trace("Loading Groovy bean definitions from " + encodedResource);
}
Closure beans = new Closure(this) {
@SuppressWarnings("serial")
Closure<Object> beans = new Closure<Object>(this) {
@Override
public Object call(Object[] args) {
invokeBeanDefiningClosure((Closure) args[0]);
public Object call(Object... args) {
invokeBeanDefiningClosure((Closure<?>) args[0]);
return null;
}
};
@@ -285,7 +289,7 @@ public class GroovyBeanDefinitionReader extends AbstractBeanDefinitionReader imp
* @param closure the block or closure
* @return this {@code GroovyBeanDefinitionReader} instance
*/
public GroovyBeanDefinitionReader beans(Closure closure) {
public GroovyBeanDefinitionReader beans(Closure<?> closure) {
return invokeBeanDefiningClosure(closure);
}
@@ -309,25 +313,22 @@ public class GroovyBeanDefinitionReader extends AbstractBeanDefinitionReader imp
public AbstractBeanDefinition bean(Class<?> type, Object...args) {
GroovyBeanDefinitionWrapper current = this.currentBeanDefinition;
try {
Closure callable = null;
Collection constructorArgs = null;
Closure<?> callable = null;
Collection<Object> constructorArgs = null;
if (!ObjectUtils.isEmpty(args)) {
int index = args.length;
Object lastArg = args[index - 1];
if (lastArg instanceof Closure) {
callable = (Closure) lastArg;
if (lastArg instanceof Closure<?>) {
callable = (Closure<?>) lastArg;
index--;
}
if (index > -1) {
constructorArgs = resolveConstructorArguments(args, 0, index);
}
constructorArgs = resolveConstructorArguments(args, 0, index);
}
this.currentBeanDefinition = new GroovyBeanDefinitionWrapper(null, type, constructorArgs);
if (callable != null) {
callable.call(this.currentBeanDefinition);
}
return this.currentBeanDefinition.getBeanDefinition();
}
finally {
this.currentBeanDefinition = current;
@@ -373,10 +374,11 @@ public class GroovyBeanDefinitionReader extends AbstractBeanDefinitionReader imp
* This method overrides method invocation to create beans for each method name that
* takes a class argument.
*/
@Override
public Object invokeMethod(String name, Object arg) {
Object[] args = (Object[])arg;
if ("beans".equals(name) && args.length == 1 && args[0] instanceof Closure) {
return beans((Closure) args[0]);
return beans((Closure<?>) args[0]);
}
else if ("ref".equals(name)) {
String refName;
@@ -429,10 +431,10 @@ public class GroovyBeanDefinitionReader extends AbstractBeanDefinitionReader imp
private void finalizeDeferredProperties() {
for (DeferredProperty dp : this.deferredProperties.values()) {
if (dp.value instanceof List) {
dp.value = manageListIfNecessary((List) dp.value);
dp.value = manageListIfNecessary((List<?>) dp.value);
}
else if (dp.value instanceof Map) {
dp.value = manageMapIfNecessary((Map) dp.value);
dp.value = manageMapIfNecessary((Map<?, ?>) dp.value);
}
dp.apply();
}
@@ -444,7 +446,7 @@ public class GroovyBeanDefinitionReader extends AbstractBeanDefinitionReader imp
* @param callable the closure argument
* @return this {@code GroovyBeanDefinitionReader} instance
*/
protected GroovyBeanDefinitionReader invokeBeanDefiningClosure(Closure callable) {
protected GroovyBeanDefinitionReader invokeBeanDefiningClosure(Closure<?> callable) {
callable.setDelegate(this);
callable.call();
finalizeDeferredProperties();
@@ -483,9 +485,10 @@ public class GroovyBeanDefinitionReader extends AbstractBeanDefinitionReader imp
else if (args[0] instanceof Map) {
// named constructor arguments
if (args.length > 1 && args[1] instanceof Class) {
List constructorArgs = resolveConstructorArguments(args, 2, hasClosureArgument ? args.length - 1 : args.length);
this.currentBeanDefinition = new GroovyBeanDefinitionWrapper(beanName, (Class)args[1], constructorArgs);
Map namedArgs = (Map)args[0];
List<Object> constructorArgs =
resolveConstructorArguments(args, 2, hasClosureArgument ? args.length - 1 : args.length);
this.currentBeanDefinition = new GroovyBeanDefinitionWrapper(beanName, (Class<?>) args[1], constructorArgs);
Map<?, ?> namedArgs = (Map<?, ?>) args[0];
for (Object o : namedArgs.keySet()) {
String propName = (String) o;
setProperty(propName, namedArgs.get(propName));
@@ -494,8 +497,8 @@ public class GroovyBeanDefinitionReader extends AbstractBeanDefinitionReader imp
// factory method syntax
else {
this.currentBeanDefinition = new GroovyBeanDefinitionWrapper(beanName);
//First arg is the map containing factoryBean : factoryMethod
Map.Entry factoryBeanEntry = (Map.Entry) ((Map) args[0]).entrySet().iterator().next();
// First arg is the map containing factoryBean : factoryMethod
Map.Entry<?, ?> factoryBeanEntry = ((Map<?, ?>) args[0]).entrySet().iterator().next();
// If we have a closure body, that will be the last argument.
// In between are the constructor args
int constructorArgsTest = (hasClosureArgument ? 2 : 1);
@@ -519,12 +522,13 @@ public class GroovyBeanDefinitionReader extends AbstractBeanDefinitionReader imp
this.currentBeanDefinition.getBeanDefinition().setAbstract(true);
}
else {
List constructorArgs = resolveConstructorArguments(args, 0, hasClosureArgument ? args.length - 1 : args.length);
List<Object> constructorArgs =
resolveConstructorArguments(args, 0, hasClosureArgument ? args.length - 1 : args.length);
this.currentBeanDefinition = new GroovyBeanDefinitionWrapper(beanName, null, constructorArgs);
}
if (hasClosureArgument) {
Closure callable = (Closure) args[args.length - 1];
Closure<?> callable = (Closure<?>) args[args.length - 1];
callable.setDelegate(this);
callable.setResolveStrategy(Closure.DELEGATE_FIRST);
callable.call(this.currentBeanDefinition);
@@ -544,10 +548,10 @@ public class GroovyBeanDefinitionReader extends AbstractBeanDefinitionReader imp
constructorArgs[i] = constructorArgs[i].toString();
}
else if (constructorArgs[i] instanceof List) {
constructorArgs[i] = manageListIfNecessary((List) constructorArgs[i]);
constructorArgs[i] = manageListIfNecessary((List<?>) constructorArgs[i]);
}
else if (constructorArgs[i] instanceof Map){
constructorArgs[i] = manageMapIfNecessary((Map) constructorArgs[i]);
constructorArgs[i] = manageMapIfNecessary((Map<?, ?>) constructorArgs[i]);
}
}
return Arrays.asList(constructorArgs);
@@ -601,6 +605,7 @@ public class GroovyBeanDefinitionReader extends AbstractBeanDefinitionReader imp
* This method overrides property setting in the scope of the {@code GroovyBeanDefinitionReader}
* to set properties on the current bean definition.
*/
@Override
public void setProperty(String name, Object value) {
if (this.currentBeanDefinition != null) {
applyPropertyToBeanDefinition(name, value);
@@ -617,7 +622,7 @@ public class GroovyBeanDefinitionReader extends AbstractBeanDefinitionReader imp
else if (value instanceof Closure) {
GroovyBeanDefinitionWrapper current = this.currentBeanDefinition;
try {
Closure callable = (Closure) value;
Closure<?> callable = (Closure<?>) value;
Class<?> parameterType = callable.getParameterTypes()[0];
if (Object.class == parameterType) {
this.currentBeanDefinition = new GroovyBeanDefinitionWrapper("");
@@ -647,6 +652,7 @@ public class GroovyBeanDefinitionReader extends AbstractBeanDefinitionReader imp
* properties from the {@code GroovyBeanDefinitionReader} itself
* </ul>
*/
@Override
public Object getProperty(String name) {
Binding binding = getBinding();
if (binding != null && binding.hasVariable(name)) {
@@ -690,8 +696,8 @@ public class GroovyBeanDefinitionReader extends AbstractBeanDefinitionReader imp
}
private GroovyDynamicElementReader createDynamicElementReader(String namespace) {
XmlReaderContext readerContext = this.groovyDslXmlBeanDefinitionReader.createReaderContext(new DescriptiveResource(
"Groovy"));
XmlReaderContext readerContext = this.groovyDslXmlBeanDefinitionReader.createReaderContext(
new DescriptiveResource("Groovy"));
BeanDefinitionParserDelegate delegate = new BeanDefinitionParserDelegate(readerContext);
boolean decorating = (this.currentBeanDefinition != null);
if (!decorating) {
@@ -749,10 +755,12 @@ public class GroovyBeanDefinitionReader extends AbstractBeanDefinitionReader imp
this.metaClass = InvokerHelper.getMetaClass(this);
}
@Override
public MetaClass getMetaClass() {
return this.metaClass;
}
@Override
public Object getProperty(String property) {
if (property.equals("beanName")) {
return getBeanName();
@@ -769,14 +777,17 @@ public class GroovyBeanDefinitionReader extends AbstractBeanDefinitionReader imp
}
}
@Override
public Object invokeMethod(String name, Object args) {
return this.metaClass.invokeMethod(this, name, args);
}
@Override
public void setMetaClass(MetaClass metaClass) {
this.metaClass = metaClass;
}
@Override
public void setProperty(String property, Object newValue) {
if (!addDeferredProperty(property, newValue)) {
this.beanDefinition.getBeanDefinition().getPropertyValues().add(property, newValue);
@@ -785,7 +796,7 @@ public class GroovyBeanDefinitionReader extends AbstractBeanDefinitionReader imp
/**
* Wraps a bean definition property an ensures that any RuntimeBeanReference
* Wraps a bean definition property and ensures that any RuntimeBeanReference
* additions to it are deferred for resolution later.
*/
private class GroovyPropertyValue extends GroovyObjectSupport {
@@ -799,18 +810,21 @@ public class GroovyBeanDefinitionReader extends AbstractBeanDefinitionReader imp
this.propertyValue = propertyValue;
}
@SuppressWarnings("unused")
public void leftShift(Object value) {
InvokerHelper.invokeMethod(this.propertyValue, "leftShift", value);
updateDeferredProperties(value);
}
@SuppressWarnings("unused")
public boolean add(Object value) {
boolean retVal = (Boolean) InvokerHelper.invokeMethod(this.propertyValue, "add", value);
updateDeferredProperties(value);
return retVal;
}
public boolean addAll(Collection values) {
@SuppressWarnings("unused")
public boolean addAll(Collection<?> values) {
boolean retVal = (Boolean) InvokerHelper.invokeMethod(this.propertyValue, "addAll", values);
for (Object value : values) {
updateDeferredProperties(value);
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2019 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.
@@ -196,7 +196,7 @@ class GroovyBeanDefinitionWrapper extends GroovyObjectSupport {
// constructorArgs
else if (CONSTRUCTOR_ARGS.equals(property) && newValue instanceof List) {
ConstructorArgumentValues cav = new ConstructorArgumentValues();
List args = (List) newValue;
List<?> args = (List<?>) newValue;
for (Object arg : args) {
cav.addGenericArgumentValue(arg);
}
@@ -645,7 +645,6 @@ public abstract class AbstractAutowireCapableBeanFactory extends AbstractBeanFac
@Nullable
protected Class<?> predictBeanType(String beanName, RootBeanDefinition mbd, Class<?>... typesToMatch) {
Class<?> targetType = determineTargetType(beanName, mbd, typesToMatch);
// Apply SmartInstantiationAwareBeanPostProcessors to predict the
// eventual type after a before-instantiation shortcut.
if (targetType != null && !mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
@@ -1364,34 +1363,28 @@ public abstract class AbstractAutowireCapableBeanFactory extends AbstractBeanFac
// Give any InstantiationAwareBeanPostProcessors the opportunity to modify the
// state of the bean before properties are set. This can be used, for example,
// to support styles of field injection.
boolean continueWithPropertyPopulation = true;
if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
for (BeanPostProcessor bp : getBeanPostProcessors()) {
if (bp instanceof InstantiationAwareBeanPostProcessor) {
InstantiationAwareBeanPostProcessor ibp = (InstantiationAwareBeanPostProcessor) bp;
if (!ibp.postProcessAfterInstantiation(bw.getWrappedInstance(), beanName)) {
continueWithPropertyPopulation = false;
break;
return;
}
}
}
}
if (!continueWithPropertyPopulation) {
return;
}
PropertyValues pvs = (mbd.hasPropertyValues() ? mbd.getPropertyValues() : null);
if (mbd.getResolvedAutowireMode() == AUTOWIRE_BY_NAME || mbd.getResolvedAutowireMode() == AUTOWIRE_BY_TYPE) {
int resolvedAutowireMode = mbd.getResolvedAutowireMode();
if (resolvedAutowireMode == AUTOWIRE_BY_NAME || resolvedAutowireMode == AUTOWIRE_BY_TYPE) {
MutablePropertyValues newPvs = new MutablePropertyValues(pvs);
// Add property values based on autowire by name if applicable.
if (mbd.getResolvedAutowireMode() == AUTOWIRE_BY_NAME) {
if (resolvedAutowireMode == AUTOWIRE_BY_NAME) {
autowireByName(beanName, mbd, bw, newPvs);
}
// Add property values based on autowire by type if applicable.
if (mbd.getResolvedAutowireMode() == AUTOWIRE_BY_TYPE) {
if (resolvedAutowireMode == AUTOWIRE_BY_TYPE) {
autowireByType(beanName, mbd, bw, newPvs);
}
pvs = newPvs;
@@ -1495,7 +1488,7 @@ public abstract class AbstractAutowireCapableBeanFactory extends AbstractBeanFac
if (Object.class != pd.getPropertyType()) {
MethodParameter methodParam = BeanUtils.getWriteMethodParameter(pd);
// Do not allow eager init for type matching in case of a prioritized post-processor.
boolean eager = !PriorityOrdered.class.isInstance(bw.getWrappedInstance());
boolean eager = !(bw.getWrappedInstance() instanceof PriorityOrdered);
DependencyDescriptor desc = new AutowireByTypeDependencyDescriptor(methodParam, eager);
Object autowiredArgument = resolveDependency(desc, beanName, autowiredBeanNames, converter);
if (autowiredArgument != null) {
@@ -1127,30 +1127,30 @@ public abstract class AbstractBeanDefinition extends BeanMetadataAttributeAccess
return false;
}
AbstractBeanDefinition that = (AbstractBeanDefinition) other;
boolean rtn = ObjectUtils.nullSafeEquals(getBeanClassName(), that.getBeanClassName());
rtn = rtn &= ObjectUtils.nullSafeEquals(this.scope, that.scope);
rtn = rtn &= this.abstractFlag == that.abstractFlag;
rtn = rtn &= this.lazyInit == that.lazyInit;
rtn = rtn &= this.autowireMode == that.autowireMode;
rtn = rtn &= this.dependencyCheck == that.dependencyCheck;
rtn = rtn &= Arrays.equals(this.dependsOn, that.dependsOn);
rtn = rtn &= this.autowireCandidate == that.autowireCandidate;
rtn = rtn &= ObjectUtils.nullSafeEquals(this.qualifiers, that.qualifiers);
rtn = rtn &= this.primary == that.primary;
rtn = rtn &= this.nonPublicAccessAllowed == that.nonPublicAccessAllowed;
rtn = rtn &= this.lenientConstructorResolution == that.lenientConstructorResolution;
rtn = rtn &= ObjectUtils.nullSafeEquals(this.constructorArgumentValues, that.constructorArgumentValues);
rtn = rtn &= ObjectUtils.nullSafeEquals(this.propertyValues, that.propertyValues);
rtn = rtn &= ObjectUtils.nullSafeEquals(this.methodOverrides, that.methodOverrides);
rtn = rtn &= ObjectUtils.nullSafeEquals(this.factoryBeanName, that.factoryBeanName);
rtn = rtn &= ObjectUtils.nullSafeEquals(this.factoryMethodName, that.factoryMethodName);
rtn = rtn &= ObjectUtils.nullSafeEquals(this.initMethodName, that.initMethodName);
rtn = rtn &= this.enforceInitMethod == that.enforceInitMethod;
rtn = rtn &= ObjectUtils.nullSafeEquals(this.destroyMethodName, that.destroyMethodName);
rtn = rtn &= this.enforceDestroyMethod == that.enforceDestroyMethod;
rtn = rtn &= this.synthetic == that.synthetic;
rtn = rtn &= this.role == that.role;
return rtn && super.equals(other);
return (ObjectUtils.nullSafeEquals(getBeanClassName(), that.getBeanClassName()) &&
ObjectUtils.nullSafeEquals(this.scope, that.scope) &&
this.abstractFlag == that.abstractFlag &&
this.lazyInit == that.lazyInit &&
this.autowireMode == that.autowireMode &&
this.dependencyCheck == that.dependencyCheck &&
Arrays.equals(this.dependsOn, that.dependsOn) &&
this.autowireCandidate == that.autowireCandidate &&
ObjectUtils.nullSafeEquals(this.qualifiers, that.qualifiers) &&
this.primary == that.primary &&
this.nonPublicAccessAllowed == that.nonPublicAccessAllowed &&
this.lenientConstructorResolution == that.lenientConstructorResolution &&
ObjectUtils.nullSafeEquals(this.constructorArgumentValues, that.constructorArgumentValues) &&
ObjectUtils.nullSafeEquals(this.propertyValues, that.propertyValues) &&
ObjectUtils.nullSafeEquals(this.methodOverrides, that.methodOverrides) &&
ObjectUtils.nullSafeEquals(this.factoryBeanName, that.factoryBeanName) &&
ObjectUtils.nullSafeEquals(this.factoryMethodName, that.factoryMethodName) &&
ObjectUtils.nullSafeEquals(this.initMethodName, that.initMethodName) &&
this.enforceInitMethod == that.enforceInitMethod &&
ObjectUtils.nullSafeEquals(this.destroyMethodName, that.destroyMethodName) &&
this.enforceDestroyMethod == that.enforceDestroyMethod &&
this.synthetic == that.synthetic &&
this.role == that.role &&
super.equals(other));
}
@Override
@@ -302,6 +302,15 @@ public final class BeanDefinitionBuilder {
return this;
}
/**
* Set whether this bean is a primary autowire candidate.
* @since 5.1.11
*/
public BeanDefinitionBuilder setPrimary(boolean primary) {
this.beanDefinition.setPrimary(primary);
return this;
}
/**
* Set the role of this definition.
*/
@@ -612,6 +612,7 @@ public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements
* should <i>not</i> have their own mutexes involved in singleton creation,
* to avoid the potential for deadlocks in lazy-init situations.
*/
@Override
public final Object getSingletonMutex() {
return this.singletonObjects;
}
@@ -311,9 +311,9 @@ class DisposableBeanAdapter implements DisposableBean, Runnable, Serializable {
* assuming a "force" parameter), else logging an error.
*/
private void invokeCustomDestroyMethod(final Method destroyMethod) {
Class<?>[] paramTypes = destroyMethod.getParameterTypes();
final Object[] args = new Object[paramTypes.length];
if (paramTypes.length == 1) {
int paramCount = destroyMethod.getParameterCount();
final Object[] args = new Object[paramCount];
if (paramCount == 1) {
args[0] = Boolean.TRUE;
}
if (logger.isTraceEnabled()) {
@@ -16,7 +16,7 @@ dependencies {
optional("org.freemarker:freemarker:${freemarkerVersion}")
testCompile(project(":spring-context"))
testCompile("org.hsqldb:hsqldb:${hsqldbVersion}")
testCompile("org.hibernate:hibernate-validator:6.0.17.Final")
testCompile("org.hibernate:hibernate-validator:6.0.18.Final")
testCompile("javax.annotation:javax.annotation-api:1.3.2")
testRuntime("org.ehcache:jcache:1.0.1")
testRuntime("org.ehcache:ehcache:3.4.0")
@@ -88,6 +88,7 @@ public class JCacheAspectSupport extends AbstractCacheInvoker implements Initial
return this.cacheOperationSource;
}
@Override
public void afterPropertiesSet() {
getCacheOperationSource();
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2019 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.
@@ -110,7 +110,7 @@ public class LocalDataSourceJobStore extends JobStoreCMT {
public void shutdown() {
// Do nothing - a Spring-managed DataSource has its own lifecycle.
}
/* Quartz 2.2 initialize method */
@Override
public void initialize() {
// Do nothing - a Spring-managed DataSource has its own lifecycle.
}
@@ -138,7 +138,7 @@ public class LocalDataSourceJobStore extends JobStoreCMT {
public void shutdown() {
// Do nothing - a Spring-managed DataSource has its own lifecycle.
}
/* Quartz 2.2 initialize method */
@Override
public void initialize() {
// Do nothing - a Spring-managed DataSource has its own lifecycle.
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2017 the original author or authors.
* Copyright 2002-2019 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.
@@ -82,6 +82,7 @@ public class ResourceLoaderClassLoadHelper implements ClassLoadHelper {
}
@SuppressWarnings("unchecked")
@Override
public <T> Class<? extends T> loadClass(String name, Class<T> clazz) throws ClassNotFoundException {
return (Class<? extends T>) loadClass(name);
}
+3 -3
View File
@@ -19,16 +19,16 @@ dependencies {
optional("org.aspectj:aspectjweaver:${aspectjVersion}")
optional("org.codehaus.groovy:groovy:${groovyVersion}")
optional("org.beanshell:bsh:2.0b5")
optional("joda-time:joda-time:2.10.4")
optional("joda-time:joda-time:2.10.5")
optional("org.hibernate:hibernate-validator:5.4.3.Final")
optional("org.jetbrains.kotlin:kotlin-reflect:${kotlinVersion}")
optional("org.jetbrains.kotlin:kotlin-stdlib:${kotlinVersion}")
testCompile("org.codehaus.groovy:groovy-jsr223:${groovyVersion}")
testCompile("org.codehaus.groovy:groovy-test:${groovyVersion}")
testCompile("org.codehaus.groovy:groovy-xml:${groovyVersion}")
testCompile("org.apache.commons:commons-pool2:2.6.0")
testCompile("org.apache.commons:commons-pool2:2.6.2")
testCompile("javax.inject:javax.inject-tck:1")
testCompile("org.awaitility:awaitility:3.1.3")
testCompile("org.awaitility:awaitility:3.1.6")
testRuntime("javax.xml.bind:jaxb-api:2.3.1")
testRuntime("org.glassfish:javax.el:3.0.1-b08")
testRuntime("org.javamoney:moneta:1.3")
@@ -32,7 +32,9 @@ import org.springframework.util.StringUtils;
@SuppressWarnings("serial")
public class SimpleKey implements Serializable {
/** An empty key. */
/**
* An empty key.
*/
public static final SimpleKey EMPTY = new SimpleKey();
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2019 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,19 +17,21 @@
package org.springframework.context;
/**
* An extension of the {@link Lifecycle} interface for those objects that require to
* be started upon ApplicationContext refresh and/or shutdown in a particular order.
* The {@link #isAutoStartup()} return value indicates whether this object should
* An extension of the {@link Lifecycle} interface for those objects that require
* to be started upon {@code ApplicationContext} refresh and/or shutdown in a
* particular order.
*
* <p>The {@link #isAutoStartup()} return value indicates whether this object should
* be started at the time of a context refresh. The callback-accepting
* {@link #stop(Runnable)} method is useful for objects that have an asynchronous
* shutdown process. Any implementation of this interface <i>must</i> invoke the
* callback's {@code run()} method upon shutdown completion to avoid unnecessary
* delays in the overall ApplicationContext shutdown.
* delays in the overall {@code ApplicationContext} shutdown.
*
* <p>This interface extends {@link Phased}, and the {@link #getPhase()} method's
* return value indicates the phase within which this Lifecycle component should
* be started and stopped. The startup process begins with the <i>lowest</i> phase
* value and ends with the <i>highest</i> phase value ({@code Integer.MIN_VALUE}
* return value indicates the phase within which this {@code Lifecycle} component
* should be started and stopped. The startup process begins with the <i>lowest</i>
* phase value and ends with the <i>highest</i> phase value ({@code Integer.MIN_VALUE}
* is the lowest possible, and {@code Integer.MAX_VALUE} is the highest possible).
* The shutdown process will apply the reverse order. Any components with the
* same value will be arbitrarily ordered within the same phase.
@@ -44,9 +46,11 @@ package org.springframework.context;
*
* <p>Any {@code Lifecycle} components within the context that do not also
* implement {@code SmartLifecycle} will be treated as if they have a phase
* value of 0. That way a {@code SmartLifecycle} implementation may start
* before those {@code Lifecycle} components if it has a negative phase value,
* or it may start after those components if it has a positive phase value.
* value of {@code 0}. This allows a {@code SmartLifecycle} component to start
* before those {@code Lifecycle} components if the {@code SmartLifecycle}
* component has a negative phase value, or the {@code SmartLifecycle} component
* may start after those {@code Lifecycle} components if the {@code SmartLifecycle}
* component has a positive phase value.
*
* <p>Note that, due to the auto-startup support in {@code SmartLifecycle}, a
* {@code SmartLifecycle} bean instance will usually get initialized on startup
@@ -55,6 +59,7 @@ package org.springframework.context;
*
* @author Mark Fisher
* @author Juergen Hoeller
* @author Sam Brannen
* @since 3.0
* @see LifecycleProcessor
* @see ConfigurableApplicationContext
@@ -63,9 +68,10 @@ public interface SmartLifecycle extends Lifecycle, Phased {
/**
* The default phase for {@code SmartLifecycle}: {@code Integer.MAX_VALUE}.
* <p>This is different from the common phase 0 associated with regular
* <p>This is different from the common phase {@code 0} associated with regular
* {@link Lifecycle} implementations, putting the typically auto-started
* {@code SmartLifecycle} beans into a separate later shutdown phase.
* {@code SmartLifecycle} beans into a later startup phase and an earlier
* shutdown phase.
* @since 5.1
* @see #getPhase()
* @see org.springframework.context.support.DefaultLifecycleProcessor#getPhase(Lifecycle)
@@ -115,7 +121,8 @@ public interface SmartLifecycle extends Lifecycle, Phased {
/**
* Return the phase that this lifecycle object is supposed to run in.
* <p>The default implementation returns {@link #DEFAULT_PHASE} in order to
* let stop callbacks execute after regular {@code Lifecycle} implementations.
* let {@code stop()} callbacks execute after regular {@code Lifecycle}
* implementations.
* @see #isAutoStartup()
* @see #start()
* @see #stop(Runnable)
@@ -16,8 +16,6 @@
package org.springframework.context.annotation;
import java.util.Objects;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.lang.Nullable;
@@ -100,7 +98,7 @@ public interface DeferredImportSelector extends ImportSelector {
}
@Override
public boolean equals(Object other) {
public boolean equals(@Nullable Object other) {
if (this == other) {
return true;
}
@@ -108,13 +106,17 @@ public interface DeferredImportSelector extends ImportSelector {
return false;
}
Entry entry = (Entry) other;
return (Objects.equals(this.metadata, entry.metadata) &&
Objects.equals(this.importClassName, entry.importClassName));
return (this.metadata.equals(entry.metadata) && this.importClassName.equals(entry.importClassName));
}
@Override
public int hashCode() {
return Objects.hash(this.metadata, this.importClassName);
return (this.metadata.hashCode() * 31 + this.importClassName.hashCode());
}
@Override
public String toString() {
return this.importClassName;
}
}
}
@@ -225,18 +225,22 @@ public class GenericGroovyApplicationContext extends GenericApplicationContext i
// Implementation of the GroovyObject interface
@Override
public void setMetaClass(MetaClass metaClass) {
this.metaClass = metaClass;
}
@Override
public MetaClass getMetaClass() {
return this.metaClass;
}
@Override
public Object invokeMethod(String name, Object args) {
return this.metaClass.invokeMethod(this, name, args);
}
@Override
public void setProperty(String property, Object newValue) {
if (newValue instanceof BeanDefinition) {
registerBeanDefinition(property, (BeanDefinition) newValue);
@@ -246,6 +250,7 @@ public class GenericGroovyApplicationContext extends GenericApplicationContext i
}
}
@Override
@Nullable
public Object getProperty(String property) {
if (containsBean(property)) {
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2019 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.
@@ -237,6 +237,7 @@ public class ReloadableResourceBundleMessageSource extends AbstractResourceBased
if (mergedHolder != null) {
return mergedHolder;
}
Properties mergedProps = newProperties();
long latestTimestamp = -1;
String[] basenames = StringUtils.toStringArray(getBasenameSet());
@@ -253,6 +254,7 @@ public class ReloadableResourceBundleMessageSource extends AbstractResourceBased
}
}
}
mergedHolder = new PropertiesHolder(mergedProps, latestTimestamp);
PropertiesHolder existing = this.cachedMergedProperties.putIfAbsent(locale, mergedHolder);
if (existing != null) {
@@ -279,18 +281,28 @@ public class ReloadableResourceBundleMessageSource extends AbstractResourceBased
return filenames;
}
}
// Filenames for given Locale
List<String> filenames = new ArrayList<>(7);
filenames.addAll(calculateFilenamesForLocale(basename, locale));
if (isFallbackToSystemLocale() && !locale.equals(Locale.getDefault())) {
List<String> fallbackFilenames = calculateFilenamesForLocale(basename, Locale.getDefault());
for (String fallbackFilename : fallbackFilenames) {
if (!filenames.contains(fallbackFilename)) {
// Entry for fallback locale that isn't already in filenames list.
filenames.add(fallbackFilename);
// Filenames for default Locale, if any
if (isFallbackToSystemLocale()) {
Locale defaultLocale = Locale.getDefault();
if (!locale.equals(defaultLocale)) {
List<String> fallbackFilenames = calculateFilenamesForLocale(basename, defaultLocale);
for (String fallbackFilename : fallbackFilenames) {
if (!filenames.contains(fallbackFilename)) {
// Entry for fallback locale that isn't already in filenames list.
filenames.add(fallbackFilename);
}
}
}
}
// Filename for default bundle file
filenames.add(basename);
if (localeMap == null) {
localeMap = new ConcurrentHashMap<>();
Map<Locale, List<String>> existing = this.cachedFilenames.putIfAbsent(basename, localeMap);
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2019 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.
@@ -43,6 +43,7 @@ public class StaticMessageSource extends AbstractMessageSource {
@Override
@Nullable
protected String resolveCodeWithoutArguments(String code, Locale locale) {
return this.messages.get(code + '_' + locale.toString());
}
@@ -61,14 +61,13 @@ public class DateFormatterRegistrar implements FormatterRegistrar {
@Override
public void registerFormatters(FormatterRegistry registry) {
addDateConverters(registry);
registry.addFormatterForFieldAnnotation(new DateTimeFormatAnnotationFormatterFactory());
// In order to retain back compatibility we only register Date/Calendar
// types when a user defined formatter is specified (see SPR-10105)
if (this.dateFormatter != null) {
registry.addFormatter(this.dateFormatter);
registry.addFormatterForFieldType(Calendar.class, this.dateFormatter);
}
registry.addFormatterForFieldAnnotation(new DateTimeFormatAnnotationFormatterFactory());
}
/**
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2016 the original author or authors.
* Copyright 2002-2019 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,7 @@ import org.springframework.format.Formatter;
* (which is commonly used for HTTP date header values), as of Spring 4.3.
*
* @author Juergen Hoeller
* @author Andrei Nevedomskii
* @since 4.0
* @see java.time.Instant#parse
* @see java.time.format.DateTimeFormatter#ISO_INSTANT
@@ -40,14 +41,14 @@ public class InstantFormatter implements Formatter<Instant> {
@Override
public Instant parse(String text, Locale locale) throws ParseException {
if (text.length() > 0 && Character.isDigit(text.charAt(0))) {
// assuming UTC instant a la "2007-12-03T10:15:30.00Z"
return Instant.parse(text);
}
else {
if (text.length() > 0 && Character.isAlphabetic(text.charAt(0))) {
// assuming RFC-1123 value a la "Tue, 3 Jun 2008 11:05:30 GMT"
return Instant.from(DateTimeFormatter.RFC_1123_DATE_TIME.parse(text));
}
else {
// assuming UTC instant a la "2007-12-03T10:15:30.00Z"
return Instant.parse(text);
}
}
@Override
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2020 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.
@@ -207,7 +207,7 @@ public class InterfaceBasedMBeanInfoAssembler extends AbstractConfigurableMBeanI
* configured interfaces and is public, otherwise {@code false}.
*/
private boolean isPublicInInterface(Method method, String beanKey) {
return ((method.getModifiers() & Modifier.PUBLIC) > 0) && isDeclaredInInterface(method, beanKey);
return Modifier.isPublic(method.getModifiers()) && isDeclaredInInterface(method, beanKey);
}
/**
@@ -231,6 +231,7 @@ public class InterfaceBasedMBeanInfoAssembler extends AbstractConfigurableMBeanI
for (Class<?> ifc : ifaces) {
for (Method ifcMethod : ifc.getMethods()) {
if (ifcMethod.getName().equals(method.getName()) &&
ifcMethod.getParameterCount() == method.getParameterCount() &&
Arrays.equals(ifcMethod.getParameterTypes(), method.getParameterTypes())) {
return true;
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2017 the original author or authors.
* Copyright 2002-2019 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,7 +60,7 @@ public class CacheReproTests {
@Test
public void spr11124MultipleAnnotations() throws Exception {
public void spr11124MultipleAnnotations() {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Spr11124Config.class);
Spr11124Service bean = context.getBean(Spr11124Service.class);
bean.single(2);
@@ -71,7 +71,7 @@ public class CacheReproTests {
}
@Test
public void spr11249PrimitiveVarargs() throws Exception {
public void spr11249PrimitiveVarargs() {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Spr11249Config.class);
Spr11249Service bean = context.getBean(Spr11249Service.class);
Object result = bean.doSomething("op", 2, 3);
@@ -44,7 +44,7 @@ public class SimpleKeyGeneratorTests {
}
@Test
public void singleValue(){
public void singleValue() {
Object k1 = generateKey(new Object[] { "a" });
Object k2 = generateKey(new Object[] { "a" });
Object k3 = generateKey(new Object[] { "different" });
@@ -56,7 +56,7 @@ public class SimpleKeyGeneratorTests {
}
@Test
public void multipleValues() {
public void multipleValues() {
Object k1 = generateKey(new Object[] { "a", 1, "b" });
Object k2 = generateKey(new Object[] { "a", 1, "b" });
Object k3 = generateKey(new Object[] { "b", 1, "a" });
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2019 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.
@@ -794,6 +794,7 @@ public class AnnotationDrivenEventListenerTests {
@EventListener
@Async
@Override
public void handleAsync(AnotherTestEvent event) {
assertTrue(!Thread.currentThread().getName().equals(event.content));
this.eventCollector.addEvent(this, event);
@@ -820,6 +821,7 @@ public class AnnotationDrivenEventListenerTests {
@EventListener
@Async
@Override
public void handleAsync(AnotherTestEvent event) {
assertTrue(!Thread.currentThread().getName().equals(event.content));
this.eventCollector.addEvent(this, event);
@@ -902,7 +904,6 @@ public class AnnotationDrivenEventListenerTests {
}
@EventListener
@Retention(RetentionPolicy.RUNTIME)
public @interface ConditionalEvent {
@@ -934,18 +935,20 @@ public class AnnotationDrivenEventListenerTests {
super.handle(event);
}
@Override
@EventListener(condition = "#payload.startsWith('OK')")
@Override
public void handleString(String payload) {
super.handleString(payload);
}
@ConditionalEvent("#root.event.timestamp > #p0")
@Override
public void handleTimestamp(Long timestamp) {
collectEvent(timestamp);
}
@ConditionalEvent("@conditionEvaluator.valid(#p0)")
@Override
public void handleRatio(Double ratio) {
collectEvent(ratio);
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2016 the original author or authors.
* Copyright 2002-2019 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,7 +62,6 @@ public class StaticMessageSourceTests extends AbstractApplicationContextTests {
@Test
@Override
public void count() {
// These are only checked for current Ctx (not parent ctx)
assertCount(15);
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2019 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.
@@ -46,7 +46,7 @@ import static org.junit.Assert.*;
*/
public class DateFormattingTests {
private final FormattingConversionService conversionService = new FormattingConversionService();
private FormattingConversionService conversionService;
private DataBinder binder;
@@ -58,6 +58,7 @@ public class DateFormattingTests {
}
private void setup(DateFormatterRegistrar registrar) {
conversionService = new FormattingConversionService();
DefaultConversionService.addDefaultConverters(conversionService);
registrar.registerFormatters(conversionService);
@@ -148,6 +149,20 @@ public class DateFormattingTests {
assertEquals("10/31/09 1:05", binder.getBindingResult().getFieldValue("dateAnnotatedPattern"));
}
@Test
public void testBindDateAnnotatedPatternWithGlobalFormat() {
DateFormatterRegistrar registrar = new DateFormatterRegistrar();
DateFormatter dateFormatter = new DateFormatter();
dateFormatter.setIso(ISO.DATE_TIME);
registrar.setFormatter(dateFormatter);
setup(registrar);
MutablePropertyValues propertyValues = new MutablePropertyValues();
propertyValues.add("dateAnnotatedPattern", "10/31/09 1:05");
binder.bind(propertyValues);
assertEquals(0, binder.getBindingResult().getErrorCount());
assertEquals("10/31/09 1:05", binder.getBindingResult().getFieldValue("dateAnnotatedPattern"));
}
@Test
public void testBindDateTimeOverflow() {
MutablePropertyValues propertyValues = new MutablePropertyValues();
@@ -0,0 +1,121 @@
/*
* Copyright 2002-2019 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.format.datetime.standard;
import java.text.ParseException;
import java.time.Instant;
import java.time.format.DateTimeFormatter;
import java.util.Random;
import java.util.stream.Stream;
import org.junit.Test;
import static java.time.Instant.MAX;
import static java.time.Instant.MIN;
import static java.time.ZoneId.systemDefault;
import static org.junit.Assert.assertEquals;
/**
* Unit tests for {@link InstantFormatter}.
*
* @author Andrei Nevedomskii
* @author Sam Brannen
* @since 5.1.12
*/
public class InstantFormatterTests {
private final InstantFormatter instantFormatter = new InstantFormatter();
@Test
public void should_parse_an_ISO_formatted_string_representation_of_an_Instant() {
new ISOSerializedInstantProvider().provideArguments().forEach(input -> {
try {
Instant expected = DateTimeFormatter.ISO_INSTANT.parse(input, Instant::from);
Instant actual = instantFormatter.parse(input, null);
assertEquals(expected, actual);
}
catch (ParseException ex) {
throw new RuntimeException(ex);
}
});
}
@Test
public void should_parse_an_RFC1123_formatted_string_representation_of_an_Instant() {
new RFC1123SerializedInstantProvider().provideArguments().forEach(input -> {
try {
Instant expected = DateTimeFormatter.RFC_1123_DATE_TIME.parse(input, Instant::from);
Instant actual = instantFormatter.parse(input, null);
assertEquals(expected, actual);
}
catch (ParseException ex) {
throw new RuntimeException(ex);
}
});
}
@Test
public void should_serialize_an_Instant_using_ISO_format_and_ignoring_Locale() {
new RandomInstantProvider().randomInstantStream(MIN, MAX).forEach(instant -> {
String expected = DateTimeFormatter.ISO_INSTANT.format(instant);
String actual = instantFormatter.print(instant, null);
assertEquals(expected, actual);
});
}
private static class RandomInstantProvider {
private static final long DATA_SET_SIZE = 10;
private static final Random random = new Random();
Stream<Instant> randomInstantStream(Instant min, Instant max) {
return Stream.concat(Stream.of(Instant.now()), // make sure that the data set includes current instant
random.longs(min.getEpochSecond(), max.getEpochSecond()).limit(DATA_SET_SIZE).mapToObj(Instant::ofEpochSecond));
}
}
private static class ISOSerializedInstantProvider extends RandomInstantProvider {
Stream<String> provideArguments() {
return randomInstantStream(MIN, MAX).map(DateTimeFormatter.ISO_INSTANT::format);
}
}
private static class RFC1123SerializedInstantProvider extends RandomInstantProvider {
// RFC-1123 supports only 4-digit years
private static final Instant min = Instant.parse("0000-01-01T00:00:00.00Z");
private static final Instant max = Instant.parse("9999-12-31T23:59:59.99Z");
Stream<String> provideArguments() {
return randomInstantStream(min, max)
.map(DateTimeFormatter.RFC_1123_DATE_TIME.withZone(systemDefault())::format);
}
}
}
+2 -2
View File
@@ -79,11 +79,11 @@ dependencies {
optional("io.reactivex.rxjava2:rxjava:${rxjava2Version}")
optional("io.netty:netty-buffer")
testCompile("io.projectreactor:reactor-test")
testCompile("org.apache.tomcat.embed:tomcat-embed-core:${tomcatVersion}")
testCompile("javax.annotation:javax.annotation-api:1.3.2")
testCompile("com.google.code.findbugs:jsr305:3.0.2")
testCompile("org.xmlunit:xmlunit-matchers:2.6.2")
testCompile("javax.xml.bind:jaxb-api:2.3.1")
testCompile("com.fasterxml.woodstox:woodstox-core:5.2.0") {
testCompile("com.fasterxml.woodstox:woodstox-core:5.3.0") {
exclude group: "stax", module: "stax-api"
}
}
@@ -872,6 +872,12 @@ public class ResolvableType implements Serializable {
return forType(ownerType, this.variableResolver).resolveVariable(variable);
}
}
if (this.type instanceof WildcardType) {
ResolvableType resolved = resolveType().resolveVariable(variable);
if (resolved != null) {
return resolved;
}
}
if (this.variableResolver != null) {
return this.variableResolver.resolveVariable(variable);
}
@@ -982,7 +988,7 @@ public class ResolvableType implements Serializable {
* using the full generic type information for assignability checks.
* For example: {@code ResolvableType.forClass(MyArrayList.class)}.
* @param clazz the class to introspect ({@code null} is semantically
* equivalent to {@code Object.class} for typical use cases here}
* equivalent to {@code Object.class} for typical use cases here)
* @return a {@link ResolvableType} for the specified class
* @see #forClass(Class, Class)
* @see #forClassWithGenerics(Class, Class...)
@@ -997,7 +1003,7 @@ public class ResolvableType implements Serializable {
* {@link Class#isAssignableFrom}, which this serves as a wrapper for.
* For example: {@code ResolvableType.forRawClass(List.class)}.
* @param clazz the class to introspect ({@code null} is semantically
* equivalent to {@code Object.class} for typical use cases here}
* equivalent to {@code Object.class} for typical use cases here)
* @return a {@link ResolvableType} for the specified class
* @since 4.2
* @see #forClass(Class)
@@ -1485,10 +1491,10 @@ public class ResolvableType implements Serializable {
@Override
@Nullable
public ResolvableType resolveVariable(TypeVariable<?> variable) {
TypeVariable<?> variableToCompare = SerializableTypeWrapper.unwrap(variable);
for (int i = 0; i < this.variables.length; i++) {
TypeVariable<?> v1 = SerializableTypeWrapper.unwrap(this.variables[i]);
TypeVariable<?> v2 = SerializableTypeWrapper.unwrap(variable);
if (ObjectUtils.nullSafeEquals(v1, v2)) {
TypeVariable<?> resolvedVariable = SerializableTypeWrapper.unwrap(this.variables[i]);
if (ObjectUtils.nullSafeEquals(resolvedVariable, variableToCompare)) {
return this.generics[i];
}
}
@@ -47,12 +47,40 @@ import org.springframework.util.MimeType;
*/
public abstract class AbstractDataBufferDecoder<T> extends AbstractDecoder<T> {
private int maxInMemorySize = -1;
protected AbstractDataBufferDecoder(MimeType... supportedMimeTypes) {
super(supportedMimeTypes);
}
/**
* Configure a limit on the number of bytes that can be buffered whenever
* the input stream needs to be aggregated. This can be a result of
* decoding to a single {@code DataBuffer},
* {@link java.nio.ByteBuffer ByteBuffer}, {@code byte[]},
* {@link org.springframework.core.io.Resource Resource}, {@code String}, etc.
* It can also occur when splitting the input stream, e.g. delimited text,
* in which case the limit applies to data buffered between delimiters.
* <p>By default in 5.1 this is set to -1, unlimited. In 5.2 the default
* value for this limit is set to 256K.
* @param byteCount the max number of bytes to buffer, or -1 for unlimited
* @since 5.1.11
*/
public void setMaxInMemorySize(int byteCount) {
this.maxInMemorySize = byteCount;
}
/**
* Return the {@link #setMaxInMemorySize configured} byte count limit.
* @since 5.1.11
*/
public int getMaxInMemorySize() {
return this.maxInMemorySize;
}
@Override
public Flux<T> decode(Publisher<DataBuffer> input, ResolvableType elementType,
@Nullable MimeType mimeType, @Nullable Map<String, Object> hints) {
@@ -64,7 +92,7 @@ public abstract class AbstractDataBufferDecoder<T> extends AbstractDecoder<T> {
public Mono<T> decodeToMono(Publisher<DataBuffer> input, ResolvableType elementType,
@Nullable MimeType mimeType, @Nullable Map<String, Object> hints) {
return DataBufferUtils.join(input)
return DataBufferUtils.join(input, this.maxInMemorySize)
.map(buffer -> decodeDataBuffer(buffer, elementType, mimeType, hints));
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2020 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,14 +25,17 @@ import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.function.Consumer;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
import org.springframework.core.ResolvableType;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferLimitException;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
import org.springframework.core.io.buffer.LimitedDataBufferList;
import org.springframework.core.io.buffer.PooledDataBuffer;
import org.springframework.core.log.LogFormatUtils;
import org.springframework.lang.Nullable;
@@ -92,11 +95,44 @@ public final class StringDecoder extends AbstractDataBufferDecoder<String> {
List<byte[]> delimiterBytes = getDelimiterBytes(mimeType);
Flux<DataBuffer> inputFlux = Flux.from(input)
.flatMapIterable(buffer -> splitOnDelimiter(buffer, delimiterBytes))
.bufferUntil(buffer -> buffer == END_FRAME)
.map(StringDecoder::joinUntilEndFrame)
.doOnDiscard(PooledDataBuffer.class, DataBufferUtils::release);
Flux<DataBuffer> inputFlux = Flux.defer(() -> {
if (getMaxInMemorySize() != -1) {
// Passing limiter into endFrameAfterDelimiter helps to ensure that in case of one DataBuffer
// containing multiple lines, the limit is checked and raised immediately without accumulating
// subsequent lines. This is necessary because concatMapIterable doesn't respect doOnDiscard.
// When reactor-core#1925 is resolved, we could replace bufferUntil with:
// .windowUntil(buffer -> buffer instanceof EndFrameBuffer)
// .concatMap(fluxes -> fluxes.collect(() -> new LimitedDataBufferList(getMaxInMemorySize()), LimitedDataBufferList::add))
LimitedDataBufferList limiter = new LimitedDataBufferList(getMaxInMemorySize());
return Flux.from(input)
.concatMapIterable(buffer -> splitOnDelimiter(buffer, delimiterBytes, limiter))
.bufferUntil(buffer -> buffer == END_FRAME)
.map(StringDecoder::joinUntilEndFrame)
.doOnDiscard(PooledDataBuffer.class, DataBufferUtils::release);
}
else {
// When the decoder is unlimited (-1), concatMapIterable will cache buffers that may not
// be released if cancel is signalled before they are turned into String lines
// (see test maxInMemoryLimitReleasesUnprocessedLinesWhenUnlimited).
// When reactor-core#1925 is resolved, the workaround can be removed and the entire
// else clause possibly dropped.
ConcatMapIterableDiscardWorkaroundCache cache = new ConcatMapIterableDiscardWorkaroundCache();
return Flux.from(input)
.concatMapIterable(buffer -> cache.addAll(splitOnDelimiter(buffer, delimiterBytes, null)))
.doOnNext(cache)
.doOnCancel(cache)
.bufferUntil(buffer -> buffer == END_FRAME)
.map(StringDecoder::joinUntilEndFrame)
.doOnDiscard(PooledDataBuffer.class, DataBufferUtils::release);
}
});
return super.decode(inputFlux, elementType, mimeType, hints);
}
@@ -116,7 +152,9 @@ public final class StringDecoder extends AbstractDataBufferDecoder<String> {
* Split the given data buffer on delimiter boundaries.
* The returned Flux contains an {@link #END_FRAME} buffer after each delimiter.
*/
private List<DataBuffer> splitOnDelimiter(DataBuffer buffer, List<byte[]> delimiterBytes) {
private List<DataBuffer> splitOnDelimiter(
DataBuffer buffer, List<byte[]> delimiterBytes, @Nullable LimitedDataBufferList limiter) {
List<DataBuffer> frames = new ArrayList<>();
try {
do {
@@ -138,15 +176,28 @@ public final class StringDecoder extends AbstractDataBufferDecoder<String> {
buffer.readPosition(readPosition + length + matchingDelimiter.length);
frames.add(DataBufferUtils.retain(frame));
frames.add(END_FRAME);
if (limiter != null) {
limiter.add(frame); // enforce the limit
limiter.clear();
}
}
else {
frame = buffer.slice(readPosition, buffer.readableByteCount());
buffer.readPosition(readPosition + buffer.readableByteCount());
frames.add(DataBufferUtils.retain(frame));
if (limiter != null) {
limiter.add(frame);
}
}
}
while (buffer.readableByteCount() > 0);
}
catch (DataBufferLimitException ex) {
if (limiter != null) {
limiter.releaseAndClear();
}
throw ex;
}
catch (Throwable ex) {
for (DataBuffer frame : frames) {
DataBufferUtils.release(frame);
@@ -283,4 +334,33 @@ public final class StringDecoder extends AbstractDataBufferDecoder<String> {
new MimeType("text", "plain", DEFAULT_CHARSET), MimeTypeUtils.ALL);
}
private class ConcatMapIterableDiscardWorkaroundCache implements Consumer<DataBuffer>, Runnable {
private final List<DataBuffer> buffers = new ArrayList<>();
public List<DataBuffer> addAll(List<DataBuffer> buffersToAdd) {
this.buffers.addAll(buffersToAdd);
return buffersToAdd;
}
@Override
public void accept(DataBuffer dataBuffer) {
this.buffers.remove(dataBuffer);
}
@Override
public void run() {
this.buffers.forEach(buffer -> {
try {
DataBufferUtils.release(buffer);
}
catch (Throwable ex) {
// Keep going..
}
});
}
}
}
@@ -36,7 +36,8 @@ import org.springframework.util.ClassUtils;
import org.springframework.util.ObjectUtils;
/**
* Context about a type to convert from or to.
* Contextual descriptor about a type to convert from or to.
* Capable of representing arrays and generic collection types.
*
* @author Keith Donald
* @author Andy Clement
@@ -45,6 +46,8 @@ import org.springframework.util.ObjectUtils;
* @author Sam Brannen
* @author Stephane Nicoll
* @since 3.0
* @see ConversionService#canConvert(TypeDescriptor, TypeDescriptor)
* @see ConversionService#convert(Object, TypeDescriptor, TypeDescriptor)
*/
@SuppressWarnings("serial")
public class TypeDescriptor implements Serializable {
@@ -322,9 +325,9 @@ public class TypeDescriptor implements Serializable {
* If this type is a {@code Stream}, returns the stream's component type.
* If this type is a {@link Collection} and it is parameterized, returns the Collection's element type.
* If the Collection is not parameterized, returns {@code null} indicating the element type is not declared.
* @return the array component type or Collection element type, or {@code null} if this type is a
* Collection but its element type is not parameterized
* @throws IllegalStateException if this type is not a {@code java.util.Collection} or array type
* @return the array component type or Collection element type, or {@code null} if this type is not
* an array type or a {@code java.util.Collection} or if its element type is not parameterized
* @see #elementTypeDescriptor(Object)
*/
@Nullable
public TypeDescriptor getElementTypeDescriptor() {
@@ -351,8 +354,7 @@ public class TypeDescriptor implements Serializable {
* TypeDescriptor that is returned.
* @param element the collection or array element
* @return a element type descriptor, narrowed to the type of the provided element
* @throws IllegalStateException if this type is not a {@code java.util.Collection}
* or array type
* @see #getElementTypeDescriptor()
* @see #narrow(Object)
*/
@Nullable
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2019 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,6 +49,7 @@ import org.springframework.util.StringUtils;
*
* @author Juergen Hoeller
* @since 28.12.2003
* @see #FileSystemResource(String)
* @see #FileSystemResource(File)
* @see #FileSystemResource(Path)
* @see java.io.File
@@ -108,6 +109,15 @@ public class FileSystemResource extends AbstractResource implements WritableReso
* <p>In contrast to {@link PathResource}, this variant strictly follows the
* general {@link FileSystemResource} conventions, in particular in terms of
* path cleaning and {@link #createRelative(String)} handling.
* <p>Note: When building relative resources via {@link #createRelative},
* the relative path will apply <i>at the same directory level</i>:
* e.g. Paths.get("C:/dir1"), relative path "dir2" -> "C:/dir2"!
* If you prefer to have relative paths built underneath the given root directory,
* use the {@link #FileSystemResource(String) constructor with a file path}
* to append a trailing slash to the root path: "C:/dir1/", which indicates
* this directory as root for all relative paths. Alternatively, consider
* using {@link PathResource#PathResource(Path)} for {@code java.nio.path.Path}
* resolution in {@code createRelative}, always nesting relative paths.
* @param filePath a Path handle to a file
* @since 5.1
* @see #FileSystemResource(File)
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2019 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.
@@ -44,15 +44,16 @@ import org.springframework.util.Assert;
* in {@link FileSystemResource#FileSystemResource(Path) FileSystemResource},
* applying Spring's standard String-based path transformations but
* performing all operations via the {@link java.nio.file.Files} API.
* This {@code PathResource} is effectively a pure {@code java.nio.path.Path}
* based alternative with different {@code createRelative} behavior.
*
* @author Philippe Marschall
* @author Juergen Hoeller
* @since 4.0
* @see java.nio.file.Path
* @see java.nio.file.Files
* @deprecated as of 5.1.1, in favor of {@link FileSystemResource#FileSystemResource(Path)}
* @see FileSystemResource
*/
@Deprecated
public class PathResource extends AbstractResource implements WritableResource {
private final Path path;
@@ -252,7 +253,7 @@ public class PathResource extends AbstractResource implements WritableResource {
* @see java.nio.file.Path#resolve(String)
*/
@Override
public Resource createRelative(String relativePath) throws IOException {
public Resource createRelative(String relativePath) {
return new PathResource(this.path.resolve(relativePath));
}
@@ -0,0 +1,37 @@
/*
* Copyright 2002-2019 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.core.io.buffer;
/**
* Exception that indicates the cumulative number of bytes consumed from a
* stream of {@link DataBuffer DataBuffer}'s exceeded some pre-configured limit.
* This can be raised when data buffers are cached and aggregated, e.g.
* {@link DataBufferUtils#join}. Or it could also be raised when data buffers
* have been released but a parsed representation is being aggregated, e.g. async
* parsing with Jackson.
*
* @author Rossen Stoyanchev
* @since 5.1.11
*/
@SuppressWarnings("serial")
public class DataBufferLimitException extends IllegalStateException {
public DataBufferLimitException(String message) {
super(message);
}
}
@@ -437,14 +437,36 @@ public abstract class DataBufferUtils {
* @since 5.0.3
*/
public static Mono<DataBuffer> join(Publisher<DataBuffer> dataBuffers) {
Assert.notNull(dataBuffers, "'dataBuffers' must not be null");
return join(dataBuffers, -1);
}
return Flux.from(dataBuffers)
.collectList()
/**
* Variant of {@link #join(Publisher)} that behaves the same way up until
* the specified max number of bytes to buffer. Once the limit is exceeded,
* {@link DataBufferLimitException} is raised.
* @param buffers the data buffers that are to be composed
* @param maxByteCount the max number of bytes to buffer, or -1 for unlimited
* @return a buffer with the aggregated content, possibly an empty Mono if
* the max number of bytes to buffer is exceeded.
* @throws DataBufferLimitException if maxByteCount is exceeded
* @since 5.1.11
*/
@SuppressWarnings("unchecked")
public static Mono<DataBuffer> join(Publisher<? extends DataBuffer> buffers, int maxByteCount) {
Assert.notNull(buffers, "'dataBuffers' must not be null");
if (buffers instanceof Mono) {
return (Mono<DataBuffer>) buffers;
}
// TODO: Drop doOnDiscard(LimitedDataBufferList.class, ...) (reactor-core#1924)
return Flux.from(buffers)
.collect(() -> new LimitedDataBufferList(maxByteCount), LimitedDataBufferList::add)
.filter(list -> !list.isEmpty())
.map(list -> list.get(0).factory().join(list))
.doOnDiscard(LimitedDataBufferList.class, LimitedDataBufferList::releaseAndClear)
.doOnDiscard(PooledDataBuffer.class, DataBufferUtils::release);
}
@@ -0,0 +1,157 @@
/*
* Copyright 2002-2019 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.core.io.buffer;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.function.Predicate;
import reactor.core.publisher.Flux;
/**
* Custom {@link List} to collect data buffers with and enforce a
* limit on the total number of bytes buffered. For use with "collect" or
* other buffering operators in declarative APIs, e.g. {@link Flux}.
*
* <p>Adding elements increases the byte count and if the limit is exceeded,
* {@link DataBufferLimitException} is raised. {@link #clear()} resets the
* count. Remove and set are not supported.
*
* <p><strong>Note:</strong> This class does not automatically release the
* buffers it contains. It is usually preferable to use hooks such as
* {@link Flux#doOnDiscard} that also take care of cancel and error signals,
* or otherwise {@link #releaseAndClear()} can be used.
*
* @author Rossen Stoyanchev
* @since 5.1.11
*/
@SuppressWarnings("serial")
public class LimitedDataBufferList extends ArrayList<DataBuffer> {
private final int maxByteCount;
private int byteCount;
public LimitedDataBufferList(int maxByteCount) {
this.maxByteCount = maxByteCount;
}
@Override
public boolean add(DataBuffer buffer) {
boolean result = super.add(buffer);
if (result) {
updateCount(buffer.readableByteCount());
}
return result;
}
@Override
public void add(int index, DataBuffer buffer) {
super.add(index, buffer);
updateCount(buffer.readableByteCount());
}
@Override
public boolean addAll(Collection<? extends DataBuffer> collection) {
boolean result = super.addAll(collection);
collection.forEach(buffer -> updateCount(buffer.readableByteCount()));
return result;
}
@Override
public boolean addAll(int index, Collection<? extends DataBuffer> collection) {
boolean result = super.addAll(index, collection);
collection.forEach(buffer -> updateCount(buffer.readableByteCount()));
return result;
}
private void updateCount(int bytesToAdd) {
if (this.maxByteCount < 0) {
return;
}
if (bytesToAdd > Integer.MAX_VALUE - this.byteCount) {
raiseLimitException();
}
else {
this.byteCount += bytesToAdd;
if (this.byteCount > this.maxByteCount) {
raiseLimitException();
}
}
}
private void raiseLimitException() {
// Do not release here, it's likely down via doOnDiscard..
throw new DataBufferLimitException(
"Exceeded limit on max bytes to buffer : " + this.maxByteCount);
}
@Override
public DataBuffer remove(int index) {
throw new UnsupportedOperationException();
}
@Override
public boolean remove(Object o) {
throw new UnsupportedOperationException();
}
@Override
protected void removeRange(int fromIndex, int toIndex) {
throw new UnsupportedOperationException();
}
@Override
public boolean removeAll(Collection<?> c) {
throw new UnsupportedOperationException();
}
@Override
public boolean removeIf(Predicate<? super DataBuffer> filter) {
throw new UnsupportedOperationException();
}
@Override
public DataBuffer set(int index, DataBuffer element) {
throw new UnsupportedOperationException();
}
@Override
public void clear() {
this.byteCount = 0;
super.clear();
}
/**
* Shortcut to {@link DataBufferUtils#release release} all data buffers and
* then {@link #clear()}.
*/
public void releaseAndClear() {
forEach(buf -> {
try {
DataBufferUtils.release(buf);
}
catch (Throwable ex) {
// Keep going..
}
});
clear();
}
}
@@ -110,6 +110,11 @@ public abstract class ClassUtils {
*/
private static final Set<Class<?>> javaLanguageInterfaces;
/**
* Cache for equivalent methods on an interface implemented by the declaring class.
*/
private static final Map<Method, Method> interfaceMethodCache = new ConcurrentReferenceHashMap<>(256);
static {
primitiveWrapperTypeMap.put(Boolean.class, boolean.class);
@@ -1291,13 +1296,16 @@ public abstract class ClassUtils {
* @see #getMostSpecificMethod
*/
public static Method getInterfaceMethodIfPossible(Method method) {
if (Modifier.isPublic(method.getModifiers()) && !method.getDeclaringClass().isInterface()) {
Class<?> current = method.getDeclaringClass();
if (!Modifier.isPublic(method.getModifiers()) || method.getDeclaringClass().isInterface()) {
return method;
}
return interfaceMethodCache.computeIfAbsent(method, key -> {
Class<?> current = key.getDeclaringClass();
while (current != null && current != Object.class) {
Class<?>[] ifcs = current.getInterfaces();
for (Class<?> ifc : ifcs) {
try {
return ifc.getMethod(method.getName(), method.getParameterTypes());
return ifc.getMethod(key.getName(), key.getParameterTypes());
}
catch (NoSuchMethodException ex) {
// ignore
@@ -1305,8 +1313,8 @@ public abstract class ClassUtils {
}
current = current.getSuperclass();
}
}
return method;
return key;
});
}
/**
@@ -417,14 +417,14 @@ public abstract class StringUtils {
int pos = 0; // our position in the old string
int patLen = oldPattern.length();
while (index >= 0) {
sb.append(inString.substring(pos, index));
sb.append(inString, pos, index);
sb.append(newPattern);
pos = index + patLen;
index = inString.indexOf(oldPattern, pos);
}
// append any characters to the right of a match
sb.append(inString.substring(pos));
sb.append(inString, pos, inString.length());
return sb.toString();
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2019 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,38 +16,53 @@
package org.springframework.util.unit;
import java.util.Objects;
/**
* A standard set of data size units.
* A standard set of {@link DataSize} units.
*
* <p>The unit prefixes used in this class are
* <a href="https://en.wikipedia.org/wiki/Binary_prefix">binary prefixes</a>
* indicating multiplication by powers of 2. The following table displays the
* enum constants defined in this class and corresponding values.
*
* <p>
* <table border="1">
* <tr><th>Constant</th><th>Data Size</th><th>Power&nbsp;of&nbsp;2</th><th>Size in Bytes</th></tr>
* <tr><td>{@link #BYTES}</td><td>1B</td><td>2^0</td><td>1</td></tr>
* <tr><td>{@link #KILOBYTES}</td><td>1KB</td><td>2^10</td><td>1,024</td></tr>
* <tr><td>{@link #MEGABYTES}</td><td>1MB</td><td>2^20</td><td>1,048,576</td></tr>
* <tr><td>{@link #GIGABYTES}</td><td>1GB</td><td>2^30</td><td>1,073,741,824</td></tr>
* <tr><td>{@link #TERABYTES}</td><td>1TB</td><td>2^40</td><td>1,099,511,627,776</td></tr>
* </table>
*
* @author Stephane Nicoll
* @author Sam Brannen
* @since 5.1
* @see DataSize
*/
public enum DataUnit {
/**
* Bytes.
* Bytes, represented by suffix {@code B}.
*/
BYTES("B", DataSize.ofBytes(1)),
/**
* Kilobytes.
* Kilobytes, represented by suffix {@code KB}.
*/
KILOBYTES("KB", DataSize.ofKilobytes(1)),
/**
* Megabytes.
* Megabytes, represented by suffix {@code MB}.
*/
MEGABYTES("MB", DataSize.ofMegabytes(1)),
/**
* Gigabytes.
* Gigabytes, represented by suffix {@code GB}.
*/
GIGABYTES("GB", DataSize.ofGigabytes(1)),
/**
* Terabytes.
* Terabytes, represented by suffix {@code TB}.
*/
TERABYTES("TB", DataSize.ofTerabytes(1));
@@ -68,18 +83,18 @@ public enum DataUnit {
/**
* Return the {@link DataUnit} matching the specified {@code suffix}.
* @param suffix one of the standard suffix
* @param suffix one of the standard suffixes
* @return the {@link DataUnit} matching the specified {@code suffix}
* @throws IllegalArgumentException if the suffix does not match any
* of this enum's constants
* @throws IllegalArgumentException if the suffix does not match the suffix
* of any of this enum's constants
*/
public static DataUnit fromSuffix(String suffix) {
for (DataUnit candidate : values()) {
if (Objects.equals(candidate.suffix, suffix)) {
if (candidate.suffix.equals(suffix)) {
return candidate;
}
}
throw new IllegalArgumentException("Unknown unit '" + suffix + "'");
throw new IllegalArgumentException("Unknown data unit suffix '" + suffix + "'");
}
}
@@ -684,10 +684,16 @@ public class ResolvableTypeTests {
@Test
public void resolveBoundedTypeVariableResult() throws Exception {
ResolvableType type = ResolvableType.forMethodReturnType(Methods.class.getMethod("boundedTypeVaraibleResult"));
ResolvableType type = ResolvableType.forMethodReturnType(Methods.class.getMethod("boundedTypeVariableResult"));
assertThat(type.resolve(), equalTo((Class) CharSequence.class));
}
@Test
public void resolveBoundedTypeVariableWildcardResult() throws Exception {
ResolvableType type = ResolvableType.forMethodReturnType(Methods.class.getMethod("boundedTypeVariableWildcardResult"));
assertThat(type.getGeneric(1).asCollection().resolveGeneric(), equalTo((Class) CharSequence.class));
}
@Test
public void resolveVariableNotFound() throws Exception {
ResolvableType type = ResolvableType.forMethodReturnType(Methods.class.getMethod("typedReturn"));
@@ -695,27 +701,14 @@ public class ResolvableTypeTests {
}
@Test
public void resolveTypeVaraibleFromMethodReturn() throws Exception {
ResolvableType type = ResolvableType.forMethodReturnType(Methods.class.getMethod("typedReturn"));
assertThat(type.resolve(), nullValue());
}
@Test
public void resolveTypeVaraibleFromMethodReturnWithInstanceClass() throws Exception {
ResolvableType type = ResolvableType.forMethodReturnType(
Methods.class.getMethod("typedReturn"), TypedMethods.class);
assertThat(type.resolve(), equalTo((Class) String.class));
}
@Test
public void resolveTypeVaraibleFromSimpleInterfaceType() {
public void resolveTypeVariableFromSimpleInterfaceType() {
ResolvableType type = ResolvableType.forClass(
MySimpleInterfaceType.class).as(MyInterfaceType.class);
assertThat(type.resolveGeneric(), equalTo((Class) String.class));
}
@Test
public void resolveTypeVaraibleFromSimpleCollectionInterfaceType() {
public void resolveTypeVariableFromSimpleCollectionInterfaceType() {
ResolvableType type = ResolvableType.forClass(
MyCollectionInterfaceType.class).as(MyInterfaceType.class);
assertThat(type.resolveGeneric(), equalTo((Class) Collection.class));
@@ -723,14 +716,14 @@ public class ResolvableTypeTests {
}
@Test
public void resolveTypeVaraibleFromSimpleSuperclassType() {
public void resolveTypeVariableFromSimpleSuperclassType() {
ResolvableType type = ResolvableType.forClass(
MySimpleSuperclassType.class).as(MySuperclassType.class);
assertThat(type.resolveGeneric(), equalTo((Class) String.class));
}
@Test
public void resolveTypeVaraibleFromSimpleCollectionSuperclassType() {
public void resolveTypeVariableFromSimpleCollectionSuperclassType() {
ResolvableType type = ResolvableType.forClass(
MyCollectionSuperclassType.class).as(MySuperclassType.class);
assertThat(type.resolveGeneric(), equalTo((Class) Collection.class));
@@ -1459,7 +1452,9 @@ public class ResolvableTypeTests {
void charSequenceParameter(List<CharSequence> cs);
<R extends CharSequence & Serializable> R boundedTypeVaraibleResult();
<R extends CharSequence & Serializable> R boundedTypeVariableResult();
Map<String, ? extends List<? extends CharSequence>> boundedTypeVariableWildcardResult();
void nested(Map<Map<String, Integer>, Map<Byte, Long>> p);
@@ -488,6 +488,20 @@ public class AnnotatedElementUtilsTests {
assertTrue(isAnnotated(element, name));
}
@Test
public void getMergedAnnotationWithImplicitAliasesWithDefaultsInMetaAnnotationOnComposedAnnotation() {
Class<?> element = ImplicitAliasesWithDefaultsClass.class;
String name = AliasesWithDefaults.class.getName();
AliasesWithDefaults annotation = getMergedAnnotation(element, AliasesWithDefaults.class);
assertNotNull("Should find @AliasesWithDefaults on " + element.getSimpleName(), annotation);
assertEquals("a1", "ImplicitAliasesWithDefaults", annotation.a1());
assertEquals("a2", "ImplicitAliasesWithDefaults", annotation.a2());
// Verify contracts between utility methods:
assertTrue(isAnnotated(element, name));
}
@Test
public void getMergedAnnotationAttributesWithInvalidConventionBasedComposedAnnotation() {
Class<?> element = InvalidConventionBasedComposedContextConfigClass.class;
@@ -958,7 +972,6 @@ public class AnnotatedElementUtilsTests {
String[] xmlConfigFiles() default {};
}
@ContextConfig
@Retention(RetentionPolicy.RUNTIME)
@interface AliasedComposedContextConfig {
@@ -999,6 +1012,27 @@ public class AnnotatedElementUtilsTests {
@interface ComposedImplicitAliasesContextConfig {
}
@Retention(RetentionPolicy.RUNTIME)
@interface AliasesWithDefaults {
@AliasFor("a2")
String a1() default "AliasesWithDefaults";
@AliasFor("a1")
String a2() default "AliasesWithDefaults";
}
@Retention(RetentionPolicy.RUNTIME)
@AliasesWithDefaults
@interface ImplicitAliasesWithDefaults {
@AliasFor(annotation = AliasesWithDefaults.class, attribute = "a1")
String b1() default "ImplicitAliasesWithDefaults";
@AliasFor(annotation = AliasesWithDefaults.class, attribute = "a2")
String b2() default "ImplicitAliasesWithDefaults";
}
@ImplicitAliasesContextConfig
@Retention(RetentionPolicy.RUNTIME)
@interface TransitiveImplicitAliasesContextConfig {
@@ -1296,6 +1330,10 @@ public class AnnotatedElementUtilsTests {
static class ImplicitAliasesContextConfigClass3 {
}
@ImplicitAliasesWithDefaults
static class ImplicitAliasesWithDefaultsClass {
}
@TransitiveImplicitAliasesContextConfig(groovy = "test.groovy")
static class TransitiveImplicitAliasesContextConfigClass {
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -29,6 +29,7 @@ import reactor.test.StepVerifier;
import org.springframework.core.ResolvableType;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferLimitException;
import org.springframework.util.MimeType;
import org.springframework.util.MimeTypeUtils;
@@ -126,6 +127,33 @@ public class StringDecoderTests extends AbstractDecoderTestCase<StringDecoder> {
.verify());
}
@Test
public void maxInMemoryLimit() {
Flux<DataBuffer> input = Flux.just(
stringBuffer("abc\n"), stringBuffer("defg\n"), stringBuffer("hijkl\n"));
this.decoder.setMaxInMemorySize(4);
testDecode(input, String.class, step ->
step.expectNext("abc", "defg").verifyError(DataBufferLimitException.class));
}
@Test // gh-24312
public void maxInMemoryLimitReleaseUnprocessedLinesFromCurrentBuffer() {
Flux<DataBuffer> input = Flux.just(
stringBuffer("TOO MUCH DATA\nanother line\n\nand another\n"));
this.decoder.setMaxInMemorySize(5);
testDecode(input, String.class, step -> step.verifyError(DataBufferLimitException.class));
}
@Test // gh-24339
public void maxInMemoryLimitReleaseUnprocessedLinesWhenUnlimited() {
Flux<DataBuffer> input = Flux.just(stringBuffer("Line 1\nLine 2\nLine 3\n"));
this.decoder.setMaxInMemorySize(-1);
testDecodeCancel(input, ResolvableType.forClass(String.class), null, Collections.emptyMap());
}
@Test
public void decodeNewLineIncludeDelimiters() {
this.decoder = StringDecoder.allMimeTypes(StringDecoder.DEFAULT_DELIMITERS, false);
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2019 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.core.io;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
@@ -129,6 +130,14 @@ public class ResourceTests {
assertEquals(new FileSystemResource(file), resource);
}
@Test
public void fileSystemResourceWithFile() throws IOException {
File file = new File(getClass().getResource("Resource.class").getFile());
Resource resource = new FileSystemResource(file);
doTestResource(resource);
assertEquals(new FileSystemResource(file), resource);
}
@Test
public void testFileSystemResourceWithFilePath() throws Exception {
Path filePath = Paths.get(getClass().getResource("Resource.class").toURI());
@@ -48,11 +48,14 @@ import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.buffer.support.DataBufferTestUtils;
import static org.junit.Assert.*;
import static org.mockito.ArgumentMatchers.*;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.anyLong;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.isA;
import static org.mockito.Mockito.*;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
/**
* @author Arjen Poutsma
@@ -716,14 +719,25 @@ public class DataBufferUtilsTests extends AbstractDataBufferAllocatingTestCase {
Mono<DataBuffer> result = DataBufferUtils.join(flux);
StepVerifier.create(result)
.consumeNextWith(dataBuffer -> {
assertEquals("foobarbaz",
DataBufferTestUtils.dumpString(dataBuffer, StandardCharsets.UTF_8));
release(dataBuffer);
.consumeNextWith(buf -> {
assertEquals("foobarbaz", DataBufferTestUtils.dumpString(buf, StandardCharsets.UTF_8));
release(buf);
})
.verifyComplete();
}
@Test
public void joinWithLimit() {
DataBuffer foo = stringBuffer("foo");
DataBuffer bar = stringBuffer("bar");
DataBuffer baz = stringBuffer("baz");
Flux<DataBuffer> flux = Flux.just(foo, bar, baz);
Mono<DataBuffer> result = DataBufferUtils.join(flux, 8);
StepVerifier.create(result)
.verifyError(DataBufferLimitException.class);
}
@Test
public void joinErrors() {
DataBuffer foo = stringBuffer("foo");
@@ -0,0 +1,63 @@
/*
* Copyright 2002-2019 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.core.io.buffer;
import java.nio.charset.StandardCharsets;
import org.junit.Test;
import static org.junit.Assert.fail;
/**
* Unit tests for {@link LimitedDataBufferList}.
* @author Rossen Stoyanchev
* @since 5.1.11
*/
public class LimitedDataBufferListTests {
private final static DataBufferFactory bufferFactory = new DefaultDataBufferFactory();
@Test
public void limitEnforced() {
try {
new LimitedDataBufferList(5).add(toDataBuffer("123456"));
fail();
}
catch (DataBufferLimitException ex) {
// Expected
}
}
@Test
public void limitIgnored() {
new LimitedDataBufferList(-1).add(toDataBuffer("123456"));
}
@Test
public void clearResetsCount() {
LimitedDataBufferList list = new LimitedDataBufferList(5);
list.add(toDataBuffer("12345"));
list.clear();
list.add(toDataBuffer("12345"));
}
private static DataBuffer toDataBuffer(String value) {
return bufferFactory.wrap(value.getBytes(StandardCharsets.UTF_8));
}
}
@@ -20,6 +20,7 @@ import java.io.Serializable;
import java.lang.annotation.Annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@@ -37,7 +38,7 @@ import org.springframework.core.type.classreading.MetadataReaderFactory;
import org.springframework.core.type.classreading.SimpleMetadataReaderFactory;
import org.springframework.stereotype.Component;
import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
/**
@@ -70,7 +71,7 @@ public class AnnotationMetadataTests {
@Test
public void standardAnnotationMetadataForSubclass() {
AnnotationMetadata metadata = new StandardAnnotationMetadata(AnnotatedComponentSubClass.class, true);
doTestSubClassAnnotationInfo(metadata);
doTestSubClassAnnotationInfo(metadata, false);
}
@Test
@@ -78,10 +79,10 @@ public class AnnotationMetadataTests {
MetadataReaderFactory metadataReaderFactory = new SimpleMetadataReaderFactory();
MetadataReader metadataReader = metadataReaderFactory.getMetadataReader(AnnotatedComponentSubClass.class.getName());
AnnotationMetadata metadata = metadataReader.getAnnotationMetadata();
doTestSubClassAnnotationInfo(metadata);
doTestSubClassAnnotationInfo(metadata, true);
}
private void doTestSubClassAnnotationInfo(AnnotationMetadata metadata) {
private void doTestSubClassAnnotationInfo(AnnotationMetadata metadata, boolean asm) {
assertThat(metadata.getClassName(), is(AnnotatedComponentSubClass.class.getName()));
assertThat(metadata.isInterface(), is(false));
assertThat(metadata.isAnnotation(), is(false));
@@ -93,11 +94,26 @@ public class AnnotationMetadataTests {
assertThat(metadata.isAnnotated(Component.class.getName()), is(false));
assertThat(metadata.isAnnotated(Scope.class.getName()), is(false));
assertThat(metadata.isAnnotated(SpecialAttr.class.getName()), is(false));
if (asm) {
assertThat(metadata.isAnnotated(NamedComposedAnnotation.class.getName()), is(false));
assertThat(metadata.hasAnnotation(NamedComposedAnnotation.class.getName()), is(false));
assertThat(metadata.getAnnotationTypes(), is(emptyCollectionOf(String.class)));
}
else {
assertThat(metadata.isAnnotated(NamedComposedAnnotation.class.getName()), is(true));
assertThat(metadata.hasAnnotation(NamedComposedAnnotation.class.getName()), is(true));
assertThat(metadata.getAnnotationTypes(), containsInAnyOrder(NamedComposedAnnotation.class.getName()));
}
assertThat(metadata.hasAnnotation(Component.class.getName()), is(false));
assertThat(metadata.hasAnnotation(Scope.class.getName()), is(false));
assertThat(metadata.hasAnnotation(SpecialAttr.class.getName()), is(false));
assertThat(metadata.getAnnotationTypes().size(), is(0));
assertThat(metadata.hasMetaAnnotation(Component.class.getName()), is(false));
assertThat(metadata.hasMetaAnnotation(MetaAnnotation.class.getName()), is(false));
assertThat(metadata.getAnnotationAttributes(Component.class.getName()), nullValue());
assertThat(metadata.getAnnotationAttributes(MetaAnnotation.class.getName(), false), nullValue());
assertThat(metadata.getAnnotationAttributes(MetaAnnotation.class.getName(), true), nullValue());
assertThat(metadata.getAnnotatedMethods(DirectAnnotation.class.getName()).size(), equalTo(0));
assertThat(metadata.isAnnotated(IsAnnotatedAnnotation.class.getName()), equalTo(false));
assertThat(metadata.getAllAnnotationAttributes(DirectAnnotation.class.getName()), nullValue());
@@ -262,13 +278,18 @@ public class AnnotationMetadataTests {
assertThat(metadata.getInterfaceNames().length, is(1));
assertThat(metadata.getInterfaceNames()[0], is(Serializable.class.getName()));
assertThat(metadata.isAnnotated(NamedComposedAnnotation.class.getName()), is(true));
assertThat(metadata.isAnnotated(Component.class.getName()), is(true));
assertThat(metadata.hasAnnotation(Component.class.getName()), is(true));
assertThat(metadata.hasAnnotation(Scope.class.getName()), is(true));
assertThat(metadata.hasAnnotation(SpecialAttr.class.getName()), is(true));
assertThat(metadata.getAnnotationTypes().size(), is(6));
assertThat(metadata.getAnnotationTypes().contains(Component.class.getName()), is(true));
assertThat(metadata.getAnnotationTypes().contains(Scope.class.getName()), is(true));
assertThat(metadata.getAnnotationTypes().contains(SpecialAttr.class.getName()), is(true));
assertThat(metadata.hasAnnotation(NamedComposedAnnotation.class.getName()), is(true));
assertThat(metadata.getAnnotationTypes(),
containsInAnyOrder(Component.class.getName(), Scope.class.getName(),
SpecialAttr.class.getName(), DirectAnnotation.class.getName(),
MetaMetaAnnotation.class.getName(),
EnumSubclasses.class.getName(),
NamedComposedAnnotation.class.getName()));
AnnotationAttributes compAttrs = (AnnotationAttributes) metadata.getAnnotationAttributes(Component.class.getName());
assertThat(compAttrs.size(), is(1));
@@ -465,6 +486,7 @@ public class AnnotationMetadataTests {
@DirectAnnotation(value = "direct", additional = "", additionalArray = {})
@MetaMetaAnnotation
@EnumSubclasses({SubclassEnum.FOO, SubclassEnum.BAR})
@NamedComposedAnnotation
private static class AnnotatedComponent implements Serializable {
@TestAutowired
@@ -545,6 +567,7 @@ public class AnnotationMetadataTests {
@NamedAnnotation3(name = "name 3")
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Inherited
public @interface NamedComposedAnnotation {
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2020 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,8 @@
package org.springframework.expression.spel.standard;
import java.util.concurrent.atomic.AtomicInteger;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.EvaluationException;
@@ -65,15 +67,15 @@ public class SpelExpression implements Expression {
// Holds the compiled form of the expression (if it has been compiled)
@Nullable
private CompiledExpression compiledAst;
private volatile CompiledExpression compiledAst;
// Count of many times as the expression been interpreted - can trigger compilation
// when certain limit reached
private volatile int interpretedCount = 0;
private final AtomicInteger interpretedCount = new AtomicInteger(0);
// The number of times compilation was attempted and failed - enables us to eventually
// give up trying to compile it when it just doesn't seem to be possible.
private volatile int failedAttempts = 0;
private final AtomicInteger failedAttempts = new AtomicInteger(0);
/**
@@ -116,16 +118,17 @@ public class SpelExpression implements Expression {
@Override
@Nullable
public Object getValue() throws EvaluationException {
if (this.compiledAst != null) {
CompiledExpression compiledAst = this.compiledAst;
if (compiledAst != null) {
try {
EvaluationContext context = getEvaluationContext();
return this.compiledAst.getValue(context.getRootObject().getValue(), context);
return compiledAst.getValue(context.getRootObject().getValue(), context);
}
catch (Throwable ex) {
// If running in mixed mode, revert to interpreted
if (this.configuration.getCompilerMode() == SpelCompilerMode.MIXED) {
this.interpretedCount = 0;
this.compiledAst = null;
this.interpretedCount.set(0);
}
else {
// Running in SpelCompilerMode.immediate mode - propagate exception to caller
@@ -144,10 +147,11 @@ public class SpelExpression implements Expression {
@Override
@Nullable
public <T> T getValue(@Nullable Class<T> expectedResultType) throws EvaluationException {
if (this.compiledAst != null) {
CompiledExpression compiledAst = this.compiledAst;
if (compiledAst != null) {
try {
EvaluationContext context = getEvaluationContext();
Object result = this.compiledAst.getValue(context.getRootObject().getValue(), context);
Object result = compiledAst.getValue(context.getRootObject().getValue(), context);
if (expectedResultType == null) {
return (T) result;
}
@@ -159,8 +163,8 @@ public class SpelExpression implements Expression {
catch (Throwable ex) {
// If running in mixed mode, revert to interpreted
if (this.configuration.getCompilerMode() == SpelCompilerMode.MIXED) {
this.interpretedCount = 0;
this.compiledAst = null;
this.interpretedCount.set(0);
}
else {
// Running in SpelCompilerMode.immediate mode - propagate exception to caller
@@ -179,15 +183,16 @@ public class SpelExpression implements Expression {
@Override
@Nullable
public Object getValue(Object rootObject) throws EvaluationException {
if (this.compiledAst != null) {
CompiledExpression compiledAst = this.compiledAst;
if (compiledAst != null) {
try {
return this.compiledAst.getValue(rootObject, getEvaluationContext());
return compiledAst.getValue(rootObject, getEvaluationContext());
}
catch (Throwable ex) {
// If running in mixed mode, revert to interpreted
if (this.configuration.getCompilerMode() == SpelCompilerMode.MIXED) {
this.interpretedCount = 0;
this.compiledAst = null;
this.interpretedCount.set(0);
}
else {
// Running in SpelCompilerMode.immediate mode - propagate exception to caller
@@ -207,9 +212,10 @@ public class SpelExpression implements Expression {
@Override
@Nullable
public <T> T getValue(Object rootObject, @Nullable Class<T> expectedResultType) throws EvaluationException {
if (this.compiledAst != null) {
CompiledExpression compiledAst = this.compiledAst;
if (compiledAst != null) {
try {
Object result = this.compiledAst.getValue(rootObject, getEvaluationContext());
Object result = compiledAst.getValue(rootObject, getEvaluationContext());
if (expectedResultType == null) {
return (T)result;
}
@@ -221,8 +227,8 @@ public class SpelExpression implements Expression {
catch (Throwable ex) {
// If running in mixed mode, revert to interpreted
if (this.configuration.getCompilerMode() == SpelCompilerMode.MIXED) {
this.interpretedCount = 0;
this.compiledAst = null;
this.interpretedCount.set(0);
}
else {
// Running in SpelCompilerMode.immediate mode - propagate exception to caller
@@ -244,15 +250,16 @@ public class SpelExpression implements Expression {
public Object getValue(EvaluationContext context) throws EvaluationException {
Assert.notNull(context, "EvaluationContext is required");
if (this.compiledAst != null) {
CompiledExpression compiledAst = this.compiledAst;
if (compiledAst != null) {
try {
return this.compiledAst.getValue(context.getRootObject().getValue(), context);
return compiledAst.getValue(context.getRootObject().getValue(), context);
}
catch (Throwable ex) {
// If running in mixed mode, revert to interpreted
if (this.configuration.getCompilerMode() == SpelCompilerMode.MIXED) {
this.interpretedCount = 0;
this.compiledAst = null;
this.interpretedCount.set(0);
}
else {
// Running in SpelCompilerMode.immediate mode - propagate exception to caller
@@ -273,9 +280,10 @@ public class SpelExpression implements Expression {
public <T> T getValue(EvaluationContext context, @Nullable Class<T> expectedResultType) throws EvaluationException {
Assert.notNull(context, "EvaluationContext is required");
if (this.compiledAst != null) {
CompiledExpression compiledAst = this.compiledAst;
if (compiledAst != null) {
try {
Object result = this.compiledAst.getValue(context.getRootObject().getValue(), context);
Object result = compiledAst.getValue(context.getRootObject().getValue(), context);
if (expectedResultType != null) {
return ExpressionUtils.convertTypedValue(context, new TypedValue(result), expectedResultType);
}
@@ -286,8 +294,8 @@ public class SpelExpression implements Expression {
catch (Throwable ex) {
// If running in mixed mode, revert to interpreted
if (this.configuration.getCompilerMode() == SpelCompilerMode.MIXED) {
this.interpretedCount = 0;
this.compiledAst = null;
this.interpretedCount.set(0);
}
else {
// Running in SpelCompilerMode.immediate mode - propagate exception to caller
@@ -307,15 +315,16 @@ public class SpelExpression implements Expression {
public Object getValue(EvaluationContext context, Object rootObject) throws EvaluationException {
Assert.notNull(context, "EvaluationContext is required");
if (this.compiledAst != null) {
CompiledExpression compiledAst = this.compiledAst;
if (compiledAst != null) {
try {
return this.compiledAst.getValue(rootObject, context);
return compiledAst.getValue(rootObject, context);
}
catch (Throwable ex) {
// If running in mixed mode, revert to interpreted
if (this.configuration.getCompilerMode() == SpelCompilerMode.MIXED) {
this.interpretedCount = 0;
this.compiledAst = null;
this.interpretedCount.set(0);
}
else {
// Running in SpelCompilerMode.immediate mode - propagate exception to caller
@@ -338,9 +347,10 @@ public class SpelExpression implements Expression {
Assert.notNull(context, "EvaluationContext is required");
if (this.compiledAst != null) {
CompiledExpression compiledAst = this.compiledAst;
if (compiledAst != null) {
try {
Object result = this.compiledAst.getValue(rootObject, context);
Object result = compiledAst.getValue(rootObject, context);
if (expectedResultType != null) {
return ExpressionUtils.convertTypedValue(context, new TypedValue(result), expectedResultType);
}
@@ -351,8 +361,8 @@ public class SpelExpression implements Expression {
catch (Throwable ex) {
// If running in mixed mode, revert to interpreted
if (this.configuration.getCompilerMode() == SpelCompilerMode.MIXED) {
this.interpretedCount = 0;
this.compiledAst = null;
this.interpretedCount.set(0);
}
else {
// Running in SpelCompilerMode.immediate mode - propagate exception to caller
@@ -473,48 +483,58 @@ public class SpelExpression implements Expression {
* @param expressionState the expression state used to determine compilation mode
*/
private void checkCompile(ExpressionState expressionState) {
this.interpretedCount++;
this.interpretedCount.incrementAndGet();
SpelCompilerMode compilerMode = expressionState.getConfiguration().getCompilerMode();
if (compilerMode != SpelCompilerMode.OFF) {
if (compilerMode == SpelCompilerMode.IMMEDIATE) {
if (this.interpretedCount > 1) {
if (this.interpretedCount.get() > 1) {
compileExpression();
}
}
else {
// compilerMode = SpelCompilerMode.MIXED
if (this.interpretedCount > INTERPRETED_COUNT_THRESHOLD) {
if (this.interpretedCount.get() > INTERPRETED_COUNT_THRESHOLD) {
compileExpression();
}
}
}
}
/**
* Perform expression compilation. This will only succeed once exit descriptors for all nodes have
* been determined. If the compilation fails and has failed more than 100 times the expression is
* no longer considered suitable for compilation.
* Perform expression compilation. This will only succeed once exit descriptors for
* all nodes have been determined. If the compilation fails and has failed more than
* 100 times the expression is no longer considered suitable for compilation.
* @return whether this expression has been successfully compiled
*/
public boolean compileExpression() {
if (this.failedAttempts > FAILED_ATTEMPTS_THRESHOLD) {
CompiledExpression compiledAst = this.compiledAst;
if (compiledAst != null) {
// Previously compiled
return true;
}
if (this.failedAttempts.get() > FAILED_ATTEMPTS_THRESHOLD) {
// Don't try again
return false;
}
if (this.compiledAst == null) {
synchronized (this.expression) {
// Possibly compiled by another thread before this thread got into the sync block
if (this.compiledAst != null) {
return true;
}
SpelCompiler compiler = SpelCompiler.getCompiler(this.configuration.getCompilerClassLoader());
this.compiledAst = compiler.compile(this.ast);
if (this.compiledAst == null) {
this.failedAttempts++;
}
synchronized (this) {
if (this.compiledAst != null) {
// Compiled by another thread before this thread got into the sync block
return true;
}
SpelCompiler compiler = SpelCompiler.getCompiler(this.configuration.getCompilerClassLoader());
compiledAst = compiler.compile(this.ast);
if (compiledAst != null) {
// Successfully compiled
this.compiledAst = compiledAst;
return true;
}
else {
// Failed to compile
this.failedAttempts.incrementAndGet();
return false;
}
}
return (this.compiledAst != null);
}
/**
@@ -524,8 +544,8 @@ public class SpelExpression implements Expression {
*/
public void revertToInterpreted() {
this.compiledAst = null;
this.interpretedCount = 0;
this.failedAttempts = 0;
this.interpretedCount.set(0);
this.failedAttempts.set(0);
}
/**
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2019 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.
@@ -220,6 +220,7 @@ public class SingleColumnRowMapper<T> implements RowMapper<T> {
* (with the required type specified only once).
* @param requiredType the type that each result object is expected to match
* @since 4.1
* @see #newInstance(Class, ConversionService)
*/
public static <T> SingleColumnRowMapper<T> newInstance(Class<T> requiredType) {
return new SingleColumnRowMapper<>(requiredType);
@@ -229,10 +230,15 @@ public class SingleColumnRowMapper<T> implements RowMapper<T> {
* Static factory method to create a new {@code SingleColumnRowMapper}
* (with the required type specified only once).
* @param requiredType the type that each result object is expected to match
* @param conversionService the {@link ConversionService} for converting a fetched value
* @param conversionService the {@link ConversionService} for converting a
* fetched value, or {@code null} for none
* @since 5.0.4
* @see #newInstance(Class)
* @see #setConversionService
*/
public static <T> SingleColumnRowMapper<T> newInstance(Class<T> requiredType, @Nullable ConversionService conversionService) {
public static <T> SingleColumnRowMapper<T> newInstance(
Class<T> requiredType, @Nullable ConversionService conversionService) {
SingleColumnRowMapper<T> rowMapper = newInstance(requiredType);
rowMapper.setConversionService(conversionService);
return rowMapper;
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -29,6 +29,7 @@ import java.util.HashMap;
import java.util.Map;
import org.springframework.jdbc.InvalidResultSetAccessException;
import org.springframework.lang.Nullable;
/**
* The default implementation of Spring's {@link SqlRowSet} interface, wrapping a
@@ -160,6 +161,7 @@ public class ResultSetWrappingSqlRowSet implements SqlRowSet {
* @see java.sql.ResultSet#getBigDecimal(int)
*/
@Override
@Nullable
public BigDecimal getBigDecimal(int columnIndex) throws InvalidResultSetAccessException {
try {
return this.resultSet.getBigDecimal(columnIndex);
@@ -173,6 +175,7 @@ public class ResultSetWrappingSqlRowSet implements SqlRowSet {
* @see java.sql.ResultSet#getBigDecimal(String)
*/
@Override
@Nullable
public BigDecimal getBigDecimal(String columnLabel) throws InvalidResultSetAccessException {
return getBigDecimal(findColumn(columnLabel));
}
@@ -223,6 +226,7 @@ public class ResultSetWrappingSqlRowSet implements SqlRowSet {
* @see java.sql.ResultSet#getDate(int)
*/
@Override
@Nullable
public Date getDate(int columnIndex) throws InvalidResultSetAccessException {
try {
return this.resultSet.getDate(columnIndex);
@@ -236,6 +240,7 @@ public class ResultSetWrappingSqlRowSet implements SqlRowSet {
* @see java.sql.ResultSet#getDate(String)
*/
@Override
@Nullable
public Date getDate(String columnLabel) throws InvalidResultSetAccessException {
return getDate(findColumn(columnLabel));
}
@@ -244,6 +249,7 @@ public class ResultSetWrappingSqlRowSet implements SqlRowSet {
* @see java.sql.ResultSet#getDate(int, Calendar)
*/
@Override
@Nullable
public Date getDate(int columnIndex, Calendar cal) throws InvalidResultSetAccessException {
try {
return this.resultSet.getDate(columnIndex, cal);
@@ -257,6 +263,7 @@ public class ResultSetWrappingSqlRowSet implements SqlRowSet {
* @see java.sql.ResultSet#getDate(String, Calendar)
*/
@Override
@Nullable
public Date getDate(String columnLabel, Calendar cal) throws InvalidResultSetAccessException {
return getDate(findColumn(columnLabel), cal);
}
@@ -349,6 +356,7 @@ public class ResultSetWrappingSqlRowSet implements SqlRowSet {
* @see java.sql.ResultSet#getNString(int)
*/
@Override
@Nullable
public String getNString(int columnIndex) throws InvalidResultSetAccessException {
try {
return this.resultSet.getNString(columnIndex);
@@ -362,6 +370,7 @@ public class ResultSetWrappingSqlRowSet implements SqlRowSet {
* @see java.sql.ResultSet#getNString(String)
*/
@Override
@Nullable
public String getNString(String columnLabel) throws InvalidResultSetAccessException {
return getNString(findColumn(columnLabel));
}
@@ -370,6 +379,7 @@ public class ResultSetWrappingSqlRowSet implements SqlRowSet {
* @see java.sql.ResultSet#getObject(int)
*/
@Override
@Nullable
public Object getObject(int columnIndex) throws InvalidResultSetAccessException {
try {
return this.resultSet.getObject(columnIndex);
@@ -383,6 +393,7 @@ public class ResultSetWrappingSqlRowSet implements SqlRowSet {
* @see java.sql.ResultSet#getObject(String)
*/
@Override
@Nullable
public Object getObject(String columnLabel) throws InvalidResultSetAccessException {
return getObject(findColumn(columnLabel));
}
@@ -391,6 +402,7 @@ public class ResultSetWrappingSqlRowSet implements SqlRowSet {
* @see java.sql.ResultSet#getObject(int, Map)
*/
@Override
@Nullable
public Object getObject(int columnIndex, Map<String, Class<?>> map) throws InvalidResultSetAccessException {
try {
return this.resultSet.getObject(columnIndex, map);
@@ -404,6 +416,7 @@ public class ResultSetWrappingSqlRowSet implements SqlRowSet {
* @see java.sql.ResultSet#getObject(String, Map)
*/
@Override
@Nullable
public Object getObject(String columnLabel, Map<String, Class<?>> map) throws InvalidResultSetAccessException {
return getObject(findColumn(columnLabel), map);
}
@@ -412,6 +425,7 @@ public class ResultSetWrappingSqlRowSet implements SqlRowSet {
* @see java.sql.ResultSet#getObject(int, Class)
*/
@Override
@Nullable
public <T> T getObject(int columnIndex, Class<T> type) throws InvalidResultSetAccessException {
try {
return this.resultSet.getObject(columnIndex, type);
@@ -425,6 +439,7 @@ public class ResultSetWrappingSqlRowSet implements SqlRowSet {
* @see java.sql.ResultSet#getObject(String, Class)
*/
@Override
@Nullable
public <T> T getObject(String columnLabel, Class<T> type) throws InvalidResultSetAccessException {
return getObject(findColumn(columnLabel), type);
}
@@ -454,6 +469,7 @@ public class ResultSetWrappingSqlRowSet implements SqlRowSet {
* @see java.sql.ResultSet#getString(int)
*/
@Override
@Nullable
public String getString(int columnIndex) throws InvalidResultSetAccessException {
try {
return this.resultSet.getString(columnIndex);
@@ -467,6 +483,7 @@ public class ResultSetWrappingSqlRowSet implements SqlRowSet {
* @see java.sql.ResultSet#getString(String)
*/
@Override
@Nullable
public String getString(String columnLabel) throws InvalidResultSetAccessException {
return getString(findColumn(columnLabel));
}
@@ -475,6 +492,7 @@ public class ResultSetWrappingSqlRowSet implements SqlRowSet {
* @see java.sql.ResultSet#getTime(int)
*/
@Override
@Nullable
public Time getTime(int columnIndex) throws InvalidResultSetAccessException {
try {
return this.resultSet.getTime(columnIndex);
@@ -488,6 +506,7 @@ public class ResultSetWrappingSqlRowSet implements SqlRowSet {
* @see java.sql.ResultSet#getTime(String)
*/
@Override
@Nullable
public Time getTime(String columnLabel) throws InvalidResultSetAccessException {
return getTime(findColumn(columnLabel));
}
@@ -496,6 +515,7 @@ public class ResultSetWrappingSqlRowSet implements SqlRowSet {
* @see java.sql.ResultSet#getTime(int, Calendar)
*/
@Override
@Nullable
public Time getTime(int columnIndex, Calendar cal) throws InvalidResultSetAccessException {
try {
return this.resultSet.getTime(columnIndex, cal);
@@ -509,6 +529,7 @@ public class ResultSetWrappingSqlRowSet implements SqlRowSet {
* @see java.sql.ResultSet#getTime(String, Calendar)
*/
@Override
@Nullable
public Time getTime(String columnLabel, Calendar cal) throws InvalidResultSetAccessException {
return getTime(findColumn(columnLabel), cal);
}
@@ -517,6 +538,7 @@ public class ResultSetWrappingSqlRowSet implements SqlRowSet {
* @see java.sql.ResultSet#getTimestamp(int)
*/
@Override
@Nullable
public Timestamp getTimestamp(int columnIndex) throws InvalidResultSetAccessException {
try {
return this.resultSet.getTimestamp(columnIndex);
@@ -530,6 +552,7 @@ public class ResultSetWrappingSqlRowSet implements SqlRowSet {
* @see java.sql.ResultSet#getTimestamp(String)
*/
@Override
@Nullable
public Timestamp getTimestamp(String columnLabel) throws InvalidResultSetAccessException {
return getTimestamp(findColumn(columnLabel));
}
@@ -538,6 +561,7 @@ public class ResultSetWrappingSqlRowSet implements SqlRowSet {
* @see java.sql.ResultSet#getTimestamp(int, Calendar)
*/
@Override
@Nullable
public Timestamp getTimestamp(int columnIndex, Calendar cal) throws InvalidResultSetAccessException {
try {
return this.resultSet.getTimestamp(columnIndex, cal);
@@ -551,6 +575,7 @@ public class ResultSetWrappingSqlRowSet implements SqlRowSet {
* @see java.sql.ResultSet#getTimestamp(String, Calendar)
*/
@Override
@Nullable
public Timestamp getTimestamp(String columnLabel, Calendar cal) throws InvalidResultSetAccessException {
return getTimestamp(findColumn(columnLabel), cal);
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2019 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.Calendar;
import java.util.Map;
import org.springframework.jdbc.InvalidResultSetAccessException;
import org.springframework.lang.Nullable;
/**
* Mirror interface for {@link javax.sql.RowSet}, representing a disconnected variant of
@@ -74,6 +75,7 @@ public interface SqlRowSet extends Serializable {
* @return an BigDecimal object representing the column value
* @see java.sql.ResultSet#getBigDecimal(int)
*/
@Nullable
BigDecimal getBigDecimal(int columnIndex) throws InvalidResultSetAccessException;
/**
@@ -82,6 +84,7 @@ public interface SqlRowSet extends Serializable {
* @return an BigDecimal object representing the column value
* @see java.sql.ResultSet#getBigDecimal(String)
*/
@Nullable
BigDecimal getBigDecimal(String columnLabel) throws InvalidResultSetAccessException;
/**
@@ -122,6 +125,7 @@ public interface SqlRowSet extends Serializable {
* @return a Date object representing the column value
* @see java.sql.ResultSet#getDate(int)
*/
@Nullable
Date getDate(int columnIndex) throws InvalidResultSetAccessException;
/**
@@ -130,6 +134,7 @@ public interface SqlRowSet extends Serializable {
* @return a Date object representing the column value
* @see java.sql.ResultSet#getDate(String)
*/
@Nullable
Date getDate(String columnLabel) throws InvalidResultSetAccessException;
/**
@@ -139,6 +144,7 @@ public interface SqlRowSet extends Serializable {
* @return a Date object representing the column value
* @see java.sql.ResultSet#getDate(int, Calendar)
*/
@Nullable
Date getDate(int columnIndex, Calendar cal) throws InvalidResultSetAccessException;
/**
@@ -148,6 +154,7 @@ public interface SqlRowSet extends Serializable {
* @return a Date object representing the column value
* @see java.sql.ResultSet#getDate(String, Calendar)
*/
@Nullable
Date getDate(String columnLabel, Calendar cal) throws InvalidResultSetAccessException;
/**
@@ -222,6 +229,7 @@ public interface SqlRowSet extends Serializable {
* @since 4.1.3
* @see java.sql.ResultSet#getNString(int)
*/
@Nullable
String getNString(int columnIndex) throws InvalidResultSetAccessException;
/**
@@ -232,6 +240,7 @@ public interface SqlRowSet extends Serializable {
* @since 4.1.3
* @see java.sql.ResultSet#getNString(String)
*/
@Nullable
String getNString(String columnLabel) throws InvalidResultSetAccessException;
/**
@@ -240,6 +249,7 @@ public interface SqlRowSet extends Serializable {
* @return a Object representing the column value
* @see java.sql.ResultSet#getObject(int)
*/
@Nullable
Object getObject(int columnIndex) throws InvalidResultSetAccessException;
/**
@@ -248,6 +258,7 @@ public interface SqlRowSet extends Serializable {
* @return a Object representing the column value
* @see java.sql.ResultSet#getObject(String)
*/
@Nullable
Object getObject(String columnLabel) throws InvalidResultSetAccessException;
/**
@@ -257,6 +268,7 @@ public interface SqlRowSet extends Serializable {
* @return a Object representing the column value
* @see java.sql.ResultSet#getObject(int, Map)
*/
@Nullable
Object getObject(int columnIndex, Map<String, Class<?>> map) throws InvalidResultSetAccessException;
/**
@@ -266,6 +278,7 @@ public interface SqlRowSet extends Serializable {
* @return a Object representing the column value
* @see java.sql.ResultSet#getObject(String, Map)
*/
@Nullable
Object getObject(String columnLabel, Map<String, Class<?>> map) throws InvalidResultSetAccessException;
/**
@@ -273,9 +286,10 @@ public interface SqlRowSet extends Serializable {
* @param columnIndex the column index
* @param type the Java type to convert the designated column to
* @return a Object representing the column value
* @see java.sql.ResultSet#getObject(int)
* @since 4.1.3
* @see java.sql.ResultSet#getObject(int, Class)
*/
@Nullable
<T> T getObject(int columnIndex, Class<T> type) throws InvalidResultSetAccessException;
/**
@@ -283,9 +297,10 @@ public interface SqlRowSet extends Serializable {
* @param columnLabel the column label
* @param type the Java type to convert the designated column to
* @return a Object representing the column value
* @see java.sql.ResultSet#getObject(int)
* @since 4.1.3
* @see java.sql.ResultSet#getObject(String, Class)
*/
@Nullable
<T> T getObject(String columnLabel, Class<T> type) throws InvalidResultSetAccessException;
/**
@@ -310,6 +325,7 @@ public interface SqlRowSet extends Serializable {
* @return a String representing the column value
* @see java.sql.ResultSet#getString(int)
*/
@Nullable
String getString(int columnIndex) throws InvalidResultSetAccessException;
/**
@@ -318,6 +334,7 @@ public interface SqlRowSet extends Serializable {
* @return a String representing the column value
* @see java.sql.ResultSet#getString(String)
*/
@Nullable
String getString(String columnLabel) throws InvalidResultSetAccessException;
/**
@@ -326,6 +343,7 @@ public interface SqlRowSet extends Serializable {
* @return a Time object representing the column value
* @see java.sql.ResultSet#getTime(int)
*/
@Nullable
Time getTime(int columnIndex) throws InvalidResultSetAccessException;
/**
@@ -334,6 +352,7 @@ public interface SqlRowSet extends Serializable {
* @return a Time object representing the column value
* @see java.sql.ResultSet#getTime(String)
*/
@Nullable
Time getTime(String columnLabel) throws InvalidResultSetAccessException;
/**
@@ -343,6 +362,7 @@ public interface SqlRowSet extends Serializable {
* @return a Time object representing the column value
* @see java.sql.ResultSet#getTime(int, Calendar)
*/
@Nullable
Time getTime(int columnIndex, Calendar cal) throws InvalidResultSetAccessException;
/**
@@ -352,6 +372,7 @@ public interface SqlRowSet extends Serializable {
* @return a Time object representing the column value
* @see java.sql.ResultSet#getTime(String, Calendar)
*/
@Nullable
Time getTime(String columnLabel, Calendar cal) throws InvalidResultSetAccessException;
/**
@@ -360,6 +381,7 @@ public interface SqlRowSet extends Serializable {
* @return a Timestamp object representing the column value
* @see java.sql.ResultSet#getTimestamp(int)
*/
@Nullable
Timestamp getTimestamp(int columnIndex) throws InvalidResultSetAccessException;
/**
@@ -368,6 +390,7 @@ public interface SqlRowSet extends Serializable {
* @return a Timestamp object representing the column value
* @see java.sql.ResultSet#getTimestamp(String)
*/
@Nullable
Timestamp getTimestamp(String columnLabel) throws InvalidResultSetAccessException;
/**
@@ -377,6 +400,7 @@ public interface SqlRowSet extends Serializable {
* @return a Timestamp object representing the column value
* @see java.sql.ResultSet#getTimestamp(int, Calendar)
*/
@Nullable
Timestamp getTimestamp(int columnIndex, Calendar cal) throws InvalidResultSetAccessException;
/**
@@ -386,6 +410,7 @@ public interface SqlRowSet extends Serializable {
* @return a Timestamp object representing the column value
* @see java.sql.ResultSet#getTimestamp(String, Calendar)
*/
@Nullable
Timestamp getTimestamp(String columnLabel, Calendar cal) throws InvalidResultSetAccessException;
@@ -205,7 +205,7 @@
<value>1</value>
</property>
<property name="cannotAcquireLockCodes">
<value>1205</value>
<value>1205,3572</value>
</property>
<property name="deadlockLoserCodes">
<value>1213</value>
@@ -858,6 +858,7 @@ public class DataSourceTransactionManagerTests {
public void testTransactionWithIsolationAndReadOnly() throws Exception {
given(con.getTransactionIsolation()).willReturn(Connection.TRANSACTION_READ_COMMITTED);
given(con.getAutoCommit()).willReturn(true);
given(con.isReadOnly()).willReturn(true);
TransactionTemplate tt = new TransactionTemplate(tm);
tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
@@ -875,11 +876,13 @@ public class DataSourceTransactionManagerTests {
assertTrue("Hasn't thread connection", !TransactionSynchronizationManager.hasResource(ds));
InOrder ordered = inOrder(con);
ordered.verify(con).setReadOnly(true);
ordered.verify(con).setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);
ordered.verify(con).setAutoCommit(false);
ordered.verify(con).commit();
ordered.verify(con).setAutoCommit(true);
ordered.verify(con).setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);
ordered.verify(con).setReadOnly(false);
verify(con).close();
}
@@ -890,6 +893,7 @@ public class DataSourceTransactionManagerTests {
given(con.getAutoCommit()).willReturn(true);
Statement stmt = mock(Statement.class);
given(con.createStatement()).willReturn(stmt);
given(con.isReadOnly()).willReturn(true);
TransactionTemplate tt = new TransactionTemplate(tm);
tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
@@ -906,11 +910,13 @@ public class DataSourceTransactionManagerTests {
assertTrue("Hasn't thread connection", !TransactionSynchronizationManager.hasResource(ds));
InOrder ordered = inOrder(con, stmt);
ordered.verify(con).setReadOnly(true);
ordered.verify(con).setAutoCommit(false);
ordered.verify(stmt).executeUpdate("SET TRANSACTION READ ONLY");
ordered.verify(stmt).close();
ordered.verify(con).commit();
ordered.verify(con).setAutoCommit(true);
ordered.verify(con).setReadOnly(false);
ordered.verify(con).close();
}
@@ -89,6 +89,7 @@ class CachedMessageProducer implements MessageProducer, QueueSender, TopicPublis
return this.target.getDisableMessageTimestamp();
}
@Override
public void setDeliveryDelay(long deliveryDelay) throws JMSException {
if (this.originalDeliveryDelay == null) {
this.originalDeliveryDelay = this.target.getDeliveryDelay();
@@ -96,6 +97,7 @@ class CachedMessageProducer implements MessageProducer, QueueSender, TopicPublis
this.target.setDeliveryDelay(deliveryDelay);
}
@Override
public long getDeliveryDelay() throws JMSException {
return this.target.getDeliveryDelay();
}
@@ -340,7 +340,7 @@ public class MappingJackson2MessageConverter implements SmartMessageConverter, B
* sets the resulting value (either a mapped id or the raw Java class name)
* into the configured type id message property.
* @param object the payload object to set a type id for
* @param message the JMS Message to set the type id on
* @param message the JMS Message on which to set the type id property
* @throws JMSException if thrown by JMS methods
* @see #getJavaTypeForMessage(javax.jms.Message)
* @see #setTypeIdPropertyName(String)
@@ -442,7 +442,7 @@ public class MappingJackson2MessageConverter implements SmartMessageConverter, B
* <p>The default implementation parses the configured type id property name
* and consults the configured type id mapping. This can be overridden with
* a different strategy, e.g. doing some heuristics based on message origin.
* @param message the JMS Message to set the type id on
* @param message the JMS Message from which to get the type id property
* @throws JMSException if thrown by JMS methods
* @see #setTypeIdOnMessage(Object, javax.jms.Message)
* @see #setTypeIdPropertyName(String)
@@ -560,7 +560,6 @@ public class MultiServerUserRegistry implements SimpUserRegistry, SmartApplicati
}
return map;
}
}
}
+1 -1
View File
@@ -9,7 +9,7 @@ dependencies {
optional(project(":spring-context"))
optional(project(":spring-web"))
optional("org.eclipse.persistence:org.eclipse.persistence.jpa:2.7.4")
optional("org.hibernate:hibernate-core:5.3.12.Final")
optional("org.hibernate:hibernate-core:5.3.15.Final")
optional("javax.servlet:javax.servlet-api:3.1.0")
testCompile("org.aspectj:aspectjweaver:${aspectjVersion}")
testCompile("org.hsqldb:hsqldb:${hsqldbVersion}")
+2 -2
View File
@@ -60,8 +60,8 @@ dependencies {
testCompile("javax.ejb:javax.ejb-api:3.2")
testCompile("javax.interceptor:javax.interceptor-api:1.2.2")
testCompile("javax.mail:javax.mail-api:1.6.2")
testCompile("org.hibernate:hibernate-core:5.3.12.Final")
testCompile("org.hibernate:hibernate-validator:6.0.17.Final")
testCompile("org.hibernate:hibernate-core:5.3.15.Final")
testCompile("org.hibernate:hibernate-validator:6.0.18.Final")
// Enable use of the JUnit Platform Runner
testCompile("org.junit.platform:junit-platform-runner")
testCompile("org.junit.jupiter:junit-jupiter-params")
@@ -16,6 +16,10 @@
package org.springframework.mock.web;
import java.time.DateTimeException;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import javax.servlet.http.Cookie;
import org.springframework.lang.Nullable;
@@ -28,6 +32,7 @@ import org.springframework.util.StringUtils;
*
* @author Vedran Pavic
* @author Juergen Hoeller
* @author Sam Brannen
* @since 5.1
*/
public class MockCookie extends Cookie {
@@ -35,12 +40,15 @@ public class MockCookie extends Cookie {
private static final long serialVersionUID = 4312531139502726325L;
@Nullable
private ZonedDateTime expires;
@Nullable
private String sameSite;
/**
* Constructor with the cookie name and value.
* Construct a new {@link MockCookie} with the supplied name and value.
* @param name the name
* @param value the value
* @see Cookie#Cookie(String, String)
@@ -49,12 +57,29 @@ public class MockCookie extends Cookie {
super(name, value);
}
/**
* Set the "Expires" attribute for this cookie.
* @since 5.1.11
*/
public void setExpires(@Nullable ZonedDateTime expires) {
this.expires = expires;
}
/**
* Add the "SameSite" attribute to the cookie.
* Get the "Expires" attribute for this cookie.
* @since 5.1.11
* @return the "Expires" attribute for this cookie, or {@code null} if not set
*/
@Nullable
public ZonedDateTime getExpires() {
return this.expires;
}
/**
* Set the "SameSite" attribute for this cookie.
* <p>This limits the scope of the cookie such that it will only be attached
* to same site requests if {@code "Strict"} or cross-site requests if
* {@code "Lax"}.
* to same-site requests if the supplied value is {@code "Strict"} or cross-site
* requests if the supplied value is {@code "Lax"}.
* @see <a href="https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis#section-4.1.2.7">RFC6265 bis</a>
*/
public void setSameSite(@Nullable String sameSite) {
@@ -62,7 +87,8 @@ public class MockCookie extends Cookie {
}
/**
* Return the "SameSite" attribute, or {@code null} if not set.
* Get the "SameSite" attribute for this cookie.
* @return the "SameSite" attribute for this cookie, or {@code null} if not set
*/
@Nullable
public String getSameSite() {
@@ -71,7 +97,7 @@ public class MockCookie extends Cookie {
/**
* Factory method that parses the value of a "Set-Cookie" header.
* Factory method that parses the value of the supplied "Set-Cookie" header.
* @param setCookieHeader the "Set-Cookie" value; never {@code null} or empty
* @return the created cookie
*/
@@ -94,6 +120,15 @@ public class MockCookie extends Cookie {
else if (StringUtils.startsWithIgnoreCase(attribute, "Max-Age")) {
cookie.setMaxAge(Integer.parseInt(extractAttributeValue(attribute, setCookieHeader)));
}
else if (StringUtils.startsWithIgnoreCase(attribute, "Expires")) {
try {
cookie.setExpires(ZonedDateTime.parse(extractAttributeValue(attribute, setCookieHeader),
DateTimeFormatter.RFC_1123_DATE_TIME));
}
catch (DateTimeException ex) {
// ignore invalid date formats
}
}
else if (StringUtils.startsWithIgnoreCase(attribute, "Path")) {
cookie.setPath(extractAttributeValue(attribute, setCookieHeader));
}
@@ -26,6 +26,8 @@ import java.io.Writer;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
@@ -345,9 +347,15 @@ public class MockHttpServletResponse implements HttpServletResponse {
if (maxAge >= 0) {
buf.append("; Max-Age=").append(maxAge);
buf.append("; Expires=");
HttpHeaders headers = new HttpHeaders();
headers.setExpires(maxAge > 0 ? System.currentTimeMillis() + 1000L * maxAge : 0);
buf.append(headers.getFirst(HttpHeaders.EXPIRES));
ZonedDateTime expires = (cookie instanceof MockCookie ? ((MockCookie) cookie).getExpires() : null);
if (expires != null) {
buf.append(expires.format(DateTimeFormatter.RFC_1123_DATE_TIME));
}
else {
HttpHeaders headers = new HttpHeaders();
headers.setExpires(maxAge > 0 ? System.currentTimeMillis() + 1000L * maxAge : 0);
buf.append(headers.getFirst(HttpHeaders.EXPIRES));
}
}
if (cookie.getSecure()) {
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2019 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.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.file.InvalidPathException;
import java.util.Collections;
import java.util.Enumeration;
import java.util.EventListener;
@@ -294,8 +295,10 @@ public class MockServletContext implements ServletContext {
@Nullable
public Set<String> getResourcePaths(String path) {
String actualPath = (path.endsWith("/") ? path : path + "/");
Resource resource = this.resourceLoader.getResource(getResourceLocation(actualPath));
String resourceLocation = getResourceLocation(actualPath);
Resource resource = null;
try {
resource = this.resourceLoader.getResource(resourceLocation);
File file = resource.getFile();
String[] fileList = file.list();
if (ObjectUtils.isEmpty(fileList)) {
@@ -311,9 +314,10 @@ public class MockServletContext implements ServletContext {
}
return resourcePaths;
}
catch (IOException ex) {
catch (InvalidPathException | IOException ex ) {
if (logger.isWarnEnabled()) {
logger.warn("Could not get resource paths for " + resource, ex);
logger.warn("Could not get resource paths for " +
(resource != null ? resource : resourceLocation), ex);
}
return null;
}
@@ -322,19 +326,22 @@ public class MockServletContext implements ServletContext {
@Override
@Nullable
public URL getResource(String path) throws MalformedURLException {
Resource resource = this.resourceLoader.getResource(getResourceLocation(path));
if (!resource.exists()) {
return null;
}
String resourceLocation = getResourceLocation(path);
Resource resource = null;
try {
resource = this.resourceLoader.getResource(resourceLocation);
if (!resource.exists()) {
return null;
}
return resource.getURL();
}
catch (MalformedURLException ex) {
throw ex;
}
catch (IOException ex) {
catch (InvalidPathException | IOException ex) {
if (logger.isWarnEnabled()) {
logger.warn("Could not get URL for " + resource, ex);
logger.warn("Could not get URL for resource " +
(resource != null ? resource : resourceLocation), ex);
}
return null;
}
@@ -343,16 +350,19 @@ public class MockServletContext implements ServletContext {
@Override
@Nullable
public InputStream getResourceAsStream(String path) {
Resource resource = this.resourceLoader.getResource(getResourceLocation(path));
if (!resource.exists()) {
return null;
}
String resourceLocation = getResourceLocation(path);
Resource resource = null;
try {
resource = this.resourceLoader.getResource(resourceLocation);
if (!resource.exists()) {
return null;
}
return resource.getInputStream();
}
catch (IOException ex) {
catch (InvalidPathException | IOException ex) {
if (logger.isWarnEnabled()) {
logger.warn("Could not open InputStream for " + resource, ex);
logger.warn("Could not open InputStream for resource " +
(resource != null ? resource : resourceLocation), ex);
}
return null;
}
@@ -459,13 +469,16 @@ public class MockServletContext implements ServletContext {
@Override
@Nullable
public String getRealPath(String path) {
Resource resource = this.resourceLoader.getResource(getResourceLocation(path));
String resourceLocation = getResourceLocation(path);
Resource resource = null;
try {
resource = this.resourceLoader.getResource(resourceLocation);
return resource.getFile().getAbsolutePath();
}
catch (IOException ex) {
catch (InvalidPathException | IOException ex) {
if (logger.isWarnEnabled()) {
logger.warn("Could not determine real path of resource " + resource, ex);
logger.warn("Could not determine real path of resource " +
(resource != null ? resource : resourceLocation), ex);
}
return null;
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2019 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.function.Consumer;
import org.springframework.http.HttpHeaders;
import org.springframework.http.client.reactive.ClientHttpConnector;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import org.springframework.http.codec.ClientCodecConfigurer;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.MultiValueMap;
@@ -136,12 +137,25 @@ class DefaultWebTestClientBuilder implements WebTestClient.Builder {
return this;
}
@Override
public WebTestClient.Builder codecs(Consumer<ClientCodecConfigurer> configurer) {
this.webClientBuilder.codecs(configurer);
return this;
}
@Override
public WebTestClient.Builder exchangeStrategies(ExchangeStrategies strategies) {
this.webClientBuilder.exchangeStrategies(strategies);
return this;
}
@SuppressWarnings("deprecation")
@Override
public WebTestClient.Builder exchangeStrategies(Consumer<ExchangeStrategies.Builder> configurer) {
this.webClientBuilder.exchangeStrategies(configurer);
return this;
}
@Override
public WebTestClient.Builder responseTimeout(Duration timeout) {
this.responseTimeout = timeout;
@@ -36,6 +36,7 @@ import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.client.reactive.ClientHttpConnector;
import org.springframework.http.client.reactive.ClientHttpRequest;
import org.springframework.http.codec.ClientCodecConfigurer;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.lang.Nullable;
import org.springframework.util.MultiValueMap;
@@ -77,6 +78,7 @@ import org.springframework.web.util.UriBuilderFactory;
* and Spring Kotlin extensions to perform integration tests on an embedded WebFlux server.
*
* @author Rossen Stoyanchev
* @author Brian Clozel
* @since 5.0
* @see StatusAssertions
* @see HeaderAssertions
@@ -434,13 +436,36 @@ public interface WebTestClient {
*/
Builder filters(Consumer<List<ExchangeFilterFunction>> filtersConsumer);
/**
* Configure the codecs for the {@code WebClient} in the
* {@link #exchangeStrategies(ExchangeStrategies) underlying}
* {@code ExchangeStrategies}.
* @param configurer the configurer to apply
* @since 5.1.13
*/
Builder codecs(Consumer<ClientCodecConfigurer> configurer);
/**
* Configure the {@link ExchangeStrategies} to use.
* <p>By default {@link ExchangeStrategies#withDefaults()} is used.
* <p>For most cases, prefer using {@link #codecs(Consumer)} which allows
* customizing the codecs in the {@code ExchangeStrategies} rather than
* replace them. That ensures multiple parties can contribute to codecs
* configuration.
* <p>By default this is set to {@link ExchangeStrategies#withDefaults()}.
* @param strategies the strategies to use
*/
Builder exchangeStrategies(ExchangeStrategies strategies);
/**
* Customize the strategies configured via
* {@link #exchangeStrategies(ExchangeStrategies)}. This method is
* designed for use in scenarios where multiple parties wish to update
* the {@code ExchangeStrategies}.
* @deprecated as of 5.1.13 in favor of {@link #codecs(Consumer)}
*/
@Deprecated
Builder exchangeStrategies(Consumer<ExchangeStrategies.Builder> configurer);
/**
* Max amount of time to wait for responses.
* <p>By default 5 seconds.
@@ -877,7 +902,7 @@ public interface WebTestClient {
* @since 5.1
* @see #xpath(String, Map, Object...)
*/
default XpathAssertions xpath(String expression, Object... args){
default XpathAssertions xpath(String expression, Object... args) {
return xpath(expression, null, args);
}
@@ -891,7 +916,7 @@ public interface WebTestClient {
* @param args arguments to parameterize the expression
* @since 5.1
*/
XpathAssertions xpath(String expression, @Nullable Map<String, String> namespaces, Object... args);
XpathAssertions xpath(String expression, @Nullable Map<String, String> namespaces, Object... args);
/**
* Assert the response body content with the given {@link Consumer}.
@@ -16,6 +16,9 @@
package org.springframework.mock.web;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
@@ -67,8 +70,8 @@ public class MockCookieTests {
@Test
public void parseHeaderWithAttributes() {
MockCookie cookie = MockCookie.parse(
"SESSION=123; Domain=example.com; Max-Age=60; Path=/; Secure; HttpOnly; SameSite=Lax");
MockCookie cookie = MockCookie.parse("SESSION=123; Domain=example.com; Max-Age=60; " +
"Expires=Tue, 8 Oct 2019 19:50:00 GMT; Path=/; Secure; HttpOnly; SameSite=Lax");
assertCookie(cookie, "SESSION", "123");
assertEquals("example.com", cookie.getDomain());
@@ -76,9 +79,27 @@ public class MockCookieTests {
assertEquals("/", cookie.getPath());
assertTrue(cookie.getSecure());
assertTrue(cookie.isHttpOnly());
assertEquals(ZonedDateTime.parse("Tue, 8 Oct 2019 19:50:00 GMT",
DateTimeFormatter.RFC_1123_DATE_TIME), cookie.getExpires());
assertEquals("Lax", cookie.getSameSite());
}
@Test
public void parseHeaderWithZeroExpiresAttribute() {
MockCookie cookie = MockCookie.parse("SESSION=123; Expires=0");
assertCookie(cookie, "SESSION", "123");
assertNull(cookie.getExpires());
}
@Test
public void parseHeaderWithBogusExpiresAttribute() {
MockCookie cookie = MockCookie.parse("SESSION=123; Expires=bogus");
assertCookie(cookie, "SESSION", "123");
assertNull(cookie.getExpires());
}
private void assertCookie(MockCookie cookie, String name, String value) {
assertEquals(name, cookie.getName());
assertEquals(value, cookie.getValue());
@@ -109,8 +130,8 @@ public class MockCookieTests {
@Test
public void parseHeaderWithAttributesCaseSensitivity() {
MockCookie cookie = MockCookie.parse(
"SESSION=123; domain=example.com; max-age=60; path=/; secure; httponly; samesite=Lax");
MockCookie cookie = MockCookie.parse("SESSION=123; domain=example.com; max-age=60; " +
"expires=Tue, 8 Oct 2019 19:50:00 GMT; path=/; secure; httponly; samesite=Lax");
assertCookie(cookie, "SESSION", "123");
assertEquals("example.com", cookie.getDomain());
@@ -118,6 +139,8 @@ public class MockCookieTests {
assertEquals("/", cookie.getPath());
assertTrue(cookie.getSecure());
assertTrue(cookie.isHttpOnly());
assertEquals(ZonedDateTime.parse("Tue, 8 Oct 2019 19:50:00 GMT",
DateTimeFormatter.RFC_1123_DATE_TIME), cookie.getExpires());
assertEquals("Lax", cookie.getSameSite());
}
@@ -39,6 +39,7 @@ import static org.junit.Assert.*;
* @author Rob Winch
* @author Sam Brannen
* @author Brian Clozel
* @author Vedran Pavic
* @since 19.02.2006
*/
public class MockHttpServletResponseTests {
@@ -338,6 +339,32 @@ public class MockHttpServletResponseTests {
assertPrimarySessionCookie("999");
}
/**
* @since 5.1.11
*/
@Test
public void setCookieHeaderWithExpiresAttribute() {
String cookieValue = "SESSION=123; Path=/; Max-Age=100; Expires=Tue, 8 Oct 2019 19:50:00 GMT; Secure; " +
"HttpOnly; SameSite=Lax";
response.setHeader(HttpHeaders.SET_COOKIE, cookieValue);
assertNumCookies(1);
assertEquals(cookieValue, response.getHeader(HttpHeaders.SET_COOKIE));
}
/**
* @since 5.1.12
*/
@Test
public void setCookieHeaderWithZeroExpiresAttribute() {
String cookieValue = "SESSION=123; Path=/; Max-Age=100; Expires=0";
response.setHeader(HttpHeaders.SET_COOKIE, cookieValue);
assertNumCookies(1);
String header = response.getHeader(HttpHeaders.SET_COOKIE);
assertNotEquals(cookieValue, header);
// We don't assert the actual Expires value since it is based on the current time.
assertTrue(header.startsWith("SESSION=123; Path=/; Max-Age=100; Expires="));
}
@Test
public void addCookieHeader() {
response.addHeader(HttpHeaders.SET_COOKIE, "SESSION=123; Path=/; Secure; HttpOnly; SameSite=Lax");
@@ -351,6 +378,31 @@ public class MockHttpServletResponseTests {
assertCookieValues("123", "999");
}
/**
* @since 5.1.11
*/
@Test
public void addCookieHeaderWithExpiresAttribute() {
String cookieValue = "SESSION=123; Path=/; Max-Age=100; Expires=Tue, 8 Oct 2019 19:50:00 GMT; Secure; " +
"HttpOnly; SameSite=Lax";
response.addHeader(HttpHeaders.SET_COOKIE, cookieValue);
assertEquals(cookieValue, response.getHeader(HttpHeaders.SET_COOKIE));
}
/**
* @since 5.1.12
*/
@Test
public void addCookieHeaderWithZeroExpiresAttribute() {
String cookieValue = "SESSION=123; Path=/; Max-Age=100; Expires=0";
response.addHeader(HttpHeaders.SET_COOKIE, cookieValue);
assertNumCookies(1);
String header = response.getHeader(HttpHeaders.SET_COOKIE);
assertNotEquals(cookieValue, header);
// We don't assert the actual Expires value since it is based on the current time.
assertTrue(header.startsWith("SESSION=123; Path=/; Max-Age=100; Expires="));
}
@Test
public void addCookie() {
MockCookie mockCookie = new MockCookie("SESSION", "123");
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2017 the original author or authors.
* Copyright 2002-2019 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,8 @@
package org.springframework.mock.web;
import java.io.InputStream;
import java.net.URL;
import java.util.Map;
import java.util.Set;
@@ -24,7 +26,9 @@ import javax.servlet.RequestDispatcher;
import javax.servlet.ServletRegistration;
import org.junit.Test;
import org.junit.jupiter.api.condition.OS;
import org.springframework.core.io.FileSystemResourceLoader;
import org.springframework.http.MediaType;
import static org.junit.Assert.assertEquals;
@@ -34,6 +38,8 @@ import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
/**
* Unit tests for {@link MockServletContext}.
*
* @author Juergen Hoeller
* @author Chris Beams
* @author Sam Brannen
@@ -45,27 +51,27 @@ public class MockServletContextTests {
@Test
public void listFiles() {
public void getResourcePaths() {
Set<String> paths = sc.getResourcePaths("/web");
assertNotNull(paths);
assertTrue(paths.contains("/web/MockServletContextTests.class"));
}
@Test
public void listSubdirectories() {
public void getResourcePathsWithSubdirectories() {
Set<String> paths = sc.getResourcePaths("/");
assertNotNull(paths);
assertTrue(paths.contains("/web/"));
}
@Test
public void listNonDirectory() {
public void getResourcePathsWithNonDirectory() {
Set<String> paths = sc.getResourcePaths("/web/MockServletContextTests.class");
assertNull(paths);
}
@Test
public void listInvalidPath() {
public void getResourcePathsWithInvalidPath() {
Set<String> paths = sc.getResourcePaths("/web/invalid");
assertNull(paths);
}
@@ -194,4 +200,50 @@ public class MockServletContextTests {
assertEquals(0, filterRegistrations.size());
}
/**
* @since 5.1.11
*/
@Test
public void getResourcePathsWithRelativePathToWindowsCDrive() {
MockServletContext servletContext = new MockServletContext( "org/springframework/mock", new FileSystemResourceLoader());
Set<String> paths = servletContext.getResourcePaths("C:\\temp");
assertNull(paths);
}
/**
* @since 5.1.11
*/
@Test
public void getResourceWithRelativePathToWindowsCDrive() throws Exception {
MockServletContext servletContext = new MockServletContext( "org/springframework/mock", new FileSystemResourceLoader());
URL resource = servletContext.getResource("C:\\temp");
assertNull(resource);
}
/**
* @since 5.1.11
*/
@Test
public void getResourceAsStreamWithRelativePathToWindowsCDrive() {
MockServletContext servletContext = new MockServletContext( "org/springframework/mock", new FileSystemResourceLoader());
InputStream inputStream = servletContext.getResourceAsStream("C:\\temp");
assertNull(inputStream);
}
/**
* @since 5.1.11
*/
@Test
public void getRealPathWithRelativePathToWindowsCDrive() {
MockServletContext servletContext = new MockServletContext( "org/springframework/mock", new FileSystemResourceLoader());
String realPath = servletContext.getRealPath("C:\\temp");
if (OS.WINDOWS.isCurrentOs()) {
assertNull(realPath);
}
else {
assertNotNull(realPath);
}
}
}
@@ -0,0 +1,206 @@
/*
* Copyright 2002-2019 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.test.web.servlet.samples.spr;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Controller;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.annotation.RequestScope;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import static org.junit.Assert.assertEquals;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.forwardedUrl;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.springframework.test.web.servlet.setup.MockMvcBuilders.webAppContextSetup;
/**
* Integration tests for {@link ControllerAdvice @ControllerAdvice}.
*
* <p>Introduced in conjunction with
* <a href="https://github.com/spring-projects/spring-framework/issues/24017">gh-24017</a>.
*
* @author Sam Brannen
* @since 5.1.12
*/
@RunWith(SpringRunner.class)
@WebAppConfiguration
public class ControllerAdviceIntegrationTests {
@Autowired
WebApplicationContext wac;
MockMvc mockMvc;
@Before
public void setUpMockMvc() {
this.mockMvc = webAppContextSetup(wac).build();
resetCounters();
}
@Test
public void controllerAdviceIsAppliedOnlyOnce() throws Exception {
this.mockMvc.perform(get("/test").param("requestParam", "foo"))//
.andExpect(status().isOk())//
.andExpect(forwardedUrl("singleton:1;prototype:1;request-scoped:1;requestParam:foo"));
assertEquals(1, SingletonControllerAdvice.invocationCount.get());
assertEquals(1, PrototypeControllerAdvice.invocationCount.get());
assertEquals(1, RequestScopedControllerAdvice.invocationCount.get());
}
@Test
public void prototypeAndRequestScopedControllerAdviceBeansAreNotCached() throws Exception {
this.mockMvc.perform(get("/test").param("requestParam", "foo"))//
.andExpect(status().isOk())//
.andExpect(forwardedUrl("singleton:1;prototype:1;request-scoped:1;requestParam:foo"));
// singleton @ControllerAdvice beans should not be instantiated again.
assertEquals(0, SingletonControllerAdvice.instanceCount.get());
// prototype and request-scoped @ControllerAdvice beans should be instantiated once per request.
assertEquals(1, PrototypeControllerAdvice.instanceCount.get());
assertEquals(1, RequestScopedControllerAdvice.instanceCount.get());
this.mockMvc.perform(get("/test").param("requestParam", "bar"))//
.andExpect(status().isOk())//
.andExpect(forwardedUrl("singleton:2;prototype:2;request-scoped:2;requestParam:bar"));
// singleton @ControllerAdvice beans should not be instantiated again.
assertEquals(0, SingletonControllerAdvice.instanceCount.get());
// prototype and request-scoped @ControllerAdvice beans should be instantiated once per request.
assertEquals(2, PrototypeControllerAdvice.instanceCount.get());
assertEquals(2, RequestScopedControllerAdvice.instanceCount.get());
}
private static void resetCounters() {
SingletonControllerAdvice.invocationCount.set(0);
SingletonControllerAdvice.instanceCount.set(0);
PrototypeControllerAdvice.invocationCount.set(0);
PrototypeControllerAdvice.instanceCount.set(0);
RequestScopedControllerAdvice.invocationCount.set(0);
RequestScopedControllerAdvice.instanceCount.set(0);
}
@Configuration
@EnableWebMvc
static class Config {
@Bean
TestController testController() {
return new TestController();
}
@Bean
SingletonControllerAdvice singletonControllerAdvice() {
return new SingletonControllerAdvice();
}
@Bean
@Scope("prototype")
PrototypeControllerAdvice prototypeControllerAdvice() {
return new PrototypeControllerAdvice();
}
@Bean
@RequestScope
RequestScopedControllerAdvice requestScopedControllerAdvice() {
return new RequestScopedControllerAdvice();
}
}
@ControllerAdvice
static class SingletonControllerAdvice {
static final AtomicInteger instanceCount = new AtomicInteger();
static final AtomicInteger invocationCount = new AtomicInteger();
{
instanceCount.incrementAndGet();
}
@ModelAttribute
void initModel(Model model) {
model.addAttribute("singleton", invocationCount.incrementAndGet());
}
}
@ControllerAdvice
static class PrototypeControllerAdvice {
static final AtomicInteger instanceCount = new AtomicInteger();
static final AtomicInteger invocationCount = new AtomicInteger();
{
instanceCount.incrementAndGet();
}
@ModelAttribute
void initModel(Model model) {
model.addAttribute("prototype", invocationCount.incrementAndGet());
}
}
@ControllerAdvice
static class RequestScopedControllerAdvice {
static final AtomicInteger instanceCount = new AtomicInteger();
static final AtomicInteger invocationCount = new AtomicInteger();
{
instanceCount.incrementAndGet();
}
@ModelAttribute
void initModel(@RequestParam String requestParam, Model model) {
model.addAttribute("requestParam", requestParam);
model.addAttribute("request-scoped", invocationCount.incrementAndGet());
}
}
@Controller
static class TestController {
@GetMapping("/test")
String get(Model model) {
Map<String, Object> map = model.asMap();
return "singleton:" + map.get("singleton") +
";prototype:" + map.get("prototype") +
";request-scoped:" + map.get("request-scoped") +
";requestParam:" + map.get("requestParam");
}
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2019 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.
@@ -40,6 +40,7 @@ import static org.mockito.BDDMockito.*;
* Mock object based tests for TransactionInterceptor.
*
* @author Rod Johnson
* @author Juergen Hoeller
* @since 16.03.2003
*/
public class TransactionInterceptorTests extends AbstractTransactionAspectTests {
@@ -49,7 +50,7 @@ public class TransactionInterceptorTests extends AbstractTransactionAspectTests
@Override
protected Object advised(Object target, PlatformTransactionManager ptm, TransactionAttributeSource[] tas) throws Exception {
protected Object advised(Object target, PlatformTransactionManager ptm, TransactionAttributeSource[] tas) {
TransactionInterceptor ti = new TransactionInterceptor();
ti.setTransactionManager(ptm);
ti.setTransactionAttributeSources(tas);
+3 -3
View File
@@ -38,7 +38,7 @@ dependencies {
exclude group: "javax.servlet", module: "javax.servlet-api"
}
optional("org.eclipse.jetty:jetty-reactive-httpclient:1.0.3")
optional("com.squareup.okhttp3:okhttp:3.14.3")
optional("com.squareup.okhttp3:okhttp:3.14.6")
optional("org.apache.httpcomponents:httpclient:4.5.10") {
exclude group: "commons-logging", module: "commons-logging"
}
@@ -47,7 +47,7 @@ dependencies {
}
optional("commons-fileupload:commons-fileupload:1.4")
optional("org.synchronoss.cloud:nio-multipart-parser:1.1.0")
optional("com.fasterxml.woodstox:woodstox-core:5.2.0") { // woodstox before aalto
optional("com.fasterxml.woodstox:woodstox-core:5.3.0") { // woodstox before aalto
exclude group: "stax", module: "stax-api"
}
optional("com.fasterxml:aalto-xml:1.1.1")
@@ -75,7 +75,7 @@ dependencies {
testCompile("org.apache.tomcat.embed:tomcat-embed-core:${tomcatVersion}")
testCompile("org.eclipse.jetty:jetty-server")
testCompile("org.eclipse.jetty:jetty-servlet")
testCompile("com.squareup.okhttp3:mockwebserver:3.14.3")
testCompile("com.squareup.okhttp3:mockwebserver:3.14.6")
testCompile("org.jetbrains.kotlin:kotlin-reflect:${kotlinVersion}")
testCompile("org.skyscreamer:jsonassert:1.5.0")
testCompile("org.xmlunit:xmlunit-matchers:2.6.2")
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2020 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.
@@ -42,6 +42,10 @@ import static java.time.format.DateTimeFormatter.RFC_1123_DATE_TIME;
*/
public final class ContentDisposition {
private static final String INVALID_HEADER_FIELD_PARAMETER_FORMAT =
"Invalid header field parameter format (as defined in RFC 5987)";
@Nullable
private final String type;
@@ -201,11 +205,11 @@ public final class ContentDisposition {
if (this.filename != null) {
if (this.charset == null || StandardCharsets.US_ASCII.equals(this.charset)) {
sb.append("; filename=\"");
sb.append(this.filename).append('\"');
sb.append(escapeQuotationsInFilename(this.filename)).append('\"');
}
else {
sb.append("; filename*=");
sb.append(encodeHeaderFieldParam(this.filename, this.charset));
sb.append(encodeFilename(this.filename, this.charset));
}
}
if (this.size != null) {
@@ -271,15 +275,23 @@ public final class ContentDisposition {
String attribute = part.substring(0, eqIndex);
String value = (part.startsWith("\"", eqIndex + 1) && part.endsWith("\"") ?
part.substring(eqIndex + 2, part.length() - 1) :
part.substring(eqIndex + 1, part.length()));
part.substring(eqIndex + 1));
if (attribute.equals("name") ) {
name = value;
}
else if (attribute.equals("filename*") ) {
filename = decodeHeaderFieldParam(value);
charset = Charset.forName(value.substring(0, value.indexOf('\'')));
Assert.isTrue(UTF_8.equals(charset) || ISO_8859_1.equals(charset),
"Charset should be UTF-8 or ISO-8859-1");
int idx1 = value.indexOf('\'');
int idx2 = value.indexOf('\'', idx1 + 1);
if (idx1 != -1 && idx2 != -1) {
charset = Charset.forName(value.substring(0, idx1).trim());
Assert.isTrue(UTF_8.equals(charset) || ISO_8859_1.equals(charset),
"Charset should be UTF-8 or ISO-8859-1");
filename = decodeFilename(value.substring(idx2 + 1), charset);
}
else {
// US ASCII
filename = decodeFilename(value, StandardCharsets.US_ASCII);
}
}
else if (attribute.equals("filename") && (filename == null)) {
filename = value;
@@ -359,22 +371,15 @@ public final class ContentDisposition {
/**
* Decode the given header field param as describe in RFC 5987.
* <p>Only the US-ASCII, UTF-8 and ISO-8859-1 charsets are supported.
* @param input the header field param
* @param filename the header field param
* @param charset the charset to use
* @return the encoded header field param
* @see <a href="https://tools.ietf.org/html/rfc5987">RFC 5987</a>
*/
private static String decodeHeaderFieldParam(String input) {
Assert.notNull(input, "Input String should not be null");
int firstQuoteIndex = input.indexOf('\'');
int secondQuoteIndex = input.indexOf('\'', firstQuoteIndex + 1);
// US_ASCII
if (firstQuoteIndex == -1 || secondQuoteIndex == -1) {
return input;
}
Charset charset = Charset.forName(input.substring(0, firstQuoteIndex));
Assert.isTrue(UTF_8.equals(charset) || ISO_8859_1.equals(charset),
"Charset should be UTF-8 or ISO-8859-1");
byte[] value = input.substring(secondQuoteIndex + 1, input.length()).getBytes(charset);
private static String decodeFilename(String filename, Charset charset) {
Assert.notNull(filename, "'input' String` should not be null");
Assert.notNull(charset, "'charset' should not be null");
byte[] value = filename.getBytes(charset);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
int index = 0;
while (index < value.length) {
@@ -383,13 +388,18 @@ public final class ContentDisposition {
bos.write((char) b);
index++;
}
else if (b == '%') {
char[] array = { (char)value[index + 1], (char)value[index + 2]};
bos.write(Integer.parseInt(String.valueOf(array), 16));
else if (b == '%' && index < value.length - 2) {
char[] array = new char[]{(char) value[index + 1], (char) value[index + 2]};
try {
bos.write(Integer.parseInt(String.valueOf(array), 16));
}
catch (NumberFormatException ex) {
throw new IllegalArgumentException(INVALID_HEADER_FIELD_PARAMETER_FORMAT, ex);
}
index+=3;
}
else {
throw new IllegalArgumentException("Invalid header field parameter format (as defined in RFC 5987)");
throw new IllegalArgumentException(INVALID_HEADER_FIELD_PARAMETER_FORMAT);
}
}
return new String(bos.toByteArray(), charset);
@@ -401,6 +411,23 @@ public final class ContentDisposition {
c == '.' || c == '^' || c == '_' || c == '`' || c == '|' || c == '~';
}
private static String escapeQuotationsInFilename(String filename) {
if (filename.indexOf('"') == -1 && filename.indexOf('\\') == -1) {
return filename;
}
boolean escaped = false;
StringBuilder sb = new StringBuilder();
for (char c : filename.toCharArray()) {
sb.append((c == '"' && !escaped) ? "\\\"" : c);
escaped = (!escaped && c == '\\');
}
// Remove backslash at the end..
if (escaped) {
sb.deleteCharAt(sb.length() - 1);
}
return sb.toString();
}
/**
* Encode the given header field param as describe in RFC 5987.
* @param input the header field param
@@ -409,14 +436,11 @@ public final class ContentDisposition {
* @return the encoded header field param
* @see <a href="https://tools.ietf.org/html/rfc5987">RFC 5987</a>
*/
private static String encodeHeaderFieldParam(String input, Charset charset) {
Assert.notNull(input, "Input String should not be null");
Assert.notNull(charset, "Charset should not be null");
if (StandardCharsets.US_ASCII.equals(charset)) {
return input;
}
Assert.isTrue(UTF_8.equals(charset) || ISO_8859_1.equals(charset),
"Charset should be UTF-8 or ISO-8859-1");
private static String encodeFilename(String input, Charset charset) {
Assert.notNull(input, "`input` is required");
Assert.notNull(charset, "`charset` is required");
Assert.isTrue(!StandardCharsets.US_ASCII.equals(charset), "ASCII does not require encoding");
Assert.isTrue(UTF_8.equals(charset) || ISO_8859_1.equals(charset), "Only UTF-8 and ISO-8859-1 supported.");
byte[] source = input.getBytes(charset);
int len = source.length;
StringBuilder sb = new StringBuilder(len << 1);
@@ -449,7 +473,11 @@ public final class ContentDisposition {
Builder name(String name);
/**
* Set the value of the {@literal filename} parameter.
* Set the value of the {@literal filename} parameter. The given
* filename will be formatted as quoted-string, as defined in RFC 2616,
* section 2.2, and any quote characters within the filename value will
* be escaped with a backslash, e.g. {@code "foo\"bar.txt"} becomes
* {@code "foo\\\"bar.txt"}.
*/
Builder filename(String filename);
@@ -530,12 +558,14 @@ public final class ContentDisposition {
@Override
public Builder filename(String filename) {
Assert.hasText(filename, "No filename");
this.filename = filename;
return this;
}
@Override
public Builder filename(String filename, Charset charset) {
Assert.hasText(filename, "No filename");
this.filename = filename;
this.charset = charset;
return this;
@@ -374,7 +374,7 @@ public final class ResponseCookie extends HttpCookie {
}
int char1 = domain.charAt(0);
int charN = domain.charAt(domain.length() - 1);
if (char1 == '.' || char1 == '-' || charN == '.' || charN == '-') {
if (char1 == '-' || charN == '.' || charN == '-') {
throw new IllegalArgumentException("Invalid first/last char in cookie domain: " + domain);
}
for (int i = 0, c = -1; i < domain.length(); i++) {
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2019 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.
@@ -63,6 +63,12 @@ public interface ClientCodecConfigurer extends CodecConfigurer {
@Override
ClientDefaultCodecs defaultCodecs();
/**
* {@inheritDoc}.
*/
@Override
ClientCodecConfigurer clone();
/**
* Static factory method for a {@code ClientCodecConfigurer}.
@@ -89,6 +95,8 @@ public interface ClientCodecConfigurer extends CodecConfigurer {
* <p>By default if this is not set, and Jackson is available, the
* {@link #jackson2JsonDecoder} override is used instead. Use this property
* if you want to further customize the SSE decoder.
* <p>Note that {@link #maxInMemorySize(int)}, if configured, will be
* applied to the given decoder.
* @param decoder the decoder to use
*/
void serverSentEventDecoder(Decoder<?> decoder);
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2019 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.http.codec;
import java.util.List;
import java.util.function.Consumer;
import org.springframework.core.codec.Decoder;
import org.springframework.core.codec.Encoder;
import org.springframework.lang.Nullable;
/**
* Defines a common interface for configuring either client or server HTTP
@@ -87,6 +89,15 @@ public interface CodecConfigurer {
*/
List<HttpMessageWriter<?>> getWriters();
/**
* Create a copy of this {@link CodecConfigurer}. The returned clone has its
* own lists of default and custom codecs and generally can be configured
* independently. Keep in mind however that codec instances (if any are
* configured) are themselves not cloned.
* @since 5.1.12
*/
CodecConfigurer clone();
/**
* Customize or replace the HTTP message readers and writers registered by
@@ -98,6 +109,8 @@ public interface CodecConfigurer {
/**
* Override the default Jackson JSON {@code Decoder}.
* <p>Note that {@link #maxInMemorySize(int)}, if configured, will be
* applied to the given decoder.
* @param decoder the decoder instance to use
* @see org.springframework.http.codec.json.Jackson2JsonDecoder
*/
@@ -112,6 +125,8 @@ public interface CodecConfigurer {
/**
* Override the default Protobuf {@code Decoder}.
* <p>Note that {@link #maxInMemorySize(int)}, if configured, will be
* applied to the given decoder.
* @param decoder the decoder instance to use
* @since 5.1
* @see org.springframework.http.codec.protobuf.ProtobufDecoder
@@ -129,6 +144,8 @@ public interface CodecConfigurer {
/**
* Override the default JAXB2 {@code Decoder}.
* <p>Note that {@link #maxInMemorySize(int)}, if configured, will be
* applied to the given decoder.
* @param decoder the decoder instance to use
* @since 5.1.3
* @see org.springframework.http.codec.xml.Jaxb2XmlDecoder
@@ -143,6 +160,22 @@ public interface CodecConfigurer {
*/
void jaxb2Encoder(Encoder<?> encoder);
/**
* Configure a limit on the number of bytes that can be buffered whenever
* the input stream needs to be aggregated. This can be a result of
* decoding to a single {@code DataBuffer},
* {@link java.nio.ByteBuffer ByteBuffer}, {@code byte[]},
* {@link org.springframework.core.io.Resource Resource}, {@code String}, etc.
* It can also occur when splitting the input stream, e.g. delimited text,
* in which case the limit applies to data buffered between delimiters.
* <p>By default this is not set, in which case individual codec defaults
* apply. In 5.1 most codecs are not limited except {@code FormHttpMessageReader}
* which is limited to 256K. In 5.2 all codecs are limited to 256K by default.
* @param byteCount the max number of bytes to buffer, or -1 for unlimited
* @since 5.1.11
*/
void maxInMemorySize(int byteCount);
/**
* Whether to log form data at DEBUG level, and headers at TRACE level.
* Both may contain sensitive information.
@@ -159,18 +192,66 @@ public interface CodecConfigurer {
*/
interface CustomCodecs {
/**
* Register a custom codec. This is expected to be one of the following:
* <ul>
* <li>{@link HttpMessageReader}
* <li>{@link HttpMessageWriter}
* <li>{@link Encoder} (wrapped internally with {@link EncoderHttpMessageWriter})
* <li>{@link Decoder} (wrapped internally with {@link DecoderHttpMessageReader})
* </ul>
* @param codec the codec to register
* @since 5.1.13
*/
void register(Object codec);
/**
* Variant of {@link #register(Object)} that also applies the below
* properties, if configured, via {@link #defaultCodecs()}:
* <ul>
* <li>{@link CodecConfigurer.DefaultCodecs#maxInMemorySize(int) maxInMemorySize}
* <li>{@link CodecConfigurer.DefaultCodecs#enableLoggingRequestDetails(boolean) enableLoggingRequestDetails}
* </ul>
* <p>The properties are applied every time {@link #getReaders()} or
* {@link #getWriters()} are used to obtain the list of configured
* readers or writers.
* @param codec the codec to register and apply default config to
* @since 5.1.13
*/
void registerWithDefaultConfig(Object codec);
/**
* Variant of {@link #register(Object)} that also allows the caller to
* apply the properties from {@link DefaultCodecConfig} to the given
* codec. If you want to apply all the properties, prefer using
* {@link #registerWithDefaultConfig(Object)}.
* <p>The consumer is called every time {@link #getReaders()} or
* {@link #getWriters()} are used to obtain the list of configured
* readers or writers.
* @param codec the codec to register
* @param configConsumer consumer of the default config
* @since 5.1.13
*/
void registerWithDefaultConfig(Object codec, Consumer<DefaultCodecConfig> configConsumer);
/**
* Add a custom {@code Decoder} internally wrapped with
* {@link DecoderHttpMessageReader}).
* @param decoder the decoder to add
* @deprecated as of 5.1.13, use {@link #register(Object)} or
* {@link #registerWithDefaultConfig(Object)} instead.
*/
@Deprecated
void decoder(Decoder<?> decoder);
/**
* Add a custom {@code Encoder}, internally wrapped with
* {@link EncoderHttpMessageWriter}.
* @param encoder the encoder to add
* @deprecated as of 5.1.13, use {@link #register(Object)} or
* {@link #registerWithDefaultConfig(Object)} instead.
*/
@Deprecated
void encoder(Encoder<?> encoder);
/**
@@ -178,7 +259,10 @@ public interface CodecConfigurer {
* {@link DecoderHttpMessageReader} consider using the shortcut
* {@link #decoder(Decoder)} instead.
* @param reader the reader to add
* @deprecated as of 5.1.13, use {@link #register(Object)} or
* {@link #registerWithDefaultConfig(Object)} instead.
*/
@Deprecated
void reader(HttpMessageReader<?> reader);
/**
@@ -186,8 +270,49 @@ public interface CodecConfigurer {
* {@link EncoderHttpMessageWriter} consider using the shortcut
* {@link #encoder(Encoder)} instead.
* @param writer the writer to add
* @deprecated as of 5.1.13, use {@link #register(Object)} or
* {@link #registerWithDefaultConfig(Object)} instead.
*/
@Deprecated
void writer(HttpMessageWriter<?> writer);
/**
* Register a callback for the {@link DefaultCodecConfig configuration}
* applied to default codecs. This allows custom codecs to follow general
* guidelines applied to default ones, such as logging details and limiting
* the amount of buffered data.
* @param codecsConfigConsumer the default codecs configuration callback
* @deprecated as of 5.1.13, use {@link #registerWithDefaultConfig(Object)}
* or {@link #registerWithDefaultConfig(Object, Consumer)} instead.
*/
@Deprecated
void withDefaultCodecConfig(Consumer<DefaultCodecConfig> codecsConfigConsumer);
}
/**
* Exposes the values of properties configured through
* {@link #defaultCodecs()} that are applied to default codecs.
* The main purpose of this interface is to provide access to them so they
* can also be applied to custom codecs if needed.
* @since 5.1.12
* @see CustomCodecs#registerWithDefaultConfig(Object, Consumer)
*/
interface DefaultCodecConfig {
/**
* Get the configured limit on the number of bytes that can be buffered whenever
* the input stream needs to be aggregated.
*/
@Nullable
Integer maxInMemorySize();
/**
* Whether to log form data at DEBUG level, and headers at TRACE level.
* Both may contain sensitive information.
*/
@Nullable
Boolean isEnableLoggingRequestDetails();
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2019 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.
@@ -30,6 +30,7 @@ import reactor.core.publisher.Mono;
import org.springframework.core.ResolvableType;
import org.springframework.core.codec.Hints;
import org.springframework.core.io.buffer.DataBufferLimitException;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.core.log.LogFormatUtils;
import org.springframework.http.MediaType;
@@ -62,6 +63,8 @@ public class FormHttpMessageReader extends LoggingCodecSupport
private Charset defaultCharset = DEFAULT_CHARSET;
private int maxInMemorySize = 256 * 1024;
/**
* Set the default character set to use for reading form data when the
@@ -80,6 +83,26 @@ public class FormHttpMessageReader extends LoggingCodecSupport
return this.defaultCharset;
}
/**
* Set the max number of bytes for input form data. As form data is buffered
* before it is parsed, this helps to limit the amount of buffering. Once
* the limit is exceeded, {@link DataBufferLimitException} is raised.
* <p>By default this is set to 256K.
* @param byteCount the max number of bytes to buffer, or -1 for unlimited
* @since 5.1.11
*/
public void setMaxInMemorySize(int byteCount) {
this.maxInMemorySize = byteCount;
}
/**
* Return the {@link #setMaxInMemorySize configured} byte count limit.
* @since 5.1.11
*/
public int getMaxInMemorySize() {
return this.maxInMemorySize;
}
@Override
public boolean canRead(ResolvableType elementType, @Nullable MediaType mediaType) {
@@ -105,7 +128,7 @@ public class FormHttpMessageReader extends LoggingCodecSupport
MediaType contentType = message.getHeaders().getContentType();
Charset charset = getMediaTypeCharset(contentType);
return DataBufferUtils.join(message.getBody())
return DataBufferUtils.join(message.getBody(), getMaxInMemorySize())
.map(buffer -> {
CharBuffer charBuffer = charset.decode(buffer.asByteBuffer());
String body = charBuffer.toString();
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2019 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,12 @@ public interface ServerCodecConfigurer extends CodecConfigurer {
@Override
ServerDefaultCodecs defaultCodecs();
/**
* {@inheritDoc}.
*/
@Override
ServerCodecConfigurer clone();
/**
* Static factory method for a {@code ServerCodecConfigurer}.
@@ -76,6 +82,23 @@ public interface ServerCodecConfigurer extends CodecConfigurer {
*/
interface ServerDefaultCodecs extends DefaultCodecs {
/**
* Configure the {@code HttpMessageReader} to use for multipart requests.
* <p>By default, if
* <a href="https://github.com/synchronoss/nio-multipart">Synchronoss NIO Multipart</a>
* is present, this is set to
* {@link org.springframework.http.codec.multipart.MultipartHttpMessageReader
* MultipartHttpMessageReader} created with an instance of
* {@link org.springframework.http.codec.multipart.SynchronossPartHttpMessageReader
* SynchronossPartHttpMessageReader}.
* <p>Note that {@link #maxInMemorySize(int)} and/or
* {@link #enableLoggingRequestDetails(boolean)}, if configured, will be
* applied to the given reader, if applicable.
* @param reader the message reader to use for multipart requests.
* @since 5.1.11
*/
void multipartReader(HttpMessageReader<?> reader);
/**
* Configure the {@code Encoder} to use for Server-Sent Events.
* <p>By default if this is not set, and Jackson is available, the
@@ -38,6 +38,7 @@ import org.springframework.core.codec.CodecException;
import org.springframework.core.codec.DecodingException;
import org.springframework.core.codec.Hints;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferLimitException;
import org.springframework.core.log.LogFormatUtils;
import org.springframework.http.codec.HttpMessageDecoder;
import org.springframework.http.server.reactive.ServerHttpRequest;
@@ -57,6 +58,9 @@ import org.springframework.util.MimeType;
*/
public abstract class AbstractJackson2Decoder extends Jackson2CodecSupport implements HttpMessageDecoder<Object> {
private int maxInMemorySize = -1;
/**
* Until https://github.com/FasterXML/jackson-core/issues/476 is resolved,
* we need to ensure buffer recycling is off.
@@ -74,6 +78,29 @@ public abstract class AbstractJackson2Decoder extends Jackson2CodecSupport imple
}
/**
* Set the max number of bytes that can be buffered by this decoder. This
* is either the size of the entire input when decoding as a whole, or the
* size of one top-level JSON object within a JSON stream. When the limit
* is exceeded, {@link DataBufferLimitException} is raised.
* <p>By default in 5.1 this is set to -1, unlimited. In 5.2 the default
* value for this limit is set to 256K.
* @param byteCount the max number of bytes to buffer, or -1 for unlimited
* @since 5.1.11
*/
public void setMaxInMemorySize(int byteCount) {
this.maxInMemorySize = byteCount;
}
/**
* Return the {@link #setMaxInMemorySize configured} byte count limit.
* @since 5.1.11
*/
public int getMaxInMemorySize() {
return this.maxInMemorySize;
}
@Override
public boolean canDecode(ResolvableType elementType, @Nullable MimeType mimeType) {
JavaType javaType = getObjectMapper().getTypeFactory().constructType(elementType.getType());
@@ -87,7 +114,7 @@ public abstract class AbstractJackson2Decoder extends Jackson2CodecSupport imple
@Nullable MimeType mimeType, @Nullable Map<String, Object> hints) {
Flux<TokenBuffer> tokens = Jackson2Tokenizer.tokenize(
Flux.from(input), this.jsonFactory, getObjectMapper(), true);
Flux.from(input), this.jsonFactory, getObjectMapper(), true, getMaxInMemorySize());
return decodeInternal(tokens, elementType, mimeType, hints);
}
@@ -96,7 +123,7 @@ public abstract class AbstractJackson2Decoder extends Jackson2CodecSupport imple
@Nullable MimeType mimeType, @Nullable Map<String, Object> hints) {
Flux<TokenBuffer> tokens = Jackson2Tokenizer.tokenize(
Flux.from(input), this.jsonFactory, getObjectMapper(), false);
Flux.from(input), this.jsonFactory, getObjectMapper(), false, getMaxInMemorySize());
return decodeInternal(tokens, elementType, mimeType, hints).singleOrEmpty();
}
@@ -34,6 +34,7 @@ import reactor.core.publisher.Flux;
import org.springframework.core.codec.DecodingException;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferLimitException;
import org.springframework.core.io.buffer.DataBufferUtils;
/**
@@ -60,30 +61,39 @@ final class Jackson2Tokenizer {
private int arrayDepth;
private final int maxInMemorySize;
private int byteCount;
// TODO: change to ByteBufferFeeder when supported by Jackson
// See https://github.com/FasterXML/jackson-core/issues/478
private final ByteArrayFeeder inputFeeder;
private Jackson2Tokenizer(
JsonParser parser, DeserializationContext deserializationContext, boolean tokenizeArrayElements) {
private Jackson2Tokenizer(JsonParser parser, DeserializationContext deserializationContext,
boolean tokenizeArrayElements, int maxInMemorySize) {
this.parser = parser;
this.deserializationContext = deserializationContext;
this.tokenizeArrayElements = tokenizeArrayElements;
this.tokenBuffer = new TokenBuffer(parser, deserializationContext);
this.inputFeeder = (ByteArrayFeeder) this.parser.getNonBlockingInputFeeder();
this.maxInMemorySize = maxInMemorySize;
}
private Flux<TokenBuffer> tokenize(DataBuffer dataBuffer) {
int bufferSize = dataBuffer.readableByteCount();
byte[] bytes = new byte[dataBuffer.readableByteCount()];
dataBuffer.read(bytes);
DataBufferUtils.release(dataBuffer);
try {
this.inputFeeder.feedInput(bytes, 0, bytes.length);
return parseTokenBufferFlux();
List<TokenBuffer> result = parseTokenBufferFlux();
assertInMemorySize(bufferSize, result);
return Flux.fromIterable(result);
}
catch (JsonProcessingException ex) {
return Flux.error(new DecodingException("JSON decoding error: " + ex.getOriginalMessage(), ex));
@@ -96,7 +106,8 @@ final class Jackson2Tokenizer {
private Flux<TokenBuffer> endOfInput() {
this.inputFeeder.endOfInput();
try {
return parseTokenBufferFlux();
List<TokenBuffer> result = parseTokenBufferFlux();
return Flux.fromIterable(result);
}
catch (JsonProcessingException ex) {
return Flux.error(new DecodingException("JSON decoding error: " + ex.getOriginalMessage(), ex));
@@ -106,16 +117,21 @@ final class Jackson2Tokenizer {
}
}
private Flux<TokenBuffer> parseTokenBufferFlux() throws IOException {
private List<TokenBuffer> parseTokenBufferFlux() throws IOException {
List<TokenBuffer> result = new ArrayList<>();
while (true) {
// SPR-16151: Smile data format uses null to separate documents
boolean previousNull = false;
while (!this.parser.isClosed()) {
JsonToken token = this.parser.nextToken();
// SPR-16151: Smile data format uses null to separate documents
if (token == JsonToken.NOT_AVAILABLE ||
(token == null && (token = this.parser.nextToken()) == null)) {
token == null && previousNull) {
break;
}
else if (token == null ) { // !previousNull
previousNull = true;
continue;
}
updateDepth(token);
if (!this.tokenizeArrayElements) {
processTokenNormal(token, result);
@@ -124,7 +140,7 @@ final class Jackson2Tokenizer {
processTokenArray(token, result);
}
}
return Flux.fromIterable(result);
return result;
}
private void updateDepth(JsonToken token) {
@@ -171,18 +187,40 @@ final class Jackson2Tokenizer {
(token == JsonToken.END_ARRAY && this.arrayDepth == 0));
}
private void assertInMemorySize(int currentBufferSize, List<TokenBuffer> result) {
if (this.maxInMemorySize >= 0) {
if (!result.isEmpty()) {
this.byteCount = 0;
}
else if (currentBufferSize > Integer.MAX_VALUE - this.byteCount) {
raiseLimitException();
}
else {
this.byteCount += currentBufferSize;
if (this.byteCount > this.maxInMemorySize) {
raiseLimitException();
}
}
}
}
private void raiseLimitException() {
throw new DataBufferLimitException(
"Exceeded limit on max bytes per JSON object: " + this.maxInMemorySize);
}
/**
* Tokenize the given {@code Flux<DataBuffer>} into {@code Flux<TokenBuffer>}.
* @param dataBuffers the source data buffers
* @param jsonFactory the factory to use
* @param objectMapper the current mapper instance
* @param tokenizeArrayElements if {@code true} and the "top level" JSON object is
* @param tokenizeArrays if {@code true} and the "top level" JSON object is
* an array, each element is returned individually immediately after it is received
* @return the resulting token buffers
*/
public static Flux<TokenBuffer> tokenize(Flux<DataBuffer> dataBuffers, JsonFactory jsonFactory,
ObjectMapper objectMapper, boolean tokenizeArrayElements) {
ObjectMapper objectMapper, boolean tokenizeArrays, int maxInMemorySize) {
try {
JsonParser parser = jsonFactory.createNonBlockingByteArrayParser();
@@ -191,7 +229,7 @@ final class Jackson2Tokenizer {
context = ((DefaultDeserializationContext) context).createInstance(
objectMapper.getDeserializationConfig(), parser, objectMapper.getInjectableValues());
}
Jackson2Tokenizer tokenizer = new Jackson2Tokenizer(parser, context, tokenizeArrayElements);
Jackson2Tokenizer tokenizer = new Jackson2Tokenizer(parser, context, tokenizeArrays, maxInMemorySize);
return dataBuffers.flatMap(tokenizer::tokenize, Flux::error, tokenizer::endOfInput);
}
catch (IOException ex) {
@@ -65,6 +65,14 @@ public class MultipartHttpMessageReader extends LoggingCodecSupport
}
/**
* Return the configured parts reader.
* @since 5.1.11
*/
public HttpMessageReader<Part> getPartReader() {
return this.partReader;
}
@Override
public List<MediaType> getReadableMediaTypes() {
return Collections.singletonList(MediaType.MULTIPART_FORM_DATA);
@@ -149,6 +149,16 @@ public class MultipartHttpMessageWriter extends LoggingCodecSupport
return Collections.unmodifiableList(this.partWriters);
}
/**
* Return the configured form writer.
* @since 5.1.13
*/
@Nullable
public HttpMessageWriter<MultiValueMap<String, String>> getFormWriter() {
return this.formWriter;
}
/**
* Set the character set to use for part headers such as
* "Content-Disposition" (and its filename parameter).
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2019 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.
@@ -40,14 +40,18 @@ import org.synchronoss.cloud.nio.multipart.NioMultipartParser;
import org.synchronoss.cloud.nio.multipart.NioMultipartParserListener;
import org.synchronoss.cloud.nio.multipart.PartBodyStreamStorageFactory;
import org.synchronoss.cloud.nio.stream.storage.StreamStorage;
import reactor.core.publisher.BaseSubscriber;
import reactor.core.publisher.Flux;
import reactor.core.publisher.FluxSink;
import reactor.core.publisher.Mono;
import reactor.core.publisher.SignalType;
import org.springframework.core.ResolvableType;
import org.springframework.core.codec.DecodingException;
import org.springframework.core.codec.Hints;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.core.io.buffer.DataBufferLimitException;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
import org.springframework.core.log.LogFormatUtils;
@@ -69,15 +73,83 @@ import org.springframework.util.Assert;
* @author Sebastien Deleuze
* @author Rossen Stoyanchev
* @author Arjen Poutsma
* @author Brian Clozel
* @since 5.0
* @see <a href="https://github.com/synchronoss/nio-multipart">Synchronoss NIO Multipart</a>
* @see MultipartHttpMessageReader
*/
public class SynchronossPartHttpMessageReader extends LoggingCodecSupport implements HttpMessageReader<Part> {
private final DataBufferFactory bufferFactory = new DefaultDataBufferFactory();
// Static DataBufferFactory to copy from FileInputStream or wrap bytes[].
private static final DataBufferFactory bufferFactory = new DefaultDataBufferFactory();
private final PartBodyStreamStorageFactory streamStorageFactory = new DefaultPartBodyStreamStorageFactory();
private int maxInMemorySize = -1;
private long maxDiskUsagePerPart = -1;
private int maxParts = -1;
/**
* Configure the maximum amount of memory that is allowed to use per part.
* When the limit is exceeded:
* <ul>
* <li>file parts are written to a temporary file.
* <li>non-file parts are rejected with {@link DataBufferLimitException}.
* </ul>
* <p>By default in 5.1 this is set to -1 in which case this limit is
* not enforced and all parts may be written to disk and are limited only
* by the {@link #setMaxDiskUsagePerPart(long) maxDiskUsagePerPart} property.
* In 5.2 this default value for this limit is set to 256K.
* @param byteCount the in-memory limit in bytes, or -1 for unlimited
* @since 5.1.11
*/
public void setMaxInMemorySize(int byteCount) {
this.maxInMemorySize = byteCount;
}
/**
* Get the {@link #setMaxInMemorySize configured} maximum in-memory size.
* @since 5.1.11
*/
public int getMaxInMemorySize() {
return this.maxInMemorySize;
}
/**
* Configure the maximum amount of disk space allowed for file parts.
* <p>By default this is set to -1.
* @param maxDiskUsagePerPart the disk limit in bytes, or -1 for unlimited
* @since 5.1.11
*/
public void setMaxDiskUsagePerPart(long maxDiskUsagePerPart) {
this.maxDiskUsagePerPart = maxDiskUsagePerPart;
}
/**
* Get the {@link #setMaxDiskUsagePerPart configured} maximum disk usage.
* @since 5.1.11
*/
public long getMaxDiskUsagePerPart() {
return this.maxDiskUsagePerPart;
}
/**
* Specify the maximum number of parts allowed in a given multipart request.
* @since 5.1.11
*/
public void setMaxParts(int maxParts) {
this.maxParts = maxParts;
}
/**
* Return the {@link #setMaxParts configured} limit on the number of parts.
* @since 5.1.11
*/
public int getMaxParts() {
return this.maxParts;
}
@Override
@@ -91,10 +163,9 @@ public class SynchronossPartHttpMessageReader extends LoggingCodecSupport implem
(mediaType == null || MediaType.MULTIPART_FORM_DATA.isCompatibleWith(mediaType));
}
@Override
public Flux<Part> read(ResolvableType elementType, ReactiveHttpInputMessage message, Map<String, Object> hints) {
return Flux.create(new SynchronossPartGenerator(message, this.bufferFactory, this.streamStorageFactory))
return Flux.create(new SynchronossPartGenerator(message))
.doOnNext(part -> {
if (!Hints.isLoggingSuppressed(hints)) {
LogFormatUtils.traceDebug(logger, traceOn -> Hints.getLogPrefix(hints) + "Parsed " +
@@ -105,7 +176,6 @@ public class SynchronossPartHttpMessageReader extends LoggingCodecSupport implem
});
}
@Override
public Mono<Part> readMono(ResolvableType elementType, ReactiveHttpInputMessage message, Map<String, Object> hints) {
return Mono.error(new UnsupportedOperationException("Cannot read multipart request body into single Part"));
@@ -113,27 +183,27 @@ public class SynchronossPartHttpMessageReader extends LoggingCodecSupport implem
/**
* Consume and feed input to the Synchronoss parser, then listen for parser
* output events and adapt to {@code Flux<Sink<Part>>}.
* Subscribe to the input stream and feed the Synchronoss parser. Then listen
* for parser output, creating parts, and pushing them into the FluxSink.
*/
private static class SynchronossPartGenerator implements Consumer<FluxSink<Part>> {
private class SynchronossPartGenerator extends BaseSubscriber<DataBuffer> implements Consumer<FluxSink<Part>> {
private final ReactiveHttpInputMessage inputMessage;
private final DataBufferFactory bufferFactory;
private final LimitedPartBodyStreamStorageFactory storageFactory = new LimitedPartBodyStreamStorageFactory();
private final PartBodyStreamStorageFactory streamStorageFactory;
@Nullable
private NioMultipartParserListener listener;
SynchronossPartGenerator(ReactiveHttpInputMessage inputMessage, DataBufferFactory bufferFactory,
PartBodyStreamStorageFactory streamStorageFactory) {
@Nullable
private NioMultipartParser parser;
public SynchronossPartGenerator(ReactiveHttpInputMessage inputMessage) {
this.inputMessage = inputMessage;
this.bufferFactory = bufferFactory;
this.streamStorageFactory = streamStorageFactory;
}
@Override
public void accept(FluxSink<Part> emitter) {
public void accept(FluxSink<Part> sink) {
HttpHeaders headers = this.inputMessage.getHeaders();
MediaType mediaType = headers.getContentType();
Assert.state(mediaType != null, "No content type set");
@@ -142,40 +212,63 @@ public class SynchronossPartHttpMessageReader extends LoggingCodecSupport implem
Charset charset = Optional.ofNullable(mediaType.getCharset()).orElse(StandardCharsets.UTF_8);
MultipartContext context = new MultipartContext(mediaType.toString(), length, charset.name());
NioMultipartParserListener listener = new FluxSinkAdapterListener(emitter, this.bufferFactory, context);
NioMultipartParser parser = Multipart
.multipart(context)
.usePartBodyStreamStorageFactory(this.streamStorageFactory)
.forNIO(listener);
this.listener = new FluxSinkAdapterListener(sink, context, this.storageFactory);
this.inputMessage.getBody().subscribe(buffer -> {
byte[] resultBytes = new byte[buffer.readableByteCount()];
buffer.read(resultBytes);
try {
parser.write(resultBytes);
this.parser = Multipart
.multipart(context)
.usePartBodyStreamStorageFactory(this.storageFactory)
.forNIO(this.listener);
this.inputMessage.getBody().subscribe(this);
}
@Override
protected void hookOnNext(DataBuffer buffer) {
Assert.state(this.parser != null && this.listener != null, "Not initialized yet");
int size = buffer.readableByteCount();
this.storageFactory.increaseByteCount(size);
byte[] resultBytes = new byte[size];
buffer.read(resultBytes);
try {
this.parser.write(resultBytes);
}
catch (IOException ex) {
cancel();
int index = this.storageFactory.getCurrentPartIndex();
this.listener.onError("Parser error for part [" + index + "]", ex);
}
finally {
DataBufferUtils.release(buffer);
}
}
@Override
protected void hookOnError(Throwable ex) {
if (this.listener != null) {
int index = this.storageFactory.getCurrentPartIndex();
this.listener.onError("Failure while parsing part[" + index + "]", ex);
}
}
@Override
protected void hookOnComplete() {
if (this.listener != null) {
this.listener.onAllPartsFinished();
}
}
@Override
protected void hookFinally(SignalType type) {
try {
if (this.parser != null) {
this.parser.close();
}
catch (IOException ex) {
listener.onError("Exception thrown providing input to the parser", ex);
}
finally {
DataBufferUtils.release(buffer);
}
}, ex -> {
try {
listener.onError("Request body input error", ex);
parser.close();
}
catch (IOException ex2) {
listener.onError("Exception thrown while closing the parser", ex2);
}
}, () -> {
try {
parser.close();
}
catch (IOException ex) {
listener.onError("Exception thrown while closing the parser", ex);
}
});
}
catch (IOException ex) {
// ignore
}
}
private int getContentLength(HttpHeaders headers) {
@@ -186,6 +279,53 @@ public class SynchronossPartHttpMessageReader extends LoggingCodecSupport implem
}
private class LimitedPartBodyStreamStorageFactory implements PartBodyStreamStorageFactory {
private final PartBodyStreamStorageFactory storageFactory = (maxInMemorySize > 0 ?
new DefaultPartBodyStreamStorageFactory(maxInMemorySize) :
new DefaultPartBodyStreamStorageFactory());
private int index = 1;
private boolean isFilePart;
private long partSize;
public int getCurrentPartIndex() {
return this.index;
}
@Override
public StreamStorage newStreamStorageForPartBody(Map<String, List<String>> headers, int index) {
this.index = index;
this.isFilePart = (MultipartUtils.getFileName(headers) != null);
this.partSize = 0;
if (maxParts > 0 && index > maxParts) {
throw new DecodingException("Too many parts (" + index + " allowed)");
}
return this.storageFactory.newStreamStorageForPartBody(headers, index);
}
public void increaseByteCount(long byteCount) {
this.partSize += byteCount;
if (maxInMemorySize > 0 && !this.isFilePart && this.partSize >= maxInMemorySize) {
throw new DataBufferLimitException("Part[" + this.index + "] " +
"exceeded the in-memory limit of " + maxInMemorySize + " bytes");
}
if (maxDiskUsagePerPart > 0 && this.isFilePart && this.partSize > maxDiskUsagePerPart) {
throw new DecodingException("Part[" + this.index + "] " +
"exceeded the disk usage limit of " + maxDiskUsagePerPart + " bytes");
}
}
public void partFinished() {
this.index++;
this.isFilePart = false;
this.partSize = 0;
}
}
/**
* Listen for parser output and adapt to {@code Flux<Sink<Part>>}.
*/
@@ -193,43 +333,46 @@ public class SynchronossPartHttpMessageReader extends LoggingCodecSupport implem
private final FluxSink<Part> sink;
private final DataBufferFactory bufferFactory;
private final MultipartContext context;
private final LimitedPartBodyStreamStorageFactory storageFactory;
private final AtomicInteger terminated = new AtomicInteger(0);
FluxSinkAdapterListener(FluxSink<Part> sink, DataBufferFactory factory, MultipartContext context) {
FluxSinkAdapterListener(
FluxSink<Part> sink, MultipartContext context, LimitedPartBodyStreamStorageFactory factory) {
this.sink = sink;
this.bufferFactory = factory;
this.context = context;
this.storageFactory = factory;
}
@Override
public void onPartFinished(StreamStorage storage, Map<String, List<String>> headers) {
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.putAll(headers);
this.storageFactory.partFinished();
this.sink.next(createPart(storage, httpHeaders));
}
private Part createPart(StreamStorage storage, HttpHeaders httpHeaders) {
String filename = MultipartUtils.getFileName(httpHeaders);
if (filename != null) {
return new SynchronossFilePart(httpHeaders, filename, storage, this.bufferFactory);
return new SynchronossFilePart(httpHeaders, filename, storage);
}
else if (MultipartUtils.isFormField(httpHeaders, this.context)) {
String value = MultipartUtils.readFormParameterValue(storage, httpHeaders);
return new SynchronossFormFieldPart(httpHeaders, this.bufferFactory, value);
return new SynchronossFormFieldPart(httpHeaders, value);
}
else {
return new SynchronossPart(httpHeaders, storage, this.bufferFactory);
return new SynchronossPart(httpHeaders, storage);
}
}
@Override
public void onError(String message, Throwable cause) {
if (this.terminated.getAndIncrement() == 0) {
this.sink.error(new RuntimeException(message, cause));
this.sink.error(new DecodingException(message, cause));
}
}
@@ -256,14 +399,10 @@ public class SynchronossPartHttpMessageReader extends LoggingCodecSupport implem
private final HttpHeaders headers;
private final DataBufferFactory bufferFactory;
AbstractSynchronossPart(HttpHeaders headers, DataBufferFactory bufferFactory) {
AbstractSynchronossPart(HttpHeaders headers) {
Assert.notNull(headers, "HttpHeaders is required");
Assert.notNull(bufferFactory, "DataBufferFactory is required");
this.name = MultipartUtils.getFieldName(headers);
this.headers = headers;
this.bufferFactory = bufferFactory;
}
@Override
@@ -276,10 +415,6 @@ public class SynchronossPartHttpMessageReader extends LoggingCodecSupport implem
return this.headers;
}
DataBufferFactory getBufferFactory() {
return this.bufferFactory;
}
@Override
public String toString() {
return "Part '" + this.name + "', headers=" + this.headers;
@@ -291,15 +426,15 @@ public class SynchronossPartHttpMessageReader extends LoggingCodecSupport implem
private final StreamStorage storage;
SynchronossPart(HttpHeaders headers, StreamStorage storage, DataBufferFactory factory) {
super(headers, factory);
SynchronossPart(HttpHeaders headers, StreamStorage storage) {
super(headers);
Assert.notNull(storage, "StreamStorage is required");
this.storage = storage;
}
@Override
public Flux<DataBuffer> content() {
return DataBufferUtils.readInputStream(getStorage()::getInputStream, getBufferFactory(), 4096);
return DataBufferUtils.readInputStream(getStorage()::getInputStream, bufferFactory, 4096);
}
protected StreamStorage getStorage() {
@@ -315,8 +450,8 @@ public class SynchronossPartHttpMessageReader extends LoggingCodecSupport implem
private final String filename;
SynchronossFilePart(HttpHeaders headers, String filename, StreamStorage storage, DataBufferFactory factory) {
super(headers, storage, factory);
SynchronossFilePart(HttpHeaders headers, String filename, StreamStorage storage) {
super(headers, storage);
this.filename = filename;
}
@@ -375,8 +510,8 @@ public class SynchronossPartHttpMessageReader extends LoggingCodecSupport implem
private final String content;
SynchronossFormFieldPart(HttpHeaders headers, DataBufferFactory bufferFactory, String content) {
super(headers, bufferFactory);
SynchronossFormFieldPart(HttpHeaders headers, String content) {
super(headers);
this.content = content;
}
@@ -388,9 +523,7 @@ public class SynchronossPartHttpMessageReader extends LoggingCodecSupport implem
@Override
public Flux<DataBuffer> content() {
byte[] bytes = this.content.getBytes(getCharset());
DataBuffer buffer = getBufferFactory().allocateBuffer(bytes.length);
buffer.write(bytes);
return Flux.just(buffer);
return Flux.just(bufferFactory.wrap(bytes));
}
private Charset getCharset() {
@@ -36,6 +36,7 @@ import org.springframework.core.ResolvableType;
import org.springframework.core.codec.Decoder;
import org.springframework.core.codec.DecodingException;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferLimitException;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
@@ -101,10 +102,24 @@ public class ProtobufDecoder extends ProtobufCodecSupport implements Decoder<Mes
}
/**
* The max size allowed per message.
* <p>By default in 5.1 this is set to 64K. In 5.2 the default for this limit
* is set to 256K.
* @param maxMessageSize the max size per message, or -1 for unlimited
*/
public void setMaxMessageSize(int maxMessageSize) {
this.maxMessageSize = maxMessageSize;
}
/**
* Return the {@link #setMaxMessageSize configured} message size limit.
* @since 5.1.11
*/
public int getMaxMessageSize() {
return this.maxMessageSize;
}
@Override
public boolean canDecode(ResolvableType elementType, @Nullable MimeType mimeType) {
@@ -127,7 +142,7 @@ public class ProtobufDecoder extends ProtobufCodecSupport implements Decoder<Mes
public Mono<Message> decodeToMono(Publisher<DataBuffer> inputStream, ResolvableType elementType,
@Nullable MimeType mimeType, @Nullable Map<String, Object> hints) {
return DataBufferUtils.join(inputStream).map(dataBuffer -> {
return DataBufferUtils.join(inputStream, getMaxMessageSize()).map(dataBuffer -> {
try {
Message.Builder builder = getMessageBuilder(elementType.toClass());
ByteBuffer buffer = dataBuffer.asByteBuffer();
@@ -198,9 +213,9 @@ public class ProtobufDecoder extends ProtobufCodecSupport implements Decoder<Mes
if (!readMessageSize(input)) {
return messages;
}
if (this.messageBytesToRead > this.maxMessageSize) {
throw new DecodingException(
"The number of bytes to read from the incoming stream " +
if (this.maxMessageSize > 0 && this.messageBytesToRead > this.maxMessageSize) {
throw new DataBufferLimitException(
"The number of bytes to read for message " +
"(" + this.messageBytesToRead + ") exceeds " +
"the configured limit (" + this.maxMessageSize + ")");
}
@@ -17,7 +17,10 @@
package org.springframework.http.codec.support;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import org.springframework.core.ResolvableType;
import org.springframework.core.codec.Decoder;
@@ -34,13 +37,14 @@ import org.springframework.util.Assert;
* client and server specific variants.
*
* @author Rossen Stoyanchev
* @author Brian Clozel
* @since 5.0
*/
class BaseCodecConfigurer implements CodecConfigurer {
abstract class BaseCodecConfigurer implements CodecConfigurer {
private final BaseDefaultCodecs defaultCodecs;
protected final BaseDefaultCodecs defaultCodecs;
private final DefaultCustomCodecs customCodecs = new DefaultCustomCodecs();
protected final DefaultCustomCodecs customCodecs;
/**
@@ -50,8 +54,25 @@ class BaseCodecConfigurer implements CodecConfigurer {
BaseCodecConfigurer(BaseDefaultCodecs defaultCodecs) {
Assert.notNull(defaultCodecs, "'defaultCodecs' is required");
this.defaultCodecs = defaultCodecs;
this.customCodecs = new DefaultCustomCodecs();
}
/**
* Create a deep copy of the given {@link BaseCodecConfigurer}.
* @since 5.1.12
*/
protected BaseCodecConfigurer(BaseCodecConfigurer other) {
this.defaultCodecs = other.cloneDefaultCodecs();
this.customCodecs = new DefaultCustomCodecs(other.customCodecs);
}
/**
* Sub-classes should override this to create deep copy of
* {@link BaseDefaultCodecs} which can can be client or server specific.
* @since 5.1.12
*/
protected abstract BaseDefaultCodecs cloneDefaultCodecs();
@Override
public DefaultCodecs defaultCodecs() {
@@ -70,97 +91,154 @@ class BaseCodecConfigurer implements CodecConfigurer {
@Override
public List<HttpMessageReader<?>> getReaders() {
this.defaultCodecs.applyDefaultConfig(this.customCodecs);
List<HttpMessageReader<?>> result = new ArrayList<>();
result.addAll(this.defaultCodecs.getTypedReaders());
result.addAll(this.customCodecs.getTypedReaders());
result.addAll(this.customCodecs.getTypedReaders().keySet());
result.addAll(this.defaultCodecs.getObjectReaders());
result.addAll(this.customCodecs.getObjectReaders());
result.addAll(this.customCodecs.getObjectReaders().keySet());
result.addAll(this.defaultCodecs.getCatchAllReaders());
return result;
}
@Override
public List<HttpMessageWriter<?>> getWriters() {
return getWritersInternal(false);
}
this.defaultCodecs.applyDefaultConfig(this.customCodecs);
/**
* Internal method that returns the configured writers.
* @param forMultipart whether to returns writers for general use ("false"),
* or for multipart requests only ("true"). Generally the two sets are the
* same except for the multipart writer itself.
*/
protected List<HttpMessageWriter<?>> getWritersInternal(boolean forMultipart) {
List<HttpMessageWriter<?>> result = new ArrayList<>();
result.addAll(this.defaultCodecs.getTypedWriters(forMultipart));
result.addAll(this.customCodecs.getTypedWriters());
result.addAll(this.defaultCodecs.getObjectWriters(forMultipart));
result.addAll(this.customCodecs.getObjectWriters());
result.addAll(this.defaultCodecs.getTypedWriters());
result.addAll(this.customCodecs.getTypedWriters().keySet());
result.addAll(this.defaultCodecs.getObjectWriters());
result.addAll(this.customCodecs.getObjectWriters().keySet());
result.addAll(this.defaultCodecs.getCatchAllWriters());
return result;
}
@Override
public abstract CodecConfigurer clone();
/**
* Default implementation of {@code CustomCodecs}.
*/
private static final class DefaultCustomCodecs implements CustomCodecs {
protected static final class DefaultCustomCodecs implements CustomCodecs {
private final List<HttpMessageReader<?>> typedReaders = new ArrayList<>();
private final Map<HttpMessageReader<?>, Boolean> typedReaders = new LinkedHashMap<>(4);
private final List<HttpMessageWriter<?>> typedWriters = new ArrayList<>();
private final Map<HttpMessageWriter<?>, Boolean> typedWriters = new LinkedHashMap<>(4);
private final List<HttpMessageReader<?>> objectReaders = new ArrayList<>();
private final Map<HttpMessageReader<?>, Boolean> objectReaders = new LinkedHashMap<>(4);
private final List<HttpMessageWriter<?>> objectWriters = new ArrayList<>();
private final Map<HttpMessageWriter<?>, Boolean> objectWriters = new LinkedHashMap<>(4);
private final List<Consumer<DefaultCodecConfig>> defaultConfigConsumers = new ArrayList<>(4);
DefaultCustomCodecs() {
}
/**
* Create a deep copy of the given {@link DefaultCustomCodecs}.
* @since 5.1.12
*/
DefaultCustomCodecs(DefaultCustomCodecs other) {
other.typedReaders.putAll(this.typedReaders);
other.typedWriters.putAll(this.typedWriters);
other.objectReaders.putAll(this.objectReaders);
other.objectWriters.putAll(this.objectWriters);
}
@Override
public void register(Object codec) {
addCodec(codec, false);
}
@Override
public void registerWithDefaultConfig(Object codec) {
addCodec(codec, true);
}
@Override
public void registerWithDefaultConfig(Object codec, Consumer<DefaultCodecConfig> configConsumer) {
addCodec(codec, false);
this.defaultConfigConsumers.add(configConsumer);
}
@SuppressWarnings("deprecation")
@Override
public void decoder(Decoder<?> decoder) {
reader(new DecoderHttpMessageReader<>(decoder));
addCodec(decoder, false);
}
@SuppressWarnings("deprecation")
@Override
public void encoder(Encoder<?> encoder) {
writer(new EncoderHttpMessageWriter<>(encoder));
addCodec(encoder, false);
}
@SuppressWarnings("deprecation")
@Override
public void reader(HttpMessageReader<?> reader) {
boolean canReadToObject = reader.canRead(ResolvableType.forClass(Object.class), null);
(canReadToObject ? this.objectReaders : this.typedReaders).add(reader);
addCodec(reader, false);
}
@SuppressWarnings("deprecation")
@Override
public void writer(HttpMessageWriter<?> writer) {
boolean canWriteObject = writer.canWrite(ResolvableType.forClass(Object.class), null);
(canWriteObject ? this.objectWriters : this.typedWriters).add(writer);
addCodec(writer, false);
}
@SuppressWarnings("deprecation")
@Override
public void withDefaultCodecConfig(Consumer<DefaultCodecConfig> codecsConfigConsumer) {
this.defaultConfigConsumers.add(codecsConfigConsumer);
}
private void addCodec(Object codec, boolean applyDefaultConfig) {
if (codec instanceof Decoder) {
codec = new DecoderHttpMessageReader<>((Decoder<?>) codec);
}
else if (codec instanceof Encoder) {
codec = new EncoderHttpMessageWriter<>((Encoder<?>) codec);
}
if (codec instanceof HttpMessageReader) {
HttpMessageReader<?> reader = (HttpMessageReader<?>) codec;
boolean canReadToObject = reader.canRead(ResolvableType.forClass(Object.class), null);
(canReadToObject ? this.objectReaders : this.typedReaders).put(reader, applyDefaultConfig);
}
else if (codec instanceof HttpMessageWriter) {
HttpMessageWriter<?> writer = (HttpMessageWriter<?>) codec;
boolean canWriteObject = writer.canWrite(ResolvableType.forClass(Object.class), null);
(canWriteObject ? this.objectWriters : this.typedWriters).put(writer, applyDefaultConfig);
}
else {
throw new IllegalArgumentException("Unexpected codec type: " + codec.getClass().getName());
}
}
// Package private accessors...
List<HttpMessageReader<?>> getTypedReaders() {
Map<HttpMessageReader<?>, Boolean> getTypedReaders() {
return this.typedReaders;
}
List<HttpMessageWriter<?>> getTypedWriters() {
Map<HttpMessageWriter<?>, Boolean> getTypedWriters() {
return this.typedWriters;
}
List<HttpMessageReader<?>> getObjectReaders() {
Map<HttpMessageReader<?>, Boolean> getObjectReaders() {
return this.objectReaders;
}
List<HttpMessageWriter<?>> getObjectWriters() {
Map<HttpMessageWriter<?>, Boolean> getObjectWriters() {
return this.objectWriters;
}
List<Consumer<DefaultCodecConfig>> getDefaultConfigConsumers() {
return this.defaultConfigConsumers;
}
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2019 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,7 +19,9 @@ package org.springframework.http.codec.support;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import org.springframework.core.codec.AbstractDataBufferDecoder;
import org.springframework.core.codec.ByteArrayDecoder;
import org.springframework.core.codec.ByteArrayEncoder;
import org.springframework.core.codec.ByteBufferDecoder;
@@ -35,13 +37,19 @@ import org.springframework.http.codec.CodecConfigurer;
import org.springframework.http.codec.DecoderHttpMessageReader;
import org.springframework.http.codec.EncoderHttpMessageWriter;
import org.springframework.http.codec.FormHttpMessageReader;
import org.springframework.http.codec.FormHttpMessageWriter;
import org.springframework.http.codec.HttpMessageReader;
import org.springframework.http.codec.HttpMessageWriter;
import org.springframework.http.codec.ResourceHttpMessageWriter;
import org.springframework.http.codec.ServerSentEventHttpMessageReader;
import org.springframework.http.codec.json.AbstractJackson2Decoder;
import org.springframework.http.codec.json.Jackson2JsonDecoder;
import org.springframework.http.codec.json.Jackson2JsonEncoder;
import org.springframework.http.codec.json.Jackson2SmileDecoder;
import org.springframework.http.codec.json.Jackson2SmileEncoder;
import org.springframework.http.codec.multipart.MultipartHttpMessageReader;
import org.springframework.http.codec.multipart.MultipartHttpMessageWriter;
import org.springframework.http.codec.multipart.SynchronossPartHttpMessageReader;
import org.springframework.http.codec.protobuf.ProtobufDecoder;
import org.springframework.http.codec.protobuf.ProtobufEncoder;
import org.springframework.http.codec.protobuf.ProtobufHttpMessageWriter;
@@ -57,7 +65,7 @@ import org.springframework.util.ClassUtils;
* @author Rossen Stoyanchev
* @author Sebastien Deleuze
*/
class BaseDefaultCodecs implements CodecConfigurer.DefaultCodecs {
class BaseDefaultCodecs implements CodecConfigurer.DefaultCodecs, CodecConfigurer.DefaultCodecConfig {
static final boolean jackson2Present;
@@ -67,6 +75,8 @@ class BaseDefaultCodecs implements CodecConfigurer.DefaultCodecs {
private static final boolean protobufPresent;
static final boolean synchronossMultipartPresent;
static {
ClassLoader classLoader = BaseCodecConfigurer.class.getClassLoader();
jackson2Present = ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", classLoader) &&
@@ -74,6 +84,7 @@ class BaseDefaultCodecs implements CodecConfigurer.DefaultCodecs {
jackson2SmilePresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.smile.SmileFactory", classLoader);
jaxb2Present = ClassUtils.isPresent("javax.xml.bind.Binder", classLoader);
protobufPresent = ClassUtils.isPresent("com.google.protobuf.Message", classLoader);
synchronossMultipartPresent = ClassUtils.isPresent("org.synchronoss.cloud.nio.multipart.NioMultipartParser", classLoader);
}
@@ -95,11 +106,33 @@ class BaseDefaultCodecs implements CodecConfigurer.DefaultCodecs {
@Nullable
private Encoder<?> jaxb2Encoder;
private boolean enableLoggingRequestDetails = false;
@Nullable
private Integer maxInMemorySize;
@Nullable
private Boolean enableLoggingRequestDetails;
private boolean registerDefaults = true;
BaseDefaultCodecs() {
}
/**
* Create a deep copy of the given {@link BaseDefaultCodecs}.
*/
protected BaseDefaultCodecs(BaseDefaultCodecs other) {
this.jackson2JsonDecoder = other.jackson2JsonDecoder;
this.jackson2JsonEncoder = other.jackson2JsonEncoder;
this.protobufDecoder = other.protobufDecoder;
this.protobufEncoder = other.protobufEncoder;
this.jaxb2Decoder = other.jaxb2Decoder;
this.jaxb2Encoder = other.jaxb2Encoder;
this.maxInMemorySize = other.maxInMemorySize;
this.enableLoggingRequestDetails = other.enableLoggingRequestDetails;
this.registerDefaults = other.registerDefaults;
}
@Override
public void jackson2JsonDecoder(Decoder<?> decoder) {
this.jackson2JsonDecoder = decoder;
@@ -130,12 +163,25 @@ class BaseDefaultCodecs implements CodecConfigurer.DefaultCodecs {
this.jaxb2Encoder = encoder;
}
@Override
public void maxInMemorySize(int byteCount) {
this.maxInMemorySize = byteCount;
}
@Override
@Nullable
public Integer maxInMemorySize() {
return this.maxInMemorySize;
}
@Override
public void enableLoggingRequestDetails(boolean enable) {
this.enableLoggingRequestDetails = enable;
}
protected boolean isEnableLoggingRequestDetails() {
@Override
@Nullable
public Boolean isEnableLoggingRequestDetails() {
return this.enableLoggingRequestDetails;
}
@@ -155,25 +201,109 @@ class BaseDefaultCodecs implements CodecConfigurer.DefaultCodecs {
return Collections.emptyList();
}
List<HttpMessageReader<?>> readers = new ArrayList<>();
readers.add(new DecoderHttpMessageReader<>(new ByteArrayDecoder()));
readers.add(new DecoderHttpMessageReader<>(new ByteBufferDecoder()));
readers.add(new DecoderHttpMessageReader<>(new DataBufferDecoder()));
readers.add(new DecoderHttpMessageReader<>(new ResourceDecoder()));
readers.add(new DecoderHttpMessageReader<>(StringDecoder.textPlainOnly()));
addCodec(readers, new DecoderHttpMessageReader<>(new ByteArrayDecoder()));
addCodec(readers, new DecoderHttpMessageReader<>(new ByteBufferDecoder()));
addCodec(readers, new DecoderHttpMessageReader<>(new DataBufferDecoder()));
addCodec(readers, new DecoderHttpMessageReader<>(new ResourceDecoder()));
addCodec(readers, new DecoderHttpMessageReader<>(StringDecoder.textPlainOnly()));
if (protobufPresent) {
Decoder<?> decoder = this.protobufDecoder != null ? this.protobufDecoder : new ProtobufDecoder();
readers.add(new DecoderHttpMessageReader<>(decoder));
addCodec(readers, new DecoderHttpMessageReader<>(decoder));
}
addCodec(readers, new FormHttpMessageReader());
FormHttpMessageReader formReader = new FormHttpMessageReader();
formReader.setEnableLoggingRequestDetails(this.enableLoggingRequestDetails);
readers.add(formReader);
// client vs server..
extendTypedReaders(readers);
return readers;
}
/**
* Initialize a codec and add it to the List.
* @since 5.1.13
*/
protected <T> void addCodec(List<T> codecs, T codec) {
initCodec(codec);
codecs.add(codec);
}
/**
* Apply {@link #maxInMemorySize()} and {@link #enableLoggingRequestDetails},
* if configured by the application, to the given codec , including any
* codec it contains.
*/
private void initCodec(@Nullable Object codec) {
if (codec instanceof DecoderHttpMessageReader) {
codec = ((DecoderHttpMessageReader) codec).getDecoder();
}
else if (codec instanceof ServerSentEventHttpMessageReader) {
codec = ((ServerSentEventHttpMessageReader) codec).getDecoder();
}
if (codec == null) {
return;
}
Integer size = this.maxInMemorySize;
if (size != null) {
if (codec instanceof AbstractDataBufferDecoder) {
((AbstractDataBufferDecoder<?>) codec).setMaxInMemorySize(size);
}
if (protobufPresent) {
if (codec instanceof ProtobufDecoder) {
((ProtobufDecoder) codec).setMaxMessageSize(size);
}
}
if (jackson2Present) {
if (codec instanceof AbstractJackson2Decoder) {
((AbstractJackson2Decoder) codec).setMaxInMemorySize(size);
}
}
if (jaxb2Present) {
if (codec instanceof Jaxb2XmlDecoder) {
((Jaxb2XmlDecoder) codec).setMaxInMemorySize(size);
}
}
if (codec instanceof FormHttpMessageReader) {
((FormHttpMessageReader) codec).setMaxInMemorySize(size);
}
if (synchronossMultipartPresent) {
if (codec instanceof SynchronossPartHttpMessageReader) {
((SynchronossPartHttpMessageReader) codec).setMaxInMemorySize(size);
}
}
}
Boolean enable = this.enableLoggingRequestDetails;
if (enable != null) {
if (codec instanceof FormHttpMessageReader) {
((FormHttpMessageReader) codec).setEnableLoggingRequestDetails(enable);
}
if (codec instanceof MultipartHttpMessageReader) {
((MultipartHttpMessageReader) codec).setEnableLoggingRequestDetails(enable);
}
if (synchronossMultipartPresent) {
if (codec instanceof SynchronossPartHttpMessageReader) {
((SynchronossPartHttpMessageReader) codec).setEnableLoggingRequestDetails(enable);
}
}
if (codec instanceof FormHttpMessageWriter) {
((FormHttpMessageWriter) codec).setEnableLoggingRequestDetails(enable);
}
if (codec instanceof MultipartHttpMessageWriter) {
((MultipartHttpMessageWriter) codec).setEnableLoggingRequestDetails(enable);
}
}
if (codec instanceof MultipartHttpMessageReader) {
initCodec(((MultipartHttpMessageReader) codec).getPartReader());
}
else if (codec instanceof MultipartHttpMessageWriter) {
initCodec(((MultipartHttpMessageWriter) codec).getFormWriter());
}
}
/**
* Hook for client or server specific typed readers.
*/
@@ -189,16 +319,19 @@ class BaseDefaultCodecs implements CodecConfigurer.DefaultCodecs {
}
List<HttpMessageReader<?>> readers = new ArrayList<>();
if (jackson2Present) {
readers.add(new DecoderHttpMessageReader<>(getJackson2JsonDecoder()));
addCodec(readers, new DecoderHttpMessageReader<>(getJackson2JsonDecoder()));
}
if (jackson2SmilePresent) {
readers.add(new DecoderHttpMessageReader<>(new Jackson2SmileDecoder()));
addCodec(readers, new DecoderHttpMessageReader<>(new Jackson2SmileDecoder()));
}
if (jaxb2Present) {
Decoder<?> decoder = this.jaxb2Decoder != null ? this.jaxb2Decoder : new Jaxb2XmlDecoder();
readers.add(new DecoderHttpMessageReader<>(decoder));
addCodec(readers, new DecoderHttpMessageReader<>(decoder));
}
// client vs server..
extendObjectReaders(readers);
return readers;
}
@@ -215,19 +348,29 @@ class BaseDefaultCodecs implements CodecConfigurer.DefaultCodecs {
if (!this.registerDefaults) {
return Collections.emptyList();
}
List<HttpMessageReader<?>> result = new ArrayList<>();
result.add(new DecoderHttpMessageReader<>(StringDecoder.allMimeTypes()));
return result;
List<HttpMessageReader<?>> readers = new ArrayList<>();
addCodec(readers, new DecoderHttpMessageReader<>(StringDecoder.allMimeTypes()));
return readers;
}
/**
* Return writers that support specific types.
* @param forMultipart whether to returns writers for general use ("false"),
* or for multipart requests only ("true"). Generally the two sets are the
* same except for the multipart writer itself.
* Return all writers that support specific types.
*/
@SuppressWarnings({"rawtypes" })
final List<HttpMessageWriter<?>> getTypedWriters() {
if (!this.registerDefaults) {
return Collections.emptyList();
}
List<HttpMessageWriter<?>> writers = getBaseTypedWriters();
extendTypedWriters(writers);
return writers;
}
/**
* Return "base" typed writers only, i.e. common to client and server.
*/
@SuppressWarnings("unchecked")
final List<HttpMessageWriter<?>> getTypedWriters(boolean forMultipart) {
final List<HttpMessageWriter<?>> getBaseTypedWriters() {
if (!this.registerDefaults) {
return Collections.emptyList();
}
@@ -237,10 +380,6 @@ class BaseDefaultCodecs implements CodecConfigurer.DefaultCodecs {
writers.add(new EncoderHttpMessageWriter<>(new DataBufferEncoder()));
writers.add(new ResourceHttpMessageWriter());
writers.add(new EncoderHttpMessageWriter<>(CharSequenceEncoder.textPlainOnly()));
// No client or server specific multipart writers currently..
if (!forMultipart) {
extendTypedWriters(writers);
}
if (protobufPresent) {
Encoder<?> encoder = this.protobufEncoder != null ? this.protobufEncoder : new ProtobufEncoder();
writers.add(new ProtobufHttpMessageWriter((Encoder) encoder));
@@ -256,14 +395,20 @@ class BaseDefaultCodecs implements CodecConfigurer.DefaultCodecs {
/**
* Return Object writers (JSON, XML, SSE).
* @param forMultipart whether to returns writers for general use ("false"),
* or for multipart requests only ("true"). Generally the two sets are the
* same except for the multipart writer itself.
*/
final List<HttpMessageWriter<?>> getObjectWriters(boolean forMultipart) {
final List<HttpMessageWriter<?>> getObjectWriters() {
if (!this.registerDefaults) {
return Collections.emptyList();
}
List<HttpMessageWriter<?>> writers = getBaseObjectWriters();
extendObjectWriters(writers);
return writers;
}
/**
* Return "base" object writers only, i.e. common to client and server.
*/
final List<HttpMessageWriter<?>> getBaseObjectWriters() {
List<HttpMessageWriter<?>> writers = new ArrayList<>();
if (jackson2Present) {
writers.add(new EncoderHttpMessageWriter<>(getJackson2JsonEncoder()));
@@ -275,10 +420,6 @@ class BaseDefaultCodecs implements CodecConfigurer.DefaultCodecs {
Encoder<?> encoder = this.jaxb2Encoder != null ? this.jaxb2Encoder : new Jaxb2XmlEncoder();
writers.add(new EncoderHttpMessageWriter<>(encoder));
}
// No client or server specific multipart writers currently..
if (!forMultipart) {
extendObjectWriters(writers);
}
return writers;
}
@@ -300,15 +441,36 @@ class BaseDefaultCodecs implements CodecConfigurer.DefaultCodecs {
return result;
}
void applyDefaultConfig(BaseCodecConfigurer.DefaultCustomCodecs customCodecs) {
applyDefaultConfig(customCodecs.getTypedReaders());
applyDefaultConfig(customCodecs.getObjectReaders());
applyDefaultConfig(customCodecs.getTypedWriters());
applyDefaultConfig(customCodecs.getObjectWriters());
customCodecs.getDefaultConfigConsumers().forEach(consumer -> consumer.accept(this));
}
private void applyDefaultConfig(Map<?, Boolean> readers) {
readers.entrySet().stream()
.filter(Map.Entry::getValue)
.map(Map.Entry::getKey)
.forEach(this::initCodec);
}
// Accessors for use in subclasses...
protected Decoder<?> getJackson2JsonDecoder() {
return (this.jackson2JsonDecoder != null ? this.jackson2JsonDecoder : new Jackson2JsonDecoder());
if (this.jackson2JsonDecoder == null) {
this.jackson2JsonDecoder = new Jackson2JsonDecoder();
}
return this.jackson2JsonDecoder;
}
protected Encoder<?> getJackson2JsonEncoder() {
return (this.jackson2JsonEncoder != null ? this.jackson2JsonEncoder : new Jackson2JsonEncoder());
if (this.jackson2JsonEncoder == null) {
this.jackson2JsonEncoder = new Jackson2JsonEncoder();
}
return this.jackson2JsonEncoder;
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2019 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,6 +49,17 @@ class ClientDefaultCodecsImpl extends BaseDefaultCodecs implements ClientCodecCo
private Supplier<List<HttpMessageWriter<?>>> partWritersSupplier;
ClientDefaultCodecsImpl() {
}
ClientDefaultCodecsImpl(ClientDefaultCodecsImpl other) {
super(other);
this.multipartCodecs = (other.multipartCodecs != null ?
new DefaultMultipartCodecs(other.multipartCodecs) : null);
this.sseDecoder = other.sseDecoder;
}
/**
* Set a supplier for part writers to use when
* {@link #multipartCodecs()} are not explicitly configured.
@@ -73,27 +84,28 @@ class ClientDefaultCodecsImpl extends BaseDefaultCodecs implements ClientCodecCo
this.sseDecoder = decoder;
}
@Override
public ClientDefaultCodecsImpl clone() {
ClientDefaultCodecsImpl codecs = new ClientDefaultCodecsImpl();
codecs.multipartCodecs = this.multipartCodecs;
codecs.sseDecoder = this.sseDecoder;
codecs.partWritersSupplier = this.partWritersSupplier;
return codecs;
}
@Override
protected void extendObjectReaders(List<HttpMessageReader<?>> objectReaders) {
objectReaders.add(new ServerSentEventHttpMessageReader(getSseDecoder()));
}
@Nullable
private Decoder<?> getSseDecoder() {
return (this.sseDecoder != null ? this.sseDecoder : jackson2Present ? getJackson2JsonDecoder() : null);
Decoder<?> decoder = (this.sseDecoder != null ?
this.sseDecoder :
jackson2Present ? getJackson2JsonDecoder() : null);
addCodec(objectReaders, new ServerSentEventHttpMessageReader(decoder));
}
@Override
protected void extendTypedWriters(List<HttpMessageWriter<?>> typedWriters) {
FormHttpMessageWriter formWriter = new FormHttpMessageWriter();
formWriter.setEnableLoggingRequestDetails(isEnableLoggingRequestDetails());
MultipartHttpMessageWriter multipartWriter = new MultipartHttpMessageWriter(getPartWriters(), formWriter);
multipartWriter.setEnableLoggingRequestDetails(isEnableLoggingRequestDetails());
typedWriters.add(multipartWriter);
addCodec(typedWriters, new MultipartHttpMessageWriter(getPartWriters(), new FormHttpMessageWriter()));
}
private List<HttpMessageWriter<?>> getPartWriters() {
@@ -116,6 +128,15 @@ class ClientDefaultCodecsImpl extends BaseDefaultCodecs implements ClientCodecCo
private final List<HttpMessageWriter<?>> writers = new ArrayList<>();
DefaultMultipartCodecs() {
}
DefaultMultipartCodecs(DefaultMultipartCodecs other) {
this.writers.addAll(other.writers);
}
@Override
public ClientCodecConfigurer.MultipartCodecs encoder(Encoder<?> encoder) {
writer(new EncoderHttpMessageWriter<>(encoder));
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2019 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,11 @@
package org.springframework.http.codec.support;
import java.util.ArrayList;
import java.util.List;
import org.springframework.http.codec.ClientCodecConfigurer;
import org.springframework.http.codec.HttpMessageWriter;
/**
* Default implementation of {@link ClientCodecConfigurer}.
@@ -26,14 +30,41 @@ import org.springframework.http.codec.ClientCodecConfigurer;
*/
public class DefaultClientCodecConfigurer extends BaseCodecConfigurer implements ClientCodecConfigurer {
public DefaultClientCodecConfigurer() {
super(new ClientDefaultCodecsImpl());
((ClientDefaultCodecsImpl) defaultCodecs()).setPartWritersSupplier(() -> getWritersInternal(true));
((ClientDefaultCodecsImpl) defaultCodecs()).setPartWritersSupplier(this::getPartWriters);
}
private DefaultClientCodecConfigurer(DefaultClientCodecConfigurer other) {
super(other);
((ClientDefaultCodecsImpl) defaultCodecs()).setPartWritersSupplier(this::getPartWriters);
}
@Override
public ClientDefaultCodecs defaultCodecs() {
return (ClientDefaultCodecs) super.defaultCodecs();
}
@Override
public DefaultClientCodecConfigurer clone() {
return new DefaultClientCodecConfigurer(this);
}
@Override
protected BaseDefaultCodecs cloneDefaultCodecs() {
return new ClientDefaultCodecsImpl((ClientDefaultCodecsImpl) defaultCodecs());
}
private List<HttpMessageWriter<?>> getPartWriters() {
List<HttpMessageWriter<?>> result = new ArrayList<>();
result.addAll(this.customCodecs.getTypedWriters().keySet());
result.addAll(this.defaultCodecs.getBaseTypedWriters());
result.addAll(this.customCodecs.getObjectWriters().keySet());
result.addAll(this.defaultCodecs.getBaseObjectWriters());
result.addAll(this.defaultCodecs.getCatchAllWriters());
return result;
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2019 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.
@@ -26,13 +26,28 @@ import org.springframework.http.codec.ServerCodecConfigurer;
*/
public class DefaultServerCodecConfigurer extends BaseCodecConfigurer implements ServerCodecConfigurer {
public DefaultServerCodecConfigurer() {
super(new ServerDefaultCodecsImpl());
}
private DefaultServerCodecConfigurer(BaseCodecConfigurer other) {
super(other);
}
@Override
public ServerDefaultCodecs defaultCodecs() {
return (ServerDefaultCodecs) super.defaultCodecs();
}
@Override
public DefaultServerCodecConfigurer clone() {
return new DefaultServerCodecConfigurer(this);
}
@Override
protected BaseDefaultCodecs cloneDefaultCodecs() {
return new ServerDefaultCodecsImpl((ServerDefaultCodecsImpl) defaultCodecs());
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2019 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,7 +25,6 @@ import org.springframework.http.codec.ServerSentEventHttpMessageWriter;
import org.springframework.http.codec.multipart.MultipartHttpMessageReader;
import org.springframework.http.codec.multipart.SynchronossPartHttpMessageReader;
import org.springframework.lang.Nullable;
import org.springframework.util.ClassUtils;
/**
* Default implementation of {@link ServerCodecConfigurer.ServerDefaultCodecs}.
@@ -34,15 +33,28 @@ import org.springframework.util.ClassUtils;
*/
class ServerDefaultCodecsImpl extends BaseDefaultCodecs implements ServerCodecConfigurer.ServerDefaultCodecs {
private static final boolean synchronossMultipartPresent =
ClassUtils.isPresent("org.synchronoss.cloud.nio.multipart.NioMultipartParser",
DefaultServerCodecConfigurer.class.getClassLoader());
@Nullable
private HttpMessageReader<?> multipartReader;
@Nullable
private Encoder<?> sseEncoder;
ServerDefaultCodecsImpl() {
}
ServerDefaultCodecsImpl(ServerDefaultCodecsImpl other) {
super(other);
this.multipartReader = other.multipartReader;
this.sseEncoder = other.sseEncoder;
}
@Override
public void multipartReader(HttpMessageReader<?> reader) {
this.multipartReader = reader;
}
@Override
public void serverSentEventEncoder(Encoder<?> encoder) {
this.sseEncoder = encoder;
@@ -51,16 +63,14 @@ class ServerDefaultCodecsImpl extends BaseDefaultCodecs implements ServerCodecCo
@Override
protected void extendTypedReaders(List<HttpMessageReader<?>> typedReaders) {
if (this.multipartReader != null) {
addCodec(typedReaders, this.multipartReader);
return;
}
if (synchronossMultipartPresent) {
boolean enable = isEnableLoggingRequestDetails();
SynchronossPartHttpMessageReader partReader = new SynchronossPartHttpMessageReader();
partReader.setEnableLoggingRequestDetails(enable);
typedReaders.add(partReader);
MultipartHttpMessageReader reader = new MultipartHttpMessageReader(partReader);
reader.setEnableLoggingRequestDetails(enable);
typedReaders.add(reader);
addCodec(typedReaders, partReader);
addCodec(typedReaders, new MultipartHttpMessageReader(partReader));
}
}
@@ -43,6 +43,7 @@ import org.springframework.core.codec.CodecException;
import org.springframework.core.codec.DecodingException;
import org.springframework.core.codec.Hints;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferLimitException;
import org.springframework.core.log.LogFormatUtils;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
@@ -78,6 +79,8 @@ public class Jaxb2XmlDecoder extends AbstractDecoder<Object> {
private Function<Unmarshaller, Unmarshaller> unmarshallerProcessor = Function.identity();
private int maxInMemorySize = -1;
public Jaxb2XmlDecoder() {
super(MimeTypeUtils.APPLICATION_XML, MimeTypeUtils.TEXT_XML);
@@ -110,6 +113,29 @@ public class Jaxb2XmlDecoder extends AbstractDecoder<Object> {
return this.unmarshallerProcessor;
}
/**
* Set the max number of bytes that can be buffered by this decoder.
* This is either the size of the entire input when decoding as a whole, or when
* using async parsing with Aalto XML, it is the size of one top-level XML tree.
* When the limit is exceeded, {@link DataBufferLimitException} is raised.
* <p>By default in 5.1 this is set to -1, unlimited. In 5.2 the default
* value for this limit is set to 256K.
* @param byteCount the max number of bytes to buffer, or -1 for unlimited
* @since 5.1.11
*/
public void setMaxInMemorySize(int byteCount) {
this.maxInMemorySize = byteCount;
this.xmlEventDecoder.setMaxInMemorySize(byteCount);
}
/**
* Return the {@link #setMaxInMemorySize configured} byte count limit.
* @since 5.1.11
*/
public int getMaxInMemorySize() {
return this.maxInMemorySize;
}
@Override
public boolean canDecode(ResolvableType elementType, @Nullable MimeType mimeType) {
@@ -166,7 +192,7 @@ public class Jaxb2XmlDecoder extends AbstractDecoder<Object> {
}
}
private Unmarshaller initUnmarshaller(Class<?> outputClass) throws JAXBException {
private Unmarshaller initUnmarshaller(Class<?> outputClass) throws CodecException, JAXBException {
Unmarshaller unmarshaller = this.jaxbContexts.createUnmarshaller(outputClass);
return this.unmarshallerProcessor.apply(unmarshaller);
}
@@ -135,7 +135,7 @@ public class Jaxb2XmlEncoder extends AbstractSingleValueEncoder<Object> {
}).flux();
}
private Marshaller initMarshaller(Class<?> clazz) throws JAXBException {
private Marshaller initMarshaller(Class<?> clazz) throws CodecException, JAXBException {
Marshaller marshaller = this.jaxbContexts.createMarshaller(clazz);
marshaller.setProperty(Marshaller.JAXB_ENCODING, StandardCharsets.UTF_8.name());
marshaller = this.marshallerProcessor.apply(marshaller);
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2017 the original author or authors.
* Copyright 2002-2019 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,12 +24,13 @@ import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;
import org.springframework.util.Assert;
import org.springframework.core.codec.CodecException;
/**
* Holder for {@link JAXBContext} instances.
*
* @author Arjen Poutsma
* @author Juergen Hoeller
* @since 5.0
*/
final class JaxbContextContainer {
@@ -37,24 +38,26 @@ final class JaxbContextContainer {
private final ConcurrentMap<Class<?>, JAXBContext> jaxbContexts = new ConcurrentHashMap<>(64);
public Marshaller createMarshaller(Class<?> clazz) throws JAXBException {
public Marshaller createMarshaller(Class<?> clazz) throws CodecException, JAXBException {
JAXBContext jaxbContext = getJaxbContext(clazz);
return jaxbContext.createMarshaller();
}
public Unmarshaller createUnmarshaller(Class<?> clazz) throws JAXBException {
public Unmarshaller createUnmarshaller(Class<?> clazz) throws CodecException, JAXBException {
JAXBContext jaxbContext = getJaxbContext(clazz);
return jaxbContext.createUnmarshaller();
}
private JAXBContext getJaxbContext(Class<?> clazz) throws JAXBException {
Assert.notNull(clazz, "Class must not be null");
JAXBContext jaxbContext = this.jaxbContexts.get(clazz);
if (jaxbContext == null) {
jaxbContext = JAXBContext.newInstance(clazz);
this.jaxbContexts.putIfAbsent(clazz, jaxbContext);
}
return jaxbContext;
private JAXBContext getJaxbContext(Class<?> clazz) throws CodecException {
return this.jaxbContexts.computeIfAbsent(clazz, key -> {
try {
return JAXBContext.newInstance(clazz);
}
catch (JAXBException ex) {
throw new CodecException(
"Could not create JAXBContext for class [" + clazz + "]: " + ex.getMessage(), ex);
}
});
}
}
@@ -40,6 +40,7 @@ import reactor.core.publisher.Flux;
import org.springframework.core.ResolvableType;
import org.springframework.core.codec.AbstractDecoder;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferLimitException;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.lang.Nullable;
import org.springframework.util.ClassUtils;
@@ -88,26 +89,51 @@ public class XmlEventDecoder extends AbstractDecoder<XMLEvent> {
boolean useAalto = aaltoPresent;
private int maxInMemorySize = -1;
public XmlEventDecoder() {
super(MimeTypeUtils.APPLICATION_XML, MimeTypeUtils.TEXT_XML);
}
/**
* Set the max number of bytes that can be buffered by this decoder. This
* is either the size the entire input when decoding as a whole, or when
* using async parsing via Aalto XML, it is size one top-level XML tree.
* When the limit is exceeded, {@link DataBufferLimitException} is raised.
* <p>By default in 5.1 this is set to -1, unlimited. In 5.2 the default
* value for this limit is set to 256K.
* @param byteCount the max number of bytes to buffer, or -1 for unlimited
* @since 5.1.11
*/
public void setMaxInMemorySize(int byteCount) {
this.maxInMemorySize = byteCount;
}
/**
* Return the {@link #setMaxInMemorySize configured} byte count limit.
* @since 5.1.11
*/
public int getMaxInMemorySize() {
return this.maxInMemorySize;
}
@Override
@SuppressWarnings({"rawtypes", "unchecked"}) // on JDK 9 where XMLEventReader is Iterator<Object>
public Flux<XMLEvent> decode(Publisher<DataBuffer> input, ResolvableType elementType,
@Nullable MimeType mimeType, @Nullable Map<String, Object> hints) {
if (this.useAalto) {
AaltoDataBufferToXmlEvent mapper = new AaltoDataBufferToXmlEvent();
AaltoDataBufferToXmlEvent mapper = new AaltoDataBufferToXmlEvent(this.maxInMemorySize);
return Flux.from(input)
.flatMapIterable(mapper)
.doFinally(signalType -> mapper.endOfInput());
}
else {
return DataBufferUtils.join(input).
flatMapIterable(buffer -> {
return DataBufferUtils.join(input, getMaxInMemorySize())
.flatMapIterable(buffer -> {
try {
InputStream is = buffer.asInputStream();
Iterator eventReader = inputFactory.createXMLEventReader(is);
@@ -139,10 +165,22 @@ public class XmlEventDecoder extends AbstractDecoder<XMLEvent> {
private final XMLEventAllocator eventAllocator = EventAllocatorImpl.getDefaultInstance();
private final int maxInMemorySize;
private int byteCount;
private int elementDepth;
public AaltoDataBufferToXmlEvent(int maxInMemorySize) {
this.maxInMemorySize = maxInMemorySize;
}
@Override
public List<? extends XMLEvent> apply(DataBuffer dataBuffer) {
try {
increaseByteCount(dataBuffer);
this.streamReader.getInputFeeder().feedInput(dataBuffer.asByteBuffer());
List<XMLEvent> events = new ArrayList<>();
while (true) {
@@ -156,8 +194,12 @@ public class XmlEventDecoder extends AbstractDecoder<XMLEvent> {
if (event.isEndDocument()) {
break;
}
checkDepthAndResetByteCount(event);
}
}
if (this.maxInMemorySize > 0 && this.byteCount > this.maxInMemorySize) {
raiseLimitException();
}
return events;
}
catch (XMLStreamException ex) {
@@ -168,6 +210,35 @@ public class XmlEventDecoder extends AbstractDecoder<XMLEvent> {
}
}
private void increaseByteCount(DataBuffer dataBuffer) {
if (this.maxInMemorySize > 0) {
if (dataBuffer.readableByteCount() > Integer.MAX_VALUE - this.byteCount) {
raiseLimitException();
}
else {
this.byteCount += dataBuffer.readableByteCount();
}
}
}
private void checkDepthAndResetByteCount(XMLEvent event) {
if (this.maxInMemorySize > 0) {
if (event.isStartElement()) {
this.byteCount = this.elementDepth == 1 ? 0 : this.byteCount;
this.elementDepth++;
}
else if (event.isEndElement()) {
this.elementDepth--;
this.byteCount = this.elementDepth == 1 ? 0 : this.byteCount;
}
}
}
private void raiseLimitException() {
throw new DataBufferLimitException(
"Exceeded limit on max bytes per XML top-level node: " + this.maxInMemorySize);
}
public void endOfInput() {
this.streamReader.getInputFeeder().endOfInput();
}

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