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

Change date/time format to ISO8601 on JSON serialization #548

Merged
merged 7 commits into from
Jul 31, 2013
Merged
Show file tree
Hide file tree
Changes from 4 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
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,17 @@

import java.util.List;

import javax.servlet.ServletContext;

import br.com.caelum.vraptor.config.BasicConfiguration;
import br.com.caelum.vraptor.ioc.Component;
import br.com.caelum.vraptor.serialization.gson.adapters.HibernateProxySerializer;

import com.google.common.collect.Lists;
import com.google.gson.JsonSerializer;

@Component
@SuppressWarnings("rawtypes")
public class DefaultJsonSerializers implements JsonSerializers {

private static boolean isHibernateProxyPresent;
Expand All @@ -22,16 +26,19 @@ public class DefaultJsonSerializers implements JsonSerializers {
}
private List<JsonSerializer> serializers;

@SuppressWarnings("rawtypes")
public DefaultJsonSerializers(List<JsonSerializer> serializers) {
public DefaultJsonSerializers(List<JsonSerializer> serializers, ServletContext servletContext) {
this.serializers = Lists.newArrayList(serializers);
if (isHibernateProxyPresent) {
if (isHibernateProxyPresent)
this.serializers.add(new HibernateProxySerializer());

String packagesParam = servletContext.getInitParameter(BasicConfiguration.BASE_PACKAGES_PARAMETER_NAME);
if ((packagesParam != null) && (packagesParam.contains("br.com.caelum.vraptor.serialization.gson.adapters.iso8601"))) {
this.serializers.add(new br.com.caelum.vraptor.serialization.gson.adapters.iso8601.CalendarSerializer());
this.serializers.add(new br.com.caelum.vraptor.serialization.gson.adapters.iso8601.DateSerializer());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be unnecessary. If these serializers are @Components, it should work without these 4 lines.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But doesn´t work without this code, and I don´t know what I can do.
If exists classes @Component adapters.CalendarSerializer and @Component adapters.iso8601.CalendarSerializer, the provider inject in List serializers two instances of @Component adapters.CalendarSerializer and no instance of @Component adapters.iso8601.CalendarSerializer regardless of the web.xml package configuration being configured.
Feel free to call me on the talk if necessary

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see your point =/

Maybe we could include and document a context-param which enables the iso8601 strategy, while using the adapters.CalendarSerializer...

}
}

public List<JsonSerializer> getSerializers() {
return serializers;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package br.com.caelum.vraptor.serialization.gson.adapters.iso8601;

import java.lang.reflect.Type;
import java.text.ParseException;
import java.util.Calendar;

import br.com.caelum.vraptor.converter.ConversionError;
import br.com.caelum.vraptor.util.ISO8601Util;

import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonParseException;

public class CalendarDeserializer implements JsonDeserializer<Calendar> {

public Calendar deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {

try {
String value = json.getAsString();

Calendar calendar = ISO8601Util.toCalendar(value);

return calendar;
} catch (ParseException e) {
throw new ConversionError("Error to convert Calendar: " + e.getMessage());
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package br.com.caelum.vraptor.serialization.gson.adapters.iso8601;

import java.lang.reflect.Type;
import java.util.Calendar;

import br.com.caelum.vraptor.util.ISO8601Util;

import com.google.gson.JsonElement;
import com.google.gson.JsonPrimitive;
import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer;

public class CalendarSerializer implements JsonSerializer<Calendar> {

public JsonElement serialize(Calendar calendar, Type typeOfSrc, JsonSerializationContext context) {

String json = ISO8601Util.fromCalendar(calendar);

return new JsonPrimitive(json);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package br.com.caelum.vraptor.serialization.gson.adapters.iso8601;

import java.lang.reflect.Type;
import java.text.ParseException;
import java.util.Date;

import br.com.caelum.vraptor.converter.ConversionError;
import br.com.caelum.vraptor.util.ISO8601Util;

import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonParseException;

public class DateDeserializer implements JsonDeserializer<Date> {

public Date deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {

try {
String value = json.getAsString();

Date date = ISO8601Util.toDate(value);

return date;
} catch (ParseException e) {
throw new ConversionError("Error to convert Date: " + e.getMessage());
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package br.com.caelum.vraptor.serialization.gson.adapters.iso8601;

import java.lang.reflect.Type;
import java.util.Date;

import br.com.caelum.vraptor.util.ISO8601Util;

import com.google.gson.JsonElement;
import com.google.gson.JsonPrimitive;
import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer;

public class DateSerializer implements JsonSerializer<Date> {

public JsonElement serialize(Date date, Type typeOfSrc, JsonSerializationContext context) {

String json = ISO8601Util.fromDate(date);

return new JsonPrimitive(json);
}
}
108 changes: 108 additions & 0 deletions vraptor-core/src/main/java/br/com/caelum/vraptor/util/ISO8601Util.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@

/***
* Copyright (c) 2009 Caelum - www.caelum.com.br/opensource
* All rights reserved.
*
* 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 br.com.caelum.vraptor.util;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.TimeZone;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.xml.datatype.DatatypeConfigurationException;

/**
* Helper class for handling ISO8601 strings of the following format:
* "1982-06-10T05:00:00.000-03:00". It also supports parsing the "Z" timezone.
*
* @author Rafael Dipold
*/
public final class ISO8601Util {

/** Default Extended Format */
private static final String DEFAULT_ISO8601_FORMAT = "yyyy-MM-dd'T'HH:mm:ss.SSSZ";

private static final String REGEX_ISO8601 = "^(\\d{4})-?(\\d\\d)-?(\\d\\d)(?:T(\\d\\d)(?::?(\\d\\d)(?::?(\\d\\d)(?:\\.(\\d+))?)?)?(Z|([+-])(\\d\\d):?(\\d\\d)?)?)?$";
//1 2 3 4 5 6 7 8 9 10 11

/** SimpleDateFormat is not thread-safe, so give one to each thread */
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not create a new one every time?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd replace this thread local with a method which returns a configured SimpleDateFormat.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I make this way because the SimpleDateFormat is not thread-safe
If I follow your suggestion, and if there are many requests at same time, can not be dangerous?
More details: http://blog.vityuk.com/2011/03/java-formatters-best-practices.html

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you create a new SimpleDateFormat every time, you won't have this problem. So, what you really should do is transform this ISO8601Util into a @Component (request-scoped) and change ThreadLocal<SimpleDateFormat> formatter to

SimpleDateFormat formatter = new SimpleDateFormat(DEFAULT_ISO8601_FORMAT);

And leave the thread control to the DI container.

private static final ThreadLocal<SimpleDateFormat> formatter = new ThreadLocal<SimpleDateFormat>() {
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat(DEFAULT_ISO8601_FORMAT);
}
};

/** Transform Calendar to ISO8601 string. */
public static String fromCalendar(final Calendar calendar) {
formatter.get().setTimeZone(calendar.getTimeZone());
return fromDate(calendar.getTime());
}

/** Transform java.util.Date to ISO8601 string. */
public static String fromDate(final Date date) {
String formatted = formatter.get().format(date);
formatted = formatted.replaceAll("[+-]00:?00$", "Z");
return formatted;
}

/** Get current date and time formatted as ISO8601 string. */
public static String now() {
return fromCalendar(GregorianCalendar.getInstance());
}

/** Transform ISO8601 string to Calendar
* @throws DatatypeConfigurationException */
public static Calendar toCalendar(final String iso8601String) throws ParseException {
Pattern pattern = Pattern.compile(REGEX_ISO8601);
Matcher matcher = pattern.matcher(iso8601String);

if (matcher.matches()) {
int year = matcher.group(1) != null ? Integer.valueOf(matcher.group(1)) : 0;
int month = matcher.group(2) != null ? Integer.valueOf(matcher.group(2)) - 1 : 0;
int day = matcher.group(3) != null ? Integer.valueOf(matcher.group(3)) : 0;

int h = (matcher.group(4) != null ? Integer.valueOf(matcher.group(4)) : 0);
int m = (matcher.group(5) != null ? Integer.valueOf(matcher.group(5)) : 0);
int s = (matcher.group(6) != null ? Integer.valueOf(matcher.group(6)) : 0);
int ms = Math.round(Float.parseFloat("0." + (matcher.group(7) != null ? matcher.group(7) : "0")) * 1000);

TimeZone timeZone = TimeZone.getTimeZone("GMT" + (matcher.group(8) != null ? matcher.group(8) : ""));

Calendar calendar = GregorianCalendar.getInstance(timeZone);
calendar.set(Calendar.YEAR, year);
calendar.set(Calendar.MONTH, month);
calendar.set(Calendar.DAY_OF_MONTH, day);
calendar.set(Calendar.HOUR_OF_DAY, h);
calendar.set(Calendar.MINUTE, m);
calendar.set(Calendar.SECOND, s);
calendar.set(Calendar.MILLISECOND, ms);

return calendar;
}
else
throw new java.text.ParseException("Unparseable ISO8601 date format: " + iso8601String, 0);
}

/** Transform ISO8601 string to java.util.Date */
public static Date toDate(final String iso8601String) throws ParseException {
Calendar calendar = toCalendar(iso8601String);
return calendar.getTime();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,17 @@
import java.util.Collections;
import java.util.GregorianCalendar;
import java.util.List;
import java.util.TimeZone;

import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletResponse;

import org.hibernate.proxy.HibernateProxy;
import org.hibernate.proxy.LazyInitializer;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.mockito.Mock;

import br.com.caelum.vraptor.interceptor.DefaultTypeNameExtractor;
import br.com.caelum.vraptor.serialization.HibernateProxyInitializer;
Expand All @@ -49,19 +52,22 @@ public class GsonJSONSerializationTest {
private DefaultTypeNameExtractor extractor;

private HibernateProxyInitializer initializer;

private ServletContext context;

@Before
@SuppressWarnings("rawtypes")
public void setup() throws Exception {
this.stream = new ByteArrayOutputStream();

response = mock(HttpServletResponse.class);
context = mock(ServletContext.class);
when(response.getWriter()).thenReturn(new PrintWriter(stream));
extractor = new DefaultTypeNameExtractor();
initializer = new HibernateProxyInitializer();

this.serialization = new GsonJSONSerialization(response, extractor, initializer,
new DefaultJsonSerializers(Collections.<JsonSerializer> emptyList()));
new DefaultJsonSerializers(Collections.<JsonSerializer> emptyList(), context));
}

public static class Address {
Expand Down Expand Up @@ -456,7 +462,7 @@ public void shouldUseCollectionConverterWhenItExists() {
List<JsonSerializer> adapters = new ArrayList<JsonSerializer>();
adapters.add(new CollectionSerializer());

GsonJSONSerialization serialization = new GsonJSONSerialization(response, extractor, initializer, new DefaultJsonSerializers(adapters));
GsonJSONSerialization serialization = new GsonJSONSerialization(response, extractor, initializer, new DefaultJsonSerializers(adapters, context));

serialization.withoutRoot().from(new MyCollection()).serialize();
assertThat(result(), is(equalTo(expectedResult)));
Expand All @@ -468,7 +474,7 @@ public void shouldSerializeCalendarLikeXstream() {
List<JsonSerializer> adapters = new ArrayList<JsonSerializer>();
adapters.add(new CalendarSerializer());

GsonJSONSerialization serialization = new GsonJSONSerialization(response, extractor, initializer, new DefaultJsonSerializers(adapters));
GsonJSONSerialization serialization = new GsonJSONSerialization(response, extractor, initializer, new DefaultJsonSerializers(adapters, context));

Client c = new Client("renan");
c.included = new GregorianCalendar(2012, 8, 3);
Expand All @@ -483,6 +489,26 @@ public void shouldSerializeCalendarLikeXstream() {
assertThat(result, is(equalTo(expectedResult)));
}

@Test
@SuppressWarnings("rawtypes")
public void shouldSerializeCalendarLikeISO8601() {
List<JsonSerializer> adapters = new ArrayList<JsonSerializer>();
adapters.add(new br.com.caelum.vraptor.serialization.gson.adapters.iso8601.CalendarSerializer());

GsonJSONSerialization serialization = new GsonJSONSerialization(response, extractor, initializer, new DefaultJsonSerializers(adapters, context));

Client c = new Client("Rafael");
c.included = new GregorianCalendar(2013, 6, 27, 9, 52, 38);
c.included.setTimeZone(TimeZone.getTimeZone("America/Sao_Paulo"));

serialization.from(c).serialize();
String result = result();

String expectedResult = "{\"client\":{\"name\":\"Rafael\",\"included\":\"2013-07-27T09:52:38.000-0300\"}}";

assertThat(result, is(equalTo(expectedResult)));
}

@Test
public void shouldExcludeAllPrimitiveFields() {
String expectedResult = "{\"order\":{}}";
Expand Down
Loading