/*
 * Copyright 2019 the original author or authors.
 *
 * 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 io.restassured.path.xml;

import groovy.lang.GroovyRuntimeException;
import groovy.util.XmlSlurper;
import groovy.util.slurpersupport.GPathResult;
import groovy.xml.XmlUtil;
import io.restassured.internal.common.assertion.AssertParameter;
import io.restassured.internal.common.path.ObjectConverter;
import io.restassured.internal.path.xml.*;
import io.restassured.internal.path.xml.mapping.XmlObjectDeserializer;
import io.restassured.path.xml.config.XmlParserType;
import io.restassured.path.xml.config.XmlPathConfig;
import io.restassured.path.xml.element.Node;
import io.restassured.path.xml.element.NodeChildren;
import io.restassured.path.xml.exception.XmlPathException;
import io.restassured.path.xml.mapper.factory.JAXBObjectMapperFactory;
import org.apache.commons.lang3.Validate;
import org.xml.sax.InputSource;
import org.xml.sax.XMLReader;

import java.io.File;
import java.io.InputStream;
import java.io.Reader;
import java.net.URI;
import java.util.*;
import java.util.Map.Entry;

import static io.restassured.path.xml.XmlPath.CompatibilityMode.XML;

/**
 * XmlPath is an alternative to using XPath for easily getting values from an XML document. It follows the Groovy GPath syntax
 * described here.
Let's say we have an XML defined as;
 *

 * <shopping>
 * <category type="groceries">
 * <item>
 * <name>Chocolate</name>
 * <price>10</price>
 * </item>
 * <item>
 * <name>Coffee</name>
 * <price>20</price>
 * </item>
 * </category>
 * <category type="supplies">
 * <item>
 * <name>Paper</name>
 * <price>5</price>
 * </item>
 * <item quantity="4">
 * <name>Pens</name>
 * <price>15</price>
 * </item>
 * </category>
 * <category type="present">
 * <item when="Aug 10">
 * <name>Kathryn's Birthday</name>
 * <price>200</price>
 * </item>
 * </category>
 * </shopping>
 *

 *

 * Get the name of the first category item:
 *

 *     String name = with(XML).get("shopping.category.item[0].name");
 *

 *

 * To get the number of category items:
 *

 *     int items = with(XML).get("shopping.category.item.size()");
 *

 *

 * Get a specific category:
 *

 *     Node category = with(XML).get("shopping.category[0]");
 *

 *

 * To get the number of categories with type attribute equal to 'groceries':
 *

 *    int items = with(XML).get("shopping.category.findAll { it.@type == 'groceries' }.size()");
 *

 *

 * Get all items with price greater than or equal to 10 and less than or equal to 20:
 *

 * List<Node> itemsBetweenTenAndTwenty = with(XML).get("shopping.category.item.findAll { item -> def price = item.price.toFloat(); price >= 10 && price <= 20 }");
 *

 *

 * Get the chocolate price:
 *

 * int priceOfChocolate = with(XML).getInt("**.find { it.name == 'Chocolate' }.price"
 *

 *

 * You can also parse HTML by setting compatibility mode to HTML:
 *

 * XmlPath xmlPath = new XmlPath(CompatibilityMode.HTML,<some html>);
 *

 * The XmlPath implementation of rest-assured uses a Groovy shell to evaluate expressions so be careful when injecting
 * user input into the expression. For example avoid doing this:
 *

 * String type = System.console().readLine();
 * List<Map> books = with(Object).get("shopping.category.findAll { it.@type == '"+type+"' }");
 *

 * Instead use the {@link #param(java.lang.String, java.lang.Object)} method like this:
 *

 * String type = System.console().readLine();
 * List<Map> books = with(Object).param("type", type).get("shopping.category.findAll { it.@type == type}");
 *

 */
public class XmlPath {
    public static XmlPathConfig config = null;

    private final CompatibilityMode mode;
    private LazyXmlParser lazyXmlParser;
    private XmlPathConfig xmlPathConfig = null;
    /**
     * Parameters for groovy console (not initialized here to save memory for queries that don't use params)
     */
    private Map<String, Object> params;

    private String rootPath = "";

    /**
     * Instantiate a new XmlPath instance.
     *
     * @param text The text containing the XML document
     */
    public XmlPath(String text) {
        this(XML, text);
    }

    /**
     * Instantiate a new XmlPath instance.
     *
     * @param stream The stream containing the XML document
     */
    public XmlPath(InputStream stream) {
        this(XML, stream);
    }

    /**
     * Instantiate a new XmlPath instance.
     *
     * @param source The source containing the XML document
     */
    public XmlPath(InputSource source) {
        this(XML, source);
    }

    /**
     * Instantiate a new XmlPath instance.
     *
     * @param file The file containing the XML document
     */
    public XmlPath(File file) {
        this(XML, file);
    }

    /**
     * Instantiate a new XmlPath instance.
     *
     * @param reader The reader containing the XML document
     */
    public XmlPath(Reader reader) {
        this(XML, reader);
    }

    /**
     * Instantiate a new XmlPath instance.
     *
     * @param uri The URI containing the XML document
     */
    public XmlPath(URI uri) {
        this(XML, uri);
    }

    /**
     * Instantiate a new XmlPath instance.
     *
     * @param mode The compatibility mode
     * @param text The text containing the XML document
     */
    public XmlPath(CompatibilityMode mode, String text) {
        Validate.notNull(mode, "Compatibility mode cannot be null");
        this.mode = mode;
        lazyXmlParser = parseText(text);
    }

    /**
     * Instantiate a new XmlPath instance.
     *
     * @param mode   The compatibility mode
     * @param stream The stream containing the XML document
     */
    public XmlPath(CompatibilityMode mode, InputStream stream) {
        Validate.notNull(mode, "Compatibility mode cannot be null");
        this.mode = mode;
        lazyXmlParser = parseInputStream(stream);
    }

    /**
     * Instantiate a new XmlPath instance.
     *
     * @param mode   The compatibility mode
     * @param source The source containing the XML document
     */
    public XmlPath(CompatibilityMode mode, InputSource source) {
        Validate.notNull(mode, "Compatibility mode cannot be null");
        this.mode = mode;
        lazyXmlParser = parseInputSource(source);
    }

    /**
     * Instantiate a new XmlPath instance.
     *
     * @param mode The compatibility mode
     * @param file The file containing the XML document
     */
    public XmlPath(CompatibilityMode mode, File file) {
        Validate.notNull(mode, "Compatibility mode cannot be null");
        this.mode = mode;
        lazyXmlParser = parseFile(file);
    }

    /**
     * Instantiate a new XmlPath instance.
     *
     * @param mode   The compatibility mode
     * @param reader The reader containing the XML document
     */
    public XmlPath(CompatibilityMode mode, Reader reader) {
        Validate.notNull(mode, "Compatibility mode cannot be null");
        this.mode = mode;
        lazyXmlParser = parseReader(reader);
    }

    /**
     * Instantiate a new XmlPath instance.
     *
     * @param mode The compatibility mode
     * @param uri  The URI containing the XML document
     */
    public XmlPath(CompatibilityMode mode, URI uri) {
        Validate.notNull(mode, "Compatibility mode cannot be null");
        this.mode = mode;
        lazyXmlParser = parseURI(uri);
    }

    /**
     * Configure XmlPath to use a specific JAXB object mapper factory
     *
     * @param factory The JAXB object mapper factory instance
     * @return a new XmlPath instance
     */
    public XmlPath using(JAXBObjectMapperFactory factory) {
        return new XmlPath(this, getXmlPathConfig().jaxbObjectMapperFactory(factory));
    }

    /**
     * Configure XmlPath to with a specific XmlPathConfig.
     *
     * @param config The XmlPath config
     * @return a new XmlPath instance
     */
    public XmlPath using(XmlPathConfig config) {
        return new XmlPath(this, config);
    }

    private XmlPath(XmlPath xmlPath, XmlPathConfig config) {
        this.xmlPathConfig = config;
        this.mode = xmlPath.mode;
        this.lazyXmlParser = xmlPath.lazyXmlParser.changeCompatibilityMode(mode).changeConfig(config);
        if (xmlPath.params != null) {
            this.params = new HashMap<String, Object>(xmlPath.params);
        }
    }

    /**
     * Get the entire XML graph as an Object
     * this url.
     *
     * @return The XML Node. A {@link java.lang.ClassCastException} will be thrown if the object
     * cannot be casted to the expected type.
     */
    public Node get() {
        return (Node) get("$");
    }

    /**
     * Get the result of an XML path expression. For syntax details please refer to
     * this url.
     *
     * @param path The XML path.
     * @param <T>  The type of the return value.
     * @return The object matching the XML path. A {@link java.lang.ClassCastException} will be thrown if the object
     * cannot be casted to the expected type.
     */
    public <T> T get(String path) {
        AssertParameter.notNull(path, "path");
        return getFromPath(path, true);
    }

    /**
     * Get the result of an XML path expression as a list. For syntax details please refer to
     * this url.
     *
     * @param path The XML path.
     * @param <T>  The list type
     * @return The object matching the XML path. A {@link java.lang.ClassCastException} will be thrown if the object
     * cannot be casted to the expected type.
     */
    public <T> List<T> getList(String path) {
        return getAsList(path);
    }

    /**
     * Get the result of an XML path expression as a list. For syntax details please refer to
     * this url.
     *
     * @param path        The XML path.
     * @param genericType The generic list type
     * @param <T>         The type
     * @return The object matching the XML path. A {@link java.lang.ClassCastException} will be thrown if the object
     * cannot be casted to the expected type.
     */
    public <T> List<T> getList(String path, Class<T> genericType) {
        return getAsList(path, genericType);
    }

    /**
     * Get the result of an XML path expression as a map. For syntax details please refer to
     * this url.
     *
     * @param path The XML path.
     * @param <K>  The type of the expected key
     * @param <V>  The type of the expected value
     * @return The object matching the XML path. A {@link java.lang.ClassCastException} will be thrown if the object
     * cannot be casted to the expected type.
     */
    public <K, V> Map<K, V> getMap(String path) {
        return get(path);
    }

    /**
     * Get the result of an XML path expression as a map. For syntax details please refer to
     * this url.
     *
     * @param path      The XML path.
     * @param keyType   The type of the expected key
     * @param valueType The type of the expected value
     * @param <K>       The type of the expected key
     * @param <V>       The type of the expected value
     * @return The object matching the XML path. A {@link java.lang.ClassCastException} will be thrown if the object
     * cannot be casted to the expected type.
     */
    public <K, V> Map<K, V> getMap(String path, Class<K> keyType, Class<V> valueType) {
        final Map<K, V> originalMap = get(path);
        final Map<K, V> newMap = new HashMap<K, V>();
        for (Entry<K, V> entry : originalMap.entrySet()) {
            final K key = entry.getKey() == null ? null : convertObjectTo(entry.getKey(), keyType);
            final V value = entry.getValue() == null ? null : convertObjectTo(entry.getValue(), valueType);
            newMap.put(key, value);
        }
        return Collections.unmodifiableMap(newMap);
    }

