/*
 * Copyright (C) 2008-2018 Fabrizio Montesi <[email protected]>
 *
 * This library 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 library 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 library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
 * MA 02110-1301  USA
 */

package jolie.net;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.StringReader;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Base64;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Result;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

import jolie.Interpreter;
import jolie.js.JsUtils;
import jolie.lang.Constants;
import jolie.lang.NativeType;
import jolie.net.http.HttpMessage;
import jolie.net.http.HttpParser;
import jolie.net.http.HttpUtils;
import jolie.net.http.Method;
import jolie.net.http.MultiPartFormDataParser;
import jolie.net.ports.Interface;
import jolie.net.protocols.CommProtocol;
import jolie.runtime.ByteArray;
import jolie.runtime.Value;
import jolie.runtime.ValueVector;
import jolie.runtime.VariablePath;
import jolie.runtime.typing.OneWayTypeDescription;
import jolie.runtime.typing.OperationTypeDescription;
import jolie.runtime.typing.RequestResponseTypeDescription;
import jolie.runtime.typing.Type;
import jolie.runtime.typing.TypeCastingException;
import jolie.tracer.ProtocolTraceAction;
import jolie.util.LocationParser;
import jolie.xml.XmlUtils;

/**
 * HTTP protocol implementation
 * 
 * @author Fabrizio Montesi 14 Nov 2012 - Saverio Giallorenzo - Fabrizio Montesi: support for status
 *         codes
 */
public class HttpProtocol extends CommProtocol implements HttpUtils.HttpProtocol {
	private static final int DEFAULT_STATUS_CODE = 200;
	private static final int DEFAULT_REDIRECTION_STATUS_CODE = 303;
	private static final String DEFAULT_CONTENT_TYPE = "application/octet-stream"; // default content type per RFC
																					// 2616#7.2.1
	private static final String DEFAULT_FORMAT = "xml";
	private static final Map< Integer, String > STATUS_CODE_DESCRIPTIONS = new HashMap<>();
	private static final Set< Integer > LOCATION_REQUIRED_STATUS_CODES = new HashSet<>();

	static {
		LOCATION_REQUIRED_STATUS_CODES.add( 301 );
		LOCATION_REQUIRED_STATUS_CODES.add( 302 );
		LOCATION_REQUIRED_STATUS_CODES.add( 303 );
		LOCATION_REQUIRED_STATUS_CODES.add( 307 );
		LOCATION_REQUIRED_STATUS_CODES.add( 308 );
	}

	static {
		// Initialise the HTTP Status code map.
		STATUS_CODE_DESCRIPTIONS.put( 100, "Continue" );
		STATUS_CODE_DESCRIPTIONS.put( 101, "Switching Protocols" );
		STATUS_CODE_DESCRIPTIONS.put( 102, "Processing" );
		STATUS_CODE_DESCRIPTIONS.put( 200, "OK" );
		STATUS_CODE_DESCRIPTIONS.put( 201, "Created" );
		STATUS_CODE_DESCRIPTIONS.put( 202, "Accepted" );
		STATUS_CODE_DESCRIPTIONS.put( 203, "Non-Authoritative Information" );
		STATUS_CODE_DESCRIPTIONS.put( 204, "No Content" );
		STATUS_CODE_DESCRIPTIONS.put( 205, "Reset Content" );
		STATUS_CODE_DESCRIPTIONS.put( 206, "Partial Content" );
		STATUS_CODE_DESCRIPTIONS.put( 207, "Multi-Status" );
		STATUS_CODE_DESCRIPTIONS.put( 208, "Already Reported" );
		STATUS_CODE_DESCRIPTIONS.put( 226, "IM Used" );
		STATUS_CODE_DESCRIPTIONS.put( 300, "Multiple Choices" );
		STATUS_CODE_DESCRIPTIONS.put( 301, "Moved Permanently" );
		STATUS_CODE_DESCRIPTIONS.put( 302, "Found" );
		STATUS_CODE_DESCRIPTIONS.put( 303, "See Other" );
		STATUS_CODE_DESCRIPTIONS.put( 304, "Not Modified" );
		STATUS_CODE_DESCRIPTIONS.put( 305, "Use Proxy" );
		STATUS_CODE_DESCRIPTIONS.put( 306, "Reserved" );
		STATUS_CODE_DESCRIPTIONS.put( 307, "Temporary Redirect" );
		STATUS_CODE_DESCRIPTIONS.put( 308, "Permanent Redirect" );
		STATUS_CODE_DESCRIPTIONS.put( 400, "Bad Request" );
		STATUS_CODE_DESCRIPTIONS.put( 401, "Unauthorized" );
		STATUS_CODE_DESCRIPTIONS.put( 402, "Payment Required" );
		STATUS_CODE_DESCRIPTIONS.put( 403, "Forbidden" );
		STATUS_CODE_DESCRIPTIONS.put( 404, "Not Found" );
		STATUS_CODE_DESCRIPTIONS.put( 405, "Method Not Allowed" );
		STATUS_CODE_DESCRIPTIONS.put( 406, "Not Acceptable" );
		STATUS_CODE_DESCRIPTIONS.put( 407, "Proxy Authentication Required" );
		STATUS_CODE_DESCRIPTIONS.put( 408, "Request Timeout" );
		STATUS_CODE_DESCRIPTIONS.put( 409, "Conflict" );
		STATUS_CODE_DESCRIPTIONS.put( 410, "Gone" );
		STATUS_CODE_DESCRIPTIONS.put( 411, "Length Required" );
		STATUS_CODE_DESCRIPTIONS.put( 412, "Precondition Failed" );
		STATUS_CODE_DESCRIPTIONS.put( 413, "Request Entity Too Large" );
		STATUS_CODE_DESCRIPTIONS.put( 414, "Request-URI Too Long" );
		STATUS_CODE_DESCRIPTIONS.put( 415, "Unsupported Media Type" );
		STATUS_CODE_DESCRIPTIONS.put( 416, "Requested Range Not Satisfiable" );
		STATUS_CODE_DESCRIPTIONS.put( 417, "Expectation Failed" );
		STATUS_CODE_DESCRIPTIONS.put( 422, "Unprocessable Entity" );
		STATUS_CODE_DESCRIPTIONS.put( 423, "Locked" );
		STATUS_CODE_DESCRIPTIONS.put( 424, "Failed Dependency" );
		STATUS_CODE_DESCRIPTIONS.put( 426, "Upgrade Required" );
		STATUS_CODE_DESCRIPTIONS.put( 427, "Unassigned" );
		STATUS_CODE_DESCRIPTIONS.put( 428, "Precondition Required" );
		STATUS_CODE_DESCRIPTIONS.put( 429, "Too Many Requests" );
		STATUS_CODE_DESCRIPTIONS.put( 430, "Unassigned" );
		STATUS_CODE_DESCRIPTIONS.put( 431, "Request Header Fields Too Large" );
		STATUS_CODE_DESCRIPTIONS.put( 500, "Internal Server Error" );
		STATUS_CODE_DESCRIPTIONS.put( 501, "Not Implemented" );
		STATUS_CODE_DESCRIPTIONS.put( 502, "Bad Gateway" );
		STATUS_CODE_DESCRIPTIONS.put( 503, "Service Unavailable" );
		STATUS_CODE_DESCRIPTIONS.put( 504, "Gateway Timeout" );
		STATUS_CODE_DESCRIPTIONS.put( 505, "HTTP Version Not Supported" );
		STATUS_CODE_DESCRIPTIONS.put( 507, "Insufficient Storage" );
		STATUS_CODE_DESCRIPTIONS.put( 508, "Loop Detected" );
		STATUS_CODE_DESCRIPTIONS.put( 509, "Unassigned" );
		STATUS_CODE_DESCRIPTIONS.put( 510, "Not Extended" );
		STATUS_CODE_DESCRIPTIONS.put( 511, "Network Authentication Required" );
	}

