Is there a generic way of reading complex XML using SaxParser? - java

I am using SaxParser to read the large complex XML file. I do not wish to create the model class as I do not know the exact data which will be coming in the XML so I am trying to find if there is a generic way of reading the XML data using some sort of Context.
I have used a similar approach for JSON using the Jackson, which worked very well for me. Since I am new to Sax Parser, I cannot completely understand how to achieve the same. for complex inner values, I am unable to establish a parent-child relationship and I am unable to build relationships between tags and attributes.
Following is the code I have so far:
ContextNode my generic class to store all XML information using the parent-child relationships.
#Getter
#Setter
#ToString
#NoArgsConstructor
public class ContextNode {
protected String name;
protected String value;
protected ArrayList<ContextNode> children = new ArrayList<>();
protected ContextNode parent;
//Constructor 1: To store the simple field information.
public ContextNode(final String name, final String value) {
this.name = name;
this.value = value;
}
//Constructor 2: To store the complex field which has inner elements.
public ContextNode(final ContextNode parent, final String name, final String value) {
this(name, value);
this.parent = parent;
}
Following is my method to parse XML using SAX within EventReader.class
public class EventReader{
//Method to read XML events and create pre-hash string from it.
public static void xmlParser(final InputStream xmlStream) {
final SAXParserFactory factory = SAXParserFactory.newInstance();
try {
final SAXParser saxParser = factory.newSAXParser();
final SaxHandler handler = new SaxHandler();
saxParser.parse(xmlStream, handler);
} catch (ParserConfigurationException | SAXException | IOException e) {
e.printStackTrace();
}
}
}
Following is my SaxHandler:
import org.xml.sax.Attributes;
import org.xml.sax.helpers.DefaultHandler;
import java.util.HashMap;
public class SaxHandler extends DefaultHandler {
private final List<String> XML_IGNORE_FIELDS = Arrays.asList("person:personDocument","DocumentBody","DocumentList");
private final List<String> EVENT_TYPES = Arrays.asList("person");
private Map<String, String> XML_NAMESPACES = null;
private ContextNode contextNode = null;
private StringBuilder currentValue = new StringBuilder();
#Override
public void startDocument() {
ConstantEventInfo.XML_NAMESPACES = new HashMap<>();
}
#Override
public void startElement(final String uri, final String localName, final String qName, final Attributes attributes) {
//For every new element in XML reset the StringBuilder.
currentValue.setLength(0);
if (qName.equalsIgnoreCase("person:personDocument")) {
// Add the attributes and name-spaces to Map
for (int att = 0; att < attributes.getLength(); att++) {
if (attributes.getQName(att).contains(":")) {
//Find all Namespaces within the XML Header information and save it to the Map for future use.
XML_NAMESPACES.put(attributes.getQName(att).substring(attributes.getQName(att).indexOf(":") + 1), attributes.getValue(att));
} else {
//Find all other attributes within XML and store this information within Map.
XML_NAMESPACES.put(attributes.getQName(att), attributes.getValue(att));
}
}
} else if (EVENT_TYPES.contains(qName)) {
contextNode = new ContextNode("type", qName);
}
}
#Override
public void characters(char ch[], int start, int length) {
currentValue.append(ch, start, length);
}
#Override
public void endElement(final String uri, final String localName, final String qName) {
if (!XML_IGNORE_FIELDS.contains(qName)) {
if (!EVENT_TYPES.contains(qName)) {
System.out.println("QName : " + qName + " Value : " + currentValue);
contextNode.children.add(new ContextNode(qName, currentValue.toString()));
}
}
}
#Override
public void endDocument() {
System.out.println(contextNode.getChildren().toString());
System.out.println("End of Document");
}
}
Following is my TestCase which will call the method xmlParser
#Test
public void xmlReader() throws Exception {
final InputStream xmlStream = getClass().getResourceAsStream("/xmlFileContents.xml");
EventReader.xmlParser(xmlStream);
}
Following is the XML I need to read using a generic approach:
<?xml version="1.0" ?>
<person:personDocument xmlns:person="https://example.com" schemaVersion="1.2" creationDate="2020-03-03T13:07:51.709Z">
<DocumentBody>
<DocumentList>
<Person>
<bithTime>2020-03-04T11:00:30.000+01:00</bithTime>
<name>Batman</name>
<Place>London</Place>
<hobbies>
<hobby>painting</hobby>
<hobby>football</hobby>
</hobbies>
<jogging distance="10.3">daily</jogging>
<purpose2>
<id>1</id>
<purpose>Dont know</purpose>
</purpose2>
</Person>
</DocumentList>
</DocumentBody>
</person:personDocument>

Providing the answer as it can be helpful to someone in the future:
First we need to create a class ContextNode which can hold the information:
#Getter
#Setter
public class ContextNode {
protected String name;
protected String value;
protected ArrayList<ContextNode> attributes = new ArrayList<>();
protected ArrayList<ContextNode> children = new ArrayList<>();
protected ContextNode parent;
protected Map<String, String> namespaces;
public ContextNode(final ContextNode parent, final String name, final String value) {
this.parent = parent;
this.name = name;
this.value = value;
this.namespaces = parent.namespaces;
}
public ContextNode(final Map<String, String> namespaces) {
this.namespaces = namespaces;
}
public ContextNode(final Map<String, String> namespaces) {
this.namespaces = namespaces;
}
}
Then we can read the XML and store the information in the context node:
import lombok.Getter;
import org.xml.sax.Attributes;
import org.xml.sax.helpers.DefaultHandler;
import java.security.NoSuchAlgorithmException;
import java.util.*;
public class SaxHandler extends DefaultHandler {
//Variables needed to store the required information during the parsing of the XML document.
private final Deque<String> path = new ArrayDeque<>();
private final StringBuilder currentValue = new StringBuilder();
private ContextNode currentNode = null;
private ContextNode rootNode = null;
private Map<String, String> currentAttributes;
private final HashMap<String, String> contextHeader = new HashMap<>();
#Override
public void startElement(final String uri, final String localName, final String qName, final Attributes attributes) {
//Put every XML tag within the stack at the beginning of the XML tag.
path.push(qName);
//Reset attributes for every element
currentAttributes = new HashMap<>();
//Get the path from Deque as / separated values.
final String p = path();
//If the XML tag contains the Namespaces or attributes then add to respective Namespaces Map or Attributes Map.
if (attributes.getLength() > 0) {
//Loop over every attribute and add them to respective Map.
for (int att = 0; att < attributes.getLength(); att++) {
//If the attributes contain the : then consider them as namespaces.
if (attributes.getQName(att).contains(":") && attributes.getQName(att).startsWith("xmlns:")) {
contextHeader.put(attributes.getQName(att).substring(attributes.getQName(att).indexOf(":") + 1), attributes.getValue(att));
} else {
currentAttributes.put(attributes.getQName(att), attributes.getValue(att).trim());
}
}
}
if (rootNode == null) {
rootNode = new ContextNode(contextHeader);
currentNode = rootNode;
rootNode.children.add(new ContextNode(rootNode, "type", qName));
} else if (currentNode != null) {
ContextNode n = new ContextNode(currentNode, qName, (String) null);
currentNode.children.add(n);
currentNode = n;
}
}
#Override
public void characters(char[] ch, int start, int length) {
currentValue.append(ch, start, length);
}
#Override
public void endElement(final String uri, final String localName, final String qName) {
try {
System.out.println("completed reading");
System.out.println(rootNode);
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
rootNode = null;
//At the end of the XML element tag reset the value for next element.
currentValue.setLength(0);
//After completing the particular element reading, remove that element from the stack.
path.pop();
}
private String path() {
return String.join("/", this.path);
}
}
You may need to make some additional changes based on your particular requirement. This is just a sample that gives some idea.

Related

How to parse whole XML document using SAX parser in Java

So Im pretty new to Java coming from C#! They are pretty similar programming languages, so im getting a hang of it quickly, but there is a problem I have been battling with for a while, that I hope you can help me solve!
So Im using my SAX parser to parse the XML document, and it works fine, but Im having problems parsing the whole xml document, and don't know how to parse the attribute value in the top element.
My xml document is as follows:
This is the code snippet where I believe the problem lies! This code works for parsing all of tecaj elements and their attributes/content values, but not "datum" attribute:
public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException
{
this.elementStack.push(qName);
CurrencyModel model = new CurrencyModel();
if("tecajnica".equals(qName))
{
if(attributes != null)
{
model.setDatum(attributes.getValue(0));
}
}
else if("tecaj".equals(qName))
{
if(attributes != null)
{
model.setOznaka(attributes.getValue(0));
model.setSifra(attributes.getValue(1));
}
}
this.objectStack.push(model);
}
So I have a model class that looks like this:
public class CurrencyModel
{
public String getDatum() {
return datum;
}
public void setDatum(String datum) {
this.datum = datum;
}
public String getOznaka() {
return oznaka;
}
public void setOznaka(String oznaka) {
this.oznaka = oznaka;
}
public String getSifra() {
return sifra;
}
public void setSifra(String sifra) {
this.sifra = sifra;
}
public double getValue() {
return value;
}
public void setValue(double value) {
this.value = value;
}
String datum;
String oznaka;
String sifra;
double value;
#Override
public String toString() {
return "CurrencyModel{" +
"datum=" + datum +
", oznaka='" + oznaka + '\'' +
", sifra='" + sifra + '\'' +
", value=" + value +
'}';
}
}
So each object of type CurrencyModel has its date property that is supposed to get the value of the attribute from its respected "tecajnica" element. It works for all of the other properties but "Datum". At first I was parsing it as Date type, but as that didn't work I tried parsing it as a String. Now it works without any errors, but always sets the object "Datum" property to null...
Output looks as follows:
Any help and suggestions will be much appreciated!!! Thank you in advance!
You can use JAXB parser instead of SAX, It converts each element tag into a Java objects and easily configurable too. But we need to create classes for each element tag in the XML file as mentioned in this article https://www.javatpoint.com/jaxb-tutorial
As per your data your root class will be like below:
package com.a.b.c;
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
#XmlRootElement(name="DtecBs")
public class DtecBs {
private String datum;
private List<tecjnica> tecjnicaList;
#XmlAttribute
public String getDatum() {
return datum;
}
public void setDatum(String datum) {
this.datum = datum;
}
#XmlElement(name="tecjnica")
public List<tecjnica> getTecjnicaList() {
return tecjnicaList;
}
public void setTecjnicaList(List<tecjnica> tecjnicaList) {
this.tecjnicaList = tecjnicaList;
}
}
Following method helps to convert XML into JavaObject:
Required parameters are:
InputStream (Inputstream of the XML file)
Class (Class name of the root element in the XML com.a.b.c.DtecBs)
DtecBs dtecbsObj = (DtecBs)convertXmltoJavaObject(is,className);
public Object convertXmltoJavaObject(InputStream is, Class className) throws JAXBException, ParserConfigurationException, SAXException {
//Disable XXE
SAXParserFactory spf = SAXParserFactory.newInstance();
spf.setFeature("http://xml.org/sax/features/external-general-entities", false);
spf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
spf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
//Do unmarshall operation
Source xmlSource = new SAXSource(spf.newSAXParser().getXMLReader(),new InputSource(is));
JAXBContext jaxbContext = JAXBContext.newInstance(className);
Unmarshaller jaxbUnmarshaller = jaxbContext.createUnmarshaller();
JAXBIntrospector ji = jaxbContext.createJAXBIntrospector();
return ji.getValue(jaxbUnmarshaller.unmarshal(xmlSource));
}

Dynamic properties in Java from xml

I'm fiddling around with an idea but can't get a grip on it.
I have an xml file with 100+ properties defining the runtime environment of a somewhat large program. These are exposed as variables through a class . At the moment, for each option in the xml file, there is a variable in the class plus public getter and private setter.
Each time we need a new option, we have to define it in the xml file and create the variable plus methods in the RuntimenEnvironment class.
Now, what I would like to do is something like this: I want to rewrite the class in such a way, that it exposes new options from the xml file as vars without having to touch the class.
My xml file uses this structure:
<option>
<name>theName</name>
<type>eg int</type>
<value>20</value>
<constant>THE_NAME</constant>
</option>
Can I write code in java that dynamically creates the vars at runtime and exposes them through a method without actually writing the method?
Is this possible?
Thanks in advance,
Chris
Couple of options I could think of are:
If the name is unique a map can be populated with name as the key.
If you are interested only in options then a list of Options can be
populated from the XML.
Below is the sample code implemented with SAX parser
Handler Class
public class OptionsParser extends DefaultHandler {
private final StringBuilder valueBuffer = new StringBuilder();
private final Map<String, Option> resultAsMap = new HashMap<String, Option>();
private final List<Option> options = new ArrayList<Option>();
//variable to store the values from xml temporarily
private Option temp;
public List<Option> getOptions() {
return options;
}
public Map<String, Option> getResultAsMap() {
return resultAsMap;
}
#Override
public void startElement(final String uri, final String localName, final String qName,
final Attributes attributes) throws SAXException {
if("option".equalsIgnoreCase(qName)) {
temp = new Option();
}
}
#Override
public void endElement(final String uri, final String localName, final String qName)
throws SAXException {
//read the value into a string to set them to option object
final String value = valueBuffer.toString().trim();
switch (qName) {
case "name":
temp.setName(value);
// set the value into map and name of the option is the key
resultAsMap.put(value, temp);
break;
case "type":
temp.setType(value);
break;
case "value":
temp.setValue(value);
break;
case "constant":
temp.setConstant(value);
break;
case "option":
// this is the end of option tag add it to the list
options.add(temp);
temp = null;
break;
default:
break;
}
//reset the buffer after every iteration
valueBuffer.setLength(0);
}
#Override
public void characters(final char[] ch, final int start, final int length)
throws SAXException {
//read the value into a buffer
valueBuffer.append(ch, start, length);
}
}
Option POJO
public class Option {
private String name;
private String type;
private String value;
private String constant;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
public String getConstant() {
return constant;
}
public void setConstant(String constant) {
this.constant = constant;
}
}
Input XML
<options>
<option>
<name>option1</name>
<type>int</type>
<value>20</value>
<constant>const1</constant>
</option>
<option>
<name>option2</name>
<type>string</type>
<value>testValue</value>
<constant>const2</constant>
</option>
</options>
Sample Main class
public class ParseXML {
public static void main(String[] args) {
final OptionsParser handler = new OptionsParser();
try {
SAXParserFactory.newInstance().newSAXParser()
.parse("C:/luna/sample/inputs/options.xml", handler);
} catch (SAXException | IOException | ParserConfigurationException e) {
System.err.println("Somethig went wrong while parsing the input file the exception is -- " + e.getMessage() + " -- ");
}
Map<String, Option> result = handler.getResultAsMap();
Collection<Option> values = result.values();
for (Option option : values) {
System.out.println(option.getName());
}
}
}
I'll talk about json configuration files. But XML should also be similar. Jackson provides way to deserialise JSON and create dynamic object.
If the names of your options (theName) are unique, you can create dynamic beans. Your xml will then look like:
<theName>
<type>eg int</type>
<value>20</value>
<constant>THE_NAME</constant>
</theName>
See, I am talking about json, so its actually:
theName: {
type: "int"
value: 20
constant: "THE_NAME" }
Dynamic beans contain a Map, so your options will be stored in a Map<String, Option>, where Option is a POJO containing type, value and constant fields.
You should be able to access your options by iterating the map. There is no need to create variables dynamically.
This blog entry has got details about how to convert json to POJO

JAXB get raw content of element [duplicate]

I use REST and i was wondering if i can tell jaxb to insert a string field "as-it-is" into the outgoing xml.
Certainly i count unpack it before returning, but i would like to save this step.
#XmlRootElement(name="unnestedResponse")
public class Response{
#Insert annotation here ;-)
private String alreadyXml;
private int otherDate; ...
}
Is there a possability to tell JAXB to just use the String as it is without escapting? I want that the client does not have to parse my response and then parse this field.
greetings,
m
You can use the #XmlAnyElement and specify a DomHandler to keep a portion of the XML document as a String.
Customer
import javax.xml.bind.annotation.*;
#XmlRootElement
public class Customer {
private String bio;
#XmlAnyElement(BioHandler.class)
public String getBio() {
return bio;
}
public void setBio(String bio) {
this.bio = bio;
}
}
BioHandler
import java.io.*;
import javax.xml.bind.ValidationEventHandler;
import javax.xml.bind.annotation.DomHandler;
import javax.xml.transform.Source;
import javax.xml.transform.stream.*;
public class BioHandler implements DomHandler<String, StreamResult> {
private static final String BIO_START_TAG = "<bio>";
private static final String BIO_END_TAG = "</bio>";
private StringWriter xmlWriter = new StringWriter();
public StreamResult createUnmarshaller(ValidationEventHandler errorHandler) {
return new StreamResult(xmlWriter);
}
public String getElement(StreamResult rt) {
String xml = rt.getWriter().toString();
int beginIndex = xml.indexOf(BIO_START_TAG) + BIO_START_TAG.length();
int endIndex = xml.indexOf(BIO_END_TAG);
return xml.substring(beginIndex, endIndex);
}
public Source marshal(String n, ValidationEventHandler errorHandler) {
try {
String xml = BIO_START_TAG + n.trim() + BIO_END_TAG;
StringReader xmlReader = new StringReader(xml);
return new StreamSource(xmlReader);
} catch(Exception e) {
throw new RuntimeException(e);
}
}
}
For More Information
http://blog.bdoughan.com/2011/04/xmlanyelement-and-non-dom-properties.html
Following bdoughan's answer did not work for me as I encountered errors during marshalling when the text contained the '& character (e.g. in URLs or when using HTML entities such as e.g. " ").
I was able to resolve this by changing the custom DomHandler's marshal method to
public Source marshal(String et, ValidationEventHandler veh) {
Node node = new SimpleTextNode(et);
return new DOMSource(node);
}
where SimpleTextNode implements the Node interface as follows:
class SimpleTextNode implements Node {
String nodeValue = "";
#Override
public SimpleTextNode(String nodeValue) {
this.nodeValue = nodeValue;
}
#Override
public short getNodeType() {
return TEXT_NODE;
}
// the remaining methods of the Node interface are not needed during marshalling
// you can just use the code template of your IDE...
...
}
PS: I would have loved to leave this as a comment to bdoughan's answer, but unfortunately I have way too little reputation :-(

Generic Map for JAXB

Ok, so we all know that Maps are somewhat of a pain in JAXB.
I here present an alternative to the current solutions. My main objective is to get feedback on any and all potential problems with this solution. Maybe it is not even a good solution for some reasons.
When I played around with the standard Generic Map Adapter it seemed like the adapters for the classes were not used. The classes are instead scanned, forcing me to mark my data model with JAXB annotations and adding default constructors where I don't want them (I'm talking about complex classes that I want to store in Maps, not simple data types). Above all, this makes my internal data model public thereby breaking encapsulation since the generated XML is a direct representation of the internal structures.
The "workaround" I did was to combine the adapter with the Marshall.Listener and Unmarshall.Listner thereby being able to extract additional annotation information. A field would then be
#XmlElement(name = "testMap")
#XmlJavaTypeAdapter(MapAdapter.class)
#MapKeyValueAdapters(key=SomeComplexClassAdapter.class)
private final HashMap<SomeComplexClass, String> testMap2 = new HashMap<SomeComplexClass, String>();
This additional annotation accepts both key and value as arguments. If omitted the functionality falls back on standard qualification for the omitted. The example above will use the given adapter for the key and standard handling for the value.
Here the annotation.
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import javax.xml.bind.annotation.adapters.XmlAdapter;
/**
* This annotation holds the adapters for the key and value used in the MapAdapter.
*/
#Retention(RUNTIME)
#Target({ FIELD })
public #interface MapKeyValueAdapters {
/**
* Points to the class that converts the value type to a bound type or vice versa. See {#link XmlAdapter} for more
* details.
*/
Class<? extends XmlAdapter<?, ?>> key() default UNDEFINED.class;
/**
* Points to the class that converts the value type to a bound type or vice versa. See {#link XmlAdapter} for more
* details.
*/
Class<? extends XmlAdapter<?, ?>> value() default UNDEFINED.class;
static final class UNDEFINED extends XmlAdapter<String, String> {
#Override
public String unmarshal(String v) throws Exception {
return null;
}
#Override
public String marshal(String v) throws Exception {
return null;
}
}
}
Here so the adapter
import java.io.StringReader;
import java.io.StringWriter;
import java.lang.annotation.Annotation;
import java.lang.annotation.IncompleteAnnotationException;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBElement;
import javax.xml.bind.JAXBIntrospector;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlElementWrapper;
import javax.xml.bind.annotation.XmlType;
import javax.xml.bind.annotation.adapters.XmlAdapter;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
import javax.xml.namespace.QName;
/**
* This class represents a general purpose Map adapter. It is capable of handling any type of class implementing the Map
* interface and has a no-args constructor.
*/
public class MapAdapter extends XmlAdapter<MapAdapter.Wrapper, Map<Object, Object>> {
private static final String XSI_NS = "xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"";
private static final String XSI_TYPE = "xsi:type";
private static final String CDATA_START = "<![CDATA[";
private static final String CDATA_END = "]]>";
private final MarshallerListener marshallerListener = new MarshallerListener();
private final UnmarshallerListener unmarshallerListener = new UnmarshallerListener();
private final JAXBContext context;
public MapAdapter(JAXBContext inContext) {
context = inContext;
}
#SuppressWarnings({ "unchecked", "rawtypes" })
#Override
public Map<Object, Object> unmarshal(Wrapper inWrapper) throws Exception {
if (inWrapper == null) {
return null;
}
Info info = null;
for (Info element : unmarshallerListener.infoList) {
if (element.field.equals(inWrapper.field)) {
info = element;
}
}
if (info != null) {
Class<Map<Object, Object>> clazz = (Class<Map<Object, Object>>) Class.forName(inWrapper.mapClass);
Map<Object, Object> outMap = clazz.newInstance();
XmlAdapter<Object, Object> keyAdapter = null;
XmlAdapter<Object, Object> valueAdapter = null;
if (info.adapters.key() != MapKeyValueAdapters.UNDEFINED.class) {
keyAdapter = (XmlAdapter<Object, Object>) info.adapters.key().getConstructor().newInstance();
}
if (info.adapters.value() != MapKeyValueAdapters.UNDEFINED.class) {
valueAdapter = (XmlAdapter<Object, Object>) info.adapters.value().getConstructor().newInstance();
}
Unmarshaller um = context.createUnmarshaller();
for (MapEntry entry : inWrapper.mapList) {
Object key = ((JAXBElement) um.unmarshal(new StringReader(entry.key))).getValue();
if (keyAdapter != null) {
key = keyAdapter.unmarshal(key);
}
Object value = ((JAXBElement) um.unmarshal(new StringReader(entry.value))).getValue();
if (valueAdapter != null) {
value = valueAdapter.unmarshal(value);
}
outMap.put(key, value);
}
return outMap;
} else {
throw new IllegalStateException("Adapter info could not be found.");
}
}
#SuppressWarnings("unchecked")
#Override
public Wrapper marshal(Map<Object, Object> inMap) throws Exception {
if (inMap == null) {
return null;
}
Info info = null;
for (Info element : marshallerListener.infoList) {
if (element.map == inMap) {
info = element;
}
}
if (info != null) {
Wrapper outWrapper = new Wrapper();
outWrapper.mapClass = inMap.getClass().getName();
outWrapper.field = info.field;
Marshaller m = context.createMarshaller();
m.setProperty(Marshaller.JAXB_FRAGMENT, true);
JAXBIntrospector introspector = context.createJAXBIntrospector();
XmlAdapter<Object, Object> keyAdapter = null;
XmlAdapter<Object, Object> valueAdapter = null;
if (info.adapters.key() != MapKeyValueAdapters.UNDEFINED.class) {
keyAdapter = (XmlAdapter<Object, Object>) info.adapters.key().getConstructor().newInstance();
}
if (info.adapters.value() != MapKeyValueAdapters.UNDEFINED.class) {
valueAdapter = (XmlAdapter<Object, Object>) info.adapters.value().getConstructor().newInstance();
}
for (Map.Entry<?, ?> entry : inMap.entrySet()) {
MapEntry jaxbEntry = new MapEntry();
outWrapper.mapList.add(jaxbEntry);
Object key = entry.getKey();
if (key != null) {
Class<Object> clazz = Object.class;
if (keyAdapter != null) {
key = keyAdapter.marshal(key);
clazz = (Class<Object>) key.getClass();
}
if (introspector.getElementName(key) == null) {
// The value of clazz determines if the qualification is written or not; Object.class generates the
// qualification.
key = new JAXBElement<Object>(new QName("key"), clazz, key);
}
StringWriter writer = new StringWriter();
m.marshal(key, writer);
jaxbEntry.key = format("key", writer.toString());
}
Object value = entry.getValue();
if (value != null) {
Class<Object> clazz = Object.class;
if (valueAdapter != null) {
value = valueAdapter.marshal(value);
clazz = (Class<Object>) value.getClass();
}
if (introspector.getElementName(value) == null) {
// The value of clazz determines if the qualification is written or not; Object.class generates the
// qualification.
value = new JAXBElement<Object>(new QName("value"), clazz, value);
}
StringWriter writer = new StringWriter();
m.marshal(value, writer);
jaxbEntry.value = format("value", writer.toString());
}
}
return outWrapper;
} else {
throw new IllegalStateException("Adapter info could not be found.");
}
}
private String format(String inTagName, String inXML) {
String element = "<" + inTagName;
// Remove unneeded namespaces, they are already declared in the top node.
int beginIndex = inXML.indexOf(XSI_TYPE);
if (beginIndex != -1) {
int endIndex = inXML.indexOf(" ", beginIndex);
element += " " + inXML.substring(beginIndex, endIndex) + " " + XSI_NS;
}
beginIndex = inXML.indexOf('>');
element += inXML.substring(beginIndex);
return CDATA_START + element + CDATA_END;
}
#XmlType(name = "map")
static class Wrapper {
#XmlElement(name = "mapClass")
private String mapClass;
#XmlElement(name = "field")
private String field;
#XmlElementWrapper(name = "map")
#XmlElement(name = "entry")
private final List<MapEntry> mapList = new ArrayList<MapEntry>();
}
#XmlType(name = "mapEntry")
static class MapEntry {
#XmlElement(name = "key")
private String key;
#XmlElement(name = "value")
private String value;
}
public Marshaller.Listener getMarshallerListener() {
return marshallerListener;
}
public Unmarshaller.Listener getUnmarshallerListener() {
return unmarshallerListener;
}
private static class MarshallerListener extends Marshaller.Listener {
private final List<Info> infoList = new ArrayList<Info>();
#Override
public void beforeMarshal(Object inSource) {
extractInfo(infoList, inSource);
}
}
private class UnmarshallerListener extends Unmarshaller.Listener {
private final List<Info> infoList = new ArrayList<Info>();
#Override
public void beforeUnmarshal(Object inTarget, Object inParent) {
extractInfo(infoList, inTarget);
}
}
private static void extractInfo(List<Info> inList, Object inObject) {
for (Field field : inObject.getClass().getDeclaredFields()) {
for (Annotation a : field.getAnnotations()) {
if (a.annotationType() == XmlJavaTypeAdapter.class) {
if (((XmlJavaTypeAdapter) a).value() == MapAdapter.class) {
MapKeyValueAdapters adapters = field.getAnnotation(MapKeyValueAdapters.class);
if (adapters == null) {
throw new IncompleteAnnotationException(XmlJavaTypeAdapter.class, "; XmlJavaTypeAdapter specifies "
+ MapAdapter.class.getName() + " for field " + field.getName() + " in "
+ inObject.getClass().getName() + ". This must be used in combination with annotation "
+ MapKeyValueAdapters.class.getName());
}
try {
field.setAccessible(true);
Map<?, ?> value = (Map<?, ?>) field.get(inObject);
if (value != null) {
Info info = new Info();
info.field = field.getName();
info.map = value;
info.adapters = adapters;
inList.add(info);
}
} catch (Exception e) {
throw new RuntimeException("Failed extracting annotation information from " + field.getName() + " in "
+ inObject.getClass().getName(), e);
}
}
}
}
}
}
private static class Info {
private String field;
private Map<?, ?> map;
private MapKeyValueAdapters adapters;
}
}
Note that the adapter is capable of handling all types of Maps as long as it has a default constructor.
Finally the code to set up the usage of the adapter.
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;
/**
* Singleton that manages the JAXB functionality.
*/
public enum JAXBManager {
INSTANCE;
private JAXBContext context;
private JAXBManager() {
try {
context = JAXBContext.newInstance(SomeComplexClass.class.getPackage().getName());
} catch (JAXBException e) {
throw new RuntimeException(e);
}
}
public Marshaller createMarshaller() throws JAXBException {
Marshaller m = context.createMarshaller();
MapAdapter adapter = new MapAdapter(context);
m.setAdapter(adapter);
m.setListener(adapter.getMarshallerListener());
m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);
return m;
}
public Unmarshaller createUnmarshaller() throws JAXBException {
Unmarshaller um = context.createUnmarshaller();
MapAdapter adapter = new MapAdapter(context);
um.setAdapter(adapter);
um.setListener(adapter.getUnmarshallerListener());
return um;
}
}
This could generate an output of something like
<testMap2>
<mapClass>java.util.HashMap</mapClass>
<field>testMap2</field>
<map>
<entry>
<key><![CDATA[<key><number>1357</number><type>Unspecified</type></key>]]></key>
<value><![CDATA[<value xsi:type="xs:string" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">gn</value>]]></value>
</entry>
</map>
</testMap2>
As can be seen the qualification info is not needed for the key since we already know the adapter to use.
Also note that I add CDATA to the output. I have implemented a simple character escape handler that respects this (not included in this code example).
Due to our release cycles I have a bit of time before the opportunity opens for implementing this functionality in our code so I therefore thought it would be wise to check with the community if there are any problems with this solution or if there are better ways already in the JAXB specification that I have overlooked. I also assume that there are sections of the code that can be done in better ways.
Thanks for comments.
Here is my proposal for a workaround:
Make the map XmlTransient
Use a wrapped List for the marshalling
reinit the map from the list whenever it is needed
if you need to keep the list and the map in sync use an add(order) function
Example Customer with a Map of Orders
#XmlRootElement
#XmlAccessorType(XmlAccessType.FIELD)
public static class Order {
#XmlID
String orderId;
String item;
int count;
}
#XmlRootElement(name = "customer")
#XmlAccessorType(XmlAccessType.FIELD)
public static class Customer {
String name;
String firstname;
#XmlElementWrapper(name = "orders")
#XmlElement(name = "order")
List<Order> orders = new ArrayList<Order>();
#XmlTransient
private Map<String, Order> ordermap = new LinkedHashMap<String, Order>();
/**
* reinitialize the order list
*/
public void reinit() {
for (Order order : orders) {
ordermap.put(order.orderId, order);
}
}
/**
* add the given order to the internal list and map
* #param order - the order to add
*/
public void addOrder(Order order) {
orders.add(order);
ordermap.put(order.orderId,order);
}
}
Example XML
<customer>
<name>Doe</name>
<firstname>John</firstname>
<orders>
<order>
<orderId>Id1</orderId>
<item>Item 1</item>
<count>1</count>
</order>
<order>
<orderId>Id2</orderId>
<item>Item 2</item>
<count>2</count>
</order>
</orders>
</customer>
Mininimal complete and verifiable example
An example according to
https://stackoverflow.com/help/mcve
can be found at
https://github.com/BITPlan/com.bitplan.simplerest/blob/master/src/test/java/com/bitplan/jaxb/TestJaxbFactory.java#L390

Jersey REST Service Output Format

I need to format the output (xml) of a restful service using Jersey according to following scenario
I have a class with key value pair as follows.
#XmlRootElement(name="columnValues")
public class KeyValueDTO {
private String key;
private String val;
#XmlElement(name="column")
public String getKey() {
return key;
}
#XmlElement(name="value")
public String getVal() {
return val;
}
}
Suppose I have list like this which is returned by rest service:
List<KeyValueDTO> mylist = new ArrayList<KeyValueDTO>();
KeyValueDTO dto1 = new KeyValueDTO();
dto1.key = "Name";
dto1.val = "alex";
KeyValueDTO dto2 = new KeyValueDTO();
dto2.key = "Age";
dto2.val = 23
mylist.add(dto1);
mylist.add(dt02);
And I want to generate the output as follow
<Name>alex</Name>
<Age>20</Age>
But currently it is giving following output
<column>Name</column>
<value>alex</column>
<column>Age</column>
<value>20</column>
Can anyone let me know how to achieve this?
You could try using an XmlAdapter:
public class KeyValueAdapter extends XmlAdapter<String, List<KeyValueDTO>> {
#Override
public List<KeyValueDTO> unmarshal(String v) throws Exception {
// Needs implementation
return null;
}
#Override
public String marshal(List<KeyValueDTO> vs) throws Exception {
StringBuffer buffer = new StringBuffer();
for (KeyValueDTO v: vs) {
buffer.append(String.format("<%s>%s</%1$s>", v.key, v.val));
}
return buffer.toString();
}
}
And then add that adapter to your bean:
#XmlRootElement
public static class Wrapper {
#XmlJavaTypeAdapter(KeyValueAdapter.class)
List<KeyValueDTO> dtos;
}

Categories

Resources