    /**
     * Get an XML document as a Java Object.
     *
     * @param objectType The type of the java object.
     * @param <T>        The type of the java object
     * @return A Java object representation of the XML document
     */
    public <T> T getObject(String path, Class<T> objectType) {
        Object object = getFromPath(path, false);
        return getObjectAsType(object, objectType);
    }

    private <T> T getObjectAsType(Object object, Class<T> objectType) {
        if (object == null) {
            return null;
        } else if (object instanceof GPathResult) {
            try {
                object = XmlUtil.serialize((GPathResult) object);
            } catch (GroovyRuntimeException e) {
                throw new IllegalArgumentException("Failed to convert XML to Java Object. If you're trying convert to a list then use the getList method instead.", e);
            }
        } else if (object instanceof groovy.util.slurpersupport.Node) {
            object = GroovyNodeSerializer.toXML((groovy.util.slurpersupport.Node) object);
        }

        XmlPathConfig cfg = new XmlPathConfig(getXmlPathConfig());
        if (cfg.hasCustomJaxbObjectMapperFactory()) {
            cfg = cfg.defaultParserType(XmlParserType.JAXB);
        }

        if (!(object instanceof String)) {
            throw new IllegalStateException("Internal error: XML object was not an instance of String, please report to the REST Assured mailing-list.");
        }

        //noinspection RedundantCast
        return (T) XmlObjectDeserializer.deserialize((String) object, objectType, cfg);
    }

    private <T> T getFromPath(String path, boolean convertToJavaObject) {
        final GPathResult input = lazyXmlParser.invoke();
        final XMLAssertion xmlAssertion = new XMLAssertion();
        if (params != null) {
            xmlAssertion.setParams(params);
        }
        final String root = rootPath.equals("") ? rootPath : rootPath.endsWith(".") ? rootPath : rootPath + ".";
        xmlAssertion.setKey(root + path);
        //noinspection unchecked
        return (T) xmlAssertion.getResult(input, convertToJavaObject, true);
    }

    /**
     * Get the result of an XML path expression as an int. For syntax details please refer to
     * this url.
     *
     * @param path The XML path.
     * @return The object matching the XML path. A {@link java.lang.ClassCastException} will be thrown if the object
     * cannot be casted to the expected type.
     */
    public int getInt(String path) {
        final Object object = get(path);
        return convertObjectTo(object, Integer.class);
    }

    /**
     * Get the result of an XML path expression as a boolean. For syntax details please refer to
     * this url.
     *
     * @param path The XML path.
     * @return The object matching the XML path. A {@link java.lang.ClassCastException} will be thrown if the object
     * cannot be casted to the expected type.
     */
    public boolean getBoolean(String path) {
        Object object = get(path);
        return convertObjectTo(object, Boolean.class);
    }

    /**
     * Get the result of an XML path expression as a {@link Node}. For syntax details please refer to
     * this url.
     *
     * @param path The XML path.
     * @return The object matching the XML path. A {@link java.lang.ClassCastException} will be thrown if the object
     * cannot be casted to the expected type.
     */
    public Node getNode(String path) {
        return convertObjectTo(get(path), Node.class);
    }

    /**
     * Get the result of an XML path expression as a {@link NodeChildren}. For syntax details please refer to
     * this url.
     *
     * @param path The XML path.
     * @return The object matching the XML path. A {@link java.lang.ClassCastException} will be thrown if the object
     * cannot be casted to the expected type.
     */
    public NodeChildren getNodeChildren(String path) {
        return convertObjectTo(get(path), NodeChildren.class);
    }

    /**
     * Get the result of an XML path expression as a char. For syntax details please refer to
     * this url.
     *
     * @param path The XML path.
     * @return The object matching the XML path. A {@link java.lang.ClassCastException} will be thrown if the object
     * cannot be casted to the expected type.
     */
    public char getChar(String path) {
        Object object = get(path);
        return convertObjectTo(object, Character.class);
    }

    /**
     * Get the result of an XML path expression as a byte. For syntax details please refer to
     * this url.
     *
     * @param path The XML path.
     * @return The object matching the XML path. A {@link java.lang.ClassCastException} will be thrown if the object
     * cannot be casted to the expected type.
     */
    public byte getByte(String path) {
        Object object = get(path);
        return convertObjectTo(object, Byte.class);
    }

    /**
     * Get the result of an XML path expression as a short. For syntax details please refer to
     * this url.
     *
     * @param path The XML path.
     * @return The object matching the XML path. A {@link java.lang.ClassCastException} will be thrown if the object
     * cannot be casted to the expected type.
     */
    public short getShort(String path) {
        Object object = get(path);
        return convertObjectTo(object, Short.class);
    }

    /**
     * Get the result of an XML path expression as a float. For syntax details please refer to
     * this url.
     *
     * @param path The XML path.
     * @return The object matching the XML path. A {@link java.lang.ClassCastException} will be thrown if the object
     * cannot be casted to the expected type.
     */
    public float getFloat(String path) {
        Object object = get(path);
        return convertObjectTo(object, Float.class);
    }

    /**
     * Get the result of an XML path expression as a double. For syntax details please refer to
     * this url.
     *
     * @param path The XML path.
     * @return The object matching the XML path. A {@link java.lang.ClassCastException} will be thrown if the object
     * cannot be casted to the expected type.
     */
    public double getDouble(String path) {
        Object object = get(path);
        return convertObjectTo(object, Double.class);
    }

    /**
     * Get the result of an XML path expression as a long. For syntax details please refer to
     * this url.
     *
     * @param path The XML path.
     * @return The object matching the XML path. A {@link java.lang.ClassCastException} will be thrown if the object
     * cannot be casted to the expected type.
     */
    public long getLong(String path) {
        Object object = get(path);
        return convertObjectTo(object, Long.class);
    }

    /**
     * Get the result of an XML path expression as a string. For syntax details please refer to
     * this url.
     *
     * @param path The XML path.
     * @return The object matching the XML path. A {@link java.lang.ClassCastException} will be thrown if the object
     * cannot be casted to the expected type.
     */
    public String getString(String path) {
        Object object = get(path);
        return convertObjectTo(object, String.class);
    }

    /**
     * Get the result of an XML path expression as a UUID. For syntax details please refer to
     * this url.
     *
     * @param path The XML path.
     * @return The object matching the XML path. A {@link java.lang.ClassCastException} will be thrown if the object
     * cannot be casted to the expected type.
     */
    public UUID getUUID(String path) {
        Object object = get(path);
        return convertObjectTo(object, UUID.class);
    }

    /**
     * Add a parameter for the expression. Example:
     *

     * String type = System.console().readLine();
     * List<Map> books = with(Object).param("type", type).get("shopping.category.findAll { it.@type == type}");
     *

     *
     * @param key   The name of the parameter. Just use this name in your expression as a variable
     * @param value The value of the parameter
     * @return New XmlPath instance with the parameter set
     */
    public XmlPath param(String key, Object value) {
        XmlPath newP = new XmlPath(this, getXmlPathConfig());
        if (newP.params == null) {
            newP.params = new HashMap<String, Object>();
        }
        newP.params.put(key, value);
        return newP;
    }

    /**
     * Peeks into the XML/HTML that XmlPath will parse by printing it to the console. You can
     * continue working with XmlPath afterwards. This is mainly for debug purposes. If you want to return a prettified version of the content
     * see {@link #prettify()}. If you want to return a prettified version of the content and also print it to the console use {@link #prettyPrint()}.
     *

     *

     * Note that the content is not guaranteed to be looking exactly like the it does at the source. This is because once you peek
     * the content has been downloaded and transformed into another data structure (used by XmlPath) and the XML is rendered
     * from this data structure.
     *

     *
     * @return The same XmlPath instance
     */
    public XmlPath peek() {
        final GPathResult result = lazyXmlParser.invoke();
        final String render = XmlRenderer.render(result);
        System.out.println(render);
        return this;
    }

    /**
     * Peeks into the XML/HTML that XmlPath will parse by printing it to the console in a prettified manner. You can
     * continue working with XmlPath afterwards. This is mainly for debug purposes. If you want to return a prettified version of the content
     * see {@link #prettify()}. If you want to return a prettified version of the content and also print it to the console use {@link #prettyPrint()}.
     *

     *

     * Note that the content is not guaranteed to be looking exactly like the it does at the source. This is because once you peek
     * the content has been downloaded and transformed into another data structure (used by XmlPath) and the XML is rendered
     * from this data structure.
     *

     *
     * @return The same XmlPath instance
     */
    public XmlPath prettyPeek() {
        final GPathResult result = lazyXmlParser.invoke();
        final String prettify = XmlPrettifier.prettify(result);
        System.out.println(prettify);
        return this;
    }

    /**
     * Get the XML as a prettified string.
     *

     *

     * Note that the content is not guaranteed to be looking exactly like the it does at the source. This is because once you peek
     * the content has been downloaded and transformed into another data structure (used by XmlPath) and the XML is rendered
     * from this data structure.
     *

     *
     * @return The XML as a prettified String.
     */
    public String prettify() {
        return XmlPrettifier.prettify(lazyXmlParser.invoke());
    }

    /**
     * Get and print the XML as a prettified string.
     *

     *

     * Note that the content is not guaranteed to be looking exactly like the it does at the source. This is because once you peek
     * the content has been downloaded and transformed into another data structure (used by XmlPath) and the XML is rendered
     * from this data structure.
     *

     *
     * @return The XML as a prettified String.
     */
    public String prettyPrint() {
        final String pretty = prettify();
        System.out.println(pretty);
        return pretty;
    }

    /**
     * Instantiate a new XmlPath instance.
     *
     * @param text The text containing the XML document
     */
    public static XmlPath given(String text) {
        return new XmlPath(text);
    }

    /**
     * Instantiate a new XmlPath instance.
     *
     * @param stream The stream containing the XML document
     */
    public static XmlPath given(InputStream stream) {
        return new XmlPath(stream);
    }

    /**
     * Instantiate a new XmlPath instance.
     *
     * @param source The source containing the XML document
     */
    public static XmlPath given(InputSource source) {
        return new XmlPath(source);
    }

