/*
 * 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.config;

import org.apache.http.client.HttpClient;
import org.apache.http.client.params.ClientPNames;
import org.apache.http.client.params.CookiePolicy;
import org.apache.http.cookie.params.CookieSpecPNames;
import org.apache.http.entity.mime.HttpMultipartMode;
import org.apache.http.impl.client.DefaultHttpClient;

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

import static io.restassured.internal.common.assertion.AssertParameter.notNull;
import static java.util.Arrays.asList;

/**
 * Configure the Apache HTTP Client parameters.
 *
Note that you can't configure the redirect settings from this config. Please use {@link RedirectConfig} for this purpose.

 *

 * The following parameters are applied per default:
 *

 *

 * Parameter name	Parameter value	Description
 *

 *

 * {@link ClientPNames#COOKIE_POLICY}	{@link CookiePolicy#IGNORE_COOKIES}	Don't automatically set response cookies in subsequent requests.
 *

 *

 * {@link CookieSpecPNames#DATE_PATTERNS}	[EEE, dd-MMM-yyyy HH:mm:ss z, EEE, dd MMM yyyy HH:mm:ss z]	Defines valid date patterns to be used for parsing non-standard
 * expires attribute.
 *

 *

 *

 *

 * You can also specify a http client factory that is used to create the http client instances that REST Assured uses ({@link #httpClientFactory(HttpClientConfig.HttpClientFactory)}).
 * By default the {@link DefaultHttpClient} is used. It's also possible to specify whether or not this instance should be reused in multiple requests. By default the http client instance is not reused.
 *

 *
 * @see org.apache.http.client.params.ClientPNames
 * @see org.apache.http.client.params.CookiePolicy
 * @see org.apache.http.params.CoreProtocolPNames
 */
    public class HttpClientConfig implements Config {

    private static final boolean SHOULD_REUSE_HTTP_CLIENT_INSTANCE_BY_DEFAULT = false;
    private static final HttpClient NO_HTTP_CLIENT = null;

    private final boolean shouldReuseHttpClientInstance;
    private final Map<String, ?> httpClientParams;
    private final HttpMultipartMode httpMultipartMode;
    private final HttpClientFactory httpClientFactory;
    private final boolean isUserConfigured;
    private volatile HttpClient httpClient;

    /**
     * Creates a new  HttpClientConfig instance with the {@value org.apache.http.client.params.ClientPNames#COOKIE_POLICY} parameter set to {@value org.apache.http.client.params.CookiePolicy#IGNORE_COOKIES}.
     */
    public HttpClientConfig() {
        this.httpClientFactory = defaultHttpClientFactory();

        this.httpClientParams = new HashMap<String, Object>() {
            {
                put(ClientPNames.COOKIE_POLICY, CookiePolicy.IGNORE_COOKIES);
                put(CookieSpecPNames.DATE_PATTERNS, asList("EEE, dd-MMM-yyyy HH:mm:ss z", "EEE, dd MMM yyyy HH:mm:ss z"));
            }
        };
        this.httpMultipartMode = HttpMultipartMode.STRICT;
        this.shouldReuseHttpClientInstance = SHOULD_REUSE_HTTP_CLIENT_INSTANCE_BY_DEFAULT;
        this.httpClient = null;
        this.isUserConfigured = false;
    }

    private HttpClientConfig(HttpClientFactory httpClientFactory, Map<String, ?> httpClientParams, HttpMultipartMode httpMultipartMode,
                             boolean shouldReuseHttpClientInstance, HttpClient abstractHttpClient, boolean isUserConfigured) {
        notNull(httpClientParams, "httpClientParams");
        notNull(httpMultipartMode, "httpMultipartMode");
        notNull(httpClientFactory, "Http Client factory");
        this.shouldReuseHttpClientInstance = shouldReuseHttpClientInstance;
        this.httpClientFactory = httpClientFactory;
        this.httpClientParams = new HashMap<String, Object>(httpClientParams);
        this.httpMultipartMode = httpMultipartMode;
        this.httpClient = abstractHttpClient;
        this.isUserConfigured = isUserConfigured;
    }

    /**
     * Creates a new  HttpClientConfig instance with the parameters defined by the httpClientParams.
     */
    public HttpClientConfig(Map<String, ?> httpClientParams) {
        this(defaultHttpClientFactory(), httpClientParams, HttpMultipartMode.STRICT, SHOULD_REUSE_HTTP_CLIENT_INSTANCE_BY_DEFAULT, NO_HTTP_CLIENT, true);
    }

    /**
     * @return The configured parameters
     */
    public Map<String, ?> params() {
        return Collections.unmodifiableMap(httpClientParams);
    }

    /**
     * @return The same HttpClientConfig instance. Only here for syntactic sugar.
     */
    public HttpClientConfig and() {
        return this;
    }

    /**
     * Instruct REST Assured to reuse the configured http client instance for multiple requests. By default REST Assured
     * will create a new {@link org.apache.http.client.HttpClient} instance for each request. Note that for this to work
     * the configuration must be defined statically, for example:
     *

     *

     * RestAssured.config = newConfig().httpClient(httpClientConfig().reuseHttpClientInstance());
     *

     *
     * @return An updated HttpClientConfig
     * @see #httpClientFactory(HttpClientConfig.HttpClientFactory)
     */
    public HttpClientConfig reuseHttpClientInstance() {
        return new HttpClientConfig(httpClientFactory, httpClientParams, httpMultipartMode, true, httpClient, true);
    }

    /**
     * Instruct REST Assured not to reuse the configured http client instance for multiple requests. This is the default behavior.
     *
     * @return An updated HttpClientConfig
     * @see #reuseHttpClientInstance()
     */
    public HttpClientConfig dontReuseHttpClientInstance() {
        return new HttpClientConfig(httpClientFactory, httpClientParams, httpMultipartMode, false, NO_HTTP_CLIENT, true);
    }

    /**
     * If this method returns true then REST Assured will reuse the same {@link org.apache.http.client.HttpClient} instance created
     * by the {@link #httpClientInstance()} method for all requests. If false is returned then REST Assured creates a new instance for each request.
     *

     * By default false is returned.
     *

     * Note that for this to work the configuration must be defined statically, for example:
     *

     * RestAssured.config = newConfig().httpClient(httpClientConfig().reuseHttpClientInstance());
     *

     *
     * @return true if the same HTTP Client instance should be reused between several requests, false otherwise.
     */
    public boolean isConfiguredToReuseTheSameHttpClientInstance() {
        return shouldReuseHttpClientInstance;
    }

    /**
     * Set a http client parameter.
     *
     * @param parameterName  The name of the parameter
     * @param parameterValue The value of the parameter (may be null)
     * @param <T>            The parameter type
     * @return An updated HttpClientConfig
     */
    public <T> HttpClientConfig setParam(String parameterName, T parameterValue) {
        notNull(parameterName, "Parameter name");
        final Map<String, Object> newParams = new HashMap<String, Object>(httpClientParams);
        newParams.put(parameterName, parameterValue);
        return new HttpClientConfig(httpClientFactory, newParams, httpMultipartMode, shouldReuseHttpClientInstance, NO_HTTP_CLIENT, true);
    }

    /**
     * Replaces the currently configured parameters with the ones supplied by httpClientParams. This method is the same as {@link #setParams(java.util.Map)}.
     *
     * @param httpClientParams The parameters to set.
     * @return An updated HttpClientConfig
     */
    public HttpClientConfig withParams(Map<String, ?> httpClientParams) {
        return new HttpClientConfig(httpClientFactory, httpClientParams, httpMultipartMode, shouldReuseHttpClientInstance, NO_HTTP_CLIENT, true);
    }

    /**
     * Replaces the currently configured parameters with the ones supplied by httpClientParams. This method is the same as {@link #withParams(java.util.Map)}.
     *
     * @param httpClientParams The parameters to set.
     * @return An updated HttpClientConfig
     */
    public HttpClientConfig setParams(Map<String, ?> httpClientParams) {
        return withParams(httpClientParams);
    }

    /**
     * Add the given parameters to an already configured number of parameters.
     *
     * @param httpClientParams The parameters.
     * @return An updated HttpClientConfig
     */
    public HttpClientConfig addParams(Map<String, ?> httpClientParams) {
        notNull(httpClientParams, "httpClientParams");
        final Map<String, Object> newParams = new HashMap<String, Object>(this.httpClientParams);
        newParams.putAll(httpClientParams);
        return new HttpClientConfig(httpClientFactory, newParams, httpMultipartMode, shouldReuseHttpClientInstance, NO_HTTP_CLIENT, true);
    }

    /**
     * Set the http client factory that Rest Assured should use when making request. For each request REST Assured will invoke the factory to get the a the HttpClient instance.
     *
     * @param httpClientFactory The http client factory to use.
     * @return An updated HttpClientConfig
     */
    public HttpClientConfig httpClientFactory(HttpClientFactory httpClientFactory) {
        return new HttpClientConfig(httpClientFactory, httpClientParams, httpMultipartMode, shouldReuseHttpClientInstance, NO_HTTP_CLIENT, true);
    }

    /**
     * @return The configured http client that will create an {@link org.apache.http.client.HttpClient} instances that's used by REST Assured when making a request.
     */
    public HttpClient httpClientInstance() {
        if (isConfiguredToReuseTheSameHttpClientInstance()) {
            if (httpClient == NO_HTTP_CLIENT) {
                httpClient = httpClientFactory.createHttpClient();
            }
            return httpClient;
        }
        return httpClientFactory.createHttpClient();
    }

    /**
     * Specify the HTTP Multipart mode when sending multi-part data.
     *
     * @param httpMultipartMode The multi-part mode to set.
     * @return An updated HttpClientConfig
     */
    public HttpClientConfig httpMultipartMode(HttpMultipartMode httpMultipartMode) {
        return new HttpClientConfig(httpClientFactory, httpClientParams, httpMultipartMode, shouldReuseHttpClientInstance, httpClient, true);
    }

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

    /**
     * @return The http multi-part mode.
     */
    public HttpMultipartMode httpMultipartMode() {
        return httpMultipartMode;
    }

    private static HttpClientFactory defaultHttpClientFactory() {
        return new HttpClientFactory() {
            public HttpClient createHttpClient() {
                return new DefaultHttpClient();
            }
        };
    }

    public boolean isUserConfigured() {
        return isUserConfigured;
    }

    /**
     * A factory for creating and configuring a custom http client instance that will be used by REST Assured.
     */
    public interface HttpClientFactory {
        /**
         * Create an instance of {@link HttpClient} that'll be used by REST Assured when making requests. By default
         * REST Assured creates a {@link DefaultHttpClient}.
         *

         * Important: Version 1.9.0 of REST Assured ONLY supports instances of {@link org.apache.http.impl.client.AbstractHttpClient}. The API is
         * how ever prepared for future upgrades.
         *

         *
         * @return An instance of {@link HttpClient}.
         */
        HttpClient createHttpClient();
    }
}


