/*
 * 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.Collections;
import java.util.HashMap;
import java.util.Map;

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

/**
 * Allows you to specify configuration for the encoder
 */
public class EncoderConfig implements Config {

    private static final String UTF_8 = "UTF-8";
    private static final String ISO_8859_1 = "ISO-8859-1";

    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 String defaultQueryParameterCharset;
    private final boolean shouldAppendDefaultContentCharsetToContentTypeIfUndefined;
    private final Map<String, ContentType> contentEncoders;
    private final Map<String, String> contentTypeToDefaultCharset;
    private final boolean isUserDefined;

    /**
     * Configure the encoder config to use {@value org.apache.http.protocol.HTTP#DEFAULT_CONTENT_CHARSET} for content encoding and UTF-8.
     * for query parameter encoding.
     *

     * The reason for choosing UTF-8 as default for query parameters even though US-ASCII is standard according to the URI Syntax specification is
     * that it's nowadays recommended by w3 to use UTF-8. Different web servers
     * seem to take different approaches though, for example Jetty uses UTF-8 as default but Tomcat uses US-ASCII. Since REST Assured is a test
     * framework first and Jetty is more popular for testing REST Assured uses UTF-8.
     *

     */
    public EncoderConfig() {
        this(ISO_8859_1, UTF_8, true, new HashMap<String, ContentType>(), DEFAULT_CHARSET_FOR_CONTENT_TYPE, true);
    }

    public EncoderConfig(String defaultContentCharset, String defaultQueryParameterCharset) {
        this(defaultContentCharset, defaultQueryParameterCharset, true, new HashMap<String, ContentType>(), DEFAULT_CHARSET_FOR_CONTENT_TYPE, true);
    }

    private EncoderConfig(String defaultContentCharset, String defaultQueryParameterCharset,
                          boolean shouldAppendDefaultContentCharsetToContentTypeIfUndefined,
                          Map<String, ContentType> encoders, Map<String, String> contentTypeToDefaultCharset,
                          boolean isUserDefined) {
        Validate.notBlank(defaultContentCharset, "Default encoder content charset to cannot be blank. See \"appendDefaultContentCharsetToContentTypeIfMissing\" method if you like to disable automatically appending the charset to the content-type.");
        Validate.notBlank(defaultQueryParameterCharset, "Default protocol charset to cannot be blank.");
        this.contentTypeToDefaultCharset = new HashMap<String, String>(contentTypeToDefaultCharset);
        this.defaultContentCharset = defaultContentCharset;
        this.defaultQueryParameterCharset = defaultQueryParameterCharset;
        this.shouldAppendDefaultContentCharsetToContentTypeIfUndefined = shouldAppendDefaultContentCharsetToContentTypeIfUndefined;
        this.contentEncoders = encoders;
        this.isUserDefined = isUserDefined;
    }

    /**
     * @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 default content charset for all content-types (unless specified in {@link #defaultCharsetForContentType(String)}.
     */
    public String defaultContentCharset() {
        return defaultContentCharset;
    }

    /**
     * @return The default query parameter charset
     */
    public String defaultQueryParameterCharset() {
        return defaultQueryParameterCharset;
    }

    /**
     * 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 EncoderConfig}
     */
    public EncoderConfig 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 EncoderConfig(charset, defaultQueryParameterCharset, shouldAppendDefaultContentCharsetToContentTypeIfUndefined, contentEncoders, map, true);
    }

    /**
     * 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 EncoderConfig}
     */
    public EncoderConfig 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 EncoderConfig(charset, defaultQueryParameterCharset, shouldAppendDefaultContentCharsetToContentTypeIfUndefined, contentEncoders, map, true);
    }

    /**
     * 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 EncoderConfig}
     */
    public EncoderConfig 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 EncoderConfig}
     */
    public EncoderConfig defaultCharsetForContentType(Charset charset, String contentType) {
        notNull(charset, "Charset");
        return defaultCharsetForContentType(charset.toString(), contentType);
    }

    /**
     * Specify the default charset for the body/content in the request specification
     *
     * @param charset The charset to use.
     * @return A new instance of {@link EncoderConfig}
     */
    public EncoderConfig defaultContentCharset(Charset charset) {
        String charsetAsString = notNull(charset, Charset.class).toString();
        return new EncoderConfig(charsetAsString, defaultQueryParameterCharset, shouldAppendDefaultContentCharsetToContentTypeIfUndefined, contentEncoders, contentTypeToDefaultCharset, true);
    }

    /**
     * Specify the default charset for query parameters
     *
     * @param charset The charset to use.
     * @return A new instance of {@link EncoderConfig}
     */
    public EncoderConfig defaultQueryParameterCharset(String charset) {
        return new EncoderConfig(defaultContentCharset, charset, shouldAppendDefaultContentCharsetToContentTypeIfUndefined, contentEncoders, contentTypeToDefaultCharset, true);
    }

    /**
     * Specify the default charset for query parameters
     *
     * @param charset The charset to use.
     * @return A new instance of {@link EncoderConfig}
     */
    @SuppressWarnings("UnusedDeclaration")
    public EncoderConfig defaultQueryParameterCharset(Charset charset) {
        String charsetAsString = notNull(charset, Charset.class).toString();
        return new EncoderConfig(defaultContentCharset, charsetAsString, shouldAppendDefaultContentCharsetToContentTypeIfUndefined, contentEncoders, contentTypeToDefaultCharset, true);
    }

    /**
     * Specify the default charset for the body/content in the request specification
     *
     * @param charset The charset to use.
     * @return A new instance of {@link EncoderConfig}
     */
    public EncoderConfig defaultContentCharset(String charset) {
        return new EncoderConfig(charset, defaultQueryParameterCharset, shouldAppendDefaultContentCharsetToContentTypeIfUndefined, contentEncoders, contentTypeToDefaultCharset, true);
    }

    /**
     * Tells whether REST Assured should automatically append the content charset to the content-type header if not defined explicitly.
     *

     * Note that this does not affect multipart form data.
     *

     *

     * Default is true.
     *

     *
     * @param shouldAddDefaultContentCharsetToContentTypeIfMissing Whether REST Assured should automatically append the content charset to the content-type header if not defined explicitly.
     * @return A new {@link EncoderConfig} instance
     */
    public EncoderConfig appendDefaultContentCharsetToContentTypeIfUndefined(boolean shouldAddDefaultContentCharsetToContentTypeIfMissing) {
        return new EncoderConfig(defaultContentCharset, defaultQueryParameterCharset, shouldAddDefaultContentCharsetToContentTypeIfMissing, contentEncoders, contentTypeToDefaultCharset, true);
    }

    /**
     * Tells whether REST Assured should automatically append the content charset to the content-type header if not defined explicitly.
     *

     * Note that this does not affect multipart form data.
     *

     *
     * @return true if REST Assured should automatically append the content charset to the content-type header if not defined explicitly.
     */
    public boolean shouldAppendDefaultContentCharsetToContentTypeIfUndefined() {
        return shouldAppendDefaultContentCharsetToContentTypeIfUndefined;
    }

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

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

    public boolean isUserConfigured() {
        return isUserDefined;
    }

    /**
     * @see #encodeContentTypeAs(String, ContentType)
     */
    public boolean hasContentEncoders() {
        return !contentEncoders.isEmpty();
    }

    /**
     * @return A map of all specified content encoders
     * @see #encodeContentTypeAs(String, ContentType)
     */
    public Map<String, ContentType> contentEncoders() {
        return Collections.unmodifiableMap(contentEncoders);
    }

    /**
     * Encodes the content (body) of the request specified with the given contentType with the same
     * encoder used by the supplied encoder. This is useful only if REST Assured picks the wrong
     * encoder (or can't recognize it) for the given content-type.
     *
     * @param contentType The content-type to encode with a specific encoder.
     * @param encoder     The encoder to use for the given content-type.
     * @return A new {@link EncoderConfig} instance
     */
    public EncoderConfig encodeContentTypeAs(String contentType, ContentType encoder) {
        notNull(contentType, "Content-Type to encode");
        notNull(encoder, ContentType.class);
        Map<String, ContentType> newMap = new HashMap<String, ContentType>(contentEncoders);
        newMap.put(contentType, encoder);
        return new EncoderConfig(defaultContentCharset, defaultQueryParameterCharset, shouldAppendDefaultContentCharsetToContentTypeIfUndefined, newMap, contentTypeToDefaultCharset, true);
    }
}


