/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * 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 org.apache.xerces.parsers;

import java.io.File;
import java.io.FileInputStream;
import java.io.FilterInputStream;
import java.io.FilterReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.net.URL;
import java.util.Properties;

import org.apache.xerces.impl.Constants;
import org.apache.xerces.impl.XMLEntityDescription;
import org.apache.xerces.impl.XMLErrorReporter;
import org.apache.xerces.impl.dtd.XMLDTDProcessor;
import org.apache.xerces.impl.msg.XMLMessageFormatter;
import org.apache.xerces.util.SecurityManager;
import org.apache.xerces.util.SymbolTable;
import org.apache.xerces.xni.Augmentations;
import org.apache.xerces.xni.XMLDTDHandler;
import org.apache.xerces.xni.XMLLocator;
import org.apache.xerces.xni.XMLResourceIdentifier;
import org.apache.xerces.xni.XMLString;
import org.apache.xerces.xni.XNIException;
import org.apache.xerces.xni.grammars.XMLGrammarPool;
import org.apache.xerces.xni.parser.XMLComponentManager;
import org.apache.xerces.xni.parser.XMLConfigurationException;
import org.apache.xerces.xni.parser.XMLDTDFilter;
import org.apache.xerces.xni.parser.XMLDTDScanner;
import org.apache.xerces.xni.parser.XMLDTDSource;
import org.apache.xerces.xni.parser.XMLEntityResolver;
import org.apache.xerces.xni.parser.XMLInputSource;

/**
 * This configuration enhances Xerces support for the JAXP secure processing feature.
 * 
 * @author Michael Glavassevich, IBM
 * 
 * @version $Id: SecureProcessingConfiguration.java 1718452 2015-12-07 20:17:51Z mrglavas $
 */
