/
com/planet_ink/coffee_mud/Abilities/Common/
com/planet_ink/coffee_mud/Abilities/Diseases/
com/planet_ink/coffee_mud/Abilities/Druid/
com/planet_ink/coffee_mud/Abilities/Fighter/
com/planet_ink/coffee_mud/Abilities/Languages/
com/planet_ink/coffee_mud/Abilities/Misc/
com/planet_ink/coffee_mud/Abilities/Prayers/
com/planet_ink/coffee_mud/Abilities/Properties/
com/planet_ink/coffee_mud/Abilities/Skills/
com/planet_ink/coffee_mud/Abilities/Songs/
com/planet_ink/coffee_mud/Abilities/Specializations/
com/planet_ink/coffee_mud/Abilities/Spells/
com/planet_ink/coffee_mud/Abilities/Thief/
com/planet_ink/coffee_mud/Abilities/Traps/
com/planet_ink/coffee_mud/Behaviors/
com/planet_ink/coffee_mud/CharClasses/
com/planet_ink/coffee_mud/CharClasses/interfaces/
com/planet_ink/coffee_mud/Commands/
com/planet_ink/coffee_mud/Commands/interfaces/
com/planet_ink/coffee_mud/Common/
com/planet_ink/coffee_mud/Common/interfaces/
com/planet_ink/coffee_mud/Exits/interfaces/
com/planet_ink/coffee_mud/Items/Armor/
com/planet_ink/coffee_mud/Items/Basic/
com/planet_ink/coffee_mud/Items/BasicTech/
com/planet_ink/coffee_mud/Items/CompTech/
com/planet_ink/coffee_mud/Items/MiscMagic/
com/planet_ink/coffee_mud/Items/Weapons/
com/planet_ink/coffee_mud/Items/interfaces/
com/planet_ink/coffee_mud/Libraries/
com/planet_ink/coffee_mud/Libraries/interfaces/
com/planet_ink/coffee_mud/Locales/
com/planet_ink/coffee_mud/MOBS/
com/planet_ink/coffee_mud/Races/
com/planet_ink/coffee_mud/Races/interfaces/
com/planet_ink/coffee_mud/WebMacros/
com/planet_ink/coffee_mud/WebMacros/interfaces/
com/planet_ink/coffee_mud/core/
com/planet_ink/coffee_mud/core/collections/
com/planet_ink/coffee_mud/core/interfaces/
com/planet_ink/coffee_mud/core/intermud/
com/planet_ink/coffee_mud/core/intermud/i3/
com/planet_ink/coffee_web/server/
com/planet_ink/siplet/applet/
lib/
resources/factions/
resources/fakedb/
resources/progs/autoplayer/
resources/quests/holidays/
web/
web/admin.templates/
web/admin/grinder/
web/admin/images/
web/clan.templates/
web/pub.templates/
web/pub/images/mxp/
web/pub/sounds/
web/pub/textedit/
package com.planet_ink.coffee_web.util;

import java.net.UnknownHostException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.Set;
import java.util.logging.Logger;

import com.planet_ink.coffee_web.http.MIMEType;
import com.planet_ink.coffee_web.interfaces.FileCacheManager;
import com.planet_ink.coffee_web.interfaces.FileManager;
import com.planet_ink.coffee_web.interfaces.HTTPFileGetter;
import com.planet_ink.coffee_web.interfaces.MimeConverterManager;
import com.planet_ink.coffee_web.interfaces.ServletSessionManager;
import com.planet_ink.coffee_web.interfaces.SimpleServletManager;
import com.planet_ink.coffee_web.server.WebServer;
import com.planet_ink.coffee_mud.core.collections.KeyPairWildSearchTree;
import com.planet_ink.coffee_mud.core.collections.Pair;
import com.planet_ink.coffee_mud.core.collections.Triad;
import com.planet_ink.coffee_mud.core.collections.KeyPairSearchTree;

/*
   Copyright 2012-2019 Bo Zimmerman

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

	   http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/

/**
 * Configuration for coffeewebserver
 * @author Bo Zimmerman
 *
 */
public class CWConfig implements Cloneable
{
	public  static final int	  DEFAULT_HTP_LISTEN_PORT 		= 80;
	public  static final int	  DEFAULT_SSL_PORT 				= 443;

	private static final String   DEFAULT_PAGE 					= "index.html"; // this would normally be configurable as a list
	private static final String   DEFAULT_ERROR_PAGE 			= "root\\errorpage.cwhtml";
	private static final String   DEFAULT_BROWSE_PAGE 			= "root\\browsepage.cwhtml";

	public  static final long  	  DEFAULT_THROTTLE_BYTES 		= Long.MAX_VALUE / 2;

	// configuration for the request thread pool
	private static final int	  DEFAULT_CORE_THREAD_POOL_SIZE	= 1;
	private static final int	  DEFAULT_MAX_THREAD_POOL_SIZE 	= 10;
	private static final int	  DEFAULT_THREAD_KEEP_ALIVE_MS 	= 60 * 1000; // max age of idle threads
	private static final int	  DEFAULT_THREAD_TIMEOUT_SECS	= 30; //Timeout for active request threads
	private static final int	  DEFAULT_THREAD_QUEUE_SIZE		= 500;//Above this and they start getting rejected

	private static final long	  DEFAULT_FILECACHE_EXPIRE_MS	= 5 * 60 * 1000; 		// 5 minutes -- how long a cache entry lived
	private static final long	  DEFAULT_FILECACHE_MAX_BYTES	= 10 * 1024 * 1024;	// the maximum number of bytes this cache will hold total
	private static final long	  DEFAULT_FILECACHE_MAX_FBYTES	= 2 * 1024 * 1024;	// the maximum size of file the cache will hold

	private static final long	  DEFAULT_FILECOMP_MAX_FBYTES	= 16 * 1024 * 1024;	// the maximum size of file the cache will hold

	private static final long	  DEFAULT_MAX_BODY_BYTES   		= 1024 * 1024 * 2; // maximum size of a request body
	private static final long	  DEFAULT_MAX_IDLE_MILLIS  		= 30 * 1000;		// maximum time a request can be idle (between reads)
	private static final int	  DEFAULT_LINE_BUFFER_SIZE		= 4096; // maximum length of a single line in the main request
	private static final int	  DEFAULT_MAX_ALIVE_SECS 		= 15;	// maximum age, in seconds, of a request connection
	private static final int	  DEFAULT_MAX_PIPELINED_REQUESTS= 10;	// maximum number of requests per connection

	private static final String   DEFAULT_SSL_KEYSTORE_TYPE		= "JKS";
	private static final String   DEFAULT_SSL_KEYMANAGER_ENC	= "SunX509";