	private static class Parameters {
		private static final String KEEP_ALIVE = "keepAlive";
		private static final String DEBUG = "debug";
		private static final String COOKIES = "cookies";
		private static final String METHOD = "method";
		private static final String ALIAS = "alias";
		private static final String MULTIPART_HEADERS = "multipartHeaders";
		private static final String CONCURRENT = "concurrent";
		private static final String USER_AGENT = "userAgent";
		private static final String HOST = "host";
		private static final String HEADERS = "headers";
		private static final String ADD_HEADERS = "addHeader";
		private static final String STATUS_CODE = "statusCode";
		private static final String REDIRECT = "redirect";
		private static final String DEFAULT_OPERATION = "default";
		private static final String COMPRESSION = "compression";
		private static final String COMPRESSION_TYPES = "compressionTypes";
		private static final String REQUEST_COMPRESSION = "requestCompression";
		private static final String FORMAT = "format";
		private static final String RESPONSE_HEADER = "responseHeaders";
		private static final String JSON_ENCODING = "json_encoding";
		private static final String REQUEST_USER = "request";
		private static final String RESPONSE_USER = "response";
		private static final String HEADER_USER = "headers";
		private static final String CHARSET = "charset";
		private static final String CONTENT_TYPE = "contentType";
		private static final String CONTENT_TRANSFER_ENCODING = "contentTransferEncoding";
		private static final String CONTENT_DISPOSITION = "contentDisposition";
		private static final String DROP_URI_PATH = "dropURIPath";
		private static final String CACHE_CONTROL = "cacheControl";
		private static final String FORCE_CONTENT_DECODING = "forceContentDecoding";

		private static class MultiPartHeaders {
			private static final String FILENAME = "filename";
		}
	}

	private static class Headers {
		private static final String JOLIE_MESSAGE_ID = "X-Jolie-MessageID";
		private static final String JOLIE_RESOURCE_PATH = "X-Jolie-ServicePath";
	}

	private static class ContentTypes {
		private static final String APPLICATION_JSON = "application/json";
		private static final String APPLICATION_NDJSON = "application/x-ndjson";
	}

	private String inputId = null;
	private final Transformer transformer;
	private final DocumentBuilderFactory docBuilderFactory;
	private final DocumentBuilder docBuilder;
	private final URI uri;
	private final boolean inInputPort;
	private MultiPartFormDataParser multiPartFormDataParser = null;

	@Override
	public String name() {
		return "http";
	}

	@Override
	public boolean isThreadSafe() {
		return checkBooleanParameter( Parameters.CONCURRENT );
	}

	public HttpProtocol(
		VariablePath configurationPath,
		URI uri,
		boolean inInputPort,
		TransformerFactory transformerFactory,
		DocumentBuilderFactory docBuilderFactory,
		DocumentBuilder docBuilder )
		throws TransformerConfigurationException {
		super( configurationPath );
		this.uri = uri;
		this.inInputPort = inInputPort;
		this.transformer = transformerFactory.newTransformer();
		this.docBuilderFactory = docBuilderFactory;
		this.docBuilder = docBuilder;

		transformer.setOutputProperty( OutputKeys.OMIT_XML_DECLARATION, "yes" );
		transformer.setOutputProperty( OutputKeys.INDENT, "no" );
	}

	public String getMultipartHeaderForPart( String operationName, String partName ) {
		if( hasOperationSpecificParameter( operationName, Parameters.MULTIPART_HEADERS ) ) {
			Value v = getOperationSpecificParameterFirstValue( operationName, Parameters.MULTIPART_HEADERS );
			if( v.hasChildren( partName ) ) {
				v = v.getFirstChild( partName );
				if( v.hasChildren( Parameters.MultiPartHeaders.FILENAME ) ) {
					v = v.getFirstChild( Parameters.MultiPartHeaders.FILENAME );
					return v.strValue();
				}
			}
		}
		return null;
	}

	private final static String BOUNDARY = "----jol13h77p77bound4r155";

	private void send_appendCookies( CommMessage message, String hostname, StringBuilder headerBuilder ) {
		Value cookieParam = null;
		if( hasOperationSpecificParameter( message.operationName(), Parameters.COOKIES ) ) {
			cookieParam = getOperationSpecificParameterFirstValue( message.operationName(), Parameters.COOKIES );
		} else if( hasParameter( Parameters.COOKIES ) ) {
			cookieParam = getParameterFirstValue( Parameters.COOKIES );
		}
		if( cookieParam != null ) {
			Value cookieConfig;
			String domain;
			StringBuilder cookieSB = new StringBuilder();
			for( Entry< String, ValueVector > entry : cookieParam.children().entrySet() ) {
				cookieConfig = entry.getValue().first();
				if( message.value().hasChildren( cookieConfig.strValue() ) ) {
					domain =
						cookieConfig.hasChildren( "domain" ) ? cookieConfig.getFirstChild( "domain" ).strValue() : "";
					if( domain.isEmpty() || hostname.endsWith( domain ) ) {
						cookieSB
							.append( entry.getKey() )
							.append( '=' )
							.append( message.value().getFirstChild( cookieConfig.strValue() ).strValue() )
							.append( ";" );
					}
				}
			}
			if( cookieSB.length() > 0 ) {
				headerBuilder
					.append( "Cookie: " )
					.append( cookieSB )
					.append( HttpUtils.CRLF );
			}
		}
	}

	private void send_appendSetCookieHeader( CommMessage message, StringBuilder headerBuilder ) {
		Value cookieParam = null;
		if( hasOperationSpecificParameter( message.operationName(), Parameters.COOKIES ) ) {
			cookieParam = getOperationSpecificParameterFirstValue( message.operationName(), Parameters.COOKIES );
		} else if( hasParameter( Parameters.COOKIES ) ) {
			cookieParam = getParameterFirstValue( Parameters.COOKIES );
		}
		if( cookieParam != null ) {
			Value cookieConfig;
			for( Entry< String, ValueVector > entry : cookieParam.children().entrySet() ) {
				cookieConfig = entry.getValue().first();
				if( message.value().hasChildren( cookieConfig.strValue() ) ) {
					headerBuilder
						.append( "Set-Cookie: " )
						.append( entry.getKey() ).append( '=' )
						.append( message.value().getFirstChild( cookieConfig.strValue() ).strValue() )
						.append( "; expires=" )
						.append(
							cookieConfig.hasChildren( "expires" ) ? cookieConfig.getFirstChild( "expires" ).strValue()
								: "" )
						.append( "; domain=" )
						.append(
							cookieConfig.hasChildren( "domain" ) ? cookieConfig.getFirstChild( "domain" ).strValue()
								: "" )
						.append( "; path=" )
						.append(
							cookieConfig.hasChildren( "path" ) ? cookieConfig.getFirstChild( "path" ).strValue() : "" );
					if( cookieConfig.hasChildren( "secure" )
						&& cookieConfig.getFirstChild( "secure" ).intValue() > 0 ) {
						headerBuilder.append( "; secure" );
					}
					headerBuilder.append( HttpUtils.CRLF );
				}
			}
		}
	}

	private String encoding = null;
	private String responseFormat = null;
	private boolean headRequest = false;

	private static void send_appendQuerystring( Value value, StringBuilder headerBuilder )
		throws IOException {
		if( value.hasChildren() ) {
			headerBuilder.append( '?' );
			Iterator< Entry< String, ValueVector > > nodesIt = value.children().entrySet().iterator();
			while( nodesIt.hasNext() ) {
				Entry< String, ValueVector > entry = nodesIt.next();
				Iterator< Value > vecIt = entry.getValue().iterator();
				while( vecIt.hasNext() ) {
					Value v = vecIt.next();
					headerBuilder
						.append( URLEncoder.encode( entry.getKey(), HttpUtils.URL_DECODER_ENC ) )
						.append( '=' )
						.append( URLEncoder.encode( v.strValue(), HttpUtils.URL_DECODER_ENC ) );

					if( vecIt.hasNext() || nodesIt.hasNext() ) {
						headerBuilder.append( '&' );
					}
				}
			}
		}
	}

	private void send_appendJsonQueryString( CommMessage message, StringBuilder headerBuilder )
		throws IOException {
		if( message.value().isDefined() || message.value().hasChildren() ) {
			headerBuilder.append( "?" );
			StringBuilder builder = new StringBuilder();
			JsUtils.valueToJsonString( message.value(), true, getSendType( message ), builder );
			headerBuilder.append( URLEncoder.encode( builder.toString(), HttpUtils.URL_DECODER_ENC ) );
		}
	}

