/**
 * The MIT License (MIT)
 *
 * Copyright (c) 2014-2019 <mickael.jeanroy@gmail.com>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

package com.github.mjeanroy.junit.servers.servers;

import java.io.File;
import java.io.FileFilter;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import static com.github.mjeanroy.junit.servers.commons.Preconditions.notBlank;
import static com.github.mjeanroy.junit.servers.commons.Preconditions.notNull;
import static com.github.mjeanroy.junit.servers.commons.Preconditions.positive;
import static com.github.mjeanroy.junit.servers.servers.AbstractConfiguration.DEFAULT_CLASSPATH;
import static com.github.mjeanroy.junit.servers.servers.AbstractConfiguration.DEFAULT_PATH;
import static com.github.mjeanroy.junit.servers.servers.AbstractConfiguration.DEFAULT_PORT;
import static com.github.mjeanroy.junit.servers.servers.AbstractConfiguration.DEFAULT_WEBAPP;

/**
 * Builder for {@link AbstractConfiguration} instances, should be extended by custom configuration implementation.
 *
 * @param <T> Type of extended builder.
 * @param <U> Type of {@link AbstractConfiguration} implementation.
 */
public abstract class AbstractConfigurationBuilder<T extends AbstractConfigurationBuilder<T, U>, U extends AbstractConfiguration> {

	/**
	 * Path value.
	 *
	 * @see AbstractConfiguration#getPath()
	 */
	private String path;

	/**
	 * Webapp value.
	 *
	 * @see AbstractConfiguration#getWebapp()
	 */
	private String webapp;

	/**
	 * Port value.
	 *
	 * @see AbstractConfiguration#getPort()
	 */
	private int port;

	/**
	 * Classpath value.
	 *
	 * @see AbstractConfiguration#getClasspath()
	 */
	private String classpath;

	/**
	 * Parent classloader.
	 *
	 * @see AbstractConfiguration#getParentClassLoader()
	 */
	private ClassLoader parentClassLoader;

	/**
	 * Map of properties.
	 *
	 * @see AbstractConfiguration#getEnvProperties()
	 */
	private final Map<String, String> envProperties;

	/**
	 * List of executable hooks.
	 *
	 * @see AbstractConfiguration#getHooks()
	 */
	private final List<Hook> hooks;

	/**
	 * The path of the custom web.xml descriptor.
	 *
	 * @see AbstractConfiguration#getOverrideDescriptor()
	 */
	private String overrideDescriptor;

	/**
	 * Build default configuration.
	 */
	protected AbstractConfigurationBuilder() {
		this.path = DEFAULT_PATH;
		this.webapp = DEFAULT_WEBAPP;
		this.port = DEFAULT_PORT;
		this.classpath = DEFAULT_CLASSPATH;
		this.envProperties = new LinkedHashMap<>();
		this.hooks = new ArrayList<>();
		this.parentClassLoader = null;
	}

	/**
	 * The {@code this} object, useful to get correct chaining.
	 *
	 * @return this.
	 */
	protected abstract T self();

	/**
	 * Build the final configuration instance.
	 *
	 * @return The configuration instance.
	 */
	public abstract U build();

	/**
	 * Get current {@link #path}.
	 *
	 * @return {@link #path}.
	 */
	public String getPath() {
		return path;
	}

	/**
	 * Get current {@link #webapp}.
	 *
	 * @return {@link #webapp}.
	 */
	public String getWebapp() {
		return webapp;
	}

	/**
	 * Get current {@link #classpath}.
	 *
	 * @return {@link #classpath}.
	 */
	public String getClasspath() {
		return classpath;
	}

	/**
	 * Get current {@link #port}.
	 *
	 * @return {@link #port}.
	 */
	public int getPort() {
		return port;
	}

	/**
	 * Get current {@link #envProperties}.
	 *
	 * @return {@link #envProperties}.
	 */
	public Map<String, String> getEnvProperties() {
		return envProperties;
	}

	/**
	 * Get current {@link #hooks}.
	 *
	 * @return {@link #hooks}.
	 */
	public List<Hook> getHooks() {
		return hooks;
	}

	/**
	 * Get current {@link #parentClassLoader}.
	 *
	 * @return {@link #parentClassLoader}.
	 * @deprecated Use {@link #getParentClassLoader()} instead.
	 */
	@Deprecated
	public ClassLoader getParentClasspath() {
		return getParentClassLoader();
	}

	/**
	 * Get current {@link #parentClassLoader}.
	 *
	 * @return {@link #parentClassLoader}.
	 */
	public ClassLoader getParentClassLoader() {
		return parentClassLoader;
	}

	/**
	 * Get current {@link #overrideDescriptor}.
	 *
	 * @return {@link #overrideDescriptor}.
	 */
	public String getOverrideDescriptor() {
		return overrideDescriptor;
	}

	/**
	 * Change {@link #path} value.
	 *
	 * @param path New {@link #path} value.
	 * @return this
	 * @throws NullPointerException If {@code path} is {@code null}.
	 */
	public T withPath(String path) {
		this.path = notNull(path, "path");
		return self();
	}

	/**
	 * Change {@link #webapp} value.
	 *
	 * @param webapp New {@link #webapp} value.
	 * @return this
	 * @throws NullPointerException If {@code webapp} is {@code null}.
	 */
	public T withWebapp(String webapp) {
		this.webapp = notNull(webapp, "webapp");
		return self();
	}

	/**
	 * Change {@link #webapp} value.
	 *
	 * @param webapp New {@link #webapp} value (the absolute path will be used).
	 * @return this
	 * @throws NullPointerException If {@code webapp} is null.
	 */
	public T withWebapp(File webapp) {
		notNull(webapp, "webapp");
		this.webapp = webapp.getAbsolutePath();
		return self();
	}

	/**
	 * Change {@link #port} value.
	 *
	 * @param port New {@link #port} value.
	 * @return this
	 * @throws IllegalArgumentException If {@code port} is strictly lower than zero.
	 */
	public T withPort(int port) {
		this.port = positive(port, "port");
		return self();
	}

	/**
	 * Change {@link #classpath} value.
	 *
	 * @param classpath New {@link #classpath} value.
	 * @return this
	 */
	public T withClasspath(String classpath) {
		this.classpath = classpath;
		return self();
	}

	/**
	 * Add new property entry to the {@link #envProperties} map.
	 *
	 * @param name Property name.
	 * @param value Property value.
	 * @return this
	 * @throws NullPointerException If {@code name} or {@code value} are {@code null}.
	 * @throws IllegalArgumentException if {@code name} is empty or blank.
	 */
	public T withProperty(String name, String value) {
		this.envProperties.put(
			notBlank(name, "name"),
			notNull(value, "value")
		);
		return self();
	}

	/**
	 * Add new executable hook to the {@link #hooks} list.
	 *
	 * @param hook Hook.
	 * @return this
	 * @throws NullPointerException if {@code hook} is {@code null}.
	 */
	public T withHook(Hook hook) {
		this.hooks.add(notNull(hook, "hook"));
		return self();
	}

	/**
	 * Change {@link #parentClassLoader} value.
	 *
	 * @param cls The class that will be used to get classloader.
	 * @param filter The file filter.
	 * @return this
	 * @deprecated The {@code filter} parameter will be useless in the next release for JDK9 compatibility, please use {@link #withParentClassLoader(Class)} instead.
	 */
	@Deprecated
	public T withParentClasspath(Class<?> cls, FileFilter filter) {
		return withParentClassLoader(cls);
	}

	/**
	 * Change {@link #parentClassLoader} value.
	 *
	 * @param cls The class that will be used to get classloader.
	 * @return this
	 * @deprecated Use {@link #withParentClassLoader(Class)} instead.
	 */
	@Deprecated
	public T withParentClasspath(Class<?> cls) {
		notNull(cls, "Base class");
		return withParentClassLoader(cls.getClassLoader());
	}

	/**
	 * Change {@link #parentClassLoader} value.
	 *
	 * @param cls The class that will be used to get classloader.
	 * @return this
	 */
	public T withParentClassLoader(Class<?> cls) {
		notNull(cls, "Base class");
		return withParentClassLoader(cls.getClassLoader());
	}

	/**
	 * Change {@link #parentClassLoader} value.
	 *
	 * @param classpath New classpath entry.
	 * @param others Other (optional) classpath urls.
	 * @return this
	 */
	public T withParentClasspath(URL classpath, URL... others) {
		Set<URL> classpathUrls = new HashSet<>();
		classpathUrls.add(classpath);
		Collections.addAll(classpathUrls, others);
		return withParentClasspath(classpathUrls);
	}

	/**
	 * Change {@link #parentClassLoader} value.
	 *
	 * @param classpath New {@link #parentClassLoader} value.
	 * @return this
	 */
	public T withParentClasspath(Collection<URL> classpath) {
		int nbUrls = classpath.size();
		if (nbUrls > 0) {
			URL[] urls = classpath.toArray(new URL[nbUrls]);
			URLClassLoader urlClassLoader = new URLClassLoader(urls);
			withParentClassLoader(urlClassLoader);
		}

		return self();
	}

	private T withParentClassLoader(ClassLoader parentClassLoader) {
		this.parentClassLoader = parentClassLoader;
		return self();
	}

	/**
	 * Change {@link #overrideDescriptor} (path of the custom web.xml file descriptor=.
	 *
	 * @param overrideDescriptor The new {@link #overrideDescriptor} value.
	 * @return this
	 */
	public T withOverrideDescriptor(String overrideDescriptor) {
		this.overrideDescriptor = overrideDescriptor;
		return self();
	}
}


/**
 * The MIT License (MIT)
 *
 * Copyright (c) 2014-2019 <mickael.jeanroy@gmail.com>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

package com.github.mjeanroy.junit.servers.commons;

import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashSet;
import java.util.Locale;
import java.util.Set;
import java.util.TimeZone;

import static java.util.Collections.addAll;

/**
 * Static date utilities.
 *
 *

 *
 * Internal API: these methods are part of the internal API and may be removed, have their signature change,
 * or have their access level decreased from public to protected, package, or private in future versions without notice.
 */
public final class Dates {

	private Dates() {
	}

	/**
	 * Parse date using given pattern.
	 * Note that this method will return null if pattern is not
	 * valid or throws a {@link ParseException}.
	 *
	 * @param date Date.
	 * @param pattern Pattern.
	 * @param patterns Other pattern to test.
	 * @return Date, null if pattern is not valid.
	 */
	static Date parse(String date, String pattern, String... patterns) {
		Set<String> set = new HashSet<>();
		set.add(pattern);
		if (patterns != null) {
			addAll(set, patterns);
		}

		for (String p : set) {
			try {
				return df(p).parse(date);
			}
			catch (ParseException ex) {
				// Skip pattern
			}
		}

		return null;
	}

	/**
	 * Parse date using given pattern and return time value.
	 * Note that this method will return null if pattern is not
	 * valid or throws a {@link ParseException}.
	 *
	 * @param date Date.
	 * @param pattern Pattern.
	 * @param patterns Other pattern to test.
	 * @return Time value, null if pattern is not valid.
	 */
	public static Long getTime(String date, String pattern, String... patterns) {
		Date d = parse(date, pattern, patterns);
		return d == null ? null : d.getTime();
	}

