package hirondelle.web4j.webmaster;

import hirondelle.web4j.ApplicationInfo;
import hirondelle.web4j.BuildImpl;
import hirondelle.web4j.model.AppException;
import hirondelle.web4j.model.DateTime;
import hirondelle.web4j.readconfig.Config;
import hirondelle.web4j.util.Args;
import hirondelle.web4j.util.Consts;
import hirondelle.web4j.util.Util;

import java.io.PrintWriter;
import java.io.StringWriter;
import java.io.Writer;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.text.DecimalFormat;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.TimeZone;
import java.util.TreeMap;
import java.util.regex.Pattern;

import javax.servlet.ServletConfig;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;

/**
 Email diagnostic information to support staff when an error occurs. 
 
 <P>Uses the following settings in <tt>web.xml</tt>:
<ul>
 <li> <tt>Webmaster</tt> - the 'from' address.
 <li> <tt>TroubleTicketMailingList</tt> - the 'to' addresses for the support staff.
 <li> <tt>PoorPerformanceThreshold</tt> - when the response time exceeds this level, then 
 a <tt>TroubleTicket</tt> is sent
 <li>  <tt>MinimumIntervalBetweenTroubleTickets</tt> - throttles down emission of 
 <tt>TroubleTicket</tt>s, where many might be emitted in rapid succession, from the 
 same underlying cause
</ul>

 <P>The {@link hirondelle.web4j.Controller} will create and send a <tt>TroubleTicket</tt> when: 
 <ul>
 <li>an unexpected problem (a bug) occurs. The bug corresponds to an unexpected 
 {@link Throwable} emitted by either the application or the framework. 
 <li>the response time exceeds the <tt>PoorPerformanceThreshold</tt> configured in 
 <tt>web.xml</tt>.
 </ul> 

 <P><em>Warning</em>: some e-mail spam filters may incorrectly treat the default content of these trouble 
 tickets (example below) as spam.
 
 <P>Example content of a <tt>TroubleTicket</tt>, as returned by {@link #toString()}:
<PRE>
{@code
Error for web application Fish And Chips Club/4.6.2.0
*** java.lang.RuntimeException: Testing application behavior upon failure. ***
--------------------------------------------------------
Time of error : 2011-09-12 19:59:32
Occurred for user : blah
Web application Build Date: Sat Jul 09 00:00:00 ADT 2011
Web application Author : Hirondelle Systems
Web application Link : http://www.web4j.com/
Web application Message : Uses web4j.jar version 4.6.2

Request Info:
--------------------------------------------------------
HTTP Method: GET
Context Path: /fish
ServletPath: /webmaster/testfailure/ForceFailure.do
URI: /fish/webmaster/testfailure/ForceFailure.do
URL: http://localhost:8081/fish/webmaster/testfailure/ForceFailure.do
Header accept = text/html,application/xhtml+xml,application/xml;q=0.9
Header accept-charset = UTF-8,*
Header accept-encoding = gzip,deflate
Header accept-language = en-us,en;q=0.5
Header connection = keep-alive
Header cookie = JSESSIONID=2C326412C32F6F823673A5FBD1C883A7
Header host = localhost:8081
Header keep-alive = 115
Header referer = http://localhost:8081/fish/webmaster/performance/ShowPerformance.do
Header user-agent = Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.22) ..[elided]..
Cookie JSESSIONID=2C326412C32F6F823673A5FBD1C883A7

Client Info:
--------------------------------------------------------
User IP: 127.0.0.1
User hostname: 127.0.0.1

Session Info
--------------------------------------------------------
Logged in user name : blah
Timeout : 900 seconds.
Session Attributes javax.servlet.jsp.jstl.fmt.request.charset = UTF-8
Session Attributes web4j_key_for_form_source_id = 214c125310311a6e4eda5aa6448b3c47d0a85d31
Session Attributes web4j_key_for_locale = en
Session Attributes web4j_key_for_previous_form_source_id = f2d6ae8487e555f90ae7c186f370b05bcf46f0d0

Memory Info:
--------------------------------------------------------
JRE Memory
 total:  66,650,112
 used:    7,515,624 (11%)
 free:   59,134,488 (89%)


Server And Servlet Info:
--------------------------------------------------------
Name: localhost
Port: 8081
Info: Apache Tomcat/6.0.10
JRE default TimeZone: America/Halifax
JRE default Locale: English (Canada)
awt.toolkit: sun.awt.windows.WToolkit
catalina.base: C:\johanley\Projects\TomcatInstance
catalina.home: C:\Program Files\Tomcat6
catalina.useNaming: true
common.loader: ${catalina.home}/lib,${catalina.home}/lib/*.jar
file.encoding: UTF-8
file.encoding.pkg: sun.io
file.separator: \
java.awt.graphicsenv: sun.awt.Win32GraphicsEnvironment
java.awt.printerjob: sun.awt.windows.WPrinterJob
java.class.path: .;C:\Program Files\Java\jre1.6.0_07\lib\ext\QTJava.zip;C:\Program Files\Tomcat6\bin\bootstrap.jar
java.class.version: 49.0
java.endorsed.dirs: C:\Program Files\Tomcat6\endorsed
java.ext.dirs: C:\jdk1.5.0\jre\lib\ext
java.home: C:\jdk1.5.0\jre
java.io.tmpdir: C:\johanley\Projects\TomcatInstance\temp
java.library.path: C:\jdk1.5.0\bin;.;C:\WINDOWS\system32;C:\WINDOWS;C:\jdk1.5.0\bin;..e[lided]..
java.naming.factory.initial: org.apache.naming.java.javaURLContextFactory
java.naming.factory.url.pkgs: org.apache.naming
java.runtime.name: Java(TM) 2 Runtime Environment, Standard Edition
java.runtime.version: 1.5.0_07-b03
java.specification.name: Java Platform API Specification
java.specification.vendor: Sun Microsystems Inc.
java.specification.version: 1.5
java.util.logging.config.file: C:\johanley\Projects\TomcatInstance\conf\logging.properties
java.util.logging.manager: org.apache.juli.ClassLoaderLogManager
java.vendor: Sun Microsystems Inc.
java.vendor.url: http://java.sun.com/
java.vendor.url.bug: http://java.sun.com/cgi-bin/bugreport.cgi
java.version: 1.5.0_07
java.vm.info: mixed mode, sharing
java.vm.name: Java HotSpot(TM) Client VM
java.vm.specification.name: Java Virtual Machine Specification
java.vm.specification.vendor: Sun Microsystems Inc.
java.vm.specification.version: 1.0
java.vm.vendor: Sun Microsystems Inc.
java.vm.version: 1.5.0_07-b03
line.separator: 

os.arch: x86
os.name: Windows XP
os.version: 5.1
package.access: sun.,org.apache.catalina.,org.apache.coyote.,org.apache.tomcat.,org.apache.jasper.,sun.beans.
package.definition: sun.,java.,org.apache.catalina.,org.apache.coyote.,org.apache.tomcat.,org.apache.jasper.
path.separator: ;
server.loader: 
shared.loader: 
sun.arch.data.model: 32
sun.boot.class.path: C:\jdk1.5.0\jre\lib\rt.jar;...[elided]
sun.boot.library.path: C:\jdk1.5.0\jre\bin
sun.cpu.endian: little
sun.cpu.isalist: 
sun.desktop: windows
sun.io.unicode.encoding: UnicodeLittle
sun.jnu.encoding: Cp1252
sun.management.compiler: HotSpot Client Compiler
sun.os.patch.level: Service Pack 3
tomcat.util.buf.StringCache.byte.enabled: true
user.country: CA
user.dir: C:\johanley\Projects\TomcatInstance
user.home: C:\Documents and Settings\John
user.language: en
user.name: John
user.timezone: America/Halifax
user.variant: 
java.class.path: 
.
C:\Program Files\Java\jre1.6.0_07\lib\ext\QTJava.zip
C:\Program Files\Tomcat6\bin\bootstrap.jar
Servlet : Controller
Servlet init-param:  AccessControlDbConnectionString = java:comp/env/jdbc/fish_access
Servlet init-param:  AllowStringAsBuildingBlock = YES
Servlet init-param:  BigDecimalDisplayFormat = #,##0.00
Servlet init-param:  BooleanFalseDisplayFormat = <input type='checkbox' name='false' value='false' readonly notab>
Servlet init-param:  BooleanTrueDisplayFormat = <input type='checkbox' name='true' value='true' checked readonly notab>
Servlet init-param:  CharacterEncoding = UTF-8
Servlet init-param:  DateTimeFormatForPassingParamsToDb = YYYY-MM-DD^hh:mm:ss^YYYY-MM-DD hh:mm:ss
Servlet init-param:  DecimalSeparator = PERIOD
Servlet init-param:  DecimalStyle = HALF_EVEN,2
Servlet init-param:  DefaultDbConnectionString = java:comp/env/jdbc/fish
Servlet init-param:  DefaultLocale = en
Servlet init-param:  DefaultUserTimeZone = America/Halifax
Servlet init-param:  EmptyOrNullDisplayFormat = -
Servlet init-param:  ErrorCodeForDuplicateKey = 1062
Servlet init-param:  ErrorCodeForForeignKey = 1216,1217,1451,1452
Servlet init-param:  FetchSize = 25
Servlet init-param:  FullyValidateFileUploads = ON
Servlet init-param:  HasAutoGeneratedKeys = true
Servlet init-param:  IgnorableParamValue = 
Servlet init-param:  ImplementationFor.hirondelle.web4j.security.LoginTasks = hirondelle.fish.all.preferences.Login
Servlet init-param:  ImplicitMappingRemoveBasePackage = hirondelle.fish
Servlet init-param:  IntegerDisplayFormat = #,###
Servlet init-param:  IsSQLPrecompilationAttempted = true
Servlet init-param:  LoggingDirectory = C:\log\fish\
Servlet init-param:  LoggingLevels = hirondelle.fish.level=FINE, hirondelle.web4j.level=FINE
Servlet init-param:  MailServerConfig = mail.host=mail.blah.com
Servlet init-param:  MailServerCredentials = NONE
Servlet init-param:  MaxFileUploadRequestSize = 1048576
Servlet init-param:  MaxHttpRequestSize = 51200
Servlet init-param:  MaxRequestParamValueSize = 51200
Servlet init-param:  MaxRows = 300
Servlet init-param:  MinimumIntervalBetweenTroubleTickets = 30
Servlet init-param:  PoorPerformanceThreshold = 20
Servlet init-param:  SpamDetectionInFirewall = OFF
Servlet init-param:  SqlEditorDefaultTxIsolationLevel = DATABASE_DEFAULT
Servlet init-param:  SqlFetcherDefaultTxIsolationLevel = DATABASE_DEFAULT
Servlet init-param:  TimeZoneHint = NONE
Servlet init-param:  TranslationDbConnectionString = java:comp/env/jdbc/fish_translation
Servlet init-param:  TroubleTicketMailingList = blah@blah.com
Servlet init-param:  Webmaster = blah@blah.com

Stack Trace:
--------------------------------------------------------
java.lang.RuntimeException: Testing application behavior upon failure.
  at hirondelle.fish.webmaster.testfailure.ForceFailure.execute(ForceFailure.java:29)
  at hirondelle.web4j.Controller.checkOwnershipThenExecuteAction(Unknown Source)
  at hirondelle.web4j.Controller.processRequest(Unknown Source)
  at hirondelle.web4j.Controller.doGet(Unknown Source)
  ...[elided]...
 }
</PRE>
*/
public final class TroubleTicket {