	private static final String   DEFAULT_DEBUG_FLAG			= "OFF";

	private static final String   DEFAULT_ACCESSLOG_FLAG		= "OFF";

	private static final long	  DEFAULT_SESSION_IDLE_MILLIS 	= 30 * 60 * 1000;		// maximum time a session can be idle (between requests)
	private static final long	  DEFAULT_SESSION_AGE_MILLIS  	= 24 *60 * 60 * 1000;	// maximum time a session can be in existence

	private static final Integer  ALL_PORTS						= Integer.valueOf(-1);
	private static final String   ALL_HOSTS						= "";

	private Map<String,String>	 	 servlets					= new HashMap<String,String>();
	private Map<String,String>	  	 fileConverts				= new HashMap<String,String>();

	private final Map<String,String> miscFlags					= new HashMap<String,String>();
	private final Set<DisableFlag>	 disableFlags				= new HashSet<DisableFlag>();

	private SimpleServletManager  servletMan					= null;
	private ServletSessionManager sessions						= null;
	private MimeConverterManager  converters					= null;
	private FileCacheManager	  fileCache						= null;
	private Logger				  logger						= null;
	private HTTPFileGetter		  fileGetter					= null;
	private WebServer			  coffeeWebServer				= null;
	private FileManager			  fileManager					= new CWFileManager();

	private String  sslKeystorePath		 = null;
	private String  sslKeystorePassword  = null;
	private String	sslKeystoreType		 = DEFAULT_SSL_KEYSTORE_TYPE;
	private String	sslKeyManagerEncoding= DEFAULT_SSL_KEYMANAGER_ENC;

	private int[]	httpListenPorts		 = new int[]{DEFAULT_HTP_LISTEN_PORT};
	private int[]	httpsListenPorts	 = new int[]{DEFAULT_SSL_PORT};
	private String 	bindAddress			 = null;

	private int		coreThreadPoolSize	 = DEFAULT_CORE_THREAD_POOL_SIZE;
	private int		maxThreadPoolSize	 = DEFAULT_MAX_THREAD_POOL_SIZE;
	private int		maxThreadIdleMs		 = DEFAULT_THREAD_KEEP_ALIVE_MS;
	private int		maxThreadTimeoutSecs = DEFAULT_THREAD_TIMEOUT_SECS;
	private int		maxThreadQueueSize	 = DEFAULT_THREAD_QUEUE_SIZE;

	private long	fileCacheExpireMs	 = DEFAULT_FILECACHE_EXPIRE_MS;
	private long	fileCacheMaxBytes	 = DEFAULT_FILECACHE_MAX_BYTES;
	private long	fileCacheMaxFileBytes= DEFAULT_FILECACHE_MAX_FBYTES;
	private long	fileCompMaxFileBytes = DEFAULT_FILECOMP_MAX_FBYTES;

	private long	sessionMaxIdleMs	 = DEFAULT_SESSION_IDLE_MILLIS;
	private long	sessionMaxAgeMs		 = DEFAULT_SESSION_AGE_MILLIS;

	private long	requestMaxBodyBytes	 = DEFAULT_MAX_BODY_BYTES;
	private long	requestMaxIdleMs	 = DEFAULT_MAX_IDLE_MILLIS;
	private long	requestLineBufBytes	 = DEFAULT_LINE_BUFFER_SIZE;
	private int		requestMaxAliveSecs	 = DEFAULT_MAX_ALIVE_SECS;
	private int		requestMaxPerConn	 = DEFAULT_MAX_PIPELINED_REQUESTS;

	private String	defaultPage			 = DEFAULT_PAGE;
	private String	errorPage			 = DEFAULT_ERROR_PAGE;
	private String	browsePage			 = DEFAULT_BROWSE_PAGE;

	private String  debugFlag			 = DEFAULT_DEBUG_FLAG;
	private boolean isDebugging			 = false;

	private String  accessLogFlag		 = DEFAULT_ACCESSLOG_FLAG;

	private Map<String,Map<Integer,KeyPairSearchTree<String>>> 		mounts	= new HashMap<String,Map<Integer,KeyPairSearchTree<String>>>();
	private Map<String,Map<Integer,KeyPairSearchTree<WebAddress>>>  fwds	= new HashMap<String,Map<Integer,KeyPairSearchTree<WebAddress>>>();
	private Map<String,Map<Integer,KeyPairSearchTree<ThrottleSpec>>>outs	= new HashMap<String,Map<Integer,KeyPairSearchTree<ThrottleSpec>>>();
	private Map<String,Map<Integer,KeyPairSearchTree<ChunkSpec>>>	chunks	= new HashMap<String,Map<Integer,KeyPairSearchTree<ChunkSpec>>>();
	private Map<String,Map<Integer,KeyPairSearchTree<String>>> 		browse	= new HashMap<String,Map<Integer,KeyPairSearchTree<String>>>();
	private Map<String,Map<Integer,KeyPairSearchTree<String>>> 		cgimnts	= new HashMap<String,Map<Integer,KeyPairSearchTree<String>>>();

	public enum DupPolicy { ENUMERATE, OVERWRITE }

	public enum DisableFlag { RANGED }

	private DupPolicy dupPolicy = DupPolicy.OVERWRITE;

	/**
	 * @return the debugFlag
	 */
	public final String getDebugFlag()
	{
		return debugFlag;
	}
	/**
	 * @param debugFlag the defaultDebugFlag to set
	 */
	public final void setDebugFlag(final String debugFlag)
	{
		this.debugFlag = debugFlag;
		isDebugging=!debugFlag.equalsIgnoreCase("OFF");
	}
	/**
	 * @return the isDebugging
	 */
	public final boolean isDebugging()
	{
		return isDebugging;
	}

	/**
	 * @return the dupPolicy
	 */
	public final DupPolicy getDupPolicy()
	{
		return dupPolicy;
	}
	/**
	 * @param dupPolicy the dupPolicy to set
	 */
	public final void setDupPolicy(final DupPolicy dupPolicy)
	{
		this.dupPolicy = dupPolicy;
	}
	/**
	 * @param dupPolicy the dupPolicy to set
	 */
	public final void setDupPolicy(final String dupPolicy)
	{
		try
		{
			this.dupPolicy=DupPolicy.valueOf(dupPolicy.toUpperCase().trim());
		}
		catch(final Exception e)
		{
			this.dupPolicy = DupPolicy.OVERWRITE;
		}
	}

	/**
	 * @return the accessLogFlag
	 */
	public final String getAccessLogFlag()
	{
		return accessLogFlag;
	}
	/**
	 * @param accessLogFlag the accessLogFlag to set
	 */
	public final void setAccessLogFlag(final String accessLogFlag)
	{
		this.accessLogFlag = accessLogFlag;
	}