/*
 * 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.json;

import groovy.json.JsonBuilder;
import groovy.json.JsonOutput;
import io.restassured.common.mapper.TypeRef;
import io.restassured.internal.common.assertion.AssertParameter;
import io.restassured.internal.common.path.ObjectConverter;
import io.restassured.internal.path.json.ConfigurableJsonSlurper;
import io.restassured.internal.path.json.JSONAssertion;
import io.restassured.internal.path.json.JsonPrettifier;
import io.restassured.internal.path.json.mapping.JsonObjectDeserializer;
import io.restassured.path.json.config.JsonParserType;
import io.restassured.path.json.config.JsonPathConfig;
import io.restassured.path.json.exception.JsonPathException;
import io.restassured.path.json.mapper.factory.GsonObjectMapperFactory;
import io.restassured.path.json.mapper.factory.Jackson1ObjectMapperFactory;
import io.restassured.path.json.mapper.factory.Jackson2ObjectMapperFactory;

import java.io.*;
import java.net.URL;
import java.util.*;
import java.util.Map.Entry;

/**
 * JsonPath is an alternative to using XPath for easily getting values from a Object document. It follows the
 * Groovy GPath syntax when getting an object from the document. You can regard it as an alternative to XPath for JSON.
 * E.g. given the following Object document:
 *

 * { "store": {
 *   "book": [
 *    { "category": "reference",
 *      "author": "Nigel Rees",
 *      "title": "Sayings of the Century",
 *      "price": 8.95
 *    },
 *    { "category": "fiction",
 *      "author": "Evelyn Waugh",
 *      "title": "Sword of Honour",
 *      "price": 12.99
 *    },
 *    { "category": "fiction",
 *      "author": "Herman Melville",
 *      "title": "Moby Dick",
 *      "isbn": "0-553-21311-3",
 *      "price": 8.99
 *    },
 *    { "category": "fiction",
 *      "author": "J. R. R. Tolkien",
 *      "title": "The Lord of the Rings",
 *      "isbn": "0-395-19395-8",
 *      "price": 22.99
 *    }
 *  ],
 *    "bicycle": {
 *      "color": "red",
 *      "price": 19.95
 *    }
 *  }
 * }
 *

 * To get a list of all book categories:
 *

 * List<String> categories = with(Object).get("store.book.category");
 *

 *

 * Get the first book category:
 *

 * String category = with(Object).get("store.book[0].category");
 *

 *

 * Get the last book category:
 *

 * String category = with(Object).get("store.book[-1].category");
 *

 *

 * Get all books with price between 5 and 15:
 *

 * List<Map> books = with(Object).get("store.book.findAll { book -> book.price >= 5 && book.price <= 15 }");
 *

 *

 * The JsonPath 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 name = System.console().readLine();
 * List<Map> books = with(Object).get("store.book.findAll { book -> book.author == " + name + " }");
 *

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

 * String name = System.console().readLine();
 * List<Map> books = with(Object).param("name", name).get("store.book.findAll { book -> book.author == name }");
 *

 */
public class JsonPath {

    public static JsonPathConfig config = null;

    private final JsonParser jsonParser;
    private JsonPathConfig jsonPathConfig = null;
    private String rootPath = "";
    /**
     * Parameters for groovy console (not initialized here to save memory for queries that don't use params)
     */
    private Map<String, Object> params;

