001// Licensed under the Apache License, Version 2.0 (the "License");
002// you may not use this file except in compliance with the License.
003// You may obtain a copy of the License at
004//
005// http://www.apache.org/licenses/LICENSE-2.0
006//
007// Unless required by applicable law or agreed to in writing, software
008// distributed under the License is distributed on an "AS IS" BASIS,
009// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
010// See the License for the specific language governing permissions and
011// limitations under the License.
012
013package org.apache.tapestry5.internal.services.exceptions;
014
015import org.apache.commons.io.IOUtils;
016import org.apache.tapestry5.SymbolConstants;
017import org.apache.tapestry5.func.F;
018import org.apache.tapestry5.func.Flow;
019import org.apache.tapestry5.func.Mapper;
020import org.apache.tapestry5.func.Reducer;
021import org.apache.tapestry5.internal.TapestryInternalUtils;
022import org.apache.tapestry5.ioc.annotations.Inject;
023import org.apache.tapestry5.ioc.annotations.Symbol;
024import org.apache.tapestry5.ioc.internal.util.CollectionFactory;
025import org.apache.tapestry5.ioc.internal.util.InternalUtils;
026import org.apache.tapestry5.ioc.services.ExceptionAnalysis;
027import org.apache.tapestry5.ioc.services.ExceptionAnalyzer;
028import org.apache.tapestry5.ioc.services.ExceptionInfo;
029import org.apache.tapestry5.ioc.util.ExceptionUtils;
030import org.apache.tapestry5.services.ExceptionReporter;
031import org.apache.tapestry5.services.Request;
032import org.apache.tapestry5.services.RequestGlobals;
033import org.slf4j.Logger;
034
035import java.io.File;
036import java.io.IOException;
037import java.io.PrintWriter;
038import java.lang.reflect.Array;
039import java.util.*;
040import java.util.concurrent.atomic.AtomicInteger;
041
042@SuppressWarnings("ResultOfMethodCallIgnored")
043public class ExceptionReporterImpl implements ExceptionReporter
044{
045    private static final Reducer<Integer, Integer> MAX = new Reducer<Integer, Integer>()
046    {
047        @Override
048        public Integer reduce(Integer accumulator, Integer element)
049        {
050            return Math.max(accumulator, element);
051        }
052    };
053
054    private static final Mapper<String, Integer> STRING_TO_LENGTH = new Mapper<String, Integer>()
055    {
056        @Override
057        public Integer map(String element)
058        {
059            return element.length();
060        }
061    };
062
063    @Inject
064    @Symbol(SymbolConstants.EXCEPTION_REPORTS_DIR)
065    private File logDir;
066
067    @Inject
068    @Symbol(SymbolConstants.CONTEXT_PATH)
069    private String contextPath;
070
071    @Inject
072    @Symbol(SymbolConstants.RESTRICTIVE_ENVIRONMENT)
073    private boolean restrictive;
074
075    @Inject
076    private ExceptionAnalyzer analyzer;
077
078    private final AtomicInteger uid = new AtomicInteger();
079
080    @Inject
081    private Logger logger;
082
083    @Inject
084    private RequestGlobals requestGlobals;
085
086    @Override
087    public void reportException(Throwable exception)
088    {
089        Date date = new Date();
090        String fileName = String.format(
091                "exception-%tY%<tm%<td-%<tH%<tM%<tS-%<tL.%d.txt", date,
092                uid.getAndIncrement());
093
094        File folder;
095
096        try
097        {
098            if (restrictive)
099            {
100                // Good luck with this; all exceptions written to a single folder.
101                folder = logDir;
102            } else
103            {
104                String folderName = String.format("%tY-%<tm-%<td/%<tH/%<tM", date);
105                folder = new File(logDir, folderName);
106
107                folder.mkdirs();
108            }
109
110            File log = new File(folder, fileName);
111
112            writeExceptionToFile(exception, log);
113
114            logger.warn(String.format("Wrote exception report to %s", toURI(log)));
115        } catch (Exception ex)
116        {
117            logger.error(String.format("Unable to write exception report %s: %s",
118                    fileName, ExceptionUtils.toMessage(ex)));
119
120            logger.error("Original exception:", exception);
121        }
122    }
123
124    private String toURI(File file)
125    {
126        try
127        {
128            return file.toURI().toString();
129        } catch (Exception e)
130        {
131            return file.toString();
132        }
133    }
134
135    private void writeExceptionToFile(Throwable exception, File log) throws IOException
136    {
137        log.createNewFile();
138        ExceptionAnalysis analysis = analyzer.analyze(exception);
139        PrintWriter writer = null;
140        try
141        {
142            writer = new PrintWriter(log);
143            writeException(writer, analysis);
144        } finally
145        {
146            IOUtils.closeQuietly(writer);
147        }
148    }
149
150    interface PropertyWriter
151    {
152        void write(String name, Object value);
153    }
154
155    private final static Mapper<ExceptionInfo, Flow<String>> EXCEPTION_INFO_TO_PROPERTY_NAMES =
156            new Mapper<ExceptionInfo, Flow<String>>()
157            {
158                @Override
159                public Flow<String> map(ExceptionInfo element)
160                {
161                    return F.flow(element.getPropertyNames());
162                }
163            };
164
165    private void writeException(final PrintWriter writer, ExceptionAnalysis analysis)
166    {
167        final Formatter f = new Formatter(writer);
168        writer.print("EXCEPTION STACK:\n\n");
169        Request request = requestGlobals.getRequest();
170
171        // Figure out what all the property names are so that we can set the width of the column that lists
172        // property names.
173        Flow<String> propertyNames = F.flow(analysis.getExceptionInfos())
174                .mapcat(EXCEPTION_INFO_TO_PROPERTY_NAMES).append("Exception type", "Message");
175
176        if (request != null)
177        {
178            propertyNames = propertyNames.concat(request.getParameterNames()).concat(request.getHeaderNames());
179        }
180
181        final int maxPropertyNameLength = propertyNames.map(STRING_TO_LENGTH).reduce(MAX, 0);
182
183        final String propertyNameFormat = "  %" + maxPropertyNameLength + "s: %s\n";
184
185        PropertyWriter pw = new PropertyWriter()
186        {
187            @SuppressWarnings("rawtypes")
188            @Override
189            public void write(String name, Object value)
190            {
191                if (value.getClass().isArray())
192                {
193                    write(name, toList(value));
194                    return;
195                }
196
197                if (value instanceof Iterable)
198                {
199                    boolean first = true;
200                    Iterable iterable = (Iterable) value;
201                    Iterator i = iterable.iterator();
202                    while (i.hasNext())
203                    {
204                        if (first)
205                        {
206                            f.format(propertyNameFormat, name, i.next());
207                            first = false;
208                        } else
209                        {
210                            for (int j = 0; j < maxPropertyNameLength + 4; j++)
211                                writer.write(' ');
212
213                            writer.println(i.next());
214                        }
215                    }
216                    return;
217                }
218
219                // TODO: Handling of arrays & collections
220                f.format(propertyNameFormat, name, value);
221            }
222
223            @SuppressWarnings({"rawtypes", "unchecked"})
224            private List toList(Object array)
225            {
226                int count = Array.getLength(array);
227                List result = new ArrayList(count);
228                for (int i = 0; i < count; i++)
229                {
230                    result.add(Array.get(array, i));
231                }
232                return result;
233            }
234        };
235
236        boolean first = true;
237
238        for (ExceptionInfo info : analysis.getExceptionInfos())
239        {
240            if (first)
241            {
242                writer.println();
243                first = false;
244            }
245            pw.write("Exception type", info.getClassName());
246            pw.write("Message", info.getMessage());
247            for (String name : info.getPropertyNames())
248            {
249                pw.write(name, info.getProperty(name));
250            }
251            if (!info.getStackTrace().isEmpty())
252            {
253                writer.write("\n  Stack trace:\n");
254                for (StackTraceElement e : info.getStackTrace())
255                {
256                    f.format("  - %s\n", e.toString());
257                }
258            }
259            writer.println();
260        }
261
262        if (request != null)
263        {
264            writer.print("REQUEST:\n\nBasic Information:\n");
265            List<String> flags = CollectionFactory.newList();
266            if (request.isXHR())
267            {
268                flags.add("XHR");
269            }
270            if (request.isRequestedSessionIdValid())
271            {
272                flags.add("requestedSessionIdValid");
273            }
274            if (request.isSecure())
275            {
276                flags.add("secure");
277            }
278            pw.write("contextPath", contextPath);
279            if (!flags.isEmpty())
280            {
281                pw.write("flags", InternalUtils.joinSorted(flags));
282            }
283            pw.write("method", request.getMethod());
284            pw.write("path", request.getPath());
285            pw.write("locale", request.getLocale());
286            pw.write("serverName", request.getServerName());
287            pw.write("remoteHost", request.getRemoteHost());
288            writer.print("\nHeaders:\n");
289            for (String name : request.getHeaderNames())
290            {
291                pw.write(name, request.getHeader(name));
292            }
293            if (!request.getParameterNames().isEmpty())
294            {
295                writer.print("\nParameters:\n");
296                for (String name : request.getParameterNames())
297                {
298                    // TODO: Support multi-value parameters
299                    pw.write(name, request.getParameters(name));
300                }
301            }
302            // TODO: Session if it exists
303        }
304
305        writer.print("\nSYSTEM INFORMATION:");
306
307        Runtime runtime = Runtime.getRuntime();
308
309        f.format("\n\nMemory:\n  %,15d bytes free\n  %,15d bytes total\n  %,15d bytes max\n",
310                runtime.freeMemory(),
311                runtime.totalMemory(),
312                runtime.maxMemory());
313
314        Thread[] threads = TapestryInternalUtils.getAllThreads();
315
316        int maxThreadNameLength = 0;
317
318        for (Thread t : threads)
319        {
320            maxThreadNameLength = Math.max(maxThreadNameLength, t.getName().length());
321        }
322
323        String format = "\n%s %" + maxThreadNameLength + "s %s";
324
325        f.format("\n%,d Threads:", threads.length);
326
327        for (Thread t : threads)
328        {
329            f.format(format,
330                    Thread.currentThread() == t ? "*" : " ",
331                    t.getName(),
332                    t.getState().name());
333            if (t.isDaemon())
334            {
335                writer.write(", daemon");
336            }
337            if (!t.isAlive())
338            {
339                writer.write(", NOT alive");
340            }
341            if (t.isInterrupted())
342            {
343                writer.write(", interrupted");
344            }
345            if (t.getPriority() != Thread.NORM_PRIORITY)
346            {
347                f.format(", priority %d", t.getPriority());
348            }
349        }
350        writer.println();
351
352        f.close();
353    }
354
355}