/*
 * 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.mapper.ObjectMapper;
import io.restassured.mapper.ObjectMapperType;
import io.restassured.path.json.mapper.factory.*;
import io.restassured.path.xml.mapper.factory.DefaultJAXBObjectMapperFactory;
import io.restassured.path.xml.mapper.factory.JAXBObjectMapperFactory;
import org.apache.commons.lang3.Validate;

/**
 * Allows you to specify configuration for the object mapping functionality.
 */
public class ObjectMapperConfig implements Config {

    private final ObjectMapper defaultObjectMapper;
    private final ObjectMapperType defaultObjectMapperType;
    private final GsonObjectMapperFactory gsonObjectMapperFactory;
    private final Jackson1ObjectMapperFactory jackson1ObjectMapperFactory;
    private final Jackson2ObjectMapperFactory jackson2ObjectMapperFactory;
    private final JAXBObjectMapperFactory jaxbObjectMapperFactory;
    private final JohnzonObjectMapperFactory johnzonObjectMapperFactory;
    private final boolean isUserConfigured;

    /**
     * Default object mapper configuration that uses no explicit object mapper. An object mapper
     * will be found automatically in classpath if available. For more details see documentation.
     *

     * Also default object mapper factories will be used.
     */
    public ObjectMapperConfig() {
        defaultObjectMapper = null;
        defaultObjectMapperType = null;
        gsonObjectMapperFactory = new DefaultGsonObjectMapperFactory();
        jackson1ObjectMapperFactory = new DefaultJackson1ObjectMapperFactory();
        jackson2ObjectMapperFactory = new DefaultJackson2ObjectMapperFactory();
        jaxbObjectMapperFactory = new DefaultJAXBObjectMapperFactory();
        johnzonObjectMapperFactory = new DefaultJohnzonObjectMapperFactory();
        isUserConfigured = false;
    }

    /**
     * Creates an object mapper configuration that uses the specified object mapper as default.
     *
     * @param defaultObjectMapperType The object mapper to use. If null then classpath scanning will be used.
     */
    public ObjectMapperConfig(ObjectMapperType defaultObjectMapperType) {
        this(null, defaultObjectMapperType, new DefaultGsonObjectMapperFactory(), new DefaultJackson1ObjectMapperFactory(),
                new DefaultJackson2ObjectMapperFactory(), new DefaultJAXBObjectMapperFactory(),
                    new DefaultJohnzonObjectMapperFactory(), true);
    }

    /**
     * Creates an object mapper configuration that uses the specified object mapper as default.
     *
     * @param defaultObjectMapper The object mapper to use. If null then classpath scanning will be used.
     */
    public ObjectMapperConfig(ObjectMapper defaultObjectMapper) {
        this(defaultObjectMapper, null, new DefaultGsonObjectMapperFactory(), new DefaultJackson1ObjectMapperFactory(),
                new DefaultJackson2ObjectMapperFactory(), new DefaultJAXBObjectMapperFactory(),
                    new DefaultJohnzonObjectMapperFactory(), true);
    }

    private ObjectMapperConfig(ObjectMapper defaultObjectMapper, ObjectMapperType defaultObjectMapperType,
                               GsonObjectMapperFactory gsonObjectMapperFactory, Jackson1ObjectMapperFactory jackson1ObjectMapperFactory,
                               Jackson2ObjectMapperFactory jackson2ObjectMapperFactory, JAXBObjectMapperFactory jaxbObjectMapperFactory,
                               JohnzonObjectMapperFactory johnzonObjectMapperFactory, boolean isUserConfigured) {
        Validate.notNull(gsonObjectMapperFactory, GsonObjectMapperFactory.class.getSimpleName() + " cannot be null");
        Validate.notNull(jackson1ObjectMapperFactory, Jackson1ObjectMapperFactory.class.getSimpleName() + " cannot be null");
        Validate.notNull(jackson2ObjectMapperFactory, Jackson2ObjectMapperFactory.class.getSimpleName() + " cannot be null");
        Validate.notNull(jaxbObjectMapperFactory, JAXBObjectMapperFactory.class.getSimpleName() + " cannot be null");
        this.defaultObjectMapperType = defaultObjectMapperType;
        this.defaultObjectMapper = defaultObjectMapper;
        this.gsonObjectMapperFactory = gsonObjectMapperFactory;
        this.jackson1ObjectMapperFactory = jackson1ObjectMapperFactory;
        this.jackson2ObjectMapperFactory = jackson2ObjectMapperFactory;
        this.jaxbObjectMapperFactory = jaxbObjectMapperFactory;
        this.johnzonObjectMapperFactory = johnzonObjectMapperFactory;
        this.isUserConfigured = isUserConfigured;
    }

    public ObjectMapperType defaultObjectMapperType() {
        return defaultObjectMapperType;
    }

    public boolean hasDefaultObjectMapperType() {
        return defaultObjectMapperType != null;
    }

    /**
     * Creates an object mapper configuration that uses the specified object mapper as default.
     *
     * @param defaultObjectMapperType The object mapper to use. If null then classpath scanning will be used.
     */
    public ObjectMapperConfig defaultObjectMapperType(ObjectMapperType defaultObjectMapperType) {
        return new ObjectMapperConfig(defaultObjectMapper, defaultObjectMapperType, gsonObjectMapperFactory,
                jackson1ObjectMapperFactory, jackson2ObjectMapperFactory, jaxbObjectMapperFactory,
                    johnzonObjectMapperFactory, true);
    }

    public ObjectMapper defaultObjectMapper() {
        return defaultObjectMapper;
    }

    public boolean hasDefaultObjectMapper() {
        return defaultObjectMapper != null;
    }

