I'm learning the basics of angular with a simple todo app. I have a simple spring boot backend which works fine. I'm currently struggling the best way to pass the date from a angular bootstrap datepicker to the backend. As the code is now the datatypes do not match.
Is the ideal way to convert it to seconds and convert the seconds back to a java date on the backend?
My ToDo entity:
#Id
#GeneratedValue
private long id;
private #NonNull
String taskName;
private Date dueDate;
private String extraNote;
private boolean taskCompleted;
Where I get the user input when he creates a new todo:
#Input() toDoData = { taskName: '', taskCompleted: false, extraNote: '', dueDate: Date};
addToDo() {
this.todoService.addToDo(this.toDoData).subscribe((result) => {
this.todoService.addToDo(this.toDoData);
});
}
Add todo part of my todoService:
addToDo(todo): Observable<any> {
console.log(todo);
return this.http.post<any>(this.API + 'todos', JSON.stringify(todo), this.httpOptions).pipe(
tap((todo) => console.log(`added todo w/ id=${todo.id}`)),
catchError(this.handleError<any>('addTodo'))
);
}
Thanks for any help!
EDIT (Added ToDoController):
#RestController
public class ToDoController {
private ToDoRepository repository;
public ToDoController(ToDoRepository repository) {
this.repository = repository;
}
#GetMapping("/todos")
List<ToDo> all() {
return repository.findAll();
}
#PostMapping("/todos")
ToDo newToDo(#RequestBody ToDo newToDo) {
return repository.save(newToDo);
}
#GetMapping("/todos/{id}")
ToDo one(#PathVariable Long id) {
return repository.findById(id)
.orElseThrow(() -> new ToDoNotFoundException(id));
}
#PutMapping("/todos/{id}")
ToDo replaceToDo(#RequestBody ToDo newToDo, #PathVariable Long id) {
return repository.findById(id)
.map(toDo -> {
toDo.setTaskName(newToDo.getTaskName());
toDo.setDueDate(newToDo.getDueDate());
toDo.setExtraNote(newToDo.getExtraNote());
toDo.setTaskCompleted(newToDo.getTaskCompleted());
return repository.save(toDo);
})
.orElseGet(() -> {
newToDo.setId(id);
return repository.save(newToDo);
});
}
#DeleteMapping("/todos/{id}")
void deleteToDo(#PathVariable Long id) {
repository.deleteById(id);
}
#GetMapping("/deleteall")
#CrossOrigin(origins = "http://localhost:4200")
public void deleteAll() {
repository.deleteAll();
}
#GetMapping("/init")
#CrossOrigin(origins = "http://localhots:4200")
public void createDefaults() {
Date date = new Date();
repository.save(new ToDo("PMB", date, false));
repository.save(new ToDo("GMDU", date, false));
repository.save(new ToDo("INMA", date, true));
repository.save(new ToDo("SLGP", date, false));
}
}
First of all. When using dates you should take into account summer/winter time issues and therefore I would suggest to use a LocalDate(Time) class.
That said:
I would create a
#Configuration
public class JacksonConfig {
#Bean
#Primary
public ObjectMapper serializingObjectMapper() {
ObjectMapper objectMapper = new ObjectMapper();
// Mirror the default Spring Boot Jackson settings
objectMapper.configure(MapperFeature.DEFAULT_VIEW_INCLUSION, false);
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
JavaTimeModule javaTimeModule = new JavaTimeModule();
javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateSerializer());
javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateDeserializer());
objectMapper.registerModule(javaTimeModule);
return objectMapper;
}
public static class LocalDateSerializer extends JsonSerializer<LocalDateTime> {
#Override
public void serialize(LocalDateTime value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
gen.writeNumber(value.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli());
}
}
public static class LocalDateDeserializer extends JsonDeserializer<LocalDateTime> {
#Override
public LocalDateTime deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
return LocalDateTime.ofInstant(Instant.ofEpochMilli(p.getValueAsLong()), ZoneId.systemDefault());
}
}
}
that converts all dates/times etc to milliseconds and from milliseconds back to localDate(Time). You can find plenty examples on the internet.
This acts as a filter for all dates/times that pass the endpoints in your application.
That way you do not need to concern yourself with any conversion issues anymore.
Then you will need to implement a dateToMillisecond and visa versa routine in Angular and use it where you need to convert te dates. You could intercept http traffic and do the same, but that might be a bit more complicated to implement.
Sample
function timeFromMillis(millis) {
if (_.isEmpty(millis)) {
return undefined;
}
const momentTimeStamp = moment.unix(millis / 1000);
if (!momentTimeStamp.isValid()) {
return undefined;
}
return momentTimeStamp.toDate();
}
function timeToMillis(obj) {
if (!_.isEmpty(obj)) {
if (obj instanceof moment) {
return obj.valueOf();
} else if (obj instanceof Date) {
return obj.getTime();
else if (angular.isString(obj)) {
return parseDateString(obj).getTime();
} else {
return angular.undefined;
}
}
}
function parseDateString(dateString) {
if (angular.isDefined(dateString) && dateString) {
return moment(dateString, 'YYYY-MM-DD').toDate();
}
}
you have 2 options using timeStamp as long pasing long from angular to beckend and from back to angular This is the way i prefer because timestamp is unique
Or you can use custom serializer deserializer
public class CustomDateTimeSerializer extends StdSerializer<DateTime> {
private static DateTimeFormatter formatter =
DateTimeFormat.forPattern("yyyy-MM-dd HH:mm");
public CustomDateTimeSerializer() {
this(null);
}
protected CustomDateTimeSerializer(Class<DateTime> t) {
super(t);
}
#Override
public void serialize(DateTime value, JsonGenerator gen, SerializerProvider serializerProvider) throws IOException {
gen.writeString(formatter.print(value));
}
public class CustomDateTimeDeserializer extends JsonDeserializer<DateTime> {
#Override
public DateTime deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException {
DateTimeFormatter formatter = DateTimeFormat.forPattern("yyyy-MM-dd HH:mm");
DateTime dateTime = formatter.parseDateTime(jsonParser.getText());
return dateTime;
}
}
ps: i have used yoda time in serializer and deserializer
I ran into this and it turned out to be timezone issue. From frontend it was sending json with "2019-04-01" but backend was converting it to "2019-03-31"
A lot of the code is not using java.time so I found adding below to application.properties file to be best solution. I also added global date format, but change timezone to what you need.
spring.jackson.date-format=yyyy-MM-dd
spring.jackson.time-zone=America/Chicago
Related
In a Spring Boot applicaion with AngularJS frontend, a "Pin" field value has to be blackened on serialization, i.e., if the Pin field value is null in the POJO, the according JSON field has to remain blank; if the field value contains data, it has to be replaced with a "***" string.
Does Jackson provide a feature to get this done?
You can do it easily like following without any Custom Serializer
public class Pojo {
#JsonIgnore
private String pin;
#JsonProperty("pin")
public String getPin() {
if(pin == null) {
return "";
} else {
return "***";
}
}
#JsonProperty("pin")
public void setPin(String pin) {
this.pin = pin;
}
#JsonIgnore
public String getPinValue() {
return pin;
}
}
You can use Pojo.getPinValue() to get the exact value.
Try the following example.
public class Card {
public int id;
public String pin;
}
public class CardSerializer extends StdSerializer<Card> {
public CardSerializer() {
this(null);
}
public CardSerializer(Class<Card> t) {
super(t);
}
#Override
public void serialize(Card value, JsonGenerator jgen, SerializerProvider provider)
throws IOException, JsonProcessingException {
jgen.writeStartObject();
jgen.writeNumberField("id", value.id);
jgen.writeStringField("pin", "****");
jgen.writeEndObject();
}
}
Then you need to register your customer serializer with the ObjectMapper
Card card = new Card(1, "12345");
ObjectMapper mapper = new ObjectMapper();
SimpleModule module = new SimpleModule();
module.addSerializer(Card.class, new CardSerializer());
mapper.registerModule(module);
String serialized = mapper.writeValueAsString(card);
There are some improvements you can do here like registering the serializer directly on the class, but you can read more about it here Section 4 - http://www.baeldung.com/jackson-custom-serialization
We have a service which currently consumes JSON. We want to slightly restructure this JSON (move one property one level up) but also implement graceful migration so that our service could process old structure as well as new structure. We're using Jackson for JSON deserialization.
How do we restructure JSON prior to deserialization with Jackson?
Here's a MCVE.
Assume our old JSON looks as follows:
{"reference": {"number" : "one", "startDate" : [2016, 11, 16], "serviceId" : "0815"}}
We want to move serviceId one level up:
{"reference": {"number" : "one", "startDate" : [2016, 11, 16]}, "serviceId" : "0815"}
This are the classes we want to deserialize from both old an new JSONs:
public final static class Container {
public final Reference reference;
public final String serviceId;
#JsonCreator
public Container(#JsonProperty("reference") Reference reference, #JsonProperty("serviceId") String serviceId) {
this.reference = reference;
this.serviceId = serviceId;
}
}
public final static class Reference {
public final String number;
public final LocalDate startDate;
#JsonCreator
public Reference(#JsonProperty("number") String number, #JsonProperty("startDate") LocalDate startDate) {
this.number = number;
this.startDate = startDate;
}
}
We only want serviceId in Container, not in both classes.
What I've got working is the following deserializer:
public static class ServiceIdMigratingContainerDeserializer extends JsonDeserializer<Container> {
private final ObjectMapper objectMapper;
{
objectMapper = new ObjectMapper();
objectMapper.registerModule(new JavaTimeModule());
objectMapper.configure(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_AS_NULL, true);
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
}
#Override
public Container deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
ObjectNode node = p.readValueAsTree();
migrate(node);
return objectMapper.treeToValue(node, Container.class);
}
private void migrate(ObjectNode containerNode) {
TreeNode referenceNode = containerNode.get("reference");
if (referenceNode != null && referenceNode.isObject()) {
TreeNode serviceIdNode = containerNode.get("serviceId");
if (serviceIdNode == null) {
TreeNode referenceServiceIdNode = referenceNode.get("serviceId");
if (referenceServiceIdNode != null && referenceServiceIdNode.isValueNode()) {
containerNode.set("serviceId", (ValueNode) referenceServiceIdNode);
}
}
}
}
}
This deserializer first retrieves the tree, manipulates it and then deserializers it using an own instance of ObjectMapper. It works but we really dislike the fact that we have another instance of ObjectMapper here. If we don't create it and somehow use the system-wide instance of ObjectMapper we get an infinite cycle because when we try to call objectMapper.treeToValue, our deserializer gets called recursively. So this works (with an own instance of ObjectMapper) but it is not an optimal solution.
Another method I've tried was using a BeanDeserializerModifier and a own JsonDeserializer which "wraps" the default serializer:
public static class ServiceIdMigrationBeanDeserializerModifier extends BeanDeserializerModifier {
#Override
public JsonDeserializer<?> modifyDeserializer(DeserializationConfig config, BeanDescription beanDesc,
JsonDeserializer<?> defaultDeserializer) {
if (beanDesc.getBeanClass() == Container.class) {
return new ModifiedServiceIdMigratingContainerDeserializer((JsonDeserializer<Container>) defaultDeserializer);
} else {
return defaultDeserializer;
}
}
}
public static class ModifiedServiceIdMigratingContainerDeserializer extends JsonDeserializer<Container> {
private final JsonDeserializer<Container> defaultDeserializer;
public ModifiedServiceIdMigratingContainerDeserializer(JsonDeserializer<Container> defaultDeserializer) {
this.defaultDeserializer = defaultDeserializer;
}
#Override
public Container deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
ObjectNode node = p.readValueAsTree();
migrate(node);
return defaultDeserializer.deserialize(new TreeTraversingParser(node, p.getCodec()), ctxt);
}
private void migrate(ObjectNode containerNode) {
TreeNode referenceNode = containerNode.get("reference");
if (referenceNode != null && referenceNode.isObject()) {
TreeNode serviceIdNode = containerNode.get("serviceId");
if (serviceIdNode == null) {
TreeNode referenceServiceIdNode = referenceNode.get("serviceId");
if (referenceServiceIdNode != null && referenceServiceIdNode.isValueNode()) {
containerNode.set("serviceId", (ValueNode) referenceServiceIdNode);
}
}
}
}
}
"Wrapping" a default deserializer seems to be a better approach, but this fails with an NPE:
java.lang.NullPointerException
at com.fasterxml.jackson.databind.deser.BeanDeserializer._deserializeOther(BeanDeserializer.java:157)
at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:150)
at de.db.vz.rikernpushadapter.migration.ServiceIdMigrationTest$ModifiedServiceIdMigratingContainerDeserializer.deserialize(ServiceIdMigrationTest.java:235)
at de.db.vz.rikernpushadapter.migration.ServiceIdMigrationTest$ModifiedServiceIdMigratingContainerDeserializer.deserialize(ServiceIdMigrationTest.java:1)
at com.fasterxml.jackson.databind.ObjectReader._bindAndClose(ObjectReader.java:1623)
at com.fasterxml.jackson.databind.ObjectReader.readValue(ObjectReader.java:1217)
at ...
The whole MCVE code is in the following PasteBin. It is a single-class all-containing test case which demonstrates both approaches. The migratesViaDeserializerModifierAndUnmarshalsServiceId fails.
So this leaves me with a question:
How do we restructure JSON prior to deserialization with Jackson?
In the best traditions, right after posting the question, I've managed to solve this.
Two things:
I had to do newJsonParser.nextToken(); to avoid NPE.
Extend DelegatingDeserializer
Here's a working DelegatingDeserializer:
public static class ModifiedServiceIdMigratingContainerDeserializer
extends DelegatingDeserializer {
public ModifiedServiceIdMigratingContainerDeserializer(JsonDeserializer<?> defaultDeserializer) {
super(defaultDeserializer);
}
#Override
protected JsonDeserializer<?> newDelegatingInstance(JsonDeserializer<?> newDelegatee) {
return new ModifiedServiceIdMigratingContainerDeserializer(newDelegatee);
}
#Override
public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
return super.deserialize(restructure(p), ctxt);
}
#Override
public Object deserialize(JsonParser p, DeserializationContext ctxt, Object intoValue) throws IOException,
JsonProcessingException {
return super.deserialize(restructure(p), ctxt, intoValue);
}
public Object deserializeWithType(JsonParser jp, DeserializationContext ctxt, TypeDeserializer typeDeserializer)
throws IOException, JsonProcessingException {
return super.deserializeWithType(restructure(jp), ctxt, typeDeserializer);
}
public JsonParser restructure(JsonParser p) throws IOException, JsonParseException {
final ObjectNode node = p.readValueAsTree();
migrate(node);
final TreeTraversingParser newJsonParser = new TreeTraversingParser(node, p.getCodec());
newJsonParser.nextToken();
return newJsonParser;
}
private void migrate(ObjectNode containerNode) {
TreeNode referenceNode = containerNode.get("reference");
if (referenceNode != null && referenceNode.isObject()) {
TreeNode serviceIdNode = containerNode.get("serviceId");
if (serviceIdNode == null) {
TreeNode referenceServiceIdNode = referenceNode.get("serviceId");
if (referenceServiceIdNode != null && referenceServiceIdNode.isValueNode()) {
containerNode.set("serviceId", (ValueNode) referenceServiceIdNode);
}
}
}
}
}
I have mapped a custom deserializer to convert Strings on dd/MM/yyyy pattern to LocalDate so I can call my services with a more readable signature..
This is my dto class that is used as a Jersey #BeanParam to transport data between layers:
public class ProdutoFilterDto implements FilterDto {
private static final long serialVersionUID = -4998167328470565406L;
#QueryParam("dataInicial")
#JsonDeserialize(using = CustomLocalDateDeserializer.class)
private LocalDate dataInicial;
#QueryParam("dataInicial")
#JsonDeserialize(using = CustomLocalDateDeserializer.class)
private LocalDate dataFinal;
public LocalDate getDataInicial() {
return dataInicial;
}
public void setDataInicial(LocalDate dataInicial) {
this.dataInicial = dataInicial;
}
public LocalDate getDataFinal() {
return dataFinal;
}
public void setDataFinal(LocalDate dataFinal) {
this.dataFinal = dataFinal;
}
}
and this is my custom deserializer:
public class CustomLocalDateDeserializer extends JsonDeserializer<LocalDate> {
#Override
public LocalDate deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd/MM/yyyy");
final String data = p.getValueAsString();
return (LocalDate) formatter.parse(data);
}
}
Its being used on this jersey service:
#Path("produto")
#Produces(MediaType.APPLICATION_JSON)
#Consumes(MediaType.APPLICATION_JSON)
public class ProdutoService {
...
#GET
#Path("query")
#Override
public Response query(
#QueryParam("offset") #DefaultValue(value = "0") Integer offSet,
#QueryParam("limit") #DefaultValue(value = "10") Integer limit,
#BeanParam ProdutoFilterDto filter) { ... }
...
}
I am calling like this:
${host goes here}/produto/query?dataInicial=11/09/1992
The problem is that the deserializer method is never called and the bean param variable remains null..
MessageBodyReaders aren't used for #QueryParam. You seem to be expecting the Jackson MessageBodyReader to handle this deserialization, but it doesn't work like that.
Instead you will want to use a ParamConverter, which will need to be registered through a ParamConverterProvider. For example:
#Provider
public class LocalDateParamConverterProvider implements ParamConverterProvider {
final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd/MM/yyyy");
#Override
public <T> ParamConverter<T> getConverter(
Class<T> rawType, Type genericType, Annotation[] antns) {
if (LocalDate.class == rawType) {
return new ParamConverter<T>() {
#Override
public T fromString(String string) {
try {
LocalDate localDate = LocalDate.parse(string, formatter);
return rawType.cast(localDate);
} catch (Exception ex) {
throw new BadRequestException(ex);
}
}
#Override
public String toString(T t) {
LocalDate localDate = (LocalDate) t;
return formatter.format(localDate);
}
};
}
return null;
}
}
Now LocalDate will work with #QueryParam and other #XxxParams also.
Some things to note:
If your goal is to parse both your #XxxParams and your JSON body into a bean this will not work. I'm not sure how that would work, but I'm sure it would involve a lot of hacking, and I wouldn't recommend it.
Your cast to (LocalDate) won't work. It's an illegal cast to java.time.format.Parsed. See correct way in code example.
Related to the above point. I was pulling out my hair for a good hour trying to figure out why I was getting a 404, using your parse code. With a 404, the last place I thought to look was in the ParamConverter. But it seems any uncaught exceptions that are thrown in the ParamConverter, will cause a 404. Doesn't make much sense right? The head pounding led me to this, which led me to this, which seems to be a poor specification
"if the field or property is annotated with
#MatrixParam, #QueryParam or #PathParam then an implementation MUST generate an instance of
NotFoundException (404 status) that wraps the thrown exception and no entity
"
Moral of the story: make sure to catch any possible exceptions in the ParamConverter!
See Also:
Good article on ParamConverters
There are many examples of Jackson to/from java.util.Date code but they all seem to leverage POJO annotation. I have generic Maps of scalars that I wish to de/serialize to JSON. This is the current deserializer setup; very simple:
public class JSONUtils {
static {
DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS");
mapper = new ObjectMapper();
mapper.configure(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS, true);
mapper.setDateFormat(df); // this works for outbounds but has no effect on inbounds
mapper.getDeserializationConfig().with(df); // Gave this a shot but still does not sniff strings for a format that we declare should be treated as java.util.Date
}
public static Map<String,Object> parseJSON(InputStream is) {
Map<String,Object> data = null;
try {
data = mapper.readValue(is, Map.class);
} catch(Exception e) {
// ...
}
return data;
}
I grok that a dateserializer can turn java.util.Date into a ISO 8601-ish string. It's going the other way that puzzles me. Clearly, in a JSON doc with no context, a string is a string so I cannot know if it was once a date. So I am prepared to duck type this and examine all strings being deserialized and if they smell like YYYY-MM-DDTHH:MM:SS.sss datetimes, then I will make a java.util.Date instead of just passing back a String. So given:
{ "name": "buzz",
"theDate": "2013-09-10T12:00:00.000"
}
will yield
Map<String,Object> m = mapper.readValue(is, Map.class);
Object o1 = m.get("name"); // o1 is instanceof String
Object o2 = m.get("theDate"); // o2 is instanceof Date
But this means that the deserializer has to return two different types and I have not been able to figure out how to do this in Jackson. Does anyone know of a good, compact example that will sniff for date-like strings and turn them into Dates, leaving others as Strings?
I've been looking for the answer on a related subject recently and come up with the following solution, thanks to Justin Musgrove and his article Custom jackson date deserializer.
Basically, the idea is to replace standard deserializer for Object.class that will convert any string in the specified format to the Date object or fallback to the standard behaviour otherwise. Obviously, this operation comes at cost of extra processing, so you'd want to keep a dedicated instance of ObjectMapper configured for this and only use it when absolutely necessary or if prepared doing second pass anyway.
Note that the Date string format in your example has no timezone component, which may cause some issues, but I leave the format as requested. You can use a parser of your choice in place of the FastDateFormat from Apache Commons Lang. I actually use Instant in my case.
CustomObjectDeserializer.java
import java.io.IOException;
import org.apache.commons.lang3.time.FastDateFormat;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.JsonTokenId;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.deser.std.UntypedObjectDeserializer;
public class CustomObjectDeserializer extends UntypedObjectDeserializer {
private static final long serialVersionUID = 1L;
private static final FastDateFormat format = FastDateFormat.getInstance("yyyy-MM-dd'T'HH:mm:ss.SSS");
public CustomObjectDeserializer() {
super(null, null);
}
#Override
public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
if (p.getCurrentTokenId() == JsonTokenId.ID_STRING) {
try {
String value = p.getText();
// put your own parser here
return format.parse(value);
} catch (Exception e) {
return super.deserialize(p, ctxt);
}
} else {
return super.deserialize(p, ctxt);
}
}
}
JSONUtils.java
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
public class JSONUtils {
private static final ObjectMapper mapper = new ObjectMapper();
static {
mapper.configure(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS, true);
SimpleModule module = new SimpleModule("DateConverter");
// register a new deserializer extending and replacing UntypedObjectDeserializer
module.addDeserializer(Object.class, new CustomObjectDeserializer());
mapper.registerModule(module);
}
public static Map<String, Object> parseJSON(InputStream is) {
Map<String, Object> data = null;
try {
data = mapper.readValue(is, Map.class);
} catch (Exception e) {
// ...
e.printStackTrace();
}
return data;
}
public static void main(String[] args) throws Exception {
String input = "{\"name\": \"buzz\", \"theDate\": \"2013-09-10T12:00:00.000\"}";
InputStream is = new ByteArrayInputStream(input.getBytes(StandardCharsets.UTF_8));
Map<String, Object> m = mapper.readValue(is, Map.class);
Object o1 = m.get("name"); // o1 is instanceof String
Object o2 = m.get("theDate"); // o2 is instanceof Date
System.out.println(o1.getClass().getName() + " : " + o1);
System.out.println(o2.getClass().getName() + " : " + o2);
}
}
If you have a POJO, you can easy use annotation on get and set method with serializer and deserializer.
following an example that serialize and deserialize objects in different ways: List<POJO> to String, String to Map and Map to List<POJO> again. Obviously, in the map the Date values are as String.
This solution is thread safe because uses org.joda.time.format.DateTimeFormat and org.joda.time.format.DateTimeFormatter, you can find more info herein this post How to deserialize JS date using Jackson? and this link http://fahdshariff.blogspot.co.uk/2010/08/dateformat-with-multiple-threads.html
My POJO:
#JsonAutoDetect
public class QueueTask implements Serializable {
private static final long serialVersionUID = -4411796657106403937L;
public enum ActivitiQueueStatus {
IN_PROGRESS(AsyncProcessingWorkflowContentModel.InProgressTask.TYPE.getLocalName()), //
IN_QUEUE(AsyncProcessingWorkflowContentModel.InQueueTask.TYPE.getLocalName());
private String value;
private ActivitiQueueStatus(final String value) {
this.value = value;
}
public static ActivitiQueueStatus enumOf(final String value) {
for (ActivitiQueueStatus enum_i : values()) {
if (enum_i.value.equals(value))
return enum_i;
}
throw new IllegalArgumentException("value '" + value + "' is not a valid enum");
}
}
private String user;
private Date creationDate;
private int noRowsSelected;
private ActivitiQueueStatus status;
public String getUser() {
return user;
}
public void setUser(String user) {
this.user = user;
}
#JsonSerialize(using = JsonDateSerializer.class)
public Date getCreationDate() {
return creationDate;
}
#JsonDeserialize(using = JsonDateDeSerializer.class)
public void setCreationDate(Date creationDate) {
this.creationDate = creationDate;
}
public int getNoRowsSelected() {
return noRowsSelected;
}
public void setNoRowsSelected(int noRowsSelected) {
this.noRowsSelected = noRowsSelected;
}
public ActivitiQueueStatus getStatus() {
return status;
}
public void setStatus(ActivitiQueueStatus status) {
this.status = status;
}
}
My Serializer:
#Component
public class JsonDateDeSerializer extends JsonDeserializer<Date> {
// use joda library for thread safe issue
private static final DateTimeFormatter dateFormat = DateTimeFormat.forPattern("dd/MM/yyyy hh:mm:ss");
#Override
public Date deserialize(final JsonParser jp, final DeserializationContext ctxt) throws IOException, JsonProcessingException {
if (jp.getCurrentToken().equals(JsonToken.VALUE_STRING))
return dateFormat.parseDateTime(jp.getText().toString()).toDate();
return null;
}
}
and Deserializer:
#Component
public class JsonDateSerializer extends JsonSerializer<Date> {
// use joda library for thread safe issue
private static final DateTimeFormatter dateFormat = DateTimeFormat.forPattern("dd/MM/yyyy hh:mm:ss");
#Override
public void serialize(final Date date, final JsonGenerator gen, final SerializerProvider provider) throws IOException, JsonProcessingException {
final String formattedDate = dateFormat.print(date.getTime());
gen.writeString(formattedDate);
}
}
My Service:
public class ServiceMock {
// mock this parameter for usage.
public List<QueueTask> getActiveActivities(QName taskStatus) {
final List<QueueTask> listToReturn = new LinkedList<QueueTask>();
final SimpleDateFormat dateFormat = new SimpleDateFormat("dd/MM/yyyy hh:mm:ss");
Date d1 = null, d2 = null, d3 = null, d4 = null, d5 = null;
try {
d1 = dateFormat.parse("01/02/2013 12:44:44");
d2 = dateFormat.parse("21/12/2013 16:44:44");
d3 = dateFormat.parse("21/12/2013 16:45:44");
d4 = dateFormat.parse("21/12/2013 16:44:46");
d5 = dateFormat.parse("11/09/2013 16:44:44");
} catch (ParseException e) {
}
QueueTask dataSet = new QueueTask();
dataSet = new QueueTask();
dataSet.setUser("user_b");
dataSet.setStatus(ActivitiQueueStatus.enumOf("placeInQueue"));
dataSet.setNoRowsSelected(500);
dataSet.setCreationDate(d1);
listToReturn.add(dataSet);
dataSet = new QueueTask();
dataSet.setUser("user_d");
dataSet.setStatus(ActivitiQueueStatus.enumOf("placeInQueue"));
dataSet.setNoRowsSelected(300);
dataSet.setCreationDate(d2);
listToReturn.add(dataSet);
dataSet = new QueueTask();
dataSet.setUser("user_a");
dataSet.setStatus(ActivitiQueueStatus.enumOf("inProgress"));
dataSet.setNoRowsSelected(700);
dataSet.setCreationDate(d3);
listToReturn.add(dataSet);
dataSet = new QueueTask();
dataSet.setUser("user_k");
dataSet.setStatus(ActivitiQueueStatus.enumOf("inProgress"));
dataSet.setNoRowsSelected(700);
dataSet.setCreationDate(d4);
listToReturn.add(dataSet);
dataSet = new QueueTask();
dataSet.setUser("user_l");
dataSet.setStatus(ActivitiQueueStatus.enumOf("inProgress"));
dataSet.setNoRowsSelected(700);
dataSet.setCreationDate(d5);
listToReturn.add(dataSet);
return listToReturn;
}
}
MAIN usage:
public class SerializationServiceTest {
private static final Logger LOGGER = LoggerFactory.getLogger(OUPQueueStatusServiceIT.class);
public void testGetActiveActivitiesSerialization() throws Exception {
LOGGER.info("testGetActiveActivitiesSerialization - start");
ServiceMock mockedService = new ServiceMock();
// AsyncProcessingWorkflowContentModel.InProgressTask.TYPE is an QName, mock this calling
List<QueueTask> tasks = mockedService.getActiveActivities(AsyncProcessingWorkflowContentModel.InProgressTask.TYPE);
assertNotNull(tasks);
assertTrue(tasks.size() == 5);
assertNotNull(tasks.get(0).getUser());
assertNotNull(tasks.get(0).getCreationDate());
assertNotNull(tasks.get(0).getStatus());
assertNotNull(tasks.get(0).getNoRowsSelected());
final ObjectMapper mapper = new ObjectMapper();
final String jsonString = mapper.writeValueAsString(tasks);
assertNotNull(jsonString);
assertTrue(jsonString.contains("creationDate"));
// test serialization from string to Map
final List<Map<String, Object>> listOfMap = mapper.readValue(jsonString, new TypeReference<List<Map<String, Object>>>() {
});
assertNotNull(listOfMap);
final DateFormat formatter = new SimpleDateFormat("dd/MM/yyyy hh:mm:ss");
for (Map<String, Object> map_i : listOfMap) {
// check date value
assertTrue(map_i.containsKey("creationDate"));
final Date date = formatter.parse("" + map_i.get("creationDate"));
assertNotNull(date);
assertNotNull(map_i.get("user"));
assertNotNull(map_i.get("status"));
assertNotNull(ActivitiQueueStatus.valueOf("" + map_i.get("status")));
assertNotNull(map_i.get("noRowsSelected"));
}
// test de-serialization
List<QueueTask> deserializedTaskList = mapper.convertValue(listOfMap, new TypeReference<List<QueueTask>>() {
});
assertNotNull(deserializedTaskList);
assertTrue(deserializedTaskList.size() == 5);
for (QueueTask t : deserializedTaskList) {
assertNotNull(t.getUser());
assertNotNull(t.getCreationDate());
assertNotNull(t.getDownloadType());
assertNotNull(t.getStatus());
}
LOGGER.info("testGetActiveActivitiesSerialization - end");
}
public static void main(String[] args) throws Exception {
new SerializationServiceTest().SerializationServiceTest();
}
}
After some weeks poking around on this (and no other comments or answers), I now believe what I seek is NOT possible in Jackson. Deserialization of JSON into a Map with ducktyping for dates must occur after-the-fact. There is no way to interpose the parse stream, sniff the string for YYYY-MM-DDTHH:MM:SS.SSS and upon match substitute a Date object instead of String. You must let Jackson build the Map, then outside of Jackson go back to the top and walk the Map, sniffing for dates.
I will add that since I have a very specific duck I am looking for, the fastest implementation to turn the String into a Date is a hand-rolled thing about 120 lines long that validates and sets up the proper integer m-d-y-h-m-s-ms for Calendar then calls getTime(). 10,000,000 conversions takes 4240 millis, or about 2.3m/sec.
Before the joda-time lobby pipes up, yes, I tried that first:
// This is set up ONCE, outside the timing loop:
DateTimeFormatter format = ISODateTimeFormat.dateHourMinuteSecondMillis();
// These are in the timing loop:
while(loop) {
DateTime time = format.parseDateTime("2013-09-09T14:45:00.123");
Date d = time.toDate();
}
takes about 9630 millis to run, about 1.04m/sec; half the speed. But that's still WAY faster than the "out of the box use javax" option:
java.util.Calendar c2 = javax.xml.bind.DatatypeConverter.parseDateTime(s);
Date d = c2.getTime();
This takes 30428 mills to run, about .33m/sec -- almost 7x slower than the handroll.
SimpleDateFormat is not thread-safe so therefore was not considered in for use in converter utility where I cannot make any assumptions about the callers.
Here is a basic example on how to use Jackson to serialize deserialize a date from an object
public class JacksonSetup {
private static class JacksonSerializer {
private static JacksonSerializer instance;
private JacksonSerializer() {
}
public static JacksonSerializer getInstance() {
if (instance == null)
instance = new JacksonSerializer();
return instance;
}
public <E extends ModelObject> void writeTo(E object, Class<E> type, OutputStream out) throws IOException {
ObjectMapper mapper = getMapper();
mapper.writeValue(out, object);
}
public <E extends ModelObject> void writeTo(E object, Class<E> type, Writer out) throws IOException {
ObjectMapper mapper = getMapper();
mapper.writeValue(out, object);
}
public <E extends ModelObject> E read(String input, Class<E> type) throws IOException {
ObjectMapper mapper = getMapper();
E result = (E) mapper.readValue(input, type);
return result;
}
private ObjectMapper getMapper() {
ObjectMapper mapper = new ObjectMapper();
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
AnnotationIntrospector introspector = new JaxbAnnotationIntrospector(mapper.getTypeFactory());
mapper.setAnnotationIntrospector(introspector);
return mapper;
}
}
private static class JaxbDateSerializer extends XmlAdapter<String, Date> {
private SimpleDateFormat dateFormat = new SimpleDateFormat("MM-dd-yyyy");
#Override
public String marshal(Date date) throws Exception {
return dateFormat.format(date);
}
#Override
public Date unmarshal(String date) throws Exception {
return dateFormat.parse(date);
}
}
private static abstract class ModelObject {
}
private static class Person extends ModelObject {
private String name;
private Date bday;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
#XmlElement(name = "birth-day")
#XmlJavaTypeAdapter(JaxbDateSerializer.class)
public Date getBday() {
return bday;
}
public void setBday(Date bday) {
this.bday = bday;
}
}
public static void main(String[] args) {
try {
Person person = new Person();
person.setName("Jhon Doe");
person.setBday(new Date());
Writer writer = new StringWriter();
JacksonSerializer.getInstance().writeTo(person, Person.class, writer);
System.out.println(writer.toString());
} catch (Exception e) {
e.printStackTrace();
}
}
}
I have a model object with an XMLGregorianCalendar field. How can I bind it to an input field?
For string fields I'm using:
#springFormInput("model.object.stringfield" "")
but can't work out the corresponding code for an XMLGregorianCalendar
You may be better off converting the XMLGregorianCalendar to something easier to handle like Calendar or Date before handing it off to the presentation layer.
Here's a solution. It uses jodatime but could probably be changed not to:
For the view (velocity in this case):
#springFormInput("model.object.xmlgregoriancalendar.field" "")
For the controller:
#InitBinder
public void binder(WebDataBinder binder) {
binder.registerCustomEditor(XMLGregorianCalendar.class, new PropertyEditorSupport() {
public void setAsText(String value) {
setValue(createXMLGregorianCalendar(value));
}
public String getAsText() {
return new SimpleDateFormat("dd/MM/yyyy").format(((XMLGregorianCalendar)getValue()).toGregorianCalendar().getTime());
}
});
}
private XMLGregorianCalendar createXMLGregorianCalendar(String date) {
LocalDateTime result = DateTimeFormat.forPattern("dd/MM/yyyy").parseDateTime(date).toLocalDateTime();
return xmlDF().newXMLGregorianCalendar(result.toDateTime().toGregorianCalendar());
}
private static DatatypeFactory xmlDF() {
try {
return DatatypeFactory.newInstance();
} catch (DatatypeConfigurationException ex) {
throw new RuntimeException(ex);
}
}