/* To change this template, choose Tools | Templates
 * and open the template in the editor. */
package lu.tudor.santec.gecamed.esante.gui.webservice;

import java.awt.EventQueue;
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.StringReader;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.net.ConnectException;
import java.net.InetAddress;
import java.net.SocketException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.Set;
import java.util.TimeZone;
import java.util.TreeSet;
import java.util.UUID;

import javax.net.ssl.SSLSocket;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import lu.tudor.santec.gecamed.address.ejb.session.beans.AddressManagerBean;
import lu.tudor.santec.gecamed.address.ejb.session.interfaces.AddressManagerInterface;
import lu.tudor.santec.gecamed.core.gui.GECAMedIconNames;
import lu.tudor.santec.gecamed.core.gui.GECAMedModule;
import lu.tudor.santec.gecamed.core.gui.LoginScreen;
import lu.tudor.santec.gecamed.core.gui.MainFrame;
import lu.tudor.santec.gecamed.core.gui.widgets.ErrorDialog;
import lu.tudor.santec.gecamed.core.gui.widgets.GECAMedBaseDialogImpl;
import lu.tudor.santec.gecamed.core.utils.FileUtils;
import lu.tudor.santec.gecamed.core.utils.GECAMedUtils;
import lu.tudor.santec.gecamed.core.utils.ManagerFactory;
import lu.tudor.santec.gecamed.core.utils.SSNChecker;
import lu.tudor.santec.gecamed.esante.ejb.entity.beans.CdaDocument;
import lu.tudor.santec.gecamed.esante.ejb.entity.beans.Dsp;
import lu.tudor.santec.gecamed.esante.ejb.entity.beans.ESanteProperty;
import lu.tudor.santec.gecamed.esante.ejb.session.beans.CDAManagerBean;
import lu.tudor.santec.gecamed.esante.ejb.session.beans.ESanteConfigManagerBean;
import lu.tudor.santec.gecamed.esante.gui.data.Configuration;
import lu.tudor.santec.gecamed.esante.gui.data.FixSizeQueue;
import lu.tudor.santec.gecamed.esante.gui.dialogs.ConnectingDialog;
import lu.tudor.santec.gecamed.esante.gui.dialogs.ESanteDialog;
import lu.tudor.santec.gecamed.esante.gui.dialogs.LoginDialog;
import lu.tudor.santec.gecamed.esante.utils.ESanteUtils;
import lu.tudor.santec.gecamed.esante.utils.exceptions.SendingStoppedException;
import lu.tudor.santec.gecamed.importexport.utils.XPathAPI;
import lu.tudor.santec.gecamed.patient.ejb.entity.beans.Patient;
import lu.tudor.santec.gecamed.patient.gui.PatientManagerModule;
import lu.tudor.santec.gecamed.patient.utils.PatientNameFormatter;
import lu.tudor.santec.i18n.Translatrix;

import org.apache.commons.lang3.StringEscapeUtils;
import org.apache.log4j.Level;
import org.apache.log4j.Logger;
import org.jfree.util.Log;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;

/**
 * @author jens.ferring(at)tudor.lu
 * 
 */
public class SOAPUtilities extends Thread {
	/* ======================================== */
	// CONSTANTS
	/* ======================================== */

	private static final String LOG_DIR = System.getProperty("user.home") + File.separator + LoginScreen.FOLDER + File.separator + "wsdl_logs";

	public static final int DATE_OPTION_CREATED = 0;
	public static final int DATE_OPTION_NOT_BEFORE = 1;
	public static final int DATE_OPTION_NOT_AFTER = 2;

	public static final int MAX_LOGGING_FILES = 50;

	private static final String TEMPLATE_PATH = "/lu/tudor/santec/gecamed/esante/resources/";

	/* ---------------------------------------- */
	// APPLICATION CONSTANTS
	/* ---------------------------------------- */

	public static final String APP_NAME = System.getProperty("APP_NAME"); // "GECAMed";
	public static final String APP_VERSION = System.getProperty("APP_VERSION");

	/** The date format used for ATNA message logging */
	private static final SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ");

	/* ======================================== */
	// MEMBERS
	/* ======================================== */

	private static FixSizeQueue<File> logQueue = null;

	private static HashMap<String, String> wsdlTemplateMap;

	private static HashMap<String, String> countryCodeCache;

	private static Calendar wsdlValidityDate;

	private static DateFormat formatUTC = GECAMedUtils.getDateFormatter("yyyy-MM-dd'T'HH:mm:ss.SS'Z'");

	private static Transformer xmlTransformer;

	private static Logger logger = Logger.getLogger(SOAPUtilities.class.getName());

	private SoapSender sender;
	
	private static int messageLogLevel = WebserviceConstants.MESSAGE_LOGLEVEL_NOT_SET;

	/* ======================================== */
	// CONSTRUCTORS
	/* ======================================== */

	static {
		formatUTC.setTimeZone(TimeZone.getTimeZone("UTC"));
		try {
			xmlTransformer = TransformerFactory.newInstance().newTransformer();
			xmlTransformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
			xmlTransformer.setOutputProperty(OutputKeys.INDENT, "yes");
		} catch (Exception e) {
			logger.log(Level.ERROR, e.getMessage(), e);
		}
	}

