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.Config;
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.Arrays;
019    import java.util.Enumeration;
020    import java.util.HashMap;
021    import java.util.Iterator;
022    import java.util.List;
023    import java.util.Locale;
024    import java.util.Map;
025    import java.util.Set;
026    import java.util.TimeZone;
027    import java.util.TreeMap;
028    import java.util.regex.Pattern;
029    
030    import javax.servlet.ServletConfig;
031    import javax.servlet.http.Cookie;
032    import javax.servlet.http.HttpServletRequest;
033    import javax.servlet.http.HttpSession;
034    
035    /**
036     Email diagnostic information to support staff when an error occurs. 
037     
038     <P>Uses the following settings in <tt>web.xml</tt>:
039    <ul>
040     <li> <tt>Webmaster</tt> - the 'from' address.
041     <li> <tt>TroubleTicketMailingList</tt> - the 'to' addresses for the support staff.
042     <li> <tt>PoorPerformanceThreshold</tt> - when the response time exceeds this level, then 
043     a <tt>TroubleTicket</tt> is sent
044     <li>  <tt>MinimumIntervalBetweenTroubleTickets</tt> - throttles down emission of 
045     <tt>TroubleTicket</tt>s, where many might be emitted in rapid succession, from the 
046     same underlying cause
047    </ul>
048    
049     <P>The {@link hirondelle.web4j.Controller} will create and send a <tt>TroubleTicket</tt> when: 
050     <ul>
051     <li>an unexpected problem (a bug) occurs. The bug corresponds to an unexpected 
052     {@link Throwable} emitted by either the application or the framework. 
053     <li>the response time exceeds the <tt>PoorPerformanceThreshold</tt> configured in 
054     <tt>web.xml</tt>.
055     </ul> 
056    
057     <P><em>Warning</em>: some e-mail spam filters may incorrectly treat the default content of these trouble 
058     tickets (example below) as spam.
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      /** Called by the framework upon startup, to save the name of the server (Servlet 3.0 wouldn't need this). */
246     public static void init(ServletConfig aConfig){
247       fConfig = aConfig;
248     }
249      
250      /**
251       Constructor.
252       
253       @param aException has caused the problem.
254       @param aRequest original underlying HTTP request.
255      */
256      public TroubleTicket(Throwable aException, HttpServletRequest aRequest){
257        fException = aException;
258        fRequest = aRequest;
259        buildBodyOfMessage();
260      }
261      
262      /**
263       Constuctor sets custom content for the body of the email.
264       
265       <P>When using this constructor, the detailed information shown in the class 
266       comment is not generated. 
267       @param aCustomBody the desired body of the email.
268      */
269      public TroubleTicket(String aCustomBody) {
270        Args.checkForContent(aCustomBody);
271        fRequest = null;
272        fException = null;
273        fBody.append(aCustomBody);
274      }
275      
276      /**
277       Return extensive listing of items which may be useful in solving the problem.
278      
279       <P>See example in the class comment.
280      */
281      @Override public String toString(){
282        return fBody.toString();
283      }
284    
285      /**
286       Send an email to the <tt>TroubleTicketMailingList</tt> recipients configured in 
287       <tt>web.xml</tt>.
288        
289       <P>If sufficient time has passed since the last email of a <tt>TroubleTicket</tt>, 
290       then send an email to the webmaster whose body is {@link #toString}; otherwise do 
291       nothing.
292      
293       <P>Here, "sufficient time" is defined by a setting in <tt>web.xml</tt> named 
294       <tt>MinimumIntervalBetweenTroubleTickets</tt>. The intent is to throttle down on 
295       emails which likely have the same cause. 
296      */
297      public void mailToRecipients() throws AppException {
298        if ( hasEnoughTimePassedSinceLastEmail() ) {
299          sendEmail();
300          updateMostRecentTime();
301        }
302      }
303      
304      // PRIVATE
305      private static ServletConfig fConfig;
306      private final HttpServletRequest fRequest;
307      private final Throwable fException;
308      private final ApplicationInfo fAppInfo = BuildImpl.forApplicationInfo();
309      
310      private static final boolean DO_NOT_CREATE_SESSION = false;
311      private static final Pattern PASSWORD_PATTERN = Pattern.compile(
312        "password", Pattern.CASE_INSENSITIVE
313      );
314      
315      /**
316       The text which contains all relevant information which may be useful in solving 
317       the problem.
318      */
319      private StringBuilder fBody = new StringBuilder(); 
320      
321      /**
322       The time of the last send of a TroubleTicket email, expressed in 
323       milliseconds since the Java epoch.
324      
325       This static data is shared among requests, and all access to this 
326       field must be synchronized. This synchronization should not be a problem in practice,
327       since this class is used only when there's a problem.
328      */
329      private static long fTimeLastEmail;
330      
331      /** Build fBody from its various parts.  */
332      private void buildBodyOfMessage() {
333        addExceptionSummary();
334        addRequestInfo();
335        addClientInfo();
336        addSessionInfo();
337        addMemoryInfo();
338        addServerInfo();
339        addStackTrace();
340      }
341      
342      private void addLine(String aLine){
343        fBody.append(aLine + Consts.NEW_LINE);
344      }
345      
346      private void addStartOfSection(String aHeader){
347        addLine(Consts.EMPTY_STRING);
348        addLine(aHeader);
349        addLine("--------------------------------------------------------");
350      }
351      
352      private void addExceptionSummary(){
353        addStartOfSection(
354          "Error for web application " + fAppInfo.getName() + "/" + fAppInfo.getVersion() + 
355          "." + Consts.NEW_LINE + "*** "  + fException.toString() + " ***"
356        );
357        long nowMillis = BuildImpl.forTimeSource().currentTimeMillis();
358        TimeZone tz = new Config().getDefaultUserTimeZone();
359        DateTime now = DateTime.forInstant(nowMillis, tz);
360        addLine("Time of error : " + now.format("YYYY-MM-DD hh:mm:ss"));
361        addLine("Occurred for user : " + getLoggedInUser() );
362        addLine("Web application Build Date: " + fAppInfo.getBuildDate());
363        addLine("Web application Author : " + fAppInfo.getAuthor());
364        addLine("Web application Link : " + fAppInfo.getLink());
365        addLine("Web application Message : " + fAppInfo.getMessage());
366        if ( fException instanceof AppException ) {
367          AppException appEx = (AppException)fException;
368          Iterator errorsIter = appEx.getMessages().iterator();
369          while ( errorsIter.hasNext() ) {
370            addLine( errorsIter.next().toString() );
371          }
372        }
373      }
374      
375      private void addRequestInfo(){
376        addStartOfSection("Request Info:");
377        addLine("HTTP Method: " + fRequest.getMethod());
378        addLine("Context Path: " + fRequest.getContextPath());
379        addLine("ServletPath: " + fRequest.getServletPath());
380        addLine("URI: " + fRequest.getRequestURI());
381        addLine("URL: " + fRequest.getRequestURL().toString());
382        addRequestParams();
383        addRequestHeaders();
384        addCookies();
385      }
386      
387      private void addClientInfo(){
388        addStartOfSection("Client Info:");
389        addLine("User IP: " + fRequest.getRemoteAddr());
390        addLine("User hostname: " + fRequest.getRemoteHost());
391      }
392      
393      private void addServerInfo(){
394        addStartOfSection("Server And Servlet Info:");
395        addLine("Name: " + fRequest.getServerName());
396        addLine("Port: " + fRequest.getServerPort());
397        addLine("Info: " + fConfig.getServletContext().getServerInfo()); //in Servlet 3.0 this is attached to the request
398        addLine("JRE default TimeZone: " + TimeZone.getDefault().getID());
399        addLine("JRE default Locale: " + Locale.getDefault().getDisplayName());
400        
401        addAllSystemProperties();
402        addClassPath();
403        addLine("Servlet : " + fConfig.getServletName()); 
404        
405        Map<String, String> servletParams = new Config().getRawMap();
406        servletParams = sortMap(servletParams);
407        addMap(servletParams, "Servlet init-param: ");
408      }
409      
410      private void addAllSystemProperties(){
411        Map properties = sortMap(System.getProperties());
412        Set props = properties.entrySet();
413        Iterator iter = props.iterator();
414        while ( iter.hasNext() ) {
415          Map.Entry entry = (Map.Entry)iter.next();
416          addLine(entry.getKey() + ": " + entry.getValue());
417        }
418      }
419    
420      /**
421       Since this item tends to be very long, it is useful to place each entry 
422       on a separate line.
423      */
424      private void addClassPath(){
425        String JAVA_CLASS_PATH = "java.class.path";
426        String classPath = System.getProperty(JAVA_CLASS_PATH);
427        List pathElements = Arrays.asList( classPath.split(Consts.PATH_SEPARATOR) );
428        StringBuilder result = new StringBuilder(Consts.NEW_LINE);
429        Iterator pathElementsIter = pathElements.iterator();
430        while ( pathElementsIter.hasNext() ) {
431          String pathElement = (String)pathElementsIter.next();
432          result.append(pathElement);
433          if ( pathElementsIter.hasNext() ) {
434            result.append(Consts.NEW_LINE);
435          }
436        }
437        addLine(JAVA_CLASS_PATH + ": " + result.toString());
438      }
439      
440      private void addStackTrace(){
441        addStartOfSection("Stack Trace:");
442        addLine( getStackTrace(fException) );
443      }
444      
445      private void  addRequestParams(){
446        Map paramMap = new HashMap();
447        Enumeration namesEnum = fRequest.getParameterNames();
448        while ( namesEnum.hasMoreElements() ){
449          String name = (String)namesEnum.nextElement();
450          String values = Util.getArrayAsString( fRequest.getParameterValues(name) );
451          if( isPassword(name)){
452            paramMap.put(name, "***(masked)***");
453          }
454          else {
455            paramMap.put(name, values);
456          }
457        }
458        paramMap = sortMap(paramMap);
459        addMap(paramMap, "Req Param");
460      }
461      
462      private void addMemoryInfo(){
463        addStartOfSection("Memory Info:");
464        addLine(getMemory());
465      }
466      
467      private String getMemory(){
468        return getTotalUsedFree(Runtime.getRuntime().totalMemory(), Runtime.getRuntime().freeMemory(), "JRE Memory");
469      }
470      
471      private String getTotalUsedFree(long aTotal, long aFree, String aDescription){
472        StringBuilder result = new StringBuilder();
473        BigDecimal total = new BigDecimal(aTotal);
474        BigDecimal used = new BigDecimal(aTotal - aFree);
475        BigDecimal free = new BigDecimal(aFree);
476    
477        BigDecimal percentUsed = percent(used, total);
478        BigDecimal percentFree = percent(free, total);
479        
480        result.append(aDescription + Consts.NEW_LINE) ;
481        String totalText = format(total);
482        String format = "%-9s%" + totalText.length() + "s";
483        result.append(String.format(format, " total: ", totalText) + Consts.NEW_LINE) ;
484        result.append(String.format(format, " used: ", format(used)) +  " (" + percentUsed.intValue() + "%)" + Consts.NEW_LINE) ;
485        result.append(String.format(format, " free: ", format(free)) + " (" + percentFree.intValue() + "%)" + Consts.NEW_LINE) ;
486        return result.toString();
487      }
488    
489      private BigDecimal percent(BigDecimal aA, BigDecimal aB){
490        BigDecimal ZERO = new BigDecimal("0");
491        BigDecimal result = ZERO;
492        BigDecimal HUNDRED = new BigDecimal("100");
493        if( aB.compareTo(ZERO) != 0) {
494          result = aA.divide(aB, 2, RoundingMode.HALF_EVEN);
495          result = result.multiply(HUNDRED);
496        }
497        return result;
498      }
499      
500      private String format(BigDecimal aNumber){
501        DecimalFormat format = new DecimalFormat("#,###");
502        return format.format(aNumber); 
503      }
504      
505      private boolean isPassword(String aName){
506        return Util.contains(PASSWORD_PATTERN, aName);
507      }
508      
509      private void addRequestHeaders(){
510        Map headerMap = new HashMap();
511        Enumeration namesEnum = fRequest.getHeaderNames();
512        while ( namesEnum.hasMoreElements() ) {
513          String name = (String) namesEnum.nextElement();
514          Enumeration valuesEnum = fRequest.getHeaders(name);
515          while ( valuesEnum.hasMoreElements() ) {
516            String value = (String)valuesEnum.nextElement();
517            headerMap.put(name, value);
518          }
519        }
520        headerMap = sortMap(headerMap);
521        addMap(headerMap, "Header");
522      }
523      
524      private void addCookies(){
525        if (fRequest.getCookies() == null) return;
526        
527        List cookies = Arrays.asList(fRequest.getCookies());
528        Iterator cookiesIter = cookies.iterator();
529        while ( cookiesIter.hasNext() ) {
530          Cookie cookie = (Cookie)cookiesIter.next();
531          addLine("Cookie " + cookie.getName() + "=" + cookie.getValue());
532        }
533      }
534      
535      private String getStackTrace( Throwable aThrowable ) {
536        final Writer result = new StringWriter();
537        final PrintWriter printWriter = new PrintWriter( result );
538        aThrowable.printStackTrace( printWriter );
539        return result.toString();
540      }  
541      
542      private void addSessionInfo(){
543        addStartOfSection("Session Info");
544        
545        HttpSession session = fRequest.getSession(DO_NOT_CREATE_SESSION);
546        if ( session == null ){
547          addLine("No session existed for this request.");
548        }
549        else {
550          addLine("Logged in user name : " + getLoggedInUser());
551          addLine("Timeout : " + session.getMaxInactiveInterval() + " seconds.");
552          Map<String, String> sessionMap = new HashMap<String, String>();
553          Enumeration sessionAttrs = session.getAttributeNames();
554          while (sessionAttrs.hasMoreElements()){
555            String name = (String)sessionAttrs.nextElement();
556            Object value = session.getAttribute(name);
557            if( isPassword(name) ){
558              sessionMap.put(name, "***(masked)***");
559            }
560            else {
561              sessionMap.put(name, value.toString());
562            }
563          }
564          sessionMap = sortMap(sessionMap);
565          addMap(sessionMap, "Session Attributes");
566        }
567      }
568      
569      private String getLoggedInUser(){
570        String result = null;
571        if (fRequest.getUserPrincipal() != null) {
572          result = fRequest.getUserPrincipal().getName();
573        }
574        else {
575          result = "NONE"; 
576        }
577        return result;
578      }
579      
580      private static synchronized boolean hasEnoughTimePassedSinceLastEmail(){
581        return (System.currentTimeMillis()-fTimeLastEmail >getMinimumIntervalBetweenEmails());
582      }
583    
584      private static synchronized void updateMostRecentTime(){
585        fTimeLastEmail = System.currentTimeMillis();
586      }
587    
588      private void sendEmail() throws AppException {
589        Emailer emailer = BuildImpl.forEmailer();
590        emailer.sendFromWebmaster(new Config().getTroubleTicketMailingList(), getSubject(), toString());
591      }
592      
593      /** Text to appear in all TroubleTicket emails as the "Subject" of the email. */
594      private String getSubject(){
595        return 
596          "Servlet Error. Application : " + fAppInfo.getName() + "" +
597          "/" + fAppInfo.getVersion()
598        ;
599      }
600          
601      /**
602       Convert the number of minutes configured in web.xml into milliseconds.
603      */
604      private static long getMinimumIntervalBetweenEmails(){
605        final long MILLISECONDS_PER_SECOND = 1000;
606        final int SECONDS_PER_MINUTE = 60;
607        Long MINIMUM_INTERVAL_BETWEEN_TICKETS = new Config().getMinimumIntervalBetweenTroubleTickets();
608        return 
609          MILLISECONDS_PER_SECOND * SECONDS_PER_MINUTE *
610          MINIMUM_INTERVAL_BETWEEN_TICKETS
611        ;
612      }
613      
614      private void addMap(Map aMap, String aLineHeader){
615        Iterator iter = aMap.keySet().iterator();
616        while (iter.hasNext()){
617          String name = (String)iter.next();
618          String value = (String)aMap.get(name);
619          addLine(aLineHeader + " " + name + " = " + value);
620        }
621      }
622      
623      private Map sortMap(Map aInput){
624        Map result = new TreeMap(String.CASE_INSENSITIVE_ORDER);
625        result.putAll(aInput);
626        return result;
627      }
628    }