javaxmljdom-2

Java: JDOM.XMLOutputter don't overwrite XML File when looking to make multiple changes in a row


Explanation

I'm making program that use a XML file to save some values to use as settings, and only need to read and modify the element values in the XML. The code is written in Java and I'm using JDOM 2.0.6.1 to manage the XML file, followed this tutorial.

The code works when overwrite any element in the XML, but just 1 time. If make multiple operations to modify any value, the program start appending a new tree with the changes, which should not happen and would be the problem.

The default XML:

<?xml version="1.0" encoding="UTF-8"?>
<settings>
  <time>
    <time-work>25</time-work>
    <time-break>5</time-break>
    <time-rest>15</time-rest>
    <time-interval>3</time-interval>
  </time>
  <auto-start>
    <start-work>true</start-work>
    <start-break>true</start-break>
    <start-rest>true</start-rest>
  </auto-start>
</settings>

The Java code:

public class FileHandler {

    private final Writer settingsWriter;
    private final XMLOutputter xmlOutput;
    private final Document settingsDoc;
    private final Element settingsElement;


    FileHandler() throws IOException, JDOMException {

        Path settingsPath = Paths.get("res/settings/settings.xml");
        File settingsFile = settingsPath.toFile();
        boolean settingsExist = settingsFile.createNewFile();
        settingsWriter = Channels.newWriter(
              FileChannel.open(settingsPath, StandardOpenOption.WRITE),
              StandardCharsets.UTF_8);
        xmlOutput = new XMLOutputter();
        xmlOutput.setFormat(Format.getPrettyFormat());

        if (settingsExist) createSettings();

        SAXBuilder saxBuilder = new SAXBuilder();
        settingsDoc = saxBuilder.build(settingsFile);
        settingsElement = settingsDoc.getRootElement();

        setElementValue("time-work", "5");
        setElementValue("time-work", "10");
        setElementValue("time-work", "30");
    }

    public void setElementValue(String element, String newValue) throws IOException, JDOMException {

        Element groupElement = settingsElement.getChild(element.startsWith("time")? "time":"auto-start"),
                toChangeElement = groupElement.getChild(element);

        toChangeElement.setText(newValue);
        xmlOutput.output(settingsDoc, settingsWriter);
    }

    /*More code, irrelevant for the post*/
}

Examples

Example 1 (Works)

    FileHandler() throws IOException, JDOMException {

        Path settingsPath = Paths.get("res/settings/settings.xml");
        File settingsFile = settingsPath.toFile();
        boolean settingsExist = settingsFile.createNewFile();
        settingsWriter = Channels.newWriter(
              FileChannel.open(settingsPath, StandardOpenOption.WRITE),
              StandardCharsets.UTF_8);
        xmlOutput = new XMLOutputter();
        xmlOutput.setFormat(Format.getPrettyFormat());

        if (settingsExist) createSettings();

        SAXBuilder saxBuilder = new SAXBuilder();
        settingsDoc = saxBuilder.build(settingsFile);
        settingsElement = settingsDoc.getRootElement();

        setElementValue("time-work", "5");
    }
<?xml version="1.0" encoding="UTF-8"?>
<settings>
  <time>
    <time-work>5</time-work>
    <time-break>5</time-break>
    <time-rest>15</time-rest>
    <time-interval>3</time-interval>
  </time>
  <auto-start>
    <start-work>true</start-work>
    <start-break>true</start-break>
    <start-rest>true</start-rest>
  </auto-start>
</settings>

Example 2 (Works)

    FileHandler() throws IOException, JDOMException {

        Path settingsPath = Paths.get("res/settings/settings.xml");
        File settingsFile = settingsPath.toFile();
        boolean settingsExist = settingsFile.createNewFile();
        settingsWriter = Channels.newWriter(
              FileChannel.open(settingsPath, StandardOpenOption.WRITE),
              StandardCharsets.UTF_8);
        xmlOutput = new XMLOutputter();
        xmlOutput.setFormat(Format.getPrettyFormat());

        if (settingsExist) createSettings();

        SAXBuilder saxBuilder = new SAXBuilder();
        settingsDoc = saxBuilder.build(settingsFile);
        settingsElement = settingsDoc.getRootElement();

        setElementValue("start-rest", "2");
    }
<?xml version="1.0" encoding="UTF-8"?>
<settings>
  <time>
    <time-work>25</time-work>
    <time-break>5</time-break>
    <time-rest>15</time-rest>
    <time-interval>3</time-interval>
  </time>
  <auto-start>
    <start-work>true</start-work>
    <start-break>true</start-break>
    <start-rest>2</start-rest>
  </auto-start>
