Jesse Nelson
Jesse Nelson

Reputation: 796

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')

Upvotes: 0

Views: 6086

Answers (2)

Manojkumar Khotele
Manojkumar Khotele

Reputation: 1019

This does work.

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlText;

import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;
import java.io.IOException;
import java.io.StringReader;
import java.util.List;

public class XmlParserDemo {
public static void main(String[] args) throws IOException, XMLStreamException {
    String xmlString = "<properties>\n" +
            "    <entry key=\"token\">rent</entry>\n" +
            "    <entry key=\"affid\">true</entry>\n" +
            "    <entry key=\"domain\">checking</entry>\n" +
            "</properties>";
    XMLStreamReader sr = null;
    sr = XMLInputFactory.newFactory().createXMLStreamReader(new StringReader(xmlString));
    sr.next();
    XmlMapper mapper = new XmlMapper();
    List<Entry> entries = mapper.readValue(sr, new TypeReference<List<Entry>>() {
    });
    sr.close();
    entries.forEach(e ->
            System.out.println(e.key + ":" + e.value));

}

public static class Entry {
    @JacksonXmlProperty(isAttribute = true, localName = "key")
    private String key;
    @JacksonXmlText
    private String value;

    public String getKey() {
        return key;
    }

    public void setKey(String key) {
        this.key = key;
    }

    public String getValue() {
        return value;
    }

    public void setValue(String value) {
        this.value = value;
    }
}
}

Output is:

token:rent
affid:true
domain:checking

Upvotes: 2

Jesse Nelson
Jesse Nelson

Reputation: 796

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();
    }
}

Upvotes: 0

Related Questions