mirror of
https://github.com/spring-projects/spring-framework
synced 2026-06-08 17:33:33 +00:00
Compare commits
101 Commits
main
...
v5.1.13.RELEASE
| Author | SHA1 | Date | |
|---|---|---|---|
| e3069d4e55 | |||
| 9969cb6d83 | |||
| 04b3f5a247 | |||
| c91b47fded | |||
| f2c364b491 | |||
| 170adccf80 | |||
| 98390e1e5e | |||
| 6df8c2678b | |||
| 7f49abb6d1 | |||
| b9bf56e41d | |||
| d0e3e2acfc | |||
| 18d983c686 | |||
| d8abbc501e | |||
| 6ce19ff861 | |||
| c8ef49cc8e | |||
| f61983d908 | |||
| 7615e0b036 | |||
| afe22b84c2 | |||
| 634aba4ab6 | |||
| 26a2d3875f | |||
| 802d083df7 | |||
| f5b43a264a | |||
| 59e4755562 | |||
| 2576aa4063 | |||
| 3fbe762832 | |||
| 611bb0b92e | |||
| 32a8b9b25a | |||
| 97cad6ca8e | |||
| 015f7d8ce1 | |||
| da4e2710b4 | |||
| a368040fd6 | |||
| 5dbd3b0bbf | |||
| 197dbffe20 | |||
| 5341e77796 | |||
| f049a6ec16 | |||
| 7203da7133 | |||
| a21df0cc6d | |||
| 83683a13bb | |||
| 59165dd526 | |||
| 25554d0b21 | |||
| 4f86282b14 | |||
| b3237f3eb6 | |||
| fd68fb115d | |||
| 43e047c523 | |||
| 7fdf775394 | |||
| 59084c6b73 | |||
| 4fcc09a02c | |||
| f5b082d3b3 | |||
| c164759c41 | |||
| 2179b67706 | |||
| fbde98f36e | |||
| 529f8ba786 | |||
| f4676bb41c | |||
| 262332a7c9 | |||
| 8b1709b8f3 | |||
| aee33d8b4b | |||
| 83f03976ea | |||
| ffcd83e3a8 | |||
| 9a522946a5 | |||
| 268d029584 | |||
| 85471d0587 | |||
| 96a1a0dec5 | |||
| 40ac055d11 | |||
| c9a6f4282a | |||
| 6ed6c08ace | |||
| e4c57a9b61 | |||
| b0d8a667a1 | |||
| 9c9ee22c2b | |||
| 639dce6282 | |||
| 8e65834c44 | |||
| 64db939e4a | |||
| 3d83f869d9 | |||
| 088a653318 | |||
| bdb1a81a39 | |||
| c2e7b6341c | |||
| e75556bc7b | |||
| d3d40983d6 | |||
| 32adf77b22 | |||
| 1301c7e95d | |||
| 1a057654b2 | |||
| ca3440cb42 | |||
| 48b22292ff | |||
| e0faaa4807 | |||
| 2e4944198d | |||
| 38a1caefb8 | |||
| e731a0a164 | |||
| 0f2efdbe97 | |||
| feeeab1761 | |||
| b4cf471021 | |||
| 82751141ac | |||
| 9f43ee3304 | |||
| 614c7b0f8e | |||
| cc84533d85 | |||
| 6faf61ba40 | |||
| 80a5019534 | |||
| 03bd02a627 | |||
| f16aa4a9b5 | |||
| a3c9e8d4fc | |||
| 3814f12b67 | |||
| ceb881ab05 | |||
| 871464811c |
@@ -24,6 +24,10 @@ buildSrc/build
|
||||
/src/asciidoc/build
|
||||
target/
|
||||
|
||||
# Projects not in this branch
|
||||
integration-tests/
|
||||
spring-core/kotlin-coroutines/
|
||||
|
||||
# Eclipse artifacts, including WTP generated manifests
|
||||
.classpath
|
||||
.project
|
||||
|
||||
+10
-10
@@ -28,24 +28,24 @@ ext {
|
||||
!it.name.equals("spring-build-src") && !it.name.equals("spring-framework-bom")
|
||||
}
|
||||
|
||||
aspectjVersion = "1.9.4"
|
||||
aspectjVersion = "1.9.5"
|
||||
freemarkerVersion = "2.3.28"
|
||||
groovyVersion = "2.5.8"
|
||||
hsqldbVersion = "2.4.1"
|
||||
jackson2Version = "2.9.9"
|
||||
jettyVersion = "9.4.21.v20190926"
|
||||
jettyVersion = "9.4.25.v20191220"
|
||||
junit5Version = "5.3.2"
|
||||
kotlinVersion = "1.2.71"
|
||||
log4jVersion = "2.11.2"
|
||||
nettyVersion = "4.1.43.Final"
|
||||
reactorVersion = "Californium-SR13"
|
||||
nettyVersion = "4.1.44.Final"
|
||||
reactorVersion = "Californium-SR15"
|
||||
rxjavaVersion = "1.3.8"
|
||||
rxjavaAdapterVersion = "1.2.1"
|
||||
rxjava2Version = "2.2.12"
|
||||
slf4jVersion = "1.7.28" // spring-jcl + consistent 3rd party deps
|
||||
rxjava2Version = "2.2.17"
|
||||
slf4jVersion = "1.7.30" // spring-jcl + consistent 3rd party deps
|
||||
tiles3Version = "3.0.8"
|
||||
tomcatVersion = "9.0.26"
|
||||
undertowVersion = "2.0.26.Final"
|
||||
tomcatVersion = "9.0.30"
|
||||
undertowVersion = "2.0.29.Final"
|
||||
|
||||
gradleScriptDir = "${rootProject.projectDir}/gradle"
|
||||
withoutJclOverSlf4j = {
|
||||
@@ -142,7 +142,7 @@ configure(allprojects) { project ->
|
||||
}
|
||||
|
||||
checkstyle {
|
||||
toolVersion = "8.24"
|
||||
toolVersion = "8.28"
|
||||
configDir = rootProject.file("src/checkstyle")
|
||||
}
|
||||
|
||||
@@ -184,7 +184,7 @@ configure(allprojects) { project ->
|
||||
"https://tiles.apache.org/tiles-request/apidocs/",
|
||||
"https://tiles.apache.org/framework/apidocs/",
|
||||
"https://www.eclipse.org/aspectj/doc/released/aspectj5rt-api/",
|
||||
"https://www.ehcache.org/apidocs/2.10.4",
|
||||
"https://www.ehcache.org/apidocs/2.10.4/",
|
||||
"https://www.quartz-scheduler.org/api/2.3.0/",
|
||||
"https://fasterxml.github.io/jackson-core/javadoc/2.9/",
|
||||
"https://fasterxml.github.io/jackson-databind/javadoc/2.9/",
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
version=5.1.11.BUILD-SNAPSHOT
|
||||
version=5.1.13.RELEASE
|
||||
|
||||
+2
-2
@@ -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) {
|
||||
|
||||
@@ -4,6 +4,6 @@ dependencies {
|
||||
compile(project(":spring-beans"))
|
||||
compile(project(":spring-core"))
|
||||
optional("org.aspectj:aspectjweaver:${aspectjVersion}")
|
||||
optional("org.apache.commons:commons-pool2:2.6.0")
|
||||
optional("org.apache.commons:commons-pool2:2.6.2")
|
||||
optional("com.jamonapi:jamon:2.81")
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ dependencies {
|
||||
optional("org.codehaus.groovy:groovy-xml:${groovyVersion}")
|
||||
optional("org.jetbrains.kotlin:kotlin-reflect:${kotlinVersion}")
|
||||
optional("org.jetbrains.kotlin:kotlin-stdlib:${kotlinVersion}")
|
||||
testCompile("org.apache.tomcat.embed:tomcat-embed-core:${tomcatVersion}")
|
||||
testCompile("javax.annotation:javax.annotation-api:1.3.2")
|
||||
}
|
||||
|
||||
// This module does joint compilation for Java and Groovy code with the compileGroovy task.
|
||||
|
||||
+4
-5
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -43,9 +43,9 @@ import org.springframework.util.StringUtils;
|
||||
* Internal class that caches JavaBeans {@link java.beans.PropertyDescriptor}
|
||||
* information for a Java class. Not intended for direct use by application code.
|
||||
*
|
||||
* <p>Necessary for own caching of descriptors within the application's
|
||||
* ClassLoader, rather than rely on the JDK's system-wide BeanInfo cache
|
||||
* (in order to avoid leaks on ClassLoader shutdown).
|
||||
* <p>Necessary for Spring's own caching of bean descriptors within the application
|
||||
* {@link ClassLoader}, rather than relying on the JDK's system-wide {@link BeanInfo}
|
||||
* cache (in order to avoid leaks on individual application shutdown in a shared JVM).
|
||||
*
|
||||
* <p>Information is cached statically, so we don't need to create new
|
||||
* objects of this class for every JavaBean we manipulate. Hence, this class
|
||||
@@ -163,7 +163,6 @@ public final class CachedIntrospectionResults {
|
||||
* @return the corresponding CachedIntrospectionResults
|
||||
* @throws BeansException in case of introspection failure
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
static CachedIntrospectionResults forClass(Class<?> beanClass) throws BeansException {
|
||||
CachedIntrospectionResults results = strongClassCache.get(beanClass);
|
||||
if (results != null) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -41,8 +41,8 @@ import org.springframework.util.ObjectUtils;
|
||||
|
||||
/**
|
||||
* Decorator for a standard {@link BeanInfo} object, e.g. as created by
|
||||
* {@link Introspector#getBeanInfo(Class)}, designed to discover and register static
|
||||
* and/or non-void returning setter methods. For example:
|
||||
* {@link Introspector#getBeanInfo(Class)}, designed to discover and register
|
||||
* static and/or non-void returning setter methods. For example:
|
||||
*
|
||||
* <pre class="code">
|
||||
* public class Bean {
|
||||
@@ -145,11 +145,10 @@ class ExtendedBeanInfo implements BeanInfo {
|
||||
|
||||
public static boolean isCandidateWriteMethod(Method method) {
|
||||
String methodName = method.getName();
|
||||
Class<?>[] parameterTypes = method.getParameterTypes();
|
||||
int nParams = parameterTypes.length;
|
||||
int nParams = method.getParameterCount();
|
||||
return (methodName.length() > 3 && methodName.startsWith("set") && Modifier.isPublic(method.getModifiers()) &&
|
||||
(!void.class.isAssignableFrom(method.getReturnType()) || Modifier.isStatic(method.getModifiers())) &&
|
||||
(nParams == 1 || (nParams == 2 && int.class == parameterTypes[0])));
|
||||
(nParams == 1 || (nParams == 2 && int.class == method.getParameterTypes()[0])));
|
||||
}
|
||||
|
||||
private void handleCandidateWriteMethod(Method method) throws IntrospectionException {
|
||||
@@ -209,7 +208,7 @@ class ExtendedBeanInfo implements BeanInfo {
|
||||
}
|
||||
|
||||
private String propertyNameFor(Method method) {
|
||||
return Introspector.decapitalize(method.getName().substring(3, method.getName().length()));
|
||||
return Introspector.decapitalize(method.getName().substring(3));
|
||||
}
|
||||
|
||||
|
||||
@@ -488,7 +487,7 @@ class ExtendedBeanInfo implements BeanInfo {
|
||||
}
|
||||
|
||||
/*
|
||||
* See java.beans.IndexedPropertyDescriptor#equals(java.lang.Object)
|
||||
* See java.beans.IndexedPropertyDescriptor#equals
|
||||
*/
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
@@ -535,11 +534,13 @@ class ExtendedBeanInfo implements BeanInfo {
|
||||
public int compare(PropertyDescriptor desc1, PropertyDescriptor desc2) {
|
||||
String left = desc1.getName();
|
||||
String right = desc2.getName();
|
||||
byte[] leftBytes = left.getBytes();
|
||||
byte[] rightBytes = right.getBytes();
|
||||
for (int i = 0; i < left.length(); i++) {
|
||||
if (right.length() == i) {
|
||||
return 1;
|
||||
}
|
||||
int result = left.getBytes()[i] - right.getBytes()[i];
|
||||
int result = leftBytes[i] - rightBytes[i];
|
||||
if (result != 0) {
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -66,8 +66,7 @@ abstract class PropertyDescriptorUtils {
|
||||
Class<?> propertyType = null;
|
||||
|
||||
if (readMethod != null) {
|
||||
Class<?>[] params = readMethod.getParameterTypes();
|
||||
if (params.length != 0) {
|
||||
if (readMethod.getParameterCount() != 0) {
|
||||
throw new IntrospectionException("Bad read method arg count: " + readMethod);
|
||||
}
|
||||
propertyType = readMethod.getReturnType();
|
||||
|
||||
+45
-31
@@ -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);
|
||||
|
||||
+2
-2
@@ -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);
|
||||
}
|
||||
|
||||
+6
-13
@@ -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) {
|
||||
|
||||
+24
-24
@@ -1127,30 +1127,30 @@ public abstract class AbstractBeanDefinition extends BeanMetadataAttributeAccess
|
||||
return false;
|
||||
}
|
||||
AbstractBeanDefinition that = (AbstractBeanDefinition) other;
|
||||
boolean rtn = ObjectUtils.nullSafeEquals(getBeanClassName(), that.getBeanClassName());
|
||||
rtn = rtn &= ObjectUtils.nullSafeEquals(this.scope, that.scope);
|
||||
rtn = rtn &= this.abstractFlag == that.abstractFlag;
|
||||
rtn = rtn &= this.lazyInit == that.lazyInit;
|
||||
rtn = rtn &= this.autowireMode == that.autowireMode;
|
||||
rtn = rtn &= this.dependencyCheck == that.dependencyCheck;
|
||||
rtn = rtn &= Arrays.equals(this.dependsOn, that.dependsOn);
|
||||
rtn = rtn &= this.autowireCandidate == that.autowireCandidate;
|
||||
rtn = rtn &= ObjectUtils.nullSafeEquals(this.qualifiers, that.qualifiers);
|
||||
rtn = rtn &= this.primary == that.primary;
|
||||
rtn = rtn &= this.nonPublicAccessAllowed == that.nonPublicAccessAllowed;
|
||||
rtn = rtn &= this.lenientConstructorResolution == that.lenientConstructorResolution;
|
||||
rtn = rtn &= ObjectUtils.nullSafeEquals(this.constructorArgumentValues, that.constructorArgumentValues);
|
||||
rtn = rtn &= ObjectUtils.nullSafeEquals(this.propertyValues, that.propertyValues);
|
||||
rtn = rtn &= ObjectUtils.nullSafeEquals(this.methodOverrides, that.methodOverrides);
|
||||
rtn = rtn &= ObjectUtils.nullSafeEquals(this.factoryBeanName, that.factoryBeanName);
|
||||
rtn = rtn &= ObjectUtils.nullSafeEquals(this.factoryMethodName, that.factoryMethodName);
|
||||
rtn = rtn &= ObjectUtils.nullSafeEquals(this.initMethodName, that.initMethodName);
|
||||
rtn = rtn &= this.enforceInitMethod == that.enforceInitMethod;
|
||||
rtn = rtn &= ObjectUtils.nullSafeEquals(this.destroyMethodName, that.destroyMethodName);
|
||||
rtn = rtn &= this.enforceDestroyMethod == that.enforceDestroyMethod;
|
||||
rtn = rtn &= this.synthetic == that.synthetic;
|
||||
rtn = rtn &= this.role == that.role;
|
||||
return rtn && super.equals(other);
|
||||
return (ObjectUtils.nullSafeEquals(getBeanClassName(), that.getBeanClassName()) &&
|
||||
ObjectUtils.nullSafeEquals(this.scope, that.scope) &&
|
||||
this.abstractFlag == that.abstractFlag &&
|
||||
this.lazyInit == that.lazyInit &&
|
||||
this.autowireMode == that.autowireMode &&
|
||||
this.dependencyCheck == that.dependencyCheck &&
|
||||
Arrays.equals(this.dependsOn, that.dependsOn) &&
|
||||
this.autowireCandidate == that.autowireCandidate &&
|
||||
ObjectUtils.nullSafeEquals(this.qualifiers, that.qualifiers) &&
|
||||
this.primary == that.primary &&
|
||||
this.nonPublicAccessAllowed == that.nonPublicAccessAllowed &&
|
||||
this.lenientConstructorResolution == that.lenientConstructorResolution &&
|
||||
ObjectUtils.nullSafeEquals(this.constructorArgumentValues, that.constructorArgumentValues) &&
|
||||
ObjectUtils.nullSafeEquals(this.propertyValues, that.propertyValues) &&
|
||||
ObjectUtils.nullSafeEquals(this.methodOverrides, that.methodOverrides) &&
|
||||
ObjectUtils.nullSafeEquals(this.factoryBeanName, that.factoryBeanName) &&
|
||||
ObjectUtils.nullSafeEquals(this.factoryMethodName, that.factoryMethodName) &&
|
||||
ObjectUtils.nullSafeEquals(this.initMethodName, that.initMethodName) &&
|
||||
this.enforceInitMethod == that.enforceInitMethod &&
|
||||
ObjectUtils.nullSafeEquals(this.destroyMethodName, that.destroyMethodName) &&
|
||||
this.enforceDestroyMethod == that.enforceDestroyMethod &&
|
||||
this.synthetic == that.synthetic &&
|
||||
this.role == that.role &&
|
||||
super.equals(other));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+9
@@ -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.
|
||||
*/
|
||||
|
||||
+1
@@ -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;
|
||||
}
|
||||
|
||||
+3
-3
@@ -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")
|
||||
|
||||
+1
@@ -88,6 +88,7 @@ public class JCacheAspectSupport extends AbstractCacheInvoker implements Initial
|
||||
return this.cacheOperationSource;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterPropertiesSet() {
|
||||
getCacheOperationSource();
|
||||
|
||||
|
||||
+3
-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.
|
||||
@@ -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.
|
||||
}
|
||||
|
||||
+2
-1
@@ -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);
|
||||
}
|
||||
|
||||
@@ -19,16 +19,16 @@ dependencies {
|
||||
optional("org.aspectj:aspectjweaver:${aspectjVersion}")
|
||||
optional("org.codehaus.groovy:groovy:${groovyVersion}")
|
||||
optional("org.beanshell:bsh:2.0b5")
|
||||
optional("joda-time:joda-time:2.10.4")
|
||||
optional("joda-time:joda-time:2.10.5")
|
||||
optional("org.hibernate:hibernate-validator:5.4.3.Final")
|
||||
optional("org.jetbrains.kotlin:kotlin-reflect:${kotlinVersion}")
|
||||
optional("org.jetbrains.kotlin:kotlin-stdlib:${kotlinVersion}")
|
||||
testCompile("org.codehaus.groovy:groovy-jsr223:${groovyVersion}")
|
||||
testCompile("org.codehaus.groovy:groovy-test:${groovyVersion}")
|
||||
testCompile("org.codehaus.groovy:groovy-xml:${groovyVersion}")
|
||||
testCompile("org.apache.commons:commons-pool2:2.6.0")
|
||||
testCompile("org.apache.commons:commons-pool2:2.6.2")
|
||||
testCompile("javax.inject:javax.inject-tck:1")
|
||||
testCompile("org.awaitility:awaitility:3.1.3")
|
||||
testCompile("org.awaitility:awaitility:3.1.6")
|
||||
testRuntime("javax.xml.bind:jaxb-api:2.3.1")
|
||||
testRuntime("org.glassfish:javax.el:3.0.1-b08")
|
||||
testRuntime("org.javamoney:moneta:1.3")
|
||||
|
||||
+3
-1
@@ -32,7 +32,9 @@ import org.springframework.util.StringUtils;
|
||||
@SuppressWarnings("serial")
|
||||
public class SimpleKey implements Serializable {
|
||||
|
||||
/** An empty key. */
|
||||
/**
|
||||
* An empty key.
|
||||
*/
|
||||
public static final SimpleKey EMPTY = new SimpleKey();
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -17,19 +17,21 @@
|
||||
package org.springframework.context;
|
||||
|
||||
/**
|
||||
* An extension of the {@link Lifecycle} interface for those objects that require to
|
||||
* be started upon ApplicationContext refresh and/or shutdown in a particular order.
|
||||
* The {@link #isAutoStartup()} return value indicates whether this object should
|
||||
* An extension of the {@link Lifecycle} interface for those objects that require
|
||||
* to be started upon {@code ApplicationContext} refresh and/or shutdown in a
|
||||
* particular order.
|
||||
*
|
||||
* <p>The {@link #isAutoStartup()} return value indicates whether this object should
|
||||
* be started at the time of a context refresh. The callback-accepting
|
||||
* {@link #stop(Runnable)} method is useful for objects that have an asynchronous
|
||||
* shutdown process. Any implementation of this interface <i>must</i> invoke the
|
||||
* callback's {@code run()} method upon shutdown completion to avoid unnecessary
|
||||
* delays in the overall ApplicationContext shutdown.
|
||||
* delays in the overall {@code ApplicationContext} shutdown.
|
||||
*
|
||||
* <p>This interface extends {@link Phased}, and the {@link #getPhase()} method's
|
||||
* return value indicates the phase within which this Lifecycle component should
|
||||
* be started and stopped. The startup process begins with the <i>lowest</i> phase
|
||||
* value and ends with the <i>highest</i> phase value ({@code Integer.MIN_VALUE}
|
||||
* return value indicates the phase within which this {@code Lifecycle} component
|
||||
* should be started and stopped. The startup process begins with the <i>lowest</i>
|
||||
* phase value and ends with the <i>highest</i> phase value ({@code Integer.MIN_VALUE}
|
||||
* is the lowest possible, and {@code Integer.MAX_VALUE} is the highest possible).
|
||||
* The shutdown process will apply the reverse order. Any components with the
|
||||
* same value will be arbitrarily ordered within the same phase.
|
||||
@@ -44,9 +46,11 @@ package org.springframework.context;
|
||||
*
|
||||
* <p>Any {@code Lifecycle} components within the context that do not also
|
||||
* implement {@code SmartLifecycle} will be treated as if they have a phase
|
||||
* value of 0. That way a {@code SmartLifecycle} implementation may start
|
||||
* before those {@code Lifecycle} components if it has a negative phase value,
|
||||
* or it may start after those components if it has a positive phase value.
|
||||
* value of {@code 0}. This allows a {@code SmartLifecycle} component to start
|
||||
* before those {@code Lifecycle} components if the {@code SmartLifecycle}
|
||||
* component has a negative phase value, or the {@code SmartLifecycle} component
|
||||
* may start after those {@code Lifecycle} components if the {@code SmartLifecycle}
|
||||
* component has a positive phase value.
|
||||
*
|
||||
* <p>Note that, due to the auto-startup support in {@code SmartLifecycle}, a
|
||||
* {@code SmartLifecycle} bean instance will usually get initialized on startup
|
||||
@@ -55,6 +59,7 @@ package org.springframework.context;
|
||||
*
|
||||
* @author Mark Fisher
|
||||
* @author Juergen Hoeller
|
||||
* @author Sam Brannen
|
||||
* @since 3.0
|
||||
* @see LifecycleProcessor
|
||||
* @see ConfigurableApplicationContext
|
||||
@@ -63,9 +68,10 @@ public interface SmartLifecycle extends Lifecycle, Phased {
|
||||
|
||||
/**
|
||||
* The default phase for {@code SmartLifecycle}: {@code Integer.MAX_VALUE}.
|
||||
* <p>This is different from the common phase 0 associated with regular
|
||||
* <p>This is different from the common phase {@code 0} associated with regular
|
||||
* {@link Lifecycle} implementations, putting the typically auto-started
|
||||
* {@code SmartLifecycle} beans into a separate later shutdown phase.
|
||||
* {@code SmartLifecycle} beans into a later startup phase and an earlier
|
||||
* shutdown phase.
|
||||
* @since 5.1
|
||||
* @see #getPhase()
|
||||
* @see org.springframework.context.support.DefaultLifecycleProcessor#getPhase(Lifecycle)
|
||||
@@ -115,7 +121,8 @@ public interface SmartLifecycle extends Lifecycle, Phased {
|
||||
/**
|
||||
* Return the phase that this lifecycle object is supposed to run in.
|
||||
* <p>The default implementation returns {@link #DEFAULT_PHASE} in order to
|
||||
* let stop callbacks execute after regular {@code Lifecycle} implementations.
|
||||
* let {@code stop()} callbacks execute after regular {@code Lifecycle}
|
||||
* implementations.
|
||||
* @see #isAutoStartup()
|
||||
* @see #start()
|
||||
* @see #stop(Runnable)
|
||||
|
||||
+8
-6
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+5
@@ -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)) {
|
||||
|
||||
+19
-7
@@ -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);
|
||||
|
||||
+2
-1
@@ -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());
|
||||
}
|
||||
|
||||
+1
-2
@@ -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());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+7
-6
@@ -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
|
||||
|
||||
+3
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -207,7 +207,7 @@ public class InterfaceBasedMBeanInfoAssembler extends AbstractConfigurableMBeanI
|
||||
* configured interfaces and is public, otherwise {@code false}.
|
||||
*/
|
||||
private boolean isPublicInInterface(Method method, String beanKey) {
|
||||
return ((method.getModifiers() & Modifier.PUBLIC) > 0) && isDeclaredInInterface(method, beanKey);
|
||||
return Modifier.isPublic(method.getModifiers()) && isDeclaredInInterface(method, beanKey);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -231,6 +231,7 @@ public class InterfaceBasedMBeanInfoAssembler extends AbstractConfigurableMBeanI
|
||||
for (Class<?> ifc : ifaces) {
|
||||
for (Method ifcMethod : ifc.getMethods()) {
|
||||
if (ifcMethod.getName().equals(method.getName()) &&
|
||||
ifcMethod.getParameterCount() == method.getParameterCount() &&
|
||||
Arrays.equals(ifcMethod.getParameterTypes(), method.getParameterTypes())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
+3
-3
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2017 the original author or authors.
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -60,7 +60,7 @@ public class CacheReproTests {
|
||||
|
||||
|
||||
@Test
|
||||
public void spr11124MultipleAnnotations() throws Exception {
|
||||
public void spr11124MultipleAnnotations() {
|
||||
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Spr11124Config.class);
|
||||
Spr11124Service bean = context.getBean(Spr11124Service.class);
|
||||
bean.single(2);
|
||||
@@ -71,7 +71,7 @@ public class CacheReproTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void spr11249PrimitiveVarargs() throws Exception {
|
||||
public void spr11249PrimitiveVarargs() {
|
||||
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Spr11249Config.class);
|
||||
Spr11249Service bean = context.getBean(Spr11249Service.class);
|
||||
Object result = bean.doSomething("op", 2, 3);
|
||||
|
||||
Vendored
+2
-2
@@ -44,7 +44,7 @@ public class SimpleKeyGeneratorTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void singleValue(){
|
||||
public void singleValue() {
|
||||
Object k1 = generateKey(new Object[] { "a" });
|
||||
Object k2 = generateKey(new Object[] { "a" });
|
||||
Object k3 = generateKey(new Object[] { "different" });
|
||||
@@ -56,7 +56,7 @@ public class SimpleKeyGeneratorTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void multipleValues() {
|
||||
public void multipleValues() {
|
||||
Object k1 = generateKey(new Object[] { "a", 1, "b" });
|
||||
Object k2 = generateKey(new Object[] { "a", 1, "b" });
|
||||
Object k3 = generateKey(new Object[] { "b", 1, "a" });
|
||||
|
||||
+6
-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.
|
||||
@@ -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
-2
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
+17
-2
@@ -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();
|
||||
|
||||
+121
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -79,11 +79,11 @@ dependencies {
|
||||
optional("io.reactivex.rxjava2:rxjava:${rxjava2Version}")
|
||||
optional("io.netty:netty-buffer")
|
||||
testCompile("io.projectreactor:reactor-test")
|
||||
testCompile("org.apache.tomcat.embed:tomcat-embed-core:${tomcatVersion}")
|
||||
testCompile("javax.annotation:javax.annotation-api:1.3.2")
|
||||
testCompile("com.google.code.findbugs:jsr305:3.0.2")
|
||||
testCompile("org.xmlunit:xmlunit-matchers:2.6.2")
|
||||
testCompile("javax.xml.bind:jaxb-api:2.3.1")
|
||||
testCompile("com.fasterxml.woodstox:woodstox-core:5.2.0") {
|
||||
testCompile("com.fasterxml.woodstox:woodstox-core:5.3.0") {
|
||||
exclude group: "stax", module: "stax-api"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -872,6 +872,12 @@ public class ResolvableType implements Serializable {
|
||||
return forType(ownerType, this.variableResolver).resolveVariable(variable);
|
||||
}
|
||||
}
|
||||
if (this.type instanceof WildcardType) {
|
||||
ResolvableType resolved = resolveType().resolveVariable(variable);
|
||||
if (resolved != null) {
|
||||
return resolved;
|
||||
}
|
||||
}
|
||||
if (this.variableResolver != null) {
|
||||
return this.variableResolver.resolveVariable(variable);
|
||||
}
|
||||
@@ -982,7 +988,7 @@ public class ResolvableType implements Serializable {
|
||||
* using the full generic type information for assignability checks.
|
||||
* For example: {@code ResolvableType.forClass(MyArrayList.class)}.
|
||||
* @param clazz the class to introspect ({@code null} is semantically
|
||||
* equivalent to {@code Object.class} for typical use cases here}
|
||||
* equivalent to {@code Object.class} for typical use cases here)
|
||||
* @return a {@link ResolvableType} for the specified class
|
||||
* @see #forClass(Class, Class)
|
||||
* @see #forClassWithGenerics(Class, Class...)
|
||||
@@ -997,7 +1003,7 @@ public class ResolvableType implements Serializable {
|
||||
* {@link Class#isAssignableFrom}, which this serves as a wrapper for.
|
||||
* For example: {@code ResolvableType.forRawClass(List.class)}.
|
||||
* @param clazz the class to introspect ({@code null} is semantically
|
||||
* equivalent to {@code Object.class} for typical use cases here}
|
||||
* equivalent to {@code Object.class} for typical use cases here)
|
||||
* @return a {@link ResolvableType} for the specified class
|
||||
* @since 4.2
|
||||
* @see #forClass(Class)
|
||||
@@ -1485,10 +1491,10 @@ public class ResolvableType implements Serializable {
|
||||
@Override
|
||||
@Nullable
|
||||
public ResolvableType resolveVariable(TypeVariable<?> variable) {
|
||||
TypeVariable<?> variableToCompare = SerializableTypeWrapper.unwrap(variable);
|
||||
for (int i = 0; i < this.variables.length; i++) {
|
||||
TypeVariable<?> v1 = SerializableTypeWrapper.unwrap(this.variables[i]);
|
||||
TypeVariable<?> v2 = SerializableTypeWrapper.unwrap(variable);
|
||||
if (ObjectUtils.nullSafeEquals(v1, v2)) {
|
||||
TypeVariable<?> resolvedVariable = SerializableTypeWrapper.unwrap(this.variables[i]);
|
||||
if (ObjectUtils.nullSafeEquals(resolvedVariable, variableToCompare)) {
|
||||
return this.generics[i];
|
||||
}
|
||||
}
|
||||
|
||||
+29
-1
@@ -47,12 +47,40 @@ import org.springframework.util.MimeType;
|
||||
*/
|
||||
public abstract class AbstractDataBufferDecoder<T> extends AbstractDecoder<T> {
|
||||
|
||||
private int maxInMemorySize = -1;
|
||||
|
||||
|
||||
protected AbstractDataBufferDecoder(MimeType... supportedMimeTypes) {
|
||||
super(supportedMimeTypes);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Configure a limit on the number of bytes that can be buffered whenever
|
||||
* the input stream needs to be aggregated. This can be a result of
|
||||
* decoding to a single {@code DataBuffer},
|
||||
* {@link java.nio.ByteBuffer ByteBuffer}, {@code byte[]},
|
||||
* {@link org.springframework.core.io.Resource Resource}, {@code String}, etc.
|
||||
* It can also occur when splitting the input stream, e.g. delimited text,
|
||||
* in which case the limit applies to data buffered between delimiters.
|
||||
* <p>By default in 5.1 this is set to -1, unlimited. In 5.2 the default
|
||||
* value for this limit is set to 256K.
|
||||
* @param byteCount the max number of bytes to buffer, or -1 for unlimited
|
||||
* @since 5.1.11
|
||||
*/
|
||||
public void setMaxInMemorySize(int byteCount) {
|
||||
this.maxInMemorySize = byteCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the {@link #setMaxInMemorySize configured} byte count limit.
|
||||
* @since 5.1.11
|
||||
*/
|
||||
public int getMaxInMemorySize() {
|
||||
return this.maxInMemorySize;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Flux<T> decode(Publisher<DataBuffer> input, ResolvableType elementType,
|
||||
@Nullable MimeType mimeType, @Nullable Map<String, Object> hints) {
|
||||
@@ -64,7 +92,7 @@ public abstract class AbstractDataBufferDecoder<T> extends AbstractDecoder<T> {
|
||||
public Mono<T> decodeToMono(Publisher<DataBuffer> input, ResolvableType elementType,
|
||||
@Nullable MimeType mimeType, @Nullable Map<String, Object> hints) {
|
||||
|
||||
return DataBufferUtils.join(input)
|
||||
return DataBufferUtils.join(input, this.maxInMemorySize)
|
||||
.map(buffer -> decodeDataBuffer(buffer, elementType, mimeType, hints));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -25,14 +25,17 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.reactivestreams.Publisher;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.core.io.buffer.DataBuffer;
|
||||
import org.springframework.core.io.buffer.DataBufferLimitException;
|
||||
import org.springframework.core.io.buffer.DataBufferUtils;
|
||||
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
|
||||
import org.springframework.core.io.buffer.LimitedDataBufferList;
|
||||
import org.springframework.core.io.buffer.PooledDataBuffer;
|
||||
import org.springframework.core.log.LogFormatUtils;
|
||||
import org.springframework.lang.Nullable;
|
||||
@@ -92,11 +95,44 @@ public final class StringDecoder extends AbstractDataBufferDecoder<String> {
|
||||
|
||||
List<byte[]> delimiterBytes = getDelimiterBytes(mimeType);
|
||||
|
||||
Flux<DataBuffer> inputFlux = Flux.from(input)
|
||||
.flatMapIterable(buffer -> splitOnDelimiter(buffer, delimiterBytes))
|
||||
.bufferUntil(buffer -> buffer == END_FRAME)
|
||||
.map(StringDecoder::joinUntilEndFrame)
|
||||
.doOnDiscard(PooledDataBuffer.class, DataBufferUtils::release);
|
||||
Flux<DataBuffer> inputFlux = Flux.defer(() -> {
|
||||
if (getMaxInMemorySize() != -1) {
|
||||
|
||||
// Passing limiter into endFrameAfterDelimiter helps to ensure that in case of one DataBuffer
|
||||
// containing multiple lines, the limit is checked and raised immediately without accumulating
|
||||
// subsequent lines. This is necessary because concatMapIterable doesn't respect doOnDiscard.
|
||||
// When reactor-core#1925 is resolved, we could replace bufferUntil with:
|
||||
|
||||
// .windowUntil(buffer -> buffer instanceof EndFrameBuffer)
|
||||
// .concatMap(fluxes -> fluxes.collect(() -> new LimitedDataBufferList(getMaxInMemorySize()), LimitedDataBufferList::add))
|
||||
|
||||
LimitedDataBufferList limiter = new LimitedDataBufferList(getMaxInMemorySize());
|
||||
|
||||
return Flux.from(input)
|
||||
.concatMapIterable(buffer -> splitOnDelimiter(buffer, delimiterBytes, limiter))
|
||||
.bufferUntil(buffer -> buffer == END_FRAME)
|
||||
.map(StringDecoder::joinUntilEndFrame)
|
||||
.doOnDiscard(PooledDataBuffer.class, DataBufferUtils::release);
|
||||
}
|
||||
else {
|
||||
|
||||
// When the decoder is unlimited (-1), concatMapIterable will cache buffers that may not
|
||||
// be released if cancel is signalled before they are turned into String lines
|
||||
// (see test maxInMemoryLimitReleasesUnprocessedLinesWhenUnlimited).
|
||||
// When reactor-core#1925 is resolved, the workaround can be removed and the entire
|
||||
// else clause possibly dropped.
|
||||
|
||||
ConcatMapIterableDiscardWorkaroundCache cache = new ConcatMapIterableDiscardWorkaroundCache();
|
||||
|
||||
return Flux.from(input)
|
||||
.concatMapIterable(buffer -> cache.addAll(splitOnDelimiter(buffer, delimiterBytes, null)))
|
||||
.doOnNext(cache)
|
||||
.doOnCancel(cache)
|
||||
.bufferUntil(buffer -> buffer == END_FRAME)
|
||||
.map(StringDecoder::joinUntilEndFrame)
|
||||
.doOnDiscard(PooledDataBuffer.class, DataBufferUtils::release);
|
||||
}
|
||||
});
|
||||
|
||||
return super.decode(inputFlux, elementType, mimeType, hints);
|
||||
}
|
||||
@@ -116,7 +152,9 @@ public final class StringDecoder extends AbstractDataBufferDecoder<String> {
|
||||
* Split the given data buffer on delimiter boundaries.
|
||||
* The returned Flux contains an {@link #END_FRAME} buffer after each delimiter.
|
||||
*/
|
||||
private List<DataBuffer> splitOnDelimiter(DataBuffer buffer, List<byte[]> delimiterBytes) {
|
||||
private List<DataBuffer> splitOnDelimiter(
|
||||
DataBuffer buffer, List<byte[]> delimiterBytes, @Nullable LimitedDataBufferList limiter) {
|
||||
|
||||
List<DataBuffer> frames = new ArrayList<>();
|
||||
try {
|
||||
do {
|
||||
@@ -138,15 +176,28 @@ public final class StringDecoder extends AbstractDataBufferDecoder<String> {
|
||||
buffer.readPosition(readPosition + length + matchingDelimiter.length);
|
||||
frames.add(DataBufferUtils.retain(frame));
|
||||
frames.add(END_FRAME);
|
||||
if (limiter != null) {
|
||||
limiter.add(frame); // enforce the limit
|
||||
limiter.clear();
|
||||
}
|
||||
}
|
||||
else {
|
||||
frame = buffer.slice(readPosition, buffer.readableByteCount());
|
||||
buffer.readPosition(readPosition + buffer.readableByteCount());
|
||||
frames.add(DataBufferUtils.retain(frame));
|
||||
if (limiter != null) {
|
||||
limiter.add(frame);
|
||||
}
|
||||
}
|
||||
}
|
||||
while (buffer.readableByteCount() > 0);
|
||||
}
|
||||
catch (DataBufferLimitException ex) {
|
||||
if (limiter != null) {
|
||||
limiter.releaseAndClear();
|
||||
}
|
||||
throw ex;
|
||||
}
|
||||
catch (Throwable ex) {
|
||||
for (DataBuffer frame : frames) {
|
||||
DataBufferUtils.release(frame);
|
||||
@@ -283,4 +334,33 @@ public final class StringDecoder extends AbstractDataBufferDecoder<String> {
|
||||
new MimeType("text", "plain", DEFAULT_CHARSET), MimeTypeUtils.ALL);
|
||||
}
|
||||
|
||||
|
||||
private class ConcatMapIterableDiscardWorkaroundCache implements Consumer<DataBuffer>, Runnable {
|
||||
|
||||
private final List<DataBuffer> buffers = new ArrayList<>();
|
||||
|
||||
|
||||
public List<DataBuffer> addAll(List<DataBuffer> buffersToAdd) {
|
||||
this.buffers.addAll(buffersToAdd);
|
||||
return buffersToAdd;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void accept(DataBuffer dataBuffer) {
|
||||
this.buffers.remove(dataBuffer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
this.buffers.forEach(buffer -> {
|
||||
try {
|
||||
DataBufferUtils.release(buffer);
|
||||
}
|
||||
catch (Throwable ex) {
|
||||
// Keep going..
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -36,7 +36,8 @@ import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
|
||||
/**
|
||||
* Context about a type to convert from or to.
|
||||
* Contextual descriptor about a type to convert from or to.
|
||||
* Capable of representing arrays and generic collection types.
|
||||
*
|
||||
* @author Keith Donald
|
||||
* @author Andy Clement
|
||||
@@ -45,6 +46,8 @@ import org.springframework.util.ObjectUtils;
|
||||
* @author Sam Brannen
|
||||
* @author Stephane Nicoll
|
||||
* @since 3.0
|
||||
* @see ConversionService#canConvert(TypeDescriptor, TypeDescriptor)
|
||||
* @see ConversionService#convert(Object, TypeDescriptor, TypeDescriptor)
|
||||
*/
|
||||
@SuppressWarnings("serial")
|
||||
public class TypeDescriptor implements Serializable {
|
||||
@@ -322,9 +325,9 @@ public class TypeDescriptor implements Serializable {
|
||||
* If this type is a {@code Stream}, returns the stream's component type.
|
||||
* If this type is a {@link Collection} and it is parameterized, returns the Collection's element type.
|
||||
* If the Collection is not parameterized, returns {@code null} indicating the element type is not declared.
|
||||
* @return the array component type or Collection element type, or {@code null} if this type is a
|
||||
* Collection but its element type is not parameterized
|
||||
* @throws IllegalStateException if this type is not a {@code java.util.Collection} or array type
|
||||
* @return the array component type or Collection element type, or {@code null} if this type is not
|
||||
* an array type or a {@code java.util.Collection} or if its element type is not parameterized
|
||||
* @see #elementTypeDescriptor(Object)
|
||||
*/
|
||||
@Nullable
|
||||
public TypeDescriptor getElementTypeDescriptor() {
|
||||
@@ -351,8 +354,7 @@ public class TypeDescriptor implements Serializable {
|
||||
* TypeDescriptor that is returned.
|
||||
* @param element the collection or array element
|
||||
* @return a element type descriptor, narrowed to the type of the provided element
|
||||
* @throws IllegalStateException if this type is not a {@code java.util.Collection}
|
||||
* or array type
|
||||
* @see #getElementTypeDescriptor()
|
||||
* @see #narrow(Object)
|
||||
*/
|
||||
@Nullable
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -49,6 +49,7 @@ import org.springframework.util.StringUtils;
|
||||
*
|
||||
* @author Juergen Hoeller
|
||||
* @since 28.12.2003
|
||||
* @see #FileSystemResource(String)
|
||||
* @see #FileSystemResource(File)
|
||||
* @see #FileSystemResource(Path)
|
||||
* @see java.io.File
|
||||
@@ -108,6 +109,15 @@ public class FileSystemResource extends AbstractResource implements WritableReso
|
||||
* <p>In contrast to {@link PathResource}, this variant strictly follows the
|
||||
* general {@link FileSystemResource} conventions, in particular in terms of
|
||||
* path cleaning and {@link #createRelative(String)} handling.
|
||||
* <p>Note: When building relative resources via {@link #createRelative},
|
||||
* the relative path will apply <i>at the same directory level</i>:
|
||||
* e.g. Paths.get("C:/dir1"), relative path "dir2" -> "C:/dir2"!
|
||||
* If you prefer to have relative paths built underneath the given root directory,
|
||||
* use the {@link #FileSystemResource(String) constructor with a file path}
|
||||
* to append a trailing slash to the root path: "C:/dir1/", which indicates
|
||||
* this directory as root for all relative paths. Alternatively, consider
|
||||
* using {@link PathResource#PathResource(Path)} for {@code java.nio.path.Path}
|
||||
* resolution in {@code createRelative}, always nesting relative paths.
|
||||
* @param filePath a Path handle to a file
|
||||
* @since 5.1
|
||||
* @see #FileSystemResource(File)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -44,15 +44,16 @@ import org.springframework.util.Assert;
|
||||
* in {@link FileSystemResource#FileSystemResource(Path) FileSystemResource},
|
||||
* applying Spring's standard String-based path transformations but
|
||||
* performing all operations via the {@link java.nio.file.Files} API.
|
||||
* This {@code PathResource} is effectively a pure {@code java.nio.path.Path}
|
||||
* based alternative with different {@code createRelative} behavior.
|
||||
*
|
||||
* @author Philippe Marschall
|
||||
* @author Juergen Hoeller
|
||||
* @since 4.0
|
||||
* @see java.nio.file.Path
|
||||
* @see java.nio.file.Files
|
||||
* @deprecated as of 5.1.1, in favor of {@link FileSystemResource#FileSystemResource(Path)}
|
||||
* @see FileSystemResource
|
||||
*/
|
||||
@Deprecated
|
||||
public class PathResource extends AbstractResource implements WritableResource {
|
||||
|
||||
private final Path path;
|
||||
@@ -252,7 +253,7 @@ public class PathResource extends AbstractResource implements WritableResource {
|
||||
* @see java.nio.file.Path#resolve(String)
|
||||
*/
|
||||
@Override
|
||||
public Resource createRelative(String relativePath) throws IOException {
|
||||
public Resource createRelative(String relativePath) {
|
||||
return new PathResource(this.path.resolve(relativePath));
|
||||
}
|
||||
|
||||
|
||||
+37
@@ -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);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
+157
@@ -0,0 +1,157 @@
|
||||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.core.io.buffer;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
/**
|
||||
* Custom {@link List} to collect data buffers with and enforce a
|
||||
* limit on the total number of bytes buffered. For use with "collect" or
|
||||
* other buffering operators in declarative APIs, e.g. {@link Flux}.
|
||||
*
|
||||
* <p>Adding elements increases the byte count and if the limit is exceeded,
|
||||
* {@link DataBufferLimitException} is raised. {@link #clear()} resets the
|
||||
* count. Remove and set are not supported.
|
||||
*
|
||||
* <p><strong>Note:</strong> This class does not automatically release the
|
||||
* buffers it contains. It is usually preferable to use hooks such as
|
||||
* {@link Flux#doOnDiscard} that also take care of cancel and error signals,
|
||||
* or otherwise {@link #releaseAndClear()} can be used.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 5.1.11
|
||||
*/
|
||||
@SuppressWarnings("serial")
|
||||
public class LimitedDataBufferList extends ArrayList<DataBuffer> {
|
||||
|
||||
private final int maxByteCount;
|
||||
|
||||
private int byteCount;
|
||||
|
||||
|
||||
public LimitedDataBufferList(int maxByteCount) {
|
||||
this.maxByteCount = maxByteCount;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean add(DataBuffer buffer) {
|
||||
boolean result = super.add(buffer);
|
||||
if (result) {
|
||||
updateCount(buffer.readableByteCount());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void add(int index, DataBuffer buffer) {
|
||||
super.add(index, buffer);
|
||||
updateCount(buffer.readableByteCount());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean addAll(Collection<? extends DataBuffer> collection) {
|
||||
boolean result = super.addAll(collection);
|
||||
collection.forEach(buffer -> updateCount(buffer.readableByteCount()));
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean addAll(int index, Collection<? extends DataBuffer> collection) {
|
||||
boolean result = super.addAll(index, collection);
|
||||
collection.forEach(buffer -> updateCount(buffer.readableByteCount()));
|
||||
return result;
|
||||
}
|
||||
|
||||
private void updateCount(int bytesToAdd) {
|
||||
if (this.maxByteCount < 0) {
|
||||
return;
|
||||
}
|
||||
if (bytesToAdd > Integer.MAX_VALUE - this.byteCount) {
|
||||
raiseLimitException();
|
||||
}
|
||||
else {
|
||||
this.byteCount += bytesToAdd;
|
||||
if (this.byteCount > this.maxByteCount) {
|
||||
raiseLimitException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void raiseLimitException() {
|
||||
// Do not release here, it's likely down via doOnDiscard..
|
||||
throw new DataBufferLimitException(
|
||||
"Exceeded limit on max bytes to buffer : " + this.maxByteCount);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DataBuffer remove(int index) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean remove(Object o) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void removeRange(int fromIndex, int toIndex) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean removeAll(Collection<?> c) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean removeIf(Predicate<? super DataBuffer> filter) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public DataBuffer set(int index, DataBuffer element) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clear() {
|
||||
this.byteCount = 0;
|
||||
super.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Shortcut to {@link DataBufferUtils#release release} all data buffers and
|
||||
* then {@link #clear()}.
|
||||
*/
|
||||
public void releaseAndClear() {
|
||||
forEach(buf -> {
|
||||
try {
|
||||
DataBufferUtils.release(buf);
|
||||
}
|
||||
catch (Throwable ex) {
|
||||
// Keep going..
|
||||
}
|
||||
});
|
||||
clear();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -110,6 +110,11 @@ public abstract class ClassUtils {
|
||||
*/
|
||||
private static final Set<Class<?>> javaLanguageInterfaces;
|
||||
|
||||
/**
|
||||
* Cache for equivalent methods on an interface implemented by the declaring class.
|
||||
*/
|
||||
private static final Map<Method, Method> interfaceMethodCache = new ConcurrentReferenceHashMap<>(256);
|
||||
|
||||
|
||||
static {
|
||||
primitiveWrapperTypeMap.put(Boolean.class, boolean.class);
|
||||
@@ -1291,13 +1296,16 @@ public abstract class ClassUtils {
|
||||
* @see #getMostSpecificMethod
|
||||
*/
|
||||
public static Method getInterfaceMethodIfPossible(Method method) {
|
||||
if (Modifier.isPublic(method.getModifiers()) && !method.getDeclaringClass().isInterface()) {
|
||||
Class<?> current = method.getDeclaringClass();
|
||||
if (!Modifier.isPublic(method.getModifiers()) || method.getDeclaringClass().isInterface()) {
|
||||
return method;
|
||||
}
|
||||
return interfaceMethodCache.computeIfAbsent(method, key -> {
|
||||
Class<?> current = key.getDeclaringClass();
|
||||
while (current != null && current != Object.class) {
|
||||
Class<?>[] ifcs = current.getInterfaces();
|
||||
for (Class<?> ifc : ifcs) {
|
||||
try {
|
||||
return ifc.getMethod(method.getName(), method.getParameterTypes());
|
||||
return ifc.getMethod(key.getName(), key.getParameterTypes());
|
||||
}
|
||||
catch (NoSuchMethodException ex) {
|
||||
// ignore
|
||||
@@ -1305,8 +1313,8 @@ public abstract class ClassUtils {
|
||||
}
|
||||
current = current.getSuperclass();
|
||||
}
|
||||
}
|
||||
return method;
|
||||
return key;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -417,14 +417,14 @@ public abstract class StringUtils {
|
||||
int pos = 0; // our position in the old string
|
||||
int patLen = oldPattern.length();
|
||||
while (index >= 0) {
|
||||
sb.append(inString.substring(pos, index));
|
||||
sb.append(inString, pos, index);
|
||||
sb.append(newPattern);
|
||||
pos = index + patLen;
|
||||
index = inString.indexOf(oldPattern, pos);
|
||||
}
|
||||
|
||||
// append any characters to the right of a match
|
||||
sb.append(inString.substring(pos));
|
||||
sb.append(inString, pos, inString.length());
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -16,38 +16,53 @@
|
||||
|
||||
package org.springframework.util.unit;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* A standard set of data size units.
|
||||
* A standard set of {@link DataSize} units.
|
||||
*
|
||||
* <p>The unit prefixes used in this class are
|
||||
* <a href="https://en.wikipedia.org/wiki/Binary_prefix">binary prefixes</a>
|
||||
* indicating multiplication by powers of 2. The following table displays the
|
||||
* enum constants defined in this class and corresponding values.
|
||||
*
|
||||
* <p>
|
||||
* <table border="1">
|
||||
* <tr><th>Constant</th><th>Data Size</th><th>Power of 2</th><th>Size in Bytes</th></tr>
|
||||
* <tr><td>{@link #BYTES}</td><td>1B</td><td>2^0</td><td>1</td></tr>
|
||||
* <tr><td>{@link #KILOBYTES}</td><td>1KB</td><td>2^10</td><td>1,024</td></tr>
|
||||
* <tr><td>{@link #MEGABYTES}</td><td>1MB</td><td>2^20</td><td>1,048,576</td></tr>
|
||||
* <tr><td>{@link #GIGABYTES}</td><td>1GB</td><td>2^30</td><td>1,073,741,824</td></tr>
|
||||
* <tr><td>{@link #TERABYTES}</td><td>1TB</td><td>2^40</td><td>1,099,511,627,776</td></tr>
|
||||
* </table>
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
* @author Sam Brannen
|
||||
* @since 5.1
|
||||
* @see DataSize
|
||||
*/
|
||||
public enum DataUnit {
|
||||
|
||||
/**
|
||||
* Bytes.
|
||||
* Bytes, represented by suffix {@code B}.
|
||||
*/
|
||||
BYTES("B", DataSize.ofBytes(1)),
|
||||
|
||||
/**
|
||||
* Kilobytes.
|
||||
* Kilobytes, represented by suffix {@code KB}.
|
||||
*/
|
||||
KILOBYTES("KB", DataSize.ofKilobytes(1)),
|
||||
|
||||
/**
|
||||
* Megabytes.
|
||||
* Megabytes, represented by suffix {@code MB}.
|
||||
*/
|
||||
MEGABYTES("MB", DataSize.ofMegabytes(1)),
|
||||
|
||||
/**
|
||||
* Gigabytes.
|
||||
* Gigabytes, represented by suffix {@code GB}.
|
||||
*/
|
||||
GIGABYTES("GB", DataSize.ofGigabytes(1)),
|
||||
|
||||
/**
|
||||
* Terabytes.
|
||||
* Terabytes, represented by suffix {@code TB}.
|
||||
*/
|
||||
TERABYTES("TB", DataSize.ofTerabytes(1));
|
||||
|
||||
@@ -68,18 +83,18 @@ public enum DataUnit {
|
||||
|
||||
/**
|
||||
* Return the {@link DataUnit} matching the specified {@code suffix}.
|
||||
* @param suffix one of the standard suffix
|
||||
* @param suffix one of the standard suffixes
|
||||
* @return the {@link DataUnit} matching the specified {@code suffix}
|
||||
* @throws IllegalArgumentException if the suffix does not match any
|
||||
* of this enum's constants
|
||||
* @throws IllegalArgumentException if the suffix does not match the suffix
|
||||
* of any of this enum's constants
|
||||
*/
|
||||
public static DataUnit fromSuffix(String suffix) {
|
||||
for (DataUnit candidate : values()) {
|
||||
if (Objects.equals(candidate.suffix, suffix)) {
|
||||
if (candidate.suffix.equals(suffix)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
throw new IllegalArgumentException("Unknown unit '" + suffix + "'");
|
||||
throw new IllegalArgumentException("Unknown data unit suffix '" + suffix + "'");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -684,10 +684,16 @@ public class ResolvableTypeTests {
|
||||
|
||||
@Test
|
||||
public void resolveBoundedTypeVariableResult() throws Exception {
|
||||
ResolvableType type = ResolvableType.forMethodReturnType(Methods.class.getMethod("boundedTypeVaraibleResult"));
|
||||
ResolvableType type = ResolvableType.forMethodReturnType(Methods.class.getMethod("boundedTypeVariableResult"));
|
||||
assertThat(type.resolve(), equalTo((Class) CharSequence.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveBoundedTypeVariableWildcardResult() throws Exception {
|
||||
ResolvableType type = ResolvableType.forMethodReturnType(Methods.class.getMethod("boundedTypeVariableWildcardResult"));
|
||||
assertThat(type.getGeneric(1).asCollection().resolveGeneric(), equalTo((Class) CharSequence.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveVariableNotFound() throws Exception {
|
||||
ResolvableType type = ResolvableType.forMethodReturnType(Methods.class.getMethod("typedReturn"));
|
||||
@@ -695,27 +701,14 @@ public class ResolvableTypeTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveTypeVaraibleFromMethodReturn() throws Exception {
|
||||
ResolvableType type = ResolvableType.forMethodReturnType(Methods.class.getMethod("typedReturn"));
|
||||
assertThat(type.resolve(), nullValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveTypeVaraibleFromMethodReturnWithInstanceClass() throws Exception {
|
||||
ResolvableType type = ResolvableType.forMethodReturnType(
|
||||
Methods.class.getMethod("typedReturn"), TypedMethods.class);
|
||||
assertThat(type.resolve(), equalTo((Class) String.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveTypeVaraibleFromSimpleInterfaceType() {
|
||||
public void resolveTypeVariableFromSimpleInterfaceType() {
|
||||
ResolvableType type = ResolvableType.forClass(
|
||||
MySimpleInterfaceType.class).as(MyInterfaceType.class);
|
||||
assertThat(type.resolveGeneric(), equalTo((Class) String.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveTypeVaraibleFromSimpleCollectionInterfaceType() {
|
||||
public void resolveTypeVariableFromSimpleCollectionInterfaceType() {
|
||||
ResolvableType type = ResolvableType.forClass(
|
||||
MyCollectionInterfaceType.class).as(MyInterfaceType.class);
|
||||
assertThat(type.resolveGeneric(), equalTo((Class) Collection.class));
|
||||
@@ -723,14 +716,14 @@ public class ResolvableTypeTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveTypeVaraibleFromSimpleSuperclassType() {
|
||||
public void resolveTypeVariableFromSimpleSuperclassType() {
|
||||
ResolvableType type = ResolvableType.forClass(
|
||||
MySimpleSuperclassType.class).as(MySuperclassType.class);
|
||||
assertThat(type.resolveGeneric(), equalTo((Class) String.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveTypeVaraibleFromSimpleCollectionSuperclassType() {
|
||||
public void resolveTypeVariableFromSimpleCollectionSuperclassType() {
|
||||
ResolvableType type = ResolvableType.forClass(
|
||||
MyCollectionSuperclassType.class).as(MySuperclassType.class);
|
||||
assertThat(type.resolveGeneric(), equalTo((Class) Collection.class));
|
||||
@@ -1459,7 +1452,9 @@ public class ResolvableTypeTests {
|
||||
|
||||
void charSequenceParameter(List<CharSequence> cs);
|
||||
|
||||
<R extends CharSequence & Serializable> R boundedTypeVaraibleResult();
|
||||
<R extends CharSequence & Serializable> R boundedTypeVariableResult();
|
||||
|
||||
Map<String, ? extends List<? extends CharSequence>> boundedTypeVariableWildcardResult();
|
||||
|
||||
void nested(Map<Map<String, Integer>, Map<Byte, Long>> p);
|
||||
|
||||
|
||||
+39
-1
@@ -488,6 +488,20 @@ public class AnnotatedElementUtilsTests {
|
||||
assertTrue(isAnnotated(element, name));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getMergedAnnotationWithImplicitAliasesWithDefaultsInMetaAnnotationOnComposedAnnotation() {
|
||||
Class<?> element = ImplicitAliasesWithDefaultsClass.class;
|
||||
String name = AliasesWithDefaults.class.getName();
|
||||
AliasesWithDefaults annotation = getMergedAnnotation(element, AliasesWithDefaults.class);
|
||||
|
||||
assertNotNull("Should find @AliasesWithDefaults on " + element.getSimpleName(), annotation);
|
||||
assertEquals("a1", "ImplicitAliasesWithDefaults", annotation.a1());
|
||||
assertEquals("a2", "ImplicitAliasesWithDefaults", annotation.a2());
|
||||
|
||||
// Verify contracts between utility methods:
|
||||
assertTrue(isAnnotated(element, name));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getMergedAnnotationAttributesWithInvalidConventionBasedComposedAnnotation() {
|
||||
Class<?> element = InvalidConventionBasedComposedContextConfigClass.class;
|
||||
@@ -958,7 +972,6 @@ public class AnnotatedElementUtilsTests {
|
||||
String[] xmlConfigFiles() default {};
|
||||
}
|
||||
|
||||
|
||||
@ContextConfig
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@interface AliasedComposedContextConfig {
|
||||
@@ -999,6 +1012,27 @@ public class AnnotatedElementUtilsTests {
|
||||
@interface ComposedImplicitAliasesContextConfig {
|
||||
}
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@interface AliasesWithDefaults {
|
||||
|
||||
@AliasFor("a2")
|
||||
String a1() default "AliasesWithDefaults";
|
||||
|
||||
@AliasFor("a1")
|
||||
String a2() default "AliasesWithDefaults";
|
||||
}
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@AliasesWithDefaults
|
||||
@interface ImplicitAliasesWithDefaults {
|
||||
|
||||
@AliasFor(annotation = AliasesWithDefaults.class, attribute = "a1")
|
||||
String b1() default "ImplicitAliasesWithDefaults";
|
||||
|
||||
@AliasFor(annotation = AliasesWithDefaults.class, attribute = "a2")
|
||||
String b2() default "ImplicitAliasesWithDefaults";
|
||||
}
|
||||
|
||||
@ImplicitAliasesContextConfig
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@interface TransitiveImplicitAliasesContextConfig {
|
||||
@@ -1296,6 +1330,10 @@ public class AnnotatedElementUtilsTests {
|
||||
static class ImplicitAliasesContextConfigClass3 {
|
||||
}
|
||||
|
||||
@ImplicitAliasesWithDefaults
|
||||
static class ImplicitAliasesWithDefaultsClass {
|
||||
}
|
||||
|
||||
@TransitiveImplicitAliasesContextConfig(groovy = "test.groovy")
|
||||
static class TransitiveImplicitAliasesContextConfigClass {
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -29,6 +29,7 @@ import reactor.test.StepVerifier;
|
||||
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.core.io.buffer.DataBuffer;
|
||||
import org.springframework.core.io.buffer.DataBufferLimitException;
|
||||
import org.springframework.util.MimeType;
|
||||
import org.springframework.util.MimeTypeUtils;
|
||||
|
||||
@@ -126,6 +127,33 @@ public class StringDecoderTests extends AbstractDecoderTestCase<StringDecoder> {
|
||||
.verify());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void maxInMemoryLimit() {
|
||||
Flux<DataBuffer> input = Flux.just(
|
||||
stringBuffer("abc\n"), stringBuffer("defg\n"), stringBuffer("hijkl\n"));
|
||||
|
||||
this.decoder.setMaxInMemorySize(4);
|
||||
testDecode(input, String.class, step ->
|
||||
step.expectNext("abc", "defg").verifyError(DataBufferLimitException.class));
|
||||
}
|
||||
|
||||
@Test // gh-24312
|
||||
public void maxInMemoryLimitReleaseUnprocessedLinesFromCurrentBuffer() {
|
||||
Flux<DataBuffer> input = Flux.just(
|
||||
stringBuffer("TOO MUCH DATA\nanother line\n\nand another\n"));
|
||||
|
||||
this.decoder.setMaxInMemorySize(5);
|
||||
testDecode(input, String.class, step -> step.verifyError(DataBufferLimitException.class));
|
||||
}
|
||||
|
||||
@Test // gh-24339
|
||||
public void maxInMemoryLimitReleaseUnprocessedLinesWhenUnlimited() {
|
||||
Flux<DataBuffer> input = Flux.just(stringBuffer("Line 1\nLine 2\nLine 3\n"));
|
||||
|
||||
this.decoder.setMaxInMemorySize(-1);
|
||||
testDecodeCancel(input, ResolvableType.forClass(String.class), null, Collections.emptyMap());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void decodeNewLineIncludeDelimiters() {
|
||||
this.decoder = StringDecoder.allMimeTypes(StringDecoder.DEFAULT_DELIMITERS, false);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -17,6 +17,7 @@
|
||||
package org.springframework.core.io;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
@@ -129,6 +130,14 @@ public class ResourceTests {
|
||||
assertEquals(new FileSystemResource(file), resource);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fileSystemResourceWithFile() throws IOException {
|
||||
File file = new File(getClass().getResource("Resource.class").getFile());
|
||||
Resource resource = new FileSystemResource(file);
|
||||
doTestResource(resource);
|
||||
assertEquals(new FileSystemResource(file), resource);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFileSystemResourceWithFilePath() throws Exception {
|
||||
Path filePath = Paths.get(getClass().getResource("Resource.class").toURI());
|
||||
|
||||
+21
-7
@@ -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");
|
||||
|
||||
+63
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
+32
-9
@@ -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 {
|
||||
}
|
||||
|
||||
|
||||
+70
-50
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -16,6 +16,8 @@
|
||||
|
||||
package org.springframework.expression.spel.standard;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import org.springframework.core.convert.TypeDescriptor;
|
||||
import org.springframework.expression.EvaluationContext;
|
||||
import org.springframework.expression.EvaluationException;
|
||||
@@ -65,15 +67,15 @@ public class SpelExpression implements Expression {
|
||||
|
||||
// Holds the compiled form of the expression (if it has been compiled)
|
||||
@Nullable
|
||||
private CompiledExpression compiledAst;
|
||||
private volatile CompiledExpression compiledAst;
|
||||
|
||||
// Count of many times as the expression been interpreted - can trigger compilation
|
||||
// when certain limit reached
|
||||
private volatile int interpretedCount = 0;
|
||||
private final AtomicInteger interpretedCount = new AtomicInteger(0);
|
||||
|
||||
// The number of times compilation was attempted and failed - enables us to eventually
|
||||
// give up trying to compile it when it just doesn't seem to be possible.
|
||||
private volatile int failedAttempts = 0;
|
||||
private final AtomicInteger failedAttempts = new AtomicInteger(0);
|
||||
|
||||
|
||||
/**
|
||||
@@ -116,16 +118,17 @@ public class SpelExpression implements Expression {
|
||||
@Override
|
||||
@Nullable
|
||||
public Object getValue() throws EvaluationException {
|
||||
if (this.compiledAst != null) {
|
||||
CompiledExpression compiledAst = this.compiledAst;
|
||||
if (compiledAst != null) {
|
||||
try {
|
||||
EvaluationContext context = getEvaluationContext();
|
||||
return this.compiledAst.getValue(context.getRootObject().getValue(), context);
|
||||
return compiledAst.getValue(context.getRootObject().getValue(), context);
|
||||
}
|
||||
catch (Throwable ex) {
|
||||
// If running in mixed mode, revert to interpreted
|
||||
if (this.configuration.getCompilerMode() == SpelCompilerMode.MIXED) {
|
||||
this.interpretedCount = 0;
|
||||
this.compiledAst = null;
|
||||
this.interpretedCount.set(0);
|
||||
}
|
||||
else {
|
||||
// Running in SpelCompilerMode.immediate mode - propagate exception to caller
|
||||
@@ -144,10 +147,11 @@ public class SpelExpression implements Expression {
|
||||
@Override
|
||||
@Nullable
|
||||
public <T> T getValue(@Nullable Class<T> expectedResultType) throws EvaluationException {
|
||||
if (this.compiledAst != null) {
|
||||
CompiledExpression compiledAst = this.compiledAst;
|
||||
if (compiledAst != null) {
|
||||
try {
|
||||
EvaluationContext context = getEvaluationContext();
|
||||
Object result = this.compiledAst.getValue(context.getRootObject().getValue(), context);
|
||||
Object result = compiledAst.getValue(context.getRootObject().getValue(), context);
|
||||
if (expectedResultType == null) {
|
||||
return (T) result;
|
||||
}
|
||||
@@ -159,8 +163,8 @@ public class SpelExpression implements Expression {
|
||||
catch (Throwable ex) {
|
||||
// If running in mixed mode, revert to interpreted
|
||||
if (this.configuration.getCompilerMode() == SpelCompilerMode.MIXED) {
|
||||
this.interpretedCount = 0;
|
||||
this.compiledAst = null;
|
||||
this.interpretedCount.set(0);
|
||||
}
|
||||
else {
|
||||
// Running in SpelCompilerMode.immediate mode - propagate exception to caller
|
||||
@@ -179,15 +183,16 @@ public class SpelExpression implements Expression {
|
||||
@Override
|
||||
@Nullable
|
||||
public Object getValue(Object rootObject) throws EvaluationException {
|
||||
if (this.compiledAst != null) {
|
||||
CompiledExpression compiledAst = this.compiledAst;
|
||||
if (compiledAst != null) {
|
||||
try {
|
||||
return this.compiledAst.getValue(rootObject, getEvaluationContext());
|
||||
return compiledAst.getValue(rootObject, getEvaluationContext());
|
||||
}
|
||||
catch (Throwable ex) {
|
||||
// If running in mixed mode, revert to interpreted
|
||||
if (this.configuration.getCompilerMode() == SpelCompilerMode.MIXED) {
|
||||
this.interpretedCount = 0;
|
||||
this.compiledAst = null;
|
||||
this.interpretedCount.set(0);
|
||||
}
|
||||
else {
|
||||
// Running in SpelCompilerMode.immediate mode - propagate exception to caller
|
||||
@@ -207,9 +212,10 @@ public class SpelExpression implements Expression {
|
||||
@Override
|
||||
@Nullable
|
||||
public <T> T getValue(Object rootObject, @Nullable Class<T> expectedResultType) throws EvaluationException {
|
||||
if (this.compiledAst != null) {
|
||||
CompiledExpression compiledAst = this.compiledAst;
|
||||
if (compiledAst != null) {
|
||||
try {
|
||||
Object result = this.compiledAst.getValue(rootObject, getEvaluationContext());
|
||||
Object result = compiledAst.getValue(rootObject, getEvaluationContext());
|
||||
if (expectedResultType == null) {
|
||||
return (T)result;
|
||||
}
|
||||
@@ -221,8 +227,8 @@ public class SpelExpression implements Expression {
|
||||
catch (Throwable ex) {
|
||||
// If running in mixed mode, revert to interpreted
|
||||
if (this.configuration.getCompilerMode() == SpelCompilerMode.MIXED) {
|
||||
this.interpretedCount = 0;
|
||||
this.compiledAst = null;
|
||||
this.interpretedCount.set(0);
|
||||
}
|
||||
else {
|
||||
// Running in SpelCompilerMode.immediate mode - propagate exception to caller
|
||||
@@ -244,15 +250,16 @@ public class SpelExpression implements Expression {
|
||||
public Object getValue(EvaluationContext context) throws EvaluationException {
|
||||
Assert.notNull(context, "EvaluationContext is required");
|
||||
|
||||
if (this.compiledAst != null) {
|
||||
CompiledExpression compiledAst = this.compiledAst;
|
||||
if (compiledAst != null) {
|
||||
try {
|
||||
return this.compiledAst.getValue(context.getRootObject().getValue(), context);
|
||||
return compiledAst.getValue(context.getRootObject().getValue(), context);
|
||||
}
|
||||
catch (Throwable ex) {
|
||||
// If running in mixed mode, revert to interpreted
|
||||
if (this.configuration.getCompilerMode() == SpelCompilerMode.MIXED) {
|
||||
this.interpretedCount = 0;
|
||||
this.compiledAst = null;
|
||||
this.interpretedCount.set(0);
|
||||
}
|
||||
else {
|
||||
// Running in SpelCompilerMode.immediate mode - propagate exception to caller
|
||||
@@ -273,9 +280,10 @@ public class SpelExpression implements Expression {
|
||||
public <T> T getValue(EvaluationContext context, @Nullable Class<T> expectedResultType) throws EvaluationException {
|
||||
Assert.notNull(context, "EvaluationContext is required");
|
||||
|
||||
if (this.compiledAst != null) {
|
||||
CompiledExpression compiledAst = this.compiledAst;
|
||||
if (compiledAst != null) {
|
||||
try {
|
||||
Object result = this.compiledAst.getValue(context.getRootObject().getValue(), context);
|
||||
Object result = compiledAst.getValue(context.getRootObject().getValue(), context);
|
||||
if (expectedResultType != null) {
|
||||
return ExpressionUtils.convertTypedValue(context, new TypedValue(result), expectedResultType);
|
||||
}
|
||||
@@ -286,8 +294,8 @@ public class SpelExpression implements Expression {
|
||||
catch (Throwable ex) {
|
||||
// If running in mixed mode, revert to interpreted
|
||||
if (this.configuration.getCompilerMode() == SpelCompilerMode.MIXED) {
|
||||
this.interpretedCount = 0;
|
||||
this.compiledAst = null;
|
||||
this.interpretedCount.set(0);
|
||||
}
|
||||
else {
|
||||
// Running in SpelCompilerMode.immediate mode - propagate exception to caller
|
||||
@@ -307,15 +315,16 @@ public class SpelExpression implements Expression {
|
||||
public Object getValue(EvaluationContext context, Object rootObject) throws EvaluationException {
|
||||
Assert.notNull(context, "EvaluationContext is required");
|
||||
|
||||
if (this.compiledAst != null) {
|
||||
CompiledExpression compiledAst = this.compiledAst;
|
||||
if (compiledAst != null) {
|
||||
try {
|
||||
return this.compiledAst.getValue(rootObject, context);
|
||||
return compiledAst.getValue(rootObject, context);
|
||||
}
|
||||
catch (Throwable ex) {
|
||||
// If running in mixed mode, revert to interpreted
|
||||
if (this.configuration.getCompilerMode() == SpelCompilerMode.MIXED) {
|
||||
this.interpretedCount = 0;
|
||||
this.compiledAst = null;
|
||||
this.interpretedCount.set(0);
|
||||
}
|
||||
else {
|
||||
// Running in SpelCompilerMode.immediate mode - propagate exception to caller
|
||||
@@ -338,9 +347,10 @@ public class SpelExpression implements Expression {
|
||||
|
||||
Assert.notNull(context, "EvaluationContext is required");
|
||||
|
||||
if (this.compiledAst != null) {
|
||||
CompiledExpression compiledAst = this.compiledAst;
|
||||
if (compiledAst != null) {
|
||||
try {
|
||||
Object result = this.compiledAst.getValue(rootObject, context);
|
||||
Object result = compiledAst.getValue(rootObject, context);
|
||||
if (expectedResultType != null) {
|
||||
return ExpressionUtils.convertTypedValue(context, new TypedValue(result), expectedResultType);
|
||||
}
|
||||
@@ -351,8 +361,8 @@ public class SpelExpression implements Expression {
|
||||
catch (Throwable ex) {
|
||||
// If running in mixed mode, revert to interpreted
|
||||
if (this.configuration.getCompilerMode() == SpelCompilerMode.MIXED) {
|
||||
this.interpretedCount = 0;
|
||||
this.compiledAst = null;
|
||||
this.interpretedCount.set(0);
|
||||
}
|
||||
else {
|
||||
// Running in SpelCompilerMode.immediate mode - propagate exception to caller
|
||||
@@ -473,48 +483,58 @@ public class SpelExpression implements Expression {
|
||||
* @param expressionState the expression state used to determine compilation mode
|
||||
*/
|
||||
private void checkCompile(ExpressionState expressionState) {
|
||||
this.interpretedCount++;
|
||||
this.interpretedCount.incrementAndGet();
|
||||
SpelCompilerMode compilerMode = expressionState.getConfiguration().getCompilerMode();
|
||||
if (compilerMode != SpelCompilerMode.OFF) {
|
||||
if (compilerMode == SpelCompilerMode.IMMEDIATE) {
|
||||
if (this.interpretedCount > 1) {
|
||||
if (this.interpretedCount.get() > 1) {
|
||||
compileExpression();
|
||||
}
|
||||
}
|
||||
else {
|
||||
// compilerMode = SpelCompilerMode.MIXED
|
||||
if (this.interpretedCount > INTERPRETED_COUNT_THRESHOLD) {
|
||||
if (this.interpretedCount.get() > INTERPRETED_COUNT_THRESHOLD) {
|
||||
compileExpression();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Perform expression compilation. This will only succeed once exit descriptors for all nodes have
|
||||
* been determined. If the compilation fails and has failed more than 100 times the expression is
|
||||
* no longer considered suitable for compilation.
|
||||
* Perform expression compilation. This will only succeed once exit descriptors for
|
||||
* all nodes have been determined. If the compilation fails and has failed more than
|
||||
* 100 times the expression is no longer considered suitable for compilation.
|
||||
* @return whether this expression has been successfully compiled
|
||||
*/
|
||||
public boolean compileExpression() {
|
||||
if (this.failedAttempts > FAILED_ATTEMPTS_THRESHOLD) {
|
||||
CompiledExpression compiledAst = this.compiledAst;
|
||||
if (compiledAst != null) {
|
||||
// Previously compiled
|
||||
return true;
|
||||
}
|
||||
if (this.failedAttempts.get() > FAILED_ATTEMPTS_THRESHOLD) {
|
||||
// Don't try again
|
||||
return false;
|
||||
}
|
||||
if (this.compiledAst == null) {
|
||||
synchronized (this.expression) {
|
||||
// Possibly compiled by another thread before this thread got into the sync block
|
||||
if (this.compiledAst != null) {
|
||||
return true;
|
||||
}
|
||||
SpelCompiler compiler = SpelCompiler.getCompiler(this.configuration.getCompilerClassLoader());
|
||||
this.compiledAst = compiler.compile(this.ast);
|
||||
if (this.compiledAst == null) {
|
||||
this.failedAttempts++;
|
||||
}
|
||||
|
||||
synchronized (this) {
|
||||
if (this.compiledAst != null) {
|
||||
// Compiled by another thread before this thread got into the sync block
|
||||
return true;
|
||||
}
|
||||
SpelCompiler compiler = SpelCompiler.getCompiler(this.configuration.getCompilerClassLoader());
|
||||
compiledAst = compiler.compile(this.ast);
|
||||
if (compiledAst != null) {
|
||||
// Successfully compiled
|
||||
this.compiledAst = compiledAst;
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
// Failed to compile
|
||||
this.failedAttempts.incrementAndGet();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return (this.compiledAst != null);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -524,8 +544,8 @@ public class SpelExpression implements Expression {
|
||||
*/
|
||||
public void revertToInterpreted() {
|
||||
this.compiledAst = null;
|
||||
this.interpretedCount = 0;
|
||||
this.failedAttempts = 0;
|
||||
this.interpretedCount.set(0);
|
||||
this.failedAttempts.set(0);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -220,6 +220,7 @@ public class SingleColumnRowMapper<T> implements RowMapper<T> {
|
||||
* (with the required type specified only once).
|
||||
* @param requiredType the type that each result object is expected to match
|
||||
* @since 4.1
|
||||
* @see #newInstance(Class, ConversionService)
|
||||
*/
|
||||
public static <T> SingleColumnRowMapper<T> newInstance(Class<T> requiredType) {
|
||||
return new SingleColumnRowMapper<>(requiredType);
|
||||
@@ -229,10 +230,15 @@ public class SingleColumnRowMapper<T> implements RowMapper<T> {
|
||||
* Static factory method to create a new {@code SingleColumnRowMapper}
|
||||
* (with the required type specified only once).
|
||||
* @param requiredType the type that each result object is expected to match
|
||||
* @param conversionService the {@link ConversionService} for converting a fetched value
|
||||
* @param conversionService the {@link ConversionService} for converting a
|
||||
* fetched value, or {@code null} for none
|
||||
* @since 5.0.4
|
||||
* @see #newInstance(Class)
|
||||
* @see #setConversionService
|
||||
*/
|
||||
public static <T> SingleColumnRowMapper<T> newInstance(Class<T> requiredType, @Nullable ConversionService conversionService) {
|
||||
public static <T> SingleColumnRowMapper<T> newInstance(
|
||||
Class<T> requiredType, @Nullable ConversionService conversionService) {
|
||||
|
||||
SingleColumnRowMapper<T> rowMapper = newInstance(requiredType);
|
||||
rowMapper.setConversionService(conversionService);
|
||||
return rowMapper;
|
||||
|
||||
+26
-1
@@ -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>
|
||||
|
||||
+6
@@ -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();
|
||||
}
|
||||
|
||||
+2
-2
@@ -340,7 +340,7 @@ public class MappingJackson2MessageConverter implements SmartMessageConverter, B
|
||||
* sets the resulting value (either a mapped id or the raw Java class name)
|
||||
* into the configured type id message property.
|
||||
* @param object the payload object to set a type id for
|
||||
* @param message the JMS Message to set the type id on
|
||||
* @param message the JMS Message on which to set the type id property
|
||||
* @throws JMSException if thrown by JMS methods
|
||||
* @see #getJavaTypeForMessage(javax.jms.Message)
|
||||
* @see #setTypeIdPropertyName(String)
|
||||
@@ -442,7 +442,7 @@ public class MappingJackson2MessageConverter implements SmartMessageConverter, B
|
||||
* <p>The default implementation parses the configured type id property name
|
||||
* and consults the configured type id mapping. This can be overridden with
|
||||
* a different strategy, e.g. doing some heuristics based on message origin.
|
||||
* @param message the JMS Message to set the type id on
|
||||
* @param message the JMS Message from which to get the type id property
|
||||
* @throws JMSException if thrown by JMS methods
|
||||
* @see #setTypeIdOnMessage(Object, javax.jms.Message)
|
||||
* @see #setTypeIdPropertyName(String)
|
||||
|
||||
-1
@@ -560,7 +560,6 @@ public class MultiServerUserRegistry implements SimpUserRegistry, SmartApplicati
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ dependencies {
|
||||
optional(project(":spring-context"))
|
||||
optional(project(":spring-web"))
|
||||
optional("org.eclipse.persistence:org.eclipse.persistence.jpa:2.7.4")
|
||||
optional("org.hibernate:hibernate-core:5.3.12.Final")
|
||||
optional("org.hibernate:hibernate-core:5.3.15.Final")
|
||||
optional("javax.servlet:javax.servlet-api:3.1.0")
|
||||
testCompile("org.aspectj:aspectjweaver:${aspectjVersion}")
|
||||
testCompile("org.hsqldb:hsqldb:${hsqldbVersion}")
|
||||
|
||||
@@ -60,8 +60,8 @@ dependencies {
|
||||
testCompile("javax.ejb:javax.ejb-api:3.2")
|
||||
testCompile("javax.interceptor:javax.interceptor-api:1.2.2")
|
||||
testCompile("javax.mail:javax.mail-api:1.6.2")
|
||||
testCompile("org.hibernate:hibernate-core:5.3.12.Final")
|
||||
testCompile("org.hibernate:hibernate-validator:6.0.17.Final")
|
||||
testCompile("org.hibernate:hibernate-core:5.3.15.Final")
|
||||
testCompile("org.hibernate:hibernate-validator:6.0.18.Final")
|
||||
// Enable use of the JUnit Platform Runner
|
||||
testCompile("org.junit.platform:junit-platform-runner")
|
||||
testCompile("org.junit.jupiter:junit-jupiter-params")
|
||||
|
||||
@@ -16,6 +16,10 @@
|
||||
|
||||
package org.springframework.mock.web;
|
||||
|
||||
import java.time.DateTimeException;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
|
||||
import javax.servlet.http.Cookie;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
@@ -28,6 +32,7 @@ import org.springframework.util.StringUtils;
|
||||
*
|
||||
* @author Vedran Pavic
|
||||
* @author Juergen Hoeller
|
||||
* @author Sam Brannen
|
||||
* @since 5.1
|
||||
*/
|
||||
public class MockCookie extends Cookie {
|
||||
@@ -35,12 +40,15 @@ public class MockCookie extends Cookie {
|
||||
private static final long serialVersionUID = 4312531139502726325L;
|
||||
|
||||
|
||||
@Nullable
|
||||
private ZonedDateTime expires;
|
||||
|
||||
@Nullable
|
||||
private String sameSite;
|
||||
|
||||
|
||||
/**
|
||||
* Constructor with the cookie name and value.
|
||||
* Construct a new {@link MockCookie} with the supplied name and value.
|
||||
* @param name the name
|
||||
* @param value the value
|
||||
* @see Cookie#Cookie(String, String)
|
||||
@@ -49,12 +57,29 @@ public class MockCookie extends Cookie {
|
||||
super(name, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the "Expires" attribute for this cookie.
|
||||
* @since 5.1.11
|
||||
*/
|
||||
public void setExpires(@Nullable ZonedDateTime expires) {
|
||||
this.expires = expires;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the "SameSite" attribute to the cookie.
|
||||
* Get the "Expires" attribute for this cookie.
|
||||
* @since 5.1.11
|
||||
* @return the "Expires" attribute for this cookie, or {@code null} if not set
|
||||
*/
|
||||
@Nullable
|
||||
public ZonedDateTime getExpires() {
|
||||
return this.expires;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the "SameSite" attribute for this cookie.
|
||||
* <p>This limits the scope of the cookie such that it will only be attached
|
||||
* to same site requests if {@code "Strict"} or cross-site requests if
|
||||
* {@code "Lax"}.
|
||||
* to same-site requests if the supplied value is {@code "Strict"} or cross-site
|
||||
* requests if the supplied value is {@code "Lax"}.
|
||||
* @see <a href="https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis#section-4.1.2.7">RFC6265 bis</a>
|
||||
*/
|
||||
public void setSameSite(@Nullable String sameSite) {
|
||||
@@ -62,7 +87,8 @@ public class MockCookie extends Cookie {
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the "SameSite" attribute, or {@code null} if not set.
|
||||
* Get the "SameSite" attribute for this cookie.
|
||||
* @return the "SameSite" attribute for this cookie, or {@code null} if not set
|
||||
*/
|
||||
@Nullable
|
||||
public String getSameSite() {
|
||||
@@ -71,7 +97,7 @@ public class MockCookie extends Cookie {
|
||||
|
||||
|
||||
/**
|
||||
* Factory method that parses the value of a "Set-Cookie" header.
|
||||
* Factory method that parses the value of the supplied "Set-Cookie" header.
|
||||
* @param setCookieHeader the "Set-Cookie" value; never {@code null} or empty
|
||||
* @return the created cookie
|
||||
*/
|
||||
@@ -94,6 +120,15 @@ public class MockCookie extends Cookie {
|
||||
else if (StringUtils.startsWithIgnoreCase(attribute, "Max-Age")) {
|
||||
cookie.setMaxAge(Integer.parseInt(extractAttributeValue(attribute, setCookieHeader)));
|
||||
}
|
||||
else if (StringUtils.startsWithIgnoreCase(attribute, "Expires")) {
|
||||
try {
|
||||
cookie.setExpires(ZonedDateTime.parse(extractAttributeValue(attribute, setCookieHeader),
|
||||
DateTimeFormatter.RFC_1123_DATE_TIME));
|
||||
}
|
||||
catch (DateTimeException ex) {
|
||||
// ignore invalid date formats
|
||||
}
|
||||
}
|
||||
else if (StringUtils.startsWithIgnoreCase(attribute, "Path")) {
|
||||
cookie.setPath(extractAttributeValue(attribute, setCookieHeader));
|
||||
}
|
||||
|
||||
+11
-3
@@ -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;
|
||||
}
|
||||
|
||||
+15
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -23,6 +23,7 @@ import java.util.function.Consumer;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.client.reactive.ClientHttpConnector;
|
||||
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
|
||||
import org.springframework.http.codec.ClientCodecConfigurer;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
@@ -136,12 +137,25 @@ class DefaultWebTestClientBuilder implements WebTestClient.Builder {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public WebTestClient.Builder codecs(Consumer<ClientCodecConfigurer> configurer) {
|
||||
this.webClientBuilder.codecs(configurer);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public WebTestClient.Builder exchangeStrategies(ExchangeStrategies strategies) {
|
||||
this.webClientBuilder.exchangeStrategies(strategies);
|
||||
return this;
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
@Override
|
||||
public WebTestClient.Builder exchangeStrategies(Consumer<ExchangeStrategies.Builder> configurer) {
|
||||
this.webClientBuilder.exchangeStrategies(configurer);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public WebTestClient.Builder responseTimeout(Duration timeout) {
|
||||
this.responseTimeout = timeout;
|
||||
|
||||
+28
-3
@@ -36,6 +36,7 @@ import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.client.reactive.ClientHttpConnector;
|
||||
import org.springframework.http.client.reactive.ClientHttpRequest;
|
||||
import org.springframework.http.codec.ClientCodecConfigurer;
|
||||
import org.springframework.http.codec.ServerCodecConfigurer;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
@@ -77,6 +78,7 @@ import org.springframework.web.util.UriBuilderFactory;
|
||||
* and Spring Kotlin extensions to perform integration tests on an embedded WebFlux server.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Brian Clozel
|
||||
* @since 5.0
|
||||
* @see StatusAssertions
|
||||
* @see HeaderAssertions
|
||||
@@ -434,13 +436,36 @@ public interface WebTestClient {
|
||||
*/
|
||||
Builder filters(Consumer<List<ExchangeFilterFunction>> filtersConsumer);
|
||||
|
||||
/**
|
||||
* Configure the codecs for the {@code WebClient} in the
|
||||
* {@link #exchangeStrategies(ExchangeStrategies) underlying}
|
||||
* {@code ExchangeStrategies}.
|
||||
* @param configurer the configurer to apply
|
||||
* @since 5.1.13
|
||||
*/
|
||||
Builder codecs(Consumer<ClientCodecConfigurer> configurer);
|
||||
|
||||
/**
|
||||
* Configure the {@link ExchangeStrategies} to use.
|
||||
* <p>By default {@link ExchangeStrategies#withDefaults()} is used.
|
||||
* <p>For most cases, prefer using {@link #codecs(Consumer)} which allows
|
||||
* customizing the codecs in the {@code ExchangeStrategies} rather than
|
||||
* replace them. That ensures multiple parties can contribute to codecs
|
||||
* configuration.
|
||||
* <p>By default this is set to {@link ExchangeStrategies#withDefaults()}.
|
||||
* @param strategies the strategies to use
|
||||
*/
|
||||
Builder exchangeStrategies(ExchangeStrategies strategies);
|
||||
|
||||
/**
|
||||
* Customize the strategies configured via
|
||||
* {@link #exchangeStrategies(ExchangeStrategies)}. This method is
|
||||
* designed for use in scenarios where multiple parties wish to update
|
||||
* the {@code ExchangeStrategies}.
|
||||
* @deprecated as of 5.1.13 in favor of {@link #codecs(Consumer)}
|
||||
*/
|
||||
@Deprecated
|
||||
Builder exchangeStrategies(Consumer<ExchangeStrategies.Builder> configurer);
|
||||
|
||||
/**
|
||||
* Max amount of time to wait for responses.
|
||||
* <p>By default 5 seconds.
|
||||
@@ -877,7 +902,7 @@ public interface WebTestClient {
|
||||
* @since 5.1
|
||||
* @see #xpath(String, Map, Object...)
|
||||
*/
|
||||
default XpathAssertions xpath(String expression, Object... args){
|
||||
default XpathAssertions xpath(String expression, Object... args) {
|
||||
return xpath(expression, null, args);
|
||||
}
|
||||
|
||||
@@ -891,7 +916,7 @@ public interface WebTestClient {
|
||||
* @param args arguments to parameterize the expression
|
||||
* @since 5.1
|
||||
*/
|
||||
XpathAssertions xpath(String expression, @Nullable Map<String, String> namespaces, Object... args);
|
||||
XpathAssertions xpath(String expression, @Nullable Map<String, String> namespaces, Object... args);
|
||||
|
||||
/**
|
||||
* Assert the response body content with the given {@link Consumer}.
|
||||
|
||||
@@ -16,6 +16,9 @@
|
||||
|
||||
package org.springframework.mock.web;
|
||||
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.ExpectedException;
|
||||
@@ -67,8 +70,8 @@ public class MockCookieTests {
|
||||
|
||||
@Test
|
||||
public void parseHeaderWithAttributes() {
|
||||
MockCookie cookie = MockCookie.parse(
|
||||
"SESSION=123; Domain=example.com; Max-Age=60; Path=/; Secure; HttpOnly; SameSite=Lax");
|
||||
MockCookie cookie = MockCookie.parse("SESSION=123; Domain=example.com; Max-Age=60; " +
|
||||
"Expires=Tue, 8 Oct 2019 19:50:00 GMT; Path=/; Secure; HttpOnly; SameSite=Lax");
|
||||
|
||||
assertCookie(cookie, "SESSION", "123");
|
||||
assertEquals("example.com", cookie.getDomain());
|
||||
@@ -76,9 +79,27 @@ public class MockCookieTests {
|
||||
assertEquals("/", cookie.getPath());
|
||||
assertTrue(cookie.getSecure());
|
||||
assertTrue(cookie.isHttpOnly());
|
||||
assertEquals(ZonedDateTime.parse("Tue, 8 Oct 2019 19:50:00 GMT",
|
||||
DateTimeFormatter.RFC_1123_DATE_TIME), cookie.getExpires());
|
||||
assertEquals("Lax", cookie.getSameSite());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parseHeaderWithZeroExpiresAttribute() {
|
||||
MockCookie cookie = MockCookie.parse("SESSION=123; Expires=0");
|
||||
|
||||
assertCookie(cookie, "SESSION", "123");
|
||||
assertNull(cookie.getExpires());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parseHeaderWithBogusExpiresAttribute() {
|
||||
MockCookie cookie = MockCookie.parse("SESSION=123; Expires=bogus");
|
||||
|
||||
assertCookie(cookie, "SESSION", "123");
|
||||
assertNull(cookie.getExpires());
|
||||
}
|
||||
|
||||
private void assertCookie(MockCookie cookie, String name, String value) {
|
||||
assertEquals(name, cookie.getName());
|
||||
assertEquals(value, cookie.getValue());
|
||||
@@ -109,8 +130,8 @@ public class MockCookieTests {
|
||||
|
||||
@Test
|
||||
public void parseHeaderWithAttributesCaseSensitivity() {
|
||||
MockCookie cookie = MockCookie.parse(
|
||||
"SESSION=123; domain=example.com; max-age=60; path=/; secure; httponly; samesite=Lax");
|
||||
MockCookie cookie = MockCookie.parse("SESSION=123; domain=example.com; max-age=60; " +
|
||||
"expires=Tue, 8 Oct 2019 19:50:00 GMT; path=/; secure; httponly; samesite=Lax");
|
||||
|
||||
assertCookie(cookie, "SESSION", "123");
|
||||
assertEquals("example.com", cookie.getDomain());
|
||||
@@ -118,6 +139,8 @@ public class MockCookieTests {
|
||||
assertEquals("/", cookie.getPath());
|
||||
assertTrue(cookie.getSecure());
|
||||
assertTrue(cookie.isHttpOnly());
|
||||
assertEquals(ZonedDateTime.parse("Tue, 8 Oct 2019 19:50:00 GMT",
|
||||
DateTimeFormatter.RFC_1123_DATE_TIME), cookie.getExpires());
|
||||
assertEquals("Lax", cookie.getSameSite());
|
||||
}
|
||||
|
||||
|
||||
+52
@@ -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");
|
||||
|
||||
+57
-5
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+206
@@ -0,0 +1,206 @@
|
||||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.test.web.servlet.samples.spr;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Scope;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.test.context.junit4.SpringRunner;
|
||||
import org.springframework.test.context.web.WebAppConfiguration;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.ControllerAdvice;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.context.WebApplicationContext;
|
||||
import org.springframework.web.context.annotation.RequestScope;
|
||||
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.forwardedUrl;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
import static org.springframework.test.web.servlet.setup.MockMvcBuilders.webAppContextSetup;
|
||||
|
||||
/**
|
||||
* Integration tests for {@link ControllerAdvice @ControllerAdvice}.
|
||||
*
|
||||
* <p>Introduced in conjunction with
|
||||
* <a href="https://github.com/spring-projects/spring-framework/issues/24017">gh-24017</a>.
|
||||
*
|
||||
* @author Sam Brannen
|
||||
* @since 5.1.12
|
||||
*/
|
||||
@RunWith(SpringRunner.class)
|
||||
@WebAppConfiguration
|
||||
public class ControllerAdviceIntegrationTests {
|
||||
|
||||
@Autowired
|
||||
WebApplicationContext wac;
|
||||
|
||||
MockMvc mockMvc;
|
||||
|
||||
@Before
|
||||
public void setUpMockMvc() {
|
||||
this.mockMvc = webAppContextSetup(wac).build();
|
||||
resetCounters();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void controllerAdviceIsAppliedOnlyOnce() throws Exception {
|
||||
this.mockMvc.perform(get("/test").param("requestParam", "foo"))//
|
||||
.andExpect(status().isOk())//
|
||||
.andExpect(forwardedUrl("singleton:1;prototype:1;request-scoped:1;requestParam:foo"));
|
||||
|
||||
assertEquals(1, SingletonControllerAdvice.invocationCount.get());
|
||||
assertEquals(1, PrototypeControllerAdvice.invocationCount.get());
|
||||
assertEquals(1, RequestScopedControllerAdvice.invocationCount.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void prototypeAndRequestScopedControllerAdviceBeansAreNotCached() throws Exception {
|
||||
this.mockMvc.perform(get("/test").param("requestParam", "foo"))//
|
||||
.andExpect(status().isOk())//
|
||||
.andExpect(forwardedUrl("singleton:1;prototype:1;request-scoped:1;requestParam:foo"));
|
||||
|
||||
// singleton @ControllerAdvice beans should not be instantiated again.
|
||||
assertEquals(0, SingletonControllerAdvice.instanceCount.get());
|
||||
// prototype and request-scoped @ControllerAdvice beans should be instantiated once per request.
|
||||
assertEquals(1, PrototypeControllerAdvice.instanceCount.get());
|
||||
assertEquals(1, RequestScopedControllerAdvice.instanceCount.get());
|
||||
|
||||
this.mockMvc.perform(get("/test").param("requestParam", "bar"))//
|
||||
.andExpect(status().isOk())//
|
||||
.andExpect(forwardedUrl("singleton:2;prototype:2;request-scoped:2;requestParam:bar"));
|
||||
|
||||
// singleton @ControllerAdvice beans should not be instantiated again.
|
||||
assertEquals(0, SingletonControllerAdvice.instanceCount.get());
|
||||
// prototype and request-scoped @ControllerAdvice beans should be instantiated once per request.
|
||||
assertEquals(2, PrototypeControllerAdvice.instanceCount.get());
|
||||
assertEquals(2, RequestScopedControllerAdvice.instanceCount.get());
|
||||
}
|
||||
|
||||
private static void resetCounters() {
|
||||
SingletonControllerAdvice.invocationCount.set(0);
|
||||
SingletonControllerAdvice.instanceCount.set(0);
|
||||
PrototypeControllerAdvice.invocationCount.set(0);
|
||||
PrototypeControllerAdvice.instanceCount.set(0);
|
||||
RequestScopedControllerAdvice.invocationCount.set(0);
|
||||
RequestScopedControllerAdvice.instanceCount.set(0);
|
||||
}
|
||||
|
||||
|
||||
@Configuration
|
||||
@EnableWebMvc
|
||||
static class Config {
|
||||
|
||||
@Bean
|
||||
TestController testController() {
|
||||
return new TestController();
|
||||
}
|
||||
|
||||
@Bean
|
||||
SingletonControllerAdvice singletonControllerAdvice() {
|
||||
return new SingletonControllerAdvice();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Scope("prototype")
|
||||
PrototypeControllerAdvice prototypeControllerAdvice() {
|
||||
return new PrototypeControllerAdvice();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@RequestScope
|
||||
RequestScopedControllerAdvice requestScopedControllerAdvice() {
|
||||
return new RequestScopedControllerAdvice();
|
||||
}
|
||||
}
|
||||
|
||||
@ControllerAdvice
|
||||
static class SingletonControllerAdvice {
|
||||
|
||||
static final AtomicInteger instanceCount = new AtomicInteger();
|
||||
static final AtomicInteger invocationCount = new AtomicInteger();
|
||||
|
||||
{
|
||||
instanceCount.incrementAndGet();
|
||||
}
|
||||
|
||||
@ModelAttribute
|
||||
void initModel(Model model) {
|
||||
model.addAttribute("singleton", invocationCount.incrementAndGet());
|
||||
}
|
||||
}
|
||||
|
||||
@ControllerAdvice
|
||||
static class PrototypeControllerAdvice {
|
||||
|
||||
static final AtomicInteger instanceCount = new AtomicInteger();
|
||||
static final AtomicInteger invocationCount = new AtomicInteger();
|
||||
|
||||
{
|
||||
instanceCount.incrementAndGet();
|
||||
}
|
||||
|
||||
@ModelAttribute
|
||||
void initModel(Model model) {
|
||||
model.addAttribute("prototype", invocationCount.incrementAndGet());
|
||||
}
|
||||
}
|
||||
|
||||
@ControllerAdvice
|
||||
static class RequestScopedControllerAdvice {
|
||||
|
||||
static final AtomicInteger instanceCount = new AtomicInteger();
|
||||
static final AtomicInteger invocationCount = new AtomicInteger();
|
||||
|
||||
{
|
||||
instanceCount.incrementAndGet();
|
||||
}
|
||||
|
||||
@ModelAttribute
|
||||
void initModel(@RequestParam String requestParam, Model model) {
|
||||
model.addAttribute("requestParam", requestParam);
|
||||
model.addAttribute("request-scoped", invocationCount.incrementAndGet());
|
||||
}
|
||||
}
|
||||
|
||||
@Controller
|
||||
static class TestController {
|
||||
|
||||
@GetMapping("/test")
|
||||
String get(Model model) {
|
||||
Map<String, Object> map = model.asMap();
|
||||
return "singleton:" + map.get("singleton") +
|
||||
";prototype:" + map.get("prototype") +
|
||||
";request-scoped:" + map.get("request-scoped") +
|
||||
";requestParam:" + map.get("requestParam");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+3
-2
@@ -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);
|
||||
|
||||
@@ -38,7 +38,7 @@ dependencies {
|
||||
exclude group: "javax.servlet", module: "javax.servlet-api"
|
||||
}
|
||||
optional("org.eclipse.jetty:jetty-reactive-httpclient:1.0.3")
|
||||
optional("com.squareup.okhttp3:okhttp:3.14.3")
|
||||
optional("com.squareup.okhttp3:okhttp:3.14.6")
|
||||
optional("org.apache.httpcomponents:httpclient:4.5.10") {
|
||||
exclude group: "commons-logging", module: "commons-logging"
|
||||
}
|
||||
@@ -47,7 +47,7 @@ dependencies {
|
||||
}
|
||||
optional("commons-fileupload:commons-fileupload:1.4")
|
||||
optional("org.synchronoss.cloud:nio-multipart-parser:1.1.0")
|
||||
optional("com.fasterxml.woodstox:woodstox-core:5.2.0") { // woodstox before aalto
|
||||
optional("com.fasterxml.woodstox:woodstox-core:5.3.0") { // woodstox before aalto
|
||||
exclude group: "stax", module: "stax-api"
|
||||
}
|
||||
optional("com.fasterxml:aalto-xml:1.1.1")
|
||||
@@ -75,7 +75,7 @@ dependencies {
|
||||
testCompile("org.apache.tomcat.embed:tomcat-embed-core:${tomcatVersion}")
|
||||
testCompile("org.eclipse.jetty:jetty-server")
|
||||
testCompile("org.eclipse.jetty:jetty-servlet")
|
||||
testCompile("com.squareup.okhttp3:mockwebserver:3.14.3")
|
||||
testCompile("com.squareup.okhttp3:mockwebserver:3.14.6")
|
||||
testCompile("org.jetbrains.kotlin:kotlin-reflect:${kotlinVersion}")
|
||||
testCompile("org.skyscreamer:jsonassert:1.5.0")
|
||||
testCompile("org.xmlunit:xmlunit-matchers:2.6.2")
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -42,6 +42,10 @@ import static java.time.format.DateTimeFormatter.RFC_1123_DATE_TIME;
|
||||
*/
|
||||
public final class ContentDisposition {
|
||||
|
||||
private static final String INVALID_HEADER_FIELD_PARAMETER_FORMAT =
|
||||
"Invalid header field parameter format (as defined in RFC 5987)";
|
||||
|
||||
|
||||
@Nullable
|
||||
private final String type;
|
||||
|
||||
@@ -201,11 +205,11 @@ public final class ContentDisposition {
|
||||
if (this.filename != null) {
|
||||
if (this.charset == null || StandardCharsets.US_ASCII.equals(this.charset)) {
|
||||
sb.append("; filename=\"");
|
||||
sb.append(this.filename).append('\"');
|
||||
sb.append(escapeQuotationsInFilename(this.filename)).append('\"');
|
||||
}
|
||||
else {
|
||||
sb.append("; filename*=");
|
||||
sb.append(encodeHeaderFieldParam(this.filename, this.charset));
|
||||
sb.append(encodeFilename(this.filename, this.charset));
|
||||
}
|
||||
}
|
||||
if (this.size != null) {
|
||||
@@ -271,15 +275,23 @@ public final class ContentDisposition {
|
||||
String attribute = part.substring(0, eqIndex);
|
||||
String value = (part.startsWith("\"", eqIndex + 1) && part.endsWith("\"") ?
|
||||
part.substring(eqIndex + 2, part.length() - 1) :
|
||||
part.substring(eqIndex + 1, part.length()));
|
||||
part.substring(eqIndex + 1));
|
||||
if (attribute.equals("name") ) {
|
||||
name = value;
|
||||
}
|
||||
else if (attribute.equals("filename*") ) {
|
||||
filename = decodeHeaderFieldParam(value);
|
||||
charset = Charset.forName(value.substring(0, value.indexOf('\'')));
|
||||
Assert.isTrue(UTF_8.equals(charset) || ISO_8859_1.equals(charset),
|
||||
"Charset should be UTF-8 or ISO-8859-1");
|
||||
int idx1 = value.indexOf('\'');
|
||||
int idx2 = value.indexOf('\'', idx1 + 1);
|
||||
if (idx1 != -1 && idx2 != -1) {
|
||||
charset = Charset.forName(value.substring(0, idx1).trim());
|
||||
Assert.isTrue(UTF_8.equals(charset) || ISO_8859_1.equals(charset),
|
||||
"Charset should be UTF-8 or ISO-8859-1");
|
||||
filename = decodeFilename(value.substring(idx2 + 1), charset);
|
||||
}
|
||||
else {
|
||||
// US ASCII
|
||||
filename = decodeFilename(value, StandardCharsets.US_ASCII);
|
||||
}
|
||||
}
|
||||
else if (attribute.equals("filename") && (filename == null)) {
|
||||
filename = value;
|
||||
@@ -359,22 +371,15 @@ public final class ContentDisposition {
|
||||
/**
|
||||
* Decode the given header field param as describe in RFC 5987.
|
||||
* <p>Only the US-ASCII, UTF-8 and ISO-8859-1 charsets are supported.
|
||||
* @param input the header field param
|
||||
* @param filename the header field param
|
||||
* @param charset the charset to use
|
||||
* @return the encoded header field param
|
||||
* @see <a href="https://tools.ietf.org/html/rfc5987">RFC 5987</a>
|
||||
*/
|
||||
private static String decodeHeaderFieldParam(String input) {
|
||||
Assert.notNull(input, "Input String should not be null");
|
||||
int firstQuoteIndex = input.indexOf('\'');
|
||||
int secondQuoteIndex = input.indexOf('\'', firstQuoteIndex + 1);
|
||||
// US_ASCII
|
||||
if (firstQuoteIndex == -1 || secondQuoteIndex == -1) {
|
||||
return input;
|
||||
}
|
||||
Charset charset = Charset.forName(input.substring(0, firstQuoteIndex));
|
||||
Assert.isTrue(UTF_8.equals(charset) || ISO_8859_1.equals(charset),
|
||||
"Charset should be UTF-8 or ISO-8859-1");
|
||||
byte[] value = input.substring(secondQuoteIndex + 1, input.length()).getBytes(charset);
|
||||
private static String decodeFilename(String filename, Charset charset) {
|
||||
Assert.notNull(filename, "'input' String` should not be null");
|
||||
Assert.notNull(charset, "'charset' should not be null");
|
||||
byte[] value = filename.getBytes(charset);
|
||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||
int index = 0;
|
||||
while (index < value.length) {
|
||||
@@ -383,13 +388,18 @@ public final class ContentDisposition {
|
||||
bos.write((char) b);
|
||||
index++;
|
||||
}
|
||||
else if (b == '%') {
|
||||
char[] array = { (char)value[index + 1], (char)value[index + 2]};
|
||||
bos.write(Integer.parseInt(String.valueOf(array), 16));
|
||||
else if (b == '%' && index < value.length - 2) {
|
||||
char[] array = new char[]{(char) value[index + 1], (char) value[index + 2]};
|
||||
try {
|
||||
bos.write(Integer.parseInt(String.valueOf(array), 16));
|
||||
}
|
||||
catch (NumberFormatException ex) {
|
||||
throw new IllegalArgumentException(INVALID_HEADER_FIELD_PARAMETER_FORMAT, ex);
|
||||
}
|
||||
index+=3;
|
||||
}
|
||||
else {
|
||||
throw new IllegalArgumentException("Invalid header field parameter format (as defined in RFC 5987)");
|
||||
throw new IllegalArgumentException(INVALID_HEADER_FIELD_PARAMETER_FORMAT);
|
||||
}
|
||||
}
|
||||
return new String(bos.toByteArray(), charset);
|
||||
@@ -401,6 +411,23 @@ public final class ContentDisposition {
|
||||
c == '.' || c == '^' || c == '_' || c == '`' || c == '|' || c == '~';
|
||||
}
|
||||
|
||||
private static String escapeQuotationsInFilename(String filename) {
|
||||
if (filename.indexOf('"') == -1 && filename.indexOf('\\') == -1) {
|
||||
return filename;
|
||||
}
|
||||
boolean escaped = false;
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (char c : filename.toCharArray()) {
|
||||
sb.append((c == '"' && !escaped) ? "\\\"" : c);
|
||||
escaped = (!escaped && c == '\\');
|
||||
}
|
||||
// Remove backslash at the end..
|
||||
if (escaped) {
|
||||
sb.deleteCharAt(sb.length() - 1);
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode the given header field param as describe in RFC 5987.
|
||||
* @param input the header field param
|
||||
@@ -409,14 +436,11 @@ public final class ContentDisposition {
|
||||
* @return the encoded header field param
|
||||
* @see <a href="https://tools.ietf.org/html/rfc5987">RFC 5987</a>
|
||||
*/
|
||||
private static String encodeHeaderFieldParam(String input, Charset charset) {
|
||||
Assert.notNull(input, "Input String should not be null");
|
||||
Assert.notNull(charset, "Charset should not be null");
|
||||
if (StandardCharsets.US_ASCII.equals(charset)) {
|
||||
return input;
|
||||
}
|
||||
Assert.isTrue(UTF_8.equals(charset) || ISO_8859_1.equals(charset),
|
||||
"Charset should be UTF-8 or ISO-8859-1");
|
||||
private static String encodeFilename(String input, Charset charset) {
|
||||
Assert.notNull(input, "`input` is required");
|
||||
Assert.notNull(charset, "`charset` is required");
|
||||
Assert.isTrue(!StandardCharsets.US_ASCII.equals(charset), "ASCII does not require encoding");
|
||||
Assert.isTrue(UTF_8.equals(charset) || ISO_8859_1.equals(charset), "Only UTF-8 and ISO-8859-1 supported.");
|
||||
byte[] source = input.getBytes(charset);
|
||||
int len = source.length;
|
||||
StringBuilder sb = new StringBuilder(len << 1);
|
||||
@@ -449,7 +473,11 @@ public final class ContentDisposition {
|
||||
Builder name(String name);
|
||||
|
||||
/**
|
||||
* Set the value of the {@literal filename} parameter.
|
||||
* Set the value of the {@literal filename} parameter. The given
|
||||
* filename will be formatted as quoted-string, as defined in RFC 2616,
|
||||
* section 2.2, and any quote characters within the filename value will
|
||||
* be escaped with a backslash, e.g. {@code "foo\"bar.txt"} becomes
|
||||
* {@code "foo\\\"bar.txt"}.
|
||||
*/
|
||||
Builder filename(String filename);
|
||||
|
||||
@@ -530,12 +558,14 @@ public final class ContentDisposition {
|
||||
|
||||
@Override
|
||||
public Builder filename(String filename) {
|
||||
Assert.hasText(filename, "No filename");
|
||||
this.filename = filename;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Builder filename(String filename, Charset charset) {
|
||||
Assert.hasText(filename, "No filename");
|
||||
this.filename = filename;
|
||||
this.charset = charset;
|
||||
return this;
|
||||
|
||||
@@ -374,7 +374,7 @@ public final class ResponseCookie extends HttpCookie {
|
||||
}
|
||||
int char1 = domain.charAt(0);
|
||||
int charN = domain.charAt(domain.length() - 1);
|
||||
if (char1 == '.' || char1 == '-' || charN == '.' || charN == '-') {
|
||||
if (char1 == '-' || charN == '.' || charN == '-') {
|
||||
throw new IllegalArgumentException("Invalid first/last char in cookie domain: " + domain);
|
||||
}
|
||||
for (int i = 0, c = -1; i < domain.length(); i++) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -63,6 +63,12 @@ public interface ClientCodecConfigurer extends CodecConfigurer {
|
||||
@Override
|
||||
ClientDefaultCodecs defaultCodecs();
|
||||
|
||||
/**
|
||||
* {@inheritDoc}.
|
||||
*/
|
||||
@Override
|
||||
ClientCodecConfigurer clone();
|
||||
|
||||
|
||||
/**
|
||||
* Static factory method for a {@code ClientCodecConfigurer}.
|
||||
@@ -89,6 +95,8 @@ public interface ClientCodecConfigurer extends CodecConfigurer {
|
||||
* <p>By default if this is not set, and Jackson is available, the
|
||||
* {@link #jackson2JsonDecoder} override is used instead. Use this property
|
||||
* if you want to further customize the SSE decoder.
|
||||
* <p>Note that {@link #maxInMemorySize(int)}, if configured, will be
|
||||
* applied to the given decoder.
|
||||
* @param decoder the decoder to use
|
||||
*/
|
||||
void serverSentEventDecoder(Decoder<?> decoder);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -17,9 +17,11 @@
|
||||
package org.springframework.http.codec;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.springframework.core.codec.Decoder;
|
||||
import org.springframework.core.codec.Encoder;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* Defines a common interface for configuring either client or server HTTP
|
||||
@@ -87,6 +89,15 @@ public interface CodecConfigurer {
|
||||
*/
|
||||
List<HttpMessageWriter<?>> getWriters();
|
||||
|
||||
/**
|
||||
* Create a copy of this {@link CodecConfigurer}. The returned clone has its
|
||||
* own lists of default and custom codecs and generally can be configured
|
||||
* independently. Keep in mind however that codec instances (if any are
|
||||
* configured) are themselves not cloned.
|
||||
* @since 5.1.12
|
||||
*/
|
||||
CodecConfigurer clone();
|
||||
|
||||
|
||||
/**
|
||||
* Customize or replace the HTTP message readers and writers registered by
|
||||
@@ -98,6 +109,8 @@ public interface CodecConfigurer {
|
||||
|
||||
/**
|
||||
* Override the default Jackson JSON {@code Decoder}.
|
||||
* <p>Note that {@link #maxInMemorySize(int)}, if configured, will be
|
||||
* applied to the given decoder.
|
||||
* @param decoder the decoder instance to use
|
||||
* @see org.springframework.http.codec.json.Jackson2JsonDecoder
|
||||
*/
|
||||
@@ -112,6 +125,8 @@ public interface CodecConfigurer {
|
||||
|
||||
/**
|
||||
* Override the default Protobuf {@code Decoder}.
|
||||
* <p>Note that {@link #maxInMemorySize(int)}, if configured, will be
|
||||
* applied to the given decoder.
|
||||
* @param decoder the decoder instance to use
|
||||
* @since 5.1
|
||||
* @see org.springframework.http.codec.protobuf.ProtobufDecoder
|
||||
@@ -129,6 +144,8 @@ public interface CodecConfigurer {
|
||||
|
||||
/**
|
||||
* Override the default JAXB2 {@code Decoder}.
|
||||
* <p>Note that {@link #maxInMemorySize(int)}, if configured, will be
|
||||
* applied to the given decoder.
|
||||
* @param decoder the decoder instance to use
|
||||
* @since 5.1.3
|
||||
* @see org.springframework.http.codec.xml.Jaxb2XmlDecoder
|
||||
@@ -143,6 +160,22 @@ public interface CodecConfigurer {
|
||||
*/
|
||||
void jaxb2Encoder(Encoder<?> encoder);
|
||||
|
||||
/**
|
||||
* Configure a limit on the number of bytes that can be buffered whenever
|
||||
* the input stream needs to be aggregated. This can be a result of
|
||||
* decoding to a single {@code DataBuffer},
|
||||
* {@link java.nio.ByteBuffer ByteBuffer}, {@code byte[]},
|
||||
* {@link org.springframework.core.io.Resource Resource}, {@code String}, etc.
|
||||
* It can also occur when splitting the input stream, e.g. delimited text,
|
||||
* in which case the limit applies to data buffered between delimiters.
|
||||
* <p>By default this is not set, in which case individual codec defaults
|
||||
* apply. In 5.1 most codecs are not limited except {@code FormHttpMessageReader}
|
||||
* which is limited to 256K. In 5.2 all codecs are limited to 256K by default.
|
||||
* @param byteCount the max number of bytes to buffer, or -1 for unlimited
|
||||
* @since 5.1.11
|
||||
*/
|
||||
void maxInMemorySize(int byteCount);
|
||||
|
||||
/**
|
||||
* Whether to log form data at DEBUG level, and headers at TRACE level.
|
||||
* Both may contain sensitive information.
|
||||
@@ -159,18 +192,66 @@ public interface CodecConfigurer {
|
||||
*/
|
||||
interface CustomCodecs {
|
||||
|
||||
/**
|
||||
* Register a custom codec. This is expected to be one of the following:
|
||||
* <ul>
|
||||
* <li>{@link HttpMessageReader}
|
||||
* <li>{@link HttpMessageWriter}
|
||||
* <li>{@link Encoder} (wrapped internally with {@link EncoderHttpMessageWriter})
|
||||
* <li>{@link Decoder} (wrapped internally with {@link DecoderHttpMessageReader})
|
||||
* </ul>
|
||||
* @param codec the codec to register
|
||||
* @since 5.1.13
|
||||
*/
|
||||
void register(Object codec);
|
||||
|
||||
/**
|
||||
* Variant of {@link #register(Object)} that also applies the below
|
||||
* properties, if configured, via {@link #defaultCodecs()}:
|
||||
* <ul>
|
||||
* <li>{@link CodecConfigurer.DefaultCodecs#maxInMemorySize(int) maxInMemorySize}
|
||||
* <li>{@link CodecConfigurer.DefaultCodecs#enableLoggingRequestDetails(boolean) enableLoggingRequestDetails}
|
||||
* </ul>
|
||||
* <p>The properties are applied every time {@link #getReaders()} or
|
||||
* {@link #getWriters()} are used to obtain the list of configured
|
||||
* readers or writers.
|
||||
* @param codec the codec to register and apply default config to
|
||||
* @since 5.1.13
|
||||
*/
|
||||
void registerWithDefaultConfig(Object codec);
|
||||
|
||||
/**
|
||||
* Variant of {@link #register(Object)} that also allows the caller to
|
||||
* apply the properties from {@link DefaultCodecConfig} to the given
|
||||
* codec. If you want to apply all the properties, prefer using
|
||||
* {@link #registerWithDefaultConfig(Object)}.
|
||||
* <p>The consumer is called every time {@link #getReaders()} or
|
||||
* {@link #getWriters()} are used to obtain the list of configured
|
||||
* readers or writers.
|
||||
* @param codec the codec to register
|
||||
* @param configConsumer consumer of the default config
|
||||
* @since 5.1.13
|
||||
*/
|
||||
void registerWithDefaultConfig(Object codec, Consumer<DefaultCodecConfig> configConsumer);
|
||||
|
||||
/**
|
||||
* Add a custom {@code Decoder} internally wrapped with
|
||||
* {@link DecoderHttpMessageReader}).
|
||||
* @param decoder the decoder to add
|
||||
* @deprecated as of 5.1.13, use {@link #register(Object)} or
|
||||
* {@link #registerWithDefaultConfig(Object)} instead.
|
||||
*/
|
||||
@Deprecated
|
||||
void decoder(Decoder<?> decoder);
|
||||
|
||||
/**
|
||||
* Add a custom {@code Encoder}, internally wrapped with
|
||||
* {@link EncoderHttpMessageWriter}.
|
||||
* @param encoder the encoder to add
|
||||
* @deprecated as of 5.1.13, use {@link #register(Object)} or
|
||||
* {@link #registerWithDefaultConfig(Object)} instead.
|
||||
*/
|
||||
@Deprecated
|
||||
void encoder(Encoder<?> encoder);
|
||||
|
||||
/**
|
||||
@@ -178,7 +259,10 @@ public interface CodecConfigurer {
|
||||
* {@link DecoderHttpMessageReader} consider using the shortcut
|
||||
* {@link #decoder(Decoder)} instead.
|
||||
* @param reader the reader to add
|
||||
* @deprecated as of 5.1.13, use {@link #register(Object)} or
|
||||
* {@link #registerWithDefaultConfig(Object)} instead.
|
||||
*/
|
||||
@Deprecated
|
||||
void reader(HttpMessageReader<?> reader);
|
||||
|
||||
/**
|
||||
@@ -186,8 +270,49 @@ public interface CodecConfigurer {
|
||||
* {@link EncoderHttpMessageWriter} consider using the shortcut
|
||||
* {@link #encoder(Encoder)} instead.
|
||||
* @param writer the writer to add
|
||||
* @deprecated as of 5.1.13, use {@link #register(Object)} or
|
||||
* {@link #registerWithDefaultConfig(Object)} instead.
|
||||
*/
|
||||
@Deprecated
|
||||
void writer(HttpMessageWriter<?> writer);
|
||||
|
||||
/**
|
||||
* Register a callback for the {@link DefaultCodecConfig configuration}
|
||||
* applied to default codecs. This allows custom codecs to follow general
|
||||
* guidelines applied to default ones, such as logging details and limiting
|
||||
* the amount of buffered data.
|
||||
* @param codecsConfigConsumer the default codecs configuration callback
|
||||
* @deprecated as of 5.1.13, use {@link #registerWithDefaultConfig(Object)}
|
||||
* or {@link #registerWithDefaultConfig(Object, Consumer)} instead.
|
||||
*/
|
||||
@Deprecated
|
||||
void withDefaultCodecConfig(Consumer<DefaultCodecConfig> codecsConfigConsumer);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Exposes the values of properties configured through
|
||||
* {@link #defaultCodecs()} that are applied to default codecs.
|
||||
* The main purpose of this interface is to provide access to them so they
|
||||
* can also be applied to custom codecs if needed.
|
||||
* @since 5.1.12
|
||||
* @see CustomCodecs#registerWithDefaultConfig(Object, Consumer)
|
||||
*/
|
||||
interface DefaultCodecConfig {
|
||||
|
||||
/**
|
||||
* Get the configured limit on the number of bytes that can be buffered whenever
|
||||
* the input stream needs to be aggregated.
|
||||
*/
|
||||
@Nullable
|
||||
Integer maxInMemorySize();
|
||||
|
||||
/**
|
||||
* Whether to log form data at DEBUG level, and headers at TRACE level.
|
||||
* Both may contain sensitive information.
|
||||
*/
|
||||
@Nullable
|
||||
Boolean isEnableLoggingRequestDetails();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -30,6 +30,7 @@ import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.core.codec.Hints;
|
||||
import org.springframework.core.io.buffer.DataBufferLimitException;
|
||||
import org.springframework.core.io.buffer.DataBufferUtils;
|
||||
import org.springframework.core.log.LogFormatUtils;
|
||||
import org.springframework.http.MediaType;
|
||||
@@ -62,6 +63,8 @@ public class FormHttpMessageReader extends LoggingCodecSupport
|
||||
|
||||
private Charset defaultCharset = DEFAULT_CHARSET;
|
||||
|
||||
private int maxInMemorySize = 256 * 1024;
|
||||
|
||||
|
||||
/**
|
||||
* Set the default character set to use for reading form data when the
|
||||
@@ -80,6 +83,26 @@ public class FormHttpMessageReader extends LoggingCodecSupport
|
||||
return this.defaultCharset;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the max number of bytes for input form data. As form data is buffered
|
||||
* before it is parsed, this helps to limit the amount of buffering. Once
|
||||
* the limit is exceeded, {@link DataBufferLimitException} is raised.
|
||||
* <p>By default this is set to 256K.
|
||||
* @param byteCount the max number of bytes to buffer, or -1 for unlimited
|
||||
* @since 5.1.11
|
||||
*/
|
||||
public void setMaxInMemorySize(int byteCount) {
|
||||
this.maxInMemorySize = byteCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the {@link #setMaxInMemorySize configured} byte count limit.
|
||||
* @since 5.1.11
|
||||
*/
|
||||
public int getMaxInMemorySize() {
|
||||
return this.maxInMemorySize;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean canRead(ResolvableType elementType, @Nullable MediaType mediaType) {
|
||||
@@ -105,7 +128,7 @@ public class FormHttpMessageReader extends LoggingCodecSupport
|
||||
MediaType contentType = message.getHeaders().getContentType();
|
||||
Charset charset = getMediaTypeCharset(contentType);
|
||||
|
||||
return DataBufferUtils.join(message.getBody())
|
||||
return DataBufferUtils.join(message.getBody(), getMaxInMemorySize())
|
||||
.map(buffer -> {
|
||||
CharBuffer charBuffer = charset.decode(buffer.asByteBuffer());
|
||||
String body = charBuffer.toString();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -62,6 +62,12 @@ public interface ServerCodecConfigurer extends CodecConfigurer {
|
||||
@Override
|
||||
ServerDefaultCodecs defaultCodecs();
|
||||
|
||||
/**
|
||||
* {@inheritDoc}.
|
||||
*/
|
||||
@Override
|
||||
ServerCodecConfigurer clone();
|
||||
|
||||
|
||||
/**
|
||||
* Static factory method for a {@code ServerCodecConfigurer}.
|
||||
@@ -76,6 +82,23 @@ public interface ServerCodecConfigurer extends CodecConfigurer {
|
||||
*/
|
||||
interface ServerDefaultCodecs extends DefaultCodecs {
|
||||
|
||||
/**
|
||||
* Configure the {@code HttpMessageReader} to use for multipart requests.
|
||||
* <p>By default, if
|
||||
* <a href="https://github.com/synchronoss/nio-multipart">Synchronoss NIO Multipart</a>
|
||||
* is present, this is set to
|
||||
* {@link org.springframework.http.codec.multipart.MultipartHttpMessageReader
|
||||
* MultipartHttpMessageReader} created with an instance of
|
||||
* {@link org.springframework.http.codec.multipart.SynchronossPartHttpMessageReader
|
||||
* SynchronossPartHttpMessageReader}.
|
||||
* <p>Note that {@link #maxInMemorySize(int)} and/or
|
||||
* {@link #enableLoggingRequestDetails(boolean)}, if configured, will be
|
||||
* applied to the given reader, if applicable.
|
||||
* @param reader the message reader to use for multipart requests.
|
||||
* @since 5.1.11
|
||||
*/
|
||||
void multipartReader(HttpMessageReader<?> reader);
|
||||
|
||||
/**
|
||||
* Configure the {@code Encoder} to use for Server-Sent Events.
|
||||
* <p>By default if this is not set, and Jackson is available, the
|
||||
|
||||
+29
-2
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
+50
-12
@@ -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) {
|
||||
|
||||
+8
@@ -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);
|
||||
|
||||
+10
@@ -149,6 +149,16 @@ public class MultipartHttpMessageWriter extends LoggingCodecSupport
|
||||
return Collections.unmodifiableList(this.partWriters);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return the configured form writer.
|
||||
* @since 5.1.13
|
||||
*/
|
||||
@Nullable
|
||||
public HttpMessageWriter<MultiValueMap<String, String>> getFormWriter() {
|
||||
return this.formWriter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the character set to use for part headers such as
|
||||
* "Content-Disposition" (and its filename parameter).
|
||||
|
||||
+208
-75
@@ -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() {
|
||||
|
||||
+19
-4
@@ -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 + ")");
|
||||
}
|
||||
|
||||
+117
-39
@@ -17,7 +17,10 @@
|
||||
package org.springframework.http.codec.support;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.core.codec.Decoder;
|
||||
@@ -34,13 +37,14 @@ import org.springframework.util.Assert;
|
||||
* client and server specific variants.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Brian Clozel
|
||||
* @since 5.0
|
||||
*/
|
||||
class BaseCodecConfigurer implements CodecConfigurer {
|
||||
abstract class BaseCodecConfigurer implements CodecConfigurer {
|
||||
|
||||
private final BaseDefaultCodecs defaultCodecs;
|
||||
protected final BaseDefaultCodecs defaultCodecs;
|
||||
|
||||
private final DefaultCustomCodecs customCodecs = new DefaultCustomCodecs();
|
||||
protected final DefaultCustomCodecs customCodecs;
|
||||
|
||||
|
||||
/**
|
||||
@@ -50,8 +54,25 @@ class BaseCodecConfigurer implements CodecConfigurer {
|
||||
BaseCodecConfigurer(BaseDefaultCodecs defaultCodecs) {
|
||||
Assert.notNull(defaultCodecs, "'defaultCodecs' is required");
|
||||
this.defaultCodecs = defaultCodecs;
|
||||
this.customCodecs = new DefaultCustomCodecs();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a deep copy of the given {@link BaseCodecConfigurer}.
|
||||
* @since 5.1.12
|
||||
*/
|
||||
protected BaseCodecConfigurer(BaseCodecConfigurer other) {
|
||||
this.defaultCodecs = other.cloneDefaultCodecs();
|
||||
this.customCodecs = new DefaultCustomCodecs(other.customCodecs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sub-classes should override this to create deep copy of
|
||||
* {@link BaseDefaultCodecs} which can can be client or server specific.
|
||||
* @since 5.1.12
|
||||
*/
|
||||
protected abstract BaseDefaultCodecs cloneDefaultCodecs();
|
||||
|
||||
|
||||
@Override
|
||||
public DefaultCodecs defaultCodecs() {
|
||||
@@ -70,97 +91,154 @@ class BaseCodecConfigurer implements CodecConfigurer {
|
||||
|
||||
@Override
|
||||
public List<HttpMessageReader<?>> getReaders() {
|
||||
this.defaultCodecs.applyDefaultConfig(this.customCodecs);
|
||||
|
||||
List<HttpMessageReader<?>> result = new ArrayList<>();
|
||||
|
||||
result.addAll(this.defaultCodecs.getTypedReaders());
|
||||
result.addAll(this.customCodecs.getTypedReaders());
|
||||
|
||||
result.addAll(this.customCodecs.getTypedReaders().keySet());
|
||||
result.addAll(this.defaultCodecs.getObjectReaders());
|
||||
result.addAll(this.customCodecs.getObjectReaders());
|
||||
|
||||
result.addAll(this.customCodecs.getObjectReaders().keySet());
|
||||
result.addAll(this.defaultCodecs.getCatchAllReaders());
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<HttpMessageWriter<?>> getWriters() {
|
||||
return getWritersInternal(false);
|
||||
}
|
||||
this.defaultCodecs.applyDefaultConfig(this.customCodecs);
|
||||
|
||||
/**
|
||||
* Internal method that returns the configured writers.
|
||||
* @param forMultipart whether to returns writers for general use ("false"),
|
||||
* or for multipart requests only ("true"). Generally the two sets are the
|
||||
* same except for the multipart writer itself.
|
||||
*/
|
||||
protected List<HttpMessageWriter<?>> getWritersInternal(boolean forMultipart) {
|
||||
List<HttpMessageWriter<?>> result = new ArrayList<>();
|
||||
|
||||
result.addAll(this.defaultCodecs.getTypedWriters(forMultipart));
|
||||
result.addAll(this.customCodecs.getTypedWriters());
|
||||
|
||||
result.addAll(this.defaultCodecs.getObjectWriters(forMultipart));
|
||||
result.addAll(this.customCodecs.getObjectWriters());
|
||||
|
||||
result.addAll(this.defaultCodecs.getTypedWriters());
|
||||
result.addAll(this.customCodecs.getTypedWriters().keySet());
|
||||
result.addAll(this.defaultCodecs.getObjectWriters());
|
||||
result.addAll(this.customCodecs.getObjectWriters().keySet());
|
||||
result.addAll(this.defaultCodecs.getCatchAllWriters());
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public abstract CodecConfigurer clone();
|
||||
|
||||
|
||||
/**
|
||||
* Default implementation of {@code CustomCodecs}.
|
||||
*/
|
||||
private static final class DefaultCustomCodecs implements CustomCodecs {
|
||||
protected static final class DefaultCustomCodecs implements CustomCodecs {
|
||||
|
||||
private final List<HttpMessageReader<?>> typedReaders = new ArrayList<>();
|
||||
private final Map<HttpMessageReader<?>, Boolean> typedReaders = new LinkedHashMap<>(4);
|
||||
|
||||
private final List<HttpMessageWriter<?>> typedWriters = new ArrayList<>();
|
||||
private final Map<HttpMessageWriter<?>, Boolean> typedWriters = new LinkedHashMap<>(4);
|
||||
|
||||
private final List<HttpMessageReader<?>> objectReaders = new ArrayList<>();
|
||||
private final Map<HttpMessageReader<?>, Boolean> objectReaders = new LinkedHashMap<>(4);
|
||||
|
||||
private final List<HttpMessageWriter<?>> objectWriters = new ArrayList<>();
|
||||
private final Map<HttpMessageWriter<?>, Boolean> objectWriters = new LinkedHashMap<>(4);
|
||||
|
||||
private final List<Consumer<DefaultCodecConfig>> defaultConfigConsumers = new ArrayList<>(4);
|
||||
|
||||
DefaultCustomCodecs() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a deep copy of the given {@link DefaultCustomCodecs}.
|
||||
* @since 5.1.12
|
||||
*/
|
||||
DefaultCustomCodecs(DefaultCustomCodecs other) {
|
||||
other.typedReaders.putAll(this.typedReaders);
|
||||
other.typedWriters.putAll(this.typedWriters);
|
||||
other.objectReaders.putAll(this.objectReaders);
|
||||
other.objectWriters.putAll(this.objectWriters);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void register(Object codec) {
|
||||
addCodec(codec, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerWithDefaultConfig(Object codec) {
|
||||
addCodec(codec, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerWithDefaultConfig(Object codec, Consumer<DefaultCodecConfig> configConsumer) {
|
||||
addCodec(codec, false);
|
||||
this.defaultConfigConsumers.add(configConsumer);
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
@Override
|
||||
public void decoder(Decoder<?> decoder) {
|
||||
reader(new DecoderHttpMessageReader<>(decoder));
|
||||
addCodec(decoder, false);
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
@Override
|
||||
public void encoder(Encoder<?> encoder) {
|
||||
writer(new EncoderHttpMessageWriter<>(encoder));
|
||||
addCodec(encoder, false);
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
@Override
|
||||
public void reader(HttpMessageReader<?> reader) {
|
||||
boolean canReadToObject = reader.canRead(ResolvableType.forClass(Object.class), null);
|
||||
(canReadToObject ? this.objectReaders : this.typedReaders).add(reader);
|
||||
addCodec(reader, false);
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
@Override
|
||||
public void writer(HttpMessageWriter<?> writer) {
|
||||
boolean canWriteObject = writer.canWrite(ResolvableType.forClass(Object.class), null);
|
||||
(canWriteObject ? this.objectWriters : this.typedWriters).add(writer);
|
||||
addCodec(writer, false);
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
@Override
|
||||
public void withDefaultCodecConfig(Consumer<DefaultCodecConfig> codecsConfigConsumer) {
|
||||
this.defaultConfigConsumers.add(codecsConfigConsumer);
|
||||
}
|
||||
|
||||
private void addCodec(Object codec, boolean applyDefaultConfig) {
|
||||
|
||||
if (codec instanceof Decoder) {
|
||||
codec = new DecoderHttpMessageReader<>((Decoder<?>) codec);
|
||||
}
|
||||
else if (codec instanceof Encoder) {
|
||||
codec = new EncoderHttpMessageWriter<>((Encoder<?>) codec);
|
||||
}
|
||||
|
||||
if (codec instanceof HttpMessageReader) {
|
||||
HttpMessageReader<?> reader = (HttpMessageReader<?>) codec;
|
||||
boolean canReadToObject = reader.canRead(ResolvableType.forClass(Object.class), null);
|
||||
(canReadToObject ? this.objectReaders : this.typedReaders).put(reader, applyDefaultConfig);
|
||||
}
|
||||
else if (codec instanceof HttpMessageWriter) {
|
||||
HttpMessageWriter<?> writer = (HttpMessageWriter<?>) codec;
|
||||
boolean canWriteObject = writer.canWrite(ResolvableType.forClass(Object.class), null);
|
||||
(canWriteObject ? this.objectWriters : this.typedWriters).put(writer, applyDefaultConfig);
|
||||
}
|
||||
else {
|
||||
throw new IllegalArgumentException("Unexpected codec type: " + codec.getClass().getName());
|
||||
}
|
||||
}
|
||||
|
||||
// Package private accessors...
|
||||
|
||||
List<HttpMessageReader<?>> getTypedReaders() {
|
||||
Map<HttpMessageReader<?>, Boolean> getTypedReaders() {
|
||||
return this.typedReaders;
|
||||
}
|
||||
|
||||
List<HttpMessageWriter<?>> getTypedWriters() {
|
||||
Map<HttpMessageWriter<?>, Boolean> getTypedWriters() {
|
||||
return this.typedWriters;
|
||||
}
|
||||
|
||||
List<HttpMessageReader<?>> getObjectReaders() {
|
||||
Map<HttpMessageReader<?>, Boolean> getObjectReaders() {
|
||||
return this.objectReaders;
|
||||
}
|
||||
|
||||
List<HttpMessageWriter<?>> getObjectWriters() {
|
||||
Map<HttpMessageWriter<?>, Boolean> getObjectWriters() {
|
||||
return this.objectWriters;
|
||||
}
|
||||
|
||||
List<Consumer<DefaultCodecConfig>> getDefaultConfigConsumers() {
|
||||
return this.defaultConfigConsumers;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+201
-39
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -19,7 +19,9 @@ package org.springframework.http.codec.support;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.core.codec.AbstractDataBufferDecoder;
|
||||
import org.springframework.core.codec.ByteArrayDecoder;
|
||||
import org.springframework.core.codec.ByteArrayEncoder;
|
||||
import org.springframework.core.codec.ByteBufferDecoder;
|
||||
@@ -35,13 +37,19 @@ import org.springframework.http.codec.CodecConfigurer;
|
||||
import org.springframework.http.codec.DecoderHttpMessageReader;
|
||||
import org.springframework.http.codec.EncoderHttpMessageWriter;
|
||||
import org.springframework.http.codec.FormHttpMessageReader;
|
||||
import org.springframework.http.codec.FormHttpMessageWriter;
|
||||
import org.springframework.http.codec.HttpMessageReader;
|
||||
import org.springframework.http.codec.HttpMessageWriter;
|
||||
import org.springframework.http.codec.ResourceHttpMessageWriter;
|
||||
import org.springframework.http.codec.ServerSentEventHttpMessageReader;
|
||||
import org.springframework.http.codec.json.AbstractJackson2Decoder;
|
||||
import org.springframework.http.codec.json.Jackson2JsonDecoder;
|
||||
import org.springframework.http.codec.json.Jackson2JsonEncoder;
|
||||
import org.springframework.http.codec.json.Jackson2SmileDecoder;
|
||||
import org.springframework.http.codec.json.Jackson2SmileEncoder;
|
||||
import org.springframework.http.codec.multipart.MultipartHttpMessageReader;
|
||||
import org.springframework.http.codec.multipart.MultipartHttpMessageWriter;
|
||||
import org.springframework.http.codec.multipart.SynchronossPartHttpMessageReader;
|
||||
import org.springframework.http.codec.protobuf.ProtobufDecoder;
|
||||
import org.springframework.http.codec.protobuf.ProtobufEncoder;
|
||||
import org.springframework.http.codec.protobuf.ProtobufHttpMessageWriter;
|
||||
@@ -57,7 +65,7 @@ import org.springframework.util.ClassUtils;
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Sebastien Deleuze
|
||||
*/
|
||||
class BaseDefaultCodecs implements CodecConfigurer.DefaultCodecs {
|
||||
class BaseDefaultCodecs implements CodecConfigurer.DefaultCodecs, CodecConfigurer.DefaultCodecConfig {
|
||||
|
||||
static final boolean jackson2Present;
|
||||
|
||||
@@ -67,6 +75,8 @@ class BaseDefaultCodecs implements CodecConfigurer.DefaultCodecs {
|
||||
|
||||
private static final boolean protobufPresent;
|
||||
|
||||
static final boolean synchronossMultipartPresent;
|
||||
|
||||
static {
|
||||
ClassLoader classLoader = BaseCodecConfigurer.class.getClassLoader();
|
||||
jackson2Present = ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", classLoader) &&
|
||||
@@ -74,6 +84,7 @@ class BaseDefaultCodecs implements CodecConfigurer.DefaultCodecs {
|
||||
jackson2SmilePresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.smile.SmileFactory", classLoader);
|
||||
jaxb2Present = ClassUtils.isPresent("javax.xml.bind.Binder", classLoader);
|
||||
protobufPresent = ClassUtils.isPresent("com.google.protobuf.Message", classLoader);
|
||||
synchronossMultipartPresent = ClassUtils.isPresent("org.synchronoss.cloud.nio.multipart.NioMultipartParser", classLoader);
|
||||
}
|
||||
|
||||
|
||||
@@ -95,11 +106,33 @@ class BaseDefaultCodecs implements CodecConfigurer.DefaultCodecs {
|
||||
@Nullable
|
||||
private Encoder<?> jaxb2Encoder;
|
||||
|
||||
private boolean enableLoggingRequestDetails = false;
|
||||
@Nullable
|
||||
private Integer maxInMemorySize;
|
||||
|
||||
@Nullable
|
||||
private Boolean enableLoggingRequestDetails;
|
||||
|
||||
private boolean registerDefaults = true;
|
||||
|
||||
|
||||
BaseDefaultCodecs() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a deep copy of the given {@link BaseDefaultCodecs}.
|
||||
*/
|
||||
protected BaseDefaultCodecs(BaseDefaultCodecs other) {
|
||||
this.jackson2JsonDecoder = other.jackson2JsonDecoder;
|
||||
this.jackson2JsonEncoder = other.jackson2JsonEncoder;
|
||||
this.protobufDecoder = other.protobufDecoder;
|
||||
this.protobufEncoder = other.protobufEncoder;
|
||||
this.jaxb2Decoder = other.jaxb2Decoder;
|
||||
this.jaxb2Encoder = other.jaxb2Encoder;
|
||||
this.maxInMemorySize = other.maxInMemorySize;
|
||||
this.enableLoggingRequestDetails = other.enableLoggingRequestDetails;
|
||||
this.registerDefaults = other.registerDefaults;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void jackson2JsonDecoder(Decoder<?> decoder) {
|
||||
this.jackson2JsonDecoder = decoder;
|
||||
@@ -130,12 +163,25 @@ class BaseDefaultCodecs implements CodecConfigurer.DefaultCodecs {
|
||||
this.jaxb2Encoder = encoder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void maxInMemorySize(int byteCount) {
|
||||
this.maxInMemorySize = byteCount;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public Integer maxInMemorySize() {
|
||||
return this.maxInMemorySize;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void enableLoggingRequestDetails(boolean enable) {
|
||||
this.enableLoggingRequestDetails = enable;
|
||||
}
|
||||
|
||||
protected boolean isEnableLoggingRequestDetails() {
|
||||
@Override
|
||||
@Nullable
|
||||
public Boolean isEnableLoggingRequestDetails() {
|
||||
return this.enableLoggingRequestDetails;
|
||||
}
|
||||
|
||||
@@ -155,25 +201,109 @@ class BaseDefaultCodecs implements CodecConfigurer.DefaultCodecs {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
List<HttpMessageReader<?>> readers = new ArrayList<>();
|
||||
readers.add(new DecoderHttpMessageReader<>(new ByteArrayDecoder()));
|
||||
readers.add(new DecoderHttpMessageReader<>(new ByteBufferDecoder()));
|
||||
readers.add(new DecoderHttpMessageReader<>(new DataBufferDecoder()));
|
||||
readers.add(new DecoderHttpMessageReader<>(new ResourceDecoder()));
|
||||
readers.add(new DecoderHttpMessageReader<>(StringDecoder.textPlainOnly()));
|
||||
addCodec(readers, new DecoderHttpMessageReader<>(new ByteArrayDecoder()));
|
||||
addCodec(readers, new DecoderHttpMessageReader<>(new ByteBufferDecoder()));
|
||||
addCodec(readers, new DecoderHttpMessageReader<>(new DataBufferDecoder()));
|
||||
addCodec(readers, new DecoderHttpMessageReader<>(new ResourceDecoder()));
|
||||
addCodec(readers, new DecoderHttpMessageReader<>(StringDecoder.textPlainOnly()));
|
||||
if (protobufPresent) {
|
||||
Decoder<?> decoder = this.protobufDecoder != null ? this.protobufDecoder : new ProtobufDecoder();
|
||||
readers.add(new DecoderHttpMessageReader<>(decoder));
|
||||
addCodec(readers, new DecoderHttpMessageReader<>(decoder));
|
||||
}
|
||||
addCodec(readers, new FormHttpMessageReader());
|
||||
|
||||
FormHttpMessageReader formReader = new FormHttpMessageReader();
|
||||
formReader.setEnableLoggingRequestDetails(this.enableLoggingRequestDetails);
|
||||
readers.add(formReader);
|
||||
|
||||
// client vs server..
|
||||
extendTypedReaders(readers);
|
||||
|
||||
return readers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize a codec and add it to the List.
|
||||
* @since 5.1.13
|
||||
*/
|
||||
protected <T> void addCodec(List<T> codecs, T codec) {
|
||||
initCodec(codec);
|
||||
codecs.add(codec);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply {@link #maxInMemorySize()} and {@link #enableLoggingRequestDetails},
|
||||
* if configured by the application, to the given codec , including any
|
||||
* codec it contains.
|
||||
*/
|
||||
private void initCodec(@Nullable Object codec) {
|
||||
|
||||
if (codec instanceof DecoderHttpMessageReader) {
|
||||
codec = ((DecoderHttpMessageReader) codec).getDecoder();
|
||||
}
|
||||
else if (codec instanceof ServerSentEventHttpMessageReader) {
|
||||
codec = ((ServerSentEventHttpMessageReader) codec).getDecoder();
|
||||
}
|
||||
|
||||
if (codec == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Integer size = this.maxInMemorySize;
|
||||
if (size != null) {
|
||||
if (codec instanceof AbstractDataBufferDecoder) {
|
||||
((AbstractDataBufferDecoder<?>) codec).setMaxInMemorySize(size);
|
||||
}
|
||||
if (protobufPresent) {
|
||||
if (codec instanceof ProtobufDecoder) {
|
||||
((ProtobufDecoder) codec).setMaxMessageSize(size);
|
||||
}
|
||||
}
|
||||
if (jackson2Present) {
|
||||
if (codec instanceof AbstractJackson2Decoder) {
|
||||
((AbstractJackson2Decoder) codec).setMaxInMemorySize(size);
|
||||
}
|
||||
}
|
||||
if (jaxb2Present) {
|
||||
if (codec instanceof Jaxb2XmlDecoder) {
|
||||
((Jaxb2XmlDecoder) codec).setMaxInMemorySize(size);
|
||||
}
|
||||
}
|
||||
if (codec instanceof FormHttpMessageReader) {
|
||||
((FormHttpMessageReader) codec).setMaxInMemorySize(size);
|
||||
}
|
||||
if (synchronossMultipartPresent) {
|
||||
if (codec instanceof SynchronossPartHttpMessageReader) {
|
||||
((SynchronossPartHttpMessageReader) codec).setMaxInMemorySize(size);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Boolean enable = this.enableLoggingRequestDetails;
|
||||
if (enable != null) {
|
||||
if (codec instanceof FormHttpMessageReader) {
|
||||
((FormHttpMessageReader) codec).setEnableLoggingRequestDetails(enable);
|
||||
}
|
||||
if (codec instanceof MultipartHttpMessageReader) {
|
||||
((MultipartHttpMessageReader) codec).setEnableLoggingRequestDetails(enable);
|
||||
}
|
||||
if (synchronossMultipartPresent) {
|
||||
if (codec instanceof SynchronossPartHttpMessageReader) {
|
||||
((SynchronossPartHttpMessageReader) codec).setEnableLoggingRequestDetails(enable);
|
||||
}
|
||||
}
|
||||
if (codec instanceof FormHttpMessageWriter) {
|
||||
((FormHttpMessageWriter) codec).setEnableLoggingRequestDetails(enable);
|
||||
}
|
||||
if (codec instanceof MultipartHttpMessageWriter) {
|
||||
((MultipartHttpMessageWriter) codec).setEnableLoggingRequestDetails(enable);
|
||||
}
|
||||
}
|
||||
|
||||
if (codec instanceof MultipartHttpMessageReader) {
|
||||
initCodec(((MultipartHttpMessageReader) codec).getPartReader());
|
||||
}
|
||||
else if (codec instanceof MultipartHttpMessageWriter) {
|
||||
initCodec(((MultipartHttpMessageWriter) codec).getFormWriter());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for client or server specific typed readers.
|
||||
*/
|
||||
@@ -189,16 +319,19 @@ class BaseDefaultCodecs implements CodecConfigurer.DefaultCodecs {
|
||||
}
|
||||
List<HttpMessageReader<?>> readers = new ArrayList<>();
|
||||
if (jackson2Present) {
|
||||
readers.add(new DecoderHttpMessageReader<>(getJackson2JsonDecoder()));
|
||||
addCodec(readers, new DecoderHttpMessageReader<>(getJackson2JsonDecoder()));
|
||||
}
|
||||
if (jackson2SmilePresent) {
|
||||
readers.add(new DecoderHttpMessageReader<>(new Jackson2SmileDecoder()));
|
||||
addCodec(readers, new DecoderHttpMessageReader<>(new Jackson2SmileDecoder()));
|
||||
}
|
||||
if (jaxb2Present) {
|
||||
Decoder<?> decoder = this.jaxb2Decoder != null ? this.jaxb2Decoder : new Jaxb2XmlDecoder();
|
||||
readers.add(new DecoderHttpMessageReader<>(decoder));
|
||||
addCodec(readers, new DecoderHttpMessageReader<>(decoder));
|
||||
}
|
||||
|
||||
// client vs server..
|
||||
extendObjectReaders(readers);
|
||||
|
||||
return readers;
|
||||
}
|
||||
|
||||
@@ -215,19 +348,29 @@ class BaseDefaultCodecs implements CodecConfigurer.DefaultCodecs {
|
||||
if (!this.registerDefaults) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
List<HttpMessageReader<?>> result = new ArrayList<>();
|
||||
result.add(new DecoderHttpMessageReader<>(StringDecoder.allMimeTypes()));
|
||||
return result;
|
||||
List<HttpMessageReader<?>> readers = new ArrayList<>();
|
||||
addCodec(readers, new DecoderHttpMessageReader<>(StringDecoder.allMimeTypes()));
|
||||
return readers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return writers that support specific types.
|
||||
* @param forMultipart whether to returns writers for general use ("false"),
|
||||
* or for multipart requests only ("true"). Generally the two sets are the
|
||||
* same except for the multipart writer itself.
|
||||
* Return all writers that support specific types.
|
||||
*/
|
||||
@SuppressWarnings({"rawtypes" })
|
||||
final List<HttpMessageWriter<?>> getTypedWriters() {
|
||||
if (!this.registerDefaults) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
List<HttpMessageWriter<?>> writers = getBaseTypedWriters();
|
||||
extendTypedWriters(writers);
|
||||
return writers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return "base" typed writers only, i.e. common to client and server.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
final List<HttpMessageWriter<?>> getTypedWriters(boolean forMultipart) {
|
||||
final List<HttpMessageWriter<?>> getBaseTypedWriters() {
|
||||
if (!this.registerDefaults) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
@@ -237,10 +380,6 @@ class BaseDefaultCodecs implements CodecConfigurer.DefaultCodecs {
|
||||
writers.add(new EncoderHttpMessageWriter<>(new DataBufferEncoder()));
|
||||
writers.add(new ResourceHttpMessageWriter());
|
||||
writers.add(new EncoderHttpMessageWriter<>(CharSequenceEncoder.textPlainOnly()));
|
||||
// No client or server specific multipart writers currently..
|
||||
if (!forMultipart) {
|
||||
extendTypedWriters(writers);
|
||||
}
|
||||
if (protobufPresent) {
|
||||
Encoder<?> encoder = this.protobufEncoder != null ? this.protobufEncoder : new ProtobufEncoder();
|
||||
writers.add(new ProtobufHttpMessageWriter((Encoder) encoder));
|
||||
@@ -256,14 +395,20 @@ class BaseDefaultCodecs implements CodecConfigurer.DefaultCodecs {
|
||||
|
||||
/**
|
||||
* Return Object writers (JSON, XML, SSE).
|
||||
* @param forMultipart whether to returns writers for general use ("false"),
|
||||
* or for multipart requests only ("true"). Generally the two sets are the
|
||||
* same except for the multipart writer itself.
|
||||
*/
|
||||
final List<HttpMessageWriter<?>> getObjectWriters(boolean forMultipart) {
|
||||
final List<HttpMessageWriter<?>> getObjectWriters() {
|
||||
if (!this.registerDefaults) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
List<HttpMessageWriter<?>> writers = getBaseObjectWriters();
|
||||
extendObjectWriters(writers);
|
||||
return writers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return "base" object writers only, i.e. common to client and server.
|
||||
*/
|
||||
final List<HttpMessageWriter<?>> getBaseObjectWriters() {
|
||||
List<HttpMessageWriter<?>> writers = new ArrayList<>();
|
||||
if (jackson2Present) {
|
||||
writers.add(new EncoderHttpMessageWriter<>(getJackson2JsonEncoder()));
|
||||
@@ -275,10 +420,6 @@ class BaseDefaultCodecs implements CodecConfigurer.DefaultCodecs {
|
||||
Encoder<?> encoder = this.jaxb2Encoder != null ? this.jaxb2Encoder : new Jaxb2XmlEncoder();
|
||||
writers.add(new EncoderHttpMessageWriter<>(encoder));
|
||||
}
|
||||
// No client or server specific multipart writers currently..
|
||||
if (!forMultipart) {
|
||||
extendObjectWriters(writers);
|
||||
}
|
||||
return writers;
|
||||
}
|
||||
|
||||
@@ -300,15 +441,36 @@ class BaseDefaultCodecs implements CodecConfigurer.DefaultCodecs {
|
||||
return result;
|
||||
}
|
||||
|
||||
void applyDefaultConfig(BaseCodecConfigurer.DefaultCustomCodecs customCodecs) {
|
||||
applyDefaultConfig(customCodecs.getTypedReaders());
|
||||
applyDefaultConfig(customCodecs.getObjectReaders());
|
||||
applyDefaultConfig(customCodecs.getTypedWriters());
|
||||
applyDefaultConfig(customCodecs.getObjectWriters());
|
||||
customCodecs.getDefaultConfigConsumers().forEach(consumer -> consumer.accept(this));
|
||||
}
|
||||
|
||||
private void applyDefaultConfig(Map<?, Boolean> readers) {
|
||||
readers.entrySet().stream()
|
||||
.filter(Map.Entry::getValue)
|
||||
.map(Map.Entry::getKey)
|
||||
.forEach(this::initCodec);
|
||||
}
|
||||
|
||||
|
||||
// Accessors for use in subclasses...
|
||||
|
||||
protected Decoder<?> getJackson2JsonDecoder() {
|
||||
return (this.jackson2JsonDecoder != null ? this.jackson2JsonDecoder : new Jackson2JsonDecoder());
|
||||
if (this.jackson2JsonDecoder == null) {
|
||||
this.jackson2JsonDecoder = new Jackson2JsonDecoder();
|
||||
}
|
||||
return this.jackson2JsonDecoder;
|
||||
}
|
||||
|
||||
protected Encoder<?> getJackson2JsonEncoder() {
|
||||
return (this.jackson2JsonEncoder != null ? this.jackson2JsonEncoder : new Jackson2JsonEncoder());
|
||||
if (this.jackson2JsonEncoder == null) {
|
||||
this.jackson2JsonEncoder = new Jackson2JsonEncoder();
|
||||
}
|
||||
return this.jackson2JsonEncoder;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+35
-14
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -49,6 +49,17 @@ class ClientDefaultCodecsImpl extends BaseDefaultCodecs implements ClientCodecCo
|
||||
private Supplier<List<HttpMessageWriter<?>>> partWritersSupplier;
|
||||
|
||||
|
||||
ClientDefaultCodecsImpl() {
|
||||
}
|
||||
|
||||
ClientDefaultCodecsImpl(ClientDefaultCodecsImpl other) {
|
||||
super(other);
|
||||
this.multipartCodecs = (other.multipartCodecs != null ?
|
||||
new DefaultMultipartCodecs(other.multipartCodecs) : null);
|
||||
this.sseDecoder = other.sseDecoder;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set a supplier for part writers to use when
|
||||
* {@link #multipartCodecs()} are not explicitly configured.
|
||||
@@ -73,27 +84,28 @@ class ClientDefaultCodecsImpl extends BaseDefaultCodecs implements ClientCodecCo
|
||||
this.sseDecoder = decoder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ClientDefaultCodecsImpl clone() {
|
||||
ClientDefaultCodecsImpl codecs = new ClientDefaultCodecsImpl();
|
||||
codecs.multipartCodecs = this.multipartCodecs;
|
||||
codecs.sseDecoder = this.sseDecoder;
|
||||
codecs.partWritersSupplier = this.partWritersSupplier;
|
||||
return codecs;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void extendObjectReaders(List<HttpMessageReader<?>> objectReaders) {
|
||||
objectReaders.add(new ServerSentEventHttpMessageReader(getSseDecoder()));
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private Decoder<?> getSseDecoder() {
|
||||
return (this.sseDecoder != null ? this.sseDecoder : jackson2Present ? getJackson2JsonDecoder() : null);
|
||||
Decoder<?> decoder = (this.sseDecoder != null ?
|
||||
this.sseDecoder :
|
||||
jackson2Present ? getJackson2JsonDecoder() : null);
|
||||
|
||||
addCodec(objectReaders, new ServerSentEventHttpMessageReader(decoder));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void extendTypedWriters(List<HttpMessageWriter<?>> typedWriters) {
|
||||
|
||||
FormHttpMessageWriter formWriter = new FormHttpMessageWriter();
|
||||
formWriter.setEnableLoggingRequestDetails(isEnableLoggingRequestDetails());
|
||||
|
||||
MultipartHttpMessageWriter multipartWriter = new MultipartHttpMessageWriter(getPartWriters(), formWriter);
|
||||
multipartWriter.setEnableLoggingRequestDetails(isEnableLoggingRequestDetails());
|
||||
|
||||
typedWriters.add(multipartWriter);
|
||||
addCodec(typedWriters, new MultipartHttpMessageWriter(getPartWriters(), new FormHttpMessageWriter()));
|
||||
}
|
||||
|
||||
private List<HttpMessageWriter<?>> getPartWriters() {
|
||||
@@ -116,6 +128,15 @@ class ClientDefaultCodecsImpl extends BaseDefaultCodecs implements ClientCodecCo
|
||||
|
||||
private final List<HttpMessageWriter<?>> writers = new ArrayList<>();
|
||||
|
||||
|
||||
DefaultMultipartCodecs() {
|
||||
}
|
||||
|
||||
DefaultMultipartCodecs(DefaultMultipartCodecs other) {
|
||||
this.writers.addAll(other.writers);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public ClientCodecConfigurer.MultipartCodecs encoder(Encoder<?> encoder) {
|
||||
writer(new EncoderHttpMessageWriter<>(encoder));
|
||||
|
||||
+33
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -16,7 +16,11 @@
|
||||
|
||||
package org.springframework.http.codec.support;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.http.codec.ClientCodecConfigurer;
|
||||
import org.springframework.http.codec.HttpMessageWriter;
|
||||
|
||||
/**
|
||||
* Default implementation of {@link ClientCodecConfigurer}.
|
||||
@@ -26,14 +30,41 @@ import org.springframework.http.codec.ClientCodecConfigurer;
|
||||
*/
|
||||
public class DefaultClientCodecConfigurer extends BaseCodecConfigurer implements ClientCodecConfigurer {
|
||||
|
||||
|
||||
public DefaultClientCodecConfigurer() {
|
||||
super(new ClientDefaultCodecsImpl());
|
||||
((ClientDefaultCodecsImpl) defaultCodecs()).setPartWritersSupplier(() -> getWritersInternal(true));
|
||||
((ClientDefaultCodecsImpl) defaultCodecs()).setPartWritersSupplier(this::getPartWriters);
|
||||
}
|
||||
|
||||
private DefaultClientCodecConfigurer(DefaultClientCodecConfigurer other) {
|
||||
super(other);
|
||||
((ClientDefaultCodecsImpl) defaultCodecs()).setPartWritersSupplier(this::getPartWriters);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public ClientDefaultCodecs defaultCodecs() {
|
||||
return (ClientDefaultCodecs) super.defaultCodecs();
|
||||
}
|
||||
|
||||
@Override
|
||||
public DefaultClientCodecConfigurer clone() {
|
||||
return new DefaultClientCodecConfigurer(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected BaseDefaultCodecs cloneDefaultCodecs() {
|
||||
return new ClientDefaultCodecsImpl((ClientDefaultCodecsImpl) defaultCodecs());
|
||||
}
|
||||
|
||||
private List<HttpMessageWriter<?>> getPartWriters() {
|
||||
List<HttpMessageWriter<?>> result = new ArrayList<>();
|
||||
result.addAll(this.customCodecs.getTypedWriters().keySet());
|
||||
result.addAll(this.defaultCodecs.getBaseTypedWriters());
|
||||
result.addAll(this.customCodecs.getObjectWriters().keySet());
|
||||
result.addAll(this.defaultCodecs.getBaseObjectWriters());
|
||||
result.addAll(this.defaultCodecs.getCatchAllWriters());
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+16
-1
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
+24
-14
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -25,7 +25,6 @@ import org.springframework.http.codec.ServerSentEventHttpMessageWriter;
|
||||
import org.springframework.http.codec.multipart.MultipartHttpMessageReader;
|
||||
import org.springframework.http.codec.multipart.SynchronossPartHttpMessageReader;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.ClassUtils;
|
||||
|
||||
/**
|
||||
* Default implementation of {@link ServerCodecConfigurer.ServerDefaultCodecs}.
|
||||
@@ -34,15 +33,28 @@ import org.springframework.util.ClassUtils;
|
||||
*/
|
||||
class ServerDefaultCodecsImpl extends BaseDefaultCodecs implements ServerCodecConfigurer.ServerDefaultCodecs {
|
||||
|
||||
private static final boolean synchronossMultipartPresent =
|
||||
ClassUtils.isPresent("org.synchronoss.cloud.nio.multipart.NioMultipartParser",
|
||||
DefaultServerCodecConfigurer.class.getClassLoader());
|
||||
|
||||
@Nullable
|
||||
private HttpMessageReader<?> multipartReader;
|
||||
|
||||
@Nullable
|
||||
private Encoder<?> sseEncoder;
|
||||
|
||||
|
||||
ServerDefaultCodecsImpl() {
|
||||
}
|
||||
|
||||
ServerDefaultCodecsImpl(ServerDefaultCodecsImpl other) {
|
||||
super(other);
|
||||
this.multipartReader = other.multipartReader;
|
||||
this.sseEncoder = other.sseEncoder;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void multipartReader(HttpMessageReader<?> reader) {
|
||||
this.multipartReader = reader;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void serverSentEventEncoder(Encoder<?> encoder) {
|
||||
this.sseEncoder = encoder;
|
||||
@@ -51,16 +63,14 @@ class ServerDefaultCodecsImpl extends BaseDefaultCodecs implements ServerCodecCo
|
||||
|
||||
@Override
|
||||
protected void extendTypedReaders(List<HttpMessageReader<?>> typedReaders) {
|
||||
if (this.multipartReader != null) {
|
||||
addCodec(typedReaders, this.multipartReader);
|
||||
return;
|
||||
}
|
||||
if (synchronossMultipartPresent) {
|
||||
boolean enable = isEnableLoggingRequestDetails();
|
||||
|
||||
SynchronossPartHttpMessageReader partReader = new SynchronossPartHttpMessageReader();
|
||||
partReader.setEnableLoggingRequestDetails(enable);
|
||||
typedReaders.add(partReader);
|
||||
|
||||
MultipartHttpMessageReader reader = new MultipartHttpMessageReader(partReader);
|
||||
reader.setEnableLoggingRequestDetails(enable);
|
||||
typedReaders.add(reader);
|
||||
addCodec(typedReaders, partReader);
|
||||
addCodec(typedReaders, new MultipartHttpMessageReader(partReader));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@ import org.springframework.core.codec.CodecException;
|
||||
import org.springframework.core.codec.DecodingException;
|
||||
import org.springframework.core.codec.Hints;
|
||||
import org.springframework.core.io.buffer.DataBuffer;
|
||||
import org.springframework.core.io.buffer.DataBufferLimitException;
|
||||
import org.springframework.core.log.LogFormatUtils;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
@@ -78,6 +79,8 @@ public class Jaxb2XmlDecoder extends AbstractDecoder<Object> {
|
||||
|
||||
private Function<Unmarshaller, Unmarshaller> unmarshallerProcessor = Function.identity();
|
||||
|
||||
private int maxInMemorySize = -1;
|
||||
|
||||
|
||||
public Jaxb2XmlDecoder() {
|
||||
super(MimeTypeUtils.APPLICATION_XML, MimeTypeUtils.TEXT_XML);
|
||||
@@ -110,6 +113,29 @@ public class Jaxb2XmlDecoder extends AbstractDecoder<Object> {
|
||||
return this.unmarshallerProcessor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the max number of bytes that can be buffered by this decoder.
|
||||
* This is either the size of the entire input when decoding as a whole, or when
|
||||
* using async parsing with Aalto XML, it is the size of one top-level XML tree.
|
||||
* When the limit is exceeded, {@link DataBufferLimitException} is raised.
|
||||
* <p>By default in 5.1 this is set to -1, unlimited. In 5.2 the default
|
||||
* value for this limit is set to 256K.
|
||||
* @param byteCount the max number of bytes to buffer, or -1 for unlimited
|
||||
* @since 5.1.11
|
||||
*/
|
||||
public void setMaxInMemorySize(int byteCount) {
|
||||
this.maxInMemorySize = byteCount;
|
||||
this.xmlEventDecoder.setMaxInMemorySize(byteCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the {@link #setMaxInMemorySize configured} byte count limit.
|
||||
* @since 5.1.11
|
||||
*/
|
||||
public int getMaxInMemorySize() {
|
||||
return this.maxInMemorySize;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean canDecode(ResolvableType elementType, @Nullable MimeType mimeType) {
|
||||
@@ -166,7 +192,7 @@ public class Jaxb2XmlDecoder extends AbstractDecoder<Object> {
|
||||
}
|
||||
}
|
||||
|
||||
private Unmarshaller initUnmarshaller(Class<?> outputClass) throws JAXBException {
|
||||
private Unmarshaller initUnmarshaller(Class<?> outputClass) throws CodecException, JAXBException {
|
||||
Unmarshaller unmarshaller = this.jaxbContexts.createUnmarshaller(outputClass);
|
||||
return this.unmarshallerProcessor.apply(unmarshaller);
|
||||
}
|
||||
|
||||
@@ -135,7 +135,7 @@ public class Jaxb2XmlEncoder extends AbstractSingleValueEncoder<Object> {
|
||||
}).flux();
|
||||
}
|
||||
|
||||
private Marshaller initMarshaller(Class<?> clazz) throws JAXBException {
|
||||
private Marshaller initMarshaller(Class<?> clazz) throws CodecException, JAXBException {
|
||||
Marshaller marshaller = this.jaxbContexts.createMarshaller(clazz);
|
||||
marshaller.setProperty(Marshaller.JAXB_ENCODING, StandardCharsets.UTF_8.name());
|
||||
marshaller = this.marshallerProcessor.apply(marshaller);
|
||||
|
||||
+15
-12
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2017 the original author or authors.
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -24,12 +24,13 @@ import javax.xml.bind.JAXBException;
|
||||
import javax.xml.bind.Marshaller;
|
||||
import javax.xml.bind.Unmarshaller;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.core.codec.CodecException;
|
||||
|
||||
/**
|
||||
* Holder for {@link JAXBContext} instances.
|
||||
*
|
||||
* @author Arjen Poutsma
|
||||
* @author Juergen Hoeller
|
||||
* @since 5.0
|
||||
*/
|
||||
final class JaxbContextContainer {
|
||||
@@ -37,24 +38,26 @@ final class JaxbContextContainer {
|
||||
private final ConcurrentMap<Class<?>, JAXBContext> jaxbContexts = new ConcurrentHashMap<>(64);
|
||||
|
||||
|
||||
public Marshaller createMarshaller(Class<?> clazz) throws JAXBException {
|
||||
public Marshaller createMarshaller(Class<?> clazz) throws CodecException, JAXBException {
|
||||
JAXBContext jaxbContext = getJaxbContext(clazz);
|
||||
return jaxbContext.createMarshaller();
|
||||
}
|
||||
|
||||
public Unmarshaller createUnmarshaller(Class<?> clazz) throws JAXBException {
|
||||
public Unmarshaller createUnmarshaller(Class<?> clazz) throws CodecException, JAXBException {
|
||||
JAXBContext jaxbContext = getJaxbContext(clazz);
|
||||
return jaxbContext.createUnmarshaller();
|
||||
}
|
||||
|
||||
private JAXBContext getJaxbContext(Class<?> clazz) throws JAXBException {
|
||||
Assert.notNull(clazz, "Class must not be null");
|
||||
JAXBContext jaxbContext = this.jaxbContexts.get(clazz);
|
||||
if (jaxbContext == null) {
|
||||
jaxbContext = JAXBContext.newInstance(clazz);
|
||||
this.jaxbContexts.putIfAbsent(clazz, jaxbContext);
|
||||
}
|
||||
return jaxbContext;
|
||||
private JAXBContext getJaxbContext(Class<?> clazz) throws CodecException {
|
||||
return this.jaxbContexts.computeIfAbsent(clazz, key -> {
|
||||
try {
|
||||
return JAXBContext.newInstance(clazz);
|
||||
}
|
||||
catch (JAXBException ex) {
|
||||
throw new CodecException(
|
||||
"Could not create JAXBContext for class [" + clazz + "]: " + ex.getMessage(), ex);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ import reactor.core.publisher.Flux;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.core.codec.AbstractDecoder;
|
||||
import org.springframework.core.io.buffer.DataBuffer;
|
||||
import org.springframework.core.io.buffer.DataBufferLimitException;
|
||||
import org.springframework.core.io.buffer.DataBufferUtils;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.ClassUtils;
|
||||
@@ -88,26 +89,51 @@ public class XmlEventDecoder extends AbstractDecoder<XMLEvent> {
|
||||
|
||||
boolean useAalto = aaltoPresent;
|
||||
|
||||
private int maxInMemorySize = -1;
|
||||
|
||||
|
||||
public XmlEventDecoder() {
|
||||
super(MimeTypeUtils.APPLICATION_XML, MimeTypeUtils.TEXT_XML);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set the max number of bytes that can be buffered by this decoder. This
|
||||
* is either the size the entire input when decoding as a whole, or when
|
||||
* using async parsing via Aalto XML, it is size one top-level XML tree.
|
||||
* When the limit is exceeded, {@link DataBufferLimitException} is raised.
|
||||
* <p>By default in 5.1 this is set to -1, unlimited. In 5.2 the default
|
||||
* value for this limit is set to 256K.
|
||||
* @param byteCount the max number of bytes to buffer, or -1 for unlimited
|
||||
* @since 5.1.11
|
||||
*/
|
||||
public void setMaxInMemorySize(int byteCount) {
|
||||
this.maxInMemorySize = byteCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the {@link #setMaxInMemorySize configured} byte count limit.
|
||||
* @since 5.1.11
|
||||
*/
|
||||
public int getMaxInMemorySize() {
|
||||
return this.maxInMemorySize;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
@SuppressWarnings({"rawtypes", "unchecked"}) // on JDK 9 where XMLEventReader is Iterator<Object>
|
||||
public Flux<XMLEvent> decode(Publisher<DataBuffer> input, ResolvableType elementType,
|
||||
@Nullable MimeType mimeType, @Nullable Map<String, Object> hints) {
|
||||
|
||||
if (this.useAalto) {
|
||||
AaltoDataBufferToXmlEvent mapper = new AaltoDataBufferToXmlEvent();
|
||||
AaltoDataBufferToXmlEvent mapper = new AaltoDataBufferToXmlEvent(this.maxInMemorySize);
|
||||
return Flux.from(input)
|
||||
.flatMapIterable(mapper)
|
||||
.doFinally(signalType -> mapper.endOfInput());
|
||||
}
|
||||
else {
|
||||
return DataBufferUtils.join(input).
|
||||
flatMapIterable(buffer -> {
|
||||
return DataBufferUtils.join(input, getMaxInMemorySize())
|
||||
.flatMapIterable(buffer -> {
|
||||
try {
|
||||
InputStream is = buffer.asInputStream();
|
||||
Iterator eventReader = inputFactory.createXMLEventReader(is);
|
||||
@@ -139,10 +165,22 @@ public class XmlEventDecoder extends AbstractDecoder<XMLEvent> {
|
||||
|
||||
private final XMLEventAllocator eventAllocator = EventAllocatorImpl.getDefaultInstance();
|
||||
|
||||
private final int maxInMemorySize;
|
||||
|
||||
private int byteCount;
|
||||
|
||||
private int elementDepth;
|
||||
|
||||
|
||||
public AaltoDataBufferToXmlEvent(int maxInMemorySize) {
|
||||
this.maxInMemorySize = maxInMemorySize;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public List<? extends XMLEvent> apply(DataBuffer dataBuffer) {
|
||||
try {
|
||||
increaseByteCount(dataBuffer);
|
||||
this.streamReader.getInputFeeder().feedInput(dataBuffer.asByteBuffer());
|
||||
List<XMLEvent> events = new ArrayList<>();
|
||||
while (true) {
|
||||
@@ -156,8 +194,12 @@ public class XmlEventDecoder extends AbstractDecoder<XMLEvent> {
|
||||
if (event.isEndDocument()) {
|
||||
break;
|
||||
}
|
||||
checkDepthAndResetByteCount(event);
|
||||
}
|
||||
}
|
||||
if (this.maxInMemorySize > 0 && this.byteCount > this.maxInMemorySize) {
|
||||
raiseLimitException();
|
||||
}
|
||||
return events;
|
||||
}
|
||||
catch (XMLStreamException ex) {
|
||||
@@ -168,6 +210,35 @@ public class XmlEventDecoder extends AbstractDecoder<XMLEvent> {
|
||||
}
|
||||
}
|
||||
|
||||
private void increaseByteCount(DataBuffer dataBuffer) {
|
||||
if (this.maxInMemorySize > 0) {
|
||||
if (dataBuffer.readableByteCount() > Integer.MAX_VALUE - this.byteCount) {
|
||||
raiseLimitException();
|
||||
}
|
||||
else {
|
||||
this.byteCount += dataBuffer.readableByteCount();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void checkDepthAndResetByteCount(XMLEvent event) {
|
||||
if (this.maxInMemorySize > 0) {
|
||||
if (event.isStartElement()) {
|
||||
this.byteCount = this.elementDepth == 1 ? 0 : this.byteCount;
|
||||
this.elementDepth++;
|
||||
}
|
||||
else if (event.isEndElement()) {
|
||||
this.elementDepth--;
|
||||
this.byteCount = this.elementDepth == 1 ? 0 : this.byteCount;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void raiseLimitException() {
|
||||
throw new DataBufferLimitException(
|
||||
"Exceeded limit on max bytes per XML top-level node: " + this.maxInMemorySize);
|
||||
}
|
||||
|
||||
public void endOfInput() {
|
||||
this.streamReader.getInputFeeder().endOfInput();
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user