    /**
     * Creates an object mapper configuration that uses the specified object mapper as default.
     *
     * @param defaultObjectMapper The object mapper to use. If null then classpath scanning will be used.
     */
    public ObjectMapperConfig defaultObjectMapper(ObjectMapper defaultObjectMapper) {
        return new ObjectMapperConfig(defaultObjectMapper);
    }

    public GsonObjectMapperFactory gsonObjectMapperFactory() {
        return gsonObjectMapperFactory;
    }

    /**
     * Specify a custom Gson object mapper factory.
     *
     * @param gsonObjectMapperFactory The object mapper factory
     */
    public ObjectMapperConfig gsonObjectMapperFactory(GsonObjectMapperFactory gsonObjectMapperFactory) {
        return new ObjectMapperConfig(defaultObjectMapper, defaultObjectMapperType, gsonObjectMapperFactory,
                jackson1ObjectMapperFactory, jackson2ObjectMapperFactory, jaxbObjectMapperFactory,
                    johnzonObjectMapperFactory, true);
    }

    public Jackson1ObjectMapperFactory jackson1ObjectMapperFactory() {
        return jackson1ObjectMapperFactory;
    }

    /**
     * Specify a custom Jackson 1.0 object mapper factory.
     *
     * @param jackson1ObjectMapperFactory The object mapper factory
     */
    public ObjectMapperConfig jackson1ObjectMapperFactory(Jackson1ObjectMapperFactory jackson1ObjectMapperFactory) {
        return new ObjectMapperConfig(defaultObjectMapper, defaultObjectMapperType, gsonObjectMapperFactory,
                jackson1ObjectMapperFactory, jackson2ObjectMapperFactory, jaxbObjectMapperFactory,
                    johnzonObjectMapperFactory, true);
    }

    public Jackson2ObjectMapperFactory jackson2ObjectMapperFactory() {
        return jackson2ObjectMapperFactory;
    }

    /**
     * Specify a custom Jackson 1.0 object mapper factory.
     *
     * @param jackson2ObjectMapperFactory The object mapper factory
     */
    public ObjectMapperConfig jackson2ObjectMapperFactory(Jackson2ObjectMapperFactory jackson2ObjectMapperFactory) {
        return new ObjectMapperConfig(defaultObjectMapper, defaultObjectMapperType, gsonObjectMapperFactory,
                jackson1ObjectMapperFactory, jackson2ObjectMapperFactory, jaxbObjectMapperFactory,
                   johnzonObjectMapperFactory, true);
    }

    public JAXBObjectMapperFactory jaxbObjectMapperFactory() {
        return jaxbObjectMapperFactory;
    }

    public JohnzonObjectMapperFactory johnzonObjectMapperFactory() {
        return johnzonObjectMapperFactory;
    }

    /**
     * Specify a custom JAXB object mapper factory.
     *
     * @param jaxbObjectMapperFactory The object mapper factory
     */
    public ObjectMapperConfig jaxbObjectMapperFactory(JAXBObjectMapperFactory jaxbObjectMapperFactory) {
        return new ObjectMapperConfig(defaultObjectMapper, defaultObjectMapperType, gsonObjectMapperFactory,
                jackson1ObjectMapperFactory, jackson2ObjectMapperFactory, jaxbObjectMapperFactory,
                    johnzonObjectMapperFactory, true);
    }

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

    /**
     * Syntactic sugar.
     *
     * @return The same object mapper config instance.
     */
    public ObjectMapperConfig and() {
        return this;
    }

    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.module.mockmvc.config;

import io.restassured.config.*;
import io.restassured.module.spring.commons.config.AsyncConfig;
import io.restassured.module.spring.commons.config.ClientConfig;
import io.restassured.module.spring.commons.config.SpecificationConfig;

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

/**
 * Main configuration for REST Assured Mock MVC that allows you to configure advanced settings.
 *

 * Usage example:
 *

 * RestAssuredMockMvc.config = new RestAssuredMockMvcConfig().logConfig(new LogConfig(captor, true));
 *

 *

 */
public class RestAssuredMockMvcConfig implements SpecificationConfig {

    // When adding a config here don't forget to update isUserConfigured method
    private final LogConfig logConfig;
    private final EncoderConfig encoderConfig;
    private final DecoderConfig decoderConfig;
    private final SessionConfig sessionConfig;
    private final ObjectMapperConfig objectMapperConfig;
    private final JsonConfig jsonConfig;
    private final XmlConfig xmlConfig;
    private final HeaderConfig headerConfig;
    private final AsyncConfig asyncConfig;
    private final MockMvcConfig mockMvcConfig;
    private final MultiPartConfig multiPartConfig;
    private final MockMvcParamConfig paramConfig;
    private final MatcherConfig matcherConfig;

    /**
     * Create a new RestAssuredMockMvcConfig with the default configurations.
     */
    public RestAssuredMockMvcConfig() {
        this(new LogConfig(), new EncoderConfig(), new DecoderConfig(), new SessionConfig(), new ObjectMapperConfig(), new JsonConfig(), new XmlConfig(),
                new HeaderConfig(), new AsyncConfig(), new MultiPartConfig(), new MockMvcConfig(), new MockMvcParamConfig(), new MatcherConfig());
    }

    /**
     * Create a new RestAssuredMockMvcConfig with the supplied configs.
     */
    private RestAssuredMockMvcConfig(LogConfig logConfig,
                                     EncoderConfig encoderConfig,
                                     DecoderConfig decoderConfig,
                                     SessionConfig sessionConfig,
                                     ObjectMapperConfig objectMapperConfig,
                                     JsonConfig jsonConfig,
                                     XmlConfig xmlConfig,
                                     HeaderConfig headerConfig,
                                     AsyncConfig asyncConfig,
                                     MultiPartConfig multiPartConfig,
                                     MockMvcConfig mockMvcConfig,
                                     MockMvcParamConfig paramConfig,
                                     MatcherConfig matcherConfig) {
        notNull(logConfig, "Log config");
        notNull(encoderConfig, "Encoder config");
        notNull(decoderConfig, "Decoder config");
        notNull(sessionConfig, "Session config");
        notNull(objectMapperConfig, "Object mapper config");
        notNull(jsonConfig, "Json config");
        notNull(xmlConfig, "Xml config");
        notNull(headerConfig, "Header config");
        notNull(multiPartConfig, "MultiPart config");
        notNull(mockMvcConfig, "MockMvc config");
        notNull(paramConfig, "Param config");
        notNull(matcherConfig, "Matcher config");
        this.logConfig = logConfig;
        this.encoderConfig = encoderConfig;
        this.decoderConfig = decoderConfig;
        this.sessionConfig = sessionConfig;
        this.objectMapperConfig = objectMapperConfig;
        this.jsonConfig = jsonConfig;
        this.xmlConfig = xmlConfig;
        this.headerConfig = headerConfig;
        this.asyncConfig = asyncConfig;
        this.multiPartConfig = multiPartConfig;
        this.mockMvcConfig = mockMvcConfig;
        this.paramConfig = paramConfig;
        this.matcherConfig = matcherConfig;
    }