public final class SecureProcessingConfiguration extends
        XIncludeAwareParserConfiguration {
    
    //
    // Constants
    //
    
    /** Property identifier: security manager. */
    private static final String SECURITY_MANAGER_PROPERTY =
            Constants.XERCES_PROPERTY_PREFIX + Constants.SECURITY_MANAGER_PROPERTY;
    
    /** Property identifier: entity resolver. */
    private static final String ENTITY_RESOLVER_PROPERTY = 
        Constants.XERCES_PROPERTY_PREFIX + Constants.ENTITY_RESOLVER_PROPERTY;
    
    /** Feature identifier: external general entities. */
    private static final String EXTERNAL_GENERAL_ENTITIES =
        Constants.SAX_FEATURE_PREFIX + Constants.EXTERNAL_GENERAL_ENTITIES_FEATURE;

    /** Feature identifier: external parameter entities. */
    private static final String EXTERNAL_PARAMETER_ENTITIES =
        Constants.SAX_FEATURE_PREFIX + Constants.EXTERNAL_PARAMETER_ENTITIES_FEATURE;
    
    /** Feature identifier: load external DTD. */
    private static final String LOAD_EXTERNAL_DTD =
        Constants.XERCES_FEATURE_PREFIX + Constants.LOAD_EXTERNAL_DTD_FEATURE;
    
    /** Set to true for debugging */
    private static final boolean DEBUG = isDebugEnabled();
    
    /** Cache the contents of the jaxp.properties file, if used. */
    private static Properties jaxpProperties = null;

    /** Cache the timestamp of the jaxp.properties file, if used. */
    private static long lastModified = -1;
    
    /** Xerces SecurityManager default value for entity expansion limit. **/
    private static final int SECURITY_MANAGER_DEFAULT_ENTITY_EXPANSION_LIMIT = 100000;
    
    /** Xerces SecurityManager default value of number of nodes created. **/
    private static final int SECURITY_MANAGER_DEFAULT_MAX_OCCUR_NODE_LIMIT = 3000;
    
    private static final String ENTITY_EXPANSION_LIMIT_PROPERTY_NAME = "jdk.xml.entityExpansionLimit";
    private static final String MAX_OCCUR_LIMIT_PROPERTY_NAME = "jdk.xml.maxOccur";
    private static final String TOTAL_ENTITY_SIZE_LIMIT_PROPERTY_NAME = "jdk.xml.totalEntitySizeLimit";
    private static final String MAX_GENERAL_ENTITY_SIZE_LIMIT_PROPERTY_NAME = "jdk.xml.maxGeneralEntitySizeLimit";
    private static final String MAX_PARAMETER_ENTITY_SIZE_LIMIT_PROPERTY_NAME = "jdk.xml.maxParameterEntitySizeLimit";
    private static final String RESOLVE_EXTERNAL_ENTITIES_PROPERTY_NAME = "jdk.xml.resolveExternalEntities";
    
    private static final int ENTITY_EXPANSION_LIMIT_DEFAULT_VALUE = 64000;
    private static final int MAX_OCCUR_LIMIT_DEFAULT_VALUE = 5000;
    private static final int TOTAL_ENTITY_SIZE_LIMIT_DEFAULT_VALUE = 50000000;
    private static final int MAX_GENERAL_ENTITY_SIZE_LIMIT_DEFAULT_VALUE = Integer.MAX_VALUE;
    private static final int MAX_PARAMETER_ENTITY_SIZE_LIMIT_DEFAULT_VALUE = Integer.MAX_VALUE;
    private static final boolean RESOLVE_EXTERNAL_ENTITIES_DEFAULT_VALUE = true;
    
    protected final int ENTITY_EXPANSION_LIMIT_SYSTEM_VALUE;
    protected final int MAX_OCCUR_LIMIT_SYSTEM_VALUE;
    protected final int TOTAL_ENTITY_SIZE_LIMIT_SYSTEM_VALUE;
    protected final int MAX_GENERAL_ENTITY_SIZE_LIMIT_SYSTEM_VALUE;
    protected final int MAX_PARAMETER_ENTITY_SIZE_LIMIT_SYSTEM_VALUE;
    protected final boolean RESOLVE_EXTERNAL_ENTITIES_SYSTEM_VALUE;
    
    //
    // Fields
    //
    
    private final boolean fJavaSecurityManagerEnabled;
    private boolean fLimitSpecified;
    private SecurityManager fSecurityManager;
    private InternalEntityMonitor fInternalEntityMonitor;
    private final ExternalEntityMonitor fExternalEntityMonitor;
    private int fTotalEntitySize = 0;
    
    /** Default constructor. */
    public SecureProcessingConfiguration() {
        this(null, null, null);
    } // <init>()
    
    /** 
     * Constructs a parser configuration using the specified symbol table. 
     *
     * @param symbolTable The symbol table to use.
     */
    public SecureProcessingConfiguration(SymbolTable symbolTable) {
        this(symbolTable, null, null);
    } // <init>(SymbolTable)
    
    /**
     * Constructs a parser configuration using the specified symbol table and
     * grammar pool.
     * <p>
     *
     * @param symbolTable The symbol table to use.
     * @param grammarPool The grammar pool to use.
     */
    public SecureProcessingConfiguration(
            SymbolTable symbolTable,
            XMLGrammarPool grammarPool) {
        this(symbolTable, grammarPool, null); 
    } // <init>(SymbolTable,XMLGrammarPool)
    
    /**
     * Constructs a parser configuration using the specified symbol table,
     * grammar pool, and parent settings.
     * <p>
     *
     * @param symbolTable    The symbol table to use.
     * @param grammarPool    The grammar pool to use.
     * @param parentSettings The parent settings.
     */
    public SecureProcessingConfiguration(
            SymbolTable symbolTable,
            XMLGrammarPool grammarPool,
            XMLComponentManager parentSettings) {
        super(symbolTable, grammarPool, parentSettings);
        fJavaSecurityManagerEnabled = (System.getSecurityManager() != null);
        ENTITY_EXPANSION_LIMIT_SYSTEM_VALUE = getPropertyValue(ENTITY_EXPANSION_LIMIT_PROPERTY_NAME, ENTITY_EXPANSION_LIMIT_DEFAULT_VALUE);
        MAX_OCCUR_LIMIT_SYSTEM_VALUE = getPropertyValue(MAX_OCCUR_LIMIT_PROPERTY_NAME, MAX_OCCUR_LIMIT_DEFAULT_VALUE);
        TOTAL_ENTITY_SIZE_LIMIT_SYSTEM_VALUE = getPropertyValue(TOTAL_ENTITY_SIZE_LIMIT_PROPERTY_NAME, TOTAL_ENTITY_SIZE_LIMIT_DEFAULT_VALUE);
        MAX_GENERAL_ENTITY_SIZE_LIMIT_SYSTEM_VALUE = getPropertyValue(MAX_GENERAL_ENTITY_SIZE_LIMIT_PROPERTY_NAME, MAX_GENERAL_ENTITY_SIZE_LIMIT_DEFAULT_VALUE);
        MAX_PARAMETER_ENTITY_SIZE_LIMIT_SYSTEM_VALUE = getPropertyValue(MAX_PARAMETER_ENTITY_SIZE_LIMIT_PROPERTY_NAME, MAX_PARAMETER_ENTITY_SIZE_LIMIT_DEFAULT_VALUE);
        RESOLVE_EXTERNAL_ENTITIES_SYSTEM_VALUE = getPropertyValue(RESOLVE_EXTERNAL_ENTITIES_PROPERTY_NAME, RESOLVE_EXTERNAL_ENTITIES_DEFAULT_VALUE);
        if (fJavaSecurityManagerEnabled || fLimitSpecified) {
            if (!RESOLVE_EXTERNAL_ENTITIES_SYSTEM_VALUE) {
                super.setFeature(EXTERNAL_GENERAL_ENTITIES, false);
                super.setFeature(EXTERNAL_PARAMETER_ENTITIES, false);
                super.setFeature(LOAD_EXTERNAL_DTD, false);
            }
            fSecurityManager = new org.apache.xerces.util.SecurityManager();
            fSecurityManager.setEntityExpansionLimit(ENTITY_EXPANSION_LIMIT_SYSTEM_VALUE);
            fSecurityManager.setMaxOccurNodeLimit(MAX_OCCUR_LIMIT_SYSTEM_VALUE);
            super.setProperty(SECURITY_MANAGER_PROPERTY, fSecurityManager);   
        }
        fExternalEntityMonitor = new ExternalEntityMonitor();
        super.setProperty(ENTITY_RESOLVER_PROPERTY, fExternalEntityMonitor);
    }
    
    protected void checkEntitySizeLimits(int sizeOfEntity, int delta, boolean isPE) {
        fTotalEntitySize += delta;
        if (fTotalEntitySize > TOTAL_ENTITY_SIZE_LIMIT_SYSTEM_VALUE) {
            fErrorReporter.reportError(XMLMessageFormatter.XML_DOMAIN,
                    "TotalEntitySizeLimitExceeded",
                    new Object[] {new Integer(TOTAL_ENTITY_SIZE_LIMIT_SYSTEM_VALUE)},
                    XMLErrorReporter.SEVERITY_FATAL_ERROR);
        }
        if (isPE) {
            if (sizeOfEntity > MAX_PARAMETER_ENTITY_SIZE_LIMIT_SYSTEM_VALUE) {
                fErrorReporter.reportError(XMLMessageFormatter.XML_DOMAIN,
                        "MaxParameterEntitySizeLimitExceeded",
                        new Object[] {new Integer(MAX_PARAMETER_ENTITY_SIZE_LIMIT_SYSTEM_VALUE)},
                        XMLErrorReporter.SEVERITY_FATAL_ERROR);
            }
        }
        else if (sizeOfEntity > MAX_GENERAL_ENTITY_SIZE_LIMIT_SYSTEM_VALUE) {
            fErrorReporter.reportError(XMLMessageFormatter.XML_DOMAIN,
                    "MaxGeneralEntitySizeLimitExceeded",
                    new Object[] {new Integer(MAX_GENERAL_ENTITY_SIZE_LIMIT_SYSTEM_VALUE)},
                    XMLErrorReporter.SEVERITY_FATAL_ERROR);
        }
    }
    
    /**
     * Returns the value of a property.
     * 
     * @param propertyId The property identifier.
     * @return the value of the property
     * 
     * @throws XMLConfigurationException Thrown for configuration error.
     *                                   In general, components should
     *                                   only throw this exception if
     *                                   it is <strong>really</strong>
     *                                   a critical error.
     */
    public Object getProperty(String propertyId)
        throws XMLConfigurationException {
        if (SECURITY_MANAGER_PROPERTY.equals(propertyId)) {
            return fSecurityManager;
        }
        else if (ENTITY_RESOLVER_PROPERTY.equals(propertyId)) {
            return fExternalEntityMonitor;
        }
        return super.getProperty(propertyId);
    }
    
    /**
     * setProperty
     * 
     * @param propertyId 
     * @param value 
     */
    public void setProperty(String propertyId, Object value)
        throws XMLConfigurationException {
        if (SECURITY_MANAGER_PROPERTY.equals(propertyId)) {
            // Do not allow the Xerces SecurityManager to be 
            // removed if the Java SecurityManager has been installed.
            if (value == null && fJavaSecurityManagerEnabled) {
                return;
            }
            fSecurityManager = (SecurityManager) value;
            if (fSecurityManager != null) {
                // Override SecurityManager default values with the system property / jaxp.properties / config default determined values.
                if (fSecurityManager.getEntityExpansionLimit() == SECURITY_MANAGER_DEFAULT_ENTITY_EXPANSION_LIMIT) {
                    fSecurityManager.setEntityExpansionLimit(ENTITY_EXPANSION_LIMIT_SYSTEM_VALUE);
                }
                if (fSecurityManager.getMaxOccurNodeLimit() == SECURITY_MANAGER_DEFAULT_MAX_OCCUR_NODE_LIMIT) {
                    fSecurityManager.setMaxOccurNodeLimit(MAX_OCCUR_LIMIT_SYSTEM_VALUE);
                }
            }  
        }
        else if (ENTITY_RESOLVER_PROPERTY.equals(propertyId)) {
            fExternalEntityMonitor.setEntityResolver((XMLEntityResolver) value);
            return;
        }
        super.setProperty(propertyId, value);
    }
    
    /** Configures the XML 1.0 pipeline. */
    protected void configurePipeline() {
        super.configurePipeline();
        configurePipelineCommon(true);
    }
    
    /** Configures the XML 1.1 pipeline. */
    protected void configureXML11Pipeline() {
        super.configureXML11Pipeline();
        configurePipelineCommon(false);
    }
    
    private void configurePipelineCommon(boolean isXML10) {
        if (fSecurityManager != null) {
            fTotalEntitySize = 0;
            if (fInternalEntityMonitor == null) {
                fInternalEntityMonitor = new InternalEntityMonitor();
            }
            // Reconfigure DTD pipeline. Insert internal entity decl monitor.
            final XMLDTDScanner dtdScanner;
            final XMLDTDProcessor dtdProcessor;
            if (isXML10) {
                dtdScanner = fDTDScanner;
                dtdProcessor = fDTDProcessor;
            }
            else {
                dtdScanner = fXML11DTDScanner;
                dtdProcessor = fXML11DTDProcessor;
            }
            dtdScanner.setDTDHandler(fInternalEntityMonitor);
            fInternalEntityMonitor.setDTDSource(dtdScanner);
            fInternalEntityMonitor.setDTDHandler(dtdProcessor);
            dtdProcessor.setDTDSource(fInternalEntityMonitor);
        }
    }
    
    private int getPropertyValue(String propertyName, int defaultValue) {
        
        // Step #1: Use the system property first
        try {
            String propertyValue = SecuritySupport.getSystemProperty(propertyName);
            if (propertyValue != null && propertyValue.length() > 0) {
                if (DEBUG) {
                    debugPrintln("found system property \"" + propertyName + "\", value=" + propertyValue);
                }
                final int intValue = Integer.parseInt(propertyValue);
                fLimitSpecified = true;
                if (intValue > 0) {
                    return intValue;
                }
                // Treat 0 and negative numbers as no limit (i.e. max integer).
                return Integer.MAX_VALUE;
            }
        }
        // The VM ran out of memory or there was some other serious problem. Re-throw.
        catch (VirtualMachineError vme) {
            throw vme;
        }
        // ThreadDeath should always be re-thrown
        catch (ThreadDeath td) {
            throw td;
        }
        catch (Throwable e) {
            // Ignore all other exceptions/errors and continue w/ next location
            if (DEBUG) {
                debugPrintln(e.getClass().getName() + ": " + e.getMessage());
                e.printStackTrace();
            }
        }
        
        // Step #2: Use $java.home/lib/jaxp.properties
        try {
            boolean fExists = false;
            File f = null;
            try {               
                String javah = SecuritySupport.getSystemProperty("java.home");
                String configFile = javah + File.separator +
                        "lib" + File.separator + "jaxp.properties";

                f = new File(configFile);
                fExists = SecuritySupport.getFileExists(f);

            }
            catch (SecurityException se) {
                // If there is a security exception, move on to next location.
                lastModified = -1;
                jaxpProperties = null;            
            }

            synchronized (SecureProcessingConfiguration.class) {    

                boolean runBlock = false;
                FileInputStream fis = null;

                try {
                    if (lastModified >= 0) {
                        // File has been modified, or didn't previously exist. 
                        // Need to reload properties    
                        if ((fExists) &&
                            (lastModified < (lastModified = SecuritySupport.getLastModified(f)))) {  
                            runBlock = true;
                        } 
                        else {
                            if (!fExists) {
                                // file existed, but it's been deleted.
                                lastModified = -1;
                                jaxpProperties = null;
                            }
                        }        
                    } 
                    else {
                        if (fExists) { 
                            // File didn't exist, but it does now.
                            runBlock = true;
                            lastModified = SecuritySupport.getLastModified(f);
                        }    
                    }

                    if (runBlock == true) {
                        // Try to read from $java.home/lib/jaxp.properties
                        jaxpProperties = new Properties();

                        fis = SecuritySupport.getFileInputStream(f);
                        jaxpProperties.load(fis);
                    }       

                }
                catch (Exception x) {
                    lastModified = -1;
                    jaxpProperties = null;
                    // assert(x instanceof FileNotFoundException
                    //        || x instanceof SecurityException)
                    // In both cases, ignore and return the default value
                }
                finally {
                    // try to close the input stream if one was opened.
                    if (fis != null) {
                        try {
                            fis.close();
                        }
                        // Ignore the exception.
                        catch (IOException exc) {}
                    }
                }
            }

            if (jaxpProperties != null) {            
                String propertyValue = jaxpProperties.getProperty(propertyName);
                if (propertyValue != null && propertyValue.length() > 0) {
                    if (DEBUG) {
                        debugPrintln("found \"" + propertyName + "\" in jaxp.properties, value=" + propertyValue);
                    }
                    final int intValue = Integer.parseInt(propertyValue);
                    fLimitSpecified = true;
                    if (intValue > 0) {
                        return intValue;
                    }
                    // Treat 0 and negative numbers as no limit (i.e. max integer).
                    return Integer.MAX_VALUE;
                }
            }
        }
        // The VM ran out of memory or there was some other serious problem. Re-throw.
        catch (VirtualMachineError vme) {
            throw vme;
        }
        // ThreadDeath should always be re-thrown
        catch (ThreadDeath td) {
            throw td;
        }
        catch (Throwable e) {
            // Ignore all other exceptions/errors and return the default value.
            if (DEBUG) {
                debugPrintln(e.getClass().getName() + ": " + e.getMessage());
                e.printStackTrace();
            }
        }
        
        // Step #3: Return the default value.
        return defaultValue;
    }
    
    private boolean getPropertyValue(String propertyName, boolean defaultValue) {
        
        // Step #1: Use the system property first
        try {
            String propertyValue = SecuritySupport.getSystemProperty(propertyName);
            if (propertyValue != null && propertyValue.length() > 0) {
                if (DEBUG) {
                    debugPrintln("found system property \"" + propertyName + "\", value=" + propertyValue);
                }
                final boolean booleanValue = Boolean.valueOf(propertyValue).booleanValue();
                fLimitSpecified = true;
                return booleanValue;
            }
        }
        // The VM ran out of memory or there was some other serious problem. Re-throw.
        catch (VirtualMachineError vme) {
            throw vme;
        }
        // ThreadDeath should always be re-thrown
        catch (ThreadDeath td) {
            throw td;
        }
        catch (Throwable e) {
            // Ignore all other exceptions/errors and continue w/ next location
            if (DEBUG) {
                debugPrintln(e.getClass().getName() + ": " + e.getMessage());
                e.printStackTrace();
            }
        }
        
        // Step #2: Use $java.home/lib/jaxp.properties
        try {
            boolean fExists = false;
            File f = null;
            try {               
                String javah = SecuritySupport.getSystemProperty("java.home");
                String configFile = javah + File.separator +
                        "lib" + File.separator + "jaxp.properties";

                f = new File(configFile);
                fExists = SecuritySupport.getFileExists(f);

            }
            catch (SecurityException se) {
                // If there is a security exception, move on to next location.
                lastModified = -1;
                jaxpProperties = null;            
            }

            synchronized (SecureProcessingConfiguration.class) {    

                boolean runBlock = false;
                FileInputStream fis = null;

                try {
                    if (lastModified >= 0) {
                        // File has been modified, or didn't previously exist. 
                        // Need to reload properties    
                        if ((fExists) &&
                            (lastModified < (lastModified = SecuritySupport.getLastModified(f)))) {  
                            runBlock = true;
                        } 
                        else {
                            if (!fExists) {
                                // file existed, but it's been deleted.
                                lastModified = -1;
                                jaxpProperties = null;
                            }
                        }        
                    } 
                    else {
                        if (fExists) { 
                            // File didn't exist, but it does now.
                            runBlock = true;
                            lastModified = SecuritySupport.getLastModified(f);
                        }    
                    }

                    if (runBlock == true) {
                        // Try to read from $java.home/lib/jaxp.properties
                        jaxpProperties = new Properties();

                        fis = SecuritySupport.getFileInputStream(f);
                        jaxpProperties.load(fis);
                    }       

                }
                catch (Exception x) {
                    lastModified = -1;
                    jaxpProperties = null;
                    // assert(x instanceof FileNotFoundException
                    //        || x instanceof SecurityException)
                    // In both cases, ignore and return the default value
                }
                finally {
                    // try to close the input stream if one was opened.
                    if (fis != null) {
                        try {
                            fis.close();
                        }
                        // Ignore the exception.
                        catch (IOException exc) {}
                    }
                }
            }

            if (jaxpProperties != null) {            
                String propertyValue = jaxpProperties.getProperty(propertyName);
                if (propertyValue != null && propertyValue.length() > 0) {
                    if (DEBUG) {
                        debugPrintln("found \"" + propertyName + "\" in jaxp.properties, value=" + propertyValue);
                    }
                    final boolean booleanValue = Boolean.valueOf(propertyValue).booleanValue();
                    fLimitSpecified = true;
                    return booleanValue;
                }
            }
        }
        // The VM ran out of memory or there was some other serious problem. Re-throw.
        catch (VirtualMachineError vme) {
            throw vme;
        }
        // ThreadDeath should always be re-thrown
        catch (ThreadDeath td) {
            throw td;
        }
        catch (Throwable e) {
            // Ignore all other exceptions/errors and return the default value.
            if (DEBUG) {
                debugPrintln(e.getClass().getName() + ": " + e.getMessage());
                e.printStackTrace();
            }
        }
        
        // Step #3: Return the default value.
        return defaultValue;
    }
    
    //
    // Private static methods
    //
    
    /** Returns true if debug has been enabled. */
    private static boolean isDebugEnabled() {
        try {
            String val = SecuritySupport.getSystemProperty("xerces.debug");
            // Allow simply setting the prop to turn on debug
            return (val != null && (!"false".equals(val)));
        } 
        catch (SecurityException se) {}
        return false;
    } // isDebugEnabled()

    /** Prints a message to standard error if debugging is enabled. */
    private static void debugPrintln(String msg) {
        if (DEBUG) {
            System.err.println("XERCES: " + msg);
        }
    } // debugPrintln(String)
    
    /**
     * XMLDTDFilter which checks limits imposed by the application 
     * on the sizes of general and parameter entities.
     */
    final class InternalEntityMonitor implements XMLDTDFilter {
        
        /** DTD source and handler. **/
        private XMLDTDSource fDTDSource;
        private XMLDTDHandler fDTDHandler;
        
        public InternalEntityMonitor() {}

        /*
         * XMLDTDHandler methods
         */

        public void startDTD(XMLLocator locator, Augmentations augmentations)
                throws XNIException {
            if (fDTDHandler != null) {
                fDTDHandler.startDTD(locator, augmentations);
            }
        }

        public void startParameterEntity(String name,
                XMLResourceIdentifier identifier, String encoding,
                Augmentations augmentations) throws XNIException {
            if (fDTDHandler != null) {
                fDTDHandler.startParameterEntity(name, identifier, encoding, augmentations);
            }
        }

        public void textDecl(String version, String encoding,
                Augmentations augmentations) throws XNIException {
            if (fDTDHandler != null) {
                fDTDHandler.textDecl(version, encoding, augmentations);
            }
        }

        public void endParameterEntity(String name, Augmentations augmentations)
                throws XNIException {
            if (fDTDHandler != null) {
                fDTDHandler.endParameterEntity(name, augmentations);
            }
        }

        public void startExternalSubset(XMLResourceIdentifier identifier,
                Augmentations augmentations) throws XNIException {
            if (fDTDHandler != null) {
                fDTDHandler.startExternalSubset(identifier, augmentations);
            }
        }

        public void endExternalSubset(Augmentations augmentations)
                throws XNIException {
            if (fDTDHandler != null) {
                fDTDHandler.endExternalSubset(augmentations);
            }
        }

        public void comment(XMLString text, Augmentations augmentations)
                throws XNIException {
            if (fDTDHandler != null) {
                fDTDHandler.comment(text, augmentations);
            }
        }

        public void processingInstruction(String target, XMLString data,
                Augmentations augmentations) throws XNIException {
            if (fDTDHandler != null) {
                fDTDHandler.processingInstruction(target, data, augmentations);
            }
        }

        public void elementDecl(String name, String contentModel,
                Augmentations augmentations) throws XNIException {
            if (fDTDHandler != null) {
                fDTDHandler.elementDecl(name, contentModel, augmentations);
            }
        }

        public void startAttlist(String elementName, Augmentations augmentations)
                throws XNIException {
            if (fDTDHandler != null) {
                fDTDHandler.startAttlist(elementName, augmentations);
            }
        }

        public void attributeDecl(String elementName, String attributeName,
                String type, String[] enumeration, String defaultType,
                XMLString defaultValue, XMLString nonNormalizedDefaultValue,
                Augmentations augmentations) throws XNIException {
            if (fDTDHandler != null) {
                fDTDHandler.attributeDecl(elementName, attributeName,
                        type, enumeration, defaultType,
                        defaultValue, nonNormalizedDefaultValue,
                        augmentations);
            }
        }

        public void endAttlist(Augmentations augmentations) throws XNIException {
            if (fDTDHandler != null) {
                fDTDHandler.endAttlist(augmentations);
            }
        }

        public void internalEntityDecl(String name, XMLString text,
                XMLString nonNormalizedText, Augmentations augmentations)
                throws XNIException {
            checkEntitySizeLimits(text.length, text.length, name != null && name.startsWith("%"));
            if (fDTDHandler != null) {
                fDTDHandler.internalEntityDecl(name, text,
                        nonNormalizedText, augmentations);
            }
        }

        public void externalEntityDecl(String name,
                XMLResourceIdentifier identifier, Augmentations augmentations)
                throws XNIException {
            if (fDTDHandler != null) {
                fDTDHandler.externalEntityDecl(name, identifier, augmentations);
            }
        }

        public void unparsedEntityDecl(String name,
                XMLResourceIdentifier identifier, String notation,
                Augmentations augmentations) throws XNIException {
            if (fDTDHandler != null) {
                fDTDHandler.unparsedEntityDecl(name, identifier, notation, augmentations);
            }
        }

        public void notationDecl(String name, XMLResourceIdentifier identifier,
                Augmentations augmentations) throws XNIException {
            if (fDTDHandler != null) {
                fDTDHandler.notationDecl(name, identifier, augmentations);
            }
        }

        public void startConditional(short type, Augmentations augmentations)
                throws XNIException {
            if (fDTDHandler != null) {
                fDTDHandler.startConditional(type, augmentations);
            }
        }

        public void ignoredCharacters(XMLString text, Augmentations augmentations)
                throws XNIException {
            if (fDTDHandler != null) {
                fDTDHandler.ignoredCharacters(text, augmentations);
            }

        }

        public void endConditional(Augmentations augmentations) throws XNIException {
            if (fDTDHandler != null) {
                fDTDHandler.endConditional(augmentations);
            }
        }

        public void endDTD(Augmentations augmentations) throws XNIException {
            if (fDTDHandler != null) {
                fDTDHandler.endDTD(augmentations);
            }
        }

        public void setDTDSource(XMLDTDSource source) {
            fDTDSource = source;
        }

        public XMLDTDSource getDTDSource() {
            return fDTDSource;
        }
        
        /*
         * XMLDTDSource methods
         */

        public void setDTDHandler(XMLDTDHandler handler) {
            fDTDHandler = handler;
        }

        public XMLDTDHandler getDTDHandler() {
            return fDTDHandler;
        }
    }
    
    /**
     * XMLEntityResolver which checks limits imposed by the application 
     * on the sizes of general and parameter entities.
     */
    final class ExternalEntityMonitor implements XMLEntityResolver {
        
        /**
         * java.io.InputStream wrapper which check entity size limits.
         */
        final class InputStreamMonitor extends FilterInputStream {
            
            private final boolean isPE;
            private int size = 0;

            protected InputStreamMonitor(InputStream in, boolean isPE) {
                super(in);
                this.isPE = isPE;
            }
            
            public int read() throws IOException {
                int i = super.read();
                if (i != -1) {
                    ++size;
                    checkEntitySizeLimits(size, 1, isPE);
                }
                return i;
            }
            
            public int read(byte[] b, int off, int len) throws IOException {
                int i = super.read(b, off, len);
                if (i > 0) {
                    size += i;
                    checkEntitySizeLimits(size, i, isPE);
                }
                return i;
            }
        }
        
        /**
         * java.io.Reader wrapper which check entity size limits.
         */
        final class ReaderMonitor extends FilterReader {
            
            private final boolean isPE;
            private int size = 0;

            protected ReaderMonitor(Reader in, boolean isPE) {
                super(in);
                this.isPE = isPE;
            }
            
            public int read() throws IOException {
                int i = super.read();
                if (i != -1) {
                    ++size;
                    checkEntitySizeLimits(size, 1, isPE);
                }
                return i;
            }
            
            public int read(char[] cbuf, int off, int len) throws IOException {
                int i = super.read(cbuf, off, len);
                if (i > 0) {
                    size += i;
                    checkEntitySizeLimits(size, i, isPE);
                }
                return i;
            }
        }
        
        private XMLEntityResolver fEntityResolver;

        public XMLInputSource resolveEntity(XMLResourceIdentifier resourceIdentifier) throws XNIException,
                IOException {
            XMLInputSource source = null;
            if (fEntityResolver != null) {
                source = fEntityResolver.resolveEntity(resourceIdentifier);
            }
            if (fSecurityManager != null && resourceIdentifier instanceof XMLEntityDescription) {
                String name = ((XMLEntityDescription) resourceIdentifier).getEntityName();
                boolean isPE = name != null && name.startsWith("%");
                if (source == null) {
                    String publicId = resourceIdentifier.getPublicId();
                    String systemId = resourceIdentifier.getExpandedSystemId();
                    String baseSystemId = resourceIdentifier.getBaseSystemId();
                    source = new XMLInputSource(publicId, systemId, baseSystemId);
                }
                Reader reader = source.getCharacterStream();
                if (reader != null) {
                    source.setCharacterStream(new ReaderMonitor(reader, isPE));
                }
                else {
                    InputStream stream = source.getByteStream();
                    if (stream != null) {
                        source.setByteStream(new InputStreamMonitor(stream, isPE));
                    }
                    else {
                        String systemId = resourceIdentifier.getExpandedSystemId();
                        URL url = new URL(systemId);
                        stream = url.openStream();
                        source.setByteStream(new InputStreamMonitor(stream, isPE));
                    }
                }
            }
            return source;
        }
        
        /** Sets the XNI entity resolver. */
        public void setEntityResolver(XMLEntityResolver entityResolver) {
            fEntityResolver = entityResolver;
        } // setEntityResolver(XMLEntityResolver)

        /** Returns the XNI entity resolver. */
        public XMLEntityResolver getEntityResolver() {
            return fEntityResolver;
        } // getEntityResolver():XMLEntityResolver
    }  
}