	/**
	 * Format date according to given pattern.
	 *
	 * @param date Date.
	 * @param pattern Pattern.
	 * @return Formatted date.
	 */
	public static String format(Date date, String pattern) {
		return df(pattern).format(date);
	}

	/**
	 * Format timestamp according to given pattern.
	 *
	 * @param time Timestamp.
	 * @param pattern Pattern.
	 * @return Formatted date.
	 */
	static String formatTime(long time, String pattern) {
		Date date = new Date();
		date.setTime(time);
		return format(date, pattern);
	}

	private static DateFormat df(String pattern) {
		DateFormat df = new SimpleDateFormat(pattern, Locale.ENGLISH);
		df.setTimeZone(TimeZone.getTimeZone("GMT"));
		return df;
	}
}


/**
 * The MIT License (MIT)
 *
 * Copyright (c) 2014-2019 <mickael.jeanroy@gmail.com>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

package com.github.mjeanroy.junit.servers.commons;

import java.util.Collection;

import static java.lang.String.format;

/**
 * Static utilities that can be used to check object's values.
 *
 *

 *
 * Internal API: these methods are part of the internal API and may be removed, have their signature change,
 * or have their access level decreased from public to protected, package, or private in future versions without notice.
 */
public final class Preconditions {

	private Preconditions() {
	}

	/**
	 * Check that a given value is not null.
	 * It throws a {@link NullPointerException} exception if value is {@code null} or returned the
	 * value.
	 *
	 * @param value Value to check.
	 * @param name Name of parameter, it will produce an error message such as "{name} must not be null"
	 * @param <T> Type of value.
	 * @return First parameter if it is not null.
	 * @throws NullPointerException If {@code value} is {@code null}.
	 */
	public static <T> T notNull(T value, String name) {
		if (value == null) {
			throw new NullPointerException(format("%s must not be null", name));
		}
		return value;
	}

	/**
	 * Check that given collection is not empty (i.e null or with a size equal to zero).
	 * It throws a {@link IllegalArgumentException} exception if collection is empty or
	 * returned original collection otherwise.
	 *
	 * Generated message will be : {name} must not be empty
	 * Where name is the value given in parameter.
	 *
	 * @param collection Collection.
	 * @param name Name of field to produce exception message.
	 * @param <T> Type of elements.
	 * @return Original collection.
	 * @throws NullPointerException If {@code collection} is {@code null}.
	 * @throws IllegalArgumentException If {@code collection} is empty.
	 */
	public static <T> Collection<T> notEmpty(Collection<T> collection, String name) {
		notNull(collection, name);

		if (collection.isEmpty()) {
			throw new IllegalArgumentException(format("%s must not be empty", name));
		}

		return collection;
	}

	/**
	 * Check that a given string value is not blank (i.e not null, not empty and not blank).
	 * It throws a {@link NullPointerException} exception if value is null and throw an
	 * {@link java.lang.IllegalArgumentException} if value is empty or blank.
	 * Otherwise original value is returned
	 *
	 * @param value Value to check.
	 * @param name Name of parameter, it will produce an error message such as "{name} must not be null"
	 *             or "{name} must not be blank".
	 * @return First parameter if it is not blank.
	 */
	public static String notBlank(String value, String name) {
		notNull(value, name);

		if (Strings.isBlank(value)) {
			throw new IllegalArgumentException(format("%s must not be blank", name));
		}

		return value;
	}

	/**
	 * Check that a given integer is positive.
	 * If integer value is negative, it throws an {@link IllegalArgumentException} exception,
	 * otherwise integer value is returned.
	 *
	 * @param value Value to check.
	 * @param name Name of value.
	 * @return Integer value if value is positive.
	 */
	public static int positive(int value, String name) {
		return checkPositiveNumber(value, name);
	}

	/**
	 * Check that a given long value is positive.
	 * If long value is negative, it throws an {@link IllegalArgumentException} exception,
	 * otherwise integer value is returned.
	 *
	 * @param value Value to check.
	 * @param name Name of value.
	 * @return Long value if value is positive.
	 */
	public static long positive(long value, String name) {
		return checkPositiveNumber(value, name);
	}

	private static <T extends Number> T checkPositiveNumber(T value, String name) {
		if (value.doubleValue() < 0) {
			throw new IllegalArgumentException(format("%s must be positive", name));
		}
		return value;
	}
}


/**
 * The MIT License (MIT)
 *
 * Copyright (c) 2014-2019 <mickael.jeanroy@gmail.com>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

package com.github.mjeanroy.junit.servers.client.impl.okhttp3;

import com.github.mjeanroy.junit.servers.client.Cookies;
import com.github.mjeanroy.junit.servers.client.HttpHeader;
import com.github.mjeanroy.junit.servers.client.HttpHeaders;
import com.github.mjeanroy.junit.servers.client.HttpMethod;
import com.github.mjeanroy.junit.servers.client.HttpParameter;
import com.github.mjeanroy.junit.servers.client.HttpRequest;
import com.github.mjeanroy.junit.servers.client.HttpResponse;
import com.github.mjeanroy.junit.servers.client.HttpUrl;
import com.github.mjeanroy.junit.servers.client.impl.AbstractHttpRequest;
import okhttp3.Call;
import okhttp3.FormBody;
import okhttp3.MediaType;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;

import static com.github.mjeanroy.junit.servers.commons.ObjectUtils.firstNonNull;

/**
 * Implementation of {@link HttpRequest} using OkHttp library.
 *
 * @see com.github.mjeanroy.junit.servers.client.HttpClientStrategy#OK_HTTP3
 * @see http://square.github.io/okhttp
 */
class OkHttpRequest extends AbstractHttpRequest implements HttpRequest {

	/**
	 * The native OkHttp client.
	 */
	private final okhttp3.OkHttpClient client;

	/**
	 * Create apache http request.
	 *
	 * @param client Apache http client.
	 * @param httpMethod Http method.
	 * @param endpoint Http request url.
	 */
	OkHttpRequest(okhttp3.OkHttpClient client, HttpMethod httpMethod, HttpUrl endpoint) {
		super(endpoint, httpMethod);
		this.client = client;
	}

	@Override
	protected HttpResponse doExecute() throws Exception {
		final HttpUrl endpoint = getEndpoint();
		final okhttp3.HttpUrl.Builder httpUrlBuilder = new okhttp3.HttpUrl.Builder()
			.scheme(endpoint.getScheme())
			.host(endpoint.getHost())
			.port(endpoint.getPort())
			.addPathSegments(endpoint.getPath().substring(1));

		// Append all query parameters.
		for (HttpParameter queryParam : queryParams.values()) {
			httpUrlBuilder.addEncodedQueryParameter(queryParam.getEncodedName(), queryParam.getEncodedValue());
		}

		final Request.Builder builder = new Request.Builder().url(httpUrlBuilder.build());
		handleCookies(builder);
		handleHeaders(builder);
		handleBody(builder);

		final Call call = client.newCall(builder.build());

		final long start = System.nanoTime();
		final Response response = call.execute();
		final long duration = System.nanoTime() - start;

		return OkHttpResponseFactory.of(response, duration);
	}

	/**
	 * Add all HTTP headers to the final request.
	 *
	 * @param builder The OkHttp request builder.
	 * @see Request.Builder#addHeader(String, String)
	 */
	private void handleHeaders(Request.Builder builder) {
		for (HttpHeader h : headers.values()) {
			builder.addHeader(h.getName(), h.serializeValues());
		}
	}

	/**
	 * Add all cookies to the final request (i.e add the {@code Cookie} header).
	 *
	 * @param builder The OkHttp request builder.
	 * @see Request.Builder#addHeader(String, String)
	 */
	private void handleCookies(Request.Builder builder) {
		if (!cookies.isEmpty()) {
			builder.addHeader(HttpHeaders.COOKIE, Cookies.serialize(cookies));
		}
	}

	/**
	 * Add request body if appropriate:
	 *

	 *
Add request body if {@link #body} is defined.

	 *
Add form parameters ({@link #formParams} otherwise if it is not empty.

	 *

	 *
	 * @param builder The native OkHttp request builder.
	 * @see Request.Builder#method(String, RequestBody)
	 * @see RequestBody
	 */
	private void handleBody(Request.Builder builder) {
		HttpMethod method = getMethod();
		RequestBody body = hasBody() ? createBody() : null;

		// Force an empty body, as POST & PUT methods requires
		// a body element.
		if (body == null && method.isBodyAllowed()) {
			body = createEmptyBody();
		}

		builder.method(method.getVerb(), body);
	}

	/**
	 * Create the OkHttp request body:
	 *

	 *
Create body from {@link #body} value if it is defined.

	 *
Create {@link FormBody} from {@link #formParams} otherwise if it is not empty.

	 *
Returns {@code null} otherwise.

	 *

	 *
	 * @return OkHttp {@link RequestBody} instance.
	 * @see RequestBody#create(MediaType, String)
	 * @see FormBody
	 */
	private RequestBody createBody() {
		RequestBody rqBody = null;

		// Try request body first.
		if (body != null && !body.isEmpty()) {
			rqBody = RequestBody.create(null, body);
		}
		else if (!formParams.isEmpty()) {
			FormBody.Builder builder = new FormBody.Builder();
			for (HttpParameter parameter : formParams.values()) {
				String encodedName = parameter.getEncodedName();
				String encodedValue = firstNonNull(parameter.getEncodedValue(), "");
				builder.addEncoded(encodedName, encodedValue);
			}

			rqBody = builder.build();
		}

		return rqBody;
	}

	/**
	 * Create a new empty request body.
	 *
	 * @return Request body element.
	 * @see RequestBody
	 */
	private static RequestBody createEmptyBody() {
		return RequestBody.create(null, "");
	}
}


/**
 * The MIT License (MIT)
 *
 * Copyright (c) 2014-2019 <mickael.jeanroy@gmail.com>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

package com.github.mjeanroy.junit.servers.commons;

import java.util.Collection;

/**
 * Static string utilities.
 *
 *

 *
 * Internal API: these methods are part of the internal API and may be removed, have their signature change,
 * or have their access level decreased from public to protected, package, or private in future versions without notice.
 */
public final class Strings {

	private Strings() {
	}

	/**
	 * Check that given string is blank.
	 *
	 * @param value String to check.
	 * @return {@code true} if string is blank, {@code false} otherwise.
	 */
	static boolean isBlank(String value) {
		if (value == null) {
			return true;
		}

		for (char character : value.toCharArray()) {
			if (!Character.isWhitespace(character)) {
				return false;
			}
		}

		return true;
	}

	/**
	 * Check that given string is not blank.
	 *
	 * @param value String to check.
	 * @return {@code true} if string is not blank, {@code false} otherwise.
	 */
	public static boolean isNotBlank(String value) {
		return !isBlank(value);
	}

	/**
	 * Remove string prefix if and only if string value starts with
	 * the prefix, otherwise original string is returned.
	 *
	 * @param value String value.
	 * @param prefix String prefix.
	 * @return New string.
	 */
	public static String removePrefix(String value, String prefix) {
		if (value == null || prefix == null || prefix.length() > value.length()) {
			return value;
		}

		return value.startsWith(prefix) ? value.substring(prefix.length()) : value;
	}

	/**
	 * Returns the lowercase value of a given string:
	 *

	 *
Returns {@code null} if {@code value} is {@code null}.

	 *
Returns the result of {@link String#toLowerCase()} otherwise.

	 *

	 *
	 * @param value The string value.
	 * @return The lowercase value.
	 */
	public static String toLowerCase(String value) {
		return value == null ? null : value.toLowerCase();
	}