	/**
	 * @return the defaultPage
	 */
	public final String getDefaultPage()
	{
		return defaultPage;
	}

	/**
	 * @return the errorPage
	 */
	public final String getErrorPage()
	{
		return errorPage;
	}

	/**
	 * @return the directory browsePage
	 */
	public final String getBrowsePage()
	{
		return browsePage;
	}

	/**
	 * @param defaultPage the defaultPage to set
	 */
	public final void setDefaultPage(final String defaultPage)
	{
		this.defaultPage = defaultPage;
	}
	/**
	 * @return the fileCacheExpireMs
	 */
	public final long getFileCacheExpireMs()
	{
		return fileCacheExpireMs;
	}
	/**
	 * @param fileCacheExpireMs the fileCacheExpireMs to set
	 */
	public final void setFileCacheExpireMs(final long fileCacheExpireMs)
	{
		this.fileCacheExpireMs = fileCacheExpireMs;
	}

	/**
	 * @return the logger
	 */
	public Logger getLogger()
	{
		return logger;
	}
	/**
	 * @param logger the logger to set
	 */
	public void setLogger(final Logger logger)
	{
		this.logger = logger;
	}
	/**
	 * @return the fileCache
	 */
	public final FileCacheManager getFileCache()
	{
		return fileCache;
	}
	/**
	 * @param fileCache the fileCache to set
	 */
	public final void setFileCache(final FileCacheManager fileCache)
	{
		this.fileCache = fileCache;
	}
	/**
	 * @return the fileManager
	 */
	public final FileManager getFileManager()
	{
		return fileManager;
	}
	/**
	 * @param fileManager the fileManager to set
	 */
	public final void setFileManager(final FileManager fileManager)
	{
		this.fileManager = fileManager;
	}

	/**
	 * @return the fileGetter
	 */
	public HTTPFileGetter getFileGetter()
	{
		return fileGetter;
	}

	/**
	 * @param fileGetter the fileGetter to set
	 */
	public void setFileGetter(final HTTPFileGetter fileGetter)
	{
		this.fileGetter = fileGetter;
	}

	/**
	 * @return the coffeeWebServer
	 */
	public WebServer getCoffeeWebServer()
	{
		return coffeeWebServer;
	}

	/**
	 * @param coffeeWebServer the coffeeWebServer to set
	 */
	public void setCoffeeWebServer(final WebServer coffeeWebServer)
	{
		this.coffeeWebServer = coffeeWebServer;
	}
	/**
	 * @return the sessionMaxIdleMs
	 */
	public final long getSessionMaxIdleMs()
	{
		return sessionMaxIdleMs;
	}
	/**
	 * @param sessionMaxIdleMs the sessionMaxIdleMs to set
	 */
	public final void setSessionMaxIdleMs(final long sessionMaxIdleMs)
	{
		this.sessionMaxIdleMs = sessionMaxIdleMs;
	}
	/**
	 * @return the sessionMaxAgeMs
	 */
	public final long getSessionMaxAgeMs()
	{
		return sessionMaxAgeMs;
	}
	/**
	 * @param sessionMaxAgeMs the sessionMaxAgeMs to set
	 */
	public final void setSessionMaxAgeMs(final long sessionMaxAgeMs)
	{
		this.sessionMaxAgeMs = sessionMaxAgeMs;
	}
	/**
	 * @return the servletMan
	 */
	public final SimpleServletManager getServletMan()
	{
		return servletMan;
	}
	/**
	 * @param servletMan the servletMan to set
	 */
	public final void setServletMan(final SimpleServletManager servletMan)
	{
		this.servletMan = servletMan;
	}
	/**
	 * @return the sessions
	 */
	public final ServletSessionManager getSessions()
	{
		return sessions;
	}
	/**
	 * @param sessions the sessions to set
	 */
	public final void setSessions(final ServletSessionManager sessions)
	{
		this.sessions = sessions;
	}
	/**
	 * @return the converters
	 */
	public final MimeConverterManager getConverters()
	{
		return converters;
	}
	/**
	 * @param converters the converters to set
	 */
	public final void setConverters(final MimeConverterManager converters)
	{
		this.converters = converters;
	}

	/**
	 * @return the bindAddress
	 */
	public final String getBindAddress()
	{
		return bindAddress;
	}

	/**
	 * @param bindAddress the bindAddress to set
	 */
	public final void setBindAddress(final String bindAddress)
	{
		this.bindAddress = bindAddress;
	}

	/**
	 * return the proper pair for the given host and context and port
	 * and the given map of string pairs
	 * @param host the host name to search for, or "" for all hosts
	 * @param port the port to search for, or -1 for all ports
	 * @param context the context to search for -- NOT OPTIONAL!
	 * @return the proper pair for the given host and context and port
	 */
	private final Pair<String,String> getContextPair(final Map<String,Map<Integer,KeyPairSearchTree<String>>> map,
													 final String host, final int port, final String context)
	{
		Map<Integer,KeyPairSearchTree<String>> portMap=map.get(host);
		if(portMap != null)
		{
			KeyPairSearchTree<String> contexts=portMap.get(Integer.valueOf(port));
			if(contexts != null)
			{
				final Pair<String,String> pair=contexts.findLongestValue(context);
				if(pair != null)
					return pair;
			}
			contexts=portMap.get(ALL_PORTS);
			if(contexts != null)
			{
				final Pair<String,String> pair=contexts.findLongestValue(context);
				if(pair != null)
					return pair;
			}
		}
		portMap=map.get(ALL_HOSTS);
		if(portMap != null)
		{
			KeyPairSearchTree<String> contexts=portMap.get(Integer.valueOf(port));
			if(contexts != null)
			{
				final Pair<String,String> pair=contexts.findLongestValue(context);
				if(pair != null)
					return pair;
			}
			contexts=portMap.get(ALL_PORTS);
			if(contexts != null)
			{
				final Pair<String,String> pair=contexts.findLongestValue(context);
				if(pair != null)
					return pair;
			}
		}
		return null;
	}

	/**
	 * return the proper mount for the given host and context and port
	 * @param host the host name to search for, or "" for all hosts
	 * @param port the port to search for, or -1 for all ports
	 * @param context the context to search for -- NOT OPTIONAL!
	 * @return the proper mount for the given host and context and port
	 */
	public final Pair<String,String> getMount(final String host, final int port, final String context)
	{
		return this.getContextPair(mounts, host, port, context);
	}

	/**
	 * return the proper cgi-enabled mount for the given host and context and port
	 * @param host the host name to search for, or "" for all hosts
	 * @param port the port to search for, or -1 for all ports
	 * @param context the context to search for -- NOT OPTIONAL!
	 * @return the proper mount for the given host and context and port
	 */
	public final Pair<String,String> getCGIMount(final String host, final int port, final String context)
	{
		return this.getContextPair(cgimnts, host, port, context);
	}

