/*
 * Copyright (c) 2017-2023 XMLmind Software. All rights reserved.
 *
 * Author: Hussein Shafie
 *
 * This file is part of the XMLmind Assembly Processor project.
 * For conditions of distribution and use, see the accompanying legal.txt file.
 */
package com.xmlmind.xinclude;

import java.io.IOException;
import java.io.File;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.charset.Charset;
import java.util.ArrayList;
import org.w3c.dom.Node;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Attr;
import org.w3c.dom.Element;
import org.w3c.dom.Document;
import static javax.xml.XMLConstants.XML_NS_URI;
import com.xmlmind.util.ThrowableUtil;
import com.xmlmind.util.FileUtil;
import com.xmlmind.util.URIComponent;
import com.xmlmind.util.URLUtil;
import com.xmlmind.util.LocaleUtil;
import com.xmlmind.util.LoadText;
import com.xmlmind.util.XMLText;
import com.xmlmind.util.XMLResolver;
import com.xmlmind.util.Console;
import com.xmlmind.util.SimpleConsole;
import com.xmlmind.domutil.ConsoleHelper;
import com.xmlmind.domutil.DOMUtil;
import com.xmlmind.domutil.XMLResolverImpl;
import com.xmlmind.domutil.LoaderImpl;
import com.xmlmind.domutil.SaveDocument;

