Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

convert empty string to empty collection or array #54

Merged
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/Reference Doc/v0.1.0/restlight_starter/serialization.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ spring.gson.date-format=yyyy-MM-dd HH:mm:ss
```

Restlight同样支持该标准,其作用逻辑如下图所示:
![序列化器兼容springboot逻辑图.png](img/SerializationOfSpringBoot.png)
![序列化器兼容springboot逻辑图.png](../../../img/serialization_of_springboot.png)
如图所示,配置文件中Jackson和Gson相关内容要想生效,需要满足如下条件:
> 1. 用户没有手动定制HttpJsonBodySerializerAdapter
> 2. 用户没有手动注入Jackson或者Gson序列化器(FastJsonHttpBodySerializer和GsonJsonHttpBodySerializer)
Expand Down Expand Up @@ -310,4 +310,4 @@ Restlight内置了ProtoBuf序列化器的实现,对应支持的MediaType为`ap
> - 值为ByteBuf类型时直接将结果写入响应
> - 以上均不符合则使用序列化器进行序列化。
> - 如果Controlelr上未配置`@ResponseBody`,值为基本类型或基本类型包装类将返回该类型的字符串结果(调用String.valueOf())。
> - 以上均不符合则抛异常。
> - 以上均不符合则抛异常。
Binary file added docs/img/serialization_of_springboot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
import java.util.List;
import java.util.Map;
import java.util.NavigableSet;
import java.util.Optional;
import java.util.Set;
import java.util.SortedSet;
import java.util.StringTokenizer;
Expand All @@ -63,35 +64,39 @@ private ConverterUtils() {
= new HashMap<>(32);

static {
STRING_CONVERTER_MAP.put(Byte.class, Byte::valueOf);
STRING_CONVERTER_MAP.put(Byte.class, v -> StringUtils.isEmpty(v) ? null : Byte.valueOf(v));
STRING_CONVERTER_MAP.put(byte.class, Byte::parseByte);

STRING_CONVERTER_MAP.put(Character.class, v -> v.length() > 0 ? v.charAt(0) : v);
STRING_CONVERTER_MAP.put(Character.class, v -> StringUtils.isEmpty(v)
? null : v.length() > 0 ? v.charAt(0) : v);
STRING_CONVERTER_MAP.put(char.class, v -> v.length() >= 1 ? v.charAt(0) : v);

STRING_CONVERTER_MAP.put(Boolean.class, v -> (Boolean.parseBoolean(v) || "1".equals(v)) ? Boolean.TRUE :
Boolean.FALSE);
STRING_CONVERTER_MAP.put(Boolean.class, v -> StringUtils.isEmpty(v)
? null : (Boolean.parseBoolean(v) || "1".equals(v)) ? Boolean.TRUE : Boolean.FALSE);
STRING_CONVERTER_MAP.put(boolean.class, v -> (Boolean.parseBoolean(v) || "1".equals(v)));

STRING_CONVERTER_MAP.put(Short.class, Short::valueOf);
STRING_CONVERTER_MAP.put(Short.class, v -> StringUtils.isEmpty(v) ? null : Short.valueOf(v));
STRING_CONVERTER_MAP.put(short.class, Short::parseShort);

STRING_CONVERTER_MAP.put(Integer.class, Integer::valueOf);
STRING_CONVERTER_MAP.put(Integer.class, v -> StringUtils.isEmpty(v) ? null : Integer.valueOf(v));
STRING_CONVERTER_MAP.put(int.class, Integer::parseInt);

STRING_CONVERTER_MAP.put(Long.class, Long::valueOf);
STRING_CONVERTER_MAP.put(Long.class, v -> StringUtils.isEmpty(v) ? null : Long.valueOf(v));
STRING_CONVERTER_MAP.put(long.class, Long::parseLong);

STRING_CONVERTER_MAP.put(Double.class, Double::valueOf);
STRING_CONVERTER_MAP.put(Double.class, v -> StringUtils.isEmpty(v) ? null : Double.valueOf(v));
STRING_CONVERTER_MAP.put(double.class, Double::parseDouble);

STRING_CONVERTER_MAP.put(Float.class, Float::valueOf);
STRING_CONVERTER_MAP.put(Float.class, v -> StringUtils.isEmpty(v) ? null : Float.valueOf(v));
STRING_CONVERTER_MAP.put(float.class, Float::parseFloat);

STRING_CONVERTER_MAP.put(Void.class, v -> null);
STRING_CONVERTER_MAP.put(void.class, v -> null);

STRING_CONVERTER_MAP.put(BigDecimal.class, v -> BigDecimal.valueOf(Double.parseDouble(v)));
STRING_CONVERTER_MAP.put(BigDecimal.class, v -> StringUtils.isEmpty(v)
? null : BigDecimal.valueOf(Double.parseDouble(v)));

// convert a empty string to a null is not supported
STRING_CONVERTER_MAP.put(Timestamp.class, Timestamp::valueOf);
STRING_CONVERTER_MAP.put(String.class, v -> v);
STRING_CONVERTER_MAP.put(Object.class, v -> v);
Expand Down Expand Up @@ -139,17 +144,13 @@ public static String normaliseDefaultValue(String value) {
@SuppressWarnings("unchecked")
public static <T> T forceConvertStringValue(String value, Type requiredType) {
Checks.checkNotNull(requiredType, "requiredType");
if (StringUtils.isEmpty(value)) {
return null;
} else {
Function<String, Object> converter = ConverterUtils.str2ObjectConverter(requiredType);
if (converter == null) {
throw new IllegalArgumentException("Could not convert given value '"
+ value
+ "'to target type '" + requiredType + "'");
}
return (T) converter.apply(value);
Function<String, Object> converter = ConverterUtils.str2ObjectConverter(requiredType);
if (converter == null) {
throw new IllegalArgumentException("Could not convert given value '"
+ value
+ "' to target type '" + requiredType + "'");
}
return (T) converter.apply(value);
}

/**
Expand All @@ -174,7 +175,7 @@ public static Function<String, Object> str2ObjectConverter(Type requiredType, Fu
}
return p -> {
if (p == null) {
return null;
return null2ObjectConverter(requiredClass);
}
return str2Object.apply(p);
};
Expand All @@ -195,12 +196,19 @@ public static Function<Collection<String>, Object> strs2ObjectConverter(Type req
}
return p -> {
if (p == null) {
return null;
return null2ObjectConverter(requiredClass);
}
return strs2Object.apply(p);
};
}

private static Object null2ObjectConverter(Class<?> requiredType) {
if (Optional.class.isAssignableFrom(requiredType)) {
return Optional.empty();
}
return null;
}

private static Function<Collection<String>, Object> strs2ObjectConverter(Class<?> requiredClass,
Type requiredType) {
Function<Collection<String>, Object> converter;
Expand All @@ -215,6 +223,43 @@ private static Function<Collection<String>, Object> strs2ObjectConverter(Class<?
return null;
}

private static Class<?> retrieveElementType(Type requiredType) {
Class<?> elementType = null;
if (requiredType != null) {
elementType = ClassUtils.retrieveFirstGenericType(requiredType).orElse(null);
}

if (elementType == null) {
elementType = Object.class;
}

return elementType;
}

private static class Str2OptionalConverter implements Function<String, Object> {

private final Function<String, Object> elementConverter;

private Str2OptionalConverter(Function<String, Object> elementConverter) {
this.elementConverter = elementConverter;
}

private static Str2OptionalConverter of(Type requiredType) {
Function<String, Object> elementConverter = getStr2ObjectConverter(retrieveElementType(requiredType),
null);
if (elementConverter == null) {
// we don't know how to convert the elements
return null;
}
return new Str2OptionalConverter(elementConverter);
}

@Override
public Object apply(String s) {
return Optional.ofNullable(elementConverter.apply(s));
}
}

/**
* Converts a collection of {@link String} values to an array.
*/
Expand Down Expand Up @@ -300,16 +345,8 @@ private static Strs2CollectionConverter of(Class<?> requiredClass, Type required
return null;
}

Class<?> elementType = null;
if (requiredType != null) {
elementType = ClassUtils.retrieveFirstGenericType(requiredType).orElse(null);
}

if (elementType == null) {
elementType = Object.class;
}

Function<String, Object> elementConverter = getStr2ObjectConverter(elementType, null);
Function<String, Object> elementConverter = getStr2ObjectConverter(retrieveElementType(requiredType),
null);
if (elementConverter == null) {
// we don't know how to convert the elements
return null;
Expand Down Expand Up @@ -371,6 +408,11 @@ private static Function<String, Object> getStr2ObjectConverter(Class<?> required
return converter;
}

if (Optional.class.isAssignableFrom(requiredClass)
&& (converter = Str2OptionalConverter.of(requiredType)) != null) {
return converter;
}

// have a constructor that accepts a single String argument.
Constructor<?> constructor = getSingleStringParameterConstructor(requiredClass);
if (constructor != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,15 @@
import java.util.LinkedList;
import java.util.List;
import java.util.NavigableSet;
import java.util.Optional;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.concurrent.CopyOnWriteArrayList;

import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertSame;
Expand Down Expand Up @@ -65,6 +67,24 @@ void testConvertString2Primitives() {
assertNull(ConverterUtils.str2ObjectConverter(Void.class).apply("10"));
}

@Test
void testConvertString2Wrappers() {
assertNull(ConverterUtils.str2ObjectConverter(Byte.class).apply(""));
assertNull(ConverterUtils.str2ObjectConverter(Character.class).apply(""));
assertNull(ConverterUtils.str2ObjectConverter(Boolean.class).apply(""));
assertNull(ConverterUtils.str2ObjectConverter(Short.class).apply(""));
assertNull(ConverterUtils.str2ObjectConverter(Integer.class).apply(""));
assertNull(ConverterUtils.str2ObjectConverter(Long.class).apply(""));
assertNull(ConverterUtils.str2ObjectConverter(Double.class).apply(""));
assertNull(ConverterUtils.str2ObjectConverter(Float.class).apply(""));
assertNull(ConverterUtils.str2ObjectConverter(Void.class).apply(""));
assertNull(ConverterUtils.str2ObjectConverter(BigDecimal.class).apply(""));
assertThrows(IllegalArgumentException.class,
() -> ConverterUtils.str2ObjectConverter(Timestamp.class).apply(""));
assertNull(ConverterUtils.str2ObjectConverter(String.class).apply(null));
assertNull(ConverterUtils.str2ObjectConverter(Object.class).apply(null));
}

@Test
void testConvertString2Objects() {
assertEquals(new BigDecimal("10.0"), ConverterUtils.str2ObjectConverter(BigDecimal.class).apply("10.0"));
Expand Down Expand Up @@ -165,6 +185,41 @@ void testConvertString2Collection() throws NoSuchMethodException {
assertNull(ConverterUtils.str2ObjectConverter(MyList.class));
}

@Test
void testConvertString2Optional() throws NoSuchMethodException {
final Optional<String> optional0 = (Optional<String>)
ConverterUtils.str2ObjectConverter(Subject.class.getDeclaredMethod("optionalString")
.getGenericReturnType()).apply(null);
assertFalse(optional0.isPresent());

final Optional<Long> optional1 = (Optional<Long>)
ConverterUtils.str2ObjectConverter(Subject.class.getDeclaredMethod("optionalLong")
.getGenericReturnType()).apply(null);
assertFalse(optional1.isPresent());

final Optional<String[]> optional2 = (Optional<String[]>)
ConverterUtils.str2ObjectConverter(Subject.class.getDeclaredMethod("optionalArray")
.getGenericReturnType()).apply(null);
assertFalse(optional2.isPresent());

final Optional<String[]> optional3 = (Optional<String[]>)
ConverterUtils.str2ObjectConverter(Subject.class.getDeclaredMethod("optionalArray")
.getGenericReturnType()).apply("");
assertTrue(optional3.isPresent());
assertEquals(0, optional3.get().length);

final Optional<Collection<String>> optional4 = (Optional<Collection<String>>)
ConverterUtils.str2ObjectConverter(Subject.class.getDeclaredMethod("optionalCollection")
.getGenericReturnType()).apply(null);
assertFalse(optional4.isPresent());

final Optional<Collection<String>> optional5 = (Optional<Collection<String>>)
ConverterUtils.str2ObjectConverter(Subject.class.getDeclaredMethod("optionalCollection")
.getGenericReturnType()).apply("");
assertTrue(optional5.isPresent());
assertTrue(optional5.get().isEmpty());
}

@Test
void testConvertStringCollection2PrimitiveArray() {
final List<String> forTest = Arrays.asList("1", "2", "3", "4");
Expand Down Expand Up @@ -217,6 +272,14 @@ private interface Subject {
LinkedList<Double> doubleLinkedList();

Collection<int[]> intArrayCollection();

Optional<String> optionalString();

Optional<Long> optionalLong();

Optional<String[]> optionalArray();

Optional<Collection<String>> optionalCollection();
}

@Test
Expand Down Expand Up @@ -327,6 +390,11 @@ void testForceConvertStringValue() {
assertNull(ConverterUtils.forceConvertStringValue("", Integer.class));
assertThrows(IllegalArgumentException.class,
() -> ConverterUtils.forceConvertStringValue("foo", MyList.class));

assertNull(ConverterUtils.forceConvertStringValue(null, Collection.class));
assertNull(ConverterUtils.forceConvertStringValue(null, String[].class));
assertNotNull(ConverterUtils.forceConvertStringValue("", Collection.class));
assertNotNull(ConverterUtils.forceConvertStringValue("", String[].class));
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
import esa.restlight.core.util.ConverterUtils;
import esa.restlight.server.bootstrap.WebServerException;

import java.util.Optional;

public abstract class AbstractNameAndValueArgumentResolver implements ArgumentResolver {

protected final Param param;
Expand All @@ -42,12 +44,10 @@ public AbstractNameAndValueArgumentResolver(Param param, NameAndValue nav) {
public Object resolve(AsyncRequest request, AsyncResponse response) throws Exception {
Object arg = this.resolveName(nav.name, request);
if (arg == null) {
if (nav.defaultValue == null) {
if (nav.required) {
throw WebServerException.badRequest("Missing required value: " + nav.name);
}
} else {
if (nav.hasDefaultValue) {
arg = nav.defaultValue;
} else if (nav.required) {
throw WebServerException.badRequest("Missing required value: " + nav.name);
}
}
return arg;
Expand Down Expand Up @@ -94,14 +94,30 @@ private NameAndValue updateNamedValueInfo(Param param, NameAndValue nav) {
}
}
Object defaultValue = null;
if (nav.defaultValue != null) {
boolean hasDefaultValue = false;
if (nav.hasDefaultValue) {
defaultValue = nav.defaultValue;
hasDefaultValue = true;
} else if (!nav.required && (useObjectDefaultValueIfRequired(param, nav))) {
defaultValue = ObjectUtils.defaultValue(param.type());
defaultValue = defaultValue(param.type());
hasDefaultValue = true;
} else if (Optional.class.isAssignableFrom(param.type())) {
defaultValue = Optional.empty();
hasDefaultValue = true;
}

if (defaultValue instanceof String && !param.type().isInstance(defaultValue)) {
defaultValue = ConverterUtils.forceConvertStringValue((String) defaultValue, param.genericType());
hasDefaultValue = true;
}
return new NameAndValue(name, nav.required, defaultValue, hasDefaultValue);
}

private static Object defaultValue(Class<?> type) {
if (Optional.class.isAssignableFrom(type)) {
return Optional.empty();
}
return new NameAndValue(name, nav.required, defaultValue);

return ObjectUtils.defaultValue(type);
}
}
Loading