	private static void send_appendParsedAlias( String alias, Value value, StringBuilder headerBuilder )
		throws IOException {
		int offset = 0;
		List< String > aliasKeys = new ArrayList<>();
		String currStrValue;
		String currKey;
		StringBuilder result = new StringBuilder( alias );
		Matcher m = Pattern.compile( "%(!)?\\{[^\\}]*\\}" ).matcher( alias );

		while( m.find() ) {
			int displacement = 2;
			if( m.group( 1 ) == null ) { // ! is missing after %: We have to use URLEncoder
				currKey = alias.substring( m.start() + displacement, m.end() - 1 );
				if( "$".equals( currKey ) ) {
					currStrValue = URLEncoder.encode( value.strValue(), HttpUtils.URL_DECODER_ENC );
				} else {
					currStrValue =
						URLEncoder.encode( value.getFirstChild( currKey ).strValue(), HttpUtils.URL_DECODER_ENC );
					aliasKeys.add( currKey );
				}
			} else { // ! is given after %: We have to insert the string raw
				displacement = 3;
				currKey = alias.substring( m.start() + displacement, m.end() - 1 );
				if( "$".equals( currKey ) ) {
					currStrValue = value.strValue();
				} else {
					currStrValue = value.getFirstChild( currKey ).strValue();
					aliasKeys.add( currKey );
				}
			}

			result.replace(
				m.start() + offset, m.end() + offset,
				currStrValue );
			displacement++; // considering also }
			offset += currStrValue.length() - displacement - currKey.length();
		}
		// removing used keys
		aliasKeys.forEach( value.children()::remove );
		headerBuilder.append( result );
	}

	private String send_getFormat() {
		String format = DEFAULT_FORMAT;
		if( inInputPort && responseFormat != null ) {
			format = responseFormat;
			responseFormat = null;
		} else if( hasParameter( Parameters.FORMAT ) ) {
			format = getStringParameter( Parameters.FORMAT );
		}
		return format;
	}

	private static class EncodedContent {
		private ByteArray content = null;
		private String contentType = DEFAULT_CONTENT_TYPE;
		private String contentDisposition = "";
	}

	private EncodedContent send_encodeContent( CommMessage message, Method method, String charset, String format )
		throws IOException {
		EncodedContent ret = new EncodedContent();
		if( inInputPort == false && method == Method.GET ) {
			// We are building a GET request
			return ret;
		}

		if( "xml".equals( format ) ) {
			ret.contentType = "text/xml";
			Document doc = docBuilder.newDocument();
			Element root = doc.createElement( message.operationName() + ((inInputPort) ? "Response" : "") );
			doc.appendChild( root );
			if( message.isFault() ) {
				Element faultElement = doc.createElement( message.fault().faultName() );
				root.appendChild( faultElement );
				XmlUtils.valueToDocument( message.fault().value(), faultElement, doc );
			} else {
				XmlUtils.valueToDocument( message.value(), root, doc );
			}
			Source src = new DOMSource( doc );
			ByteArrayOutputStream tmpStream = new ByteArrayOutputStream();
			Result dest = new StreamResult( tmpStream );
			transformer.setOutputProperty( OutputKeys.ENCODING, charset );
			try {
				transformer.transform( src, dest );
			} catch( TransformerException e ) {
				throw new IOException( e );
			}
			ret.content = new ByteArray( tmpStream.toByteArray() );
		} else if( "binary".equals( format ) ) {
			ret.contentType = "application/octet-stream";
			ret.content = message.value().byteArrayValue();
		} else if( "html".equals( format ) ) {
			ret.contentType = "text/html";
			if( message.isFault() ) {
				StringBuilder builder = new StringBuilder();
				builder.append( "<html><head><title>" )
					.append( message.fault().faultName() )
					.append( "</title></head><body>" )
					.append( message.fault().value().strValue() )
					.append( "</body></html>" );
				ret.content = new ByteArray( builder.toString().getBytes( charset ) );
			} else {
				ret.content = new ByteArray( message.value().strValue().getBytes( charset ) );
			}
		} else if( "multipart/form-data".equals( format ) ) {
			ret.contentType = "multipart/form-data; boundary=" + BOUNDARY;
			ByteArrayOutputStream bStream = new ByteArrayOutputStream();
			StringBuilder builder = new StringBuilder();
			for( Entry< String, ValueVector > entry : message.value().children().entrySet() ) {
				if( !entry.getKey().startsWith( "@" ) ) {
					builder.append( "--" ).append( BOUNDARY ).append( HttpUtils.CRLF )
						.append( "Content-Disposition: form-data; name=\"" ).append( entry.getKey() ).append( '\"' );
					boolean isBinary = false;
					if( hasOperationSpecificParameter( message.operationName(), Parameters.MULTIPART_HEADERS ) ) {
						Value specOpParam = getOperationSpecificParameterFirstValue( message.operationName(),
							Parameters.MULTIPART_HEADERS );
						if( specOpParam.hasChildren( "partName" ) ) {
							ValueVector partNames = specOpParam.getChildren( "partName" );
							for( int p = 0; p < partNames.size(); p++ ) {
								if( partNames.get( p ).hasChildren( "part" ) ) {
									if( partNames.get( p ).getFirstChild( "part" ).strValue()
										.equals( entry.getKey() ) ) {
										isBinary = true;
										if( partNames.get( p ).hasChildren( "filename" ) ) {
											builder.append( "; filename=\"" )
												.append( partNames.get( p ).getFirstChild( "filename" ).strValue() )
												.append( "\"" );
										}
										if( partNames.get( p ).hasChildren( "contentType" ) ) {
											builder.append( HttpUtils.CRLF ).append( "Content-Type:" )
												.append( partNames.get( p ).getFirstChild( "contentType" ).strValue() );
										}
									}
								}
							}
						}
					}

					builder.append( HttpUtils.CRLF ).append( HttpUtils.CRLF );
					if( isBinary ) {
						bStream.write( builder.toString().getBytes( charset ) );
						bStream.write( entry.getValue().first().byteArrayValue().getBytes() );
						builder.delete( 0, builder.length() - 1 );
						builder.append( HttpUtils.CRLF );
					} else {
						builder.append( entry.getValue().first().strValue() ).append( HttpUtils.CRLF );
					}
				}
			}
			builder.append( "--" + BOUNDARY + "--" );
			bStream.write( builder.toString().getBytes( charset ) );
			ret.content = new ByteArray( bStream.toByteArray() );
		} else if( "x-www-form-urlencoded".equals( format ) ) {
			ret.contentType = "application/x-www-form-urlencoded";
			Iterator< Entry< String, ValueVector > > it =
				message.value().children().entrySet().iterator();
			StringBuilder builder = new StringBuilder();
			if( message.isFault() ) {
				builder.append( "faultName=" )
					.append( URLEncoder.encode( message.fault().faultName(), HttpUtils.URL_DECODER_ENC ) )
					.append( "&data=" )
					.append( URLEncoder.encode( message.fault().value().strValue(), HttpUtils.URL_DECODER_ENC ) );
			} else {
				Entry< String, ValueVector > entry;
				while( it.hasNext() ) {
					entry = it.next();
					builder.append( URLEncoder.encode( entry.getKey(), HttpUtils.URL_DECODER_ENC ) )
						.append( "=" )
						.append( URLEncoder.encode( entry.getValue().first().strValue(), HttpUtils.URL_DECODER_ENC ) );
					if( it.hasNext() ) {
						builder.append( '&' );
					}
				}
			}
			ret.content = new ByteArray( builder.toString().getBytes( charset ) );
		} else if( "json".equals( format ) ) {
			ret.contentType = ContentTypes.APPLICATION_JSON;
			StringBuilder jsonStringBuilder = new StringBuilder();
			if( message.isFault() ) {
				Value error = message.value().getFirstChild( "error" );
				error.getFirstChild( "code" ).setValue( -32000 );
				error.getFirstChild( "message" ).setValue( message.fault().faultName() );
				error.getChildren( "data" ).set( 0, message.fault().value() );
				JsUtils.faultValueToJsonString( message.value(), getSendType( message ), jsonStringBuilder );
			} else {
				JsUtils.valueToJsonString( message.value(), true, getSendType( message ), jsonStringBuilder );
			}
			ret.content = new ByteArray( jsonStringBuilder.toString().getBytes( charset ) );
		} else if( "ndjson".equals( format ) ) {
			ret.contentType = ContentTypes.APPLICATION_NDJSON;
			StringBuilder ndJsonStringBuilder = new StringBuilder();
			if( message.isFault() ) {
				Value error = message.value().getFirstChild( "error" );
				error.getFirstChild( "code" ).setValue( -32000 );
				error.getFirstChild( "message" ).setValue( message.fault().faultName() );
				error.getChildren( "data" ).set( 0, message.fault().value() );
				JsUtils.faultValueToJsonString( message.value(), getSendType( message ), ndJsonStringBuilder );
			} else {
				if( !message.value().hasChildren( "item" ) ) {
					Interpreter.getInstance().logWarning( "ndJson requires at least one child node 'item'" );
				}
				JsUtils.valueToNdJsonString( message.value(), true, getSendType( message ), ndJsonStringBuilder );
			}
			ret.content = new ByteArray( ndJsonStringBuilder.toString().getBytes( charset ) );
		} else if( "raw".equals( format ) ) {
			ret.contentType = "text/plain";
			if( message.isFault() ) {
				ret.content = new ByteArray( message.fault().value().strValue().getBytes( charset ) );
			} else {
				ret.content = new ByteArray( message.value().strValue().getBytes( charset ) );
			}
		}
		return ret;
	}

