/*
 * Copyright (c) 1998, 2018 Oracle and/or its affiliates. All rights reserved.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v. 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0,
 * or the Eclipse Distribution License v. 1.0 which is available at
 * http://www.eclipse.org/org/documents/edl-v10.php.
 *
 * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
 */

// Contributors:
//     Oracle - initial API and implementation from Oracle TopLink
package org.eclipse.persistence.internal.oxm.record;

import java.io.IOException;

import org.xml.sax.Attributes;
import org.xml.sax.ContentHandler;
import org.xml.sax.DTDHandler;
import org.xml.sax.EntityResolver;
import org.xml.sax.ErrorHandler;
import org.xml.sax.InputSource;
import org.xml.sax.Locator;
import org.xml.sax.SAXException;
import org.xml.sax.SAXNotRecognizedException;
import org.xml.sax.SAXNotSupportedException;
import org.xml.sax.ext.LexicalHandler;

import javax.xml.XMLConstants;
import javax.xml.namespace.QName;
import javax.xml.validation.ValidatorHandler;

import org.eclipse.persistence.internal.core.sessions.CoreAbstractSession;
import org.eclipse.persistence.internal.oxm.ConversionManager;
import org.eclipse.persistence.internal.oxm.Constants;
import org.eclipse.persistence.internal.oxm.MediaType;
import org.eclipse.persistence.internal.oxm.mappings.Field;
import org.eclipse.persistence.internal.oxm.mappings.Mapping;
import org.eclipse.persistence.oxm.mappings.nullpolicy.AbstractNullPolicy;

/**
 * INTERNAL:
 * <p><b>Purpose:</b>Provide a wrapper for an org.xml.sax.XMLReader instance and define some extra
 * event methods that can be used by TopLink during the unmarshal process. These events are no ops
 * in this class, but may be overridden in subclasses.
 * <p><b>Responsibilities</b><ul>
 * <li>Wrap an instance of org.xml.sax.XMLReader and provide all the required API</li>
 * <li>Provide empty implementations of some callback methods that can be overridden in subclasses</li>
 *
 *  @see org.eclipse.persistence.internal.oxm.record.DOMReader
 *  @author  mmacivor
 *  @since   release specific (what release of product did this appear in)
 */

public class XMLReader implements org.xml.sax.XMLReader {

    public static final String NAMESPACE_PREFIXES_FEATURE = "http://xml.org/sax/features/namespace-prefixes";
    public static final String REPORT_IGNORED_ELEMENT_CONTENT_WHITESPACE_FEATURE = "http://java.sun.com/xml/schema/features/report-ignored-element-content-whitespace";

    private org.xml.sax.XMLReader reader;
    private boolean supportsLexicalHandler;
    private LexicalHandlerWrapper lexicalHandlerWrapper;
    protected ValidatingContentHandler validatingContentHandler;
    private boolean namespaceAware;
    private char namespaceSeparator;
    protected Locator locator;

    public XMLReader(org.xml.sax.XMLReader internalReader) {
        this();
        this.reader = internalReader;
    }

    public XMLReader() {
        this.supportsLexicalHandler = true;
        namespaceAware = true;
        namespaceSeparator = Constants.COLON;
    }

    /**
     * INTERNAL:
     * return the Locator object associated with this reader
     * @since 2.4
     */
    public Locator getLocator(){
        return locator;
    }

    /**
     * INTERNAL:
     * set the Locator object to associate with this reader
     * @since 2.4
     */
    public void setLocator(Locator newLocator){
        locator = newLocator;
    }

    public ContentHandler getContentHandler () {
        return reader.getContentHandler();
    }

    public void setContentHandler (ContentHandler handler) {
        if(validatingContentHandler != null) {
            validatingContentHandler.setContentHandler(handler);
        } else {
            reader.setContentHandler(handler);
        }
    }

    /**
     * INTERNAL:
     * Determine if namespaces will be considered during marshal/unmarshal operations.
     * @since 2.4
     */
    public boolean isNamespaceAware() {
        return namespaceAware;
    }

