Overriding XmlAdapter for 3rd party library class - java

I am generating xml using jaxbMarshaller for a third party library class. Since library XmlAdapter which converts Calendar object to string is not using TimeZone field, so marshaller is generating wrong xml for every Calendar field of pojo class.
3rd Party library XmlAdapter is using below class for Calendar to string conversion:
public class DateConversion {
public static String printDate(Calendar value) {
if(value != null) {
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd");
return format.format(value.getTime());
}
return null;
}
}
So I want to override the behavior of XmlAdapter for Calendar field and tried below example but seems it is not working:
my custom XmlAdapter is using below class for conversion:
public class DateConversion {
public static String printDate(Calendar value, TimeZone timeZone) {
if(value != null) {
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd");
format.setTimeZone(timeZone);
return format.format(value.getTime());
}
return null;
}
}
and then I have done registry like:
public #Nullable
String toPdxmlString(final #NotNull Deals input) {
try {
final Marshaller marshaller = jaxbContext.createMarshaller();
final DateFormatterAdapter dateFormatterAdapter = new DateFormatterAdapter(PdxmlDateTimeUtil.FXONLINE_DEFAULT_DEAL_TIMEZONE);
marshaller.setAdapter(dateFormatterAdapter);
StringWriter writer = new StringWriter();
marshaller.marshal(input, writer);
return writer.toString();
} catch (JAXBException exception) {
LOGGER.error("Unable to marshall the given input Deals: {}, into String using JAXB Context: {}, ... ", input, jaxbContext, exception);
}
return null;
}
Can anyone help me to know if this is doable or not, if yes where I am going wrong?

So I found my solution. I extended XmlAdapter of 3rd party library and pluged in TimeZone field in DateConversion like:
public class DateFormatterAdapter extends Adapter2 {
private final TimeZone timeZone;
public DateFormatterAdapter(final TimeZone timeZone) {
this.timeZone = timeZone;
}
#Override
public Calendar unmarshal(String value) {
return javax.xml.bind.DatatypeConverter.parseDate(value);
}
#Override
public String marshal(Calendar calendar) {
return DateConversion.printDate(calendar, timeZone);
}
}
At last registered the extended XmlAdapter as:
public #Nullable
String toPdxmlString(final #NotNull Deals input) {
try {
final Marshaller marshaller = jaxbContext.createMarshaller();
final DateFormatterAdapter dateFormatterAdapter = new DateFormatterAdapter(PdxmlDateTimeUtil.FXONLINE_DEFAULT_DEAL_TIMEZONE);
marshaller.setAdapter(Adapter2.class, dateFormatterAdapter);
StringWriter writer = new StringWriter();
marshaller.marshal(input, writer);
return writer.toString();
} catch (JAXBException exception) {
LOGGER.error("Unable to marshall the given input Deals: {}, into String using JAXB Context: {}, ... ", input, jaxbContext, exception);
}
return null;
}

Related

Specify an adapter for multiple xsd types

