Modify XML

In my installer project, I needed to modify an existing configuration file in XML format.

For this example, I have decided to use org.w3c.dom and javax.xml.xpath to show how you can locate, and modify elements in an XML document.

First we need to create an object of type org.w3c.dom.Document. A simple document can be created this way in code:

DocumentBuilderFactory domFactory = DocumentBuilderFactory.newInstance();  
DocumentBuilder documentBuilder = domFactory.newDocumentBuilder();  
Document document = documentBuilder.parse(new InputSource(new StringReader("<configuration>" +  
  "<component>" +
  "<class>com._4thex.xml.ExampleComponent</class>" +
  "</component>" +
  "</configuration>")));

In practice, you would not hardcode the XML content, but rather load if from a file or a stream. The InputSource constructor takes a Reader as argument, so it is easy to replace the StringReader with a FileReader.

We can now use the XPathExpression class to pin-point the location in the XML content, where we want to inject our new element:

XPathFactory factory = XPathFactory.newInstance();  
XPath xpath = factory.newXPath();  
XPathExpression configurationExpression = xpath.compile("/configuration");  

XPath is basically a way to describe a location in some XML data. /configuration means the element named configuration that is at the root of the document. I am going to assume for the purpose of this article, that you are familiar with XPath, if not you may want to read up on it. Here are a few good resources:

W3Schools.com - XPath Tutorial
W3.org - XML Path Language

Currently the XML document looks something like this:

<configuration>  
  <component>
    <class>com._4thex.xml.ExampleComponent</class>
  </component>
</configuration>  

Let's say we want to insert a new component element. First we will have to create the element to insert:

Element componentElement = document.createElement("component");  
Element classElement = document.createElement("class");  
classElement.setTextContent("com._4thex.xml.SalesComponent");  
componentElement.appendChild(classElement);  

Let's create a method, so that we can insert the new component by simply calling:

injector.appendChild(configurationExpression, componentElement);  

The method implementation will look like this:

public void appendChild(XPathExpression parent, Element element) throws Exception {  
    Node parentNode = (Node)parent.evaluate(document, XPathConstants.NODE);
    parentNode.appendChild(element);
}

First we locate the parent node; in this case the configuration element, and then we simply append the new element.

The XML document now looks like this:

<configuration>  
  <component>
    <class>com._4thex.xml.ExampleComponent</class>
  </component>
  <component>
    <class>com._4thex.xml.SalesComponent</class>
  </component>
</configuration>  

Now let's look at inserting a new element at a particular location. We want to insert a description element as a child of the component element that has a class element, that contains the text ExampleComponent. First we need to create the new element:

Element descriptionElement = document.createElement("description");  
descriptionElement.setTextContent("Component that handles the sales");  

Now we need to define the XPath expression pointing to the class element:

XPathExpression componentExpr = xpath.compile("//class[contains(text(),'ExampleComponent')]");  

We will define a new method to be able to insert a new element before another element, so that we can just call:

injector.insertBeforeSibling(componentExpr, descriptionElement);  

Here is the method implementation:

    public void insertBeforeSibling(XPathExpression sibling, Element element) throws Exception {
        Element siblingNode = (Element)sibling.evaluate(document, XPathConstants.NODE);
        siblingNode.getParentNode().insertBefore(element, siblingNode);
    }

We find the class element, and then we get the parent node; the component element. Finally we call insertBefore.

We might also want to be able to insert a new element after a particular element. For that we will define another method like this:

    public void insertAfterSibling(XPathExpression sibling, Element element) throws Exception {
        Element siblingNode = (Element)sibling.evaluate(document, XPathConstants.NODE);
        siblingNode.getParentNode().insertBefore(element, siblingNode.getNextSibling());
    }

Notice that we call the getNextSibling method. Inserting after an element is the same as inserting before the next sibling.

Here it the complete example:

package com._4thex.xml;

import java.io.StringReader;  
import java.io.StringWriter;

import javax.xml.parsers.DocumentBuilder;  
import javax.xml.parsers.DocumentBuilderFactory;  
import javax.xml.transform.OutputKeys;  
import javax.xml.transform.Transformer;  
import javax.xml.transform.TransformerFactory;  
import javax.xml.transform.dom.DOMSource;  
import javax.xml.transform.stream.StreamResult;  
import javax.xml.xpath.XPath;  
import javax.xml.xpath.XPathConstants;  
import javax.xml.xpath.XPathExpression;  
import javax.xml.xpath.XPathFactory;

import org.w3c.dom.Document;  
import org.w3c.dom.Element;  
import org.w3c.dom.Node;  
import org.xml.sax.InputSource;

public class Injector {

    public static void main(String[] args) throws Exception {
        DocumentBuilderFactory domFactory = DocumentBuilderFactory.newInstance();
        DocumentBuilder documentBuilder = domFactory.newDocumentBuilder();
        Document document = documentBuilder.parse(new InputSource(new StringReader("<configuration>" +
                "<component>" +
                "<class>com._4thex.xml.ExampleComponent</class>" +
                "</component>" +
                "</configuration>")));

        Injector injector = new Injector(document);

        XPathFactory factory = XPathFactory.newInstance();
        XPath xpath = factory.newXPath();
        XPathExpression configurationExpression = xpath.compile("/configuration");

        Element componentElement = document.createElement("component");
        Element classElement = document.createElement("class");
        classElement.setTextContent("com._4thex.xml.SalesComponent");
        componentElement.appendChild(classElement);

        injector.appendChild(configurationExpression, componentElement);

        Element descriptionElement = document.createElement("description");
        descriptionElement.setTextContent("Component that handles the sales");

        XPathExpression componentExpr = xpath.compile("//class[contains(text(),'ExampleComponent')]");
        injector.insertBeforeSibling(componentExpr, descriptionElement);

        Element priceElement = document.createElement("price");
        priceElement.setTextContent("5.99");

        XPathExpression classExpr = xpath.compile("//class[text() = 'com._4thex.xml.ExampleComponent']");
        injector.insertAfterSibling(classExpr, priceElement);

        Transformer transformer = TransformerFactory.newInstance().newTransformer();
        transformer.setOutputProperty(OutputKeys.INDENT, "yes");
        transformer.setOutputProperty(OutputKeys.METHOD, "xml");

        StreamResult result = new StreamResult(new StringWriter());
        DOMSource source = new DOMSource(document);
        transformer.transform(source, result);

        String xmlOutput = result.getWriter().toString();
        System.out.println(xmlOutput);
    }

    private Document document;

    public Injector(Document document) {
        this.document = document;
    }

    public void appendChild(XPathExpression parent, Element element) throws Exception {
        Node parentNode = (Node)parent.evaluate(document, XPathConstants.NODE);
        parentNode.appendChild(element);
    }

    public void insertBeforeSibling(XPathExpression sibling, Element element) throws Exception {
        Element siblingNode = (Element)sibling.evaluate(document, XPathConstants.NODE);
        siblingNode.getParentNode().insertBefore(element, siblingNode);
    }

    public void insertAfterSibling(XPathExpression sibling, Element element) throws Exception {
        Element siblingNode = (Element)sibling.evaluate(document, XPathConstants.NODE);
        siblingNode.getParentNode().insertBefore(element, siblingNode.getNextSibling());
    }
}