/*
 * Copyright (c) 2015, 2017, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */
package javax.xml.catalog;

import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.util.HashMap;
import java.util.Map;
import jdk.xml.internal.SecuritySupport;

/**
 * The CatalogFeatures holds a collection of features and properties.
 * <p>
 *
 *
 * <table class="plain">
 * <caption>Catalog Features</caption>
 * <thead>
 * <tr>
 * <th rowspan="2">Feature</th>
 * <th rowspan="2">Description</th>
 * <th rowspan="2">Property Name</th>
 * <th rowspan="2">System Property [1]</th>
 * <th rowspan="2">jaxp.properties [1]</th>
 * <th colspan="2" style="text-align:center">Value [2]</th>
 * <th rowspan="2">Action</th>
 * </tr>
 * <tr>
 * <th>Type</th>
 * <th>Value</th>
 * </tr>
 * </thead>
 * <tbody>
 *
 * <tr>
 * <td><a id="FILES">FILES</a></td>
 * <td>A semicolon-delimited list of URIs to locate the catalog files.
 * The URIs must be absolute and have a URL protocol handler for the URI scheme.
 * </td>
 * <td>javax.xml.catalog.files</td>
 * <td>javax.xml.catalog.files</td>
 * <td>javax.xml.catalog.files</td>
 * <td>String</td>
 * <td>URIs</td>
 * <td>
 * Reads the first catalog as the current catalog; Loads others if no match
 * is found in the current catalog including delegate catalogs if any.
 * </td>
 * </tr>
 *
 * <tr>
 * <td rowspan="2"><a id="PREFER">PREFER</a></td>
 * <td rowspan="2">Indicates the preference between the public and system
 * identifiers. The default value is public [3].</td>
 * <td rowspan="2">javax.xml.catalog.prefer</td>
 * <td rowspan="2">N/A</td>
 * <td rowspan="2">N/A</td>
 * <td rowspan="2">String</td>
 * <td>{@code system}</td>
 * <td>Searches system entries for a match; Searches public entries when
 * external identifier specifies only a public identifier</td>
 * </tr>
 * <tr>
 * <td>{@code public}</td>
 * <td>Searches system entries for a match; Searches public entries when
 * there is no matching system entry.</td>
 * </tr>
 *
 * <tr>
 * <td rowspan="2"><a id="DEFER">DEFER</a></td>
 * <td rowspan="2">Indicates that the alternative catalogs including those
 * specified in delegate entries or nextCatalog are not read until they are
 * needed. The default value is true.</td>
 * <td rowspan="2">javax.xml.catalog.defer [4]</td>
 * <td rowspan="2">javax.xml.catalog.defer</td>
 * <td rowspan="2">javax.xml.catalog.defer</td>
 * <td rowspan="2">String</td>
 * <td>{@code true}</td>
 * <td>Loads alternative catalogs as needed.
 * </td>
 * </tr>
 * <tr>
 * <td>{@code false}</td>
 * <td>Loads all catalogs[5]. </td>
 * </tr>
 *
 * <tr>
 * <td rowspan="3"><a id="RESOLVE">RESOLVE</a></td>
 * <td rowspan="3">Determines the action if there is no matching entry found after
 * all of the specified catalogs are exhausted. The default is strict.</td>
 * <td rowspan="3">javax.xml.catalog.resolve [4]</td>
 * <td rowspan="3">javax.xml.catalog.resolve</td>
 * <td rowspan="3">javax.xml.catalog.resolve</td>
 * <td rowspan="3">String</td>
 * <td>{@code strict}</td>
 * <td>Throws CatalogException if there is no match.
 * </td>
 * </tr>
 * <tr>
 * <td>{@code continue}</td>
 * <td>Allows the XML parser to continue as if there is no match.
 * </td>
 * </tr>
 * <tr>
 * <td>{@code ignore}</td>
 * <td>Tells the XML parser to skip the external references if there no match.
 * </td>
 * </tr>
 *
 * </tbody>
 * </table>
 * <p>
 * <b>[1]</b> There is no System property for the features that marked as "N/A".
 *
 * <p>
 * <b>[2]</b> The value shall be exactly as listed in this table, case-sensitive.
 * Any unspecified value will result in {@link IllegalArgumentException}.
 * <p>
 * <b>[3]</b> The Catalog specification defined complex rules on
 * <a href="https://www.oasis-open.org/committees/download.php/14809/xml-catalogs.html#attrib.prefer">
 * the prefer attribute</a>. Although the prefer can be public or system, the
 * specification actually made system the preferred option, that is, no matter
 * the option, a system entry is always used if found. Public entries are only
 * considered if the prefer is public and system entries are not found. It is
 * therefore recommended that the prefer attribute be set as public
 * (which is the default).
 * <p>
 * <b>[4]</b> Although non-standard attributes in the OASIS Catalog specification,
 * {@code defer} and {@code resolve} are recognized by the Java Catalog API the
 * same as the {@code prefer} as being an attribute in the catalog entry of the
 * main catalog. Note that only the attributes specified for the catalog entry
 * of the main Catalog file will be used.
  * <p>
 * <b>[5]</b> If the intention is to share an entire catalog store, it may be desirable to
 * set the property {@code javax.xml.catalog.defer} to false to allow the entire
 * catalog to be pre-loaded.
 * <p>
 * <h3>Scope and Order</h3>
 * Features and properties can be set through the catalog file, the Catalog API,
 * system properties, and {@code jaxp.properties}, with a preference in the same order.
 * <p>
 * Properties that are specified as attributes in the catalog file for the
 * catalog and group entries shall take preference over any of the other settings.
 * For example, if a {@code prefer} attribute is set in the catalog file as in
 * {@code <catalog prefer="public">}, any other input for the "prefer" property
 * is not necessary or will be ignored.
 * <p>
 * Properties set through the Catalog API override those that may have been set
 * by system properties and/or in {@code jaxp.properties}. In case of multiple
 * interfaces, the latest in a procedure shall take preference. For
 * {@link Feature#FILES}, this means that the URI(s) specified through the methods
 * of the {@link CatalogManager} will override any that may have been entered
 * through the {@link Builder}.
 *
 * <p>
 * System properties when set shall override those in {@code jaxp.properties}.
 * <p>
 * The {@code jaxp.properties} file is typically in the conf directory of the Java
 * installation. The file is read only once by the JAXP implementation and
 * its values are then cached for future use. If the file does not exist
 * when the first attempt is made to read from it, no further attempts are
 * made to check for its existence. It is not possible to change the value
 * of any properties in {@code jaxp.properties} after it has been read.
 * <p>
 * A CatalogFeatures instance can be created through its builder as illustrated
 * in the following sample code:
 * <pre>{@code
                CatalogFeatures f = CatalogFeatures.builder()
                        .with(Feature.FILES, "file:///etc/xml/catalog")
                        .with(Feature.PREFER, "public")
                        .with(Feature.DEFER, "true")
                        .with(Feature.RESOLVE, "ignore")
                        .build();
 * }</pre>
 *
 * <p>
 * <h3>JAXP XML Processor Support</h3>
 * The Catalog Features are supported throughout the JAXP processors, including
 * SAX and DOM ({@link javax.xml.parsers}), and StAX parsers ({@link javax.xml.stream}),
 * Schema Validation ({@link javax.xml.validation}), and XML Transformation
 * ({@link javax.xml.transform}). The features described above can be set through JAXP
 * factories or processors that define a setProperty or setAttribute interface.
 * For example, the following code snippet sets a URI to a catalog file on a SAX
 * parser through the {@code javax.xml.catalog.files} property:
 * <p>
 * <pre>{@code
 *      SAXParserFactory spf = SAXParserFactory.newInstance();
 *      spf.setFeature(XMLConstants.USE_CATALOG, true); [1]
 *      SAXParser parser = spf.newSAXParser();
 *      parser.setProperty(CatalogFeatures.Feature.FILES.getPropertyName(), "file:///etc/xml/catalog");
 * }</pre>
 * <p>
 * [1] Note that this statement is not required since the default value of
 * {@link javax.xml.XMLConstants#USE_CATALOG USE_CATALOG} is true.
 *
 * <p>
 * The JAXP Processors' support for Catalog depends on both the
 * {@link javax.xml.XMLConstants#USE_CATALOG USE_CATALOG} feature and the
 * existence of valid Catalog file(s). A JAXP processor will use the Catalog
 * only when the feature is true and valid Catalog file(s) are specified through
 * the {@code javax.xml.catalog.files} property. It will make no attempt to use
 * the Catalog if either {@link javax.xml.XMLConstants#USE_CATALOG USE_CATALOG}
 * is set to false, or there is no Catalog file specified.
 *
 * <p>
 * The JAXP processors will observe the default settings of the
 * {@link javax.xml.catalog.CatalogFeatures}. The processors, for example, will
 * report an Exception by default when no matching entry is found since the
 * default value of the {@code javax.xml.catalog.resolve} property is strict.
 *
 * <p>
 * The JAXP processors give preference to user-specified custom resolvers. If such
 * a resolver is registered, it will be used over the CatalogResolver. If it returns
 * null however, the processors will continue resolving with the CatalogResolver.
 * If it returns an empty source, no attempt will be made by the CatalogResolver.
 *
 * <p>
 * The Catalog support is available for any process in the JAXP library that
 * supports a resolver. The following table lists all such processes.
 *
 * <p>
 * <h3><a id="ProcessesWithCatalogSupport">Processes with Catalog Support</a></h3>
 *
 * <table class="striped">
 * <caption>Processes with Catalog Support</caption>
 * <thead>
 * <tr>
 * <th>Process</th>
 * <th>Catalog Entry Type</th>
 * <th>Example</th>
 * </tr>
 * </thead>
 * <tbody>
 * <tr>
 * <td>DTDs and external entities</td>
 * <td>public, system</td>
 * <td>
 * <pre>{@literal
   The following DTD reference:
   <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">

   Can be resolved using the following Catalog entry:
   <public publicId="-//W3C//DTD XHTML 1.0 Strict//EN" uri="catalog/xhtml1-strict.dtd"/>
   or
   <systemSuffix systemIdSuffix="html1-strict.dtd" uri="catalog/xhtml1-strict.dtd"/>
 * }</pre>
 * </td>
 * </tr>
 * <tr>
 * <td>XInclude</td>
 * <td>uri</td>
 * <td>
 * <pre>{@literal
   The following XInclude element:
   <xi:include href="http://openjdk.java.net/xml/disclaimer.xml"/>

   can be resolved using a URI entry:
   <uri name="http://openjdk.java.net/xml/disclaimer.xml" uri="file:///pathto/local/disclaimer.xml"/>
   or
   <uriSuffix uriSuffix="disclaimer.xml" uri="file:///pathto/local/disclaimer.xml"/>
 * }</pre>
 * </td>
 * </tr>
 * <tr>
 * <td>XSD import</td>
 * <td>uri</td>
 * <td>
 * <pre>{@literal
   The following import element:
    <xsd:import namespace="http://openjdk.java.net/xsd/XSDImport_person"
                schemaLocation="http://openjdk.java.net/xsd/XSDImport_person.xsd"/>

   can be resolved using a URI entry:
   <uri name="http://openjdk.java.net/xsd/XSDImport_person.xsd" uri="file:///pathto/local/XSDImport_person.xsd"/>
   or
   <uriSuffix uriSuffix="XSDImport_person.xsd" uri="file:///pathto/local/XSDImport_person.xsd"/>
   or
   <uriSuffix uriSuffix="http://openjdk.java.net/xsd/XSDImport_person" uri="file:///pathto/local/XSDImport_person.xsd"/>
 * }</pre>
 * </td>
 * </tr>
 * <tr>
 * <td>XSD include</td>
 * <td>uri</td>
 * <td>
 * <pre>{@literal
   The following include element:
   <xsd:include schemaLocation="http://openjdk.java.net/xsd/XSDInclude_person.xsd"/>

   can be resolved using a URI entry:
   <uri name="http://openjdk.java.net/xsd/XSDInclude_person.xsd" uri="file:///pathto/local/XSDInclude_person.xsd"/>
   or
   <uriSuffix uriSuffix="XSDInclude_person.xsd" uri="file:///pathto/local/XSDInclude_person.xsd"/>
 * }</pre>
 * </td>
 * </tr>
 * <tr>
 * <td>XSL import and include</td>
 * <td>uri</td>
 * <td>
 * <pre>{@literal
   The following include element:
   <xsl:include href="http://openjdk.java.net/xsl/include.xsl"/>

   can be resolved using a URI entry:
   <uri name="http://openjdk.java.net/xsl/include.xsl" uri="file:///pathto/local/include.xsl"/>
   or
   <uriSuffix uriSuffix="include.xsl" uri="file:///pathto/local/include.xsl"/>
 * }</pre>
 * </td>
 * </tr>
 * <tr>
 * <td>XSL document function</td>
 * <td>uri</td>
 * <td>
 * <pre>{@literal
   The document in the following element:
   <xsl:variable name="dummy" select="document('http://openjdk.java.net/xsl/list.xml')"/>

   can be resolved using a URI entry:
   <uri name="http://openjdk.java.net/xsl/list.xml" uri="file:///pathto/local/list.xml"/>
   or
   <uriSuffix uriSuffix="list.xml" uri="file:///pathto/local/list.xml"/>
 * }</pre>
 * </td>
 * </tr>
 * </tbody>
 * </table>
 *
 * @since 9
 */