/*package*/ final class XIncluder extends Includer {
    public static final String XINCLUDE_NS_URI = 
        "http://www.w3.org/2001/XInclude";

    public static final String  LOCAL_ATTRIBUTES_NS_URI =
        "http://www.w3.org/2001/XInclude/local-attributes";

    private String idAttrNamespace, idAttrLocalName;
    private String[] langAttrNames;
    
    // -----------------------------------------------------------------------

    public XIncluder() {
        this(null);
    }

    public XIncluder(Console c) {
        super(c);

        idAttrNamespace = XML_NS_URI;
        idAttrLocalName = "id";
        langAttrNames = new String[] { XML_NS_URI, "lang" };
    }

    /**
     * Specifies the name of the ID attribute in the form of a 
     * namespace URI and local name pair.
     * <p>By default, it's "<tt>xml:id</tt>", but it could be "<tt>id</tt>".
     *
     * @see #getIdAttribute
     */
    public void setIdAttribute(String namespace, String localName) {
        idAttrNamespace = namespace;
        idAttrLocalName = localName;
    }

    /**
     * Returns a string pair containing the namespace URI and local name
     * of the ID attribute.
     *
     * @see #setIdAttribute
     */
    public String[] getIdAttribute() {
        return new String[] { idAttrNamespace, idAttrLocalName };
    }

    /**
     * Specifies the names of the language attributes in the form of 
     * namespace URI and local name pairs.
     * <p>By default, it's "<tt>xml:lang</tt>", but it could be 
     * "<tt>lang</tt>" or both "<tt>xml:lang</tt>" and "<tt>lang</tt>".
     *
     * @see #getLangAttributes
     */
    public void setLangAttributes(String... names) {
        if (names.length < 2 || (names.length % 2) != 0) {
            throw new IllegalArgumentException();
        }
        langAttrNames = names.clone();
    }

    /**
     * Returns string pairss containing the namespace URI and local name
     * of the lang attribute.
     *
     * @see #setLangAttributes
     */
    public String[] getLangAttributes() {
        return langAttrNames;
    }

    // -----------------------------------------
    // detectInclusion
    // -----------------------------------------

    protected Incl detectInclusion(Element element) 
        throws InclusionException {
        if (!DOMUtil.hasName(element, XINCLUDE_NS_URI, "include")) {
            return null;
        }

        String href = null;
        URL url = null;
        String xpointer = null;
        String fragid = null;
        boolean parseText = false;
        String textEncoding = null;
        Attr[] attributes = null;
        Element fallback = null;

        // href ---

        Attr hrefAttr = element.getAttributeNodeNS(null, "href");
        if (hrefAttr != null) {
            href = hrefAttr.getValue().trim();

            if (URIComponent.getFragment(href) != null) {
                throw new InclusionException(
                    Msg.msg("invalidAttr", href, "href"));
            }

            if (href.length() > 0) {
                try {
                    String resolved = null;
                    if (resolver != null) {
                        resolved = resolver.resolveURI(href);
                    }

                    if (resolved != null) {
                        url = URLUtil.createURL(resolved);
                    } else {
                        url = URLUtil.createURL(DOMUtil.getBaseURL(element), 
                                                href);
                    }
                } catch (MalformedURLException ignored) {
                    throw new InclusionException(
                        Msg.msg("invalidAttr", href, "href"));
                }
            }
        }

        if (url == null) {
            // Equivalent to href=""
            url = DOMUtil.getBaseURL(element);
        }

        // xpointer ---

        xpointer = DOMUtil.getNonEmptyAttribute(element, null, "xpointer");

        // fragid ---

        fragid = DOMUtil.getNonEmptyAttribute(element, null, "fragid");

        // parse ---

        String value = DOMUtil.getNonEmptyAttribute(element, null, "parse");
        if (value != null) {
            value = value.toLowerCase();
            if ("text".equals(value) ||
                "text/plain".equals(value) ||
                value.startsWith("text/")) {
                parseText = true;
            } else if ("xml".equals(value) ||
                       "application/xml".equals(value) ||
                       value.endsWith("+xml")) {
                parseText = false;
            } else {
                // Should be a resource error, not a fatal error.
                throw new InclusionException(
                    Msg.msg("unsupportedAttr", value, "parse"));
            }
        }

        // encoding ---

        textEncoding = DOMUtil.getNonEmptyAttribute(element, null, "encoding");
        if (textEncoding != null) {
            try {
                Charset.forName(textEncoding);
            } catch (Exception ignored) {
                throw new InclusionException(
                    Msg.msg("invalidAttr", value, "encoding"));
            }
        }

        // attributes ---

        ArrayList<Attr> attrList = null;

        NamedNodeMap map = element.getAttributes();
        if (map != null) {
            final int count = map.getLength();
            for (int i = 0; i < count; ++i) {
                Attr attr = (Attr) map.item(i);

                String attrNS = attr.getNamespaceURI();
                String attrName = attr.getLocalName();

                if (("set-xml-id".equals(attrName) && attrNS == null) ||
                    (attrNS != null && !XML_NS_URI.equals(attrNS))) {
                    if (attrList == null) {
                        attrList = new ArrayList<Attr>();
                    }
                    attrList.add(attr);
                }

                // Otherwise ignore XInclude attributes such as accept,
                // accept-language and also ignore erroneous attributes 
                // --having no namespace or in the XML namespace-- 
                // set by the user.
            }
        }

        if (attrList != null) {
            attributes = new Attr[attrList.size()];
            attrList.toArray(attributes);
        }

        // fallback ---
        // More that one fallback and other xi:* elements not reported
        // as an error.

        fallback = 
            DOMUtil.getChildElementByName(element, XINCLUDE_NS_URI, "fallback");

        XIncl xincl = new XIncl(element, href, url, xpointer, fragid,
                                parseText, textEncoding, attributes, fallback);
        if (xincl.pointer != null) {
            xincl.pointer.setIdAttribute(idAttrNamespace, idAttrLocalName);
        }

        return xincl;
    }

    private static final ElementXPointer DOCUMENT_CONTENTS_POINTER = 
        new ElementXPointer(/*id*/ null, new int[] {1});

    private static final class XIncl extends Incl {
        /**
         * The location of the document containing the nodes to be included 
         * (AKA the target document). May be <code>null</code>.
         */
        public final String href;

        /**
         * The URL of the document containing the nodes to be included 
         * (AKA the target document). Never <code>null</code>.
         */
        public final URL url;

        /**
         * The string representation of the XPointer. May be <code>null</code>.
         */
        public final String xpointer; 

        /**
         * The string representation of the fragment identifier.
         * May be <code>null</code>.
         */
        public final String fragid; 

        /**
         * <code>true</code> if the target document is to be included as text;
         * <code>false</code> otherwise.
         */
        public final boolean parseText; 

        /**
         * The encoding of the target document. 
         * Ignored unless {@link #parseText} is <code>true</code>. 
         * May be <code>null</code>.
         */
        public final String textEncoding;

        /**
         * Other attributes of the <tt>xi:include</tt> element.
         * May be <code>null</code>. If not <code>null</code>,
         * attributes are still attached to the XInclude element.
         * <p>These attributes are used to implement the Attribute Copying
         * feature of XInclude 1.1. Example: <tt>set-xml-id</tt>.
         */
        public final Attr[] attributes;

        /**
         * The <tt>xi:fallback</tt> element if any. May be <code>null</code>.
         * If not <code>null</code>, still attached to the XInclude element.
         */
        public final Element fallback;

        /**
         * Compiled form of <tt>xpointer</tt> or <tt>fragid</tt> 
         * (in case it's an XPointer).
         */
        public ElementXPointer pointer;

        /**
         * Compiled form of <tt>fragid</tt> 
         * (in case it's <em>not</em> an XPointer).
         */
        public TextFragmentIdentifier textFragmentIdentifier;

        public XIncl(Element directive, 
                     String href, URL url, String xpointer, String fragid,
                     boolean parseText, String textEncoding, 
                     Attr[] attributes, Element fallback) 
            throws InclusionException {
            super(directive);

            this.href = href;
            this.url = url;
            this.xpointer = xpointer;
            this.fragid = fragid;
            this.parseText = parseText;
            this.textEncoding = textEncoding;
            this.attributes = attributes;
            this.fallback = fallback;

            parsePointer();
        }

        private void parsePointer() 
            throws InclusionException {
            pointer = null;
            textFragmentIdentifier = null;

            if (xpointer == null && fragid == null && href == null) {
                throw new InclusionException(Msg.msg("missingAttr", "href"));
            }

             // Error not reported:
             // xpointer and fragid are both present but not the same.
             // But it's a recoverable error.

            if (parseText) {
                if (xpointer != null) {
                    throw new InclusionException(Msg.msg("xpointerToText"));
                }

                if (fragid != null) {
                    textFragmentIdentifier =
                        TextFragmentIdentifier.parse(fragid);
                    if (textFragmentIdentifier == null) {
                        throw new InclusionException(
                            Msg.msg("invalidAttr", fragid, "fragid"));
                    }
                }
            } else {
                if (xpointer == null && fragid == null) {
                    pointer = DOCUMENT_CONTENTS_POINTER;
                } else {
                    String spec = (xpointer != null)? xpointer : fragid;
                    pointer = ElementXPointer.parse(spec);
                    if (pointer == null) {
                        throw new InclusionException(
                            Msg.msg("cannotParseXPointer", spec));
                    }
                }
            }
        }
    }

    // -----------------------------------------
    // fetchIncluded
    // -----------------------------------------

    protected void fetchIncluded(Incl incl)
        throws IOException, InclusionException {
        XIncl xincl = (XIncl) incl;

        Node[] nodes = null;
        Document doc = xincl.directiveElement.getOwnerDocument();

        if (xincl.parseText) {
            try {
                String text = loadText(xincl);
                nodes = new Node[] { doc.createTextNode(text) };
            } catch (IOException e) {
                // Resource error.
                nodes = fallbackNodes(xincl, e);
            }
        } else {
            try {
                LoadedDocument sourceDoc = fetchDoc(xincl.url);
                nodes = copyIncludedNodes(xincl, sourceDoc.document);
                copyAttributes(xincl, nodes);
            } catch (IOException e) {
                // Resource error.
                // Should make a difference between non-well-formed XML 
                // which is a resource error and a missing resource.
                nodes = fallbackNodes(xincl, e);
            }
        }

        xincl.replacementNodes = nodes;
    }

    private String loadText(XIncl xincl)
        throws IOException {
        String text = null;

        try {
            if (xincl.textEncoding != null) {
                text = URLUtil.loadString(xincl.url, xincl.textEncoding);
            } else {
                text = LoadText.loadText(xincl.url, 
                                         /*fallbackEncoding*/ null, 
                                         /*out_encoding*/ null,
                                         LoadText.ALL_ENCODING_DETECTORS);
            }
        } catch (IOException e) {
            // Resource error.
            throw new IOException(
                Msg.msg("cannotLoadText", xincl.url, ThrowableUtil.reason(e)));
        }

        if (xincl.textFragmentIdentifier != null) {
            text = xincl.textFragmentIdentifier.getFragment(text);
            if (text == null) {
                // Resource error.
                throw new IOException(
                    Msg.msg("noSuchTextFragment", xincl.fragid, xincl.url));
            }
        }

        return text;
    }

    private Node[] fallbackNodes(XIncl xincl, Exception e) 
        throws IOException, InclusionException {
        if (xincl.fallback == null) {
            throw new InclusionException(Msg.msg("noFallback", xincl.url, 
                                                 ThrowableUtil.reason(e)));
        }

        // ---

        console.warning(xincl.directiveElement,
                        Msg.msg("usingFallback", xincl.url,
                                ThrowableUtil.reason(e)));

        if (xincl.fallback.getFirstChild() == null) {
            // Empty xi:fallback, just remove the xi:include.
            return REMOVE_DIRECTIVE_ELEMENT;
        } else {
            int count = 0;

            Node child = xincl.fallback.getFirstChild();
            while (child != null) {
                ++count;
                child = child.getNextSibling();
            }

            // ---

            Node[] copy = new Node[count];
            count = 0;

            child = xincl.fallback.getFirstChild();
            while (child != null) {
                // In the case of the child nodes of xi:fallback, there is
                // nothing special to do about xml:base and xml:lang.
                //
                // cloneNode is user data aware.
                copy[count++] = child.cloneNode(/*deep*/ true);
                child = child.getNextSibling();
            }

            return copy;
        }
    }

    private Node[] copyIncludedNodes(XIncl xincl, Document sourceDoc) 
        throws IOException {
        Node[] nodes = null;

        if (xincl.pointer == DOCUMENT_CONTENTS_POINTER) {
            nodes = copyDocumentNodes(xincl, sourceDoc);
        } else if (xincl.pointer != null) {
            Element included = xincl.pointer.findElement(sourceDoc);
            if (included != null) {
                nodes = new Node[] { copyIncludedNode(xincl, included) };
            }
        }

        if (nodes == null || nodes.length == 0) {
            // Resource error.
            throw new IOException(Msg.msg("includedNodesNotFound", 
                                          xincl.url, xincl.pointer));
        }

        return nodes;
    }

    private Node[] copyDocumentNodes(XIncl xincl, Document sourceDoc) {
        int nodeCount = 0;

        // Skip DOCTYPE and empty text nodes.
        Node node = sourceDoc.getFirstChild();
        while (node != null) {
            switch (node.getNodeType()) {
            case Node.COMMENT_NODE:
            case Node.PROCESSING_INSTRUCTION_NODE:
            case Node.ELEMENT_NODE:
                ++nodeCount;
                break;
            }

            node = node.getNextSibling();
        }
        
        // ---

        Node[] nodes = new Node[nodeCount];
        nodeCount = 0;

        node = sourceDoc.getFirstChild();
        while (node != null) {
            switch (node.getNodeType()) {
            case Node.COMMENT_NODE:
            case Node.PROCESSING_INSTRUCTION_NODE:
            case Node.ELEMENT_NODE:
                nodes[nodeCount++] = copyIncludedNode(xincl, node);
                break;
            }

            node = node.getNextSibling();
        }

        return nodes;
    }

    private Node copyIncludedNode(XIncl xincl, Node included) {
        Document targetDoc = xincl.directiveElement.getOwnerDocument();
        // importNode is user data aware.
        Node copy = targetDoc.importNode(included, /*deep*/ true);

        if (included.getNodeType() == Node.ELEMENT_NODE) {
            Element elementCopy = (Element) copy;

            // Base URI Fixup ---

            Element includeParent = 
                DOMUtil.getParentElement(xincl.directiveElement);

            URL includedBaseURL = DOMUtil.getBaseURL(included);
            if (includedBaseURL != null) {
                URL parentBaseURL = null;
                if (includeParent != null) {
                    parentBaseURL = DOMUtil.getBaseURL(includeParent);
                } else {
                    parentBaseURL = DOMUtil.getDocumentURL(
                        xincl.directiveElement.getOwnerDocument());
                }

                if (!includedBaseURL.equals(parentBaseURL)) {
                    String location;
                    if (parentBaseURL == null) {
                        location = includedBaseURL.toExternalForm();
                    } else {
                        location = URLUtil.getRawRelativePath(includedBaseURL,
                                                              parentBaseURL);
                    }

                    elementCopy.setAttributeNS(XML_NS_URI, "xml:base",
                                               location);
                } else {
                    // Not specified in the spec: get rid of redundant
                    // attribute.
                    elementCopy.removeAttributeNS(XML_NS_URI, "base");
                }
            }

            // Language Fixup ---

            String includedLang = lookupLang((Element) included);
            String parentLang = null; // not "" to force adding lang 
                                      // attributes when includeParent
                                      // is unknown.
            if (includeParent != null) {
                parentLang = lookupLang(includeParent);
            }

            final int count = langAttrNames.length;
            if (!includedLang.equals(parentLang)) {
                for (int i = 0; i < count; i += 2) {
                    elementCopy.setAttributeNS(langAttrNames[i],
                                               langAttrNames[i+1],
                                               includedLang);
                }
            } else {
                // Not specified in the spec: get rid of redundant
                // attribute.
                for (int i = 0; i < count; i += 2) {
                    elementCopy.removeAttributeNS(langAttrNames[i],
                                                  langAttrNames[i+1]);
                }
            }
        }

        return copy;
    }

    private String lookupLang(Element elem) {
        String lang = lookupAttribute(elem, langAttrNames);
        if (lang != null) {
            lang = lang.trim();
            if (lang.length() > 0) {
                lang = LocaleUtil.normalizeLang(lang);
            }
        } else {
            lang = ""; // Undetermined.
        }
        
        return lang;
    }
    
    private static String lookupAttribute(Element element, String[] attrNames) {
        final int count = attrNames.length;
        do {
            for (int i = 0; i < count; i += 2) {
                String value =
                    DOMUtil.getAttribute(element, attrNames[i], attrNames[i+1]);
                if (value != null) {
                    return value;
                }
            }

            element = DOMUtil.getParentElement(element);
        } while (element != null);
        
        return null;
    }
    
    private void copyAttributes(XIncl xincl, Node[] nodes) {
        if (xincl.attributes != null) {
            for (Node node : nodes) {
                if (node.getNodeType() == Node.ELEMENT_NODE) {
                    Element element = (Element) node;

                    for (Attr attr : xincl.attributes) {
                        String nsURI = attr.getNamespaceURI();
                        String qName = attr.getName();
                        String value = attr.getValue();

                        if (DOMUtil.hasName(attr, null, "set-xml-id")) {
                            if (value.length() == 0) {
                                element.removeAttributeNS(XML_NS_URI, "id");
                            } else {
                                element.setAttributeNS(XML_NS_URI, "xml:id",
                                                       value);
                            }
                        } else if (LOCAL_ATTRIBUTES_NS_URI.equals(nsURI)) {
                            element.setAttributeNS(null, attr.getLocalName(),
                                                   value);
                        } else {
                            element.setAttributeNS(nsURI, qName, value);
                        }
                    }
                }
            }
        }
    }

    // -----------------------------------------------------------------------
    // Main
    // -----------------------------------------------------------------------

    public static void main(String[] args) {
        File outFile = null;
        String[] idAttrName = null;
        Console.MessageType verbosity = Console.MessageType.WARNING;

        int i = 0;
        final int argCount = args.length;
        for (; i < argCount; ++i) {
            String arg = args[i];

            if ("-o".equals(arg)) {
                if (i+1 >= argCount) {
                    usage(null);
                    /*NOTREACHED*/
                }

                outFile = (new File(args[i+1])).getAbsoluteFile();
                ++i;
            } else if ("-id".equals(arg)) {
                if (i+1 >= argCount) {
                    usage(null);
                    /*NOTREACHED*/
                }

                idAttrName = DOMUtil.parseName(args[i+1]);
                if (idAttrName == null) {
                    usage("cannot parse '" + args[i+1] + "' as an XML name" +
                          "; should be xml:id OR" +
                          " [ '{' namespace_URI '}' ] local_part");
                    /*NOTREACHED*/
                }

                ++i;
            } else if ("-v".equals(arg)) {
                verbosity = Console.MessageType.INFO;
            } else if ("-vv".equals(arg)) {
                verbosity = Console.MessageType.VERBOSE;
            } else if ("-vvv".equals(arg)) {
                verbosity = Console.MessageType.DEBUG;
            } else {
                if (arg.startsWith("-")) {
                    usage("'" + arg + "', unknown option");
                    /*NOTREACHED*/
                }

                // Done with options.
                break;
            }
        }

        int inputCount = argCount - i;
        if (inputCount <= 0 || outFile == null) {
            usage(null);
            /*NOTREACHED*/
        }

        if (inputCount > 1 && outFile.isFile()) {
            usage("'" + outFile + "', not an output directory");
            /*NOTREACHED*/
        }

        File[] inFiles = new File[inputCount];
        int j = 0;

        for (; i < argCount; ++i) {
            File inFile = new File(args[i]);

            if (!inFile.isFile()) {
                usage("'" + inFile + "', not a file");
                /*NOTREACHED*/
            }
            inFiles[j++] = inFile;
        }

        // ---

        ConsoleHelper console = new ConsoleHelper(
            new SimpleConsole(/*prefix*/ null, /*showMessageType*/ true, 
                              /*errorLevel*/ Console.MessageType.INFO));
        console.setVerbosity(verbosity);

        XMLResolver resolver = null;
        if (System.getProperty("xml.catalog.files") != null) {
            // Works if xmlresolver.jar is in the CLASSPATH.
            try {
                resolver = new XMLResolverImpl(/*urls*/ null);
            } catch (Exception ignored) {}
        }

        Document[] docs = new Document[inputCount];

        LoaderImpl loader = new LoaderImpl(/*xincludeAware*/ false, console);
        if (resolver != null) {
            loader.setEntityResolver(resolver);
        }

        for (int k = 0; k < inputCount; ++k) {
            File inFile = inFiles[k];

            console.info(Msg.msg("loadingDoc", inFile));

            try {
                docs[k] = loader.load(inFile);
            } catch (IOException e) {
                console.error(Msg.msg("cannotLoadDoc", 
                                      inFile, ThrowableUtil.reason(e)));
                System.exit(2);
            }
        }

        // ---

        int exitCode = 0;

        console.info(Msg.msg("processingDocs"));

        String xincludeError = null;

        XIncluder xincluder = new XIncluder(console);
        if (resolver != null) {
            xincluder.setResolver(resolver);
        }
        if (idAttrName != null) {
            xincluder.setIdAttribute(idAttrName[0], idAttrName[1]);
        }

        try {
            if (!xincluder.process(docs)) {
                xincludeError = Msg.msg("foundInclusionErrors");
            }
        } catch (IOException e) {
            xincludeError = ThrowableUtil.reason(e);
        }

        if (xincludeError != null) {
            console.error(Msg.msg("cannotProcessDocs", xincludeError));
            exitCode = 3;
        }

        // ---

        File outDir;
        if (outFile.isDirectory()) {
            outDir = outFile;
        } else {
            outDir = outFile.getParentFile();
        }

        try {
            if (!outDir.isDirectory()) {
                FileUtil.checkedMkdirs(outDir);
            }

            for (int k = 0; k < inputCount; ++k) {
                Document doc = docs[k];

                File saveFile;
                if (inputCount == 1 && !outFile.isDirectory()) {
                    saveFile = outFile;
                } else {
                    saveFile = new File(outDir, inFiles[k].getName());
                }

                console.info(Msg.msg("savingDoc", saveFile));

                SaveDocument.save(doc, saveFile);
            }
        } catch (IOException e) {
            console.error(Msg.msg("cannotSaveDocs", ThrowableUtil.reason(e)));
            if (exitCode == 0) {
                exitCode = 4;
            }
        }

        System.exit(exitCode);
    }

    private static void usage(String error) {
        if (error != null) {
            System.err.println("*** ERROR: " + error);
        }
        System.err.println(
            "usage: java com.xmlmind.xinclude.XIncluder [ -v|-vv|-vvv ]\n" +
            " [ -id id_attr_name ] -o out_file_or_dir in_file ... in_file\n" +
            "Transcludes Xincludes found in input XML files in_file and\n" +
            "creates processed files in out_file_or_dir.\n" +
            "Options:\n" +
            "  -o out_file_or_dir Required. Specifies an output file or\n" +
            "      directory. If multiple input files are specified,\n" +
            "      out_file_or_dir must be a directory.\n" +
            "  -id id_attr_name Specified the name of the ID attribute.\n" +
            "      By default it's xml:id.\n" +
            "      Syntax: xml:id OR [ '{' namespace_URI '}' ] local_part.\n" +
            "  -v|-vv|-vvv Verbose");
        System.exit(1);
    }
}