    /**
     * Instantiate a new JsonPath instance.
     *
     * @param text The text containing the Object document
     */
    public JsonPath(String text) {
        jsonParser = parseText(text);
    }

    /**
     * Instantiate a new JsonPath instance.
     *
     * @param url The url containing the Object document
     */
    public JsonPath(URL url) {
        jsonParser = parseURL(url);
    }

    /**
     * Instantiate a new JsonPath instance.
     *
     * @param stream The stream containing the Object document
     */
    public JsonPath(InputStream stream) {
        jsonParser = parseInputStream(stream);
    }

    /**
     * Instantiate a new JsonPath instance.
     *
     * @param file The file containing the Object document
     */
    public JsonPath(File file) {
        jsonParser = parseFile(file);
    }

    /**
     * Instantiate a new JsonPath instance.
     *
     * @param reader The reader containing the Object document
     */
    public JsonPath(Reader reader) {
        jsonParser = parseReader(reader);
    }

    private JsonPath(JsonPath jsonPath, JsonPathConfig jsonPathConfig) {
        this.jsonPathConfig = jsonPathConfig;
        this.jsonParser = jsonPath.jsonParser;
        this.rootPath = jsonPath.rootPath;
        if (jsonPath.params != null) {
            this.params = new HashMap<String, Object>(jsonPath.params);
        }
    }

    /**
     * Get a Object graph with no named root element as a Java object. This is just a short-cut for
     *

     *

     *     get("");
     *

     * or
     *

     *     get("$");
     *

     *
     * @return The object matching the Object graph. This may be any primitive type, a List or a Map.  A {@link java.lang.ClassCastException} will be thrown if the object
     * cannot be casted to the expected type.
     */
    public <T> T get() {
        return get("");
    }

    /**
     * Get the result of an Object path expression as a boolean.
     *
     * @param path The Object path.
     * @return The object matching the Object path. This may be any primitive type, a List or a Map.  A {@link java.lang.ClassCastException} will be thrown if the object
     * cannot be casted to the expected type.
     */
    public <T> T get(String path) {
        final JSONAssertion jsonAssertion = createJsonAssertion(path, params);
        final Object json = jsonParser.parseWith(createConfigurableJsonSlurper());
        return (T) jsonAssertion.getResult(json, null);
    }

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

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

    /**
     * Get the result of an Object path expression as an int.
     *
     * @param path The Object path.
     * @return The int matching the Object path. A {@link java.lang.ClassCastException} will be thrown if the object
     * cannot be casted to the expected type.
     */
    public int getInt(String path) {
        //The type returned from Groovy depends on the input, so we need to handle different numerical types.
        Object value = get(path);
        if (value instanceof Integer) {
            return (Integer) value;
        } else if (value instanceof Short) {
            return ((Short) value).intValue();
        } else if (value instanceof Long) {
            return ((Long) value).intValue();
        } else {
            return ObjectConverter.convertObjectTo(value, Integer.class);
        }
    }

    /**
     * Get the result of an Object path expression as a byte.
     *
     * @param path The Object path.
     * @return The object matching the Object path. A {@link java.lang.ClassCastException} will be thrown if the object
     * cannot be casted to the expected type.
     */
    public byte getByte(String path) {
        //The type returned from Groovy depends on the input, so we need to handle different numerical types.
        Object value = get(path);
        if (value instanceof Byte) {
            return (Byte) value;
        } else if (value instanceof Long) {
            return ((Long) value).byteValue();
        } else if (value instanceof Integer) {
            return ((Integer) value).byteValue();
        } else {
            return ObjectConverter.convertObjectTo(value, Byte.class);
        }
    }

    /**
     * Get the result of an Object path expression as a short.
     *
     * @param path The Object path.
     * @return The object matching the Object path. A {@link java.lang.ClassCastException} will be thrown if the object
     * cannot be casted to the expected type.
     */
    public short getShort(String path) {
        //The type returned from Groovy depends on the input, so we need to handle different numerical types.
        Object value = get(path);
        if (value instanceof Short) {
            return (Short) value;
        } else if (value instanceof Long) {
            return ((Long) value).shortValue();
        } else if (value instanceof Integer) {
            return ((Integer) value).shortValue();
        } else {
            return ObjectConverter.convertObjectTo(value, Short.class);
        }
    }

    /**
     * Get the result of an Object path expression as a float.
     *
     * @param path The Object path.
     * @return The object matching the Object path. A {@link java.lang.ClassCastException} will be thrown if the object
     * cannot be casted to the expected type.
     */
    public float getFloat(String path) {
        final Object value = get(path);
        //Groovy will always return a Double for floating point values.
        if (value instanceof Double) {
            return ((Double) value).floatValue();
        } else {
            return ObjectConverter.convertObjectTo(value, Float.class);
        }
    }

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

    /**
     * Get the result of an Object path expression as a long.
     *
     * @param path The Object path.
     * @return The object matching the Object path. A {@link java.lang.ClassCastException} will be thrown if the object
     * cannot be casted to the expected type.
     */
    public long getLong(String path) {
        //The type returned from Groovy depends on the input, so we need to handle different numerical types.
        Object value = get(path);
        if (value instanceof Long) {
            return (Long) value;
        } else if (value instanceof Short) {
            return ((Short) value).longValue();
        } else if (value instanceof Integer) {
            return ((Integer) value).longValue();
        } else {
            return ObjectConverter.convertObjectTo(value, Long.class);
        }
    }

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

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