  /** Called by the framework upon startup, to save the name of the server (Servlet 3.0 wouldn't need this). */
 public static void init(ServletConfig aConfig){
   fConfig = aConfig;
 }
  
  /**
   Constructor.
   
   @param aException has caused the problem.
   @param aRequest original underlying HTTP request.
  */
  public TroubleTicket(Throwable aException, HttpServletRequest aRequest){
    fException = aException;
    fRequest = aRequest;
    buildBodyOfMessage();
  }
  
  /**
   Constuctor sets custom content for the body of the email.
   
   <P>When using this constructor, the detailed information shown in the class 
   comment is not generated. 
   @param aCustomBody the desired body of the email.
  */
  public TroubleTicket(String aCustomBody) {
    Args.checkForContent(aCustomBody);
    fRequest = null;
    fException = null;
    fBody.append(aCustomBody);
  }
  
  /**
   Return extensive listing of items which may be useful in solving the problem.
  
   <P>See example in the class comment.
  */
  @Override public String toString(){
    return fBody.toString();
  }

  /**
   Send an email to the <tt>TroubleTicketMailingList</tt> recipients configured in 
   <tt>web.xml</tt>.
    
   <P>If sufficient time has passed since the last email of a <tt>TroubleTicket</tt>, 
   then send an email to the webmaster whose body is {@link #toString}; otherwise do 
   nothing.
  
   <P>Here, "sufficient time" is defined by a setting in <tt>web.xml</tt> named 
   <tt>MinimumIntervalBetweenTroubleTickets</tt>. The intent is to throttle down on 
   emails which likely have the same cause. 
  */
  public void mailToRecipients() throws AppException {
    if ( hasEnoughTimePassedSinceLastEmail() ) {
      sendEmail();
      updateMostRecentTime();
    }
  }
  
