javaspring-bootjackson-dataformat-xmlxmlmapper

Jackson xml map attribute value to property


I am integrating with an old system and have a need to parse the following xml into my object. I am trying to do this with jackson but I can't get the mapping to work. Anyone know how to map the following xml to the pojo?

@JacksonXmlRootElement(localName = "properties")
@Data
public class Example {
    private String token;
    private String affid;
    private String domain;
}

xml example:

<properties>
    <entry key="token">rent</entry>
    <entry key="affid">true</entry>
    <entry key="domain">checking</entry>
</properties>

I have tried adding

@JacksonXmlProperty(isAttribute = true, localName = "key")

to the properties but this of course doesn't work and I do not see another way to get this to work. Any ideas?

I am using the mapper like so...

ObjectMapper xmlMapper = new XmlMapper();
dto = xmlMapper.readValue(XML_STRING, Example .class);

I am using the following dependencies

compile('org.springframework.boot:spring-boot-starter-web')
runtime('org.springframework.boot:spring-boot-devtools')
compileOnly('org.projectlombok:lombok')
testCompile('org.springframework.boot:spring-boot-starter-test')
compile('org.apache.commons:commons-lang3:3.5')
compile('com.fasterxml.jackson.dataformat:jackson-dataformat-xml')
compile('com.squareup.okhttp3:okhttp:3.10.0')

Solution

  • I have looked through Jackson thoroughly and it doesn't seem that there is a way to accomplish this. However, I will share my solution here in case it is useful to someone else.

    package com.example.config;
    
    import com.example.dto.Example;
    import lombok.extern.slf4j.Slf4j;
    import org.apache.commons.lang3.StringUtils;
    import org.springframework.http.HttpInputMessage;
    import org.springframework.http.HttpOutputMessage;
    import org.springframework.http.MediaType;
    import org.springframework.http.converter.AbstractHttpMessageConverter;
    import org.springframework.http.converter.HttpMessageNotReadableException;
    import org.springframework.http.converter.HttpMessageNotWritableException;
    import org.springframework.http.converter.StringHttpMessageConverter;
    import org.w3c.dom.Node;
    import org.xml.sax.InputSource;
    
    import javax.xml.xpath.XPath;
    import javax.xml.xpath.XPathConstants;
    import javax.xml.xpath.XPathExpressionException;
    import javax.xml.xpath.XPathFactory;
    import java.io.IOException;
    import java.io.Reader;
    import java.io.StringReader;
    
    public class Converter extends AbstractHttpMessageConverter<Example> {
        private static final XPath XPATH_INSTANCE = XPathFactory.newInstance().newXPath();
        private static final StringHttpMessageConverter MESSAGE_CONVERTER = new StringHttpMessageConverter();
    
        @Override
        protected boolean supports(Class<?> aClass) {
            return aClass == Example.class;
        }
    
        @Override
        protected Example readInternal(Class<? extends LongFormDTO> aClass, HttpInputMessage httpInputMessage) throws IOException, HttpMessageNotReadableException {
            String responseString = MESSAGE_CONVERTER.read(String.class, httpInputMessage);
            Reader xmlInput = new StringReader(responseString);
            InputSource inputSource = new InputSource(xmlInput);
            Example dto = new Example();
            Node xml;
    
            try {
                xml  = (Node) XPATH_INSTANCE.evaluate("/properties", inputSource, XPathConstants.NODE);
            } catch (XPathExpressionException e) {
                log.error("Unable to parse  response", e);
                return dto;
            }
    
            log.info("processing populate application response={}", responseString);
    
            dto.setToken(getString("token", xml));
            dto.setAffid(getInt("affid", xml, 36));
            dto.domain(getString("domain", xml));
    
            xmlInput.close();
            return dto;
        }
    
        private String getString(String propName, Node xml, String defaultValue) {
            String xpath = String.format("//entry[@key='%s']/text()", propName);
            try {
                String value = (String) XPATH_INSTANCE.evaluate(xpath, xml, XPathConstants.STRING);
                return StringUtils.isEmpty(value) ? defaultValue : value;
            } catch (XPathExpressionException e) {
                log.error("Received error retrieving property={} from xml", propName, e);
            }
            return defaultValue;
        }
    
        private String getString(String propName, Node xml) {
            return getString(propName, xml, null);
        }
    
        private int getInt(String propName, Node xml, int defaultValue) {
            String stringValue = getString(propName, xml);
            if (!StringUtils.isEmpty(stringValue)) {
                try {
                    return Integer.parseInt(stringValue);
                } catch (NumberFormatException e) {
                    log.error("Attempted to parse value={} as integer but received error", stringValue, e);
                }
            }
            return defaultValue;
        }
    
        private int getInt(String propName, Node xml) {
            return getInt(propName, xml,0);
        }
    
        private boolean getBoolean(String propName, Node xml) {
            String stringValue = getString(propName, xml );
            return Boolean.valueOf(stringValue);
        }
    
        @Override
        protected void writeInternal(Example dto, HttpOutputMessage httpOutputMessage) throws IOException, HttpMessageNotWritableException {
            throw new UnsupportedOperationException("Responses of type=" + MediaType.TEXT_PLAIN_VALUE + " are not supported");
        }
    }
    

    I chose to hide this in a message converter so I don't have to look at it again but you can apply these steps where you see fit. If you choose this route, you will need to configure a rest template to use this converter. If not, it is important to cache the xml into a Node object as regenerating each time will be very costly.

    package com.example.config;
    
    import org.springframework.boot.web.client.RestTemplateBuilder;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.context.annotation.Primary;
    import org.springframework.http.MediaType;
    import org.springframework.http.client.OkHttp3ClientHttpRequestFactory;
    import org.springframework.web.client.RestTemplate;
    
    import java.util.ArrayList;
    import java.util.Arrays;
    import java.util.Collections;
    import java.util.List;
    
    @Configuration
    public class RestConfig { 
        @Bean
        @Primary
        public RestTemplate restTemplate() {
            return new RestTemplate(new OkHttp3ClientHttpRequestFactory());
        }
    
        @Bean
        public RestTemplate restTemplateLe(RestTemplateBuilder builder) {
            List<HttpMessageConverter<?>> messageConverters = new ArrayList<>();
            ExampleConverter exampleConverter = new ExampleConverter();
            exampleConverter.setSupportedMediaTypes(Collections.singletonList(MediaType.TEXT_PLAIN));
            messageConverters.add(exampleConverter);
    
            return builder.messageConverters(messageConverters)
                          .requestFactory(new OkHttp3ClientHttpRequestFactory())
                          .build();
        }
    }