    /**
     * Instantiate a new XmlPath instance.
     *
     * @param file The file containing the XML document
     */
    public static XmlPath given(File file) {
        return new XmlPath(file);
    }

    /**
     * Instantiate a new XmlPath instance.
     *
     * @param reader The reader containing the XML document
     */
    public static XmlPath given(Reader reader) {
        return new XmlPath(reader);
    }

    /**
     * Instantiate a new XmlPath instance.
     *
     * @param uri The URI containing the XML document
     */
    public static XmlPath given(URI uri) {
        return new XmlPath(uri);
    }

    public static XmlPath with(InputStream stream) {
        return new XmlPath(stream);
    }

    /**
     * Instantiate a new XmlPath instance.
     *
     * @param text The text containing the XML document
     */
    public static XmlPath with(String text) {
        return new XmlPath(text);
    }

    /**
     * Instantiate a new XmlPath instance.
     *
     * @param source The source containing the XML document
     */
    public static XmlPath with(InputSource source) {
        return new XmlPath(source);
    }

    /**
     * Instantiate a new XmlPath instance.
     *
     * @param file The file containing the XML document
     */
    public static XmlPath with(File file) {
        return new XmlPath(file);
    }

    /**
     * Instantiate a new XmlPath instance.
     *
     * @param reader The reader containing the XML document
     */
    public static XmlPath with(Reader reader) {
        return new XmlPath(reader);
    }

    /**
     * Instantiate a new XmlPath instance.
     *
     * @param uri The URI containing the XML document
     */
    public static XmlPath with(URI uri) {
        return new XmlPath(uri);
    }

    /**
     * Instantiate a new XmlPath instance.
     *
     * @param stream The stream containing the XML document
     */
    public static XmlPath from(InputStream stream) {
        return new XmlPath(stream);
    }

    /**
     * Instantiate a new XmlPath instance.
     *
     * @param text The text containing the XML document
     */
    public static XmlPath from(String text) {
        return new XmlPath(text);
    }

    /**
     * Instantiate a new XmlPath instance.
     *
     * @param source The source containing the XML document
     */
    public static XmlPath from(InputSource source) {
        return new XmlPath(source);
    }

    /**
     * Instantiate a new XmlPath instance.
     *
     * @param file The file containing the XML document
     */
    public static XmlPath from(File file) {
        return new XmlPath(file);
    }

    /**
     * Instantiate a new XmlPath instance.
     *
     * @param reader The reader containing the XML document
     */
    public static XmlPath from(Reader reader) {
        return new XmlPath(reader);
    }

    /**
     * Instantiate a new XmlPath instance.
     *
     * @param uri The URI containing the XML document
     */
    public static XmlPath from(URI uri) {
        return new XmlPath(uri);
    }

    private LazyXmlParser parseText(final String text) {
        return new LazyXmlParser(getXmlPathConfig(), mode) {
            protected GPathResult method(XmlSlurper slurper) throws Exception {
                return slurper.parseText(text);
            }
        };
    }

    /**
     * Set the root path of the document so that you don't need to write the entire path. E.g.
     *

     * final XmlPath xmlPath = new XmlPath(XML).setRoot("shopping.category.item");
     * assertThat(xmlPath.getInt("size()"), equalTo(5));
     * assertThat(xmlPath.getList("children().list()", String.class), hasItem("Pens"));
     *

     *
     * @param rootPath The root path to use.
     * @deprecated Use {@link #setRootPath(String)} instead
     */
    @Deprecated
    public XmlPath setRoot(String rootPath) {
        return setRootPath(rootPath);
    }

    /**
     * Set the root path of the document so that you don't need to write the entire path. E.g.
     *

     * final XmlPath xmlPath = new XmlPath(XML).setRootPath("shopping.category.item");
     * assertThat(xmlPath.getInt("size()"), equalTo(5));
     * assertThat(xmlPath.getList("children().list()", String.class), hasItem("Pens"));
     *

     *
     * @param rootPath The root path to use.
     */
    public XmlPath setRootPath(String rootPath) {
        AssertParameter.notNull(rootPath, "Root path");
        this.rootPath = rootPath;
        return this;
    }

    private <T> List<T> getAsList(String path) {
        return getAsList(path, null);
    }

    private <T> List<T> getAsList(String path, final Class<?> explicitType) {
        Object returnObject = get(path);
        if (returnObject instanceof NodeChildren) {
            final NodeChildren nodeChildren = (NodeChildren) returnObject;
            returnObject = convertElementsListTo(nodeChildren.list(), explicitType);
        } else if (!(returnObject instanceof List)) {
            final List<T> asList = new ArrayList<T>();
            if (returnObject != null) {
                final T e;
                if (explicitType == null) {
                    e = (T) returnObject.toString();
                } else {
                    e = (T) convertObjectTo(returnObject, explicitType);
                }
                asList.add(e);
            }
            returnObject = asList;
        } else if (explicitType != null) {
            final List<?> returnObjectAsList = (List<?>) returnObject;
            final List<T> convertedList = new ArrayList<T>();
            for (Object o : returnObjectAsList) {
                convertedList.add((T) convertObjectTo(o, explicitType));
            }
            returnObject = convertedList;
        }
        return returnObject == null ? null : Collections.unmodifiableList((List<T>) returnObject);
    }

    private List<Object> convertElementsListTo(List<Node> list, Class<?> explicitType) {
        List<Object> convertedList = new ArrayList<Object>();
        if (list != null && list.size() > 0) {
            for (Node node : list) {
                if (explicitType == null) {
                    convertedList.add(node.toString());
                } else {
                    convertedList.add(convertObjectTo(node, explicitType));
                }
            }
        }
        return convertedList;
    }

    private <T> T convertObjectTo(Object object, Class<T> explicitType) {
        if (object instanceof NodeBase && !ObjectConverter.canConvert(object, explicitType)) {
            return getObjectAsType(((NodeBase) object).getBackingGroovyObject(), explicitType);
        }
        return ObjectConverter.convertObjectTo(object, explicitType);
    }

    private LazyXmlParser parseInputStream(final InputStream stream) {
        return new LazyXmlParser(getXmlPathConfig(), mode) {
            protected GPathResult method(XmlSlurper slurper) throws Exception {
                return slurper.parse(stream);
            }
        };
    }

    private LazyXmlParser parseReader(final Reader reader) {
        return new LazyXmlParser(getXmlPathConfig(), mode) {
            protected GPathResult method(XmlSlurper slurper) throws Exception {
                return slurper.parse(reader);
            }
        };
    }

    private LazyXmlParser parseFile(final File file) {
        return new LazyXmlParser(getXmlPathConfig(), mode) {
            protected GPathResult method(XmlSlurper slurper) throws Exception {
                return slurper.parse(file);
            }
        };
    }

    private LazyXmlParser parseURI(final URI uri) {
        return new LazyXmlParser(getXmlPathConfig(), mode) {
            protected GPathResult method(XmlSlurper slurper) throws Exception {
                return slurper.parse(uri.toString());
            }
        };
    }

    private LazyXmlParser parseInputSource(final InputSource source) {
        return new LazyXmlParser(getXmlPathConfig(), mode) {
            protected GPathResult method(XmlSlurper slurper) throws Exception {
                return slurper.parse(source);
            }
        };
    }

    private XmlPathConfig getXmlPathConfig() {
        XmlPathConfig cfg;
        if (config == null && xmlPathConfig == null) {
            cfg = new XmlPathConfig();
        } else if (xmlPathConfig != null) {
            cfg = xmlPathConfig;
        } else {
            cfg = config;
        }
        return cfg;
    }

    private static abstract class LazyXmlParser {
        protected abstract GPathResult method(XmlSlurper slurper) throws Exception;

        private volatile XmlPathConfig config;
        private volatile CompatibilityMode compatibilityMode;

        protected LazyXmlParser(XmlPathConfig config, CompatibilityMode compatibilityMode) {
            this.config = config;
            this.compatibilityMode = compatibilityMode;
        }

        public LazyXmlParser changeCompatibilityMode(CompatibilityMode mode) {
            this.compatibilityMode = mode;
            return this;
        }

        public LazyXmlParser changeConfig(XmlPathConfig config) {
            this.config = config;
            return this;
        }

        private GPathResult input;
        private boolean isInputParsed;

        public synchronized GPathResult invoke() {
            if (isInputParsed) {
                return input;
            }
            isInputParsed = true;
            try {
                final XmlSlurper slurper;
                if (compatibilityMode == XML) {
                    slurper = new XmlSlurper(config.isValidating(), config.isNamespaceAware(), config.isAllowDocTypeDeclaration());
                } else {
                    XMLReader p = new org.ccil.cowan.tagsoup.Parser();
                    slurper = new XmlSlurper(p);
                }

                // Apply features
                Map<String, Boolean> features = config.features();
                for (Entry<String, Boolean> feature : features.entrySet()) {
                    slurper.setFeature(feature.getKey(), feature.getValue());
                }

                // Apply properties
                Map<String, Object> properties = config.properties();
                for (Entry<String, Object> feature : properties.entrySet()) {
                    slurper.setProperty(feature.getKey(), feature.getValue());
                }

                final GPathResult result = method(slurper);
                if (config.hasDeclaredNamespaces()) {
                    result.declareNamespace(config.declaredNamespaces());
                }
                input = result;
                return input;
            } catch (Exception e) {
                throw new XmlPathException("Failed to parse the XML document", e);
            }
        }
    }

    public static enum CompatibilityMode {
        XML, HTML
    }

    /**
     * Resets static XmlPath configuration to default values
     */
    public static void reset() {
        XmlPath.config = null;
    }
}


/*
 * Copyright 2019 the original author or authors.
 *
 * 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 io.restassured.builder;

import io.restassured.RestAssured;
import io.restassured.authentication.AuthenticationScheme;
import io.restassured.config.LogConfig;
import io.restassured.config.RestAssuredConfig;
import io.restassured.config.SessionConfig;
import io.restassured.filter.Filter;
import io.restassured.filter.log.LogDetail;
import io.restassured.filter.log.RequestLoggingFilter;
import io.restassured.http.ContentType;
import io.restassured.http.Cookie;
import io.restassured.http.Cookies;
import io.restassured.internal.RequestSpecificationImpl;
import io.restassured.internal.SpecificationMerger;
import io.restassured.internal.log.LogRepository;
import io.restassured.mapper.ObjectMapper;
import io.restassured.mapper.ObjectMapperType;
import io.restassured.specification.MultiPartSpecification;
import io.restassured.specification.ProxySpecification;
import io.restassured.specification.RequestSpecification;

import java.io.File;
import java.io.InputStream;
import java.io.PrintStream;
import java.net.URI;
import java.security.KeyStore;
import java.util.Collection;
import java.util.List;
import java.util.Map;

import static io.restassured.RestAssured.*;
import static io.restassured.internal.common.assertion.AssertParameter.notNull;

/**
 * You can use the builder to construct a request specification. The specification can be used as e.g.
 *

 * ResponseSpecification responseSpec = new ResponseSpecBuilder().expectStatusCode(200).build();
 * RequestSpecification requestSpec = new RequestSpecBuilder().addParam("parameter1", "value1").build();
 *
 * given(responseSpec, requestSpec).post("/something");
 *

 *

 * or
 *

 * RequestSpecification requestSpec = new RequestSpecBuilder().addParameter("parameter1", "value1").build();
 *
 * given().
 *         spec(requestSpec).
 * expect().
 *         body("x.y.z", equalTo("something")).
 * when().
 *        get("/something");
 *

 */