	/**
	 * Join all strings to a single one using given separator.
	 *
	 * @param separator String separator.
	 * @param values All string to join.
	 * @return The final string.
	 */
	public static String join(String separator, Collection<String> values) {
		StringBuilder builder = new StringBuilder();
		boolean first = true;

		for (String value : values) {
			if (!first) {
				builder.append(separator);
			}

			builder.append(value);
			first = false;
		}

		return builder.toString();
	}
}


/**
 * The MIT License (MIT)
 *
 * Copyright (c) 2014-2019 <mickael.jeanroy@gmail.com>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

package com.github.mjeanroy.junit.servers.client.impl.async;

import com.github.mjeanroy.junit.servers.client.Cookies;
import com.github.mjeanroy.junit.servers.client.HttpHeader;
import com.github.mjeanroy.junit.servers.client.HttpHeaders;
import com.github.mjeanroy.junit.servers.client.HttpMethod;
import com.github.mjeanroy.junit.servers.client.HttpParameter;
import com.github.mjeanroy.junit.servers.client.HttpRequest;
import com.github.mjeanroy.junit.servers.client.HttpResponse;
import com.github.mjeanroy.junit.servers.client.HttpUrl;
import com.github.mjeanroy.junit.servers.client.impl.AbstractHttpRequest;
import io.netty.handler.codec.http.cookie.Cookie;
import org.asynchttpclient.ListenableFuture;
import org.asynchttpclient.Request;
import org.asynchttpclient.RequestBuilder;
import org.asynchttpclient.Response;
import org.asynchttpclient.uri.Uri;
import org.asynchttpclient.util.Utf8UrlEncoder;

import static java.lang.System.nanoTime;

/**
 * Implementation for {@link HttpRequest} that use async-http-client
 * under the hood.
 *
 * @see https://asynchttpclient.github.io/
 * @see com.github.mjeanroy.junit.servers.client.HttpClientStrategy#ASYNC_HTTP_CLIENT
 */
class AsyncHttpRequest extends AbstractHttpRequest implements HttpRequest {

	/**
	 * Original http client, will be used to execute http request.
	 */
	private final org.asynchttpclient.AsyncHttpClient client;

	/**
	 * Create http request.
	 *
	 * @param client Client used to execute request using async-http-client.
	 * @param httpMethod Http method.
	 * @param url Request URL.
	 */
	AsyncHttpRequest(org.asynchttpclient.AsyncHttpClient client, HttpMethod httpMethod, HttpUrl url) {
		super(url, httpMethod);
		this.client = client;
	}

	@Override
	protected HttpResponse doExecute() throws Exception {
		final HttpUrl endpoint = getEndpoint();
		final String scheme = endpoint.getScheme();
		final String userInfo = null;
		final String host = endpoint.getHost();
		final int port = endpoint.getPort();
		final String path = Utf8UrlEncoder.encodePath(endpoint.getPath());
		final String query = null;
		final String fragment = null;
		final Uri uri = new Uri(scheme, userInfo, host, port, path, query, fragment);

		final String method = getMethod().getVerb();
		final RequestBuilder builder = new RequestBuilder(method, true).setUri(uri);

		handleQueryParameters(builder);
		handleBody(builder);
		handleHeaders(builder);
		handleCookies(builder);

		final Request request = builder.build();
		final ListenableFuture<Response> future = client.executeRequest(request);

		final long start = nanoTime();
		final Response response = future.get();
		final long duration = nanoTime() - start;

		return AsyncHttpResponseFactory.of(response, duration);
	}

	/**
	 * Add query parameter to the final HTTP request.
	 *
	 * @param builder The pending HTTP request.
	 * @see RequestBuilder#addQueryParam(String, String)
	 */
	private void handleQueryParameters(RequestBuilder builder) {
		for (HttpParameter p : queryParams.values()) {
			builder.addQueryParam(p.getEncodedName(), p.getEncodedValue());
		}
	}

	/**
	 * Add body entity to the final HTTP request.
	 *
	 * @param builder The pending HTTP request.
	 * @see RequestBuilder#addFormParam(String, String)
	 * @see RequestBuilder#setBody(String)
	 */
	private void handleBody(RequestBuilder builder) {
		if (!hasBody()) {
			return;
		}

		if (body != null) {
			handleRequestBody(builder);
		}
		else {
			handleFormParameters(builder);
		}
	}

	/**
	 * Serialize form parameters to the request body.
	 *
	 * @param builder The pending HTTP request.
	 * @see RequestBuilder#addFormParam(String, String)
	 */
	private void handleFormParameters(RequestBuilder builder) {
		for (HttpParameter p : formParams.values()) {
			builder.addFormParam(p.getName(), p.getValue());
		}
	}

	/**
	 * Set body to the final HTTP request.
	 *
	 * @param builder The pending HTTP request.
	 * @see RequestBuilder#setBody(String)
	 */
	private void handleRequestBody(RequestBuilder builder) {
		builder.setBody(body);
	}

	/**
	 * Add headers to the final HTTP request.
	 *
	 * @param builder The pending HTTP request.
	 * @see RequestBuilder#addHeader(CharSequence, String)
	 */
	private void handleHeaders(RequestBuilder builder) {
		for (HttpHeader header : headers.values()) {
			builder.addHeader(header.getName(), header.getValues());
		}
	}

	/**
	 * Add cookies to the final HTTP request.
	 *
	 * @param builder The pending HTTP request.
	 * @see RequestBuilder#addCookie(Cookie)
	 */
	private void handleCookies(RequestBuilder builder) {
		if (!cookies.isEmpty()) {
			builder.addHeader(HttpHeaders.COOKIE, Cookies.serialize(cookies));
		}
	}
}


/**
 * The MIT License (MIT)
 *
 * Copyright (c) 2014-2019 <mickael.jeanroy@gmail.com>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

package com.github.mjeanroy.junit.servers.client.impl.ning;

import com.github.mjeanroy.junit.servers.client.Cookies;
import com.github.mjeanroy.junit.servers.client.HttpHeader;
import com.github.mjeanroy.junit.servers.client.HttpHeaders;
import com.github.mjeanroy.junit.servers.client.HttpMethod;
import com.github.mjeanroy.junit.servers.client.HttpParameter;
import com.github.mjeanroy.junit.servers.client.HttpRequest;
import com.github.mjeanroy.junit.servers.client.HttpResponse;
import com.github.mjeanroy.junit.servers.client.HttpUrl;
import com.github.mjeanroy.junit.servers.client.impl.AbstractHttpRequest;
import com.ning.http.client.AsyncHttpClient;
import com.ning.http.client.Request;
import com.ning.http.client.RequestBuilder;
import com.ning.http.client.Response;
import com.ning.http.client.uri.Uri;
import com.ning.http.util.UTF8UrlEncoder;

import static java.lang.System.nanoTime;

/**
 * Implementation for {@link HttpRequest} that use (ning) async-http-client
 * under the hood.
 *
 * @see https://github.com/ning/async-http-client
 * @see com.github.mjeanroy.junit.servers.client.HttpClientStrategy#NING_ASYNC_HTTP_CLIENT
 */
class NingAsyncHttpRequest extends AbstractHttpRequest implements HttpRequest {

	/**
	 * Original http client, will be used to execute http request.
	 */
	private final AsyncHttpClient client;

	/**
	 * Create http request.
	 *
	 * @param client Client used to execute request using async-http-client.
	 * @param httpMethod Http method.
	 * @param url Request URL.
	 */
	NingAsyncHttpRequest(AsyncHttpClient client, HttpMethod httpMethod, HttpUrl url) {
		super(url, httpMethod);
		this.client = client;
	}

	@Override
	protected HttpResponse doExecute() throws Exception {
		final HttpUrl endpoint = getEndpoint();
		final String scheme = endpoint.getScheme();
		final String userInfo = null;
		final String host = endpoint.getHost();
		final int port = endpoint.getPort();
		final String path = UTF8UrlEncoder.encodePath(endpoint.getPath());
		final String query = null;
		final Uri uri = new Uri(scheme, userInfo, host, port, path, query);

		final String method = getMethod().getVerb();
		final RequestBuilder builder = new RequestBuilder(method, true).setUri(uri);

		handleQueryParameters(builder);
		handleBody(builder);
		handleHeaders(builder);
		handleCookies(builder);

		final Request request = builder.build();
		final long start = nanoTime();
		final Response response = client.executeRequest(request).get();
		final long duration = nanoTime() - start;

		return NingAsyncHttpResponseFactory.of(response, duration);
	}

	/**
	 * Add query parameter to the final HTTP request.
	 *
	 * @param builder The pending HTTP request.
	 * @see RequestBuilder#addQueryParam(String, String)
	 */
	private void handleQueryParameters(RequestBuilder builder) {
		for (HttpParameter p : queryParams.values()) {
			builder.addQueryParam(p.getEncodedName(), p.getEncodedValue());
		}
	}

	/**
	 * Add request body:
	 *
	 *

	 *
Set request body if {@link #body} is defined.

	 *
Add form parameters ({@link #formParams}) otherwise if it is not empty.

	 *

	 *
	 * @param builder The pending HTTP request.
	 * @see RequestBuilder#addFormParam(String, String)
	 * @see RequestBuilder#setBody(String)
	 */
	private void handleBody(RequestBuilder builder) {
		if (!hasBody()) {
			return;
		}

		if (body != null) {
			handleRequestBody(builder);
		}
		else {
			handleFormParameters(builder);
		}
	}

	/**
	 * Add form parameters to the final HTTP request.
	 *
	 * @param builder The pending HTTP request.
	 * @see RequestBuilder#addFormParam(String, String)
	 */
	private void handleFormParameters(RequestBuilder builder) {
		for (HttpParameter p : formParams.values()) {
			builder.addFormParam(p.getName(), p.getValue());
		}
	}

	/**
	 * Set request body value.
	 *
	 * @param builder The pending HTTP request.
	 * @see RequestBuilder#setBody(String)
	 */
	private void handleRequestBody(RequestBuilder builder) {
		builder.setBody(body);
	}

	/**
	 * Add cookies to the final HTTP request.
	 *
	 * @param builder The pending HTTP request.
	 * @see RequestBuilder#addCookie(com.ning.http.client.cookie.Cookie)
	 */
	private void handleCookies(RequestBuilder builder) {
		if (!cookies.isEmpty()) {
			builder.addHeader(HttpHeaders.COOKIE, Cookies.serialize(cookies));
		}
	}

	/**
	 * Add request headers.
	 *
	 * @param builder The pending HTTP request.
	 * @see RequestBuilder#addHeader(String, String)
	 */
	private void handleHeaders(RequestBuilder builder) {
		for (HttpHeader header : headers.values()) {
			builder.addHeader(header.getName(), header.serializeValues());
		}
	}
}


/**
 * The MIT License (MIT)
 *
 * Copyright (c) 2014-2019 <mickael.jeanroy@gmail.com>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

package com.github.mjeanroy.junit.servers.commons;

import java.util.Map;

/**
 * Static utility to create easily toString implementation.
 *
 *

 *
 * Internal API: these methods are part of the internal API and may be removed, have their signature change,
 * or have their access level decreased from public to protected, package, or private in future versions without notice.
 */
