/*
 Copyright (c) 2009, The JAP-Team, JonDos GmbH
 All rights reserved.
 Redistribution and use in source and binary forms, with or without modification,
 are permitted provided that the following conditions are met:

  - Redistributions of source code must retain the above copyright notice,
 this list of conditions and the following disclaimer.

  - Redistributions in binary form must reproduce the above copyright notice,
 this list of conditions and the following disclaimer in the documentation and/or
 other materials provided with the distribution.

  - Neither the name of the University of Technology Dresden, Germany, nor the name of
 the JonDos GmbH, nor the names of their contributors may be used to endorse or
 promote products derived from this software without specific prior written permission.


 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS'' AND ANY EXPRESS
 OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
 AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS
 BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
 OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
 IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE
 */
package jondo;

import java.io.File;
import java.io.IOException;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.SocketException;
import java.net.URL;

import java.security.SecureRandom;
import java.sql.Timestamp;
import java.util.Arrays;
import java.util.Date;
import java.util.Enumeration;
import java.util.Locale;
import java.util.Observable;
import java.util.Observer;
import java.util.Vector;

import jondo.interfaces.IAntiCensorshipForwarding;
import jondo.interfaces.IConfiguration;
import jondo.interfaces.IConfigurationListener;
import jondo.interfaces.IForwardingListener;
import jondo.interfaces.IForwardingServerStatistics;
import jondo.interfaces.IPaymentEventListener;
import jondo.interfaces.IServiceEventListener;

//import org.apache.commons.logging.Log;
import org.w3c.dom.Document;
import org.w3c.dom.Element;

import anon.AnonServerDescription;
import anon.AnonServiceEventListener;
import anon.ErrorCodes;
import anon.crypto.Util;
import anon.error.AccountEmptyException;
import anon.error.INotRecoverableException;
import anon.error.ServiceSignatureException;
import anon.pay.BIConnection;
import anon.pay.IPaymentListener;
import anon.pay.PayAccount;
import anon.pay.PayAccountsFile;
import anon.pay.PaymentInstanceDBEntry;
import anon.pay.PayAccountsFile.AccountAlreadyExistingException;
import anon.pay.xml.XMLAccountInfo;
import anon.pay.xml.XMLErrorMessage;
import anon.proxy.AnonProxy;
import anon.proxy.BrowserIdentification;
import anon.proxy.DirectProxy;
import anon.proxy.JonDoFoxHeader;
import anon.proxy.JonDonymXHeaders;
import anon.proxy.DirectProxy.RequestInfo;
import anon.infoservice.AbstractDatabaseEntry;
import anon.infoservice.BlacklistedCascadeIDEntry;
import anon.infoservice.Database;
import anon.infoservice.IDistributable;
import anon.infoservice.IDistributor;
import anon.infoservice.IMutableProxyInterface;
import anon.infoservice.IProxyInterfaceGetter;
import anon.infoservice.ImmutableProxyInterface;
import anon.infoservice.InfoServiceDBEntry;
import anon.infoservice.InfoServiceHolder;
import anon.infoservice.MixCascade;
import anon.infoservice.ProxyInterface;
import anon.infoservice.StatusInfo;
import anon.infoservice.update.AbstractMixCascadeUpdater;
import anon.infoservice.update.AccountUpdater;
import anon.infoservice.update.InfoServiceUpdater;
import anon.infoservice.update.PaymentInstanceUpdater;
import anon.infoservice.update.PerformanceInfoUpdater;
import anon.infoservice.update.ServiceExitAddressUpdater;
import logging.AbstractLog4jLog;
import logging.ILog;
import logging.LogHolder;
import logging.LogType;
import logging.LogLevel;
import anon.client.AbstractAutoSwitchedMixCascadeContainer;
import anon.client.DummyTrafficControlChannel;
import anon.error.TrustException;
import anon.client.TrustModel;
import anon.client.TrustModel.TrustAttribute;
import anon.client.crypto.KeyPool;
import anon.crypto.AsymmetricCryptoKeyPair;
import anon.crypto.JAPCertificate;
import anon.crypto.SignatureVerifier;
import anon.error.AnonServiceException;
import anon.forward.server.ForwardSchedulerStatistics;
import anon.forward.server.ForwardServerManager;
import anon.forward.server.ServerSocketPropagandist;
import anon.util.AbstractMemorizingPasswordReader;
import anon.util.ClassUtil;
import anon.util.IMiscPasswordReader;
import anon.util.JAPMessages;
import anon.util.SocketGuard;
import anon.util.Updater;
import anon.util.XMLParseException;
import anon.util.XMLUtil;
import anon.util.Updater.ObservableInfo;
import anon.util.captcha.ICaptchaSender;
import anon.util.captcha.IImageEncodedCaptcha;

/**
 * The Controller is the central interface for controlling the JonDonym
 * software. It can be used stand-alone in your own Java application.
 * jondo.console.JonDoConsole is a reference implementation for using the
 * Controller interface. <br>
 * <p>
 * FOR DEVELOPERS OF THE CONTROLLER ITSELF: If you change the Controller
 * methods, keep in mind that no references to anonlib classes (anon.*) are
 * allowed for the Controller interface. These references should be hidden from
 * a user of the Controller, so that changes inside the anonlib do not affect
 * external development projects. For the same reason, you should not change
 * existing Controller methods if there is not clear benefit. (you might keep
 * the old method, and declare it as deprecated).
 * </p>
 * <p>
 * The only exceptions for Controller interface references to the anonlib are:
 * <ul>
 * <li>anon.error</li>
 * <li>anon.crypto</li>
 * <li>logging (the instances of ILog may be used for the init method)</li>
 * </ul>
 * </p>
 * But remember: also these references should only be exceptions. Do not use
 * these references in the Controller interface if they are not absolutely
 * needed for implementing a good code structure. Create wrapper classes around
 * anonlib objects wherever possible.
 * 
 * TODO make trust model / filter editable, that is: show selection of editable
 * attributes
 * 
 * @author Rolf Wendolsky
 */