    /**
     * Set the Log config.
     *
     * @param logConfig The {@link LogConfig} to set
     * @return An updated RestAssuredMockMvcConfig
     */
    public RestAssuredMockMvcConfig logConfig(LogConfig logConfig) {
        notNull(logConfig, "Log config");
        return new RestAssuredMockMvcConfig(logConfig, encoderConfig, decoderConfig, sessionConfig, objectMapperConfig, jsonConfig, xmlConfig, headerConfig, asyncConfig, multiPartConfig, mockMvcConfig, paramConfig, matcherConfig);
    }

    /**
     * Set the session config.
     *
     * @param sessionConfig The {@link SessionConfig} to set
     * @return An updated RestAssuredMockMvcConfig
     */
    public RestAssuredMockMvcConfig sessionConfig(SessionConfig sessionConfig) {
        notNull(sessionConfig, "Session config");
        return new RestAssuredMockMvcConfig(logConfig, encoderConfig, decoderConfig, sessionConfig, objectMapperConfig, jsonConfig, xmlConfig, headerConfig, asyncConfig, multiPartConfig, mockMvcConfig, paramConfig, matcherConfig);
    }

    /**
     * Set the object mapper config.
     *
     * @param objectMapperConfig The {@link ObjectMapperConfig} to set
     * @return An updated RestAssuredMockMvcConfig
     */
    public RestAssuredMockMvcConfig objectMapperConfig(ObjectMapperConfig objectMapperConfig) {
        notNull(objectMapperConfig, "Object mapper config");
        return new RestAssuredMockMvcConfig(logConfig, encoderConfig, decoderConfig, sessionConfig, objectMapperConfig, jsonConfig, xmlConfig, headerConfig, asyncConfig, multiPartConfig, mockMvcConfig, paramConfig, matcherConfig);
    }

    /**
     * Set the Json config.
     *
     * @param jsonConfig The {@link JsonConfig} to set
     * @return An updated RestAssuredMockMvcConfig
     */
    public RestAssuredMockMvcConfig jsonConfig(JsonConfig jsonConfig) {
        notNull(jsonConfig, "JsonConfig");
        return new RestAssuredMockMvcConfig(logConfig, encoderConfig, decoderConfig, sessionConfig,
                objectMapperConfig, jsonConfig, xmlConfig, headerConfig, asyncConfig, multiPartConfig, mockMvcConfig, paramConfig, matcherConfig);
    }

    /**
     * Set the Xml config.
     *
     * @param xmlConfig The {@link XmlConfig} to set
     * @return An updated RestAssuredMockMvcConfig
     */
    public RestAssuredMockMvcConfig xmlConfig(XmlConfig xmlConfig) {
        notNull(xmlConfig, "XmlConfig");
        return new RestAssuredMockMvcConfig(logConfig, encoderConfig, decoderConfig, sessionConfig,
                objectMapperConfig, jsonConfig, xmlConfig, headerConfig, asyncConfig, multiPartConfig, mockMvcConfig, paramConfig, matcherConfig);
    }

    /**
     * Set the encoder config
     *
     * @param encoderConfig The {@link EncoderConfig} to set
     * @return An updated RestAssuredMockMvcConfig
     */
    public RestAssuredMockMvcConfig encoderConfig(EncoderConfig encoderConfig) {
        notNull(encoderConfig, "EncoderConfig");
        return new RestAssuredMockMvcConfig(logConfig, encoderConfig, decoderConfig, sessionConfig,
                objectMapperConfig, jsonConfig, xmlConfig, headerConfig, asyncConfig, multiPartConfig, mockMvcConfig, paramConfig, matcherConfig);
    }

    /**
     * Set the decoder config
     *
     * @param decoderConfig The {@link DecoderConfig} to set
     * @return An updated RestAssuredMockMvcConfig
     */
    public RestAssuredMockMvcConfig decoderConfig(DecoderConfig decoderConfig) {
        notNull(encoderConfig, DecoderConfig.class);
        return new RestAssuredMockMvcConfig(logConfig, encoderConfig, decoderConfig, sessionConfig,
                objectMapperConfig, jsonConfig, xmlConfig, headerConfig, asyncConfig, multiPartConfig, mockMvcConfig, paramConfig, matcherConfig);
    }

    /**
     * Set the header config
     *
     * @param headerConfig The {@link HeaderConfig} to set
     * @return An updated RestAssuredMockMvcConfig
     */
    public RestAssuredMockMvcConfig headerConfig(HeaderConfig headerConfig) {
        notNull(headerConfig, "HeaderConfig");
        return new RestAssuredMockMvcConfig(logConfig, encoderConfig, decoderConfig, sessionConfig,
                objectMapperConfig, jsonConfig, xmlConfig, headerConfig, asyncConfig, multiPartConfig, mockMvcConfig, paramConfig, matcherConfig);
    }

    /**
     * Set the async config
     *
     * @param asyncConfig The {@link AsyncConfig} to set
     * @return An updated RestAssuredMockMvcConfig
     */
    public RestAssuredMockMvcConfig asyncConfig(AsyncConfig asyncConfig) {
        notNull(asyncConfig, AsyncConfig.class);
        return new RestAssuredMockMvcConfig(logConfig, encoderConfig, decoderConfig, sessionConfig,
                objectMapperConfig, jsonConfig, xmlConfig, headerConfig, asyncConfig, multiPartConfig, mockMvcConfig, paramConfig, matcherConfig);
    }

    /**
     * Set the MockMVC config
     *
     * @param mockMvcConfig The {@link MockMvcConfig} to set
     * @return An updated RestAssuredMockMvcConfig
     */
    public RestAssuredMockMvcConfig mockMvcConfig(MockMvcConfig mockMvcConfig) {
        notNull(mockMvcConfig, MockMvcConfig.class);
        return new RestAssuredMockMvcConfig(logConfig, encoderConfig, decoderConfig, sessionConfig,
                objectMapperConfig, jsonConfig, xmlConfig, headerConfig, asyncConfig, multiPartConfig, mockMvcConfig, paramConfig, matcherConfig);
    }

    /**
     * Set the multi-part config
     *
     * @param multiPartConfig The {@link MultiPartConfig} to set
     * @return An updated RestAssuredMockMvcConfig
     */
    public RestAssuredMockMvcConfig multiPartConfig(MultiPartConfig multiPartConfig) {
        notNull(multiPartConfig, MultiPartConfig.class);
        return new RestAssuredMockMvcConfig(logConfig, encoderConfig, decoderConfig, sessionConfig,
                objectMapperConfig, jsonConfig, xmlConfig, headerConfig, asyncConfig, multiPartConfig, mockMvcConfig, paramConfig, matcherConfig);
    }

    public ClientConfig getClientConfig() {
        return getMockMvcConfig();
    }

    public SpecificationConfig clientConfig(ClientConfig clientConfig) {
        if (!(clientConfig instanceof MockMvcConfig)) {
            throw new IllegalArgumentException("Wrong ClientConfig type supplied");
        }
        return mockMvcConfig((MockMvcConfig) clientConfig);
    }