public class CatalogFeatures {

    /**
     * The constant name of the javax.xml.catalog.files property as described
     * in the property table above.
     */
    static final String CATALOG_FILES = "javax.xml.catalog.files";

    /**
     * The javax.xml.catalog.prefer property as described
     * in the property table above.
     */
    static final String CATALOG_PREFER = "javax.xml.catalog.prefer";

    /**
     * The javax.xml.catalog.defer property as described
     * in the property table above.
     */
    static final String CATALOG_DEFER = "javax.xml.catalog.defer";

    /**
     * The javax.xml.catalog.resolve property as described
     * in the property table above.
     */
    static final String CATALOG_RESOLVE = "javax.xml.catalog.resolve";

    //values for the prefer property
    static final String PREFER_SYSTEM = "system";
    static final String PREFER_PUBLIC = "public";

    //values for the defer property
    static final String DEFER_TRUE = "true";
    static final String DEFER_FALSE = "false";

    //values for the Resolve property
    static final String RESOLVE_STRICT = "strict";
    static final String RESOLVE_CONTINUE = "continue";
    static final String RESOLVE_IGNORE = "ignore";

    /**
     * A Feature type as defined in the
     * <a href="CatalogFeatures.html#CatalogFeatures">Catalog Features table</a>.
     */
    public static enum Feature {
        /**
         * The {@code javax.xml.catalog.files} property as described in
         * item <a href="CatalogFeatures.html#FILES">FILES</a> of the
         * Catalog Features table.
         */
        FILES(CATALOG_FILES, null, true),
        /**
         * The {@code javax.xml.catalog.prefer} property as described in
         * item <a href="CatalogFeatures.html#PREFER">PREFER</a> of the
         * Catalog Features table.
         */
        PREFER(CATALOG_PREFER, PREFER_PUBLIC, false),
        /**
         * The {@code javax.xml.catalog.defer} property as described in
         * item <a href="CatalogFeatures.html#DEFER">DEFER</a> of the
         * Catalog Features table.
         */
        DEFER(CATALOG_DEFER, DEFER_TRUE, true),
        /**
         * The {@code javax.xml.catalog.resolve} property as described in
         * item <a href="CatalogFeatures.html#RESOLVE">RESOLVE</a> of the
         * Catalog Features table.
         */
        RESOLVE(CATALOG_RESOLVE, RESOLVE_STRICT, true);