	private static boolean isLocationNeeded( int statusCode ) {
		return LOCATION_REQUIRED_STATUS_CODES.contains( statusCode );
	}

	private void send_appendResponseUserHeader( CommMessage message, StringBuilder headerBuilder ) {
		Value responseHeaderParameters = null;
		if( hasOperationSpecificParameter( message.operationName(), Parameters.RESPONSE_USER ) ) {
			responseHeaderParameters =
				getOperationSpecificParameterFirstValue( message.operationName(), Parameters.RESPONSE_USER );
			if( (responseHeaderParameters != null)
				&& (responseHeaderParameters.hasChildren( Parameters.HEADER_USER )) ) {
				for( Entry< String, ValueVector > entry : responseHeaderParameters
					.getFirstChild( Parameters.HEADER_USER ).children().entrySet() )
					headerBuilder.append( entry.getKey() ).append( ": " ).append( entry.getValue().first().strValue() )
						.append( HttpUtils.CRLF );
			}
		}

		responseHeaderParameters = null;
		if( hasParameter( Parameters.RESPONSE_USER ) ) {
			responseHeaderParameters = getParameterFirstValue( Parameters.RESPONSE_USER );

			if( (responseHeaderParameters != null)
				&& (responseHeaderParameters.hasChildren( Parameters.HEADER_USER )) ) {
				for( Entry< String, ValueVector > entry : responseHeaderParameters
					.getFirstChild( Parameters.HEADER_USER ).children().entrySet() )
					headerBuilder.append( entry.getKey() ).append( ": " ).append( entry.getValue().first().strValue() )
						.append( HttpUtils.CRLF );
			}
		}
	}

	private void send_appendResponseHeaders( CommMessage message, StringBuilder headerBuilder ) {
		int statusCode = DEFAULT_STATUS_CODE;
		String statusDescription = null;

		if( hasParameter( Parameters.STATUS_CODE ) ) {
			statusCode = getIntParameter( Parameters.STATUS_CODE );
			if( !STATUS_CODE_DESCRIPTIONS.containsKey( statusCode ) ) {
				Interpreter.getInstance().logWarning( "HTTP protocol for operation " +
					message.operationName() +
					" is sending a message with status code " +
					statusCode +
					", which is not in the HTTP specifications." );
				statusDescription = "Internal Server Error";
			} else if( isLocationNeeded( statusCode ) && !hasParameter( Parameters.REDIRECT ) ) {
				// if statusCode is a redirection code, location parameter is needed
				Interpreter.getInstance().logWarning( "HTTP protocol for operation " +
					message.operationName() +
					" is sending a message with status code " +
					statusCode +
					", which expects a redirect parameter but the latter is not set." );
			}
		} else if( hasParameter( Parameters.REDIRECT ) ) {
			statusCode = DEFAULT_REDIRECTION_STATUS_CODE;
		} else if( message.isFault() ) {
			statusCode = 500;
		}

		if( statusDescription == null ) {
			statusDescription = STATUS_CODE_DESCRIPTIONS.get( statusCode );
		}
		headerBuilder.append( "HTTP/1.1 " ).append( statusCode ).append( " " ).append( statusDescription )
			.append( HttpUtils.CRLF );

		// if redirect has been set, the redirect location parameter is set
		if( hasParameter( Parameters.REDIRECT ) ) {
			headerBuilder.append( "Location: " ).append( getStringParameter( Parameters.REDIRECT ) )
				.append( HttpUtils.CRLF );
		}

		send_appendSetCookieHeader( message, headerBuilder );
		headerBuilder.append( "Server: Jolie" ).append( HttpUtils.CRLF );
		StringBuilder cacheControlHeader = new StringBuilder();
		if( hasParameter( Parameters.CACHE_CONTROL ) ) {
			Value cacheControl = getParameterFirstValue( Parameters.CACHE_CONTROL );
			if( cacheControl.hasChildren( "maxAge" ) ) {
				cacheControlHeader.append( "max-age=" ).append( cacheControl.getFirstChild( "maxAge" ).intValue() );
			}
		}
		if( cacheControlHeader.length() > 0 ) {
			headerBuilder.append( "Cache-Control: " ).append( cacheControlHeader ).append( HttpUtils.CRLF );
		}
	}

	private static void send_appendRequestMethod( Method method, StringBuilder headerBuilder ) {
		headerBuilder.append( method.id() );
	}

	private void send_appendRequestPath( CommMessage message, Method method, String qsFormat,
		StringBuilder headerBuilder )
		throws IOException {
		String path = uri.getRawPath();
		if( uri.getScheme().equals( "localsocket" ) || path == null || path.isEmpty()
			|| checkBooleanParameter( Parameters.DROP_URI_PATH, false ) ) {
			headerBuilder.append( '/' );
		} else {
			if( path.charAt( 0 ) != '/' ) {
				headerBuilder.append( '/' );
			}
			headerBuilder.append( path );
			final Matcher m = LocationParser.RESOURCE_SEPARATOR_PATTERN.matcher( path );
			if( m.find() ) {
				if( !m.find() ) {
					headerBuilder.append( LocationParser.RESOURCE_SEPARATOR );
				}
			}
		}

		if( hasOperationSpecificParameter( message.operationName(), Parameters.ALIAS ) ) {
			String alias = getOperationSpecificStringParameter( message.operationName(), Parameters.ALIAS );
			send_appendParsedAlias( alias, message.value(), headerBuilder );
		} else {
			headerBuilder.append( message.operationName() );
		}

		if( method == Method.GET ) {
			if( qsFormat.equals( "json" ) ) {
				send_appendJsonQueryString( message, headerBuilder );
			} else {
				send_appendQuerystring( message.value(), headerBuilder );
			}
		}
	}

	private static void send_appendAuthorizationHeader( CommMessage message, StringBuilder headerBuilder ) {
		if( message.value()
			.hasChildren( jolie.lang.Constants.Predefined.HTTP_BASIC_AUTHENTICATION.token().content() ) ) {
			Value v = message.value()
				.getFirstChild( jolie.lang.Constants.Predefined.HTTP_BASIC_AUTHENTICATION.token().content() );
			// String realm = v.getFirstChild( "realm" ).strValue();
			String userpass =
				v.getFirstChild( "userid" ).strValue() + ":" +
					v.getFirstChild( "password" ).strValue();
			Base64.Encoder encoder = Base64.getEncoder();
			userpass = encoder.encodeToString( userpass.getBytes() );
			headerBuilder.append( "Authorization: Basic " ).append( userpass ).append( HttpUtils.CRLF );
			message.value().children()
				.remove( jolie.lang.Constants.Predefined.HTTP_BASIC_AUTHENTICATION.token().content() );
		}
	}

	private void send_appendRequestUserHeader( CommMessage message, StringBuilder headerBuilder ) {
		Value responseHeaderParameters = null;
		if( hasOperationSpecificParameter( message.operationName(), Parameters.REQUEST_USER ) ) {
			responseHeaderParameters =
				getOperationSpecificParameterFirstValue( message.operationName(), Parameters.RESPONSE_USER );
			if( (responseHeaderParameters != null)
				&& (responseHeaderParameters.hasChildren( Parameters.HEADER_USER )) ) {
				for( Entry< String, ValueVector > entry : responseHeaderParameters
					.getFirstChild( Parameters.HEADER_USER ).children().entrySet() )
					headerBuilder.append( entry.getKey() ).append( ": " ).append( entry.getValue().first().strValue() )
						.append( HttpUtils.CRLF );
			}
		}

		responseHeaderParameters = null;
		if( hasParameter( Parameters.RESPONSE_USER ) ) {
			responseHeaderParameters = getParameterFirstValue( Parameters.REQUEST_USER );
			if( (responseHeaderParameters != null)
				&& (responseHeaderParameters.hasChildren( Parameters.HEADER_USER )) ) {
				for( Entry< String, ValueVector > entry : responseHeaderParameters
					.getFirstChild( Parameters.HEADER_USER ).children().entrySet() )
					headerBuilder.append( entry.getKey() ).append( ": " ).append( entry.getValue().first().strValue() )
						.append( HttpUtils.CRLF );
			}
		}
	}