    /**
     * Set the parameter config
     *
     * @param paramConfig The {@link MockMvcParamConfig} to set
     * @return An updated RestAssuredMockMvcConfig
     */
    public RestAssuredMockMvcConfig paramConfig(MockMvcParamConfig paramConfig) {
        notNull(paramConfig, MultiPartConfig.class);
        return new RestAssuredMockMvcConfig(logConfig, encoderConfig, decoderConfig, sessionConfig,
                objectMapperConfig, jsonConfig, xmlConfig, headerConfig, asyncConfig, multiPartConfig, mockMvcConfig, paramConfig, matcherConfig);
    }

    /**
     * Set the matcher config
     *
     * @param matcherConfig The {@link MockMvcParamConfig} to set
     * @return An updated RestAssuredMockMvcConfig
     */
    public RestAssuredMockMvcConfig matcherConfig(MatcherConfig matcherConfig) {
        notNull(matcherConfig, MatcherConfig.class);
        return new RestAssuredMockMvcConfig(logConfig, encoderConfig, decoderConfig, sessionConfig,
                objectMapperConfig, jsonConfig, xmlConfig, headerConfig, asyncConfig, multiPartConfig, mockMvcConfig, paramConfig, matcherConfig);
    }

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

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

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

    /**
     * @return The LogConfig
     */
    public LogConfig getLogConfig() {
        return logConfig;
    }

    /**
     * @return The EncoderConfig
     */
    public EncoderConfig getEncoderConfig() {
        return encoderConfig;
    }

    /**
     * @return The DecoderConfig
     */
    public DecoderConfig getDecoderConfig() {
        return decoderConfig;
    }

    /**
     * @return The SessionConfig
     */
    public SessionConfig getSessionConfig() {
        return sessionConfig;
    }

    /**
     * @return The ObjectMapperConfig
     */
    public ObjectMapperConfig getObjectMapperConfig() {
        return objectMapperConfig;
    }

    /**
     * @return The JsonPath Config
     */
    public JsonConfig getJsonConfig() {
        return jsonConfig;
    }

    /**
     * @return The Xml Config
     */
    public XmlConfig getXmlConfig() {
        return xmlConfig;
    }

    /**
     * @return The Header Config
     */
    public HeaderConfig getHeaderConfig() {
        return headerConfig;
    }

    /**
     * @return The MockMvcAsync Config
     */
    public AsyncConfig getAsyncConfig() {
        return asyncConfig;
    }

    /**
     * @return The MultiPart Config
     */
    public MultiPartConfig getMultiPartConfig() {
        return multiPartConfig;
    }

    /**
     * @return The Param Config
     */
    public ParamConfig getParamConfig() {
        return paramConfig;
    }

    public SpecificationConfig paramConfig(ParamConfig paramConfig) {
        if (!(paramConfig instanceof MockMvcParamConfig)) {
            throw new IllegalArgumentException("Wrong ClientConfig type supplied");
        }
        return paramConfig((MockMvcParamConfig) paramConfig);
    }

    public MockMvcParamConfig getMockMvcParamConfig() {
        return paramConfig;
    }

    /**
     * @return The MockMvc Config
     */
    public MockMvcConfig getMockMvcConfig() {
        return mockMvcConfig;
    }

    /**
     * @return The Matcher Config
     */
    public MatcherConfig getMatcherConfig() {
        return matcherConfig;
    }

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

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