  // PRIVATE
  private static ServletConfig fConfig;
  private final HttpServletRequest fRequest;
  private final Throwable fException;
  private final ApplicationInfo fAppInfo = BuildImpl.forApplicationInfo();
  
  private static final boolean DO_NOT_CREATE_SESSION = false;
  private static final Pattern PASSWORD_PATTERN = Pattern.compile(
    "password", Pattern.CASE_INSENSITIVE
  );
  
  /**
   The text which contains all relevant information which may be useful in solving 
   the problem.
  */
  private StringBuilder fBody = new StringBuilder(); 
  
  /**
   The time of the last send of a TroubleTicket email, expressed in 
   milliseconds since the Java epoch.
  
   This static data is shared among requests, and all access to this 
   field must be synchronized. This synchronization should not be a problem in practice,
   since this class is used only when there's a problem.
  */
  private static long fTimeLastEmail;
  
  /** Build fBody from its various parts.  */
  private void buildBodyOfMessage() {
    addExceptionSummary();
    addRequestInfo();
    addClientInfo();
    addSessionInfo();
    addMemoryInfo();
    addServerInfo();
    addStackTrace();
  }
  
  private void addLine(String aLine){
    fBody.append(aLine + Consts.NEW_LINE);
  }
  
  private void addStartOfSection(String aHeader){
    addLine(Consts.EMPTY_STRING);
    addLine(aHeader);
    addLine("--------------------------------------------------------");
  }
  
