/*******************************************************************************
 * Copyright (c) 2010, 2016 Ericsson
 *
 * All rights reserved. This program and the accompanying materials are
 * made available under the terms of the Eclipse Public License 2.0 which
 * accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *   Patrick Tasse - Initial API and implementation
 *   Matthew Khouzam - Add support for default parsers
 *******************************************************************************/

package org.eclipse.tracecompass.tmf.core.parsers.custom;

import static org.eclipse.tracecompass.common.core.NonNullUtils.checkNotNull;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeSet;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactoryConfigurationError;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import org.eclipse.core.runtime.Platform;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.tracecompass.common.core.xml.XmlUtils;
import org.eclipse.tracecompass.internal.tmf.core.Activator;
import org.eclipse.tracecompass.tmf.core.project.model.TmfTraceType;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

/**
 * Trace definition for custom text traces.
 *
 * @author Patrick Tassé
 */
public class CustomTxtTraceDefinition extends CustomTraceDefinition {

    /** Input lines */
    public List<InputLine> inputs;

    /**
     * Custom text label used internally and therefore should not be
     * externalized
     */
    public static final String CUSTOM_TXT_CATEGORY = "Custom Text"; //$NON-NLS-1$


    /** File name of the default definition file */
    protected static final String CUSTOM_TXT_TRACE_DEFINITIONS_DEFAULT_FILE_NAME = "custom_txt_default_parsers.xml"; //$NON-NLS-1$
    /** File name of the definition file */
    protected static final String CUSTOM_TXT_TRACE_DEFINITIONS_FILE_NAME = "custom_txt_parsers.xml"; //$NON-NLS-1$

    /** Path of the definition file */
    protected static final String CUSTOM_TXT_TRACE_DEFINITIONS_DEFAULT_PATH_NAME =
            Platform.getInstallLocation().getURL().getPath() + "templates/org.eclipse.linuxtools.tmf.core/" + //$NON-NLS-1$
                    CUSTOM_TXT_TRACE_DEFINITIONS_DEFAULT_FILE_NAME;
    /** Path of the definition file */
    protected static final String CUSTOM_TXT_TRACE_DEFINITIONS_PATH_NAME =
            Activator.getDefault().getStateLocation().addTrailingSeparator().append(CUSTOM_TXT_TRACE_DEFINITIONS_FILE_NAME).toString();

    /**
     * Legacy path to the XML definitions file (in the UI plug-in of linuxtools) TODO Remove
     * once we feel the transition phase is over.
     */
    private static final String CUSTOM_TXT_TRACE_DEFINITIONS_PATH_NAME_LEGACY_UI =
            Activator.getDefault().getStateLocation().removeLastSegments(1).addTrailingSeparator()
                    .append("org.eclipse.linuxtools.tmf.ui") //$NON-NLS-1$
                    .append(CUSTOM_TXT_TRACE_DEFINITIONS_FILE_NAME).toString();

    /**
     * Legacy path to the XML definitions file (in the core plug-in of linuxtools) TODO Remove
     * once we feel the transition phase is over.
     */
    private static final String CUSTOM_TXT_TRACE_DEFINITIONS_PATH_NAME_LEGACY_CORE =
            Activator.getDefault().getStateLocation().removeLastSegments(1).addTrailingSeparator()
                    .append("org.eclipse.linuxtools.tmf.core") //$NON-NLS-1$
                    .append(CUSTOM_TXT_TRACE_DEFINITIONS_FILE_NAME).toString();