    public boolean isUserConfigured() {
        // When adding a config here don't forget to update merging in MockMvcRequestSpecificationImpl#mergeConfig and potentially also ConfigConverter#convertToRestAssuredConfig
        return decoderConfig.isUserConfigured() || encoderConfig.isUserConfigured() || logConfig.isUserConfigured() || sessionConfig.isUserConfigured()
                || objectMapperConfig.isUserConfigured() || xmlConfig.isUserConfigured() || jsonConfig.isUserConfigured() || headerConfig.isUserConfigured()
                || asyncConfig.isUserConfigured() || multiPartConfig.isUserConfigured() || mockMvcConfig.isUserConfigured() || paramConfig.isUserConfigured()
                || matcherConfig.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.filter.log;

import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.Charset;

public class UrlDecoder {

	private UrlDecoder() {
	}

	// Copy of  the private method in URLEncodedUtils

	/**
	 * Decode/unescape a portion of a URL, to use with the query part ensure {@code plusAsBlank} is true.
	 *
	 * @param content     the portion to decode
	 * @param charset     the charset to use
	 * @param plusAsBlank if {@code true}, then convert '+' to space (e.g. for www-url-form-encoded content), otherwise leave as is.
	 * @return encoded string
	 */
	public static String urlDecode(final String content, final Charset charset, final boolean plusAsBlank) {
		if (content == null) {
			return null;
		}
		final ByteBuffer bb = ByteBuffer.allocate(content.length());
		final CharBuffer cb = CharBuffer.wrap(content);
		while (cb.hasRemaining()) {
			final char c = cb.get();
			if (c == '%' && cb.remaining() >= 2) {
				final char uc = cb.get();
				final char lc = cb.get();
				final int u = Character.digit(uc, 16);
				final int l = Character.digit(lc, 16);
				if (u != -1 && l != -1) {
					bb.put((byte) ((u << 4) + l));
				} else {
					bb.put((byte) '%');
					bb.put((byte) uc);
					bb.put((byte) lc);
				}
			} else if (plusAsBlank && c == '+') {
				bb.put((byte) ' ');
			} else {
				bb.put((byte) c);
			}
		}
		bb.flip();
		return charset.decode(bb).toString();
	}

}


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

import io.restassured.builder.ResponseBuilder;
import io.restassured.filter.Filter;
import io.restassured.filter.FilterContext;
import io.restassured.internal.RestAssuredResponseImpl;
import io.restassured.internal.print.ResponsePrinter;
import io.restassured.response.Response;
import io.restassured.specification.FilterableRequestSpecification;
import io.restassured.specification.FilterableResponseSpecification;
import org.apache.commons.lang3.Validate;
import org.hamcrest.Matcher;

import java.io.PrintStream;

import static io.restassured.RestAssured.config;

class StatusCodeBasedLoggingFilter implements Filter {

    private final PrintStream stream;
    private final Matcher<?> matcher;
    private final LogDetail logDetail;
    private final boolean shouldPrettyPrint;

    /**
     * Log to system out
     *
     * @param matcher The matcher for the logging to take place
     */
    public StatusCodeBasedLoggingFilter(Matcher<? super Integer> matcher) {
        this(System.out, matcher);
    }

    /**
     * Instantiate a error logger using a specific print stream
     *
     * @param stream  The stream to log errors to.
     * @param matcher The matcher for the logging to take place
     */
    public StatusCodeBasedLoggingFilter(PrintStream stream, Matcher<? super Integer> matcher) {
        this(LogDetail.ALL, stream, matcher);
    }

    /**
     * Instantiate a logger using a specific print stream and a specific log detail
     *
     * @param logDetail The log detail
     * @param stream    The stream to log errors to.
     * @param matcher   The matcher for the logging to take place
     */
    public StatusCodeBasedLoggingFilter(LogDetail logDetail, PrintStream stream, Matcher<? super Integer> matcher) {
        this(logDetail, isPrettyPrintingEnabled(), stream, matcher);
    }

    /**
     * Instantiate a logger using a specific print stream and a specific log detail  and the option to pretty printing
     *
     * @param logDetail   The log detail
     * @param prettyPrint Enabled pretty printing if possible
     * @param stream      The stream to log errors to.
     * @param matcher     The matcher for the logging to take place
     */
    public StatusCodeBasedLoggingFilter(LogDetail logDetail, boolean prettyPrint, PrintStream stream, Matcher<? super Integer> matcher) {
        Validate.notNull(logDetail, "Log details cannot be null");
        Validate.notNull(stream, "Print stream cannot be null");
        Validate.notNull(matcher, "Matcher cannot be null");
        if (logDetail == LogDetail.PARAMS || logDetail == LogDetail.URI || logDetail == LogDetail.METHOD) {
            throw new IllegalArgumentException(String.format("%s is not a valid %s for a response.", logDetail, LogDetail.class.getSimpleName()));
        }
        this.shouldPrettyPrint = prettyPrint;
        this.logDetail = logDetail;
        this.stream = stream;
        this.matcher = matcher;
    }

    public Response filter(FilterableRequestSpecification requestSpec, FilterableResponseSpecification responseSpec, FilterContext ctx) {
        Response response = ctx.next(requestSpec, responseSpec);
        final int statusCode = response.statusCode();
        if (matcher.matches(statusCode)) {
            ResponsePrinter.print(response, response, stream, logDetail, shouldPrettyPrint);
            final byte[] responseBody;
            if (logDetail == LogDetail.BODY || logDetail == LogDetail.ALL) {
                responseBody = response.asByteArray();
            } else {
                responseBody = null;
            }
            response = cloneResponseIfNeeded(response, responseBody);
        }

        return response;
    }

    /*
     * If body expectations are defined we need to return a new Response otherwise the stream
     * has been closed due to the logging.
     */
    private Response cloneResponseIfNeeded(Response response, byte[] responseAsString) {
        if (responseAsString != null && response instanceof RestAssuredResponseImpl && !((RestAssuredResponseImpl) response).getHasExpectations()) {
            final Response build = new ResponseBuilder().clone(response).setBody(responseAsString).build();
            ((RestAssuredResponseImpl) build).setHasExpectations(true);
            return build;
        }
        return response;
    }

    private static boolean isPrettyPrintingEnabled() {
        return config == null || config.getLogConfig().isPrettyPrintingEnabled();
    }

    private void throwIAE(LogDetail params) {
        throw new IllegalArgumentException(String.format("%s is not a valid %s for a response.", params, LogDetail.class.getSimpleName()));
    }
}


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

import io.restassured.internal.common.assertion.AssertParameter;
import org.apache.http.conn.ssl.SSLSocketFactory;
import org.apache.http.conn.ssl.X509HostnameVerifier;

import java.security.KeyStore;

import static org.apache.http.conn.ssl.SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER;
import static org.apache.http.conn.ssl.SSLSocketFactory.STRICT_HOSTNAME_VERIFIER;

/**
 * A specification for more advanced usages of certificate authentication. Example usage:
 *

 * given().auth().certificate("keystore.jks", "my_password", certAuthSettings().allowAllHostNames());
 *

 */
public class CertificateAuthSettings {
    private static final int UNDEFINED_PORT = -1;
    private final String keystoreType;
    private final String trustStoreType;
    private final int port;
    private final KeyStore trustStore;
    private final KeyStore keyStore;
    private final X509HostnameVerifier x509HostnameVerifier;
    private final SSLSocketFactory sslSocketFactory;

    /**
     * Create a new instance of the Certificate Authentication Options with the default settings of:
     *

     *
keystoreType = {@link java.security.KeyStore#getDefaultType()}

     *
trustStoreType = {@link java.security.KeyStore#getDefaultType()}

     *
port = 443

     *
trustStore = null

     *
keyStore = null

     *
x509HostnameVerifier = {@link org.apache.http.conn.ssl.SSLSocketFactory#STRICT_HOSTNAME_VERIFIER}

     *
SSLSocketFactory = null

     *

     *
     * @see #certAuthSettings()
     */
    public CertificateAuthSettings() {
        this(KeyStore.getDefaultType(), KeyStore.getDefaultType(), UNDEFINED_PORT, null, null, STRICT_HOSTNAME_VERIFIER, null);
    }

    /**
     * @param keystoreType         The certificate type, by default {@link java.security.KeyStore#getDefaultType()}.
     * @param trustStoreType       The trust store type, by default {@link java.security.KeyStore#getDefaultType()}.
     * @param port                 The port, by default 443.
     * @param trustStore           The trust store, by default no provider is used (null).
     * @param x509HostnameVerifier The X509HostnameVerifier to use
     * @param sslSocketFactory     The SSLSocketFactory to use
     */
    private CertificateAuthSettings(String keystoreType, String trustStoreType, int port, KeyStore trustStore, KeyStore keyStore, X509HostnameVerifier x509HostnameVerifier,
                                    SSLSocketFactory sslSocketFactory) {
        AssertParameter.notNull(keystoreType, "Certificate type");
        AssertParameter.notNull(trustStoreType, "Trust store type");
        this.keystoreType = keystoreType;
        this.keyStore = keyStore;
        this.trustStoreType = trustStoreType;
        this.port = port;
        this.trustStore = trustStore;
        this.x509HostnameVerifier = x509HostnameVerifier;
        this.sslSocketFactory = sslSocketFactory;
    }

    public String getKeyStoreType() {
        return keystoreType;
    }

    public String getTrustStoreType() {
        return trustStoreType;
    }

    public int getPort() {
        return port;
    }

    public KeyStore getTrustStore() {
        return trustStore;
    }

    public KeyStore getKeyStore() {
        return keyStore;
    }

    public SSLSocketFactory getSSLSocketFactory() {
        return sslSocketFactory;
    }

    /**
     * Configure the CertificateAuthSettings to use strict host name verification (this is the default behavior).
     *
     * @return A new CertificateAuthSettings instance
     * @see org.apache.http.conn.ssl.SSLSocketFactory#STRICT_HOSTNAME_VERIFIER
     */
    public CertificateAuthSettings strictHostnames() {
        return new CertificateAuthSettings(keystoreType, trustStoreType, port, trustStore, keyStore, STRICT_HOSTNAME_VERIFIER, sslSocketFactory);
    }

    /**
     * Configure the CertificateAuthSettings to allow all host names.
     *
     * @return A new CertificateAuthSettings instance
     * @see org.apache.http.conn.ssl.SSLSocketFactory#ALLOW_ALL_HOSTNAME_VERIFIER
     */
    public CertificateAuthSettings allowAllHostnames() {
        return new CertificateAuthSettings(keystoreType, trustStoreType, port, trustStore, keyStore, ALLOW_ALL_HOSTNAME_VERIFIER, sslSocketFactory);
    }

    /**
     * Configure the CertificateAuthSettings to use the provided {@link X509HostnameVerifier} instance.
     *
     * @return A new CertificateAuthSettings instance
     * @see org.apache.http.conn.ssl.SSLSocketFactory#ALLOW_ALL_HOSTNAME_VERIFIER
     * @see #allowAllHostnames()
     */
    public CertificateAuthSettings x509HostnameVerifier(X509HostnameVerifier x509HostnameVerifier) {
        return new CertificateAuthSettings(keystoreType, trustStoreType, port, trustStore, keyStore, x509HostnameVerifier, sslSocketFactory);
    }

    /**
     * @return The configured X509HostnameVerifier
     */
    public X509HostnameVerifier getX509HostnameVerifier() {
        return x509HostnameVerifier;
    }

    /**
     * @param keystoreType The keystore type, by default {@link java.security.KeyStore#getDefaultType()}.
     * @return A new instance of {@link CertificateAuthSettings} with the updated setting.
     */
    public CertificateAuthSettings keyStoreType(String keystoreType) {
        return new CertificateAuthSettings(keystoreType, trustStoreType, port, trustStore, keyStore, x509HostnameVerifier, sslSocketFactory);
    }

    /**
     * @param trustStoreType The trust store type, by default {@link java.security.KeyStore#getDefaultType()}.
     * @return A new instance of {@link CertificateAuthSettings} with the updated setting.
     */
    public CertificateAuthSettings trustStoreType(String trustStoreType) {
        return new CertificateAuthSettings(keystoreType, trustStoreType, port, trustStore, keyStore, x509HostnameVerifier, sslSocketFactory);
    }

    /**
     * @param port The port, by default 443.
     * @return A new instance of {@link CertificateAuthSettings} with the updated setting.
     */
    public CertificateAuthSettings port(int port) {
        return new CertificateAuthSettings(keystoreType, trustStoreType, port, trustStore, keyStore, x509HostnameVerifier, sslSocketFactory);
    }

    /**
     * @param trustStore The trust store to use, by default no (null) trust store is used.
     * @return A new instance of {@link CertificateAuthSettings} with the updated setting.
     */
    public CertificateAuthSettings trustStore(KeyStore trustStore) {
        return new CertificateAuthSettings(keystoreType, trustStoreType, port, trustStore, keyStore, x509HostnameVerifier, sslSocketFactory);
    }

    /**
     * @param keyStore The key store to use, by default no (null) key store is used.
     * @return A new instance of {@link CertificateAuthSettings} with the updated setting.
     */
    public CertificateAuthSettings keyStore(KeyStore keyStore) {
        return new CertificateAuthSettings(keystoreType, trustStoreType, port, trustStore, keyStore, x509HostnameVerifier, sslSocketFactory);
    }

    /**
     * @param sslSocketFactory The SSLSocketFactory to use
     * @return A new instance of {@link CertificateAuthSettings} with the updated setting.
     */
    public CertificateAuthSettings sslSocketFactory(SSLSocketFactory sslSocketFactory) {
        return new CertificateAuthSettings(keystoreType, trustStoreType, port, trustStore, keyStore, x509HostnameVerifier, sslSocketFactory);
    }

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

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

    /**
     * Syntactic sugar.
     *
     * @return The same CertificateAuthOptions instance.
     */
    public CertificateAuthSettings using() {
        return this;
    }

    /**
     * Create a new instance of {@link CertificateAuthSettings} with default values. Same as called {@link CertificateAuthSettings#CertificateAuthSettings()} but a bit more "fluent".
     *
     * @return a new instance of {@link CertificateAuthSettings} with default values.
     */
    public static CertificateAuthSettings certAuthSettings() {
        return new CertificateAuthSettings();
    }
}


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

import io.restassured.path.xml.XmlPath;
import io.restassured.path.xml.mapper.factory.DefaultJAXBObjectMapperFactory;
import io.restassured.path.xml.mapper.factory.JAXBObjectMapperFactory;
import io.restassured.path.xml.mapping.XmlPathObjectDeserializer;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;

import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.Map;

/**
 * Allows you to configure how XmlPath will handle object mapping (de-serialization).
 */
public class XmlPathConfig {

    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 XmlPathObjectDeserializer defaultDeserializer;
    private final JAXBObjectMapperFactory jaxbObjectMapperFactory;
    private final XmlParserType defaultParserType;
    private final String charset;
    private final Map<String, Boolean> features;
    private final Map<String, Object> properties;
    private final Map<String, String> declaredNamespaces;
    private final boolean validating;
    private final boolean namespaceAware;
    private final boolean allowDocTypeDeclaration;

    /**
     * Create a new instance of a XmlPathConfig based on the properties in the supplied config.
     *
     * @param config The config to copy.
     */
    public XmlPathConfig(XmlPathConfig config) {
        this(config.jaxbObjectMapperFactory(), config.defaultParserType(), config.defaultDeserializer(), config.charset(),
                new HashMap<String, Boolean>(), new HashMap<String, String>(), new HashMap<String, Object>(), config.isValidating(),
                config.isNamespaceAware(), config.isAllowDocTypeDeclaration());
    }

    /**
     * Creates a new XmlPathConfig that is configured to use the default JAXBObjectMapperFactory.
     */
    public XmlPathConfig() {
        this(new DefaultJAXBObjectMapperFactory(), null, null, defaultCharset(), new HashMap<String, Boolean>(), new HashMap<String, String>(),
                new HashMap<String, Object>(), DEFAULT_VALIDATING, DEFAULT_NAMESPACE_AWARE, DEFAULT_ALLOW_DOC_TYPE_DECLARATION);
    }

    /**
     * Create a new XmlPathConfig that uses the defaultCharset when deserializing XML data.
     */
    public XmlPathConfig(String defaultCharset) {
        this(new DefaultJAXBObjectMapperFactory(), null, null, defaultCharset, new HashMap<String, Boolean>(), new HashMap<String, String>(), new HashMap<String, Object>(),
                DEFAULT_VALIDATING, DEFAULT_NAMESPACE_AWARE, DEFAULT_ALLOW_DOC_TYPE_DECLARATION);

    }

    private XmlPathConfig(JAXBObjectMapperFactory jaxbObjectMapperFactory, XmlParserType defaultParserType,
                          XmlPathObjectDeserializer defaultDeserializer, String charset, Map<String, Boolean> features,
                          Map<String, String> declaredNamespaces, Map<String, Object> properties, boolean validating,
                          boolean namespaceAware, boolean allowDocTypeDeclaration) {
        charset = StringUtils.trimToNull(charset);
        if (charset == null) throw new IllegalArgumentException("Charset cannot be empty");
        this.charset = charset;
        this.defaultDeserializer = defaultDeserializer;
        this.defaultParserType = defaultParserType;
        this.jaxbObjectMapperFactory = jaxbObjectMapperFactory;
        this.features = features;
        this.declaredNamespaces = declaredNamespaces;
        this.properties = properties;
        this.validating = validating;
        this.namespaceAware = namespaceAware;
        this.allowDocTypeDeclaration = allowDocTypeDeclaration;
    }

    private static String defaultCharset() {
        return Charset.defaultCharset().name();
    }

    /**
     * @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);
    }

    /**
     * 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 XmlPathConfig features(Map<String, Boolean> features) {
        Validate.notNull(features, "Features cannot be null");
        return new XmlPathConfig(jaxbObjectMapperFactory, defaultParserType, defaultDeserializer, charset, features, declaredNamespaces,
                properties, validating, namespaceAware, allowDocTypeDeclaration);
    }

    /**
     * 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 XmlPathConfig instance
     * @see org.xml.sax.XMLReader#setFeature(java.lang.String, boolean)
     */
    public XmlPathConfig 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 XmlPathConfig(jaxbObjectMapperFactory, defaultParserType, defaultDeserializer, charset, newFeatures, declaredNamespaces, properties, validating, namespaceAware, allowDocTypeDeclaration);
    }

    /**
     * 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 XmlPathConfig validating(boolean isValidating) {
        return new XmlPathConfig(jaxbObjectMapperFactory, defaultParserType, defaultDeserializer, charset, features, declaredNamespaces, properties, isValidating, namespaceAware, allowDocTypeDeclaration);
    }

    /**
     * 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 if XmlPath should provide support for XML namespaces (default is {@value #DEFAULT_NAMESPACE_AWARE}).
     * Note that this is only applicable when {@link XmlPath.CompatibilityMode} is equal to
     * {@link XmlPath.CompatibilityMode#XML}.
     *
     * @param namespaceAware true if the parser should provide support for XML namespaces; false otherwise.
     * @return A new XmlPathConfig instance
     */
    public XmlPathConfig namespaceAware(boolean namespaceAware) {
        return new XmlPathConfig(jaxbObjectMapperFactory, defaultParserType, defaultDeserializer, charset, features, declaredNamespaces, properties, validating, namespaceAware, allowDocTypeDeclaration);
    }

    /**
     * Whether XmlPath should provide support for XML namespaces
     *
     * @return a boolean indicating whether this is true or false
     */
    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 XmlPathConfig allowDocTypeDeclaration(boolean allowDocTypeDeclaration) {
        return new XmlPathConfig(jaxbObjectMapperFactory, defaultParserType, defaultDeserializer, charset, features, declaredNamespaces, properties, validating, namespaceAware, allowDocTypeDeclaration);
    }

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

    /**
     * @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 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 A new XmlPathConfig instance
     * @see org.xml.sax.XMLReader#setFeature(java.lang.String, boolean)
     */
    public XmlPathConfig properties(Map<String, Object> properties) {
        Validate.notNull(properties, "Properties cannot be null");
        return new XmlPathConfig(jaxbObjectMapperFactory, defaultParserType, defaultDeserializer, charset, features, declaredNamespaces,
                properties, validating, namespaceAware, allowDocTypeDeclaration);
    }

    /**
     * Set a value of a property.
     *
     * @param name  The property name
     * @param value The value of the property
     * @return A new XmlPathConfig instance
     * @see org.xml.sax.XMLReader#setFeature(java.lang.String, boolean)
     */
    public XmlPathConfig 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 XmlPathConfig(jaxbObjectMapperFactory, defaultParserType, defaultDeserializer, charset, features, declaredNamespaces,
                newProperties, validating, namespaceAware, allowDocTypeDeclaration);
    }