  private void addExceptionSummary(){
    addStartOfSection(
      "Error for web application " + fAppInfo.getName() + "/" + fAppInfo.getVersion() + 
      "." + Consts.NEW_LINE + "*** "  + fException.toString() + " ***"
    );
    long nowMillis = BuildImpl.forTimeSource().currentTimeMillis();
    TimeZone tz = new Config().getDefaultUserTimeZone();
    DateTime now = DateTime.forInstant(nowMillis, tz);
    addLine("Time of error : " + now.format("YYYY-MM-DD hh:mm:ss"));
    addLine("Occurred for user : " + getLoggedInUser() );
    addLine("Web application Build Date: " + fAppInfo.getBuildDate());
    addLine("Web application Author : " + fAppInfo.getAuthor());
    addLine("Web application Link : " + fAppInfo.getLink());
    addLine("Web application Message : " + fAppInfo.getMessage());
    if ( fException instanceof AppException ) {
      AppException appEx = (AppException)fException;
      Iterator errorsIter = appEx.getMessages().iterator();
      while ( errorsIter.hasNext() ) {
        addLine( errorsIter.next().toString() );
      }
    }
  }
  
  private void addRequestInfo(){
    addStartOfSection("Request Info:");
    addLine("HTTP Method: " + fRequest.getMethod());
    addLine("Context Path: " + fRequest.getContextPath());
    addLine("ServletPath: " + fRequest.getServletPath());
    addLine("URI: " + fRequest.getRequestURI());
    addLine("URL: " + fRequest.getRequestURL().toString());
    addRequestParams();
    addRequestHeaders();
    addCookies();
  }
  
  private void addClientInfo(){
    addStartOfSection("Client Info:");
    addLine("User IP: " + fRequest.getRemoteAddr());
    addLine("User hostname: " + fRequest.getRemoteHost());
  }
  