    private static final String CUSTOM_TXT_TRACE_DEFINITION_ROOT_ELEMENT = Messages.CustomTxtTraceDefinition_definitionRootElement;
    private static final String DEFINITION_ELEMENT = Messages.CustomTxtTraceDefinition_definition;
    private static final String CATEGORY_ATTRIBUTE = Messages.CustomTxtTraceDefinition_category;
    private static final String TAG_ATTRIBUTE = Messages.CustomTxtTraceDefinition_tag;
    private static final String NAME_ATTRIBUTE = Messages.CustomTxtTraceDefinition_name;
    private static final String TIME_STAMP_OUTPUT_FORMAT_ELEMENT = Messages.CustomTxtTraceDefinition_timestampOutputFormat;
    private static final String INPUT_LINE_ELEMENT = Messages.CustomTxtTraceDefinition_inputLine;
    private static final String CARDINALITY_ELEMENT = Messages.CustomTxtTraceDefinition_cardinality;
    private static final String MIN_ATTRIBUTE = Messages.CustomTxtTraceDefinition_min;
    private static final String MAX_ATTRIBUTE = Messages.CustomTxtTraceDefinition_max;
    private static final String REGEX_ELEMENT = Messages.CustomTxtTraceDefinition_regEx;
    private static final String EVENT_TYPE_ELEMENT = Messages.CustomTxtTraceDefinition_eventType;
    private static final String INPUT_DATA_ELEMENT = Messages.CustomTxtTraceDefinition_inputData;
    private static final String ACTION_ATTRIBUTE = Messages.CustomTxtTraceDefinition_action;
    private static final String FORMAT_ATTRIBUTE = Messages.CustomTxtTraceDefinition_format;
    private static final String OUTPUT_COLUMN_ELEMENT = Messages.CustomTxtTraceDefinition_outputColumn;

    /**
     * This is the value that the extension sets for traceContentType to be able
     * to load an XML parser
     **/
    private static final String TRACE_CONTENT_TYPE_ATTRIBUTE_VALUE = "text"; //$NON-NLS-1$

    /**
     * Default constructor.
     */
    public CustomTxtTraceDefinition() {
        this(CUSTOM_TXT_CATEGORY, "", new ArrayList<InputLine>(0), new ArrayList<OutputColumn>(0), ""); //$NON-NLS-1$ //$NON-NLS-2$
    }

    /**
     * Full constructor.
     *
     * @param category
     *            Category of the trace type
     * @param traceType
     *            Name of the trace type
     * @param inputs
     *            List of inputs
     * @param outputs
     *            List of output columns
     * @param timeStampOutputFormat
     *            The timestamp format to use
     */
    public CustomTxtTraceDefinition(String category, String traceType, List<InputLine> inputs,
            List<OutputColumn> outputs, String timeStampOutputFormat) {
        this.categoryName = category;
        this.definitionName = traceType;
        this.inputs = inputs;
        this.outputs = outputs;
        this.timeStampOutputFormat = timeStampOutputFormat;
    }

    /**
     * Wrapper to store a line of the log file
     */
    public static class InputLine {

        /** Data columns of this line */
        public List<InputData> columns;

        /** Cardinality of this line (see {@link Cardinality}) */
        public Cardinality cardinality;

        /** Parent line */
        public InputLine parentInput;

        /** Level of this line */
        public int level;

        /** Next input line in the file */
        public InputLine nextInput;

        /** Children of this line (if one "event" spans many lines) */
        public List<InputLine> childrenInputs;

        /** Event type associated with this line
         * @since 2.1*/
        public String eventType;

        private String regex;
        private Pattern pattern;

        /**
         * Default (empty) constructor.
         */
        public InputLine() {
        }

        /**
         * Constructor.
         *
         * @param cardinality
         *            Cardinality of this line.
         * @param regex
         *            Regex
         * @param columns
         *            Columns to use
         */
        public InputLine(Cardinality cardinality, String regex, List<InputData> columns) {
            this.cardinality = cardinality;
            this.regex = regex;
            this.columns = columns;
        }

        /**
         * Set the regex of this input line
         *
         * @param regex
         *            Regex to set
         */
        public void setRegex(String regex) {
            this.regex = regex;
            this.pattern = null;
        }

        /**
         * Get the current regex
         *
         * @return The current regex
         */
        public String getRegex() {
            return regex;
        }

        /**
         * Get the Pattern object of this line's regex
         *
         * @return The Pattern
         * @throws PatternSyntaxException
         *             If the regex does not parse correctly
         */
        public Pattern getPattern() throws PatternSyntaxException {
            if (pattern == null) {
                pattern = Pattern.compile(regex);
            }
            return pattern;
        }

