/* Copyright (C) 2023-2026 Stephan Kreutzer
 *
 * This file is part of dimgra_to_xml_1, a submodule of the
 * digital_publishing_workflow_tools package.
 *
 * dimgra_to_xml_1 is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License version 3 or any later version,
 * as published by the Free Software Foundation.
 *
 * dimgra_to_xml_1 is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of´
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Affero General Public License 3 for more details.
 *
 * You should have received a copy of the GNU Affero General Public License 3
 * along with dimgra_to_xml_1. If not, see <http://www.gnu.org/licenses/>.
 */
/**
 * @file $/dimgra_to_xml/dimgra_to_xml_1/dimgra_to_xml_1.java
 * @brief Converts a multidimensional, typed graph structure into XML fields.
 * @attention This program does a lossy conversion: order and IDs are not
 *     represented/exported/preserved.
 * @author Stephan Kreutzer
 * @since 2023-06-13
 */


import java.util.List;
import java.util.ArrayList;
import java.util.Locale;
import java.util.ResourceBundle;
import java.text.MessageFormat;
import java.io.File;
import java.io.BufferedWriter;
import java.io.OutputStreamWriter;
import java.io.FileOutputStream;
import java.io.StringWriter;
import java.io.PrintWriter;
import java.io.FileNotFoundException;
import java.io.UnsupportedEncodingException;
import java.io.IOException;
import java.net.URLDecoder;
import java.util.Map;
import java.util.HashMap;
import javax.xml.stream.XMLInputFactory;
import java.io.InputStream;
import java.io.FileInputStream;
import javax.xml.stream.XMLEventReader;
import javax.xml.stream.events.XMLEvent;
import javax.xml.stream.events.StartElement;
import javax.xml.stream.events.Attribute;
import javax.xml.namespace.QName;
import javax.xml.stream.XMLStreamException;

public class dimgra_to_xml_1
{
    public static void main(String args[])
    {
        System.out.print("dimgra_to_xml_1 Copyright (C) 2023-2026 Stephan Kreutzer\n" +
                         "This program comes with ABSOLUTELY NO WARRANTY.\n" +
                         "This is free software, and you are welcome to redistribute it\n" +
                         "under certain conditions. See the GNU Affero General Public License 3\n" +
                         "or any later version for details. Also, see the source code repository\n" +
                         "https://gitlab.com/publishing-systems/digital_publishing_workflow_tools/ and\n" +
                         "the project website https://publishing-systems.org.\n\n");

        dimgra_to_xml_1 instance = new dimgra_to_xml_1();

        try
        {
            instance.run(args);
        }
        catch (ProgramTerminationException ex)
        {
            instance.handleTermination(ex);
        }

        if (instance.resultInfoFile != null)
        {
            try
            {
                BufferedWriter writer = new BufferedWriter(
                                        new OutputStreamWriter(
                                        new FileOutputStream(instance.resultInfoFile),
                                        "UTF-8"));

                writer.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
                writer.write("<!-- This file was created by dimgra_to_xml_1, which is free software licensed under the GNU Affero General Public License 3 or any later version (see https://gitlab.com/publishing-systems/digital_publishing_workflow_tools/ and https://publishing-systems.org). -->\n");
                writer.write("<dimgra-to-xml-1-result-information>\n");
                writer.write("  <success>\n");

                if (instance.getInfoMessages().size() > 0)
                {
                    writer.write("    <info-messages>\n");

                    for (int i = 0, max = instance.getInfoMessages().size(); i < max; i++)
                    {
                        InfoMessage infoMessage = instance.getInfoMessages().get(i);

                        writer.write("      <info-message number=\"" + i + "\">\n");
                        writer.write("        <timestamp>" + infoMessage.getTimestamp() + "</timestamp>\n");

                        String infoMessageText = infoMessage.getMessage();
                        String infoMessageId = infoMessage.getId();
                        String infoMessageBundle = infoMessage.getBundle();
                        Object[] infoMessageArguments = infoMessage.getArguments();

                        if (infoMessageBundle != null)
                        {
                            // Ampersand needs to be the first, otherwise it would double-encode
                            // other entities.
                            infoMessageBundle = infoMessageBundle.replaceAll("&", "&amp;");
                            infoMessageBundle = infoMessageBundle.replaceAll("<", "&lt;");
                            infoMessageBundle = infoMessageBundle.replaceAll(">", "&gt;");

                            writer.write("        <id-bundle>" + infoMessageBundle + "</id-bundle>\n");
                        }

                        if (infoMessageId != null)
                        {
                            // Ampersand needs to be the first, otherwise it would double-encode
                            // other entities.
                            infoMessageId = infoMessageId.replaceAll("&", "&amp;");
                            infoMessageId = infoMessageId.replaceAll("<", "&lt;");
                            infoMessageId = infoMessageId.replaceAll(">", "&gt;");

                            writer.write("        <id>" + infoMessageId + "</id>\n");
                        }

                        if (infoMessageText != null)
                        {
                            // Ampersand needs to be the first, otherwise it would double-encode
                            // other entities.
                            infoMessageText = infoMessageText.replaceAll("&", "&amp;");
                            infoMessageText = infoMessageText.replaceAll("<", "&lt;");
                            infoMessageText = infoMessageText.replaceAll(">", "&gt;");

                            writer.write("        <message>" + infoMessageText + "</message>\n");
                        }

                        if (infoMessageArguments != null)
                        {
                            writer.write("        <arguments>\n");

                            int argumentCount = infoMessageArguments.length;

                            for (int j = 0; j < argumentCount; j++)
                            {
                                if (infoMessageArguments[j] == null)
                                {
                                    writer.write("          <argument number=\"" + j + "\">\n");
                                    writer.write("            <class></class>\n");
                                    writer.write("            <value>null</value>\n");
                                    writer.write("          </argument>\n");

                                    continue;
                                }

                                String className = infoMessageArguments[j].getClass().getName();

                                // Ampersand needs to be the first, otherwise it would double-encode
                                // other entities.
                                className = className.replaceAll("&", "&amp;");
                                className = className.replaceAll("<", "&lt;");
                                className = className.replaceAll(">", "&gt;");

                                String value = infoMessageArguments[j].toString();

                                // Ampersand needs to be the first, otherwise it would double-encode
                                // other entities.
                                value = value.replaceAll("&", "&amp;");
                                value = value.replaceAll("<", "&lt;");
                                value = value.replaceAll(">", "&gt;");

                                writer.write("          <argument number=\"" + j + "\">\n");
                                writer.write("            <class>" + className + "</class>\n");
                                writer.write("            <value>" + value + "</value>\n");
                                writer.write("          </argument>\n");
                            }

                            writer.write("        </arguments>\n");
                        }

                        Exception exception = infoMessage.getException();

                        if (exception != null)
                        {
                            writer.write("        <exception>\n");

                            String className = exception.getClass().getName();

                            // Ampersand needs to be the first, otherwise it would double-encode
                            // other entities.
                            className = className.replaceAll("&", "&amp;");
                            className = className.replaceAll("<", "&lt;");
                            className = className.replaceAll(">", "&gt;");

                            writer.write("          <class>" + className + "</class>\n");

                            StringWriter stringWriter = new StringWriter();
                            PrintWriter printWriter = new PrintWriter(stringWriter);
                            exception.printStackTrace(printWriter);
                            String stackTrace = stringWriter.toString();

                            // Ampersand needs to be the first, otherwise it would double-encode
                            // other entities.
                            stackTrace = stackTrace.replaceAll("&", "&amp;");
                            stackTrace = stackTrace.replaceAll("<", "&lt;");
                            stackTrace = stackTrace.replaceAll(">", "&gt;");

                            writer.write("          <stack-trace>" + stackTrace + "</stack-trace>\n");
                            writer.write("        </exception>\n");
                        }

                        writer.write("      </info-message>\n");
                    }

                    writer.write("    </info-messages>\n");
                }

                writer.write("  </success>\n");
                writer.write("</dimgra-to-xml-1-result-information>\n");
                writer.flush();
                writer.close();
            }
            catch (FileNotFoundException ex)
            {
                ex.printStackTrace();
                System.exit(-1);
            }
            catch (UnsupportedEncodingException ex)
            {
                ex.printStackTrace();
                System.exit(-1);
            }
            catch (IOException ex)
            {
                ex.printStackTrace();
                System.exit(-1);
            }
        }
    }