	SOAPUtilities(SoapSender sender) {
		this.sender = sender;
	}

	/* ======================================== */
	// STATIC METHODS
	/* ======================================== */

	/**
	 * Get a list of patients matching the given parameter from the eSanté server.
	 * 
	 * @param name
	 *            The patient's surname
	 * @param firstname
	 *            The patient's first name
	 * @param birthdate
	 *            The patient's birthday
	 * @return A list of potential patients
	 * @throws Exception
	 */
	public static ArrayList<Dsp> queryPatient(SoapSender sender, Configuration config) throws Exception {
		// get template
		String t = getTemplateContent(WebserviceConstants.TEMPLATE_INF_12);
		Dsp dsp = sender.getDsp();
		String name = dsp.getFullFamilyName();
		String firstName = dsp.getGivenName();
		String birthDate = dsp.getBirthDateString(Dsp.DSP_DATE_FORMAT);
		String matricule = dsp.getSsn(); // matricule = "1866020206926";
		String dspOid = dsp.getShortDspOid(); // dspOid = "6692220040";

		// **** replace the needed variable
		// t = t.replace(WebserviceConstants.PLACEHOLDER_AUTHENTICATION, Security.getAuthentication(config));
		t = t.replace(WebserviceConstants.PLACEHOLDER_SAML_ASSERTION, Security.getUserSamlAssertion(sender, false));

		// enter the search criteria
		if (!GECAMedUtils.isEmpty(dspOid)) {
			t = t.replace(WebserviceConstants.PLACEHOLDER_NAME, " ");
			t = t.replace(WebserviceConstants.PLACEHOLDER_FIRSTNAME, " ");
			t = t.replace(WebserviceConstants.PLACEHOLDER_BIRTHDATE, "00000000");
			t = replaceInRequestTemplate(t, WebserviceConstants.PLACEHOLDER_PDQ_PARAMETER, WebserviceConstants.PH_TEMP_DSP_ID, dspOid); // , "1.3.182.2.4.2");
		} else if (!GECAMedUtils.isEmpty(matricule) && !SSNChecker.PATTERN_EMPTY_SSN.matcher(matricule).matches()) {
			t = t.replace(WebserviceConstants.PLACEHOLDER_NAME, " ");
			t = t.replace(WebserviceConstants.PLACEHOLDER_FIRSTNAME, " ");
			t = t.replace(WebserviceConstants.PLACEHOLDER_BIRTHDATE, "00000000");
			t = replaceInRequestTemplate(t, WebserviceConstants.PLACEHOLDER_PDQ_PARAMETER, WebserviceConstants.PH_TEMP_SSN, matricule); // , "1.3.182.4.4");
		} else {
			t = t.replace(WebserviceConstants.PLACEHOLDER_NAME, GECAMedUtils.isEmpty(name) ? " " : StringEscapeUtils.escapeXml(name));
			t = t.replace(WebserviceConstants.PLACEHOLDER_FIRSTNAME, GECAMedUtils.isEmpty(firstName) ? " " : StringEscapeUtils.escapeXml(firstName));
			t = t.replace(WebserviceConstants.PLACEHOLDER_BIRTHDATE, GECAMedUtils.isEmpty(birthDate) ? "00000000" : StringEscapeUtils.escapeXml(birthDate));
			t = t.replace(WebserviceConstants.PLACEHOLDER_PDQ_PARAMETER, "");
		}
		// **** end replacing the variables in template

		// **** call the WS
		sender.resetSender("PDQ");
		sender.setWebserviceUrl(config.getServiceUrl(ESanteProperty.PROP_SERVICE_ITI_21));
		try {
			callSOAP(t, sender);
		} catch (WebserviceException e) {
			switch (e.getErrorType()) {
			case WebserviceException.TYPE_PATIENT_NOT_FOUND:
				return null;

			default:
				throw e;
			}
		}

		// extract each patient in an array
		ArrayList<Dsp> dspList = new ArrayList<Dsp>();
		NodeList nodes = (NodeList) ESanteUtils.parseXPath("//*[local-name()='patient']", sender.getResponseMessage(), false);

		for (int i = 0; i < nodes.getLength(); i++) {
			dspList.add(new Dsp(nodes.item(i)));
		}

		return dspList;
	}