	private void send_appendHeader( StringBuilder headerBuilder ) {
		if( hasParameter( Parameters.ADD_HEADERS ) ) {
			Value v = getParameterFirstValue( Parameters.ADD_HEADERS );
			if( v.hasChildren( "header" ) ) {
				for( Value head : v.getChildren( "header" ) ) {
					String header = head.strValue() + ": "
						+ head.getFirstChild( "value" ).strValue();
					headerBuilder.append( header ).append( HttpUtils.CRLF );
				}
			}
		}
	}

	private Method send_getRequestMethod( CommMessage message )
		throws IOException {
		return hasOperationSpecificParameter( message.operationName(), Parameters.METHOD )
			? Method.fromString( getOperationSpecificStringParameter( message.operationName(), Parameters.METHOD ) )
			: hasParameterValue( Parameters.METHOD ) ? Method.fromString( getStringParameter( Parameters.METHOD ) )
				: Method.POST;
	}

	private void send_appendRequestHeaders( CommMessage message, Method method, String qsFormat,
		StringBuilder headerBuilder )
		throws IOException {
		send_appendRequestMethod( method, headerBuilder );
		headerBuilder.append( ' ' );
		send_appendRequestPath( message, method, qsFormat, headerBuilder );
		headerBuilder.append( " HTTP/1.1" ).append( HttpUtils.CRLF );
		String host = uri.getHost();
		if( uri.getScheme().equals( "localsocket" ) ) {
			/*
			 * in this case we need to replace the localsocket path with a host, that is the default one
			 * localhost
			 */
			host = "localhost";
		}
		headerBuilder.append( "Host: " ).append( host ).append( HttpUtils.CRLF );
		send_appendCookies( message, uri.getHost(), headerBuilder );
		send_appendAuthorizationHeader( message, headerBuilder );
		if( checkBooleanParameter( Parameters.COMPRESSION, true ) ) {
			String requestCompression = getStringParameter( Parameters.REQUEST_COMPRESSION );
			if( requestCompression.equals( "gzip" ) || requestCompression.equals( "deflate" ) ) {
				encoding = requestCompression;
				headerBuilder.append( "Accept-Encoding: " ).append( encoding ).append( HttpUtils.CRLF );
			} else {
				headerBuilder.append( "Accept-Encoding: gzip, deflate" ).append( HttpUtils.CRLF );
			}
		}
		send_appendHeader( headerBuilder );
	}

	private void send_appendGenericHeaders(
		CommMessage message,
		EncodedContent encodedContent,
		String charset,
		StringBuilder headerBuilder )
		throws IOException {
		if( checkBooleanParameter( Parameters.KEEP_ALIVE, true ) == false || channel().toBeClosed() ) {
			channel().setToBeClosed( true );
			headerBuilder.append( "Connection: close" ).append( HttpUtils.CRLF );
		}
		if( checkBooleanParameter( Parameters.CONCURRENT, true ) ) {
			headerBuilder.append( Headers.JOLIE_MESSAGE_ID ).append( ": " ).append( message.id() )
				.append( HttpUtils.CRLF );
		}

		headerBuilder.append( Headers.JOLIE_RESOURCE_PATH ).append( ": " ).append( message.resourcePath() )
			.append( HttpUtils.CRLF );

		String contentType = getStringParameter( Parameters.CONTENT_TYPE );
		if( contentType.length() > 0 ) {
			encodedContent.contentType = contentType;
		}
		encodedContent.contentType = encodedContent.contentType.toLowerCase();

		headerBuilder.append( "Content-Type: " ).append( encodedContent.contentType );
		if( charset != null ) {
			headerBuilder.append( "; charset=" ).append( charset.toLowerCase() );
		}
		headerBuilder.append( HttpUtils.CRLF );

		if( encodedContent.content != null ) {
			String transferEncoding = getStringParameter( Parameters.CONTENT_TRANSFER_ENCODING );
			if( transferEncoding.length() > 0 ) {
				headerBuilder.append( "Content-Transfer-Encoding: " ).append( transferEncoding )
					.append( HttpUtils.CRLF );
			}

			String contentDisposition = getStringParameter( Parameters.CONTENT_DISPOSITION );
			if( contentDisposition.length() > 0 ) {
				encodedContent.contentDisposition = contentDisposition;
				headerBuilder.append( "Content-Disposition: " ).append( encodedContent.contentDisposition )
					.append( HttpUtils.CRLF );
			}

			boolean compression = encoding != null && checkBooleanParameter( Parameters.COMPRESSION, true );
			String compressionTypes = getStringParameter(
				Parameters.COMPRESSION_TYPES,
				"text/html text/css text/plain text/xml text/x-js application/json application/javascript application/x-www-form-urlencoded application/xhtml+xml application/xml" )
					.toLowerCase();
			if( compression && !compressionTypes.equals( "*" )
				&& !compressionTypes.contains( encodedContent.contentType ) ) {
				compression = false;
			}
			if( compression ) {


				Interpreter.getInstance().tracer().trace( () -> {
					try {
						final String traceMessage = encodedContent.content.toString( charset );
						return new ProtocolTraceAction( ProtocolTraceAction.Type.HTTP, "HTTP COMPRESSING MESSAGE",
							message.resourcePath(), traceMessage, null );
					} catch( UnsupportedEncodingException e ) {
						return new ProtocolTraceAction( ProtocolTraceAction.Type.HTTP, "HTTP COMPRESSING MESSAGE",
							message.resourcePath(), e.getMessage(), null );
					}

				} );
				encodedContent.content = HttpUtils.encode( encoding, encodedContent.content, headerBuilder );
			}

			headerBuilder.append( "Content-Length: " ).append( encodedContent.content.size() ).append( HttpUtils.CRLF );
		} else {
			headerBuilder.append( "Content-Length: 0" ).append( HttpUtils.CRLF );
		}
	}

	private String prepareSendDebugString( CharSequence header, EncodedContent encodedContent, String charset,
		boolean showContent )
		throws UnsupportedEncodingException {
		StringBuilder debugSB = new StringBuilder();
		debugSB.append( "[HTTP debug] Sending:\n" )
			.append( header );
		if( showContent && encodedContent != null && encodedContent.content != null ) {
			debugSB.append( encodedContent.content.toString( charset ) );
		}
		return debugSB.toString();
	}

	private void send_logDebugInfo( CharSequence header, EncodedContent encodedContent, String charset )
		throws IOException {
		if( checkBooleanParameter( Parameters.DEBUG ) ) {
			boolean showContent = false;
			if( getParameterVector( Parameters.DEBUG ).first().getFirstChild( "showContent" ).intValue() > 0
				&& encodedContent.content != null ) {
				showContent = true;
			}
			Interpreter.getInstance().logInfo( prepareSendDebugString( header, encodedContent, charset, showContent ) );
		}
	}