    public int run(String args[])
    {
        if (args.length < 2)
        {
            throw constructTermination("messageArgumentsMissing", null, getI10nString("messageArgumentsMissingUsage") + "\n\tdimgra_to_xml_1 " + getI10nString("messageParameterList") + "\n");
        }

        File resultInfoFile = new File(args[1]);

        try
        {
            resultInfoFile = resultInfoFile.getCanonicalFile();
        }
        catch (SecurityException ex)
        {
            throw constructTermination("messageResultInfoFileCantGetCanonicalPath", ex, null, resultInfoFile.getAbsolutePath());
        }
        catch (IOException ex)
        {
            throw constructTermination("messageResultInfoFileCantGetCanonicalPath", ex, null, resultInfoFile.getAbsolutePath());
        }

        if (resultInfoFile.exists() == true)
        {
            if (resultInfoFile.isFile() == true)
            {
                if (resultInfoFile.canWrite() != true)
                {
                    throw constructTermination("messageResultInfoFileIsntWritable", null, null, resultInfoFile.getAbsolutePath());
                }
            }
            else
            {
                throw constructTermination("messageResultInfoPathIsntAFile", null, null, resultInfoFile.getAbsolutePath());
            }
        }

        dimgra_to_xml_1.resultInfoFile = resultInfoFile;

        String programPath = dimgra_to_xml_1.class.getProtectionDomain().getCodeSource().getLocation().getPath();

        try
        {
            programPath = new File(programPath).getCanonicalPath() + File.separator;
            programPath = URLDecoder.decode(programPath, "UTF-8");
        }
        catch (UnsupportedEncodingException ex)
        {
            throw constructTermination("messageCantDetermineProgramPath", ex, null);
        }
        catch (IOException ex)
        {
            throw constructTermination("messageCantDetermineProgramPath", ex, null);
        }

        File jobFile = new File(args[0]);

        try
        {
            jobFile = jobFile.getCanonicalFile();
        }
        catch (SecurityException ex)
        {
            throw constructTermination("messageJobFileCantGetCanonicalPath", ex, null, jobFile.getAbsolutePath());
        }
        catch (IOException ex)
        {
            throw constructTermination("messageJobFileCantGetCanonicalPath", ex, null, jobFile.getAbsolutePath());
        }

        if (jobFile.exists() != true)
        {
            throw constructTermination("messageJobFileDoesntExist", null, null, jobFile.getAbsolutePath());
        }

        if (jobFile.isFile() != true)
        {
            throw constructTermination("messageJobPathIsntAFile", null, null, jobFile.getAbsolutePath());
        }

        if (jobFile.canRead() != true)
        {
            throw constructTermination("messageJobFileIsntReadable", null, null, jobFile.getAbsolutePath());
        }

        System.out.println("dimgra_to_xml_1: " + getI10nStringFormatted("messageCallDetails", jobFile.getAbsolutePath(), resultInfoFile.getAbsolutePath()));


        File inputFile = null;
        File outputFile = null;
        //File tempDirectory = null;
        String dimgraPrefix = "";
        Map<String, Mapping> mappings = new HashMap<String, Mapping>();

        String rootElementName = null;
        String rootElementNamespace = null;
        String rootElementPrefix = null;
        boolean rootElementOutputNamespace = true;

        String outerContainerElementName = null;
        String outerContainerElementNamespace = null;
        String outerContainerElementPrefix = null;
        boolean outerContainerElementOutputNamespace = true;

        String nodeValueElementName = null;
        String nodeValueElementNamespace = null;
        String nodeValueElementPrefix = null;
        boolean nodeValueElementOutputNamespace = true;

        try
        {
            XMLInputFactory inputFactory = XMLInputFactory.newInstance();
            InputStream in = new FileInputStream(jobFile);
            XMLEventReader eventReader = inputFactory.createXMLEventReader(in);

            while (eventReader.hasNext() == true)
            {
                XMLEvent event = eventReader.nextEvent();

                if (event.isStartElement() == true)
                {
                    String elementName = event.asStartElement().getName().getLocalPart();

                    if (elementName.equals("input-file") == true)
                    {
                        Attribute attributePath = event.asStartElement().getAttributeByName(new QName("path"));

                        if (attributePath == null)
                        {
                            throw constructTermination("messageJobFileEntryIsMissingAnAttribute", null, null, jobFile.getAbsolutePath(), elementName, "path");
                        }

                        if (inputFile != null)
                        {
                            throw constructTermination("messageJobFileElementConfiguredMoreThanOnce", null, null, jobFile.getAbsolutePath(), elementName);
                        }

                        inputFile = new File(attributePath.getValue());

                        if (inputFile.isAbsolute() != true)
                        {
                            inputFile = new File(jobFile.getAbsoluteFile().getParent() + File.separator + attributePath.getValue());
                        }

                        try
                        {
                            inputFile = inputFile.getCanonicalFile();
                        }
                        catch (SecurityException ex)
                        {
                            throw constructTermination("messageInputFileCantGetCanonicalPath", ex, null, inputFile.getAbsolutePath(), jobFile.getAbsolutePath());
                        }
                        catch (IOException ex)
                        {
                            throw constructTermination("messageInputFileCantGetCanonicalPath", ex, null, inputFile.getAbsolutePath(), jobFile.getAbsolutePath());
                        }

                        if (inputFile.exists() != true)
                        {
                            throw constructTermination("messageInputFileDoesntExist", null, null, inputFile.getAbsolutePath(), jobFile.getAbsolutePath());
                        }

                        if (inputFile.isFile() != true)
                        {
                            throw constructTermination("messageInputPathIsntAFile", null, null, inputFile.getAbsolutePath(), jobFile.getAbsolutePath());
                        }

                        if (inputFile.canRead() != true)
                        {
                            throw constructTermination("messageInputFileIsntReadable", null, null, inputFile.getAbsolutePath(), jobFile.getAbsolutePath());
                        }
                    }
                    else if (elementName.equals("output-file") == true)
                    {
                        Attribute attributePath = event.asStartElement().getAttributeByName(new QName("path"));

                        if (attributePath == null)
                        {
                            throw constructTermination("messageJobFileEntryIsMissingAnAttribute", null, null, jobFile.getAbsolutePath(), elementName, "path");
                        }

                        if (outputFile != null)
                        {
                            throw constructTermination("messageJobFileElementConfiguredMoreThanOnce", null, null, jobFile.getAbsolutePath(), elementName);
                        }

                        outputFile = new File(attributePath.getValue());

                        if (outputFile.isAbsolute() != true)
                        {
                            outputFile = new File(jobFile.getAbsoluteFile().getParent() + File.separator + attributePath.getValue());
                        }

                        try
                        {
                            outputFile = outputFile.getCanonicalFile();
                        }
                        catch (SecurityException ex)
                        {
                            throw constructTermination("messageOutputFileCantGetCanonicalPath", ex, null, outputFile.getAbsolutePath(), jobFile.getAbsolutePath());
                        }
                        catch (IOException ex)
                        {
                            throw constructTermination("messageOutputFileCantGetCanonicalPath", ex, null, outputFile.getAbsolutePath(), jobFile.getAbsolutePath());
                        }

                        if (outputFile.exists() == true)
                        {
                            if (outputFile.isFile() == true)
                            {
                                boolean deleteSuccessful = false;

                                try
                                {
                                    deleteSuccessful = outputFile.delete();
                                }
                                catch (SecurityException ex)
                                {

                                }

                                if (deleteSuccessful != true)
                                {
                                    if (outputFile.canWrite() != true)
                                    {
                                        throw constructTermination("messageOutputFileExistsButIsntWritable", null, null, outputFile.getAbsolutePath(), jobFile.getAbsolutePath());
                                    }
                                }
                            }
                            else
                            {
                                throw constructTermination("messageOutputPathExistsButIsntAFile", null, null, outputFile.getAbsolutePath(), jobFile.getAbsolutePath());
                            }
                        }
                    }
                    /*
                    else if (elementName.equals("temp-directory") == true)
                    {
                        Attribute attributePath = event.asStartElement().getAttributeByName(new QName("path"));

                        if (attributePath == null)
                        {
                            throw constructTermination("messageJobFileEntryIsMissingAnAttribute", null, null, jobFile.getAbsolutePath(), elementName, "path");
                        }

                        if (tempDirectory != null)
                        {
                            throw constructTermination("messageJobFileElementConfiguredMoreThanOnce", null, null, jobFile.getAbsolutePath(), elementName);
                        }

                        tempDirectory = new File(attributePath.getValue());

                        if (tempDirectory.isAbsolute() != true)
                        {
                            tempDirectory = new File(jobFile.getAbsoluteFile().getParent() + File.separator + attributePath.getValue());
                        }

                        try
                        {
                            tempDirectory = tempDirectory.getCanonicalFile();
                        }
                        catch (SecurityException ex)
                        {
                            throw constructTermination("messageTempDirectoryCantGetCanonicalPath", ex, null, tempDirectory.getAbsolutePath());
                        }
                        catch (IOException ex)
                        {
                            throw constructTermination("messageTempDirectoryCantGetCanonicalPath", ex, null, tempDirectory.getAbsolutePath());
                        }
                    }
                    */
                    else if (elementName.equals("settings") == true)
                    {
                        Attribute attributeDimgraPrefix = event.asStartElement().getAttributeByName(new QName("dimgra-prefix"));

                        if (attributeDimgraPrefix != null)
                        {
                            dimgraPrefix = attributeDimgraPrefix.getValue();
                        }
                    }
                    else if (elementName.equals("root-element") == true)
                    {
                        Attribute attributeName = event.asStartElement().getAttributeByName(new QName("name"));

                        if (attributeName == null)
                        {
                            throw constructTermination("messageJobFileEntryIsMissingAnAttribute", null, null, jobFile.getAbsolutePath(), elementName, "name");
                        }

                        Attribute attributeNamespace = event.asStartElement().getAttributeByName(new QName("namespace"));

                        if (attributeNamespace == null)
                        {
                            throw constructTermination("messageJobFileEntryIsMissingAnAttribute", null, null, jobFile.getAbsolutePath(), elementName, "namespace");
                        }

                        Attribute attributePrefix = event.asStartElement().getAttributeByName(new QName("prefix"));

                        if (attributePrefix == null)
                        {
                            throw constructTermination("messageJobFileEntryIsMissingAnAttribute", null, null, jobFile.getAbsolutePath(), elementName, "prefix");
                        }

                        Attribute attributeOutputNamespace = event.asStartElement().getAttributeByName(new QName("output-namespace"));

                        if (attributeOutputNamespace == null)
                        {
                            throw constructTermination("messageJobFileEntryIsMissingAnAttribute", null, null, jobFile.getAbsolutePath(), elementName, "output-namespace");
                        }

                        if (rootElementName != null)
                        {
                            throw constructTermination("messageJobFileElementConfiguredMoreThanOnce", null, null, jobFile.getAbsolutePath(), elementName);
                        }

                        /** @todo Check if this is a valid XML element name. */
                        rootElementName = attributeName.getValue();
                        rootElementNamespace = attributeNamespace.getValue();
                        rootElementPrefix = attributePrefix.getValue();
                        rootElementOutputNamespace = attributeOutputNamespace.getValue().equals("true") == true;
                    }
                    else if (elementName.equals("outer-container") == true)
                    {
                        Attribute attributeName = event.asStartElement().getAttributeByName(new QName("name"));

                        if (attributeName == null)
                        {
                            throw constructTermination("messageJobFileEntryIsMissingAnAttribute", null, null, jobFile.getAbsolutePath(), elementName, "name");
                        }

                        Attribute attributeNamespace = event.asStartElement().getAttributeByName(new QName("namespace"));

                        if (attributeNamespace == null)
                        {
                            throw constructTermination("messageJobFileEntryIsMissingAnAttribute", null, null, jobFile.getAbsolutePath(), elementName, "namespace");
                        }

                        Attribute attributePrefix = event.asStartElement().getAttributeByName(new QName("prefix"));

                        if (attributePrefix == null)
                        {
                            throw constructTermination("messageJobFileEntryIsMissingAnAttribute", null, null, jobFile.getAbsolutePath(), elementName, "prefix");
                        }

                        Attribute attributeOutputNamespace = event.asStartElement().getAttributeByName(new QName("output-namespace"));

                        if (attributeOutputNamespace == null)
                        {
                            throw constructTermination("messageJobFileEntryIsMissingAnAttribute", null, null, jobFile.getAbsolutePath(), elementName, "output-namespace");
                        }

                        if (outerContainerElementName != null)
                        {
                            throw constructTermination("messageJobFileElementConfiguredMoreThanOnce", null, null, jobFile.getAbsolutePath(), elementName);
                        }

                        /** @todo Check if this is a valid XML element name. */
                        outerContainerElementName = attributeName.getValue();
                        outerContainerElementNamespace = attributeNamespace.getValue();
                        outerContainerElementPrefix = attributePrefix.getValue();
                        outerContainerElementOutputNamespace = attributeOutputNamespace.getValue().equals("true") == true;
                    }
                    else if (elementName.equals("node-value-element") == true)
                    {
                        Attribute attributeName = event.asStartElement().getAttributeByName(new QName("name"));

                        if (attributeName == null)
                        {
                            throw constructTermination("messageJobFileEntryIsMissingAnAttribute", null, null, jobFile.getAbsolutePath(), elementName, "name");
                        }

                        Attribute attributeNamespace = event.asStartElement().getAttributeByName(new QName("namespace"));

                        if (attributeNamespace == null)
                        {
                            throw constructTermination("messageJobFileEntryIsMissingAnAttribute", null, null, jobFile.getAbsolutePath(), elementName, "namespace");
                        }

                        Attribute attributePrefix = event.asStartElement().getAttributeByName(new QName("prefix"));

                        if (attributePrefix == null)
                        {
                            throw constructTermination("messageJobFileEntryIsMissingAnAttribute", null, null, jobFile.getAbsolutePath(), elementName, "prefix");
                        }

                        Attribute attributeOutputNamespace = event.asStartElement().getAttributeByName(new QName("output-namespace"));

                        if (attributeOutputNamespace == null)
                        {
                            throw constructTermination("messageJobFileEntryIsMissingAnAttribute", null, null, jobFile.getAbsolutePath(), elementName, "output-namespace");
                        }

                        if (nodeValueElementName != null)
                        {
                            throw constructTermination("messageJobFileElementConfiguredMoreThanOnce", null, null, jobFile.getAbsolutePath(), elementName);
                        }

                        /** @todo Check if this is a valid XML element name. */
                        nodeValueElementName = attributeName.getValue();
                        nodeValueElementNamespace = attributeNamespace.getValue();
                        nodeValueElementPrefix = attributePrefix.getValue();
                        nodeValueElementOutputNamespace = attributeOutputNamespace.getValue().equals("true") == true;
                    }
                    else if (elementName.equals("element") == true)
                    {
                        Attribute attributeName = event.asStartElement().getAttributeByName(new QName("name"));

                        if (attributeName == null)
                        {
                            throw constructTermination("messageJobFileEntryIsMissingAnAttribute", null, null, jobFile.getAbsolutePath(), elementName, "name");
                        }

                        Attribute attributeNamespace = event.asStartElement().getAttributeByName(new QName("namespace"));

                        if (attributeNamespace == null)
                        {
                            throw constructTermination("messageJobFileEntryIsMissingAnAttribute", null, null, jobFile.getAbsolutePath(), elementName, "namespace");
                        }

                        Attribute attributePrefix = event.asStartElement().getAttributeByName(new QName("prefix"));

                        if (attributePrefix == null)
                        {
                            throw constructTermination("messageJobFileEntryIsMissingAnAttribute", null, null, jobFile.getAbsolutePath(), elementName, "prefix");
                        }

                        Attribute attributeOutputNamespace = event.asStartElement().getAttributeByName(new QName("output-namespace"));

                        if (attributeOutputNamespace == null)
                        {
                            throw constructTermination("messageJobFileEntryIsMissingAnAttribute", null, null, jobFile.getAbsolutePath(), elementName, "output-namespace");
                        }

                        if (mappings.containsKey(attributeNamespace.getValue()) == true)
                        {
                            throw constructTermination("messageJobFileSettingElementNamespaceConfiguredMoreThanOnce", null, null, jobFile.getAbsolutePath(), elementName, attributeNamespace.getValue());
                        }

                        /** @todo Check if attributeName.getValue() is a valid XML element name. */
                        mappings.put(attributeNamespace.getValue(), new Mapping(attributeNamespace.getValue(),
                                                                                attributePrefix.getValue(),
                                                                                attributeName.getValue(),
                                                                                attributeOutputNamespace.getValue().equals("true") == true));
                    }
                }
            }
        }
        catch (XMLStreamException ex)
        {
            throw constructTermination("messageJobFileErrorWhileReading", ex, null, jobFile.getAbsolutePath());
        }
        catch (SecurityException ex)
        {
            throw constructTermination("messageJobFileErrorWhileReading", ex, null, jobFile.getAbsolutePath());
        }
        catch (IOException ex)
        {
            throw constructTermination("messageJobFileErrorWhileReading", ex, null, jobFile.getAbsolutePath());
        }

        if (inputFile == null)
        {
            throw constructTermination("messageJobFileInputFileIsntConfigured", null, null, jobFile.getAbsolutePath(), "input-file");
        }

        if (outputFile == null)
        {
            throw constructTermination("messageJobFileOutputFileIsntConfigured", null, null, jobFile.getAbsolutePath(), "output-file");
        }

        if (rootElementName == null)
        {
            throw constructTermination("messageJobFileElementIsntConfigured", null, null, jobFile.getAbsolutePath(), "root-element");
        }

        if (rootElementOutputNamespace == true &&
            rootElementNamespace.isEmpty() == true)
        {
            throw constructTermination("messageJobFileOutputNamespaceButNamespaceIsEmpty", null, null, jobFile.getAbsolutePath(), "root-element");
        }

        if (outerContainerElementName == null)
        {
            throw constructTermination("messageJobFileElementIsntConfigured", null, null, jobFile.getAbsolutePath(), "outer-container");
        }

        if (outerContainerElementOutputNamespace == true &&
            outerContainerElementNamespace.isEmpty() == true)
        {
            throw constructTermination("messageJobFileOutputNamespaceButNamespaceIsEmpty", null, null, jobFile.getAbsolutePath(), "outer-container");
        }

        if (nodeValueElementName == null)
        {
            throw constructTermination("messageJobFileElementIsntConfigured", null, null, jobFile.getAbsolutePath(), "node-value-element");
        }

        if (nodeValueElementOutputNamespace == true &&
            nodeValueElementNamespace.isEmpty() == true)
        {
            throw constructTermination("messageJobFileOutputNamespaceButNamespaceIsEmpty", null, null, jobFile.getAbsolutePath(), "node-value-element");
        }

        /*
        if (tempDirectory == null)
        {
            tempDirectory = new File(programPath + "temp");
        }

        if (tempDirectory.exists() == true)
        {
            if (tempDirectory.isDirectory() == true)
            {
                if (tempDirectory.canWrite() != true)
                {
                    throw constructTermination("messageTempDirectoryIsntWritable", null, null, tempDirectory.getAbsolutePath());
                }
            }
            else
            {
                throw constructTermination("messageTempPathIsntADirectory", null, null, tempDirectory.getAbsolutePath());
            }
        }
        else
        {
            try
            {
                tempDirectory.mkdirs();
            }
            catch (SecurityException ex)
            {
                throw constructTermination("messageTempDirectoryCantCreate", ex, null, tempDirectory.getAbsolutePath());
            }
        }
        */


        Map<String, Node> nodes = new HashMap<String, Node>();
        List<Edge> edges = new ArrayList<Edge>();
        Map<String, Map<String, String>> establishingDimensionsIdChain = new HashMap<String, Map<String, String>>();

        try
        {
            XMLInputFactory inputFactory = XMLInputFactory.newInstance();

            // This is a converter for generic XML, don't want to deal with legacy DTD
            // remnants. Consider using $/xml_dtd_entity_resolver/xml_dtd_entity_resolver_1.
            inputFactory.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, false);
            inputFactory.setProperty(XMLInputFactory.SUPPORT_DTD, false);

            InputStream in = new FileInputStream(inputFile);
            XMLEventReader eventReader = inputFactory.createXMLEventReader(in);

            while (eventReader.hasNext() == true)
            {
                XMLEvent event = eventReader.nextEvent();

                if (event.isStartElement() == true)
                {
                    String elementName = event.asStartElement().getName().getLocalPart();

                    if (elementName.equals("node") == true)
                    {
                        Attribute attributeId = event.asStartElement().getAttributeByName(new QName("id"));

                        if (attributeId == null)
                        {
                            throw constructTermination("messageInputFileEntryIsMissingAnAttribute", null, null, inputFile.getAbsolutePath(), elementName, "id");
                        }

                        StringBuilder nodeValue = new StringBuilder();
                        StringBuilder nodeText = new StringBuilder();
                        boolean handledTitle = false;
                        boolean handledText = false;

                        while (eventReader.hasNext() == true)
                        {
                            event = eventReader.nextEvent();

                            if (event.isStartElement() == true)
                            {
                                if (event.asStartElement().getName().getLocalPart().equals("data") == true)
                                {
                                    Attribute attributeKey = event.asStartElement().getAttributeByName(new QName("key"));

                                    if (attributeKey == null)
                                    {
                                        throw constructTermination("messageInputFileEntryIsMissingAnAttribute", null, null, jobFile.getAbsolutePath(), event.asStartElement().getName().getLocalPart(), "key");
                                    }

                                    if (attributeKey.getValue().equals("htx-scheme-id://org.hypertext-systems.20180702T071630Z/dimgra.20200601T122500Z/dimgra-1.20200601T122500Z/node-title.20200601T122500Z") == true)
                                    {
                                        if (handledTitle == true)
                                        {
                                            throw constructTermination("messageInputFileNodeTitleMoreThanOnce", null, null, inputFile.getAbsolutePath());
                                        }

                                        while (eventReader.hasNext() == true)
                                        {
                                            event = eventReader.nextEvent();

                                            if (event.isCharacters() == true)
                                            {
                                                nodeValue.append(event.asCharacters().getData());
                                            }
                                            else if (event.isEndElement() == true)
                                            {
                                                if (event.asEndElement().getName().getLocalPart().equals("data") == true)
                                                {
                                                    break;
                                                }
                                            }
                                        }

                                        handledTitle = true;
                                    }
                                    else if (attributeKey.getValue().equals("htx-scheme-id://org.hypertext-systems.20180702T071630Z/dimgra.20200601T122500Z/dimgra-1.20200601T122500Z/node-text.20200601T122500Z") == true)
                                    {
                                        if (handledText == true)
                                        {
                                            throw constructTermination("messageInputFileNodeTextMoreThanOnce", null, null, inputFile.getAbsolutePath());
                                        }

                                        while (eventReader.hasNext() == true)
                                        {
                                            event = eventReader.nextEvent();

                                            if (event.isCharacters() == true)
                                            {
                                                nodeText.append(event.asCharacters().getData());
                                            }
                                            else if (event.isEndElement() == true)
                                            {
                                                if (event.asEndElement().getName().getLocalPart().equals("data") == true)
                                                {
                                                    break;
                                                }
                                            }
                                        }

                                        handledText = true;
                                    }
                                }
                            }
                            else if (event.isEndElement() == true)
                            {
                                if (event.asEndElement().getName().getLocalPart().equals("node") == true)
                                {
                                    break;
                                }
                            }
                        }

                        if (handledTitle != true)
                        {
                            throw constructTermination("messageInputFileNodeTitleMissing", null, null, inputFile.getAbsolutePath());
                        }

                        if (nodes.containsKey(attributeId.getValue()) != true)
                        {
                            nodes.put(attributeId.getValue(), new Node(attributeId.getValue(), nodeValue.toString(), nodeText.toString()));
                        }
                        else
                        {
                            throw constructTermination("messageInputFileDuplicateNodeId", null, null, inputFile.getAbsolutePath(), attributeId.getValue());
                        }
                    }
                    else if (elementName.equals("edge") == true)
                    {
                        Attribute attributeSource = event.asStartElement().getAttributeByName(new QName("source"));

                        if (attributeSource == null)
                        {
                            throw constructTermination("messageInputFileEntryIsMissingAnAttribute", null, null, jobFile.getAbsolutePath(), elementName, "source");
                        }

                        Attribute attributeTarget = event.asStartElement().getAttributeByName(new QName("target"));

                        if (attributeTarget == null)
                        {
                            throw constructTermination("messageInputFileEntryIsMissingAnAttribute", null, null, jobFile.getAbsolutePath(), elementName, "target");
                        }

                        StringBuilder edgeValue = new StringBuilder();
                        boolean handled = false;
                        boolean isEstablishingDimension = false;

                        while (eventReader.hasNext() == true)
                        {
                            event = eventReader.nextEvent();

                            if (event.isStartElement() == true)
                            {
                                if (event.asStartElement().getName().getLocalPart().equals("data") == true)
                                {
                                    Attribute attributeKey = event.asStartElement().getAttributeByName(new QName("key"));

                                    if (attributeKey == null)
                                    {
                                        throw constructTermination("messageInputFileEntryIsMissingAnAttribute", null, null, jobFile.getAbsolutePath(), event.asStartElement().getName().getLocalPart(), "key");
                                    }

                                    if (attributeKey.getValue().equals("htx-scheme-id://org.hypertext-systems.20180702T071630Z/dimgra.20200601T122500Z/dimgra-1.20200601T122500Z/dimension-target.20200601T122500Z") == true)
                                    {
                                        if (handled == true)
                                        {
                                            throw constructTermination("messageInputFileEdgeDimensionMoreThanOnce", null, null, inputFile.getAbsolutePath());
                                        }

                                        while (eventReader.hasNext() == true)
                                        {
                                            event = eventReader.nextEvent();

                                            if (event.isCharacters() == true)
                                            {
                                                edgeValue.append(event.asCharacters().getData());
                                            }
                                            else if (event.isEndElement() == true)
                                            {
                                                if (event.asEndElement().getName().getLocalPart().equals("data") == true)
                                                {
                                                    break;
                                                }
                                            }
                                        }

                                        handled = true;
                                    }
                                    else if (attributeKey.getValue().equals("htx-scheme-id://org.hypertext-systems.20180702T071630Z/dimgra.20200601T122500Z/dimgra-1.20200601T122500Z/dimension-establishing.20200601T122500Z") == true)
                                    {
                                        if (eventReader.hasNext() != true)
                                        {
                                            throw constructTermination("messageInputFilePrematureEnd", null, null, inputFile.getAbsolutePath());
                                        }

                                        event = eventReader.nextEvent();

                                        if (event.isCharacters() != true)
                                        {
                                            throw constructTermination("messageInputFileEdgeDimensionNotCharacters", null, null, inputFile.getAbsolutePath());
                                        }

                                        if (event.asCharacters().getData().equals("true") == true)
                                        {
                                            isEstablishingDimension = true;
                                        }
                                    }
                                }
                            }
                            else if (event.isEndElement() == true)
                            {
                                if (event.asEndElement().getName().getLocalPart().equals("edge") == true)
                                {
                                    break;
                                }
                            }
                        }

                        if (handled != true)
                        {
                            throw constructTermination("messageInputFileEdgeDimensionMissing", null, null, inputFile.getAbsolutePath());
                        }

                        edges.add(new Edge(attributeSource.getValue(),
                                           attributeTarget.getValue(),
                                           edgeValue.toString(),
                                           isEstablishingDimension));

                        if (isEstablishingDimension == true)
                        {
                            if (establishingDimensionsIdChain.containsKey(edgeValue.toString()) != true)
                            {
                                establishingDimensionsIdChain.put(edgeValue.toString(), new HashMap<String, String>());
                            }

                            establishingDimensionsIdChain.get(edgeValue.toString()).put(attributeSource.getValue(),
                                                                                        attributeTarget.getValue());
                        }
                    }
                }
            }
        }
        catch (XMLStreamException ex)
        {
            throw constructTermination("messageInputFileErrorWhileReading", ex, null, inputFile.getAbsolutePath());
        }
        catch (SecurityException ex)
        {
            throw constructTermination("messageInputFileErrorWhileReading", ex, null, inputFile.getAbsolutePath());
        }
        catch (IOException ex)
        {
            throw constructTermination("messageInputFileErrorWhileReading", ex, null, inputFile.getAbsolutePath());
        }