    /**
     * Disables external DTD loading.
     *

     * This is a shortcut for doing:

     *

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

     *

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

    /**
     * @return The charset to assume when parsing XML data
     */
    public String charset() {
        return charset;
    }

    /**
     * @return A new XmlPathConfig instance with that assumes the supplied charset when parsing XML documents.
     */
    public XmlPathConfig charset(String charset) {
        return new XmlPathConfig(jaxbObjectMapperFactory, defaultParserType, defaultDeserializer, charset, features, declaredNamespaces,
                properties, validating, namespaceAware, allowDocTypeDeclaration);
    }

    public XmlParserType defaultParserType() {
        return defaultParserType;
    }

    public boolean hasDefaultParserType() {
        return defaultParserType != null;
    }

    public boolean hasCustomJaxbObjectMapperFactory() {
        return jaxbObjectMapperFactory() != null && jaxbObjectMapperFactory().getClass() != DefaultJAXBObjectMapperFactory.class;
    }

    public XmlPathObjectDeserializer defaultDeserializer() {
        return defaultDeserializer;
    }

    public boolean hasDefaultDeserializer() {
        return defaultDeserializer != null;
    }

    /**
     * Creates an xml path configuration that uses the specified parser type as default.
     *
     * @param defaultParserType The default parser type to use. If null then classpath scanning will be used.
     */
    public XmlPathConfig defaultParserType(XmlParserType defaultParserType) {
        return new XmlPathConfig(jaxbObjectMapperFactory, defaultParserType, defaultDeserializer, charset, features, declaredNamespaces,
                properties, validating, namespaceAware, allowDocTypeDeclaration);
    }

