You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@tapestry.apache.org by hl...@apache.org on 2014/05/31 01:57:28 UTC

[1/3] git commit: Include a list of threads in the default exception report page

Repository: tapestry-5
Updated Branches:
  refs/heads/master 5b311716e -> ce6e66ea3


Include a list of threads in the default exception report page


Project: http://git-wip-us.apache.org/repos/asf/tapestry-5/repo
Commit: http://git-wip-us.apache.org/repos/asf/tapestry-5/commit/08807a12
Tree: http://git-wip-us.apache.org/repos/asf/tapestry-5/tree/08807a12
Diff: http://git-wip-us.apache.org/repos/asf/tapestry-5/diff/08807a12

Branch: refs/heads/master
Commit: 08807a1266b49cc5d76ac38705ab7fe31e893b6e
Parents: 5b31171
Author: Howard M. Lewis Ship <hl...@apache.org>
Authored: Fri May 30 16:05:33 2014 -0700
Committer: Howard M. Lewis Ship <hl...@apache.org>
Committed: Fri May 30 16:55:24 2014 -0700

----------------------------------------------------------------------
 54_RELEASE_NOTES.md                             |   4 +
 .../corelib/pages/ExceptionReport.java          | 100 ++++++++++++++++++-
 .../META-INF/assets/core/ExceptionReport.css    |  13 +++
 .../tapestry5/corelib/pages/ExceptionReport.tml |  21 ++++
 4 files changed, 135 insertions(+), 3 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/08807a12/54_RELEASE_NOTES.md
----------------------------------------------------------------------
diff --git a/54_RELEASE_NOTES.md b/54_RELEASE_NOTES.md
index 8044c85..23fc912 100644
--- a/54_RELEASE_NOTES.md
+++ b/54_RELEASE_NOTES.md
@@ -155,6 +155,10 @@ The FormFragment component's visibleBound parameter is no longer supported; it w
 far up the DOM above the FormFragment's client-side element should be searched when determining visibility. This may
 resurface in the future as a CSS expression, but is currently not supported.
 
+## ExceptionReport Page
+
+The default exception report page has been modified to display a list of threads.
+
 ## Symbol tapestry.asset-path-prefix
 
 The definition of the symbol 'tapestry.asset-path-prefix' has changed; it no longer includes the leading and trailing

http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/08807a12/tapestry-core/src/main/java/org/apache/tapestry5/corelib/pages/ExceptionReport.java
----------------------------------------------------------------------
diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/corelib/pages/ExceptionReport.java b/tapestry-core/src/main/java/org/apache/tapestry5/corelib/pages/ExceptionReport.java
index e6db1ed..1ca7e25 100644
--- a/tapestry-core/src/main/java/org/apache/tapestry5/corelib/pages/ExceptionReport.java
+++ b/tapestry-core/src/main/java/org/apache/tapestry5/corelib/pages/ExceptionReport.java
@@ -21,16 +21,20 @@ import org.apache.tapestry5.annotations.ContentType;
 import org.apache.tapestry5.annotations.Import;
 import org.apache.tapestry5.annotations.Property;
 import org.apache.tapestry5.annotations.UnknownActivationContextCheck;
+import org.apache.tapestry5.func.F;
+import org.apache.tapestry5.func.Mapper;
 import org.apache.tapestry5.internal.InternalConstants;
 import org.apache.tapestry5.internal.services.PageActivationContextCollector;
 import org.apache.tapestry5.internal.services.ReloadHelper;
 import org.apache.tapestry5.ioc.annotations.Inject;
 import org.apache.tapestry5.ioc.annotations.Symbol;
+import org.apache.tapestry5.ioc.internal.util.CollectionFactory;
 import org.apache.tapestry5.ioc.internal.util.InternalUtils;
 import org.apache.tapestry5.services.*;
 
 import java.net.MalformedURLException;
 import java.net.URL;
+import java.util.Arrays;
 import java.util.List;
 import java.util.regex.Pattern;
 
@@ -99,17 +103,42 @@ public class ExceptionReport implements ExceptionReporter
 
     @Inject
     private ReloadHelper reloadHelper;
-    
+
     @Inject
     private URLEncoder urlEncoder;
 
     @Property
     private String rootURL;
 
+    @Property
+    private ThreadInfo thread;
+
+    public class ThreadInfo implements Comparable<ThreadInfo>
+    {
+        public final String className, name, flags;
+
+        public final ThreadGroup group;
+
+        public ThreadInfo(String className, String name, String flags, ThreadGroup group)
+        {
+            this.className = className;
+            this.name = name;
+            this.flags = flags;
+            this.group = group;
+        }
+
+        @Override
+        public int compareTo(ThreadInfo o)
+        {
+            return name.compareTo(o.name);
+        }
+    }
+
     private final String pathSeparator = System.getProperty(PATH_SEPARATOR_PROPERTY);
 
-    public boolean isShowActions() {
-        return failurePage != null && ! request.isXHR();
+    public boolean isShowActions()
+    {
+        return failurePage != null && !request.isXHR();
     }
 
     public void reportException(Throwable exception)
@@ -182,4 +211,69 @@ public class ExceptionReport implements ExceptionReporter
 
         return getPropertyValue().split(pathSeparator);
     }
+
+    private Thread[] assembleThreads()
+    {
+        ThreadGroup rootGroup = Thread.currentThread().getThreadGroup();
+
+        while (true)
+        {
+            ThreadGroup parentGroup = rootGroup.getParent();
+            if (parentGroup == null)
+            {
+                break;
+            }
+            rootGroup = parentGroup;
+        }
+
+        Thread[] threads = new Thread[rootGroup.activeCount()];
+
+        while (true)
+        {
+            // A really ugly API. threads.length must be larger than
+            // the actual number of threads, just so we can determine
+            // if we're done.
+            int count = rootGroup.enumerate(threads, true);
+            if (count < threads.length)
+            {
+                return Arrays.copyOf(threads, count);
+            }
+            threads = new Thread[threads.length * 2];
+        }
+    }
+
+    public List<ThreadInfo> getThreads()
+    {
+        return F.flow(assembleThreads()).map(new Mapper<Thread, ThreadInfo>()
+        {
+            @Override
+            public ThreadInfo map(Thread t)
+            {
+                List<String> flags = CollectionFactory.newList();
+
+                if (t.isDaemon())
+                {
+                    flags.add("daemon");
+                }
+                if (!t.isAlive())
+                {
+                    flags.add("NOT alive");
+                }
+                if (t.isInterrupted())
+                {
+                    flags.add("interrupted");
+                }
+
+                if (t.getPriority() != Thread.NORM_PRIORITY)
+                {
+                    flags.add("priority " + t.getPriority());
+                }
+
+                return new ThreadInfo(Thread.currentThread() == t ? "active-thread" : "",
+                        t.getName(),
+                        InternalUtils.join(flags),
+                        t.getThreadGroup());
+            }
+        }).sort().toList();
+    }
 }

http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/08807a12/tapestry-core/src/main/resources/META-INF/assets/core/ExceptionReport.css
----------------------------------------------------------------------
diff --git a/tapestry-core/src/main/resources/META-INF/assets/core/ExceptionReport.css b/tapestry-core/src/main/resources/META-INF/assets/core/ExceptionReport.css
index e770da3..87fdab6 100644
--- a/tapestry-core/src/main/resources/META-INF/assets/core/ExceptionReport.css
+++ b/tapestry-core/src/main/resources/META-INF/assets/core/ExceptionReport.css
@@ -1,3 +1,16 @@
 body {
     padding-top: 55px;
+}
+
+table.exception-report-threads td, table.exception-report-threads th {
+    text-align: right;
+}
+
+table.exception-report-threads tr td:last-child,
+table.exception-report-threads tr th:last-child {
+    text-align: left;
+}
+
+table.exception-report-threads tr.active-thread {
+    font-weight:  bold;
 }
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/08807a12/tapestry-core/src/main/resources/org/apache/tapestry5/corelib/pages/ExceptionReport.tml
----------------------------------------------------------------------
diff --git a/tapestry-core/src/main/resources/org/apache/tapestry5/corelib/pages/ExceptionReport.tml b/tapestry-core/src/main/resources/org/apache/tapestry5/corelib/pages/ExceptionReport.tml
index c26ebf1..0190b80 100644
--- a/tapestry-core/src/main/resources/org/apache/tapestry5/corelib/pages/ExceptionReport.tml
+++ b/tapestry-core/src/main/resources/org/apache/tapestry5/corelib/pages/ExceptionReport.tml
@@ -94,6 +94,27 @@
                     </dl>
                 </t:if>
 
+                <h3>Threads</h3>
+
+                <table class="table table-compact table-striped exception-report-threads">
+                    <thread>
+                        <tr>
+                            <th>Thread Name</th>
+                            <th>Group Name</th>
+                            <th>Flags</th>
+                        </tr>
+                    </thread>
+                    <tbody>
+
+                        <tr t:type="loop" source="threads" value="thread" class="${thread.className}">
+                            <td class="thread-name">${thread.name}</td>
+                            <td>${thread.group?.name}</td>
+                            <td>${thread.flags}</td>
+                        </tr>
+                    </tbody>
+                </table>
+
+
                 <h3>System Properties</h3>
                 <dl>
                     <t:loop source="systemProperties" value="propertyName">


[2/3] git commit: Integrate an exception reporting service that will write exception report text files to the file system

Posted by hl...@apache.org.
Integrate an exception reporting service that will write exception report text files to the file system


Project: http://git-wip-us.apache.org/repos/asf/tapestry-5/repo
Commit: http://git-wip-us.apache.org/repos/asf/tapestry-5/commit/511813f3
Tree: http://git-wip-us.apache.org/repos/asf/tapestry-5/tree/511813f3
Diff: http://git-wip-us.apache.org/repos/asf/tapestry-5/diff/511813f3

Branch: refs/heads/master
Commit: 511813f381c7724024cc818977e9a85f7eade22a
Parents: 08807a1
Author: Howard M. Lewis Ship <hl...@apache.org>
Authored: Fri May 30 16:55:07 2014 -0700
Committer: Howard M. Lewis Ship <hl...@apache.org>
Committed: Fri May 30 16:57:01 2014 -0700

----------------------------------------------------------------------
 54_RELEASE_NOTES.md                             |   4 +
 .../org/apache/tapestry5/SymbolConstants.java   |  14 +-
 .../corelib/pages/ExceptionReport.java          |  42 +--
 .../internal/TapestryInternalUtils.java         |  31 +-
 .../DefaultRequestExceptionHandler.java         |  56 +--
 .../exceptions/ExceptionReporterImpl.java       | 340 +++++++++++++++++++
 .../tapestry5/modules/TapestryModule.java       |   3 +
 .../services/exceptions/ExceptionReporter.java  |  33 ++
 .../services/exceptions/package-info.java       |   4 +
 .../tapestry5/corelib/pages/ExceptionReport.tml |   4 +-
 .../DefaultRequestExceptionHandlerTest.java     | 117 ++++---
 11 files changed, 535 insertions(+), 113 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/511813f3/54_RELEASE_NOTES.md
----------------------------------------------------------------------
diff --git a/54_RELEASE_NOTES.md b/54_RELEASE_NOTES.md
index 23fc912..eda173b 100644
--- a/54_RELEASE_NOTES.md
+++ b/54_RELEASE_NOTES.md
@@ -159,6 +159,10 @@ resurface in the future as a CSS expression, but is currently not supported.
 
 The default exception report page has been modified to display a list of threads.
 