        private final String name;
        private final String defaultValue;
        private String value;
        private final boolean hasSystem;

        /**
         * Constructs a CatalogFeature instance.
         * @param name the name of the feature
         * @param value the value of the feature
         * @param hasSystem a flag to indicate whether the feature is supported
         * with a System property
         */
        Feature(String name, String value, boolean hasSystem) {
            this.name = name;
            this.defaultValue = value;
            this.hasSystem = hasSystem;
        }

        /**
         * Checks whether the specified property is equal to the current property.
         * @param propertyName the name of a property
         * @return true if the specified property is the current property, false
         * otherwise
         */
        boolean equalsPropertyName(String propertyName) {
            return name.equals(propertyName);
        }

        /**
         * Returns the name of the corresponding System Property.
         *
         * @return the name of the System Property
         */
        public String getPropertyName() {
            return name;
        }

        /**
         * Returns the default value of the property.
         * @return the default value of the property
         */
        public String defaultValue() {
            return defaultValue;
        }

        /**
         * Returns the value of the property.
         * @return the value of the property
         */
        String getValue() {
            return value;
        }

        /**
         * Checks whether System property is supported for the feature.
         * @return true it is supported, false otherwise
         */
        boolean hasSystemProperty() {
            return hasSystem;
        }
    }

    /**
     * States of the settings of a property, in the order: default value,
     * jaxp.properties file, jaxp system properties, and jaxp api properties
     */
    static enum State {
        /** represents the default state of a feature. */
        DEFAULT("default"),
        /** indicates the value of the feature is read from jaxp.properties. */
        JAXPDOTPROPERTIES("jaxp.properties"),
        /** indicates the value of the feature is read from its System property. */
        SYSTEMPROPERTY("system property"),
        /** indicates the value of the feature is specified through the API. */
        APIPROPERTY("property"),
        /** indicates the value of the feature is specified as a catalog attribute. */
        CATALOGATTRIBUTE("catalog attribute");

