/* Woodstox XML processor
 *
 * Copyright (c) 2004- Tatu Saloranta, tatu.saloranta@iki.fi
 *
 * Licensed under the License specified in the file LICENSE which is
 * included with the source code.
 * You may not use this file except in compliance with the License.
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.ctc.wstx.sr;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.Reader;
import java.net.URL;
import java.util.*;

import javax.xml.stream.Location;
import javax.xml.stream.XMLStreamReader;
import javax.xml.stream.XMLStreamException;
import javax.xml.transform.stream.StreamSource;

import org.codehaus.stax2.validation.*;

import com.ctc.wstx.api.ReaderConfig;
import com.ctc.wstx.cfg.ErrorConsts;
import com.ctc.wstx.cfg.XmlConsts;
import com.ctc.wstx.exc.WstxException;
import com.ctc.wstx.io.*;
import com.ctc.wstx.dtd.DTDId;
import com.ctc.wstx.dtd.DTDSubset;
import com.ctc.wstx.dtd.DTDValidatorBase;
import com.ctc.wstx.dtd.FullDTDReader;
import com.ctc.wstx.util.ExceptionUtil;
import com.ctc.wstx.util.URLUtil;

/**
 * Implementation of {@link XMLStreamReader} that builds on
 * {@link BasicStreamReader}, but adds full DTD-handling, including
 * DTD validation
 */
