Extracted AbstractJsonHttpMessageConverter from GsonHttpMessageConverter
Generic type resolution algorithm in GenericTypeResolver shared between Jackson and Gson. Issue: SPR-15381
This commit is contained in:
parent
ea98ee820a
commit
e5fdd4cd1d
|
@ -21,12 +21,8 @@ import java.lang.reflect.ParameterizedType;
|
||||||
import java.lang.reflect.Type;
|
import java.lang.reflect.Type;
|
||||||
import java.lang.reflect.TypeVariable;
|
import java.lang.reflect.TypeVariable;
|
||||||
import java.lang.reflect.WildcardType;
|
import java.lang.reflect.WildcardType;
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
import org.springframework.util.Assert;
|
import org.springframework.util.Assert;
|
||||||
import org.springframework.util.ConcurrentReferenceHashMap;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper class for resolving generic types against type variables.
|
* Helper class for resolving generic types against type variables.
|
||||||
|
@ -42,24 +38,6 @@ import org.springframework.util.ConcurrentReferenceHashMap;
|
||||||
*/
|
*/
|
||||||
public abstract class GenericTypeResolver {
|
public abstract class GenericTypeResolver {
|
||||||
|
|
||||||
/** Cache from Class to TypeVariable Map */
|
|
||||||
@SuppressWarnings("rawtypes")
|
|
||||||
private static final Map<Class<?>, Map<TypeVariable, Type>> typeVariableCache =
|
|
||||||
new ConcurrentReferenceHashMap<>();
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Determine the target type for the given parameter specification.
|
|
||||||
* @param methodParameter the method parameter specification
|
|
||||||
* @return the corresponding generic parameter type
|
|
||||||
* @deprecated as of Spring 4.0, use {@link MethodParameter#getGenericParameterType()}
|
|
||||||
*/
|
|
||||||
@Deprecated
|
|
||||||
public static Type getTargetType(MethodParameter methodParameter) {
|
|
||||||
Assert.notNull(methodParameter, "MethodParameter must not be null");
|
|
||||||
return methodParameter.getGenericParameterType();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determine the target type for the given generic parameter type.
|
* Determine the target type for the given generic parameter type.
|
||||||
* @param methodParameter the method parameter specification
|
* @param methodParameter the method parameter specification
|
||||||
|
@ -80,7 +58,6 @@ public abstract class GenericTypeResolver {
|
||||||
* @param method the method to introspect
|
* @param method the method to introspect
|
||||||
* @param clazz the class to resolve type variables against
|
* @param clazz the class to resolve type variables against
|
||||||
* @return the corresponding generic parameter or return type
|
* @return the corresponding generic parameter or return type
|
||||||
* @see #resolveReturnTypeForGenericMethod
|
|
||||||
*/
|
*/
|
||||||
public static Class<?> resolveReturnType(Method method, Class<?> clazz) {
|
public static Class<?> resolveReturnType(Method method, Class<?> clazz) {
|
||||||
Assert.notNull(method, "Method must not be null");
|
Assert.notNull(method, "Method must not be null");
|
||||||
|
@ -88,106 +65,6 @@ public abstract class GenericTypeResolver {
|
||||||
return ResolvableType.forMethodReturnType(method, clazz).resolve(method.getReturnType());
|
return ResolvableType.forMethodReturnType(method, clazz).resolve(method.getReturnType());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Determine the target type for the generic return type of the given
|
|
||||||
* <em>generic method</em>, where formal type variables are declared on
|
|
||||||
* the given method itself.
|
|
||||||
* <p>For example, given a factory method with the following signature,
|
|
||||||
* if {@code resolveReturnTypeForGenericMethod()} is invoked with the reflected
|
|
||||||
* method for {@code creatProxy()} and an {@code Object[]} array containing
|
|
||||||
* {@code MyService.class}, {@code resolveReturnTypeForGenericMethod()} will
|
|
||||||
* infer that the target return type is {@code MyService}.
|
|
||||||
* <pre class="code">{@code public static <T> T createProxy(Class<T> clazz)}</pre>
|
|
||||||
* <h4>Possible Return Values</h4>
|
|
||||||
* <ul>
|
|
||||||
* <li>the target return type, if it can be inferred</li>
|
|
||||||
* <li>the {@linkplain Method#getReturnType() standard return type}, if
|
|
||||||
* the given {@code method} does not declare any {@linkplain
|
|
||||||
* Method#getTypeParameters() formal type variables}</li>
|
|
||||||
* <li>the {@linkplain Method#getReturnType() standard return type}, if the
|
|
||||||
* target return type cannot be inferred (e.g., due to type erasure)</li>
|
|
||||||
* <li>{@code null}, if the length of the given arguments array is shorter
|
|
||||||
* than the length of the {@linkplain
|
|
||||||
* Method#getGenericParameterTypes() formal argument list} for the given
|
|
||||||
* method</li>
|
|
||||||
* </ul>
|
|
||||||
* @param method the method to introspect, never {@code null}
|
|
||||||
* @param args the arguments that will be supplied to the method when it is
|
|
||||||
* invoked (never {@code null})
|
|
||||||
* @param classLoader the ClassLoader to resolve class names against, if necessary
|
|
||||||
* (may be {@code null})
|
|
||||||
* @return the resolved target return type, the standard return type, or {@code null}
|
|
||||||
* @since 3.2.5
|
|
||||||
* @see #resolveReturnType
|
|
||||||
*/
|
|
||||||
public static Class<?> resolveReturnTypeForGenericMethod(Method method, Object[] args, ClassLoader classLoader) {
|
|
||||||
Assert.notNull(method, "Method must not be null");
|
|
||||||
Assert.notNull(args, "Argument array must not be null");
|
|
||||||
|
|
||||||
TypeVariable<Method>[] declaredTypeVariables = method.getTypeParameters();
|
|
||||||
Type genericReturnType = method.getGenericReturnType();
|
|
||||||
Type[] methodArgumentTypes = method.getGenericParameterTypes();
|
|
||||||
|
|
||||||
// No declared type variables to inspect, so just return the standard return type.
|
|
||||||
if (declaredTypeVariables.length == 0) {
|
|
||||||
return method.getReturnType();
|
|
||||||
}
|
|
||||||
|
|
||||||
// The supplied argument list is too short for the method's signature, so
|
|
||||||
// return null, since such a method invocation would fail.
|
|
||||||
if (args.length < methodArgumentTypes.length) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure that the type variable (e.g., T) is declared directly on the method
|
|
||||||
// itself (e.g., via <T>), not on the enclosing class or interface.
|
|
||||||
boolean locallyDeclaredTypeVariableMatchesReturnType = false;
|
|
||||||
for (TypeVariable<Method> currentTypeVariable : declaredTypeVariables) {
|
|
||||||
if (currentTypeVariable.equals(genericReturnType)) {
|
|
||||||
locallyDeclaredTypeVariableMatchesReturnType = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (locallyDeclaredTypeVariableMatchesReturnType) {
|
|
||||||
for (int i = 0; i < methodArgumentTypes.length; i++) {
|
|
||||||
Type currentMethodArgumentType = methodArgumentTypes[i];
|
|
||||||
if (currentMethodArgumentType.equals(genericReturnType)) {
|
|
||||||
return args[i].getClass();
|
|
||||||
}
|
|
||||||
if (currentMethodArgumentType instanceof ParameterizedType) {
|
|
||||||
ParameterizedType parameterizedType = (ParameterizedType) currentMethodArgumentType;
|
|
||||||
Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
|
|
||||||
for (Type typeArg : actualTypeArguments) {
|
|
||||||
if (typeArg.equals(genericReturnType)) {
|
|
||||||
Object arg = args[i];
|
|
||||||
if (arg instanceof Class) {
|
|
||||||
return (Class<?>) arg;
|
|
||||||
}
|
|
||||||
else if (arg instanceof String && classLoader != null) {
|
|
||||||
try {
|
|
||||||
return classLoader.loadClass((String) arg);
|
|
||||||
}
|
|
||||||
catch (ClassNotFoundException ex) {
|
|
||||||
throw new IllegalStateException(
|
|
||||||
"Could not resolve specific class name argument [" + arg + "]", ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// Consider adding logic to determine the class of the typeArg, if possible.
|
|
||||||
// For now, just fall back...
|
|
||||||
return method.getReturnType();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fall back...
|
|
||||||
return method.getReturnType();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolve the single type argument of the given generic interface against the given
|
* Resolve the single type argument of the given generic interface against the given
|
||||||
* target method which is assumed to return the given interface or an implementation
|
* target method which is assumed to return the given interface or an implementation
|
||||||
|
@ -248,81 +125,75 @@ public abstract class GenericTypeResolver {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolve the specified generic type against the given TypeVariable map.
|
* Resolve the given generic type against the given context class,
|
||||||
* @param genericType the generic type to resolve
|
* substituting type variables as far as possible.
|
||||||
* @param map the TypeVariable Map to resolved against
|
* @param genericType the (potentially) generic type
|
||||||
* @return the type if it resolves to a Class, or {@code Object.class} otherwise
|
* @param contextClass a context class for the target type, for example a class
|
||||||
* @deprecated as of Spring 4.0 in favor of {@link ResolvableType}
|
* in which the target type appears in a method signature (can be {@code null})
|
||||||
|
* @return the resolved type (possibly the given generic type as-is)
|
||||||
|
* @since 5.0
|
||||||
*/
|
*/
|
||||||
@Deprecated
|
public static Type resolveType(Type genericType, Class<?> contextClass) {
|
||||||
@SuppressWarnings("rawtypes")
|
if (contextClass != null) {
|
||||||
public static Class<?> resolveType(Type genericType, Map<TypeVariable, Type> map) {
|
if (genericType instanceof TypeVariable) {
|
||||||
return ResolvableType.forType(genericType, new TypeVariableMapVariableResolver(map)).resolve(Object.class);
|
ResolvableType resolvedTypeVariable = resolveVariable(
|
||||||
|
(TypeVariable<?>) genericType, ResolvableType.forClass(contextClass));
|
||||||
|
if (resolvedTypeVariable != ResolvableType.NONE) {
|
||||||
|
return resolvedTypeVariable.resolve();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (genericType instanceof ParameterizedType) {
|
||||||
|
ResolvableType resolvedType = ResolvableType.forType(genericType);
|
||||||
|
if (resolvedType.hasUnresolvableGenerics()) {
|
||||||
|
ParameterizedType parameterizedType = (ParameterizedType) genericType;
|
||||||
|
Class<?>[] generics = new Class<?>[parameterizedType.getActualTypeArguments().length];
|
||||||
|
Type[] typeArguments = parameterizedType.getActualTypeArguments();
|
||||||
|
for (int i = 0; i < typeArguments.length; i++) {
|
||||||
|
Type typeArgument = typeArguments[i];
|
||||||
|
if (typeArgument instanceof TypeVariable) {
|
||||||
|
ResolvableType resolvedTypeArgument = resolveVariable(
|
||||||
|
(TypeVariable<?>) typeArgument, ResolvableType.forClass(contextClass));
|
||||||
|
if (resolvedTypeArgument != ResolvableType.NONE) {
|
||||||
|
generics[i] = resolvedTypeArgument.resolve();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
generics[i] = ResolvableType.forType(typeArgument).resolve();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
generics[i] = ResolvableType.forType(typeArgument).resolve();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ResolvableType.forClassWithGenerics(resolvedType.getRawClass(), generics).getType();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return genericType;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private static ResolvableType resolveVariable(TypeVariable<?> typeVariable, ResolvableType contextType) {
|
||||||
* Build a mapping of {@link TypeVariable#getName TypeVariable names} to
|
ResolvableType resolvedType;
|
||||||
* {@link Class concrete classes} for the specified {@link Class}. Searches
|
if (contextType.hasGenerics()) {
|
||||||
* all super types, enclosing types and interfaces.
|
resolvedType = ResolvableType.forType(typeVariable, contextType);
|
||||||
* @deprecated as of Spring 4.0 in favor of {@link ResolvableType}
|
if (resolvedType.resolve() != null) {
|
||||||
*/
|
return resolvedType;
|
||||||
@Deprecated
|
|
||||||
@SuppressWarnings("rawtypes")
|
|
||||||
public static Map<TypeVariable, Type> getTypeVariableMap(Class<?> clazz) {
|
|
||||||
Map<TypeVariable, Type> typeVariableMap = typeVariableCache.get(clazz);
|
|
||||||
if (typeVariableMap == null) {
|
|
||||||
typeVariableMap = new HashMap<>();
|
|
||||||
buildTypeVariableMap(ResolvableType.forClass(clazz), typeVariableMap);
|
|
||||||
typeVariableCache.put(clazz, Collections.unmodifiableMap(typeVariableMap));
|
|
||||||
}
|
|
||||||
return typeVariableMap;
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("rawtypes")
|
|
||||||
private static void buildTypeVariableMap(ResolvableType type, Map<TypeVariable, Type> typeVariableMap) {
|
|
||||||
if (type != ResolvableType.NONE) {
|
|
||||||
if (type.getType() instanceof ParameterizedType) {
|
|
||||||
TypeVariable<?>[] variables = type.resolve().getTypeParameters();
|
|
||||||
for (int i = 0; i < variables.length; i++) {
|
|
||||||
ResolvableType generic = type.getGeneric(i);
|
|
||||||
while (generic.getType() instanceof TypeVariable<?>) {
|
|
||||||
generic = generic.resolveType();
|
|
||||||
}
|
|
||||||
if (generic != ResolvableType.NONE) {
|
|
||||||
typeVariableMap.put(variables[i], generic.getType());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
buildTypeVariableMap(type.getSuperType(), typeVariableMap);
|
|
||||||
for (ResolvableType interfaceType : type.getInterfaces()) {
|
|
||||||
buildTypeVariableMap(interfaceType, typeVariableMap);
|
|
||||||
}
|
|
||||||
if (type.resolve().isMemberClass()) {
|
|
||||||
buildTypeVariableMap(ResolvableType.forClass(type.resolve().getEnclosingClass()), typeVariableMap);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ResolvableType superType = contextType.getSuperType();
|
||||||
@SuppressWarnings({"serial", "rawtypes"})
|
if (superType != ResolvableType.NONE) {
|
||||||
private static class TypeVariableMapVariableResolver implements ResolvableType.VariableResolver {
|
resolvedType = resolveVariable(typeVariable, superType);
|
||||||
|
if (resolvedType.resolve() != null) {
|
||||||
private final Map<TypeVariable, Type> typeVariableMap;
|
return resolvedType;
|
||||||
|
|
||||||
public TypeVariableMapVariableResolver(Map<TypeVariable, Type> typeVariableMap) {
|
|
||||||
this.typeVariableMap = typeVariableMap;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public ResolvableType resolveVariable(TypeVariable<?> variable) {
|
|
||||||
Type type = this.typeVariableMap.get(variable);
|
|
||||||
return (type != null ? ResolvableType.forType(type) : null);
|
|
||||||
}
|
}
|
||||||
|
for (ResolvableType ifc : contextType.getInterfaces()) {
|
||||||
@Override
|
resolvedType = resolveVariable(typeVariable, ifc);
|
||||||
public Object getSource() {
|
if (resolvedType.resolve() != null) {
|
||||||
return this.typeVariableMap;
|
return resolvedType;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return ResolvableType.NONE;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright 2002-2015 the original author or authors.
|
* Copyright 2002-2017 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -18,8 +18,6 @@ package org.springframework.core;
|
||||||
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
import java.lang.reflect.ParameterizedType;
|
|
||||||
import java.lang.reflect.Type;
|
|
||||||
import java.lang.reflect.TypeVariable;
|
import java.lang.reflect.TypeVariable;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
@ -109,26 +107,6 @@ public class BridgeMethodResolverTests {
|
||||||
assertFalse("Should not be bridge method", BridgeMethodResolver.isBridgeMethodFor(bridge, other, MyBar.class));
|
assertFalse("Should not be bridge method", BridgeMethodResolver.isBridgeMethodFor(bridge, other, MyBar.class));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
@Deprecated
|
|
||||||
public void testCreateTypeVariableMap() throws Exception {
|
|
||||||
Map<TypeVariable, Type> typeVariableMap = GenericTypeResolver.getTypeVariableMap(MyBar.class);
|
|
||||||
TypeVariable<?> barT = findTypeVariable(InterBar.class, "T");
|
|
||||||
assertEquals(String.class, typeVariableMap.get(barT));
|
|
||||||
|
|
||||||
typeVariableMap = GenericTypeResolver.getTypeVariableMap(MyFoo.class);
|
|
||||||
TypeVariable<?> fooT = findTypeVariable(Foo.class, "T");
|
|
||||||
assertEquals(String.class, typeVariableMap.get(fooT));
|
|
||||||
|
|
||||||
typeVariableMap = GenericTypeResolver.getTypeVariableMap(ExtendsEnclosing.ExtendsEnclosed.ExtendsReallyDeepNow.class);
|
|
||||||
TypeVariable<?> r = findTypeVariable(Enclosing.Enclosed.ReallyDeepNow.class, "R");
|
|
||||||
TypeVariable<?> s = findTypeVariable(Enclosing.Enclosed.class, "S");
|
|
||||||
TypeVariable<?> t = findTypeVariable(Enclosing.class, "T");
|
|
||||||
assertEquals(Long.class, typeVariableMap.get(r));
|
|
||||||
assertEquals(Integer.class, typeVariableMap.get(s));
|
|
||||||
assertEquals(String.class, typeVariableMap.get(t));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testDoubleParameterization() throws Exception {
|
public void testDoubleParameterization() throws Exception {
|
||||||
Method objectBridge = MyBoo.class.getDeclaredMethod("foo", Object.class);
|
Method objectBridge = MyBoo.class.getDeclaredMethod("foo", Object.class);
|
||||||
|
@ -228,14 +206,6 @@ public class BridgeMethodResolverTests {
|
||||||
assertEquals(bridgedMethod, BridgeMethodResolver.findBridgedMethod(bridgeMethod));
|
assertEquals(bridgedMethod, BridgeMethodResolver.findBridgedMethod(bridgeMethod));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
@Deprecated
|
|
||||||
public void testSPR2454() throws Exception {
|
|
||||||
Map<TypeVariable, Type> typeVariableMap = GenericTypeResolver.getTypeVariableMap(YourHomer.class);
|
|
||||||
TypeVariable<?> variable = findTypeVariable(MyHomer.class, "L");
|
|
||||||
assertEquals(AbstractBounded.class, ((ParameterizedType) typeVariableMap.get(variable)).getRawType());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testSPR2603() throws Exception {
|
public void testSPR2603() throws Exception {
|
||||||
Method objectBridge = YourHomer.class.getDeclaredMethod("foo", Bounded.class);
|
Method objectBridge = YourHomer.class.getDeclaredMethod("foo", Bounded.class);
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright 2002-2016 the original author or authors.
|
* Copyright 2002-2017 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -17,11 +17,7 @@
|
||||||
package org.springframework.core;
|
package org.springframework.core;
|
||||||
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.lang.reflect.Method;
|
|
||||||
import java.lang.reflect.Type;
|
|
||||||
import java.lang.reflect.TypeVariable;
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
|
@ -76,71 +72,11 @@ public class GenericTypeResolverTests {
|
||||||
resolveReturnTypeArgument(findMethod(MyTypeWithMethods.class, "object"), MyInterfaceType.class));
|
resolveReturnTypeArgument(findMethod(MyTypeWithMethods.class, "object"), MyInterfaceType.class));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
@Deprecated
|
|
||||||
public void testResolveType() {
|
|
||||||
Method intMessageMethod = findMethod(MyTypeWithMethods.class, "readIntegerInputMessage", MyInterfaceType.class);
|
|
||||||
MethodParameter intMessageMethodParam = new MethodParameter(intMessageMethod, 0);
|
|
||||||
assertEquals(MyInterfaceType.class,
|
|
||||||
resolveType(intMessageMethodParam.getGenericParameterType(), new HashMap<>()));
|
|
||||||
|
|
||||||
Method intArrMessageMethod = findMethod(MyTypeWithMethods.class, "readIntegerArrayInputMessage",
|
|
||||||
MyInterfaceType[].class);
|
|
||||||
MethodParameter intArrMessageMethodParam = new MethodParameter(intArrMessageMethod, 0);
|
|
||||||
assertEquals(MyInterfaceType[].class,
|
|
||||||
resolveType(intArrMessageMethodParam.getGenericParameterType(), new HashMap<>()));
|
|
||||||
|
|
||||||
Method genericArrMessageMethod = findMethod(MySimpleTypeWithMethods.class, "readGenericArrayInputMessage",
|
|
||||||
Object[].class);
|
|
||||||
MethodParameter genericArrMessageMethodParam = new MethodParameter(genericArrMessageMethod, 0);
|
|
||||||
Map<TypeVariable, Type> varMap = getTypeVariableMap(MySimpleTypeWithMethods.class);
|
|
||||||
assertEquals(Integer[].class, resolveType(genericArrMessageMethodParam.getGenericParameterType(), varMap));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testBoundParameterizedType() {
|
public void testBoundParameterizedType() {
|
||||||
assertEquals(B.class, resolveTypeArgument(TestImpl.class, TestIfc.class));
|
assertEquals(B.class, resolveTypeArgument(TestImpl.class, TestIfc.class));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
@Deprecated
|
|
||||||
public void testGetTypeVariableMap() throws Exception {
|
|
||||||
Map<TypeVariable, Type> map;
|
|
||||||
|
|
||||||
map = GenericTypeResolver.getTypeVariableMap(MySimpleInterfaceType.class);
|
|
||||||
assertThat(map.toString(), equalTo("{T=class java.lang.String}"));
|
|
||||||
|
|
||||||
map = GenericTypeResolver.getTypeVariableMap(MyCollectionInterfaceType.class);
|
|
||||||
assertThat(map.toString(), equalTo("{T=java.util.Collection<java.lang.String>}"));
|
|
||||||
|
|
||||||
map = GenericTypeResolver.getTypeVariableMap(MyCollectionSuperclassType.class);
|
|
||||||
assertThat(map.toString(), equalTo("{T=java.util.Collection<java.lang.String>}"));
|
|
||||||
|
|
||||||
map = GenericTypeResolver.getTypeVariableMap(MySimpleTypeWithMethods.class);
|
|
||||||
assertThat(map.toString(), equalTo("{T=class java.lang.Integer}"));
|
|
||||||
|
|
||||||
map = GenericTypeResolver.getTypeVariableMap(TopLevelClass.class);
|
|
||||||
assertThat(map.toString(), equalTo("{}"));
|
|
||||||
|
|
||||||
map = GenericTypeResolver.getTypeVariableMap(TypedTopLevelClass.class);
|
|
||||||
assertThat(map.toString(), equalTo("{T=class java.lang.Integer}"));
|
|
||||||
|
|
||||||
map = GenericTypeResolver.getTypeVariableMap(TypedTopLevelClass.TypedNested.class);
|
|
||||||
assertThat(map.size(), equalTo(2));
|
|
||||||
Type t = null;
|
|
||||||
Type x = null;
|
|
||||||
for (Map.Entry<TypeVariable, Type> entry : map.entrySet()) {
|
|
||||||
if (entry.getKey().toString().equals("T")) {
|
|
||||||
t = entry.getValue();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
x = entry.getValue();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
assertThat(t, equalTo((Type) Integer.class));
|
|
||||||
assertThat(x, equalTo((Type) Long.class));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void getGenericsCannotBeResolved() throws Exception {
|
public void getGenericsCannotBeResolved() throws Exception {
|
||||||
// SPR-11030
|
// SPR-11030
|
||||||
|
|
|
@ -17,9 +17,7 @@
|
||||||
package org.springframework.http.codec.json;
|
package org.springframework.http.codec.json;
|
||||||
|
|
||||||
import java.lang.annotation.Annotation;
|
import java.lang.annotation.Annotation;
|
||||||
import java.lang.reflect.ParameterizedType;
|
|
||||||
import java.lang.reflect.Type;
|
import java.lang.reflect.Type;
|
||||||
import java.lang.reflect.TypeVariable;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
@ -32,14 +30,14 @@ import com.fasterxml.jackson.databind.JavaType;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.fasterxml.jackson.databind.type.TypeFactory;
|
import com.fasterxml.jackson.databind.type.TypeFactory;
|
||||||
|
|
||||||
|
import org.springframework.core.GenericTypeResolver;
|
||||||
import org.springframework.core.MethodParameter;
|
import org.springframework.core.MethodParameter;
|
||||||
import org.springframework.core.ResolvableType;
|
import org.springframework.core.ResolvableType;
|
||||||
import org.springframework.util.Assert;
|
import org.springframework.util.Assert;
|
||||||
import org.springframework.util.MimeType;
|
import org.springframework.util.MimeType;
|
||||||
import org.springframework.util.MimeTypeUtils;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Base class providing support methods for Jackson 2 encoding and decoding.
|
* Base class providing support methods for Jackson 2.9 encoding and decoding.
|
||||||
*
|
*
|
||||||
* @author Sebastien Deleuze
|
* @author Sebastien Deleuze
|
||||||
* @author Rossen Stoyanchev
|
* @author Rossen Stoyanchev
|
||||||
|
@ -62,99 +60,26 @@ public abstract class Jackson2CodecSupport {
|
||||||
new MimeType("application", "*+json", StandardCharsets.UTF_8));
|
new MimeType("application", "*+json", StandardCharsets.UTF_8));
|
||||||
|
|
||||||
|
|
||||||
protected final ObjectMapper mapper;
|
protected final ObjectMapper objectMapper;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor with a Jackson {@link ObjectMapper} to use.
|
* Constructor with a Jackson {@link ObjectMapper} to use.
|
||||||
*/
|
*/
|
||||||
protected Jackson2CodecSupport(ObjectMapper mapper) {
|
protected Jackson2CodecSupport(ObjectMapper objectMapper) {
|
||||||
Assert.notNull(mapper, "ObjectMapper must not be null");
|
Assert.notNull(objectMapper, "ObjectMapper must not be null");
|
||||||
this.mapper = mapper;
|
this.objectMapper = objectMapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
protected boolean supportsMimeType(MimeType mimeType) {
|
protected boolean supportsMimeType(MimeType mimeType) {
|
||||||
return mimeType == null ||
|
return (mimeType == null ||
|
||||||
JSON_MIME_TYPES.stream().anyMatch(m -> m.isCompatibleWith(mimeType));
|
JSON_MIME_TYPES.stream().anyMatch(m -> m.isCompatibleWith(mimeType)));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Return the Jackson {@link JavaType} for the specified type and context class.
|
|
||||||
* <p>The default implementation returns {@code typeFactory.constructType(type, contextClass)},
|
|
||||||
* but this can be overridden in subclasses, to allow for custom generic collection handling.
|
|
||||||
* For instance:
|
|
||||||
* <pre class="code">
|
|
||||||
* protected JavaType getJavaType(Type type) {
|
|
||||||
* if (type instanceof Class && List.class.isAssignableFrom((Class)type)) {
|
|
||||||
* return TypeFactory.collectionType(ArrayList.class, MyBean.class);
|
|
||||||
* } else {
|
|
||||||
* return super.getJavaType(type);
|
|
||||||
* }
|
|
||||||
* }
|
|
||||||
* </pre>
|
|
||||||
* @param type the generic type to return the Jackson JavaType for
|
|
||||||
* @param contextClass a context class for the target type, for example a class
|
|
||||||
* in which the target type appears in a method signature (can be {@code null})
|
|
||||||
* @return the Jackson JavaType
|
|
||||||
*/
|
|
||||||
protected JavaType getJavaType(Type type, Class<?> contextClass) {
|
protected JavaType getJavaType(Type type, Class<?> contextClass) {
|
||||||
TypeFactory typeFactory = this.mapper.getTypeFactory();
|
TypeFactory typeFactory = this.objectMapper.getTypeFactory();
|
||||||
if (contextClass != null) {
|
return typeFactory.constructType(GenericTypeResolver.resolveType(type, contextClass));
|
||||||
ResolvableType resolvedType = ResolvableType.forType(type);
|
|
||||||
if (type instanceof TypeVariable) {
|
|
||||||
ResolvableType resolvedTypeVariable = resolveVariable(
|
|
||||||
(TypeVariable<?>) type, ResolvableType.forClass(contextClass));
|
|
||||||
if (resolvedTypeVariable != ResolvableType.NONE) {
|
|
||||||
return typeFactory.constructType(resolvedTypeVariable.resolve());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (type instanceof ParameterizedType && resolvedType.hasUnresolvableGenerics()) {
|
|
||||||
ParameterizedType parameterizedType = (ParameterizedType) type;
|
|
||||||
Class<?>[] generics = new Class<?>[parameterizedType.getActualTypeArguments().length];
|
|
||||||
Type[] typeArguments = parameterizedType.getActualTypeArguments();
|
|
||||||
for (int i = 0; i < typeArguments.length; i++) {
|
|
||||||
Type typeArgument = typeArguments[i];
|
|
||||||
if (typeArgument instanceof TypeVariable) {
|
|
||||||
ResolvableType resolvedTypeArgument = resolveVariable(
|
|
||||||
(TypeVariable<?>) typeArgument, ResolvableType.forClass(contextClass));
|
|
||||||
if (resolvedTypeArgument != ResolvableType.NONE) {
|
|
||||||
generics[i] = resolvedTypeArgument.resolve();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
generics[i] = ResolvableType.forType(typeArgument).resolve();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
generics[i] = ResolvableType.forType(typeArgument).resolve();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return typeFactory.constructType(ResolvableType.
|
|
||||||
forClassWithGenerics(resolvedType.getRawClass(), generics).getType());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return typeFactory.constructType(type);
|
|
||||||
}
|
|
||||||
|
|
||||||
private ResolvableType resolveVariable(TypeVariable<?> typeVariable, ResolvableType contextType) {
|
|
||||||
ResolvableType resolvedType;
|
|
||||||
if (contextType.hasGenerics()) {
|
|
||||||
resolvedType = ResolvableType.forType(typeVariable, contextType);
|
|
||||||
if (resolvedType.resolve() != null) {
|
|
||||||
return resolvedType;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
resolvedType = resolveVariable(typeVariable, contextType.getSuperType());
|
|
||||||
if (resolvedType.resolve() != null) {
|
|
||||||
return resolvedType;
|
|
||||||
}
|
|
||||||
for (ResolvableType ifc : contextType.getInterfaces()) {
|
|
||||||
resolvedType = resolveVariable(typeVariable, ifc);
|
|
||||||
if (resolvedType.resolve() != null) {
|
|
||||||
return resolvedType;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ResolvableType.NONE;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected Map<String, Object> getHints(ResolvableType actualType) {
|
protected Map<String, Object> getHints(ResolvableType actualType) {
|
||||||
|
|
|
@ -41,7 +41,7 @@ import org.springframework.util.Assert;
|
||||||
import org.springframework.util.MimeType;
|
import org.springframework.util.MimeType;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Decode a byte stream into JSON and convert to Object's with Jackson 2.6+.
|
* Decode a byte stream into JSON and convert to Object's with Jackson 2.9.
|
||||||
*
|
*
|
||||||
* @author Sebastien Deleuze
|
* @author Sebastien Deleuze
|
||||||
* @author Rossen Stoyanchev
|
* @author Rossen Stoyanchev
|
||||||
|
@ -66,10 +66,10 @@ public class Jackson2JsonDecoder extends Jackson2CodecSupport implements HttpMes
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean canDecode(ResolvableType elementType, MimeType mimeType) {
|
public boolean canDecode(ResolvableType elementType, MimeType mimeType) {
|
||||||
JavaType javaType = this.mapper.getTypeFactory().constructType(elementType.getType());
|
JavaType javaType = this.objectMapper.getTypeFactory().constructType(elementType.getType());
|
||||||
// Skip String (CharSequenceDecoder + "*/*" comes after)
|
// Skip String (CharSequenceDecoder + "*/*" comes after)
|
||||||
return !CharSequence.class.isAssignableFrom(elementType.resolve(Object.class)) &&
|
return (!CharSequence.class.isAssignableFrom(elementType.resolve(Object.class)) &&
|
||||||
this.mapper.canDeserialize(javaType) && supportsMimeType(mimeType);
|
this.objectMapper.canDeserialize(javaType) && supportsMimeType(mimeType));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -102,9 +102,9 @@ public class Jackson2JsonDecoder extends Jackson2CodecSupport implements HttpMes
|
||||||
JavaType javaType = getJavaType(elementType.getType(), contextClass);
|
JavaType javaType = getJavaType(elementType.getType(), contextClass);
|
||||||
Class<?> jsonView = (Class<?>) hints.get(Jackson2CodecSupport.JSON_VIEW_HINT);
|
Class<?> jsonView = (Class<?>) hints.get(Jackson2CodecSupport.JSON_VIEW_HINT);
|
||||||
|
|
||||||
ObjectReader reader = jsonView != null ?
|
ObjectReader reader = (jsonView != null ?
|
||||||
this.mapper.readerWithView(jsonView).forType(javaType) :
|
this.objectMapper.readerWithView(jsonView).forType(javaType) :
|
||||||
this.mapper.readerFor(javaType);
|
this.objectMapper.readerFor(javaType));
|
||||||
|
|
||||||
return objectDecoder.decode(inputStream, elementType, mimeType, hints)
|
return objectDecoder.decode(inputStream, elementType, mimeType, hints)
|
||||||
.map(dataBuffer -> {
|
.map(dataBuffer -> {
|
||||||
|
|
|
@ -52,7 +52,7 @@ import org.springframework.util.MimeType;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Encode from an {@code Object} stream to a byte stream of JSON objects,
|
* Encode from an {@code Object} stream to a byte stream of JSON objects,
|
||||||
* using Jackson 2.6+.
|
* using Jackson 2.9.
|
||||||
*
|
*
|
||||||
* @author Sebastien Deleuze
|
* @author Sebastien Deleuze
|
||||||
* @author Arjen Poutsma
|
* @author Arjen Poutsma
|
||||||
|
@ -105,7 +105,7 @@ public class Jackson2JsonEncoder extends Jackson2CodecSupport implements HttpMes
|
||||||
@Override
|
@Override
|
||||||
public boolean canEncode(ResolvableType elementType, MimeType mimeType) {
|
public boolean canEncode(ResolvableType elementType, MimeType mimeType) {
|
||||||
Class<?> clazz = elementType.getRawClass();
|
Class<?> clazz = elementType.getRawClass();
|
||||||
return this.mapper.canSerialize(clazz) && supportsMimeType(mimeType);
|
return (this.objectMapper.canSerialize(clazz) && supportsMimeType(mimeType));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -137,14 +137,15 @@ public class Jackson2JsonEncoder extends Jackson2CodecSupport implements HttpMes
|
||||||
private DataBuffer encodeValue(Object value, MimeType mimeType, DataBufferFactory bufferFactory,
|
private DataBuffer encodeValue(Object value, MimeType mimeType, DataBufferFactory bufferFactory,
|
||||||
ResolvableType elementType, Map<String, Object> hints) {
|
ResolvableType elementType, Map<String, Object> hints) {
|
||||||
|
|
||||||
TypeFactory typeFactory = this.mapper.getTypeFactory();
|
TypeFactory typeFactory = this.objectMapper.getTypeFactory();
|
||||||
JavaType javaType = typeFactory.constructType(elementType.getType());
|
JavaType javaType = typeFactory.constructType(elementType.getType());
|
||||||
if (elementType.isInstance(value)) {
|
if (elementType.isInstance(value)) {
|
||||||
javaType = getJavaType(elementType.getType(), null);
|
javaType = getJavaType(elementType.getType(), null);
|
||||||
}
|
}
|
||||||
|
|
||||||
Class<?> jsonView = (Class<?>) hints.get(Jackson2CodecSupport.JSON_VIEW_HINT);
|
Class<?> jsonView = (Class<?>) hints.get(Jackson2CodecSupport.JSON_VIEW_HINT);
|
||||||
ObjectWriter writer = jsonView != null ? this.mapper.writerWithView(jsonView): this.mapper.writer();
|
ObjectWriter writer = (jsonView != null ?
|
||||||
|
this.objectMapper.writerWithView(jsonView) : this.objectMapper.writer());
|
||||||
|
|
||||||
if (javaType != null && javaType.isContainerType()) {
|
if (javaType != null && javaType.isContainerType()) {
|
||||||
writer = writer.forType(javaType);
|
writer = writer.forType(javaType);
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright 2002-2015 the original author or authors.
|
* Copyright 2002-2017 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -58,14 +58,19 @@ public abstract class AbstractGenericHttpMessageConverter<T> extends AbstractHtt
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean supports(Class<?> clazz) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean canRead(Type type, Class<?> contextClass, MediaType mediaType) {
|
public boolean canRead(Type type, Class<?> contextClass, MediaType mediaType) {
|
||||||
return canRead(contextClass, mediaType);
|
return canRead(mediaType);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean canWrite(Type type, Class<?> clazz, MediaType mediaType) {
|
public boolean canWrite(Type type, Class<?> clazz, MediaType mediaType) {
|
||||||
return canWrite(clazz, mediaType);
|
return canWrite(mediaType);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -102,7 +107,6 @@ public abstract class AbstractGenericHttpMessageConverter<T> extends AbstractHtt
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void writeInternal(T t, HttpOutputMessage outputMessage)
|
protected void writeInternal(T t, HttpOutputMessage outputMessage)
|
||||||
throws IOException, HttpMessageNotWritableException {
|
throws IOException, HttpMessageNotWritableException {
|
||||||
|
@ -113,7 +117,7 @@ public abstract class AbstractGenericHttpMessageConverter<T> extends AbstractHtt
|
||||||
/**
|
/**
|
||||||
* Abstract template method that writes the actual body. Invoked from {@link #write}.
|
* Abstract template method that writes the actual body. Invoked from {@link #write}.
|
||||||
* @param t the object to write to the output message
|
* @param t the object to write to the output message
|
||||||
* @param type the type of object to write, can be {@code null} if not specified.
|
* @param type the type of object to write (may be {@code null})
|
||||||
* @param outputMessage the HTTP output message to write to
|
* @param outputMessage the HTTP output message to write to
|
||||||
* @throws IOException in case of I/O errors
|
* @throws IOException in case of I/O errors
|
||||||
* @throws HttpMessageNotWritableException in case of conversion errors
|
* @throws HttpMessageNotWritableException in case of conversion errors
|
||||||
|
|
|
@ -49,31 +49,6 @@ public class ResourceRegionHttpMessageConverter extends AbstractGenericHttpMessa
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected boolean supports(Class<?> clazz) {
|
|
||||||
// should not be called as we override canRead/canWrite
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean canRead(Type type, Class<?> contextClass, MediaType mediaType) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Object read(Type type, Class<?> contextClass, HttpInputMessage inputMessage)
|
|
||||||
throws IOException, HttpMessageNotReadableException {
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected ResourceRegion readInternal(Class<?> clazz, HttpInputMessage inputMessage)
|
|
||||||
throws IOException, HttpMessageNotReadableException {
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
protected MediaType getDefaultContentType(Object object) {
|
protected MediaType getDefaultContentType(Object object) {
|
||||||
|
@ -90,6 +65,30 @@ public class ResourceRegionHttpMessageConverter extends AbstractGenericHttpMessa
|
||||||
return MediaTypeFactory.getMediaType(resource).orElse(MediaType.APPLICATION_OCTET_STREAM);
|
return MediaTypeFactory.getMediaType(resource).orElse(MediaType.APPLICATION_OCTET_STREAM);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean canRead(Class<?> clazz, MediaType mediaType) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean canRead(Type type, Class<?> contextClass, MediaType mediaType) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object read(Type type, Class<?> contextClass, HttpInputMessage inputMessage)
|
||||||
|
throws IOException, HttpMessageNotReadableException {
|
||||||
|
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected ResourceRegion readInternal(Class<?> clazz, HttpInputMessage inputMessage)
|
||||||
|
throws IOException, HttpMessageNotReadableException {
|
||||||
|
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean canWrite(Class<?> clazz, MediaType mediaType) {
|
public boolean canWrite(Class<?> clazz, MediaType mediaType) {
|
||||||
return canWrite(clazz, null, mediaType);
|
return canWrite(clazz, null, mediaType);
|
||||||
|
@ -138,6 +137,7 @@ public class ResourceRegionHttpMessageConverter extends AbstractGenericHttpMessa
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
protected void writeResourceRegion(ResourceRegion region, HttpOutputMessage outputMessage) throws IOException {
|
protected void writeResourceRegion(ResourceRegion region, HttpOutputMessage outputMessage) throws IOException {
|
||||||
Assert.notNull(region, "ResourceRegion must not be null");
|
Assert.notNull(region, "ResourceRegion must not be null");
|
||||||
HttpHeaders responseHeaders = outputMessage.getHeaders();
|
HttpHeaders responseHeaders = outputMessage.getHeaders();
|
||||||
|
@ -195,8 +195,6 @@ public class ResourceRegionHttpMessageConverter extends AbstractGenericHttpMessa
|
||||||
print(out, "--" + boundaryString + "--");
|
print(out, "--" + boundaryString + "--");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private static void println(OutputStream os) throws IOException {
|
private static void println(OutputStream os) throws IOException {
|
||||||
os.write('\r');
|
os.write('\r');
|
||||||
os.write('\n');
|
os.write('\n');
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright 2002-2016 the original author or authors.
|
* Copyright 2002-2017 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -17,9 +17,7 @@
|
||||||
package org.springframework.http.converter.json;
|
package org.springframework.http.converter.json;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.lang.reflect.ParameterizedType;
|
|
||||||
import java.lang.reflect.Type;
|
import java.lang.reflect.Type;
|
||||||
import java.lang.reflect.TypeVariable;
|
|
||||||
import java.nio.charset.Charset;
|
import java.nio.charset.Charset;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
@ -40,7 +38,7 @@ import com.fasterxml.jackson.databind.exc.InvalidDefinitionException;
|
||||||
import com.fasterxml.jackson.databind.ser.FilterProvider;
|
import com.fasterxml.jackson.databind.ser.FilterProvider;
|
||||||
import com.fasterxml.jackson.databind.type.TypeFactory;
|
import com.fasterxml.jackson.databind.type.TypeFactory;
|
||||||
|
|
||||||
import org.springframework.core.ResolvableType;
|
import org.springframework.core.GenericTypeResolver;
|
||||||
import org.springframework.http.HttpInputMessage;
|
import org.springframework.http.HttpInputMessage;
|
||||||
import org.springframework.http.HttpOutputMessage;
|
import org.springframework.http.HttpOutputMessage;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
|
@ -56,7 +54,7 @@ import org.springframework.util.TypeUtils;
|
||||||
* Abstract base class for Jackson based and content type independent
|
* Abstract base class for Jackson based and content type independent
|
||||||
* {@link HttpMessageConverter} implementations.
|
* {@link HttpMessageConverter} implementations.
|
||||||
*
|
*
|
||||||
* <p>Compatible with Jackson 2.6 and higher, as of Spring 4.3.
|
* <p>Compatible with Jackson 2.9 and higher, as of Spring 5.0.
|
||||||
*
|
*
|
||||||
* @author Arjen Poutsma
|
* @author Arjen Poutsma
|
||||||
* @author Keith Donald
|
* @author Keith Donald
|
||||||
|
@ -204,12 +202,6 @@ public abstract class AbstractJackson2HttpMessageConverter extends AbstractGener
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
protected boolean supports(Class<?> clazz) {
|
|
||||||
// should not be called, since we override canRead/Write instead
|
|
||||||
throw new UnsupportedOperationException();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Object readInternal(Class<?> clazz, HttpInputMessage inputMessage)
|
protected Object readInternal(Class<?> clazz, HttpInputMessage inputMessage)
|
||||||
throws IOException, HttpMessageNotReadableException {
|
throws IOException, HttpMessageNotReadableException {
|
||||||
|
@ -238,10 +230,11 @@ public abstract class AbstractJackson2HttpMessageConverter extends AbstractGener
|
||||||
return this.objectMapper.readValue(inputMessage.getBody(), javaType);
|
return this.objectMapper.readValue(inputMessage.getBody(), javaType);
|
||||||
}
|
}
|
||||||
catch (InvalidDefinitionException ex) {
|
catch (InvalidDefinitionException ex) {
|
||||||
throw new HttpMessageNotReadableException("Could not read document: " + ex.getMessage(), ex, HttpStatus.INTERNAL_SERVER_ERROR);
|
throw new HttpMessageNotReadableException(
|
||||||
|
"Could not map JSON to target object of " + javaType, ex, HttpStatus.INTERNAL_SERVER_ERROR);
|
||||||
}
|
}
|
||||||
catch (IOException ex) {
|
catch (IOException ex) {
|
||||||
throw new HttpMessageNotReadableException("Could not read document: " + ex.getMessage(), ex);
|
throw new HttpMessageNotReadableException("Could not read JSON document: " + ex.getMessage(), ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -293,7 +286,7 @@ public abstract class AbstractJackson2HttpMessageConverter extends AbstractGener
|
||||||
|
|
||||||
}
|
}
|
||||||
catch (JsonProcessingException ex) {
|
catch (JsonProcessingException ex) {
|
||||||
throw new HttpMessageNotWritableException("Could not write content: " + ex.getMessage(), ex);
|
throw new HttpMessageNotWritableException("Could not write JSON document: " + ex.getMessage(), ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -315,18 +308,6 @@ public abstract class AbstractJackson2HttpMessageConverter extends AbstractGener
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the Jackson {@link JavaType} for the specified type and context class.
|
* Return the Jackson {@link JavaType} for the specified type and context class.
|
||||||
* <p>The default implementation returns {@code typeFactory.constructType(type, contextClass)},
|
|
||||||
* but this can be overridden in subclasses, to allow for custom generic collection handling.
|
|
||||||
* For instance:
|
|
||||||
* <pre class="code">
|
|
||||||
* protected JavaType getJavaType(Type type) {
|
|
||||||
* if (type instanceof Class && List.class.isAssignableFrom((Class)type)) {
|
|
||||||
* return TypeFactory.collectionType(ArrayList.class, MyBean.class);
|
|
||||||
* } else {
|
|
||||||
* return super.getJavaType(type);
|
|
||||||
* }
|
|
||||||
* }
|
|
||||||
* </pre>
|
|
||||||
* @param type the generic type to return the Jackson JavaType for
|
* @param type the generic type to return the Jackson JavaType for
|
||||||
* @param contextClass a context class for the target type, for example a class
|
* @param contextClass a context class for the target type, for example a class
|
||||||
* in which the target type appears in a method signature (can be {@code null})
|
* in which the target type appears in a method signature (can be {@code null})
|
||||||
|
@ -334,65 +315,7 @@ public abstract class AbstractJackson2HttpMessageConverter extends AbstractGener
|
||||||
*/
|
*/
|
||||||
protected JavaType getJavaType(Type type, Class<?> contextClass) {
|
protected JavaType getJavaType(Type type, Class<?> contextClass) {
|
||||||
TypeFactory typeFactory = this.objectMapper.getTypeFactory();
|
TypeFactory typeFactory = this.objectMapper.getTypeFactory();
|
||||||
if (contextClass != null) {
|
return typeFactory.constructType(GenericTypeResolver.resolveType(type, contextClass));
|
||||||
ResolvableType resolvedType = ResolvableType.forType(type);
|
|
||||||
if (type instanceof TypeVariable) {
|
|
||||||
ResolvableType resolvedTypeVariable = resolveVariable(
|
|
||||||
(TypeVariable<?>) type, ResolvableType.forClass(contextClass));
|
|
||||||
if (resolvedTypeVariable != ResolvableType.NONE) {
|
|
||||||
return typeFactory.constructType(resolvedTypeVariable.resolve());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (type instanceof ParameterizedType && resolvedType.hasUnresolvableGenerics()) {
|
|
||||||
ParameterizedType parameterizedType = (ParameterizedType) type;
|
|
||||||
Class<?>[] generics = new Class<?>[parameterizedType.getActualTypeArguments().length];
|
|
||||||
Type[] typeArguments = parameterizedType.getActualTypeArguments();
|
|
||||||
for (int i = 0; i < typeArguments.length; i++) {
|
|
||||||
Type typeArgument = typeArguments[i];
|
|
||||||
if (typeArgument instanceof TypeVariable) {
|
|
||||||
ResolvableType resolvedTypeArgument = resolveVariable(
|
|
||||||
(TypeVariable<?>) typeArgument, ResolvableType.forClass(contextClass));
|
|
||||||
if (resolvedTypeArgument != ResolvableType.NONE) {
|
|
||||||
generics[i] = resolvedTypeArgument.resolve();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
generics[i] = ResolvableType.forType(typeArgument).resolve();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
generics[i] = ResolvableType.forType(typeArgument).resolve();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return typeFactory.constructType(ResolvableType.
|
|
||||||
forClassWithGenerics(resolvedType.getRawClass(), generics).getType());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return typeFactory.constructType(type);
|
|
||||||
}
|
|
||||||
|
|
||||||
private ResolvableType resolveVariable(TypeVariable<?> typeVariable, ResolvableType contextType) {
|
|
||||||
ResolvableType resolvedType;
|
|
||||||
if (contextType.hasGenerics()) {
|
|
||||||
resolvedType = ResolvableType.forType(typeVariable, contextType);
|
|
||||||
if (resolvedType.resolve() != null) {
|
|
||||||
return resolvedType;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ResolvableType superType = contextType.getSuperType();
|
|
||||||
if (superType != ResolvableType.NONE) {
|
|
||||||
resolvedType = resolveVariable(typeVariable, superType);
|
|
||||||
if (resolvedType.resolve() != null) {
|
|
||||||
return resolvedType;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (ResolvableType ifc : contextType.getInterfaces()) {
|
|
||||||
resolvedType = resolveVariable(typeVariable, ifc);
|
|
||||||
if (resolvedType.resolve() != null) {
|
|
||||||
return resolvedType;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ResolvableType.NONE;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -0,0 +1,161 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2002-2017 the original author 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
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.http.converter.json;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.io.OutputStreamWriter;
|
||||||
|
import java.io.Reader;
|
||||||
|
import java.io.Writer;
|
||||||
|
import java.lang.reflect.Type;
|
||||||
|
import java.nio.charset.Charset;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
|
import org.springframework.core.GenericTypeResolver;
|
||||||
|
import org.springframework.http.HttpHeaders;
|
||||||
|
import org.springframework.http.HttpInputMessage;
|
||||||
|
import org.springframework.http.HttpOutputMessage;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.http.converter.AbstractGenericHttpMessageConverter;
|
||||||
|
import org.springframework.http.converter.HttpMessageNotReadableException;
|
||||||
|
import org.springframework.http.converter.HttpMessageNotWritableException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Common base class for plain JSON converters, e.g. Gson and JSON-B.
|
||||||
|
*
|
||||||
|
* <p>Note that the Jackson converters have a dedicated class hierarchy
|
||||||
|
* due to their multi-format support.
|
||||||
|
*
|
||||||
|
* @author Juergen Hoeller
|
||||||
|
* @since 5.0
|
||||||
|
* @see GsonHttpMessageConverter
|
||||||
|
* @see JsonbHttpMessageConverter
|
||||||
|
* @see #readInternal(Type, Reader)
|
||||||
|
* @see #writeInternal(Object, Type, Writer)
|
||||||
|
*/
|
||||||
|
public abstract class AbstractJsonHttpMessageConverter extends AbstractGenericHttpMessageConverter<Object> {
|
||||||
|
|
||||||
|
public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
|
||||||
|
|
||||||
|
private String jsonPrefix;
|
||||||
|
|
||||||
|
|
||||||
|
public AbstractJsonHttpMessageConverter() {
|
||||||
|
super(MediaType.APPLICATION_JSON, new MediaType("application", "*+json"));
|
||||||
|
setDefaultCharset(DEFAULT_CHARSET);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Specify a custom prefix to use for JSON output. Default is none.
|
||||||
|
* @see #setPrefixJson
|
||||||
|
*/
|
||||||
|
public void setJsonPrefix(String jsonPrefix) {
|
||||||
|
this.jsonPrefix = jsonPrefix;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicate whether the JSON output by this view should be prefixed with ")]}', ".
|
||||||
|
* Default is {@code false}.
|
||||||
|
* <p>Prefixing the JSON string in this manner is used to help prevent JSON
|
||||||
|
* Hijacking. The prefix renders the string syntactically invalid as a script
|
||||||
|
* so that it cannot be hijacked.
|
||||||
|
* This prefix should be stripped before parsing the string as JSON.
|
||||||
|
* @see #setJsonPrefix
|
||||||
|
*/
|
||||||
|
public void setPrefixJson(boolean prefixJson) {
|
||||||
|
this.jsonPrefix = (prefixJson ? ")]}', " : null);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final Object read(Type type, Class<?> contextClass, HttpInputMessage inputMessage)
|
||||||
|
throws IOException, HttpMessageNotReadableException {
|
||||||
|
|
||||||
|
return readResolved(GenericTypeResolver.resolveType(type, contextClass), inputMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected final Object readInternal(Class<?> clazz, HttpInputMessage inputMessage)
|
||||||
|
throws IOException, HttpMessageNotReadableException {
|
||||||
|
|
||||||
|
return readResolved(clazz, inputMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Object readResolved(Type resolvedType, HttpInputMessage inputMessage)
|
||||||
|
throws IOException, HttpMessageNotReadableException {
|
||||||
|
|
||||||
|
Reader reader = getReader(inputMessage);
|
||||||
|
try {
|
||||||
|
return readInternal(resolvedType, reader);
|
||||||
|
}
|
||||||
|
catch (Exception ex) {
|
||||||
|
throw new HttpMessageNotReadableException("Could not read JSON document: " + ex.getMessage(), ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected final void writeInternal(Object o, Type type, HttpOutputMessage outputMessage)
|
||||||
|
throws IOException, HttpMessageNotWritableException {
|
||||||
|
|
||||||
|
Writer writer = getWriter(outputMessage);
|
||||||
|
if (this.jsonPrefix != null) {
|
||||||
|
writer.append(this.jsonPrefix);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
writeInternal(o, type, writer);
|
||||||
|
}
|
||||||
|
catch (Exception ex) {
|
||||||
|
throw new HttpMessageNotWritableException("Could not write JSON document: " + ex.getMessage(), ex);
|
||||||
|
}
|
||||||
|
writer.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Template method that reads the JSON-bound object from the given {@link Reader}.
|
||||||
|
* @param resolvedType the resolved generic type
|
||||||
|
* @param reader the {@code} Reader to use
|
||||||
|
* @return the JSON-bound object
|
||||||
|
* @throws Exception in case of read/parse failures
|
||||||
|
*/
|
||||||
|
protected abstract Object readInternal(Type resolvedType, Reader reader) throws Exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Template method that writes the JSON-bound object to the given {@link Writer}.
|
||||||
|
* @param o the object to write to the output message
|
||||||
|
* @param type the type of object to write (may be {@code null})
|
||||||
|
* @param writer the {@code} Writer to use
|
||||||
|
* @throws Exception in case of write failures
|
||||||
|
*/
|
||||||
|
protected abstract void writeInternal(Object o, Type type, Writer writer) throws Exception;
|
||||||
|
|
||||||
|
|
||||||
|
private static Reader getReader(HttpInputMessage inputMessage) throws IOException {
|
||||||
|
return new InputStreamReader(inputMessage.getBody(), getCharset(inputMessage.getHeaders()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Writer getWriter(HttpOutputMessage outputMessage) throws IOException {
|
||||||
|
return new OutputStreamWriter(outputMessage.getBody(), getCharset(outputMessage.getHeaders()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Charset getCharset(HttpHeaders headers) {
|
||||||
|
Charset charset = (headers.getContentType() != null ? headers.getContentType().getCharset() : null);
|
||||||
|
return (charset != null ? charset : DEFAULT_CHARSET);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright 2002-2016 the original author or authors.
|
* Copyright 2002-2017 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -16,33 +16,18 @@
|
||||||
|
|
||||||
package org.springframework.http.converter.json;
|
package org.springframework.http.converter.json;
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStreamReader;
|
|
||||||
import java.io.OutputStreamWriter;
|
|
||||||
import java.io.Reader;
|
import java.io.Reader;
|
||||||
|
import java.io.Writer;
|
||||||
import java.lang.reflect.Type;
|
import java.lang.reflect.Type;
|
||||||
import java.nio.charset.Charset;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
|
|
||||||
import com.google.gson.Gson;
|
import com.google.gson.Gson;
|
||||||
import com.google.gson.JsonIOException;
|
|
||||||
import com.google.gson.JsonParseException;
|
|
||||||
import com.google.gson.reflect.TypeToken;
|
|
||||||
|
|
||||||
import org.springframework.http.HttpHeaders;
|
|
||||||
import org.springframework.http.HttpInputMessage;
|
|
||||||
import org.springframework.http.HttpOutputMessage;
|
|
||||||
import org.springframework.http.MediaType;
|
|
||||||
import org.springframework.http.converter.AbstractGenericHttpMessageConverter;
|
|
||||||
import org.springframework.http.converter.HttpMessageNotReadableException;
|
|
||||||
import org.springframework.http.converter.HttpMessageNotWritableException;
|
|
||||||
import org.springframework.util.Assert;
|
import org.springframework.util.Assert;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Implementation of {@link org.springframework.http.converter.HttpMessageConverter}
|
* Implementation of {@link org.springframework.http.converter.HttpMessageConverter}
|
||||||
* that can read and write JSON using the
|
* that can read and write JSON using the
|
||||||
* <a href="https://code.google.com/p/google-gson/">Google Gson</a> library's
|
* <a href="https://code.google.com/p/google-gson/">Google Gson</a> library.
|
||||||
* {@link Gson} class.
|
|
||||||
*
|
*
|
||||||
* <p>This converter can be used to bind to typed beans or untyped {@code HashMap}s.
|
* <p>This converter can be used to bind to typed beans or untyped {@code HashMap}s.
|
||||||
* By default, it supports {@code application/json} and {@code application/*+json} with
|
* By default, it supports {@code application/json} and {@code application/*+json} with
|
||||||
|
@ -51,37 +36,43 @@ import org.springframework.util.Assert;
|
||||||
* <p>Tested against Gson 2.6; compatible with Gson 2.0 and higher.
|
* <p>Tested against Gson 2.6; compatible with Gson 2.0 and higher.
|
||||||
*
|
*
|
||||||
* @author Roy Clarkson
|
* @author Roy Clarkson
|
||||||
|
* @author Juergen Hoeller
|
||||||
* @since 4.1
|
* @since 4.1
|
||||||
|
* @see com.google.gson.Gson
|
||||||
|
* @see com.google.gson.GsonBuilder
|
||||||
* @see #setGson
|
* @see #setGson
|
||||||
* @see #setSupportedMediaTypes
|
|
||||||
*/
|
*/
|
||||||
public class GsonHttpMessageConverter extends AbstractGenericHttpMessageConverter<Object> {
|
public class GsonHttpMessageConverter extends AbstractJsonHttpMessageConverter {
|
||||||
|
|
||||||
public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
|
private Gson gson;
|
||||||
|
|
||||||
|
|
||||||
private Gson gson = new Gson();
|
|
||||||
|
|
||||||
private String jsonPrefix;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Construct a new {@code GsonHttpMessageConverter}.
|
* Construct a new {@code GsonHttpMessageConverter} with default configuration.
|
||||||
*/
|
*/
|
||||||
public GsonHttpMessageConverter() {
|
public GsonHttpMessageConverter() {
|
||||||
super(MediaType.APPLICATION_JSON, new MediaType("application", "*+json"));
|
this(new Gson());
|
||||||
this.setDefaultCharset(DEFAULT_CHARSET);
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a new {@code GsonHttpMessageConverter} with the given delegate.
|
||||||
|
* @param gson the Gson instance to use
|
||||||
|
* @since 5.0
|
||||||
|
*/
|
||||||
|
public GsonHttpMessageConverter(Gson gson) {
|
||||||
|
setGson(gson);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the {@code Gson} instance to use.
|
* Set the {@code Gson} instance to use.
|
||||||
* If not set, a default {@link Gson#Gson() Gson} instance is used.
|
* If not set, a default {@link Gson#Gson() Gson} instance will be used.
|
||||||
* <p>Setting a custom-configured {@code Gson} is one way to take further
|
* <p>Setting a custom-configured {@code Gson} is one way to take further
|
||||||
* control of the JSON serialization process.
|
* control of the JSON serialization process.
|
||||||
|
* @see #GsonHttpMessageConverter(Gson)
|
||||||
*/
|
*/
|
||||||
public void setGson(Gson gson) {
|
public void setGson(Gson gson) {
|
||||||
Assert.notNull(gson, "'gson' is required");
|
Assert.notNull(gson, "A Gson instance is required");
|
||||||
this.gson = gson;
|
this.gson = gson;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -92,119 +83,19 @@ public class GsonHttpMessageConverter extends AbstractGenericHttpMessageConverte
|
||||||
return this.gson;
|
return this.gson;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Specify a custom prefix to use for JSON output. Default is none.
|
|
||||||
* @see #setPrefixJson
|
|
||||||
*/
|
|
||||||
public void setJsonPrefix(String jsonPrefix) {
|
|
||||||
this.jsonPrefix = jsonPrefix;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Indicate whether the JSON output by this view should be prefixed with ")]}', ".
|
|
||||||
* Default is {@code false}.
|
|
||||||
* <p>Prefixing the JSON string in this manner is used to help prevent JSON
|
|
||||||
* Hijacking. The prefix renders the string syntactically invalid as a script
|
|
||||||
* so that it cannot be hijacked.
|
|
||||||
* This prefix should be stripped before parsing the string as JSON.
|
|
||||||
* @see #setJsonPrefix
|
|
||||||
*/
|
|
||||||
public void setPrefixJson(boolean prefixJson) {
|
|
||||||
this.jsonPrefix = (prefixJson ? ")]}', " : null);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean canRead(Class<?> clazz, MediaType mediaType) {
|
protected Object readInternal(Type resolvedType, Reader reader) throws Exception {
|
||||||
return canRead(mediaType);
|
return getGson().fromJson(reader, resolvedType);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean canWrite(Class<?> clazz, MediaType mediaType) {
|
protected void writeInternal(Object o, Type type, Writer writer) throws Exception {
|
||||||
return canWrite(mediaType);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected boolean supports(Class<?> clazz) {
|
|
||||||
// should not be called, since we override canRead/Write instead
|
|
||||||
throw new UnsupportedOperationException();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Object readInternal(Class<?> clazz, HttpInputMessage inputMessage)
|
|
||||||
throws IOException, HttpMessageNotReadableException {
|
|
||||||
|
|
||||||
TypeToken<?> token = getTypeToken(clazz);
|
|
||||||
return readTypeToken(token, inputMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Object read(Type type, Class<?> contextClass, HttpInputMessage inputMessage)
|
|
||||||
throws IOException, HttpMessageNotReadableException {
|
|
||||||
|
|
||||||
TypeToken<?> token = getTypeToken(type);
|
|
||||||
return readTypeToken(token, inputMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return the Gson {@link TypeToken} for the specified type.
|
|
||||||
* <p>The default implementation returns {@code TypeToken.get(type)}, but
|
|
||||||
* this can be overridden in subclasses to allow for custom generic
|
|
||||||
* collection handling. For instance:
|
|
||||||
* <pre class="code">
|
|
||||||
* protected TypeToken<?> getTypeToken(Type type) {
|
|
||||||
* if (type instanceof Class && List.class.isAssignableFrom((Class<?>) type)) {
|
|
||||||
* return new TypeToken<ArrayList<MyBean>>() {};
|
|
||||||
* }
|
|
||||||
* else {
|
|
||||||
* return super.getTypeToken(type);
|
|
||||||
* }
|
|
||||||
* }
|
|
||||||
* </pre>
|
|
||||||
* @param type the type for which to return the TypeToken
|
|
||||||
* @return the type token
|
|
||||||
*/
|
|
||||||
protected TypeToken<?> getTypeToken(Type type) {
|
|
||||||
return TypeToken.get(type);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Object readTypeToken(TypeToken<?> token, HttpInputMessage inputMessage) throws IOException {
|
|
||||||
Reader json = new InputStreamReader(inputMessage.getBody(), getCharset(inputMessage.getHeaders()));
|
|
||||||
try {
|
|
||||||
return this.gson.fromJson(json, token.getType());
|
|
||||||
}
|
|
||||||
catch (JsonParseException ex) {
|
|
||||||
throw new HttpMessageNotReadableException("Could not read JSON: " + ex.getMessage(), ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Charset getCharset(HttpHeaders headers) {
|
|
||||||
if (headers == null || headers.getContentType() == null || headers.getContentType().getCharset() == null) {
|
|
||||||
return DEFAULT_CHARSET;
|
|
||||||
}
|
|
||||||
return headers.getContentType().getCharset();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void writeInternal(Object o, Type type, HttpOutputMessage outputMessage)
|
|
||||||
throws IOException, HttpMessageNotWritableException {
|
|
||||||
|
|
||||||
Charset charset = getCharset(outputMessage.getHeaders());
|
|
||||||
OutputStreamWriter writer = new OutputStreamWriter(outputMessage.getBody(), charset);
|
|
||||||
try {
|
|
||||||
if (this.jsonPrefix != null) {
|
|
||||||
writer.append(this.jsonPrefix);
|
|
||||||
}
|
|
||||||
if (type != null) {
|
if (type != null) {
|
||||||
this.gson.toJson(o, type, writer);
|
getGson().toJson(o, type, writer);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
this.gson.toJson(o, writer);
|
getGson().toJson(o, writer);
|
||||||
}
|
|
||||||
writer.close();
|
|
||||||
}
|
|
||||||
catch (JsonIOException ex) {
|
|
||||||
throw new HttpMessageNotWritableException("Could not write JSON: " + ex.getMessage(), ex);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,7 +17,7 @@
|
||||||
package org.springframework.http.converter.json;
|
package org.springframework.http.converter.json;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.lang.reflect.Type;
|
import java.lang.reflect.Field;
|
||||||
import java.nio.charset.Charset;
|
import java.nio.charset.Charset;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
@ -25,7 +25,6 @@ import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import com.google.gson.reflect.TypeToken;
|
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
import org.springframework.core.ParameterizedTypeReference;
|
import org.springframework.core.ParameterizedTypeReference;
|
||||||
|
@ -149,25 +148,15 @@ public class GsonHttpMessageConverterTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
public void readGenerics() throws IOException {
|
public void readGenerics() throws Exception {
|
||||||
GsonHttpMessageConverter converter = new GsonHttpMessageConverter() {
|
Field beansList = ListHolder.class.getField("listField");
|
||||||
@Override
|
|
||||||
protected TypeToken<?> getTypeToken(Type type) {
|
|
||||||
if (type instanceof Class && List.class.isAssignableFrom((Class<?>) type)) {
|
|
||||||
return new TypeToken<ArrayList<MyBean>>() {
|
|
||||||
};
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return super.getTypeToken(type);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
String body = "[{\"bytes\":[1,2],\"array\":[\"Foo\",\"Bar\"]," +
|
String body = "[{\"bytes\":[1,2],\"array\":[\"Foo\",\"Bar\"]," +
|
||||||
"\"number\":42,\"string\":\"Foo\",\"bool\":true,\"fraction\":42.0}]";
|
"\"number\":42,\"string\":\"Foo\",\"bool\":true,\"fraction\":42.0}]";
|
||||||
MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes(StandardCharsets.UTF_8));
|
MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes(StandardCharsets.UTF_8));
|
||||||
inputMessage.getHeaders().setContentType(new MediaType("application", "json"));
|
inputMessage.getHeaders().setContentType(new MediaType("application", "json"));
|
||||||
|
|
||||||
List<MyBean> results = (List<MyBean>) converter.read(List.class, inputMessage);
|
List<MyBean> results = (List<MyBean>) converter.read(beansList.getGenericType(), MyBeanListHolder.class, inputMessage);
|
||||||
assertEquals(1, results.size());
|
assertEquals(1, results.size());
|
||||||
MyBean result = results.get(0);
|
MyBean result = results.get(0);
|
||||||
assertEquals("Foo", result.getString());
|
assertEquals("Foo", result.getString());
|
||||||
|
@ -180,7 +169,7 @@ public class GsonHttpMessageConverterTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
public void readParameterizedType() throws IOException {
|
public void readParameterizedType() throws Exception {
|
||||||
ParameterizedTypeReference<List<MyBean>> beansList = new ParameterizedTypeReference<List<MyBean>>() {
|
ParameterizedTypeReference<List<MyBean>> beansList = new ParameterizedTypeReference<List<MyBean>>() {
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -189,7 +178,6 @@ public class GsonHttpMessageConverterTests {
|
||||||
MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes(StandardCharsets.UTF_8));
|
MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes(StandardCharsets.UTF_8));
|
||||||
inputMessage.getHeaders().setContentType(new MediaType("application", "json"));
|
inputMessage.getHeaders().setContentType(new MediaType("application", "json"));
|
||||||
|
|
||||||
GsonHttpMessageConverter converter = new GsonHttpMessageConverter();
|
|
||||||
List<MyBean> results = (List<MyBean>) converter.read(beansList.getType(), null, inputMessage);
|
List<MyBean> results = (List<MyBean>) converter.read(beansList.getType(), null, inputMessage);
|
||||||
assertEquals(1, results.size());
|
assertEquals(1, results.size());
|
||||||
MyBean result = results.get(0);
|
MyBean result = results.get(0);
|
||||||
|
@ -281,4 +269,14 @@ public class GsonHttpMessageConverterTests {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static class ListHolder<E> {
|
||||||
|
|
||||||
|
public List<E> listField;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static class MyBeanListHolder extends ListHolder<MyBean> {
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue