<commit_before> /**************************************************************************
 *   Copyright (C) 2008-2015 by <NAME> <<EMAIL>>     *
 *                                                                         *
 *   This program is free software; you can redistribute it and/or modify  *
 *   it under the terms of the GNU Library General Public License as       *
 *   published by the Free Software Foundation; either version 2 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 General Public License for more details.                          *
 *                                                                         *
 *   You should have received a copy of the GNU Library General Public     *
 *   License along with this program; if not, write to the                 *
 *   Free Software Foundation, Inc.,                                       *
 *   59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.             *
 *                                                                         *
 *   For details about the authors of this software, see the AUTHORS file. *
 ***************************************************************************/

package jolie.net;

import com.google.gwt.user.client.rpc.SerializationException;
import com.google.gwt.user.server.rpc.RPC;
import com.google.gwt.user.server.rpc.RPCRequest;
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.net.URI;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
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 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.util.LocationParser;
import jolie.xml.XmlUtils;
import joliex.gwt.client.JolieService;
import joliex.gwt.server.JolieGWTConverter;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

/**
 * HTTP protocol implementation
 * @author <NAME>
 * 14 Nov 2012 - <NAME> - <NAME>: support for status codes
 */
public class HttpProtocol extends CommProtocol
{
	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 > statusCodeDescriptions = new HashMap< Integer, String >();
	private static final Set< Integer > locationRequiredStatusCodes = new HashSet< Integer >();
	
	static {
		locationRequiredStatusCodes.add( 301 );
		locationRequiredStatusCodes.add( 302 );
		locationRequiredStatusCodes.add( 303 );
		locationRequiredStatusCodes.add( 307 );
		locationRequiredStatusCodes.add( 308 );
	}
	
	static {
		// Initialise the HTTP Status code map.
		statusCodeDescriptions.put( 100,"Continue" );
		statusCodeDescriptions.put( 101,"Switching Protocols" );
		statusCodeDescriptions.put( 102,"Processing" );
		statusCodeDescriptions.put( 200,"OK" );
		statusCodeDescriptions.put( 201,"Created" );
		statusCodeDescriptions.put( 202,"Accepted" );
		statusCodeDescriptions.put( 203,"Non-Authoritative Information" );
		statusCodeDescriptions.put( 204,"No Content" );
		statusCodeDescriptions.put( 205,"Reset Content" );
		statusCodeDescriptions.put( 206,"Partial Content" );
		statusCodeDescriptions.put( 207,"Multi-Status" );
		statusCodeDescriptions.put( 208,"Already Reported" );
		statusCodeDescriptions.put( 226,"IM Used" );
		statusCodeDescriptions.put( 300,"Multiple Choices" );
		statusCodeDescriptions.put( 301,"Moved Permanently" );
		statusCodeDescriptions.put( 302,"Found" );
		statusCodeDescriptions.put( 303,"See Other" );
		statusCodeDescriptions.put( 304,"Not Modified" );
		statusCodeDescriptions.put( 305,"Use Proxy" );
		statusCodeDescriptions.put( 306,"Reserved" );
		statusCodeDescriptions.put( 307,"Temporary Redirect" );
		statusCodeDescriptions.put( 308,"Permanent Redirect" );
		statusCodeDescriptions.put( 400,"Bad Request" );
		statusCodeDescriptions.put( 401,"Unauthorized" );
		statusCodeDescriptions.put( 402,"Payment Required" );
		statusCodeDescriptions.put( 403,"Forbidden" );
		statusCodeDescriptions.put( 404,"Not Found" );
		statusCodeDescriptions.put( 405,"Method Not Allowed" );
		statusCodeDescriptions.put( 406,"Not Acceptable" );
		statusCodeDescriptions.put( 407,"Proxy Authentication Required" );
		statusCodeDescriptions.put( 408,"Request Timeout" );
		statusCodeDescriptions.put( 409,"Conflict" );
		statusCodeDescriptions.put( 410,"Gone" );
		statusCodeDescriptions.put( 411,"Length Required" );
		statusCodeDescriptions.put( 412,"Precondition Failed" );
		statusCodeDescriptions.put( 413,"Request Entity Too Large" );
		statusCodeDescriptions.put( 414,"Request-URI Too Long" );
		statusCodeDescriptions.put( 415,"Unsupported Media Type" );
		statusCodeDescriptions.put( 416,"Requested Range Not Satisfiable" );
		statusCodeDescriptions.put( 417,"Expectation Failed" );
		statusCodeDescriptions.put( 422,"Unprocessable Entity" );
		statusCodeDescriptions.put( 423,"Locked" );
		statusCodeDescriptions.put( 424,"Failed Dependency" );
		statusCodeDescriptions.put( 426,"Upgrade Required" );
		statusCodeDescriptions.put( 427,"Unassigned" );
		statusCodeDescriptions.put( 428,"Precondition Required" );
		statusCodeDescriptions.put( 429,"Too Many Requests" );
		statusCodeDescriptions.put( 430,"Unassigned" );
		statusCodeDescriptions.put( 431,"Request Header Fields Too Large" );
		statusCodeDescriptions.put( 500,"Internal Server Error" );
		statusCodeDescriptions.put( 501,"Not Implemented" );
		statusCodeDescriptions.put( 502,"Bad Gateway" );
		statusCodeDescriptions.put( 503,"Service Unavailable" );
		statusCodeDescriptions.put( 504,"Gateway Timeout" );
		statusCodeDescriptions.put( 505,"HTTP Version Not Supported" );
		statusCodeDescriptions.put( 507,"Insufficient Storage" );
		statusCodeDescriptions.put( 508,"Loop Detected" );
		statusCodeDescriptions.put( 509,"Unassigned" );
		statusCodeDescriptions.put( 510,"Not Extended" );
		statusCodeDescriptions.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 JSON_ENCODING = "json_encoding";
		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 class MultiPartHeaders {
			private static final String FILENAME = "filename";
		}
	}

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

	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;
	