	/**
	 * Tries to load the DSP that belongs to the given patient from the DB.<br>
	 * If there is no DSP for this patient the method checks, whether a valid DSP OID (idLuxembourg) is set for this patient and tries to query it from the
	 * eSanté platform.<br>
	 * If no DSP is found, the Luxembourg ID is deleted from this patient and <code>null</code> is returned.
	 * 
	 * @param patient
	 *            The patient to get the DSP of
	 * @param informUser
	 *            A flag, to decide if a dialog will be shown, in case that no DSP is found.
	 * @return The DSP from the database or <code>null</code>, if no DSP can be found and queried.
	 * @throws SendingStoppedException
	 *             If the user cancelled the eSanté login.
	 */
	public static Dsp getValidDsp(Patient patient, boolean informUser) throws SendingStoppedException {
		Dsp dsp = null;

		// load the DSP from the database
		dsp = CDAManagerBean.getInstance().getLinkedDsp(patient.getId());

		if (dsp == null) {
			Configuration config;
			SoapSender sender;
			ArrayList<Dsp> dspList;

			if (GECAMedUtils.isEmpty(patient.getIdLuxembourg()))
				return null;

			try {
				config = LoginDialog.getConfiguration();
				if (config == null)
					// the user didn't log in
					throw new SendingStoppedException(true);

				/* **************************************** */
				// query for a DSP with the given DSP OID
				/* **************************************** */

				// No DSP entry in DB. Patient was probably imported with this Luxembourg ID.
				dsp = new Dsp(patient);
				sender = new SoapSender(dsp, false);
				sender.setDsp(dsp);

				try {
					dspList = SOAPUtilities.queryPatient(sender, config);
				} catch (WebserviceException e) {
					// if (e.getErrorType() == WebserviceException.TYPE_)

					dspList = null;
				}

				if (dspList == null || dspList.size() != 1) {
					// delete the invalid Luxembourg ID from the patient
					patient.setIdLuxembourg(null);
					CDAManagerBean.getInstance().updateDspOid(null, patient.getId(), null);
					PatientManagerModule.getInstance().eSanteIdChanged(null, patient.getId());

					if (informUser) {
						// inform the user about that the DSP ID is not valid
						GECAMedBaseDialogImpl.showMessageDialog(MainFrame.getInstance(),
								Translatrix.getTranslationString("esante.exceptions.invalidDspOid.title"),
								Translatrix.getTranslationString("esante.exceptions.invalidDspOid.message"), GECAMedBaseDialogImpl.OK_BUTTON_MODE,
								GECAMedModule.getBigIcon(GECAMedIconNames.WARNING));
					}

					return null;
				} else {
					// TODO save the queried DSP
					dsp = dspList.get(0);
					dsp.setPatient(patient);
					dsp.setPatientId(patient.getId());
					dsp = CDAManagerBean.getInstance().linkDsp(dsp);
				}
			} catch (SendingStoppedException e) {
				throw e;
			} catch (Exception e) {
				logger.error("Error while trying to log into eSanté health record", e);
				throw new SendingStoppedException(false);
			}
		}

		return dsp;
	}

	/**
	 * Return the content of an eSante web service XML template
	 * 
	 * @param templateName
	 *            The filename of the template without the path
	 * @return The template as XML document string
	 */
	public static String getTemplateContent(String templateName) {
		InputStream is;
		BufferedInputStream bis = null;
		byte[] data;
		StringBuilder xmlBuilder;
		String xml;

		if (wsdlTemplateMap == null || wsdlValidityDate == null || wsdlValidityDate.before(new GregorianCalendar())) {
			wsdlTemplateMap = ESanteConfigManagerBean.getInstance().loadTemplateFiles(WebserviceConstants.TEMPLATE_NAMES);
			wsdlValidityDate = GECAMedUtils.setTime2Zero(new GregorianCalendar());
			wsdlValidityDate.add(Calendar.DAY_OF_YEAR, 1);
			logger.info("Following WSDL templates have been loaded from the server and cached on client:\n"
					+ GECAMedUtils.print(wsdlTemplateMap.keySet(), ", ", false));
		}

		xml = wsdlTemplateMap.get(templateName);
		if (xml != null) {
			logger.debug("WSDL template for \"" + templateName + "\" loaded from cache");
			return xml;
		}

		// data = ESanteConfigManagerBean.getInstance().loadTemplateFile(templateName);
		// if (data != null)
		// {
		// try
		// {
		// xml = new String(data, "UTF8");
		// logger.info("WSDL template for \""+templateName+"\" loaded from server");
		// }
		// catch (UnsupportedEncodingException e1)
		// {
		// logger.error("Wrong encoding used to load template file");
		// }
		// }
		// else
		{
			try {
				is = SOAPUtilities.class.getResourceAsStream(TEMPLATE_PATH + "templates/" + templateName);
				xmlBuilder = new StringBuilder();

				if (is == null) {
					logger.warn("Template \"" + templateName + "\" not found");
					return null;
				}

				bis = new BufferedInputStream(is);
				data = new byte[is.available()];

				while (bis.read(data) > 0) {
					xmlBuilder.append(new String(data));
				}
				xml = xmlBuilder.toString();
				logger.info("WSDL template for \"" + templateName + "\" loaded from Client-JAR");
			} catch (Exception e) {
				logger.error(e.getMessage(), e);
			} finally {
				if (bis != null) {
					try {
						bis.close();
					} catch (IOException e) {
						logger.error(e.getMessage(), e);
					}
				}
			}
		}

		// cache the template in the client cache
		wsdlTemplateMap.put(templateName, xml);
		logger.info("WSDL template for \"" + templateName + "\" cached");

		return xml;
	}

	public static void clearTemplatesCache() {
		wsdlTemplateMap = null;
		logger.info("WSDL template cache cleared.");
	}

	/**
	 * Logs an Audit Trail an Node Authentication (ATNA) message. Necessary headers are added automatically. The message will be logged locally. However, if an
	 * url of an atna server has been configured in the DB, the message will also be sent to the server.<br/>
	 * <br/>
	 * <i>The actual message logging is suppressed if message logging has been set to {@link WebserviceConstants#MESSAGE_LOGLEVEL_NONE none</i>
	 * 
	 * @param atnaMessage
	 *            The atna message that should be sent to the server
	 * @throws Exception
	 *             If an invalid url has been stored in db
	 */
	public static void sendAtnaMessage(String atnaMessage, String type) throws Exception {

		// if atna logging is disabled, do nothing
		if(getMessageLogLevel()<WebserviceConstants.MESSAGE_LOGLEVEL_ATNA){
			return;
		}
		
		StringBuilder msg = new StringBuilder();
		String timestamp = df.format(new Date());

		atnaMessage = atnaMessage.replace(WebserviceConstants.PLACEHOLDER_SUBMISSION_TIME,
				timestamp.substring(0, timestamp.length() - 2) + ":" + timestamp.substring(timestamp.length() - 2));
		msg.append("<85>1 ").append(timestamp.substring(0, timestamp.length() - 2) + ":" + timestamp.substring(timestamp.length() - 2)).append(" ")
				.append(InetAddress.getLocalHost().getCanonicalHostName()).append(" GECAMed 123324 IHE+DICOM - ").append(atnaMessage);

		// add message length
		int messageLength = msg.toString().getBytes("UTF-8").length;
		msg.insert(0, Integer.toString(messageLength) + " ");

		// only send the message to the server, if an atna service has been configured in the db
		if (LoginDialog.getConfiguration().isAtnaEnabled()) {
			// get pointer to socket
			SSLSocket sslSocket = LoginDialog.getConfiguration().getAtnaSocket();
			// it's output stream
			OutputStream sout = sslSocket.getOutputStream();
			// and put it on the wire
			sout.write(msg.toString().getBytes("UTF8"));
			sout.flush();

			sout.close();
			sslSocket.close();
		}
		// log the message to file system
		log(msg.toString(), "_ATNA_" + type);
	}

	/**
	 * Sends the mime message to the server. http metadata, message content and attachment (if applicable) are taken into account. MTOM will be used but no
	 * message chunking (so far).
	 * 
	 * @param sender
	 *            The sender object containing all elements for composing the request with metadata and attachment
	 * @throws IOException
	 *             Check message for details
	 * @throws SendingStoppedException
	 *             In case of user interruption
	 */
	public void sendMimeMessage(SoapSender sender) throws IOException, SendingStoppedException {

		DataOutputStream binaryOut = null;
		ByteArrayOutputStream messageContent = null;
		try {
			sender.checkThreadStopped();

			// output stream for sending request to the webservice
			try {
				binaryOut = sender.createDataOutputStream();
			} catch (IOException ioe) {
				ESanteDialog.showMessageDialog(
						MainFrame.getInstance(),
						Translatrix.getTranslationString("esante.exceptions.unableToContactWebservice.title"),
						Translatrix.getTranslationString("esante.exceptions.unableToContactWebservice.message", new String[] { sender.getPath()}), ESanteDialog.OK_BUTTON_MODE,
						GECAMedModule.getScaledIcon(GECAMedIconNames.WARNING, 32));
				
				throw new SendingStoppedException(false);
			}
			// log binary content also to a file
			messageContent = new ByteArrayOutputStream();

			if (sender.sendAsMimeMultipart()) {
				// generate a uuid for mime boundary
				String mimeUuid = "uuid_" + UUID.randomUUID();

				// add a mime section for the message.
				messageContent.write(("--" + mimeUuid).getBytes("UTF-8"));
				messageContent.write("Content-Type: application/xop+xml; charset=utf-8; type=\"application/soap+xml;\"\r\n".getBytes("UTF-8"));
				messageContent.write("Content-Transfer-Encoding: binary\r\n".getBytes("UTF-8"));
				messageContent.write("Content-ID: <root.message@cxf.apache.org>\r\n\r\n".getBytes("UTF-8"));
				messageContent.write(sender.getRequest().getBytes("UTF-8"));

				// check if an attachment has to be added
				if (sender.hasAttachment()) {
					// add an additional mime section containing the attachment (currently only one attachment is supported)
					messageContent.write(("\r\n--" + mimeUuid + "\r\n").getBytes("UTF-8"));
					messageContent.write("Content-Type: application/pdf\r\n".getBytes("UTF-8"));
					messageContent.write("Content-Transfer-Encoding: binary\r\n".getBytes("UTF-8"));
					messageContent.write("Content-ID: <Attachment1>\r\n\r\n".getBytes("UTF-8"));
					// now add the binary attachment to the content
					messageContent.write(sender.getAttachment());
				}
				// end the message content envelope by setting the final mime tag
				messageContent.write(("--" + mimeUuid + "--\r\n").getBytes("UTF-8"));
				// convert the message content to a byte array
				byte[] messageContentArray = messageContent.toByteArray();

				// the whole request will first be written to a byte array for being able to log the raw request as it will be send via the wire
				ByteArrayOutputStream requestWithMeta = new ByteArrayOutputStream();

				// now create the general mime multipart header
				requestWithMeta.write(("POST " + sender.getPath() + " HTTP/1.0\r\n").getBytes("UTF-8"));
				requestWithMeta.write(("Host: " + sender.getHostname() + "\r\n").getBytes("UTF-8"));
				requestWithMeta.write(("Content-Length: " + messageContentArray.length + "\r\n").getBytes("UTF-8"));
				requestWithMeta
						.write(("Content-Type: multipart/related; type=\"application/xop+xml\"; start=\"<root.message@cxf.apache.org>\"; start-info=\"application/soap+xml;charset=utf-8\"; boundary="
								+ mimeUuid + "\r\n").getBytes("UTF-8"));
				requestWithMeta.write(("User-Agent: Axis2\r\n\r\n").getBytes("UTF-8"));
				// and add the message content
				requestWithMeta.write(messageContentArray);
				requestWithMeta.flush();
				// now write everything to the socket
				binaryOut.write(requestWithMeta.toByteArray());
				binaryOut.flush();
				
				String prefix = GECAMedUtils.getDateFormatter(GECAMedUtils.DATE_FORMAT_FILE_TIME).format(new Date());;
				// and write it to the log file
				switch (getMessageLogLevel()) {
				case  WebserviceConstants.MESSAGE_LOGLEVEL_FULL:
					log(sender.getAttachment(), prefix + "_REQUEST_CDA-DOCUMENT_" + sender.getLogSuffix(), "xml");
					log(sender.getAttachmentPayload(), prefix + "_REQUEST_CDA-PAYLOAD_" + sender.getLogSuffix(), "pdf");

				case WebserviceConstants.MESSAGE_LOGLEVEL_MESSAGE:
					log(requestWithMeta.toByteArray(), prefix + "_REQUEST_" + sender.getLogSuffix(), "xml");
					break;

				default:
					// nothing will be logged
					break;
				}


			} else {
				// assemble the soap request
				// if(sender.isProxyUsed()){
				// messageContent.write(("CONNECT " + sender.getHostname() + ":" + sender.getPort() + " HTTP/1.0\r\n\r\n").getBytes("UTF-8"));

				// Add Proxy Authorization if proxyUser and proxyPass is set
				// try {
				// String proxyUserPass = String.format("%s:%s",
				// System.getProperty("http.proxyUser"),
				// System.getProperty("http.proxyPass"));
				//
				// proxyConnect.concat(" HTTP/1.0\nProxy-Authorization:Basic "
				// + Base64.encode(proxyUserPass.getBytes()));
				// } catch (Exception e) {
				// } finally {
				// proxyConnect.concat("\n\n");
				// }
				// } else{

				messageContent.write(("POST " + sender.getPath() + " HTTP/1.0\r\n").getBytes("UTF-8"));
				// }
				messageContent.write(("Host: " + sender.getHostname() + ":" + sender.getPort() + "\r\n").getBytes("UTF-8"));
				messageContent.write(("Content-Length: " + sender.getRequest().length() + "\r\n").getBytes("UTF-8"));
				messageContent.write(("Content-Type: application/soap+xml; charset=\"utf-8\"\r\n").getBytes("UTF-8"));
				messageContent.write(("User-Agent: Axis2\r\n\r\n").getBytes("UTF-8"));
				messageContent.write(sender.getRequest().getBytes("UTF-8"));
				// now write everything to the socket
				binaryOut.write(messageContent.toByteArray());
				binaryOut.flush();
				// and write it to the log file
				
				switch (getMessageLogLevel()) {
				case  WebserviceConstants.MESSAGE_LOGLEVEL_FULL:
				case WebserviceConstants.MESSAGE_LOGLEVEL_MESSAGE:
					log(messageContent.toByteArray(), "_REQUEST_" + sender.getLogSuffix());
					break;
				default:
					// no message logging for other levels
					break;
				}
			}

			sender.checkThreadStopped();

		} catch (IOException e) {
			if (binaryOut != null) {
				binaryOut.close();
			}
			if (messageContent != null) {
				messageContent.close();
			}
			if (e instanceof ConnectException) {
				ESanteDialog.showMessageDialog(MainFrame.getInstance(), Translatrix.getTranslationString("Connection Error"),
						"Connection was refused by service " + sender.getHostname() + ":" + sender.getPort() + sender.getPath(), ESanteDialog.OK_BUTTON_MODE,
						GECAMedModule.getBigIcon(GECAMedIconNames.WARNING));
			}
			throw e;
		} 
		
		try {
			// now read the webservice response
			DataInputStream dis = sender.createDataInputStream();
			byte[] buffer = new byte[2048];
			ByteArrayOutputStream responseMessage = new ByteArrayOutputStream();
			int bytesRead = -1;

			// loop till whole stream has been read
			while ((bytesRead = dis.read(buffer)) != -1) {
				sender.checkThreadStopped();

				// write the next portion of received bytes to the array
				responseMessage.write(buffer, 0, bytesRead);
			}

			// and log it to the file system
			switch (getMessageLogLevel()) {
			case  WebserviceConstants.MESSAGE_LOGLEVEL_FULL:
			case WebserviceConstants.MESSAGE_LOGLEVEL_MESSAGE:
				log(responseMessage.toByteArray(), "_RESPONSE_" + sender.getLogSuffix());
				break;
			default:
				// no message logging for other levels
				break;
			}
			
			// store the response in the sender object
			sender.setResponse(responseMessage.toByteArray());
		} catch(RuntimeException re){
			sender.setException(re);
		} catch (Throwable e) {
			// this may happen, if the thread is stopped and is no problem in that case
			// if (!sender.isStopped())
			logger.error("Error while trying to read response", e);
		} finally {
			sender.closeSocket();
		}
	}

