/*
 * Copyright (c) 2002-2010 Gargoyle Software Inc.
 *
 * Licensed 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 com.gargoylesoftware.htmlunit.javascript.configuration;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.WeakHashMap;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.xml.sax.InputSource;
import org.xml.sax.SAXParseException;

import com.gargoylesoftware.htmlunit.BrowserVersion;
import com.gargoylesoftware.htmlunit.html.HtmlAbbreviated;
import com.gargoylesoftware.htmlunit.html.HtmlAcronym;
import com.gargoylesoftware.htmlunit.html.HtmlAddress;
import com.gargoylesoftware.htmlunit.html.HtmlBackgroundSound;
import com.gargoylesoftware.htmlunit.html.HtmlBidirectionalOverride;
import com.gargoylesoftware.htmlunit.html.HtmlBig;
import com.gargoylesoftware.htmlunit.html.HtmlBlink;
import com.gargoylesoftware.htmlunit.html.HtmlBlockQuote;
import com.gargoylesoftware.htmlunit.html.HtmlBold;
import com.gargoylesoftware.htmlunit.html.HtmlCenter;
import com.gargoylesoftware.htmlunit.html.HtmlCitation;
import com.gargoylesoftware.htmlunit.html.HtmlCode;
import com.gargoylesoftware.htmlunit.html.HtmlDefinition;
import com.gargoylesoftware.htmlunit.html.HtmlDefinitionDescription;
import com.gargoylesoftware.htmlunit.html.HtmlDefinitionTerm;
import com.gargoylesoftware.htmlunit.html.HtmlDivision;
import com.gargoylesoftware.htmlunit.html.HtmlElement;
import com.gargoylesoftware.htmlunit.html.HtmlEmphasis;
import com.gargoylesoftware.htmlunit.html.HtmlExample;
import com.gargoylesoftware.htmlunit.html.HtmlHeading1;
import com.gargoylesoftware.htmlunit.html.HtmlHeading2;
import com.gargoylesoftware.htmlunit.html.HtmlHeading3;
import com.gargoylesoftware.htmlunit.html.HtmlHeading4;
import com.gargoylesoftware.htmlunit.html.HtmlHeading5;
import com.gargoylesoftware.htmlunit.html.HtmlHeading6;
import com.gargoylesoftware.htmlunit.html.HtmlInlineQuotation;
import com.gargoylesoftware.htmlunit.html.HtmlItalic;
import com.gargoylesoftware.htmlunit.html.HtmlKeyboard;
import com.gargoylesoftware.htmlunit.html.HtmlListing;
import com.gargoylesoftware.htmlunit.html.HtmlMarquee;
import com.gargoylesoftware.htmlunit.html.HtmlMultiColumn;
import com.gargoylesoftware.htmlunit.html.HtmlNoBreak;
import com.gargoylesoftware.htmlunit.html.HtmlNoEmbed;
import com.gargoylesoftware.htmlunit.html.HtmlNoFrames;
import com.gargoylesoftware.htmlunit.html.HtmlNoScript;
import com.gargoylesoftware.htmlunit.html.HtmlPlainText;
import com.gargoylesoftware.htmlunit.html.HtmlS;
import com.gargoylesoftware.htmlunit.html.HtmlSample;
import com.gargoylesoftware.htmlunit.html.HtmlSmall;
import com.gargoylesoftware.htmlunit.html.HtmlSpan;
import com.gargoylesoftware.htmlunit.html.HtmlStrike;
import com.gargoylesoftware.htmlunit.html.HtmlStrong;
import com.gargoylesoftware.htmlunit.html.HtmlSubscript;
import com.gargoylesoftware.htmlunit.html.HtmlSuperscript;
import com.gargoylesoftware.htmlunit.html.HtmlTableBody;
import com.gargoylesoftware.htmlunit.html.HtmlTableColumn;
import com.gargoylesoftware.htmlunit.html.HtmlTableColumnGroup;
import com.gargoylesoftware.htmlunit.html.HtmlTableFooter;
import com.gargoylesoftware.htmlunit.html.HtmlTableHeader;
import com.gargoylesoftware.htmlunit.html.HtmlTeletype;
import com.gargoylesoftware.htmlunit.html.HtmlUnderlined;
import com.gargoylesoftware.htmlunit.html.HtmlVariable;
import com.gargoylesoftware.htmlunit.javascript.SimpleScriptable;
import com.gargoylesoftware.htmlunit.javascript.StrictErrorHandler;
import com.gargoylesoftware.htmlunit.javascript.host.html.HTMLDivElement;
import com.gargoylesoftware.htmlunit.javascript.host.html.HTMLHeadingElement;
import com.gargoylesoftware.htmlunit.javascript.host.html.HTMLQuoteElement;
import com.gargoylesoftware.htmlunit.javascript.host.html.HTMLSpanElement;
import com.gargoylesoftware.htmlunit.javascript.host.html.HTMLTableColElement;
import com.gargoylesoftware.htmlunit.javascript.host.html.HTMLTableSectionElement;

/**
 * A container for all the JavaScript configuration information.
 * TODO - Need to add the logic to support the browser and JavaScript conditionals in the Class elements.
 *
 * @version $Revision: 5754 $
 * @author <a href="mailto:mbowler@GargoyleSoftware.com">Mike Bowler</a>
 * @author Chris Erskine
 * @author Ahmed Ashour
 */