  private void addServerInfo(){
    addStartOfSection("Server And Servlet Info:");
    addLine("Name: " + fRequest.getServerName());
    addLine("Port: " + fRequest.getServerPort());
    addLine("Info: " + fConfig.getServletContext().getServerInfo()); //in Servlet 3.0 this is attached to the request
    addLine("JRE default TimeZone: " + TimeZone.getDefault().getID());
    addLine("JRE default Locale: " + Locale.getDefault().getDisplayName());
    
    addAllSystemProperties();
    addClassPath();
    addLine("Servlet : " + fConfig.getServletName()); 
    
    Map<String, String> servletParams = new Config().getRawMap();
    servletParams = sortMap(servletParams);
    addMap(servletParams, "Servlet init-param: ");
  }
  
  private void addAllSystemProperties(){
    Map properties = sortMap(System.getProperties());
    Set props = properties.entrySet();
    Iterator iter = props.iterator();
    while ( iter.hasNext() ) {
      Map.Entry entry = (Map.Entry)iter.next();
      addLine(entry.getKey() + ": " + entry.getValue());
    }
  }

  /**
   Since this item tends to be very long, it is useful to place each entry 
   on a separate line.
  */
  private void addClassPath(){
    String JAVA_CLASS_PATH = "java.class.path";
    String classPath = System.getProperty(JAVA_CLASS_PATH);
    List pathElements = Arrays.asList( classPath.split(Consts.PATH_SEPARATOR) );
    StringBuilder result = new StringBuilder(Consts.NEW_LINE);
    Iterator pathElementsIter = pathElements.iterator();
    while ( pathElementsIter.hasNext() ) {
      String pathElement = (String)pathElementsIter.next();
      result.append(pathElement);
      if ( pathElementsIter.hasNext() ) {
        result.append(Consts.NEW_LINE);
      }
    }
    addLine(JAVA_CLASS_PATH + ": " + result.toString());
  }
  
  private void addStackTrace(){
    addStartOfSection("Stack Trace:");
    addLine( getStackTrace(fException) );
  }
  
  private void  addRequestParams(){
    Map paramMap = new HashMap();
    Enumeration namesEnum = fRequest.getParameterNames();
    while ( namesEnum.hasMoreElements() ){
      String name = (String)namesEnum.nextElement();
      String values = Util.getArrayAsString( fRequest.getParameterValues(name) );
      if( isPassword(name)){
        paramMap.put(name, "***(masked)***");
      }
      else {
        paramMap.put(name, values);
      }
    }
    paramMap = sortMap(paramMap);
    addMap(paramMap, "Req Param");
  }
  
  private void addMemoryInfo(){
    addStartOfSection("Memory Info:");
    addLine(getMemory());
  }
  
  private String getMemory(){
    return getTotalUsedFree(Runtime.getRuntime().totalMemory(), Runtime.getRuntime().freeMemory(), "JRE Memory");
  }
  
  private String getTotalUsedFree(long aTotal, long aFree, String aDescription){
    StringBuilder result = new StringBuilder();
    BigDecimal total = new BigDecimal(aTotal);
    BigDecimal used = new BigDecimal(aTotal - aFree);
    BigDecimal free = new BigDecimal(aFree);

    BigDecimal percentUsed = percent(used, total);
    BigDecimal percentFree = percent(free, total);
    
    result.append(aDescription + Consts.NEW_LINE) ;
    String totalText = format(total);
    String format = "%-9s%" + totalText.length() + "s";
    result.append(String.format(format, " total: ", totalText) + Consts.NEW_LINE) ;
    result.append(String.format(format, " used: ", format(used)) +  " (" + percentUsed.intValue() + "%)" + Consts.NEW_LINE) ;
    result.append(String.format(format, " free: ", format(free)) + " (" + percentFree.intValue() + "%)" + Consts.NEW_LINE) ;
    return result.toString();
  }

  private BigDecimal percent(BigDecimal aA, BigDecimal aB){
    BigDecimal ZERO = new BigDecimal("0");
    BigDecimal result = ZERO;
    BigDecimal HUNDRED = new BigDecimal("100");
    if( aB.compareTo(ZERO) != 0) {
      result = aA.divide(aB, 2, RoundingMode.HALF_EVEN);
      result = result.multiply(HUNDRED);
    }
    return result;
  }
  
  private String format(BigDecimal aNumber){
    DecimalFormat format = new DecimalFormat("#,###");
    return format.format(aNumber); 
  }
  
  private boolean isPassword(String aName){
    return Util.contains(PASSWORD_PATTERN, aName);
  }
  
