/*
 * 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.domutil;

import java.io.IOException;
import java.io.File;
import java.io.InputStream;
import java.io.BufferedInputStream;
import java.net.URL;
import javax.xml.parsers.SAXParserFactory;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import org.xml.sax.ErrorHandler;
import org.xml.sax.InputSource;
import org.xml.sax.EntityResolver;
import org.xml.sax.XMLReader;
import org.w3c.dom.Document;
import com.xmlmind.util.ThrowableUtil;
import com.xmlmind.util.FileUtil;
import com.xmlmind.util.URLUtil;
import com.xmlmind.util.Console;
import com.xmlmind.util.SimpleConsole;
import com.xmlmind.domutil.ConsoleHelper;

/**
 * An implementation of {@link Loader} making use by default of the built-in
 * XInclude processor (possibly XInclude 1.0, not 1.1; possibly limited) 
 * of the XML parser.
 */
public class LoaderImpl implements Loader {
    private static boolean[] addElementPointer = new boolean[1];

    /**
     * <b>Not part of the public, documented, API</b>: specifies whether
     * to add element XPath pointers to {@link NodeLocation}s during 
     * the loading of XML documents.
     * <p>This facility is needed to implement XXE's <b>Check Assembly</b>.
     *
     * @see #isAddingElementPointer
     */
    public static void setAddingElementPointer(boolean add) {
        synchronized (addElementPointer) {
            addElementPointer[0] = add;
        }
    }

    /**
     * <b>Not part of the public, documented, API</b>: returns <code>true</code>
     * if element XPath pointers are added to {@link NodeLocation}s during 
     * the loading of XML documents.
     * <p>This facility is needed to implement XXE's <b>Check Assembly</b>.
     *
     * @see #setAddingElementPointer
     */
    public static boolean isAddingElementPointer() {
        synchronized (addElementPointer) {
            return addElementPointer[0];
        }
    }

    // -----------------------------------------------------------------------

    protected ConsoleHelper console;
    protected boolean xincludeAware;

    protected EntityResolver entityResolver;

    // -----------------------------------------------------------------------

    public LoaderImpl() {
        this(true, null);
    }

    @SuppressWarnings("this-escape")
    public LoaderImpl(boolean xincludeAware, Console c) {
        this.xincludeAware = xincludeAware;
        setConsole(c);
    }

    public void setConsole(Console c) {
        if (c == null) {
            c = new SimpleConsole();
        }
        console = ((c instanceof ConsoleHelper)? 
                   (ConsoleHelper) c : new ConsoleHelper(c));
    }

    public ConsoleHelper getConsole() {
        return console;
    }

    public void setEntityResolver(EntityResolver resolver) {
        entityResolver = resolver;
    }

    public EntityResolver getEntityResolver() {
        return entityResolver;
    }

    public Document load(File file) 
        throws IOException {
        return load(FileUtil.fileToURL(file));
    }

    public Document load(URL url) 
        throws IOException {
        InputStream in = 
            new BufferedInputStream(URLUtil.openStreamNoCache(url));

        Document doc;
        try {
            doc = load(in, url);
        } finally {
            in.close();
        }

        return doc;
    }

    public Document load(InputStream in, URL url) 
        throws IOException {
        Document doc;
        XMLReader parser;
        try {
            doc = DOMUtil.newDocument();

            parser = createXMLReader(xincludeAware);
            parser.setEntityResolver(entityResolver);
        } catch (Exception shouldNotHappen) {
            throw new IOException(ThrowableUtil.reason(shouldNotHappen));
        }
        
        SAXToDOM domBuilder = new SAXToDOM(doc, isAddingElementPointer());
        parser.setContentHandler(domBuilder);
        parser.setErrorHandler(domBuilder);

        InputSource input = new InputSource(in);

        String systemId = null;
        if (url != null) {
            systemId = url.toExternalForm();
            input.setSystemId(systemId);
        }

        try {
            parser.parse(input);
        } catch (SAXParseException e) {
            throw new IOException(format(e), e);
        } catch (SAXException e) {
            throw new IOException(ThrowableUtil.reason(e), e);
        }

        if (systemId != null) {
            doc.setDocumentURI(systemId);
        }

        return doc;
    }

    public static XMLReader createXMLReader(boolean xincludeAware) {
        XMLReader parser = null;
        try {
            SAXParserFactory factory = SAXParserFactory.newInstance();

            factory.setNamespaceAware(true);
            factory.setValidating(false);
            factory.setXIncludeAware(xincludeAware);

            // For Xerces which otherwise, does not support "x-MacRoman".
            try {
                factory.setFeature(
                    "http://apache.org/xml/features/allow-java-encodings",
                    true);
            } catch (Exception ignored) {}

            parser = factory.newSAXParser().getXMLReader();
        } catch (Exception e) {
            throw new RuntimeException(Msg.msg("cannotCreateSAXParser", 
                                               ThrowableUtil.reason(e)));
        }

        return parser;
    }

    public static final String format(SAXParseException e) {
        StringBuilder buffer = new StringBuilder();

        if (e.getSystemId() != null) {
            buffer.append(e.getSystemId());
        }

        buffer.append(':');
        if (e.getLineNumber() > 0) {
            buffer.append(e.getLineNumber());
        }

        buffer.append(':');
        if (e.getColumnNumber() > 0) {
            buffer.append(e.getColumnNumber());
        }

        buffer.append(':');
        buffer.append(Msg.msg("parseError"));
        buffer.append(": ");
        buffer.append(e.getMessage());

        return buffer.toString();
    }
}