+## ExceptionReporter Service
+
+A new service, `ExceptionReporter`, will now create a text file on the file system for each runtime request processing exception.
+
 ## Symbol tapestry.asset-path-prefix
 
 The definition of the symbol 'tapestry.asset-path-prefix' has changed; it no longer includes the leading and trailing

http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/511813f3/tapestry-core/src/main/java/org/apache/tapestry5/SymbolConstants.java
----------------------------------------------------------------------
diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/SymbolConstants.java b/tapestry-core/src/main/java/org/apache/tapestry5/SymbolConstants.java
index d2526ae..6ab3759 100644
--- a/tapestry-core/src/main/java/org/apache/tapestry5/SymbolConstants.java
+++ b/tapestry-core/src/main/java/org/apache/tapestry5/SymbolConstants.java
@@ -1,5 +1,3 @@
-// Copyright 2008-2014 The Apache Software Foundation
-//
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
 // You may obtain a copy of the License at
@@ -474,15 +472,25 @@ public class SymbolConstants
      * @since 5.4
      */
     public static final String FORM_FIELD_CSS_CLASS = "tapestry.form-field-css-class";
-    
+
     /**
      * Defines whether {@link java.text.DateFormat} instances created by Tapestry should be
      * lenient or not by default. The default value is <code>false</code>.
+     *
      * @since 5.4
      */
     public static final String LENIENT_DATE_FORMAT = "tapestry.lenient-date-format";
 
     /**
+     * The directory to which exception report files should be written. The default is appropriate
+     * for development: {@code build/exceptions}, and should be changed for production.
+     *
+     * @see org.apache.tapestry5.services.exceptions.ExceptionReporter
+     * @since 5.4
+     */
+    
+    public static final String EXCEPTION_REPORTS_DIR = "tapestry.exception-reports-dir";
+    /**
      * Defines whether {@link CSSURLRewriter} will throw an exception when a CSS file
      * references an URL which doesn't exist. The default value is <code>false</code>.
      * 

http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/511813f3/tapestry-core/src/main/java/org/apache/tapestry5/corelib/pages/ExceptionReport.java
----------------------------------------------------------------------
diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/corelib/pages/ExceptionReport.java b/tapestry-core/src/main/java/org/apache/tapestry5/corelib/pages/ExceptionReport.java
index 1ca7e25..bdc853a 100644
--- a/tapestry-core/src/main/java/org/apache/tapestry5/corelib/pages/ExceptionReport.java
+++ b/tapestry-core/src/main/java/org/apache/tapestry5/corelib/pages/ExceptionReport.java
@@ -1,5 +1,3 @@
-// Copyright 2006-2013 The Apache Software Foundation
-//
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
 // You may obtain a copy of the License at
@@ -24,6 +22,7 @@ import org.apache.tapestry5.annotations.UnknownActivationContextCheck;
 import org.apache.tapestry5.func.F;
 import org.apache.tapestry5.func.Mapper;
 import org.apache.tapestry5.internal.InternalConstants;
+import org.apache.tapestry5.internal.TapestryInternalUtils;
 import org.apache.tapestry5.internal.services.PageActivationContextCollector;
 import org.apache.tapestry5.internal.services.ReloadHelper;
 import org.apache.tapestry5.ioc.annotations.Inject;
@@ -34,7 +33,6 @@ import org.apache.tapestry5.services.*;
 
 import java.net.MalformedURLException;
 import java.net.URL;
-import java.util.Arrays;
 import java.util.List;
 import java.util.regex.Pattern;
 
@@ -115,14 +113,15 @@ public class ExceptionReport implements ExceptionReporter
 
     public class ThreadInfo implements Comparable<ThreadInfo>
     {
-        public final String className, name, flags;
+        public final String className, name, state, flags;
 
         public final ThreadGroup group;
 
-        public ThreadInfo(String className, String name, String flags, ThreadGroup group)
+        public ThreadInfo(String className, String name, String state, String flags, ThreadGroup group)
         {
             this.className = className;
             this.name = name;
+            this.state = state;
             this.flags = flags;
             this.group = group;
         }
@@ -212,39 +211,9 @@ public class ExceptionReport implements ExceptionReporter
         return getPropertyValue().split(pathSeparator);
     }
 
-    private Thread[] assembleThreads()
-    {
-        ThreadGroup rootGroup = Thread.currentThread().getThreadGroup();
-
-        while (true)
-        {
-            ThreadGroup parentGroup = rootGroup.getParent();
-            if (parentGroup == null)
-            {
-                break;
-            }
-            rootGroup = parentGroup;
-        }
-
-        Thread[] threads = new Thread[rootGroup.activeCount()];
-
-        while (true)
-        {
-            // A really ugly API. threads.length must be larger than
-            // the actual number of threads, just so we can determine
-            // if we're done.
-            int count = rootGroup.enumerate(threads, true);
-            if (count < threads.length)
-            {
-                return Arrays.copyOf(threads, count);
-            }
-            threads = new Thread[threads.length * 2];
-        }
-    }
-
     public List<ThreadInfo> getThreads()
     {
-        return F.flow(assembleThreads()).map(new Mapper<Thread, ThreadInfo>()
+        return F.flow(TapestryInternalUtils.getAllThreads()).map(new Mapper<Thread, ThreadInfo>()
         {
             @Override
             public ThreadInfo map(Thread t)
@@ -271,6 +240,7 @@ public class ExceptionReport implements ExceptionReporter
 
                 return new ThreadInfo(Thread.currentThread() == t ? "active-thread" : "",
                         t.getName(),
+                        t.getState().name(),
                         InternalUtils.join(flags),
                         t.getThreadGroup());
             }

http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/511813f3/tapestry-core/src/main/java/org/apache/tapestry5/internal/TapestryInternalUtils.java
----------------------------------------------------------------------
diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/internal/TapestryInternalUtils.java b/tapestry-core/src/main/java/org/apache/tapestry5/internal/TapestryInternalUtils.java
index 6c6138b..3435d64 100644
--- a/tapestry-core/src/main/java/org/apache/tapestry5/internal/TapestryInternalUtils.java
+++ b/tapestry-core/src/main/java/org/apache/tapestry5/internal/TapestryInternalUtils.java
@@ -1,5 +1,3 @@
-// Copyright 2006-2013 The Apache Software Foundation
-//
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
 // You may obtain a copy of the License at
@@ -33,6 +31,7 @@ import java.io.OutputStream;
 import java.lang.annotation.Annotation;
 import java.lang.ref.Reference;
 import java.lang.reflect.Type;
+import java.util.Arrays;
 import java.util.List;
 import java.util.Map;
 import java.util.regex.Pattern;
@@ -587,5 +586,33 @@ public class TapestryInternalUtils
 
         return ref == null ? null : ref.get();
     }
+
+    /**
+     * Gathers together an array containing all the threads.
+     * @since 5.4 */
+    public static Thread[] getAllThreads() {
+        ThreadGroup rootGroup = Thread.currentThread().getThreadGroup();
+
+        while (true) {
+            ThreadGroup parentGroup = rootGroup.getParent();
+            if (parentGroup == null) {
+                break;
+            }
+            rootGroup = parentGroup;
+        }
+
+        Thread[] threads = new Thread[rootGroup.activeCount()];
+
+        while (true) {
+            // A really ugly API. threads.length must be larger than
+            // the actual number of threads, just so we can determine
+            // if we're done.
+            int count = rootGroup.enumerate(threads, true);
+            if (count < threads.length) {
+                return Arrays.copyOf(threads, count);
+            }
+            threads = new Thread[threads.length * 2];
+        }
+    }
 }
 

http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/511813f3/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/DefaultRequestExceptionHandler.java
----------------------------------------------------------------------
diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/DefaultRequestExceptionHandler.java b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/DefaultRequestExceptionHandler.java
index 70f72cb..de6b50b 100644
--- a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/DefaultRequestExceptionHandler.java
+++ b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/DefaultRequestExceptionHandler.java
@@ -1,5 +1,3 @@
-// Copyright 2006-2013 The Apache Software Foundation
-//
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
 // You may obtain a copy of the License at
@@ -44,14 +42,14 @@ import java.util.Map.Entry;
  * servlet spec's standard error handling, the default exception handler allows configuring handlers for specific types of
  * exceptions. The error-page/exception-type configuration in web.xml does not work in Tapestry application as errors are
  * wrapped in Tapestry's exception types (see {@link OperationException} and {@link ComponentEventException} ).
- *
+ * <p/>
  * Configurations are flexible. You can either contribute a {@link ExceptionHandlerAssistant} to use arbitrary complex logic
  * for error handling or a page class to render for the specific exception. Additionally, exceptions can carry context for the
  * error page. Exception context is formed either from the name of Exception (e.g. SmtpNotRespondingException -> ServiceFailure mapping
  * would render a page with URL /servicefailure/smtpnotresponding) or they can implement {@link ContextAwareException} interface.