public final class ToStringBuilder {

	/**
	 * Field separator.
	 */
	private static final String SEPARATOR = ", ";

	/**
	 * Character prepended before any iterable output values.
	 */
	private static final String OPEN_ARRAY = "[";

	/**
	 * Character appended after any iterable output values.
	 */
	private static final String CLOSE_ARRAY = "]";

	/**
	 * Characters separating field name with its value.
	 */
	private static final String FIELD_VALUE_SEPARATOR = ": ";

	/**
	 * Character prepended to object values output.
	 */
	private static final String START_OBJ = "{";

	/**
	 * Character appended to object values output.
	 */
	private static final String END_OBJ = "}";

	/**
	 * The pending string returned at the end.
	 */
	private final StringBuilder sb;

	/**
	 * Flag to know if the first field has already been appended.
	 */
	private boolean first;

	// Use static factory instead
	private ToStringBuilder(String klassName) {
		this.sb = new StringBuilder(klassName).append(START_OBJ);
		this.first = true;
	}

	/**
	 * Create initial builder.
	 *
	 * @param klass The class name.
	 * @return The builder.
	 */
	public static ToStringBuilder create(Class<?> klass) {
		return new ToStringBuilder(klass.getSimpleName());
	}

	/**
	 * Append new string value.
	 *
	 * @param name The name of the field.
	 * @param value The field value.
	 * @return The current builder (for chaining).
	 */
	public ToStringBuilder append(String name, String value) {
		return appendValue(name, value);
	}

	/**
	 * Append new integer value.
	 *
	 * @param name The name of the field.
	 * @param value The field value.
	 * @return The current builder (for chaining).
	 */
	public ToStringBuilder append(String name, Integer value) {
		return appendValue(name, value);
	}

	/**
	 * Append new long value.
	 *
	 * @param name The name of the field.
	 * @param value The field value.
	 * @return The current builder (for chaining).
	 */
	public ToStringBuilder append(String name, Long value) {
		return appendValue(name, value);
	}

	/**
	 * Append new boolean value.
	 *
	 * @param name The name of the field.
	 * @param value The field value.
	 * @return The current builder (for chaining).
	 */
	public ToStringBuilder append(String name, Boolean value) {
		return appendValue(name, value);
	}

	/**
	 * Append new map value.
	 *
	 * @param name The name of the field.
	 * @param map The map.
	 * @return The current builder (for chaining).
	 */
	public ToStringBuilder append(String name, Map<?, ?> map) {
		StringBuilder sb = new StringBuilder(START_OBJ);
		for (Map.Entry<?, ?> entry : map.entrySet()) {
			sb.append(entry.getKey()).append(FIELD_VALUE_SEPARATOR).append(formatValue(entry.getValue()));
		}

		sb.append(END_OBJ);

		return appendFormattedValue(name, sb.toString());
	}

	/**
	 * Append new object value.
	 *
	 * @param name The name of the field.
	 * @param value The field value.
	 * @return The current builder (for chaining).
	 */
	public ToStringBuilder append(String name, Object value) {
		return appendValue(name, value);
	}

	/**
	 * Append new boolean value.
	 *
	 * @param name The name of the field.
	 * @param values The collection of values.
	 * @param <T> Type of elements in collection.
	 * @return The current builder (for chaining).
	 */
	public <T> ToStringBuilder append(String name, Iterable<T> values) {
		StringBuilder pending = new StringBuilder().append(OPEN_ARRAY);
		boolean firstItem = true;

		for (T value : values) {
			if (!firstItem) {
				pending.append(SEPARATOR);
			}

			pending.append(formatValue(value));
			firstItem = false;
		}

		return appendFormattedValue(name, pending.append(CLOSE_ARRAY).toString());
	}

	/**
	 * Format value (i.e wrapped string with double quote for example), and append
	 * the field the final output.
	 *
	 * @param name Field name.
	 * @param value The value.
	 * @return The current builder.
	 */
	private ToStringBuilder appendValue(String name, Object value) {
		return appendFormattedValue(name, formatValue(value));
	}

	/**
	 * Append formatted value to the final output.
	 * @param name Field name.
	 * @param value Field value.
	 * @return The current builder.
	 */
	private ToStringBuilder appendFormattedValue(String name, Object value) {
		if (!first) {
			sb.append(SEPARATOR);
		}

		sb.append(name).append(FIELD_VALUE_SEPARATOR).append(value);
		first = false;
		return this;
	}

	/**
	 * Build the final string.
	 *
	 * @return The final string.
	 */
	public String build() {
		return sb.append(END_OBJ).toString();
	}

	/**
	 * Format value:
	 *

	 *
Returns {@code null} if {@code value} is null.

	 *
Wrapped value inside double quote if it is a string value.

	 *
Returns the result of {@code toString} function otherwise.

	 *

	 *
	 * @param value The value to raw.
	 * @return The formatted value.
	 */
	private static String formatValue(Object value) {
		if (value == null) {
			return null;
		}

		return value instanceof CharSequence ? "\"" + value + "\"" : value.toString();
	}
}


/**
 * The MIT License (MIT)
 *
 * Copyright (c) 2014-2019 <mickael.jeanroy@gmail.com>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

package com.github.mjeanroy.junit.servers.client;

import com.github.mjeanroy.junit.servers.exceptions.UrlException;

import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

import static com.github.mjeanroy.junit.servers.commons.ObjectUtils.firstNonNull;
import static com.github.mjeanroy.junit.servers.commons.Preconditions.notNull;
import static com.github.mjeanroy.junit.servers.commons.UrlUtils.ensureAbsolutePath;
import static java.util.Collections.unmodifiableMap;

/**
 * URL object without:
 *
 *

 *
Authority part.

 *
Query string.

 *
Fragment.

 *

 */
public class HttpUrl {

	/**
	 * Default URL scheme.
	 */
	private static final HttpScheme DEFAULT_SCHEME = HttpScheme.HTTP;

	/**
	 * Default URL host.
	 */
	private static final String DEFAULT_HOST = "localhost";

	/**
	 * Default URL path.
	 */
	private static final String DEFAULT_PATH = "/";

	private static final String SCHEME_SEPARATOR = "://";
	private static final String PORT_SEPARATOR = ":";

	/**
	 * Parse URL string to create new {@link HttpUrl} instance.
	 *
	 * @param endpoint URL value.
	 * @return The final {@link HttpUrl} instance.
	 * @throws IllegalArgumentException If URL is malformed.
	 */
	public static HttpUrl parse(String endpoint) {
		try {
			final URL url = new URL(endpoint);
			final String protocol = url.getProtocol();
			final String host = url.getHost();
			final int port = url.getPort();
			final String path = url.getPath();
			return new Builder()
				.withScheme(protocol)
				.withHost(host)
				.withPort(port >= 0 ? port : url.getDefaultPort())
				.withPath(path)
				.build();
		}
		catch (MalformedURLException ex) {
			throw new IllegalArgumentException(ex);
		}
	}

	/**
	 * URL Scheme.
	 */
	private final HttpScheme scheme;

	/**
	 * URL Host.
	 */
	private final String host;

	/**
	 * URL Port.
	 */
	private final int port;

	/**
	 * URL Path.
	 */
	private final String path;

	/**
	 * Create URL.
	 *
	 * @param scheme URL Scheme.
	 * @param host URL Host.
	 * @param port URL Port.
	 * @param path URL Path.
	 */
	private HttpUrl(HttpScheme scheme, String host, int port, String path) {
		this.scheme = scheme;
		this.host = host;
		this.port = port;
		this.path = path;
	}

	/**
	 * Get URL Scheme.
	 *
	 * @return URL Scheme.
	 * @see #scheme
	 */
	public String getScheme() {
		return scheme.getProtocol();
	}

	/**
	 * Get URL Host.
	 *
	 * @return URL Host.
	 * @see #host
	 */
	public String getHost() {
		return host;
	}

	/**
	 * Get URL Port.
	 *
	 * @return URL Port.
	 * @see #port
	 */
	public int getPort() {
		return port;
	}

	/**
	 * Get URL Path.
	 * @return URL Path.
	 * @see #path
	 */
	public String getPath() {
		return path;
	}

	/**
	 * Create new {@link URI} from URL fields.
	 *
	 * @return URI instance.
	 * @throws IllegalArgumentException If {@code URI} cannot be built because of {@link URISyntaxException}.
	 */
	public URI toURI() {
		final String protocol = scheme.getProtocol();
		try {
			return new URI(protocol, null, host, port, path, null, null);
		}
		catch (URISyntaxException ex) {
			throw new UrlException(protocol, host, port, path, ex);
		}
	}

	@Override
	public String toString() {
		final String protocol = scheme.getProtocol();
		final String port = String.valueOf(this.port);
		final int size = protocol.length()
			+ host.length()
			+ port.length()
			+ path.length()
			+ SCHEME_SEPARATOR.length()
			+ PORT_SEPARATOR.length();

		return new StringBuilder(size)
			.append(protocol)
			.append(SCHEME_SEPARATOR)
			.append(host)
			.append(PORT_SEPARATOR)
			.append(port)
			.append(path)
			.toString();
	}

	@Override
	public boolean equals(Object o) {
		if (o == this) {
			return true;
		}

		if (o instanceof HttpUrl) {
			HttpUrl u = (HttpUrl) o;
			return Objects.equals(scheme, u.scheme)
				&& Objects.equals(host, u.host)
				&& Objects.equals(port, u.port)
				&& Objects.equals(path, u.path);
		}

		return false;
	}

	@Override
	public int hashCode() {
		return Objects.hash(scheme, host, port, path);
	}

	/**
	 * Builder used to create {@link HttpUrl} instances.
	 */
	public static class Builder {

		/**
		 * The URL Scheme, default to {@code "http"}.
		 * @see HttpUrl#DEFAULT_SCHEME
		 */
		private HttpScheme scheme;

		/**
		 * The URL Host, default to {@code "localhost"}.
		 * @see HttpUrl#DEFAULT_HOST
		 */
		private String host;

		/**
		 * The URL Port, default to {@code 80} if scheme is {@code "http"}, 443 if scheme is {@code https}.
		 */
		private Integer port;

		/**
		 * The URL Path, default to {@code "/"}.
		 * @see HttpUrl#DEFAULT_PATH
		 */
		private String path;

		/**
		 * Create builder.
		 */
		public Builder() {
		}

		/**
		 * Update URL Scheme.
		 *
		 * @param scheme New URL Scheme.
		 * @return The builder (for chaining).
		 */
		public Builder withScheme(String scheme) {
			notNull(scheme, "scheme");

			HttpScheme result = HttpScheme.parse(scheme);
			if (result == null) {
				throw new IllegalArgumentException(String.format("Unknown scheme: %s", scheme));
			}

			this.scheme = result;
			return this;
		}

		/**
		 * Update URL Host.
		 *
		 * @param host New URL Host.
		 * @return The builder (for chaining).
		 */
		public Builder withHost(String host) {
			this.host = notNull(host, "host");
			return this;
		}

		/**
		 * Update URL Port.
		 *
		 * @param port New URL Port.
		 * @return The builder (for chaining).
		 */
		public Builder withPort(int port) {
			this.port = port;
			return this;
		}

		/**
		 * Update URL Path.
		 *
		 * @param path New URL Path.
		 * @return The builder (for chaining).
		 */
		public Builder withPath(String path) {
			this.path = notNull(path, "path");
			return this;
		}