public class RequestSpecBuilder {

    private static final String SSL = "SSL";
    private RequestSpecificationImpl spec;

    public RequestSpecBuilder() {
        this.spec = (RequestSpecificationImpl) new RequestSpecificationImpl(baseURI, port, basePath, authentication, filters(),
                requestSpecification, urlEncodingEnabled, config, new LogRepository(), proxy).config(RestAssured.config());
    }

    /**
     * Specify a String request body (such as e.g. JSON or XML) to be sent with the request. This works for the
     * POST, PUT and PATCH methods only. Trying to do this for the other http methods will cause an exception to be thrown.
     *

     *
     * @param body The body to send.
     * @return The request specification builder
     */
    public RequestSpecBuilder setBody(String body) {
        spec.body(body);
        return this;
    }

    /**
     * Specify a byte array request body to be sent with the request. This only works for the
     * POST http method. Trying to do this for the other http methods will cause an exception to be thrown.
     *
     * @param body The body to send.
     * @return The request specification builder
     */
    public RequestSpecBuilder setBody(byte[] body) {
        spec.body(body);
        return this;
    }

    /**
     * Specify an Object request content that will automatically be serialized to JSON or XML and sent with the request.
     * If the object is a primitive or Number the object will
     * be converted to a String and put in the request body. This works for the POST, PUT and PATCH methods only.
     * Trying to do this for the other http methods will cause an exception to be thrown.
     *

     *
     * @param object The object to serialize and send with the request
     * @return The request specification
     */
    public RequestSpecBuilder setBody(Object object) {
        spec.body(object);
        return this;
    }

    /**
     * Specify an Object request content that will automatically be serialized to JSON or XML and sent with the request using a specific object mapper.
     * This works for the POST, PATCH and PUT methods only. Trying to do this for the other http methods will cause an exception to be thrown.
     *
     * @param object The object to serialize and send with the request
     * @param mapper The object mapper
     * @return The request specification
     */
    public RequestSpecBuilder setBody(Object object, ObjectMapper mapper) {
        spec.body(object, mapper);
        return this;
    }

    /**
     * Specify an Object request content that will automatically be serialized to JSON or XML and sent with the request using a specific object mapper type.
     * This works for the POST, PATCH and PUT methods only. Trying to do this for the other http methods will cause an exception to be thrown.
     *

     * Example of use:
     *

     * Message message = new Message();
     * message.setMessage("My beautiful message");
     *
     * given().
     *         body(message, ObjectMapper.GSON).
     * expect().
     *         content(equalTo("Response to a beautiful message")).
     * when().
     *         post("/beautiful-message");
     *

     *

     *
     * @param object     The object to serialize and send with the request
     * @param mapperType The object mapper type to be used
     * @return The request specification
     */
    public RequestSpecBuilder setBody(Object object, ObjectMapperType mapperType) {
        spec.body(object, mapperType);
        return this;
    }

    /**
     * Add cookies to be sent with the request as Map e.g:
     *
     * @param cookies The Map containing the cookie names and their values to set in the request.
     * @return The request specification builder
     */
    public RequestSpecBuilder addCookies(Map<String, ?> cookies) {
        spec.cookies(cookies);
        return this;
    }

    /**
     * Add a detailed cookie
     *
     * @param cookie The cookie to add.
     * @return The request specification builder
     */
    public RequestSpecBuilder addCookie(Cookie cookie) {
        spec.cookie(cookie);
        return this;
    }

    /**
     * Add a cookie to be sent with the request.
     *
     * @param key                  The cookie key
     * @param value                The cookie value
     * @param cookieNameValuePairs Additional cookies values. This will actually create two cookies with the same name but with different values.
     * @return The request specification builder
     */
    public RequestSpecBuilder addCookie(String key, Object value, Object... cookieNameValuePairs) {
        spec.cookie(key, value, cookieNameValuePairs);
        return this;
    }

    /**
     * Add a cookie without value to be sent with the request.
     *
     * @param name The cookie name
     * @return The request specification builder
     */
    public RequestSpecBuilder addCookie(String name) {
        spec.cookie(name);
        return this;
    }

    /**
     * Specify multiple detailed cookies that'll be sent with the request.
     *
     * @param cookies The cookies to set in the request.
     * @return The request specification builder
     * @see RequestSpecification#cookies(Cookies)
     */
    public RequestSpecBuilder addCookies(Cookies cookies) {
        spec.cookies(cookies);
        return this;
    }

    /**
     * Add a filter that will be used in the request.
     *
     * @param filter The filter to add
     * @return RequestSpecBuilder builder
     */
    public RequestSpecBuilder addFilter(Filter filter) {
        spec.filter(filter);
        return this;
    }

    /**
     * Add filters that will be used in the request.
     *
     * @param filters The filters to add
     * @return RequestSpecBuilder builder
     */
    public RequestSpecBuilder addFilters(List<Filter> filters) {
        spec.filters(filters);
        return this;
    }

    /**
     * Add parameters to be sent with the request as Map.
     *
     * @param parametersMap The Map containing the parameter names and their values to send with the request.
     * @return The request specification builder
     */
    public RequestSpecBuilder addParams(Map<String, ?> parametersMap) {
        spec.params(parametersMap);
        return this;
    }

    /**
     * Add a parameter to be sent with the request.
     *
     * @param parameterName   The parameter name
     * @param parameterValues Zero to many parameter values for this parameter name.
     * @return The request specification builder
     */
    public RequestSpecBuilder addParam(String parameterName, Object... parameterValues) {
        spec.param(parameterName, parameterValues);
        return this;
    }

    /**
     * Add a multi-value parameter to be sent with the request.
     *
     * @param parameterName   The parameter key
     * @param parameterValues The parameter values
     * @return The request specification builder
     */
    public RequestSpecBuilder addParam(String parameterName, Collection<?> parameterValues) {
        spec.param(parameterName, parameterValues);
        return this;
    }

    /**
     * Method to remove parameter added with {@link #addParam(String, Object...)} from map.
     * Removes all values of this parameter
     *
     * @param parameterName The parameter key
     * @return The request specification builder
     */
    public RequestSpecBuilder removeParam(String parameterName) {
        spec.removeParam(parameterName);
        return this;
    }

    /**
     * Add a query parameter to be sent with the request. This method is the same as {@link #addParam(String, java.util.Collection)}
     * for all HTTP methods except POST where this method can be used to differentiate between form and query params.
     *
     * @param parameterName   The parameter key
     * @param parameterValues The parameter values
     * @return The request specification builder
     * @see #addQueryParam(String, Object...)
     */
    public RequestSpecBuilder addQueryParam(String parameterName, Collection<?> parameterValues) {
        spec.queryParam(parameterName, parameterValues);
        return this;
    }

    /**
     * Add query parameters to be sent with the request as a Map. This method is the same as {@link #addParams(java.util.Map)}
     * for all HTTP methods except POST where this method can be used to differentiate between form and query params.
     *
     * @param parametersMap The Map containing the parameter names and their values to send with the request.
     * @return The request specification builder
     */
    public RequestSpecBuilder addQueryParams(Map<String, ?> parametersMap) {
        spec.queryParams(parametersMap);
        return this;
    }

    /**
     * Add a query parameter to be sent with the request. This method is the same as {@link #addParam(String, Object...)} )}
     * for all HTTP methods except POST where this method can be used to differentiate between form and query params.
     *
     * @param parameterName   The parameter key
     * @param parameterValues Zero to many parameter values for this parameter name.
     * @return The request specification builder
     */
    public RequestSpecBuilder addQueryParam(String parameterName, Object... parameterValues) {
        spec.queryParam(parameterName, parameterValues);
        return this;
    }

    /**
     * Method to remove parameter added with from map.
     * Removes all values of this parameter
     *
     * @param parameterName The parameter key
     * @return The request specification builder
     */
    public RequestSpecBuilder removeQueryParam(String parameterName) {
        spec.removeQueryParam(parameterName);
        return this;
    }

    /**
     * Add a form parameter to be sent with the request. This method is the same as {@link #addParam(String, java.util.Collection)}
     * for all HTTP methods except PUT where this method can be used to differentiate between form and query params.
     *
     * @param parameterName   The parameter key
     * @param parameterValues The parameter values
     * @return The request specification builder
     * @see #addFormParam(String, Object...)
     */
    public RequestSpecBuilder addFormParam(String parameterName, Collection<?> parameterValues) {
        spec.formParam(parameterName, parameterValues);
        return this;
    }

    /**
     * Add query parameters to be sent with the request as a Map. This method is the same as {@link #addParams(java.util.Map)}
     * for all HTTP methods except POST where this method can be used to differentiate between form and query params.
     *
     * @param parametersMap The Map containing the parameter names and their values to send with the request.
     * @return The request specification builder
     */
    public RequestSpecBuilder addFormParams(Map<String, ?> parametersMap) {
        spec.formParams(parametersMap);
        return this;
    }

    /**
     * Add a form parameter to be sent with the request. This method is the same as {@link #addParam(String, Object...)} )}
     * for all HTTP methods except PUT where this method can be used to differentiate between form and query params.
     *
     * @param parameterName   The parameter key
     * @param parameterValues Zero to many parameter values for this parameter name.
     * @return The request specification builder
     * @see #addFormParam(String, Object...)
     */
    public RequestSpecBuilder addFormParam(String parameterName, Object... parameterValues) {
        spec.formParam(parameterName, parameterValues);
        return this;
    }

    /**
     * Method to remove parameter added with {@link #addFormParam(String, Object...)} from map.
     * Removes all values of this parameter
     *
     * @param parameterName The parameter key
     * @return The request specification builder
     */
    public RequestSpecBuilder removeFormParam(String parameterName) {
        spec.removeFormParam(parameterName);
        return this;
    }