</settings>

Example 3 (Fail)

    FileHandler() throws IOException, JDOMException {

        Path settingsPath = Paths.get("res/settings/settings.xml");
        File settingsFile = settingsPath.toFile();
        boolean settingsExist = settingsFile.createNewFile();
        settingsWriter = Channels.newWriter(
              FileChannel.open(settingsPath, StandardOpenOption.WRITE),
              StandardCharsets.UTF_8);
        xmlOutput = new XMLOutputter();
        xmlOutput.setFormat(Format.getPrettyFormat());

        if (settingsExist) createSettings();

        SAXBuilder saxBuilder = new SAXBuilder();
        settingsDoc = saxBuilder.build(settingsFile);
        settingsElement = settingsDoc.getRootElement();

        setElementValue("time-work", "5");
        setElementValue("time-work", "10");
        setElementValue("time-work", "30");
    }
<?xml version="1.0" encoding="UTF-8"?>
<settings>
  <time>
    <time-work>5</time-work>
    <time-break>5</time-break>
    <time-rest>15</time-rest>
    <time-interval>3</time-interval>
  </time>
  <auto-start>
    <start-work>true</start-work>
    <start-break>true</start-break>
    <start-rest>true</start-rest>
  </auto-start>
</settings>
<?xml version="1.0" encoding="UTF-8"?>
<settings>
  <time>
    <time-work>10</time-work>
    <time-break>5</time-break>
    <time-rest>15</time-rest>
    <time-interval>3</time-interval>
  </time>
  <auto-start>
    <start-work>true</start-work>
    <start-break>true</start-break>
    <start-rest>true</start-rest>
  </auto-start>
</settings>
<?xml version="1.0" encoding="UTF-8"?>
<settings>
  <time>
    <time-work>30</time-work>
    <time-break>5</time-break>
    <time-rest>15</time-rest>
    <time-interval>3</time-interval>
  </time>
  <auto-start>
    <start-work>true</start-work>
    <start-break>true</start-break>
    <start-rest>true</start-rest>
  </auto-start>
</settings>

Example 4 (Fail)

    FileHandler() throws IOException, JDOMException {

        Path settingsPath = Paths.get("res/settings/settings.xml");
        File settingsFile = settingsPath.toFile();
        boolean settingsExist = settingsFile.createNewFile();
        settingsWriter = Channels.newWriter(
              FileChannel.open(settingsPath, StandardOpenOption.WRITE),
              StandardCharsets.UTF_8);
        xmlOutput = new XMLOutputter();
        xmlOutput.setFormat(Format.getPrettyFormat());

        if (settingsExist) createSettings();

        SAXBuilder saxBuilder = new SAXBuilder();
        settingsDoc = saxBuilder.build(settingsFile);
        settingsElement = settingsDoc.getRootElement();

        setElementValue("time-work", "5");
        setElementValue("time-break", "10");
        setElementValue("time-rest", "30");
    }
<?xml version="1.0" encoding="UTF-8"?>
<settings>
  <time>
    <time-work>5</time-work>
    <time-break>5</time-break>
    <time-rest>15</time-rest>
    <time-interval>3</time-interval>
  </time>
  <auto-start>
    <start-work>true</start-work>
    <start-break>true</start-break>
    <start-rest>true</start-rest>
  </auto-start>
</settings>
<?xml version="1.0" encoding="UTF-8"?>
<settings>
  <time>
    <time-work>5</time-work>
    <time-break>10</time-break>
    <time-rest>15</time-rest>
    <time-interval>3</time-interval>
  </time>
  <auto-start>
    <start-work>true</start-work>
    <start-break>true</start-break>
    <start-rest>true</start-rest>
  </auto-start>
</settings>
<?xml version="1.0" encoding="UTF-8"?>
<settings>
  <time>
    <time-work>5</time-work>
    <time-break>10</time-break>
    <time-rest>30</time-rest>
    <time-interval>3</time-interval>
  </time>
  <auto-start>
    <start-work>true</start-work>
    <start-break>true</start-break>
    <start-rest>true</start-rest>
  </auto-start>
</settings>