		/**
		 * Create immutable {@link HttpUrl} instance from current field values.
		 *
		 * @return URL.
		 */
		public HttpUrl build() {
			final HttpScheme scheme = firstNonNull(this.scheme, DEFAULT_SCHEME);
			final String host = firstNonNull(this.host, DEFAULT_HOST);
			final String path = firstNonNull(this.path, DEFAULT_PATH);
			final int port = firstNonNull(this.port, scheme.getDefaultPort());
			return new HttpUrl(scheme, host, port, ensureAbsolutePath(path));
		}
	}

	/**
	 * Supported scheme: currently only {@code "http"} and {@code "https"} protocols.
	 */
	private enum HttpScheme {
		/**
		 * The {@code "http"} protocol.
		 */
		HTTP("http", 80),

		/**
		 * The {@code "https"}protocol.
		 */
		HTTPS("https", 443);

		/**
		 * The protocol identifier to use in URL.
		 */
		private final String protocol;

		/**
		 * The default port for the given protocol.
		 */
		private final int defaultPort;

		HttpScheme(String protocol, int defaultPort) {
			this.protocol = protocol;
			this.defaultPort = defaultPort;
		}

		/**
		 * Get default port for the given scheme.
		 *
		 * @return Default port.
		 */
		private int getDefaultPort() {
			return defaultPort;
		}

		/**
		 * Get protocol prefix to use in URL.
		 *
		 * @return The protocol.
		 */
		private String getProtocol() {
			return protocol;
		}

		private static final Map<String, HttpScheme> VALUES = unmodifiableMap(new HashMap<String, HttpScheme>() {{
			put(HttpScheme.HTTP.getProtocol(), HttpScheme.HTTP);
			put(HttpScheme.HTTPS.getProtocol(), HttpScheme.HTTPS);
		}});

		/**
		 * Parse protocol to get the corresponding scheme.
		 *
		 * @param protocol The protocol.
		 * @return The scheme, {@code null} if it does not exist.
		 */
		private static HttpScheme parse(String protocol) {
			return VALUES.get(protocol.toLowerCase());
		}
	}
}


/**
 * The MIT License (MIT)
 *
 * Copyright (c) 2014-2019 <mickael.jeanroy@gmail.com>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

package com.github.mjeanroy.junit.servers.commons;

import com.github.mjeanroy.junit.servers.exceptions.Utf8EncodingException;

import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;

/**
 * Static IO utilities.
 *
 *

 *
 * Internal API: these methods are part of the internal API and may be removed, have their signature change,
 * or have their access level decreased from public to protected, package, or private in future versions without notice.
 */
public final class EncoderUtils {

	// Ensure non instantiation.
	private EncoderUtils() {
	}

	/**
	 * Translates a string into {@code application/x-www-form-urlencoded}
	 * format using UTF-8 encoding.
	 *
	 * @param value The string value.
	 * @return The encoded value.
	 * @throws Utf8EncodingException If, for some weird reason, UTF-8 encoding is not supported.
	 */
	public static String urlEncode(String value) {
		try {
			return URLEncoder.encode(value, StandardCharsets.UTF_8.displayName());
		}
		catch (UnsupportedEncodingException ex) {
			throw new Utf8EncodingException(ex);
		}
	}
}


package com.nurkiewicz.lazyseq;

import java.util.*;
import java.util.function.*;
import java.util.stream.Collector;
import java.util.stream.Stream;

/**
 * @author Tomasz Nurkiewicz
 * @since 5/6/13, 9:20 PM
 */
public abstract class LazySeq<E> extends AbstractList<E> {

	@SuppressWarnings("unchecked")
	public static <E> LazySeq<E> empty() {
		return Nil.instance();
	}

	public abstract E head();

	public Optional<E> headOption() {
		return Optional.of(head());
	}

	public abstract LazySeq<E> tail();

	public static <E> LazySeq<E> of(E element) {
		return cons(element, empty());
	}

	public static <E> LazySeq<E> of(E element, Supplier<LazySeq<E>> tailFun) {
		return cons(element, tailFun);
	}

	public static <E> LazySeq<E> of(E element1, E element2) {
		return cons(element1, of(element2));
	}

	public static <E> LazySeq<E> of(E element1, E element2, Supplier<LazySeq<E>> tailFun) {
		return cons(element1, of(element2, tailFun));
	}

	public static <E> LazySeq<E> of(E element1, E element2, E element3) {
		return cons(element1, of(element2, element3));
	}

	public static <E> LazySeq<E> of(E element1, E element2, E element3, Supplier<LazySeq<E>> tailFun) {
		return cons(element1, of(element2, element3, tailFun));
	}

	@SafeVarargs
	public static <E> LazySeq<E> of(E... elements) {
		return of(Arrays.asList(elements).iterator());
	}

	public static <E> LazySeq<E> of(Iterable<E> elements) {
		return of(elements.iterator());
	}

	public static <E> LazySeq<E> of(Iterator<E> iterator) {
		if (iterator.hasNext()) {
			return cons(iterator.next(), () -> of(iterator));
		} else {
			return empty();
		}
	}

	public static <E> LazySeq<E> concat(Iterable<E> elements, Supplier<LazySeq<E>> tailFun) {
		return concat(elements.iterator(), tailFun);
	}

	public static <E> LazySeq<E> concat(Iterable<E> elements, LazySeq<E> tail) {
		return concat(elements.iterator(), tail);
	}

	public static <E> LazySeq<E> concat(Iterator<E> iterator, LazySeq<E> tail) {
		if (iterator.hasNext()) {
			return concatNonEmptyIterator(iterator, tail);
		} else {
			return tail;
		}
	}

	public static <E> LazySeq<E> concat(Iterator<E> iterator, Supplier<LazySeq<E>> tailFun) {
		if (iterator.hasNext()) {
			return concatNonEmptyIterator(iterator, tailFun);
		} else {
			return tailFun.get();
		}
	}

	private static <E> LazySeq<E> concatNonEmptyIterator(Iterator<E> iterator, LazySeq<E> tail) {
		final E next = iterator.next();
		if (iterator.hasNext()) {
			return cons(next, concatNonEmptyIterator(iterator, tail));
		} else {
			return cons(next, tail);
		}
	}

	private static <E> LazySeq<E> concatNonEmptyIterator(Iterator<E> iterator, Supplier<LazySeq<E>> tailFun) {
		final E next = iterator.next();
		if (iterator.hasNext()) {
			return cons(next, concatNonEmptyIterator(iterator, tailFun));
		} else {
			return cons(next, tailFun);
		}
	}

	public static <E> LazySeq<E> cons(E head, Supplier<LazySeq<E>> tailFun) {
		return new Cons<>(head, tailFun);
	}

	public static <E> LazySeq<E> cons(E head, LazySeq<E> tail) {
		return new FixedCons<>(head, tail);
	}

	public static <E> LazySeq<E> iterate(E initial, Function<E, E> fun) {
		return new Cons<>(initial, () -> iterate(fun.apply(initial), fun));
	}

	public static <E> Collector<E, LazySeq<E>, LazySeq<E>> toLazySeq() {
		return DummyLazySeqCollector.getInstance();
	}

	public static <E> LazySeq<E> tabulate(int start, Function<Integer, E> generator) {
		return cons(generator.apply(start), () -> tabulate(start + 1, generator));
	}

	public static <E> LazySeq<E> continually(Supplier<E> generator) {
		return cons(generator.get(), () -> continually(generator));
	}

	public static <E> LazySeq<E> continually(Iterable<E> cycle) {
		if (!cycle.iterator().hasNext()) {
			return empty();
		}
		return continuallyUnsafe(cycle);
	}

	private static <E> LazySeq<E> continuallyUnsafe(Iterable<E> cycle) {
		return concat(cycle, () -> continually(cycle));
	}

	public static <E> LazySeq<E> continually(E value) {
		return cons(value, () -> continually(value));
	}

	public static LazySeq<Integer> numbers(int start) {
		return numbers(start, 1);
	}

	public static LazySeq<Integer> numbers(int start, int step) {
		return cons(start, () -> numbers(start + step, step));
	}

	public static LazySeq<Double> numbers(double start) {
		return numbers(start, 1.0);
	}

	public static LazySeq<Double> numbers(double start, double step) {
		return cons(start, () -> numbers(start + step, step));
	}

	protected abstract boolean isTailDefined();

	@Override
	public E get(final int index) {
		if (index < 0) {
			throw new IndexOutOfBoundsException(Integer.toString(index));
		}
		LazySeq<E> cur = this;
		for (int curIdx = index; curIdx > 0; --curIdx) {
			if (cur.tail().isEmpty()) {
				throw new IndexOutOfBoundsException(Integer.toString(index));
			}
			cur = cur.tail();
		}
		return cur.head();
	}

	public abstract <R> LazySeq<R> map(Function<? super E, ? extends R> mapper);

	@Override
	public Stream<E> stream() {
		return new LazySeqStream<>(this);
	}

	@Override
	public Stream<E> parallelStream() {
		return stream();
	}

	@Override
	public String toString() {
		return mkString("[", ", ", "]", true);
	}

	/**
	 * Returns a string representation of this {@link LazySeq} by concatenating the elements
	 * using the separator sep.
	 */
	public String mkString(String sep) {
		return mkString("", sep, "");
	}

	/**
	 * Returns a string representation of this {@link LazySeq}.
	 * The string result starts with the string start and ends with end,
	 * whereas sep is as separator string between the elements of this sequence.
	 */
	public String mkString(String start, String sep, String end) {
		return mkString(start, sep, end, false);
	}

	/**
	 * Returns a string representation of this {@link LazySeq}.
	 * The string result starts with the string start and ends with end,
	 * whereas sep is as separator string between the elements of this sequence.
	 * If lazy is true the evaluation stops at the first unspecific element.
	 */
	public String mkString(String start, String sep, String end, boolean lazy) {
		final StringBuilder s = new StringBuilder(start);
		LazySeq<E> cur = this;
		while (!cur.isEmpty()) {
			s.append(cur.head());
			if (!lazy || cur.isTailDefined()) {
				if (!cur.tail().isEmpty()) {
					s.append(sep);
				}
				cur = cur.tail();
			} else {
				s.append(sep).append("?");
				break;
			}
		}
		return s.append(end).toString();
	}

	public abstract LazySeq<E> filter(Predicate<? super E> predicate);

	public abstract <R> LazySeq<R> flatMap(Function<? super E, ? extends Iterable<? extends R>> mapper);

	public LazySeq<E> limit(long maxSize) {
		return take(maxSize);
	}

	/**
	 * Converts this {@link LazySeq} to immutable {@link List}.
	 *

	 * Notice that this method will eventually fail at runtime when called on infinite sequence.
	 *
	 * @return {@link List} of all elements in this lazy sequence.
	 */
	public List<E> toList() {
		return Collections.unmodifiableList(this.force());
	}

	public LazySeq<E> take(long maxSize) {
		if (maxSize < 0) {
			throw new IllegalArgumentException(Long.toString(maxSize));
		}
		if (maxSize == 0) {
			return empty();
		}
		return takeUnsafe(maxSize);
	}

	protected abstract LazySeq<E> takeUnsafe(long maxSize);

