package hirondelle.web4j.webmaster;
import hirondelle.web4j.ApplicationInfo;
import hirondelle.web4j.BuildImpl;
import hirondelle.web4j.Controller;
import hirondelle.web4j.model.AppException;
import hirondelle.web4j.model.DateTime;
import hirondelle.web4j.readconfig.InitParam;
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.util.ArrayList;
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.StringTokenizer;
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>Example content of a <tt>TroubleTicket</tt>, as returned by {@link #toString()} :
<PRE>
{@code
Error for web application Fish And Chips/3.0.0.
java.lang.RuntimeException: Testing application behavior upon failure.
--------------------------------------------------------
Time of error : 2010-01-27 20:54:49
Occurred for user : blah
Web application Build Date: Fri Mar 16 00:00:00 ADT 2007
Web application Author : Hirondelle Systems
Web application Link : http://www.javapractices.com/apps/web4j/javadoc/summary.html
Web application Message :
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/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;
Header accept-charset = UTF-8,*
Header accept-language = en-us,en;q=0.5
Header connection = keep-alive
Header cookie = JSESSIONID=3969C9FD46EKLJDD0ASDKLFJAS
Header host = localhost:8081
Header keep-alive = 300
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.8.1.5) Gecko/
Header x-mcproxyfilter = ************
Cookie JSESSIONID=AS3213ASD5F1AS
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 = ISO-8859-1
Session Attributes web4j_key_for_errors = Messages : + [] Has Been Displayed : false
Session Attributes web4j_key_for_locale = en
Session Attributes web4j_key_for_messages = Messages : + [] Has Been Displayed : false
Server And Servlet Info:
--------------------------------------------------------
Name: localhost
Port: 8081
Info: Apache Tomcat/5.5.23
JRE default TimeZone: America/Halifax
JRE default Locale: English (United States)
awt.toolkit: sun.awt.windows.WToolkit
catalina.base: C:\Program Files\Tomcat5\Tomcat 5.5
catalina.home: C:\Program Files\Tomcat5\Tomcat 5.5
catalina.useNaming: true
common.loader: ${catalina.home}/common/classes
file.encoding: Cp1252
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\Tomcat5\Tomcat 5.5\bin\bootstrap.jar
java.class.version: 49.0
java.endorsed.dirs: C:\Program Files\Tomcat5\Tomcat 5.5\common\endorsed
java.ext.dirs: C:\Program Files\Java\jre1.5.0_10\lib\ext
java.home: C:\Program Files\Java\jre1.5.0_10
java.io.tmpdir: C:\Program Files\Tomcat5\Tomcat 5.5\temp
java.library.path: C:\Program Files\Tomcat5\Tomcat 5.5\bin;
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_10-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:\Program Files\Tomcat5\Tomcat 5.5\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_10
java.vm.info: mixed mode
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_10-b03
line.separator:
os.arch: x86
os.name: Windows XP
os.version: 5.1
path.separator: ;
server.loader: ${catalina.home}/server/classes,${catalina.home}/server/lib/*.jar
shared.loader: ${catalina.base}/shared/classes,${catalina.base}/shared/lib/*.jar
sun.arch.data.model: 32
sun.boot.class.path: C:\Program Files\Java\jre1.5.0_10\lib\rt.jar;
sun.boot.library.path: C:\Program Files\Java\jre1.5.0_10\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 2
tomcat.util.buf.StringCache.byte.enabled: true
user.country: US
user.dir: C:\Program Files\Tomcat5\Tomcat 5.5
user.home: C:\
user.language: en
user.name: SYSTEM
user.timezone: America/Halifax
user.variant:
java.class.path:
C:\Program Files\Tomcat5\Tomcat 5.5\bin\bootstrap.jar
Servlet : Controller
Servlet init-param: BigDecimalDisplayFormat = #,##0.00
Servlet init-param: CharacterEncoding = ISO-8859-1
Servlet init-param: DecimalSeparator = PERIOD
Servlet init-param: DefaultLocale = en
Servlet init-param: DefaultUserTimeZone = America/Halifax
Servlet init-param: EmailInSeparateThread = YES
Servlet init-param: EmptyOrNullDisplayFormat = -
Servlet init-param: ErrorCodeForDuplicateKey = 1062
Servlet init-param: FetchSize = 25
Servlet init-param: FloatDisplayFormat = #,###.00
Servlet init-param: HasAutoGeneratedKeys = true
Servlet init-param: IgnorableParamValue =
Servlet init-param: ImplicitMappingAddSuffix = .do
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: 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: 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:21)
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 extract config information from
<tt>web.xml</tt>.
*/
public static void init(ServletConfig aConfig, ApplicationInfo aAppInfo){
fConfig = aConfig;
fMINIMUM_INTERVAL_BETWEEN_TICKETS = new Long(
fMinimumIntervalBetweenTickets.fetch(aConfig).getValue()
);
String timeZoneSetting = fDefaultTimeZone.fetch(aConfig).getValue();
fDEFAULT_TIME_ZONE = TimeZone.getTimeZone(timeZoneSetting);
fAppInfo = aAppInfo;
setTroubleTicketMailingList(aConfig);
}
/**
Constructor.
@param aException has caused the problem.
@param aRequest original underlying HTTP request.
*/
public TroubleTicket(Throwable aException, HttpServletRequest aRequest){
fRequest = aRequest;
fException = aException;
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 mailToWebmaster() throws AppException {
if ( hasEnoughTimePassedSinceLastEmail() ) {
sendEmail();
updateMostRecentTime();
}
}
// PRIVATE //
private static ServletConfig fConfig;
private static ApplicationInfo fAppInfo;
private final HttpServletRequest fRequest;
private final Throwable fException;
private static final boolean DO_NOT_CREATE = 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.
*/
private static long fTimeLastEmail;
/** Item configured in web.xml. */
private static InitParam fMinimumIntervalBetweenTickets = new InitParam(
"MinimumIntervalBetweenTroubleTickets", "30"
);
/** Minimum number of minutes between Trouble Tickets. */
private static Long fMINIMUM_INTERVAL_BETWEEN_TICKETS;
/** The DefaultUserTimeZone setting in web.xml. Defaults to GMT. */
private static final InitParam fDefaultTimeZone = new InitParam("DefaultUserTimeZone", "GMT");
private static TimeZone fDEFAULT_TIME_ZONE;
/** Item configured in web.xml. */
private static InitParam fTroubleTicketMailingList = new InitParam(
"TroubleTicketMailingList", "NONE"
);
/**
List or email addresses, for all receivers of TroubleTickets.
If empty, then not sent at all.
*/
private static List<String> fTROUBLE_TICKET_MAILING_LIST = new ArrayList<String>();
private static void setTroubleTicketMailingList(ServletConfig aConfig){
String rawList = fTroubleTicketMailingList.fetch(aConfig).getValue();
StringTokenizer parser = new StringTokenizer(rawList, ",");
while (parser.hasMoreElements()){
String emailAddr = (String)parser.nextElement();
fTROUBLE_TICKET_MAILING_LIST.add(emailAddr);
}
}
/** Build fBody from its various parts. */
private void buildBodyOfMessage() {
addExceptionSummary();
addRequestInfo();
addClientInfo();
addSessionInfo();
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 now = BuildImpl.forTimeSource().currentTimeMillis();
addLine("Time of error : " + DateTime.now(fDEFAULT_TIME_ZONE).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());
addLine("JRE default TimeZone: " + TimeZone.getDefault().getID());
addLine("JRE default Locale: " + Locale.getDefault().getDisplayName());
addAllSystemProperties();
addClassPath();
addLine("Servlet : " + fConfig.getServletName());
Map<String, Object> servletParams = new HashMap<String, Object>();
Enumeration paramNames = fConfig.getInitParameterNames();
while (paramNames.hasMoreElements()){
String name = (String)paramNames.nextElement();
String value = fConfig.getInitParameter(name);
servletParams.put(name, value);
}
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 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);
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 void sendEmail() throws AppException {
Emailer emailer = BuildImpl.forEmailer();
emailer.sendFromWebmaster(fTROUBLE_TICKET_MAILING_LIST, 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()
;
}
private static synchronized void updateMostRecentTime(){
fTimeLastEmail = System.currentTimeMillis();
}
/**
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;
return
MILLISECONDS_PER_SECOND * SECONDS_PER_MINUTE *
fMINIMUM_INTERVAL_BETWEEN_TICKETS.longValue()
;
}
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;
}
}