    /**
     * Get the result of an Object path expression as a list.
     *
     * @param path The Object path.
     * @param <T>  The list type
     * @return The object matching the Object 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 get(path);
    }

    /**
     * Get the result of an Object path expression as a list.
     *
     * @param path        The Object path.
     * @param genericType The generic list type
     * @param <T>         The type
     * @return The object matching the Object 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) {
        if (genericType == null) {
            throw new IllegalArgumentException("Generic type cannot be null");
        }
        final List<T> original = get(path);
        final List<T> newList = new LinkedList<T>();
        if (original != null) {
            for (T t : original) {
                T e;
                if (t instanceof Map && !genericType.isAssignableFrom(Map.class)) {
                    // TODO Avoid double parsing
                    String str = objectToString(t);
                    //noinspection unchecked
                    e = (T) jsonStringToObject(str, genericType);
                } else {
                    e = ObjectConverter.convertObjectTo(t, genericType);
                }
                newList.add(e);
            }
        }
        return Collections.unmodifiableList(newList);
    }

    /**
     * Get the result of an Object path expression as a map.
     *
     * @param path The Object path.
     * @param <K>  The type of the expected key
     * @param <V>  The type of the expected value
     * @return The map matching the Object 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 Object path expression as a map.
     *
     * @param path      The Object 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 map matching the Object 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 : ObjectConverter.convertObjectTo(entry.getKey(), keyType);
            final V value = entry.getValue() == null ? null : ObjectConverter.convertObjectTo(entry.getValue(), valueType);
            newMap.put(key, value);
        }
        return Collections.unmodifiableMap(newMap);
    }

    /**
     * Get the result of a Object path expression as a java Object.
     * E.g. given the following Object document:
     *

     * { "store": {
     *   "book": [
     *    { "category": "reference",
     *      "author": "Nigel Rees",
     *      "title": "Sayings of the Century",
     *      "price": 8.95
     *    },
     *    { "category": "fiction",
     *      "author": "Evelyn Waugh",
     *      "title": "Sword of Honour",
     *      "price": 12.99
     *    },
     *    { "category": "fiction",
     *      "author": "Herman Melville",
     *      "title": "Moby Dick",
     *      "isbn": "0-553-21311-3",
     *      "price": 8.99
     *    },
     *    { "category": "fiction",
     *      "author": "J. R. R. Tolkien",
     *      "title": "The Lord of the Rings",
     *      "isbn": "0-395-19395-8",
     *      "price": 22.99
     *    }
     *  ],
     *    "bicycle": {
     *      "color": "red",
     *      "price": 19.95
     *    }
     *  }
     * }
     *

     * And a Java object like this:
     *

     *

     * public class Book {
     *      private String category;
     *      private String author;
     *      private String title;
     *      private String isbn;
     *      private float price;
     *
     *      public String getCategory() {
     *         return category;
     *      }
     *
     *     public void setCategory(String category) {
     *         this.category = category;
     *     }
     *
     *    public String getAuthor() {
     *          return author;
     *     }
     *
     *    public void setAuthor(String author) {
     *         this.author = author;
     *    }
     *
     *    public String getTitle() {
     *         return title;
     *    }
     *
     *    public void setTitle(String title) {
     *        this.title = title;
     *    }
     *
     *    public String getIsbn() {
     *             return isbn;
     *    }
     *
     *    public void setIsbn(String isbn) {
     *          this.isbn = isbn;
     *    }
     *
     *    public float getPrice() {
     *        return price;
     *    }
     *
     *    public void setPrice(float price) {
     *             this.price = price;
     *   }
     * }
     *

     *

     * Then
     *

     * Book book = from(Object).getObject("store.book[2]", Book.class);
     *

     *

     * maps the second book to a Book instance.
     *
     * @param path       The path to the object to map
     * @param objectType The class type of the expected object
     * @param <T>        The type of the expected object
     * @return The object
     */
    public <T> T getObject(String path, Class<T> objectType) {
        Object object = getJsonObject(path);
        if (object == null) {
            return null;
        } else if (object instanceof List || object instanceof Map) {
            // TODO Avoid double parsing
            object = objectToString(object);
        } else {
            return ObjectConverter.convertObjectTo(object, objectType);
        }

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

        return (T) jsonStringToObject((String) object, objectType);
    }

    /**
     * Get the result of a Object path expression as a java Object with generic type.
     * E.g. given the following Object document:
     *

     * { "store": {
     *   "book": [
     *    { "category": "reference",
     *      "author": "Nigel Rees",
     *      "title": "Sayings of the Century",
     *      "price": 8.95
     *    },
     *    { "category": "fiction",
     *      "author": "Evelyn Waugh",
     *      "title": "Sword of Honour",
     *      "price": 12.99
     *    },
     *    { "category": "fiction",
     *      "author": "Herman Melville",
     *      "title": "Moby Dick",
     *      "isbn": "0-553-21311-3",
     *      "price": 8.99
     *    },
     *    { "category": "fiction",
     *      "author": "J. R. R. Tolkien",
     *      "title": "The Lord of the Rings",
     *      "isbn": "0-395-19395-8",
     *      "price": 22.99
     *    }
     *  ],
     *    "bicycle": {
     *      "color": "red",
     *      "price": 19.95
     *    }
     *  }
     * }
     *

     * And you want to get a book as a Map<String, Object>:
     *

     * Then
     *

     * Map<String, Object> book = from(Object).getObject("store.book[2]", new TypeRef<Map<String, Object>>() {});
     *

     *

     * maps the second book to a Book instance.
     *
     * @param path       The path to the object to map
     * @param typeRef    The class type of the expected object
     * @param <T>        The type of the expected object
     * @return The object
     */
    public <T> T getObject(String path, TypeRef<T> typeRef) {
        AssertParameter.notNull("objectType", "Type ref");
        return getObject(path, typeRef.getTypeAsClass());
    }

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

     * String name = System.console().readLine();
     * List<Map> books = with(Object).param("name", name).get("store.book.findAll { book -> book.author == name }");
     *

     *
     * @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 JsonPath instance with the parameter set
     */
    public JsonPath param(String key, Object value) {
        JsonPath newP = new JsonPath(this, config);
        if (newP.params == null) {
            newP.params = new HashMap<String, Object>();
        }
        newP.params.put(key, value);
        return newP;
    }

    /**
     * Peeks into the JSON that JsonPath will parse by printing it to the console. You can
     * continue working with JsonPath 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 JsonPath) and the JSON is rendered
     * from this data structure.
     *

     *
     * @return The same JsonPath instance
     */
    public JsonPath peek() {
        System.out.println(toJsonString());
        return this;
    }

    /**
     * Peeks into the JSON that JsonPath will parse by printing it to the console in a prettified manner. You can
     * continue working with JsonPath 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 JsonPath) and the JSON is rendered
     * from this data structure.
     *

     *
     * @return The same JsonPath instance
     */
    public JsonPath prettyPeek() {
        prettyPrint();
        return this;
    }

    /**
     * Get the JSON 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 JsonPath) and the JSON is rendered
     * from this data structure.
     *

     *
     * @return The JSON as a prettified String.
     */
    public String prettify() {
        final String jsonString = toJsonString();
        return JsonPrettifier.prettifyJson(jsonString);
    }

    /**
     * Get and print the JSON 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 JsonPath) and the JSON is rendered
     * from this data structure.
     *

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

    /**
     * Configure JsonPath to use a specific Gson object mapper factory
     *
     * @param factory The gson object mapper factory instance
     * @return a new JsonPath instance
     */
    public JsonPath using(GsonObjectMapperFactory factory) {
        return new JsonPath(this, jsonPathConfig.gsonObjectMapperFactory(factory));
    }

    /**
     * Configure JsonPath to use a specific Jackson object mapper factory
     *
     * @param factory The Jackson object mapper factory instance
     * @return a new JsonPath instance
     */
    public JsonPath using(Jackson1ObjectMapperFactory factory) {
        return new JsonPath(this, getJsonPathConfig().jackson1ObjectMapperFactory(factory));
    }

    /**
     * Configure JsonPath to use a specific Jackson 2 object mapper factory
     *
     * @param factory The Jackson 2 object mapper factory instance
     * @return a new JsonPath instance
     */
    public JsonPath using(Jackson2ObjectMapperFactory factory) {
        return new JsonPath(this, getJsonPathConfig().jackson2ObjectMapperFactory(factory));
    }

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

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

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

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

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

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

    /**
     * Instantiate a new JsonPath instance.
     *
     * @param url The URL containing the Object document
     */
    public static JsonPath given(URL url) {
        return new JsonPath(url);
    }

    /**
     * Instantiate a new JsonPath instance.
     *
     * @param stream The stream containing the Object document
     */
    public static JsonPath with(InputStream stream) {
        return new JsonPath(stream);
    }

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

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

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

    /**
     * Instantiate a new JsonPath instance.
     *
     * @param url The URI containing the Object document
     */
    public static JsonPath with(URL url) {
        return new JsonPath(url);
    }

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

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

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

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