	/**
	 * Determines the information that should be written to disk regarding the message transfer.<br/>
	 * <br/>
	 * This might include:
	 * <ul>
	 * <li>The complete request <i>(levels normal, full)</i></li>
	 * <li>The CDA document extracted from the request <i>(level full)</i></li>
	 * <li>The PDF/a document extracted from the CDA document <i>(level full)</i></li>
	 * </ul>
	 * 
	 * @return One of the following three log level:<br/>
	 *         <ul>
	 *         <li>{@link WebserviceConstants#MESSAGE_LOGLEVEL_FULL full upload}</li>
	 *         <li>{@link WebserviceConstants#MESSAGE_LOGLEVEL_MESSAGE request}</li>
	 *         <li>{@link WebserviceConstants#MESSAGE_LOGLEVEL_NONE nothing}</li>
	 *         </ul>
	 */
	private static int getMessageLogLevel() {

		if (SOAPUtilities.messageLogLevel == WebserviceConstants.MESSAGE_LOGLEVEL_NOT_SET) {
			String level = ESanteConfigManagerBean.getInstance().getESantePropertyValue(ESanteProperty.PROP_MESSAGE_LOGLEVEL);

			try {
				// check if a valid loglevel has been configured
				SOAPUtilities.messageLogLevel = Integer.parseInt(level);
			} catch (NumberFormatException e) {
			}
			// it seems like the parameter is missing in the database. Thus deactivate message logging
			if ((SOAPUtilities.messageLogLevel == WebserviceConstants.MESSAGE_LOGLEVEL_NOT_SET)
					|| (SOAPUtilities.messageLogLevel < WebserviceConstants.MESSAGE_LOGLEVEL_NONE)) {
				SOAPUtilities.messageLogLevel = WebserviceConstants.MESSAGE_LOGLEVEL_NONE;
			}
		}

		return SOAPUtilities.messageLogLevel;
	}
	
	/**
	 * Sets the log level for the message logging
	 * @param messageLogLevel One of the following three log level:<br/>
	 *         <ul>
	 *         <li>{@link WebserviceConstants#MESSAGE_LOGLEVEL_FULL full upload}</li>
	 *         <li>{@link WebserviceConstants#MESSAGE_LOGLEVEL_MESSAGE request}</li>
	 *         <li>{@link WebserviceConstants#MESSAGE_LOGLEVEL_NONE nothing}</li>
	 *         </ul>
	 */
	public static void setMessageLogLevel(int messageLogLevel) {
		SOAPUtilities.messageLogLevel = messageLogLevel;
	}
	
	/**
	 * Search a byte array within another one
	 * 
	 * @param data
	 *            The byte array within which a pattern should be located
	 * @param pattern
	 *            The pattern that should be found
	 * @param position
	 *            The starting position
	 * @return The index of the pattern
	 */
	public static int indexOf(byte[] data, byte[] pattern, int position) {
		int[] failure = computeFailure(pattern);
		int j = 0;
		if (position < 0) {
			position = 0;
		}
		for (int i = position; i < data.length; i++) {
			while (j > 0 && pattern[j] != data[i]) {
				j = failure[j - 1];
			}

			if (pattern[j] == data[i]) {
				j++;
			}
			if (j == pattern.length) {
				return i - pattern.length + 1;
			}
		}
		return -1;
	}

	/**
	 * Computes the failure function using a boot-strapping process, where the pattern is matched against itself.
	 */
	private static int[] computeFailure(byte[] pattern) {
		int[] failure = new int[pattern.length];
		int j = 0;
		for (int i = 1; i < pattern.length; i++) {
			while (j > 0 && pattern[j] != pattern[i]) {
				j = failure[j - 1];
			}
			if (pattern[j] == pattern[i]) {
				j++;
			}
			failure[i] = j;
		}
		return failure;
	}