	public LazySeq<E> drop(long startInclusive) {
		if (startInclusive < 0) {
			throw new IllegalArgumentException(Long.toString(startInclusive));
		}
		return dropUnsafe(startInclusive);
	}

	protected LazySeq<E> dropUnsafe(long startInclusive) {
		if (startInclusive > 0) {
			return tail().drop(startInclusive - 1);
		} else {
			return this;
		}
	}

	@Override
	public LazySeq<E> subList(int fromIndex, int toIndex) {
		return slice(fromIndex, toIndex);
	}

	public LazySeq<E> slice(long startInclusive, long endExclusive) {
		if (startInclusive < 0 || startInclusive > endExclusive) {
			throw new IllegalArgumentException("slice(" + startInclusive + ", " + endExclusive + ")");
		}
		return dropUnsafe(startInclusive).takeUnsafe(endExclusive - startInclusive);
	}

	public void forEach(Consumer<? super E> action) {
		action.accept(head());
		tail().forEach(action);
	}

	public Optional<E> reduce(BinaryOperator<E> accumulator) {
		if (isEmpty() || tail().isEmpty()) {
			return Optional.empty();
		}
		return Optional.of(tail().reduce(head(), accumulator));
	}

	public  U reduce(U identity, BiFunction accumulator) {
		U result = identity;
		LazySeq<E> cur = this;
		while (!cur.isEmpty()) {
			result = accumulator.apply(result, cur.head());
			cur = cur.tail();
		}
		return result;
	}

	public <C extends Comparable<? super C>> Optional<E> maxBy(Function<E, C> propertyFun) {
		return max(propertyFunToComparator(propertyFun));
	}

	public Optional<E> max(Comparator<? super E> comparator) {
		return greatestByComparator(comparator);
	}

	public <C extends Comparable<? super C>> Optional<E> minBy(Function<E, C> propertyFun) {
		return min(propertyFunToComparator(propertyFun));
	}

	public Optional<E> min(Comparator<? super E> comparator) {
		return greatestByComparator(comparator.reversed());
	}

	private <C extends Comparable<? super C>> Comparator<? super E> propertyFunToComparator(Function<E, C> propertyFun) {
		return (a, b) -> {
			final C aProperty = propertyFun.apply(a);
			final C bProperty = propertyFun.apply(b);
			return aProperty.compareTo(bProperty);
		};
	}

	private Optional<E> greatestByComparator(Comparator<? super E> comparator) {
		if (tail().isEmpty()) {
			return Optional.of(head());
		}
		E minSoFar = head();
		LazySeq<E> cur = this.tail();
		while (!cur.isEmpty()) {
			minSoFar = maxByComparator(minSoFar, cur.head(), comparator);
			cur = cur.tail();
		}
		return Optional.of(minSoFar);
	}

	private static <E> E maxByComparator(E first, E second, Comparator<? super E> comparator) {
		return comparator.compare(first, second) >= 0 ? first : second;
	}

	@Override
	public int size() {
		return 1 + tail().size();
	}

	@Override
	public Iterator<E> iterator() {
		return new LazySeqIterator<>(this);
	}

	public boolean anyMatch(Predicate<? super E> predicate) {
		return predicate.test(head()) || tail().anyMatch(predicate);
	}

	public boolean allMatch(Predicate<? super E> predicate) {
		return predicate.test(head()) && tail().allMatch(predicate);
	}

	public boolean noneMatch(Predicate<? super E> predicate) {
		return allMatch(predicate.negate());
	}

	public <S, R> LazySeq<R> zip(LazySeq<? extends S> second, BiFunction<? super E, ? super S, ? extends R> zipper) {
		if (second.isEmpty()) {
			return empty();
		} else {
			final R headsZipped = zipper.apply(head(), second.head());
			return cons(headsZipped, () -> tail().zip(second.tail(), zipper));
		}
	}

	public LazySeq<E> takeWhile(Predicate<? super E> predicate) {
		if (predicate.test(head())) {
			return cons(head(), () -> tail().takeWhile(predicate));
		} else {
			return empty();
		}
	}

	public LazySeq<E> dropWhile(Predicate<? super E> predicate) {
		if (predicate.test(head())) {
			return tail().dropWhile(predicate);
		} else {
			return this;
		}
	}

	public LazySeq<List<E>> sliding(int size) {
		if (size <= 0) {
			throw new IllegalArgumentException(Integer.toString(size));
		}
		return slidingUnsafe(size);
	}

	protected LazySeq<List<E>> slidingUnsafe(int size) {
		final List<E> window = take(size).toList();
		return cons(window, () -> tail().slidingFullOnly(size));
	}

	protected LazySeq<List<E>> slidingFullOnly(int size) {
		final List<E> window = take(size).toList();
		if (window.size() < size) {
			return empty();
		} else {
			return cons(window, () -> tail().slidingFullOnly(size));
		}
	}

	public LazySeq<List<E>> grouped(int size) {
		if (size <= 0) {
			throw new IllegalArgumentException(Integer.toString(size));
		}
		return groupedUnsafe(size);
	}

	protected LazySeq<List<E>> groupedUnsafe(int size) {
		final List<E> window = take(size).toList();
		return cons(window, () -> drop(size).groupedUnsafe(size));
	}

	public LazySeq<E> scan(E initial, BinaryOperator<E> fun) {
		return cons(initial, () -> tail().scan(fun.apply(initial, head()), fun));
	}

	public LazySeq<E> distinct() {
		return filterOutSeen(new HashSet<>());
	}

	private LazySeq<E> filterOutSeen(Set<E> exclude) {
		final LazySeq<E> moreDistinct = filter(e -> !exclude.contains(e));
		if (moreDistinct.isEmpty()) {
			return empty();
		}
		final E next = moreDistinct.head();
		exclude.add(next);
		return cons(next, () -> moreDistinct.tail().filterOutSeen(exclude));
	}

	@SuppressWarnings("unchecked")
	public LazySeq<E> sorted() {
		return sorted((o1, o2) -> ((Comparable<E>) o1).compareTo(o2));
	}

	public LazySeq<E> sorted(Comparator<? super E> comparator) {
		final ArrayList<E> list = new ArrayList<>(this);
		list.sort(comparator);
		return of(list);
	}

	public boolean startsWith(Iterable<E> prefix) {
		return startsWith(prefix.iterator());
	}

	public boolean startsWith(Iterator<E> iterator) {
		return !iterator.hasNext() ||
				head().equals(iterator.next()) && tail().startsWith(iterator);
	}

	public LazySeq<E> force() {
		tail().force();
		return this;
	}

	@Override
	public boolean equals(Object o) {
		if (this == o) return true;
		if (!(o instanceof LazySeq)) return false;

		LazySeq right = (LazySeq) o;
		return !right.isEmpty() &&
				head().equals(right.head()) &&
				tail().equals(right.tail());
	}

	@Override
	public int hashCode() {
		return head().hashCode() + tail().hashCode() * 31;
	}

}


/*
 * JBoss, Home of Professional Open Source
 * Copyright 2011, Red Hat, Inc. and individual contributors
 * by the @authors tag. See the copyright.txt in the distribution for a
 * full listing of individual contributors.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */

package org.mobicents.ha.javax.sip.cache;

import gov.nist.core.CommonLogger;
import gov.nist.core.StackLogger;
import gov.nist.javax.sip.SipProviderImpl;
import gov.nist.javax.sip.message.SIPResponse;
import gov.nist.javax.sip.stack.AbstractHASipDialog;
import gov.nist.javax.sip.stack.SIPDialog;

import java.text.ParseException;
import java.util.Map;
import java.util.Map.Entry;

import javax.sip.PeerUnavailableException;
import javax.sip.SipFactory;
import javax.sip.address.Address;
import javax.sip.header.ContactHeader;
import javax.transaction.RollbackException;
import javax.transaction.Status;
import javax.transaction.TransactionManager;

import org.jboss.cache.Cache;
import org.jboss.cache.CacheException;
import org.jboss.cache.Fqn;
import org.jboss.cache.Node;
import org.jboss.cache.config.Configuration;
import org.mobicents.cache.CacheData;
import org.mobicents.cache.MobicentsCache;
import org.mobicents.ha.javax.sip.ClusteredSipStack;
import org.mobicents.ha.javax.sip.HASipDialog;
import org.mobicents.ha.javax.sip.HASipDialogFactory;

/**
 * @author jean.deruelle@gmail.com
 * @author martins
 *
 */
public class SIPDialogCacheData extends CacheData {
	private static final String APPDATA = "APPDATA";
	private ClusteredSipStack clusteredSipStack;
	private static StackLogger logger = CommonLogger.getLogger(SIPDialogCacheData.class);
	public SIPDialogCacheData(Fqn nodeFqn, MobicentsCache mobicentsCache, ClusteredSipStack clusteredSipStack) {
		super(nodeFqn, mobicentsCache);
		this.clusteredSipStack = clusteredSipStack;
	}

	public SIPDialog getSIPDialog(String dialogId) throws SipCacheException {
		HASipDialog haSipDialog = null;
		final Cache jbossCache = getMobicentsCache().getJBossCache();
		Configuration config = jbossCache.getConfiguration();
		final boolean isBuddyReplicationEnabled = config.getBuddyReplicationConfig() != null && config.getBuddyReplicationConfig().isEnabled();
		TransactionManager transactionManager = config.getRuntimeConfig().getTransactionManager();
		boolean doTx = false;
		try {
			if(logger.isLoggingEnabled(StackLogger.TRACE_DEBUG)) {
				logger.logDebug("transaction manager :" + transactionManager);
			}
			if(transactionManager != null && transactionManager.getTransaction() == null) {
				if(logger.isLoggingEnabled(StackLogger.TRACE_DEBUG)) {
					logger.logDebug("transaction manager begin transaction");
				}
				transactionManager.begin();
				doTx = true;
	        }
			// Issue 1517 : http://code.google.com/p/restcomm/issues/detail?id=1517
			// Adding code to handle Buddy replication to force data gravitation
			if(isBuddyReplicationEnabled) {
				if(logger.isLoggingEnabled(StackLogger.TRACE_DEBUG)) {
					logger.logDebug("forcing data gravitation since buddy replication is enabled");
				}
				jbossCache.getInvocationContext().getOptionOverrides().setForceDataGravitation(true);
			}

            final Node<String,Object> childNode = getNode().getChild(dialogId);
			if(childNode != null) {
				try {
					final Map<String, Object> dialogMetaData = childNode.getData();
					final Object dialogAppData = childNode.get(APPDATA);

					haSipDialog = createDialog(dialogId, dialogMetaData, dialogAppData);

				} catch (CacheException e) {
					throw new SipCacheException("A problem occured while retrieving the following dialog " + dialogId + " from the Cache", e);
				}
			}
		} catch (Exception ex) {
			try {
				logger.logError("Problem taking dialog from cache. We will try to rollback the transction is any.",
						ex);
				if(transactionManager != null) {
					// Let's set it no matter what.
					transactionManager.setRollbackOnly();
				}
			} catch (Exception exn) {
				logger.logError("Problem rolling back session mgmt transaction",
						exn);
			}
		} finally {
			if (doTx) {
				try {
					if (transactionManager.getTransaction().getStatus() != Status.STATUS_MARKED_ROLLBACK) {
						if(logger.isLoggingEnabled(StackLogger.TRACE_DEBUG)) {
							logger.logDebug("transaction manager committing transaction");
						}
						transactionManager.commit();
					} else {
						if(logger.isLoggingEnabled(StackLogger.TRACE_DEBUG)) {
							logger.logDebug("endBatch(): rolling back batch");
						}
						transactionManager.rollback();
					}
				} catch (RollbackException re) {
					// Do nothing here since cache may rollback automatically.
					logger.logWarning("endBatch(): rolling back transaction with exception: "
									+ re);
				} catch (RuntimeException re) {
					throw re;
				} catch (Exception e) {
					throw new RuntimeException(
							"endTransaction(): Caught Exception ending batch: ",
							e);
				}
			}
		}
		return (SIPDialog) haSipDialog;
	}