    /**
     * Instantiate a new JsonPath instance.
     *
     * @param url The URI containing the Object document
     */
    public static JsonPath from(URL url) {
        return new JsonPath(url);
    }

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

     * final JsonPath jsonPath = new JsonPath(Object).setRoot("store.book");
     * assertThat(jsonPath.getInt("size()"), equalTo(4));
     * assertThat(jsonPath.getList("author", String.class), hasItem("J. R. R. Tolkien"));
     *

     *
     * @param rootPath The root path to use.
     * @deprecated Use {@link #setRootPath(String)} instead
     */
    @Deprecated
    public JsonPath 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 JsonPath jsonPath = new JsonPath(Object).setRootPath("store.book");
     * assertThat(jsonPath.getInt("size()"), equalTo(4));
     * assertThat(jsonPath.getList("author", String.class), hasItem("J. R. R. Tolkien"));
     *

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

    private JsonParser parseInputStream(final InputStream stream) {
        return new JsonParser() {
            @Override
            public Object doParseWith(final ConfigurableJsonSlurper slurper) {
                return new ExceptionCatcher() {
                    protected Object method() throws Exception {
                        return slurper.parse(toReader(stream));
                    }
                }.invoke();
            }
        };
    }

    private JsonParser parseReader(final Reader reader) {
        return new JsonParser() {
            @Override
            public Object doParseWith(final ConfigurableJsonSlurper slurper) {
                return new ExceptionCatcher() {
                    protected Object method() throws Exception {
                        return slurper.parse(reader);
                    }
                }.invoke();
            }
        };
    }

    private JsonParser parseFile(final File file) {
        return new JsonParser() {
            @Override
            public Object doParseWith(final ConfigurableJsonSlurper slurper) {
                return new ExceptionCatcher() {
                    protected Object method() throws Exception {
                        return slurper.parse(new FileReader(file));
                    }
                }.invoke();
            }
        };
    }

    private JsonParser parseText(final String text) {
        return new JsonParser() {
            @Override
            public Object doParseWith(final ConfigurableJsonSlurper slurper) {
                return new ExceptionCatcher() {
                    protected Object method() throws Exception {
                        return slurper.parseText(text);
                    }
                }.invoke();
            }
        };
    }

    private JsonParser parseURL(final URL url) {
        return new JsonParser() {
            @Override
            public Object doParseWith(final ConfigurableJsonSlurper slurper) {
                return new ExceptionCatcher() {
                    protected Object method() throws Exception {
                        return slurper.parse(toReader(url.openStream()));
                    }
                }.invoke();
            }
        };
    }

    private BufferedReader toReader(InputStream in) {
        final JsonPathConfig cfg = getJsonPathConfig();
        try {
            return new BufferedReader(new InputStreamReader(in, cfg.charset()));
        } catch (UnsupportedEncodingException e) {
            throw new IllegalArgumentException("Charset is invalid", e);
        }
    }

    private abstract class ExceptionCatcher {

        protected abstract Object method() throws Exception;

        public Object invoke() {
            try {
                return method();
            } catch (Exception e) {
                throw new JsonPathException("Failed to parse the JSON document", e);
            }
        }
    }

    public <T> T getJsonObject(String path) {
        final JSONAssertion jsonAssertion = createJsonAssertion(path, params);
        final Object json = jsonParser.parseWith(createConfigurableJsonSlurper());
        return (T) jsonAssertion.getAsJsonObject(json);
    }

    private JSONAssertion createJsonAssertion(String path, Map<String, Object> params) {
        AssertParameter.notNull(path, "path");
        final JSONAssertion jsonAssertion = new JSONAssertion();
        final boolean appendDot = !rootPath.equals("") && !rootPath.endsWith(".") && !path.startsWith("[");
        final String root = appendDot ? rootPath + "." : rootPath;
        jsonAssertion.setKey(root + path);
        if (params != null) {
            jsonAssertion.setParams(params);
        }
        return jsonAssertion;
    }

    private ConfigurableJsonSlurper createConfigurableJsonSlurper() {
        JsonPathConfig cfg = getJsonPathConfig();
        return new ConfigurableJsonSlurper(cfg.numberReturnType());
    }

    private JsonPathConfig getJsonPathConfig() {
        JsonPathConfig cfg;
        if (config == null && jsonPathConfig == null) {
            cfg = new JsonPathConfig();
        } else if (jsonPathConfig != null) {
            cfg = jsonPathConfig;
        } else {
            cfg = config;
        }
        return cfg;
    }

    private abstract class JsonParser {
        private Object json;

        public final Object parseWith(ConfigurableJsonSlurper slurper) {
            if (json == null) {
                json = doParseWith(slurper);
            }
            return json;
        }

        abstract Object doParseWith(ConfigurableJsonSlurper slurper);

    }

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

    private String toJsonString() {
        final Object json = jsonParser.parseWith(createConfigurableJsonSlurper());
        final String jsonString;
        if (json instanceof Map) {
            jsonString = JsonOutput.toJson((Map) json);
        } else {
            jsonString = JsonOutput.toJson(json);
        }
        return jsonString;
    }

    private String objectToString(Object object) {
        return new JsonBuilder(object).toString();
    }

    private Object jsonStringToObject(String object, Class objectType) {
        JsonPathConfig cfg = new JsonPathConfig(getJsonPathConfig());
        if (cfg.hasCustomJackson10ObjectMapperFactory()) {
            cfg = cfg.defaultParserType(JsonParserType.JACKSON_1);
        } else if (cfg.hasCustomGsonObjectMapperFactory()) {
            cfg = cfg.defaultParserType(JsonParserType.GSON);
        } else if (cfg.hasCustomJackson20ObjectMapperFactory()) {
            cfg = cfg.defaultParserType(JsonParserType.JACKSON_2);
        } else if (cfg.hasCustomJohnzonObjectMapperFactory()) {
            cfg = cfg.defaultParserType(JsonParserType.JOHNZON);
        }

        //noinspection unchecked
        return JsonObjectDeserializer.deserialize(object, objectType, cfg);
    }
}


/*
 * 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.config;

import io.restassured.path.xml.XmlPath;
import org.apache.commons.lang3.Validate;

import java.util.HashMap;
import java.util.Map;

/**
 * Allows you to configure properties of XML and HTML parsing.
 */
public class XmlConfig implements Config {
    private static final boolean DEFAULT_VALIDATING = false;
    private static final boolean DEFAULT_NAMESPACE_AWARE = true;
    private static final boolean DEFAULT_ALLOW_DOC_TYPE_DECLARATION = false;

    private final Map<String, Object> properties;
    private final Map<String, Boolean> features;
    private final Map<String, String> declaredNamespaces;
    private final boolean validating;
    private final boolean namespaceAware;
    private final boolean allowDocTypeDeclaration;
    private final boolean isUserConfigured;

    /**
     * Create a new instance of XmlConfig without any features and that is namespace unaware.
     */
    public XmlConfig() {
        this(new HashMap<String, Boolean>(), new HashMap<String, String>(), new HashMap<String, Object>(),
                DEFAULT_VALIDATING, DEFAULT_NAMESPACE_AWARE, DEFAULT_ALLOW_DOC_TYPE_DECLARATION, false);
    }