Example 5 (Fail)

    FileHandler() throws IOException, JDOMException {

        Path settingsPath = Paths.get("res/settings/settings.xml");
        File settingsFile = settingsPath.toFile();
        boolean settingsExist = settingsFile.createNewFile();
        settingsWriter = Channels.newWriter(
              FileChannel.open(settingsPath, StandardOpenOption.WRITE),
              StandardCharsets.UTF_8);
        xmlOutput = new XMLOutputter();
        xmlOutput.setFormat(Format.getPrettyFormat());

        if (settingsExist) createSettings();

        SAXBuilder saxBuilder = new SAXBuilder();
        settingsDoc = saxBuilder.build(settingsFile);
        settingsElement = settingsDoc.getRootElement();

        setElementValue("time-work", "5");
        setElementValue("start-break", "false");
        setElementValue("time-interval", "2");
    }
<?xml version="1.0" encoding="UTF-8"?>
<settings>
  <time>
    <time-work>5</time-work>
    <time-break>5</time-break>
    <time-rest>15</time-rest>
    <time-interval>3</time-interval>
  </time>
  <auto-start>
    <start-work>true</start-work>
    <start-break>true</start-break>
    <start-rest>true</start-rest>
  </auto-start>
</settings>
<?xml version="1.0" encoding="UTF-8"?>
<settings>
  <time>
    <time-work>5</time-work>
    <time-break>5</time-break>
    <time-rest>15</time-rest>
    <time-interval>3</time-interval>
  </time>
  <auto-start>
    <start-work>true</start-work>
    <start-break>false</start-break>
    <start-rest>true</start-rest>
  </auto-start>
</settings>
<?xml version="1.0" encoding="UTF-8"?>
<settings>
  <time>
    <time-work>5</time-work>
    <time-break>5</time-break>
    <time-rest>15</time-rest>
    <time-interval>2</time-interval>
  </time>
  <auto-start>
    <start-work>true</start-work>
    <start-break>false</start-break>
    <start-rest>true</start-rest>
  </auto-start>
</settings>

What I tried

Tried to close the writer and use re-building the "Document settingsDoc" inside the function.

Everything I tried ended in the same result, appending a new XML tree after overwriting just 1.


Solution

  • Alredy Solved

    So, for anyone that find this question, the solution I use was:

    public class FileHandler {
    
        private final Writer settingsWriter;
        private final XMLOutputter xmlOutput;
        private final Document settingsDoc;
        private final Element settingsElement;
        private final FileChannel settingsChannel;
        private final Path settingsPath;
    
    
        FileHandler() throws IOException, JDOMException {
    
            settingsPath = Paths.get("res/settings/settings.xml");
            File settingsFile = settingsPath.toFile();
            boolean settingsExist = settingsFile.createNewFile();
            settingsChannel = FileChannel.open(settingsPath, StandardOpenOption.WRITE);
            settingsWriter = Channels.newWriter(
                    settingsChannel,
                    StandardCharsets.UTF_8);
            xmlOutput = new XMLOutputter();
            xmlOutput.setFormat(Format.getPrettyFormat());
    
            if (settingsExist) createSettings();
    
            SAXBuilder saxBuilder = new SAXBuilder();
            settingsDoc = saxBuilder.build(settingsFile);
            settingsElement = settingsDoc.getRootElement();
        }
    
        /*Irrelevant code for the answer*/
    
        public void setElementValues(boolean[] newStart, String[] newTexts) throws IOException, JDOMException {
    
            Element timeElement = settingsElement.getChild("time"),
                    autoStartElement = settingsElement.getChild("auto-start");
            List<Element> timeElements = timeElement.getChildren(),
                            autoStartElements = autoStartElement.getChildren();
    
            for (int index = 0; index < autoStartElements.size(); ++index) {
                autoStartElements.get(index).setText(String.valueOf(newStart[index]));
            }
    
            for (int index = 0; index < timeElements.size(); ++index) {
                timeElements.get(index).setText(newTexts[index]);
            }
    
            settingsChannel.truncate(0);
            settingsWriter.flush();
            settingsChannel.force(true);
            xmlOutput.output(settingsDoc, settingsWriter);
            settingsWriter.flush();
            settingsChannel.force(true);
        }
    

    Explanation

    Based in the comment of @Michal Kay, I made various changes in the structure:

    1. Change the method to require all the values to change and update all the XML.

    2. Change the global variables of the class to make global FileChannel settingsChannel (before was an inline object).

    3. Every operation, the entire content of the file it's replaced, with this steps:

      1. Remove all the content with .truncate(0) (this set the length of the file to 0)

      2. Update the writer instance with .flush() (If don't, the data don't reach the channel)

      3. Force the data to reach the disk with .force(true) (If don't, the Writer class save the data in the cache)

      4. Write the new data in the XML file with the XMLOutputter (This doesn't change)

      5. Update all again using the 2 and 3 step.

    That it's how i solved it.