public final class JavaScriptConfiguration {

    private static final Log LOG = LogFactory.getLog(JavaScriptConfiguration.class);

    /** The JavaScript configuration XML document. */
    private static Document XmlDocument_;

    /** Constant indicating that this function/property is used by the specified browser version. */
    public static final int ENABLED   = 1;

    /** Constant indicating that this function/property is not used by the specified browser version. */
    public static final int DISABLED  = 2;

    /** Constant indicating that this function/property is not defined in the configuration file. */
    public static final int NOT_FOUND = 3;

    /** Cache of browser versions and their corresponding JavaScript configurations. */
    private static Map<BrowserVersion, JavaScriptConfiguration> ConfigurationMap_ =
        new WeakHashMap<BrowserVersion, JavaScriptConfiguration>(11);

    private static Map<String, String> ClassnameMap_ = new HashMap<String, String>();

    private static Map<Class < ? extends HtmlElement>, Class < ? extends SimpleScriptable>> HtmlJavaScriptMap_;

    private final Map<String, ClassConfiguration> configuration_;

    /**
     * Constructor is only called from {@link #getInstance(BrowserVersion)} which is synchronized.
     * @param browser the browser version to use
     */
    private JavaScriptConfiguration(final BrowserVersion browser) {
        if (XmlDocument_ == null) {
            loadConfiguration();
        }
        if (XmlDocument_ == null) {
            throw new IllegalStateException("Configuration was not initialized - see log for details");
        }
        configuration_ = buildUsageMap(browser);
    }

    /**
     * Test for a configuration having been loaded for testing.
     *
     * @return boolean - true if the XmlDocument has been loaded;
     */
    protected static boolean isDocumentLoaded() {
        return XmlDocument_ != null;
    }

    /**
     * Resets this class to its initial state. This method is used for testing only.
     */
    protected static void resetClassForTesting() {
        XmlDocument_ = null;
        ConfigurationMap_ = new WeakHashMap<BrowserVersion, JavaScriptConfiguration>(11);
    }

    /**
     * Sets the document configuration for testing.
     * @param document - The configuration document
     */
    protected static void setXmlDocument(final Document document) {
        XmlDocument_ = document;
    }

    /**
     * Gets the configuration file and make it an input reader and then pass to the method to read the file.
     */
    protected static void loadConfiguration() {
        try {
            final Reader reader = getConfigurationFileAsReader();
            if (reader == null) {
                LOG.error("Unable to load JavaScriptConfiguration.xml");
            }
            else {
                loadConfiguration(reader);
                reader.close();
            }
        }
        catch (final Exception e) {
            LOG.error("Error when loading JavascriptConfiguration.xml", e);
            e.printStackTrace();
        }
    }

