import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.StringReader;
import java.net.URL;
import java.util.Objects;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import org.xml.sax.ext.DefaultHandler2;

public class DOMBuilderProvidedExternalSubsetTest {

    private DocumentBuilderFactory parserFactory;

    public void setUp() throws Exception {
        parserFactory = DocumentBuilderFactory.newInstance();
        parserFactory.setNamespaceAware(true);
        //parserFactory.setExpandEntityReferences(false);
    }

    public void testDoctypeNoExternalSubset() throws Exception {
        testExternalSubsetRequested("doctype-no-ext-subset.xml");
    }

    public void testNoDoctype() throws Exception {
        testExternalSubsetRequested("no-doctype-decl.xml");
    }

    public void testDoctypeWithIntSubset() throws Exception {
        testExternalSubsetRequested("doctype-int-subset.xml");
    }

    private void testExternalSubsetRequested(String resource) throws Exception {
        System.out.println(resource);

        DocumentBuilder domBuilder = parserFactory.newDocumentBuilder();
        TestHandler testHandler = new TestHandler();
        domBuilder.setErrorHandler(testHandler);
        domBuilder.setEntityResolver(testHandler);

        InputSource source = new InputSource(getResource(resource).toString());
        Document document = domBuilder.parse(source);

        Node body = document.getElementsByTagName("body").item(0);
        assertEquals("text content",
                body.getTextContent().trim(), "Foo \u00A0 Bar\u2122");
    }

    private static URL getResource(String name) throws IOException {
        URL resource = DOMBuilderProvidedExternalSubsetTest.class.getResource(name);
        if (resource == null) {
            throw new FileNotFoundException("Resource not found: " + name);
        }
        return resource;
    }

    private static void assertEquals(String subject, String actual, String expected)
            throws AssertionError {
        if (!Objects.equals(actual, expected)) {
            throw new AssertionError("Expected " + subject
                    + ": \"" + expected + "\", but was: \"" + actual + "\"");
        }
    }

    public static void main(String[] args) throws Exception {
        DOMBuilderProvidedExternalSubsetTest
                suite = new DOMBuilderProvidedExternalSubsetTest();
        suite.setUp();
        if (run(suite::testDoctypeNoExternalSubset)
                & run(suite::testNoDoctype)
                & run(suite::testDoctypeWithIntSubset)) {
            // success
        } else {
            System.exit(1);
        }
    }

    private static boolean run(ThrowingRunnable testCase) {
        try {
            testCase.run();
            return true;
        } catch (Throwable e) {
            e.printStackTrace();
            return false;
        }
    }


    @FunctionalInterface
    static interface ThrowingRunnable {
        void run() throws Throwable;
    }


    static class TestHandler extends DefaultHandler2 {

        private static InputSource htmlEntities() {
            return new InputSource(new StringReader("<!ENTITY nbsp '&#xA0;'>"));
        }

        @Override
        public InputSource getExternalSubset(String name, String baseURI)
                throws SAXException, IOException {
            return "html".equals(name) ? htmlEntities() : null;
        }

        @Override
        public void warning(SAXParseException e) throws SAXException {
            System.err.append("[warning] ").println(e);
        }

        @Override
        public void error(SAXParseException e) throws SAXException {
            System.err.append("[error] ").println(e);
        }

        @Override
        public void fatalError(SAXParseException e) throws SAXException {
            // Parser should signal an exception in any case
        }

    } // class TestHandler


}