  private void addRequestHeaders(){
    Map headerMap = new HashMap();
    Enumeration namesEnum = fRequest.getHeaderNames();
    while ( namesEnum.hasMoreElements() ) {
      String name = (String) namesEnum.nextElement();
      Enumeration valuesEnum = fRequest.getHeaders(name);
      while ( valuesEnum.hasMoreElements() ) {
        String value = (String)valuesEnum.nextElement();
        headerMap.put(name, value);
      }
    }
    headerMap = sortMap(headerMap);
    addMap(headerMap, "Header");
  }
  
  private void addCookies(){
    if (fRequest.getCookies() == null) return;
    
    List cookies = Arrays.asList(fRequest.getCookies());
    Iterator cookiesIter = cookies.iterator();
    while ( cookiesIter.hasNext() ) {
      Cookie cookie = (Cookie)cookiesIter.next();
      addLine("Cookie " + cookie.getName() + "=" + cookie.getValue());
    }
  }
  
  private String getStackTrace( Throwable aThrowable ) {
    final Writer result = new StringWriter();
    final PrintWriter printWriter = new PrintWriter( result );
    aThrowable.printStackTrace( printWriter );
    return result.toString();
  }  
  
  private void addSessionInfo(){
    addStartOfSection("Session Info");
    
    HttpSession session = fRequest.getSession(DO_NOT_CREATE_SESSION);
    if ( session == null ){
      addLine("No session existed for this request.");
    }
    else {
      addLine("Logged in user name : " + getLoggedInUser());
      addLine("Timeout : " + session.getMaxInactiveInterval() + " seconds.");
      Map<String, String> sessionMap = new HashMap<String, String>();
      Enumeration sessionAttrs = session.getAttributeNames();
      while (sessionAttrs.hasMoreElements()){
        String name = (String)sessionAttrs.nextElement();
        Object value = session.getAttribute(name);
        if( isPassword(name) ){
          sessionMap.put(name, "***(masked)***");
        }
        else {
          sessionMap.put(name, value.toString());
        }
      }
      sessionMap = sortMap(sessionMap);
      addMap(sessionMap, "Session Attributes");
    }
  }
  
  private String getLoggedInUser(){
    String result = null;
    if (fRequest.getUserPrincipal() != null) {
      result = fRequest.getUserPrincipal().getName();
    }
    else {
      result = "NONE"; 
    }
    return result;
  }
  
  private static synchronized boolean hasEnoughTimePassedSinceLastEmail(){
    return (System.currentTimeMillis()-fTimeLastEmail >getMinimumIntervalBetweenEmails());
  }

  private static synchronized void updateMostRecentTime(){
    fTimeLastEmail = System.currentTimeMillis();
  }

  private void sendEmail() throws AppException {
    Emailer emailer = BuildImpl.forEmailer();
    emailer.sendFromWebmaster(new Config().getTroubleTicketMailingList(), getSubject(), toString());
  }
  
  /** Text to appear in all TroubleTicket emails as the "Subject" of the email. */
  private String getSubject(){
    return 
      "Servlet Error. Application : " + fAppInfo.getName() + "" +
      "/" + fAppInfo.getVersion()
    ;
  }
      
  /**
   Convert the number of minutes configured in web.xml into milliseconds.
  */
  private static long getMinimumIntervalBetweenEmails(){
    final long MILLISECONDS_PER_SECOND = 1000;
    final int SECONDS_PER_MINUTE = 60;
    Long MINIMUM_INTERVAL_BETWEEN_TICKETS = new Config().getMinimumIntervalBetweenTroubleTickets();
    return 
      MILLISECONDS_PER_SECOND * SECONDS_PER_MINUTE *
      MINIMUM_INTERVAL_BETWEEN_TICKETS
    ;
  }
  
  private void addMap(Map aMap, String aLineHeader){
    Iterator iter = aMap.keySet().iterator();
    while (iter.hasNext()){
      String name = (String)iter.next();
      String value = (String)aMap.get(name);
      addLine(aLineHeader + " " + name + " = " + value);
    }
  }
  
  private Map sortMap(Map aInput){
    Map result = new TreeMap(String.CASE_INSENSITIVE_ORDER);
    result.putAll(aInput);
    return result;
  }
}