	/**
	 * return the proper browse code for the given host and context and port
	 * @param host the host name to search for, or "" for all hosts
	 * @param port the port to search for, or -1 for all ports
	 * @param context the context to search for -- NOT OPTIONAL!
	 * @return the proper browse code for the given host and context and port
	 */
	public final String getBrowseCode(final String host, final int port, final String context)
	{
		final Pair<String,String> p = this.getContextPair(browse, host, port, context);
		if(p == null)
			return null;
		return p.second;
	}

	/**
	 * return the proper forward for the given host and context and port
	 * @param host the host name to search for, or "" for all hosts
	 * @param port the port to search for, or -1 for all ports
	 * @param context the context to search for -- NOT OPTIONAL!
	 * @return the proper forward for the given host and context and port
	 */
	public final Pair<String,WebAddress> getPortForward(final String host, final int port, final String context)
	{
		Map<Integer,KeyPairSearchTree<WebAddress>> portMap=fwds.get(host);
		if(portMap != null)
		{
			KeyPairSearchTree<WebAddress> contexts=portMap.get(Integer.valueOf(port));
			if(contexts != null)
			{
				final Pair<String,WebAddress> pair=contexts.findLongestValue(context);
				if(pair != null)
					return pair;
			}
			contexts=portMap.get(ALL_PORTS);
			if(contexts != null)
			{
				final Pair<String,WebAddress> pair=contexts.findLongestValue(context);
				if(pair != null)
					return pair;
			}
		}
		portMap=fwds.get(ALL_HOSTS);
		if(portMap != null)
		{
			KeyPairSearchTree<WebAddress> contexts=portMap.get(Integer.valueOf(port));
			if(contexts != null)
			{
				final Pair<String,WebAddress> pair=contexts.findLongestValue(context);
				if(pair != null)
					return pair;
			}
			contexts=portMap.get(ALL_PORTS);
			if(contexts != null)
			{
				final Pair<String,WebAddress> pair=contexts.findLongestValue(context);
				if(pair != null)
					return pair;
			}
		}
		return null;
	}

	/**
	 * return the chunked encoding for the given host and context and port
	 * @param host the host name to search for, or "" for all hosts
	 * @param port the port to search for, or -1 for all ports
	 * @param context the context to search for -- NOT OPTIONAL!
	 * @return the chunk for the given host and context and port
	 */
	public final ChunkSpec getChunkSpec(final String host, final int port, final String context)
	{
		Map<Integer,KeyPairSearchTree<ChunkSpec>> portMap=chunks.get(host);
		if(portMap != null)
		{
			KeyPairSearchTree<ChunkSpec> contexts=portMap.get(Integer.valueOf(port));
			if(contexts != null)
			{
				final Pair<String,ChunkSpec> pair=contexts.findLongestValue(context);
				if(pair != null)
					return pair.second;
			}
			contexts=portMap.get(ALL_PORTS);
			if(contexts != null)
			{
				final Pair<String,ChunkSpec> pair=contexts.findLongestValue(context);
				if(pair != null)
					return pair.second;
			}
		}
		portMap=chunks.get(ALL_HOSTS);
		if(portMap != null)
		{
			KeyPairSearchTree<ChunkSpec> contexts=portMap.get(Integer.valueOf(port));
			if(contexts != null)
			{
				final Pair<String,ChunkSpec> pair=contexts.findLongestValue(context);
				if(pair != null)
					return pair.second;
			}
			contexts=portMap.get(ALL_PORTS);
			if(contexts != null)
			{
				final Pair<String,ChunkSpec> pair=contexts.findLongestValue(context);
				if(pair != null)
					return pair.second;
			}
		}
		return null;
	}

	/**
	 * return the throttle bytes, in or out, for the given host and context and port
	 * @param host the host name to search for, or "" for all hosts
	 * @param port the port to search for, or -1 for all ports
	 * @param context the context to search for -- NOT OPTIONAL!
	 * @return the throttle bytes, in or out
	 */
	private final ThrottleSpec getThrottleBytes(final String host, final int port, final String context,
			final Map<String,Map<Integer,KeyPairSearchTree<ThrottleSpec>>> spec)
	{
		Map<Integer,KeyPairSearchTree<ThrottleSpec>> portMap=spec.get(host);
		if(portMap != null)
		{
			KeyPairSearchTree<ThrottleSpec> contexts=portMap.get(Integer.valueOf(port));
			if(contexts != null)
			{
				final Pair<String,ThrottleSpec> pair=contexts.findLongestValue(context);
				if(pair != null)
					return pair.second;
			}
			contexts=portMap.get(ALL_PORTS);
			if(contexts != null)
			{
				final Pair<String,ThrottleSpec> pair=contexts.findLongestValue(context);
				if(pair != null)
					return pair.second;
			}
		}
		portMap=spec.get(ALL_HOSTS);
		if(portMap != null)
		{
			KeyPairSearchTree<ThrottleSpec> contexts=portMap.get(Integer.valueOf(port));
			if(contexts != null)
			{
				final Pair<String,ThrottleSpec> pair=contexts.findLongestValue(context);
				if(pair != null)
					return pair.second;
			}
			contexts=portMap.get(ALL_PORTS);
			if(contexts != null)
			{
				final Pair<String,ThrottleSpec> pair=contexts.findLongestValue(context);
				if(pair != null)
					return pair.second;
			}
		}
		return null;
	}

	/**
	 * return the response (out) throttle bytes spec, for the given host and context and port
	 * @param host the host name to search for, or "" for all hosts
	 * @param port the port to search for, or -1 for all ports
	 * @param context the context to search for -- NOT OPTIONAL!
	 * @return the throttle bytes, in or out
	 */
	public final ThrottleSpec getResponseThrottle(final String host, final int port, final String context)
	{
		return getThrottleBytes(host,port,context,outs);
	}