	/**
	 * JUST FOR DEBUGGING !!!
	 * 
	 * @param message
	 *            What to write
	 * @param prefix
	 *            How to mark the file
	 */
	public static void log(String message, String name) {
		try {
			
			log(message.getBytes("UTF-8"), name);
		} catch (UnsupportedEncodingException e) {
			Log.warn("UTF8 encoding does not seem to be supported on this system.");
		} catch (NullPointerException npe) {
			Log.error("The message that should be logged, did not contain any data.");
		}
	}

	
	/**
	 * Logs the content of a message to the file system. The logfile will always be of type xml
	 * 
	 * @param message
	 *            The message that should be logged
	 * @param name
	 *            The name of the logfile (w/o extension)
	 * @return A prefix (consisting of the exact date of the logfile creation) that can be used to prefix related files
	 */
	public static String log(byte[] message, String name) {
		String prefix = GECAMedUtils.getDateFormatter(GECAMedUtils.DATE_FORMAT_FILE_TIME).format(new Date());
		log(message, prefix + name, "xml");

		return prefix;
	}

	/**
	 * JUST FOR DEBUGGING !!!
	 * 
	 * @param message
	 *            What to write
	 * @param name
	 *            How to mark the file
	 */
	public static void log(byte[] message, String name, String extension) {
		if (name == null || message == null)
			return;

		try {
			File logDir = new File(LOG_DIR);

			if (!logDir.exists() && !logDir.mkdirs()) {
				logger.warn("Couldn't log WSDL transaction file, because folder \"" + LOG_DIR + "\" couldn't be created.");
				return;
			}

			File logFile = new File(logDir, name + "."+extension);

			if (!logFile.exists())
				logFile.createNewFile();

			FileUtils.writeFile(message, logFile);

			if (logQueue == null) {
				logQueue = new FixSizeQueue<File>(MAX_LOGGING_FILES);
				File[] files = logFile.getParentFile().listFiles(new FilenameFilter() {
					public boolean accept(File dir, String name) {
						return name.endsWith(".xml");
					}
				});
				Set<File> sortedFiles = new TreeSet<File>();

				for (File f : files)
					sortedFiles.add(f);

				for (File f : sortedFiles) {
					f = logQueue.push(f);
					if (f != null)
						f.delete();
				}
			}
			File toDelete = logQueue.push(logFile);
			if (toDelete != null)
				toDelete.delete();

			logger.info("Message logged in file \"" + logFile.getAbsolutePath() + "\"");
		} catch (IOException e) {
			Log.warn("Problem while trying to log WSDL transaction");
		}
	}

	/**
	 * Performs a SOAP web service call to the given URL with the given XML
	 * 
	 * @param xmlData
	 *            xmlData The request as XML to send with the web service
	 * @param sender
	 *            The URL to which the request is sent
	 * @throws Exception
	 *             See exception details
	 */
	public static void callSOAP(String xmlData, SoapSender sender) throws Exception {
		callSOAP(xmlData, sender, false);
	}

	/**
	 * Performs a SOAP web service call to the given URL with the given XML
	 * 
	 * @param xmlData
	 *            xmlData The request as XML to send with the web service
	 * @param sender
	 *            The URL to which the request is sent
	 * @param sendAsMultipart
	 *            IF this flag is set, the request will be sent as mime multipart message otherwise as normal SOAP request. However, if the sender contains an
	 *            attachment object, this flag is automatically set.
	 * @throws Exception
	 *             See exception details
	 */
	public static void callSOAP(String xmlData, SoapSender sender, boolean sendAsMultipart) throws Exception {

		sender.writeRequest(xmlData);
		sender.setSendAsMimeMultipart(sendAsMultipart);
		sender.checkThreadStopped();
		startSendThread(sender);

		if (sender.isException()) {
			throw sender.getException();
		}
	}

	public static String getXMLFragment(String fromNode, String xmldata) {
		int startPos;
		int endPos;

		startPos = xmldata.indexOf("<" + fromNode);
		endPos = xmldata.indexOf("</" + fromNode);
		endPos += ("</" + fromNode + ">").length();

		return xmldata.substring(startPos, endPos);
	}

	public static String getFormattedDate(int option) {
		Date date;
		switch (option) {
		case DATE_OPTION_CREATED:
		case DATE_OPTION_NOT_BEFORE:
			date = new Date();
			break;

		case DATE_OPTION_NOT_AFTER:
			date = new Date();
			date.setTime(date.getTime() + (1 * 60 * 60 * 1000)); // add 1 hour
			break;
		default:
			return null;
		}

		return formatUTC.format(date);
	}

	public static void startSendThread(SoapSender sender) throws Exception {
		try {
			sender.start();

			if (!sender.runInBackground()) {
				if (((ConnectingDialog) sender.getDialog()).showCenteredDialog() != ConnectingDialog.OK_OPTION) {
					sender.stop();

					try {
						// wait and give the sender a chance to stop the thread
						sleep(500);
					} catch (InterruptedException e) {
						// simply go on
					}

					sender.setSuccess(false);
					throw new SendingStoppedException(true);
				}
			}
		} catch (ClassCastException e) {
			logger.error("This method shouldn't be called from the server!");
			sender.setSuccess(false);
		}
		sender.setSuccess(true);
	}

