Compare commits

...

66 Commits

Author SHA1 Message Date
Spring Buildmaster 96f1a899ca Release version 5.1.12.RELEASE 2019-12-03 08:05:53 +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
131 changed files with 3160 additions and 645 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
+7 -7
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.24.v20191120"
junit5Version = "5.3.2"
kotlinVersion = "1.2.71"
log4jVersion = "2.11.2"
nettyVersion = "4.1.43.Final"
reactorVersion = "Californium-SR13"
reactorVersion = "Californium-SR14"
rxjavaVersion = "1.3.8"
rxjavaAdapterVersion = "1.2.1"
rxjava2Version = "2.2.12"
rxjava2Version = "2.2.15"
slf4jVersion = "1.7.28" // spring-jcl + consistent 3rd party deps
tiles3Version = "3.0.8"
tomcatVersion = "9.0.26"
undertowVersion = "2.0.26.Final"
tomcatVersion = "9.0.29"
undertowVersion = "2.0.28.Final"
gradleScriptDir = "${rootProject.projectDir}/gradle"
withoutJclOverSlf4j = {
@@ -142,7 +142,7 @@ configure(allprojects) { project ->
}
checkstyle {
toolVersion = "8.24"
toolVersion = "8.27"
configDir = rootProject.file("src/checkstyle")
}
+1 -1
View File
@@ -1 +1 @@
version=5.1.11.BUILD-SNAPSHOT
version=5.1.12.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,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) {
@@ -1128,28 +1128,28 @@ public abstract class AbstractBeanDefinition extends BeanMetadataAttributeAccess
}
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;
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);
}
@@ -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);
}
+1 -1
View File
@@ -28,7 +28,7 @@ dependencies {
testCompile("org.codehaus.groovy:groovy-xml:${groovyVersion}")
testCompile("org.apache.commons:commons-pool2:2.6.0")
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")
@@ -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-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);
}
}
}
+1 -1
View File
@@ -83,7 +83,7 @@ dependencies {
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"
}
}
@@ -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));
}
@@ -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,10 +95,16 @@ public final class StringDecoder extends AbstractDataBufferDecoder<String> {
List<byte[]> delimiterBytes = getDelimiterBytes(mimeType);
// TODO: Drop Consumer and use bufferUntil with Supplier<LimistedDataBufferList> (reactor-core#1925)
// TODO: Drop doOnDiscard(LimitedDataBufferList.class, ...) (reactor-core#1924)
LimitedDataBufferConsumer limiter = new LimitedDataBufferConsumer(getMaxInMemorySize());
Flux<DataBuffer> inputFlux = Flux.from(input)
.flatMapIterable(buffer -> splitOnDelimiter(buffer, delimiterBytes))
.doOnNext(limiter)
.bufferUntil(buffer -> buffer == END_FRAME)
.map(StringDecoder::joinUntilEndFrame)
.doOnDiscard(LimitedDataBufferList.class, LimitedDataBufferList::releaseAndClear)
.doOnDiscard(PooledDataBuffer.class, DataBufferUtils::release);
return super.decode(inputFlux, elementType, mimeType, hints);
@@ -283,4 +292,35 @@ public final class StringDecoder extends AbstractDataBufferDecoder<String> {
new MimeType("text", "plain", DEFAULT_CHARSET), MimeTypeUtils.ALL);
}
/**
* Temporary measure for reactor-core#1925.
* Consumer that adds to a {@link LimitedDataBufferList} to enforce limits.
*/
private static class LimitedDataBufferConsumer implements Consumer<DataBuffer> {
private final LimitedDataBufferList bufferList;
public LimitedDataBufferConsumer(int maxInMemorySize) {
this.bufferList = new LimitedDataBufferList(maxInMemorySize);
}
@Override
public void accept(DataBuffer buffer) {
if (buffer == END_FRAME) {
this.bufferList.clear();
}
else {
try {
this.bufferList.add(buffer);
}
catch (DataBufferLimitException ex) {
DataBufferUtils.release(buffer);
throw ex;
}
}
}
}
}
@@ -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
@@ -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();
}
}
@@ -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 + "'");
}
}
@@ -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,20 @@ public class StringDecoderTests extends AbstractDecoderTestCase<StringDecoder> {
.verify());
}
@Test
public void decodeNewLineWithLimit() {
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
public void decodeNewLineIncludeDelimiters() {
this.decoder = StringDecoder.allMimeTypes(StringDecoder.DEFAULT_DELIMITERS, false);
@@ -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-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();
}
@@ -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.14.Final")
optional("javax.servlet:javax.servlet-api:3.1.0")
testCompile("org.aspectj:aspectjweaver:${aspectjVersion}")
testCompile("org.hsqldb:hsqldb:${hsqldbVersion}")
+1 -1
View File
@@ -61,7 +61,7 @@ dependencies {
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-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.
@@ -142,6 +142,12 @@ class DefaultWebTestClientBuilder implements WebTestClient.Builder {
return this;
}
@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;
@@ -77,6 +77,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
@@ -436,11 +437,25 @@ public interface WebTestClient {
/**
* Configure the {@link ExchangeStrategies} to use.
* <p>By default {@link ExchangeStrategies#withDefaults()} is used.
* <p>Note that in a scenario where the builder is configured by
* multiple parties, it is preferable to use
* {@link #exchangeStrategies(Consumer)} in order to customize the same
* {@code ExchangeStrategies}. This method here sets the strategies that
* everyone else then can customize.
* <p>By default this is {@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}.
* @since 5.1.12
*/
Builder exchangeStrategies(Consumer<ExchangeStrategies.Builder> configurer);
/**
* Max amount of time to wait for responses.
* <p>By default 5 seconds.
@@ -877,7 +892,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 +906,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,135 @@
/*
* 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.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.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.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();
}
@Test
public void controllerAdviceIsAppliedOnlyOnce() throws Exception {
assertEquals(0, SingletonControllerAdvice.counter.get());
assertEquals(0, RequestScopedControllerAdvice.counter.get());
this.mockMvc.perform(get("/test"))//
.andExpect(status().isOk())//
.andExpect(forwardedUrl("singleton:1;request-scoped:1"));
assertEquals(1, SingletonControllerAdvice.counter.get());
assertEquals(1, RequestScopedControllerAdvice.counter.get());
}
@Configuration
@EnableWebMvc
static class Config {
@Bean
TestController testController() {
return new TestController();
}
@Bean
SingletonControllerAdvice singletonControllerAdvice() {
return new SingletonControllerAdvice();
}
@Bean
@RequestScope
RequestScopedControllerAdvice requestScopedControllerAdvice() {
return new RequestScopedControllerAdvice();
}
}
@ControllerAdvice
static class SingletonControllerAdvice {
static final AtomicInteger counter = new AtomicInteger();
@ModelAttribute
void initModel(Model model) {
model.addAttribute("singleton", counter.incrementAndGet());
}
}
@ControllerAdvice
static class RequestScopedControllerAdvice {
static final AtomicInteger counter = new AtomicInteger();
@ModelAttribute
void initModel(Model model) {
model.addAttribute("request-scoped", counter.incrementAndGet());
}
}
@Controller
static class TestController {
@GetMapping("/test")
String get(Model model) {
return "singleton:" + model.asMap().get("singleton") + ";request-scoped:"
+ model.asMap().get("request-scoped");
}
}
}
@@ -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.4")
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.4")
testCompile("org.jetbrains.kotlin:kotlin-reflect:${kotlinVersion}")
testCompile("org.skyscreamer:jsonassert:1.5.0")
testCompile("org.xmlunit:xmlunit-matchers:2.6.2")
@@ -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}.
@@ -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
@@ -143,6 +154,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.
@@ -188,6 +215,38 @@ public interface CodecConfigurer {
* @param writer the writer to add
*/
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
* @since 5.1.12
*/
void withDefaultCodecConfig(Consumer<DefaultCodecConfig> codecsConfigConsumer);
}
/**
* Common options applied to default codecs and passed in a callback to custom codecs
* so they get a chance to align their behavior on the default ones.
* @since 5.1.12
*/
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.
*/
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,20 @@ 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}.
* @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);
@@ -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 + ")");
}
@@ -18,6 +18,7 @@ package org.springframework.http.codec.support;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import org.springframework.core.ResolvableType;
import org.springframework.core.codec.Decoder;
@@ -34,13 +35,16 @@ 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 boolean customCodecsInitialized;
private final DefaultCustomCodecs customCodecs = new DefaultCustomCodecs();
protected final BaseDefaultCodecs defaultCodecs;
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,6 +91,7 @@ class BaseCodecConfigurer implements CodecConfigurer {
@Override
public List<HttpMessageReader<?>> getReaders() {
initializeCustomCodecs();
List<HttpMessageReader<?>> result = new ArrayList<>();
result.addAll(this.defaultCodecs.getTypedReaders());
@@ -87,6 +109,7 @@ class BaseCodecConfigurer implements CodecConfigurer {
return getWritersInternal(false);
}
/**
* Internal method that returns the configured writers.
* @param forMultipart whether to returns writers for general use ("false"),
@@ -94,6 +117,7 @@ class BaseCodecConfigurer implements CodecConfigurer {
* same except for the multipart writer itself.
*/
protected List<HttpMessageWriter<?>> getWritersInternal(boolean forMultipart) {
initializeCustomCodecs();
List<HttpMessageWriter<?>> result = new ArrayList<>();
result.addAll(this.defaultCodecs.getTypedWriters(forMultipart));
@@ -106,11 +130,21 @@ class BaseCodecConfigurer implements CodecConfigurer {
return result;
}
@Override
public abstract CodecConfigurer clone();
private void initializeCustomCodecs() {
if(!this.customCodecsInitialized) {
this.customCodecs.configConsumers.forEach(consumer -> consumer.accept(this.defaultCodecs));
this.customCodecsInitialized = true;
}
}
/**
* 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<>();
@@ -120,6 +154,21 @@ class BaseCodecConfigurer implements CodecConfigurer {
private final List<HttpMessageWriter<?>> objectWriters = new ArrayList<>();
private final List<Consumer<DefaultCodecConfig>> configConsumers = new ArrayList<>();
DefaultCustomCodecs() {
}
/**
* Create a deep copy of the given {@link DefaultCustomCodecs}.
* @since 5.1.12
*/
DefaultCustomCodecs(DefaultCustomCodecs other) {
other.typedReaders.addAll(this.typedReaders);
other.typedWriters.addAll(this.typedWriters);
other.objectReaders.addAll(this.objectReaders);
other.objectWriters.addAll(this.objectWriters);
}
@Override
public void decoder(Decoder<?> decoder) {
@@ -143,6 +192,10 @@ class BaseCodecConfigurer implements CodecConfigurer {
(canWriteObject ? this.objectWriters : this.typedWriters).add(writer);
}
@Override
public void withDefaultCodecConfig(Consumer<DefaultCodecConfig> codecsConfigConsumer) {
this.configConsumers.add(codecsConfigConsumer);
}
// Package private accessors...
@@ -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.
@@ -20,6 +20,7 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.springframework.core.codec.AbstractDataBufferDecoder;
import org.springframework.core.codec.ByteArrayDecoder;
import org.springframework.core.codec.ByteArrayEncoder;
import org.springframework.core.codec.ByteBufferDecoder;
@@ -38,6 +39,7 @@ import org.springframework.http.codec.FormHttpMessageReader;
import org.springframework.http.codec.HttpMessageReader;
import org.springframework.http.codec.HttpMessageWriter;
import org.springframework.http.codec.ResourceHttpMessageWriter;
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;
@@ -57,7 +59,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;
@@ -95,11 +97,32 @@ class BaseDefaultCodecs implements CodecConfigurer.DefaultCodecs {
@Nullable
private Encoder<?> jaxb2Encoder;
@Nullable
private Integer maxInMemorySize;
private boolean enableLoggingRequestDetails = false;
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 +153,24 @@ 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
public boolean isEnableLoggingRequestDetails() {
return this.enableLoggingRequestDetails;
}
@@ -155,17 +190,20 @@ 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()));
readers.add(new DecoderHttpMessageReader<>(init(new ByteArrayDecoder())));
readers.add(new DecoderHttpMessageReader<>(init(new ByteBufferDecoder())));
readers.add(new DecoderHttpMessageReader<>(init(new DataBufferDecoder())));
readers.add(new DecoderHttpMessageReader<>(init(new ResourceDecoder())));
readers.add(new DecoderHttpMessageReader<>(init(StringDecoder.textPlainOnly())));
if (protobufPresent) {
Decoder<?> decoder = this.protobufDecoder != null ? this.protobufDecoder : new ProtobufDecoder();
Decoder<?> decoder = this.protobufDecoder != null ? this.protobufDecoder : init(new ProtobufDecoder());
readers.add(new DecoderHttpMessageReader<>(decoder));
}
FormHttpMessageReader formReader = new FormHttpMessageReader();
if (this.maxInMemorySize != null) {
formReader.setMaxInMemorySize(this.maxInMemorySize);
}
formReader.setEnableLoggingRequestDetails(this.enableLoggingRequestDetails);
readers.add(formReader);
@@ -174,6 +212,28 @@ class BaseDefaultCodecs implements CodecConfigurer.DefaultCodecs {
return readers;
}
private <T extends Decoder<?>> T init(T decoder) {
if (this.maxInMemorySize != null) {
if (decoder instanceof AbstractDataBufferDecoder) {
((AbstractDataBufferDecoder<?>) decoder).setMaxInMemorySize(this.maxInMemorySize);
}
if (decoder instanceof ProtobufDecoder) {
((ProtobufDecoder) decoder).setMaxMessageSize(this.maxInMemorySize);
}
if (jackson2Present) {
if (decoder instanceof AbstractJackson2Decoder) {
((AbstractJackson2Decoder) decoder).setMaxInMemorySize(this.maxInMemorySize);
}
}
if (jaxb2Present) {
if (decoder instanceof Jaxb2XmlDecoder) {
((Jaxb2XmlDecoder) decoder).setMaxInMemorySize(this.maxInMemorySize);
}
}
}
return decoder;
}
/**
* Hook for client or server specific typed readers.
*/
@@ -189,13 +249,13 @@ class BaseDefaultCodecs implements CodecConfigurer.DefaultCodecs {
}
List<HttpMessageReader<?>> readers = new ArrayList<>();
if (jackson2Present) {
readers.add(new DecoderHttpMessageReader<>(getJackson2JsonDecoder()));
readers.add(new DecoderHttpMessageReader<>(init(getJackson2JsonDecoder())));
}
if (jackson2SmilePresent) {
readers.add(new DecoderHttpMessageReader<>(new Jackson2SmileDecoder()));
readers.add(new DecoderHttpMessageReader<>(init(new Jackson2SmileDecoder())));
}
if (jaxb2Present) {
Decoder<?> decoder = this.jaxb2Decoder != null ? this.jaxb2Decoder : new Jaxb2XmlDecoder();
Decoder<?> decoder = this.jaxb2Decoder != null ? this.jaxb2Decoder : init(new Jaxb2XmlDecoder());
readers.add(new DecoderHttpMessageReader<>(decoder));
}
extendObjectReaders(readers);
@@ -216,7 +276,7 @@ class BaseDefaultCodecs implements CodecConfigurer.DefaultCodecs {
return Collections.emptyList();
}
List<HttpMessageReader<?>> result = new ArrayList<>();
result.add(new DecoderHttpMessageReader<>(StringDecoder.allMimeTypes()));
result.add(new DecoderHttpMessageReader<>(init(StringDecoder.allMimeTypes())));
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.
@@ -49,6 +49,17 @@ class ClientDefaultCodecsImpl extends BaseDefaultCodecs implements ClientCodecCo
private Supplier<List<HttpMessageWriter<?>>> partWritersSupplier;
ClientDefaultCodecsImpl() {
}
ClientDefaultCodecsImpl(ClientDefaultCodecsImpl other) {
super(other);
this.multipartCodecs = new DefaultMultipartCodecs(other.multipartCodecs);
this.sseDecoder = other.sseDecoder;
this.partWritersSupplier = other.partWritersSupplier;
}
/**
* Set a supplier for part writers to use when
* {@link #multipartCodecs()} are not explicitly configured.
@@ -73,6 +84,14 @@ 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) {
@@ -116,6 +135,17 @@ class ClientDefaultCodecsImpl extends BaseDefaultCodecs implements ClientCodecCo
private final List<HttpMessageWriter<?>> writers = new ArrayList<>();
DefaultMultipartCodecs() {
}
DefaultMultipartCodecs(@Nullable DefaultMultipartCodecs other) {
if (other != null) {
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.
@@ -26,14 +26,30 @@ import org.springframework.http.codec.ClientCodecConfigurer;
*/
public class DefaultClientCodecConfigurer extends BaseCodecConfigurer implements ClientCodecConfigurer {
public DefaultClientCodecConfigurer() {
super(new ClientDefaultCodecsImpl());
((ClientDefaultCodecsImpl) defaultCodecs()).setPartWritersSupplier(() -> getWritersInternal(true));
}
private DefaultClientCodecConfigurer(DefaultClientCodecConfigurer other) {
super(other);
}
@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());
}
}
@@ -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.
@@ -39,10 +39,28 @@ class ServerDefaultCodecsImpl extends BaseDefaultCodecs implements ServerCodecCo
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,10 +69,18 @@ class ServerDefaultCodecsImpl extends BaseDefaultCodecs implements ServerCodecCo
@Override
protected void extendTypedReaders(List<HttpMessageReader<?>> typedReaders) {
if (this.multipartReader != null) {
typedReaders.add(this.multipartReader);
return;
}
if (synchronossMultipartPresent) {
boolean enable = isEnableLoggingRequestDetails();
SynchronossPartHttpMessageReader partReader = new SynchronossPartHttpMessageReader();
Integer size = maxInMemorySize();
if (size != null) {
partReader.setMaxInMemorySize(size);
}
partReader.setEnableLoggingRequestDetails(enable);
typedReaders.add(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();
}
@@ -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 javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;
import org.springframework.http.converter.HttpMessageConversionException;
import org.springframework.util.Assert;
/**
* Abstract base class for {@link org.springframework.http.converter.HttpMessageConverter HttpMessageConverters}
@@ -106,19 +105,15 @@ public abstract class AbstractJaxb2HttpMessageConverter<T> extends AbstractXmlHt
* @throws HttpMessageConversionException in case of JAXB errors
*/
protected final JAXBContext getJaxbContext(Class<?> clazz) {
Assert.notNull(clazz, "Class must not be null");
JAXBContext jaxbContext = this.jaxbContexts.get(clazz);
if (jaxbContext == null) {
return this.jaxbContexts.computeIfAbsent(clazz, key -> {
try {
jaxbContext = JAXBContext.newInstance(clazz);
this.jaxbContexts.putIfAbsent(clazz, jaxbContext);
return JAXBContext.newInstance(clazz);
}
catch (JAXBException ex) {
throw new HttpMessageConversionException(
"Could not instantiate JAXBContext for class [" + clazz + "]: " + ex.getMessage(), ex);
"Could not create JAXBContext for class [" + clazz + "]: " + ex.getMessage(), ex);
}
}
return jaxbContext;
});
}
}
@@ -62,8 +62,17 @@ public abstract class AbstractListenerWriteProcessor<T> implements Processor<T,
@Nullable
private volatile T currentData;
/* Indicates "onComplete" was received during the (last) write. */
private volatile boolean subscriberCompleted;
/**
* Indicates we're waiting for one last isReady-onWritePossible cycle
* after "onComplete" because some Servlet containers expect this to take
* place prior to calling AsyncContext.complete().
* See https://github.com/eclipse-ee4j/servlet-api/issues/273
*/
private volatile boolean readyToCompleteAfterLastWrite;
private final WriteResultPublisher resultPublisher;
private final String logPrefix;
@@ -344,7 +353,8 @@ public abstract class AbstractListenerWriteProcessor<T> implements Processor<T,
}
@Override
public <T> void onComplete(AbstractListenerWriteProcessor<T> processor) {
processor.changeStateToComplete(this);
processor.readyToCompleteAfterLastWrite = true;
processor.changeStateToReceived(this);
}
},
@@ -352,7 +362,10 @@ public abstract class AbstractListenerWriteProcessor<T> implements Processor<T,
@SuppressWarnings("deprecation")
@Override
public <T> void onWritePossible(AbstractListenerWriteProcessor<T> processor) {
if (processor.changeState(this, WRITING)) {
if (processor.readyToCompleteAfterLastWrite) {
processor.changeStateToComplete(RECEIVED);
}
else if (processor.changeState(this, WRITING)) {
T data = processor.currentData;
Assert.state(data != null, "No data");
try {
@@ -360,7 +373,8 @@ public abstract class AbstractListenerWriteProcessor<T> implements Processor<T,
if (processor.changeState(WRITING, REQUESTED)) {
processor.currentData = null;
if (processor.subscriberCompleted) {
processor.changeStateToComplete(REQUESTED);
processor.readyToCompleteAfterLastWrite = true;
processor.changeStateToReceived(REQUESTED);
}
else {
processor.writingPaused();
@@ -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.
@@ -36,49 +36,55 @@ import org.springframework.web.context.ServletContextAware;
/**
* Factory to create a {@code ContentNegotiationManager} and configure it with
* one or more {@link ContentNegotiationStrategy} instances.
* {@link ContentNegotiationStrategy} instances.
*
* <p>As of 5.0 you can set the exact strategies to use via
* {@link #setStrategies(List)}.
*
* <p>As an alternative you can also rely on the set of defaults described below
* which can be turned on or off or customized through the methods of this
* builder:
* <p>This factory offers properties that in turn result in configuring the
* underlying strategies. The table below shows the property names, their
* default settings, as well as the strategies that they help to configure:
*
* <table>
* <tr>
* <th>Property Setter</th>
* <th>Default Value</th>
* <th>Underlying Strategy</th>
* <th>Default Setting</th>
* <th>Enabled Or Not</th>
* </tr>
* <tr>
* <td>{@link #setFavorPathExtension}</td>
* <td>{@link PathExtensionContentNegotiationStrategy Path Extension strategy}</td>
* <td>On</td>
* <td>{@link #setFavorPathExtension favorPathExtension}</td>
* <td>true</td>
* <td>{@link PathExtensionContentNegotiationStrategy}</td>
* <td>Enabled</td>
* </tr>
* <tr>
* <td>{@link #setFavorParameter favorParameter}</td>
* <td>{@link ParameterContentNegotiationStrategy Parameter strategy}</td>
* <td>false</td>
* <td>{@link ParameterContentNegotiationStrategy}</td>
* <td>Off</td>
* </tr>
* <tr>
* <td>{@link #setIgnoreAcceptHeader ignoreAcceptHeader}</td>
* <td>{@link HeaderContentNegotiationStrategy Header strategy}</td>
* <td>On</td>
* <td>false</td>
* <td>{@link HeaderContentNegotiationStrategy}</td>
* <td>Enabled</td>
* </tr>
* <tr>
* <td>{@link #setDefaultContentType defaultContentType}</td>
* <td>{@link FixedContentNegotiationStrategy Fixed content strategy}</td>
* <td>Not set</td>
* <td>null</td>
* <td>{@link FixedContentNegotiationStrategy}</td>
* <td>Off</td>
* </tr>
* <tr>
* <td>{@link #setDefaultContentTypeStrategy defaultContentTypeStrategy}</td>
* <td>null</td>
* <td>{@link ContentNegotiationStrategy}</td>
* <td>Not set</td>
* <td>Off</td>
* </tr>
* </table>
*
* <strong>Note:</strong> if you must use URL-based content type resolution,
* <p>As of 5.0 you can set the exact strategies to use via
* {@link #setStrategies(List)}.
*
* <p><strong>Note:</strong> if you must use URL-based content type resolution,
* the use of a query parameter is simpler and preferable to the use of a path
* extension since the latter can cause issues with URI variables, path
* parameters, and URI decoding. Consider setting {@link #setFavorPathExtension}
@@ -154,22 +154,27 @@ public class GroovyWebApplicationContext extends AbstractRefreshableWebApplicati
// 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) {
this.metaClass.setProperty(this, property, newValue);
}
@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.
@@ -20,6 +20,7 @@ import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import org.springframework.aop.scope.ScopedProxyUtils;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryUtils;
import org.springframework.context.ApplicationContext;
@@ -42,6 +43,7 @@ import org.springframework.web.bind.annotation.ControllerAdvice;
* @author Rossen Stoyanchev
* @author Brian Clozel
* @author Juergen Hoeller
* @author Sam Brannen
* @since 3.2
*/
public class ControllerAdviceBean implements Ordered {
@@ -187,6 +189,7 @@ public class ControllerAdviceBean implements Ordered {
*/
public static List<ControllerAdviceBean> findAnnotatedBeans(ApplicationContext context) {
return Arrays.stream(BeanFactoryUtils.beanNamesForTypeIncludingAncestors(context, Object.class))
.filter(name -> !ScopedProxyUtils.isScopedTarget(name))
.filter(name -> context.findAnnotationOnBean(name, ControllerAdvice.class) != null)
.map(name -> new ControllerAdviceBean(name, context))
.collect(Collectors.toList());
@@ -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,12 +19,15 @@ package org.springframework.web.server;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
/**
* Exception for errors that fit response status 405 (method not allowed).
@@ -37,7 +40,7 @@ public class MethodNotAllowedException extends ResponseStatusException {
private final String method;
private final Set<HttpMethod> supportedMethods;
private final Set<HttpMethod> httpMethods;
public MethodNotAllowedException(HttpMethod method, Collection<HttpMethod> supportedMethods) {
@@ -51,10 +54,21 @@ public class MethodNotAllowedException extends ResponseStatusException {
supportedMethods = Collections.emptySet();
}
this.method = method;
this.supportedMethods = Collections.unmodifiableSet(new HashSet<>(supportedMethods));
this.httpMethods = Collections.unmodifiableSet(new HashSet<>(supportedMethods));
}
/**
* Return a Map with an "Allow" header.
* @since 5.1.11
*/
@Override
public Map<String, String> getHeaders() {
return !CollectionUtils.isEmpty(this.httpMethods) ?
Collections.singletonMap("Allow", StringUtils.collectionToDelimitedString(this.httpMethods, ", ")) :
Collections.emptyMap();
}
/**
* Return the HTTP method for the failed request.
*/
@@ -66,6 +80,7 @@ public class MethodNotAllowedException extends ResponseStatusException {
* Return the list of supported HTTP methods.
*/
public Set<HttpMethod> getSupportedMethods() {
return this.supportedMethods;
return this.httpMethods;
}
}
@@ -18,9 +18,11 @@ package org.springframework.web.server;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.util.CollectionUtils;
/**
* Exception for errors that fit response status 406 (not acceptable).
@@ -51,6 +53,17 @@ public class NotAcceptableStatusException extends ResponseStatusException {
}
/**
* Return a Map with an "Accept" header, or an empty map.
* @since 5.1.11
*/
@Override
public Map<String, String> getHeaders() {
return !CollectionUtils.isEmpty(this.supportedMediaTypes) ?
Collections.singletonMap("Accept", MediaType.toString(this.supportedMediaTypes)) :
Collections.emptyMap();
}
/**
* Return the list of supported content types in cases when the Accept
* header is parsed but not supported, or an empty list otherwise.
@@ -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,6 +16,9 @@
package org.springframework.web.server;
import java.util.Collections;
import java.util.Map;
import org.springframework.core.NestedExceptionUtils;
import org.springframework.core.NestedRuntimeException;
import org.springframework.http.HttpStatus;
@@ -72,12 +75,21 @@ public class ResponseStatusException extends NestedRuntimeException {
/**
* The HTTP status that fits the exception (never {@code null}).
* Return the HTTP status associated with this exception.
*/
public HttpStatus getStatus() {
return this.status;
}
/**
* Return response headers associated with the exception, possibly required
* for the given status code (e.g. "Allow", "Accept").
* @since 5.1.11
*/
public Map<String, String> getHeaders() {
return Collections.emptyMap();
}
/**
* The reason explaining the exception (potentially {@code null} or empty).
*/
@@ -86,6 +98,7 @@ public class ResponseStatusException extends NestedRuntimeException {
return this.reason;
}
@Override
public String getMessage() {
String msg = this.status + (this.reason != null ? " \"" + this.reason + "\"" : "");
@@ -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.
@@ -22,6 +22,7 @@ import reactor.core.publisher.Mono;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.lang.Nullable;
import org.springframework.web.server.ResponseStatusException;
import org.springframework.web.server.ServerWebExchange;
@@ -62,8 +63,7 @@ public class ResponseStatusExceptionHandler implements WebExceptionHandler {
@Override
public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {
HttpStatus status = resolveStatus(ex);
if (status == null || !exchange.getResponse().setStatusCode(status)) {
if (!updateResponse(exchange.getResponse(), ex)) {
return Mono.error(ex);
}
@@ -86,16 +86,25 @@ public class ResponseStatusExceptionHandler implements WebExceptionHandler {
return "Resolved [" + reason + "] for HTTP " + request.getMethod() + " " + path;
}
@Nullable
private HttpStatus resolveStatus(Throwable ex) {
private boolean updateResponse(ServerHttpResponse response, Throwable ex) {
boolean result = false;
HttpStatus status = determineStatus(ex);
if (status == null) {
Throwable cause = ex.getCause();
if (cause != null) {
status = resolveStatus(cause);
if (status != null) {
if (response.setStatusCode(status)) {
if (ex instanceof ResponseStatusException) {
((ResponseStatusException) ex).getHeaders()
.forEach((name, value) -> response.getHeaders().add(name, value));
}
result = true;
}
}
return status;
else {
Throwable cause = ex.getCause();
if (cause != null) {
result = updateResponse(response, cause);
}
}
return result;
}
/**
@@ -85,6 +85,31 @@ public class ResponseCookieTests {
});
}
@Test
public void domainChecks() {
Arrays.asList("abc", "abc.org", "abc-def.org", "abc3.org", ".abc.org")
.forEach(domain -> ResponseCookie.from("n", "v").domain(domain).build());
Arrays.asList("-abc.org", "abc.org.", "abc.org-", "-abc.org", "abc.org-")
.forEach(domain -> {
try {
ResponseCookie.from("n", "v").domain(domain).build();
}
catch (IllegalArgumentException ex) {
assertThat(ex.getMessage(), Matchers.containsString("Invalid first/last char"));
}
});
Arrays.asList("abc..org", "abc.-org", "abc-.org")
.forEach(domain -> {
try {
ResponseCookie.from("n", "v").domain(domain).build();
}
catch (IllegalArgumentException ex) {
assertThat(ex.getMessage(), Matchers.containsString("invalid cookie domain char"));
}
});
}
}
@@ -20,7 +20,6 @@ import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.function.Consumer;
import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.TreeNode;
@@ -36,6 +35,7 @@ import reactor.test.StepVerifier;
import org.springframework.core.codec.DecodingException;
import org.springframework.core.io.buffer.AbstractLeakCheckingTestCase;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferLimitException;
import static java.util.Arrays.*;
import static java.util.Collections.*;
@@ -181,11 +181,68 @@ public class Jackson2TokenizerTests extends AbstractLeakCheckingTestCase {
testTokenize(asList("[1", ",2,", "3]"), asList("1", "2", "3"), true);
}
private void testTokenize(List<String> input, List<String> output, boolean tokenize) {
StepVerifier.FirstStep<String> builder = StepVerifier.create(decode(input, tokenize, -1));
output.forEach(expected -> builder.assertNext(actual -> {
try {
JSONAssert.assertEquals(expected, actual, true);
}
catch (JSONException ex) {
throw new RuntimeException(ex);
}
}));
builder.verifyComplete();
}
@Test
public void testLimit() {
List<String> source = asList("[",
"{", "\"id\":1,\"name\":\"Dan\"", "},",
"{", "\"id\":2,\"name\":\"Ron\"", "},",
"{", "\"id\":3,\"name\":\"Bartholomew\"", "}",
"]");
String expected = String.join("", source);
int maxInMemorySize = expected.length();
StepVerifier.create(decode(source, false, maxInMemorySize))
.expectNext(expected)
.verifyComplete();
StepVerifier.create(decode(source, false, maxInMemorySize - 2))
.verifyError(DataBufferLimitException.class);
}
@Test
public void testLimitTokenized() {
List<String> source = asList("[",
"{", "\"id\":1, \"name\":\"Dan\"", "},",
"{", "\"id\":2, \"name\":\"Ron\"", "},",
"{", "\"id\":3, \"name\":\"Bartholomew\"", "}",
"]");
String expected = "{\"id\":3,\"name\":\"Bartholomew\"}";
int maxInMemorySize = expected.length();
StepVerifier.create(decode(source, true, maxInMemorySize))
.expectNext("{\"id\":1,\"name\":\"Dan\"}")
.expectNext("{\"id\":2,\"name\":\"Ron\"}")
.expectNext(expected)
.verifyComplete();
StepVerifier.create(decode(source, true, maxInMemorySize - 1))
.expectNext("{\"id\":1,\"name\":\"Dan\"}")
.expectNext("{\"id\":2,\"name\":\"Ron\"}")
.verifyError(DataBufferLimitException.class);
}
@Test
public void errorInStream() {
DataBuffer buffer = stringBuffer("{\"id\":1,\"name\":");
Flux<DataBuffer> source = Flux.just(buffer).concatWith(Flux.error(new RuntimeException()));
Flux<TokenBuffer> result = Jackson2Tokenizer.tokenize(source, this.jsonFactory, this.objectMapper, true);
Flux<TokenBuffer> result = Jackson2Tokenizer.tokenize(source, this.jsonFactory, this.objectMapper, true, -1);
StepVerifier.create(result)
.expectError(RuntimeException.class)
@@ -195,7 +252,7 @@ public class Jackson2TokenizerTests extends AbstractLeakCheckingTestCase {
@Test // SPR-16521
public void jsonEOFExceptionIsWrappedAsDecodingError() {
Flux<DataBuffer> source = Flux.just(stringBuffer("{\"status\": \"noClosingQuote}"));
Flux<TokenBuffer> tokens = Jackson2Tokenizer.tokenize(source, this.jsonFactory, this.objectMapper, false);
Flux<TokenBuffer> tokens = Jackson2Tokenizer.tokenize(source, this.jsonFactory, this.objectMapper, false, -1);
StepVerifier.create(tokens)
.expectError(DecodingException.class)
@@ -203,12 +260,13 @@ public class Jackson2TokenizerTests extends AbstractLeakCheckingTestCase {
}
private void testTokenize(List<String> source, List<String> expected, boolean tokenizeArrayElements) {
private Flux<String> decode(List<String> source, boolean tokenize, int maxInMemorySize) {
Flux<TokenBuffer> tokens = Jackson2Tokenizer.tokenize(
Flux.fromIterable(source).map(this::stringBuffer),
this.jsonFactory, this.objectMapper, tokenizeArrayElements);
this.jsonFactory, this.objectMapper, tokenize, maxInMemorySize);
Flux<String> result = tokens
return tokens
.map(tokenBuffer -> {
try {
TreeNode root = this.objectMapper.readTree(tokenBuffer.asParser());
@@ -218,10 +276,6 @@ public class Jackson2TokenizerTests extends AbstractLeakCheckingTestCase {
throw new UncheckedIOException(ex);
}
});
StepVerifier.FirstStep<String> builder = StepVerifier.create(result);
expected.forEach(s -> builder.assertNext(new JSONAssertConsumer(s)));
builder.verifyComplete();
}
private DataBuffer stringBuffer(String value) {
@@ -231,24 +285,4 @@ public class Jackson2TokenizerTests extends AbstractLeakCheckingTestCase {
return buffer;
}
private static class JSONAssertConsumer implements Consumer<String> {
private final String expected;
JSONAssertConsumer(String expected) {
this.expected = expected;
}
@Override
public void accept(String s) {
try {
JSONAssert.assertEquals(this.expected, s, true);
}
catch (JSONException ex) {
throw new RuntimeException(ex);
}
}
}
}
@@ -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,15 +17,21 @@
package org.springframework.http.codec.multipart;
import java.io.File;
import java.io.IOException;
import java.nio.channels.ReadableByteChannel;
import java.time.Duration;
import java.util.Map;
import java.util.function.Consumer;
import org.junit.Test;
import org.reactivestreams.Subscription;
import reactor.core.publisher.BaseSubscriber;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import org.springframework.core.ResolvableType;
import org.springframework.core.codec.DecodingException;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferUtils;
@@ -38,23 +44,32 @@ import org.springframework.mock.http.client.reactive.test.MockClientHttpRequest;
import org.springframework.mock.http.server.reactive.test.MockServerHttpRequest;
import org.springframework.util.MultiValueMap;
import static java.util.Collections.*;
import static org.junit.Assert.*;
import static org.springframework.core.ResolvableType.*;
import static org.springframework.http.HttpHeaders.*;
import static org.springframework.http.MediaType.*;
import static java.util.Collections.emptyMap;
import static java.util.Collections.singletonMap;
import static org.hamcrest.core.StringStartsWith.startsWith;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.springframework.core.ResolvableType.forClassWithGenerics;
import static org.springframework.http.HttpHeaders.CONTENT_TYPE;
import static org.springframework.http.MediaType.MULTIPART_FORM_DATA;
/**
* Unit tests for {@link SynchronossPartHttpMessageReader}.
*
* @author Sebastien Deleuze
* @author Rossen Stoyanchev
* @author Brian Clozel
*/
public class SynchronossPartHttpMessageReaderTests {
private final MultipartHttpMessageReader reader =
new MultipartHttpMessageReader(new SynchronossPartHttpMessageReader());
private static final ResolvableType PARTS_ELEMENT_TYPE =
forClassWithGenerics(MultiValueMap.class, String.class, Part.class);
@Test
public void canRead() {
@@ -86,10 +101,10 @@ public class SynchronossPartHttpMessageReaderTests {
MultiValueMap<String, Part> parts = this.reader.readMono(elementType, request, emptyMap()).block();
assertEquals(2, parts.size());
assertTrue(parts.containsKey("fooPart"));
Part part = parts.getFirst("fooPart");
assertTrue(parts.containsKey("filePart"));
Part part = parts.getFirst("filePart");
assertTrue(part instanceof FilePart);
assertEquals("fooPart", part.name());
assertEquals("filePart", part.name());
assertEquals("foo.txt", ((FilePart) part).filename());
DataBuffer buffer = DataBufferUtils.join(part.content()).block();
assertEquals(12, buffer.readableByteCount());
@@ -97,24 +112,23 @@ public class SynchronossPartHttpMessageReaderTests {
buffer.read(byteContent);
assertEquals("Lorem Ipsum.", new String(byteContent));
assertTrue(parts.containsKey("barPart"));
part = parts.getFirst("barPart");
assertTrue(parts.containsKey("textPart"));
part = parts.getFirst("textPart");
assertTrue(part instanceof FormFieldPart);
assertEquals("barPart", part.name());
assertEquals("bar", ((FormFieldPart) part).value());
assertEquals("textPart", part.name());
assertEquals("sample-text", ((FormFieldPart) part).value());
}
@Test // SPR-16545
public void transferTo() {
public void transferTo() throws IOException {
ServerHttpRequest request = generateMultipartRequest();
ResolvableType elementType = forClassWithGenerics(MultiValueMap.class, String.class, Part.class);
MultiValueMap<String, Part> parts = this.reader.readMono(elementType, request, emptyMap()).block();
MultiValueMap<String, Part> parts = this.reader.readMono(PARTS_ELEMENT_TYPE, request, emptyMap()).block();
assertNotNull(parts);
FilePart part = (FilePart) parts.getFirst("fooPart");
FilePart part = (FilePart) parts.getFirst("filePart");
assertNotNull(part);
File dest = new File(System.getProperty("java.io.tmpdir") + "/" + part.filename());
File dest = File.createTempFile(part.filename(), "multipart");
part.transferTo(dest).block(Duration.ofSeconds(5));
assertTrue(dest.exists());
@@ -125,22 +139,83 @@ public class SynchronossPartHttpMessageReaderTests {
@Test
public void bodyError() {
ServerHttpRequest request = generateErrorMultipartRequest();
ResolvableType elementType = forClassWithGenerics(MultiValueMap.class, String.class, Part.class);
StepVerifier.create(this.reader.readMono(elementType, request, emptyMap())).verifyError();
StepVerifier.create(this.reader.readMono(PARTS_ELEMENT_TYPE, request, emptyMap())).verifyError();
}
@Test
public void readPartsWithoutDemand() {
ServerHttpRequest request = generateMultipartRequest();
Mono<MultiValueMap<String, Part>> parts = this.reader.readMono(PARTS_ELEMENT_TYPE, request, emptyMap());
ZeroDemandSubscriber subscriber = new ZeroDemandSubscriber();
parts.subscribe(subscriber);
subscriber.cancel();
}
@Test
public void gh23768() throws IOException {
ReadableByteChannel channel = new ClassPathResource("invalid.multipart", getClass()).readableChannel();
Flux<DataBuffer> body = DataBufferUtils.readByteChannel(() -> channel, new DefaultDataBufferFactory(), 1024);
MediaType contentType = new MediaType("multipart", "form-data",
singletonMap("boundary", "NbjrKgjbsaMLdnMxMfDpD6myWomYc0qNX0w"));
ServerHttpRequest request = MockServerHttpRequest.post("/")
.contentType(contentType)
.body(body);
Mono<MultiValueMap<String, Part>> parts = this.reader.readMono(PARTS_ELEMENT_TYPE, request, emptyMap());
StepVerifier.create(parts)
.assertNext(result -> assertTrue(result.isEmpty()))
.verifyComplete();
}
@Test
public void readTooManyParts() {
testMultipartExceptions(reader -> reader.setMaxParts(1), ex -> {
assertEquals(DecodingException.class, ex.getClass());
assertThat(ex.getMessage(), startsWith("Failure while parsing part[2]"));
assertEquals("Too many parts (2 allowed)", ex.getCause().getMessage());
});
}
@Test
public void readFilePartTooBig() {
testMultipartExceptions(reader -> reader.setMaxDiskUsagePerPart(5), ex -> {
assertEquals(DecodingException.class, ex.getClass());
assertThat(ex.getMessage(), startsWith("Failure while parsing part[1]"));
assertEquals("Part[1] exceeded the disk usage limit of 5 bytes", ex.getCause().getMessage());
});
}
@Test
public void readPartHeadersTooBig() {
testMultipartExceptions(reader -> reader.setMaxInMemorySize(1), ex -> {
assertEquals(DecodingException.class, ex.getClass());
assertThat(ex.getMessage(), startsWith("Failure while parsing part[1]"));
assertEquals("Part[1] exceeded the in-memory limit of 1 bytes", ex.getCause().getMessage());
});
}
private void testMultipartExceptions(
Consumer<SynchronossPartHttpMessageReader> configurer, Consumer<Throwable> assertions) {
SynchronossPartHttpMessageReader reader = new SynchronossPartHttpMessageReader();
configurer.accept(reader);
MultipartHttpMessageReader multipartReader = new MultipartHttpMessageReader(reader);
StepVerifier.create(multipartReader.readMono(PARTS_ELEMENT_TYPE, generateMultipartRequest(), emptyMap()))
.consumeErrorWith(assertions)
.verify();
}
private ServerHttpRequest generateMultipartRequest() {
MultipartBodyBuilder partsBuilder = new MultipartBodyBuilder();
partsBuilder.part("fooPart", new ClassPathResource("org/springframework/http/codec/multipart/foo.txt"));
partsBuilder.part("barPart", "bar");
partsBuilder.part("filePart", new ClassPathResource("org/springframework/http/codec/multipart/foo.txt"));
partsBuilder.part("textPart", "sample-text");
MockClientHttpRequest outputMessage = new MockClientHttpRequest(HttpMethod.POST, "/");
new MultipartHttpMessageWriter()
.write(Mono.just(partsBuilder.build()), null, MediaType.MULTIPART_FORM_DATA, outputMessage, null)
.block(Duration.ofSeconds(5));
return MockServerHttpRequest.post("/")
.contentType(outputMessage.getHeaders().getContentType())
.body(outputMessage.getBody());
@@ -152,4 +227,12 @@ public class SynchronossPartHttpMessageReaderTests {
.body(Flux.just(new DefaultDataBufferFactory().wrap("invalid content".getBytes())));
}
private static class ZeroDemandSubscriber extends BaseSubscriber<MultiValueMap<String, Part>> {
@Override
protected void hookOnSubscribe(Subscription subscription) {
// Just subscribe without requesting
}
}
}
@@ -22,6 +22,7 @@ import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import org.junit.Test;
import reactor.core.publisher.Flux;
@@ -59,7 +60,11 @@ import org.springframework.http.codec.xml.Jaxb2XmlDecoder;
import org.springframework.http.codec.xml.Jaxb2XmlEncoder;
import org.springframework.util.MimeTypeUtils;
import static org.junit.Assert.*;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.springframework.core.ResolvableType.forClass;
/**
@@ -122,6 +127,47 @@ public class ClientCodecConfigurerTests {
.filter(e -> e == decoder).orElse(null));
}
@Test
public void cloneConfigurer() {
ClientCodecConfigurer clone = this.configurer.clone();
Jackson2JsonDecoder jackson2Decoder = new Jackson2JsonDecoder();
clone.defaultCodecs().serverSentEventDecoder(jackson2Decoder);
clone.defaultCodecs().multipartCodecs().encoder(new Jackson2SmileEncoder());
clone.defaultCodecs().multipartCodecs().writer(new ResourceHttpMessageWriter());
// Clone has the customizations
Decoder<?> sseDecoder = clone.getReaders().stream()
.filter(reader -> reader instanceof ServerSentEventHttpMessageReader)
.map(reader -> ((ServerSentEventHttpMessageReader) reader).getDecoder())
.findFirst()
.get();
List<HttpMessageWriter<?>> multipartWriters = clone.getWriters().stream()
.filter(writer -> writer instanceof MultipartHttpMessageWriter)
.flatMap(writer -> ((MultipartHttpMessageWriter) writer).getPartWriters().stream())
.collect(Collectors.toList());
assertSame(jackson2Decoder, sseDecoder);
assertEquals(2, multipartWriters.size());
// Original does not have the customizations
sseDecoder = this.configurer.getReaders().stream()
.filter(reader -> reader instanceof ServerSentEventHttpMessageReader)
.map(reader -> ((ServerSentEventHttpMessageReader) reader).getDecoder())
.findFirst()
.get();
multipartWriters = this.configurer.getWriters().stream()
.filter(writer -> writer instanceof MultipartHttpMessageWriter)
.flatMap(writer -> ((MultipartHttpMessageWriter) writer).getPartWriters().stream())
.collect(Collectors.toList());
assertNotSame(jackson2Decoder, sseDecoder);
assertEquals(10, multipartWriters.size());
}
private Decoder<?> getNextDecoder(List<HttpMessageReader<?>> readers) {
HttpMessageReader<?> reader = readers.get(this.index.getAndIncrement());
@@ -16,8 +16,11 @@
package org.springframework.http.codec.support;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import com.google.protobuf.ExtensionRegistry;
import org.junit.Test;
@@ -42,6 +45,8 @@ import org.springframework.http.codec.FormHttpMessageReader;
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.ServerSentEventHttpMessageWriter;
import org.springframework.http.codec.json.Jackson2JsonDecoder;
import org.springframework.http.codec.json.Jackson2JsonEncoder;
import org.springframework.http.codec.json.Jackson2SmileDecoder;
@@ -267,6 +272,83 @@ public class CodecConfigurerTests {
assertEncoderInstance(jaxb2Encoder);
}
@Test
public void cloneCustomCodecs() {
this.configurer.registerDefaults(false);
CodecConfigurer clone = this.configurer.clone();
clone.customCodecs().encoder(new Jackson2JsonEncoder());
clone.customCodecs().decoder(new Jackson2JsonDecoder());
clone.customCodecs().reader(new ServerSentEventHttpMessageReader());
clone.customCodecs().writer(new ServerSentEventHttpMessageWriter());
assertEquals(0, this.configurer.getReaders().size());
assertEquals(0, this.configurer.getWriters().size());
assertEquals(2, clone.getReaders().size());
assertEquals(2, clone.getWriters().size());
}
@Test
public void cloneDefaultCodecs() {
CodecConfigurer clone = this.configurer.clone();
Jackson2JsonDecoder jacksonDecoder = new Jackson2JsonDecoder();
Jackson2JsonEncoder jacksonEncoder = new Jackson2JsonEncoder();
Jaxb2XmlDecoder jaxb2Decoder = new Jaxb2XmlDecoder();
Jaxb2XmlEncoder jaxb2Encoder = new Jaxb2XmlEncoder();
ProtobufDecoder protoDecoder = new ProtobufDecoder();
ProtobufEncoder protoEncoder = new ProtobufEncoder();
clone.defaultCodecs().jackson2JsonDecoder(jacksonDecoder);
clone.defaultCodecs().jackson2JsonEncoder(jacksonEncoder);
clone.defaultCodecs().jaxb2Decoder(jaxb2Decoder);
clone.defaultCodecs().jaxb2Encoder(jaxb2Encoder);
clone.defaultCodecs().protobufDecoder(protoDecoder);
clone.defaultCodecs().protobufEncoder(protoEncoder);
// Clone has the customized the customizations
List<Decoder<?>> decoders = clone.getReaders().stream()
.filter(reader -> reader instanceof DecoderHttpMessageReader)
.map(reader -> ((DecoderHttpMessageReader<?>) reader).getDecoder())
.collect(Collectors.toList());
List<Encoder<?>> encoders = clone.getWriters().stream()
.filter(writer -> writer instanceof EncoderHttpMessageWriter)
.map(reader -> ((EncoderHttpMessageWriter<?>) reader).getEncoder())
.collect(Collectors.toList());
assertTrue(decoders.containsAll(Arrays.asList(jacksonDecoder, jaxb2Decoder, protoDecoder)));
assertTrue(encoders.containsAll(Arrays.asList(jacksonEncoder, jaxb2Encoder, protoEncoder)));
// Original does not have the customizations
decoders = this.configurer.getReaders().stream()
.filter(reader -> reader instanceof DecoderHttpMessageReader)
.map(reader -> ((DecoderHttpMessageReader<?>) reader).getDecoder())
.collect(Collectors.toList());
encoders = this.configurer.getWriters().stream()
.filter(writer -> writer instanceof EncoderHttpMessageWriter)
.map(reader -> ((EncoderHttpMessageWriter<?>) reader).getEncoder())
.collect(Collectors.toList());
assertFalse(decoders.containsAll(Arrays.asList(jacksonDecoder, jaxb2Decoder, protoDecoder)));
assertFalse(encoders.containsAll(Arrays.asList(jacksonEncoder, jaxb2Encoder, protoEncoder)));
}
@Test
public void withDefaultCodecConfig() {
AtomicBoolean callbackCalled = new AtomicBoolean(false);
this.configurer.defaultCodecs().enableLoggingRequestDetails(true);
this.configurer.customCodecs().withDefaultCodecConfig(config -> {
assertTrue(config.isEnableLoggingRequestDetails());
callbackCalled.compareAndSet(false, true);
});
this.configurer.getReaders();
assertTrue(callbackCalled.get());
}
private Decoder<?> getNextDecoder(List<HttpMessageReader<?>> readers) {
HttpMessageReader<?> reader = readers.get(this.index.getAndIncrement());
assertEquals(DecoderHttpMessageReader.class, reader.getClass());
@@ -313,10 +395,21 @@ public class CodecConfigurerTests {
private static class TestCodecConfigurer extends BaseCodecConfigurer {
TestCodecConfigurer() {
super(new TestDefaultCodecs());
super(new BaseDefaultCodecs());
}
private static class TestDefaultCodecs extends BaseDefaultCodecs {
TestCodecConfigurer(TestCodecConfigurer other) {
super(other);
}
@Override
protected BaseDefaultCodecs cloneDefaultCodecs() {
return new BaseDefaultCodecs((BaseDefaultCodecs) defaultCodecs());
}
@Override
public CodecConfigurer clone() {
return new TestCodecConfigurer(this);
}
}
@@ -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.
@@ -60,7 +60,11 @@ import org.springframework.http.codec.xml.Jaxb2XmlDecoder;
import org.springframework.http.codec.xml.Jaxb2XmlEncoder;
import org.springframework.util.MimeTypeUtils;
import static org.junit.Assert.*;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.springframework.core.ResolvableType.forClass;
/**
@@ -124,13 +128,85 @@ public class ServerCodecConfigurerTests {
.filter(e -> e == encoder).orElse(null));
}
@Test
public void maxInMemorySize() {
int size = 99;
this.configurer.defaultCodecs().maxInMemorySize(size);
List<HttpMessageReader<?>> readers = this.configurer.getReaders();
assertEquals(13, readers.size());
assertEquals(size, ((ByteArrayDecoder) getNextDecoder(readers)).getMaxInMemorySize());
assertEquals(size, ((ByteBufferDecoder) getNextDecoder(readers)).getMaxInMemorySize());
assertEquals(size, ((DataBufferDecoder) getNextDecoder(readers)).getMaxInMemorySize());
assertEquals(size, ((ResourceDecoder) getNextDecoder(readers)).getMaxInMemorySize());
assertEquals(size, ((StringDecoder) getNextDecoder(readers)).getMaxInMemorySize());
assertEquals(size, ((ProtobufDecoder) getNextDecoder(readers)).getMaxMessageSize());
assertEquals(size, ((FormHttpMessageReader) nextReader(readers)).getMaxInMemorySize());
assertEquals(size, ((SynchronossPartHttpMessageReader) nextReader(readers)).getMaxInMemorySize());
MultipartHttpMessageReader multipartReader = (MultipartHttpMessageReader) nextReader(readers);
SynchronossPartHttpMessageReader reader = (SynchronossPartHttpMessageReader) multipartReader.getPartReader();
assertEquals(size, (reader).getMaxInMemorySize());
assertEquals(size, ((Jackson2JsonDecoder) getNextDecoder(readers)).getMaxInMemorySize());
assertEquals(size, ((Jackson2SmileDecoder) getNextDecoder(readers)).getMaxInMemorySize());
assertEquals(size, ((Jaxb2XmlDecoder) getNextDecoder(readers)).getMaxInMemorySize());
assertEquals(size, ((StringDecoder) getNextDecoder(readers)).getMaxInMemorySize());
}
@Test
public void cloneConfigurer() {
ServerCodecConfigurer clone = this.configurer.clone();
MultipartHttpMessageReader reader = new MultipartHttpMessageReader(new SynchronossPartHttpMessageReader());
Jackson2JsonEncoder encoder = new Jackson2JsonEncoder();
clone.defaultCodecs().multipartReader(reader);
clone.defaultCodecs().serverSentEventEncoder(encoder);
// Clone has the customizations
HttpMessageReader<?> actualReader = clone.getReaders().stream()
.filter(r -> r instanceof MultipartHttpMessageReader)
.findFirst()
.get();
Encoder<?> actualEncoder = clone.getWriters().stream()
.filter(writer -> writer instanceof ServerSentEventHttpMessageWriter)
.map(writer -> ((ServerSentEventHttpMessageWriter) writer).getEncoder())
.findFirst()
.get();
assertSame(reader, actualReader);
assertSame(encoder, actualEncoder);
// Original does not have the customizations
actualReader = this.configurer.getReaders().stream()
.filter(r -> r instanceof MultipartHttpMessageReader)
.findFirst()
.get();
actualEncoder = this.configurer.getWriters().stream()
.filter(writer -> writer instanceof ServerSentEventHttpMessageWriter)
.map(writer -> ((ServerSentEventHttpMessageWriter) writer).getEncoder())
.findFirst()
.get();
assertNotSame(reader, actualReader);
assertNotSame(encoder, actualEncoder);
}
private Decoder<?> getNextDecoder(List<HttpMessageReader<?>> readers) {
HttpMessageReader<?> reader = readers.get(this.index.getAndIncrement());
HttpMessageReader<?> reader = nextReader(readers);
assertEquals(DecoderHttpMessageReader.class, reader.getClass());
return ((DecoderHttpMessageReader<?>) reader).getDecoder();
}
private HttpMessageReader<?> nextReader(List<HttpMessageReader<?>> readers) {
return readers.get(this.index.getAndIncrement());
}
private Encoder<?> getNextEncoder(List<HttpMessageWriter<?>> writers) {
HttpMessageWriter<?> writer = writers.get(this.index.getAndIncrement());
assertEquals(EncoderHttpMessageWriter.class, writer.getClass());
@@ -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.
@@ -28,8 +28,10 @@ import reactor.test.StepVerifier;
import org.springframework.core.io.buffer.AbstractLeakCheckingTestCase;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferLimitException;
import static org.junit.Assert.*;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
/**
* @author Arjen Poutsma
@@ -44,11 +46,12 @@ public class XmlEventDecoderTests extends AbstractLeakCheckingTestCase {
private XmlEventDecoder decoder = new XmlEventDecoder();
@Test
public void toXMLEventsAalto() {
Flux<XMLEvent> events =
this.decoder.decode(stringBuffer(XML), null, null, Collections.emptyMap());
this.decoder.decode(stringBufferMono(XML), null, null, Collections.emptyMap());
StepVerifier.create(events)
.consumeNextWith(e -> assertTrue(e.isStartDocument()))
@@ -69,7 +72,7 @@ public class XmlEventDecoderTests extends AbstractLeakCheckingTestCase {
decoder.useAalto = false;
Flux<XMLEvent> events =
this.decoder.decode(stringBuffer(XML), null, null, Collections.emptyMap());
this.decoder.decode(stringBufferMono(XML), null, null, Collections.emptyMap());
StepVerifier.create(events)
.consumeNextWith(e -> assertTrue(e.isStartDocument()))
@@ -86,10 +89,32 @@ public class XmlEventDecoderTests extends AbstractLeakCheckingTestCase {
.verify();
}
@Test
public void toXMLEventsWithLimit() {
this.decoder.setMaxInMemorySize(6);
Flux<String> source = Flux.just(
"<pojo>", "<foo>", "foofoo", "</foo>", "<bar>", "barbarbar", "</bar>", "</pojo>");
Flux<XMLEvent> events = this.decoder.decode(
source.map(this::stringBuffer), null, null, Collections.emptyMap());
StepVerifier.create(events)
.consumeNextWith(e -> assertTrue(e.isStartDocument()))
.consumeNextWith(e -> assertStartElement(e, "pojo"))
.consumeNextWith(e -> assertStartElement(e, "foo"))
.consumeNextWith(e -> assertCharacters(e, "foofoo"))
.consumeNextWith(e -> assertEndElement(e, "foo"))
.consumeNextWith(e -> assertStartElement(e, "bar"))
.expectError(DataBufferLimitException.class)
.verify();
}
@Test
public void decodeErrorAalto() {
Flux<DataBuffer> source = Flux.concat(
stringBuffer("<pojo>"),
stringBufferMono("<pojo>"),
Flux.error(new RuntimeException()));
Flux<XMLEvent> events =
@@ -107,7 +132,7 @@ public class XmlEventDecoderTests extends AbstractLeakCheckingTestCase {
decoder.useAalto = false;
Flux<DataBuffer> source = Flux.concat(
stringBuffer("<pojo>"),
stringBufferMono("<pojo>"),
Flux.error(new RuntimeException()));
Flux<XMLEvent> events =
@@ -133,13 +158,15 @@ public class XmlEventDecoderTests extends AbstractLeakCheckingTestCase {
assertEquals(expectedData, event.asCharacters().getData());
}
private Mono<DataBuffer> stringBuffer(String value) {
return Mono.defer(() -> {
byte[] bytes = value.getBytes(StandardCharsets.UTF_8);
DataBuffer buffer = this.bufferFactory.allocateBuffer(bytes.length);
buffer.write(bytes);
return Mono.just(buffer);
});
private DataBuffer stringBuffer(String value) {
byte[] bytes = value.getBytes(StandardCharsets.UTF_8);
DataBuffer buffer = this.bufferFactory.allocateBuffer(bytes.length);
buffer.write(bytes);
return buffer;
}
private Mono<DataBuffer> stringBufferMono(String value) {
return Mono.defer(() -> Mono.just(stringBuffer(value)));
}
}
@@ -16,6 +16,10 @@
package org.springframework.mock.web.test;
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;
}

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