- *
+ * <p/>
  * If no configured exception type is found, the default exception page {@link SymbolConstants#EXCEPTION_REPORT_PAGE} is rendered.
- * This fallback exception page must implement the {@link ExceptionReporter} interface.
+ * This fallback exception page must implement the {@link org.apache.tapestry5.services.ExceptionReporter} interface.
  */
 public class DefaultRequestExceptionHandler implements RequestExceptionHandler
 {
@@ -71,30 +69,28 @@ public class DefaultRequestExceptionHandler implements RequestExceptionHandler
 
     private final LinkSource linkSource;
 
+    private final org.apache.tapestry5.services.exceptions.ExceptionReporter exceptionReporter;
+
     // should be Class<? extends Throwable>, Object but it's not allowed to configure subtypes
     private final Map<Class, Object> configuration;
 
     /**
-     * @param pageCache
-     * @param renderer
-     * @param logger
-     * @param pageName
-     * @param request
-     * @param response
-     * @param componentClassResolver
-     * @param linkSource
-     * @param serviceResources
-     * @param configuration A map of Exception class and handler values. A handler is either a page class or an ExceptionHandlerAssistant. ExceptionHandlerAssistant can be a class
-     * in which case the instance is autobuilt.
+     * @param configuration
+     *         A map of Exception class and handler values. A handler is either a page class or an ExceptionHandlerAssistant. ExceptionHandlerAssistant can be a class
      */
     @SuppressWarnings("rawtypes")
-    public DefaultRequestExceptionHandler(RequestPageCache pageCache, PageResponseRenderer renderer, Logger logger,
-
+    public DefaultRequestExceptionHandler(RequestPageCache pageCache,
+                                          PageResponseRenderer renderer,
+                                          Logger logger,
                                           @Symbol(SymbolConstants.EXCEPTION_REPORT_PAGE)
                                           String pageName,
-
-                                          Request request, Response response, ComponentClassResolver componentClassResolver,
-                                          LinkSource linkSource, ServiceResources serviceResources, Map<Class, Object> configuration)
+                                          Request request,
+                                          Response response,
+                                          ComponentClassResolver componentClassResolver,
+                                          LinkSource linkSource,
+                                          ServiceResources serviceResources,
+                                          org.apache.tapestry5.services.exceptions.ExceptionReporter exceptionReporter,
+                                          Map<Class, Object> configuration)
     {
         this.pageCache = pageCache;
         this.renderer = renderer;
@@ -104,6 +100,7 @@ public class DefaultRequestExceptionHandler implements RequestExceptionHandler
         this.response = response;
         this.componentClassResolver = componentClassResolver;
         this.linkSource = linkSource;
+        this.exceptionReporter = exceptionReporter;
 
         Map<Class<ExceptionHandlerAssistant>, ExceptionHandlerAssistant> handlerAssistants = new HashMap<Class<ExceptionHandlerAssistant>, ExceptionHandlerAssistant>();
 
@@ -131,14 +128,14 @@ public class DefaultRequestExceptionHandler implements RequestExceptionHandler
 
     /**
      * Handles the exception thrown at some point the request was being processed
-     *
+     * <p/>
      * First checks if there was a specific exception handler/page configured for this exception type, it's super class or super-super class.
      * Renders the default exception page if none was configured.
      *
-     * @param exception The exception that was thrown
-     *
+     * @param exception
+     *         The exception that was thrown
      */
-    @SuppressWarnings({ "rawtypes", "unchecked" })
+    @SuppressWarnings({"rawtypes", "unchecked"})
     public void handleRequestException(Throwable exception) throws IOException
     {
         // skip handling of known exceptions if there are none configured
@@ -235,6 +232,10 @@ public class DefaultRequestExceptionHandler implements RequestExceptionHandler
     {
         logger.error(String.format("Processing of request failed with uncaught exception: %s", exception), exception);
 
+        // In the case where one of the contributed rules, above, changes the behavior, then we don't report the
+        // exception. This is just for exceptions that are going to be rendered, real failures.
+        exceptionReporter.reportException(exception);
+
         // TAP5-233: Make sure the client knows that an error occurred.
 
         response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
@@ -249,7 +250,7 @@ public class DefaultRequestExceptionHandler implements RequestExceptionHandler
 
         Page page = pageCache.get(pageName);
 
-        ExceptionReporter rootComponent = (ExceptionReporter) page.getRootComponent();
+        org.apache.tapestry5.services.ExceptionReporter rootComponent = (org.apache.tapestry5.services.ExceptionReporter) page.getRootComponent();
 
         // Let the page set up for the new exception.
 
@@ -262,7 +263,8 @@ public class DefaultRequestExceptionHandler implements RequestExceptionHandler
      * Form exception context either from the name of the exception, or the context the exception contains if it's of type
      * {@link ContextAwareException}
      *
-     * @param exception The exception that the context is formed for
+     * @param exception
+     *         The exception that the context is formed for
      * @return Returns an array of objects to be used as the exception context
      */
     @SuppressWarnings({"unchecked", "rawtypes"})

http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/511813f3/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/exceptions/ExceptionReporterImpl.java
----------------------------------------------------------------------
diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/exceptions/ExceptionReporterImpl.java b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/exceptions/ExceptionReporterImpl.java
new file mode 100644
index 0000000..f3dd3c5
--- /dev/null
+++ b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/exceptions/ExceptionReporterImpl.java
@@ -0,0 +1,340 @@
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package org.apache.tapestry5.internal.services.exceptions;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.tapestry5.SymbolConstants;
+import org.apache.tapestry5.func.F;
+import org.apache.tapestry5.func.Flow;
+import org.apache.tapestry5.func.Mapper;
+import org.apache.tapestry5.func.Reducer;
+import org.apache.tapestry5.internal.TapestryInternalUtils;
+import org.apache.tapestry5.ioc.annotations.Inject;
+import org.apache.tapestry5.ioc.annotations.Symbol;
+import org.apache.tapestry5.ioc.internal.util.CollectionFactory;
+import org.apache.tapestry5.ioc.internal.util.InternalUtils;
+import org.apache.tapestry5.ioc.services.ExceptionAnalysis;
+import org.apache.tapestry5.ioc.services.ExceptionAnalyzer;
+import org.apache.tapestry5.ioc.services.ExceptionInfo;
+import org.apache.tapestry5.ioc.util.ExceptionUtils;
+import org.apache.tapestry5.services.Request;
+import org.apache.tapestry5.services.RequestGlobals;
+import org.apache.tapestry5.services.exceptions.ExceptionReporter;
+import org.slf4j.Logger;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.lang.reflect.Array;
+import java.util.*;
+import java.util.concurrent.atomic.AtomicInteger;
+
+@SuppressWarnings("ResultOfMethodCallIgnored")
+public class ExceptionReporterImpl implements ExceptionReporter
+{
+    private static final Reducer<Integer, Integer> MAX = new Reducer<Integer, Integer>()
+    {
+        @Override
+        public Integer reduce(Integer accumulator, Integer element)
+        {
+            return Math.max(accumulator, element);
+        }
+    };
+
+    private static final Mapper<String, Integer> STRING_TO_LENGTH = new Mapper<String, Integer>()
+    {
+        @Override
+        public Integer map(String element)
+        {
+            return element.length();
+        }
+    };
+
+    @Inject
+    @Symbol(SymbolConstants.EXCEPTION_REPORTS_DIR)
+    private File logDir;
+
+    @Inject
+    @Symbol(SymbolConstants.CONTEXT_PATH)
+    private String contextPath;
+
+    @Inject
+    private ExceptionAnalyzer analyzer;
+
+    private final AtomicInteger uid = new AtomicInteger();
+
+    @Inject
+    private Logger logger;
+
+    @Inject
+    private RequestGlobals requestGlobals;
+
+    @Override
+    public void reportException(Throwable exception)
+    {
+        Date date = new Date();
+        String folderName = String.format("%tY/%<tm/%<td/%<tH/%<tM", date);
+        String fileName = String.format(
+                "exception-%tY%<tm%<td-%<tH%<tM%<tS-%<tL.%d.txt", date,
+                uid.getAndIncrement());
+
+        try
+        {
+            File folder = new File(logDir, folderName);
+            folder.mkdirs();
+
+            File log = new File(folder, fileName);
+
+            writeExceptionToFile(exception, log);
+
+            logger.warn(String.format("Wrote exception report to %s", toURI(log)));
+        } catch (Exception ex)
+        {
+            logger.error(String.format("Unable to write exception report %s: %s",
+                    fileName, ExceptionUtils.toMessage(ex)));
+
+            logger.error("Original exception:", exception);
+        }
+    }
+
+    private String toURI(File file)
+    {
+        try
+        {
+            return file.toURI().toString();
+        } catch (Exception e)
+        {
+            return file.toString();
+        }
+    }
+
+    private void writeExceptionToFile(Throwable exception, File log) throws IOException
+    {
+        log.createNewFile();
+        ExceptionAnalysis analysis = analyzer.analyze(exception);
+        PrintWriter writer = null;
+        try
+        {
+            writer = new PrintWriter(log);
+            writeException(writer, analysis);
+        } finally
+        {
+            IOUtils.closeQuietly(writer);
+        }
+    }
+
+    interface PropertyWriter
+    {
+        void write(String name, Object value);
+    }
+
+    private final static Mapper<ExceptionInfo, Flow<String>> EXCEPTION_INFO_TO_PROPERTY_NAMES =
+            new Mapper<ExceptionInfo, Flow<String>>()
+            {
+                @Override
+                public Flow<String> map(ExceptionInfo element)
+                {
+                    return F.flow(element.getPropertyNames());
+                }
+            };
+
+    private void writeException(final PrintWriter writer, ExceptionAnalysis analysis)
+    {
+        final Formatter f = new Formatter(writer);
+        writer.print("EXCEPTION STACK:\n\n");
+        Request request = requestGlobals.getRequest();
+
+        // Figure out what all the property names are so that we can set the width of the column that lists
+        // property names.
+        Flow<String> propertyNames = F.flow(analysis.getExceptionInfos())
+                .mapcat(EXCEPTION_INFO_TO_PROPERTY_NAMES).append("Exception type", "Message");
+
+        if (request != null)
+        {
+            propertyNames = propertyNames.concat(request.getParameterNames()).concat(request.getHeaderNames());
+        }
+
+        final int maxPropertyNameLength = propertyNames.map(STRING_TO_LENGTH).reduce(MAX, 0);
+
+        final String propertyNameFormat = "  %" + maxPropertyNameLength + "s: %s\n";
+
+        PropertyWriter pw = new PropertyWriter()
+        {
+            @SuppressWarnings("rawtypes")
+            @Override
+            public void write(String name, Object value)
+            {
+                if (value.getClass().isArray())
+                {
+                    write(name, toList(value));
+                    return;
+                }
+
+                if (value instanceof Iterable)
+                {
+                    boolean first = true;
+                    Iterable iterable = (Iterable) value;
+                    Iterator i = iterable.iterator();
+                    while (i.hasNext())
+                    {
+                        if (first)
+                        {
+                            f.format(propertyNameFormat, name, i.next());
+                            first = false;
+                        } else
+                        {
+                            for (int j = 0; j < maxPropertyNameLength + 4; j++)
+                                writer.write(' ');
+
+                            writer.println(i.next());
+                        }
+                    }
+                    return;
+                }
+
+                // TODO: Handling of arrays & collections
+                f.format(propertyNameFormat, name, value);
+            }
+
+            @SuppressWarnings({"rawtypes", "unchecked"})
+            private List toList(Object array)
+            {
+                int count = Array.getLength(array);
+                List result = new ArrayList(count);
+                for (int i = 0; i < count; i++)
+                {
+                    result.add(Array.get(array, i));
+                }
+                return result;
+            }
+        };
+
+        boolean first = true;
+
+        for (ExceptionInfo info : analysis.getExceptionInfos())
+        {
+            if (first)
+            {
+                writer.println();
+                first = false;
+            }
+            pw.write("Exception type", info.getClassName());
+            pw.write("Message", info.getMessage());
+            for (String name : info.getPropertyNames())
+            {
+                pw.write(name, info.getProperty(name));
+            }
+            if (!info.getStackTrace().isEmpty())
+            {
+                writer.write("\n  Stack trace:\n");
+                for (StackTraceElement e : info.getStackTrace())
+                {
+                    f.format("  - %s\n", e.toString());
+                }
+            }
+            writer.println();
+        }
+
+        if (request != null)
+        {
+            writer.print("REQUEST:\n\nBasic Information:\n");
+            List<String> flags = CollectionFactory.newList();
+            if (request.isXHR())
+            {
+                flags.add("XHR");
+            }
+            if (request.isRequestedSessionIdValid())
+            {
+                flags.add("requestedSessionIdValid");
+            }
+            if (request.isSecure())
+            {
+                flags.add("secure");
+            }
+            pw.write("contextPath", contextPath);
+            if (!flags.isEmpty())
+            {
+                pw.write("flags", InternalUtils.joinSorted(flags));
+            }
+            pw.write("method", request.getMethod());
+            pw.write("path", request.getPath());
+            pw.write("locale", request.getLocale());
+            pw.write("serverName", request.getServerName());
+            writer.print("\nHeaders:\n");
+            for (String name : request.getHeaderNames())
+            {
+                pw.write(name, request.getHeader(name));
+            }
+            if (!request.getParameterNames().isEmpty())
+            {
+                writer.print("\nParameters:\n");
+                for (String name : request.getParameterNames())
+                {
+                    // TODO: Support multi-value parameters
+                    pw.write(name, request.getParameters(name));
+                }
+            }
+            // TODO: Session if it exists
+        }
+
+        writer.print("\nSYSTEM INFORMATION:");
+
+        Runtime runtime = Runtime.getRuntime();
+
+        f.format("\n\nMemory:\n  %,15d bytes free\n  %,15d bytes total\n  %,15d bytes max\n",
+                runtime.freeMemory(),
+                runtime.totalMemory(),
+                runtime.maxMemory());
+
+        Thread[] threads = TapestryInternalUtils.getAllThreads();
+
+        int maxThreadNameLength = 0;
+
+        for (Thread t : threads)
+        {
+            maxThreadNameLength = Math.max(maxThreadNameLength, t.getName().length());
+        }
+
+        String format = "\n%s %" + maxThreadNameLength + "s %s";
+
+        f.format("\n%,d Threads:", threads.length);
+
+        for (Thread t : threads)
+        {
+            f.format(format,
+                    Thread.currentThread() == t ? "*" : " ",
+                    t.getName(),
+                    t.getState().name());
+            if (t.isDaemon())
+            {
+                writer.write(", daemon");
+            }
+            if (!t.isAlive())
+            {
+                writer.write(", NOT alive");
+            }
+            if (t.isInterrupted())
+            {
+                writer.write(", interrupted");
+            }
+            if (t.getPriority() != Thread.NORM_PRIORITY)
+            {
+                f.format(", priority %d", t.getPriority());
+            }
+        }
+        writer.println();
+
+        f.close();
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/511813f3/tapestry-core/src/main/java/org/apache/tapestry5/modules/TapestryModule.java
----------------------------------------------------------------------
diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/modules/TapestryModule.java b/tapestry-core/src/main/java/org/apache/tapestry5/modules/TapestryModule.java
index d13c974..78e1523 100644
--- a/tapestry-core/src/main/java/org/apache/tapestry5/modules/TapestryModule.java
+++ b/tapestry-core/src/main/java/org/apache/tapestry5/modules/TapestryModule.java
@@ -38,6 +38,7 @@ import org.apache.tapestry5.internal.services.*;
 import org.apache.tapestry5.internal.services.ajax.AjaxFormUpdateFilter;
 import org.apache.tapestry5.internal.services.ajax.AjaxResponseRendererImpl;
 import org.apache.tapestry5.internal.services.ajax.MultiZoneUpdateEventResultProcessor;
+import org.apache.tapestry5.internal.services.exceptions.ExceptionReporterImpl;
 import org.apache.tapestry5.internal.services.linktransform.LinkTransformerImpl;
 import org.apache.tapestry5.internal.services.linktransform.LinkTransformerInterceptor;
 import org.apache.tapestry5.internal.services.messages.PropertiesFileParserImpl;
@@ -370,6 +371,7 @@ public final class TapestryModule
         binder.bind(PathConstructor.class, PathConstructorImpl.class);
         binder.bind(DateUtilities.class, DateUtilitiesImpl.class);
         binder.bind(PartialTemplateRenderer.class, PartialTemplateRendererImpl.class);
+        binder.bind(org.apache.tapestry5.services.exceptions.ExceptionReporter.class, ExceptionReporterImpl.class);
     }
 
     // ========================================================================
@@ -2126,6 +2128,7 @@ public final class TapestryModule
         // TAP5-2187
         configuration.add(SymbolConstants.STRICT_CSS_URL_REWRITING, "false");
 
+        configuration.add(SymbolConstants.EXCEPTION_REPORTS_DIR, "build/exceptions");
     }
 
     /**

http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/511813f3/tapestry-core/src/main/java/org/apache/tapestry5/services/exceptions/ExceptionReporter.java
----------------------------------------------------------------------
diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/services/exceptions/ExceptionReporter.java b/tapestry-core/src/main/java/org/apache/tapestry5/services/exceptions/ExceptionReporter.java
new file mode 100644
index 0000000..97a94ec
--- /dev/null
+++ b/tapestry-core/src/main/java/org/apache/tapestry5/services/exceptions/ExceptionReporter.java
@@ -0,0 +1,33 @@
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package org.apache.tapestry5.services.exceptions;
+
+/**
+ * Services used to report request handling exceptions. This is invoked <em>before</em> the exception report page is rendered.
+ * The default implementation converts the exception into a well formatted text file, with content similar to the default
+ * {@link org.apache.tapestry5.corelib.pages.ExceptionReport} page, and stores this file on the file system.
+ * <p/>
+ * Exception report files are stored beneath a root directory, with intermediate folders for year, month, day, hour, and minute.
+ * <p/>
+ * Directories are created as necessary; however, there is nothing in place to delete exceptions.
+ *
+ * @see org.apache.tapestry5.SymbolConstants#EXCEPTION_REPORTS_DIR
+ * @since 5.4
+ */
+public interface ExceptionReporter
+{
+    /**
+     * Records the exception.
+     */
+    void reportException(Throwable exception);
+}

http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/511813f3/tapestry-core/src/main/java/org/apache/tapestry5/services/exceptions/package-info.java
----------------------------------------------------------------------
diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/services/exceptions/package-info.java b/tapestry-core/src/main/java/org/apache/tapestry5/services/exceptions/package-info.java
new file mode 100644
index 0000000..20dfa5f
--- /dev/null
+++ b/tapestry-core/src/main/java/org/apache/tapestry5/services/exceptions/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * Services related to the generation of exception reports.
+ */
+package org.apache.tapestry5.services.exceptions;
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/511813f3/tapestry-core/src/main/resources/org/apache/tapestry5/corelib/pages/ExceptionReport.tml
----------------------------------------------------------------------
diff --git a/tapestry-core/src/main/resources/org/apache/tapestry5/corelib/pages/ExceptionReport.tml b/tapestry-core/src/main/resources/org/apache/tapestry5/corelib/pages/ExceptionReport.tml
index 0190b80..9bd1f25 100644
--- a/tapestry-core/src/main/resources/org/apache/tapestry5/corelib/pages/ExceptionReport.tml
+++ b/tapestry-core/src/main/resources/org/apache/tapestry5/corelib/pages/ExceptionReport.tml
@@ -96,11 +96,12 @@
 
                 <h3>Threads</h3>
 
-                <table class="table table-compact table-striped exception-report-threads">
+                <table class="table table-condensed table-hover table-striped exception-report-threads">
                     <thread>
                         <tr>
                             <th>Thread Name</th>
                             <th>Group Name</th>
+                            <th>State</th>
                             <th>Flags</th>
                         </tr>
                     </thread>
@@ -109,6 +110,7 @@
                         <tr t:type="loop" source="threads" value="thread" class="${thread.className}">
                             <td class="thread-name">${thread.name}</td>
                             <td>${thread.group?.name}</td>
+                            <td>${thread.state}</td>
                             <td>${thread.flags}</td>
                         </tr>
                     </tbody>

http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/511813f3/tapestry-core/src/test/java/org/apache/tapestry5/internal/services/DefaultRequestExceptionHandlerTest.java
----------------------------------------------------------------------
diff --git a/tapestry-core/src/test/java/org/apache/tapestry5/internal/services/DefaultRequestExceptionHandlerTest.java b/tapestry-core/src/test/java/org/apache/tapestry5/internal/services/DefaultRequestExceptionHandlerTest.java
index 4370020..ddab492 100644
--- a/tapestry-core/src/test/java/org/apache/tapestry5/internal/services/DefaultRequestExceptionHandlerTest.java
+++ b/tapestry-core/src/test/java/org/apache/tapestry5/internal/services/DefaultRequestExceptionHandlerTest.java
@@ -1,5 +1,3 @@
-// Copyright 2006, 2008, 2010, 2011 The Apache Software Foundation
-//
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
 // You may obtain a copy of the License at
@@ -14,12 +12,6 @@
 
 package org.apache.tapestry5.internal.services;
 
-import java.io.IOException;
-import java.security.AccessControlException;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
 import org.apache.tapestry5.ContextAwareException;
 import org.apache.tapestry5.ExceptionHandlerAssistant;
 import org.apache.tapestry5.Link;
@@ -28,13 +20,21 @@ import org.apache.tapestry5.ioc.ServiceResources;
 import org.apache.tapestry5.services.ComponentClassResolver;
 import org.apache.tapestry5.services.Request;
 import org.apache.tapestry5.services.Response;
+import org.apache.tapestry5.services.exceptions.ExceptionReporter;
 import org.easymock.EasyMock;
 import org.slf4j.Logger;
 import org.testng.annotations.BeforeMethod;
 import org.testng.annotations.Test;
 
+import java.io.IOException;
+import java.security.AccessControlException;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
 @SuppressWarnings("serial")
-public class DefaultRequestExceptionHandlerTest extends InternalBaseTestCase {
+public class DefaultRequestExceptionHandlerTest extends InternalBaseTestCase
+{
     private Map<Class, Object> mockConfiguration = new HashMap<Class, Object>();
     RequestPageCache pageCache;
     PageResponseRenderer renderer;
@@ -44,25 +44,31 @@ public class DefaultRequestExceptionHandlerTest extends InternalBaseTestCase {
     ComponentClassResolver componentClassResolver;
     LinkSource linkSource;
     ServiceResources serviceResources;
+
     private DefaultRequestExceptionHandler exceptionHandler;
 
-    private static class MyContextAwareException extends Throwable implements ContextAwareException {
+
+    private static class MyContextAwareException extends Throwable implements ContextAwareException
+    {
         private Object[] context;
 
-        public MyContextAwareException(Object[] context) {
+        public MyContextAwareException(Object[] context)
+        {
             this.context = context;
         }
 
-        public Object[] getContext() {
+        public Object[] getContext()
+        {
             return context;
         }
 
     }
-    
-    private static class MyPage {
-        
+
+    private static class MyPage
+    {
+
     }
-    
+
     @BeforeMethod
     public void setup_tests() throws Exception
     {
@@ -75,28 +81,45 @@ public class DefaultRequestExceptionHandlerTest extends InternalBaseTestCase {
         componentClassResolver = mockComponentClassResolver();
         linkSource = mockLinkSource();
         serviceResources = mockServiceResources();
-          mockConfiguration.put(AccessControlException.class, MyPage.class);
-        mockConfiguration.put(MyContextAwareException.class, new ExceptionHandlerAssistant() {
-                      public Object handleRequestException(Throwable exception, List<Object> exceptionContext)
-                          throws IOException {
-                      return null;
-                  }
-              });
-        exceptionHandler = new DefaultRequestExceptionHandler(pageCache, renderer, logger, "exceptionpage", request, response, componentClassResolver, linkSource, serviceResources, mockConfiguration);
+        mockConfiguration.put(AccessControlException.class, MyPage.class);
+        mockConfiguration.put(MyContextAwareException.class, new ExceptionHandlerAssistant()
+        {
+            public Object handleRequestException(Throwable exception, List<Object> exceptionContext)
+                    throws IOException
+            {
+                return null;
+            }
+        });
+
+        ExceptionReporter noopExceptionReporter = new ExceptionReporter()
+        {
+            @Override
+            public void reportException(Throwable exception)
+            {
+
+            }
+        };
+
+        exceptionHandler = new DefaultRequestExceptionHandler(pageCache, renderer, logger, "exceptionpage", request, response, componentClassResolver, linkSource, serviceResources, noopExceptionReporter, mockConfiguration);
     }
-    
+
 
     @Test
-    public void noContextWhenExceptionDoesntContainMessage() {
-        Object[] context = exceptionHandler.formExceptionContext(new RuntimeException() {
+    public void noContextWhenExceptionDoesntContainMessage()
+    {
+        Object[] context = exceptionHandler.formExceptionContext(new RuntimeException()
+        {
         });
         assertEquals(context.length, 0);
     }
 
     @Test
-    public void contextIsExceptionMessage() {
-        Object[] context = exceptionHandler.formExceptionContext(new RuntimeException() {
-            public String getMessage() {
+    public void contextIsExceptionMessage()
+    {
+        Object[] context = exceptionHandler.formExceptionContext(new RuntimeException()
+        {
+            public String getMessage()
+            {
                 return "HelloWorld";
             }
         });
@@ -105,7 +128,8 @@ public class DefaultRequestExceptionHandlerTest extends InternalBaseTestCase {
     }
 
     @Test
-    public void contextIsExceptionType() {
+    public void contextIsExceptionType()
+    {
         Object[] context = exceptionHandler.formExceptionContext(new IllegalArgumentException("Value not allowed"));
         assertEquals(context.length, 1);
         assertTrue(context[0] instanceof String);
@@ -113,42 +137,47 @@ public class DefaultRequestExceptionHandlerTest extends InternalBaseTestCase {
     }
 
     @Test
-    public void contextIsProvidedByContextAwareException() {
-        Object[] sourceContext = new Object[] { new Integer(10), this };
+    public void contextIsProvidedByContextAwareException()
+    {
+        Object[] sourceContext = new Object[]{new Integer(10), this};
 
-        Object[] context = exceptionHandler.formExceptionContext(new MyContextAwareException(sourceContext) {
+        Object[] context = exceptionHandler.formExceptionContext(new MyContextAwareException(sourceContext)
+        {
         });
         assertEquals(context, sourceContext);
 
     }
-    
+
     @Test
-    public void handleRequestExceptionWithConfiguredPage() throws IOException {
-        train_resolvePageClassNameToPageName(componentClassResolver, MyPage.class.getName(), "mypage" );
+    public void handleRequestExceptionWithConfiguredPage() throws IOException
+    {
+        train_resolvePageClassNameToPageName(componentClassResolver, MyPage.class.getName(), "mypage");
         Link link = mockLink();
         expect(linkSource.createPageRenderLink("mypage", false, new Object[]{"accesscontrol"})).andReturn(link);
         expect(request.isXHR()).andReturn(false);
         response.sendRedirect(link);
         EasyMock.expectLastCall();
         replay();
-        
+
         exceptionHandler.handleRequestException(new AccessControlException("No permission"));
     }
-    
+
     @Test
-    public void handleRequestExceptionWithConfiguredAssistant() throws IOException {
-        ExceptionHandlerAssistant assistant = new ExceptionHandlerAssistant() {
+    public void handleRequestExceptionWithConfiguredAssistant() throws IOException
+    {
+        ExceptionHandlerAssistant assistant = new ExceptionHandlerAssistant()
+        {
             public Object handleRequestException(Throwable exception, List<Object> exceptionContext)
                     throws IOException
             {
                 return null;
             }
         };
-        
+
         mockConfiguration.put(MyContextAwareException.class, assistant);
         replay();
-        
+
         exceptionHandler.handleRequestException(new MyContextAwareException(new Object[]{}));
     }
-    
+
 }


[3/3] git commit: Integrate an exception reporting service that will write exception report text files to the file system

Posted by hl...@apache.org.
Integrate an exception reporting service that will write exception report text files to the file system


Project: http://git-wip-us.apache.org/repos/asf/tapestry-5/repo
Commit: http://git-wip-us.apache.org/repos/asf/tapestry-5/commit/ce6e66ea
Tree: http://git-wip-us.apache.org/repos/asf/tapestry-5/tree/ce6e66ea
Diff: http://git-wip-us.apache.org/repos/asf/tapestry-5/diff/ce6e66ea

Branch: refs/heads/master
Commit: ce6e66ea388455fea5363b718d8323299a601854
Parents: 511813f
Author: Howard M. Lewis Ship <hl...@apache.org>
Authored: Fri May 30 16:57:31 2014 -0700
Committer: Howard M. Lewis Ship <hl...@apache.org>
Committed: Fri May 30 16:57:31 2014 -0700

----------------------------------------------------------------------
 .../src/main/java/org/apache/tapestry5/SymbolConstants.java    | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/ce6e66ea/tapestry-core/src/main/java/org/apache/tapestry5/SymbolConstants.java
----------------------------------------------------------------------
diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/SymbolConstants.java b/tapestry-core/src/main/java/org/apache/tapestry5/SymbolConstants.java
index 6ab3759..4208479 100644
--- a/tapestry-core/src/main/java/org/apache/tapestry5/SymbolConstants.java
+++ b/tapestry-core/src/main/java/org/apache/tapestry5/SymbolConstants.java
@@ -488,12 +488,12 @@ public class SymbolConstants
      * @see org.apache.tapestry5.services.exceptions.ExceptionReporter
      * @since 5.4
      */
-    
+
     public static final String EXCEPTION_REPORTS_DIR = "tapestry.exception-reports-dir";
     /**
-     * Defines whether {@link CSSURLRewriter} will throw an exception when a CSS file
+     * Defines whether {@link org.apache.tapestry5.internal.services.assets.CSSURLRewriter} will throw an exception when a CSS file
      * references an URL which doesn't exist. The default value is <code>false</code>.
-     * 
+     *
      * @since 5.4
      */
     public static final String STRICT_CSS_URL_REWRITING = "tapestry.strict-css-url-rewriting";


[2/3] git commit: Integrate an exception reporting service that will write exception report text files to the file system

Posted by hl...@apache.org.
Integrate an exception reporting service that will write exception report text files to the file system


Project: http://git-wip-us.apache.org/repos/asf/tapestry-5/repo
Commit: http://git-wip-us.apache.org/repos/asf/tapestry-5/commit/511813f3
Tree: http://git-wip-us.apache.org/repos/asf/tapestry-5/tree/511813f3
Diff: http://git-wip-us.apache.org/repos/asf/tapestry-5/diff/511813f3

Branch: refs/heads/master
Commit: 511813f381c7724024cc818977e9a85f7eade22a
Parents: 08807a1
Author: Howard M. Lewis Ship <hl...@apache.org>
Authored: Fri May 30 16:55:07 2014 -0700
Committer: Howard M. Lewis Ship <hl...@apache.org>
Committed: Fri May 30 16:57:01 2014 -0700

----------------------------------------------------------------------
 54_RELEASE_NOTES.md                             |   4 +
 .../org/apache/tapestry5/SymbolConstants.java   |  14 +-
 .../corelib/pages/ExceptionReport.java          |  42 +--
 .../internal/TapestryInternalUtils.java         |  31 +-
 .../DefaultRequestExceptionHandler.java         |  56 +--
 .../exceptions/ExceptionReporterImpl.java       | 340 +++++++++++++++++++
 .../tapestry5/modules/TapestryModule.java       |   3 +
 .../services/exceptions/ExceptionReporter.java  |  33 ++
 .../services/exceptions/package-info.java       |   4 +
 .../tapestry5/corelib/pages/ExceptionReport.tml |   4 +-
 .../DefaultRequestExceptionHandlerTest.java     | 117 ++++---
 11 files changed, 535 insertions(+), 113 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/511813f3/54_RELEASE_NOTES.md
----------------------------------------------------------------------
diff --git a/54_RELEASE_NOTES.md b/54_RELEASE_NOTES.md
index 23fc912..eda173b 100644
--- a/54_RELEASE_NOTES.md
+++ b/54_RELEASE_NOTES.md
@@ -159,6 +159,10 @@ resurface in the future as a CSS expression, but is currently not supported.
 
 The default exception report page has been modified to display a list of threads.
 
+## ExceptionReporter Service
+
+A new service, `ExceptionReporter`, will now create a text file on the file system for each runtime request processing exception.
+
 ## Symbol tapestry.asset-path-prefix
 
 The definition of the symbol 'tapestry.asset-path-prefix' has changed; it no longer includes the leading and trailing

http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/511813f3/tapestry-core/src/main/java/org/apache/tapestry5/SymbolConstants.java
----------------------------------------------------------------------
diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/SymbolConstants.java b/tapestry-core/src/main/java/org/apache/tapestry5/SymbolConstants.java
index d2526ae..6ab3759 100644
--- a/tapestry-core/src/main/java/org/apache/tapestry5/SymbolConstants.java
+++ b/tapestry-core/src/main/java/org/apache/tapestry5/SymbolConstants.java
@@ -1,5 +1,3 @@
-// Copyright 2008-2014 The Apache Software Foundation
-//
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
 // You may obtain a copy of the License at
@@ -474,15 +472,25 @@ public class SymbolConstants
      * @since 5.4
      */
     public static final String FORM_FIELD_CSS_CLASS = "tapestry.form-field-css-class";
-    
+
     /**
      * Defines whether {@link java.text.DateFormat} instances created by Tapestry should be
      * lenient or not by default. The default value is <code>false</code>.
+     *
      * @since 5.4
      */
     public static final String LENIENT_DATE_FORMAT = "tapestry.lenient-date-format";
 
     /**
+     * The directory to which exception report files should be written. The default is appropriate
+     * for development: {@code build/exceptions}, and should be changed for production.
+     *
+     * @see org.apache.tapestry5.services.exceptions.ExceptionReporter
+     * @since 5.4
+     */
+    
+    public static final String EXCEPTION_REPORTS_DIR = "tapestry.exception-reports-dir";
+    /**
      * Defines whether {@link CSSURLRewriter} will throw an exception when a CSS file
      * references an URL which doesn't exist. The default value is <code>false</code>.
      * 

http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/511813f3/tapestry-core/src/main/java/org/apache/tapestry5/corelib/pages/ExceptionReport.java
----------------------------------------------------------------------
diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/corelib/pages/ExceptionReport.java b/tapestry-core/src/main/java/org/apache/tapestry5/corelib/pages/ExceptionReport.java
index 1ca7e25..bdc853a 100644
--- a/tapestry-core/src/main/java/org/apache/tapestry5/corelib/pages/ExceptionReport.java
+++ b/tapestry-core/src/main/java/org/apache/tapestry5/corelib/pages/ExceptionReport.java
@@ -1,5 +1,3 @@
-// Copyright 2006-2013 The Apache Software Foundation
-//
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
 // You may obtain a copy of the License at
@@ -24,6 +22,7 @@ import org.apache.tapestry5.annotations.UnknownActivationContextCheck;
 import org.apache.tapestry5.func.F;
 import org.apache.tapestry5.func.Mapper;
 import org.apache.tapestry5.internal.InternalConstants;
+import org.apache.tapestry5.internal.TapestryInternalUtils;
 import org.apache.tapestry5.internal.services.PageActivationContextCollector;
 import org.apache.tapestry5.internal.services.ReloadHelper;
 import org.apache.tapestry5.ioc.annotations.Inject;
@@ -34,7 +33,6 @@ import org.apache.tapestry5.services.*;
 
 import java.net.MalformedURLException;
 import java.net.URL;
-import java.util.Arrays;
 import java.util.List;
 import java.util.regex.Pattern;
 
@@ -115,14 +113,15 @@ public class ExceptionReport implements ExceptionReporter
 
     public class ThreadInfo implements Comparable<ThreadInfo>
     {
-        public final String className, name, flags;
+        public final String className, name, state, flags;
 
         public final ThreadGroup group;
 
-        public ThreadInfo(String className, String name, String flags, ThreadGroup group)
+        public ThreadInfo(String className, String name, String state, String flags, ThreadGroup group)
         {
             this.className = className;
             this.name = name;
+            this.state = state;
             this.flags = flags;
             this.group = group;
         }
@@ -212,39 +211,9 @@ public class ExceptionReport implements ExceptionReporter
         return getPropertyValue().split(pathSeparator);
     }
 
-    private Thread[] assembleThreads()
-    {
-        ThreadGroup rootGroup = Thread.currentThread().getThreadGroup();
-
-        while (true)
-        {
-            ThreadGroup parentGroup = rootGroup.getParent();
-            if (parentGroup == null)
-            {
-                break;
-            }
-            rootGroup = parentGroup;
-        }
-
-        Thread[] threads = new Thread[rootGroup.activeCount()];
-
-        while (true)
-        {
-            // A really ugly API. threads.length must be larger than
-            // the actual number of threads, just so we can determine
-            // if we're done.
-            int count = rootGroup.enumerate(threads, true);
-            if (count < threads.length)
-            {
-                return Arrays.copyOf(threads, count);
-            }
-            threads = new Thread[threads.length * 2];
-        }
-    }
-
     public List<ThreadInfo> getThreads()
     {
-        return F.flow(assembleThreads()).map(new Mapper<Thread, ThreadInfo>()
+        return F.flow(TapestryInternalUtils.getAllThreads()).map(new Mapper<Thread, ThreadInfo>()
         {
             @Override
             public ThreadInfo map(Thread t)
@@ -271,6 +240,7 @@ public class ExceptionReport implements ExceptionReporter
 
                 return new ThreadInfo(Thread.currentThread() == t ? "active-thread" : "",
                         t.getName(),
+                        t.getState().name(),
                         InternalUtils.join(flags),
                         t.getThreadGroup());
             }

http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/511813f3/tapestry-core/src/main/java/org/apache/tapestry5/internal/TapestryInternalUtils.java
----------------------------------------------------------------------
diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/internal/TapestryInternalUtils.java b/tapestry-core/src/main/java/org/apache/tapestry5/internal/TapestryInternalUtils.java
index 6c6138b..3435d64 100644
--- a/tapestry-core/src/main/java/org/apache/tapestry5/internal/TapestryInternalUtils.java
+++ b/tapestry-core/src/main/java/org/apache/tapestry5/internal/TapestryInternalUtils.java
@@ -1,5 +1,3 @@
-// Copyright 2006-2013 The Apache Software Foundation
-//
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
 // You may obtain a copy of the License at
@@ -33,6 +31,7 @@ import java.io.OutputStream;
 import java.lang.annotation.Annotation;
 import java.lang.ref.Reference;
 import java.lang.reflect.Type;
+import java.util.Arrays;
 import java.util.List;
 import java.util.Map;
 import java.util.regex.Pattern;
@@ -587,5 +586,33 @@ public class TapestryInternalUtils
 
         return ref == null ? null : ref.get();
     }
+
+    /**
+     * Gathers together an array containing all the threads.
+     * @since 5.4 */
+    public static Thread[] getAllThreads() {
+        ThreadGroup rootGroup = Thread.currentThread().getThreadGroup();
+
+        while (true) {
+            ThreadGroup parentGroup = rootGroup.getParent();
+            if (parentGroup == null) {
+                break;
+            }
+            rootGroup = parentGroup;
+        }
+
+        Thread[] threads = new Thread[rootGroup.activeCount()];
+
+        while (true) {
+            // A really ugly API. threads.length must be larger than
+            // the actual number of threads, just so we can determine
+            // if we're done.
+            int count = rootGroup.enumerate(threads, true);
+            if (count < threads.length) {
+                return Arrays.copyOf(threads, count);
+            }
+            threads = new Thread[threads.length * 2];
+        }
+    }
 }
 

http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/511813f3/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/DefaultRequestExceptionHandler.java
----------------------------------------------------------------------
diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/DefaultRequestExceptionHandler.java b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/DefaultRequestExceptionHandler.java
index 70f72cb..de6b50b 100644
--- a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/DefaultRequestExceptionHandler.java
+++ b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/DefaultRequestExceptionHandler.java
@@ -1,5 +1,3 @@
-// Copyright 2006-2013 The Apache Software Foundation
-//
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
 // You may obtain a copy of the License at
@@ -44,14 +42,14 @@ import java.util.Map.Entry;
  * servlet spec's standard error handling, the default exception handler allows configuring handlers for specific types of
  * exceptions. The error-page/exception-type configuration in web.xml does not work in Tapestry application as errors are
  * wrapped in Tapestry's exception types (see {@link OperationException} and {@link ComponentEventException} ).
- *
+ * <p/>
  * Configurations are flexible. You can either contribute a {@link ExceptionHandlerAssistant} to use arbitrary complex logic
  * for error handling or a page class to render for the specific exception. Additionally, exceptions can carry context for the
  * error page. Exception context is formed either from the name of Exception (e.g. SmtpNotRespondingException -> ServiceFailure mapping
  * would render a page with URL /servicefailure/smtpnotresponding) or they can implement {@link ContextAwareException} interface.
- *
+ * <p/>
  * If no configured exception type is found, the default exception page {@link SymbolConstants#EXCEPTION_REPORT_PAGE} is rendered.
- * This fallback exception page must implement the {@link ExceptionReporter} interface.
+ * This fallback exception page must implement the {@link org.apache.tapestry5.services.ExceptionReporter} interface.
  */
 public class DefaultRequestExceptionHandler implements RequestExceptionHandler
 {
@@ -71,30 +69,28 @@ public class DefaultRequestExceptionHandler implements RequestExceptionHandler
 
     private final LinkSource linkSource;
 
+    private final org.apache.tapestry5.services.exceptions.ExceptionReporter exceptionReporter;
+
     // should be Class<? extends Throwable>, Object but it's not allowed to configure subtypes
     private final Map<Class, Object> configuration;
 
     /**
-     * @param pageCache
-     * @param renderer
-     * @param logger
-     * @param pageName
-     * @param request
-     * @param response
-     * @param componentClassResolver
-     * @param linkSource
-     * @param serviceResources
-     * @param configuration A map of Exception class and handler values. A handler is either a page class or an ExceptionHandlerAssistant. ExceptionHandlerAssistant can be a class
-     * in which case the instance is autobuilt.
+     * @param configuration
+     *         A map of Exception class and handler values. A handler is either a page class or an ExceptionHandlerAssistant. ExceptionHandlerAssistant can be a class
      */
     @SuppressWarnings("rawtypes")
-    public DefaultRequestExceptionHandler(RequestPageCache pageCache, PageResponseRenderer renderer, Logger logger,
-
+    public DefaultRequestExceptionHandler(RequestPageCache pageCache,
+                                          PageResponseRenderer renderer,
+                                          Logger logger,
                                           @Symbol(SymbolConstants.EXCEPTION_REPORT_PAGE)
                                           String pageName,
-
-                                          Request request, Response response, ComponentClassResolver componentClassResolver,
-                                          LinkSource linkSource, ServiceResources serviceResources, Map<Class, Object> configuration)
+                                          Request request,
+                                          Response response,
+                                          ComponentClassResolver componentClassResolver,
+                                          LinkSource linkSource,
+                                          ServiceResources serviceResources,
+                                          org.apache.tapestry5.services.exceptions.ExceptionReporter exceptionReporter,
+                                          Map<Class, Object> configuration)
     {
         this.pageCache = pageCache;
         this.renderer = renderer;
@@ -104,6 +100,7 @@ public class DefaultRequestExceptionHandler implements RequestExceptionHandler
         this.response = response;
         this.componentClassResolver = componentClassResolver;
         this.linkSource = linkSource;
+        this.exceptionReporter = exceptionReporter;
 
         Map<Class<ExceptionHandlerAssistant>, ExceptionHandlerAssistant> handlerAssistants = new HashMap<Class<ExceptionHandlerAssistant>, ExceptionHandlerAssistant>();
 
@@ -131,14 +128,14 @@ public class DefaultRequestExceptionHandler implements RequestExceptionHandler
 
     /**
      * Handles the exception thrown at some point the request was being processed
-     *
+     * <p/>
      * First checks if there was a specific exception handler/page configured for this exception type, it's super class or super-super class.
      * Renders the default exception page if none was configured.
      *
-     * @param exception The exception that was thrown
-     *
+     * @param exception
+     *         The exception that was thrown
      */
-    @SuppressWarnings({ "rawtypes", "unchecked" })
+    @SuppressWarnings({"rawtypes", "unchecked"})
     public void handleRequestException(Throwable exception) throws IOException
     {
         // skip handling of known exceptions if there are none configured
@@ -235,6 +232,10 @@ public class DefaultRequestExceptionHandler implements RequestExceptionHandler
     {
         logger.error(String.format("Processing of request failed with uncaught exception: %s", exception), exception);
 
+        // In the case where one of the contributed rules, above, changes the behavior, then we don't report the
+        // exception. This is just for exceptions that are going to be rendered, real failures.
+        exceptionReporter.reportException(exception);
+
         // TAP5-233: Make sure the client knows that an error occurred.
 
         response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
@@ -249,7 +250,7 @@ public class DefaultRequestExceptionHandler implements RequestExceptionHandler
 
         Page page = pageCache.get(pageName);
 
-        ExceptionReporter rootComponent = (ExceptionReporter) page.getRootComponent();
+        org.apache.tapestry5.services.ExceptionReporter rootComponent = (org.apache.tapestry5.services.ExceptionReporter) page.getRootComponent();
 
         // Let the page set up for the new exception.
 
@@ -262,7 +263,8 @@ public class DefaultRequestExceptionHandler implements RequestExceptionHandler
      * Form exception context either from the name of the exception, or the context the exception contains if it's of type
      * {@link ContextAwareException}
      *
-     * @param exception The exception that the context is formed for
+     * @param exception
+     *         The exception that the context is formed for
      * @return Returns an array of objects to be used as the exception context
      */
     @SuppressWarnings({"unchecked", "rawtypes"})

http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/511813f3/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/exceptions/ExceptionReporterImpl.java
----------------------------------------------------------------------
diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/exceptions/ExceptionReporterImpl.java b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/exceptions/ExceptionReporterImpl.java
new file mode 100644
index 0000000..f3dd3c5
--- /dev/null
+++ b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/exceptions/ExceptionReporterImpl.java
@@ -0,0 +1,340 @@
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package org.apache.tapestry5.internal.services.exceptions;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.tapestry5.SymbolConstants;
+import org.apache.tapestry5.func.F;
+import org.apache.tapestry5.func.Flow;
+import org.apache.tapestry5.func.Mapper;
+import org.apache.tapestry5.func.Reducer;
+import org.apache.tapestry5.internal.TapestryInternalUtils;
+import org.apache.tapestry5.ioc.annotations.Inject;
+import org.apache.tapestry5.ioc.annotations.Symbol;
+import org.apache.tapestry5.ioc.internal.util.CollectionFactory;
+import org.apache.tapestry5.ioc.internal.util.InternalUtils;
+import org.apache.tapestry5.ioc.services.ExceptionAnalysis;
+import org.apache.tapestry5.ioc.services.ExceptionAnalyzer;
+import org.apache.tapestry5.ioc.services.ExceptionInfo;
+import org.apache.tapestry5.ioc.util.ExceptionUtils;
+import org.apache.tapestry5.services.Request;
+import org.apache.tapestry5.services.RequestGlobals;
+import org.apache.tapestry5.services.exceptions.ExceptionReporter;
+import org.slf4j.Logger;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.lang.reflect.Array;
+import java.util.*;
+import java.util.concurrent.atomic.AtomicInteger;
+
+@SuppressWarnings("ResultOfMethodCallIgnored")
+public class ExceptionReporterImpl implements ExceptionReporter
+{
+    private static final Reducer<Integer, Integer> MAX = new Reducer<Integer, Integer>()
+    {
+        @Override
+        public Integer reduce(Integer accumulator, Integer element)
+        {
+            return Math.max(accumulator, element);
+        }
+    };
+
+    private static final Mapper<String, Integer> STRING_TO_LENGTH = new Mapper<String, Integer>()
+    {
+        @Override
+        public Integer map(String element)
+        {
+            return element.length();
+        }
+    };
+
+    @Inject
+    @Symbol(SymbolConstants.EXCEPTION_REPORTS_DIR)
+    private File logDir;
+
+    @Inject
+    @Symbol(SymbolConstants.CONTEXT_PATH)
+    private String contextPath;
+
+    @Inject
+    private ExceptionAnalyzer analyzer;
+
+    private final AtomicInteger uid = new AtomicInteger();
+
+    @Inject
+    private Logger logger;
+
+    @Inject
+    private RequestGlobals requestGlobals;
+
+    @Override
+    public void reportException(Throwable exception)
+    {
+        Date date = new Date();
+        String folderName = String.format("%tY/%<tm/%<td/%<tH/%<tM", date);
+        String fileName = String.format(
+                "exception-%tY%<tm%<td-%<tH%<tM%<tS-%<tL.%d.txt", date,
+                uid.getAndIncrement());
+
+        try
+        {
+            File folder = new File(logDir, folderName);
+            folder.mkdirs();
+
+            File log = new File(folder, fileName);
+
+            writeExceptionToFile(exception, log);
+
+            logger.warn(String.format("Wrote exception report to %s", toURI(log)));
+        } catch (Exception ex)
+        {
+            logger.error(String.format("Unable to write exception report %s: %s",
+                    fileName, ExceptionUtils.toMessage(ex)));
+
+            logger.error("Original exception:", exception);
+        }
+    }
+
+    private String toURI(File file)
+    {
+        try
+        {
+            return file.toURI().toString();
+        } catch (Exception e)
+        {
+            return file.toString();
+        }
+    }
+
+    private void writeExceptionToFile(Throwable exception, File log) throws IOException
+    {
+        log.createNewFile();
+        ExceptionAnalysis analysis = analyzer.analyze(exception);
+        PrintWriter writer = null;
+        try
+        {
+            writer = new PrintWriter(log);
+            writeException(writer, analysis);
+        } finally
+        {
+            IOUtils.closeQuietly(writer);
+        }
+    }
+
+    interface PropertyWriter
+    {
+        void write(String name, Object value);
+    }
+
+    private final static Mapper<ExceptionInfo, Flow<String>> EXCEPTION_INFO_TO_PROPERTY_NAMES =
+            new Mapper<ExceptionInfo, Flow<String>>()
+            {
+                @Override
+                public Flow<String> map(ExceptionInfo element)
+                {
+                    return F.flow(element.getPropertyNames());
+                }
+            };
+
+    private void writeException(final PrintWriter writer, ExceptionAnalysis analysis)
+    {
+        final Formatter f = new Formatter(writer);
+        writer.print("EXCEPTION STACK:\n\n");
+        Request request = requestGlobals.getRequest();
+
+        // Figure out what all the property names are so that we can set the width of the column that lists
+        // property names.
+        Flow<String> propertyNames = F.flow(analysis.getExceptionInfos())
+                .mapcat(EXCEPTION_INFO_TO_PROPERTY_NAMES).append("Exception type", "Message");
+
+        if (request != null)
+        {
+            propertyNames = propertyNames.concat(request.getParameterNames()).concat(request.getHeaderNames());
+        }
+
+        final int maxPropertyNameLength = propertyNames.map(STRING_TO_LENGTH).reduce(MAX, 0);
+
+        final String propertyNameFormat = "  %" + maxPropertyNameLength + "s: %s\n";
+
+        PropertyWriter pw = new PropertyWriter()
+        {
+            @SuppressWarnings("rawtypes")
+            @Override
+            public void write(String name, Object value)
+            {
+                if (value.getClass().isArray())
+                {
+                    write(name, toList(value));
+                    return;
+                }
+
+                if (value instanceof Iterable)
+                {
+                    boolean first = true;
+                    Iterable iterable = (Iterable) value;
+                    Iterator i = iterable.iterator();
+                    while (i.hasNext())
+                    {
+                        if (first)
+                        {
+                            f.format(propertyNameFormat, name, i.next());
+                            first = false;
+                        } else
+                        {
+                            for (int j = 0; j < maxPropertyNameLength + 4; j++)
+                                writer.write(' ');
+
+                            writer.println(i.next());
+                        }
+                    }
+                    return;
+                }
+
+                // TODO: Handling of arrays & collections
+                f.format(propertyNameFormat, name, value);
+            }
+
+            @SuppressWarnings({"rawtypes", "unchecked"})
+            private List toList(Object array)
+            {
+                int count = Array.getLength(array);
+                List result = new ArrayList(count);
+                for (int i = 0; i < count; i++)
+                {
+                    result.add(Array.get(array, i));
+                }
+                return result;
+            }
+        };
+
+        boolean first = true;
+
+        for (ExceptionInfo info : analysis.getExceptionInfos())
+        {
+            if (first)
+            {
+                writer.println();
+                first = false;
+            }
+            pw.write("Exception type", info.getClassName());
+            pw.write("Message", info.getMessage());
+            for (String name : info.getPropertyNames())
+            {
+                pw.write(name, info.getProperty(name));
+            }
+            if (!info.getStackTrace().isEmpty())
+            {
+                writer.write("\n  Stack trace:\n");
+                for (StackTraceElement e : info.getStackTrace())
+                {
+                    f.format("  - %s\n", e.toString());
+                }
+            }
+            writer.println();
+        }
+
+        if (request != null)
+        {
+            writer.print("REQUEST:\n\nBasic Information:\n");
+            List<String> flags = CollectionFactory.newList();
+            if (request.isXHR())
+            {
+                flags.add("XHR");
+            }
+            if (request.isRequestedSessionIdValid())
+            {
+                flags.add("requestedSessionIdValid");
+            }
+            if (request.isSecure())
+            {
+                flags.add("secure");
+            }
+            pw.write("contextPath", contextPath);
+            if (!flags.isEmpty())
+            {
+                pw.write("flags", InternalUtils.joinSorted(flags));
+            }
+            pw.write("method", request.getMethod());
+            pw.write("path", request.getPath());
+            pw.write("locale", request.getLocale());
+            pw.write("serverName", request.getServerName());
+            writer.print("\nHeaders:\n");
+            for (String name : request.getHeaderNames())
+            {
+                pw.write(name, request.getHeader(name));
+            }
+            if (!request.getParameterNames().isEmpty())
+            {
+                writer.print("\nParameters:\n");
+                for (String name : request.getParameterNames())
+                {
+                    // TODO: Support multi-value parameters
+                    pw.write(name, request.getParameters(name));
+                }
+            }
+            // TODO: Session if it exists
+        }
+
+        writer.print("\nSYSTEM INFORMATION:");
+
+        Runtime runtime = Runtime.getRuntime();
+
+        f.format("\n\nMemory:\n  %,15d bytes free\n  %,15d bytes total\n  %,15d bytes max\n",
+                runtime.freeMemory(),
+                runtime.totalMemory(),
+                runtime.maxMemory());
+
+        Thread[] threads = TapestryInternalUtils.getAllThreads();
+
+        int maxThreadNameLength = 0;
+
+        for (Thread t : threads)
+        {
+            maxThreadNameLength = Math.max(maxThreadNameLength, t.getName().length());
+        }
+
+        String format = "\n%s %" + maxThreadNameLength + "s %s";
+
+        f.format("\n%,d Threads:", threads.length);
+
+        for (Thread t : threads)
+        {
+            f.format(format,
+                    Thread.currentThread() == t ? "*" : " ",
+                    t.getName(),
+                    t.getState().name());
+            if (t.isDaemon())
+            {
+                writer.write(", daemon");
+            }
+            if (!t.isAlive())
+            {
+                writer.write(", NOT alive");
+            }
+            if (t.isInterrupted())
+            {
+                writer.write(", interrupted");
+            }
+            if (t.getPriority() != Thread.NORM_PRIORITY)
+            {
+                f.format(", priority %d", t.getPriority());
+            }
+        }
+        writer.println();
+
+        f.close();
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/511813f3/tapestry-core/src/main/java/org/apache/tapestry5/modules/TapestryModule.java
----------------------------------------------------------------------
diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/modules/TapestryModule.java b/tapestry-core/src/main/java/org/apache/tapestry5/modules/TapestryModule.java
index d13c974..78e1523 100644
--- a/tapestry-core/src/main/java/org/apache/tapestry5/modules/TapestryModule.java
+++ b/tapestry-core/src/main/java/org/apache/tapestry5/modules/TapestryModule.java
@@ -38,6 +38,7 @@ import org.apache.tapestry5.internal.services.*;
 import org.apache.tapestry5.internal.services.ajax.AjaxFormUpdateFilter;
 import org.apache.tapestry5.internal.services.ajax.AjaxResponseRendererImpl;
 import org.apache.tapestry5.internal.services.ajax.MultiZoneUpdateEventResultProcessor;
+import org.apache.tapestry5.internal.services.exceptions.ExceptionReporterImpl;
 import org.apache.tapestry5.internal.services.linktransform.LinkTransformerImpl;
 import org.apache.tapestry5.internal.services.linktransform.LinkTransformerInterceptor;
 import org.apache.tapestry5.internal.services.messages.PropertiesFileParserImpl;
@@ -370,6 +371,7 @@ public final class TapestryModule
         binder.bind(PathConstructor.class, PathConstructorImpl.class);
         binder.bind(DateUtilities.class, DateUtilitiesImpl.class);
         binder.bind(PartialTemplateRenderer.class, PartialTemplateRendererImpl.class);
+        binder.bind(org.apache.tapestry5.services.exceptions.ExceptionReporter.class, ExceptionReporterImpl.class);
     }
 
     // ========================================================================
@@ -2126,6 +2128,7 @@ public final class TapestryModule
         // TAP5-2187
         configuration.add(SymbolConstants.STRICT_CSS_URL_REWRITING, "false");
 
+        configuration.add(SymbolConstants.EXCEPTION_REPORTS_DIR, "build/exceptions");
     }
 
     /**

http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/511813f3/tapestry-core/src/main/java/org/apache/tapestry5/services/exceptions/ExceptionReporter.java
----------------------------------------------------------------------
diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/services/exceptions/ExceptionReporter.java b/tapestry-core/src/main/java/org/apache/tapestry5/services/exceptions/ExceptionReporter.java
new file mode 100644
index 0000000..97a94ec
--- /dev/null
+++ b/tapestry-core/src/main/java/org/apache/tapestry5/services/exceptions/ExceptionReporter.java
@@ -0,0 +1,33 @@
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package org.apache.tapestry5.services.exceptions;
+
+/**
+ * Services used to report request handling exceptions. This is invoked <em>before</em> the exception report page is rendered.
+ * The default implementation converts the exception into a well formatted text file, with content similar to the default
+ * {@link org.apache.tapestry5.corelib.pages.ExceptionReport} page, and stores this file on the file system.
+ * <p/>
+ * Exception report files are stored beneath a root directory, with intermediate folders for year, month, day, hour, and minute.
+ * <p/>
+ * Directories are created as necessary; however, there is nothing in place to delete exceptions.
+ *
+ * @see org.apache.tapestry5.SymbolConstants#EXCEPTION_REPORTS_DIR
+ * @since 5.4
+ */
+public interface ExceptionReporter
+{
+    /**
+     * Records the exception.
+     */
+    void reportException(Throwable exception);
+}

http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/511813f3/tapestry-core/src/main/java/org/apache/tapestry5/services/exceptions/package-info.java
----------------------------------------------------------------------
diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/services/exceptions/package-info.java b/tapestry-core/src/main/java/org/apache/tapestry5/services/exceptions/package-info.java
new file mode 100644
index 0000000..20dfa5f
--- /dev/null
+++ b/tapestry-core/src/main/java/org/apache/tapestry5/services/exceptions/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * Services related to the generation of exception reports.
+ */
+package org.apache.tapestry5.services.exceptions;
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/511813f3/tapestry-core/src/main/resources/org/apache/tapestry5/corelib/pages/ExceptionReport.tml
----------------------------------------------------------------------
diff --git a/tapestry-core/src/main/resources/org/apache/tapestry5/corelib/pages/ExceptionReport.tml b/tapestry-core/src/main/resources/org/apache/tapestry5/corelib/pages/ExceptionReport.tml
index 0190b80..9bd1f25 100644
--- a/tapestry-core/src/main/resources/org/apache/tapestry5/corelib/pages/ExceptionReport.tml
+++ b/tapestry-core/src/main/resources/org/apache/tapestry5/corelib/pages/ExceptionReport.tml
@@ -96,11 +96,12 @@
 
                 <h3>Threads</h3>
 
-                <table class="table table-compact table-striped exception-report-threads">
+                <table class="table table-condensed table-hover table-striped exception-report-threads">
                     <thread>
                         <tr>
                             <th>Thread Name</th>
                             <th>Group Name</th>
+                            <th>State</th>
                             <th>Flags</th>
                         </tr>
                     </thread>
@@ -109,6 +110,7 @@
                         <tr t:type="loop" source="threads" value="thread" class="${thread.className}">
                             <td class="thread-name">${thread.name}</td>
                             <td>${thread.group?.name}</td>
+                            <td>${thread.state}</td>
                             <td>${thread.flags}</td>
                         </tr>
                     </tbody>

http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/511813f3/tapestry-core/src/test/java/org/apache/tapestry5/internal/services/DefaultRequestExceptionHandlerTest.java
----------------------------------------------------------------------
diff --git a/tapestry-core/src/test/java/org/apache/tapestry5/internal/services/DefaultRequestExceptionHandlerTest.java b/tapestry-core/src/test/java/org/apache/tapestry5/internal/services/DefaultRequestExceptionHandlerTest.java
index 4370020..ddab492 100644
--- a/tapestry-core/src/test/java/org/apache/tapestry5/internal/services/DefaultRequestExceptionHandlerTest.java
+++ b/tapestry-core/src/test/java/org/apache/tapestry5/internal/services/DefaultRequestExceptionHandlerTest.java
@@ -1,5 +1,3 @@
-// Copyright 2006, 2008, 2010, 2011 The Apache Software Foundation
-//
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
 // You may obtain a copy of the License at
@@ -14,12 +12,6 @@
 
 package org.apache.tapestry5.internal.services;
 
-import java.io.IOException;
-import java.security.AccessControlException;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
 import org.apache.tapestry5.ContextAwareException;
 import org.apache.tapestry5.ExceptionHandlerAssistant;
 import org.apache.tapestry5.Link;
@@ -28,13 +20,21 @@ import org.apache.tapestry5.ioc.ServiceResources;
 import org.apache.tapestry5.services.ComponentClassResolver;
 import org.apache.tapestry5.services.Request;
 import org.apache.tapestry5.services.Response;
+import org.apache.tapestry5.services.exceptions.ExceptionReporter;
 import org.easymock.EasyMock;
 import org.slf4j.Logger;
 import org.testng.annotations.BeforeMethod;
 import org.testng.annotations.Test;
 
+import java.io.IOException;
+import java.security.AccessControlException;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
 @SuppressWarnings("serial")
-public class DefaultRequestExceptionHandlerTest extends InternalBaseTestCase {
+public class DefaultRequestExceptionHandlerTest extends InternalBaseTestCase
+{
     private Map<Class, Object> mockConfiguration = new HashMap<Class, Object>();
     RequestPageCache pageCache;
     PageResponseRenderer renderer;
@@ -44,25 +44,31 @@ public class DefaultRequestExceptionHandlerTest extends InternalBaseTestCase {
     ComponentClassResolver componentClassResolver;
     LinkSource linkSource;
     ServiceResources serviceResources;
+
     private DefaultRequestExceptionHandler exceptionHandler;
 
-    private static class MyContextAwareException extends Throwable implements ContextAwareException {
+
+    private static class MyContextAwareException extends Throwable implements ContextAwareException
+    {
         private Object[] context;
 
-        public MyContextAwareException(Object[] context) {
+        public MyContextAwareException(Object[] context)
+        {
             this.context = context;
         }
 
-        public Object[] getContext() {
+        public Object[] getContext()
+        {
             return context;
         }
 
     }
-    
-    private static class MyPage {
-        
+
+    private static class MyPage
+    {
+
     }
-    
+
     @BeforeMethod
     public void setup_tests() throws Exception
     {
@@ -75,28 +81,45 @@ public class DefaultRequestExceptionHandlerTest extends InternalBaseTestCase {
         componentClassResolver = mockComponentClassResolver();
         linkSource = mockLinkSource();
         serviceResources = mockServiceResources();
-          mockConfiguration.put(AccessControlException.class, MyPage.class);
-        mockConfiguration.put(MyContextAwareException.class, new ExceptionHandlerAssistant() {
-                      public Object handleRequestException(Throwable exception, List<Object> exceptionContext)
-                          throws IOException {
-                      return null;
-                  }
-              });
-        exceptionHandler = new DefaultRequestExceptionHandler(pageCache, renderer, logger, "exceptionpage", request, response, componentClassResolver, linkSource, serviceResources, mockConfiguration);
+        mockConfiguration.put(AccessControlException.class, MyPage.class);
+        mockConfiguration.put(MyContextAwareException.class, new ExceptionHandlerAssistant()
+        {
+            public Object handleRequestException(Throwable exception, List<Object> exceptionContext)
+                    throws IOException
+            {
+                return null;
+            }
+        });
+
+        ExceptionReporter noopExceptionReporter = new ExceptionReporter()
+        {
+            @Override
+            public void reportException(Throwable exception)
+            {
+
+            }
+        };
+
+        exceptionHandler = new DefaultRequestExceptionHandler(pageCache, renderer, logger, "exceptionpage", request, response, componentClassResolver, linkSource, serviceResources, noopExceptionReporter, mockConfiguration);
     }
-    
+
 
     @Test
-    public void noContextWhenExceptionDoesntContainMessage() {
-        Object[] context = exceptionHandler.formExceptionContext(new RuntimeException() {
+    public void noContextWhenExceptionDoesntContainMessage()
+    {
+        Object[] context = exceptionHandler.formExceptionContext(new RuntimeException()
+        {
         });
         assertEquals(context.length, 0);
     }
 
     @Test
-    public void contextIsExceptionMessage() {
-        Object[] context = exceptionHandler.formExceptionContext(new RuntimeException() {
-            public String getMessage() {
+    public void contextIsExceptionMessage()
+    {
+        Object[] context = exceptionHandler.formExceptionContext(new RuntimeException()
+        {
+            public String getMessage()
+            {
                 return "HelloWorld";
             }
         });
@@ -105,7 +128,8 @@ public class DefaultRequestExceptionHandlerTest extends InternalBaseTestCase {
     }
 
     @Test
-    public void contextIsExceptionType() {
+    public void contextIsExceptionType()
+    {
         Object[] context = exceptionHandler.formExceptionContext(new IllegalArgumentException("Value not allowed"));
         assertEquals(context.length, 1);
         assertTrue(context[0] instanceof String);
@@ -113,42 +137,47 @@ public class DefaultRequestExceptionHandlerTest extends InternalBaseTestCase {
     }
 
     @Test
-    public void contextIsProvidedByContextAwareException() {
-        Object[] sourceContext = new Object[] { new Integer(10), this };
+    public void contextIsProvidedByContextAwareException()
+    {
+        Object[] sourceContext = new Object[]{new Integer(10), this};
 
-        Object[] context = exceptionHandler.formExceptionContext(new MyContextAwareException(sourceContext) {
+        Object[] context = exceptionHandler.formExceptionContext(new MyContextAwareException(sourceContext)
+        {
         });
         assertEquals(context, sourceContext);
 
     }
-    
+
     @Test
-    public void handleRequestExceptionWithConfiguredPage() throws IOException {
-        train_resolvePageClassNameToPageName(componentClassResolver, MyPage.class.getName(), "mypage" );
+    public void handleRequestExceptionWithConfiguredPage() throws IOException
+    {
+        train_resolvePageClassNameToPageName(componentClassResolver, MyPage.class.getName(), "mypage");
         Link link = mockLink();
         expect(linkSource.createPageRenderLink("mypage", false, new Object[]{"accesscontrol"})).andReturn(link);
         expect(request.isXHR()).andReturn(false);
         response.sendRedirect(link);
         EasyMock.expectLastCall();
         replay();
-        
+
         exceptionHandler.handleRequestException(new AccessControlException("No permission"));
     }
-    
+
     @Test
-    public void handleRequestExceptionWithConfiguredAssistant() throws IOException {
-        ExceptionHandlerAssistant assistant = new ExceptionHandlerAssistant() {
+    public void handleRequestExceptionWithConfiguredAssistant() throws IOException
+    {
+        ExceptionHandlerAssistant assistant = new ExceptionHandlerAssistant()
+        {
             public Object handleRequestException(Throwable exception, List<Object> exceptionContext)
                     throws IOException
             {
                 return null;
             }
         };
-        
+
         mockConfiguration.put(MyContextAwareException.class, assistant);
         replay();
-        
+
         exceptionHandler.handleRequestException(new MyContextAwareException(new Object[]{}));
     }
-    
+
 }


[3/3] git commit: Integrate an exception reporting service that will write exception report text files to the file system

Posted by hl...@apache.org.
Integrate an exception reporting service that will write exception report text files to the file system


Project: http://git-wip-us.apache.org/repos/asf/tapestry-5/repo
Commit: http://git-wip-us.apache.org/repos/asf/tapestry-5/commit/ce6e66ea
Tree: http://git-wip-us.apache.org/repos/asf/tapestry-5/tree/ce6e66ea
Diff: http://git-wip-us.apache.org/repos/asf/tapestry-5/diff/ce6e66ea

Branch: refs/heads/master
Commit: ce6e66ea388455fea5363b718d8323299a601854
Parents: 511813f
Author: Howard M. Lewis Ship <hl...@apache.org>
Authored: Fri May 30 16:57:31 2014 -0700
Committer: Howard M. Lewis Ship <hl...@apache.org>
Committed: Fri May 30 16:57:31 2014 -0700

----------------------------------------------------------------------
 .../src/main/java/org/apache/tapestry5/SymbolConstants.java    | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/ce6e66ea/tapestry-core/src/main/java/org/apache/tapestry5/SymbolConstants.java
----------------------------------------------------------------------
diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/SymbolConstants.java b/tapestry-core/src/main/java/org/apache/tapestry5/SymbolConstants.java
index 6ab3759..4208479 100644
--- a/tapestry-core/src/main/java/org/apache/tapestry5/SymbolConstants.java
+++ b/tapestry-core/src/main/java/org/apache/tapestry5/SymbolConstants.java
@@ -488,12 +488,12 @@ public class SymbolConstants
      * @see org.apache.tapestry5.services.exceptions.ExceptionReporter
      * @since 5.4
      */
-    
+
     public static final String EXCEPTION_REPORTS_DIR = "tapestry.exception-reports-dir";
     /**
-     * Defines whether {@link CSSURLRewriter} will throw an exception when a CSS file
+     * Defines whether {@link org.apache.tapestry5.internal.services.assets.CSSURLRewriter} will throw an exception when a CSS file
      * references an URL which doesn't exist. The default value is <code>false</code>.
-     * 
+     *
      * @since 5.4
      */
     public static final String STRICT_CSS_URL_REWRITING = "tapestry.strict-css-url-rewriting";