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 <hls...@apache.org> Authored: Fri May 30 16:55:07 2014 -0700 Committer: Howard M. Lewis Ship <hls...@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[]{})); } - + }