	/*
	 * (non-Javadoc)
	 * @see org.mobicents.ha.javax.sip.cache.SipCache#updateDialog(gov.nist.javax.sip.stack.SIPDialog)
	 */
	public void updateSIPDialog(SIPDialog sipDialog) throws SipCacheException {
		final String dialogId = sipDialog.getDialogId();
		final Cache jbossCache = getMobicentsCache().getJBossCache();
		Configuration config = jbossCache.getConfiguration();
		final boolean isBuddyReplicationEnabled = config.getBuddyReplicationConfig() != null && config.getBuddyReplicationConfig().isEnabled();
		TransactionManager transactionManager = config.getRuntimeConfig().getTransactionManager();
		boolean doTx = false;
		try {
			if(logger.isLoggingEnabled(StackLogger.TRACE_DEBUG)) {
				logger.logDebug("transaction manager :" + transactionManager);
			}
			if(transactionManager != null && transactionManager.getTransaction() == null) {
				if(logger.isLoggingEnabled(StackLogger.TRACE_DEBUG)) {
					logger.logDebug("transaction manager begin transaction");
				}
				transactionManager.begin();
				doTx = true;
	        }
			// Issue 1517 : http://code.google.com/p/restcomm/issues/detail?id=1517
			// Adding code to handle Buddy replication to force data gravitation
			if(isBuddyReplicationEnabled) {
				if(logger.isLoggingEnabled(StackLogger.TRACE_DEBUG)) {
					logger.logDebug("forcing data gravitation since buddy replication is enabled");
				}
				jbossCache.getInvocationContext().getOptionOverrides().setForceDataGravitation(true);
			}
			final Node<String,Object> childNode = getNode().getChild(dialogId);
			if(childNode != null) {
				try {
					final Map<String, Object> dialogMetaData = childNode.getData();

					final HASipDialog haSipDialog = (HASipDialog) sipDialog;
					final Object dialogAppData = childNode.get(APPDATA);
					updateDialog(haSipDialog, dialogMetaData, dialogAppData);
				} catch (CacheException e) {
					throw new SipCacheException("A problem occured while retrieving the following dialog " + dialogId + " from the Cache", e);
				}
			}
		} catch (Exception ex) {
			try {
				if(transactionManager != null) {
					// Let's set it no matter what.
					transactionManager.setRollbackOnly();
				}
			} catch (Exception exn) {
				logger.logError("Problem rolling back session mgmt transaction",
						exn);
			}
		} finally {
			if (doTx) {
				try {
					if (transactionManager.getTransaction().getStatus() != Status.STATUS_MARKED_ROLLBACK) {
						if(logger.isLoggingEnabled(StackLogger.TRACE_DEBUG)) {
							logger.logDebug("transaction manager committing transaction");
						}
						transactionManager.commit();
					} else {
						if(logger.isLoggingEnabled(StackLogger.TRACE_DEBUG)) {
							logger.logDebug("endBatch(): rolling back batch");
						}
						transactionManager.rollback();
					}
				} catch (RollbackException re) {
					// Do nothing here since cache may rollback automatically.
					logger.logWarning("endBatch(): rolling back transaction with exception: "
									+ re);
				} catch (RuntimeException re) {
					throw re;
				} catch (Exception e) {
					throw new RuntimeException(
							"endTransaction(): Caught Exception ending batch: ",
							e);
				}
			}
		}
	}

	public HASipDialog createDialog(String dialogId, Map<String, Object> dialogMetaData, Object dialogAppData) throws SipCacheException {
		HASipDialog haSipDialog = null;
		if(dialogMetaData != null) {
			if(logger.isLoggingEnabled(StackLogger.TRACE_DEBUG)) {
				logger.logDebug("sipStack " + this + " dialog " + dialogId + " is present in the distributed cache, recreating it locally");
			}
			final String lastResponseStringified = (String) dialogMetaData.get(AbstractHASipDialog.LAST_RESPONSE);
			try {
				final SIPResponse lastResponse = (SIPResponse) SipFactory.getInstance().createMessageFactory().createResponse(lastResponseStringified);
				haSipDialog = HASipDialogFactory.createHASipDialog(clusteredSipStack.getReplicationStrategy(), (SipProviderImpl)clusteredSipStack.getSipProviders().next(), lastResponse);
				haSipDialog.setDialogId(dialogId);
				updateDialogMetaData(dialogMetaData, dialogAppData, haSipDialog, true);
				// setLastResponse won't be called on recreation since version will be null on recreation
				haSipDialog.setLastResponse(lastResponse);
				if(logger.isLoggingEnabled(StackLogger.TRACE_DEBUG)) {
					logger.logDebug("HA SIP Dialog " + dialogId + " localTag  = " + haSipDialog.getLocalTag());
					logger.logDebug("HA SIP Dialog " + dialogId + " remoteTag  = " + haSipDialog.getRemoteTag());
					logger.logDebug("HA SIP Dialog " + dialogId + " localParty = " + haSipDialog.getLocalParty());
					logger.logDebug("HA SIP Dialog " + dialogId + " remoteParty  = " + haSipDialog.getRemoteParty());
				}
			} catch (PeerUnavailableException e) {
				throw new SipCacheException("A problem occured while retrieving the following dialog " + dialogId + " from the Cache", e);
			} catch (ParseException e) {
				throw new SipCacheException("A problem occured while retrieving the following dialog " + dialogId + " from the Cache", e);
			}
		}

		return haSipDialog;
	}

	public void updateDialog(HASipDialog haSipDialog, Map<String, Object> dialogMetaData,
			Object dialogAppData) throws SipCacheException {
		if(dialogMetaData != null) {
			final long currentVersion = haSipDialog.getVersion();
			final Long cacheVersion = ((Long)dialogMetaData.get(AbstractHASipDialog.VERSION));
			if(cacheVersion != null && currentVersion < cacheVersion.longValue()) {
				if(logger.isLoggingEnabled(StackLogger.TRACE_DEBUG)) {
					logger.logDebug("HA SIP Dialog " + haSipDialog + " with dialogId " + haSipDialog.getDialogIdToReplicate() + " is older " + currentVersion + " than the one in the cache " + cacheVersion + " updating it");
				}
				try {
					final String lastResponseStringified = (String) dialogMetaData.get(AbstractHASipDialog.LAST_RESPONSE);
					final SIPResponse lastResponse = (SIPResponse) SipFactory.getInstance().createMessageFactory().createResponse(lastResponseStringified);
					haSipDialog.setLastResponse(lastResponse);
					updateDialogMetaData(dialogMetaData, dialogAppData, haSipDialog, false);
				}  catch (PeerUnavailableException e) {
					throw new SipCacheException("A problem occured while retrieving the following dialog " + haSipDialog.getDialogIdToReplicate() + " from the TreeCache", e);
				} catch (ParseException e) {
					throw new SipCacheException("A problem occured while retrieving the following dialog " + haSipDialog.getDialogIdToReplicate() + " from the TreeCache", e);
				}
			} else {
				if(logger.isLoggingEnabled(StackLogger.TRACE_DEBUG)) {
					logger.logDebug("HA SIP Dialog " + haSipDialog + " with dialogId " + haSipDialog.getDialogIdToReplicate() + " is not older " + currentVersion + " than the one in the cache " + cacheVersion + ", not updating it");
				}
			}
		}
	}

	/**
	 * Update the haSipDialog passed in param with the dialogMetaData and app meta data
	 * @param dialogMetaData
	 * @param dialogAppData
	 * @param haSipDialog
	 * @throws ParseException
	 * @throws PeerUnavailableException
	 */
	private void updateDialogMetaData(Map<String, Object> dialogMetaData, Object dialogAppData, HASipDialog haSipDialog, boolean recreation) throws ParseException,
			PeerUnavailableException {
		haSipDialog.setMetaDataToReplicate(dialogMetaData, recreation);
		haSipDialog.setApplicationDataToReplicate(dialogAppData);
		final String contactStringified = (String) dialogMetaData.get(AbstractHASipDialog.CONTACT_HEADER);
		if(logger.isLoggingEnabled(StackLogger.TRACE_DEBUG)) {
			logger.logDebug("contactStringified " + contactStringified);
		}
		if(contactStringified != null) {
			Address contactAddress = SipFactory.getInstance().createAddressFactory().createAddress(contactStringified);
			ContactHeader contactHeader = SipFactory.getInstance().createHeaderFactory().createContactHeader(contactAddress);
			if(logger.isLoggingEnabled(StackLogger.TRACE_DEBUG)) {
				logger.logDebug("contactHeader " + contactHeader);
				logger.logDebug("contactURI " + contactHeader.getAddress().getURI());
			}
			haSipDialog.setContactHeader(contactHeader);
		}
	}

	public void putSIPDialog(SIPDialog dialog) throws SipCacheException {
		if(logger.isLoggingEnabled(StackLogger.TRACE_DEBUG)) {
			logger.logStackTrace();
		}
		final HASipDialog haSipDialog = (HASipDialog) dialog;
		final String dialogId = haSipDialog.getDialogIdToReplicate();
		if(logger.isLoggingEnabled(StackLogger.TRACE_DEBUG)) {
			logger.logDebug("put HA SIP Dialog " + dialog + " with dialog " + dialogId);
		}
		final Cache jbossCache = getMobicentsCache().getJBossCache();
		TransactionManager transactionManager = jbossCache.getConfiguration().getRuntimeConfig().getTransactionManager();
		boolean doTx = false;
		try {
			if(logger.isLoggingEnabled(StackLogger.TRACE_DEBUG)) {
				logger.logDebug("transaction manager :" + transactionManager);
			}
			if(transactionManager != null && transactionManager.getTransaction() == null) {
				if(logger.isLoggingEnabled(StackLogger.TRACE_DEBUG)) {
					logger.logDebug("transaction manager begin transaction");
				}
				transactionManager.begin();
				doTx = true;
	        }
			final Node childNode = getNode().addChild(Fqn.fromElements(dialogId));
			for (Entry<String, Object> metaData : haSipDialog.getMetaDataToReplicate().entrySet()) {
				childNode.put(metaData.getKey(), metaData.getValue());
			}
			final Object dialogAppData = haSipDialog.getApplicationDataToReplicate();
			if(dialogAppData != null) {
				childNode.put(APPDATA, dialogAppData);
			}
		} catch (Exception ex) {
			try {
				if(transactionManager != null) {
					// Let's set it no matter what.
					transactionManager.setRollbackOnly();
				}
			} catch (Exception exn) {
				logger.logError("Problem rolling back session mgmt transaction",
						exn);
			}
		} finally {
			if (doTx) {
				try {
					if (transactionManager.getTransaction().getStatus() != Status.STATUS_MARKED_ROLLBACK) {
						if(logger.isLoggingEnabled(StackLogger.TRACE_DEBUG)) {
							logger.logDebug("transaction manager committing transaction");
						}
						transactionManager.commit();
					} else {
						if(logger.isLoggingEnabled(StackLogger.TRACE_DEBUG)) {
							logger.logDebug("endBatch(): rolling back batch");
						}
						transactionManager.rollback();
					}
				} catch (RollbackException re) {
					// Do nothing here since cache may rollback automatically.
					logger.logWarning("endBatch(): rolling back transaction with exception: "
									+ re);
				} catch (RuntimeException re) {
					throw re;
				} catch (Exception e) {
					throw new RuntimeException(
							"endTransaction(): Caught Exception ending batch: ",
							e);
				}
			}
		}
	}