public class Controller
	{
		public static final String VERSION = "00.00.169";

		public static enum EnumAnonymousProxyConnection {
			ANONYMOUS_PROXY_CONNECTION_DEFAULT, ANONYMOUS_PROXY_CONNECTION_ALWAYS, ANONYMOUS_PROXY_CONNECTION_NEVER
		};

		private static final MixServiceInfo INITIAL_DUMMY_SERVICE = new MixServiceInfo(
				AbstractAutoSwitchedMixCascadeContainer.INITIAL_DUMMY_SERVICE);

		public static final String XML_ELEMENT_NAME = "JonDoController";

		private static final String MESSAGES = "JAPMessages";

		private static final String XML_ATTR_LANGUAGE = "languageLocale";
		private static final String XML_ATTR_COUNTRY = "countryLocale";
		private static final String XML_ATTR_LOG_DETAIL = "logDetail";
		private static final String XML_ATTR_LOG_LEVEL = "logLevel";
		private static final String XML_ATTR_LOG_TYPES = "logTypes";
		private static final String XML_ATTR_AUTOSWITCH = "autoswitch";
		private static final String XML_ATTR_AUTO_UPDATE = "autoupdate";

		private static final String XML_ATTR_DEFAULT_CASCADE = "defaultCascade";
		private static final String XML_ATTR_HTTP_FILTER = "httpfilter";

		private static final String XML_ATTR_PAYMENT_ANONYMOUS_PROXY_CONNECTION = "paymentAnonymousProxyConnection";
		private static final String XML_ATTR_INFOSERVICE_ANONYMOUS_PROXY_CONNECTION = "infoserviceAnonymousProxyConnection";

		private static final PaymentInstance PI_JONDOS = new PaymentInstance("ECD365B98453B316B210B55546731F52DA445F40");
		// private static final String PI_TEST =
		// "3ADE1713CAFA6470FADCC3395415F8950C42CD2E";

		private static final long TIMEOUT_RECHARGE = 1000 * 60 * 60 * 24 * 14; // 14
																																						// days

		private static final String MSG_NO_CHARGED_ACCOUNT = Controller.class.getName() + ".noChargedAccount";

		private static Vector<Updater> ms_vecUpdaters;
		private static AccountUpdater ms_accountUpdater;
		private static AccountUpdater ms_accountUpdaterInternal;
		private static ServerSocket ms_socketListener;
		private static ServerSocket ms_socketListenerTwo;
		private static boolean m_bIsVirtualBoxListener = false;
		private static DirectProxy ms_directProxy;
		private static AnonProxy ms_jondonymProxy;
		private static AutoSwitchedMixCascadeContainer ms_serviceContainer;
		private static PaymentInstance ms_currentPIID = PI_JONDOS;
		private static IConfiguration ms_configuration;
		private static final Logger ms_logger = new Logger();

		private static MixCascade m_cascadeDefault;

		private static char[] ms_accountPassword = null;
		private static PayAccount ms_currentlyCreatedAccount;
		private static ProxyInterface ms_anonymousProxyInterface;
		private static EnumAnonymousProxyConnection ms_iPaymentAnonymousProxyConnection = EnumAnonymousProxyConnection.ANONYMOUS_PROXY_CONNECTION_DEFAULT;
		private static EnumAnonymousProxyConnection ms_iInfoServiceAnonymousProxyConnection = EnumAnonymousProxyConnection.ANONYMOUS_PROXY_CONNECTION_DEFAULT;

		private static final AbstractMemorizingPasswordReader ms_passwordReaderDummy = new AbstractMemorizingPasswordReader()
		{
			protected void initPasswordDialog(Object a_message)
				{
				}

			protected String readPassword()
				{
					return null;
				}

			protected boolean askForCancel()
				{
					return true;
				}
		};

		private static final IMutableProxyInterface ms_proxyInterface = new IMutableProxyInterface()
		{
			private IProxyInterfaceGetter m_proxyGetter = new IProxyInterfaceGetter()
			{
				public ImmutableProxyInterface getProxyInterface()
					{
						SimpleProxyInterface proxyInterface = ms_configuration.getProxyInterface();
						if (proxyInterface != null)
							{
								return proxyInterface.getProxyInterface();
							}
						return null;
					}
			};

			public IProxyInterfaceGetter getProxyInterface(boolean a_bAnonInterface)
				{
					return m_proxyGetter;
				}
		};

		private static final AnonymousProxyConnectionMutableProxyInterface ms_proxyInfoService = new AnonymousProxyConnectionMutableProxyInterface(
				new IAnonymousProxyConnectionUserDefinition()
				{
					public EnumAnonymousProxyConnection getUserDefinition()
						{
							return ms_iInfoServiceAnonymousProxyConnection;
						}
				});

		private static final AnonymousProxyConnectionMutableProxyInterface ms_proxyPayment = new AnonymousProxyConnectionMutableProxyInterface(
				new IAnonymousProxyConnectionUserDefinition()
				{
					public EnumAnonymousProxyConnection getUserDefinition()
						{
							return ms_iPaymentAnonymousProxyConnection;
						}
				});

		private static Thread ms_starterThread;
		private static RunnableStarter ms_starter;
		private static final Object SYNC_STARTER = new Object();
		private static final Object SYNC_HTTP_FILTER = new Object();
		private static final Object SYNC_SERVICE_LISTENER = new Object();
		private static final Object SYNC_PAYMENT_LISTENER = new Object();
		private static final Object SYNC_FORWARDING_LISTENER = new Object();
		private static final Object SYNC_CONNECTION_ERROR = new Object();
		private static final Object SYNC_COUPON = new Object();
		private static boolean ms_bHTTPFilterOn = true;
		private static boolean m_bShuttingDown = false;
		private static boolean m_bExiting = false;
		private static Thread ms_tUpdate;
		private static Thread ms_tUpdatePayment;
		private static final Object SYNC_UPDATE = new Object();
		private static final Object SYNC_UPDATE_PAYMENT = new Object();
		private static final Object SYNC_CONFIGURATION_LISTENER = new Object();
		private static IConfigurationListener ms_configurationListener;

		private static UpdaterObservable ms_observableInfoServiceUpdater;

		private static IForwardingServerStatistics ms_forwardingStatistics;
		private static IForwardingListener ms_forwardingListener;
		private static final Observer FORWARDING_PROPAGANDA_OBSERVER = new Observer()
		{
			public void update(Observable a_observable, Object a_object)
				{
					synchronized (SYNC_FORWARDING_LISTENER)
						{
							if (ms_forwardingListener != null)
								{
									int iError = ((ServerSocketPropagandist) a_observable).getCurrentErrorCode();
									if (iError != ServerSocketPropagandist.RETURN_SUCCESS)
										{
											ms_forwardingListener.registerFailed(new Exception(
													"Registering forwarding server failed with error code: " + iError),
													((ServerSocketPropagandist) a_observable).getPort());
										}
								}
						}
				}
		};

		private static IPaymentListener ms_paymentListener;
		private static final IPaymentEventListener ms_dummyPaymentListener = new IPaymentEventListener()
		{
			public void activeAccountChanged(JonDonymAccount a_account)
				{

				}

			public void accountErrorOccured(XMLErrorMessage msg, boolean a_bIgnore)
				{

				}

			public void creditChanged(JonDonymAccount acc)
				{

				}

			public void requestedMonthlyOverusage(JonDonymAccount acc, boolean a_bSuccess)
				{

				}
		};
		private static IPaymentEventListener ms_paymentEventListener = ms_dummyPaymentListener;

		private static IServiceEventListener ms_serviceLister;
		private static AnonServiceEventListener m_listener;
		private static final IServiceEventListener m_dummyListener = new IServiceEventListener()
		{
			public void updateFinished()
				{

				}

			public int integrityErrorSignaled(MixServiceInfo a_serverDescription, int a_iDirection)
				{
					return IServiceEventListener.ANSWER_NO_ACTION;
				}

			public void updateFailed(Exception a_e)
				{

				}

			public void paymentUpdateFinished()
				{

				}

			public void paymentUpdateFailed(Exception a_e)
				{

				}

			public void connectionError(AnonServiceException a_e)
				{

				}

			public void currentServiceChanged(MixServiceInfo a_serverDescription)
				{

				}

			public void disconnected()
				{

				}

			public void connecting(MixServiceInfo a_serverDescription, boolean a_bIsReconnect)
				{

				}

			public void connectionEstablished(MixServiceInfo a_serverDescription)
				{

				}

			public void packetMixed(long a_totalBytes)
				{

				}

			public void dataChainErrorSignaled()
				{

				}
		};

		private static final String DEFAULT_INFOSERVICE_NAMES[] = new String[] {
				"880D9306B90EC8309178376B43AC26652CE52B74", "B1B2085A914FF9B838BD225F9D293C45E50812B4",
				"1E47E65976C6F7868047B6E9A06654B8AFF36A38", "AE116ECB775FF127C02DF96F5466AECAF86B93A9",
				"C2E7C64A7D245A4879737699A273F24369E53567" };
		// new String[]{"1AF4734DD3AA5BD1A8A4A2EDACAD825C711E1770"};
		private static final String DEFAULT_INFOSERVICE_HOSTNAMES[] = new String[] { "infoservice.inf.tu-dresden.de",
				"is.beneficium.de", "78.129.146.44", "72.55.137.241", "is1.anonmix.eu" };
		// new String[]{"87.230.20.187"};

		private static final int DEFAULT_INFOSERVICE_PORT_NUMBERS[][] = { { 80, 6543 }, { 80, 443 }, { 80, 443 },
				{ 80, 443 }, { 80, 443 } };

		public static synchronized boolean isInitialized()
			{
				return (ms_vecUpdaters != null);
			}

/*		public static synchronized void init(final Log a_logger, IConfiguration a_configuration) throws Exception
			{
				init(new ApacheCommonsLog(a_logger), a_configuration);
			}

		public static synchronized void init(final org.apache.log4j.Logger a_logger, IConfiguration a_configuration)
				throws Exception
			{
				init(new AbstractLog4jLog()
				{
					protected org.apache.log4j.Logger getLogger()
						{
							return a_logger;
						}
				}, a_configuration);
			}
*/
		public static synchronized void init(ILog a_logger, IConfiguration a_configuration) throws Exception
			{
				Element root = null;

				if (isInitialized())
					{
						return;
					}

				ms_configuration = a_configuration;
				if (ms_configuration == null)
					{
						throw new NullPointerException("No configuration object available!");
					}

				Locale defaultLocale = null;
				synchronized (ms_logger)
					{
						ms_logger.setLogger(a_logger);

						LogHolder.log(LogLevel.ALERT, LogType.MISC, "Initializing " + ClassUtil.getClassNameStatic() + " version "
								+ VERSION + "...");

						defaultLocale = JAPMessages.getLocale();

						root = ms_configuration.read();
						if (root != null)
							{
								defaultLocale = new Locale(
										XMLUtil.parseAttribute(root, XML_ATTR_LANGUAGE, defaultLocale.getLanguage()),
										XMLUtil.parseAttribute(root, XML_ATTR_COUNTRY, defaultLocale.getCountry()));

								XMLUtil.removeComments(root);
								XMLUtil.assertNodeName(root, XML_ELEMENT_NAME);

								LogHolder.setDetailLevel(XMLUtil.parseAttribute(root, XML_ATTR_LOG_DETAIL, LogHolder.getDetailLevel()));
								ms_logger.setLogLevel(XMLUtil.parseAttribute(root, XML_ATTR_LOG_LEVEL, ms_logger.getLogLevel()));
								ms_logger.setLogTypes(XMLUtil.parseAttribute(root, XML_ATTR_LOG_TYPES, ms_logger.getLogTypes()));

								XMLUtil.assertNodeName(root, XML_ELEMENT_NAME);
							}
					}
				setLocale(defaultLocale);

				IAntiCensorshipForwarding forwarding = ms_configuration.getForwardingSettings();

				startSocketListener(forwarding);

				if (ms_socketListener != null)
					{
						LogHolder.log(LogLevel.NOTICE, LogType.CRYPTO, "Initializing direct proxy...");
						ms_directProxy = new DirectProxy(ms_socketListener, ms_socketListenerTwo, ms_proxyInterface,
								new DirectProxy.AllowProxyConnectionCallback()
								{
									public String getApplicationName()
										{
											return ms_configuration.getApplicationName();
										}

									public String getAllowNonAnonymousSettingName()
										{
											return null;
										}

									public boolean isNonAnonymousAccessForbidden()
										{
											return true;
										}

									public boolean isConnecting()
										{
											return Controller.isConnecting();
										}

									public boolean isAskedForAnyNonAnonymousRequest()
										{
											return false;
										}

									public Answer callback(RequestInfo a_requestInfo)
										{
											return null;
										}
								});
						ms_directProxy.start();

						LogHolder.log(LogLevel.NOTICE, LogType.CRYPTO, "Initializing jondonym proxy...");
						synchronized (SYNC_SERVICE_LISTENER)
							{
								ms_jondonymProxy = new AnonProxy(ms_directProxy, ms_proxyInterface, null);
								//ms_jondonymProxy.addHTTPConnectionListener(new JonDoFoxHeader(0));
								ms_jondonymProxy.addHTTPConnectionListener(new BrowserIdentification(-1));
								ms_jondonymProxy.addHTTPConnectionListener(new JonDonymXHeaders(-2));
								if (m_listener != null)
									{
										ms_jondonymProxy.addEventListener(m_listener);
									}
							}
						ms_jondonymProxy.setDummyTraffic(DummyTrafficControlChannel.DT_MAX_INTERVAL_MS);
					}

				ForwardServerManager.getInstance().setDummyTrafficInterval(DummyTrafficControlChannel.DT_MAX_INTERVAL_MS);

				LogHolder.log(LogLevel.NOTICE, LogType.CRYPTO, "Initializing random number generator...");
				Thread secureRandomThread = new Thread(new Runnable()
				{
					public void run()
						{
							new SecureRandom().nextInt();
							KeyPool.start(false);
							LogHolder.log(LogLevel.NOTICE, LogType.CRYPTO, "Random number generator initialized.");
						}
				});
				secureRandomThread.setPriority(Thread.MIN_PRIORITY);
				secureRandomThread.start();
				try
					{
						secureRandomThread.join();
					}
				catch (InterruptedException a_e)
					{
						LogHolder.log(LogLevel.ERR, LogType.CRYPTO, "Interrupted while initialising random number generator!");
					}

				LogHolder.log(LogLevel.NOTICE, LogType.CRYPTO, "Getting password reader...");

				LogHolder.log(LogLevel.NOTICE, LogType.CRYPTO, "Initializing general settings...");
				ClassUtil.enableFindSubclasses(false); // This would otherwise start
																								// non-daemon AWT threads, blow
																								// up memory and prevent closing
																								// the app.

				if (forwarding == null)
					{
						// Store as few XML data as possible for memory optimization.
						XMLUtil.setStorageMode(XMLUtil.STORAGE_MODE_AGRESSIVE);
					}
				else
					{
						// we need most of the XML for the forwarding clients, so we cannot
						// be agressive in memory optimization
						XMLUtil.setStorageMode(XMLUtil.STORAGE_MODE_OPTIMIZED);
					}
				SignatureVerifier.getInstance().setCheckSignatures(ms_configuration.isSignatureCheckEnabled());

				Util.addDefaultCertificates("acceptedInfoServiceCAs/", JAPCertificate.CERTIFICATE_TYPE_ROOT_INFOSERVICE);
				Util.addDefaultCertificates("acceptedInfoServices/", JAPCertificate.CERTIFICATE_TYPE_INFOSERVICE);
				Util.addDefaultCertificates("acceptedMixCAs/", JAPCertificate.CERTIFICATE_TYPE_ROOT_MIX);
				Util.addDefaultCertificates("acceptedPIs/", JAPCertificate.CERTIFICATE_TYPE_PAYMENT);
				Util.addDefaultCertificates("acceptedTaCTemplates/", JAPCertificate.CERTIFICATE_TYPE_TERMS_AND_CONDITIONS);
				LogHolder.log(LogLevel.NOTICE, LogType.CRYPTO, "General settings initialized.");

				LogHolder.log(LogLevel.NOTICE, LogType.CRYPTO, "Initializing database updaters...");
				// simulate database distributor and suppress distributor warnings
				Database.registerDistributor(new IDistributor()
				{
					public void addJob(IDistributable a_distributable)
						{
						}
				});

				InfoServiceDBEntry[] defaultInfoService = anon.util.Util.createDefaultInfoServices(DEFAULT_INFOSERVICE_NAMES,
						DEFAULT_INFOSERVICE_HOSTNAMES, DEFAULT_INFOSERVICE_PORT_NUMBERS);
				for (int i = 0; i < defaultInfoService.length; i++)
					{
						Database.getInstance(InfoServiceDBEntry.class).update(defaultInfoService[i]);
					}
				InfoServiceHolder.getInstance().setPreferredInfoService(defaultInfoService[0]);

				Vector<Integer> vecPortForwarder = new Vector<Integer>();
				Vector<Integer> vecPortForwarderTry = new Vector<Integer>();
				Vector<ServerSocketPropagandist> vecPropagandists = new Vector<ServerSocketPropagandist>();
				if (forwarding != null)
					{
						Object result;
						int port;

						vecPortForwarderTry.addElement(new Integer(forwarding.getListenPort()));

						ForwardServerManager.getInstance().startForwarding();
						ForwardServerManager.getInstance().setNetBandwidth(forwarding.getMaxBandwidth());
						ForwardServerManager.getInstance().setMaximumNumberOfConnections(forwarding.getMaxConnections());

						if (vecPortForwarderTry.elementAt(0).intValue() == 0)
							{
								// use more than one and also random ports
								vecPortForwarderTry.removeAllElements();
								vecPortForwarderTry.addElement(new Integer(443));
								vecPortForwarderTry.addElement(new Integer(80));
								// choose an additional random port above 1024
								do
									{
										port = (Math.abs(new SecureRandom().nextInt()) % 64511) + 1025;
									} // do not use the JonDo listener port, this will not work...
								while (ms_socketListener != null && port == ms_socketListener.getLocalPort());
								vecPortForwarderTry.addElement(new Integer(port));
							}

						for (int i = 0; i < vecPortForwarderTry.size(); i++)
							{
								result = ForwardServerManager.getInstance().addListenSocket(vecPortForwarderTry.elementAt(i));
								if (result != null)
									{
										vecPortForwarder.addElement(vecPortForwarderTry.elementAt(i));
									}
							}

						if (vecPortForwarder.size() == 0)
							{
								String strPorts = "";
								for (int i = 0; i < vecPortForwarderTry.size(); i++)
									{
										strPorts += " " + vecPortForwarderTry.elementAt(i);
									}
								throw new SocketException("Could not bind anti-censorship forwarder to any of these local ports:"
										+ strPorts);
							}
						else
							{
								synchronized (SYNC_FORWARDING_LISTENER)
									{
										if (ms_forwardingListener != null)
											{
												// TODO host is null at the moment, because we do not
												// yet get an answer from the info service concerning
												// our ip
												ms_forwardingListener.startedForwardingServer(null, vecPortForwarder);
											}
									}
							}

						final ForwardSchedulerStatistics statistics = ForwardServerManager.getInstance().getSchedulerStatistics();
						if (statistics != null)
							{
								ms_forwardingStatistics = new IForwardingServerStatistics()
								{
									public long getCurrentBandwidthUsage()
										{
											return statistics.getCurrentBandwidthUsage();
										}

									public long getTransferedBytes()
										{
											return statistics.getTransferedBytes();
										}

									public int getAcceptedConnections()
										{
											return statistics.getAcceptedConnections();
										}

									public int getRejectedConnections()
										{
											return statistics.getRejectedConnections();
										}
								};
							}

					}

				Database.getInstance(MixCascade.class).randomize();
				if (a_configuration != null && a_configuration.getExternalDatabase() != null)
					{
						synchronized (SYNC_CONFIGURATION_LISTENER)
							{
								if (ms_configurationListener != null)
									{
										ms_configurationListener.loadingCacheDBEntries();
									}
							}
						Database.registerExternalDatabase(a_configuration.getExternalDatabase());
						Database.loadFromExternalDatabase();
					}
				else
					{
						LogHolder
								.log(
										LogLevel.WARNING,
										LogType.DB,
										"Sqlite chaching database is not used. It may take a while to fetch the InfoService data and to get the first connection.");
					}

				if (forwarding != null && vecPropagandists.size() == 0)
					{
						LogHolder.log(LogLevel.NOTICE, LogType.DB,
								"Registering the forwarding server ports at the InfoServices (this may take a while)...");

						synchronized (SYNC_FORWARDING_LISTENER)
							{
								if (ms_forwardingListener != null)
									{
										ms_forwardingListener.registeringAtInfoServices();
									}
							}

						for (int i = 0; i < vecPortForwarder.size(); i++)
							{
								LogHolder.log(LogLevel.NOTICE, LogType.DB, "Registering forwarding server port "
										+ vecPortForwarder.elementAt(i).intValue() + "...");

								synchronized (SYNC_FORWARDING_LISTENER)
									{
										if (ms_forwardingListener != null)
											{
												ms_forwardingListener.registeringAtInfoServices(null, vecPortForwarder.elementAt(i).intValue());
											}
									}

								vecPropagandists.addElement(new ServerSocketPropagandist(vecPortForwarder.elementAt(i).intValue()));
								if (vecPropagandists.lastElement().getCurrentErrorCode() == ServerSocketPropagandist.RETURN_SUCCESS)
									{
										LogHolder.log(LogLevel.ALERT, LogType.NET, "Forwarding server is listening on port "
												+ vecPropagandists.lastElement().getPort() + ".");

										synchronized (SYNC_FORWARDING_LISTENER)
											{
												if (ms_forwardingListener != null)
													{
														ms_forwardingListener.registerFinished(vecPropagandists.lastElement().getPort());
													}
											}
									}
								else
									{
										String strMsg = "Error code: " + vecPropagandists.lastElement().getCurrentErrorCode();
										if (vecPropagandists.lastElement().getCurrentErrorCode() == ServerSocketPropagandist.RETURN_VERIFICATION_ERROR)
											{
												strMsg = "Our forwarding server seems to be unreachable for the InfoServices.\n"
														+ "If your computer is behind a firewall/router, enable port forwarding.";
											}

										LogHolder.log(LogLevel.ERR, LogType.NET, strMsg);

										synchronized (SYNC_FORWARDING_LISTENER)
											{
												if (ms_forwardingListener != null)
													{
														ms_forwardingListener.registerFailed(new Exception(strMsg), vecPropagandists.lastElement()
																.getPort());
													}
											}
									}
							}

					}

				for (int i = 0; i < vecPropagandists.size(); i++)
					{
						vecPropagandists.elementAt(i).addObserver(FORWARDING_PROPAGANDA_OBSERVER);
					}

				/*
				 * LogHolder.log(LogLevel.NOTICE, LogType.CRYPTO,
				 * "Initialising trust filters..."); // predefine trust model; force
				 * premium services if charged account is available, but do not use them
				 * if not TrustModel modelDynamicPremium = new
				 * TrustModel(MSG_DEFAULT_TRUST_MODEL,
				 * TrustModel.FIRST_UNRESERVED_MODEL_ID);
				 * modelDynamicPremium.setAttribute
				 * (ForcePremiumIfAccountAvailableAttribute.class,
				 * TrustModel.TRUST_IF_TRUE);
				 * modelDynamicPremium.setAttribute(NumberOfMixesAttribute.class,
				 * TrustModel.TRUST_IF_AT_LEAST, 2);
				 * TrustModel.addTrustModel(modelDynamicPremium);
				 * TrustModel.setCurrentTrustModel(modelDynamicPremium);
				 * LogHolder.log(LogLevel.NOTICE, LogType.CRYPTO,
				 * "Trust filters initialised.");
				 */

				boolean bEnableInfoServiceAutoUpdate;

				if (root != null)
					{
						LogHolder.log(LogLevel.NOTICE, LogType.CRYPTO, "Parsing configuration...");
						Element elem;

						try
							{
								ms_iPaymentAnonymousProxyConnection = EnumAnonymousProxyConnection.valueOf(XMLUtil.parseAttribute(root,
										XML_ATTR_PAYMENT_ANONYMOUS_PROXY_CONNECTION, ms_iPaymentAnonymousProxyConnection.toString()));
							}
						catch (IllegalArgumentException a_e)
							{
								LogHolder.log(LogLevel.WARNING, LogType.MISC, "Could not read anonymous proxy settings for payment.",
										a_e);
							}

						try
							{
								ms_iInfoServiceAnonymousProxyConnection = EnumAnonymousProxyConnection.valueOf(XMLUtil.parseAttribute(
										root, XML_ATTR_INFOSERVICE_ANONYMOUS_PROXY_CONNECTION,
										ms_iInfoServiceAnonymousProxyConnection.toString()));
							}
						catch (IllegalArgumentException a_e)
							{
								LogHolder.log(LogLevel.WARNING, LogType.MISC,
										"Could not read anonymous proxy settings for InfoService.", a_e);
							}

						elem = (Element) XMLUtil.getFirstChildByName(root, PaymentInstanceDBEntry.XML_ELEMENT_CONTAINER_NAME);
						Database.getInstance(PaymentInstanceDBEntry.class).loadFromXml(elem);

						elem = (Element) XMLUtil.getFirstChildByName(root, PayAccountsFile.XML_ELEMENT_NAME);
							// if (elem != null)
							{
								LogHolder.log(LogLevel.NOTICE, LogType.CRYPTO, "Initializing pay accounts from configuration...");

								AbstractMemorizingPasswordReader passwordReader = getPasswordReader();

								synchronized (passwordReader)
									{
										passwordReader.reset();
										PayAccountsFile.init(elem, passwordReader, false, 2, ms_proxyPayment);
										if (passwordReader.countCmpletedObjects() > 0)
											{
												changeAccountPassword(ms_accountPassword, passwordReader.getPassword());
											}
									}
								LogHolder.log(LogLevel.NOTICE, LogType.CRYPTO, "Pay accounts initialized from configuration.");
							}

						if (PayAccountsFile.getInstance().getActiveAccount() != null)
							{
								ms_currentPIID = new PaymentInstance(PayAccountsFile.getInstance().getActiveAccount().getPIID());
							}

						elem = (Element) XMLUtil.getFirstChildByName(root, TrustModel.XML_ELEMENT_CONTAINER_NAME);
						if (elem != null)
							{
								LogHolder.log(LogLevel.NOTICE, LogType.CRYPTO, "Initializing trust filters from configuration...");
								TrustModel.fromXmlElement(elem);
								LogHolder.log(LogLevel.NOTICE, LogType.CRYPTO, "Trust filters initialized from configuration.");
							}

						elem = (Element) XMLUtil.getFirstChildByName(root, BlacklistedCascadeIDEntry.XML_ELEMENT_CONTAINER_NAME);
						if (elem != null)
							{
								LogHolder.log(LogLevel.NOTICE, LogType.CRYPTO, "Initializing black list from configuration...");
								Database.getInstance(BlacklistedCascadeIDEntry.class).loadFromXml(elem);
								LogHolder.log(LogLevel.NOTICE, LogType.CRYPTO, "Black list initialized from configuration.");
							}

						ms_bHTTPFilterOn = XMLUtil.parseAttribute(root, XML_ATTR_HTTP_FILTER, ms_bHTTPFilterOn);

						if (!XMLUtil.parseAttribute(root, XML_ATTR_AUTOSWITCH, true))
							{
								String id = XMLUtil.parseAttribute(root, XML_ATTR_DEFAULT_CASCADE, null);
								MixCascade cascade = (MixCascade) Database.getInstance(MixCascade.class).getEntryById(id);
								ms_serviceContainer = new AutoSwitchedMixCascadeContainer(cascade, id);
								ms_serviceContainer.setServiceAutoSwitched(false);
							}
						else
							{
								ms_serviceContainer = new AutoSwitchedMixCascadeContainer(null, null);
								ms_serviceContainer.setServiceAutoSwitched(true);
							}

						bEnableInfoServiceAutoUpdate = XMLUtil.parseAttribute(root, XML_ATTR_AUTO_UPDATE,
								ms_configuration.isInfoServiceDataUpdatedByDefault());

						LogHolder.log(LogLevel.NOTICE, LogType.CRYPTO, "Configuration parsed completely.");
					}
				else
					{
						LogHolder.log(LogLevel.ALERT, LogType.MISC, "Configuration is empty!");
						PayAccountsFile.init(null, null, false, 2, ms_proxyPayment);
						ms_serviceContainer = new AutoSwitchedMixCascadeContainer(null, null);
						ms_serviceContainer.setServiceAutoSwitched(ms_configuration.isServiceAutoSwitchedByDefault());

						bEnableInfoServiceAutoUpdate = ms_configuration.isInfoServiceDataUpdatedByDefault();

						PayAccountsFile.getInstance().setBalanceAutoUpdateEnabled(ms_configuration.isAccountDataUpdatedByDefault());
					}

				ms_serviceContainer.addObserver(new Observer()
				{
					public void update(Observable a_observable, Object a_object)
						{
							if (a_object != null && a_object instanceof MixCascade && !Controller.isRunning())
								{
									synchronized (SYNC_SERVICE_LISTENER)
										{
											if (m_listener != null)
												{
													m_listener.currentServiceChanged((MixCascade) a_object);
												}
										}
								}
						}
				});

				enableHTTPFilter(isHTTPFilterEnabled());

				// this must be after reading the configuration, because the
				// configuration might enforce anonymous connections
				LogHolder.log(LogLevel.NOTICE, LogType.CRYPTO, "Initializing database updaters...");

				InfoServiceDBEntry.setMutableProxyInterface(ms_proxyInfoService);
				BIConnection.setMutableProxyInterface(ms_proxyPayment);

				ms_observableInfoServiceUpdater = new UpdaterObservable(bEnableInfoServiceAutoUpdate);
				ObservableInfo observableInfoServiceUpdater = new Updater.ObservableInfoContainer(
						ms_observableInfoServiceUpdater.getObservableInfo())
				{
					public boolean updateImmediately()
						{
							return true;
						}
				};

				// / TODO re-implement JAPFeedback!!!

				/* TODO separate the payment instance updated from the other updaters */
				ms_vecUpdaters = new Vector<Updater>();
				ms_vecUpdaters.addElement(new InfoServiceUpdater(ms_observableInfoServiceUpdater.getObservableInfo()));
				ms_vecUpdaters.addElement(new PaymentInstanceUpdater(observableInfoServiceUpdater));
				ms_vecUpdaters.addElement(new MixCascadeUpdater(ms_observableInfoServiceUpdater.getObservableInfo()));
				ms_vecUpdaters.addElement(new PerformanceInfoUpdater(observableInfoServiceUpdater));
				ms_vecUpdaters.addElement(new ServiceExitAddressUpdater(observableInfoServiceUpdater));

				ms_accountUpdater = new AccountUpdater(false);
				ms_accountUpdaterInternal = new AccountUpdater(true);

				if (ms_observableInfoServiceUpdater.isAutoUpdateEnabled())
					{
						updateDataFromInfoService();
					}
				else
					{
						for (Updater updater : ms_vecUpdaters)
							{
								updater.start(true);
							}
					}

				if (PayAccountsFile.getInstance().isBalanceAutoUpdateEnabled())
					{
						updateAccountsFromPaymentInstance();
						ms_accountUpdaterInternal.start(true);
					}
				else
					{
						ms_accountUpdaterInternal.update();
						ms_accountUpdater.start(true);
					}

				LogHolder.log(LogLevel.NOTICE, LogType.CRYPTO, "Database updaters initialized.");

				Runtime.getRuntime().addShutdownHook(new Thread()
				{
					public void run()
						{
							LogHolder.log(LogLevel.EMERG, LogType.MISC, "Exiting, please do not interrupt...");

							m_bExiting = true;

							Controller.stop(true);
							ForwardServerManager.getInstance().shutdownForwarding();

							if (ms_vecUpdaters != null)
								{
									for (int i = 0; i < ms_vecUpdaters.size(); i++)
										{
											ms_vecUpdaters.elementAt(i).stop();
										}
								}

							if (ms_accountUpdaterInternal != null)
								{
									ms_accountUpdaterInternal.stop();
								}

							if (ms_accountUpdater != null)
								{
									ms_accountUpdater.stop();
								}

							try
								{
									Controller.activateCoupon(null);
								}
							catch (Exception a_e)
								{
									LogHolder.log(LogLevel.ERR, LogType.PAY, "Could not reset accounts!", a_e);
								}
							try
								{
									Controller.saveConfiguration();
								}
							catch (Exception a_e)
								{
									LogHolder.log(LogLevel.EMERG, LogType.MISC, "Could not save configuration during shutdown!", a_e);
								}
							LogHolder.log(LogLevel.EMERG, LogType.MISC, "Exited!");
						}
				});
			}

		public static InetAddress getListenerAddress()
			{
				synchronized (SYNC_STARTER)
					{
						return (ms_socketListener == null) ? null : ms_socketListener.getInetAddress();
					}
			}

		public static InetAddress getVirtualBoxListenerAddress()
			{
				synchronized (SYNC_STARTER)
					{
						if (!m_bIsVirtualBoxListener)
							{
								return null;
							}
						return (ms_socketListenerTwo == null) ? null : ms_socketListenerTwo.getInetAddress();
					}
			}

		public static int getListenerPort()
			{
				synchronized (SYNC_STARTER)
					{
						return ms_socketListener.getLocalPort();
					}
			}

		public static boolean changeAccountPassword(char[] a_oldPassword, char[] a_newPassword)
			{
				if (Arrays.equals(a_oldPassword, ms_accountPassword)
						|| (a_oldPassword == null && ms_accountPassword.length == 0) || ms_accountPassword == null
						&& a_oldPassword.length == 0)
					{
						if (ms_accountPassword != null)
							{
								Arrays.fill(a_oldPassword, '\0');
								Arrays.fill(ms_accountPassword, '\0');
							}
						if (a_newPassword == null)
							{
								ms_accountPassword = null;
							}
						else
							{
								ms_accountPassword = new char[a_newPassword.length];
								System.arraycopy(a_newPassword, 0, ms_accountPassword, 0, a_newPassword.length);
								Arrays.fill(a_newPassword, '\0');
							}

						try
							{
								saveConfiguration();
							}
						catch (Exception a_e)
							{
								LogHolder.log(LogLevel.ALERT, LogType.MISC, a_e);
							}

						return true;
					}

				return false;
			}

		public static Logger getLogger()
			{
				return ms_logger;
			}

		public static boolean setLocale(Locale a_locale)
			{
				try
					{
						return JAPMessages.init(a_locale, MESSAGES);
					}
				catch (Throwable t)
					{
						// Ignore if we could not load the messages for the given locale...
						LogHolder.log(LogLevel.ERR, LogType.GUI, t);
					}
				return false;
			}

		public static Locale getLocale()
			{
				return JAPMessages.getLocale();
			}

		public static void setPaymentAnonymousProxyConnecion(EnumAnonymousProxyConnection a_iAnonymousProxyConnection)
			{
				ms_iPaymentAnonymousProxyConnection = a_iAnonymousProxyConnection;
			}

		public static EnumAnonymousProxyConnection getPaymentAnonymousProxyConnecion()
			{
				return ms_iPaymentAnonymousProxyConnection;
			}

		public static void setForwardingListener(IForwardingListener a_listener)
			{
				synchronized (SYNC_FORWARDING_LISTENER)
					{
						ms_forwardingListener = a_listener;
					}
			}

		public static void enableAccountAutoUpdate(boolean a_bAutoUpdate)
			{
				PayAccountsFile.getInstance().setBalanceAutoUpdateEnabled(a_bAutoUpdate);
			}

		public static boolean isAccountAutoUpdateEnabled()
			{
				return PayAccountsFile.getInstance().isBalanceAutoUpdateEnabled();
			}

		public static void setPaymentListener(IPaymentEventListener a_eventListener)
			{
				synchronized (SYNC_PAYMENT_LISTENER)
					{
						if (ms_paymentListener != null)
							{
								PayAccountsFile.getInstance().removePaymentListener(ms_paymentListener);
							}

						if (a_eventListener == null)
							{
								a_eventListener = ms_dummyPaymentListener;
							}

						ms_paymentEventListener = a_eventListener;

						final IPaymentEventListener listener = a_eventListener;

						ms_paymentListener = new IPaymentListener()
						{

							public void accountCertRequested(MixCascade a_connectedCascade) throws AccountEmptyException
								{
								}

							public void accountError(XMLErrorMessage msg, boolean a_bIgnore)
								{
									listener.accountErrorOccured(msg, a_bIgnore);
								}

							public void accountActivated(PayAccount acc)
								{
									if (acc == null)
										{
											listener.activeAccountChanged(null);
										}
									else
										{
											listener.activeAccountChanged(new JonDonymAccount(acc));
											ms_currentPIID = new PaymentInstance(acc.getPIID());
										}
								}

							public void accountRemoved(PayAccount acc)
								{
								}

							public void accountAdded(PayAccount acc)
								{
								}

							public void creditChanged(PayAccount acc)
								{
									listener.creditChanged(new JonDonymAccount(acc));
								}

							public void gotCaptcha(ICaptchaSender a_source, IImageEncodedCaptcha a_captcha)
								{
								}
						};

						PayAccountsFile.getInstance().addPaymentListener(ms_paymentListener);
					}
			}

		public static void setConfigurationListener(IConfigurationListener a_listener)
			{
				synchronized (SYNC_CONFIGURATION_LISTENER)
					{
						ms_configurationListener = a_listener;
					}
			}

		public static void setServiceListener(IServiceEventListener a_listener)
			{
				synchronized (SYNC_SERVICE_LISTENER)
					{
						if (m_listener != null && ms_jondonymProxy != null)
							{
								ms_jondonymProxy.removeEventListener(m_listener);
								ms_serviceLister = null;
							}

						if (a_listener == null)
							{
								a_listener = m_dummyListener;
							}

						final IServiceEventListener listener = a_listener;

						m_listener = new AnonServiceEventListener()
						{
							public void connectionError(final AnonServiceException a_e)
								{
									synchronized (SYNC_CONNECTION_ERROR)
										{
											if (Controller.isRunning() && a_e != null && a_e instanceof INotRecoverableException)
												{
													new Thread()
													{
														public void run()
															{
																/*
																 * try { Thread.sleep(200); } catch
																 * (InterruptedException e) { // ignore }
																 */
																synchronized (SYNC_CONNECTION_ERROR)
																	{
																		if (Controller.isRunning())
																			{
																				Controller.stop(true);
																				listener.connectionError(a_e);
																			}
																	}
															}
													}.start();
												}
											else
												{
													listener.connectionError(a_e);
												}
										}
								}

							public void currentServiceChanged(AnonServerDescription a_serverDescription)
								{
									listener.currentServiceChanged(new MixServiceInfo((MixCascade) a_serverDescription));
								}

							public void disconnected()
								{
									listener.disconnected();
								}

							public void connecting(AnonServerDescription a_serverDescription, boolean a_bIsReconnect)
								{
									if (isRunning())
										{
											listener.connecting(new MixServiceInfo((MixCascade) a_serverDescription), a_bIsReconnect);
										}
								}

							public void connectionEstablished(AnonServerDescription a_serverDescription)
								{
									listener.connectionEstablished(new MixServiceInfo((MixCascade) a_serverDescription));
								}

							public void packetMixed(long a_totalBytes)
								{
									listener.packetMixed(a_totalBytes);
								}

							public void dataChainErrorSignaled(AnonServiceException a_e)
								{
									listener.dataChainErrorSignaled();
								}

							public void integrityErrorSignaled(AnonServiceException a_e)
								{
									int iAnswer;

									if (a_e.getErrorCode() == ErrorCodes.E_INTEGRITY_ERROR_DOWNSTREAM)
										{
											iAnswer = IServiceEventListener.INTEGRITY_ERROR_DOWNSTREAM;
										}
									else
										{
											iAnswer = IServiceEventListener.INTEGRITY_ERROR_UPSTREAM;
										}
									iAnswer = listener.integrityErrorSignaled(new MixServiceInfo((MixCascade) a_e.getService()), iAnswer);

									if (iAnswer == IServiceEventListener.ANSWER_DISCONNECT
											|| iAnswer == IServiceEventListener.ANSWER_DISCONNECT_AND_BLACKLIST)
										{
											MixCascade cascade = (MixCascade) a_e.getService();

											if (iAnswer == IServiceEventListener.ANSWER_DISCONNECT_AND_BLACKLIST)
												{
													Database.getInstance(BlacklistedCascadeIDEntry.class).update(
															new BlacklistedCascadeIDEntry(cascade));
												}

											if (cascade.equals(ms_serviceContainer.getNextCascade()))
												{
													// we have tried to switch to another service, but
													// failed...
													stop();
												}
										}
								}
						};

						if (ms_jondonymProxy != null)
							{
								ms_jondonymProxy.addEventListener(m_listener);
							}
						ms_serviceLister = a_listener;
					}
			}

		public static void setInfoServiceAnonymousProxyConnecion(EnumAnonymousProxyConnection a_iAnonymousProxyConnection)
			{
				ms_iInfoServiceAnonymousProxyConnection = a_iAnonymousProxyConnection;
			}

		public static EnumAnonymousProxyConnection getInfoServiceAnonymousProxyConnecion()
			{
				return ms_iInfoServiceAnonymousProxyConnection;
			}

		public static void stop(boolean a_bBlocking)
			{
				if (a_bBlocking)
					{
						stop_internal();
					}
				else
					{
						stop();
					}
			}

		public static void stop()
			{
				Thread thread = new Thread("JonDoController stop")
				{
					public void run()
						{
							stop_internal();
						}
				};
				thread.start();
			}

		private static void stop_internal()
			{
				if (m_bShuttingDown)
					{
						return;
					}

				synchronized (SYNC_STARTER)
					{
						if (ms_directProxy == null
								|| ms_jondonymProxy == null
								|| (ms_directProxy.isConnected() && !ms_jondonymProxy.isConnecting() && !ms_jondonymProxy.isConnected()))
							{
								return;
							}

						m_bShuttingDown = true;

						while (ms_starterThread != null && !ms_starter.isFinished())
							{
								LogHolder.log(LogLevel.NOTICE, LogType.MISC, "Interrupting startup thread...");
								ms_starterThread.interrupt();
								try
									{
										SYNC_STARTER.wait(200);
									}
								catch (InterruptedException e)
									{
										LogHolder.log(LogLevel.EXCEPTION, LogType.MISC, e);
									}
							}

						/*
						 * DirectProxy.AllowProxyConnectionCallback callback =
						 * ms_directProxy.getCallback(); if (callback != null) {
						 * callback.setRulesChanged(true); }
						 */

						ms_directProxy.reset();
						ms_jondonymProxy.stop();

						/*
						 * if (ms_socketListener != null) { try { ms_socketListener.close();
						 * } catch (IOException a_e) { LogHolder.log(LogLevel.EXCEPTION,
						 * LogType.NET, a_e); } ms_socketListener = null; }
						 */

						m_bShuttingDown = false;

						SYNC_STARTER.notifyAll();
					}
			}

		/**
		 * Deinitialises everything. Note that the controller can not long used
		 * (until init() is called again...)
		 * 
		 * @TODO: needs to be finished...
		 */
		public static void destroy()
			{
				stop_internal();
				if (ms_vecUpdaters != null)
					{
						for (Updater updater : ms_vecUpdaters)
							{
								updater.stop();
							}
						ms_vecUpdaters.removeAllElements();
					}
				ms_vecUpdaters = null;
			}

		public static void setServiceFilter(MixServiceFilter a_filter)
			{
				TrustModel.setCurrentTrustModel(a_filter.getTrustModel());
				if (!TrustModel.getCurrentTrustModel().isTrusted(getCurrentService().getMixCascade()))
					{
						switchService();
					}
			}

		public static void switchService()
			{
				if (ms_serviceContainer != null)
					{
						ms_serviceContainer.getNextRandomCascade();
					}
			}

		public static void switchService(final MixServiceInfo a_cascade)
			{
				if (ms_serviceContainer != null)
					{
						ms_serviceContainer.setCurrentCascade(a_cascade.getMixCascade());
					}
			}

		public static void enableInfoServiceAutoUpdate(boolean a_bAutoUpdate)
			{
				if (ms_observableInfoServiceUpdater != null)
					{
						ms_observableInfoServiceUpdater.setAutoUpdate(a_bAutoUpdate);
					}
			}

		public static boolean isInfoServiceAutoUpdateEnabled()
			{
				if (ms_observableInfoServiceUpdater != null)
					{
						return ms_observableInfoServiceUpdater.isAutoUpdateEnabled();
					}
				else
					{
						return false;
					}
			}

		public static void setServicesAutoSwitched(boolean a_bAutoSwitch)
			{
				if (ms_serviceContainer != null)
					{
						ms_serviceContainer.setServiceAutoSwitched(a_bAutoSwitch);
					}
			}

		public static IForwardingServerStatistics getForwardingServerStatistics()
			{
				return ms_forwardingStatistics;
			}

		public static boolean removeFromBlacklist(MixServiceInfo a_cascade)
			{
				if (a_cascade == null)
					{
						return false;
					}
				return Database.getInstance(BlacklistedCascadeIDEntry.class).remove(
						a_cascade.getMixCascade().getMixIDsAsString());
			}

		public static boolean addToBlacklist(MixServiceInfo a_cascade)
			{
				if (a_cascade == null)
					{
						return false;
					}

				boolean bDone = Database.getInstance(BlacklistedCascadeIDEntry.class).update(
						new BlacklistedCascadeIDEntry(a_cascade.getMixCascade()));

				if (a_cascade.equals(getCurrentService()))
					{
						switchService();
					}
				return bDone;
			}

		public static boolean allowDirectProxyDomain(URL a_url)
			{
				if (ms_directProxy == null)
					{
						return false;
					}
				return ms_directProxy.allowDomain(a_url);
			}

		public static boolean areServicesAutoSwitched()
			{
				if (ms_serviceContainer != null)
					{
						return ms_serviceContainer.isServiceAutoSwitched();
					}
				return true;
			}

		public static Vector<MixServiceInfo> getServices()
			{
				Vector vecCascades = Database.getInstance(MixCascade.class).getEntryList();
				Vector<MixServiceInfo> vecAvailable = new Vector<MixServiceInfo>();
				for (int i = 0; i < vecCascades.size(); i++)
					{
						vecAvailable.addElement(new MixServiceInfo((MixCascade) vecCascades.elementAt(i)));
					}
				return vecAvailable;
			}

		public static Vector<MixServiceInfo> getAvailableServices()
			{
				return getAvailableServices(TrustModel.getCurrentTrustModel());
			}

		private static Vector<MixServiceInfo> getAvailableServices(TrustModel a_filter)
			{
				Vector vecCascades = Database.getInstance(MixCascade.class).getEntryList();
				Vector<MixServiceInfo> vecAvailable = new Vector<MixServiceInfo>();
				for (int i = 0; i < vecCascades.size(); i++)
					{
						if (a_filter.isTrusted((MixCascade) vecCascades.elementAt(i)))
							{
								vecAvailable.addElement(new MixServiceInfo((MixCascade) vecCascades.elementAt(i)));
							}
					}

				MixServiceInfo defaultService = getCurrentService();
				if (defaultService != null && !vecAvailable.contains(defaultService)
						&& a_filter.isTrusted(defaultService.getMixCascade()))
					{
						vecAvailable.addElement(defaultService); // needed for custom
																											// services
					}

				return vecAvailable;
			}

		public static JonDonymAccount getActiveAccount()
			{
				PayAccount account = PayAccountsFile.getInstance().getActiveAccount();
				if (account == null)
					{
						return null;
					}
				return new JonDonymAccount(account);

			}

		public static MixServiceInfo getDummyService()
			{
				return INITIAL_DUMMY_SERVICE;
			}

		public static MixServiceInfo getCurrentService()
			{
				if (ms_jondonymProxy == null)
					return null;

				MixCascade cascade = null;

				cascade = ms_jondonymProxy.getMixCascade();

				if (cascade == null && ms_serviceContainer != null)
					{
						cascade = ms_serviceContainer.getCurrentCascade();
					}
				return new MixServiceInfo(cascade);
			}

		public static boolean isRunning()
			{
				if (ms_jondonymProxy == null)
					{
						return false;
					}
				return isConnecting() || ms_jondonymProxy.isConnected();
			}

		public static boolean isConnecting()
			{
				if (ms_jondonymProxy == null)
					{
						return false;
					}
				return ms_jondonymProxy.isConnecting() || ms_jondonymProxy.isConnected() || ms_jondonymProxy.isConnecting();
			}

		public static boolean isConnected()
			{
				if (ms_jondonymProxy == null)
					{
						return false;
					}
				return ms_jondonymProxy.isConnected();
			}

		public static void enableHTTPFilter(boolean a_bEnable)
			{
				synchronized (SYNC_HTTP_FILTER)
					{
							// if (ms_bHTTPFilterOn != a_bEnable)
							{
								ms_bHTTPFilterOn = a_bEnable;

								if (ms_bHTTPFilterOn)
									{
										ms_jondonymProxy.setHTTPDecompressionEnabled(true);
										ms_jondonymProxy.setHTTPHeaderProcessingEnabled(true, true);
									}
								else
									{
										ms_jondonymProxy.setHTTPHeaderProcessingEnabled(false, true);
										ms_jondonymProxy.setHTTPDecompressionEnabled(false);
									}
							}
					}
			}

		public static boolean isHTTPFilterEnabled()
			{
				return ms_bHTTPFilterOn;
			}

		public static Vector<PaymentInstance> getPaymentInstances()
			{
				Vector vecPIs = Database.getInstance(PaymentInstanceDBEntry.class).getEntryList();
				Vector<PaymentInstance> vecReturn = new Vector<PaymentInstance>();
				for (int i = 0; i < vecPIs.size(); i++)
					{
						vecReturn.add(new PaymentInstance(((PaymentInstanceDBEntry) vecPIs.elementAt(i)).getId()));
					}

				Enumeration enumAccounts = PayAccountsFile.getInstance().getAccounts();
				PayAccount account;
				PaymentInstance pi;

				while (enumAccounts.hasMoreElements())
					{
						account = (PayAccount) enumAccounts.nextElement();
						if (account.getPIID() != null)
							{
								pi = new PaymentInstance(account.getPIID());
								if (!vecReturn.contains(pi))
									{
										vecReturn.addElement(pi);
									}
							}
					}

				return vecReturn;

			}

		public static PaymentInstance getActivePaymentInstance()
			{
				return ms_currentPIID;
			}

		public static void setActivePaymentInstance(PaymentInstance a_pi)
			{
				if (a_pi != null)
					{
						ms_currentPIID = a_pi;

						if (!TrustModel.getCurrentTrustModel().isTrusted(getCurrentService().getMixCascade()))
							{
								switchService();
							}
					}
			}

		public static class AccountKeyPairCreator
			{
				private AsymmetricCryptoKeyPair m_acountKeyPair;
				private static AccountKeyPairCreator m_instance;

				private AccountKeyPairCreator()
					{
						getKeyPair();
					}

				public static synchronized AccountKeyPairCreator getInstance()
					{
						if (m_instance == null)
							{
								m_instance = new AccountKeyPairCreator();
							}
						return m_instance;
					}

				private synchronized AsymmetricCryptoKeyPair getKeyPair()
					{
						if (m_acountKeyPair == null)
							{
								m_acountKeyPair = PayAccountsFile.getInstance().createAccountKeyPair();
							}
						return m_acountKeyPair;
					}

				private synchronized void reset()
					{
						m_acountKeyPair = null;
					}
			}

		private static PayAccount createAccount(AccountKeyPairCreator a_keyPairCreator) throws Exception
			{
				PaymentInstanceDBEntry entry = PayAccountsFile.getInstance().getBI(ms_currentPIID.getId());

				if (entry == null)
					{
						return null;
					}
				return PayAccountsFile.getInstance().createAccount(entry, a_keyPairCreator.getKeyPair(), null, true);
			}

		/**
		 * 
		 * @param a_code
		 * @return
		 * @throws Exception
		 *           if the account could not be created
		 */
		public static boolean validateCoupon(String a_code, AccountKeyPairCreator a_keyPairCreator) throws Exception
			{
				synchronized (SYNC_COUPON)
					{
						if ((a_code = PayAccount.checkCouponCode(a_code)) == null)
							{
								return false;
							}

						Exception ex = null;
						if (ms_currentlyCreatedAccount == null)
							{
								try
									{
										ms_currentlyCreatedAccount = createAccount(a_keyPairCreator);
									}
								catch (Exception a_e)
									{
										ex = a_e;
									}
								if (ms_currentlyCreatedAccount == null || ex != null)
									{
										if (ms_currentlyCreatedAccount != null)
											{
												ms_currentlyCreatedAccount.unlock();
												PayAccountsFile.getInstance().deleteAccount(ms_currentlyCreatedAccount);
											}
										LogHolder.log(LogLevel.EMERG, LogType.PAY, "Could not create account for PI "
												+ Controller.getActivePaymentInstance().getName() + "!");
										if (ex != null)
											{
												throw ex;
											}
										return false;
									}
								else
									{
										a_keyPairCreator.reset();
										saveConfiguration();
									}
							}
						try
							{
								return PayAccountsFile.getInstance().activateCouponCode(a_code, ms_currentlyCreatedAccount, true);
							}
						catch (Exception a_e)
							{
								LogHolder.log(LogLevel.EXCEPTION, LogType.PAY, a_e);
								return false;
							}
					}
			}

		/**
		 * Returns whether the user should recharge.
		 * 
		 * @return whether the user should recharge
		 */
		public static boolean shouldRecharge()
			{
				if (getCurrentCredit(System.currentTimeMillis() + TIMEOUT_RECHARGE, true) == 0
						|| getCurrentCredit() <= 500000000)
					{
						return true;
					}
				return false;
			}

		/**
		 * Returns the current credit in bytes.
		 * 
		 * @return
		 */
		public static long getCurrentCredit()
			{
				return getCurrentCredit(System.currentTimeMillis(), true);
			}

		public static long getInactiveAccountsCurrentCredit()
			{
				return getCurrentCredit(System.currentTimeMillis(), false);
			}

		private static long getCurrentCredit(long a_time, boolean a_bActiveAccounts)
			{
				long credits = 0;
				PayAccount account;
				Vector accounts = PayAccountsFile.getInstance().getAccounts(getActivePaymentInstance().getId());
				for (int i = 0; i < accounts.size(); i++)
					{
						account = ((PayAccount) accounts.elementAt(i));
						if (account.isCharged(new Timestamp(a_time))
								&& ((a_bActiveAccounts && (account.getPrivateKey() != null && !account.isBlocked())) || (!a_bActiveAccounts && (account
										.getPrivateKey() == null || account.isBlocked()))))
							{
								credits += ((PayAccount) accounts.elementAt(i)).getCurrentCredit(); // recalculate
																																										// value
																																										// to
																																										// bytes
							}
					}
				return credits;
			}

		/**
		 * Call this method always before validation of the coupon code with "null"
		 * as argument. Then execute validateCoupon until the user gives up or the
		 * coupon code works. Then you may call this method again. If it succeeds,
		 * no further validation is needed, as the account has been activated
		 * successfully.
		 * 
		 * TODO finer granularity of error messages needed!! e.g. "X" for new users
		 * 
		 * @param a_code
		 * @return
		 * @throws Exception
		 */
		public static boolean activateCoupon(String a_code) throws Exception
			{
				synchronized (SYNC_COUPON)
					{
						boolean bRet;

						if (ms_currentlyCreatedAccount == null)
							{
								return false;
							}

						PayAccount account = ms_currentlyCreatedAccount;
						ms_currentlyCreatedAccount = null;
						bRet = PayAccountsFile.getInstance().activateCouponCode(a_code, account, false);

						account.unlock();
						if (bRet)
							{
								/*
								 * try { account.fetchAccountInfo(true); } catch (Exception a_e)
								 * { LogHolder.log(LogLevel.EMERG, LogType.PAY,
								 * "A problem occurred while updating your charged account!",
								 * a_e); }
								 */

								LogHolder.log(LogLevel.NOTICE, LogType.PAY,
										"You have created an account with " + account.getCurrentCredit() + " kbytes!");
								checkActiveAccount(account);

								ms_currentlyCreatedAccount = null;
							}
						else
							{
								// this account seems to be unusable; delete it
								PayAccountsFile.getInstance().deleteAccount(account);
							}

						return bRet;
					}
			}

		private static void checkActiveAccount(PayAccount a_account)
			{
				if (a_account == null)
					{
						return;
					}
				synchronized (PayAccountsFile.getInstance())
					{
						PayAccount accountActive = PayAccountsFile.getInstance().getActiveAccount();
						if (accountActive == null
								|| (!isConnected() && (accountActive.getCurrentCredit() == 0 || (!accountActive.getPIID().equals(
										getActivePaymentInstance().getId()) && a_account.getPIID().equals(
										getActivePaymentInstance().getId())))))
							{
								PayAccountsFile.getInstance().setActiveAccount(a_account);
							}
					}
			}

		/**
		 * Tells the program to save the configuration. Note that a configuration is
		 * automatically loaded, but might not be saved unless this method is called
		 * from outside this class.
		 * 
		 * @throws Exception
		 *           if an error occurs while saving the configuration
		 */
		private static synchronized void saveConfiguration() throws Exception
			{
				// delete accounts that are clearly unusable or already completely used
				Enumeration enumAccounts = PayAccountsFile.getInstance().getAccounts();
				PayAccount currentAccount;
				while (enumAccounts.hasMoreElements())
					{
						currentAccount = (PayAccount) enumAccounts.nextElement();
						if (currentAccount.getTransaction() == null
								|| (currentAccount.getBalance() != null && currentAccount.getBalance().getSpent() > 0 && currentAccount
										.getCurrentCredit() == 0))
							{
								// this account was clearly used completely; it it no longer
								// needed
								PayAccountsFile.getInstance().deleteAccount(currentAccount);
							}
					}

				Document doc = XMLUtil.createDocument();
				Element root = doc.createElement(XML_ELEMENT_NAME);
				XMLUtil.setAttribute(root, XML_ATTR_LANGUAGE, getLocale().getLanguage());
				XMLUtil.setAttribute(root, XML_ATTR_COUNTRY, getLocale().getCountry());
				XMLUtil.setAttribute(root, XML_ATTR_LOG_DETAIL, LogHolder.getDetailLevel());
				XMLUtil.setAttribute(root, XML_ATTR_LOG_LEVEL, ms_logger.getLogLevel());
				XMLUtil.setAttribute(root, XML_ATTR_LOG_TYPES, ms_logger.getLogTypes());
				XMLUtil.setAttribute(root, XML_ATTR_AUTOSWITCH, ms_serviceContainer.isServiceAutoSwitched());
				XMLUtil.setAttribute(root, XML_ATTR_AUTO_UPDATE, ms_observableInfoServiceUpdater.isAutoUpdateEnabled());

				XMLUtil.setAttribute(root, XML_ATTR_HTTP_FILTER, ms_bHTTPFilterOn);

				if (!ms_serviceContainer.isServiceAutoSwitched())
					{
						XMLUtil.setAttribute(root, XML_ATTR_DEFAULT_CASCADE, ms_serviceContainer.getCurrentCascade().getId());
					}

				XMLUtil.setAttribute(root, XML_ATTR_PAYMENT_ANONYMOUS_PROXY_CONNECTION,
						ms_iPaymentAnonymousProxyConnection.toString());
				XMLUtil.setAttribute(root, XML_ATTR_INFOSERVICE_ANONYMOUS_PROXY_CONNECTION,
						ms_iInfoServiceAnonymousProxyConnection.toString());

				doc.appendChild(root);
				doc.getDocumentElement().appendChild(Database.getInstance(PaymentInstanceDBEntry.class).toXmlElement(doc));
				doc.getDocumentElement().appendChild(exportAccounts(doc));
				doc.getDocumentElement().appendChild(TrustModel.toXmlElement(doc, TrustModel.XML_ELEMENT_CONTAINER_NAME));
				doc.getDocumentElement().appendChild(Database.getInstance(BlacklistedCascadeIDEntry.class).toXmlElement(doc));

				ms_configuration.write(doc.getDocumentElement());

				LogHolder.log(LogLevel.NOTICE, LogType.MISC, "Configuration was saved.");
			}

		public static boolean updateAccountsFromPaymentInstance()
			{
				synchronized (SYNC_UPDATE_PAYMENT)
					{
						if (ms_tUpdatePayment != null && ms_tUpdatePayment.isAlive())
							{
								// ignore, we are still updating
								return false;
							}

						if (m_bExiting || ms_accountUpdater == null)
							{
								return false;
							}

						ms_tUpdatePayment = new Thread("JonDoController - PaymentAccountAutoUpdate")
						{
							public void run()
								{
									boolean result = ms_accountUpdater.update();

									IServiceEventListener listener = ms_serviceLister;
									if (listener != null)
										{
											if (result)
												{
													listener.paymentUpdateFinished();
												}
											else
												{
													listener.paymentUpdateFailed(new Exception("Payment instance account update failed!"));
												}
										}
								}
						};
						ms_tUpdatePayment.setDaemon(true);
						ms_tUpdatePayment.start();
						return true;
					}
			}

		public static boolean updateDataFromInfoService()
			{
				return updateDataFromInfoService(false, 0);
			}

		/** Returns true if the IS update thread is currently running */
		public static boolean isDataFromInfoServiceUpdating()
			{
				if (ms_tUpdate != null && ms_tUpdate.isAlive())
					{
						// we are still updating
						return true;
					}
				return false;
			}

		public static boolean updateDataFromInfoService(boolean bSynchronised, int milliSecondsTimeOut)
			{
				synchronized (SYNC_UPDATE)
					{
						if (ms_tUpdate != null && ms_tUpdate.isAlive())
							{
								// ignore, we are still updating
								return false;
							}

						if (m_bExiting || ms_vecUpdaters == null)
							{
								return false;
							}

						ms_tUpdate = new Thread("JonDoController - updateDataFromInfoService")
						{
							public void run()
								{
									boolean result;
									Vector<Updater> vecUpdaters = (Vector<Updater>) ms_vecUpdaters.clone();
									Updater firstUpdater = vecUpdaters.firstElement();
									vecUpdaters.remove(0);
									result = firstUpdater.update(vecUpdaters);

									IServiceEventListener listener = ms_serviceLister;
									if (listener != null)
										{
											if (result)
												{
													listener.updateFinished();
												}
											else
												{
													listener.updateFailed(new Exception("InfoService update failed!"));
												}
										}
								}
						};
						ms_tUpdate.setDaemon(true);
						ms_tUpdate.start();
						if (bSynchronised)
							{
								try
									{
										ms_tUpdate.join(milliSecondsTimeOut);
										return !ms_tUpdate.isAlive();
									}
								catch (InterruptedException e)
									{
										return false;
									}
							}
						return true;
					}
			}

		/**
		 * Updates the status from InfoServices.
		 * 
		 * @param cascadeID
		 *          -- the Cascade ID to lock for. Note: to not confuse with the ID
		 *          returned by MixServiceInfo.getId()
		 * @return the most up to date StatusInfo for the Cascade ID, or null if an
		 *         error occurs
		 * @see MixServiceInfo.getId()
		 */
		public static StatusInfo updateStatusFromInfoService(String cascadeID)
			{
				StatusInfo info = null;
				try
					{
						Database db = Database.getInstance(MixCascade.class);
						MixCascade cascade = (MixCascade) db.getEntryById(cascadeID);
						if (cascade != null)
							{
								info = cascade.fetchCurrentStatus();
								if (info != null)
									Database.getInstance(StatusInfo.class).update(info);
							}
					}
				catch (Throwable t)
					{
						LogHolder.log(LogLevel.EXCEPTION, LogType.NET, "Exception while updateStatusFromInfoService()", t);
					}
				return info;
			}

		public static boolean updateAccount(JonDonymAccount a_account)
			{
				try
					{
						XMLAccountInfo info = a_account.getAccount().fetchAccountInfo(true);
						if (info != null)
							{
								return true;
							}
					}
				catch (Exception a_e)
					{
						LogHolder.log(LogLevel.ERR, LogType.PAY, a_e);
					}

				return false;
			}

		/*
		 * @return returns true if the account was deleted; false if it is blocked
		 * for deletion
		 */
		public static boolean deleteAccount(JonDonymAccount a_account)
			{
				if (a_account == null)
					{
						return true;
					}
				if (a_account.getAccount().isLocked())
					{
						return false;
					}
				PayAccountsFile.getInstance().deleteAccount(a_account.getAccount());
				if (!TrustModel.getCurrentTrustModel().isTrusted(getCurrentService().getMixCascade()))
					{
						switchService();
					}
				return true;
			}

		public static void setActiveAccount(JonDonymAccount a_account)
			{
				synchronized (PayAccountsFile.getInstance())
					{
						if (a_account != null && !a_account.equals(getActiveAccount()))
							{
								boolean bRestart = false;
								if (isConnected() && ms_serviceContainer.getCurrentCascade().isPayment())
									{
										bRestart = true;
										stop_internal();
									}
								PayAccountsFile.getInstance().setActiveAccount(a_account.getAccount());
								if (bRestart)
									{
										start();
									}
							}
					}
				if (!TrustModel.getCurrentTrustModel().isTrusted(getCurrentService().getMixCascade()))
					{
						switchService();
					}
			}

		public static boolean haveAccounts()
			{
				return PayAccountsFile.getInstance().getNumAccounts() > 0;
			}

		public static Vector<JonDonymAccount> getAccounts()
			{
				return getAccounts(false);
			}

		public static Vector<JonDonymAccount> getAccounts(boolean a_bMonthlyOnly)
			{
				Vector<JonDonymAccount> vecAccounts = new Vector<JonDonymAccount>();
				Enumeration enumAccounts = PayAccountsFile.getInstance().getAccounts();
				JonDonymAccount account;

				while (enumAccounts.hasMoreElements())
					{
						account = new JonDonymAccount((PayAccount) enumAccounts.nextElement());
						if (account.getAccount().getPIID().equals(getActivePaymentInstance().getId())
								&& (!a_bMonthlyOnly || (account.getMonthlyVolume() > 0 && !account.isCurrentlyInOverusage(new Date())
										&& !account.hasExpired() && !account.isLastMonthOfRate())))
							{
								vecAccounts.addElement(account);
							}
					}

				return vecAccounts;
			}

		public static boolean requestMonthlyOverusage(JonDonymAccount a_account)
			{
				XMLAccountInfo info = null;

				try
					{
						info = a_account.getAccount().requestMonthlyOverusage();
					}
				catch (Exception a_e)
					{
						LogHolder.log(LogLevel.ERR, LogType.PAY, a_e);
					}

				if (info != null)
					{
						ms_paymentEventListener.requestedMonthlyOverusage(a_account, true);
						return true;
					}

				ms_paymentEventListener.requestedMonthlyOverusage(a_account, false);
				return false;
			}

		public static Element exportAccounts(Document a_doc)
			{
				String strPassword = null;
				char[] password = ms_accountPassword;
				if (password != null)
					{
						strPassword = new String(password);
					}

				return PayAccountsFile.getInstance().toXmlElement(a_doc, strPassword, true);
			}

		private static Document exportAccountsToDocument()
			{
				Document doc = XMLUtil.createDocument();
				doc.appendChild(exportAccounts(doc));
				return doc;
			}

		public static void exportAccounts(File a_file) throws IOException
			{
				XMLUtil.write(exportAccountsToDocument(), a_file);
			}

		public static String exportAccounts()
			{
				return XMLUtil.toString(exportAccountsToDocument());
			}

		public static boolean importAccounts(File a_accountFile, boolean a_bForceCorrectPassword)
			{
				String strData = null;
				try
					{
						strData = XMLUtil.toString(XMLUtil.readXMLDocument(a_accountFile));
					}
				catch (Exception a_e)
					{
						LogHolder.log(LogLevel.ERR, LogType.PAY, a_e);
					}
				if (strData != null)
					{
						return importAccounts(strData, a_bForceCorrectPassword);
					}
				return false;
			}

		public static boolean activateAccount(JonDonymAccount a_account)
			{
				if (a_account == null)
					{
						return false;
					}
				if (a_account.isActive())
					{
						return true;
					}

				AbstractMemorizingPasswordReader passwordReader = getPasswordReader();
				try
					{
						a_account.getAccount().decryptPrivateKey(passwordReader);
					}
				catch (Exception a_e)
					{
						LogHolder.log(LogLevel.EXCEPTION, LogType.PAY, a_e);
					}

				return a_account.isActive();
			}

		public static boolean importAccounts(String a_accountData, boolean a_bForceCorrectPassword)
			{
				boolean bImportSucceeded;
				AbstractMemorizingPasswordReader passwordReader = getPasswordReader();
				synchronized (passwordReader)
					{
						passwordReader.reset();
						bImportSucceeded = importAccounts_internal(a_accountData, passwordReader, a_bForceCorrectPassword);
					}
				if (bImportSucceeded)
					{
						LogHolder.log(LogLevel.NOTICE, LogType.PAY, "Configuration was saved.");
						try
							{
								saveConfiguration();
							}
						catch (Exception a_e)
							{
								LogHolder.log(LogLevel.ALERT, LogType.PAY, a_e);
							}

						checkActiveAccount(PayAccountsFile.getInstance().getChargedAccount(getActivePaymentInstance().getId()));
						if (!TrustModel.getCurrentTrustModel().isTrusted(getCurrentService().getMixCascade()))
							{
								switchService();
							}
					}
				return bImportSucceeded;
			}

		private static boolean importAccounts_internal(String a_accountData, IMiscPasswordReader a_pwReader,
				boolean a_bForceCorrectPassword)
			{
				Document doc;

				try
					{
						doc = XMLUtil.toXMLDocument(a_accountData);
					}
				catch (XMLParseException a_e)
					{
						LogHolder.log(LogLevel.EXCEPTION, LogType.PAY, a_e);
						return false;
					}

				if (doc.getDocumentElement().getNodeName().equals(PayAccount.XML_ELEMENT_NAME))
					{
						try
							{
								PayAccount newAccount = new PayAccount(doc.getDocumentElement(), a_pwReader);

								if (a_bForceCorrectPassword && newAccount.getPrivateKey() == null)
									{
										LogHolder.log(LogLevel.EXCEPTION, LogType.PAY, "Could not decrypt account.");
										return false;
									}

								try
									{
										PayAccountsFile.getInstance().addAccount(newAccount);
										return true;
									}
								catch (AccountAlreadyExistingException a_e)
									{
										LogHolder.log(LogLevel.WARNING, LogType.PAY, "Account " + newAccount.getAccountNumber()
												+ " already existed in our configuration and was not added.");

										LogHolder.log(LogLevel.ERR, LogType.PAY, a_e);
										return false;
									}
							}
						catch (Exception a_e)
							{
								LogHolder.log(LogLevel.EXCEPTION, LogType.PAY, a_e);
								return false;
							}
					}
				else
					{
						Element elem = doc.getDocumentElement();
						if (elem.getNodeName().equals("JAP") || elem.getNodeName().equals("JonDo"))
							{
								elem = (Element) XMLUtil.getFirstChildByName(elem, PayAccountsFile.XML_ELEMENT_NAME, true);
								if (elem == null)
									{
										elem = doc.getDocumentElement();
									}
							}

						try
							{
								return PayAccountsFile.getInstance().importAccounts(elem, a_pwReader, a_bForceCorrectPassword);
							}
						catch (Exception a_e)
							{
								LogHolder.log(LogLevel.EXCEPTION, LogType.PAY, a_e);
								return false;
							}
					}
			}

		private static AbstractMemorizingPasswordReader getPasswordReader()
			{
				AbstractMemorizingPasswordReader passwordReader;
				AbstractPasswordReader cPasswordReader = ms_configuration.getPasswordReader();
				if (cPasswordReader != null)
					{
						passwordReader = cPasswordReader.getPasswordReader();
					}
				else
					{
						passwordReader = ms_passwordReaderDummy;
					}
				return passwordReader;
			}

		private static interface IAnonymousProxyConnectionUserDefinition
			{
				EnumAnonymousProxyConnection getUserDefinition();
			}

		private static final class AnonymousProxyConnectionMutableProxyInterface implements IMutableProxyInterface,
				PayAccountsFile.IAffiliateOptOut
			{
				private IAnonymousProxyConnectionUserDefinition m_userDefinition;

				public AnonymousProxyConnectionMutableProxyInterface(IAnonymousProxyConnectionUserDefinition a_userDefinition)
					{
						m_userDefinition = a_userDefinition;
					}

				private IProxyInterfaceGetter m_anonymousGetter = new IProxyInterfaceGetter()
				{
					public ImmutableProxyInterface getProxyInterface()
						{
							if (Controller.isConnected())
								{
									return ms_anonymousProxyInterface;
								}
							return null;
						}
				};

				private IProxyInterfaceGetter m_proxyGetter = ms_proxyInterface.getProxyInterface(false);

				public boolean isAffiliateAllowed()
					{
						return m_userDefinition.getUserDefinition() != EnumAnonymousProxyConnection.ANONYMOUS_PROXY_CONNECTION_ALWAYS;
					}

				public IProxyInterfaceGetter getProxyInterface(boolean a_bAnonInterface)
					{
						if (a_bAnonInterface
								|| m_userDefinition.getUserDefinition() == EnumAnonymousProxyConnection.ANONYMOUS_PROXY_CONNECTION_ALWAYS)
							{
								if (m_userDefinition.getUserDefinition() == EnumAnonymousProxyConnection.ANONYMOUS_PROXY_CONNECTION_NEVER)
									{
										return null;
									}
								return m_anonymousGetter;
							}

						return m_proxyGetter;
					}
			}

		private static class RunnableStarter implements Runnable
			{
				private boolean m_bFinished = false;

				public boolean isFinished()
					{
						return m_bFinished;
					}

				public void run()
					{
						try
							{
								MixCascade cascade;
								cascade = m_cascadeDefault;
								if (cascade != null && TrustModel.getCurrentTrustModel().isTrusted(cascade))
									{
										// implicit
										// cascade = m_cascadeDefault;
									}
								else if (ms_jondonymProxy.getMixCascade() == null)
									{
										cascade = ms_serviceContainer.getNextCascade();
									}
								else
									{
										cascade = ms_jondonymProxy.getMixCascade();
									}

								LogHolder.log(LogLevel.NOTICE, LogType.NET, "Initializing anonymous connection...");

								ms_serviceContainer.setCurrentCascade(cascade);
								try
									{
										ms_jondonymProxy.start(ms_serviceContainer);
									}
								catch (AnonServiceException a_e)
									{
										if (a_e instanceof INotRecoverableException
												&& !(ms_serviceContainer.isReconnectedAutomatically() && ms_serviceContainer
														.isServiceAutoSwitched()))
											{
												throw a_e;
											}
									}
								LogHolder.log(LogLevel.NOTICE, LogType.NET, "Anonymous connection initialized!");
								m_bFinished = true;
							}
						catch (Exception e)
							{
								LogHolder.log(LogLevel.EXCEPTION, LogType.MISC, e);
								m_bFinished = true;
								stop();
							}
						synchronized (SYNC_STARTER)
							{
								ms_starter = null;
								ms_starterThread = null;
								SYNC_STARTER.notifyAll();
							}
					}
			}

		public static boolean createCustomService(String a_customServiceHost, int a_customServicePort)
			{
				synchronized (SYNC_STARTER)
					{
						if (isRunning())
							{
								stop(true);
							}

						if (a_customServiceHost != null)
							{
								try
									{
										m_cascadeDefault = new MixCascade(a_customServiceHost, a_customServicePort);
										return true;
									}
								catch (Exception e)
									{
										LogHolder.log(LogLevel.EMERG, LogType.NET,
												"Could not create custom service with [hostname] and [port]: " + a_customServiceHost + ":"
														+ a_customServicePort);

									}
							}
						return false;
					}
			}

		public static boolean start()
			{
				if (m_bShuttingDown || m_bExiting || ms_jondonymProxy == null)
					{
						return false;
					}

				if (ms_configuration == null)
					{
						LogHolder.log(LogLevel.EMERG, LogType.NET,
								"Could not get listen address from configuration as no configuration exists!");
						return false;
					}

				synchronized (SYNC_STARTER)
					{
						// if != null, could not initialize startup thread as it is still
						// starting up from another command call or we are already up
						if (ms_starter == null)
							{
								ms_starter = new RunnableStarter();
								ms_starterThread = new Thread(ms_starter);
								ms_starterThread.start();
							}
					}

				return true;
			}

		private static void startSocketListener(IAntiCensorshipForwarding a_forwarding) throws IOException
			{
				synchronized (SYNC_STARTER)
					{
						while (ms_starter != null)
							{
								// could not initialize startup thread as it is still starting
								// up from another command call or we are already up
								try
									{
										SYNC_STARTER.wait();
									}
								catch (InterruptedException a_e)
									{
										// ignore
									}
							}

						int port;
						LogHolder.log(LogLevel.NOTICE, LogType.NET, "Starting socket listener...");
						port = ms_configuration.getListenPort();

						if (port <= 0)
							{
								if (a_forwarding != null)
									{
										LogHolder.log(LogLevel.WARNING, LogType.NET,
												"No valid listen port for JonDonym proxy. Only the forwarding server will run.");
										return;
									}
								else
									{
										throw new IOException("Invalid listen port for JonDo proxy: " + port);
									}
							}

						InetAddress host = ms_configuration.getListenHost();

						try
							{
								if (host == null)
									{
										host = InetAddress.getLocalHost();
										ms_socketListener = new ServerSocket(ms_configuration.getListenPort());
										LogHolder
												.log(LogLevel.ALERT, LogType.NET, "Warning: Listener was successfully bound to ALL hosts!");
									}
								else
									{
										ms_socketListener = new ServerSocket(port, 50, host);
										LogHolder.log(LogLevel.ALERT, LogType.NET,
												"Listener was successfully bound to: " + host.getHostAddress() + ":" + port);

										ms_socketListenerTwo = SocketGuard.createVirtualBoxServerSocket(port,
												ms_socketListener.getInetAddress());
										if (ms_socketListenerTwo != null)
											{
												m_bIsVirtualBoxListener = true;
											}
									}

								ms_anonymousProxyInterface = new ProxyInterface(host.getHostAddress(), port, null);
							}

						catch (IOException e)
							{
								throw new IOException("Could not start socket listener on host " + host.getHostAddress() + " and port "
										+ port + ": " + e.getMessage());
							}
					}
			}

		private static class AutoSwitchedMixCascadeContainer extends AbstractAutoSwitchedMixCascadeContainer
			{
				private boolean m_bAutoSwitched = true;

				public AutoSwitchedMixCascadeContainer(MixCascade a_cascade, String a_strStartupServiceId)
					{
						super(false, a_cascade, a_strStartupServiceId);
					}

				public boolean hasUserAllowedPaidServices(String a_PIID)
					{
						return false;
					}

				public void setServiceAutoSwitched(boolean a_bAutoSwitched)
					{
						m_bAutoSwitched = a_bAutoSwitched;
					}

				public boolean isServiceAutoSwitched()
					{
						return m_bAutoSwitched;
					}

				public boolean isReconnectedAutomatically()
					{
						return true;
					}
			}

		private static class MixCascadeUpdater extends AbstractMixCascadeUpdater
			{
				public MixCascadeUpdater(ObservableInfo a_observableInfo)
					{
						super(a_observableInfo);
					}

				protected AbstractDatabaseEntry getPreferredEntry()
					{
						return null; // set current cascade here!
					}

				protected void setPreferredEntry(AbstractDatabaseEntry a_preferredEntry)
					{
						// do nothing
					}
			}

		public static class ForcePremiumIfAccountAvailableAttribute extends TrustAttribute
			{
				public ForcePremiumIfAccountAvailableAttribute(int a_trustCondition, Object a_conditionValue,
						boolean a_bIgnoreNoDataAvailable)
					{
						super(a_trustCondition, a_conditionValue, a_bIgnoreNoDataAvailable);
					}

				public void checkTrust(MixCascade a_cascade) throws TrustException, ServiceSignatureException
					{
						if (a_cascade.isPayment())
							{
								if (a_cascade.getPIID() != null
										&& getActivePaymentInstance().equals(new PaymentInstance(a_cascade.getPIID()))
										&& PayAccountsFile.getInstance().getChargedAccount(a_cascade.getPIID()) != null)
									{
										if (a_cascade.getNumberOfOperators() < 3)
											{
												throw (new TrustException(a_cascade,
														JAPMessages.getString(TrustModel.MSG_EXCEPTION_NOT_ENOUGH_MIXES)));
											}

										return;
									}
								else
									{
										throw new TrustException(a_cascade, JAPMessages.getString(MSG_NO_CHARGED_ACCOUNT));
									}
							}
						else if (PayAccountsFile.getInstance().getChargedAccount(getActivePaymentInstance().getId()) != null)
							{
								throw new TrustException(a_cascade, JAPMessages.getString(TrustModel.MSG_EXCEPTION_FREE_CASCADE));
							}
					}
			};

		private static class UpdaterObservable extends Observable
			{
				private boolean bAutoUpdate;

				public UpdaterObservable(boolean a_bAutoUpdateDefault)
					{
						bAutoUpdate = a_bAutoUpdateDefault;
					}

				public void setAutoUpdate(boolean a_bAutoUpdate)
					{
						synchronized (this)
							{
								if (bAutoUpdate != a_bAutoUpdate)
									{
										bAutoUpdate = a_bAutoUpdate;
										setChanged();
										notifyObservers(observableInfo.getUpdateChanged());
									}
							}
					}

				public boolean isAutoUpdateEnabled()
					{
						return bAutoUpdate;
					}

				private ObservableInfo observableInfo = new ObservableInfo(UpdaterObservable.this)
				{
					public Integer getUpdateChanged()
						{
							return new Integer(0);
						}

					public boolean isUpdateDisabled()
						{
							return !bAutoUpdate;
						}
				};

				public ObservableInfo getObservableInfo()
					{
						return observableInfo;
					}
			}
	}