        final String literal;

        State(String literal) {
            this.literal = literal;
        }

        String literal() {
            return literal;
        }
    }

    /**
     * Values of the properties
     */
    private String[] values;

    /**
     * States of the settings for each property
     */
    private State[] states;

    /**
     * Private class constructor
     */
    private CatalogFeatures() {
    }

    /**
     * Returns a CatalogFeatures instance with default settings.
     * @return a default CatalogFeatures instance
     */
    public static CatalogFeatures defaults() {
        return CatalogFeatures.builder().build();
    }

    /**
     * Constructs a new CatalogFeatures instance with the builder.
     *
     * @param builder the builder to build the CatalogFeatures
     */
    CatalogFeatures(Builder builder) {
        init();
        setProperties(builder);
    }

    /**
     * Returns the value of the specified feature.
     *
     * @param cf the type of the Catalog feature
     * @return the value of the feature
     */
    public String get(Feature cf) {
        return values[cf.ordinal()];
    }

    /**
     * Initializes the supported properties
     */
    private void init() {
        values = new String[Feature.values().length];
        states = new State[Feature.values().length];
        for (Feature cf : Feature.values()) {
            setProperty(cf, State.DEFAULT, cf.defaultValue());
        }
        //read system properties or jaxp.properties
        readSystemProperties();
    }