	@Override
	public void run() {
		try {
			try {
				sendMimeMessage(sender);
			} catch (SocketException e) {
				if (sender.runInBackground() || sender.getDialog().isVisible()) {
					// logger.log(Level.ERROR, e.getMessage(), e);
					sender.setException(e);
				}
				// ELSE: This occurred, because the connection to the eSante server was cancelled by the user
			} catch (SendingStoppedException e) {
				// sending was cancelled by user
				logger.info(e.getMessage());
				return;
			} catch (Exception e) {
				// logger.log(Level.ERROR, e.getMessage(), e);
				sender.setException(e);
				sender.setResponse(null);
			} finally {
				if (!sender.runInBackground()) {
					EventQueue.invokeLater(new Runnable() {
						public void run() {
							// if (sender.successfull())
							// {
							sender.getDialog().okActionCalled();
							// }
							// else
							// {
							// sender.getDialog().cancelActionCalled();
							// }
						}
					});
				}
			}
		} catch (ClassCastException e) {
			logger.error("This method shouldn't be called from the server!");
		}
	}

	public static Document createDocument(String xmlData) throws Exception {
		if (xmlData == null)
			return null;

		return DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(new InputSource(new StringReader(xmlData)));
	}

	public static String nodeToString(Node node) {
		try {
			StringWriter writer = new StringWriter();
			xmlTransformer.transform(new DOMSource(node), new StreamResult(writer));

			return writer.toString();
		} catch (Exception e) {
			logger.log(Level.ERROR, e.getMessage(), e);
			return null;
		}
	}

	/**
	 * Displays an error dialog regarding inconsistent time synchronization between client and eSanté DPS.
	 * 
	 * @param gecamedPatientId
	 *            The id of the patient within GECAMed
	 */
	public static void asynchClientTimeError(int gecamedPatientId) {
		String errorDetails = new StringBuilder(500).append(
				"\nThe client time is out of sync with the eSanté server.\n\n"
						+ "Please adjust your system time or preferable enable the synchronization with a network time server.\n"
						+ "You will find further information about how to synchronize your system with a time-server under "
						+ "http://www.gecamed.lu/faq/time_synchronization.").toString();

		// indicate error to the user
		ErrorDialog.showESanteErrorDialog(MainFrame.getInstance(), "The client time is out of sync with the eSanté server.", errorDetails, gecamedPatientId,
				"TimeOutOfSync");
	}

	/**
	 * Replaces the given request template and returns the replaced version.<br>
	 * <br>
	 * <b>Example:</b> <i>The call:</i> replaceInRequestTemplate("replace this PLACEHOLDER", "PLACEHOLDER", "with $0 and $1", "this", "that") <i>returns:</i>
	 * "replace this with this and that"
	 * 
	 * @param requestTemplate
	 *            The template to replace
	 * @param placeholder
	 *            The placeholder to search for in the requestTemplate
	 * @param replacementTemplate
	 *            The template that replaces the placeholders. If this parameter is null or an empty string, the placeholder will be simply removed from the
	 *            request template.
	 * @param replacementParameter
	 *            Parameters that are replaced in the replacement. The replacement values will be xml encoded by the function
	 * @return The altered template
	 */
	public static String replaceInRequestTemplate(String requestTemplate, String placeholder, String replacementTemplate, String... replacementParameter) {
		if ((replacementTemplate != null) && (replacementTemplate.length() > 0)) {
			for (int index = 0; index < replacementParameter.length; index++) {
				replacementTemplate = replacementTemplate.replace(("$" + index), StringEscapeUtils.escapeXml(replacementParameter[index]));
			}
		} else {
			replacementTemplate = "";
		}
		return requestTemplate.replace(placeholder, replacementTemplate);
	}

	public static String getCountryCodeISO3166Alpha3(String country) {
		String code;

		country = PatientNameFormatter.unaccent(country).trim().toLowerCase().replaceAll("[ -]", "_").replaceAll("[^a-z_]", "");

		if (countryCodeCache == null)
			countryCodeCache = new HashMap<String, String>();
		code = countryCodeCache.get(country);

		if (code == null) {
			code = ((AddressManagerInterface) ManagerFactory.getRemote(AddressManagerBean.class)).getAlpha3CodeFromCountry(country);

			if (code == null) {
				// TODO: show a selection dialog for this country
				return null;
			}
			countryCodeCache.put(country, code);
		}

		return code;
	}
	
	/**
	 * Extracts the uid of the latest version of a document from a ITI-18 (get related documents) response
	 * 
	 * @param sender
	 *            The sender object containing the response from the webservice call
	 * @return The uid of the document or null if none could be found or an error occurred
	 */
	public static String extractLatestVersion(SoapSender sender) {
		Document response;
		Node latestVersion;
		String documentUid = null;
		try {
			response = SOAPUtilities.createDocument(sender.getResponseMessage());
			// first select the node representing the latest version of the document (the one with status 'approved')
			latestVersion = XPathAPI.selectSingleNode(response,
					"//*[local-name()='ExtrinsicObject' and @*[local-name()='status']='urn:oasis:names:tc:ebxml-regrep:StatusType:Approved']", false);
			// and get the uid of this document
			documentUid = XPathAPI.selectNodeValue(latestVersion, CdaDocument.XPATH_UID, false);
		} catch (SendingStoppedException e) {
			return null;
		} catch (Exception e) {
			logger.error("", e);
			return null;
		}

		return documentUid;
	}
}