    /**
     * Loads the configuration from a supplied Reader.
     *
     * @param configurationReader - A reader pointing to the configuration
     */
    protected static void loadConfiguration(final Reader configurationReader) {
        final InputSource inputSource = new InputSource(configurationReader);

        try {
            final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
            factory.setNamespaceAware(true);
            factory.setValidating(false);

            final DocumentBuilder documentBuilder = factory.newDocumentBuilder();
            documentBuilder.setErrorHandler(new StrictErrorHandler());

            XmlDocument_ = documentBuilder.parse(inputSource);
        }
        catch (final SAXParseException parseException) {
            LOG.error("line=[" + parseException.getLineNumber()
                + "] columnNumber=[" + parseException.getColumnNumber()
                + "] systemId=[" + parseException.getSystemId()
                + "] publicId=[" + parseException.getPublicId() + "]", parseException);
        }
        catch (final Exception e) {
            LOG.error("Error when loading JavascriptConfiguration.xml", e);
        }
    }

    /**
     * Returns the instance that represents the configuration for the specified {@link BrowserVersion}.
     * This method is synchronized to allow multithreaded access to the JavaScript configuration.
     * @param browserVersion the {@link BrowserVersion}
     * @return the instance for the specified {@link BrowserVersion}
     */
    public static synchronized JavaScriptConfiguration getInstance(final BrowserVersion browserVersion) {
        if (browserVersion == null) {
            throw new IllegalStateException("BrowserVersion must be defined");
        }
        JavaScriptConfiguration configuration = ConfigurationMap_.get(browserVersion);

        if (configuration == null) {
            configuration = new JavaScriptConfiguration(browserVersion);
            ConfigurationMap_.put(browserVersion, configuration);
        }
        return configuration;
    }

    /**
     * Returns the configuration that has all entries. No constraints are put on the returned entries.
     *
     * @return the instance containing all entries from the configuration file
     */
    static JavaScriptConfiguration getAllEntries() {
        final JavaScriptConfiguration configuration = new JavaScriptConfiguration(null);
        return configuration;
    }

    private static Reader getConfigurationFileAsReader() {
        final Class< ? > clazz = JavaScriptConfiguration.class;
        final String name = clazz.getPackage().getName().replace('.', '/') + '/' + "JavaScriptConfiguration.xml";
        InputStream inputStream = clazz.getClassLoader().getResourceAsStream(name);
        if (inputStream == null) {
            try {
                final String localizedName = name.replace('/', File.separatorChar);
                inputStream = new FileInputStream(localizedName);
            }
            catch (final IOException e) {
                // Fall through
            }
        }

        // If we are running maven tests then the path will be slightly different
        if (inputStream == null) {
            try {
                final String localizedName = ("./src/java" + name).replace('/', File.separatorChar);
                inputStream = new FileInputStream(localizedName);
            }
            catch (final IOException e) {
                // Fall through
            }
        }
        return new InputStreamReader(inputStream);
    }

    /**
     * Gets the set of keys for the class configurations.
     * @return the set of keys for the class configurations
     */
    public Set<String> keySet() {
        return configuration_.keySet();
    }

    private Map<String, ClassConfiguration> buildUsageMap(final BrowserVersion browser) {
        final Map<String, ClassConfiguration> classMap = new HashMap<String, ClassConfiguration>(30);
        Node node = XmlDocument_.getDocumentElement().getFirstChild();
        while (node != null) {
            if (node instanceof Element) {
                final Element element = (Element) node;
                if (element.getTagName().equals("class")) {
                    if (!testToExcludeElement(element, browser)) {
                        try {
                            final ClassConfiguration config = parseClassElement(element, browser);
                            if (config != null) {
                                classMap.put(config.getHostClass().getSimpleName(), config);
                            }
                        }
                        catch (final ClassNotFoundException e) {
                            throw new IllegalStateException("The class was not found for '"
                                    + element.getAttribute("classname") + "'");
                        }
                    }
                }
            }
            node = node.getNextSibling();
        }
        return Collections.unmodifiableMap(classMap);
    }