        /**
         * Add a child line to this line.
         *
         * @param input
         *            The child input line
         */
        public void addChild(InputLine input) {
            if (childrenInputs == null) {
                childrenInputs = new ArrayList<>(1);
            } else if (!childrenInputs.isEmpty()) {
                InputLine last = childrenInputs.get(childrenInputs.size() - 1);
                last.nextInput = input;
            }
            childrenInputs.add(input);
            input.parentInput = this;
            input.level = this.level + 1;
        }

        /**
         * Set the next input line.
         *
         * @param input
         *            The next input line
         */
        public void addNext(InputLine input) {
            if (parentInput != null) {
                int index = parentInput.childrenInputs.indexOf(this);
                parentInput.childrenInputs.add(index + 1, input);
                InputLine next = nextInput;
                nextInput = input;
                input.nextInput = next;
            }
            input.parentInput = this.parentInput;
            input.level = this.level;
        }

        /**
         * Move this line up in its parent's children.
         */
        public void moveUp() {
            if (parentInput != null) {
                int index = parentInput.childrenInputs.indexOf(this);
                if (index > 0) {
                    parentInput.childrenInputs.add(index - 1, parentInput.childrenInputs.remove(index));
                    parentInput.childrenInputs.get(index).nextInput = nextInput;
                    nextInput = parentInput.childrenInputs.get(index);
                }
            }
        }

        /**
         * Move this line down in its parent's children.
         */
        public void moveDown() {
            if (parentInput != null) {
                int index = parentInput.childrenInputs.indexOf(this);
                if (index < parentInput.childrenInputs.size() - 1) {
                    parentInput.childrenInputs.add(index + 1, parentInput.childrenInputs.remove(index));
                    nextInput = parentInput.childrenInputs.get(index).nextInput;
                    parentInput.childrenInputs.get(index).nextInput = this;
                }
            }
        }

        /**
         * Add a data column to this line
         *
         * @param column
         *            The column to add
         */
        public void addColumn(InputData column) {
            if (columns == null) {
                columns = new ArrayList<>(1);
            }
            columns.add(column);
        }

        /**
         * Get the next input lines.
         *
         * @param countMap
         *            The map of line "sets".
         * @return The next list of lines.
         */
        public List<InputLine> getNextInputs(Map<InputLine, Integer> countMap) {
            List<InputLine> nextInputs = new ArrayList<>();
            InputLine next = nextInput;
            while (next != null) {
                nextInputs.add(next);
                if (next.cardinality.min > 0) {
                    return nextInputs;
                }
                next = next.nextInput;
            }
            if (parentInput != null && parentInput.level > 0) {
                int parentCount = checkNotNull(countMap.get(parentInput));
                if (parentCount < parentInput.getMaxCount()) {
                    nextInputs.add(parentInput);
                }
                if (parentCount < parentInput.getMinCount()) {
                    return nextInputs;
                }
                nextInputs.addAll(parentInput.getNextInputs(countMap));
            }
            return nextInputs;
        }

        /**
         * Get the minimum possible amount of entries.
         *
         * @return The minimum
         */
        public int getMinCount() {
            return cardinality.min;
        }

        /**
         * Get the maximum possible amount of entries.
         *
         * @return The maximum
         */
        public int getMaxCount() {
            return cardinality.max;
        }

        @Override
        public String toString() {
            return regex + " " + cardinality; //$NON-NLS-1$
        }
    }

    /**
     * Data column for input lines.
     */
    public static class InputData {

        /** Tag of this input
         * @since 2.1*/
        public Tag tag;

        /** Name of this input for "Other" tag */
        public String name;

        /** Action id */
        public int action;

        /** Format */
        public String format;

        /**
         * Default (empty) constructor
         */
        public InputData() {
        }

        /**
         * Full constructor
         *
         * @param name
         *            Name
         * @param action
         *            Action
         * @param format
         *            Format
         */
        public InputData(String name, int action, String format) {
            this.name = name;
            this.action = action;
            this.format = format;
        }

        /**
         * Constructor with default format
         *
         * @param name
         *            Name
         * @param action
         *            Action
         */
        public InputData(String name, int action) {
            this.name = name;
            this.action = action;
        }