        try
        {
            BufferedWriter writer = new BufferedWriter(
                                    new OutputStreamWriter(
                                    new FileOutputStream(outputFile),
                                    "UTF-8"));

            writer.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
            writer.write("<");

            if (rootElementPrefix.isEmpty() != true &&
                rootElementOutputNamespace == true)
            {
                writer.write(rootElementPrefix);
                writer.write(":");
            }

            writer.write(rootElementName);

            if (rootElementOutputNamespace == true)
            {
                writer.write(" xmlns");

                if (rootElementPrefix.isEmpty() != true)
                {
                    writer.write(":");
                    writer.write(rootElementPrefix);
                }

                writer.write("=\"");

                // Ampersand needs to be the first, otherwise it would double-encode
                // other entities.
                String namespace = rootElementNamespace.replaceAll("&", "&amp;");
                namespace = namespace.replaceAll("\"", "&quot;");
                namespace = namespace.replaceAll("'", "&apos;");
                namespace = namespace.replaceAll("<", "&lt;");
                namespace = namespace.replaceAll(">", "&gt;");

                writer.write(namespace);
                writer.write("\"");
            }

            if (outerContainerElementOutputNamespace == true &&
                outerContainerElementPrefix.isEmpty() != true)
            {
                writer.write(" xmlns:");
                writer.write(outerContainerElementPrefix);
                writer.write("=\"");

                // Ampersand needs to be the first, otherwise it would double-encode
                // other entities.
                String namespace = outerContainerElementNamespace.replaceAll("&", "&amp;");
                namespace = namespace.replaceAll("\"", "&quot;");
                namespace = namespace.replaceAll("'", "&apos;");
                namespace = namespace.replaceAll("<", "&lt;");
                namespace = namespace.replaceAll(">", "&gt;");

                writer.write(namespace);
                writer.write("\"");
            }

            if (nodeValueElementOutputNamespace == true &&
                nodeValueElementPrefix.isEmpty() != true)
            {
                writer.write(" xmlns:");
                writer.write(nodeValueElementPrefix);
                writer.write("=\"");

                // Ampersand needs to be the first, otherwise it would double-encode
                // other entities.
                String namespace = nodeValueElementNamespace.replaceAll("&", "&amp;");
                namespace = namespace.replaceAll("\"", "&quot;");
                namespace = namespace.replaceAll("'", "&apos;");
                namespace = namespace.replaceAll("<", "&lt;");
                namespace = namespace.replaceAll(">", "&gt;");

                writer.write(namespace);
                writer.write("\"");
            }

            for (Map.Entry<String, Mapping> mapping : mappings.entrySet())
            {
                if (mapping.getValue().getOutputNamespace() == true &&
                    mapping.getValue().getPrefix().isEmpty() != true)
                {
                    writer.write(" xmlns:");
                    writer.write(mapping.getValue().getPrefix());
                    writer.write("=\"");

                    // Ampersand needs to be the first, otherwise it would double-encode
                    // other entities.
                    String namespace = mapping.getValue().getNamespace().replaceAll("&", "&amp;");
                    namespace = namespace.replaceAll("\"", "&quot;");
                    namespace = namespace.replaceAll("'", "&apos;");
                    namespace = namespace.replaceAll("<", "&lt;");
                    namespace = namespace.replaceAll(">", "&gt;");

                    writer.write(namespace);
                    writer.write("\"");
                }
            }

            if (dimgraPrefix.isEmpty() != true)
            {
                writer.write(" xmlns:");
                writer.write(dimgraPrefix);
                writer.write("=\"htx-scheme-id://org.hypertext-systems.20180702T071630Z/dimgra.20200601T122500Z/dimgra-1.20200601T122500Z\"");
            }

            writer.write(">");

            writer.write("<!-- This file was created by dimgra_to_xml_1, which is free software licensed under the GNU Affero General Public License 3 or any later version (see https://gitlab.com/publishing-systems/digital_publishing_workflow_tools/ and https://publishing-systems.org). -->");

            String establishingDimensionLastId = null;

            {
                Map<String, String> chain = establishingDimensionsIdChain.get(outerContainerElementNamespace);

                if (chain == null)
                {
                    throw constructTermination("messageOuterContainerElementNamespaceDimensionNotFound", null, null, outerContainerElementNamespace);
                }

                Map.Entry<String, String> entry = chain.entrySet().iterator().next();

                if (entry == null)
                {
                    throw constructTermination("messageOuterContainerElementNamespaceDimensionWithoutEdges", null, null, outerContainerElementNamespace);
                }

                String key = entry.getValue();

                // Should better not be circular...
                while (chain.containsKey(key) == true)
                {
                    key = chain.get(key);
                }

                establishingDimensionLastId = key;
            }


            /** @todo Nested loops over edges and nodes is pretty unperformant,
              * maybe rather do some lookup instead? */
            for (Edge edge : edges)
            {
                if (edge.getDimension().equals(outerContainerElementNamespace) == true &&
                    edge.getIsEstablishingDimension() == true)
                {
                    writer.write("<");

                    if (outerContainerElementOutputNamespace == true &&
                        outerContainerElementPrefix.isEmpty() != true)
                    {
                        writer.write(outerContainerElementPrefix);
                        writer.write(":");
                    }

                    writer.write(outerContainerElementName);

                    if (outerContainerElementOutputNamespace == true &&
                        outerContainerElementPrefix.isEmpty() == true)
                    {
                        writer.write(" xmlns=\"");

                        // Ampersand needs to be the first, otherwise it would double-encode
                        // other entities.
                        String namespace = outerContainerElementNamespace.replaceAll("&", "&amp;");
                        namespace = namespace.replaceAll("\"", "&quot;");
                        namespace = namespace.replaceAll("'", "&apos;");
                        namespace = namespace.replaceAll("<", "&lt;");
                        namespace = namespace.replaceAll(">", "&gt;");

                        writer.write(namespace);
                        writer.write("\"");
                    }

                    writer.write(">");

                    for (Map.Entry<String, Node> node : nodes.entrySet())
                    {
                        if (node.getKey().equals(edge.getSourceId()) == true)
                        {
                            writer.write("<");

                            if (nodeValueElementOutputNamespace == true &&
                                nodeValueElementPrefix.isEmpty() != true)
                            {
                                writer.write(nodeValueElementPrefix);
                                writer.write(":");
                            }

                            writer.write(nodeValueElementName);

                            writer.write(" id=\"");

                            {
                                // Ampersand needs to be the first, otherwise it would double-encode
                                // other entities.
                                String nodeId = node.getValue().getId().replaceAll("&", "&amp;");
                                nodeId = nodeId.replaceAll("\"", "&quot;");
                                nodeId = nodeId.replaceAll("'", "&apos;");
                                nodeId = nodeId.replaceAll("<", "&lt;");
                                nodeId = nodeId.replaceAll(">", "&gt;");

                                writer.write(nodeId);
                            }

                            writer.write("\"");

                            if (nodeValueElementOutputNamespace == true &&
                                nodeValueElementPrefix.isEmpty() == true)
                            {
                                writer.write(" xmlns=\"");

                                // Ampersand needs to be the first, otherwise it would double-encode
                                // other entities.
                                String namespace = nodeValueElementNamespace.replaceAll("&", "&amp;");
                                namespace = namespace.replaceAll("\"", "&quot;");
                                namespace = namespace.replaceAll("'", "&apos;");
                                namespace = namespace.replaceAll("<", "&lt;");
                                namespace = namespace.replaceAll(">", "&gt;");

                                writer.write(namespace);
                                writer.write("\"");
                            }

                            writer.write(">");

                            String value = node.getValue().getValue();
                            // Ampersand needs to be the first, otherwise it would double-encode
                            // other entities.
                            value = value.replaceAll("&", "&amp;");
                            value = value.replaceAll("<", "&lt;");
                            value = value.replaceAll(">", "&gt;");

                            writer.write(value);

                            if (node.getValue().getNote().isEmpty() != true)
                            {
                                if (dimgraPrefix.isEmpty() != true)
                                {
                                    writer.write("<");
                                    writer.write(dimgraPrefix);
                                    writer.write(":note>");

                                    String note = node.getValue().getNote();
                                    // Ampersand needs to be the first, otherwise it would double-encode
                                    // other entities.
                                    note = note.replaceAll("&", "&amp;");
                                    note = note.replaceAll("<", "&lt;");
                                    note = note.replaceAll(">", "&gt;");

                                    writer.write(note);

                                    writer.write("</");
                                    writer.write(dimgraPrefix);
                                    writer.write(":note>");
                                }
                                else
                                {
                                    writer.write("<note xmlns=\"htx-scheme-id://org.hypertext-systems.20180702T071630Z/dimgra.20200601T122500Z/dimgra-1.20200601T122500Z/node-text.20200601T122500Z\">");

                                    String note = node.getValue().getNote();
                                    // Ampersand needs to be the first, otherwise it would double-encode
                                    // other entities.
                                    note = note.replaceAll("&", "&amp;");
                                    note = note.replaceAll("<", "&lt;");
                                    note = note.replaceAll(">", "&gt;");

                                    writer.write(note);

                                    writer.write("</note>");
                                }
                            }

                            writer.write("</");

                            if (nodeValueElementOutputNamespace == true &&
                                nodeValueElementPrefix.isEmpty() != true)
                            {
                                writer.write(nodeValueElementPrefix);
                                writer.write(":");
                            }

                            writer.write(nodeValueElementName);
                            writer.write(">");
                        }
                    }

                    for (Edge edge2 : edges)
                    {
                        if (edge.getSourceId().equals(edge2.getSourceId()) == true &&
                            edge2.getIsEstablishingDimension() != true)
                        {
                            for (Map.Entry<String, Node> node : nodes.entrySet())
                            {
                                if (node.getKey().equals(edge2.getTargetId()) == true)
                                {
                                    if (mappings.containsKey(edge2.getDimension()) != true)
                                    {
                                        throw constructTermination("messageInputFileWithDimensionThatsNotConfiguredInJobFile", null, null, inputFile.getAbsolutePath(), edge2.getDimension(), jobFile.getAbsolutePath());
                                    }

                                    writer.write("<");

                                    if (mappings.get(edge2.getDimension()).getOutputNamespace() == true &&
                                        mappings.get(edge2.getDimension()).getPrefix().isEmpty() != true)
                                    {
                                        writer.write(mappings.get(edge2.getDimension()).getPrefix());
                                        writer.write(":");
                                    }

                                    writer.write(mappings.get(edge2.getDimension()).getElementName());

                                    writer.write(" id=\"");

                                    {
                                        // Ampersand needs to be the first, otherwise it would double-encode
                                        // other entities.
                                        String nodeId = node.getValue().getId().replaceAll("&", "&amp;");
                                        nodeId = nodeId.replaceAll("\"", "&quot;");
                                        nodeId = nodeId.replaceAll("'", "&apos;");
                                        nodeId = nodeId.replaceAll("<", "&lt;");
                                        nodeId = nodeId.replaceAll(">", "&gt;");

                                        writer.write(nodeId);
                                    }

                                    writer.write("\"");

                                    if (mappings.get(edge2.getDimension()).getOutputNamespace() == true &&
                                        mappings.get(edge2.getDimension()).getPrefix().isEmpty() == true)
                                    {
                                        writer.write(" xmlns=\"");

                                        // Ampersand needs to be the first, otherwise it would double-encode
                                        // other entities.
                                        String namespace = mappings.get(edge2.getDimension()).getNamespace().replaceAll("&", "&amp;");
                                        namespace = namespace.replaceAll("\"", "&quot;");
                                        namespace = namespace.replaceAll("'", "&apos;");
                                        namespace = namespace.replaceAll("<", "&lt;");
                                        namespace = namespace.replaceAll(">", "&gt;");

                                        writer.write(namespace);
                                        writer.write("\"");
                                    }

                                    writer.write(">");

                                    String value = node.getValue().getValue();
                                    // Ampersand needs to be the first, otherwise it would double-encode
                                    // other entities.
                                    value = value.replaceAll("&", "&amp;");
                                    value = value.replaceAll("<", "&lt;");
                                    value = value.replaceAll(">", "&gt;");

                                    writer.write(value);

                                    if (node.getValue().getNote().isEmpty() != true)
                                    {
                                        if (dimgraPrefix.isEmpty() != true)
                                        {
                                            writer.write("<");
                                            writer.write(dimgraPrefix);
                                            writer.write(":note>");

                                            String note = node.getValue().getNote();
                                            // Ampersand needs to be the first, otherwise it would double-encode
                                            // other entities.
                                            note = note.replaceAll("&", "&amp;");
                                            note = note.replaceAll("<", "&lt;");
                                            note = note.replaceAll(">", "&gt;");

                                            writer.write(note);

                                            writer.write("</");
                                            writer.write(dimgraPrefix);
                                            writer.write(":note>");
                                        }
                                        else
                                        {
                                            writer.write("<note xmlns=\"htx-scheme-id://org.hypertext-systems.20180702T071630Z/dimgra.20200601T122500Z/dimgra-1.20200601T122500Z/node-text.20200601T122500Z\">");

                                            String note = node.getValue().getNote();
                                            // Ampersand needs to be the first, otherwise it would double-encode
                                            // other entities.
                                            note = note.replaceAll("&", "&amp;");
                                            note = note.replaceAll("<", "&lt;");
                                            note = note.replaceAll(">", "&gt;");

                                            writer.write(note);

                                            writer.write("</note>");
                                        }
                                    }

                                    writer.write("</");

                                    if (mappings.get(edge2.getDimension()).getOutputNamespace() == true &&
                                        mappings.get(edge2.getDimension()).getPrefix().isEmpty() != true)
                                    {
                                        writer.write(mappings.get(edge2.getDimension()).getPrefix());
                                        writer.write(":");
                                    }

                                    writer.write(mappings.get(edge2.getDimension()).getElementName());

                                    writer.write(">");
                                }
                            }
                        }
                    }

                    writer.write("</");

                    if (outerContainerElementOutputNamespace == true &&
                        outerContainerElementPrefix.isEmpty() != true)
                    {
                        writer.write(outerContainerElementPrefix);
                        writer.write(":");
                    }

                    writer.write(outerContainerElementName);
                    writer.write(">");
                }
            }


            // Here follows a slightly adjusted duplication of the code above,
            // just filtering for the last entry in the container established
            // dimension only, which would otherwise be missing from the code
            // above.

            /** @todo Nested loops over edges and nodes is pretty unperformant,
              * maybe rather do some lookup instead? */
            for (Edge edge : edges)
            {
                if (edge.getDimension().equals(outerContainerElementNamespace) == true &&
                    edge.getIsEstablishingDimension() == true &&
                    edge.getTargetId().equals(establishingDimensionLastId) == true)
                {
                    writer.write("<");

                    if (outerContainerElementOutputNamespace == true &&
                        outerContainerElementPrefix.isEmpty() != true)
                    {
                        writer.write(outerContainerElementPrefix);
                        writer.write(":");
                    }

                    writer.write(outerContainerElementName);

                    if (outerContainerElementOutputNamespace == true &&
                        outerContainerElementPrefix.isEmpty() == true)
                    {
                        writer.write(" xmlns=\"");

                        // Ampersand needs to be the first, otherwise it would double-encode
                        // other entities.
                        String namespace = outerContainerElementNamespace.replaceAll("&", "&amp;");
                        namespace = namespace.replaceAll("\"", "&quot;");
                        namespace = namespace.replaceAll("'", "&apos;");
                        namespace = namespace.replaceAll("<", "&lt;");
                        namespace = namespace.replaceAll(">", "&gt;");

                        writer.write(namespace);
                        writer.write("\"");
                    }

                    writer.write(">");

                    for (Map.Entry<String, Node> node : nodes.entrySet())
                    {
                        if (node.getKey().equals(edge.getTargetId()) == true)
                        {
                            writer.write("<");

                            if (nodeValueElementOutputNamespace == true &&
                                nodeValueElementPrefix.isEmpty() != true)
                            {
                                writer.write(nodeValueElementPrefix);
                                writer.write(":");
                            }

                            writer.write(nodeValueElementName);

                            writer.write(" id=\"");

                            {
                                // Ampersand needs to be the first, otherwise it would double-encode
                                // other entities.
                                String nodeId = node.getValue().getId().replaceAll("&", "&amp;");
                                nodeId = nodeId.replaceAll("\"", "&quot;");
                                nodeId = nodeId.replaceAll("'", "&apos;");
                                nodeId = nodeId.replaceAll("<", "&lt;");
                                nodeId = nodeId.replaceAll(">", "&gt;");

                                writer.write(nodeId);
                            }

                            writer.write("\"");

                            if (nodeValueElementOutputNamespace == true &&
                                nodeValueElementPrefix.isEmpty() == true)
                            {
                                writer.write(" xmlns=\"");

                                // Ampersand needs to be the first, otherwise it would double-encode
                                // other entities.
                                String namespace = nodeValueElementNamespace.replaceAll("&", "&amp;");
                                namespace = namespace.replaceAll("\"", "&quot;");
                                namespace = namespace.replaceAll("'", "&apos;");
                                namespace = namespace.replaceAll("<", "&lt;");
                                namespace = namespace.replaceAll(">", "&gt;");

                                writer.write(namespace);
                                writer.write("\"");
                            }

                            writer.write(">");

                            String value = node.getValue().getValue();
                            // Ampersand needs to be the first, otherwise it would double-encode
                            // other entities.
                            value = value.replaceAll("&", "&amp;");
                            value = value.replaceAll("<", "&lt;");
                            value = value.replaceAll(">", "&gt;");

                            writer.write(value);

                            if (node.getValue().getNote().isEmpty() != true)
                            {
                                if (dimgraPrefix.isEmpty() != true)
                                {
                                    writer.write("<");
                                    writer.write(dimgraPrefix);
                                    writer.write(":note>");

                                    String note = node.getValue().getNote();
                                    // Ampersand needs to be the first, otherwise it would double-encode
                                    // other entities.
                                    note = note.replaceAll("&", "&amp;");
                                    note = note.replaceAll("<", "&lt;");
                                    note = note.replaceAll(">", "&gt;");

                                    writer.write(note);

                                    writer.write("</");
                                    writer.write(dimgraPrefix);
                                    writer.write(":note>");
                                }
                                else
                                {
                                    writer.write("<note xmlns=\"htx-scheme-id://org.hypertext-systems.20180702T071630Z/dimgra.20200601T122500Z/dimgra-1.20200601T122500Z/node-text.20200601T122500Z\">");

                                    String note = node.getValue().getNote();
                                    // Ampersand needs to be the first, otherwise it would double-encode
                                    // other entities.
                                    note = note.replaceAll("&", "&amp;");
                                    note = note.replaceAll("<", "&lt;");
                                    note = note.replaceAll(">", "&gt;");

                                    writer.write(note);

                                    writer.write("</note>");
                                }
                            }

                            writer.write("</");

                            if (nodeValueElementOutputNamespace == true &&
                                nodeValueElementPrefix.isEmpty() != true)
                            {
                                writer.write(nodeValueElementPrefix);
                                writer.write(":");
                            }

                            writer.write(nodeValueElementName);
                            writer.write(">");
                        }
                    }

                    for (Edge edge2 : edges)
                    {
                        if (edge.getTargetId().equals(edge2.getSourceId()) == true &&
                            edge2.getIsEstablishingDimension() != true)
                        {
                            for (Map.Entry<String, Node> node : nodes.entrySet())
                            {
                                if (node.getKey().equals(edge2.getTargetId()) == true)
                                {
                                    if (mappings.containsKey(edge2.getDimension()) != true)
                                    {
                                        throw constructTermination("messageInputFileWithDimensionThatsNotConfiguredInJobFile", null, null, inputFile.getAbsolutePath(), edge2.getDimension(), jobFile.getAbsolutePath());
                                    }

                                    writer.write("<");

                                    if (mappings.get(edge2.getDimension()).getOutputNamespace() == true &&
                                        mappings.get(edge2.getDimension()).getPrefix().isEmpty() != true)
                                    {
                                        writer.write(mappings.get(edge2.getDimension()).getPrefix());
                                        writer.write(":");
                                    }

                                    writer.write(mappings.get(edge2.getDimension()).getElementName());

                                    writer.write(" id=\"");

                                    {
                                        // Ampersand needs to be the first, otherwise it would double-encode
                                        // other entities.
                                        String nodeId = node.getValue().getId().replaceAll("&", "&amp;");
                                        nodeId = nodeId.replaceAll("\"", "&quot;");
                                        nodeId = nodeId.replaceAll("'", "&apos;");
                                        nodeId = nodeId.replaceAll("<", "&lt;");
                                        nodeId = nodeId.replaceAll(">", "&gt;");

                                        writer.write(nodeId);
                                    }

                                    writer.write("\"");

                                    if (mappings.get(edge2.getDimension()).getOutputNamespace() == true &&
                                        mappings.get(edge2.getDimension()).getPrefix().isEmpty() == true)
                                    {
                                        writer.write(" xmlns=\"");

                                        // Ampersand needs to be the first, otherwise it would double-encode
                                        // other entities.
                                        String namespace = mappings.get(edge2.getDimension()).getNamespace().replaceAll("&", "&amp;");
                                        namespace = namespace.replaceAll("\"", "&quot;");
                                        namespace = namespace.replaceAll("'", "&apos;");
                                        namespace = namespace.replaceAll("<", "&lt;");
                                        namespace = namespace.replaceAll(">", "&gt;");

                                        writer.write(namespace);
                                        writer.write("\"");
                                    }

                                    writer.write(">");

                                    String value = node.getValue().getValue();
                                    // Ampersand needs to be the first, otherwise it would double-encode
                                    // other entities.
                                    value = value.replaceAll("&", "&amp;");
                                    value = value.replaceAll("<", "&lt;");
                                    value = value.replaceAll(">", "&gt;");

                                    writer.write(value);

                                    if (node.getValue().getNote().isEmpty() != true)
                                    {
                                        if (dimgraPrefix.isEmpty() != true)
                                        {
                                            writer.write("<");
                                            writer.write(dimgraPrefix);
                                            writer.write(":note>");

                                            String note = node.getValue().getNote();
                                            // Ampersand needs to be the first, otherwise it would double-encode
                                            // other entities.
                                            note = note.replaceAll("&", "&amp;");
                                            note = note.replaceAll("<", "&lt;");
                                            note = note.replaceAll(">", "&gt;");

                                            writer.write(note);

                                            writer.write("</");
                                            writer.write(dimgraPrefix);
                                            writer.write(":note>");
                                        }
                                        else
                                        {
                                            writer.write("<note xmlns=\"htx-scheme-id://org.hypertext-systems.20180702T071630Z/dimgra.20200601T122500Z/dimgra-1.20200601T122500Z/node-text.20200601T122500Z\">");

                                            String note = node.getValue().getNote();
                                            // Ampersand needs to be the first, otherwise it would double-encode
                                            // other entities.
                                            note = note.replaceAll("&", "&amp;");
                                            note = note.replaceAll("<", "&lt;");
                                            note = note.replaceAll(">", "&gt;");

                                            writer.write(note);

                                            writer.write("</note>");
                                        }
                                    }

                                    writer.write("</");

                                    if (mappings.get(edge2.getDimension()).getOutputNamespace() == true &&
                                        mappings.get(edge2.getDimension()).getPrefix().isEmpty() != true)
                                    {
                                        writer.write(mappings.get(edge2.getDimension()).getPrefix());
                                        writer.write(":");
                                    }

                                    writer.write(mappings.get(edge2.getDimension()).getElementName());

                                    writer.write(">");
                                }
                            }
                        }
                    }

                    writer.write("</");

                    if (outerContainerElementOutputNamespace == true &&
                        outerContainerElementPrefix.isEmpty() != true)
                    {
                        writer.write(outerContainerElementPrefix);
                        writer.write(":");
                    }

                    writer.write(outerContainerElementName);
                    writer.write(">");
                }
            }


            writer.write("</");

            if (rootElementPrefix.isEmpty() != true &&
                rootElementOutputNamespace == true)
            {
                writer.write(rootElementPrefix);
                writer.write(":");
            }

            writer.write(rootElementName);
            writer.write(">");

            writer.flush();
            writer.close();
        }
        catch (FileNotFoundException ex)
        {
            throw constructTermination("messageOutputFileErrorWhileWriting", ex, null, outputFile.getAbsolutePath());
        }
        catch (UnsupportedEncodingException ex)
        {
            throw constructTermination("messageOutputFileErrorWhileWriting", ex, null, outputFile.getAbsolutePath());
        }
        catch (IOException ex)
        {
            throw constructTermination("messageOutputFileErrorWhileWriting", ex, null, outputFile.getAbsolutePath());
        }

