I have a custom Jackson JSON deserializer that converts a user's local timestamp to UTC:
Jackson Config:
#Configuration
public class JacksonConfiguration {
#Bean
public Module javaTimeModule() {
JavaTimeModule module = new JavaTimeModule();
module.addDeserializer(LocalDateTime.class, new JsonDatetimeDeserializer());
module.addSerializer(LocalDateTime.class, new JsonDatetimeSerializer());
return module;
}
}
Deserializer:
public class JsonDatetimeDeserializer extends JsonDeserializer<LocalDateTime> {
#Override
public LocalDateTime deserialize(JsonParser jsonParser, DeserializationContext deserializationContext)
throws IOException {
String timestamp = jsonParser.getText();
DateTimeFormatConstant dateTimeFormat = findDateTimeFormat(timestamp);
LocalDateTime deserializedTime = null;
if (dateTimeFormat != null) {
switch (dateTimeFormat) {
case ISO -> deserializedTime = localTimeToUtc(timestamp, dateTimeFormat.value());
case ISO_WITH_OFFSET -> deserializedTime = zonedTimeWithOffsetToUtc(timestamp, dateTimeFormat.value());
//Add more cases here as more formats are supported and added to DateTimeFormatConstant
}
}
return deserializedTime;
}
private DateTimeFormatConstant findDateTimeFormat(String timestamp) {
DateTimeFormatConstant currentFormat = null;
for (DateTimeFormatConstant format : DateTimeFormatConstant.values()) {
boolean isValidFormat = validateFormat(format.value(), timestamp);
if (isValidFormat) {
currentFormat = format;
break;
}
}
return currentFormat;
}
private boolean validateFormat(String format, String time) {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(format);
boolean isValid;
try {
try {
LocalDateTime localDateTime = LocalDateTime.parse(time, formatter);
localDateTime.format(formatter);
isValid = true;
} catch (DateTimeException e) {
ZonedDateTime zonedDateTime = ZonedDateTime.parse(time, formatter);
zonedDateTime.format(formatter);
isValid = true;
}
} catch (DateTimeParseException e) {
isValid = false;
}
return isValid;
}
private LocalDateTime zonedTimeWithOffsetToUtc(String time, String format) {
ZonedDateTime zonedDateTime = ZonedDateTime.parse(time, DateTimeFormatter.ofPattern(format));
ZonedDateTime utcTime = zonedDateTime.withZoneSameInstant(ZoneOffset.UTC);
return utcTime.toLocalDateTime();
}
private LocalDateTime localTimeToUtc(String time, String format) {
AuthenticationDetails authDetails = (AuthenticationDetails) SecurityContextHolder.getContext().getAuthentication().getDetails();
ZoneId zoneId = authDetails.getZoneId();
LocalDateTime localDateTime = LocalDateTime.parse(time, DateTimeFormatter.ofPattern(format));
Instant instant = localDateTime.atZone(zoneId).toInstant();
ZonedDateTime zonedDateTimeUTC = ZonedDateTime.ofInstant(instant, ZoneId.of("Etc/UTC"));
return zonedDateTimeUTC.toLocalDateTime();
}
}
I want to add a new case to this method whereby the user provides a time zone offset in the request headers. I'm not sure how I can pass a header from my controller to the custom deserializer. Any thoughts?
So far, I've looked into using ThreadLocal but I fear that may be dangerous in the future when multiple requests are being processed on the same thread.
Related
I am converting the json received from database to object User.class using Objectmapper,class structure as follows
#XmlRootElement
public class User {
public User() {
super();
}
#XmlElement
private String username=null;
#XmlElement
private Feedbacks feedbacks=null;
//getter setter....
}
User has an instance of Feedbacks class which in turn has Arraylist< Feedback>
#XmlRootElement
public class Feedbacks {
public Feedbacks() {
}
#XmlElement
private ArrayList<Feedback> FeedbackList=new ArrayList<>();
public ArrayList<Feedback> getFeedbackList() {
return FeedbackList;
}
public void setFeedbackList(ArrayList<Feedback> FeedbackList) {
this.FeedbackList = FeedbackList;
}
}
and
public class Feedback {
private String feedback=null;
//private String timeStamp=null;
/*#JsonDeserialize(using = DateDeserializer.class); */
#JsonFormat(pattern = "MM/dd/yyyy HH:mm")
private Date feedbackDate;
public Feedback (){}
}
Sample json that i retrieve from db is:
{
"userName":"Test",
"feedbacks":{
"feedbackTOList":[
{
"feedback":"Please select appropriate value..1",
"timeStamp":"03/01/2000 14:52"
},
{
"feedback":"Please select appropriate value..2",
"timeStamp":"03/01/2018 13:50"
},
{
"feedback":"Please select appropriate value..3",
"timeStamp":"02/01/2018 10:52"
}
]
}
}
Json to object conversion and sorting the list of feedback based on date:
SimpleDateFormat formatter = new SimpleDateFormat("MM/dd/yyyy HH:mm");
mapper = new ObjectMapper();
mapper.setDateFormat(formatter);
userObject= mapper.readValue(jsonPayload, User.class);
Collections.sort(user.getFeedbacks().getFeedbackList(), new
Comparator<Feedback>() {
public int compare(Feedback f1, Feedback f2) {
if (f1.getTimeStamp() == null || f2.getTimeStamp() == null)
return 0;
return (f2.getTimeStamp().compareTo(f1.getTimeStamp()));
}
});
The issue is when angular UI consumes the object from rest service,instead of Date (Wed Aug 01 16:20:00 EDT 2018]) the timestamp value is being sent as timeStamp=1533154800000.
My question is how do i send the string in given format or atleast date object but not timestamp?
So far i tried #JsonFormat(pattern = "MM/dd/yyyy HH:mm"),custom date deserializer but no luck(by referring many other posts on stackoverflow,sites),can some one please let me know what mistake am i making?
I want to pretty print json responses from Spring MVC Restcontrollers dynamically based on a http parameter (like suggested here: http://www.vinaysahni.com/best-practices-for-a-pragmatic-restful-api#pretty-print-gzip).
I have found configurations for pretty print it by static configuration, but not how to do that dynamically?
When using Spring MVC for REST, how do you enable Jackson to pretty-print rendered JSON?
Any idea how to do that?
Introducing A New Media Type
You can define a new Media Type, say, application/pretty+json and register a new HttpMessageConverter that converts to that Media Type. In fact, if client sends a request with Accept: application/pretty+json header, our new HttpMessageConverter will write the response, Otherwise, the plain old MappingJackson2HttpMessageConverter would do that.
So, extends the MappingJackson2HttpMessageConverter like following:
public class PrettyPrintJsonConverter extends MappingJackson2HttpMessageConverter {
public PrettyPrintJsonConverter() {
setPrettyPrint(true);
}
#Override
public List<MediaType> getSupportedMediaTypes() {
return Collections.singletonList(new MediaType("application", "pretty+json"));
}
#Override
public boolean canWrite(Class<?> clazz, MediaType mediaType) {
boolean canWrite = super.canWrite(clazz, mediaType);
boolean canWritePrettily = mediaType != null &&
mediaType.getSubtype().equals("pretty+json");
return canWrite && canWritePrettily;
}
}
That setPrettyPrint(true) in constructor will do the trick for us. Then we should register this HttpMessageConverter:
#EnableWebMvc
#Configuration
public class WebConfig extends WebMvcConfigurerAdapter {
#Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(new PrettyPrintJsonConverter());
}
}
As i said, if client send a request with application/pretty+json Accept header, our PrettyPrintJsonConverter will write the JSON representation Prettily. Otherwise, MappingJackson2HttpMessageConverter would write a compact JSON to the response body.
You can achieve the same with a ResponseBodyAdvice or even Interceptors but in my opinion, registering a brand new HttpMessageConverter is the better approach.
To switch to pretty rendering with a ?pretty=true parameter I use a custom MappingJackson2HttpMessageConverter
#Configuration
#RestController
public class MyController {
#Bean
MappingJackson2HttpMessageConverter currentMappingJackson2HttpMessageConverter() {
MappingJackson2HttpMessageConverter jsonConverter = new CustomMappingJackson2HttpMessageConverter();
return jsonConverter;
}
public static class Input {
public String pretty;
}
public static class Output {
#JsonIgnore
public String pretty;
}
#RequestMapping(path = "/api/test", method = {RequestMethod.GET, RequestMethod.POST})
Output test( #RequestBody(required = false) Input input,
#RequestParam(required = false, value = "pretty") String pretty)
{
if (input.pretty==null) input.pretty = pretty;
Output output = new Output();
output.pretty = input.pretty;
return output;
}
}
The converter :
public class CustomMappingJackson2HttpMessageConverter extends MappingJackson2HttpMessageConverter {
ObjectMapper objectMapper;
ObjectMapper prettyPrintObjectMapper;
public CustomMappingJackson2HttpMessageConverter() {
objectMapper = new ObjectMapper();
prettyPrintObjectMapper = new ObjectMapper();
prettyPrintObjectMapper.configure(SerializationFeature.INDENT_OUTPUT, true);
}
#Override
#SuppressWarnings("deprecation")
protected void writeInternal(Object object, Type type, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException {
JsonEncoding encoding = getJsonEncoding(outputMessage.getHeaders().getContentType());
JsonGenerator generator = this.objectMapper.getFactory().createGenerator(outputMessage.getBody(), encoding);
try {
writePrefix(generator, object);
Class<?> serializationView = null;
FilterProvider filters = null;
Object value = object;
JavaType javaType = null;
if (object instanceof MappingJacksonValue) {
MappingJacksonValue container = (MappingJacksonValue) object;
value = container.getValue();
serializationView = container.getSerializationView();
filters = container.getFilters();
}
javaType = getJavaType(type, null);
ObjectMapper currentMapper = objectMapper;
Field prettyField = ReflectionUtils.findField(object.getClass(), "pretty");
if (prettyField != null) {
Object prettyObject = ReflectionUtils.getField(prettyField, object);
if (prettyObject != null && prettyObject instanceof String) {
String pretty = (String)prettyObject;
if (pretty.equals("true"))
currentMapper = prettyPrintObjectMapper;
}
}
ObjectWriter objectWriter;
if (serializationView != null) {
objectWriter = currentMapper.writerWithView(serializationView);
}
else if (filters != null) {
objectWriter = currentMapper.writer(filters);
}
else {
objectWriter = currentMapper.writer();
}
if (javaType != null && javaType.isContainerType()) {
objectWriter = objectWriter.withType(javaType);
}
objectWriter.writeValue(generator, value);
writeSuffix(generator, object);
generator.flush();
}
catch (JsonProcessingException ex) {
throw new HttpMessageNotWritableException("Could not write content: " + ex.getMessage(), ex);
}
}
}
Franck
I like Franck Lefebure's approach, but i don't like used reflection, so here is a solution with using custom PrettyFormattedBody type + pretty formatted arrays/lists:
Spring Config:
#Bean
MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter() {
return new CustomJsonResponseMapper();
}
CustomJsonResponseMapper.java:
public class CustomJsonResponseMapper extends MappingJackson2HttpMessageConverter {
private final ObjectMapper prettyPrintObjectMapper;
public CustomJsonResponseMapper() {
super();
prettyPrintObjectMapper = initiatePrettyObjectMapper();
}
protected ObjectMapper initiatePrettyObjectMapper() {
// clone and re-configure default object mapper
final ObjectMapper prettyObjectMapper = objectMapper != null ? objectMapper.copy() : new ObjectMapper();
prettyObjectMapper.configure(SerializationFeature.INDENT_OUTPUT, true);
// for arrays - use new line for every entry
DefaultPrettyPrinter pp = new DefaultPrettyPrinter();
pp.indentArraysWith(new DefaultIndenter());
prettyObjectMapper.setDefaultPrettyPrinter(pp);
return prettyObjectMapper;
}
#Override
protected void writeInternal(final Object objectToWrite, final Type type, final HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
// based on: if objectToWrite is PrettyFormattedBody with isPretty == true => use custom formatter
// otherwise - use the default one
final Optional<PrettyFormattedBody> prettyFormatted = Optional.ofNullable(objectToWrite)
.filter(o -> o instanceof PrettyFormattedBody)
.map(o -> (PrettyFormattedBody) objectToWrite);
final boolean pretty = prettyFormatted.map(PrettyFormattedBody::isPretty).orElse(false);
final Object realObject = prettyFormatted.map(PrettyFormattedBody::getBody).orElse(objectToWrite);
if (pretty) {
// this is basically full copy of super.writeInternal(), but with custom (pretty) object mapper
MediaType contentType = outputMessage.getHeaders().getContentType();
JsonEncoding encoding = getJsonEncoding(contentType);
JsonGenerator generator = this.prettyPrintObjectMapper.getFactory().createGenerator(outputMessage.getBody(), encoding);
try {
writePrefix(generator, realObject);
Class<?> serializationView = null;
FilterProvider filters = null;
Object value = realObject;
JavaType javaType = null;
if (realObject instanceof MappingJacksonValue) {
MappingJacksonValue container = (MappingJacksonValue) realObject;
value = container.getValue();
serializationView = container.getSerializationView();
filters = container.getFilters();
}
if (type != null && value != null && TypeUtils.isAssignable(type, value.getClass())) {
javaType = getJavaType(type, null);
}
ObjectWriter objectWriter;
if (serializationView != null) {
objectWriter = this.prettyPrintObjectMapper.writerWithView(serializationView);
} else if (filters != null) {
objectWriter = this.prettyPrintObjectMapper.writer(filters);
} else {
objectWriter = this.prettyPrintObjectMapper.writer();
}
if (javaType != null && javaType.isContainerType()) {
objectWriter = objectWriter.forType(javaType);
}
objectWriter.writeValue(generator, value);
writeSuffix(generator, realObject);
generator.flush();
} catch (JsonProcessingException ex) {
throw new HttpMessageNotWritableException("Could not write JSON: " + ex.getOriginalMessage(), ex);
}
} else {
// use default formatting if isPretty property is not specified
super.writeInternal(realObject, type, outputMessage);
}
}
#Override
public boolean canWrite(Class<?> clazz, MediaType mediaType) {
// this should be mandatory overridden,
// otherwise writeInternal() won't be called with custom PrettyFormattedBody type
return (PrettyFormattedBody.class.equals(clazz) && canWrite(mediaType)) || super.canWrite(clazz, mediaType);
}
public static final class PrettyFormattedBody {
private final Object body;
private final boolean pretty;
public PrettyFormattedBody(Object body, boolean pretty) {
this.body = body;
this.pretty = pretty;
}
public Object getBody() {
return body;
}
public boolean isPretty() {
return pretty;
}
}
}
HealthController.java (pretty is an optional request parameter):
#RequestMapping(value = {"/", "/health"},
produces = APPLICATION_JSON_VALUE)
public ResponseEntity<?> health(#RequestParam Optional<String> pretty) {
return new ResponseEntity<>(
new CustomJsonResponseMapper.PrettyFormattedBody(healthResult(), pretty.isPresent()),
HttpStatus.OK);
}
Response example http://localhost:8080:
{"status":"OK","statusCode":200,"endpoints":["/aaa","/bbb","/ccc"]}
Response example http://localhost:8080?pretty:
{
"status": "OK",
"statusCode": 200,
"endpoints": [
"/aaa",
"/bbb",
"/ccc"
]
}
Another solution if Gson formatter is used (full pull request reference):
Spring Config (define 2 beans):
#Bean
public Gson gson() {
return new GsonBuilder()
.setFieldNamingPolicy(FieldNamingPolicy.IDENTITY)
.disableHtmlEscaping()
.create();
}
/**
* #return same as {#link #gson()}, but with <code>{#link Gson#prettyPrinting} == true</code>, e.g. use indentation
*/
#Bean
public Gson prettyGson() {
return new GsonBuilder()
.setFieldNamingPolicy(FieldNamingPolicy.IDENTITY)
.setPrettyPrinting()
.disableHtmlEscaping()
.create();
}
/**
* Custom JSON objects mapper: uses {#link #gson()} as a default JSON HTTP request/response mapper
* and {#link #prettyGson()} as mapper for pretty-printed JSON objects. See {#link PrettyGsonMessageConverter} for
* how pretty print is requested.
* <p>
* <b>Note:</b> {#link FieldNamingPolicy#IDENTITY} field mapping policy is important at least for
* {#link PaymentHandleResponse#getPayment()} method. See respective documentation for details.
*
* #return default HTTP request/response mapper, based on {#link #gson()} bean.
*/
#Bean
public GsonHttpMessageConverter gsonMessageConverter() {
return new PrettyGsonMessageConverter(gson(), prettyGson());
}
PrettyGsonMessageConverter.java:
/**
* Custom Gson response message converter to allow JSON pretty print, if requested.
* <p>
* The class extends default Spring {#link GsonHttpMessageConverter} adding {#link #prettyGson} mapper and processing
* {#link PrettyFormattedBody} instances.
*/
public class PrettyGsonMessageConverter extends GsonHttpMessageConverter {
/**
* JSON message converter with configured pretty print options, which is used when a response is expected to be
* pretty printed.
*/
private final Gson prettyGson;
/**
* #see GsonHttpMessageConverter#jsonPrefix
*/
private String jsonPrefix;
/**
* #param gson default (minified) JSON mapper. This value is set to {#code super.gson} property.
* #param prettyGson pretty configure JSON mapper, which is used if the body expected to be pretty printed
*/
public PrettyGsonMessageConverter(final Gson gson, final Gson prettyGson) {
super();
this.setGson(gson);
this.prettyGson = prettyGson;
}
/**
* Because base {#link GsonHttpMessageConverter#jsonPrefix} is private, but is used in overloaded
* {#link #writeInternal(Object, Type, HttpOutputMessage)} - we should copy this value.
*
* #see GsonHttpMessageConverter#setJsonPrefix(String)
*/
#Override
public void setJsonPrefix(String jsonPrefix) {
super.setJsonPrefix(jsonPrefix);
this.jsonPrefix = jsonPrefix;
}
/**
* Because base {#link GsonHttpMessageConverter#jsonPrefix} is private, but is used in overloaded
* {#link #writeInternal(Object, Type, HttpOutputMessage)} - we should copy this value.
*
* #see GsonHttpMessageConverter#setPrefixJson(boolean)
*/
#Override
public void setPrefixJson(boolean prefixJson) {
super.setPrefixJson(prefixJson);
this.jsonPrefix = (prefixJson ? ")]}', " : null);
}
/**
* Allow response JSON pretty print if {#code objectToWrite} is a {#link PrettyFormattedBody} instance with
* <code>{#link PrettyFormattedBody#isPretty() isPretty} == true</code>.
*
* #param objectToWrite if the value is {#link PrettyFormattedBody} instance with
* <code>{#link PrettyFormattedBody#isPretty() isPretty} == true</code> - use
* {#link #prettyGson} for output writing. Otherwise use base
* {#link GsonHttpMessageConverter#writeInternal(Object, Type, HttpOutputMessage)}
* #param type the type of object to write (may be {#code null})
* #param outputMessage the HTTP output message to write to
* #throws IOException in case of I/O errors
* #throws HttpMessageNotWritableException in case of conversion errors
*/
#Override
protected void writeInternal(#Nullable final Object objectToWrite,
#Nullable final Type type,
#Nonnull final HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException {
// based on: if objectToWrite is PrettyFormattedBody && isPretty == true => use custom formatter
// otherwise - use the default base GsonHttpMessageConverter#writeInternal(Object, Type, HttpOutputMessage)
Optional<PrettyFormattedBody> prettyFormatted = Optional.ofNullable(objectToWrite)
.filter(o -> o instanceof PrettyFormattedBody)
.map(o -> (PrettyFormattedBody) objectToWrite);
boolean pretty = prettyFormatted.map(PrettyFormattedBody::isPretty).orElse(false);
Object realObject = prettyFormatted.map(PrettyFormattedBody::getBody).orElse(objectToWrite);
if (pretty) {
// this is basically full copy of super.writeInternal(), but with custom (pretty) gson mapper
Charset charset = getCharset(outputMessage.getHeaders());
OutputStreamWriter writer = new OutputStreamWriter(outputMessage.getBody(), charset);
try {
if (this.jsonPrefix != null) {
writer.append(this.jsonPrefix);
}
if (type != null) {
this.prettyGson.toJson(realObject, type, writer);
} else {
this.prettyGson.toJson(realObject, writer);
}
writer.close();
} catch (JsonIOException ex) {
throw new HttpMessageNotWritableException("Could not write JSON: " + ex.getMessage(), ex);
}
} else {
// use default writer if isPretty property is not specified
super.writeInternal(realObject, type, outputMessage);
}
}
/**
* To ensure the message converter supports {#link PrettyFormattedBody} instances
*
* #param clazz response body class
* #return <b>true</b> if the {#code clazz} is {#link PrettyFormattedBody} or {#code super.supports(clazz) == true}
*/
#Override
protected boolean supports(Class<?> clazz) {
return PrettyFormattedBody.class.equals(clazz) || super.supports(clazz);
}
/**
* Just a copy-paste of {#link GsonHttpMessageConverter#getCharset(HttpHeaders)} because it is private, but used in
* {#link #writeInternal(Object, Type, HttpOutputMessage)}
*
* #param headers output message HTTP headers
* #return a charset from the {#code headers} content type or {#link GsonHttpMessageConverter#DEFAULT_CHARSET}
* otherwise.
*/
private Charset getCharset(HttpHeaders headers) {
if (headers == null || headers.getContentType() == null || headers.getContentType().getCharset() == null) {
return DEFAULT_CHARSET;
}
return headers.getContentType().getCharset();
}
}
PrettyFormattedBody.java:
public final class PrettyFormattedBody {
private final Object body;
private final boolean pretty;
private PrettyFormattedBody(#Nonnull final Object body, final boolean pretty) {
this.body = body;
this.pretty = pretty;
}
public Object getBody() {
return body;
}
public boolean isPretty() {
return pretty;
}
public static PrettyFormattedBody of(#Nonnull final Object body, final boolean pretty) {
return new PrettyFormattedBody(body, pretty);
}
}
and finally - the controller itself:
#RequestMapping(
value = {"/health", "/"},
produces = APPLICATION_JSON_VALUE)
public ResponseEntity<?> checkHealth(#RequestParam(required = false) String pretty,
#Autowired ApplicationInfo applicationInfo) {
Map<String, Object> tenantResponse = new HashMap<>();
tenantResponse.put(APP_INFO_KEY, applicationInfo);
return new ResponseEntity<>(PrettyFormattedBody.of(tenantResponse, pretty != null),
HttpStatus.OK);
}
I have a JSON response from an API like this:
{"asalas77":
{"id":23519033,"name":"Asalas77","profileIconId":22,"revisionDate":1487214366000,"summonerLevel":30}
}
And I need to extract the inner object from it. I tried using a deserializer like shown in this question Get nested JSON object with GSON using retrofit but it doesn't work for me.
public class SummonerDeserializer implements JsonDeserializer<Summoner> {
#Override
public Summoner deserialize(JsonElement je, Type type, JsonDeserializationContext jdc)
throws JsonParseException {
long id = je.getAsJsonObject().get("id").getAsLong();
String name = je.getAsJsonObject().get("name").getAsString();
int profileIconId = je.getAsJsonObject().get("profileIconId").getAsInt();
long revisionDate = je.getAsJsonObject().get("revisionDate").getAsLong();
long summonerLevel = je.getAsJsonObject().get("summonerLevel").getAsLong();
Summoner s = new Summoner();
s.setId(id);
s.setName(name);
s.setProfileIconId(profileIconId);
s.setRevisionDate(revisionDate);
s.setSummonerLevel(summonerLevel);
return s;
}
}
But the problem is I can't access the inner fields from JsonElement je and the name asalas77 is a variable (it's a search query) so I can't extract the inner object directly.
You must have a wrapper class in order not to clash deserialization strategies. Assume it's as follows:
final class SummonerResponse {
private final Summoner summoner;
private SummonerResponse(final Summoner summoner) {
this.summoner = summoner;
}
static SummonerResponse summonerResponse(final Summoner summoner) {
return new SummonerResponse(summoner);
}
Summoner getSummoner() {
return summoner;
}
}
Then you can either create a custom response deserializer:
final class SummonerWrapperDeserializer
implements JsonDeserializer<SummonerResponse> {
private static final JsonDeserializer<SummonerResponse> summonerDeserializer = new SummonerWrapperDeserializer();
private SummonerWrapperDeserializer() {
}
static JsonDeserializer<SummonerResponse> getSummonerResponseDeserializer() {
return summonerDeserializer;
}
#Override
public SummonerResponse deserialize(final JsonElement jsonElement, final Type type, final JsonDeserializationContext context)
throws JsonParseException {
// Pick the root as a JSON object
final JsonObject outerJsonObject = jsonElement.getAsJsonObject();
// And check how many properties does it have
final Iterable<? extends Entry<String, JsonElement>> outerJsonObjectEntries = outerJsonObject.entrySet();
if ( outerJsonObject.size() != 1 ) {
throw new JsonParseException("Expected one property object, the actual properties are: " + getPropertyName(outerJsonObjectEntries));
}
// If it has only one property, just get the property and take its inner value
final JsonElement innerJsonElement = outerJsonObjectEntries.iterator().next().getValue();
// Once it's obtained, just delegate the parsing to a downstream parser - no need to create Summoner instances by hands
return summonerResponse(context.deserialize(innerJsonElement, Summoner.class));
}
private static Set<String> getPropertyName(final Iterable<? extends Entry<String, JsonElement>> entries) {
final Set<String> keys = new LinkedHashSet<>();
for ( final Entry<String, JsonElement> entry : entries ) {
keys.add(entry.getKey());
}
return keys;
}
}
Or save some memory (the JSON (de)serializers require some memory because they work with JSON trees) and create a more low level type adapter:
final class SummonerResponseTypeAdapterFactory
implements TypeAdapterFactory {
private static final TypeAdapterFactory summonerResponseTypeAdapterFactory = new SummonerResponseTypeAdapterFactory();
private SummonerResponseTypeAdapterFactory() {
}
static TypeAdapterFactory getSummonerResponseTypeAdapterFactory() {
return summonerResponseTypeAdapterFactory;
}
#Override
public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> typeToken) {
// Check if we can handle SummonerResponse. Classes can be compared with `==`
if ( typeToken.getRawType() == SummonerResponse.class ) {
final TypeAdapter<SummonerResponse> typeAdapter = getSummonerResponseTypeAdapter(gson);
#SuppressWarnings("unchecked")
final TypeAdapter<T> castTypeAdapter = (TypeAdapter<T>) typeAdapter;
return castTypeAdapter;
}
return null;
}
}
final class SummonerResponseTypeAdapter
extends TypeAdapter<SummonerResponse> {
private final Gson gson;
private SummonerResponseTypeAdapter(final Gson gson) {
this.gson = gson;
}
static TypeAdapter<SummonerResponse> getSummonerResponseTypeAdapter(final Gson gson) {
return new SummonerResponseTypeAdapter(gson);
}
#Override
#SuppressWarnings("resource")
public void write(final JsonWriter out, final SummonerResponse summonerResponse)
throws IOException {
// The incoming object may be null
if ( summonerResponse == null && gson.serializeNulls() ) {
out.nullValue();
return;
}
// Generate the inner object
out.beginObject();
out.name(summonerResponse.getSummoner().name);
gson.toJson(summonerResponse.getSummoner(), Summoner.class, out);
out.endObject();
}
#Override
public SummonerResponse read(final JsonReader in)
throws IOException {
// is it a null?
if ( in.peek() == NULL ) {
return null;
}
// make sure that the inner read JSON contains an inner object
in.beginObject();
// ignore the name
in.nextName();
// delegate parsing to the backing Gson instance in order to apply downstream parsing
final Summoner summoner = gson.fromJson(in, Summoner.class);
// check if there are more properties within the inner object
if ( in.peek() == NAME ) {
throw new MalformedJsonException("Unexpected: " + in.nextName());
}
// consume the "}" token
in.endObject();
return summonerResponse(summoner);
}
}
Then any of the options above can be used like this:
final Gson gson = new GsonBuilder()
.registerTypeAdapter(SummonerResponse.class, getSummonerResponseDeserializer())
.create();
final SummonerResponse summonerResponse = gson.fromJson(JSON, SummonerResponse.class);
final Summoner summoner = summonerResponse.getSummoner();
out.println(summoner.id + " => " + summoner.name);
or
final Gson gson = new GsonBuilder()
.registerTypeAdapterFactory(getSummonerResponseTypeAdapterFactory())
.create();
final SummonerResponse summonerResponse = gson.fromJson(JSON, SummonerResponse.class);
final Summoner summoner = summonerResponse.getSummoner();
out.println(summoner.id + " => " + summoner.name);
out.println(gson.toJson(summonerResponse));
The outputs are
23519033 => Asalas77
and
23519033 => Asalas77
{"Asalas77":{"id":23519033,"name":"Asalas77","profileIconId":22,"revisionDate":1487214366000,"summonerLevel":30}}
respectively.
I am writing a program which accepts a JSON input with the following format from client:
{
"campaignID": 1,
"clientID": 1,
"pmapID": 1,
"ward": "1-Bedded (Private)",
"age": 20,
"attr1": "EXA1(A)",
"attr2": "EO",
"attr3": "11/02/2012",
"attr4": "SIN",
"attr5": "N",
"attr6": "Y"
}
I'd like to read the JSON input, save all the attributes into local variables (String, int, ...) and finally respond with a POST("JSON") which will return a single float/double value (e.g. {"PMC": 30.12} ).
public class RestletApplication extends Application
{
#Override
public synchronized Restlet createInboundRoot()
{
Router router = new Router(getContext());
router.attach("/pmc/calculate", PMCResource.class);
return router;
}
}
I have written the function so far but am lost how to read the JSON input:
public class PMCResource extends ServerResource
{
#Post("JSON")
public Representation post(Representation entity) throws ResourceException {
try {
if (entity.getMediaType().isCompatible(MediaType.APPLICATION_JSON))
{
// Read JSON file and parse onto local variables
// Do processing & return a float value
}
} catch (Exception e) {
getResponse().setStatus(Status.SERVER_ERROR_INTERNAL);
}
}
}
5 May 2016 - Edited the resource class
// Imports
public class PMCResource extends ServerResource
{
static Logger LOGGER = LoggerFactory.getLogger(PMCResource.class);
#Override
#Post("JSON")
public Representation post(Representation entity) throws ResourceException
{
PMCMatrixDAO matrix = new PMCMatrixDAOImpl();
JsonObjectBuilder response = Json.createObjectBuilder();
try
{
if (entity.getMediaType().isCompatible(MediaType.APPLICATION_JSON))
{
InputStream is = new FileInputStream(getClass().getResource("/input.json").getFile());
try (JsonReader reader = Json.createReader(is)) {
JsonObject obj = reader.readObject();
double result = matrix.calculatePMC(obj);
response.add("PMC", result);
}
}
} catch (Exception e) {
getResponse().setStatus(Status.SERVER_ERROR_INTERNAL);
}
return new StringRepresentation(response.build().toString());
}
}
The Implementation class
public class PMCMatrixDAOImpl implements PMCMatrixDAO
{
public double calculatePMC(JsonObject obj)
{
int campaignID = obj.getInt("campaignID");
int clientID = obj.getInt("clientID");
int pmapID = obj.getInt("pmapID");
String ward = obj.getString("ward");
int age = obj.getInt("age");
String attr1 = obj.getString("attr1");
String attr2 = obj.getString("attr2");
String attr3 = obj.getString("attr3");
String attr4 = obj.getString("attr4");
String attr5 = obj.getString("attr5");
String attr6 = obj.getString("attr6");
// SQL processing
double dPMC = sqlQueryCall(...);
return dPMC;
}
}
In order to parse your JSON file, and since you're using Maven I'll assume you have it on your classpath, you can do it using a FileInputStream or a FileReader. So, assuming your JSON file is called input.json and it is on the root of your src/main/resources folder, you can load it the following way:
using a FileInputStream:
InputStream is = new FileInputStream(getClass().getResource("/input.json").getFile());
try (JsonReader reader = Json.createReader(is)) {
// file processing is done here
}
using a FileReader:
FileReader fr = new FileReader(getClass().getResource("/input.json").getFile());
try (JsonReader reader = Json.createReader(fr)) {
// file processing is done here
}
Ok, so now that we have our JsonReader created, lets retrieve the contents of our JSON file:
InputStream is = new FileInputStream(getClass().getResource("/input.json").getFile());
try (JsonReader reader = Json.createReader(is)) {
JsonObject obj = reader.readObject();
// retrieve JSON contents
int campaingID = obj.getInt("campaignID");
int clientID = obj.getInt("clientID");
int pmapID = obj.getInt("pmapID");
String ward = obj.getString("ward");
int age = obj.getInt("age");
String attr1 = obj.getString("attr1");
String attr2 = obj.getString("attr2");
String attr3 = obj.getString("attr3");
String attr4 = obj.getString("attr4");
String attr5 = obj.getString("attr5");
String attr6 = obj.getString("attr6");
}
As an alternative of having several variables across your method, you could create a simple POJO, having those variable as attributes, and then populate it using Jackson:
public class MyPojo {
private int campaingID;
private int clientID;
private int pmapID;
private String ward;
private int age;
private String attr1;
private String attr2;
private String attr3;
private String attr4;
private String attr5;
private String attr6;
// getters & setters
}
Finally, in order to send the response back to your client, you could do this:
JsonObject response = Json.createObjectBuilder().add("PMC", 30.12).build();
return new StringRepresentation(response.toString());
So, the entire solution could look like this:
#Override
#Post("JSON")
public Representation post(Representation entity) throws ResourceException {
JsonObjectBuilder response = Json.createObjectBuilder();
try {
if (entity.getMediaType().isCompatible(MediaType.APPLICATION_JSON)) {
InputStream is = new FileInputStream(getClass().getResource("/input.json").getFile());
try (JsonReader reader = Json.createReader(is)) {
JsonObject obj = reader.readObject();
// retrieve JSON contents
int campaingID = obj.getInt("campaignID");
int clientID = obj.getInt("clientID");
int pmapID = obj.getInt("pmapID");
String ward = obj.getString("ward");
int age = obj.getInt("age");
String attr1 = obj.getString("attr1");
String attr2 = obj.getString("attr2");
String attr3 = obj.getString("attr3");
String attr4 = obj.getString("attr4");
String attr5 = obj.getString("attr5");
String attr6 = obj.getString("attr6");
}
// Do processing & execute your SQL query call here
double result = sqlQueryCall(...);
response.add("PMC", result);
}
} catch (Exception e) {
getResponse().setStatus(Status.SERVER_ERROR_INTERNAL);
}
return new StringRepresentation(response.build().toString());
}
As a side note, the JsonReader class belongs to the Java EE API which, for compiling purposes it's okay. Although, for running purposes, one requires the declaration of a JSON-API implementation dependency in one's Maven project. For instance:
<dependency>
<groupId>org.glassfish</groupId>
<artifactId>javax.json</artifactId>
<version>1.0.4</version>
</dependency>
Below is the way one can communicate to the REST web service through a client:
Create a simple POJO object that will contain the information to send, as mentioned above (MyPojo).
Your REST service would look something like this:
public class PMCResource extends ServerResource {
static Logger LOGGER = Logger.getLogger(RestletMain.class.getName());
#Post("JSON")
public Representation post(MyPojo entity) throws ResourceException {
PMCMatrixDAO matrix = new PMCMatrixDAOImpl();
JsonObjectBuilder response = Json.createObjectBuilder();
try {
double result = matrix.calculatePMC(entity);
response.add("PMC", result);
} catch (Exception e) {
getResponse().setStatus(Status.SERVER_ERROR_INTERNAL);
}
return new StringRepresentation(response.build().toString());
}
}
Modify your PMCMatrixDAOImpl in order to process your POJO:
public double calculatePMC(MyPojo pojo) {
(...)
}
Create a client that allows you to test your REST service:
public class PMCResourceMain {
public static void main(String[] args) {
// take into account the context-root, if exists, and path to your REST service
ClientResource resource = new ClientResource("http://<host>:<port>");
MyPojo myPojo = new MyPojo();
myPojo.setCampaingID(1);
myPojo.setClientID(1);
myPojo.setPmapID(1);
myPojo.setWard("1-Bedded (Private)");
myPojo.setAge(20);
myPojo.setAttr1("EXA1(A)");
myPojo.setAttr2("EO");
myPojo.setAttr3("11/02/2012");
myPojo.setAttr4("SIN");
myPojo.setAttr5("N");
myPojo.setAttr6("Y");
try {
resource.post(myPojo, MediaType.APPLICATION_JSON).write(System.out);
} catch (ResourceException | IOException e) {
e.printStackTrace();
}
}
}
Full Restlet documentation can be found here.
For the benefit of those who landed in the same situation as me, here's my solution:
Resource class
#Override
#Post("JSON")
public Representation post(Representation entity) throws ResourceException
{
PMCMatrixDAO matrix = new PMCMatrixDAOImpl();
JsonObjectBuilder response = Json.createObjectBuilder();
try {
String json = entity.getText(); // Get JSON input from client
Map<String, Object> map = JsonUtils.toMap(json); // Convert input into Map
double result = matrix.calculatePMC(map);
response.add("PMC", result);
} catch (IOException e) {
LOGGER.error(this.getClass() + " - IOException - " + e);
getResponse().setStatus(Status.SERVER_ERROR_INTERNAL);
}
return new StringRepresentation(response.build().toString());
}
JSON conversion utility class
public class JsonUtils {
private static final Logger LOG = LoggerFactory.getLogger(JsonUtils.class);
private JsonUtils() {
}
public static String toJson(Object object) {
String jsonString = null;
ObjectMapper mapper = new ObjectMapper();
try {
jsonString = mapper.writeValueAsString(object);
} catch (JsonProcessingException e) {
LOG.error(e.getMessage(), e);
}
return jsonString;
}
public static Map<String, Object> toMap(String jsonString) {
Map<String, Object> map = new ConcurrentHashMap<>();
ObjectMapper mapper = new ObjectMapper();
try {
map = mapper.readValue(jsonString, new TypeReference<Map<String, Object>>() {
});
} catch (IOException e) {
LOG.error(e.getMessage(), e);
}
return map;
}
}
And the implementation class which handles all the processing
public class PMCMatrixDAOImpl implements PMCMatrixDAO
{
public double calculatePMC(Map<String, Object> map)
{
int campaignID = (int) map.get("campaignID");
int clientID = (int) map.get("clientID");
int pmapID = (int) map.get("pmapID");
String ward = (String) map.get("ward");
int age = (int) map.get("age");
String attr1 = (String) map.get("attr1");
String attr2 = (String) map.get("attr2");
String attr3 = (String) map.get("attr3");
String attr4 = (String) map.get("attr4");
String attr5 = (String) map.get("attr5");
String attr6 = (String) map.get("attr6");
// SQL processing
double dPMC = sqlQueryCall(...);
return dPMC;
}
}
I am using the below code to receive Tweets from Twitter4j Search API in the form of JSON response. I am receiving the result in the form of List as specified in Twitter4j search API in the line
List<Status> tweets = result.getTweets();
The problem is that the tweets returned as List where one Status entry is having non-empty and non-null GeoLocation whereas another Status entry is having a null or empty GeoLocation. Since to retrieve the relevant fields from each Status entry (i.e. Tweet), I iterate over the List and call getters which is throwing me null for the Status entries where the GeoLocation field is null.
The approach I tried to follow:
I created a POJO TweetJSON_2 (defined at the bottom of the post) with the relevant fields and their getters and setters. I am using Jackson ObjectMapper to handle null values like below:
JsonGenerator generator = new JsonFactory().createGenerator(os);
generator.setPrettyPrinter(new DefaultPrettyPrinter());
TweetJSON_2 rawJSON;
ObjectMapper mapper = new ObjectMapper();
//mapper.setVisibility(PropertyAccessor.FIELD, Visibility.ANY);
mapper.configure(SerializationFeature.WRITE_NULL_MAP_VALUES, false);
mapper.setSerializationInclusion(Include.NON_NULL);
// ... rawJSON is populated ...
mapper.writeValue(generator, rawJSON);
However, when I am trying to get the geoLocation field from Status, using the below line which is marked with **
List<Status> tweets = result.getTweets();
I am getting the Java NullPointerException as follows:
[Mon Apr 20 11:32:47 IST 2015]{"statuses":[{"retweeted_status":{"contributors":null,"text":"<my text>",**"geo":null**,"retweeted":false,"in_reply_to_screen_name":null,"truncated":false,"lang":"en","entities":{"symbols":[],"urls":[],"hashtags": ... &include_entities=1","since_id_str":"0","completed_in":0.029}}
**Exception in thread "main" java.lang.NullPointerException
at analytics.search.twitter.SearchFieldsTweetsJSON_2.main(SearchFieldsTweetsJSON_2.java:78)**
For example: If I input a json String as
String s = "{\"first\": 123, \"second\": [{\"second_first\":null, \"second_second\":null}, {\"second_third\":null}, null], \"third\": 789, \"fourth\":null}";
The output should be like
"{\"first\": 123, \"third\": 789}";
What I want, is to replace all null elements from JSONArrays and all null key-value pairs from JSONObjects no matter at whatever level they are nested in my JSON response.
Object vs Tree Model Approach
I tried the Object Model parsing mechanism which is a javax.json.stream.JsonParser.Event based method but would need multiple times of access and object replacement on the JSON String depending on at what level the null is nested making this approach very complicated. At the same time if I use Tree Model mechanism, the entire JSON response would have to be stored as a Tree which may overflow my JVM heap memory because the JSON size can be pretty large based on my query parameters. I need to find a workable solution to overcome this problem. Any suggestions on solving the above discussed problem will be highly appreciated.
The code is as follows:
public class SearchFieldsTweetsJSON_2 {
/* Searches specific fields from Tweets in JSON format */
public static void main(String[] args) throws FileNotFoundException, IOException {
if (args.length < 2) {
System.out.println("java twitter4j.examples.search.SearchTweets [query][outputJSONFile]");
System.exit(-1);
}
ConfigurationBuilder cb = new ConfigurationBuilder();
cb.setDebugEnabled(true)
.setOAuthConsumerKey("XXXXXXXXXXXXXXXXXXXXXXXXXX")
.setOAuthConsumerSecret("XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX")
.setOAuthAccessToken("NNNNNNNNN-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX")
.setOAuthAccessTokenSecret("XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX")
.setJSONStoreEnabled(true);
Twitter twitter = new TwitterFactory(cb.build()).getInstance();
try {
Query query = new Query(args[0]);
QueryResult result;
File jsonFile = new File(args[1]);
System.out.println("File Path : " + jsonFile.getAbsolutePath());
OutputStreamWriter os = new OutputStreamWriter(new FileOutputStream(jsonFile));
JsonGenerator generator = new JsonFactory().createGenerator(os);
generator.setPrettyPrinter(new DefaultPrettyPrinter());
TweetJSON_2 rawJSON;
ObjectMapper mapper = new ObjectMapper();
//mapper.setVisibility(PropertyAccessor.FIELD, Visibility.ANY);
mapper.configure(SerializationFeature.WRITE_NULL_MAP_VALUES, false);
mapper.setSerializationInclusion(Include.NON_NULL);
do {
result = twitter.search(query);
List<Status> tweets = result.getTweets();
for (Status tweet : tweets) {
rawJSON = new TweetJSON_2();
rawJSON.setStatusId(Long.toString(tweet.getId()));
rawJSON.setUserId(Long.toString(tweet.getUser().getId()));
rawJSON.setUserName(tweet.getUser().getScreenName());
rawJSON.setStatusText(tweet.getText());
rawJSON.setGeoLocation(tweet.getGeoLocation().toString()); **<< Giving error at tweet.getGeoLocation() since GeoLocation is null**
mapper.writeValue(generator, rawJSON);
System.out.println(rawJSON.toString());
}
} while ((query = result.nextQuery()) != null);
generator.close();
System.out.println(os.toString());
} catch (TwitterException te) {
te.printStackTrace();
System.out.println("Failed to search tweets : " + te.getMessage());
System.exit(-1);
}
}
}
I have defined my TweetJSON_2 Java object as follows:
public class TweetJSON_2 {
public String statusId;
public String statusText;
public String userId;
public String userName;
public String geoLocation;
public String getStatusId() {
return statusId;
}
public void setStatusId(String statusId) {
this.statusId = statusId;
}
public String getStatusText() {
return statusText;
}
public void setStatusText(String statusText) {
this.statusText = statusText;
}
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.userId = userId;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public String getGeoLocation() {
return geoLocation;
}
public void setGeoLocation(String geoLocation) {
this.geoLocation = geoLocation;
}
#Override
public String toString() {
return "TweetJSON_2 [ statusId = " + statusId + ", statusText = " + statusText + "]";
}
}
I have tried with reconfiguring my POJO in the below way and it successfully replaced all the nulls as specified in the setter methods. Didn't need to follow either Tree or Event-based model parsing of JSON string. HTH
The modified TweetJSON_2 POJO:
public class TweetJSON_2 {
public Long statusId = null;
public String statusText = null;
public Long userId = null;
public String userName = null;
public GeoLocation geoLocation = null;
public Long getStatusId() {
if (this.statusId==null)
return new Long(0L);
return statusId;
}
public void setStatusId(Long statusId) {
if (statusId==null)
this.statusId = new Long(0L);
else
this.statusId = statusId;
}
public String getStatusText() {
if (this.statusText==null)
return new String("");
return statusText;
}
public void setStatusText(String statusText) {
if (statusText==null)
this.statusText = new String("");
else
this.statusText = statusText;
}
public Long getUserId() {
if (this.userId==null)
return new Long(0L);
return userId;
}
public void setUserId(Long userId) {
if (userId==null)
this.userId = new Long(0L);
else
this.userId = userId;
}
public String getUserName() {
if (this.userName==null)
return new String("");
return userName;
}
public void setUserName(String userName) {
if (userName==null)
this.userName = new String("");
else
this.userName = userName;
}
public GeoLocation getGeoLocation() {
if (this.geoLocation==null)
return new GeoLocation(0.0,0.0);
return geoLocation;
}
public void setGeoLocation(GeoLocation geoLocation) {
if (geoLocation==null)
this.geoLocation = new GeoLocation(0.0,0.0);
else
this.geoLocation = geoLocation;
}
#Override
public String toString() {
return "TweetJSON_2 [ statusId = " + statusId + ", statusText = " + statusText + "]";
}
}