    /**
     * Parses the class element to build the class configuration.
     * @param className the name of the class element
     * @param element the element to parse
     * @param browser the browser version under consideration
     * @return the class element to build the class configuration
     * @throws ClassNotFoundException if the specified class could not be found
     */
    private ClassConfiguration parseClassElement(final Element element,
        final BrowserVersion browser) throws ClassNotFoundException {
        final String notImplemented = element.getAttribute("notImplemented");
        if ("true".equalsIgnoreCase(notImplemented)) {
            return null;
        }
        final String hostClassName = element.getAttribute("classname");
        final String jsConstructor = element.getAttribute("jsConstructor");
        final String extendsClassName = element.getAttribute("extends");
        final String htmlClassName = element.getAttribute("htmlClass");
        boolean jsObjectFlag = false;
        final String jsObjectStr = element.getAttribute("JSObject");
        if ("true".equalsIgnoreCase(jsObjectStr)) {
            jsObjectFlag = true;
        }
        final ClassConfiguration classConfiguration = new ClassConfiguration(hostClassName, jsConstructor,
                    extendsClassName, htmlClassName, jsObjectFlag);
        final String simpleClassName = hostClassName.substring(hostClassName.lastIndexOf('.') + 1);
        ClassnameMap_.put(hostClassName, simpleClassName);
        Node node = element.getFirstChild();
        while (node != null) {
            if (node instanceof Element) {
                final Element childElement = (Element) node;
                final String tagName = childElement.getTagName();
                if (tagName.equals("property")) {
                    parsePropertyElement(classConfiguration, childElement, browser);
                }
                else if (tagName.equals("function")) {
                    parseFunctionElement(classConfiguration, childElement, browser);
                }
                else if (tagName.equals("constant")) {
                    parseConstantElement(classConfiguration, childElement, browser);
                }
                else if (tagName.equals("browser")) {
                    if (LOG.isDebugEnabled()) {
                        LOG.debug("browser tag not yet handled for class " + hostClassName);
                    }
                }
                else if (tagName.equals("doclink")) {
                    // ignore this link
                }
                else {
                    throw new IllegalStateException("Do not understand element type '"
                        + tagName + "' in '" + hostClassName + "'");
                }
            }
            node = node.getNextSibling();
        }
        return classConfiguration;
    }

    /**
     * Parse out the values for the property.
     *
     * @param classConfiguration the configuration that is being built
     * @param element the property element
     * @param browser the browser version under consideration
     */
    private void parsePropertyElement(final ClassConfiguration classConfiguration, final Element element,
        final BrowserVersion browser) {
        final String notImplemented = element.getAttribute("notImplemented");
        if ("true".equalsIgnoreCase(notImplemented)) {
            return;
        }
        if (testToExcludeElement(element, browser)) {
            return;
        }
        final String propertyName = element.getAttribute("name");
        boolean readable = false;
        boolean writable = false;
        final String readFlag = element.getAttribute("readable");
        if ("true".equalsIgnoreCase(readFlag)) {
            readable = true;
        }
        final String writeFlag = element.getAttribute("writable");
        if ("true".equalsIgnoreCase(writeFlag)) {
            writable = true;
        }
        classConfiguration.addProperty(propertyName, readable, writable);
    }

    /**
     * Parses out the values from the function element.
     *
     * @param classConfiguration the configuration that is being built
     * @param element the function element
     * @param browser the browser version under consideration
     */
    private void parseFunctionElement(final ClassConfiguration classConfiguration, final Element element,
        final BrowserVersion browser) {
        final String notImplemented = element.getAttribute("notImplemented");
        if ("true".equalsIgnoreCase(notImplemented)) {
            return;
        }
        final String propertyName = element.getAttribute("name");
        if (testToExcludeElement(element, browser)) {
            return;
        }
        classConfiguration.addFunction(propertyName);
    }

    /**
     * Parses out the values for the constant.
     *
     * @param classConfiguration the configuration that is being built
     * @param element the property element
     * @param browser the browser version under consideration
     */
    private void parseConstantElement(final ClassConfiguration classConfiguration, final Element element,
        final BrowserVersion browser) {
        if (testToExcludeElement(element, browser)) {
            return;
        }
        final String constantName = element.getAttribute("name");
        classConfiguration.addConstant(constantName);
    }

    /**
     * Test for the browser and JavaScript constraints. Returns true if any constraints are present
     * and the browser does not meet the constraints.
     * @param element the element to scan the children of
     * @param browser the browser version under consideration
     * @return true to exclude this element
     */
    private boolean testToExcludeElement(final Element element, final BrowserVersion browser) {
        if (browser == null) {
            return false;
        }
        Node node = element.getFirstChild();
        boolean browserConstraint = false;
        boolean allowBrowser = false;
        while (node != null) {
            if (node instanceof Element) {
                final Element childElement = (Element) node;
                if (childElement.getTagName().equals("browser")) {
                    browserConstraint = true;
                    if (testToIncludeForBrowserConstraint(childElement, browser)) {
                        allowBrowser = true;
                    }
                }
            }
            node = node.getNextSibling();
        }
        if (browserConstraint && !allowBrowser) {
            return true;
        }
        return false;
    }