        /**
         * Constructor
         *
         * @param tag
         *            Tag
         * @param action
         *            Action
         * @since 2.1
         */
        public InputData(Tag tag, int action) {
            this.tag = tag;
            this.action = action;
        }
    }

    /**
     * Input line cardinality
     */
    public static class Cardinality {

        /** Representation of infinity */
        public static final int INF = Integer.MAX_VALUE;

        /** Preset for [1, 1] */
        public static final Cardinality ONE = new Cardinality(1, 1);

        /** Preset for [1, inf] */
        public static final Cardinality ONE_OR_MORE = new Cardinality(1, INF);

        /** Preset for [0, 1] */
        public static final Cardinality ZERO_OR_ONE = new Cardinality(0, 1);

        /** Preset for [0, inf] */
        public static final Cardinality ZERO_OR_MORE = new Cardinality(0, INF);

        private final int min;
        private final int max;

        /**
         * Constructor.
         *
         * @param min
         *            Minimum
         * @param max
         *            Maximum
         */
        public Cardinality(int min, int max) {
            this.min = min;
            this.max = max;
        }

        @Override
        public String toString() {
            return "(" + (min >= 0 ? min : "?") + ',' + (max == INF ? "\u221E" : (max >= 0 ? max : "?")) + ')'; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$
        }

