I have a query parameter as following:
#GET
public Response myFunction(#QueryParam("start") final LocalDate start, #QueryParam("end") final LocalDate end) { ... }
For this, I created a ParamConverter<LocalDate> which converts a string to a date, and vice versa.
Now I want to use #DefaultValue annotation to declare a default value. I have two special default values:
today (for start)
the maximum value/infinity (for end)
Is it possible to use the #DefaultValue annotation for this? How?
Yes, #DefaultValue value can be used in this situation:
#GET
public Response foo(#QueryParam("start") #DefaultValue("today") LocalDate start,
#QueryParam("end") #DefaultValue("max") LocalDate end) {
...
}
Your ParamConverterProvider and ParamConverter implementations can be like:
#Provider
public class LocalDateParamConverterProvider implements ParamConverterProvider {
#Override
public <T> ParamConverter<T> getConverter(Class<T> rawType, Type genericType,
Annotation[] annotations) {
if (rawType.getName().equals(LocalDate.class.getName())) {
return new ParamConverter<T>() {
#Override
public T fromString(String value) {
return parseString(value, rawType);
}
#Override
public String toString(T value) {
return ((LocalDateTime) value)
.format(DateTimeFormatter.ISO_LOCAL_DATE);
}
};
}
return null;
}
private <T> T parseString(String value, Class<T> rawType) {
if (value == null) {
return null;
}
if ("today".equalsIgnoreCase(value)) {
return rawType.cast(LocalDate.now());
}
if ("max".equalsIgnoreCase(value)) {
return rawType.cast(LocalDate.of(9999, 12, 31));
}
try {
return rawType.cast(LocalDate.parse(value,
DateTimeFormatter.ISO_LOCAL_DATE));
} catch (Exception e) {
throw new BadRequestException(e);
}
}
}
If, for some reason, you need the parameter name, you can get it from the Annotation array:
Optional<Annotation> optional = Arrays.stream(annotations)
.filter(annotation -> annotation.annotationType().equals(QueryParam.class))
.findFirst();
if (optional.isPresent()) {
QueryParam queryParam = (QueryParam) optional.get();
String parameterName = queryParam.value();
}
Related
Gson doesnt have direct support for serializing #AutoValue classes or for Optional<> fields, but com.ryanharter.auto.value adds #AutoValue and net.dongliu:gson-java8-datatype adds Optional<> and other java8 types.
However, they dont work together AFAICT.
Test code:
public class TestOptionalWithAutoValue {
private static final Gson gson = new GsonBuilder().serializeNulls()
// doesnt matter which order these are registered in
.registerTypeAdapterFactory(new GsonJava8TypeAdapterFactory())
.registerTypeAdapterFactory(AutoValueGsonTypeAdapterFactory.create())
.create();
#Test
public void testAutoValueOptionalEmpty() {
AvoTestClass subject = AvoTestClass.create(Optional.empty());
String json = gson.toJson(subject, AvoTestClass.class);
System.out.printf("Json produced = %s%n", json);
AvoTestClass back = gson.fromJson(json, new TypeToken<AvoTestClass>() {}.getType());
assertThat(back).isEqualTo(subject);
}
#Test
public void testAutoValueOptionalFull() {
AvoTestClass subject = AvoTestClass.create(Optional.of("ok"));
String json = gson.toJson(subject, AvoTestClass.class);
System.out.printf("Json produced = '%s'%n", json);
AvoTestClass back = gson.fromJson(json, new TypeToken<AvoTestClass>() {}.getType());
assertThat(back).isEqualTo(subject);
}
}
#AutoValue
public abstract class AvoTestClass {
abstract Optional<String> sval();
public static AvoTestClass create(Optional<String> sval) {
return new AutoValue_AvoTestClass(sval);
}
public static TypeAdapter<AvoTestClass> typeAdapter(Gson gson) {
return new AutoValue_AvoTestClass.GsonTypeAdapter(gson);
}
}
#GsonTypeAdapterFactory
public abstract class AutoValueGsonTypeAdapterFactory implements TypeAdapterFactory {
public static TypeAdapterFactory create() {
return new AutoValueGson_AutoValueGsonTypeAdapterFactory();
}
}
gradle dependencies:
annotationProcessor "com.google.auto.value:auto-value:1.7.4"
annotationProcessor("com.ryanharter.auto.value:auto-value-gson-extension:1.3.1")
implementation("com.ryanharter.auto.value:auto-value-gson-runtime:1.3.1")
annotationProcessor("com.ryanharter.auto.value:auto-value-gson-factory:1.3.1")
implementation 'net.dongliu:gson-java8-datatype:1.1.0'
Fails with:
Json produced = {"sval":null}
...
java.lang.NullPointerException: Null sval
...
net.dongliu.gson.OptionalAdapter is called on serialization, but not deserialization.
Im wondering if theres a workaround, or if the answer is that Gson needs to have direct support for Optional<> ?
Glad to see you've updated your question by adding much more information and even by adding a test! :) That really makes it clear!
I'm not sure, but the generated type adapter has no mention for the default value for sval:
jsonReader.beginObject();
// [NOTE] This is where it is initialized with null, so I guess it will definitely fail if the `sval` property is not even present in the deserialized JSON object
Optional<String> sval = null;
while (jsonReader.hasNext()) {
String _name = jsonReader.nextName();
// [NOTE] This is where it skips `null` value so it even does not reach to the `OptionalAdapter` run
if (jsonReader.peek() == JsonToken.NULL) {
jsonReader.nextNull();
continue;
}
switch (_name) {
default: {
if ("sval".equals(_name)) {
TypeAdapter<Optional<String>> optional__string_adapter = this.optional__string_adapter;
if (optional__string_adapter == null) {
this.optional__string_adapter = optional__string_adapter = (TypeAdapter<Optional<String>>) gson.getAdapter(TypeToken.getParameterized(Optional.class, String.class));
}
sval = optional__string_adapter.read(jsonReader);
continue;
}
jsonReader.skipValue();
}
}
}
jsonReader.endObject();
return new AutoValue_AvoTestClass(sval);
I have no idea if there is a way to configure the default values in AutoValue or other generators you mentioned, but it looks like a bug.
If there is no any way to work around it (say, library development abandoned; it takes too much time to wait for a fix; whatever), you can always implement it yourself, however with some runtime cost (basically this how Gson works under the hood for data bag objects).
The idea is delegating the job to the built-in RuntimeTypeAdapterFactory so that it could deal with a concrete class, not an abstract one, and set all fields according to the registered type adapters (so that the Java 8 types are supported as well).
The cost here is reflection, thus that adapter may work slower than generated type adapters.
Another thing is that if a JSON property does not even encounter in the JSON object, the corresponding field will remain null.
This requires another post-deserialization type adapter.
final class SubstitutionTypeAdapterFactory
implements TypeAdapterFactory {
private final Function<? super Type, ? extends Type> substitute;
private SubstitutionTypeAdapterFactory(final Function<? super Type, ? extends Type> substitute) {
this.substitute = substitute;
}
static TypeAdapterFactory create(final Function<? super Type, ? extends Type> substitute) {
return new SubstitutionTypeAdapterFactory(substitute);
}
#Override
#Nullable
public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> typeToken) {
#Nullable
final Type substitution = substitute.apply(typeToken.getType());
if ( substitution == null ) {
return null;
}
#SuppressWarnings("unchecked")
final TypeAdapter<T> delegateTypeAdapter = (TypeAdapter<T>) gson.getDelegateAdapter(this, TypeToken.get(substitution));
return delegateTypeAdapter;
}
}
final class DefaultsTypeAdapterFactory
implements TypeAdapterFactory {
private final Function<? super Type, ? extends Type> substitute;
private final LoadingCache<Class<?>, Collection<Map.Entry<Field, ?>>> fieldsCache;
private DefaultsTypeAdapterFactory(final Function<? super Type, ? extends Type> substitute, final Function<? super Type, ?> toDefault) {
this.substitute = substitute;
fieldsCache = CacheBuilder.newBuilder()
// TODO tweak the cache
.build(new CacheLoader<Class<?>, Collection<Map.Entry<Field, ?>>>() {
#Override
public Collection<Map.Entry<Field, ?>> load(final Class<?> clazz) {
// TODO walk hieararchy
return Stream.of(clazz.getDeclaredFields())
.map(field -> {
#Nullable
final Object defaultValue = toDefault.apply(field.getGenericType());
if ( defaultValue == null ) {
return null;
}
field.setAccessible(true);
return new AbstractMap.SimpleImmutableEntry<>(field, defaultValue);
})
.filter(Objects::nonNull)
.collect(Collectors.toList());
}
});
}
static TypeAdapterFactory create(final Function<? super Type, ? extends Type> substitute, final Function<? super Type, ?> toDefault) {
return new DefaultsTypeAdapterFactory(substitute, toDefault);
}
#Override
#Nullable
public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> typeToken) {
#Nullable
final Type substitution = substitute.apply(typeToken.getType());
if ( substitution == null ) {
return null;
}
if ( !(substitution instanceof Class) ) {
return null;
}
final Collection<Map.Entry<Field, ?>> fieldsToPatch = fieldsCache.getUnchecked((Class<?>) substitution);
if ( fieldsToPatch.isEmpty() ) {
return null;
}
final TypeAdapter<T> delegateTypeAdapter = gson.getDelegateAdapter(this, typeToken);
return new TypeAdapter<T>() {
#Override
public void write(final JsonWriter out, final T value)
throws IOException {
delegateTypeAdapter.write(out, value);
}
#Override
public T read(final JsonReader in)
throws IOException {
final T value = delegateTypeAdapter.read(in);
for ( final Map.Entry<Field, ?> e : fieldsToPatch ) {
final Field field = e.getKey();
final Object defaultValue = e.getValue();
try {
if ( field.get(value) == null ) {
field.set(value, defaultValue);
}
} catch ( final IllegalAccessException ex ) {
throw new RuntimeException(ex);
}
}
return value;
}
};
}
}
#AutoValue
abstract class AvoTestClass {
abstract Optional<String> sval();
static AvoTestClass create(final Optional<String> sval) {
return new AutoValue_AvoTestClass(sval);
}
static Class<? extends AvoTestClass> type() {
return AutoValue_AvoTestClass.class;
}
}
public final class OptionalWithAutoValueTest {
private static final Map<Type, Type> autoValueClasses = ImmutableMap.<Type, Type>builder()
.put(AvoTestClass.class, AvoTestClass.type())
.build();
private static final Map<Class<?>, ?> defaultValues = ImmutableMap.<Class<?>, Object>builder()
.put(Optional.class, Optional.empty())
.build();
private static final Gson gson = new GsonBuilder()
.registerTypeAdapterFactory(new GsonJava8TypeAdapterFactory())
.registerTypeAdapterFactory(SubstitutionTypeAdapterFactory.create(autoValueClasses::get))
.registerTypeAdapterFactory(DefaultsTypeAdapterFactory.create(autoValueClasses::get, type -> {
if ( type instanceof Class ) {
return defaultValues.get(type);
}
if ( type instanceof ParameterizedType ) {
return defaultValues.get(((ParameterizedType) type).getRawType());
}
return null;
}))
.create();
#SuppressWarnings("unused")
private static Stream<Optional<String>> test() {
return Stream.of(
Optional.of("ok"),
Optional.empty()
);
}
#ParameterizedTest
#MethodSource
public void test(final Optional<String> optional) {
final AvoTestClass before = AvoTestClass.create(optional);
final String json = gson.toJson(before, AvoTestClass.class);
final AvoTestClass after = gson.fromJson(json, AvoTestClass.class);
Assert.assertEquals(before, after);
}
}
This solution is reflection-based heavily, but it's just a work-around if the generators cannot do the job (again, not sure if they can be configured so that there are no such issues).
I need to consume a REST API and I'm using Gson, which would be great if some dozens of my model classes wouldn't require a custom Gson deserializer.
I think that I should use a custom TypeAdapterFactory but the documentation is poor and I'm having an hard time.
The classes I'm interested follow more or less this pattern:
public class APIResource {
#SerializedName("id")
private Integer id;
//Constructor and getter
}
public class B extends APIResource {
#SerializedName("field")
String field;
#SerializedName("resources")
List<APIResource> resourceList;
//Constructor and getter
}
public class C extends B {
#SerializedName("other_fields")
List<Object> otherFieldList;
#SerializedName("resource")
APIResource resource;
#SerializedName("b_list")
List<B> bList;
//Constructor and getter
}
Some times the id is contained in the JSON as a string named "url" that I have to parse.
The JSONs are quite complex, containing several objects and arrays and their structure is almost aleatory.
The "url" name could be anywhere in the JSON and I can't get it to work using beginObject() and beginArray()
I think my custom TypeAdapterFactory should be something like this
public class ResourceTypeAdapterFactory implements TypeAdapterFactory {
#Override
public <T> TypeAdapter<T> create(Gson gson, #NonNull TypeToken<T> type) {
if (!APIResource.class.isAssignableFrom(type.getRawType())) {
return null;
}
TypeAdapter<T> defaultTypeAdapter = gson.getDelegateAdapter(this, type);
return new TypeAdapter<T>() {
#Override
public void write(JsonWriter out, T value) throws IOException {
defaultTypeAdapter.write(out, value);
}
#Override
public T read(JsonReader in) throws IOException {
//if the name is "url" use the urlToId method, else
return defaultTypeAdapter.read(in);
}
}.nullSafe();
}
Integer urlToId(String url) {
Matcher matcher = Pattern
.compile("/-?[0-9]+/$")
.matcher(url);
return matcher.find() ?
Integer.valueOf(matcher.group().replace("/","")):
null;
}
}
I solved it, if someone encounted the same problem this is my solution
public class ResourceTypeAdapterFactory implements TypeAdapterFactory {
#Override
public <T> TypeAdapter<T> create(Gson gson, #NonNull TypeToken<T> type) {
if (!APIResource.class.isAssignableFrom(type.getRawType())) {
return null;
}
final TypeAdapter<T> delegateAdapter = gson.getDelegateAdapter(this, type);
final TypeAdapter<JsonElement> elementAdapter = gson.getAdapter(JsonElement.class);
return new TypeAdapter<T>() {
#Override
public void write(JsonWriter out, T value) throws IOException {
delegateAdapter.write(out, value);
}
#Override
public T read(JsonReader in) throws IOException {
JsonElement tree = elementAdapter.read(in);
afterRead(tree);
return delegateAdapter.fromJsonTree(tree);
}
protected void afterRead(#NonNull JsonElement jsonElement) {
if(jsonElement instanceof JsonObject) {
JsonObject jsonObject = ((JsonObject)jsonElement);
for(Map.Entry<String,JsonElement> entry : jsonObject.entrySet()){
if(entry.getValue() instanceof JsonPrimitive) {
if(entry.getKey().equalsIgnoreCase("url")) {
String val = jsonObject.get(entry.getKey()).toString();
jsonObject.addProperty("id", urlToId(val));
}
} else {
afterRead(entry.getValue());
}
}
}
}
}.nullSafe();
}
Integer urlToId(#NonNull String url) {
Matcher matcher = Pattern
.compile("/-?[0-9]+/$")
.matcher(url.replace("\"", ""));
return matcher.find() ?
Integer.valueOf(matcher.group().replace("/","")):
null;
}
}
Trying to deserialize/serialize JSON into Java beans I've created. Really new to Jackson and this endeavor, so bear with me. I have the following:
{
"foo": {
"firstBlock": {
"myValue": 1,
"someBool": true,
"stringValue": "OK"
},
"anotherBlock": {
"values": [
{
"yikes01": 42
},
{
"yikes02": 215
}
],
"myInt": 64,
"logging": "Yes"
}
}
}
My Java beans are broken down into several as the objects in the JSON are used repeatedly, so it would be:
#JsonRootName("foo")
public class FooBean {
private FirstBlockBean firstBlock;
private AnotherBlockBean anotherBlock;
#JsonGetter("firstBlock")
public FirstBlockBean getFirstBlock() { return firstBlock; }
#JsonSetter("firstBlock")
public void setFirstBlock(FirstBlockBean firstBlock) { this.firstBlock = firstBlock; }
#JsonGetter("anotherBlock")
public AnotherBlockBean getAnotherBlock() { return anotherBlock; }
#JsonSetter("firstBlock")
public void setAnotherBlock(AnotherBlockBean anotherBlock) { this.anotherBlock = anotherBlock; }
}
#JsonRootName("firstBlock")
public class FirstBlockBean {
private int myValue;
private Boolean someBool;
private String stringValue;
#JsonGetter("myValue")
public int getMyValue() { return myValue; }
#JsonSetter("myValue")
public void setMyValue(int myValue) { this.myValue = myValue; }
#JsonGetter("someBool")
public Boolean getSomeBool() { return someBool; }
#JsonSetter("someBool")
public void setSomeBool(Boolean someBool) { this.someBool = someBool; }
#JsonGetter("stringValue")
public String getStringValue() { return stringValue; }
#JsonSetter("someBool")
public void setStringValue(String stringValue) { this.stringValue = stringValue; }
}
...and AnotherBlockBean class implemented in similar fashion (omitted for brevity.) I'm using Jackson for this, and my question is - is there a mechanism in Jackson for serializing and deserializing for this case? Ideally I'd like something along the lines of (pseudo-code below because I've not been able to surface anything via Google searches or searches on here):
// Assume "node" contains a JsonNode for the tree and foo is an uninitialized FooBean class object.
JsonHelper.deserialize(node, FooBean.class, foo);
At this point I'd be able to read the values back:
int i = foo.getFirstBlock().getMyValue();
System.out.println("i = " + i); // i = 1
Similarly I'd like to be able to take the foo instance and serialize it back into JSON with another method. Am I dreaming for wanting this sort of built-in functionality or does it exist?
The main class when working with Jackson is the ObjectMapper. It has a lot of options, take a look at the available methods.
This is an example of a typical helper class that uses the ObjectMapper to convert between Java objects and Strings.
public class JsonHelper {
private ObjectMapper objectMapper;
public JsonHelper(){
this.objectMapper = new ObjectMapper();
// Your mapping preferences here
this.objectMapper.setPropertyNamingStrategy(PropertyNamingStrategy.LOWER_CAMEL_CASE);
this.objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
this.objectMapper.setSerializationInclusion(Include.NON_NULL);
this.objectMapper.configure(Feature.ALLOW_UNQUOTED_CONTROL_CHARS, true);
}
public String serialize(Object object) {
try {
return this.objectMapper.writeValueAsString(object);
} catch (Exception e) {
// TODO Handle exception
return null;
}
}
public <T> T deserialize(String json, Class<T> clazz) {
try {
return this.objectMapper.readValue(json, clazz);
} catch (Exception e) {
// TODO Handle exception
return null;
}
}
public <T> T deserialize(String json, TypeReference<T> valueTypeRef) {
try {
return this.objectMapper.readValue(json, valueTypeRef);
} catch (Exception e) {
// TODO Handle exception
return null;
}
}
}
Some tips:
If the name of the getter and setter methods follows the usual convention, you can omit the #JsonGetter and #JsonSetter annotations and just use the #JsonProperty annotation in the field declaration
If the name of the java field is equal to the node name in the JSON, you can also omit the #JsonProperty annotation (Jackson will map JSON nodes and Java fields with matching names).
Problem:
I am deserializing enums with Jackson that don't match up with their name in the code, below is a sample of json.
{
"thing1": {"foo": "cool-guy"},
"thing2": {"foo": "loser-face"}
}
Here is the enum, I will explain the interface later.
enum Foo implements HasText {
COOL_GUY("cool-guy"), LOSER_FACE("loser-face"), // etc...
private String text;
private Foo(String text) {
this.text = text;
}
#Override
public String getText() {
return text;
}
}
I know how to solve this issue for each enum individually by making a deserializer (below) and the annotation #JsonDeserialize(using = FooDeserializer .class) on the setter method for foo.
public class FooDeserializer extends JsonDeserializer<Enum<Foo>> {
#Override
public Foo deserialize(JsonParser p, DeserializationContext context) throws Exception {
if (p.getCurrentToken().equals(JsonToken.VALUE_STRING)) {
String jsonText = p.getText();
Stream<Foo> stream = Arrays.asList(Foo.values()).stream();
return stream.filter(a -> a.getText().equals(jsonText.toLowerCase())).findAny().get();
}
throw context.mappingException(Foo.class);
}
}
Question:
Is there a way to do this abstractly? That's why I added the HasText interface to all my enums in hopes there was a way to do something like this:
public class EnumWithTextDeserializer<T extends Enum<T> & HasText> extends JsonDeserializer<T> {
#Override
public T deserialize(JsonParser p, DeserializationContext context) throws Exception {
if (p.getCurrentToken().equals(JsonToken.VALUE_STRING)) {
final String jsonText = p.getText();
final Stream<T> stream = Arrays.asList(runtimeClass().getEnumConstants()).stream();
return stream.filter(a -> a.getText().equals(jsonText.toLowerCase())).findAny().get();
}
throw context.mappingException(runtimeClass());
}
#SuppressWarnings("unchecked")
private Class<T> runtimeClass() {
ParameterizedType superclass = (ParameterizedType) getClass().getGenericSuperclass();
return (Class<T>) superclass.getActualTypeArguments()[0];
}
}
The compile won't let me annotate the setter method (#JsonDeserialize(using = EnumWithTextDeserializer.class)) with this class though because
Type mismatch: cannot convert from Class<EnumWithTextDeserializer> to Class<? extends JsonDeserializer<?>>".
Really, all I want to be able to do is deserialize these enums based on the getText() method.
In order to deserialize, you can specify your String value using #JsonValue.
public enum FooEnum implements WithText {
AWESOME("awesome-rad"),
NARLY("totally-narly");
private final String text;
FooEnum(String text) {
this.text = text;
}
#Override
#JsonValue
public String getText() {
return text;
}
}
Then executing this code to serialize/deserialize
ImmutableMap<String, FooEnum> map = ImmutableMap.of("value", FooEnum.AWESOME, "value2", FooEnum.NARLY);
final String value;
try {
value = objectMapper.writeValueAsString(map);
} catch (JsonProcessingException e) {
throw Throwables.propagate(e);
}
Map<String, FooEnum> read;
try {
read = objectMapper.readValue(value, new TypeReference<Map<String, FooEnum>>() {});
} catch (IOException e) {
throw Throwables.propagate(e);
}
I get:
read = {LinkedHashMap#4627} size = 2
0 = {LinkedHashMap$Entry#4631} "value1" -> "AWESEOME"
1 = {LinkedHashMap$Entry#4632} "value2" -> "NARLY"
I try to marshal an object and I want all the fields to be attributes. The normal fields are OK with the #XStreamAsAttribute annotation but I have two of them with a converter. For them when I marshal they are converted as field...
#XStreamAlias(value="sinistre")
public class ObjetMetierSinistreDto {
#XStreamAlias(value="S_sinistreEtat")
#XStreamAsAttribute
private String etat;
#XStreamAsAttribute
#XStreamAlias(value="S_sinistreDateSurv")
#XStreamConverter(value=JodaDateConverter.class)
private LocalDate dateSurvenanceDossier;
...
The converter:
public class JodaDateConverter implements Converter {
#Override
#SuppressWarnings("unchecked")
public boolean canConvert(final Class type) {
return (type != null) && LocalDate.class.getPackage().equals(type.getPackage());
}
#Override
public void marshal(final Object source, final HierarchicalStreamWriter writer,
final MarshallingContext context) {
writer.setValue(source.toString().replace("-", "/"));
}
#Override
#SuppressWarnings("unchecked")
public Object unmarshal(final HierarchicalStreamReader reader,
final UnmarshallingContext context) {
try {
final Class requiredType = context.getRequiredType();
final Constructor constructor = requiredType.getConstructor(Object.class);
return constructor.newInstance(reader.getValue());
} catch (final Exception e) {
throw new RuntimeException(String.format(
"Exception while deserializing a Joda Time object: %s", context.getRequiredType().getSimpleName()), e);
}
}
}
and the result:
<sinistre S_sinistreEtat="S">
<S_sinistreDateSurv>2015/02/01</S_sinistreDateSurv>
</sinistre>
and what I like:
<sinistre S_sinistreEtat="S"
S_sinistreDateSurv="2015/02/01"/>
I finally found how to solve this problem!
The JodaDateConverter should not implements Converter but extends AbstractSingleValueConverter (as the DateConverter from XStream)
Then you just need to override canConvert() and fromString() and you are good to go!
Exemple:
public class JodaDateConverter extends AbstractSingleValueConverter {
#Override
#SuppressWarnings("unchecked")
public boolean canConvert(final Class type) {
return (type != null) && LocalDate.class.getPackage().equals(type.getPackage());
}
#Override
public Object fromString(String str) {
String separator;
if(str.contains(":")){
separator = ":";
} else if(str.contains("/")){
separator = "/";
} else if(str.contains("-")){
separator = "-";
} else {
throw new RuntimeException("The date must contains ':' or '/' or '-'");
}
String[] date = str.split(separator);
if(date.length < 3){
throw new RuntimeException("The date must contains hour, minute and second");
}
return new LocalDate(Integer.valueOf(date[0]),Integer.valueOf(date[1]),Integer.valueOf(date[2]));
}
}