    /**
     * Sets properties by the Builder.
     * @param builder the CatalogFeatures builder
     */
    private void setProperties(Builder builder) {
        builder.values.entrySet().stream().forEach((entry) -> {
            setProperty(entry.getKey(), State.APIPROPERTY, entry.getValue());
        });
    }
    /**
     * Sets the value of a property, updates only if it shall override.
     *
     * @param index the index of the property
     * @param state the state of the property
     * @param value the value of the property
     * @throws IllegalArgumentException if the value is invalid
     */
    private void setProperty(Feature feature, State state, String value) {
        int index = feature.ordinal();
        if (value != null && value.length() != 0) {
            if (state != State.APIPROPERTY) {
                Util.validateFeatureInput(feature, value);
            }
            if (states[index] == null || state.compareTo(states[index]) >= 0) {
                values[index] = value;
                states[index] = state;
            }
        }
    }

    /**
     * Reads from system properties, or those in jaxp.properties
     */
    private void readSystemProperties() {
        for (Feature cf : Feature.values()) {
            getSystemProperty(cf, cf.getPropertyName());
        }
    }

    /**
     * Reads from system properties, or those in jaxp.properties
     *
     * @param cf the type of the property
     * @param sysPropertyName the name of system property
     */
    private boolean getSystemProperty(Feature cf, String sysPropertyName) {
        if (cf.hasSystemProperty()) {
            String value = SecuritySupport.getSystemProperty(sysPropertyName);
            if (value != null && !value.equals("")) {
                setProperty(cf, State.SYSTEMPROPERTY, value);
                return true;
            }

            value = SecuritySupport.readJAXPProperty(sysPropertyName);
            if (value != null && !value.equals("")) {
                setProperty(cf, State.JAXPDOTPROPERTIES, value);
                return true;
            }
        }
        return false;
    }

    /**
     * Returns an instance of the builder for creating the CatalogFeatures object.
     *
     * @return an instance of the builder
     */
    public static Builder builder() {
        return new CatalogFeatures.Builder();
    }

    /**
     * The Builder class for building the CatalogFeatures object.
     */
    public static class Builder {
        /**
         * Values of the features supported by CatalogFeatures.
         */
        Map<Feature, String> values = new HashMap<>();

        /**
         * Instantiation of Builder is not allowed.
         */
        private Builder() {}

        /**
         * Sets the value to a specified Feature.
         * @param feature the Feature to be set
         * @param value the value to be set for the Feature
         * @return this Builder instance
         * @throws IllegalArgumentException if the value is not valid for the
         * Feature or has the wrong syntax for the {@code javax.xml.catalog.files}
         * property
         */
        public Builder with(Feature feature, String value) {
            Util.validateFeatureInput(feature, value);
            values.put(feature, value);
            return this;
        }

        /**
         * Returns a CatalogFeatures object built by this builder.
         *
         * @return an instance of CatalogFeatures
         */
        public CatalogFeatures build() {
            return new CatalogFeatures(this);
        }
    }
}