    /**
     * If set to true, the reader will be aware of namespaces during marshal/unmarsal operations.
     *
     * @param namespaceAware if reader should be namespace aware
     * @since 2.6.0
     */
    public void setNamespaceAware(boolean namespaceAware) {
        this.namespaceAware = namespaceAware;
    }

    /**
     * INTERNAL:
     * The character used to separate the prefix and uri portions when namespaces are present
     * @since 2.4
     */
    public char getNamespaceSeparator(){
        return namespaceSeparator;
    }

    /**
     * Sets namespace separator.
     *
     * @param namespaceSeparator namespace separator
     * @since 2.6.0
     */
    public void setNamespaceSeparator(char namespaceSeparator) {
        this.namespaceSeparator = namespaceSeparator;
    }

    /**
     *  INTERNAL:
     *  @return The MediaType associated with this reader
     */
    public MediaType getMediaType(){
        return Constants.APPLICATION_XML;
    }


    /**
     * INTERNAL:
     * @since 2.4
     */
    public Object convertValueBasedOnSchemaType(Field xmlField, Object value, ConversionManager conversionManager, AbstractUnmarshalRecord record) {
        return xmlField.convertValueBasedOnSchemaType(value, conversionManager, record);
    }

    public DTDHandler getDTDHandler () {
        return reader.getDTDHandler();
    }

    public void setDTDHandler (DTDHandler handler) {
        reader.setDTDHandler(handler);
    }

    public void setEntityResolver (EntityResolver resolver) {
        reader.setEntityResolver(resolver);
    }

    public EntityResolver getEntityResolver () {
        return reader.getEntityResolver();
    }

    public ErrorHandler getErrorHandler () {
        return reader.getErrorHandler();
    }

    public void setErrorHandler (ErrorHandler handler) {
        if(validatingContentHandler != null) {
            validatingContentHandler.setErrorHandler(handler);
        } else {
            reader.setErrorHandler(handler);
        }
    }

    public LexicalHandler getLexicalHandler() {
        if(supportsLexicalHandler) {
            try {
                return (LexicalHandler) reader.getProperty(Constants.LEXICAL_HANDLER_PROPERTY);
            } catch (SAXException e) {
                supportsLexicalHandler = false;
            }
        }
        return null;
    }

    public void setLexicalHandler(LexicalHandler lexicalHandler) {
        if(supportsLexicalHandler) {
                if(null == lexicalHandlerWrapper) {
                    try {
                        lexicalHandlerWrapper = new LexicalHandlerWrapper(lexicalHandler);
                        reader.setProperty(Constants.LEXICAL_HANDLER_PROPERTY, lexicalHandlerWrapper);
                    } catch (SAXException e) {
                        supportsLexicalHandler = false;
                    }
                } else {
                    lexicalHandlerWrapper.setLexicalHandler(lexicalHandler);
                }
        }
    }

    public boolean getFeature (String name) throws SAXNotRecognizedException, SAXNotSupportedException {
        return reader.getFeature(name);
    }

    public void setFeature (String name, boolean value) throws SAXNotRecognizedException, SAXNotSupportedException {
        reader.setFeature(name, value);
    }

    public Object getProperty (String name) throws SAXNotRecognizedException, SAXNotSupportedException {
        if(Constants.LEXICAL_HANDLER_PROPERTY.equals(name)) {
            return getLexicalHandler();
        } else {
            return reader.getProperty(name);
        }
    }

    public void setProperty (String name, Object value) throws SAXNotRecognizedException, SAXNotSupportedException {
        if(Constants.LEXICAL_HANDLER_PROPERTY.equals(name)) {
            setLexicalHandler((LexicalHandler) value);
        } else {
            reader.setProperty(name, value);
        }
    }

    public void parse(InputSource input) throws IOException, SAXException {
        try {
            reader.parse(input);
        } catch(SAXNotSupportedException e) {
            String message = e.getMessage();
            if(message != null && message.contains("namespace-prefix")) {
                reader.setFeature(NAMESPACE_PREFIXES_FEATURE, false);
                reader.parse(input);
            } else {
                throw e;
            }
        }
    }