	public String name()
	{
		return "http";
	}

	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" );
	}
		
	private static void valueToDocument(
			Value value,
			Node node,
			Document doc
			)
	{
		node.appendChild( doc.createTextNode( value.strValue() ) );

		Element currentElement;
		for( Entry< String, ValueVector > entry : value.children().entrySet() ) {
			if ( !entry.getKey().startsWith( "@" ) ) {
				for( Value val : entry.getValue() ) {
					currentElement = doc.createElement( entry.getKey() );
					node.appendChild( currentElement );
					Map< String, ValueVector > attrs = jolie.xml.XmlUtils.getAttributesOrNull( val );
					if ( attrs != null ) {
						for( Entry< String, ValueVector > attrEntry : attrs.entrySet() ) {
							currentElement.setAttribute(
								attrEntry.getKey(),
								attrEntry.getValue().first().strValue()
								);
						}
					}
					valueToDocument( val, currentElement, doc );
				}
			}
		}
	}

	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.children().isEmpty() ) {
			headerBuilder.append( '?' );
			for( Entry< String, ValueVector > entry : value.children().entrySet() ) {
				for( Value v : entry.getValue() ) {
					headerBuilder
						.append( URLEncoder.encode( entry.getKey(), HttpUtils.URL_DECODER_ENC ) )
						.append( '=' )
						.append( URLEncoder.encode( v.strValue(), HttpUtils.URL_DECODER_ENC ) )
						.append( '&' );
				}
			}
		}
	}
        
	private void send_appendJsonQueryString( CommMessage message, StringBuilder headerBuilder )
		throws IOException
	{
		if ( !message.value().children().isEmpty() ) {
			headerBuilder.append( "?=" );
			StringBuilder builder = new StringBuilder();
			JsUtils.valueToJsonString( message.value(), 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;
		ArrayList<String> aliasKeys = new ArrayList<String>();
		String currStrValue;
		String currKey;
		StringBuilder result = new StringBuilder( alias );
		Matcher m = Pattern.compile( "%(!)?\\{[^\\}]*\\}" ).matcher( alias );

		while( m.find() ) {
			if ( m.group( 1 ) == null ) { // We have to use URLEncoder
				currKey = alias.substring( m.start() + 2, 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 { // We have to insert the string raw
				currKey = alias.substring( m.start() + 3, 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
			);
			offset += currStrValue.length() - 3 - currKey.length();
		}
		// removing used keys
		for( int k = 0; k < aliasKeys.size(); k++ ) {
			value.children().remove( aliasKeys.get( k ) );
		}
		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 );
				valueToDocument( message.fault().value(), faultElement, doc );
			} else {
				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>" );
				builder.append( message.fault().faultName() );
				builder.append( "</title></head><body>" );
				builder.append( message.fault().value().strValue() );
				builder.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 );
					builder.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=" );
				builder.append( URLEncoder.encode( message.fault().faultName(), HttpUtils.URL_DECODER_ENC ) );
				builder.append( "&data=" );
				builder.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 ( "text/x-gwt-rpc".equals( format ) ) {
			ret.contentType = "text/x-gwt-rpc";
			try {
				if ( message.isFault() ) {
					ret.content = new ByteArray(
						RPC.encodeResponseForFailure( JolieService.class.getMethods()[0], JolieGWTConverter.jolieToGwtFault( message.fault() ) ).getBytes( charset )
					);
				} else {
					joliex.gwt.client.Value v = new joliex.gwt.client.Value();
					JolieGWTConverter.jolieToGwtValue( message.value(), v );
					ret.content = new ByteArray(
						RPC.encodeResponseForSuccess( JolieService.class.getMethods()[0], v ).getBytes( charset )
					);
				}
			} catch( SerializationException e ) {
				throw new IOException( e );
			}
		} else if ( "json".equals( format ) ) {
			ret.contentType = "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(), getSendType( message ), jsonStringBuilder );
			}
			ret.content = new ByteArray( jsonStringBuilder.toString().getBytes( charset ) );
		} else if ( "raw".equals( format ) ) {
			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 locationRequiredStatusCodes.contains( statusCode );
	}

	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 ( !statusCodeDescriptions.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;
		}

		if ( statusDescription == null ) {
			statusDescription = statusCodeDescriptions.get( statusCode );
		}
		headerBuilder.append( "HTTP/1.1 " + statusCode + " " + statusDescription + HttpUtils.CRLF );
		
		// if redirect has been set, the redirect location parameter is set
		if ( hasParameter( Parameters.REDIRECT ) ) {
			headerBuilder.append( "Location: " + getStringParameter( Parameters.REDIRECT ) + 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, StringBuilder headerBuilder )
		throws IOException
	{
		String path = uri.getRawPath();
		if ( path == null || path.isEmpty() || checkBooleanParameter( Parameters.DROP_URI_PATH, false ) ) {
			headerBuilder.append( '/' );
		} else {
			if ( path.charAt( 0 ) != '/' ) {
				headerBuilder.append( '/' );
			}
			headerBuilder.append( path );
		}

		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 ) {
			boolean jsonFormat = false;
			if ( getParameterFirstValue( Parameters.METHOD ).hasChildren( "queryFormat" ) ) {
				if ( getParameterFirstValue( Parameters.METHOD ).getFirstChild( "queryFormat" ).strValue().equals( "json" ) ) {
					jsonFormat = true;
					send_appendJsonQueryString( message, headerBuilder );
				}
			}
			if ( !jsonFormat ) {
				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();
			sun.misc.BASE64Encoder encoder = new sun.misc.BASE64Encoder();
			userpass = encoder.encode( userpass.getBytes() );
			headerBuilder.append( "Authorization: Basic " ).append( userpass ).append( HttpUtils.CRLF );
		}
	}
        
        private void send_appendHeader( CommMessage message, StringBuilder headerBuilder )
	{
                Value v = getParameterFirstValue( Parameters.ADD_HEADERS );
		if ( v != null ) {
                    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
	{
		Method method =
			hasOperationSpecificParameter( message.operationName(), Parameters.METHOD ) ?
				Method.fromString( getOperationSpecificStringParameter( message.operationName(), Parameters.METHOD ).toUpperCase() )
			: hasParameter( Parameters.METHOD ) ?
				Method.fromString( getStringParameter( Parameters.METHOD ).toUpperCase() )
			:
				Method.POST;
		return method;
	}
	
	private void send_appendRequestHeaders( CommMessage message, Method method, StringBuilder headerBuilder )
		throws IOException
	{
		send_appendRequestMethod( method, headerBuilder );
		headerBuilder.append( ' ' );
		send_appendRequestPath( message, method, headerBuilder );
		headerBuilder.append( " HTTP/1.1" + HttpUtils.CRLF );
		headerBuilder.append( "Host: " + uri.getHost() + 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: " + encoding + HttpUtils.CRLF );
			} else {
				headerBuilder.append( "Accept-Encoding: gzip, deflate" + HttpUtils.CRLF );
			}
		}
		send_appendHeader( message, 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" + HttpUtils.CRLF );
		}
		if ( checkBooleanParameter( Parameters.CONCURRENT, true ) ) {
			headerBuilder.append( Headers.JOLIE_MESSAGE_ID ).append( ": " ).append( message.id() ).append( HttpUtils.CRLF );
		}

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

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

			String transferEncoding = getStringParameter( Parameters.CONTENT_TRANSFER_ENCODING );
			if ( transferEncoding.length() > 0 ) {
				headerBuilder.append( "Content-Transfer-Encoding: " + transferEncoding + HttpUtils.CRLF );
			}
			
			String contentDisposition = getStringParameter( Parameters.CONTENT_DISPOSITION );
			if ( contentDisposition.length() > 0 ) {
				encodedContent.contentDisposition = contentDisposition;
				headerBuilder.append( "Content-Disposition: " + encodedContent.contentDisposition + 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"
			).toLowerCase();
			if ( compression && !compressionTypes.equals( "*" ) && !compressionTypes.contains( encodedContent.contentType ) ) {
				compression = false;
			}
			if ( compression ) {
				encodedContent.content = HttpUtils.encode( encoding, encodedContent.content, headerBuilder );
			}

			headerBuilder.append( "Content-Length: " + (encodedContent.content.size()) + HttpUtils.CRLF ); //headerBuilder.append( "Content-Length: " + (encodedContent.content.size() + 2) + HttpUtils.CRLF );
		} else {
			headerBuilder.append( "Content-Length: 0" + HttpUtils.CRLF );
		}
	}
	
	private void send_logDebugInfo( CharSequence header, EncodedContent encodedContent, String charset )
		throws IOException
	{
		if ( checkBooleanParameter( Parameters.DEBUG ) ) {<commit_msg>http extension - remove misleading comment about the content-length calculation, it is correct as we are doing now<commit_after> /**************************************************************************
 *   Copyright (C) 2008-2015 by <NAME> <<EMAIL>>     *
 *                                                                         *
 *   This program is free software; you can redistribute it and/or modify  *
 *   it under the terms of the GNU Library General Public License as       *
 *   published by the Free Software Foundation; either version 2 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 General Public License for more details.                          *
 *                                                                         *
 *   You should have received a copy of the GNU Library General Public     *
 *   License along with this program; if not, write to the                 *
 *   Free Software Foundation, Inc.,                                       *
 *   59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.             *
 *                                                                         *
 *   For details about the authors of this software, see the AUTHORS file. *
 ***************************************************************************/

package jolie.net;

import com.google.gwt.user.client.rpc.SerializationException;
import com.google.gwt.user.server.rpc.RPC;
import com.google.gwt.user.server.rpc.RPCRequest;
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.net.URI;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
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 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.util.LocationParser;
import jolie.xml.XmlUtils;
import joliex.gwt.client.JolieService;
import joliex.gwt.server.JolieGWTConverter;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

/**
 * HTTP protocol implementation
 * @author <NAME>
 * 14 Nov 2012 - <NAME> - <NAME>: support for status codes
 */
public class HttpProtocol extends CommProtocol
{
	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 > statusCodeDescriptions = new HashMap< Integer, String >();
	private static final Set< Integer > locationRequiredStatusCodes = new HashSet< Integer >();
	
	static {
		locationRequiredStatusCodes.add( 301 );
		locationRequiredStatusCodes.add( 302 );
		locationRequiredStatusCodes.add( 303 );
		locationRequiredStatusCodes.add( 307 );
		locationRequiredStatusCodes.add( 308 );
	}
	
	static {
		// Initialise the HTTP Status code map.
		statusCodeDescriptions.put( 100,"Continue" );
		statusCodeDescriptions.put( 101,"Switching Protocols" );
		statusCodeDescriptions.put( 102,"Processing" );
		statusCodeDescriptions.put( 200,"OK" );
		statusCodeDescriptions.put( 201,"Created" );
		statusCodeDescriptions.put( 202,"Accepted" );
		statusCodeDescriptions.put( 203,"Non-Authoritative Information" );
		statusCodeDescriptions.put( 204,"No Content" );
		statusCodeDescriptions.put( 205,"Reset Content" );
		statusCodeDescriptions.put( 206,"Partial Content" );
		statusCodeDescriptions.put( 207,"Multi-Status" );
		statusCodeDescriptions.put( 208,"Already Reported" );
		statusCodeDescriptions.put( 226,"IM Used" );
		statusCodeDescriptions.put( 300,"Multiple Choices" );
		statusCodeDescriptions.put( 301,"Moved Permanently" );
		statusCodeDescriptions.put( 302,"Found" );
		statusCodeDescriptions.put( 303,"See Other" );
		statusCodeDescriptions.put( 304,"Not Modified" );
		statusCodeDescriptions.put( 305,"Use Proxy" );
		statusCodeDescriptions.put( 306,"Reserved" );
		statusCodeDescriptions.put( 307,"Temporary Redirect" );
		statusCodeDescriptions.put( 308,"Permanent Redirect" );
		statusCodeDescriptions.put( 400,"Bad Request" );
		statusCodeDescriptions.put( 401,"Unauthorized" );
		statusCodeDescriptions.put( 402,"Payment Required" );
		statusCodeDescriptions.put( 403,"Forbidden" );
		statusCodeDescriptions.put( 404,"Not Found" );
		statusCodeDescriptions.put( 405,"Method Not Allowed" );
		statusCodeDescriptions.put( 406,"Not Acceptable" );
		statusCodeDescriptions.put( 407,"Proxy Authentication Required" );
		statusCodeDescriptions.put( 408,"Request Timeout" );
		statusCodeDescriptions.put( 409,"Conflict" );
		statusCodeDescriptions.put( 410,"Gone" );
		statusCodeDescriptions.put( 411,"Length Required" );
		statusCodeDescriptions.put( 412,"Precondition Failed" );
		statusCodeDescriptions.put( 413,"Request Entity Too Large" );
		statusCodeDescriptions.put( 414,"Request-URI Too Long" );
		statusCodeDescriptions.put( 415,"Unsupported Media Type" );
		statusCodeDescriptions.put( 416,"Requested Range Not Satisfiable" );
		statusCodeDescriptions.put( 417,"Expectation Failed" );
		statusCodeDescriptions.put( 422,"Unprocessable Entity" );
		statusCodeDescriptions.put( 423,"Locked" );
		statusCodeDescriptions.put( 424,"Failed Dependency" );
		statusCodeDescriptions.put( 426,"Upgrade Required" );
		statusCodeDescriptions.put( 427,"Unassigned" );
		statusCodeDescriptions.put( 428,"Precondition Required" );
		statusCodeDescriptions.put( 429,"Too Many Requests" );
		statusCodeDescriptions.put( 430,"Unassigned" );
		statusCodeDescriptions.put( 431,"Request Header Fields Too Large" );
		statusCodeDescriptions.put( 500,"Internal Server Error" );
		statusCodeDescriptions.put( 501,"Not Implemented" );
		statusCodeDescriptions.put( 502,"Bad Gateway" );
		statusCodeDescriptions.put( 503,"Service Unavailable" );
		statusCodeDescriptions.put( 504,"Gateway Timeout" );
		statusCodeDescriptions.put( 505,"HTTP Version Not Supported" );
		statusCodeDescriptions.put( 507,"Insufficient Storage" );
		statusCodeDescriptions.put( 508,"Loop Detected" );
		statusCodeDescriptions.put( 509,"Unassigned" );
		statusCodeDescriptions.put( 510,"Not Extended" );
		statusCodeDescriptions.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 JSON_ENCODING = "json_encoding";
		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 class MultiPartHeaders {
			private static final String FILENAME = "filename";
		}
	}

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

	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;
	
	public String name()
	{
		return "http";
	}

	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" );
	}
		
	private static void valueToDocument(
			Value value,
			Node node,
			Document doc
			)
	{
		node.appendChild( doc.createTextNode( value.strValue() ) );

		Element currentElement;
		for( Entry< String, ValueVector > entry : value.children().entrySet() ) {
			if ( !entry.getKey().startsWith( "@" ) ) {
				for( Value val : entry.getValue() ) {
					currentElement = doc.createElement( entry.getKey() );
					node.appendChild( currentElement );
					Map< String, ValueVector > attrs = jolie.xml.XmlUtils.getAttributesOrNull( val );
					if ( attrs != null ) {
						for( Entry< String, ValueVector > attrEntry : attrs.entrySet() ) {
							currentElement.setAttribute(
								attrEntry.getKey(),
								attrEntry.getValue().first().strValue()
								);
						}
					}
					valueToDocument( val, currentElement, doc );
				}
			}
		}
	}

	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.children().isEmpty() ) {
			headerBuilder.append( '?' );
			for( Entry< String, ValueVector > entry : value.children().entrySet() ) {
				for( Value v : entry.getValue() ) {
					headerBuilder
						.append( URLEncoder.encode( entry.getKey(), HttpUtils.URL_DECODER_ENC ) )
						.append( '=' )
						.append( URLEncoder.encode( v.strValue(), HttpUtils.URL_DECODER_ENC ) )
						.append( '&' );
				}
			}
		}
	}
        
	private void send_appendJsonQueryString( CommMessage message, StringBuilder headerBuilder )
		throws IOException
	{
		if ( !message.value().children().isEmpty() ) {
			headerBuilder.append( "?=" );
			StringBuilder builder = new StringBuilder();
			JsUtils.valueToJsonString( message.value(), 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;
		ArrayList<String> aliasKeys = new ArrayList<String>();
		String currStrValue;
		String currKey;
		StringBuilder result = new StringBuilder( alias );
		Matcher m = Pattern.compile( "%(!)?\\{[^\\}]*\\}" ).matcher( alias );

		while( m.find() ) {
			if ( m.group( 1 ) == null ) { // We have to use URLEncoder
				currKey = alias.substring( m.start() + 2, 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 { // We have to insert the string raw
				currKey = alias.substring( m.start() + 3, 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
			);
			offset += currStrValue.length() - 3 - currKey.length();
		}
		// removing used keys
		for( int k = 0; k < aliasKeys.size(); k++ ) {
			value.children().remove( aliasKeys.get( k ) );
		}
		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 );
				valueToDocument( message.fault().value(), faultElement, doc );
			} else {
				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>" );
				builder.append( message.fault().faultName() );
				builder.append( "</title></head><body>" );
				builder.append( message.fault().value().strValue() );
				builder.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 );
					builder.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=" );
				builder.append( URLEncoder.encode( message.fault().faultName(), HttpUtils.URL_DECODER_ENC ) );
				builder.append( "&data=" );
				builder.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 ( "text/x-gwt-rpc".equals( format ) ) {
			ret.contentType = "text/x-gwt-rpc";
			try {
				if ( message.isFault() ) {
					ret.content = new ByteArray(
						RPC.encodeResponseForFailure( JolieService.class.getMethods()[0], JolieGWTConverter.jolieToGwtFault( message.fault() ) ).getBytes( charset )
					);
				} else {
					joliex.gwt.client.Value v = new joliex.gwt.client.Value();
					JolieGWTConverter.jolieToGwtValue( message.value(), v );
					ret.content = new ByteArray(
						RPC.encodeResponseForSuccess( JolieService.class.getMethods()[0], v ).getBytes( charset )
					);
				}
			} catch( SerializationException e ) {
				throw new IOException( e );
			}
		} else if ( "json".equals( format ) ) {
			ret.contentType = "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(), getSendType( message ), jsonStringBuilder );
			}
			ret.content = new ByteArray( jsonStringBuilder.toString().getBytes( charset ) );
		} else if ( "raw".equals( format ) ) {
			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 locationRequiredStatusCodes.contains( statusCode );
	}

	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 ( !statusCodeDescriptions.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;
		}

		if ( statusDescription == null ) {
			statusDescription = statusCodeDescriptions.get( statusCode );
		}
		headerBuilder.append( "HTTP/1.1 " + statusCode + " " + statusDescription + HttpUtils.CRLF );
		
		// if redirect has been set, the redirect location parameter is set
		if ( hasParameter( Parameters.REDIRECT ) ) {
			headerBuilder.append( "Location: " + getStringParameter( Parameters.REDIRECT ) + 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, StringBuilder headerBuilder )
		throws IOException
	{
		String path = uri.getRawPath();
		if ( path == null || path.isEmpty() || checkBooleanParameter( Parameters.DROP_URI_PATH, false ) ) {
			headerBuilder.append( '/' );
		} else {
			if ( path.charAt( 0 ) != '/' ) {
				headerBuilder.append( '/' );
			}
			headerBuilder.append( path );
		}

		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 ) {
			boolean jsonFormat = false;
			if ( getParameterFirstValue( Parameters.METHOD ).hasChildren( "queryFormat" ) ) {
				if ( getParameterFirstValue( Parameters.METHOD ).getFirstChild( "queryFormat" ).strValue().equals( "json" ) ) {
					jsonFormat = true;
					send_appendJsonQueryString( message, headerBuilder );
				}
			}
			if ( !jsonFormat ) {
				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();
			sun.misc.BASE64Encoder encoder = new sun.misc.BASE64Encoder();
			userpass = encoder.encode( userpass.getBytes() );
			headerBuilder.append( "Authorization: Basic " ).append( userpass ).append( HttpUtils.CRLF );
		}
	}
        
        private void send_appendHeader( CommMessage message, StringBuilder headerBuilder )
	{
                Value v = getParameterFirstValue( Parameters.ADD_HEADERS );
		if ( v != null ) {
                    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
	{
		Method method =
			hasOperationSpecificParameter( message.operationName(), Parameters.METHOD ) ?
				Method.fromString( getOperationSpecificStringParameter( message.operationName(), Parameters.METHOD ).toUpperCase() )
			: hasParameter( Parameters.METHOD ) ?
				Method.fromString( getStringParameter( Parameters.METHOD ).toUpperCase() )
			:
				Method.POST;
		return method;
	}
	
	private void send_appendRequestHeaders( CommMessage message, Method method, StringBuilder headerBuilder )
		throws IOException
	{
		send_appendRequestMethod( method, headerBuilder );
		headerBuilder.append( ' ' );
		send_appendRequestPath( message, method, headerBuilder );
		headerBuilder.append( " HTTP/1.1" + HttpUtils.CRLF );
		headerBuilder.append( "Host: " + uri.getHost() + 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: " + encoding + HttpUtils.CRLF );
			} else {
				headerBuilder.append( "Accept-Encoding: gzip, deflate" + HttpUtils.CRLF );
			}
		}
		send_appendHeader( message, 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" + HttpUtils.CRLF );
		}
		if ( checkBooleanParameter( Parameters.CONCURRENT, true ) ) {
			headerBuilder.append( Headers.JOLIE_MESSAGE_ID ).append( ": " ).append( message.id() ).append( HttpUtils.CRLF );
		}

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

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

			String transferEncoding = getStringParameter( Parameters.CONTENT_TRANSFER_ENCODING );
			if ( transferEncoding.length() > 0 ) {
				headerBuilder.append( "Content-Transfer-Encoding: " + transferEncoding + HttpUtils.CRLF );
			}
			
			String contentDisposition = getStringParameter( Parameters.CONTENT_DISPOSITION );
			if ( contentDisposition.length() > 0 ) {
				encodedContent.contentDisposition = contentDisposition;
				headerBuilder.append( "Content-Disposition: " + encodedContent.contentDisposition + 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"
			).toLowerCase();
			if ( compression && !compressionTypes.equals( "*" ) && !compressionTypes.contains( encodedContent.contentType ) ) {
				compression = false;
			}
			if ( compression ) {
				encodedContent.content = HttpUtils.encode( encoding, encodedContent.content, headerBuilder );
			}

			headerBuilder.append( "Content-Length: " + encodedContent.content.size() + HttpUtils.CRLF );
		} else {
			headerBuilder.append( "Content-Length: 0" + HttpUtils.CRLF );
		}
	}
	
	private void send_logDebugInfo( CharSequence header, EncodedContent encodedContent, String charset )
		throws IOException
	{
		if ( checkBooleanParameter( Parameters.DEBUG ) ) {
{'fraction_non_alphanumeric': 0.12482412944073162, 'fraction_numerical': 0.00836850744518701, 'mean_word_length': 3.719720550598326, 'pattern_counts': {'":': 6, '<': 67, '<?xml version=': 0, '>': 75, 'https://': 0, 'lorem ipsum': 0, 'www.': 4, 'xml': 40}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 30, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '3741005', 'n_tokens_mistral': 22942, 'n_tokens_neox': 19424, 'n_words': 4503}