    /**
     * Specify a path parameter. Path parameters are used to improve readability of the request path. E.g. instead
     * of writing:
     *

     * expect().statusCode(200).when().get("/item/"+myItem.getItemNumber()+"/buy/"+2);
     *

     * you can write:
     *

     * given().
     *         pathParam("itemNumber", myItem.getItemNumber()).
     *         pathParam("amount", 2).
     * expect().
     *          statusCode(200).
     * when().
     *        get("/item/{itemNumber}/buy/{amount}");
     *

     *

     * which improves readability and allows the path to be reusable in many tests. Another alternative is to use:
     *

     * expect().statusCode(200).when().get("/item/{itemNumber}/buy/{amount}", myItem.getItemNumber(), 2);
     *

     *
     * @param parameterName  The parameter key
     * @param parameterValue The parameter value
     * @return The request specification
     */
    public RequestSpecBuilder addPathParam(String parameterName, Object parameterValue) {
        spec.pathParam(parameterName, parameterValue);
        return this;
    }

    /**
     * Specify multiple path parameter name-value pairs. Path parameters are used to improve readability of the request path. E.g. instead
     * of writing:
     *

     * expect().statusCode(200).when().get("/item/"+myItem.getItemNumber()+"/buy/"+2);
     *

     * you can write:
     *

     * given().
     *         pathParam("itemNumber", myItem.getItemNumber(), "amount", 2).
     * expect().
     *          statusCode(200).
     * when().
     *        get("/item/{itemNumber}/buy/{amount}");
     *

     *

     * which improves readability and allows the path to be reusable in many tests. Another alternative is to use:
     *

     * expect().statusCode(200).when().get("/item/{itemNumber}/buy/{amount}", myItem.getItemNumber(), 2);
     *

     *
     * @param firstParameterName      The name of the first parameter
     * @param firstParameterValue     The value of the first parameter
     * @param parameterNameValuePairs Additional parameters in name-value pairs.
     * @return The request specification
     */
    public RequestSpecBuilder addPathParams(String firstParameterName, Object firstParameterValue, Object... parameterNameValuePairs) {
        spec.pathParams(firstParameterName, firstParameterValue, parameterNameValuePairs);
        return this;
    }

    /**
     * Specify multiple path parameter name-value pairs. Path parameters are used to improve readability of the request path. E.g. instead
     * of writing:
     *

     * expect().statusCode(200).when().get("/item/"+myItem.getItemNumber()+"/buy/"+2);
     *

     * you can write:
     *

     * Map<String,Object> pathParams = new HashMap<String,Object>();
     * pathParams.add("itemNumber",myItem.getItemNumber());
     * pathParams.add("amount",2);
     *
     * given().
     *         pathParameters(pathParams).
     * expect().
     *          statusCode(200).
     * when().
     *        get("/item/{itemNumber}/buy/{amount}");
     *

     *

     * which improves readability and allows the path to be reusable in many tests. Another alternative is to use:
     *

     * expect().statusCode(200).when().get("/item/{itemNumber}/buy/{amount}", myItem.getItemNumber(), 2);
     *

     *
     * @param parameterNameValuePairs A map containing the path parameters.
     * @return The request specification
     */
    public RequestSpecBuilder addPathParams(Map<String, ?> parameterNameValuePairs) {
        spec.pathParams(parameterNameValuePairs);
        return this;
    }

    /**
     * Method to remove parameter added with {@link #addPathParam(String, Object)} from map.
     * Removes all values of this parameter.
     *
     * @param parameterName The parameter key
     * @return The request specification builder
     */
    public RequestSpecBuilder removePathParam(String parameterName) {
        spec.removePathParam(parameterName);
        return this;
    }

    /**
     * Specify a keystore.
     *

     * RestAssured.keyStore("/truststore_javanet.jks", "test1234");
     *

     * or
     *

     * given().keyStore("/truststore_javanet.jks", "test1234"). ..
     *

     *

     *
     * @param pathToJks The path to the JKS
     * @param password  The store pass
     */
    public RequestSpecBuilder setKeyStore(String pathToJks, String password) {
        spec.keyStore(pathToJks, password);
        return this;
    }

    /**
     * The following documentation is taken from https://github.com/jgritman/httpbuilder/wiki/SSL:
     *

     *
SSL Configuration

     *

     * SSL should, for the most part, "just work." There are a few situations where it is not completely intuitive. You can follow the example below, or see HttpClient's SSLSocketFactory documentation for more information.
     *

     *
SSLPeerUnverifiedException

     *

     * If you can't connect to an SSL website, it is likely because the certificate chain is not trusted. This is an Apache HttpClient issue, but explained here for convenience. To correct the untrusted certificate, you need to import a certificate into an SSL truststore.
     *

     * First, export a certificate from the website using your browser. For example, if you go to https://dev.java.net in Firefox, you will probably get a warning in your browser. Choose "Add Exception," "Get Certificate," "View," "Details tab." Choose a certificate in the chain and export it as a PEM file. You can view the details of the exported certificate like so:
     *

     * $ keytool -printcert -file EquifaxSecureGlobaleBusinessCA-1.crt
     * Owner: CN=Equifax Secure Global eBusiness CA-1, O=Equifax Secure Inc., C=US
     * Issuer: CN=Equifax Secure Global eBusiness CA-1, O=Equifax Secure Inc., C=US
     * Serial number: 1
     * Valid from: Mon Jun 21 00:00:00 EDT 1999 until: Sun Jun 21 00:00:00 EDT 2020
     * Certificate fingerprints:
     * MD5:  8F:5D:77:06:27:C4:98:3C:5B:93:78:E7:D7:7D:9B:CC
     * SHA1: 7E:78:4A:10:1C:82:65:CC:2D:E1:F1:6D:47:B4:40:CA:D9:0A:19:45
     * Signature algorithm name: MD5withRSA
     * Version: 3
     * ....
     *

     * Now, import that into a Java keystore file:
     *

     * $ keytool -importcert -alias "equifax-ca" -file EquifaxSecureGlobaleBusinessCA-1.crt -keystore truststore_javanet.jks -storepass test1234
     * Owner: CN=Equifax Secure Global eBusiness CA-1, O=Equifax Secure Inc., C=US
     * Issuer: CN=Equifax Secure Global eBusiness CA-1, O=Equifax Secure Inc., C=US
     * Serial number: 1
     * Valid from: Mon Jun 21 00:00:00 EDT 1999 until: Sun Jun 21 00:00:00 EDT 2020
     * Certificate fingerprints:
     * MD5:  8F:5D:77:06:27:C4:98:3C:5B:93:78:E7:D7:7D:9B:CC
     * SHA1: 7E:78:4A:10:1C:82:65:CC:2D:E1:F1:6D:47:B4:40:CA:D9:0A:19:45
     * Signature algorithm name: MD5withRSA
     * Version: 3
     * ...
     * Trust this certificate? [no]:  yes
     * Certificate was added to keystore
     *

     * Now you want to use this truststore in your client:
     *

     * RestAssured.trustStore("/truststore_javanet.jks", "test1234");
     *

     * or
     *

     * given().trustStore("/truststore_javanet.jks", "test1234"). ..
     *

     *

     *
     * @param pathToJks The path to the JKS
     * @param password  The store pass
     */
    public RequestSpecBuilder setTrustStore(String pathToJks, String password) {
        spec.trustStore(pathToJks, password);
        return this;
    }

    /**
     * The following documentation is taken from https://github.com/jgritman/httpbuilder/wiki/SSL:
     *

     *
SSL Configuration

     *

     * SSL should, for the most part, "just work." There are a few situations where it is not completely intuitive. You can follow the example below, or see HttpClient's SSLSocketFactory documentation for more information.
     *

     *
SSLPeerUnverifiedException

     *

     * If you can't connect to an SSL website, it is likely because the certificate chain is not trusted. This is an Apache HttpClient issue, but explained here for convenience. To correct the untrusted certificate, you need to import a certificate into an SSL truststore.
     *

     * First, export a certificate from the website using your browser. For example, if you go to https://dev.java.net in Firefox, you will probably get a warning in your browser. Choose "Add Exception," "Get Certificate," "View," "Details tab." Choose a certificate in the chain and export it as a PEM file. You can view the details of the exported certificate like so:
     *

     * $ keytool -printcert -file EquifaxSecureGlobaleBusinessCA-1.crt
     * Owner: CN=Equifax Secure Global eBusiness CA-1, O=Equifax Secure Inc., C=US
     * Issuer: CN=Equifax Secure Global eBusiness CA-1, O=Equifax Secure Inc., C=US
     * Serial number: 1
     * Valid from: Mon Jun 21 00:00:00 EDT 1999 until: Sun Jun 21 00:00:00 EDT 2020
     * Certificate fingerprints:
     * MD5:  8F:5D:77:06:27:C4:98:3C:5B:93:78:E7:D7:7D:9B:CC
     * SHA1: 7E:78:4A:10:1C:82:65:CC:2D:E1:F1:6D:47:B4:40:CA:D9:0A:19:45
     * Signature algorithm name: MD5withRSA
     * Version: 3
     * ....
     *

     * Now, import that into a Java keystore file:
     *

     * $ keytool -importcert -alias "equifax-ca" -file EquifaxSecureGlobaleBusinessCA-1.crt -keystore truststore_javanet.jks -storepass test1234
     * Owner: CN=Equifax Secure Global eBusiness CA-1, O=Equifax Secure Inc., C=US
     * Issuer: CN=Equifax Secure Global eBusiness CA-1, O=Equifax Secure Inc., C=US
     * Serial number: 1
     * Valid from: Mon Jun 21 00:00:00 EDT 1999 until: Sun Jun 21 00:00:00 EDT 2020
     * Certificate fingerprints:
     * MD5:  8F:5D:77:06:27:C4:98:3C:5B:93:78:E7:D7:7D:9B:CC
     * SHA1: 7E:78:4A:10:1C:82:65:CC:2D:E1:F1:6D:47:B4:40:CA:D9:0A:19:45
     * Signature algorithm name: MD5withRSA
     * Version: 3
     * ...
     * Trust this certificate? [no]:  yes
     * Certificate was added to keystore
     *

     * Now you want to use this truststore in your client:
     *

     * RestAssured.trustStore("/truststore_javanet.jks", "test1234");
     *

     * or
     *

     * given().trustStore("/truststore_javanet.jks", "test1234"). ..
     *

     *

     *
     * @param pathToJks The path to the JKS
     * @param password  The store pass
     */
    public RequestSpecBuilder setTrustStore(File pathToJks, String password) {
        spec.trustStore(pathToJks, password);
        return this;
    }

