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 }