	@Override
	public void send_internal( OutputStream ostream, CommMessage message, InputStream istream )
		throws IOException {
		Method method = send_getRequestMethod( message );
		String charset = HttpUtils.getCharset( getStringParameter( Parameters.CHARSET, "utf-8" ), null );
		String format = send_getFormat();
		String contentType = null;
		StringBuilder headerBuilder = new StringBuilder();

		if( inInputPort ) {
			// We're responding to a request
			send_appendResponseHeaders( message, headerBuilder );
			send_appendResponseUserHeader( message, headerBuilder );
			send_appendHeader( headerBuilder );

		} else {
			// We're sending a notification or a solicit
			String qsFormat = "";
			if( method == Method.GET && getParameterFirstValue( Parameters.METHOD ).hasChildren( "queryFormat" ) ) {
				if( getParameterFirstValue( Parameters.METHOD ).getFirstChild( "queryFormat" ).strValue()
					.equals( "json" ) ) {
					qsFormat = format = "json";
					contentType = ContentTypes.APPLICATION_JSON;
				}
			}
			send_appendRequestUserHeader( message, headerBuilder );
			send_appendRequestHeaders( message, method, qsFormat, headerBuilder );
		}
		EncodedContent encodedContent = send_encodeContent( message, method, charset, format );
		if( contentType != null ) {
			encodedContent.contentType = contentType;
		}
		send_appendGenericHeaders( message, encodedContent, charset, headerBuilder );
		headerBuilder.append( HttpUtils.CRLF );

		send_logDebugInfo( headerBuilder, encodedContent, charset );

		Interpreter.getInstance().tracer().trace( () -> {
			try {
				final String traceMessage = prepareSendDebugString( headerBuilder, encodedContent, charset, true );
				return new ProtocolTraceAction( ProtocolTraceAction.Type.HTTP, "HTTP MESSAGE SENT",
					message.resourcePath(), traceMessage, null );
			} catch( UnsupportedEncodingException e ) {
				return new ProtocolTraceAction( ProtocolTraceAction.Type.HTTP, "HTTP MESSAGE SENT",
					message.resourcePath(), e.getMessage(), null );
			}
		} );

		inputId = message.operationName();

		ostream.write( headerBuilder.toString().getBytes( HttpUtils.URL_DECODER_ENC ) );
		if( encodedContent.content != null && !headRequest ) {
			ostream.write( encodedContent.content.getBytes() );
		}
		headRequest = false;
	}

	@Override
	public void send( OutputStream ostream, CommMessage message, InputStream istream )
		throws IOException {
		HttpUtils.send( ostream, message, istream, inInputPort, channel(), this );
	}

	private void parseXML( HttpMessage message, Value value, String charset )
		throws IOException {
		try {
			if( message.size() > 0 ) {
				DocumentBuilder builder = docBuilderFactory.newDocumentBuilder();
				InputSource src = new InputSource( new ByteArrayInputStream( message.content() ) );
				src.setEncoding( charset );
				Document doc = builder.parse( src );
				XmlUtils.documentToValue( doc, value, false );
			}
		} catch( ParserConfigurationException | SAXException pce ) {
			throw new IOException( pce );
		}
	}

	private static void parseJson( HttpMessage message, Value value, boolean strictEncoding, String charset )
		throws IOException {
		JsUtils.parseJsonIntoValue( new InputStreamReader( new ByteArrayInputStream( message.content() ), charset ),
			value, strictEncoding );
	}

	private static void parseNdJson( HttpMessage message, Value value, boolean strictEncoding, String charset )
		throws IOException {
		JsUtils.parseNdJsonIntoValue(
			new BufferedReader( new InputStreamReader( new ByteArrayInputStream( message.content() ), charset ) ),
			value, strictEncoding );
	}

	private static void parseForm( HttpMessage message, Value value, String charset )
		throws IOException {
		String line = new String( message.content(), charset );
		String[] pair;
		for( String item : line.split( "&" ) ) {
			pair = item.split( "=", 2 );
			if( pair.length > 1 ) {
				value.getChildren( URLDecoder.decode( pair[ 0 ], HttpUtils.URL_DECODER_ENC ) ).first()
					.setValue( URLDecoder.decode( pair[ 1 ], HttpUtils.URL_DECODER_ENC ) );
			}
		}
	}

	private void parseMultiPartFormData( HttpMessage message, Value value )
		// , String charset )
		throws IOException {
		multiPartFormDataParser = new MultiPartFormDataParser( message, value );
		multiPartFormDataParser.parse();
	}

	private void recv_checkForSetCookie( HttpMessage message, Value value )
		throws IOException {
		if( hasParameter( Parameters.COOKIES ) ) {
			String type;
			Value cookies = getParameterFirstValue( Parameters.COOKIES );
			Value cookieConfig;
			Value v;
			for( HttpMessage.Cookie cookie : message.setCookies() ) {
				if( cookies.hasChildren( cookie.name() ) ) {
					cookieConfig = cookies.getFirstChild( cookie.name() );
					if( cookieConfig.isString() ) {
						v = value.getFirstChild( cookieConfig.strValue() );
						type =
							cookieConfig.hasChildren( "type" ) ? cookieConfig.getFirstChild( "type" ).strValue()
								: "string";
						recv_assignCookieValue( cookie.value(), v, type );
					}
				}

				/*
				 * currValue = Value.create(); currValue.getNewChild( "expires" ).setValue( cookie.expirationDate()
				 * ); currValue.getNewChild( "path" ).setValue( cookie.path() ); currValue.getNewChild( "name"
				 * ).setValue( cookie.name() ); currValue.getNewChild( "value" ).setValue( cookie.value() );
				 * currValue.getNewChild( "domain" ).setValue( cookie.domain() ); currValue.getNewChild( "secure"
				 * ).setValue( (cookie.secure() ? 1 : 0) ); cookieVec.add( currValue );
				 */
			}
		}
	}

	private static void recv_assignCookieValue( String cookieValue, Value value, String typeKeyword )
		throws IOException {
		NativeType type = NativeType.fromString( typeKeyword );
		if( NativeType.INT == type ) {
			try {
				value.setValue( Integer.valueOf( cookieValue ) );
			} catch( NumberFormatException e ) {
				throw new IOException( e );
			}
		} else if( NativeType.LONG == type ) {
			try {
				value.setValue( Long.valueOf( cookieValue ) );
			} catch( NumberFormatException e ) {
				throw new IOException( e );
			}
		} else if( NativeType.STRING == type ) {
			value.setValue( cookieValue );
		} else if( NativeType.DOUBLE == type ) {
			try {
				value.setValue( new Double( cookieValue ) );
			} catch( NumberFormatException e ) {
				throw new IOException( e );
			}
		} else if( NativeType.BOOL == type ) {
			value.setValue( Boolean.valueOf( cookieValue ) );
		} else {
			value.setValue( cookieValue );
		}
	}

	private void recv_checkForCookies( HttpMessage message, DecodedMessage decodedMessage )
		throws IOException {
		Value cookies = null;
		if( hasOperationSpecificParameter( decodedMessage.operationName, Parameters.COOKIES ) ) {
			cookies = getOperationSpecificParameterFirstValue( decodedMessage.operationName, Parameters.COOKIES );
		} else if( hasParameter( Parameters.COOKIES ) ) {
			cookies = getParameterFirstValue( Parameters.COOKIES );
		}
		if( cookies != null ) {
			Value v;
			String type;
			for( Entry< String, String > entry : message.cookies().entrySet() ) {
				if( cookies.hasChildren( entry.getKey() ) ) {
					Value cookieConfig = cookies.getFirstChild( entry.getKey() );
					if( cookieConfig.isString() ) {
						v = decodedMessage.value.getFirstChild( cookieConfig.strValue() );
						if( cookieConfig.hasChildren( "type" ) ) {
							type = cookieConfig.getFirstChild( "type" ).strValue();
						} else {
							type = "string";
						}
						recv_assignCookieValue( entry.getValue(), v, type );
					}
				}
			}
		}
	}

	private void recv_checkForGenericHeader( HttpMessage message, DecodedMessage decodedMessage )
		throws IOException {
		Value headers = null;
		if( hasOperationSpecificParameter( decodedMessage.operationName, Parameters.HEADERS ) ) {
			headers = getOperationSpecificParameterFirstValue( decodedMessage.operationName, Parameters.HEADERS );
		} else if( hasParameter( Parameters.HEADERS ) ) {
			headers = getParameterFirstValue( Parameters.HEADERS );
		}
		if( headers != null ) {
			for( String headerName : headers.children().keySet() ) {
				String headerAlias = headers.getFirstChild( headerName ).strValue();
				decodedMessage.value.getFirstChild( headerAlias )
					.setValue( message.getPropertyOrEmptyString( headerName.replace( "_", "-" ) ) );
			}
		}
	}

	private static void recv_parseQueryString( HttpMessage message, Value value, String contentType,
		boolean strictEncoding )
		throws IOException {
		if( message.isGet() && contentType.equals( ContentTypes.APPLICATION_JSON ) ) {
			recv_parseJsonQueryString( message, value, strictEncoding );
		} else {
			Map< String, Integer > indexes = new HashMap<>();
			String queryString = message.requestPath();
			String[] kv = queryString.split( "\\?", 2 );
			Integer index;
			if( kv.length > 1 ) {
				queryString = kv[ 1 ];
				String[] params = queryString.split( "&" );
				for( String param : params ) {
					String[] ikv = param.split( "=", 2 );
					if( ikv.length > 1 ) {
						index = indexes.computeIfAbsent( ikv[ 0 ], k -> 0 );
						// the query string was already URL decoded by the HttpParser
						value.getChildren( ikv[ 0 ] ).get( index ).setValue( ikv[ 1 ] );
						indexes.put( ikv[ 0 ], index + 1 );
					}
				}
			}
		}
	}