    /**
     * Creates an json path configuration that uses the specified object de-serializer as default.
     *
     * @param defaultObjectDeserializer The object de-serializer to use. If null then classpath scanning will be used.
     */
    public XmlPathConfig defaultObjectDeserializer(XmlPathObjectDeserializer defaultObjectDeserializer) {
        return new XmlPathConfig(jaxbObjectMapperFactory, defaultParserType, defaultObjectDeserializer, charset, features, declaredNamespaces,
                properties, validating, namespaceAware, allowDocTypeDeclaration);
    }

    public JAXBObjectMapperFactory jaxbObjectMapperFactory() {
        return jaxbObjectMapperFactory;
    }

    /**
     * @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.
     *
     * @param namespacesToDeclare A map containing features that will be used by the underlying {@link groovy.util.XmlSlurper}.
     * @return A new instance of XmlPathConfig
     * @see org.xml.sax.XMLReader#setFeature(java.lang.String, boolean)
     */
    public XmlPathConfig declareNamespaces(Map<String, String> namespacesToDeclare) {
        return new XmlPathConfig(jaxbObjectMapperFactory, defaultParserType, defaultDeserializer, charset, features, namespacesToDeclare,
                properties, validating, namespaceAware, allowDocTypeDeclaration);
    }

    /**
     * Declares a namespace.
     *
     * @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 XmlPathConfig instance
     * @see org.xml.sax.XMLReader#setFeature(java.lang.String, boolean)
     */
    public XmlPathConfig declaredNamespace(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 XmlPathConfig(jaxbObjectMapperFactory, defaultParserType, defaultDeserializer, charset, features, updatedNamespaces,
                properties, validating, true, allowDocTypeDeclaration);
    }

    /**
     * Specify a custom Gson object mapper factory.
     *
     * @param jaxbObjectMapperFactory The object mapper factory
     */
    public XmlPathConfig jaxbObjectMapperFactory(JAXBObjectMapperFactory jaxbObjectMapperFactory) {
        return new XmlPathConfig(jaxbObjectMapperFactory, defaultParserType, defaultDeserializer, charset, features, declaredNamespaces,
                properties, validating, namespaceAware, allowDocTypeDeclaration);
    }

    public boolean hasDeclaredNamespaces() {
        return !declaredNamespaces.isEmpty();
    }

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

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

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