    private XmlConfig(Map<String, Boolean> features, Map<String, String> declaredNamespaces, Map<String, Object> properties,
                      boolean validating, boolean namespaceAware, boolean allowDocTypeDeclaration, boolean isUserConfigured) {
        Validate.notNull(features, "Features cannot be null");
        Validate.notNull(declaredNamespaces, "Declared namespaces cannot be null");
        Validate.notNull(properties, "Properties cannot be null");
        this.validating = validating;
        this.namespaceAware = namespaceAware;
        this.allowDocTypeDeclaration = allowDocTypeDeclaration;
        this.features = features;
        this.declaredNamespaces = declaredNamespaces;
        this.properties = properties;
        this.isUserConfigured = isUserConfigured;
    }

    /**
     * @return A map containing features that will be used by the underlying {@link groovy.util.XmlSlurper}.
     * @see org.xml.sax.XMLReader#setFeature(java.lang.String, boolean)
     */
    public Map<String, Boolean> features() {
        return new HashMap<String, Boolean>(features);
    }

    /**
     * @return A map containing properties that will be used by the underlying {@link groovy.util.XmlSlurper}.
     * @see org.xml.sax.XMLReader#setProperty(String, Object)
     */
    public Map<String, Object> properties() {
        return new HashMap<String, Object>(properties);
    }

    /**
     * Specify features that will be used when parsing XML.
     *
     * @param features A map containing features that will be used by the underlying {@link groovy.util.XmlSlurper}.
     * @return
     * @see org.xml.sax.XMLReader#setFeature(java.lang.String, boolean)
     */
    public XmlConfig features(Map<String, Boolean> features) {
        return new XmlConfig(features, declaredNamespaces, properties, validating, namespaceAware, allowDocTypeDeclaration, true);
    }

    /**
     * Specify properties that will be used when parsing XML.
     *
     * @param properties A map containing properties that will be used by the underlying {@link groovy.util.XmlSlurper}.
     * @return
     * @see org.xml.sax.XMLReader#setProperty(String, Object)
     */
    public XmlConfig properties(Map<String, Object> properties) {
        return new XmlConfig(features, declaredNamespaces, this.properties, validating, namespaceAware, allowDocTypeDeclaration, true);
    }

    /**
     * Set a value of a feature flag.
     *
     * @param uri     The feature name, which is a fully-qualified URI.
     * @param enabled The requested value of the feature (true or false).
     * @return A new XmlConfig instance
     * @see org.xml.sax.XMLReader#setFeature(java.lang.String, boolean)
     */
    public XmlConfig feature(String uri, boolean enabled) {
        Validate.notEmpty(uri, "URI cannot be empty");
        Map<String, Boolean> newFeatures = new HashMap<String, Boolean>(features);
        newFeatures.put(uri, enabled);
        return new XmlConfig(newFeatures, declaredNamespaces, properties, validating, namespaceAware, allowDocTypeDeclaration, true);
    }

    /**
     * Set a value of a property.
     *
     * @param name  The property name.
     * @param value The requested value of the feature (true or false).
     * @return A new XmlConfig instance
     * @see org.xml.sax.XMLReader#setFeature(java.lang.String, boolean)
     */
    public XmlConfig property(String name, Object value) {
        Validate.notEmpty(name, "Name cannot be empty");
        Map<String, Object> newProperties = new HashMap<String, Object>(properties);
        newProperties.put(name, value);
        return new XmlConfig(features, declaredNamespaces, newProperties, validating, namespaceAware, allowDocTypeDeclaration, true);
    }

    /**
     * @return A map containing namespaces that will be used by the underlying {@link groovy.util.XmlSlurper}.
     * @see org.xml.sax.XMLReader#setFeature(java.lang.String, boolean)
     */
    public Map<String, String> declaredNamespaces() {
        return new HashMap<String, String>(declaredNamespaces);
    }

    /**
     * Specify declared namespaces that will be used when parsing XML. Will also set {@link #namespaceAware(boolean)} to true of namespaces are not empty.
     *
Note that you cannot use this to add namespaces for the {@link org.hamcrest.xml.HasXPath} matcher.
     * This has to be done by providing a {@link javax.xml.namespace.NamespaceContext} to the matcher instance.

     *
     * @param namespacesToDeclare A map containing features that will be used by the underlying {@link groovy.util.XmlSlurper}.
     * @return A new instance of XmlConfig
     * @see org.xml.sax.XMLReader#setFeature(java.lang.String, boolean)
     */
    public XmlConfig declareNamespaces(Map<String, String> namespacesToDeclare) {
        final boolean shouldBeNamespaceAware = namespacesToDeclare == null ? namespaceAware : !namespacesToDeclare.isEmpty();
        return new XmlConfig(features, namespacesToDeclare, properties, validating, shouldBeNamespaceAware, allowDocTypeDeclaration, true);
    }

    /**
     * Declares a namespace and also sets {@link #namespaceAware(boolean)} to true.
     *

     *
Note that you cannot use this to add namespaces for the {@link org.hamcrest.xml.HasXPath} matcher.
     * This has to be done by providing a {@link javax.xml.namespace.NamespaceContext} to the matcher instance.

     *
     * @param prefix       The feature name, which is a fully-qualified URI.
     * @param namespaceURI The requested value of the feature (true or false).
     * @return A new XmlConfig instance
     * @see org.xml.sax.XMLReader#setFeature(java.lang.String, boolean)
     */
    public XmlConfig declareNamespace(String prefix, String namespaceURI) {
        Validate.notEmpty(prefix, "Prefix cannot be empty");
        Validate.notEmpty(namespaceURI, "Namespace URI cannot be empty");
        Map<String, String> updatedNamespaces = new HashMap<String, String>(declaredNamespaces);
        updatedNamespaces.put(prefix, namespaceURI);
        return new XmlConfig(features, updatedNamespaces, properties, validating, true, allowDocTypeDeclaration, true);
    }