I'm trying to map different formats onto 3 xsd types, xs:dateTime, xs:date, and xs:time. I'm doing a bit of codegen in my project, and do not have a bindings file, though I do have a package-info.
#javax.xml.bind.annotation.adapters.XmlJavaTypeAdapters({
#javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter(value = com.codegen.pojo.lib.adapters.XmlDateTimeFormatter.class, type = com.codegen.pojo.lib.types.IXmlDateTime.class)
})
#javax.xml.bind.annotation.XmlSchemaTypes({
#javax.xml.bind.annotation.XmlSchemaType(name = "dateTime", type = com.codegen.pojo.lib.types.XmlDateTime.class),
#javax.xml.bind.annotation.XmlSchemaType(name = "date", type = com.codegen.pojo.lib.types.XmlDate.class),
#javax.xml.bind.annotation.XmlSchemaType(name = "time", type = com.codegen.pojo.lib.types.XmlTime.class)
})
#com.codegen.pojo.lib.annotations.XMLDateTimeFormat(format = "yyyy-MM-dd'T'HH:mm:ss.SSSXXXX")
#com.codegen.pojo.lib.annotations.XMLDateFormat(format = "yyyy-MM-dd'T'")
#com.codegen.pojo.lib.annotations.XMLTimeFormat(format = "'T'HH:mm:ss.SSSXXXX")
package my_pkg;
All 3 of the Xml(Date/Time) classes mentioned in the XmlSchemaType annotations implement the IXmlDateTime interface.
The adapter:
public class XmlDateTimeFormatter extends XmlAdapter<String, IXmlDateTime> {
private final DateTimeFormatter dateTimeFormat;
private final DateTimeFormatter dateFormat;
private final DateTimeFormatter timeFormat;
public XmlDateTimeFormatter(String dateTimeFormatPattern, String dateFormatPattern, String timeFormatPattern) {
dateTimeFormat = dateTimeFormatPattern == null ? null : DateTimeFormatter.ofPattern(dateTimeFormatPattern);
dateFormat = dateFormatPattern == null ? null : DateTimeFormatter.ofPattern(dateFormatPattern);
timeFormat = timeFormatPattern == null ? null : DateTimeFormatter.ofPattern(timeFormatPattern);
}
#Override
public String marshal(IXmlDateTime dateTime) throws Exception {
if (dateTime instanceof XmlDateTime) {
return dateTimeFormat.format(dateTime.getCalendar().toGregorianCalendar().toZonedDateTime());
} else if (dateTime instanceof XmlDate) {
return dateFormat.format(dateTime.getCalendar().toGregorianCalendar().toZonedDateTime());
} else if (dateTime instanceof XmlTime) {
return timeFormat.format(dateTime.getCalendar().toGregorianCalendar().toZonedDateTime());
}
return null;
}
#Override
public IXmlDateTime unmarshal(String dateTime) throws Exception {
XMLGregorianCalendar calendar = DatatypeFactory.newInstance().newXMLGregorianCalendar(dateTime);
QName xmlFormat = calendar.getXMLSchemaType();
if (xmlFormat.equals(DatatypeConstants.DATETIME)) {
return new XmlDateTime(calendar);
} else if (xmlFormat.equals(DatatypeConstants.DATE)) {
return new XmlDate(calendar);
} else if (xmlFormat.equals(DatatypeConstants.TIME)) {
return new XmlTime(calendar);
}
return null;
}
The adapter is added to a Marshaller/Unmarshaller created from a JAXBContext. Unfortunately, neither the marshal or unmarshal get called. Any thoughts on how I could fix this?

Jackson ObjectMapper from MongoDB BSON

I got a JSON that I serialize to a MongoDB BasicDBObject and insert it into the DB:
String serialized = "";
try {
serialized = OBJECT_MAPPER.writeValueAsString(customEx.getOut().getBody());
} catch (JsonProcessingException e) {
e.printStackTrace();
}
collection.update(upsertQuery, BasicDBObject.parse(serialized), true, false);
On reading the DBObject from the DB I want to convert it to a POJO using the ObjectMappers' readValue() with a given class:
public static <T> T fromDB(DBObject o, Class<T> clazz) {
try {
return OBJECT_MAPPER.readValue(o.toString(), clazz);
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
The class I want to convert it to is generated from an xsd sheme and also contains timestamps/long values as following:
#XmlRootElement(name = "ItemType")
public class ItemType {
#XmlSchemaType(name = "dateTime")
protected XMLGregorianCalendar date;
[...]
However this worked fine for older Java MongoDB versions. Now the long values are serialized as BSON looking like this:
"date": {
"$numberLong": "1551172199214"
}
When I try to deserialize this using jacksons ObjectMapper I get
Cannot deserialize instance of javax.xml.datatype.XMLGregorianCalendar out of START_OBJECT token
The reason for this is clear to me because the long value is in an own BSON - Style object.
So far I already tried using BsonDocument like this:
public static <T> T fromDB(DBObject o, Class<T> clazz) {
try {
BsonDocument parse = BsonDocument.parse(o.toString());
return OBJECT_MAPPER.readValue(parse.toJson(), clazz);
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
But that still does not convert the BSON parts to JSON.
Is there any way to deserialize the BSON to a given Class using Jacksons ObjectMapper?
Or just convert it to a DBObject without using BSON parts?
If calendar field is wrapped we need to unwrap it. We can extend already implemented CoreXMLDeserializers.GregorianCalendarDeserializer:
class XmlGregorianCalendarDeserializer extends CoreXMLDeserializers.GregorianCalendarDeserializer {
#Override
public XMLGregorianCalendar deserialize(JsonParser jp, DeserializationContext ctxt)
throws IOException {
jp.nextToken(); // Skip FIELD_NAME
jp.nextToken(); // Skip VALUE_STRING
XMLGregorianCalendar calendar = super.deserialize(jp, ctxt);
jp.nextToken(); // Skip END_OBJECT
return calendar;
}
}
If XMLGregorianCalendar is always wrapped we can register this deserialiser using SimpleModule. See below example:
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ext.CoreXMLDeserializers;
import com.fasterxml.jackson.databind.module.SimpleModule;
import java.io.IOException;
import javax.xml.datatype.XMLGregorianCalendar;
public class Test {
public static void main(String[] args) throws Exception {
SimpleModule wrappedCalendarModule = new SimpleModule();
wrappedCalendarModule.addDeserializer(XMLGregorianCalendar.class, new XmlGregorianCalendarDeserializer());
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(wrappedCalendarModule);
String json = "{\n"
+ " \"date\":{\n"
+ " \"$numberLong\":\"1551172199214\"\n"
+ " },\n"
+ " \"name\":\"Rick\"\n"
+ "}";
System.out.println(mapper.readValue(json, Wrapper.class));
}
}
class Wrapper {
private XMLGregorianCalendar date;
private String name;
// getters, setters, toString
}
Above code prints:
Wrapper{date=2019-02-26T09:09:59.214Z, name='Rick'}
It looks like you need to configure the ObjectMapper to deserialize the XMLGregorianCalendar differently:
public class XMLGregorianCalendarDeserializer extends JsonDeserializer<XMLGregorianCalendar> {
private ObjectMapper objectMapper = new ObjectMapper();
#Override
public XMLGregorianCalendar deserialize(
JsonParser jsonParser, DeserializationContext deserializationContext)
throws IOException, JsonProcessingException {
Map<String, String> bsonStringAsMap =
objectMapper.readValue(
jsonParser.readValueAsTree().toString(), new TypeReference<Map<String, String>>() {});
String timestampString = bsonStringAsMap.get("$numberLong");
if (timestampString != null && !timestampString.isEmpty()) {
long timestamp = Long.parseLong(timestampString);
Date date = new Date(timestamp);
GregorianCalendar gregorianCalendar = new GregorianCalendar();
gregorianCalendar.setTime(date);
return DatatypeFactory.newInstance().newXMLGregorianCalendar(gregorianCalendar);
}
return null;
}
}
Then you can annotate the field with this and deserialize as you have been:
#JsonDeserialize(using=XMLGregorianCalendarDeserializer.class)
#XmlSchemaType(name = "dateTime")
protected XMLGregorianCalendar date;

Date field change during Retrofit uploads

I am using Retrofit2 for upload SOAP request. Date automatically changes during the upload process in 10 out 1000 requests. I have investigated a lot and pin point the issue when I started logging the call with interceptor.
Log Printed before interceptor
fromTime:2018-10-13T13:22:00
isDeleted:false
name:Unknown visitor
reason:Unknown reason
toTime:2018-10-13T23:59:59
vrm:ABC
Log Printed in Interceptor.
FromTime:2018-10-20T11:59:00
Name:Unknown visitor
Reason:Unknown reason
ToTime:2018-10-13T23:59:59
Vrm:ABC
isDeleted:false
It can be seen fromTime:2018-10-13T13:22:00 changes to FromTime:2018-10-20T11:59:00.
Build request is as follow
public EnvelopeSender build() {
OkHttpClient client = new OkHttpClient.Builder().addInterceptor(new EnvelopInterceptor()).build();
Retrofit retrofit = new Retrofit.Builder().client(client)
.baseUrl(url)
.addConverterFactory(SimpleXmlConverterFactory.create(serializer))
.addCallAdapterFactory(SynchronousCallAdapterFactory.create())
.build();
return retrofit.create(EnvelopeSender.class);
}
Serialisation factory is as follow.
public class XMLSerializerFactory {
private Strategy strategy;
private DateFormat format;
private RegistryMatcher registryMatcher;
public Serializer getSerializer() {
if (strategy == null && registryMatcher == null) {
return new Persister();
} else if (strategy == null) {
return new Persister(registryMatcher);
} else if (registryMatcher == null) {
return new Persister(strategy);
} else {
return new Persister(strategy, registryMatcher);
}
}
public XMLSerializerFactory withStrategy() {
strategy = new AnnotationStrategy();
return this;
}
public XMLSerializerFactory withDateFormat() {
format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.UK);
return this;
}
public XMLSerializerFactory withRegistryMatcher() {
if (format == null)
format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.UK);
registryMatcher = new RegistryMatcher();
registryMatcher.bind(Date.class, new DateFormatTransformer(format));
return this;
}
}
public class DateFormatTransformer implements Transform<Date> {
private DateFormat dateFormat;
public DateFormatTransformer(DateFormat dateFormat) {
this.dateFormat = dateFormat;
}
#Override
public Date read(String value) throws Exception {
return dateFormat.parse(value);
}
#Override
public String write(Date value) throws Exception {
return dateFormat.format(value);
}
}
Is it bug or I am doing something wrong?
Thanks in advance.

Java - Convert JSON to XML with values on root tag

I need to convert JSON to XML format with values in the root tag
I have and Orden class configured with #JsonRootName("orden") and added #JsonProperty to all properties.
Actually, I have an inplementation that converts JSON to XML but wich "values in child nodes".
This is my converter implementation:
public static String convertJsonObjectToXml (Object obj) throws JsonProcessingException {
ObjectMapper mapper = new ObjectMapper();
mapper.configure(SerializationFeature.WRAP_ROOT_VALUE, true);
mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
String jsonString = mapper.writeValueAsString(obj);
JSONObject json = new JSONObject(jsonString);
String xml = XML.toString(json);
return xml;
}
The result of that implementation is this:
<orden>
<formaOp>C</formaOp>
<agente>109</agente>
<tipo>C</tipo>
<precio>2.5</precio>
<tipoVenc>72</tipoVenc>
<idOrigen>156934</idOrigen>
<instrumento>TS</instrumento>
<ejecucion>SINCRONICA</ejecucion>
<agenteCtpte>3</agenteCtpte>
<comitente>0</comitente>
<fechaOrigen>2013-10-09T08:04:13</fechaOrigen>
<cantidad>10</cantidad>
</orden>
But I need the shorter, unfriendly or whatever format's name of the XML below
<orden idOrigen="156934" fechaOrigen="2014-02-19T15:11:44.000-03:00"
agente="109" agenteCtpte="3" tipo="C" ejecucion="SINCRONICA" instrumento="TS"
cantidad="10" precio="2.5" formaOp="C" tipoVenc="72"/>
Any Ideas? Thanks!
Based on the comment posted by Andreas, I used JAXB and my Orden class now is hybrid (can be converted from JSON or XML).
This is how the object is configured:
#XmlRootElement
public class IngresarOrden extends DatosOferta {
private String accion;
private ModoEjecucion ejecucion;
private String operador;
#XmlAttribute
public String getAccion() {
return accion;
}
public void setAccion(String accion) {
this.accion = accion;
}
#XmlAttribute
public ModoEjecucion getEjecucion() {
return ejecucion;
}
public void setEjecucion(ModoEjecucion ejecucion) {
this.ejecucion = ejecucion;
}
#XmlAttribute
public String getOperador() {
return operador;
}
public void setOperador(String operador) {
this.operador = operador;
}
The #XmlAttribute is the annotation needed to put the values in the root tag.
This is the full converter implementation:
public static <T> String converToXml (T obj) throws CommonsException {
JAXBContext jaxbContext;
String xml = null;
try {
OutputStream os = new ByteArrayOutputStream();
jaxbContext = JAXBContext.newInstance(obj.getClass());
Marshaller jaxbMarshaller = jaxbContext.createMarshaller();
jaxbMarshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
jaxbMarshaller.marshal(obj, os);
xml = os.toString();
} catch (JAXBException e) {
logger.error("XML converter exception: ", e);
throw new CommonsException("XML converter exception: ", e);
}
return xml;
}
Thanks!

How to deserialize a float value with a localized decimal separator with Jackson

The input stream I am parsing with Jackson contains latitude and longitude values such as here:
{
"name": "product 23",
"latitude": "52,48264",
"longitude": "13,31822"
}
For some reason the server uses commas as the decimal separator which produces an InvalidFormatException. Since I cannot change the server output format I would like to teach Jackson's ObjectMapper to handle those cases. Here is the relevant code:
public static Object getProducts(final String inputStream) {
ObjectMapper objectMapper = new ObjectMapper();
try {
return objectMapper.readValue(inputStream,
new TypeReference<Product>() {}
);
} catch (UnrecognizedPropertyException e) {
e.printStackTrace();
} catch (InvalidFormatException e) {
e.printStackTrace();
} catch (JsonMappingException e) {
e.printStackTrace();
} catch (JsonParseException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
And here is the POJO:
import com.fasterxml.jackson.annotation.JsonProperty;
public class Product {
#JsonProperty("name")
public String name;
#JsonProperty("latitude")
public float latitude;
#JsonProperty("longitude")
public float longitude;
}
How can I tell Jackson that those coordinate values come with a German locale?
I suppose a custom deserializer for the specific fields as discussed here would be the way to go. I drafted this:
public class GermanFloatDeserializer extends JsonDeserializer<Float> {
#Override
public Float deserialize(JsonParser parser, DeserializationContext context)
throws IOException {
// TODO Do some comma magic
return floatValue;
}
}
Then the POJO would look like this:
import com.fasterxml.jackson.annotation.JsonProperty;
public class Product {
#JsonProperty("name")
public String name;
#JsonDeserialize(using = GermanFloatDeserializer.class, as = Float.class)
#JsonProperty("latitude")
public float latitude;
#JsonDeserialize(using = GermanFloatDeserializer.class, as = Float.class)
#JsonProperty("longitude")
public float longitude;
}
I came up with the following solution:
public class FlexibleFloatDeserializer extends JsonDeserializer<Float> {
#Override
public Float deserialize(JsonParser parser, DeserializationContext context)
throws IOException {
String floatString = parser.getText();
if (floatString.contains(",")) {
floatString = floatString.replace(",", ".");
}
return Float.valueOf(floatString);
}
}
...
public class Product {
#JsonProperty("name")
public String name;
#JsonDeserialize(using = FlexibleFloatDeserializer.class)
#JsonProperty("latitude")
public float latitude;
#JsonDeserialize(using = FlexibleFloatDeserializer.class)
#JsonProperty("longitude")
public float longitude;
}
Still I wonder why I it does not work when I specify the return value class as as = Float.class as can be found in the documentation of JsonDeserialize. It reads as if I am supposed to use one or the other but not both. Whatsoever, the docs also claim that as = will be ignored when using = is defined:
if using() is also used it has precedence (since it directly specified deserializer, whereas this would only be used to locate the deserializer) and value of this annotation property is ignored.
With all respect to accepted answer, there is a way to get rid of those #JsonDeserialize annotations.
You need to register the custom deserializer in the ObjectMapper.
Following the tutorial from official web-site you just do something like:
ObjectMapper mapper = new ObjectMapper();
SimpleModule testModule = new SimpleModule(
"DoubleCustomDeserializer",
new com.fasterxml.jackson.core.Version(1, 0, 0, null))
.addDeserializer(Double.class, new JsonDeserializer<Double>() {
#Override
public Double deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException {
String valueAsString = jp.getValueAsString();
if (StringUtils.isEmpty(valueAsString)) {
return null;
}
return Double.parseDouble(valueAsString.replaceAll(",", "\\."));
}
});
mapper.registerModule(testModule);
If you're using Spring Boot there is a simpler method. Just define the Jackson2ObjectMapperBuilder bean somewhere in your Configuration class:
#Bean
public Jackson2ObjectMapperBuilder jacksonBuilder() {
Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder();
builder.deserializerByType(Double.class, new JsonDeserializer<Double>() {
#Override
public Double deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException {
String valueAsString = jp.getValueAsString();
if (StringUtils.isEmpty(valueAsString)) {
return null;
}
return Double.parseDouble(valueAsString.replaceAll(",", "\\."));
}
});
builder.applicationContext(applicationContext);
return builder;
}
and add the custom HttpMessageConverter to the list of WebMvcConfigurerAdapter message converters:
messageConverters.add(new MappingJackson2HttpMessageConverter(jacksonBuilder().build()));
A more general solution than the other proposed answers, which require registering individual deserializers for each type, is to provide a customized DefaultDeserializationContext to ObjectMapper.
The following implementation (which is inspired by DefaultDeserializationContext.Impl) worked for me:
class LocalizedDeserializationContext extends DefaultDeserializationContext {
private final NumberFormat format;
public LocalizedDeserializationContext(Locale locale) {
// Passing `BeanDeserializerFactory.instance` because this is what happens at
// 'jackson-databind-2.8.1-sources.jar!/com/fasterxml/jackson/databind/ObjectMapper.java:562'.
this(BeanDeserializerFactory.instance, DecimalFormat.getNumberInstance(locale));
}
private LocalizedDeserializationContext(DeserializerFactory factory, NumberFormat format) {
super(factory, null);
this.format = format;
}
private LocalizedDeserializationContext(DefaultDeserializationContext src, DeserializationConfig config, JsonParser parser, InjectableValues values, NumberFormat format) {
super(src, config, parser, values);
this.format = format;
}
#Override
public DefaultDeserializationContext with(DeserializerFactory factory) {
return new LocalizedDeserializationContext(factory, format);
}
#Override
public DefaultDeserializationContext createInstance(DeserializationConfig config, JsonParser parser, InjectableValues values) {
return new LocalizedDeserializationContext(this, config, parser, values, format);
}
#Override
public Object handleWeirdStringValue(Class<?> targetClass, String value, String msg, Object... msgArgs) throws IOException {
// This method is called when default deserialization fails.
if (targetClass == float.class || targetClass == Float.class) {
return parseNumber(value).floatValue();
}
if (targetClass == double.class || targetClass == Double.class) {
return parseNumber(value).doubleValue();
}
// TODO Handle `targetClass == BigDecimal.class`?
return super.handleWeirdStringValue(targetClass, value, msg, msgArgs);
}
// Is synchronized because `NumberFormat` isn't thread-safe.
private synchronized Number parseNumber(String value) throws IOException {
try {
return format.parse(value);
} catch (ParseException e) {
throw new IOException(e);
}
}
}
Now set up your object mapper with your desired locale:
Locale locale = Locale.forLanguageTag("da-DK");
ObjectMapper objectMapper = new ObjectMapper(null,
null,
new LocalizedDeserializationContext(locale));
If you use Spring RestTemplate, you can set it up to use objectMapper like so:
RestTemplate template = new RestTemplate();
template.setMessageConverters(
Collections.singletonList(new MappingJackson2HttpMessageConverter(objectMapper))
);
Note that the value must be represented as a string in the JSON document (i.e. {"number": "2,2"}), since e.g. {"number": 2,2} is not valid JSON and will fail to parse.

Categories