	/**
	 * @return the sslKeystorePath
	 */
	public final String getSslKeystorePath()
	{
		return sslKeystorePath;
	}
	/**
	 * @param sslKeystorePath the sslKeystorePath to set
	 */
	public final void setSslKeystorePath(final String sslKeystorePath)
	{
		this.sslKeystorePath = sslKeystorePath;
	}
	/**
	 * @return the sslKeystorePassword
	 */
	public final String getSslKeystorePassword()
	{
		return sslKeystorePassword;
	}
	/**
	 * @param sslKeystorePassword the sslKeystorePassword to set
	 */
	public final void setSslKeystorePassword(final String sslKeystorePassword)
	{
		this.sslKeystorePassword = sslKeystorePassword;
	}
	/**
	 * @return the sslKeystoreType
	 */
	public final String getSslKeystoreType()
	{
		return sslKeystoreType;
	}
	/**
	 * @param sslKeystoreType the sslKeystoreType to set
	 */
	public final void setSslKeystoreType(final String sslKeystoreType)
	{
		this.sslKeystoreType = sslKeystoreType;
	}
	/**
	 * @return the sslKeyManagerEncoding
	 */
	public final String getSslKeyManagerEncoding()
	{
		return sslKeyManagerEncoding;
	}
	/**
	 * @param sslKeyManagerEncoding the sslKeyManagerEncoding to set
	 */
	public final void setSslKeyManagerEncoding(final String sslKeyManagerEncoding)
	{
		this.sslKeyManagerEncoding = sslKeyManagerEncoding;
	}
	/**
	 * @return the httpListenPorts
	 */
	public final int[] getHttpListenPorts()
	{
		return httpListenPorts;
	}
	/**
	 * @param httpListenPorts the httpListenPorts to set
	 */
	public final void setHttpListenPorts(final int[] httpListenPorts)
	{
		this.httpListenPorts = httpListenPorts;
	}
	/**
	 * @return the httpsListenPorts
	 */
	public final int[] getHttpsListenPorts()
	{
		return httpsListenPorts;
	}
	/**
	 * @param httpsListenPorts the httpsListenPorts to set
	 */
	public final void setHttpsListenPorts(final int[] httpsListenPorts)
	{
		this.httpsListenPorts = httpsListenPorts;
	}
	/**
	 * @return the coreThreadPoolSize
	 */
	public final int getCoreThreadPoolSize()
	{
		return coreThreadPoolSize;
	}
	/**
	 * @param coreThreadPoolSize the coreThreadPoolSize to set
	 */
	public final void setCoreThreadPoolSize(final int coreThreadPoolSize)
	{
		this.coreThreadPoolSize = coreThreadPoolSize;
	}
	/**
	 * @return the maxThreadPoolSize
	 */
	public final int getMaxThreadPoolSize()
	{
		return maxThreadPoolSize;
	}
	/**
	 * @param maxThreadPoolSize the maxThreadPoolSize to set
	 */
	public final void setMaxThreadPoolSize(final int maxThreadPoolSize)
	{
		this.maxThreadPoolSize = maxThreadPoolSize;
	}
	/**
	 * @return the maxThreadIdleMs
	 */
	public final int getMaxThreadIdleMs()
	{
		return maxThreadIdleMs;
	}
	/**
	 * @param maxThreadIdleMs the maxThreadIdleMs to set
	 */
	public final void setMaxThreadIdleMs(final int maxThreadIdleMs)
	{
		this.maxThreadIdleMs = maxThreadIdleMs;
	}
	/**
	 * @return the maxThreadTimeoutSecs
	 */
	public final int getMaxThreadTimeoutSecs()
	{
		return maxThreadTimeoutSecs;
	}
	/**
	 * @param maxThreadTimeoutSecs the maxThreadTimeoutSecs to set
	 */
	public final void setMaxThreadTimeoutSecs(final int maxThreadTimeoutSecs)
	{
		this.maxThreadTimeoutSecs = maxThreadTimeoutSecs;
	}
	/**
	 * @return the maxThreadQueueSize
	 */
	public final int getMaxThreadQueueSize()
	{
		return maxThreadQueueSize;
	}
	/**
	 * @param maxThreadQueueSize the maxThreadQueueSize to set
	 */
	public final void setMaxThreadQueueSize(final int maxThreadQueueSize)
	{
		this.maxThreadQueueSize = maxThreadQueueSize;
	}

	/**
	 * @param flag the flag to check
	 * @return true if its disabled, false otherwise
	 */
	public final boolean isDisabled(final DisableFlag flag)
	{
		return (flag != null) && disableFlags.contains(flag);
	}

	/**
	 * @return the disable flags
	 */
	public Set<DisableFlag> getDisableFlags()
	{
		return disableFlags;
	}

	/**
	 * @return the fileCacheMaxBytes
	 */
	public final long getFileCacheMaxBytes()
	{
		return fileCacheMaxBytes;
	}
	/**
	 * @param fileCacheMaxBytes the fileCacheMaxBytes to set
	 */
	public final void setFileCacheMaxBytes(final long fileCacheMaxBytes)
	{
		this.fileCacheMaxBytes = fileCacheMaxBytes;
	}
	/**
	 * @return the requestMaxBodyBytes
	 */
	public final long getRequestMaxBodyBytes()
	{
		return requestMaxBodyBytes;
	}
	/**
	 * @return the fileCacheMaxFileBytes
	 */
	public long getFileCacheMaxFileBytes()
	{
		return fileCacheMaxFileBytes;
	}
	/**
	 * @param fileCacheMaxFileBytes the fileCacheMaxFileBytes to set
	 */
	public void setFileCacheMaxFileBytes(final long fileCacheMaxFileBytes)
	{
		this.fileCacheMaxFileBytes = fileCacheMaxFileBytes;
	}
	/**
	 * @return the fileCompMaxFileBytes
	 */
	public long getFileCompMaxFileBytes()
	{
		return fileCompMaxFileBytes;
	}
	/**
	 * @param fileCompMaxFileBytes the fileCompMaxFileBytes to set
	 */
	public void setFileCompMaxFileBytes(final long fileCompMaxFileBytes)
	{
		this.fileCompMaxFileBytes = fileCompMaxFileBytes;
	}
	/**
	 * @param requestMaxBodyBytes the requestMaxBodyBytes to set
	 */
	public final void setRequestMaxBodyBytes(final long requestMaxBodyBytes)
	{
		this.requestMaxBodyBytes = requestMaxBodyBytes;
	}
	/**
	 * @return the requestMaxIdleMs
	 */
	public final long getRequestMaxIdleMs()
	{
		return requestMaxIdleMs;
	}
	/**
	 * @param requestMaxIdleMs the requestMaxIdleMs to set
	 */
	public final void setRequestMaxIdleMs(final long requestMaxIdleMs)
	{
		this.requestMaxIdleMs = requestMaxIdleMs;
	}
	/**
	 * @return the requestLineBufBytes
	 */
	public final long getRequestLineBufBytes()
	{
		return requestLineBufBytes;
	}
	/**
	 * @param requestLineBufBytes the requestLineBufBytes to set
	 */
	public final void setRequestLineBufBytes(final long requestLineBufBytes)
	{
		this.requestLineBufBytes = requestLineBufBytes;
	}
	/**
	 * @return the requestMaxAliveSecs
	 */
	public final int getRequestMaxAliveSecs()
	{
		return requestMaxAliveSecs;
	}
	/**
	 * @param requestMaxAliveSecs the requestMaxAliveSecs to set
	 */
	public final void setRequestMaxAliveSecs(final int requestMaxAliveSecs)
	{
		this.requestMaxAliveSecs = requestMaxAliveSecs;
	}
	/**
	 * @return the requestMaxPerConn
	 */
	public final int getRequestMaxPerConn()
	{
		return requestMaxPerConn;
	}