    public void parse (String systemId) throws IOException, SAXException {
        try {
            reader.parse(systemId);
        } catch(SAXNotSupportedException e) {
            String message = e.getMessage();
            if(message != null && message.contains("namespace-prefix")) {
                reader.setFeature(NAMESPACE_PREFIXES_FEATURE, false);
                reader.parse(systemId);
            } else {
                throw e;
            }
        }
    }

    public void setValidatorHandler(ValidatorHandler validatorHandler) {
        ErrorHandler errorHandler = getErrorHandler();
        ContentHandler contentHandler;
        if(null == this.validatingContentHandler) {
            contentHandler = getContentHandler();
        } else {
            contentHandler = validatorHandler.getContentHandler();
            this.validatingContentHandler = null;
        }
        ValidatingContentHandler validatingContentHandler = null;
        if(null != validatorHandler) {
            validatingContentHandler = new ValidatingContentHandler(validatorHandler);
            validatingContentHandler.setContentHandler(contentHandler);
            contentHandler = validatingContentHandler;
        }
        if(null != reader) {
            reader.setContentHandler(contentHandler);
        }
        setContentHandler(contentHandler);
        this.validatingContentHandler = validatingContentHandler;
        setErrorHandler(errorHandler);
    }

    public ValidatorHandler getValidatorHandler() {
        if(null == validatingContentHandler) {
            return null;
        }
        return this.validatingContentHandler.getValidatorHandler();
    }

    public void newObjectEvent(Object object, Object parent, Mapping selfRecordMapping) {
        //no op in this class.
    }

    public Object getCurrentObject(CoreAbstractSession session, Mapping selfRecordMapping) {
        return null;
    }

    /**
     * This call back mechanism provides an opportunity for the XMLReader to
     * provide an alternate conversion.  This optimization is currently only
     * leveraged for properties annotated with @XmlInlineBinaryData.
     * @param characters The characters to be converted.
     * @param dataType The type to be converted to.
     * @return The converted value
     */
    public Object getValue(CharSequence characters, Class<?> dataType) {
        return null;
    }

    public boolean isNullRepresentedByXsiNil(AbstractNullPolicy nullPolicy){
        return nullPolicy.isNullRepresentedByXsiNil();
    }

    public boolean isNullRecord(AbstractNullPolicy nullPolicy, Attributes atts, UnmarshalRecord record) {
        boolean isNil = isNullRepresentedByXsiNil(nullPolicy) && record.isNil();
        if (!nullPolicy.ignoreAttributesForNil()) {
            return isNil && !hasAttributes(atts);
        }
        return isNil;
    }

    private boolean hasAttributes(Attributes attributes) {
        QName nilAttrName = new QName(XMLConstants.W3C_XML_SCHEMA_INSTANCE_NS_URI, Constants.SCHEMA_NIL_ATTRIBUTE);
        for (int i = 0; i < attributes.getLength(); i++) {
            if (!(nilAttrName.getNamespaceURI().equals(attributes.getURI(i)) &&
                    nilAttrName.getLocalPart().equals(attributes.getLocalName(i))) &&
                    !XMLConstants.XMLNS_ATTRIBUTE_NS_URI.equals(attributes.getURI(i))) {
                return true;
            }
        }
        return false;
    }

    public boolean isInCollection(){
        return true;
    }



    /**
     * Performance Optimization:
     * It is expensive to change the LexicalHandler on the underlying XMLReader
     * constantly through the setProperty(String, Object) mechanism.  So instead
     * the LexicalHandlerWrapper is set once this way, and the "real"
     * LexicalHandler is changed on the LexicalHandlerWrapper.
     */
    private static class LexicalHandlerWrapper implements LexicalHandler {

        private LexicalHandler lexicalHandler;

        public LexicalHandlerWrapper(LexicalHandler lexicalHandler) {
            this.lexicalHandler = lexicalHandler;
        }