    /**
     * Add headers to be sent with the request as Map.
     *
     * @param headers The Map containing the header names and their values to send with the request.
     * @return The request specification builder
     */
    public RequestSpecBuilder addHeaders(Map<String, String> headers) {
        spec.headers(headers);
        return this;
    }

    /**
     * Add a header to be sent with the request e.g:
     *
     * @param headerName  The header name
     * @param headerValue The header value
     * @return The request specification builder
     */
    public RequestSpecBuilder addHeader(String headerName, String headerValue) {
        spec.header(headerName, headerValue);
        return this;
    }

    /**
     * Specify the content type of the request.
     *
     * @param contentType The content type of the request
     * @return The request specification builder
     * @see ContentType
     */
    public RequestSpecBuilder setContentType(ContentType contentType) {
        spec.contentType(contentType);
        return this;
    }

    /**
     * Specify the content type of the request as string.
     *
     * @param contentType The content type of the request
     * @return The request specification builder
     */
    public RequestSpecBuilder setContentType(String contentType) {
        spec.contentType(contentType);
        return this;
    }

    /**
     * Specify the accept header of the request. This just a shortcut for:
     *

     * addHeader("Accept", contentType);
     *

     *
     * @param contentType The content type whose accept header {@link ContentType#getAcceptHeader()} will be used as Accept header in the request.
     * @return The request specification
     * @see ContentType
     * @see #addHeader(String, String)
     */
    public RequestSpecBuilder setAccept(ContentType contentType) {
        spec.accept(contentType);
        return this;
    }

    /**
     * Specify the accept header of the request. This just a shortcut for:
     *

     * header("Accept", contentType);
     *

     *
     * @param mediaTypes The media type(s) that will be used as Accept header in the request.
     * @return The request specification
     * @see ContentType
     * @see #addHeader(String, String)
     */
    public RequestSpecBuilder setAccept(String mediaTypes) {
        spec.accept(mediaTypes);
        return this;
    }

    /**
     * Specify a multi-part specification. Use this method if you need to specify content-type etc.
     *
     * @param multiPartSpecification Multipart specification
     * @return The request specification
     */
    public RequestSpecBuilder addMultiPart(MultiPartSpecification multiPartSpecification) {
        spec.multiPart(multiPartSpecification);
        return this;
    }

    /**
     * Specify a file to upload to the server using multi-part form data uploading.
     * It will assume that the control name is <tt>file</tt> and the content-type is <tt>application/octet-stream</tt>.
     * If this is not what you want please use an overloaded method.
     *
     * @param file The file to upload
     * @return The request specification
     */
    public RequestSpecBuilder addMultiPart(File file) {
        spec.multiPart(file);
        return this;
    }

    /**
     * Specify a file to upload to the server using multi-part form data uploading with a specific
     * control name. It will use the content-type <tt>application/octet-stream</tt>.
     * If this is not what you want please use an overloaded method.
     *
     * @param file        The file to upload
     * @param controlName Defines the control name of the body part. In HTML this is the attribute name of the input tag.
     * @return The request specification
     */
    public RequestSpecBuilder addMultiPart(String controlName, File file) {
        spec.multiPart(controlName, file);
        return this;
    }

    /**
     * Specify a file to upload to the server using multi-part form data uploading with a specific
     * control name and content-type.
     *
     * @param file        The file to upload
     * @param controlName Defines the control name of the body part. In HTML this is the attribute name of the input tag.
     * @param mimeType    The content-type
     * @return The request specification
     */
    public RequestSpecBuilder addMultiPart(String controlName, File file, String mimeType) {
        spec.multiPart(controlName, file, mimeType);
        return this;
    }

    /**
     * Specify a byte-array to upload to the server using multi-part form data.
     * It will use the content-type <tt>application/octet-stream</tt>. If this is not what you want please use an overloaded method.
     *
     * @param controlName Defines the control name of the body part. In HTML this is the attribute name of the input tag.
     * @param fileName    The name of the content you're uploading
     * @param bytes       The bytes you want to send
     * @return The request specification
     */
    public RequestSpecBuilder addMultiPart(String controlName, String fileName, byte[] bytes) {
        spec.multiPart(controlName, fileName, bytes);
        return this;
    }

    /**
     * Specify a byte-array to upload to the server using multi-part form data.
     *
     * @param controlName Defines the control name of the body part. In HTML this is the attribute name of the input tag.
     * @param fileName    The name of the content you're uploading
     * @param bytes       The bytes you want to send
     * @param mimeType    The content-type
     * @return The request specification
     */
    public RequestSpecBuilder addMultiPart(String controlName, String fileName, byte[] bytes, String mimeType) {
        spec.multiPart(controlName, fileName, bytes, mimeType);
        return this;
    }

    /**
     * Specify an inputstream to upload to the server using multi-part form data.
     * It will use the content-type <tt>application/octet-stream</tt>. If this is not what you want please use an overloaded method.
     *
     * @param controlName Defines the control name of the body part. In HTML this is the attribute name of the input tag.
     * @param fileName    The name of the content you're uploading
     * @param stream      The stream you want to send
     * @return The request specification
     */
    public RequestSpecBuilder addMultiPart(String controlName, String fileName, InputStream stream) {
        spec.multiPart(controlName, fileName, stream);
        return this;
    }

    /**
     * Specify an inputstream to upload to the server using multi-part form data.
     *
     * @param controlName Defines the control name of the body part. In HTML this is the attribute name of the input tag.
     * @param fileName    The name of the content you're uploading
     * @param stream      The stream you want to send
     * @param mimeType    The content-type
     * @return The request specification
     */
    public RequestSpecBuilder addMultiPart(String controlName, String fileName, InputStream stream, String mimeType) {
        spec.multiPart(controlName, fileName, stream, mimeType);
        return this;
    }

    /**
     * Specify a string to send to the server using multi-part form data.
     * It will use the content-type <tt>text/plain</tt>. If this is not what you want please use an overloaded method.
     *
     * @param controlName Defines the control name of the body part. In HTML this is the attribute name of the input tag.
     * @param contentBody The string to send
     * @return The request specification
     */
    public RequestSpecBuilder addMultiPart(String controlName, String contentBody) {
        spec.multiPart(controlName, contentBody);
        return this;
    }

    /**
     * Specify a string to send to the server using multi-part form data with a specific mime-type.
     *
     * @param controlName Defines the control name of the body part. In HTML this is the attribute name of the input tag.
     * @param contentBody The string to send
     * @param mimeType    The mime-type
     * @return The request specification
     */
    public RequestSpecBuilder addMultiPart(String controlName, String contentBody, String mimeType) {
        spec.multiPart(controlName, contentBody, mimeType);
        return this;
    }

    /**
     * If you need to specify some credentials when performing a request.
     *
     * @return The request specification builder
     */
    public RequestSpecBuilder setAuth(AuthenticationScheme auth) {
        spec.setAuthenticationScheme(auth);
        return this;
    }

    /**
     * Specify the port.
     *
     * @param port The port of URI
     * @return The request specification builder
     */
    public RequestSpecBuilder setPort(int port) {
        spec.port(port);
        return this;
    }

    /**
     * Specifies if Rest Assured should url encode the URL automatically. Usually this is a recommended but in some cases
     * e.g. the query parameters are already be encoded before you provide them to Rest Assured then it's useful to disable
     * URL encoding.
     *
     * @param isEnabled Specify whether or not URL encoding should be enabled or disabled.
     * @return The request specification builder
     */
    public RequestSpecBuilder setUrlEncodingEnabled(boolean isEnabled) {
        spec.urlEncodingEnabled(isEnabled);
        return this;
    }

    /**
     * Set the session id for this request. It will use the configured session id name from the configuration (by default this is {@value SessionConfig#DEFAULT_SESSION_ID_NAME}).
     * You can configure the session id name by using:
     *

     *     RestAssured.config = newConfig().sessionConfig(new SessionConfig().sessionIdName(<sessionIdName>));
     *

     * or you can use the {@link #setSessionId(String, String)} method to set it for this request only.
     *
     * @param sessionIdValue The session id value.
     * @return The request specification
     */
    public RequestSpecBuilder setSessionId(String sessionIdValue) {
        spec.sessionId(sessionIdValue);
        return this;
    }

    /**
     * Set the session id name and value for this request. It'll override the default session id name from the configuration (by default this is {@value SessionConfig#DEFAULT_SESSION_ID_NAME}).
     * You can configure the default session id name by using:
     *

     *     RestAssured.config = newConfig().sessionConfig(new SessionConfig().sessionIdName(<sessionIdName>));
     *

     * and then you can use the {@link RequestSpecBuilder#setSessionId(String)} method to set the session id value without specifying the name for each request.
     *
     * @param sessionIdName  The session id name
     * @param sessionIdValue The session id value.
     * @return The request specification
     */
    public RequestSpecBuilder setSessionId(String sessionIdName, String sessionIdValue) {
        spec.sessionId(sessionIdName, sessionIdValue);
        return this;
    }

    /**
     * Merge this builder with settings from another specification. Note that the supplied specification
     * can overwrite data in the current specification. The following settings are overwritten:
     *

     *
Port

     *
Authentication scheme</
     *
Content type

     *
Request body

     *

     * The following settings are merged:
     *

     *
Parameters

     *
Cookies

     *
Headers

     *
Filters

     *

     *
     * @param specification The specification to add
     * @return The request specification builder
     */
    public RequestSpecBuilder addRequestSpecification(RequestSpecification specification) {
        if (!(specification instanceof RequestSpecificationImpl)) {
            throw new IllegalArgumentException("Specification must be of type " + RequestSpecificationImpl.class.getClass() + ".");
        }

        RequestSpecificationImpl rs = (RequestSpecificationImpl) specification;
        SpecificationMerger.merge((RequestSpecificationImpl) spec, rs);
        return this;
    }

    /**
     * Define a configuration for redirection settings and http client parameters.
     *
     * @param config The configuration to use for this request. If null no config will be used.
     * @return The request specification builder
     */
    public RequestSpecBuilder setConfig(RestAssuredConfig config) {
        spec.config(config);
        return this;
    }

    /**
     * Build RequestSpecBuilder.
     *
     * @return The assembled request specification
     */
    public RequestSpecification build() {
        return spec;
    }

    /**
     * Add the baseUri property from the RequestSpecBuilder instead of using static field RestAssured.baseURI.
     *

     *

     * RequestSpecBuilder builder = new RequestSpecBuilder();
     * builder.setBaseUri("http://example.com");
     * RequestSpecification specs = builder.build();
     * given().specification(specs)
     *

     *
     * @param uri The URI
     * @return RequestSpecBuilder
     */
    public RequestSpecBuilder setBaseUri(String uri) {
        spec.baseUri(uri);
        return this;
    }