    /**
     * Test to see if the supplied configuration matches for the parsed configuration for the named class
     * This is a method for testing.
     *
     * @param classname - the parsed classname to test
     * @param config - the expected configuration
     * @return true if they match
     */
    protected boolean classConfigEquals(final String classname, final ClassConfiguration config) {
        final ClassConfiguration myConfig = configuration_.get(classname);
        return config.equals(myConfig);
    }

    /**
     * Gets the class configuration for the supplied JavaScript class name.
     * @param classname the js class name
     * @return the class configuration for the supplied JavaScript class name
     */
    public ClassConfiguration getClassConfiguration(final String classname) {
        return configuration_.get(classname);
    }

    private boolean testToIncludeForBrowserConstraint(final Element element, final BrowserVersion browser) {
        if ((!browser.isIE() || !"Internet Explorer".equals(element.getAttribute("name")))
            && (!browser.isFirefox() || !"Firefox".equals(element.getAttribute("name")))) {
            return false;
        }
        final String max = element.getAttribute("max-version");
        float maxVersion;
        if (max.length() == 0) {
            maxVersion = 0;
        }
        else {
            maxVersion = Float.parseFloat(max);
        }
        if (maxVersion > 0 && browser.getBrowserVersionNumeric() > maxVersion) {
            return false;
        }

        float minVersion;
        final String min = element.getAttribute("min-version");
        if (min.length() == 0) {
            minVersion = 0;
        }
        else {
            minVersion = Float.parseFloat(min);
        }
        if (minVersion > 0 && browser.getBrowserVersionNumeric() < minVersion) {
            return false;
        }
        return true;
    }

    /**
     * Returns the class for the given class name.
     * @param classname the classname that you want the implementing class for  (for testing only)
     * @return the class for the given class name
     */
    protected Class< ? > getClassObject(final String classname) {
        final ClassConfiguration config = configuration_.get(classname);
        return config.getHostClass();
    }

    /**
     * Gets the method that implements the getter for the given property based upon the class object.
     * @param clazz the actual class to use as reference
     * @param propertyName the property to find the getter for
     * @return the method that implements the getter for the given property based upon the class object
     */
    public Method getPropertyReadMethod(final Class< ? > clazz, final String propertyName) {
        final String classname = getClassnameForClass(clazz);
        return getPropertyReadMethod(classname, propertyName);
    }

    /**
     * Returns the method that implements the get function for in the class for the given class.
     *
     * @param classname the name of the class to work with
     * @param propertyName the property to find the getter for
     * @return the method that implements the get function for in the class for the given class
     */
    public Method getPropertyReadMethod(String classname, final String propertyName) {
        ClassConfiguration config;
        Method theMethod;
        while (classname.length() > 0) {
            config = configuration_.get(classname);
            if (config == null) {
                return null;
            }
            theMethod = config.getPropertyReadMethod(propertyName);
            if (theMethod != null) {
                return theMethod;
            }
            classname = config.getExtendedClassName();
        }
        return null;
    }

    private ClassConfiguration.PropertyInfo findPropertyInChain(final String classname, final String propertyName) {
        String workname = classname;
        ClassConfiguration config;
        while (workname.length() > 0) {
            config = configuration_.get(workname);
            final ClassConfiguration.PropertyInfo info = config.getPropertyInfo(propertyName);
            if (info != null) {
                return info;
            }
            workname = config.getExtendedClassName();
        }
        return null;
    }

    /**
     * Gets the method that implements the setter for the given property based upon the class object.
     * @param clazz the actual class to use as reference
     * @param propertyName the property to find the getter for
     * @return the method that implements the setter for the given property based upon the class object
     */
    public Method getPropertyWriteMethod(final Class< ? > clazz, final String propertyName) {
        final String classname = getClassnameForClass(clazz);
        return getPropertyWriteMethod(classname, propertyName);
    }