	public boolean removeSIPDialog(String dialogId) {
		if(logger.isLoggingEnabled(StackLogger.TRACE_DEBUG)) {
			logger.logDebug("remove HA SIP Dialog " + dialogId);
		}
		boolean succeeded = false;
		final Cache jbossCache = getMobicentsCache().getJBossCache();
		TransactionManager transactionManager = jbossCache.getConfiguration().getRuntimeConfig().getTransactionManager();
		boolean doTx = false;
		try {
			if(logger.isLoggingEnabled(StackLogger.TRACE_DEBUG)) {
				logger.logDebug("transaction manager :" + transactionManager);
			}
			if(transactionManager != null && transactionManager.getTransaction() == null) {
				if(logger.isLoggingEnabled(StackLogger.TRACE_DEBUG)) {
					logger.logDebug("transaction manager begin transaction");
				}
				transactionManager.begin();
				doTx = true;
	        }
			succeeded = getNode().removeChild(dialogId);
		} catch (Exception ex) {
			try {
				if(transactionManager != null) {
					// Let's set it no matter what.
					transactionManager.setRollbackOnly();
				}
			} catch (Exception exn) {
				logger.logError("Problem rolling back session mgmt transaction",
						exn);
			}
		} finally {
			if (doTx) {
				try {
					if (transactionManager.getTransaction().getStatus() != Status.STATUS_MARKED_ROLLBACK) {
						if(logger.isLoggingEnabled(StackLogger.TRACE_DEBUG)) {
							logger.logDebug("transaction manager committing transaction");
						}
						transactionManager.commit();
					} else {
						if(logger.isLoggingEnabled(StackLogger.TRACE_DEBUG)) {
							logger.logDebug("endBatch(): rolling back batch");
						}
						transactionManager.rollback();
					}
				} catch (RollbackException re) {
					// Do nothing here since cache may rollback automatically.
					logger.logWarning("endBatch(): rolling back transaction with exception: "
									+ re);
				} catch (RuntimeException re) {
					throw re;
				} catch (Exception e) {
					throw new RuntimeException(
							"endTransaction(): Caught Exception ending batch: ",
							e);
				}
			}
		}
		return succeeded;
	}

	public void evictSIPDialog(String dialogId) {
		getMobicentsCache().getJBossCache().evict(Fqn.fromElements(getNodeFqn(), Fqn.fromString(dialogId)));
		if(logger.isLoggingEnabled(StackLogger.TRACE_DEBUG)) {
			logger.logDebug("HA SIP Dialog " + dialogId + " evicted");
		}
	}
}


/*
 * TeleStax, Open Source Cloud Communications
 * Copyright 2011-2016, Telestax Inc and individual contributors
 * by the @authors tag.
 *
 * This program is free software: you can redistribute it and/or modify
 * under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation; either version 3 of
 * the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
 */

package org.mobicents.ha.javax.sip.cache.infinispan;

import gov.nist.core.StackLogger;
import gov.nist.javax.sip.stack.MessageChannel;
import gov.nist.javax.sip.stack.MessageProcessor;
import gov.nist.javax.sip.stack.MobicentsHASIPServerTransaction;
import gov.nist.javax.sip.stack.SIPServerTransaction;
import gov.nist.javax.sip.stack.SIPTransactionStack;

import java.io.IOException;
import java.net.InetAddress;
import java.text.ParseException;
import java.util.Map;

import javax.sip.PeerUnavailableException;

import org.mobicents.ha.javax.sip.ClusteredSipStack;
import org.mobicents.ha.javax.sip.cache.SipCacheException;

import org.infinispan.Cache;

/**
 * This class modifies the original SIPServerTransactionCacheData ( @see org.mobicents.ha.javax.sip.cache.hz.SIPServerTransactionCacheData )
 * to provide an implementation backed by Infinispan Cache
 *
 * @author Gergely Posfai
 * @author Andras Kokuti
 *
 */

public class SIPServerTransactionCacheData {

	private ClusteredSipStack stack;
	private StackLogger logger;
	private Cache<String, Object> serverTransactions;
	private Cache<String, Object> serverTransactionsApp;

	public SIPServerTransactionCacheData(ClusteredSipStack s,
			Cache<String, Object> serverTXCache,
			Cache<String, Object> serverTxAppCache) {
		stack = s;
		logger = s.getStackLogger();
		setServerTransactions(serverTXCache);
		setServerTransactionsApp(serverTxAppCache);
	}

	public SIPServerTransaction getServerTransaction(String txId)
			throws SipCacheException {
		SIPServerTransaction haSipServerTransaction = null;
		if(logger.isLoggingEnabled(StackLogger.TRACE_TRACE))
			logger.logDebug("getServerTransaction(" + txId + ")");

		try {
			final Map<String, Object> transactionMetaData = (Map<String, Object>)getServerTransactions().get(txId);
			final Object txAppData = getServerTransactionsApp().get(txId);

			haSipServerTransaction = createServerTransaction(txId, transactionMetaData, txAppData);

		} catch (Exception e) {
			throw new SipCacheException(e);
		}

		return haSipServerTransaction;
	}

	public void putServerTransaction(SIPServerTransaction serverTransaction)
			throws SipCacheException {
		if(logger.isLoggingEnabled(StackLogger.TRACE_TRACE))
			logger.logDebug("putServerTransaction(" + serverTransaction.getTransactionId() + ")");

		try {
			final MobicentsHASIPServerTransaction haServerTransaction = (MobicentsHASIPServerTransaction) serverTransaction;

			// meta data
			Map<String, Object> metaData = haServerTransaction.getMetaDataToReplicate();
			getServerTransactions().put(serverTransaction.getTransactionId(), metaData);

			// app data
			final Object transactionAppData = haServerTransaction.getApplicationDataToReplicate();
			if(transactionAppData != null) {
				getServerTransactionsApp().put(serverTransaction.getTransactionId(), transactionAppData);
			}
		} catch (Exception e) {
			throw new SipCacheException(e);
		}
	}

	public void removeServerTransaction(String txId)
			throws SipCacheException {
		if(logger.isLoggingEnabled(StackLogger.TRACE_TRACE))
			logger.logDebug("removeServerTransaction(" + txId + ")");
		getServerTransactions().remove(txId);
		getServerTransactionsApp().remove(txId);
	}

	public MobicentsHASIPServerTransaction createServerTransaction(String txId, Map<String, Object> transactionMetaData, Object transactionAppData) throws SipCacheException {
		MobicentsHASIPServerTransaction haServerTransaction = null;
		if(transactionMetaData != null) {
			if(logger.isLoggingEnabled(StackLogger.TRACE_DEBUG)) {
				logger.logDebug("sipStack " + this + " server transaction " + txId + " is present in the distributed cache, recreating it locally");
			}
			String channelTransport = (String) transactionMetaData.get(MobicentsHASIPServerTransaction.TRANSPORT);
			if (logger.isLoggingEnabled(StackLogger.TRACE_DEBUG)) {
				logger.logDebug(txId + " : transport " + channelTransport);
			}
			InetAddress channelIp = (InetAddress) transactionMetaData.get(MobicentsHASIPServerTransaction.PEER_IP);
			if (logger.isLoggingEnabled(StackLogger.TRACE_DEBUG)) {
				logger.logDebug(txId + " : channel peer Ip address " + channelIp);
			}
			Integer channelPort = (Integer) transactionMetaData.get(MobicentsHASIPServerTransaction.PEER_PORT);
			if (logger.isLoggingEnabled(StackLogger.TRACE_DEBUG)) {
				logger.logDebug(txId + " : channel peer port " + channelPort);
			}
			Integer myPort = (Integer) transactionMetaData.get(MobicentsHASIPServerTransaction.MY_PORT);
			if (logger.isLoggingEnabled(StackLogger.TRACE_DEBUG)) {
				logger.logDebug(txId + " : my port " + myPort);
			}
			MessageChannel messageChannel = null;
			MessageProcessor[] messageProcessors = stack.getStackMessageProcessors();
			for (MessageProcessor messageProcessor : messageProcessors) {
				if(messageProcessor.getTransport().equalsIgnoreCase(channelTransport)) {
					try {
						messageChannel = messageProcessor.createMessageChannel(channelIp, channelPort);
					} catch (IOException e) {
						logger.logError("couldn't recreate the message channel on ip address "
								+ channelIp + " and port " + channelPort, e);
					}
					break;
				}
			}

			haServerTransaction = new MobicentsHASIPServerTransaction((SIPTransactionStack) stack, messageChannel);
			haServerTransaction.setBranch(txId);
			try {
				updateServerTransactionMetaData(transactionMetaData, transactionAppData, haServerTransaction, true);
			} catch (PeerUnavailableException e) {
				throw new SipCacheException("A problem occured while retrieving the following transaction " + txId + " from the Cache", e);
			} catch (ParseException e) {
				throw new SipCacheException("A problem occured while retrieving the following transaction " + txId + " from the Cache", e);
			}
		} else {
			if(logger.isLoggingEnabled(StackLogger.TRACE_DEBUG)) {
				logger.logDebug("sipStack " + this + " server transaction " + txId + " not found in the distributed cache");
			}
		}

		return haServerTransaction;
	}

	/**
	 * Update the haSipDialog passed in param with the dialogMetaData and app meta data
	 * @param transactionMetaData
	 * @param transactionAppData
	 * @param haServerTransaction
	 * @throws ParseException
	 * @throws PeerUnavailableException
	 */
	private void updateServerTransactionMetaData(Map<String, Object> transactionMetaData, Object transactionAppData, MobicentsHASIPServerTransaction haServerTransaction, boolean recreation) throws ParseException,
			PeerUnavailableException {
		haServerTransaction.setMetaDataToReplicate(transactionMetaData, recreation);
		haServerTransaction.setApplicationDataToReplicate(transactionAppData);
	}

	/**
	 * @return the serverTransactions
	 */
	public Cache<String, Object> getServerTransactions() {
		return serverTransactions;
	}

	/**
	 * @param serverTransactions the serverTransactions to set
	 */
	public void setServerTransactions(Cache<String, Object> serverTransactions) {
		this.serverTransactions = serverTransactions;
	}

	/**
	 * @return the serverTransactionsApp
	 */
	public Cache<String, Object> getServerTransactionsApp() {
		return serverTransactionsApp;
	}

	/**
	 * @param serverTransactionsApp the serverTransactionsApp to set
	 */
	public void setServerTransactionsApp(Cache<String, Object> serverTransactionsApp) {
		this.serverTransactionsApp = serverTransactionsApp;
	}
}