        return 0;
    }

    public InfoMessage constructInfoMessage(String id,
                                            boolean outputToConsole,
                                            Exception exception,
                                            String message,
                                            Object ... arguments)
    {
        if (message == null)
        {
            if (arguments == null)
            {
                message = "dimgra_to_xml_1: " + getI10nString(id);
            }
            else
            {
                message = "dimgra_to_xml_1: " + getI10nStringFormatted(id, arguments);
            }
        }

        if (outputToConsole == true)
        {
            System.out.println(message);

            if (exception != null)
            {
                System.out.println(exception.getMessage());
                exception.printStackTrace();
            }
        }

        return new InfoMessage(id, exception, message, L10N_BUNDLE, arguments);
    }

    public ProgramTerminationException constructTermination(String id, Exception cause, String message, Object ... arguments)
    {
        if (message == null)
        {
            if (arguments == null)
            {
                message = "dimgra_to_xml_1: " + getI10nString(id);
            }
            else
            {
                message = "dimgra_to_xml_1: " + getI10nStringFormatted(id, arguments);
            }
        }

        return new ProgramTerminationException(id, cause, message, L10N_BUNDLE, arguments);
    }

    public int handleTermination(ProgramTerminationException ex)
    {
        String message = ex.getMessage();
        String id = ex.getId();
        String bundle = ex.getBundle();
        Object[] arguments = ex.getArguments();
        boolean normalTermination = ex.isNormalTermination();

        if (message != null)
        {
            System.err.println(message);
        }

        Throwable innerException = ex.getCause();

        if (innerException != null)
        {
            System.out.println(innerException.getMessage());
            innerException.printStackTrace();
        }

        if (dimgra_to_xml_1.resultInfoFile != null)
        {
            try
            {
                BufferedWriter writer = new BufferedWriter(
                                        new OutputStreamWriter(
                                        new FileOutputStream(dimgra_to_xml_1.resultInfoFile),
                                        "UTF-8"));

                writer.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
                writer.write("<!-- This file was created by dimgra_to_xml_1, which is free software licensed under the GNU Affero General Public License 3 or any later version (see https://gitlab.com/publishing-systems/digital_publishing_workflow_tools/ and https://publishing-systems.org). -->\n");
                writer.write("<dimgra-to-xml-1-result-information>\n");

                if (normalTermination == false)
                {
                    writer.write("  <failure>\n");
                }
                else
                {
                    writer.write("  <success>\n");
                }

                writer.write("    <timestamp>" + ex.getTimestamp() + "</timestamp>\n");

                if (bundle != null)
                {
                    // Ampersand needs to be the first, otherwise it would double-encode
                    // other entities.
                    bundle = bundle.replaceAll("&", "&amp;");
                    bundle = bundle.replaceAll("<", "&lt;");
                    bundle = bundle.replaceAll(">", "&gt;");

                    writer.write("    <id-bundle>" + bundle + "</id-bundle>\n");
                }

                if (id != null)
                {
                    // Ampersand needs to be the first, otherwise it would double-encode
                    // other entities.
                    id = id.replaceAll("&", "&amp;");
                    id = id.replaceAll("<", "&lt;");
                    id = id.replaceAll(">", "&gt;");

                    writer.write("    <id>" + id + "</id>\n");
                }

                if (message != null)
                {
                    // Ampersand needs to be the first, otherwise it would double-encode
                    // other entities.
                    message = message.replaceAll("&", "&amp;");
                    message = message.replaceAll("<", "&lt;");
                    message = message.replaceAll(">", "&gt;");

                    writer.write("    <message>" + message + "</message>\n");
                }

                if (arguments != null)
                {
                    writer.write("    <arguments>\n");

                    int argumentCount = arguments.length;

                    for (int i = 0; i < argumentCount; i++)
                    {
                        if (arguments[i] == null)
                        {
                            writer.write("      <argument number=\"" + i + "\">\n");
                            writer.write("        <class></class>\n");
                            writer.write("        <value>null</value>\n");
                            writer.write("      </argument>\n");

                            continue;
                        }

                        String className = arguments[i].getClass().getName();

                        // Ampersand needs to be the first, otherwise it would double-encode
                        // other entities.
                        className = className.replaceAll("&", "&amp;");
                        className = className.replaceAll("<", "&lt;");
                        className = className.replaceAll(">", "&gt;");

                        String value = arguments[i].toString();

                        // Ampersand needs to be the first, otherwise it would double-encode
                        // other entities.
                        value = value.replaceAll("&", "&amp;");
                        value = value.replaceAll("<", "&lt;");
                        value = value.replaceAll(">", "&gt;");

                        writer.write("      <argument number=\"" + i + "\">\n");
                        writer.write("        <class>" + className + "</class>\n");
                        writer.write("        <value>" + value + "</value>\n");
                        writer.write("      </argument>\n");
                    }

                    writer.write("    </arguments>\n");
                }

                if (innerException != null)
                {
                    writer.write("    <exception>\n");

                    String className = innerException.getClass().getName();

                    // Ampersand needs to be the first, otherwise it would double-encode
                    // other entities.
                    className = className.replaceAll("&", "&amp;");
                    className = className.replaceAll("<", "&lt;");
                    className = className.replaceAll(">", "&gt;");

                    writer.write("      <class>" + className + "</class>\n");

                    StringWriter stringWriter = new StringWriter();
                    PrintWriter printWriter = new PrintWriter(stringWriter);
                    innerException.printStackTrace(printWriter);
                    String stackTrace = stringWriter.toString();

                    // Ampersand needs to be the first, otherwise it would double-encode
                    // other entities.
                    stackTrace = stackTrace.replaceAll("&", "&amp;");
                    stackTrace = stackTrace.replaceAll("<", "&lt;");
                    stackTrace = stackTrace.replaceAll(">", "&gt;");

                    writer.write("      <stack-trace>" + stackTrace + "</stack-trace>\n");
                    writer.write("    </exception>\n");
                }

                if (this.getInfoMessages().size() > 0)
                {
                    writer.write("    <info-messages>\n");

                    for (int i = 0, max = this.getInfoMessages().size(); i < max; i++)
                    {
                        InfoMessage infoMessage = this.getInfoMessages().get(i);

                        writer.write("      <info-message>\n");
                        writer.write("        <timestamp>" + infoMessage.getTimestamp() + "</timestamp>\n");

                        String infoMessageText = infoMessage.getMessage();
                        String infoMessageId = ex.getId();
                        String infoMessageBundle = ex.getBundle();
                        Object[] infoMessageArguments = ex.getArguments();

                        if (infoMessageBundle != null)
                        {
                            // Ampersand needs to be the first, otherwise it would double-encode
                            // other entities.
                            infoMessageBundle = infoMessageBundle.replaceAll("&", "&amp;");
                            infoMessageBundle = infoMessageBundle.replaceAll("<", "&lt;");
                            infoMessageBundle = infoMessageBundle.replaceAll(">", "&gt;");

                            writer.write("        <id-bundle>" + infoMessageBundle + "</id-bundle>\n");
                        }

                        if (infoMessageId != null)
                        {
                            // Ampersand needs to be the first, otherwise it would double-encode
                            // other entities.
                            infoMessageId = infoMessageId.replaceAll("&", "&amp;");
                            infoMessageId = infoMessageId.replaceAll("<", "&lt;");
                            infoMessageId = infoMessageId.replaceAll(">", "&gt;");

                            writer.write("        <id>" + infoMessageId + "</id>\n");
                        }

                        if (infoMessageText != null)
                        {
                            // Ampersand needs to be the first, otherwise it would double-encode
                            // other entities.
                            infoMessageText = infoMessageText.replaceAll("&", "&amp;");
                            infoMessageText = infoMessageText.replaceAll("<", "&lt;");
                            infoMessageText = infoMessageText.replaceAll(">", "&gt;");

                            writer.write("        <message>" + infoMessageText + "</message>\n");
                        }

                        if (infoMessageArguments != null)
                        {
                            writer.write("        <arguments>\n");

                            int argumentCount = infoMessageArguments.length;

                            for (int j = 0; j < argumentCount; j++)
                            {
                                if (infoMessageArguments[j] == null)
                                {
                                    writer.write("          <argument number=\"" + j + "\">\n");
                                    writer.write("            <class></class>\n");
                                    writer.write("            <value>null</value>\n");
                                    writer.write("          </argument>\n");

                                    continue;
                                }

                                String className = infoMessageArguments[j].getClass().getName();

                                // Ampersand needs to be the first, otherwise it would double-encode
                                // other entities.
                                className = className.replaceAll("&", "&amp;");
                                className = className.replaceAll("<", "&lt;");
                                className = className.replaceAll(">", "&gt;");

                                String value = infoMessageArguments[j].toString();

                                // Ampersand needs to be the first, otherwise it would double-encode
                                // other entities.
                                value = value.replaceAll("&", "&amp;");
                                value = value.replaceAll("<", "&lt;");
                                value = value.replaceAll(">", "&gt;");

                                writer.write("          <argument number=\"" + j + "\">\n");
                                writer.write("            <class>" + className + "</class>\n");
                                writer.write("            <value>" + value + "</value>\n");
                                writer.write("          </argument>\n");
                            }

                            writer.write("        </arguments>\n");
                        }

                        writer.write("      </info-message>\n");
                    }

                    writer.write("    </info-messages>\n");
                }

                if (normalTermination == false)
                {
                    writer.write("  </failure>\n");
                }
                else
                {
                    writer.write("  </success>\n");
                }

                writer.write("</dimgra-to-xml-1-result-information>\n");
                writer.flush();
                writer.close();
            }
            catch (FileNotFoundException ex2)
            {
                ex2.printStackTrace();
            }
            catch (UnsupportedEncodingException ex2)
            {
                ex2.printStackTrace();
            }
            catch (IOException ex2)
            {
                ex2.printStackTrace();
            }
        }

        dimgra_to_xml_1.resultInfoFile = null;

        System.exit(-1);
        return -1;
    }

    public List<InfoMessage> getInfoMessages()
    {
        return this.infoMessages;
    }

    public Locale getLocale()
    {
        return Locale.getDefault();
    }

    /**
     * @brief This method interprets l10n strings from a .properties file as encoded in UTF-8.
     */
    private String getI10nString(String key)
    {
        if (this.l10nConsole == null)
        {
            this.l10nConsole = ResourceBundle.getBundle(L10N_BUNDLE, this.getLocale());
        }

        try
        {
            return new String(this.l10nConsole.getString(key).getBytes("UTF-8"), "UTF-8");
        }
        catch (UnsupportedEncodingException ex)
        {
            return this.l10nConsole.getString(key);
        }
    }

    private String getI10nStringFormatted(String i10nStringName, Object ... arguments)
    {
        MessageFormat formatter = new MessageFormat("");
        formatter.setLocale(this.getLocale());

        formatter.applyPattern(getI10nString(i10nStringName));
        return formatter.format(arguments);
    }

    public static File resultInfoFile = null;
    protected List<InfoMessage> infoMessages = new ArrayList<InfoMessage>();

    private static final String L10N_BUNDLE = "l10n.l10nDimgraToXml1Console";
    private ResourceBundle l10nConsole;
}