    /**
     * Returns the method that implements the set function in the class for the given class.
     *
     * @param classname the name of the class to work with
     * @param propertyName the property to find the setter for
     * @return the method that implements the set function in the class for the given class
     */
    public Method getPropertyWriteMethod(String classname, final String propertyName) {
        ClassConfiguration config;
        Method theMethod;
        while (classname.length() > 0) {
            config = configuration_.get(classname);
            theMethod = config.getPropertyWriteMethod(propertyName);
            if (theMethod != null) {
                return theMethod;
            }
            classname = config.getExtendedClassName();
        }
        return null;
    }

    /**
     * Gets the method that implements the setter for the given property based upon the class object.
     *
     * @param clazz the actual class to use as reference
     * @param functionName the function to find the method for
     * @return the method that implements the setter for the given property based upon the class object
     */
    public Method getFunctionMethod(final Class< ? > clazz, final String functionName) {
        final String classname = getClassnameForClass(clazz);
        return getFunctionMethod(classname, functionName);
    }

    /**
     * Returns the method that implements the given function in the class for the given class.
     *
     * @param classname the name of the class to work with
     * @param functionName the function to find the method for
     * @return the method that implements the given function in the class for the given class
     */
    public Method getFunctionMethod(String classname, final String functionName) {
        ClassConfiguration config;
        Method theMethod;
        while (classname.length() > 0) {
            config = configuration_.get(classname);
            theMethod = config.getFunctionMethod(functionName);
            if (theMethod != null) {
                return theMethod;
            }
            classname = config.getExtendedClassName();
        }
        return null;
    }

    /**
     * Checks to see if there is an entry for the given property.
     *
     * @param clazz the class the property is for
     * @param propertyName the name of the property
     * @return boolean <tt>true</tt> if the property exists
     */
    public boolean propertyExists(final Class< ? > clazz, final String propertyName) {
        final String classname = getClassnameForClass(clazz);
        return propertyExists(classname, propertyName);
    }

    /**
     * Checks to see if there is an entry for the given property.
     *
     * @param classname the class the property is for
     * @param propertyName the name of the property
     * @return boolean <tt>true</tt> if the property exists
     */
    public boolean propertyExists(final String classname, final String propertyName) {
        final ClassConfiguration.PropertyInfo info = findPropertyInChain(classname, propertyName);
        if (info == null) {
            return false;
        }
        return true;
    }

    /**
     * Returns the classname that the given class implements. If the class is
     * the input class, then the name is extracted from the type that the Input class
     * is masquerading as.
     * FIXME - Implement the Input class processing
     * @param clazz
     * @return the classname
     */
    private String getClassnameForClass(final Class< ? > clazz) {
        final String name = ClassnameMap_.get(clazz.getName());
        if (name == null) {
            throw new IllegalStateException("Did not find the mapping of the class to the classname for "
                + clazz.getName());
        }
        return name;
    }