    /**
     * Add the baseUri property from the RequestSpecBuilder instead of using static field RestAssured.baseURI.
     *

     *

     * RequestSpecification specs = new RequestSpecBuilder()
     *                                  .setBaseUri(URI.create("http://example.com"))
     *                                  .build();
     * given().specification(specs)
     *

     * uses {@link #setBaseUri(String)}
     *
     * @param uri The URI
     * @return RequestSpecBuilder
     */
    public RequestSpecBuilder setBaseUri(URI uri) {
        return setBaseUri(notNull(uri, "Base URI").toString());
    }

    /**
     * Set the base path that's prepended to each path by REST assured when making requests. E.g. let's say that
     * the base uri is http://localhost and basePath is /resource
     * then
     *

     *

     * ..when().get("/something");
     *

     *

     * will make a request to http://localhost/resource.
     *
     * @param path The base path to set.
     * @return RequestSpecBuilder
     */
    public RequestSpecBuilder setBasePath(String path) {
        spec.basePath(path);
        return this;
    }

    /**
     * Enabled logging with the specified log detail. Set a {@link LogConfig} to configure the print stream and pretty printing options.
     *
     * @param logDetail The log detail.
     * @return RequestSpecBuilder
     */
    public RequestSpecBuilder log(LogDetail logDetail) {
        notNull(logDetail, LogDetail.class);
        RestAssuredConfig restAssuredConfig = spec.getConfig();
        LogConfig logConfig;
        if (restAssuredConfig == null) {
            logConfig = new RestAssuredConfig().getLogConfig();
        } else {
            logConfig = restAssuredConfig.getLogConfig();
        }
        PrintStream printStream = logConfig.defaultStream();
        boolean prettyPrintingEnabled = logConfig.isPrettyPrintingEnabled();
        boolean shouldUrlEncodeRequestUri = logConfig.shouldUrlEncodeRequestUri();
        spec.filter(new RequestLoggingFilter(logDetail, prettyPrintingEnabled, printStream, shouldUrlEncodeRequestUri));
        return this;
    }

    /**
     * Use the supplied truststore for HTTPS requests. Shortcut for:
     *

     *

     * given().config(RestAssured.config().sslConfig(sslConfig().trustStore(truststore));
     *

     *

     *

     *
     * @param trustStore The truststore.
     * @return RequestSpecBuilder
     * @see #setKeyStore(String, String)
     */
    public RequestSpecBuilder setTrustStore(KeyStore trustStore) {
        spec.trustStore(trustStore);
        return this;
    }

    /**
     * Use the supplied keystore for HTTPS requests. Shortcut for:
     *

     *

     * given().config(RestAssured.config().sslConfig(sslConfig().keyStore(keystore));
     *

     *

     *

     *
     * @param keyStore The truststore.
     * @return RequestSpecBuilder
     * @see #setKeyStore(String, String)
     */
    public RequestSpecBuilder setKeyStore(KeyStore keyStore) {
        spec.keyStore(keyStore);
        return this;
    }

    /**
     * Use relaxed HTTP validation with SSLContext protocol {@value #SSL}. This means that you'll trust all hosts regardless if the SSL certificate is invalid. By using this
     * method you don't need to specify a keystore (see {@link #setKeyStore(String, String)} or trust store (see {@link #setTrustStore(java.security.KeyStore)}.
     *

     * This is just a shortcut for:
     *

     *

     * given().config(RestAssured.config().sslConfig(sslConfig().relaxedHTTPSValidation())). ..;
     *

     *
     * @return RequestSpecBuilder
     */
    public RequestSpecBuilder setRelaxedHTTPSValidation() {
        return setRelaxedHTTPSValidation(SSL);
    }

    /**
     * Use relaxed HTTP validation with a given SSLContext protocol. This means that you'll trust all hosts regardless if the SSL certificate is invalid. By using this
     * method you don't need to specify a keystore (see {@link #setKeyStore(String, String)} or trust store (see {@link #setTrustStore(java.security.KeyStore)}.
     *

     * This is just a shortcut for:
     *

     *

     * given().config(RestAssured.config().sslConfig(sslConfig().relaxedHTTPSValidation())). ..;
     *

     *
     * @param protocol The standard name of the requested protocol. See the SSLContext section in the Java Cryptography Architecture Standard Algorithm Name Documentation for information about standard protocol names.
     * @return RequestSpecBuilder
     */
    public RequestSpecBuilder setRelaxedHTTPSValidation(String protocol) {
        spec.relaxedHTTPSValidation(protocol);
        return this;
    }

    /**
     * Instruct REST Assured to connect to a proxy on the specified host and port.
     *
     * @param host The hostname of the proxy to connect to (for example 127.0.0.1)
     * @param port The port of the proxy to connect to (for example 8888)
     * @return RequestSpecBuilder
     */
    public RequestSpecBuilder setProxy(String host, int port) {
        spec.proxy(host, port);
        return this;
    }

    /**
     * Instruct REST Assured to connect to a proxy on the specified host on port 8888.
     *
     * @param host The hostname of the proxy to connect to (for example 127.0.0.1). Can also be a URI represented as a String.
     * @return RequestSpecBuilder
     * @see #setProxy(String)
     */
    public RequestSpecBuilder setProxy(String host) {
        spec.proxy(host);
        return this;
    }

    /**
     * Instruct REST Assured to connect to a proxy on the specified port on localhost.
     *
     * @param port The port of the proxy to connect to (for example 8888)
     * @return RequestSpecBuilder
     * @see #setProxy(int)
     */
    public RequestSpecBuilder setProxy(int port) {
        spec.proxy(port);
        return this;
    }

    /**
     * Instruct REST Assured to connect to a proxy on the specified port on localhost with a specific scheme.
     *
     * @param host   The hostname of the proxy to connect to (for example 127.0.0.1)
     * @param port   The port of the proxy to connect to (for example 8888)
     * @param scheme The http scheme (http or https)
     * @return RequestSpecBuilder
     */
    public RequestSpecBuilder setProxy(String host, int port, String scheme) {
        spec.proxy(host, port, scheme);
        return this;
    }

    /**
     * Instruct REST Assured to connect to a proxy using a URI.
     *
     * @param uri The URI of the proxy
     * @return RequestSpecBuilder
     */
    public RequestSpecBuilder setProxy(URI uri) {
        spec.proxy(uri);
        return this;
    }

    /**
     * Instruct REST Assured to connect to a proxy using a {@link ProxySpecification}.
     *
     * @param proxySpecification The proxy specification to use.
     * @return RequestSpecBuilder
     * @see RequestSpecification#proxy(ProxySpecification)
     */
    public RequestSpecBuilder setProxy(ProxySpecification proxySpecification) {
        spec.proxy(proxySpecification);
        return this;
    }

    /**
     * Syntactic sugar.
     *
     * @return the same RequestSpecBuilder instance
     */
    public RequestSpecBuilder and() {
        return this;
    }
}


/*
 * Copyright 2019 the original author or authors.
 *
 * 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 io.restassured.module.mockmvc.config;

import io.restassured.config.Config;
import io.restassured.config.ParamConfig;

import static io.restassured.config.ParamConfig.UpdateStrategy.MERGE;
import static io.restassured.config.ParamConfig.UpdateStrategy.REPLACE;
import static io.restassured.internal.common.assertion.AssertParameter.notNull;

/**
 * Determines how different parameter types in REST Assured MockMvc should be updated when adding multiple parameters
 * of the same type with the same name.
 */
public class MockMvcParamConfig extends ParamConfig implements Config {

    private final boolean userConfigured;
    private final UpdateStrategy queryParamsUpdateStrategy;
    private final UpdateStrategy formParamsUpdateStrategy;
    private final UpdateStrategy requestParameterUpdateStrategy;
    private final UpdateStrategy attributeUpdateStrategy;
    private final UpdateStrategy sessionUpdateStrategy;

    /**
     * Create a new instance where all parameters are merged
     */
    public MockMvcParamConfig() {
        this(MERGE, MERGE, MERGE, MERGE, MERGE, false);
    }

    /**
     * Create a new instance and specify update strategies for all parameter types.
     *
     * @param queryParamsUpdateStrategy      The update strategy for query parameters
     * @param formParamsUpdateStrategy       The update strategy for form parameters
     * @param requestParameterUpdateStrategy The update strategy for request parameters
     * @param sessionUpdateStrategy          The update strategy for session parameters
     */
    public MockMvcParamConfig(UpdateStrategy queryParamsUpdateStrategy,
                              UpdateStrategy formParamsUpdateStrategy,
                              UpdateStrategy requestParameterUpdateStrategy,
                              UpdateStrategy attributeUpdateStrategy,
                              UpdateStrategy sessionUpdateStrategy) {
        this(queryParamsUpdateStrategy, formParamsUpdateStrategy, requestParameterUpdateStrategy, attributeUpdateStrategy, sessionUpdateStrategy, true);
    }

    private MockMvcParamConfig(UpdateStrategy queryParamsUpdateStrategy, UpdateStrategy formParamsUpdateStrategy,
                               UpdateStrategy requestParameterUpdateStrategy, UpdateStrategy attributeUpdateStrategy,
                               UpdateStrategy sessionUpdateStrategy, boolean userConfigured) {
        notNull(queryParamsUpdateStrategy, "Query param update strategy");
        notNull(requestParameterUpdateStrategy, "Request param update strategy");
        notNull(formParamsUpdateStrategy, "Form param update strategy");
        notNull(attributeUpdateStrategy, "Attribute update strategy");
        notNull(sessionUpdateStrategy, "Session update strategy");
        this.queryParamsUpdateStrategy = queryParamsUpdateStrategy;
        this.formParamsUpdateStrategy = formParamsUpdateStrategy;
        this.requestParameterUpdateStrategy = requestParameterUpdateStrategy;
        this.attributeUpdateStrategy = attributeUpdateStrategy;
        this.userConfigured = userConfigured;
        this.sessionUpdateStrategy = sessionUpdateStrategy;
    }

    /**
     * Merge all parameter types.
     *
     * @return A new instance of {@link MockMvcParamConfig}
     */
    public MockMvcParamConfig mergeAllParameters() {
        return new MockMvcParamConfig(MERGE, MERGE, MERGE, MERGE, MERGE, true);
    }

    /**
     * Replace parameter values for all kinds of parameter types.
     *
     * @return A new instance of {@link MockMvcParamConfig}
     */
    public MockMvcParamConfig replaceAllParameters() {
        return new MockMvcParamConfig(REPLACE, REPLACE, REPLACE, REPLACE, REPLACE, true);
    }