public class ValidatingStreamReader
    extends BasicStreamReader
{
    /*
    ////////////////////////////////////////////////
    // Constants for standard StAX properties:
    ////////////////////////////////////////////////
    */

    final static String STAX_PROP_ENTITIES = "javax.xml.stream.entities";

    final static String STAX_PROP_NOTATIONS = "javax.xml.stream.notations";

    /*
    ////////////////////////////////////////////////////
    // Validation (DTD) information (entities, ...)
    ////////////////////////////////////////////////////
     */

    // // // Note: some members that logically belong here, are actually
    // // // part of superclass

    /**
     * Combined DTD set, constructed from parsed internal and external
     * entities (which may have been set via override DTD functionality).
     */
    DTDValidationSchema mDTD = null;

    /**
     * Validating reader keeps of automatically created DTD-based
     * validator, since its handling may differ from that of application
     * managed validators.
     */
    XMLValidator mAutoDtdValidator = null;

    /**
     * Flag to indicate that the DOCTYPE declaration is to be
     * overridden by value of {@link #mDTD}. Mostly needed to signal
     * "significant null" value, which means "just discard DOCTYPE if one
     * gotten from the document".
     */
    boolean mDTDOverridden = false;

    /**
     * Flag that indicates whether a DTD validator has been automatically
     * set (as per DOCTYPE declaration or override)
     */
    boolean mDtdValidatorSet = false;

    /**
     * Custom validation problem handler, if any.
     */
    protected ValidationProblemHandler mVldProbHandler = null;

    /*
    ////////////////////////////////////////////////////
    // Configuration
    ////////////////////////////////////////////////////
     */

    /*
    ////////////////////////////////////////////////////
    // Life-cycle (ctors)
    ////////////////////////////////////////////////////
     */

    private ValidatingStreamReader(InputBootstrapper bs,
                                   BranchingReaderSource input, ReaderCreator owner,
                                   ReaderConfig cfg, InputElementStack elemStack,
                                   boolean forER)
        throws IOException, XMLStreamException
    {
        super(bs, input, owner, cfg, elemStack, forER);
    }

    /**
     * Factory method for constructing readers.
     *
     * @param owner "Owner" of this reader, factory that created the reader;
     *   needed for returning updated symbol table information after parsing.
     * @param input Input source used to read the XML document.
     * @param cfg Object that contains reader configuration info.
     * @param bs Bootstrapper to use, for reading xml declaration etc.
     * @param forER True if this reader is to be (configured to be) used by
     *   an event reader. Will cause some changes to default settings, as
     *   required by contracts Woodstox XMLEventReader implementation has
     *   (with respect to lazy parsing, short text segments etc)
     */
    public static ValidatingStreamReader createValidatingStreamReader
        (BranchingReaderSource input, ReaderCreator owner,
         ReaderConfig cfg, InputBootstrapper bs, boolean forER)
        throws IOException, XMLStreamException
    {
        ValidatingStreamReader sr = new ValidatingStreamReader
            (bs, input, owner, cfg, createElementStack(cfg), forER);
        return sr;
    }

    /*
    ////////////////////////////////////////////////////
    // Public API, configuration
    ////////////////////////////////////////////////////
     */

    public Object getProperty(String name)
    {
        // DTD-specific properties...
        if (name.equals(STAX_PROP_ENTITIES)) {
            safeEnsureFinishToken();
            if (mDTD == null || !(mDTD instanceof DTDSubset)) {
                return null;
            }
            List l = ((DTDSubset) mDTD).getGeneralEntityList();
            /* Let's make a copy, so that caller can not modify
             * DTD's internal list instance
             */
            return new ArrayList(l);
        }
        if (name.equals(STAX_PROP_NOTATIONS)) {
            safeEnsureFinishToken();
            if (mDTD == null || !(mDTD instanceof DTDSubset)) {
                return null;
            }
            /* Let's make a copy, so that caller can not modify
             * DTD's internal list instance
             */
            List l = ((DTDSubset) mDTD).getNotationList();
            return new ArrayList(l);
        }
        return super.getProperty(name);
    }

    /*
    ////////////////////////////////////////////////////
    // XMLStreamReader2 (StAX2) implementation
    ////////////////////////////////////////////////////
     */

    // // // StAX2, per-reader configuration

    // no additional readable features
    //public Object getFeature(String name)

    public void setFeature(String name, Object value)
    {
        // Referring to DTD-related features?
        if (name.equals(FEATURE_DTD_OVERRIDE)) {
            mDTDOverridden = true;
            // null is ok, basically means "never use a DTD"...
            if (value != null && !(value instanceof DTDValidationSchema)) {
                throw new IllegalArgumentException("Value to set for feature "+name+" not of type DTDValidationSchema");
            }
            mDTD = (DTDValidationSchema) value;
        } else {
            super.setFeature(name, value);
        }
    }

    // Nothing to override from the base class, for these methods:
    /*
    public boolean isPropertySupported(String name)
    {
    }

    public void setProperty(String name, Object value)
    {
    }
    */

    /*
    ////////////////////////////////////////////////////
    // DTDInfo implementation (StAX 2)
    ////////////////////////////////////////////////////
     */

    public Object getProcessedDTD() {
        return mDTD;
    }

    public DTDValidationSchema getProcessedDTDSchema() {
        return mDTD;
    }

    /*
    ////////////////////////////////////////////////////
    // Stax2 validation
    ////////////////////////////////////////////////////
     */

    // @Override
    public XMLValidator validateAgainst(XMLValidationSchema schema)
        throws XMLStreamException
    {
        return mElementStack.validateAgainst(schema);
    }

    // @Override
    public XMLValidator stopValidatingAgainst(XMLValidationSchema schema)
        throws XMLStreamException
    {
        return mElementStack.stopValidatingAgainst(schema);
    }

    // @Override
    public XMLValidator stopValidatingAgainst(XMLValidator validator)
        throws XMLStreamException
    {
        return mElementStack.stopValidatingAgainst(validator);
    }

    // @Override
    public ValidationProblemHandler setValidationProblemHandler(ValidationProblemHandler h)
    {
        ValidationProblemHandler oldH = mVldProbHandler;
        mVldProbHandler = h;
        return oldH;
    }

    /*
    ////////////////////////////////////////////////////
    // Private methods, DOCTYPE handling
    ////////////////////////////////////////////////////
     */

    /**
     * This method gets called to handle remainder of DOCTYPE declaration,
     * essentially the optional internal subset. Internal subset, if such
     * exists, is always read, but whether its contents are added to the
     * read buffer depend on passed-in argument.
     *<p>
     * NOTE: Since this method overrides the default implementation, make
     * sure you do NOT change the method signature.
     *
     * @param copyContents If true, will copy contents of the internal
     *   subset of DOCTYPE declaration
     *   in the text buffer (in addition to parsing it for actual use); if
     *   false, will only do parsing.
     */
    protected void finishDTD(boolean copyContents)
        throws IOException, XMLStreamException
    {
        if (!hasConfigFlags(CFG_SUPPORT_DTD)) {
            super.finishDTD(copyContents);
            return;
        }

        /* We know there are no spaces, as this char was read and pushed
         * back earlier...
         */
        char c = getNextChar(SUFFIX_IN_DTD);
        DTDSubset intSubset = null;

        /* Do we have an internal subset? Note that we have earlier checked
         * that it has to be either '[' or closing '>'.
         */
        if (c == '[') {
            // Do we need to copy the contents of int. subset in the buffer?
            if (copyContents) {
                ((BranchingReaderSource) mInput).startBranch(mTextBuffer, mInputPtr, mCfgNormalizeLFs);
            }

            try {
                intSubset = FullDTDReader.readInternalSubset(this, mInput, mConfig,
                                                             hasConfigFlags(CFG_VALIDATE_AGAINST_DTD),
                                                             mDocXmlVersion);
            } finally {
                /* Let's close branching in any and every case (may allow
                 * graceful recovery in error cases in future
                 */
                if (copyContents) {
                    /* Need to "push back" ']' got in the succesful case
                     * (that's -1 part below);
                     * in error case it'll just be whatever last char was.
                     */
                    ((BranchingReaderSource) mInput).endBranch(mInputPtr-1);
                }
            }

            // And then we need closing '>'
            c = getNextCharAfterWS(SUFFIX_IN_DTD_INTERNAL);
        }

        if (c != '>') {
            throwUnexpectedChar(c, "; expected '>' to finish DOCTYPE declaration.");
        }

        /* But, then, we also may need to read the external subset, if
         * one was defined:
         */

        /* 19-Sep-2004, TSa: That does not need to be done, however, if
         *    there's a DTD override set.
         */
        if (mDTDOverridden) {
            // We have earlier override that's already parsed
        } else { // Nope, no override
            DTDSubset extSubset = null;

            /* 05-Mar-2006, TSa: If standalone was specified as "yes", we
             *   should not rely on any external declarations, so shouldn't
             *   we really just skip the external subset?
             */
            /* Alas: SAX (Xerces) still tries to read it... should we
             * do the Right Thing, or follow the leader? For now, let's
             * just follow the wrong example.
             */

            //if ((mDocStandalone != DOC_STANDALONE_YES) || SAX_COMPAT_MODE) {
            if (true) {
                if (mDtdPublicId != null || mDtdSystemId != null) {
                    extSubset =  findDtdExtSubset(mDtdPublicId, mDtdSystemId, intSubset);
                }
            }
            if (intSubset == null) {
                mDTD = extSubset;
            } else if (extSubset == null) {
                mDTD = intSubset;
            } else {
                mDTD = intSubset.combineWithExternalSubset(this, extSubset);
            }
        }

        if (mDTD == null) { // only if specifically overridden not to have any
            mGeneralEntities = null;
        } else {
            if (mDTD instanceof DTDSubset) {
                mGeneralEntities = ((DTDSubset) mDTD).getGeneralEntityMap();
            } else {
                /* Also, let's warn if using non-native DTD implementation,
                 * since entities and notations can not be accessed
                 */
                doReportProblem(mConfig.getXMLReporter(), ErrorConsts.WT_DT_DECL,
                                "Value to set for feature "+FEATURE_DTD_OVERRIDE+" not a native Woodstox DTD implementation (but "+mDTD.getClass()+"): can not access full entity or notation information", null);
            }
            /* 16-Jan-2006, TSa: Actually, we have both fully-validating mode,
             *   and non-validating-but-DTD-aware mode. In latter case, we'll
             *   still need to add a validator, but just to get type info
             *   and to add attribute default values if necessary.
             */
            mAutoDtdValidator = mDTD.createValidator(/*(ValidationContext)*/ mElementStack);
            mDtdValidatorSet = true; // so we won't get nags
            NsDefaultProvider nsDefs = null;
            if (mAutoDtdValidator instanceof DTDValidatorBase) {
                DTDValidatorBase dtdv = (DTDValidatorBase) mAutoDtdValidator;
                dtdv.setAttrValueNormalization(mCfgNormalizeAttrs);
                // Do we have any attribute defaults for 'xmlns' or 'xmlns:*'?
                if (dtdv.hasNsDefaults()) {
                    nsDefs = dtdv;
                }
            }
            mElementStack.setAutomaticDTDValidator(mAutoDtdValidator, nsDefs);
        }
    }

    /**
     * Method called right before handling the root element, by the base
     * class. This allows for some initialization and checks to be done
     * (not including ones that need access to actual element name)
     */
    protected void initValidation()
        throws XMLStreamException
    {
        if (hasConfigFlags(CFG_VALIDATE_AGAINST_DTD)
            && !mDtdValidatorSet) {
            /* It's ok to miss it, but it may not be what caller wants. Either
             * way, let's pass the info and continue
             */
            reportProblem(ErrorConsts.WT_DT_DECL, ErrorConsts.W_MISSING_DTD);
        }
    }

    /*
    ////////////////////////////////////////////////////
    // Private methods, external subset access
    ////////////////////////////////////////////////////
     */

    /**
     * Method called by <code>finishDTD</code>, to locate the specified
     * external DTD subset. Subset may be obtained from a cache, if cached
     * copy exists and is compatible; if not, it will be read from the
     * source identified by the public and/or system identifier passed.
     */
    private DTDSubset findDtdExtSubset(String pubId, String sysId,
                                       DTDSubset intSubset)
        throws IOException, XMLStreamException
    {
        boolean cache = hasConfigFlags(CFG_CACHE_DTDS);
        DTDId dtdId = constructDtdId(pubId, sysId);

        if (cache) {
            DTDSubset extSubset = findCachedSubset(dtdId, intSubset);
            if (extSubset != null) {
                return extSubset;
            }
        }

        // No useful cached copy? Need to read it then.
        /* For now, we do require system identifier; otherwise we don't
         * know how to resolve DTDs by public id. In future should
         * probably also have some simple catalog resolving facility?
         */
        if (sysId == null) {
            throwParseError("Can not resolve DTD with public id '"
                            +mDtdPublicId+"'; missing system identifier.");
        }
        WstxInputSource src = null;

        try {
            int xmlVersion = mDocXmlVersion;
            // 05-Feb-2006, TSa: If xmlVersion not explicitly known, defaults to 1.0
            if (xmlVersion == XmlConsts.XML_V_UNKNOWN) {
                xmlVersion = XmlConsts.XML_V_10;
            }
            /* null -> no explicit path context, use parent's
             * null -> not an entity expansion, no name.
             * Note, too, that we can NOT just pass mEntityResolver, since
             * that's the one used for general entities, whereas ext subset
             * should be resolved by the param entity resolver.
             */
            src = DefaultInputResolver.resolveEntity
                (mInput, null, null, pubId, sysId, mConfig.getDtdResolver(),
                 mConfig, xmlVersion);
        } catch (FileNotFoundException fex) {
            /* Let's catch and rethrow this just so we get more meaningful
             * description (with input source position etc)
             */
            throwParseError("(was "+fex.getClass().getName()+") "+fex.getMessage());
        }

        DTDSubset extSubset = FullDTDReader.readExternalSubset(src, mConfig, intSubset,
                                                               hasConfigFlags(CFG_VALIDATE_AGAINST_DTD),
                                                               mDocXmlVersion);
        
        if (cache) {
            /* Ok; can be cached, but only if it does NOT refer to
             * parameter entities defined in the internal subset (if
             * it does, there's no easy/efficient to check if it could
             * be used later on, plus it's unlikely it could be)
             */
            if (extSubset.isCachable()) {
                mOwner.addCachedDTD(dtdId, extSubset);
            }
        }

        return extSubset;
    }

    private DTDSubset findCachedSubset(DTDId id, DTDSubset intSubset)
        throws XMLStreamException
    {
        DTDSubset extSubset = mOwner.findCachedDTD(id);
        /* Ok, now; can use the cached copy iff it does not refer to
         * any parameter entities internal subset (if one exists)
         * defines:
         */
        if (extSubset != null) {
            if (intSubset == null || extSubset.isReusableWith(intSubset)) {
                return extSubset;
            }
        }
        return null;
    }

    /**
     * Method called to resolve path to external DTD subset, given
     * system identifier.
     */
    private URL resolveExtSubsetPath(String systemId)
        throws IOException
    {
        // Do we have a context to use for resolving?
        URL ctxt = (mInput == null) ? null : mInput.getSource();

        /* Ok, either got a context or not; let's create the URL based on
         * the id, and optional context:
         */
        if (ctxt == null) {
            /* Call will try to figure out if system id has the protocol
             * in it; if not, create a relative file, if it does, try to
             * resolve it.
             */
            return URLUtil.urlFromSystemId(systemId);
        }
        return URLUtil.urlFromSystemId(systemId, ctxt);
    }

    protected DTDId constructDtdId(String pubId, String sysId)
        throws IOException
    {
        /* Following settings will change what gets stored as DTD, so
         * they need to separate cached instances too:
         */
        int significantFlags = mConfigFlags &
            (CFG_NAMESPACE_AWARE
             | CFG_NORMALIZE_LFS | CFG_NORMALIZE_ATTR_VALUES
             /* Let's optimize non-validating case; DTD info we need
              * is less if so (no need to store content specs for one)...
              * plus, eventual functionality may be different too.
              */
             | CFG_VALIDATE_AGAINST_DTD
             /* Also, whether we support dtd++ or not may change construction
              * of settings... (currently does not, but could)
              */
             | CFG_SUPPORT_DTDPP
             /* Also, basic xml:id support does matter -- xml:id attribute
              * type is verified only if it's enabled
              */
             | CFG_XMLID_TYPING
             );
        URL sysRef = (sysId == null || sysId.length() == 0) ? null :
            resolveExtSubsetPath(sysId);

        /* 29-Mar-2006, TSa: Apparently public ids are not always very
         *   unique and/or can be mismatched with system ids, resulting
         *   in false matches if using public ids. As a result, by default
         *   Woodstox does NOT rely on public ids, when matching.
         */
        boolean usePublicId = (mConfigFlags & CFG_CACHE_DTDS_BY_PUBLIC_ID) != 0;
        if (usePublicId && pubId != null && pubId.length() > 0) {
            return DTDId.construct(pubId, sysRef, significantFlags, mXml11);
        }
        if (sysRef == null) {
            return null;
        }
        return DTDId.constructFromSystemId(sysRef, significantFlags, mXml11);
    }

    protected DTDId constructDtdId(URL sysId)
        throws IOException
    {
        int significantFlags = mConfigFlags &
            (CFG_NAMESPACE_AWARE
             | CFG_NORMALIZE_LFS | CFG_NORMALIZE_ATTR_VALUES
             /* Let's optimize non-validating case; DTD info we need
              * is less if so (no need to store content specs for one)
              */
             | CFG_VALIDATE_AGAINST_DTD
             /* Also, whether we support dtd++ or not may change construction
              * of settings... (currently does not, but could)
              */
             | CFG_SUPPORT_DTDPP
             );
        return DTDId.constructFromSystemId(sysId, significantFlags, mXml11);
    }

    /*
    ////////////////////////////////////////////////////
    // Private methods, DTD validation support
    ////////////////////////////////////////////////////
     */

    /**
     * Method called by lower-level parsing code when invalid content
     * (anything inside element with 'empty' content spec; text inside
     * non-mixed element etc) is found during basic scanning. Note
     * that actual DTD element structure problems are not reported
     * through this method.
     */
    protected void reportInvalidContent(int evtType)
        throws XMLStreamException
    {
        switch (mVldContent) {
        case XMLValidator.CONTENT_ALLOW_NONE:
            reportValidationProblem(ErrorConsts.ERR_VLD_EMPTY,
                                    mElementStack.getTopElementDesc(),
                                    ErrorConsts.tokenTypeDesc(evtType));
            break;
        case XMLValidator.CONTENT_ALLOW_WS:
            reportValidationProblem(ErrorConsts.ERR_VLD_NON_MIXED,
                                    mElementStack.getTopElementDesc());
            break;
        case XMLValidator.CONTENT_ALLOW_VALIDATABLE_TEXT:
        case XMLValidator.CONTENT_ALLOW_ANY_TEXT:
            /* Not 100% sure if this should ever happen... depends on
             * interpretation of 'any' content model?
             */
            reportValidationProblem(ErrorConsts.ERR_VLD_ANY,
                                    mElementStack.getTopElementDesc(),
                                    ErrorConsts.tokenTypeDesc(evtType));
            break;
        default: // should never occur:
            throwParseError("Internal error: trying to report invalid content for "+evtType);
        }
    }
}
