0

I am new in Java and I am working on parsing a XML file. Jumping to the question, I have to modify the given XML as below.

Given XML:

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<Elements>
    <Item>
        <Element>
            <Value>On</Value>
        </Element>
    </Item>
    <Item>
        <Element>
            <Value>On</Value>
        </Element>
    </Item>
</Elements>

Modified:

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<Elements>
    <Entry>
        <Item>
            <Element>
                <Value>On</Value>
            </Element>
        </Item>
    </Entry>
    <Entry>
        <Item>
            <Element>
                <Value>On</Value>
            </Element>
        </Item>
    </Entry>
</Elements>

I just have to create a new element 'Entry' and cut paste the already existing 'Item'. How could I achieve it?

iOS
  • 3,526
  • 3
  • 37
  • 82
  • If your input will be consistent you might want to transform the XML in a String: https://stackoverflow.com/questions/315517/is-there-a-more-elegant-way-to-convert-an-xml-document-to-a-string-in-java-than or https://stackoverflow.com/questions/5456680/xml-document-to-string. Then with REGEX pick the ```````` and wrap it, appending in both sides the ````````. – Román Oct 17 '20 at 11:03
  • Neither the input nor the output is a well-formed XML document, which makes this rather tricky. A well-formed document has a single outermost element. – Michael Kay Oct 17 '20 at 13:42
  • @MichaelKay I had just posted an example. Updated it as a proper document. – iOS Oct 17 '20 at 13:54

3 Answers3

1

By far the easiest way to do this is with an XSLT stylesheet.

String xslt = 
 "<Elements xmlns:xsl='http://www.w3.org/1999/XSL/Transform' xsl:version='1.0'>" +
 " <xsl:for-each select='//Item'>" + 
 "  <Entry><xsl:copy-of select='.'/></Entry>" + 
 " </xsl:for-each>" +
 "</Elements>";
TransformerFactory factory = TransformerFactory.newInstance();
Transformer trans = factory.newTransformer(
    new StreamSource(new StringReader(xslt)));
trans.transform(new StreamSource(inputFile), 
                new StreamResult(outputFile)); 
Michael Kay
  • 156,231
  • 11
  • 92
  • 164
0

Here are examples of mapping XML using DOM and using STAX. DOM can be used when XML size is relatively small, it loads whole XML to memory and provides access to every element of the tree. STAX is event based parser, can be used for very large XML documents. Both are low level XML handling libraries.

Variable xml is string representation of your source XML, it's omitted in the code.