	private static void recv_parseJsonQueryString( HttpMessage message, Value value, boolean strictEncoding )
		throws IOException {
		String queryString = message.requestPath();
		String[] kv = queryString.split( "\\?", 2 );
		if( kv.length > 1 ) {
			// the query string was already URL decoded by the HttpParser
			JsUtils.parseJsonIntoValue( new StringReader( kv[ 1 ] ), value, strictEncoding );
		}
	}

	/*
	 * Prints debug information about a received message
	 */
	private String getDebugMessage( HttpMessage message, String charset, boolean showContent )
		throws IOException {
		StringBuilder debugSB = new StringBuilder();
		debugSB.append( "[HTTP debug] Receiving:\n" ).append( "HTTP Code: " ).append( message.statusCode() )
			.append( "\n" ).append( "HTTP Method: " ).append( message.type().name() ).append( "\n" )
			.append( "Resource: " ).append( message.requestPath() ).append( "\n" )
			.append( "--> Header properties\n" );
		for( Entry< String, String > entry : message.properties() ) {
			debugSB.append( '\t' ).append( entry.getKey() ).append( ": " ).append( entry.getValue() ).append( '\n' );
		}
		for( HttpMessage.Cookie cookie : message.setCookies() ) {
			debugSB.append( "\tset-cookie: " ).append( cookie.toString() ).append( '\n' );
		}
		for( Entry< String, String > entry : message.cookies().entrySet() ) {
			debugSB.append( "\tcookie: " ).append( entry.getKey() ).append( '=' ).append( entry.getValue() )
				.append( '\n' );
		}
		if( showContent ) {
			debugSB.append( "--> Message content\n" )
				.append( new String( message.content(), charset ) );
		}
		return debugSB.toString();
	}

	private void recv_parseRequestFormat( String type )
		throws IOException {
		responseFormat = null;

		if( "text/xml".equals( type ) ) {
			responseFormat = "xml";
		} else if( ContentTypes.APPLICATION_JSON.equals( type ) ) {
			responseFormat = "json";
		}
	}

	private void recv_parseMessage( HttpMessage message, DecodedMessage decodedMessage, String type, String charset )
		throws IOException {
		final String operationName = message.isResponse() ? inputId : decodedMessage.operationName;
		if( getOperationSpecificStringParameter( operationName, Parameters.FORCE_CONTENT_DECODING )
			.equals( NativeType.STRING.id() ) ) {
			decodedMessage.value.setValue( new String( message.content(), charset ) );
		} else if( getOperationSpecificStringParameter( operationName, Parameters.FORCE_CONTENT_DECODING )
			.equals( NativeType.RAW.id() ) ) {
			decodedMessage.value.setValue( new ByteArray( message.content() ) );
		} else if( "text/html".equals( type ) ) {
			decodedMessage.value.setValue( new String( message.content(), charset ) );
		} else if( "application/x-www-form-urlencoded".equals( type ) ) {
			parseForm( message, decodedMessage.value, charset );
		} else if( "text/xml".equals( type ) || type.contains( "xml" ) ) {
			parseXML( message, decodedMessage.value, charset );
		} else if( "multipart/form-data".equals( type ) ) {
			parseMultiPartFormData( message, decodedMessage.value );
		} else if( "application/octet-stream".equals( type ) || type.startsWith( "image/" )
			|| "application/zip".equals( type ) ) {
			decodedMessage.value.setValue( new ByteArray( message.content() ) );
		} else if( ContentTypes.APPLICATION_NDJSON.equals( type ) || type.contains( "ndjson" ) ) {
			boolean strictEncoding = checkStringParameter( Parameters.JSON_ENCODING, "strict" );
			parseNdJson( message, decodedMessage.value, strictEncoding, charset );
		} else if( ContentTypes.APPLICATION_JSON.equals( type ) || type.contains( "json" ) ) {
			boolean strictEncoding = checkStringParameter( Parameters.JSON_ENCODING, "strict" );
			parseJson( message, decodedMessage.value, strictEncoding, charset );
		} else {
			decodedMessage.value.setValue( new String( message.content(), charset ) );
		}
	}

	private String getDefaultOperation( HttpMessage.Type t ) {
		if( hasParameter( Parameters.DEFAULT_OPERATION ) ) {
			Value dParam = getParameterFirstValue( Parameters.DEFAULT_OPERATION );
			String method = HttpUtils.httpMessageTypeToString( t );
			if( method == null || dParam.hasChildren( method ) == false ) {
				return dParam.strValue();
			} else {
				return dParam.getFirstChild( method ).strValue();
			}
		}

		return null;
	}

	private void recv_checkReceivingOperation( HttpMessage message, DecodedMessage decodedMessage ) {
		if( decodedMessage.operationName == null ) {
			final String requestPath = message.requestPath().split( "\\?", 2 )[ 0 ].substring( 1 );
			if( requestPath.startsWith( LocationParser.RESOURCE_SEPARATOR ) ) {
				final String compositePath = requestPath.substring( LocationParser.RESOURCE_SEPARATOR.length() - 1 );
				final Matcher m = LocationParser.RESOURCE_SEPARATOR_PATTERN.matcher( compositePath );
				if( m.find() ) {
					decodedMessage.resourcePath = compositePath.substring( 0, m.start() );
					decodedMessage.operationName = compositePath.substring( m.end() );
				} else {
					decodedMessage.resourcePath = compositePath;
				}
			} else {
				decodedMessage.operationName = requestPath;
				decodedMessage.resourcePath = message.getProperty( Headers.JOLIE_RESOURCE_PATH );
				if( decodedMessage.resourcePath == null ) {
					decodedMessage.resourcePath = "/";
				}
			}
		}

	}

	private void recv_checkDefaultOp( HttpMessage message, DecodedMessage decodedMessage ) {
		if( decodedMessage.resourcePath.equals( "/" )
			&& !channel().parentInputPort().canHandleInputOperation( decodedMessage.operationName ) ) {
			String defaultOpId = getDefaultOperation( message.type() );
			if( defaultOpId != null ) {
				Value body = decodedMessage.value;
				decodedMessage.value = Value.create();
				decodedMessage.value.getChildren( "data" ).add( body );
				decodedMessage.value.getFirstChild( "operation" ).setValue( decodedMessage.operationName );
				decodedMessage.value.setFirstChild( "requestUri", message.requestPath() );
				if( message.userAgent() != null ) {
					decodedMessage.value.getFirstChild( Parameters.USER_AGENT ).setValue( message.userAgent() );
				}
				Value cookies = decodedMessage.value.getFirstChild( "cookies" );
				for( Entry< String, String > cookie : message.cookies().entrySet() ) {
					cookies.getFirstChild( cookie.getKey() ).setValue( cookie.getValue() );
				}
				decodedMessage.operationName = defaultOpId;
			}
		}
	}

	private void recv_checkForMultiPartHeaders( DecodedMessage decodedMessage ) {
		if( multiPartFormDataParser != null ) {
			String target;
			for( Entry< String, MultiPartFormDataParser.PartProperties > entry : multiPartFormDataParser
				.getPartPropertiesSet() ) {
				if( entry.getValue().filename() != null ) {
					target = getMultipartHeaderForPart( decodedMessage.operationName, entry.getKey() );
					if( target != null ) {
						decodedMessage.value.getFirstChild( target ).setValue( entry.getValue().filename() );
					}
				}
			}
			multiPartFormDataParser = null;
		}
	}

	private void recv_checkForMessageProperties( HttpMessage message, DecodedMessage decodedMessage )
		throws IOException {
		recv_checkForCookies( message, decodedMessage );
		recv_checkForGenericHeader( message, decodedMessage );
		recv_checkForMultiPartHeaders( decodedMessage );
		if( message.userAgent() != null &&
			hasParameter( Parameters.USER_AGENT ) ) {
			getParameterFirstValue( Parameters.USER_AGENT ).setValue( message.userAgent() );
		}

		if( getParameterVector( Parameters.HOST ) != null ) {
			getParameterFirstValue( Parameters.HOST ).setValue( message.getPropertyOrEmptyString( Parameters.HOST ) );
		}
	}