	/**
	 * @param requestMaxPerConn the requestMaxPerConn to set
	 */
	public final void setRequestMaxPerConn(final int requestMaxPerConn)
	{
		this.requestMaxPerConn = requestMaxPerConn;
	}

	/**
	 * @return the servlets
	 */
	public final Map<String, String> getServlets()
	{
		return servlets;
	}

	/**
	 * @return the fileConverts
	 */
	public final Map<String, String> getFileConverts()
	{
		return fileConverts;
	}

	/**
	 * Get a property as an integer
	 * @param props the props to look in
	 * @param propName the name of the prop
	 * @param defaultVal what to return if its not there, or isn't an int
	 * @return
	 */
	private int getInt(final Properties props, final String propName, final int defaultVal)
	{
		try
		{
			return Integer.parseInt(getString(props,propName,""));
		}
		catch(final Exception e)
		{
			return defaultVal;
		}
	}

	/**
	 * Get a property as an long
	 * @param props the props to look in
	 * @param propName the name of the prop
	 * @param defaultVal what to return if its not there, or isn't an long
	 * @return
	 */
	private long getLong(final Properties props, final String propName, final long defaultVal)
	{
		try
		{
			return Long.parseLong(getString(props,propName,""));
		}
		catch(final Exception e)
		{
			return defaultVal;
		}
	}

	/**
	 * Get a property as a String
	 * @param props the props to look in
	 * @param propName the name of the prop
	 * @param defaultVal what to return if its not there
	 * @return
	 */
	private String getString(final Properties props, final String propName, final String defaultVal)
	{
		if(props.containsKey(propName))
		{
			return ((String)props.get(propName)).trim();
		}
		return defaultVal;
	}

	/**
	 * Returns a copy of this configuration
	 * @return a copy of this object
	 */
	public CWConfig copyOf()
	{
		try
		{
			return (CWConfig)clone();
		}
		catch(final Exception e)
		{
			return this;
		}
	}

	/**
	 * Get integer ports from a comma-delimited list
	 * @param props the properties
	 * @param portListPropName the port list property name
	 * @param defaultPorts the default to use when none found
	 * @return the new ports
	 */
	private int[] getPorts(final Properties props, final String portListPropName, int[] defaultPorts)
	{
		if(props.containsKey(portListPropName))
		{
			final StringBuilder str=new StringBuilder("");
			for(int i=0;i<defaultPorts.length;i++)
			{
				if(i>0) str.append(",");
				str.append(defaultPorts[0]);
			}
			final String[] prop=getString(props,portListPropName,str.toString()).split(",");
			int numPorts=0;
			defaultPorts=new int[prop.length];
			for (final String element : prop)
				try
				{
					defaultPorts[numPorts]=Integer.parseInt(element.trim());
					numPorts++;
				}
				catch (final Exception e)
				{
				}
			defaultPorts=Arrays.copyOf(defaultPorts, numPorts);
		}
		return defaultPorts;
	}

	/**
	 * Returns property keypairs, assuming any exist.  They are formatted with a prefix and a separator
	 * followed by the first being mounted, which is equal to the value.
	 * @param props the properties
	 * @param prefix the prefix that is shared amongst all entries
	 * @return a map of all keypairs found
	 */
	private Map<String,String> getPrefixedPairs(final Properties props, final String prefix, final char separatorChar)
	{
		Map<String,String> newMounts=null;
		for(final Object p : props.keySet())
		{
			if((p instanceof String)
			&&((String)p).toUpperCase().startsWith(prefix+separatorChar))
			{
				if(newMounts==null) newMounts = new HashMap<String,String>();
				final String key=(String)p;
				final String value=props.getProperty(key);
				final String mountPoint=key.substring(prefix.length()+1);
				newMounts.put(mountPoint,value.trim());
			}
		}
		return newMounts;
	}

	/**
	 * Parses a url host:port/context into its constituent parts and returns them
	 * @param value the url
	 * @return the pieces
	 */
	private Triad<String,Integer,String> findHostPortContext(String value)
	{
		int portDex=value.indexOf(':');
		if(portDex<0)
			return new Triad<String,Integer,String>("",ALL_PORTS,"/"+value);
		final int slashDex=value.indexOf('/');
		Integer port=ALL_PORTS;
		final String context;
		if(slashDex>0)
		{
			if(slashDex==value.length()-1)
				context="/";
			else
				context=value.substring(slashDex);
			value=value.substring(0, slashDex);
		}
		else
			context="/";
		portDex=value.indexOf(':');
		if(portDex>0)
		{
			final String possPort=value.substring(portDex+1);
			if(possPort.equals("*"))
				port=ALL_PORTS;
			else
			{
				try
				{
					port=Integer.valueOf(possPort);
				}
				catch(final NumberFormatException ne)
				{
					if(logger != null)
						logger.severe("Illegal port in forward address: "+value);
					return null;
				}
			}
			value=value.substring(0, portDex);
			if(value.equals("*"))
				value=ALL_HOSTS;
		}
		else
		if(value.equals("*"))
		{
			value=ALL_HOSTS;
			port=ALL_PORTS;
		}
		else
		{
			try
			{
				port=Integer.valueOf(value);
				value=ALL_HOSTS;
			}
			catch(final Exception e)
			{
				port=ALL_PORTS;
			}
		}
		return new Triad<String,Integer,String>(value,port,context);
	}

	/**
	 * Returns one of the named properties, even ones that don't
	 * mean anything to coffeewebserver per se.
	 * @param propName the name of the property
	 * @return the value of the property, or null if not found
	 */
	public String getMiscProp(final String propName)
	{
		return miscFlags.get(propName.toUpperCase());
	}