public static void main(String[] args) throws Exception {
        
        // parse and map XML using DOM
        DocumentBuilder db = DocumentBuilderFactory.newInstance().newDocumentBuilder();
        Document doc = db.parse(new InputSource(new StringReader(xml)));
        doc = map(doc);
        System.out.println(serialize(doc));
        
        // parse and map XML using STAX
        InputStream is = new ByteArrayInputStream(xml.getBytes());
        ByteArrayOutputStream os = map(is);
        System.out.println(os.toString(StandardCharsets.UTF_8));
    }

    /**
     * Uses DOM tree, iterating over item elements. 
     * For each item element detaches it, creates new entry element, attaches
     * detached item element to it and inserts entry element into DOM.
     */
    public static Document map(Document doc) {
        NodeList nodes = doc.getDocumentElement().getElementsByTagName("Item");
        for (int i = 0; i < nodes.getLength(); i++) {
            Node child = nodes.item(i);
            child = doc.getDocumentElement().removeChild(child);
            Element entry = doc.createElement("Entry");
            doc.adoptNode(entry);
            doc.getDocumentElement().insertBefore(entry, doc.getDocumentElement().getFirstChild());
            entry.appendChild(child);
        }
        return doc;
    }
    
    /**
     * Uses STaX, event based parser.
     * When handling parser events checks for item start- and end- events and
     * adds appropriate entry element event before or after original event.
     */
    public static ByteArrayOutputStream map(InputStream is) throws Exception {
        ByteArrayOutputStream os = new ByteArrayOutputStream();
        XMLEventReader reader = XMLInputFactory.newInstance().createXMLEventReader(is);
        XMLEventWriter writer = XMLOutputFactory.newInstance().createXMLEventWriter(os);
        XMLEventFactory ef = XMLEventFactory.newInstance();
        while(reader.hasNext()) {
            XMLEvent event = reader.nextEvent();
            if(event.isStartElement() && event.asStartElement().getName().getLocalPart().equals("Item")) {
                XMLEvent entryEvent = ef.createStartElement("", null, "Entry");
                writer.add(entryEvent);
            }
            writer.add(event);
            if(event.isEndElement() && event.asEndElement().getName().getLocalPart().equals("Item")) {
                XMLEvent entryEvent = ef.createEndElement("", null, "Entry");
                writer.add(entryEvent);
            }
        }
        return os;
    }

    /**
     * Serializes DOM
     */
    private static String serialize(Document document) {
        TransformerFactory transformerFactory = TransformerFactory.newInstance();
        removeWhitespaces(document.getDocumentElement());
        try (StringWriter writer = new StringWriter()) {
            StreamResult result = new StreamResult(writer);
            Transformer transformer = transformerFactory.newTransformer();
            transformer.setOutputProperty(OutputKeys.METHOD, "xml");
            transformer.setOutputProperty(OutputKeys.INDENT, "yes");
            transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4");
            transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
            transformer.transform(new DOMSource(document), result);
            return writer.toString();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * Removes whitespace nodes from DOM
     */
    private static void removeWhitespaces(Element element) {
        NodeList children = element.getChildNodes();
        for (int i = children.getLength() - 1; i >= 0; i--) {
            Node child = children.item(i);
            if (child instanceof Text && ((Text) child).getData().trim().isEmpty()) {
                element.removeChild(child);
            } else if (child instanceof Element) {
                removeWhitespaces((Element) child);
            }
        }
    }
Alexandra Dudkina
  • 4,302
  • 3
  • 15
  • 27
0

Using Jackson library you can read whole XML payload as List<Map> and write custom serialiser which will write it in new format. See below example:

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializable;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.jsontype.TypeSerializer;
import com.fasterxml.jackson.databind.type.CollectionType;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement;
import com.fasterxml.jackson.dataformat.xml.ser.ToXmlGenerator;

import javax.xml.namespace.QName;
import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.Map;

public class XmlMapperApp {

    public static void main(String... args) throws Exception {
        File xmlFile = new File("./resource/test.xml").getAbsoluteFile();

        XmlMapper mapper = XmlMapper.xmlBuilder()
                .enable(SerializationFeature.INDENT_OUTPUT)
                .enable(ToXmlGenerator.Feature.WRITE_XML_DECLARATION)
                .build();

        List<Map<String, Object>> items = readOldXmlFormat(xmlFile, mapper);
        String xml = writeNewXmlFormat(mapper, items);
        System.out.println(xml);
    }

    private static List<Map<String, Object>> readOldXmlFormat(File xmlFile, XmlMapper mapper) throws IOException {
        CollectionType collectionType = mapper.getTypeFactory().constructCollectionType(List.class, Map.class);
        return mapper.readValue(xmlFile, collectionType);
    }

    private static String writeNewXmlFormat(XmlMapper mapper, List<Map<String, Object>> items) throws IOException {
        return mapper.writeValueAsString(new SerializableItems(items));
    }
}

@JacksonXmlRootElement(localName = "Elements")
class SerializableItems implements JsonSerializable {

    private final List<Map<String, Object>> items;

    SerializableItems(List<Map<String, Object>> items) {
        this.items = items;
    }

    private final QName itemQName = new QName("Item");
    private final QName elementQName = new QName("Element");
    private final QName entryQName = new QName("Entry");

    @Override
    public void serialize(JsonGenerator gen, SerializerProvider serializers) throws IOException {
        ToXmlGenerator xmlGen = (ToXmlGenerator) gen;
        xmlGen.writeStartObject();
        for (Map<String, Object> item : items) {
            xmlGen.startWrappedValue(entryQName, itemQName);
            xmlGen.startWrappedValue(itemQName, elementQName);
            for (Map.Entry<String, Object> entry : item.entrySet()) {
                xmlGen.writeObjectField(entry.getKey(), entry.getValue());
            }
            xmlGen.finishWrappedValue(itemQName, elementQName);
            xmlGen.finishWrappedValue(entryQName, itemQName);
        }
        xmlGen.writeEndObject();
    }

    @Override
    public void serializeWithType(JsonGenerator gen, SerializerProvider serializers, TypeSerializer typeSer) throws IOException {
    }
}

Above code prints:

<?xml version='1.0' encoding='UTF-8'?>
<Elements>
  <Entry>
    <Item>
      <Element>
        <Value>On</Value>
      </Element>
    </Item>
  </Entry>
  <Entry>
    <Item>
      <Element>
        <Value>On</Value>
      </Element>
    </Item>
  </Entry>
</Elements>
Michał Ziober
  • 37,175
  • 18
  • 99
  • 146