	private static class DecodedMessage {
		private String operationName = null;
		private Value value = Value.create();
		private String resourcePath = "/";
		private long id = CommMessage.GENERIC_ID;
	}

	private void recv_checkForStatusCode( HttpMessage message ) {
		if( hasParameter( Parameters.STATUS_CODE ) ) {
			getParameterFirstValue( Parameters.STATUS_CODE ).setValue( message.statusCode() );
		}
	}

	@Override
	public CommMessage recv_internal( InputStream istream, OutputStream ostream )
		throws IOException {
		HttpMessage message = new HttpParser( istream ).parse();
		String charset = HttpUtils.getCharset( null, message );
		CommMessage retVal = null;
		DecodedMessage decodedMessage = new DecodedMessage();

		HttpUtils.recv_checkForChannelClosing( message, channel() );

		if( checkBooleanParameter( Parameters.DEBUG ) ) {
			boolean showContent = false;
			if( getParameterFirstValue( Parameters.DEBUG ).getFirstChild( "showContent" ).intValue() > 0
				&& message.size() > 0 ) {
				showContent = true;
			}
			Interpreter.getInstance().logInfo( getDebugMessage( message, charset, showContent ) );
		}

		// tracer
		Interpreter.getInstance().tracer().trace( () -> {
			try {
				final String traceMessage = getDebugMessage( message, charset, message.size() > 0 );
				return new ProtocolTraceAction( ProtocolTraceAction.Type.HTTP, "HTTP MESSAGE RECEIVED",
					message.requestPath(), traceMessage, null );
			} catch( IOException e ) {
				return new ProtocolTraceAction( ProtocolTraceAction.Type.HTTP, "HTTP MESSAGE RECEIVED",
					message.requestPath(), e.getMessage(), null );

			}

		} );

		recv_checkForStatusCode( message );

		encoding = message.getProperty( "accept-encoding" );
		headRequest = inInputPort && message.isHead();

		String contentType = DEFAULT_CONTENT_TYPE;
		if( message.getProperty( "content-type" ) != null ) {
			contentType = message.getProperty( "content-type" ).split( ";", 2 )[ 0 ].toLowerCase();
		}

		// URI parameter parsing
		if( message.requestPath() != null ) {
			boolean strictEncoding = checkStringParameter( Parameters.JSON_ENCODING, "strict" );
			recv_parseQueryString( message, decodedMessage.value, contentType, strictEncoding );
		}

		recv_parseRequestFormat( contentType );
		if( !message.isResponse() ) {
			recv_checkReceivingOperation( message, decodedMessage );
		}

		/* https://tools.ietf.org/html/rfc7231#section-4.3 */
		if( !message.isGet() && !message.isHead() ) {
			// body parsing
			if( message.size() > 0 ) {
				recv_parseMessage( message, decodedMessage, contentType, charset );
			}
		}

		if( !message.isResponse() ) {
			recv_checkDefaultOp( message, decodedMessage );
		}

		if( checkBooleanParameter( Parameters.CONCURRENT ) ) {
			String messageId = message.getProperty( Headers.JOLIE_MESSAGE_ID );
			if( messageId != null ) {
				try {
					decodedMessage.id = Long.parseLong( messageId );
				} catch( NumberFormatException e ) {
				}
			}
		}

		if( message.isResponse() ) {
			String responseHeader = "";
			if( hasParameter( Parameters.RESPONSE_HEADER )
				|| hasOperationSpecificParameter( inputId, Parameters.RESPONSE_HEADER ) ) {
				if( hasOperationSpecificParameter( inputId, Parameters.RESPONSE_HEADER ) ) {
					responseHeader = getOperationSpecificStringParameter( inputId, Parameters.RESPONSE_HEADER );
				} else {
					responseHeader = getStringParameter( Parameters.RESPONSE_HEADER );
				}
				for( Entry< String, String > param : message.properties() ) {
					decodedMessage.value.getFirstChild( responseHeader ).getFirstChild( param.getKey() )
						.setValue( param.getValue() );
				}
				decodedMessage.value.getFirstChild( responseHeader ).getFirstChild( Parameters.STATUS_CODE )
					.setValue( message.statusCode() );
			}

			recv_checkForSetCookie( message, decodedMessage.value );
			retVal =
				new CommMessage( decodedMessage.id, inputId, decodedMessage.resourcePath, decodedMessage.value, null );
		} else if( message.isError() == false ) {
			recv_checkForMessageProperties( message, decodedMessage );
			retVal = new CommMessage( decodedMessage.id, decodedMessage.operationName, decodedMessage.resourcePath,
				decodedMessage.value, null );
		}

		if( retVal != null && "/".equals( retVal.resourcePath() ) && channel().parentPort() != null
			&& (channel().parentPort().getInterface().containsOperation( retVal.operationName() )
				|| (channel().parentInputPort() != null
					&& channel().parentInputPort().getAggregatedOperation( retVal.operationName() ) != null)) ) {
			try {
				// The message is for this service
				boolean hasInput = false;
				OneWayTypeDescription oneWayTypeDescription = null;
				if( channel().parentInputPort() != null ) {
					if( channel().parentInputPort().getAggregatedOperation( retVal.operationName() ) != null ) {
						oneWayTypeDescription =
							channel().parentInputPort().getAggregatedOperation( retVal.operationName() )
								.getOperationTypeDescription().asOneWayTypeDescription();
						hasInput = true;
					}
				}
				if( !hasInput ) {
					Interface iface = channel().parentPort().getInterface();
					oneWayTypeDescription = iface.oneWayOperations().get( retVal.operationName() );
				}

				if( oneWayTypeDescription != null ) {
					// We are receiving a One-Way message
					oneWayTypeDescription.requestType().cast( retVal.value() );
				} else {
					hasInput = false;
					RequestResponseTypeDescription rrTypeDescription = null;
					if( channel().parentInputPort() != null ) {
						if( channel().parentInputPort().getAggregatedOperation( retVal.operationName() ) != null ) {
							rrTypeDescription =
								channel().parentInputPort().getAggregatedOperation( retVal.operationName() )
									.getOperationTypeDescription().asRequestResponseTypeDescription();
							hasInput = true;
						}
					}

					if( !hasInput ) {
						Interface iface = channel().parentPort().getInterface();
						rrTypeDescription = iface.requestResponseOperations().get( retVal.operationName() );
					}

					if( retVal.isFault() ) {
						Type faultType = rrTypeDescription.faults().get( retVal.fault().faultName() );
						if( faultType != null ) {
							faultType.cast( retVal.value() );
						}
					} else {
						if( message.isResponse() ) {
							rrTypeDescription.responseType().cast( retVal.value() );
						} else {
							rrTypeDescription.requestType().cast( retVal.value() );
						}
					}
				}
			} catch( TypeCastingException e ) {
				// TODO: do something here?
			}
		}

		return retVal;
	}

	@Override
	public CommMessage recv( InputStream istream, OutputStream ostream )
		throws IOException {
		return HttpUtils.recv( istream, ostream, inInputPort, channel(), this );
	}

	private Type getSendType( CommMessage message )
		throws IOException {
		Type ret = null;

		if( channel().parentPort() == null ) {
			throw new IOException( "Could not retrieve communication port for HTTP protocol" );
		}

		OperationTypeDescription opDesc =
			channel().parentPort().getOperationTypeDescription( message.operationName(), Constants.ROOT_RESOURCE_PATH );

		if( opDesc == null ) {
			return null;
		}

		if( opDesc.asOneWayTypeDescription() != null ) {
			if( message.isFault() ) {
				ret = Type.UNDEFINED;
			} else {
				OneWayTypeDescription ow = opDesc.asOneWayTypeDescription();
				ret = ow.requestType();
			}
		} else if( opDesc.asRequestResponseTypeDescription() != null ) {
			RequestResponseTypeDescription rr = opDesc.asRequestResponseTypeDescription();
			if( message.isFault() ) {
				ret = rr.getFaultType( message.fault().faultName() );
				if( ret == null ) {
					ret = Type.UNDEFINED;
				}
			} else {
				ret = (inInputPort) ? rr.responseType() : rr.requestType();
			}
		}

		return ret;
	}
}