	/**
	 * Parses a mime type list : file size entry
	 * @param value the mime type list, file size entry
	 * @return the pieces
	 */
	private Pair<Set<MIMEType>,Long> findMimesAndFileSizes(final String value)
	{
		final int sizeDex=value.indexOf(':');
		if(sizeDex<0)
			return null;
		final String mimesStr = value.substring(0,sizeDex).trim().toLowerCase();
		final String fileSize = value.substring(sizeDex+1).trim();
		final Long maxFileSize;
		try
		{
			maxFileSize=Long.valueOf(fileSize.trim());
		}
		catch(final NumberFormatException ne)
		{
			if(logger != null)
				logger.severe("Illegal file size in chunk spec: "+value);
			return null;
		}
		final Set<MIMEType> mimeTypes = new HashSet<MIMEType>();
		if((mimesStr.length()>0)&&(!mimesStr.equals("*")))
		{
			final String[] typesSet = mimesStr.split(",");
			for(final String type : typesSet)
			{
				MIMEType mtype = null;
				for(final MIMEType m : MIMEType.All.getValues())
					if((m.name().equals(type))
					||(type.endsWith("*") && m.name().startsWith(type.substring(0,type.length()-1)))
					||(type.startsWith("*") && m.name().endsWith(type.substring(1)))
					||(type.equals(m.getType()))
					||(type.endsWith("*") && m.getType().startsWith(type.substring(0,type.length()-1)))
					||(type.startsWith("*") && m.getType().endsWith(type.substring(1))))
					{
						mtype=m;
						break;
					}
				if(mtype == null)
				{
					for(final MIMEType m : MIMEType.All.getValues())
						if((m.getExt().equals(type))
						||(type.endsWith("*") && m.getExt().startsWith(type.substring(0,type.length()-1)))
						||(type.startsWith("*") && m.getExt().endsWith(type.substring(1))))
						{
							mtype=m;
							break;
						}
				}
				if(mtype == null)
				{
					if(logger != null)
						logger.severe("Illegal mime type spec in chunk spec: "+type);
				}
				else
				if(!mimeTypes.contains(mtype))
					mimeTypes.add(mtype);
			}
		}
		return new Pair<Set<MIMEType>,Long>(mimeTypes,maxFileSize);
	}

	/**
	 * Build throttle spec from given properties file prefix
	 * @param specCache cache of throttle specs for a given host mask
	 * @param props the main props
	 * @param prefix the throttle spec prefix, THROTTLEOUT
	 * @return null for no spec, or a new throttle spec
	 */
	private Map<String,Map<Integer,KeyPairSearchTree<ThrottleSpec>>> getThrottleBytes(final Map<String,ThrottleSpec> specCache, final Properties props, final String prefix)
	{
		final Map<String,String> newThrottles=getPrefixedPairs(props,prefix,'/');
		if(newThrottles != null)
		{
			final HashMap<String,Map<Integer,KeyPairSearchTree<ThrottleSpec>>> throts=new HashMap<String,Map<Integer,KeyPairSearchTree<ThrottleSpec>>>();
			for(final Entry<String,String> p : newThrottles.entrySet())
			{
				final String key=p.getKey();
				final Triad<String,Integer,String> from=findHostPortContext(key);
				if(from == null) continue;
				final String value=p.getValue();
				Map<Integer,KeyPairSearchTree<ThrottleSpec>> portMap=throts.get(from.first);
				if(portMap == null)
				{
					portMap=new HashMap<Integer,KeyPairSearchTree<ThrottleSpec>>();
					throts.put(from.first, portMap);
				}
				KeyPairSearchTree<ThrottleSpec> tree=portMap.get(from.second);
				if(tree == null)
				{
					tree=new KeyPairSearchTree<ThrottleSpec>();
					portMap.put(from.second, tree);
				}
				ThrottleSpec spec = (specCache != null)?specCache.get(key):null;
				if(spec == null)
				{
					long throttleBytes;
					try
					{
						throttleBytes = Long.valueOf(value).longValue();
					}
					catch(final Exception e)
					{
						throttleBytes = DEFAULT_THROTTLE_BYTES;
					}
					spec = new ThrottleSpec(throttleBytes);
					if(specCache != null)
					{
						specCache.put(key, spec);
					}
				}

				tree.addEntry(from.third,  spec);
			}
			return throts;
		}
		return null;
	}

	@SuppressWarnings({ "unchecked", "rawtypes" })
	public Map<String,Map<Integer,KeyPairSearchTree<String>>> getContextMap(final String prefix, final Properties props, final Class<? extends KeyPairSearchTree> treeClass)
	{
		Map<String,Map<Integer,KeyPairSearchTree<String>>> map = null;
		final Map<String,String> pairs=getPrefixedPairs(props,prefix,'/');
		if(pairs != null)
		{
			map=new HashMap<String,Map<Integer,KeyPairSearchTree<String>>>();
			for(final Entry<String,String> p : pairs.entrySet())
			{
				final String key=p.getKey();
				final Triad<String,Integer,String> from=findHostPortContext(key);
				if(from == null) continue;
				Map<Integer,KeyPairSearchTree<String>> portMap=map.get(from.first);
				if(portMap == null)
				{
					portMap=new HashMap<Integer,KeyPairSearchTree<String>>();
					map.put(from.first, portMap);
				}
				KeyPairSearchTree<String> tree=portMap.get(from.second);
				if(tree == null)
				{
					try
					{
						tree=treeClass.newInstance();
						portMap.put(from.second, tree);
					}
					catch (final Exception e)
					{
						e.printStackTrace();
					}
				}
				if(tree != null)
				{
					tree.addEntry(from.third, p.getValue());
				}
			}
		}
		return map;
	}