    /**
     * Returns an immutable map containing the HTML to JavaScript mappings. Keys are
     * java classes for the various HTML classes (ie HtmlInput.class) and the values
     * are the JavaScript class names (ie "HTMLAnchorElement").
     * @return the mappings
     */
    @SuppressWarnings("unchecked")
    public static synchronized Map<Class < ? extends HtmlElement>, Class < ? extends SimpleScriptable>>
    getHtmlJavaScriptMapping() {
        if (HtmlJavaScriptMap_ != null) {
            return HtmlJavaScriptMap_;
        }
        final JavaScriptConfiguration configuration = JavaScriptConfiguration.getAllEntries();

        final Map<Class < ? extends HtmlElement>, Class < ? extends SimpleScriptable>> map =
            new HashMap<Class < ? extends HtmlElement>, Class < ? extends SimpleScriptable>>();

        for (String jsClassname : configuration.keySet()) {
            ClassConfiguration classConfig = configuration.getClassConfiguration(jsClassname);
            final String htmlClassname = classConfig.getHtmlClassname();
            if (htmlClassname != null) {
                try {
                    final Class< ? extends HtmlElement> htmlClass =
                        (Class< ? extends HtmlElement>) Class.forName(htmlClassname);
                    // preload and validate that the class exists
                    if (LOG.isDebugEnabled()) {
                        LOG.debug("Mapping " + htmlClass.getName() + " to " + jsClassname);
                    }
                    while (!classConfig.isJsObject()) {
                        jsClassname = classConfig.getExtendedClassName();
                        classConfig = configuration.getClassConfiguration(jsClassname);
                    }
                    map.put(htmlClass, classConfig.getHostClass());
                }
                catch (final ClassNotFoundException e) {
                    throw new NoClassDefFoundError(e.getMessage());
                }
            }
        }
        map.put(HtmlHeading1.class, HTMLHeadingElement.class);
        map.put(HtmlHeading2.class, HTMLHeadingElement.class);
        map.put(HtmlHeading3.class, HTMLHeadingElement.class);
        map.put(HtmlHeading4.class, HTMLHeadingElement.class);
        map.put(HtmlHeading5.class, HTMLHeadingElement.class);
        map.put(HtmlHeading6.class, HTMLHeadingElement.class);

        map.put(HtmlInlineQuotation.class, HTMLQuoteElement.class);
        map.put(HtmlBlockQuote.class, HTMLQuoteElement.class);

        map.put(HtmlAbbreviated.class, HTMLSpanElement.class);
        map.put(HtmlAcronym.class, HTMLSpanElement.class);
        map.put(HtmlAddress.class, HTMLSpanElement.class);
        map.put(HtmlBackgroundSound.class, HTMLSpanElement.class);
        map.put(HtmlBidirectionalOverride.class, HTMLSpanElement.class);
        map.put(HtmlBig.class, HTMLSpanElement.class);
        map.put(HtmlBold.class, HTMLSpanElement.class);
        map.put(HtmlBlink.class, HTMLSpanElement.class);
        map.put(HtmlCenter.class, HTMLSpanElement.class);
        map.put(HtmlCitation.class, HTMLSpanElement.class);
        map.put(HtmlCode.class, HTMLSpanElement.class);
        map.put(HtmlDefinition.class, HTMLSpanElement.class);
        map.put(HtmlDefinitionDescription.class, HTMLSpanElement.class);
        map.put(HtmlDefinitionTerm.class, HTMLSpanElement.class);
        map.put(HtmlEmphasis.class, HTMLSpanElement.class);
        map.put(HtmlItalic.class, HTMLSpanElement.class);
        map.put(HtmlKeyboard.class, HTMLSpanElement.class);
        map.put(HtmlListing.class, HTMLSpanElement.class);
        map.put(HtmlMultiColumn.class, HTMLSpanElement.class);
        map.put(HtmlNoBreak.class, HTMLSpanElement.class);
        map.put(HtmlPlainText.class, HTMLSpanElement.class);
        map.put(HtmlS.class, HTMLSpanElement.class);
        map.put(HtmlSample.class, HTMLSpanElement.class);
        map.put(HtmlSmall.class, HTMLSpanElement.class);
        map.put(HtmlSpan.class, HTMLSpanElement.class);
        map.put(HtmlStrike.class, HTMLSpanElement.class);
        map.put(HtmlStrong.class, HTMLSpanElement.class);
        map.put(HtmlSubscript.class, HTMLSpanElement.class);
        map.put(HtmlSuperscript.class, HTMLSpanElement.class);
        map.put(HtmlTeletype.class, HTMLSpanElement.class);
        map.put(HtmlUnderlined.class, HTMLSpanElement.class);
        map.put(HtmlVariable.class, HTMLSpanElement.class);
        map.put(HtmlExample.class, HTMLSpanElement.class);

        map.put(HtmlDivision.class, HTMLDivElement.class);
        map.put(HtmlMarquee.class, HTMLDivElement.class);
        map.put(HtmlNoEmbed.class, HTMLDivElement.class);
        map.put(HtmlNoFrames.class, HTMLDivElement.class);
        map.put(HtmlNoScript.class, HTMLDivElement.class);

        map.put(HtmlTableBody.class, HTMLTableSectionElement.class);
        map.put(HtmlTableHeader.class, HTMLTableSectionElement.class);
        map.put(HtmlTableFooter.class, HTMLTableSectionElement.class);

        map.put(HtmlTableColumn.class, HTMLTableColElement.class);
        map.put(HtmlTableColumnGroup.class, HTMLTableColElement.class);

        HtmlJavaScriptMap_ = Collections.unmodifiableMap(map);
        return HtmlJavaScriptMap_;
    }
}