        public void setLexicalHandler(LexicalHandler lexicalHandler) {
            this.lexicalHandler = lexicalHandler;
        }

        public void comment(char[] ch, int start, int length) throws SAXException {
            if(null != lexicalHandler) {
                lexicalHandler.comment(ch, start, length);
            }
        }

        public void endCDATA() throws SAXException {
            if(null  != lexicalHandler) {
                lexicalHandler.endCDATA();
            }
        }

        public void endDTD() throws SAXException {
            if(null != lexicalHandler) {
                lexicalHandler.endDTD();
            }
        }

        public void endEntity(String name) throws SAXException {
            if(null != lexicalHandler) {
                lexicalHandler.endEntity(name);
            }
        }

        public void startCDATA() throws SAXException {
            if(null != lexicalHandler) {
                lexicalHandler.startCDATA();
            }
        }

        public void startDTD(String name, String publicId, String systemId) throws SAXException {
            if(null != lexicalHandler) {
                lexicalHandler.startCDATA();
            }
        }

        public void startEntity(String name) throws SAXException {
            if(null != lexicalHandler) {
                lexicalHandler.startEntity(name);
            }
        }

    }

    /**
     * Validate the SAX events reported to the ContentHandler.  This class is
     * being used rather than a ValidatorHandler in order to prevent default
     * values from being populated.
     */
    protected static class ValidatingContentHandler implements ContentHandler {

        private ValidatorHandler validatorHandler;
        private ContentHandler contentHandler;

        public ValidatingContentHandler(ValidatorHandler validatorHandler) {
            this.validatorHandler = validatorHandler;
        }

        public ContentHandler getContentHandler() {
            return contentHandler;
        }

        public void setContentHandler(ContentHandler contentHandler) {
            this.contentHandler = contentHandler;
        }

        public void setErrorHandler(ErrorHandler errorHandler) {
            validatorHandler.setErrorHandler(errorHandler);
        }

        public ValidatorHandler getValidatorHandler() {
            return validatorHandler;
        }

        public void setValidatorHandler(ValidatorHandler validatorHandler) {
            this.validatorHandler = validatorHandler;
        }

        public void setDocumentLocator(Locator locator) {
            validatorHandler.setDocumentLocator(locator);
            contentHandler.setDocumentLocator(locator);
        }

        public void startDocument() throws SAXException {
            validatorHandler.startDocument();
            contentHandler.startDocument();
        }

        public void endDocument() throws SAXException {
            validatorHandler.endDocument();
            contentHandler.endDocument();
        }

        public void startPrefixMapping(String prefix, String uri) throws SAXException {
            validatorHandler.startPrefixMapping(prefix, uri);
            contentHandler.startPrefixMapping(prefix, uri);
        }

        public void endPrefixMapping(String prefix) throws SAXException {
            validatorHandler.endPrefixMapping(prefix);
            contentHandler.endPrefixMapping(prefix);
        }

        public void startElement(String uri, String localName, String qName, Attributes atts) throws SAXException {
            validatorHandler.startElement(uri, localName, qName, atts);
            contentHandler.startElement(uri, localName, qName, atts);
        }

        public void endElement(String uri, String localName, String qName) throws SAXException {
            validatorHandler.endElement(uri, localName, qName);
            contentHandler.endElement(uri, localName, qName);
        }

        public void characters(char[] ch, int start, int length) throws SAXException {
            validatorHandler.characters(ch, start, length);
            contentHandler.characters(ch, start, length);
        }

        public void ignorableWhitespace(char[] ch, int start, int length) throws SAXException {
            validatorHandler.ignorableWhitespace(ch, start, length);
            contentHandler.characters(ch, start, length);
        }

        public void processingInstruction(String target, String data) throws SAXException {
            validatorHandler.processingInstruction(target, data);
            contentHandler.processingInstruction(target, data);
        }

        public void skippedEntity(String name) throws SAXException {
            validatorHandler.skippedEntity(name);
            contentHandler.skippedEntity(name);
        }

    }

}