        @Override
        public int hashCode() {
            final int prime = 31;
            int result = 1;
            result = prime * result + max;
            result = prime * result + min;
            return result;
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj) {
                return true;
            }
            if (obj == null) {
                return false;
            }
            if (!(obj instanceof Cardinality)) {
                return false;
            }
            Cardinality other = (Cardinality) obj;
            return (this.min == other.min && this.max == other.max);
        }
    }

    @Override
    public void save() {
        save(CUSTOM_TXT_TRACE_DEFINITIONS_PATH_NAME);
    }

    @Override
    public void save(String path) {
        try {
            DocumentBuilder db = XmlUtils.newSafeDocumentBuilderFactory().newDocumentBuilder();

            // The following allows xml parsing without access to the dtd
            db.setEntityResolver(createEmptyEntityResolver());

            // The following catches xml parsing exceptions
            db.setErrorHandler(createErrorHandler());

            Document doc = null;
            File file = new File(path);
            if (file.canRead()) {
                doc = db.parse(file);
                if (!doc.getDocumentElement().getNodeName().equals(CUSTOM_TXT_TRACE_DEFINITION_ROOT_ELEMENT)) {
                    Activator.logError(String.format("Error saving CustomTxtTraceDefinition: path=%s is not a valid custom parser file", path)); //$NON-NLS-1$
                    return;
                }
            } else {
                doc = db.newDocument();
                Node node = doc.createElement(CUSTOM_TXT_TRACE_DEFINITION_ROOT_ELEMENT);
                doc.appendChild(node);
            }

            Element root = doc.getDocumentElement();

            Element oldDefinitionElement = findDefinitionElement(root, categoryName, definitionName);
            if (oldDefinitionElement != null) {
                root.removeChild(oldDefinitionElement);
            }
            Element definitionElement = doc.createElement(DEFINITION_ELEMENT);
            root.appendChild(definitionElement);
            definitionElement.setAttribute(CATEGORY_ATTRIBUTE, categoryName);
            definitionElement.setAttribute(NAME_ATTRIBUTE, definitionName);

            if (timeStampOutputFormat != null && !timeStampOutputFormat.isEmpty()) {
                Element formatElement = doc.createElement(TIME_STAMP_OUTPUT_FORMAT_ELEMENT);
                definitionElement.appendChild(formatElement);
                formatElement.appendChild(doc.createTextNode(timeStampOutputFormat));
            }

            if (inputs != null) {
                for (InputLine inputLine : inputs) {
                    definitionElement.appendChild(createInputLineElement(inputLine, doc));
                }
            }

            if (outputs != null) {
                for (OutputColumn output : outputs) {
                    Element outputColumnElement = doc.createElement(OUTPUT_COLUMN_ELEMENT);
                    definitionElement.appendChild(outputColumnElement);
                    outputColumnElement.setAttribute(TAG_ATTRIBUTE, output.tag.name());
                    outputColumnElement.setAttribute(NAME_ATTRIBUTE, output.name);
                }
            }

            Transformer transformer = XmlUtils.newSecureTransformer();
            transformer.setOutputProperty(OutputKeys.INDENT, "yes"); //$NON-NLS-1$

            // initialize StreamResult with File object to save to file
            StreamResult result = new StreamResult(new StringWriter());
            DOMSource source = new DOMSource(doc);
            transformer.transform(source, result);
            String xmlString = result.getWriter().toString();

            try (FileWriter writer = new FileWriter(file);) {
                writer.write(xmlString);
            }

            TmfTraceType.addCustomTraceType(CustomTxtTrace.class, categoryName, definitionName);

        } catch (ParserConfigurationException | TransformerFactoryConfigurationError | TransformerException | IOException | SAXException e) {
            Activator.logError("Error saving CustomTxtTraceDefinition: path=" + path, e); //$NON-NLS-1$
        }
    }

    private Element createInputLineElement(InputLine inputLine, Document doc) {
        Element inputLineElement = doc.createElement(INPUT_LINE_ELEMENT);

        Element cardinalityElement = doc.createElement(CARDINALITY_ELEMENT);
        inputLineElement.appendChild(cardinalityElement);
        cardinalityElement.setAttribute(MIN_ATTRIBUTE, Integer.toString(inputLine.cardinality.min));
        cardinalityElement.setAttribute(MAX_ATTRIBUTE, Integer.toString(inputLine.cardinality.max));

        Element regexElement = doc.createElement(REGEX_ELEMENT);
        inputLineElement.appendChild(regexElement);
        regexElement.appendChild(doc.createTextNode(inputLine.regex));

        if (inputLine.eventType != null) {
            Element eventTypeElement = doc.createElement(EVENT_TYPE_ELEMENT);
            inputLineElement.appendChild(eventTypeElement);
            eventTypeElement.appendChild(doc.createTextNode(inputLine.eventType));
        }

        if (inputLine.columns != null) {
            for (InputData inputData : inputLine.columns) {
                Element inputDataElement = doc.createElement(INPUT_DATA_ELEMENT);
                inputLineElement.appendChild(inputDataElement);
                inputDataElement.setAttribute(TAG_ATTRIBUTE, inputData.tag.name());
                inputDataElement.setAttribute(NAME_ATTRIBUTE, inputData.name);
                inputDataElement.setAttribute(ACTION_ATTRIBUTE, Integer.toString(inputData.action));
                if (inputData.format != null) {
                    inputDataElement.setAttribute(FORMAT_ATTRIBUTE, inputData.format);
                }
            }
        }

        if (inputLine.childrenInputs != null) {
            for (InputLine childInputLine : inputLine.childrenInputs) {
                inputLineElement.appendChild(createInputLineElement(childInputLine, doc));
            }
        }

        return inputLineElement;
    }

    /**
     * Load all custom text trace definitions, including the user-defined and
     * default (built-in) parsers.
     *
     * @return The loaded trace definitions
     */
    public static CustomTxtTraceDefinition[] loadAll() {
        return loadAll(true);
    }

    /**
     * Load all custom text trace definitions, including the user-defined and,
     * optionally, the default (built-in) parsers.
     *
     * @param includeDefaults
     *            if true, the default (built-in) parsers are included
     *
     * @return The loaded trace definitions
     */
    public static CustomTxtTraceDefinition[] loadAll(boolean includeDefaults) {
        File defaultFile = new File(CUSTOM_TXT_TRACE_DEFINITIONS_PATH_NAME);
        File legacyFileCore = new File(CUSTOM_TXT_TRACE_DEFINITIONS_PATH_NAME_LEGACY_CORE);
        File legacyFileUI = new File(CUSTOM_TXT_TRACE_DEFINITIONS_PATH_NAME_LEGACY_UI);

        /*
         * If there is no file at the expected location, check the legacy
         * locations instead.
         */
        if (!defaultFile.exists()) {
            if (legacyFileCore.exists()) {
                transferDefinitions(CUSTOM_TXT_TRACE_DEFINITIONS_PATH_NAME_LEGACY_CORE);
            } else if (legacyFileUI.exists()) {
                transferDefinitions(CUSTOM_TXT_TRACE_DEFINITIONS_PATH_NAME_LEGACY_UI);
            }
        }

        Set<CustomTxtTraceDefinition> defs = new TreeSet<>(Comparator.comparing(CustomTxtTraceDefinition::getCategoryName).thenComparing(CustomTxtTraceDefinition::getDefinitionName));
        defs.addAll(Arrays.asList(loadAll(CUSTOM_TXT_TRACE_DEFINITIONS_PATH_NAME)));
        if (includeDefaults) {
            defs.addAll(Arrays.asList(loadAll(CUSTOM_TXT_TRACE_DEFINITIONS_DEFAULT_PATH_NAME)));

            // Also load definitions contributed by extensions
            Collection<String> paths = getExtensionDefinitionsPaths(TRACE_CONTENT_TYPE_ATTRIBUTE_VALUE);
            for (String customTraceDefinitionPath : paths) {
                defs.addAll(Arrays.asList(loadAll(customTraceDefinitionPath)));
            }
        }
        return defs.toArray(new CustomTxtTraceDefinition[0]);

    }

    private static void transferDefinitions(String defFile) {
        CustomTxtTraceDefinition[] oldDefs = loadAll(defFile);
        for (CustomTxtTraceDefinition def : oldDefs) {
            /* Save in the new location */
            def.save();
        }
    }

    /**
     * Load a specific text trace definition file.
     *
     * @param path
     *            The path to the file to load
     * @return The loaded trace definitions
     */
    public static CustomTxtTraceDefinition[] loadAll(String path) {
        try {
            DocumentBuilder db = XmlUtils.newSafeDocumentBuilderFactory().newDocumentBuilder();

            // The following allows xml parsing without access to the dtd
            db.setEntityResolver(createEmptyEntityResolver());

            // The following catches xml parsing exceptions
            db.setErrorHandler(createErrorHandler());

            File file = new File(path);
            if (!file.canRead()) {
                return new CustomTxtTraceDefinition[0];
            }
            Document doc = db.parse(file);

            Element root = doc.getDocumentElement();
            if (!root.getNodeName().equals(CUSTOM_TXT_TRACE_DEFINITION_ROOT_ELEMENT)) {
                return new CustomTxtTraceDefinition[0];
            }

            ArrayList<CustomTxtTraceDefinition> defList = new ArrayList<>();
            NodeList nodeList = root.getChildNodes();
            for (int i = 0; i < nodeList.getLength(); i++) {
                Node node = nodeList.item(i);
                if (node instanceof Element && node.getNodeName().equals(DEFINITION_ELEMENT)) {
                    CustomTxtTraceDefinition def = extractDefinition((Element) node);
                    if (def != null) {
                        defList.add(def);
                    }
                }
            }
            return defList.toArray(new CustomTxtTraceDefinition[0]);
        } catch (ParserConfigurationException e) {
            Activator.logError("Error loading all in CustomTxtTraceDefinition: path=" + path, e); //$NON-NLS-1$
        } catch (SAXException e) {
            Activator.logError("Error loading all in CustomTxtTraceDefinition: path=" + path, e); //$NON-NLS-1$
        } catch (IOException e) {
            Activator.logError("Error loading all in CustomTxtTraceDefinition: path=" + path, e); //$NON-NLS-1$
        }
        return new CustomTxtTraceDefinition[0];
    }

    /**
     * Load a single definition.
     *
     * @param categoryName
     *            Category of the definition to load
     * @param definitionName
     *            Name of the definition to load
     * @return The loaded trace definition
     */
    public static CustomTxtTraceDefinition load(String categoryName, String definitionName) {
        try {
            DocumentBuilder db = XmlUtils.newSafeDocumentBuilderFactory().newDocumentBuilder();

            // The following allows xml parsing without access to the dtd
            db.setEntityResolver(createEmptyEntityResolver());

            // The following catches xml parsing exceptions
            db.setErrorHandler(createErrorHandler());

            CustomTxtTraceDefinition value = lookupDefinition(categoryName, definitionName, db, CUSTOM_TXT_TRACE_DEFINITIONS_PATH_NAME);
            if (value == null) {
                return lookupDefinition(categoryName, definitionName, db, CUSTOM_TXT_TRACE_DEFINITIONS_DEFAULT_PATH_NAME);
            }
            return value;
        } catch (ParserConfigurationException | SAXException | IOException e) {
            Activator.logError("Error loading CustomTxtTraceDefinition: definitionName=" + definitionName, e); //$NON-NLS-1$
        }
        return null;
    }

    private static CustomTxtTraceDefinition lookupDefinition(String categoryName, String definitionName, DocumentBuilder db, String source) throws SAXException, IOException {
        File file = new File(source);
        if (!file.exists()) {
            return null;
        }
        Document doc = db.parse(file);

        Element root = doc.getDocumentElement();
        if (!root.getNodeName().equals(CUSTOM_TXT_TRACE_DEFINITION_ROOT_ELEMENT)) {
            return null;
        }

        Element definitionElement = findDefinitionElement(root, categoryName, definitionName);
        if (definitionElement != null) {
            return extractDefinition(definitionElement);
        }
        return null;
    }

    private static Element findDefinitionElement(Element root, String categoryName, String definitionName) {
        NodeList nodeList = root.getChildNodes();
        for (int i = 0; i < nodeList.getLength(); i++) {
            Node node = nodeList.item(i);
            if (node instanceof Element && node.getNodeName().equals(DEFINITION_ELEMENT)) {
                Element element = (Element) node;
                String categoryAttribute = element.getAttribute(CATEGORY_ATTRIBUTE);
                if (categoryAttribute.isEmpty()) {
                    categoryAttribute = CUSTOM_TXT_CATEGORY;
                }
                String nameAttribute = element.getAttribute(NAME_ATTRIBUTE);
                if (categoryName.equals(categoryAttribute) &&
                        definitionName.equals(nameAttribute)) {
                    return element;
                }
            }
        }
        return null;
    }

    /**
     * Get the definition from a definition element.
     *
     * @param definitionElement
     *            The Element to extract from
     * @return The loaded trace definition
     */
    public static CustomTxtTraceDefinition extractDefinition(Element definitionElement) {
        CustomTxtTraceDefinition def = new CustomTxtTraceDefinition();

        def.categoryName = definitionElement.getAttribute(CATEGORY_ATTRIBUTE);
        if (def.categoryName.isEmpty()) {
            def.categoryName = CUSTOM_TXT_CATEGORY;
        }
        def.definitionName = definitionElement.getAttribute(NAME_ATTRIBUTE);
        if (def.definitionName.isEmpty()) {
            return null;
        }

        NodeList nodeList = definitionElement.getChildNodes();
        for (int i = 0; i < nodeList.getLength(); i++) {
            Node node = nodeList.item(i);
            String nodeName = node.getNodeName();
            if (nodeName.equals(TIME_STAMP_OUTPUT_FORMAT_ELEMENT)) {
                Element formatElement = (Element) node;
                def.timeStampOutputFormat = formatElement.getTextContent();
            } else if (nodeName.equals(INPUT_LINE_ELEMENT)) {
                InputLine inputLine = extractInputLine((Element) node);
                if (inputLine != null) {
                    def.inputs.add(inputLine);
                }
            } else if (nodeName.equals(OUTPUT_COLUMN_ELEMENT)) {
                Element outputColumnElement = (Element) node;
                Entry<@NonNull Tag, @NonNull String> entry = extractTagAndName(outputColumnElement, TAG_ATTRIBUTE, NAME_ATTRIBUTE);
                OutputColumn outputColumn = new OutputColumn(entry.getKey(), entry.getValue());
                def.outputs.add(outputColumn);
            }
        }
        return def;
    }

    private static InputLine extractInputLine(Element inputLineElement) {
        InputLine inputLine = new InputLine();
        NodeList nodeList = inputLineElement.getChildNodes();
        for (int i = 0; i < nodeList.getLength(); i++) {
            Node node = nodeList.item(i);
            String nodeName = node.getNodeName();
            if (nodeName.equals(CARDINALITY_ELEMENT)) {
                Element cardinalityElement = (Element) node;
                try {
                    int min = Integer.parseInt(cardinalityElement.getAttribute(MIN_ATTRIBUTE));
                    int max = Integer.parseInt(cardinalityElement.getAttribute(MAX_ATTRIBUTE));
                    inputLine.cardinality = new Cardinality(min, max);
                } catch (NumberFormatException e) {
                    return null;
                }
            } else if (nodeName.equals(REGEX_ELEMENT)) {
                Element regexElement = (Element) node;
                inputLine.regex = regexElement.getTextContent();
            } else if (nodeName.equals(EVENT_TYPE_ELEMENT)) {
                Element eventTypeElement = (Element) node;
                inputLine.eventType = eventTypeElement.getTextContent();
            } else if (nodeName.equals(INPUT_DATA_ELEMENT)) {
                Element inputDataElement = (Element) node;
                InputData inputData = new InputData();
                Entry<@NonNull Tag, @NonNull String> entry = extractTagAndName(inputDataElement, TAG_ATTRIBUTE, NAME_ATTRIBUTE);
                inputData.tag = checkNotNull(entry.getKey());
                inputData.name = checkNotNull(entry.getValue());
                inputData.action = Integer.parseInt(inputDataElement.getAttribute(ACTION_ATTRIBUTE));
                inputData.format = inputDataElement.getAttribute(FORMAT_ATTRIBUTE);
                inputLine.addColumn(inputData);
            } else if (nodeName.equals(INPUT_LINE_ELEMENT)) {
                Element childInputLineElement = (Element) node;
                InputLine childInputLine = extractInputLine(childInputLineElement);
                if (childInputLine != null) {
                    inputLine.addChild(childInputLine);
                }
            }
        }
        return inputLine;
    }

    /**
     * Delete a definition from the currently loaded ones.
     *
     * @param categoryName
     *            The category of the definition to delete
     * @param definitionName
     *            The name of the definition to delete
     */
    public static void delete(String categoryName, String definitionName) {
        try {
            DocumentBuilder db = XmlUtils.newSafeDocumentBuilderFactory().newDocumentBuilder();

            // The following allows xml parsing without access to the dtd
            db.setEntityResolver(createEmptyEntityResolver());

            // The following catches xml parsing exceptions
            db.setErrorHandler(createErrorHandler());

            File file = new File(CUSTOM_TXT_TRACE_DEFINITIONS_PATH_NAME);
            Document doc = db.parse(file);

            Element root = doc.getDocumentElement();
            if (!root.getNodeName().equals(CUSTOM_TXT_TRACE_DEFINITION_ROOT_ELEMENT)) {
                return;
            }

            Element definitionElement = findDefinitionElement(root, categoryName, definitionName);
            if (definitionElement != null) {
                root.removeChild(definitionElement);
            }

            Transformer transformer = XmlUtils.newSecureTransformer();
            transformer.setOutputProperty(OutputKeys.INDENT, "yes"); //$NON-NLS-1$

            // initialize StreamResult with File object to save to file
            StreamResult result = new StreamResult(new StringWriter());
            DOMSource source = new DOMSource(doc);
            transformer.transform(source, result);
            String xmlString = result.getWriter().toString();

            try (FileWriter writer = new FileWriter(file);) {
                writer.write(xmlString);
            }

            TmfTraceType.removeCustomTraceType(CustomTxtTrace.class, categoryName, definitionName);
            // Check if default definition needs to be reloaded
            TmfTraceType.addCustomTraceType(CustomTxtTrace.class, categoryName, definitionName);

        } catch (ParserConfigurationException | SAXException | IOException | TransformerFactoryConfigurationError | TransformerException e) {
            Activator.logError("Error deleting CustomTxtTraceDefinition: definitionName=" + definitionName, e); //$NON-NLS-1$
        }
    }

}