    /**
     * Set form parameter update strategy to the given value.
     *
     * @param updateStrategy The update strategy to use for form parameters
     * @return A new instance of {@link MockMvcParamConfig}
     */
    public MockMvcParamConfig formParamsUpdateStrategy(UpdateStrategy updateStrategy) {
        return new MockMvcParamConfig(queryParamsUpdateStrategy, updateStrategy, requestParameterUpdateStrategy, attributeUpdateStrategy, sessionUpdateStrategy, true);
    }

    /**
     * Set request parameter update strategy to the given value.
     * A "request parameter" is a parameter that will turn into a form or query parameter depending on the request. For example:
     * <p>
     * given().param("name", "value").when().get("/x"). ..
     * </p>
     *
     * @param updateStrategy The update strategy to use for request parameters
     * @return A new instance of {@link MockMvcParamConfig}
     */
    public MockMvcParamConfig requestParamsUpdateStrategy(UpdateStrategy updateStrategy) {
        return new MockMvcParamConfig(queryParamsUpdateStrategy, formParamsUpdateStrategy, updateStrategy, attributeUpdateStrategy, sessionUpdateStrategy, true);
    }

    /**
     * Set query parameter update strategy to the given value.
     *
     * @param updateStrategy The update strategy to use for query parameters
     * @return A new instance of {@link MockMvcParamConfig}
     */
    public MockMvcParamConfig queryParamsUpdateStrategy(UpdateStrategy updateStrategy) {
        return new MockMvcParamConfig(updateStrategy, formParamsUpdateStrategy, requestParameterUpdateStrategy, attributeUpdateStrategy, sessionUpdateStrategy, true);
    }

    /**
     * Set attribute update strategy to the given value.
     *
     * @param updateStrategy The update strategy to use for attribute parameters
     * @return A new instance of {@link MockMvcParamConfig}
     */
    public MockMvcParamConfig attributeUpdateStrategy(UpdateStrategy updateStrategy) {
        return new MockMvcParamConfig(queryParamsUpdateStrategy, formParamsUpdateStrategy, requestParameterUpdateStrategy, updateStrategy, sessionUpdateStrategy, true);
    }

    /**
     * Set session parameter update strategy to the given value.
     *
     * @param updateStrategy The update strategy to use for session parameters
     * @return A new instance of {@link MockMvcParamConfig}
     */
    public MockMvcParamConfig sessionAttributesUpdateStrategy(UpdateStrategy updateStrategy) {
        return new MockMvcParamConfig(queryParamsUpdateStrategy, formParamsUpdateStrategy, requestParameterUpdateStrategy, attributeUpdateStrategy, updateStrategy, true);
    }

    /**
     * @return The update strategy for form parameters
     */
    public UpdateStrategy attributeUpdateStrategy() {
        return attributeUpdateStrategy;
    }

    /**
     * @return The update strategy for form parameters
     */
    public UpdateStrategy formParamsUpdateStrategy() {
        return formParamsUpdateStrategy;
    }

    /**
     * @return The update strategy for request parameters
     */
    public UpdateStrategy requestParamsUpdateStrategy() {
        return requestParameterUpdateStrategy;
    }

    /**
     * @return The update strategy for query parameters
     */
    public UpdateStrategy queryParamsUpdateStrategy() {
        return queryParamsUpdateStrategy;
    }

    /**
     * @return The update strategy for query parameters
     */
    public UpdateStrategy sessionAttributesUpdateStrategy() {
        return sessionUpdateStrategy;
    }

    /**
     * {@inheritDoc}
     */
    public boolean isUserConfigured() {
        return userConfigured;
    }

    /**
     * @return A static way to create a new MockMvcParamConfig instance without calling "new" explicitly. Mainly for syntactic sugar.
     */
    public static MockMvcParamConfig paramConfig() {
        return new MockMvcParamConfig();
    }

    /**
     * Syntactic sugar.
     *
     * @return The same MockMvcParamConfig instance.
     */
    public MockMvcParamConfig and() {
        return this;
    }

    /**
     * Syntactic sugar.
     *
     * @return The same MockMvcParamConfig instance.
     */
    public MockMvcParamConfig with() {
        return this;
    }
}


/*
 * Copyright 2019 the original author or authors.
 *
 * 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 io.restassured.matcher;

import io.restassured.internal.matcher.xml.XmlDtdMatcher;
import io.restassured.internal.matcher.xml.XmlXsdMatcher;
import io.restassured.response.Response;
import org.hamcrest.Matcher;

import java.io.File;
import java.io.InputStream;
import java.io.Reader;
import java.net.URL;

import static org.hamcrest.Matchers.*;

/**
 * Providers Hamcrest matchers that may be useful when validating a response.
 */
public class RestAssuredMatchers {

    /**
     * Evaluates to true if an XML string matches the supplied XSD (Xml Schema).
     *
     * @param xsd The XSD to match
     * @return The XSD matcher
     */
    public static XmlXsdMatcher matchesXsd(String xsd) {
        return XmlXsdMatcher.matchesXsd(xsd);
    }

    /**
     * Evaluates to true if an XML string matches the supplied XSD (Xml Schema).
     *
     * @param xsd The XSD to match
     * @return The XSD matcher
     */
    public static XmlXsdMatcher matchesXsd(InputStream xsd) {
        return XmlXsdMatcher.matchesXsd(xsd);
    }

    /**
     * Evaluates to true if an XML string matches the supplied XSD (Xml Schema).
     *
     * @param xsd The XSD to match
     * @return The XSD matcher
     */
    public static XmlXsdMatcher matchesXsd(Reader xsd) {
        return XmlXsdMatcher.matchesXsd(xsd);
    }

    /**
     * Evaluates to true if an XML string matches the supplied XSD (Xml Schema).
     *
     * @param xsd The XSD to match
     * @return The XSD matcher
     */
    public static XmlXsdMatcher matchesXsd(File xsd) {
        return XmlXsdMatcher.matchesXsd(xsd);
    }

    /**
     * Evaluates to true if an XML file in classpath matches the supplied XSD.
     *
     * @param path The path to the XSD located in classpath
     * @return The DTD matcher
     */
    public static XmlXsdMatcher matchesXsdInClasspath(String path) {
        return XmlXsdMatcher.matchesXsdInClasspath(path);
    }

    /**
     * Evaluates to true if an XML string matches the supplied DTD.
     *
     * @param dtd The DTD to match
     * @return The DTD matcher
     */
    public static Matcher<String> matchesDtd(String dtd) {
        return XmlDtdMatcher.matchesDtd(dtd);
    }

    /**
     * Evaluates to true if an XML string matches the supplied DTD.
     *
     * @param dtd The DTD to match
     * @return The DTD matcher
     */
    public static Matcher<String> matchesDtd(InputStream dtd) {
        return XmlDtdMatcher.matchesDtd(dtd);
    }

    /**
     * Evaluates to true if an XML string matches the supplied DTD.
     *
     * @param dtd The DTD to match
     * @return The DTD matcher
     */
    public static Matcher<String> matchesDtd(File dtd) {
        return XmlDtdMatcher.matchesDtd(dtd);
    }

    /**
     * Evaluates to true if an XML string matches the supplied DTD.
     *
     * @param url The DTD to match
     * @return The DTD matcher
     */
    public static Matcher<String> matchesDtd(URL url) {
        return XmlDtdMatcher.matchesDtd(url);
    }

    /**
     * Evaluates to true if an XML file in classpath matches the supplied DTD.
     *
     * @param path The path to the DTD file in classpath
     * @return The DTD matcher
     */
    public static Matcher<String> matchesDtdInClasspath(String path) {
        return XmlDtdMatcher.matchesDtdInClasspath(path);
    }

    /**
     * Creates a {@link ResponseAwareMatcher} that extracts the given path from the response and
     * wraps it in a {@link org.hamcrest.Matchers#equalTo(Object)} matcher. This is useful if you have a resource that e.g. returns the given JSON:
     *

     * {
     *      "userId" : "my-id",
     *      "playerId" : "my-id"
     * }
     *

     * you can then test it like this:
     *

     * get("/x").then().body("userId", equalToPath("playerId"));
     *

     *
     * @param path The path to check
     * @return A {@link ResponseAwareMatcher}
     */
    public static ResponseAwareMatcher<Response> equalToPath(final String path) {
        return response -> equalTo(response.path(path));
    }

    /**
     * Creates a {@link ResponseAwareMatcher} that extracts the given path from the response and
     * wraps it in a {@link org.hamcrest.Matchers#equalTo(Object)} matcher. This is useful if you have a resource that e.g. returns the given JSON:
     *

     * {
     *      "userId" : "my-id",
     *      "href" : "http://localhost:8080/my-id"
     * }
     *

     * you can then test it like this:
     *

     * get("/x").then().body("href", endsWithPath("userId"));
     *

     *
     * @param path The path to check
     * @return A {@link ResponseAwareMatcher}
     */
    public static ResponseAwareMatcher<Response> endsWithPath(final String path) {
        return response -> endsWith(response.<String>path(path));
    }

    /**
     * Creates a {@link ResponseAwareMatcher} that extracts the given path from the response and
     * wraps it in a {@link org.hamcrest.Matchers#equalTo(Object)} matcher. This is useful if you have a resource that e.g. returns the given JSON:
     *

     * {
     *      "userId" : "my-id",
     *      "baseUri" : "http://localhost:8080",
     *      "href" : "http://localhost:8080/my-id"
     * }
     *

     * you can then test it like this:
     *

     * get("/x").then().body("href", startsWithPath("baseUri"));
     *

     *
     * @param path The path to check
     * @return A {@link ResponseAwareMatcher}
     */
    public static ResponseAwareMatcher<Response> startsWithPath(final String path) {
        return response -> startsWith(response.<String>path(path));
    }

    /**
     * Creates a {@link ResponseAwareMatcher} that extracts the given path from the response and
     * wraps it in a {@link org.hamcrest.Matchers#equalTo(Object)} matcher. This is useful if you have a resource that e.g. returns the given JSON:
     *

     * {
     *      "userId" : "my-id",
     *      "href" : "http://localhost:8080/my-id"
     * }
     *

     * you can then test it like this:
     *

     * get("/x").then().body("href", containsPath("userId"));
     *

     *
     * @param path The path to check
     * @return A {@link ResponseAwareMatcher}
     */
    public static ResponseAwareMatcher<Response> containsPath(final String path) {
        return response -> containsString(response.<String>path(path));
    }

    public static DetailedCookieMatcher detailedCookie() {
        return new DetailedCookieMatcher();
    }
}