    /**
     * Disables external DTD loading.
     *

     * This is a shortcut for doing:

     *

     * setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
     *

     *

     *
     * @return A new XmlConfig instance
     * @see #feature(String, boolean)
     */
    public XmlConfig disableLoadingOfExternalDtd() {
        Map<String, Boolean> newFeatures = new HashMap<String, Boolean>(features);
        newFeatures.put("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
        return new XmlConfig(newFeatures, declaredNamespaces, properties, validating, namespaceAware, allowDocTypeDeclaration, true);
    }

    /**
     * Configure if XmlPath should validate documents as they are parsed (default is {@value #DEFAULT_VALIDATING}).
     * Note that this is only applicable when {@link XmlPath.CompatibilityMode} is equal to
     * {@link XmlPath.CompatibilityMode#XML}.
     *
     * @param isValidating true if the parser should validate documents as they are parsed; false otherwise.
     * @return A new XmlPathConfig instance
     */
    public XmlConfig validating(boolean isValidating) {
        return new XmlConfig(features, declaredNamespaces, properties, isValidating, namespaceAware, allowDocTypeDeclaration, true);
    }

    /**
     * Whether XmlPath should validate documents as they are parsed.
     *
     * @return a boolean indicating whether this is true or false
     */
    public boolean isValidating() {
        return validating;
    }

    /**
     * Configure whether or not REST Assured should be aware of namespaces when parsing XML (default is {@value #DEFAULT_NAMESPACE_AWARE}).
     * Note that this is only applicable when {@link XmlPath.CompatibilityMode} is equal to
     * {@link XmlPath.CompatibilityMode#XML}.
     *
     * @param shouldBeAwareOfNamespaces true if xml parsing should take namespaces into account.
     * @return A new XmlConfig instance
     */
    public XmlConfig namespaceAware(boolean shouldBeAwareOfNamespaces) {
        return new XmlConfig(features, declaredNamespaces, properties, validating, shouldBeAwareOfNamespaces, allowDocTypeDeclaration, true);
    }

    /**
     * @return true if REST Assured should be namespace aware.
     */
    public boolean isNamespaceAware() {
        return namespaceAware;
    }

    /**
     * Configure if XmlPath should provide support for DOCTYPE declarations (default is {@value #DEFAULT_ALLOW_DOC_TYPE_DECLARATION}).
     * Note that this is only applicable when {@link XmlPath.CompatibilityMode} is equal to
     * {@link XmlPath.CompatibilityMode#XML}.
     *
     * @param allowDocTypeDeclaration true if the parser should provide support for DOCTYPE declarations; false otherwise.
     * @return A new XmlPathConfig instance
     */
    public XmlConfig allowDocTypeDeclaration(boolean allowDocTypeDeclaration) {
        return new XmlConfig(features, declaredNamespaces, properties, validating, namespaceAware, allowDocTypeDeclaration, true);
    }

    /**
     * Whether XmlPath should provide support for DOCTYPE declarations
     *
     * @return a boolean indicating whether this is true or false
     */
    public boolean isAllowDocTypeDeclaration() {
        return allowDocTypeDeclaration;
    }

    /**
     * For syntactic sugar.
     *
     * @return The same XmlConfig instance
     */
    public XmlConfig with() {
        return this;
    }

    /**
     * For syntactic sugar.
     *
     * @return The same XmlConfig instance
     */
    public XmlConfig and() {
        return this;
    }

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

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


/*
 * 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.config;

import io.restassured.http.ContentType;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;

import java.nio.charset.Charset;
import java.util.*;

import static io.restassured.internal.common.assertion.AssertParameter.notNull;
import static org.apache.commons.lang3.StringUtils.trim;

/**
 * Allows you to specify configuration for the decoder.
 */
public class DecoderConfig implements Config {

    private static final boolean DEFAULT_NO_WRAP_FOR_INFLATE_ENCODED_STREAMS = false;

    private static final String UTF_8 = "UTF-8";
    private static final Map<String, String> DEFAULT_CHARSET_FOR_CONTENT_TYPE = new HashMap<String, String>() {{
        put(ContentType.JSON.toString(), UTF_8);
        put("text/json", UTF_8);
    }};
    private final String defaultContentCharset;
    private final List<ContentDecoder> contentDecoders;
    private final Map<String, String> contentTypeToDefaultCharset;
    private final boolean useNoWrapForInflateDecoding;
    private final boolean isUserConfigured;

    /**
     * Configure the decoder config to use the default charset as specified by {@link java.nio.charset.Charset#defaultCharset()} for content decoding.
     */
    public DecoderConfig() {
        this(Charset.defaultCharset().toString(), DEFAULT_NO_WRAP_FOR_INFLATE_ENCODED_STREAMS, false, DEFAULT_CHARSET_FOR_CONTENT_TYPE, defaultContentEncoders());
    }

    /**
     * Configure the decoder config to use supplied defaultContentCharset for content decoding if a charset is not specified in the response.
     *
     * @param defaultContentCharset The charset to use if not specifically specified in the response.
     */
    public DecoderConfig(String defaultContentCharset) {
        this(defaultContentCharset, DEFAULT_NO_WRAP_FOR_INFLATE_ENCODED_STREAMS, true, DEFAULT_CHARSET_FOR_CONTENT_TYPE, defaultContentEncoders());
    }

    /**
     * Configure the decoder config to use {@link java.nio.charset.Charset#defaultCharset()} for content decoding.
     * Also specify the content decoders that will be presented to the server when making a request (using the Accept-Encoding header).
     * If the server supports any of these encodings then REST Assured will automatically perform decoding of the response accordingly.
     *

     * By default {@link ContentDecoder#GZIP} and {@link ContentDecoder#DEFLATE} are used.
     *

     *
     * @param contentDecoder            The first content decoder
     * @param additionalContentDecoders Optional additional content decoders
     */
    public DecoderConfig(ContentDecoder contentDecoder, ContentDecoder... additionalContentDecoders) {
        this(Charset.defaultCharset().toString(), DEFAULT_NO_WRAP_FOR_INFLATE_ENCODED_STREAMS, true, DEFAULT_CHARSET_FOR_CONTENT_TYPE, merge(contentDecoder, additionalContentDecoders));
    }

    private DecoderConfig(String defaultContentCharset, boolean useNoWrapForInflateDecoding, boolean isUserConfigured,
                          Map<String, String> contentTypeToDefaultCharset, ContentDecoder... contentDecoders) {
        this(defaultContentCharset, useNoWrapForInflateDecoding, isUserConfigured,
                contentDecoders == null ? Collections.<ContentDecoder>emptyList() : Arrays.asList(contentDecoders), contentTypeToDefaultCharset);
    }

    private DecoderConfig(String defaultContentCharset, boolean useNoWrapForInflateDecoding, boolean isUserConfigured, List<ContentDecoder> contentDecoders, Map<String, String> contentTypeToDefaultCharset) {
        Validate.notBlank(defaultContentCharset, "Default decoder content charset to cannot be blank");
        this.contentTypeToDefaultCharset = new HashMap<String, String>(contentTypeToDefaultCharset);
        this.defaultContentCharset = defaultContentCharset;
        this.contentDecoders = Collections.unmodifiableList(contentDecoders == null ? Collections.<ContentDecoder>emptyList() : contentDecoders);
        this.useNoWrapForInflateDecoding = useNoWrapForInflateDecoding;
        this.isUserConfigured = isUserConfigured;
    }

    /**
     * Specify the default charset to use for the specific content-type if it's not specified in the content-type header explicitly
     *
     * @param charset     The charset to use as default (unless specified explicitly)
     * @param contentType The content-type
     * @return A new instance of {@link DecoderConfig}
     */
    public DecoderConfig defaultCharsetForContentType(String charset, String contentType) {
        notNull(charset, "Charset");
        notNull(contentType, "ContentType");
        Map<String, String> map = new HashMap<String, String>(contentTypeToDefaultCharset);
        map.put(trim(contentType).toLowerCase(), trim(charset));
        return new DecoderConfig(charset, useNoWrapForInflateDecoding, true, contentDecoders, map);
    }

    /**
     * Specify the default charset to use for the specific content-type if it's not specified in the content-type header explicitly
     *
     * @param charset     The charset to use as default (unless specified explicitly)
     * @param contentType The content-type
     * @return A new instance of {@link DecoderConfig}
     */
    public DecoderConfig defaultCharsetForContentType(Charset charset, String contentType) {
        notNull(charset, "Charset");
        return defaultCharsetForContentType(charset.toString(), contentType);
    }

    /**
     * Specify the default charset to use for the specific content-type if it's not specified in the content-type header explicitly
     *
     * @param charset     The charset to use as default (unless specified explicitly)
     * @param contentType The content-type
     * @return A new instance of {@link DecoderConfig}
     */
    public DecoderConfig defaultCharsetForContentType(Charset charset, ContentType contentType) {
        notNull(charset, "Charset");
        return defaultCharsetForContentType(charset.toString(), contentType);
    }

    /**
     * Specify the default charset to use for the specific content-type if it's not specified in the content-type header explicitly
     *
     * @param charset     The charset to use as default (unless specified explicitly)
     * @param contentType The content-type
     * @return A new instance of {@link DecoderConfig}
     */
    public DecoderConfig defaultCharsetForContentType(String charset, ContentType contentType) {
        notNull(charset, "Charset");
        notNull(contentType, ContentType.class);
        Map<String, String> map = new HashMap<String, String>(contentTypeToDefaultCharset);
        for (String ct : contentType.getContentTypeStrings()) {
            map.put(ct.toLowerCase(), trim(charset));
        }
        return new DecoderConfig(charset, useNoWrapForInflateDecoding, true, contentDecoders, map);
    }

    /**
     * @return The default charset for a specific content-type. It will have precedence over {@link #defaultContentCharset()}.
     */
    public String defaultCharsetForContentType(String contentType) {
        if (StringUtils.isEmpty(contentType)) {
            return defaultContentCharset();
        }
        String charset = contentTypeToDefaultCharset.get(trim(contentType).toLowerCase());
        if (charset == null) {
            return defaultContentCharset();
        }
        return charset;
    }

    /**
     * @return A map that contains default charset for a specific content-type. It will have precedence over {@link #defaultContentCharset()}.
     */
    public String defaultCharsetForContentType(ContentType contentType) {
        if (contentType == null) {
            return defaultContentCharset();
        }
        return defaultCharsetForContentType(contentType.toString());
    }

    /**
     * @return A map that contains default charset for a specific content-type. It will have precedence over {@link #defaultContentCharset()}.
     */
    public boolean hasDefaultCharsetForContentType(String contentType) {
        return !StringUtils.isBlank(contentType) && contentTypeToDefaultCharset.containsKey(trim(contentType).toLowerCase());
    }

    /**
     * @return The configured default content charset that'll be used by REST Assured if no charset is specified in the response.
     */
    public String defaultContentCharset() {
        return defaultContentCharset;
    }

    /**
     * @return The configured content decoders.
     */
    public List<ContentDecoder> contentDecoders() {
        return contentDecoders;
    }

    /**
     * If the parameter 'nowrap' is true then the ZLIB header and checksum fields will not be used. This provides compatibility with the
     * compression format used by both GZIP and PKZIP.
     *

     * Note: When using the 'nowrap' option it is also necessary to provide
     * an extra "dummy" byte as input. This is required by the ZLIB native
     * library in order to support certain optimizations.
     *

     * Setting no wrap to true is required when communicating with servers not using RFC 1950 (such as PHP which uses RFC 1951).
     * See stackoverflow for more details.
     *

     * Default is {@value DecoderConfig#DEFAULT_NO_WRAP_FOR_INFLATE_ENCODED_STREAMS}.
     *
     * @param nowrap if true then support GZIP compatible compression
     * @return A new instance of the DecoderConfig.
     */
    public DecoderConfig useNoWrapForInflateDecoding(boolean nowrap) {
        return new DecoderConfig(defaultContentCharset, nowrap, true, contentDecoders, contentTypeToDefaultCharset);
    }

    /**
     * @return true if no wrap should be used for inflate encoded streams.
     * @see #useNoWrapForInflateDecoding(boolean)
     */
    public boolean shouldUseNoWrapForInflateDecoding() {
        return useNoWrapForInflateDecoding;
    }

    /**
     * Specify the default charset of the content in the response that's assumed if no charset is explicitly specified in the response.
     *
     * @param charset The expected charset
     * @return A new instance of the DecoderConfig.
     */
    @SuppressWarnings("UnusedDeclaration")
    public DecoderConfig defaultContentCharset(String charset) {
        return new DecoderConfig(charset, useNoWrapForInflateDecoding, true, contentDecoders, contentTypeToDefaultCharset);
    }

    /**
     * Specify the default charset of the content in the response that's assumed if no charset is explicitly specified in the response.
     *
     * @param charset The expected charset
     * @return A new instance of the DecoderConfig.
     */
    @SuppressWarnings("UnusedDeclaration")
    public DecoderConfig defaultContentCharset(Charset charset) {
        String charsetAsString = notNull(charset, Charset.class).toString();
        return new DecoderConfig(charsetAsString, useNoWrapForInflateDecoding, true, contentDecoders, contentTypeToDefaultCharset);
    }

    /**
     * Specify the content decoders that will be presented to the server when making a request (using the Accept-Encoding header).
     * If the server supports any of these encodings then REST Assured will automatically perform decoding of the response accordingly.
     *

     * By default {@link ContentDecoder#GZIP} and {@link ContentDecoder#DEFLATE} are used.
     *

     *
     * @param contentDecoder            The first content decoder
     * @param additionalContentDecoders Optional additional content decoders
     * @return A new instance of the DecoderConfig.
     */
    public DecoderConfig contentDecoders(ContentDecoder contentDecoder, ContentDecoder... additionalContentDecoders) {
        return new DecoderConfig(defaultContentCharset, useNoWrapForInflateDecoding, true, contentTypeToDefaultCharset, merge(contentDecoder, additionalContentDecoders));
    }

    /**
     * Specify that no content decoders should be used by REST Assured.
     *
     * @return A new instance of the DecoderConfig.
     * @see #contentDecoders(DecoderConfig.ContentDecoder, DecoderConfig.ContentDecoder...)
     */
    public DecoderConfig noContentDecoders() {
        return new DecoderConfig(defaultContentCharset, useNoWrapForInflateDecoding, true, contentTypeToDefaultCharset);
    }

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

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

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

    private static ContentDecoder[] defaultContentEncoders() {
        return new ContentDecoder[]{ContentDecoder.GZIP, ContentDecoder.DEFLATE};
    }

    private static ContentDecoder[] merge(ContentDecoder contentDecoder, ContentDecoder[] additionalContentDecoders) {
        Validate.notNull(contentDecoder, "Content decoder cannot be null");
        final ContentDecoder[] contentDecoders;
        if (additionalContentDecoders == null || additionalContentDecoders.length == 0) {
            contentDecoders = new ContentDecoder[]{contentDecoder};
        } else {
            contentDecoders = new ContentDecoder[additionalContentDecoders.length + 1];
            contentDecoders[0] = contentDecoder;
            System.arraycopy(additionalContentDecoders, 0, contentDecoders, 1, additionalContentDecoders.length);
        }
        return contentDecoders;
    }

    public boolean isUserConfigured() {
        return isUserConfigured;
    }

    /**
     * Predefined content encoders in REST Assured. Will also automatically specify the Accept-Encoder header.
     */
    public enum ContentDecoder {
        /**
         * GZIP compression support for both requests and responses.
         */
        GZIP, DEFLATE
    }
}