001    package hirondelle.web4j.webmaster;
002    
003    import hirondelle.web4j.ApplicationInfo;
004    import hirondelle.web4j.BuildImpl;
005    import hirondelle.web4j.model.AppException;
006    import hirondelle.web4j.model.DateTime;
007    import hirondelle.web4j.readconfig.InitParam;
008    import hirondelle.web4j.util.Args;
009    import hirondelle.web4j.util.Consts;
010    import hirondelle.web4j.util.Util;
011    
012    import java.io.PrintWriter;
013    import java.io.StringWriter;
014    import java.io.Writer;
015    import java.math.BigDecimal;
016    import java.math.RoundingMode;
017    import java.text.DecimalFormat;
018    import java.util.ArrayList;
019    import java.util.Arrays;
020    import java.util.Enumeration;
021    import java.util.HashMap;
022    import java.util.Iterator;
023    import java.util.List;
024    import java.util.Locale;
025    import java.util.Map;
026    import java.util.Set;
027    import java.util.StringTokenizer;
028    import java.util.TimeZone;
029    import java.util.TreeMap;
030    import java.util.regex.Pattern;
031    
032    import javax.servlet.ServletConfig;
033    import javax.servlet.http.Cookie;
034    import javax.servlet.http.HttpServletRequest;
035    import javax.servlet.http.HttpSession;
036    
037    /**
038     Email diagnostic information to support staff when an error occurs. 
039     
040     <P>Uses the following settings in <tt>web.xml</tt> :
041    <ul>
042     <li> <tt>Webmaster</tt> - the 'from' address.
043     <li> <tt>TroubleTicketMailingList</tt> - the 'to' addresses for the support staff.
044     <li> <tt>PoorPerformanceThreshold</tt> - when the response time exceeds this level, then 
045     a <tt>TroubleTicket</tt> 
046     is sent
047     <li>  <tt>MinimumIntervalBetweenTroubleTickets</tt> - throttles down emission of 
048     <tt>TroubleTicket</tt>s, where many might be emitted in rapid succession, from the 
049     same underlying cause
050    </ul>
051    
052     <P>The {@link hirondelle.web4j.Controller} will create and send a <tt>TroubleTicket</tt> when : 
053     <ul>
054     <li>an unexpected problem (a bug) occurs. The bug corresponds to an unexpected 
055     {@link Throwable} emitted by either the application or the framework. 
056     <li>the response time exceeds the <tt>PoorPerformanceThreshold</tt> configured in 
057     <tt>web.xml</tt>.
058     </ul> 
059     
060     <P>Example content of a <tt>TroubleTicket</tt>, as returned by {@link #toString()} :
061    <PRE>
062    {@code
063    Error for web application Fish And Chips Club/4.6.2.0
064    *** java.lang.RuntimeException: Testing application behavior upon failure. ***
065    --------------------------------------------------------
066    Time of error : 2011-09-12 19:59:32
067    Occurred for user : blah
068    Web application Build Date: Sat Jul 09 00:00:00 ADT 2011
069    Web application Author : Hirondelle Systems
070    Web application Link : http://www.web4j.com/
071    Web application Message : Uses web4j.jar version 4.6.2
072    
073    Request Info:
074    --------------------------------------------------------
075    HTTP Method: GET
076    Context Path: /fish
077    ServletPath: /webmaster/testfailure/ForceFailure.do
078    URI: /fish/webmaster/testfailure/ForceFailure.do
079    URL: http://localhost:8081/fish/webmaster/testfailure/ForceFailure.do
080    Header accept = text/html,application/xhtml+xml,application/xml;q=0.9
081    Header accept-charset = UTF-8,*
082    Header accept-encoding = gzip,deflate
083    Header accept-language = en-us,en;q=0.5
084    Header connection = keep-alive
085    Header cookie = JSESSIONID=2C326412C32F6F823673A5FBD1C883A7
086    Header host = localhost:8081
087    Header keep-alive = 115
088    Header referer = http://localhost:8081/fish/webmaster/performance/ShowPerformance.do
089    Header user-agent = Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.22) ..[elided]..
090    Cookie JSESSIONID=2C326412C32F6F823673A5FBD1C883A7
091    
092    Client Info:
093    --------------------------------------------------------
094    User IP: 127.0.0.1
095    User hostname: 127.0.0.1
096    
097    Session Info
098    --------------------------------------------------------
099    Logged in user name : blah
100    Timeout : 900 seconds.
101    Session Attributes javax.servlet.jsp.jstl.fmt.request.charset = UTF-8
102    Session Attributes web4j_key_for_form_source_id = 214c125310311a6e4eda5aa6448b3c47d0a85d31
103    Session Attributes web4j_key_for_locale = en
104    Session Attributes web4j_key_for_previous_form_source_id = f2d6ae8487e555f90ae7c186f370b05bcf46f0d0
105    
106    Memory Info:
107    --------------------------------------------------------
108    JRE Memory
109     total:  66,650,112
110     used:    7,515,624 (11%)
111     free:   59,134,488 (89%)
112    
113    
114    Server And Servlet Info:
115    --------------------------------------------------------
116    Name: localhost
117    Port: 8081
118    Info: Apache Tomcat/6.0.10
119    JRE default TimeZone: America/Halifax
120    JRE default Locale: English (Canada)
121    awt.toolkit: sun.awt.windows.WToolkit
122    catalina.base: C:\johanley\Projects\TomcatInstance
123    catalina.home: C:\Program Files\Tomcat6
124    catalina.useNaming: true
125    common.loader: ${catalina.home}/lib,${catalina.home}/lib/*.jar
126    file.encoding: UTF-8
127    file.encoding.pkg: sun.io
128    file.separator: \
129    java.awt.graphicsenv: sun.awt.Win32GraphicsEnvironment
130    java.awt.printerjob: sun.awt.windows.WPrinterJob
131    java.class.path: .;C:\Program Files\Java\jre1.6.0_07\lib\ext\QTJava.zip;C:\Program Files\Tomcat6\bin\bootstrap.jar
132    java.class.version: 49.0
133    java.endorsed.dirs: C:\Program Files\Tomcat6\endorsed
134    java.ext.dirs: C:\jdk1.5.0\jre\lib\ext
135    java.home: C:\jdk1.5.0\jre
136    java.io.tmpdir: C:\johanley\Projects\TomcatInstance\temp
137    java.library.path: C:\jdk1.5.0\bin;.;C:\WINDOWS\system32;C:\WINDOWS;C:\jdk1.5.0\bin;..e[lided]..
138    java.naming.factory.initial: org.apache.naming.java.javaURLContextFactory
139    java.naming.factory.url.pkgs: org.apache.naming
140    java.runtime.name: Java(TM) 2 Runtime Environment, Standard Edition
141    java.runtime.version: 1.5.0_07-b03
142    java.specification.name: Java Platform API Specification
143    java.specification.vendor: Sun Microsystems Inc.
144    java.specification.version: 1.5
145    java.util.logging.config.file: C:\johanley\Projects\TomcatInstance\conf\logging.properties
146    java.util.logging.manager: org.apache.juli.ClassLoaderLogManager
147    java.vendor: Sun Microsystems Inc.
148    java.vendor.url: http://java.sun.com/
149    java.vendor.url.bug: http://java.sun.com/cgi-bin/bugreport.cgi
150    java.version: 1.5.0_07
151    java.vm.info: mixed mode, sharing
152    java.vm.name: Java HotSpot(TM) Client VM
153    java.vm.specification.name: Java Virtual Machine Specification
154    java.vm.specification.vendor: Sun Microsystems Inc.
155    java.vm.specification.version: 1.0
156    java.vm.vendor: Sun Microsystems Inc.
157    java.vm.version: 1.5.0_07-b03
158    line.separator: 
159    
160    os.arch: x86
161    os.name: Windows XP
162    os.version: 5.1
163    package.access: sun.,org.apache.catalina.,org.apache.coyote.,org.apache.tomcat.,org.apache.jasper.,sun.beans.
164    package.definition: sun.,java.,org.apache.catalina.,org.apache.coyote.,org.apache.tomcat.,org.apache.jasper.
165    path.separator: ;
166    server.loader: 
167    shared.loader: 
168    sun.arch.data.model: 32
169    sun.boot.class.path: C:\jdk1.5.0\jre\lib\rt.jar;...[elided]
170    sun.boot.library.path: C:\jdk1.5.0\jre\bin
171    sun.cpu.endian: little
172    sun.cpu.isalist: 
173    sun.desktop: windows
174    sun.io.unicode.encoding: UnicodeLittle
175    sun.jnu.encoding: Cp1252
176    sun.management.compiler: HotSpot Client Compiler
177    sun.os.patch.level: Service Pack 3
178    tomcat.util.buf.StringCache.byte.enabled: true
179    user.country: CA
180    user.dir: C:\johanley\Projects\TomcatInstance
181    user.home: C:\Documents and Settings\John
182    user.language: en
183    user.name: John
184    user.timezone: America/Halifax
185    user.variant: 
186    java.class.path: 
187    .
188    C:\Program Files\Java\jre1.6.0_07\lib\ext\QTJava.zip
189    C:\Program Files\Tomcat6\bin\bootstrap.jar
190    Servlet : Controller
191    Servlet init-param:  AccessControlDbConnectionString = java:comp/env/jdbc/fish_access
192    Servlet init-param:  AllowStringAsBuildingBlock = YES
193    Servlet init-param:  BigDecimalDisplayFormat = #,##0.00
194    Servlet init-param:  BooleanFalseDisplayFormat = <input type='checkbox' name='false' value='false' readonly notab>
195    Servlet init-param:  BooleanTrueDisplayFormat = <input type='checkbox' name='true' value='true' checked readonly notab>
196    Servlet init-param:  CharacterEncoding = UTF-8
197    Servlet init-param:  DateTimeFormatForPassingParamsToDb = YYYY-MM-DD^hh:mm:ss^YYYY-MM-DD hh:mm:ss
198    Servlet init-param:  DecimalSeparator = PERIOD
199    Servlet init-param:  DecimalStyle = HALF_EVEN,2
200    Servlet init-param:  DefaultDbConnectionString = java:comp/env/jdbc/fish
201    Servlet init-param:  DefaultLocale = en
202    Servlet init-param:  DefaultUserTimeZone = America/Halifax
203    Servlet init-param:  EmptyOrNullDisplayFormat = -
204    Servlet init-param:  ErrorCodeForDuplicateKey = 1062
205    Servlet init-param:  ErrorCodeForForeignKey = 1216,1217,1451,1452
206    Servlet init-param:  FetchSize = 25
207    Servlet init-param:  FullyValidateFileUploads = ON
208    Servlet init-param:  HasAutoGeneratedKeys = true
209    Servlet init-param:  IgnorableParamValue = 
210    Servlet init-param:  ImplementationFor.hirondelle.web4j.security.LoginTasks = hirondelle.fish.all.preferences.Login
211    Servlet init-param:  ImplicitMappingRemoveBasePackage = hirondelle.fish
212    Servlet init-param:  IntegerDisplayFormat = #,###
213    Servlet init-param:  IsSQLPrecompilationAttempted = true
214    Servlet init-param:  LoggingDirectory = C:\log\fish\
215    Servlet init-param:  LoggingLevels = hirondelle.fish.level=FINE, hirondelle.web4j.level=FINE
216    Servlet init-param:  MailServerConfig = mail.host=mail.blah.com
217    Servlet init-param:  MailServerCredentials = NONE
218    Servlet init-param:  MaxFileUploadRequestSize = 1048576
219    Servlet init-param:  MaxHttpRequestSize = 51200
220    Servlet init-param:  MaxRequestParamValueSize = 51200
221    Servlet init-param:  MaxRows = 300
222    Servlet init-param:  MinimumIntervalBetweenTroubleTickets = 30
223    Servlet init-param:  PoorPerformanceThreshold = 20
224    Servlet init-param:  SpamDetectionInFirewall = OFF
225    Servlet init-param:  SqlEditorDefaultTxIsolationLevel = DATABASE_DEFAULT
226    Servlet init-param:  SqlFetcherDefaultTxIsolationLevel = DATABASE_DEFAULT
227    Servlet init-param:  TimeZoneHint = NONE
228    Servlet init-param:  TranslationDbConnectionString = java:comp/env/jdbc/fish_translation
229    Servlet init-param:  TroubleTicketMailingList = blah@blah.com
230    Servlet init-param:  Webmaster = blah@blah.com
231    
232    Stack Trace:
233    --------------------------------------------------------
234    java.lang.RuntimeException: Testing application behavior upon failure.
235      at hirondelle.fish.webmaster.testfailure.ForceFailure.execute(ForceFailure.java:29)
236      at hirondelle.web4j.Controller.checkOwnershipThenExecuteAction(Unknown Source)
237      at hirondelle.web4j.Controller.processRequest(Unknown Source)
238      at hirondelle.web4j.Controller.doGet(Unknown Source)
239      ...[elided]...
240     }
241    </PRE>
242    */
243    public final class TroubleTicket {
244    
245      /** 
246       Called by the framework upon startup, to extract config information from 
247       <tt>web.xml</tt>.   
248      */
249      public static void init(ServletConfig aConfig, ApplicationInfo aAppInfo){
250        fConfig = aConfig;
251        fMINIMUM_INTERVAL_BETWEEN_TICKETS = new Long(
252          fMinimumIntervalBetweenTickets.fetch(aConfig).getValue()
253        );
254        
255        String timeZoneSetting = fDefaultTimeZone.fetch(aConfig).getValue();
256        fDEFAULT_TIME_ZONE = TimeZone.getTimeZone(timeZoneSetting);
257        
258        fAppInfo = aAppInfo;
259        setTroubleTicketMailingList(aConfig);
260      }
261    
262      /**
263       Constructor.
264       
265       @param aException has caused the problem.
266       @param aRequest original underlying HTTP request.
267      */
268      public TroubleTicket(Throwable aException, HttpServletRequest aRequest){
269        fRequest = aRequest;
270        fException = aException;
271        buildBodyOfMessage();
272      }
273      
274      /**
275       Constuctor sets custom content for the body of the email.
276       
277       <P>When using this constructor, the detailed information shown in the class 
278       comment is not generated. 
279       @param aCustomBody the desired body of the email.
280      */
281      public TroubleTicket(String aCustomBody) {
282        Args.checkForContent(aCustomBody);
283        fRequest = null;
284        fException = null;
285        fBody.append(aCustomBody);
286      }
287      
288      /**
289       Return extensive listing of items which may be useful in solving the problem.
290      
291       <P>See example in the class comment.
292      */
293      @Override public String toString(){
294        return fBody.toString();
295      }
296    
297      /**
298       Send an email to the <tt>TroubleTicketMailingList</tt> recipients configured in 
299       <tt>web.xml</tt>.
300        
301       <P>If sufficient time has passed since the last email of a <tt>TroubleTicket</tt>, 
302       then send an email to the webmaster whose body is {@link #toString}; otherwise do 
303       nothing.
304      
305       <P>Here, "sufficient time" is defined by a setting in <tt>web.xml</tt> named 
306       <tt>MinimumIntervalBetweenTroubleTickets</tt>. The intent is to throttle down on 
307       emails which likely have the same cause. 
308      */
309      public void mailToWebmaster() throws AppException {
310        if ( hasEnoughTimePassedSinceLastEmail() ) {
311          sendEmail();
312          updateMostRecentTime();
313        }
314      }
315      
316      // PRIVATE
317      private static ServletConfig fConfig;
318      private static ApplicationInfo fAppInfo;  
319      private final HttpServletRequest fRequest;
320      private final Throwable fException;
321      private static final boolean DO_NOT_CREATE = false;
322      private static final Pattern PASSWORD_PATTERN = Pattern.compile(
323        "password", Pattern.CASE_INSENSITIVE
324      );
325      
326      /**
327       The text which contains all relevant information which may be useful in solving 
328       the problem.
329      */
330      private StringBuilder fBody = new StringBuilder(); 
331      
332      /**
333       The time of the last send of a TroubleTicket email, expressed in 
334       milliseconds since the Java epoch.
335      
336       This static data is shared among requests, and all access to this 
337       field must be synchronized.
338      */
339      private static long fTimeLastEmail;
340      
341      /** Item configured in web.xml.  */
342      private static InitParam fMinimumIntervalBetweenTickets = new InitParam(
343        "MinimumIntervalBetweenTroubleTickets", "30"
344      );
345      
346      /** Minimum number of minutes between Trouble Tickets.   */
347      private static Long fMINIMUM_INTERVAL_BETWEEN_TICKETS;
348      
349      /** The DefaultUserTimeZone setting in web.xml. Defaults to GMT. */
350      private static final InitParam fDefaultTimeZone = new InitParam("DefaultUserTimeZone", "GMT");
351      private static TimeZone fDEFAULT_TIME_ZONE;
352      
353      /** Item configured in web.xml.  */
354      private static InitParam fTroubleTicketMailingList = new InitParam(
355        "TroubleTicketMailingList", "NONE"
356      );
357      
358      /** 
359       List or email addresses, for all receivers of TroubleTickets. 
360       If empty, then not sent at all.  
361      */
362      private static List<String> fTROUBLE_TICKET_MAILING_LIST = new ArrayList<String>();
363      
364      private static void setTroubleTicketMailingList(ServletConfig aConfig){
365        String rawList = fTroubleTicketMailingList.fetch(aConfig).getValue();
366        StringTokenizer parser = new StringTokenizer(rawList, ",");
367        while (parser.hasMoreElements()){
368          String emailAddr = (String)parser.nextElement();
369          fTROUBLE_TICKET_MAILING_LIST.add(emailAddr);
370        }
371      }
372    
373      /** Build fBody from its various parts.  */
374      private void buildBodyOfMessage() {
375        addExceptionSummary();
376        addRequestInfo();
377        addClientInfo();
378        addSessionInfo();
379        addMemoryInfo();
380        addServerInfo();
381        addStackTrace();
382      }
383      
384      private void addLine(String aLine){
385        fBody.append(aLine + Consts.NEW_LINE);
386      }
387      
388      private void addStartOfSection(String aHeader){
389        addLine(Consts.EMPTY_STRING);
390        addLine(aHeader);
391        addLine("--------------------------------------------------------");
392      }
393      
394      private void addExceptionSummary(){
395        addStartOfSection(
396          "Error for web application " + fAppInfo.getName() + "/" + fAppInfo.getVersion() + 
397          "." + Consts.NEW_LINE + "*** "  + fException.toString() + " ***"
398        );
399        long nowMillis = BuildImpl.forTimeSource().currentTimeMillis();
400        DateTime now = DateTime.forInstant(nowMillis, fDEFAULT_TIME_ZONE);
401        addLine("Time of error : " + now.format("YYYY-MM-DD hh:mm:ss"));
402        addLine("Occurred for user : " + getLoggedInUser() );
403        addLine("Web application Build Date: " + fAppInfo.getBuildDate());
404        addLine("Web application Author : " + fAppInfo.getAuthor());
405        addLine("Web application Link : " + fAppInfo.getLink());
406        addLine("Web application Message : " + fAppInfo.getMessage());
407        if ( fException instanceof AppException ) {
408          AppException appEx = (AppException)fException;
409          Iterator errorsIter = appEx.getMessages().iterator();
410          while ( errorsIter.hasNext() ) {
411            addLine( errorsIter.next().toString() );
412          }
413        }
414      }
415      
416      private void addRequestInfo(){
417        addStartOfSection("Request Info:");
418        addLine("HTTP Method: " + fRequest.getMethod());
419        addLine("Context Path: " + fRequest.getContextPath());
420        addLine("ServletPath: " + fRequest.getServletPath());
421        addLine("URI: " + fRequest.getRequestURI());
422        addLine("URL: " + fRequest.getRequestURL().toString());
423        addRequestParams();
424        addRequestHeaders();
425        addCookies();
426      }
427      
428      private void addClientInfo(){
429        addStartOfSection("Client Info:");
430        addLine("User IP: " + fRequest.getRemoteAddr());
431        addLine("User hostname: " + fRequest.getRemoteHost());
432      }
433      
434      private void addServerInfo(){
435        addStartOfSection("Server And Servlet Info:");
436        addLine("Name: " + fRequest.getServerName());
437        addLine("Port: " + fRequest.getServerPort());
438        addLine("Info: " + fConfig.getServletContext().getServerInfo());
439        addLine("JRE default TimeZone: " + TimeZone.getDefault().getID());
440        addLine("JRE default Locale: " + Locale.getDefault().getDisplayName());
441        
442        addAllSystemProperties();
443        addClassPath();
444        addLine("Servlet : " + fConfig.getServletName());
445        
446        Map<String, Object> servletParams = new HashMap<String, Object>();
447        Enumeration paramNames = fConfig.getInitParameterNames();
448        while (paramNames.hasMoreElements()){
449          String name = (String)paramNames.nextElement();
450          String value = fConfig.getInitParameter(name);
451          servletParams.put(name, value);
452        }
453        servletParams = sortMap(servletParams);
454        addMap(servletParams, "Servlet init-param: ");
455      }
456      
457      private void addAllSystemProperties(){
458        Map properties = sortMap(System.getProperties());
459        Set props = properties.entrySet();
460        Iterator iter = props.iterator();
461        while ( iter.hasNext() ) {
462          Map.Entry entry = (Map.Entry)iter.next();
463          addLine(entry.getKey() + ": " + entry.getValue());
464        }
465      }
466    
467      /**
468       Since this item tends to be very long, it is useful to place each entry 
469       on a separate line.
470      */
471      private void addClassPath(){
472        String JAVA_CLASS_PATH = "java.class.path";
473        String classPath = System.getProperty(JAVA_CLASS_PATH);
474        List pathElements = Arrays.asList( classPath.split(Consts.PATH_SEPARATOR) );
475        StringBuilder result = new StringBuilder(Consts.NEW_LINE);
476        Iterator pathElementsIter = pathElements.iterator();
477        while ( pathElementsIter.hasNext() ) {
478          String pathElement = (String)pathElementsIter.next();
479          result.append(pathElement);
480          if ( pathElementsIter.hasNext() ) {
481            result.append(Consts.NEW_LINE);
482          }
483        }
484        addLine(JAVA_CLASS_PATH + ": " + result.toString());
485      }
486      
487      private void addStackTrace(){
488        addStartOfSection("Stack Trace:");
489        addLine( getStackTrace(fException) );
490      }
491      
492      private void  addRequestParams(){
493        Map paramMap = new HashMap();
494        Enumeration namesEnum = fRequest.getParameterNames();
495        while ( namesEnum.hasMoreElements() ){
496          String name = (String)namesEnum.nextElement();
497          String values = Util.getArrayAsString( fRequest.getParameterValues(name) );
498          if( isPassword(name)){
499            paramMap.put(name, "***(masked)***");
500          }
501          else {
502            paramMap.put(name, values);
503          }
504        }
505        paramMap = sortMap(paramMap);
506        addMap(paramMap, "Req Param");
507      }
508      
509      private void addMemoryInfo(){
510        addStartOfSection("Memory Info:");
511        addLine(getMemory());
512      }
513      
514      private String getMemory(){
515        return getTotalUsedFree(Runtime.getRuntime().totalMemory(), Runtime.getRuntime().freeMemory(), "JRE Memory");
516      }
517      
518      private String getTotalUsedFree(long aTotal, long aFree, String aDescription){
519        StringBuilder result = new StringBuilder();
520        BigDecimal total = new BigDecimal(aTotal);
521        BigDecimal used = new BigDecimal(aTotal - aFree);
522        BigDecimal free = new BigDecimal(aFree);
523    
524        BigDecimal percentUsed = percent(used, total);
525        BigDecimal percentFree = percent(free, total);
526        
527        result.append(aDescription + Consts.NEW_LINE) ;
528        String totalText = format(total);
529        String format = "%-9s%" + totalText.length() + "s";
530        result.append(String.format(format, " total: ", totalText) + Consts.NEW_LINE) ;
531        result.append(String.format(format, " used: ", format(used)) +  " (" + percentUsed.intValue() + "%)" + Consts.NEW_LINE) ;
532        result.append(String.format(format, " free: ", format(free)) + " (" + percentFree.intValue() + "%)" + Consts.NEW_LINE) ;
533        return result.toString();
534      }
535    
536      private BigDecimal percent(BigDecimal aA, BigDecimal aB){
537        BigDecimal ZERO = new BigDecimal("0");
538        BigDecimal result = ZERO;
539        BigDecimal HUNDRED = new BigDecimal("100");
540        if( aB.compareTo(ZERO) != 0) {
541          result = aA.divide(aB, 2, RoundingMode.HALF_EVEN);
542          result = result.multiply(HUNDRED);
543        }
544        return result;
545      }
546      
547      private String format(BigDecimal aNumber){
548        DecimalFormat format = new DecimalFormat("#,###");
549        return format.format(aNumber); 
550      }
551      
552      private boolean isPassword(String aName){
553        return Util.contains(PASSWORD_PATTERN, aName);
554      }
555      
556      private void addRequestHeaders(){
557        Map headerMap = new HashMap();
558        Enumeration namesEnum = fRequest.getHeaderNames();
559        while ( namesEnum.hasMoreElements() ) {
560          String name = (String) namesEnum.nextElement();
561          Enumeration valuesEnum = fRequest.getHeaders(name);
562          while ( valuesEnum.hasMoreElements() ) {
563            String value = (String)valuesEnum.nextElement();
564            headerMap.put(name, value);
565          }
566        }
567        headerMap = sortMap(headerMap);
568        addMap(headerMap, "Header");
569      }
570      
571      private void addCookies(){
572        if (fRequest.getCookies() == null) return;
573        
574        List cookies = Arrays.asList(fRequest.getCookies());
575        Iterator cookiesIter = cookies.iterator();
576        while ( cookiesIter.hasNext() ) {
577          Cookie cookie = (Cookie)cookiesIter.next();
578          addLine("Cookie " + cookie.getName() + "=" + cookie.getValue());
579        }
580      }
581      
582      private String getStackTrace( Throwable aThrowable ) {
583        final Writer result = new StringWriter();
584        final PrintWriter printWriter = new PrintWriter( result );
585        aThrowable.printStackTrace( printWriter );
586        return result.toString();
587      }  
588      
589      private void addSessionInfo(){
590        addStartOfSection("Session Info");
591        
592        HttpSession session = fRequest.getSession(DO_NOT_CREATE);
593        if ( session == null ){
594          addLine("No session existed for this request.");
595        }
596        else {
597          addLine("Logged in user name : " + getLoggedInUser());
598          addLine("Timeout : " + session.getMaxInactiveInterval() + " seconds.");
599          Map<String, String> sessionMap = new HashMap<String, String>();
600          Enumeration sessionAttrs = session.getAttributeNames();
601          while (sessionAttrs.hasMoreElements()){
602            String name = (String)sessionAttrs.nextElement();
603            Object value = session.getAttribute(name);
604            if( isPassword(name) ){
605              sessionMap.put(name, "***(masked)***");
606            }
607            else {
608              sessionMap.put(name, value.toString());
609            }
610          }
611          sessionMap = sortMap(sessionMap);
612          addMap(sessionMap, "Session Attributes");
613        }
614      }
615      
616      private String getLoggedInUser(){
617        String result = null;
618        if (fRequest.getUserPrincipal() != null) {
619          result = fRequest.getUserPrincipal().getName();
620        }
621        else {
622          result = "NONE"; 
623        }
624        return result;
625      }
626      
627      private static synchronized boolean hasEnoughTimePassedSinceLastEmail(){
628        return (System.currentTimeMillis()-fTimeLastEmail >getMinimumIntervalBetweenEmails());
629      }
630      
631      private void sendEmail() throws AppException {
632        Emailer emailer = BuildImpl.forEmailer();
633        emailer.sendFromWebmaster(fTROUBLE_TICKET_MAILING_LIST, getSubject(), toString());
634      }
635      
636      /**
637       Text to appear in all TroubleTicket emails as the "Subject" of the email.
638      */
639      private String getSubject(){
640        return 
641          "Servlet Error. Application : " + fAppInfo.getName() + "" +
642          "/" + fAppInfo.getVersion()
643        ;
644      }
645          
646      private static synchronized void updateMostRecentTime(){
647        fTimeLastEmail = System.currentTimeMillis();
648      }
649    
650      /**
651       Convert the number of minutes configured in web.xml into milliseconds.
652      */
653      private static long getMinimumIntervalBetweenEmails(){
654        final long MILLISECONDS_PER_SECOND = 1000;
655        final int SECONDS_PER_MINUTE = 60;
656        return 
657          MILLISECONDS_PER_SECOND * SECONDS_PER_MINUTE *
658          fMINIMUM_INTERVAL_BETWEEN_TICKETS.longValue()
659        ;
660      }
661      
662      private void addMap(Map aMap, String aLineHeader){
663        Iterator iter = aMap.keySet().iterator();
664        while (iter.hasNext()){
665          String name = (String)iter.next();
666          String value = (String)aMap.get(name);
667          addLine(aLineHeader + " " + name + " = " + value);
668        }
669      }
670      
671      private Map sortMap(Map aInput){
672        Map result = new TreeMap(String.CASE_INSENSITIVE_ORDER);
673        result.putAll(aInput);
674        return result;
675      }
676    }