Search code examples
javaxmldomchannelfile-locking

Xml appending to end of file instead of replacing file


For more detailed information regarding the motivation behind this goal view my previous question. I (again) decided to ask this as a new question entirely as I thought that it had evolved sufficiently to merit doing so. As a summary, I intend to use JDOM in combination with NIO in order to:

  1. Gain an exclusive file lock on an xml file.
  2. Read the file into a Document object.
  3. Make arbitrary changes (with lock still active!).
  4. Write the changes back to the xml file.
  5. Release the file lock.

Having solved an issue with reading the xml file via a channel by overriding the close behaviour of FilterInputStream, I now have a locked channel which I can write to using Transformer.transform(). The issue however is that instead of replacing the original file, Transformer.Transform is appending the new file to the end of the original file instead of replacing it (see image).

enter image description here

The issue is not with the Document object itself as can be seen by printing the string returned from the following method, using the Document object as input:

public String toXMLString(Node node) {
    try {
        this.removeBlankTextNodes(node);
        StringWriter sw = new StringWriter();
        TransformerFactory tf = TransformerFactory.newInstance();
        Transformer transformer = tf.newTransformer();
        transformer.transform(new DOMSource(node), new StreamResult(sw));
        return sw.toString();
    } catch (TransformerException ex) {
        throw new RuntimeException("Error converting to String", ex);
    }
}

Neither does the append issue occur when a new channel is established to the same file and used as the result of Transformer.transform(). So this issue only appears to occur when the same channel is used to read and write (perhaps this is why they chose to close the channel automatically when calling DocumentBuilder.parse().

I have checked the documentation quite thoroughly and cannot find any related options for specifying the output of Transformer.transform (I have searched Transformer/Transformer factory/StreamResult), although since many of the classes are abstract I am struggling to find the actual implementation code. When examining the StreamResult object in debugger, it actually appears that the append option is set to false, so my main suspicion is that I need to somehow clear the channel (or a related buffer?) after the read operation has completed. One last thing which I tried is to open the channel with just about every option apart from 'APPEND' using the code channel.open(Paths.get(path), StandardOpenOption.CREATE); this again has no effect. Note that I cannot just close and reopen the channel as this will release the file lock. Any pointers/advice would be great! Code below:

import java.nio.channels.*;

import javax.xml.parsers.*;
import javax.xml.transform.*;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.xpath.*;

import org.w3c.dom.*;
import org.xml.sax.SAXException;

public class Test2{ 
    String path = "...Test 2.xml";

    public Test2(){
        Document doc = null;
        DocumentBuilderFactory dbFactory;
        DocumentBuilder dBuilder;
        NodeList itemList;
        Transformer transformer;
        FileChannel channel; 
        Element newElement;
        int prevNumber;
        TransformerFactory transformerFactory ;
        DOMSource source;
        StreamResult result;
        NonClosingInputStream ncis = null;
        try {
            channel = new RandomAccessFile(new File(path), "rw").getChannel();
            FileLock lock = channel.lock(0L, Long.MAX_VALUE, false);

            try {
                dbFactory = DocumentBuilderFactory.newInstance();
                dBuilder = dbFactory.newDocumentBuilder();
                ncis = new NonClosingInputStream(Channels.newInputStream(channel));
                doc = dBuilder.parse(ncis);
            } catch (SAXException | IOException | ParserConfigurationException e) {
                e.printStackTrace();
            }
            doc.getDocumentElement().normalize();
            itemList = doc.getElementsByTagName("Item");
            newElement = doc.createElement("Item");
            prevNumber = Integer.parseInt(((Element) itemList.item(itemList.getLength() - 1)).getAttribute("Number"));
            newElement.setAttribute("Number", (prevNumber + 1) + "");
            doc.getDocumentElement().appendChild(newElement);



            transformerFactory = TransformerFactory.newInstance();
            transformer = transformerFactory.newTransformer();
            source = new DOMSource(doc);

            //channel.open(Paths.get(path), StandardOpenOption.CREATE);
            result = new StreamResult(Channels.newOutputStream(channel));   

            transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4");
            transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no");
            transformer.setOutputProperty(OutputKeys.METHOD, "xml");
            transformer.setOutputProperty(OutputKeys.INDENT, "yes");
            transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
            transformer.transform(source, result);
            channel.close();
        } catch (IOException | TransformerException e) {
            e.printStackTrace();
        } finally {
            try {
                ncis.reallyClose();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    class NonClosingInputStream extends FilterInputStream {

        public NonClosingInputStream(InputStream it) {
            super(it);
        }

        @Override
        public void close() throws IOException {
            // Do nothing.
        }

        public void reallyClose() throws IOException {
            // Actually close.
            in.close();
        }
    }

    public static void main(String[] args){
        new Test2();
    }
}

Solution

  • This is almost surely happening because your channel is positioned at the end of the file after reading it in.

    You should be able to remedy this by doing the following before carrying out the transform:

    channel.truncate(0);
    

    this should truncate the file to a size of 0 and reset the position to 0.

    http://docs.oracle.com/javase/7/docs/api/java/nio/channels/FileChannel.html#truncate(long)