	/**
	 *
	 * @param props
	 */
	public void load(final Properties props)
	{
		miscFlags.clear();
		for(final Object propName : props.keySet())
			miscFlags.put(propName.toString().toUpperCase(), getString(props,propName.toString(),""));
		sslKeystorePath=getString(props,"SSLKEYSTOREPATH",sslKeystorePath);
		sslKeystorePassword=getString(props,"SSLKEYSTOREPASSWORD",sslKeystorePassword);
		sslKeystoreType=getString(props,"SSLKEYSTORETYPE",sslKeystoreType);
		sslKeyManagerEncoding=getString(props,"SSLKEYMANAGERENCODING",sslKeyManagerEncoding);

		httpListenPorts=getPorts(props,"PORT",httpListenPorts);
		httpsListenPorts=getPorts(props,"SSLPORT",httpsListenPorts);
		bindAddress=getString(props,"BIND",null);

		coreThreadPoolSize=getInt(props,"CORETHREADPOOLSIZE",coreThreadPoolSize);
		maxThreadPoolSize=getInt(props,"MAXTHREADS",maxThreadPoolSize);
		maxThreadIdleMs=getInt(props,"MAXTHREADIDLEMILLIS",maxThreadIdleMs);
		maxThreadQueueSize=getInt(props,"MAXTHREADQUEUESIZE",maxThreadIdleMs);
		if(!props.containsKey("MAXTHREADTIMEOUTSECS") && props.containsKey("REQUESTTIMEOUTMINS"))
			maxThreadTimeoutSecs=getInt(props,"REQUESTTIMEOUTMINS",1)*60;
		else
			maxThreadTimeoutSecs=getInt(props,"MAXTHREADTIMEOUTSECS",maxThreadTimeoutSecs);
		fileCacheExpireMs=getLong(props,"FILECACHEEXPIREMS",fileCacheExpireMs);
		fileCacheMaxBytes=getLong(props,"FILECACHEMAXBYTES",fileCacheMaxBytes);
		fileCacheMaxFileBytes=getLong(props,"FILECACHEMAXFILEBYTES",fileCacheMaxFileBytes);
		fileCompMaxFileBytes=getLong(props,"FILECOMPMAXBYTES",fileCompMaxFileBytes);

		requestMaxBodyBytes=getLong(props,"REQUESTMAXBODYBYTES",requestMaxBodyBytes);
		requestMaxIdleMs=getLong(props,"REQUESTMAXIDLEMS",requestMaxIdleMs);
		requestLineBufBytes=getLong(props,"REQUESTLINEBUFBYTES",requestLineBufBytes);
		if(!props.containsKey("REQUESTMAXALIVESECS") && props.containsKey("REQUESTTIMEOUTMINS"))
			requestMaxAliveSecs=getInt(props,"REQUESTTIMEOUTMINS",1)*60;
		else
			requestMaxAliveSecs=getInt(props,"REQUESTMAXALIVESECS",requestMaxAliveSecs);
		requestMaxPerConn=getInt(props,"REQUESTMAXPERCONN",requestMaxPerConn);
		defaultPage=getString(props,"DEFAULTPAGE",defaultPage);
		errorPage=getString(props,"ERRORPAGE",errorPage);
		browsePage=getString(props,"BROWSEPAGE",browsePage);
		setDebugFlag(getString(props,"DEBUGFLAG",debugFlag));
		setDupPolicy(getString(props,"DUPPOLICY",dupPolicy.toString()));
		setAccessLogFlag(getString(props,"ACCESSLOGS",accessLogFlag));

		final String[] disableStrs=getString(props,"DISABLE","").split(",");
		disableFlags.clear();
		if((disableStrs.length>0)&&(disableStrs[0].trim().length()>0))
		for(String disable : disableStrs)
		{
			disable=disable.toUpperCase().trim();
			try
			{
				disableFlags.add(DisableFlag.valueOf(disable));
			}
			catch(final Exception e)
			{
				getLogger().severe("Unknown DISABLE flag in coffeeweb.ini: "+disable);
			}
		}

		final Map<String,String> newServlets=getPrefixedPairs(props,"SERVLET",'/');
		if(newServlets != null)
			servlets=newServlets;
		final Map<String,String> newConverts=getPrefixedPairs(props,"MIMECONVERT",'.');
		if(newConverts != null)
			fileConverts=newConverts;
		final Map<String,Map<Integer,KeyPairSearchTree<String>>> newMounts = getContextMap("MOUNT",props,KeyPairSearchTree.class);
		if(newMounts != null)
			mounts = newMounts;
		final Map<String,Map<Integer,KeyPairSearchTree<String>>> newCGIMounts = getContextMap("CGIMOUNT",props,KeyPairWildSearchTree.class);
		if(newCGIMounts != null)
			cgimnts = newCGIMounts;

		final Map<String,Map<Integer,KeyPairSearchTree<String>>> newBrowse = getContextMap("BROWSE",props,KeyPairSearchTree.class);
		if(newBrowse != null)
			browse = newBrowse;

		final Map<String,String> extraMimeTypes=getPrefixedPairs(props,"MIME",'.');
		if(extraMimeTypes != null)
			for(final String key : extraMimeTypes.keySet())
			{
				final String type=extraMimeTypes.get(key);
				if(type.indexOf('/')>0)
					MIMEType.All.addMIMEType(key.toLowerCase(), type);
			}

		final Map<String,String> newForwards=getPrefixedPairs(props,"FORWARD",'/');
		if(newForwards != null)
		{
			fwds=new HashMap<String,Map<Integer,KeyPairSearchTree<WebAddress>>>();
			for(final Entry<String,String> p : newForwards.entrySet())
			{
				final String key=p.getKey();
				final Triad<String,Integer,String> from=findHostPortContext(key);
				if(from == null) continue;
				final String value=p.getValue();
				final Triad<String,Integer,String> to=findHostPortContext(value);
				if(to == null) continue;
				if(to.second==ALL_PORTS)
					to.second=Integer.valueOf(DEFAULT_HTP_LISTEN_PORT);
				Map<Integer,KeyPairSearchTree<WebAddress>> portMap=fwds.get(from.first);
				if(portMap == null)
				{
					portMap=new HashMap<Integer,KeyPairSearchTree<WebAddress>>();
					fwds.put(from.first, portMap);
				}
				KeyPairSearchTree<WebAddress> tree=portMap.get(from.second);
				if(tree == null)
				{
					tree=new KeyPairSearchTree<WebAddress>();
					portMap.put(from.second, tree);
				}
				try
				{
					tree.addEntry(from.third,  new WebAddress(to.first,to.second.intValue(),to.third));
				}
				catch(final UnknownHostException ue)
				{
					getLogger().severe("Unresolved host in forward address: "+value);
					continue;
				}
			}
		}

		final Map<String,ThrottleSpec> specCache = new HashMap<String, ThrottleSpec>();
		final Map<String,Map<Integer,KeyPairSearchTree<ThrottleSpec>>> throttleOutSpec = this.getThrottleBytes(specCache, props, "THROTTLEOUTPUT");
		if(throttleOutSpec != null)
		{
			outs = throttleOutSpec;
		}

		final int chunkSize = getInt(props,"CHUNKSIZE",0);
		final Map<String,String> newChunks=getPrefixedPairs(props,"CHUNKALLOW",'/');
		if((newChunks != null) && (chunkSize > 0))
		{
			chunks=new HashMap<String,Map<Integer,KeyPairSearchTree<ChunkSpec>>>();
			for(final Entry<String,String> p : newChunks.entrySet())
			{
				final String key=p.getKey();
				final Triad<String,Integer,String> from=findHostPortContext(key);
				if(from == null) continue;
				final String value=p.getValue();
				final Pair<Set<MIMEType>,Long> to=findMimesAndFileSizes(value);
				if(to == null) continue;
				Map<Integer,KeyPairSearchTree<ChunkSpec>> portMap=chunks.get(from.first);
				if(portMap == null)
				{
					portMap=new HashMap<Integer,KeyPairSearchTree<ChunkSpec>>();
					chunks.put(from.first, portMap);
				}
				KeyPairSearchTree<ChunkSpec> tree=portMap.get(from.second);
				if(tree == null)
				{
					tree=new KeyPairSearchTree<ChunkSpec>();
					portMap.put(from.second, tree);
				}
				tree.addEntry(from.third,  new ChunkSpec(chunkSize,to.first,to.second.longValue()));
			}
		}

	}
}