View Javadoc

1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.myfaces.renderkit;
20  
21  import java.beans.BeanInfo;
22  import java.beans.Introspector;
23  import java.beans.PropertyDescriptor;
24  import java.io.ByteArrayOutputStream;
25  import java.io.IOException;
26  import java.io.InputStream;
27  import java.io.PrintWriter;
28  import java.io.Serializable;
29  import java.io.StringWriter;
30  import java.io.Writer;
31  import java.lang.reflect.Method;
32  import java.text.DateFormat;
33  import java.util.ArrayList;
34  import java.util.Arrays;
35  import java.util.Collection;
36  import java.util.Date;
37  import java.util.EnumSet;
38  import java.util.HashMap;
39  import java.util.Iterator;
40  import java.util.List;
41  import java.util.Map;
42  import java.util.SortedMap;
43  import java.util.TreeMap;
44  import java.util.logging.Level;
45  import java.util.logging.Logger;
46  import java.util.regex.Matcher;
47  import java.util.regex.Pattern;
48  
49  import javax.el.Expression;
50  import javax.el.ValueExpression;
51  import javax.faces.FacesException;
52  import javax.faces.component.EditableValueHolder;
53  import javax.faces.component.UIColumn;
54  import javax.faces.component.UIComponent;
55  import javax.faces.component.UIData;
56  import javax.faces.component.UIViewRoot;
57  import javax.faces.component.visit.VisitCallback;
58  import javax.faces.component.visit.VisitContext;
59  import javax.faces.component.visit.VisitHint;
60  import javax.faces.component.visit.VisitResult;
61  import javax.faces.context.ExternalContext;
62  import javax.faces.context.FacesContext;
63  import javax.faces.context.PartialResponseWriter;
64  import javax.faces.context.ResponseWriter;
65  import javax.faces.el.MethodBinding;
66  import javax.faces.el.ValueBinding;
67  import javax.faces.render.Renderer;
68  import javax.faces.view.Location;
69  import javax.servlet.http.HttpServletResponse;
70  
71  import org.apache.myfaces.buildtools.maven2.plugin.builder.annotation.JSFWebConfigParam;
72  import org.apache.myfaces.lifecycle.ViewNotFoundException;
73  import org.apache.myfaces.shared.renderkit.html.HtmlResponseWriterImpl;
74  import org.apache.myfaces.shared.util.ClassUtils;
75  import org.apache.myfaces.shared.util.StateUtils;
76  import org.apache.myfaces.spi.WebConfigProvider;
77  import org.apache.myfaces.spi.WebConfigProviderFactory;
78  import org.apache.myfaces.view.facelets.component.UIRepeat;
79  import org.apache.myfaces.view.facelets.el.ContextAware;
80  
81  /**
82   * This class provides utility methods to generate the
83   * MyFaces error and debug pages. 
84   *
85   * @author Jacob Hookom (ICLA with ASF filed)
86   * @author Jakob Korherr (refactored and moved here from javax.faces.webapp._ErrorPageWriter)
87   */
88  public final class ErrorPageWriter
89  {
90  
91      /**
92       * This bean aims to generate the error page html for inclusion on a facelet error page via
93       * <ui:include src="javax.faces.error.xhtml" />. When performing this include the facelet
94       * "myfaces-dev-error-include.xhtml" will be included. This facelet references to the ErrorPageBean.
95       * This also works for custom error page templates.
96       * The bean is added to the ViewMap of the UIViewRoot, which is 
97       * displaying the error page, in RestoreViewExecutor.execute().
98       * @author Jakob Korherr
99       */
100     public static class ErrorPageBean implements Serializable
101     {
102 
103         private static final long serialVersionUID = -79513324193326616L;
104 
105         public String getErrorPageHtml() throws IOException
106         {
107             FacesContext facesContext = FacesContext.getCurrentInstance();
108             Map<String, Object> requestMap = facesContext.getExternalContext().getRequestMap();
109 
110             Throwable t = (Throwable) requestMap.get(EXCEPTION_KEY);
111             if (t == null)
112             {
113                 throw new IllegalStateException("No Exception to handle");
114             }
115 
116             UIViewRoot view = (UIViewRoot) requestMap.get(VIEW_KEY);
117 
118             StringWriter writer = new StringWriter();
119             ErrorPageWriter.debugHtml(writer, facesContext, view, null, t);
120             String html = writer.toString();
121 
122             // change the HTML in the buffer to be included in an existing html page
123             String body;
124             try
125             {
126                 body = html.substring(html.indexOf("<body>") + "<body>".length(), html.indexOf("</body>"));
127             }
128             catch (Exception e)
129             {
130                 // no body found - return the entire html
131                 return html;
132             }
133 
134             String head;
135             try
136             {
137                 head = html.substring(html.indexOf("<head>") + "<head>".length(), html.indexOf("</head>"));
138             }
139             catch (Exception e)
140             {
141                 // no head found - return entire body
142                 return body;
143             }
144 
145             // extract style and script information from head and add it to body
146             StringBuilder builder = new StringBuilder(body);
147             // extract <style>
148             int startIndex = 0;
149             while (true)
150             {
151                 try
152                 {
153                     int endIndex = head.indexOf("</style>", startIndex) + "</style>".length();
154                     builder.append(head.substring(head.indexOf("<style", startIndex), endIndex));
155                     startIndex = endIndex;
156                 }
157                 catch (Exception e)
158                 {
159                     // no style found - break extraction
160                     break;
161                 }
162             }
163             // extract <script>
164             startIndex = 0;
165             while (true)
166             {
167                 try
168                 {
169                     int endIndex = head.indexOf("</script>", startIndex) + "</script>".length();
170                     builder.append(head.substring(head.indexOf("<script", startIndex), endIndex));
171                     startIndex = endIndex;
172                 }
173                 catch (Exception e)
174                 {
175                     // no script found - break extraction
176                     break;
177                 }
178             }
179 
180             return builder.toString();
181         }
182 
183     }
184 
185     /**
186      * The key which is used to store the ErrorPageBean in the view map of a facelet error page.
187      */
188     public static final String ERROR_PAGE_BEAN_KEY = "__myFacesErrorPageBean";
189 
190     private static final String EXCEPTION_KEY = "javax.servlet.error.exception";
191     public static final String VIEW_KEY = "org.apache.myfaces.error.UIViewRoot";
192 
193     private static final Logger log = Logger.getLogger(ErrorPageWriter.class.getName());
194 
195     private final static String TS = "&lt;";
196 
197     private static final String ERROR_TEMPLATE = "META-INF/rsc/myfaces-dev-error.xml";
198 
199     /**
200      * Indicate the template name used to render the default error page used by MyFaces specific 
201      * error handler implementation. 
202      *
203      * <p>See org.apache.myfaces.ERROR_HANDLING for details about
204      * how to enable/disable it.</p>
205      */
206     @JSFWebConfigParam(defaultValue="META-INF/rsc/myfaces-dev-error.xml", since="1.2.4")
207     private static final String ERROR_TEMPLATE_RESOURCE = "org.apache.myfaces.ERROR_TEMPLATE_RESOURCE";
208 
209     private static String[] errorParts;
210 
211     private static final String DEBUG_TEMPLATE = "META-INF/rsc/myfaces-dev-debug.xml";
212 
213     /**
214      * Indicate the template name used to render the default debug page (see ui:debug tag).
215      */
216     @JSFWebConfigParam(defaultValue="META-INF/rsc/myfaces-dev-debug.xml", since="1.2.4")
217     private static final String DEBUG_TEMPLATE_RESOURCE = "org.apache.myfaces.DEBUG_TEMPLATE_RESOURCE";
218 
219     private static String[] debugParts;
220 
221     private static final String REGEX_PATTERN = ".*?\\Q,Id:\\E\\s*(\\S+)\\s*\\].*?";
222 
223     private final static String[] IGNORE = new String[] { "parent", "rendererType" };
224 
225     private final static String[] ALWAYS_WRITE = new String[] { "class", "clientId" };
226 
227     /**
228      * Extended debug info is stored under this key in the request
229      * map for every UIInput component when in Development mode.
230      * ATTENTION: this constant is duplicate in javax.faces.component.UIInput
231      */
232     public static final String DEBUG_INFO_KEY = "org.apache.myfaces.debug.DEBUG_INFO";
233 
234     /**
235      * The number of facets of this component which have already been visited while
236      * creating the extended component tree is saved under this key in the component's
237      * attribute map.
238      */
239     private static final String VISITED_FACET_COUNT_KEY = "org.apache.myfaces.debug.VISITED_FACET_COUNT";
240     //private static Map<UIComponent, Integer> visitedFacetCount = new HashMap<UIComponent, Integer>();
241 
242     /**
243      * Indicate if myfaces is responsible to handle errors. 
244      * See http://wiki.apache.org/myfaces/Handling_Server_Errors for details.
245      */
246     @JSFWebConfigParam(defaultValue="false, on Development Project stage: true",
247                        expectedValues="true,false", since="1.2.4")
248     public static final String ERROR_HANDLING_PARAMETER = "org.apache.myfaces.ERROR_HANDLING";
249 
250     public ErrorPageWriter()
251     {
252         super();
253     }
254 
255     /**
256      * Generates the HTML error page for the given Throwable 
257      * and writes it to the given writer.
258      * @param writer
259      * @param faces
260      * @param e
261      * @throws IOException
262      */
263     public static void debugHtml(Writer writer, FacesContext faces, Throwable e) throws IOException
264     {
265         debugHtml(writer, faces, faces.getViewRoot(), null,  e);
266     }
267 
268     private static void debugHtml(Writer writer, FacesContext faces, UIViewRoot view,
269                                   Collection<UIComponent> components, Throwable... exs) throws IOException
270     {
271         _init(faces);
272         Date now = new Date();
273 
274         for (int i = 0; i < errorParts.length; i++)
275         {
276             if ("view".equals((errorParts[i])))
277             {
278                 if (faces.getViewRoot() != null)
279                 {
280                     String viewId = faces.getViewRoot().getViewId();
281                     writer.write("viewId=" + viewId);
282                     writer.write("<br/>");
283                     String realPath = null;
284                     try
285                     {
286                         //Could not work on tomcat 7 running by cargo
287                         realPath = faces.getExternalContext().getRealPath(viewId);
288                     }
289                     catch(Throwable e)
290                     {
291                         //swallow it
292                     }
293                     if (realPath != null)
294                     {
295                         writer.write("location=" + realPath);
296                         writer.write("<br/>");
297                     }
298                     writer.write("phaseId=" + faces.getCurrentPhaseId());
299                     writer.write("<br/>");
300                     writer.write("<br/>");
301                 }
302             }
303             else if ("message".equals(errorParts[i]))
304             {
305                 boolean printed = false;
306                 //Iterator<UIComponent> iterator = null;
307                 //if (components != null)
308                 //{ 
309                 //    iterator = components.iterator();
310                 //}
311                 for (Throwable e : exs)
312                 {
313                     String msg = e.getMessage();
314                     if (printed)
315                     {
316                         writer.write("<br/>");
317                     }
318                     if (msg != null)
319                     {
320                         writer.write(msg.replaceAll("<", TS));
321                     }
322                     else
323                     {
324                         writer.write(e.getClass().getName());
325                     }
326                     printed = true;
327                 }
328             }
329             else if ("trace".equals(errorParts[i]))
330             {
331                 boolean printed = false;
332                 for (Throwable e : exs)
333                 {
334                     if (printed)
335                     {
336                         writer.write("\n");
337                     }
338                     _writeException(writer, e);
339                     printed = true;
340                 }
341             }
342             else if ("now".equals(errorParts[i]))
343             {
344                 writer.write(DateFormat.getDateTimeInstance().format(now));
345             }
346             else if ("tree".equals(errorParts[i]))
347             {
348                 if (view != null)
349                 {
350                     List<String> errorIds = _getErrorId(components, exs);
351                     _writeComponent(faces, writer, view, errorIds, true);
352                 }
353             }
354             else if ("vars".equals(errorParts[i]))
355             {
356                 _writeVariables(writer, faces, view);
357             }
358             else if ("cause".equals(errorParts[i]))
359             {
360                 boolean printed = false;
361                 Iterator<UIComponent> iterator = null;
362                 if (components != null)
363                 {
364                     iterator = components.iterator();
365                 }
366                 for (Throwable e : exs)
367                 {
368                     if (printed)
369                     {
370                         writer.write("<br/>");
371                     }
372                     _writeCause(writer, e);
373                     if (iterator != null)
374                     {
375                         UIComponent uiComponent = iterator.next();
376                         if (uiComponent != null)
377                         {
378                             _writeComponent(faces, writer, uiComponent, null, /* writeChildren */false);
379                         }
380                     }
381                     printed = true;
382                 }
383             }
384             else
385             {
386                 writer.write(errorParts[i]);
387             }
388         }
389     }
390 
391     /**
392      * Generates the HTML debug page for the current view
393      * and writes it to the given writer.
394      * @param writer
395      * @param faces
396      * @throws IOException
397      */
398     public static void debugHtml(Writer writer, FacesContext faces) throws IOException
399     {
400         _init(faces);
401         Date now = new Date();
402         for (int i = 0; i < debugParts.length; i++)
403         {
404             if ("message".equals(debugParts[i]))
405             {
406                 writer.write(faces.getViewRoot().getViewId());
407             }
408             else if ("now".equals(debugParts[i]))
409             {
410                 writer.write(DateFormat.getDateTimeInstance().format(now));
411             }
412             else if ("tree".equals(debugParts[i]))
413             {
414                 _writeComponent(faces, writer, faces.getViewRoot(), null, true);
415             }
416             else if ("extendedtree".equals(debugParts[i]))
417             {
418                 _writeExtendedComponentTree(writer, faces);
419             }
420             else if ("vars".equals(debugParts[i]))
421             {
422                 _writeVariables(writer, faces, faces.getViewRoot());
423             }
424             else
425             {
426                 writer.write(debugParts[i]);
427             }
428         }
429     }
430 
431     public static void handle(FacesContext facesContext, Collection<UIComponent> components,
432                               Throwable... exs) throws FacesException
433     {
434         for (Throwable ex : exs)
435         {
436             _prepareExceptionStack(ex);
437         }
438 
439         if (!facesContext.getExternalContext().isResponseCommitted())
440         {
441             facesContext.getExternalContext().responseReset();
442         }
443 
444         int responseStatus = -1;
445         for (Throwable ex : exs)
446         {
447             if (ex instanceof ViewNotFoundException)
448             {
449                 responseStatus = HttpServletResponse.SC_NOT_FOUND;
450                 break;
451             }
452             else
453             {
454                 responseStatus = HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
455             }
456         }
457         if (responseStatus != -1)
458         {
459             facesContext.getExternalContext().setResponseStatus(responseStatus);
460         }
461 
462         // normal request --> html error page
463         facesContext.getExternalContext().setResponseContentType("text/html");
464         facesContext.getExternalContext().setResponseCharacterEncoding("UTF-8");
465         try
466         {
467             // We need the real one, because the one returned from FacesContext.getResponseWriter()
468             // is configured with the encoding of the view.
469             Writer writer = facesContext.getExternalContext().getResponseOutputWriter();
470             debugHtml(writer, facesContext, facesContext.getViewRoot(), components, exs);
471         }
472         catch(IOException ioe)
473         {
474             throw new FacesException("Could not write the error page", ioe);
475         }
476 
477         // mark the response as complete
478         facesContext.responseComplete();
479     }
480 
481     /**
482      * Handles the given Throwbale in the following way:
483      * If there is no <error-page> entry in web.xml, try to reset the current HttpServletResponse,
484      * generate the error page and call responseComplete(). If this fails, rethrow the Exception.
485      * If there is an <error-page> entry in web.xml, save the current UIViewRoot in the RequestMap
486      * with the key "org.apache.myfaces.error.UIViewRoot" to access it on the error page and
487      * rethrow the Exception to let it flow up to FacesServlet.service() and thus be handled by the container.
488      * @param facesContext
489      * @param ex
490      * @throws FacesException
491      * @deprecated Use MyFacesExceptionHandlerWrapperImpl and handle() method
492      */
493     @Deprecated
494     public static void handleThrowable(FacesContext facesContext, Throwable ex) throws FacesException
495     {
496         _prepareExceptionStack(ex);
497 
498         boolean errorPageWritten = false;
499 
500         // check if an error page is present in web.xml
501         // if so, do not generate an error page
502         //WebXml webXml = WebXml.getWebXml(facesContext.getExternalContext());
503         //if (webXml.isErrorPagePresent())
504         WebConfigProvider webConfigProvider = WebConfigProviderFactory.getWebConfigProviderFactory(
505                 facesContext.getExternalContext()).getWebConfigProvider(facesContext.getExternalContext());
506 
507         if(webConfigProvider.isErrorPagePresent(facesContext.getExternalContext()))
508         {
509             // save current view in the request map to access it on the error page
510             facesContext.getExternalContext().getRequestMap().put(VIEW_KEY, facesContext.getViewRoot());
511         }
512         else
513         {
514             // check for org.apache.myfaces.ERROR_HANDLING
515             // do not generate an error page if it is false
516             String errorHandling = facesContext.getExternalContext().getInitParameter(ERROR_HANDLING_PARAMETER);
517             boolean errorHandlingDisabled = (errorHandling != null && errorHandling.equalsIgnoreCase("false"));
518             if (!errorHandlingDisabled)
519             {
520                 // write the error page
521                 Object response = facesContext.getExternalContext().getResponse();
522                 if (response instanceof HttpServletResponse)
523                 {
524                     HttpServletResponse httpResp = (HttpServletResponse) response;
525                     if (!httpResp.isCommitted())
526                     {
527                         httpResp.reset();
528                         if (facesContext.getPartialViewContext().isAjaxRequest())
529                         {
530                             // ajax request --> xml error page 
531                             httpResp.setContentType("text/xml; charset=UTF-8");
532                             try
533                             {
534                                 Writer writer = httpResp.getWriter();
535                                 // can't use facesContext.getResponseWriter(), because it might not have been set
536                                 ResponseWriter responseWriter = new HtmlResponseWriterImpl(writer, "text/xml", "utf-8");
537                                 PartialResponseWriter partialWriter = new PartialResponseWriter(responseWriter);
538                                 partialWriter.startDocument();
539                                 partialWriter.startError(ex.getClass().getName());
540                                 if (ex.getCause() != null)
541                                 {
542                                     partialWriter.write(ex.getCause().toString());
543                                 }
544                                 else if (ex.getMessage() != null)
545                                 {
546                                     partialWriter.write(ex.getMessage());
547                                 }
548                                 partialWriter.endError();
549                                 partialWriter.endDocument();
550                             }
551                             catch(IOException ioe)
552                             {
553                                 throw new FacesException("Could not write the error page", ioe);
554                             }
555                         }
556                         else
557                         {
558                             // normal request --> html error page
559                             httpResp.setContentType("text/html; charset=UTF-8");
560                             try
561                             {
562                                 Writer writer = httpResp.getWriter();
563                                 debugHtml(writer, facesContext, ex);
564                             }
565                             catch(IOException ioe)
566                             {
567                                 throw new FacesException("Could not write the error page", ioe);
568                             }
569                         }
570                         log.log(Level.SEVERE, "An exception occurred", ex);
571 
572                         // mark the response as complete
573                         facesContext.responseComplete();
574 
575                         errorPageWritten = true;
576                     }
577                 }
578             }
579         }
580 
581         // rethrow the throwable, if we did not write the error page
582         if (!errorPageWritten)
583         {
584             if (ex instanceof FacesException)
585             {
586                 throw (FacesException) ex;
587             }
588             if (ex instanceof RuntimeException)
589             {
590                 throw (RuntimeException) ex;
591             }
592             throw new FacesException(ex);
593         }
594 
595     }
596 
597     private static String _getErrorTemplate(FacesContext context)
598     {
599         String errorTemplate = context.getExternalContext().getInitParameter(ERROR_TEMPLATE_RESOURCE);
600         if (errorTemplate != null)
601         {
602             return errorTemplate;
603         }
604         return ERROR_TEMPLATE;
605     }
606 
607     private static String _getDebugTemplate(FacesContext context)
608     {
609         String debugTemplate = context.getExternalContext().getInitParameter(DEBUG_TEMPLATE_RESOURCE);
610         if (debugTemplate != null)
611         {
612             return debugTemplate;
613         }
614         return DEBUG_TEMPLATE;
615     }
616 
617     private static void _init(FacesContext context) throws IOException
618     {
619         if (errorParts == null)
620         {
621             errorParts = _splitTemplate(_getErrorTemplate(context));
622         }
623 
624         if (debugParts == null)
625         {
626             debugParts = _splitTemplate(_getDebugTemplate(context));
627         }
628     }
629 
630     private static String[] _splitTemplate(String rsc) throws IOException
631     {
632         InputStream is = ClassUtils.getContextClassLoader().getResourceAsStream(rsc);
633         if (is == null)
634         {
635             // try to get the resource from ExternalContext
636             is = FacesContext.getCurrentInstance().getExternalContext().getResourceAsStream(rsc);
637             if (is == null)
638             {
639                 // fallback
640                 is = ErrorPageWriter.class.getClassLoader().getResourceAsStream(rsc);
641             }
642         }
643 
644         if (is == null)
645         {
646             // throw an IllegalArgumentException instead of a FileNotFoundException,
647             // because when using <ui:debug /> this error is hard to trace,
648             // because the Exception is thrown in the Renderer and so it seems like
649             // the facelet (or jsp) does not exist.
650             throw new IllegalArgumentException("Could not find resource " + rsc);
651         }
652         ByteArrayOutputStream baos = new ByteArrayOutputStream();
653         byte[] buff = new byte[512];
654         int read;
655         while ((read = is.read(buff)) != -1)
656         {
657             baos.write(buff, 0, read);
658         }
659         String str = baos.toString();
660         return str.split("@@");
661     }
662 
663     private static List<String> _getErrorId(Collection<UIComponent> components, Throwable... exs)
664     {
665         List<String> list = null;
666         for (Throwable e : exs)
667         {
668             String message = e.getMessage();
669 
670             if (message == null)
671             {
672                 continue;
673             }
674 
675             Pattern pattern = Pattern.compile(REGEX_PATTERN);
676             Matcher matcher = pattern.matcher(message);
677 
678             while (matcher.find())
679             {
680                 if (list == null)
681                 {
682                     list = new ArrayList<String>();
683                 }
684                 list.add(matcher.group(1));
685             }
686         }
687         if (list != null && list.size() > 0)
688         {
689             return list;
690         }
691         else if (components != null)
692         {
693             list = new ArrayList<String>();
694             for (UIComponent uiComponent : components)
695             {
696                 if (uiComponent  != null)
697                 {
698                     list.add(uiComponent.getId());
699                 }
700             }
701             return list;
702         }
703         return null;
704     }
705 
706     private static void _writeException(Writer writer, Throwable e) throws IOException
707     {
708         StringWriter str = new StringWriter(256);
709         PrintWriter pstr = new PrintWriter(str);
710         e.printStackTrace(pstr);
711         pstr.close();
712         writer.write(str.toString().replaceAll("<", TS));
713     }
714 
715     private static void _writeCause(Writer writer, Throwable ex) throws IOException
716     {
717         String msg = ex.getMessage();
718         String contextAwareLocation = null;
719         if (ex instanceof ContextAware)
720         {
721             ContextAware caex = (ContextAware) ex;
722             contextAwareLocation = caex.getLocation().toString() + "    " +
723                                    caex.getQName() + "=\"" +
724                                    caex.getExpressionString() + "\"";
725         }
726         while (ex.getCause() != null)
727         {
728             ex = ex.getCause();
729             if (ex instanceof ContextAware)
730             {
731                 ContextAware caex = (ContextAware) ex;
732                 contextAwareLocation = caex.getLocation().toString() + "    " +
733                                        caex.getQName() + "=\"" +
734                                        caex.getExpressionString() + "\"";
735             }
736             if (ex.getMessage() != null)
737             {
738                 msg = ex.getMessage();
739             }
740         }
741 
742         if (msg != null)
743         {
744             msg = ex.getClass().getName() + " - " + msg;
745             writer.write(msg.replaceAll("<", TS));
746         }
747         else
748         {
749             writer.write(ex.getClass().getName());
750         }
751         StackTraceElement stackTraceElement = ex.getStackTrace()[0];
752         writer.write("<br/> at " + stackTraceElement.toString());
753 
754         if (contextAwareLocation != null)
755         {
756             writer.write("<br/> <br/>");
757             writer.write(contextAwareLocation);
758             writer.write("<br/>");
759         }
760     }
761 
762     private static void _writeVariables(Writer writer, FacesContext faces, UIViewRoot view) throws IOException
763     {
764         ExternalContext ctx = faces.getExternalContext();
765         _writeVariables(writer, ctx.getRequestParameterMap(), "Request Parameters");
766         _writeVariables(writer, ctx.getRequestMap(), "Request Attributes");
767         if (view != null)
768         {
769           _writeVariables(writer, view.getViewMap(), "View Attributes");
770         }
771         if (ctx.getSession(false) != null)
772         {
773             _writeVariables(writer, ctx.getSessionMap(), "Session Attributes");
774         }
775         _writeVariables(writer, ctx.getFlash(), "Flash Attributes");
776         _writeVariables(writer, ctx.getApplicationMap(), "Application Attributes");
777     }
778 
779     private static void _writeVariables(Writer writer, Map<String, ? extends Object> vars, String caption)
780             throws IOException
781     {
782         writer.write("<table><caption>");
783         writer.write(caption);
784         writer.write("</caption><thead><tr><th style=\"width: 10%; \">Name</th>"
785                      + "<th style=\"width: 90%; \">Value</th></tr></thead><tbody>");
786         boolean written = false;
787         if (!vars.isEmpty())
788         {
789             SortedMap<String, Object> sortedMap = new TreeMap<String, Object>(vars);
790             for (Map.Entry<String, Object> entry : sortedMap.entrySet())
791             {
792                 String key = entry.getKey().toString();
793                 if (key.indexOf('.') == -1)
794                 {
795                     writer.write("<tr><td>");
796                     writer.write(key.replaceAll("<", TS));
797                     writer.write("</td><td>");
798                     Object value = entry.getValue();
799                     // in some (very rare) situations value can be null or not null
800                     // but with null toString() representation
801                     if (value != null && value.toString() != null)
802                     {
803                         writer.write(value.toString().replaceAll("<", TS));
804                     }
805                     else
806                     {
807                         writer.write("null");
808                     }
809                     writer.write("</td></tr>");
810                     written = true;
811                 }
812             }
813         }
814         if (!written)
815         {
816             writer.write("<tr><td colspan=\"2\"><em>None</em></td></tr>");
817         }
818         writer.write("</tbody></table>");
819     }
820 
821     private static void _writeComponent(FacesContext faces, Writer writer, UIComponent c, List<String> highlightId,
822                                         boolean writeChildren) throws IOException
823     {
824         writer.write("<dl><dt");
825         if (_isText(c))
826         {
827             writer.write(" class=\"uicText\"");
828         }
829         if (highlightId != null)
830         {
831             if ((highlightId.size() > 0))
832             {
833                 String id = c.getId();
834                 if (highlightId.contains(id))
835                 {
836                     writer.write(" class=\"highlightComponent\"");
837                 }
838             }
839         }
840         writer.write(">");
841 
842         boolean hasChildren = (c.getChildCount() > 0 || c.getFacetCount() > 0) && writeChildren;
843 
844         int stateSize = 0;
845 
846         Object state = c.saveState(faces);
847         if (state != null)
848         {
849             try
850             {
851                 byte[] stateBytes = StateUtils.getAsByteArray(state, faces.getExternalContext());
852                 stateSize = stateBytes.length;
853             }
854             catch (Exception e)
855             {
856                 stateSize = -1;
857                 if (log.isLoggable(Level.FINEST))
858                 {
859                     log.fine("Could not determine state size: " + e.getMessage());
860                 }
861             }
862         }
863         _writeStart(writer, c, hasChildren, true);
864         writer.write(" - State size:" + stateSize + " bytes");
865         writer.write("</dt>");
866         if (hasChildren)
867         {
868             if (c.getFacetCount() > 0)
869             {
870                 for (Map.Entry<String, UIComponent> entry : c.getFacets().entrySet())
871                 {
872                     writer.write("<dd class=\"uicFacet\">");
873                     writer.write("<span>");
874                     writer.write(entry.getKey());
875                     writer.write("</span>");
876                     _writeComponent(faces, writer, entry.getValue(), highlightId, true);
877                     writer.write("</dd>");
878                 }
879             }
880             if (c.getChildCount() > 0)
881             {
882                 for (int i = 0, childCount = c.getChildCount(); i < childCount; i++)
883                 {
884                     UIComponent child = c.getChildren().get(i);
885                     writer.write("<dd>");
886                     _writeComponent(faces, writer, child, highlightId, writeChildren);
887                     writer.write("</dd>");
888                 }
889             }
890             writer.write("<dt>");
891             _writeEnd(writer, c);
892             writer.write("</dt>");
893         }
894         writer.write("</dl>");
895     }
896 
897     /**
898      * Creates the Extended Component Tree via UIViewRoot.visitTree()
899      * and ExtendedComponentTreeVisitCallback as VisitCallback.
900      *
901      * @param writer
902      * @param facesContext
903      * @throws IOException
904      */
905     private static void _writeExtendedComponentTree(Writer writer,
906             FacesContext facesContext) throws IOException
907     {
908         VisitContext visitContext = VisitContext.createVisitContext(
909                 facesContext, null, EnumSet.of(VisitHint.SKIP_UNRENDERED));
910         facesContext.getViewRoot().visitTree(visitContext, new ExtendedComponentTreeVisitCallback(writer));
911         _clearVisitedFacetCountMap(facesContext);
912     }
913 
914     /**
915      * The VisitCallback that is used to create the Extended Component Tree.
916      *
917      * @author Jakob Korherr
918      */
919     private static class ExtendedComponentTreeVisitCallback implements VisitCallback
920     {
921 
922         private Writer _writer;
923 
924         public ExtendedComponentTreeVisitCallback(Writer writer)
925         {
926             _writer = writer;
927         }
928 
929         @SuppressWarnings("unchecked")
930         public VisitResult visit(VisitContext context, UIComponent target)
931         {
932             final Map<String, Object> requestMap = context.getFacesContext()
933                     .getExternalContext().getRequestMap();
934 
935             try
936             {
937                 if (!(target instanceof UIViewRoot))
938                 {
939                     _writer.write("<dd>");
940                 }
941 
942                 UIComponent parent = target.getParent();
943                 boolean hasChildren = (target.getChildCount() > 0 || target.getFacetCount() > 0);
944                 String facetName = _getFacetName(target);
945 
946                 if (!(target instanceof UIColumn))
947                 {
948                     if (parent instanceof UIColumn
949                             && ((parent.getChildCount() > 0 && parent.getChildren().get(0) == target)
950                                     ||  (facetName != null &&
951                                             _getVisitedFacetCount(context.getFacesContext(), parent) == 0)))
952                     {
953                         if (parent.getParent() instanceof UIData
954                                 && _isFirstUIColumn(parent.getParent(), (UIColumn) parent))
955                         {
956                             _writer.write("<span>Row: ");
957                             int rowIndex = ((UIData) parent.getParent()).getRowIndex();
958                             _writer.write("" + rowIndex);
959                             if (rowIndex == -1)
960                             {
961                                 // tell the user that rowIndex == -1 stands for visiting column-facets
962                                 _writer.write(" (all column facets)");
963                             }
964                             _writer.write("</span>");
965                         }
966                         _writer.write("<dl><dt>");
967                         _writeStart(_writer, parent, true, false);
968                         _writer.write("</dt><dd>");
969                     }
970 
971                     if (facetName != null)
972                     {
973                         _writer.write("<span>" + facetName + "</span>");
974                         _incrementVisitedFacetCount(context.getFacesContext(), parent);
975                     }
976                     _writer.write("<dl><dt");
977                     if (_isText(target))
978                     {
979                         _writer.write(" class=\"uicText\"");
980                     }
981                     _writer.write(">");
982 
983                     Map<String, List<Object[]>> debugInfos = null;
984                     // is the target a EditableValueHolder component?
985                     // If so, debug infos from DebugPhaseListener should be available
986                     if (target instanceof EditableValueHolder)
987                     {
988                         // get the debug info
989                         debugInfos = (Map<String, List<Object[]>>) requestMap
990                                 .get(DEBUG_INFO_KEY + target.getClientId());
991                     }
992 
993                     // Get the component's renderer.
994                     // Note that getRenderer(FacesContext context) is definded in UIComponent,
995                     // but it is protected, so we have to use reflection!
996                     Renderer renderer = null;
997                     try
998                     {
999                         Method getRenderer = UIComponent.class.getDeclaredMethod(
1000                                 "getRenderer", FacesContext.class);
1001                         // make it accessible for us!
1002                         getRenderer.setAccessible(true);
1003                         renderer = (Renderer) getRenderer.invoke(target, context.getFacesContext());
1004                     }
1005                     catch (Exception e)
1006                     {
1007                         // nothing - do not output renderer information
1008                     }
1009 
1010                     // write the component start
1011                     _writeStart(_writer, target, (hasChildren || debugInfos != null || renderer != null), false);
1012                     _writer.write("</dt>");
1013 
1014                     if (renderer != null)
1015                     {
1016                         // write renderer info
1017                         _writer.write("<div class=\"renderer\">Rendered by ");
1018                         _writer.write(renderer.getClass().getCanonicalName());
1019                         _writer.write("</div>");
1020 
1021                         if (!hasChildren && debugInfos == null)
1022                         {
1023                             // close the component
1024                             _writer.write("<dt>");
1025                             _writeEnd(_writer, target);
1026                             _writer.write("</dt>");
1027                         }
1028                     }
1029 
1030                     if (debugInfos != null)
1031                     {
1032                         final String fieldid = target.getClientId() + "_lifecycle";
1033                         _writer.write("<div class=\"lifecycle_values_wrapper\">");
1034                         _writer.write("<a href=\"#\" onclick=\"toggle('");
1035                         _writer.write(fieldid);
1036                         _writer.write("'); return false;\"><span id=\"");
1037                         _writer.write(fieldid);
1038                         _writer.write("Off\">+</span><span id=\"");
1039                         _writer.write(fieldid);
1040                         _writer.write("On\" style=\"display: none;\">-</span> Value Lifecycle</a>");
1041                         _writer.write("<div id=\"");
1042                         _writer.write(fieldid);
1043                         _writer.write("\" class=\"lifecycle_values\">");
1044 
1045                         // process any available debug info
1046                         for (Map.Entry<String, List<Object[]>> entry : debugInfos.entrySet())
1047                         {
1048                             _writer.write("<span>");
1049                             _writer.write(entry.getKey());
1050                             _writer.write("</span><ol>");
1051                             int i = 0;
1052                             for (Object[] debugInfo : entry.getValue())
1053                             {
1054                                 // structure of the debug-info array:
1055                                 //     - 0: phase
1056                                 //     - 1: old value
1057                                 //     - 2: new value
1058                                 //     - 3: StackTraceElement List
1059 
1060                                 // oldValue and newValue could be null
1061                                 String oldValue = debugInfo[1] == null ? "null" : debugInfo[1].toString();
1062                                 String newValue = debugInfo[2] == null ? "null" : debugInfo[2].toString();
1063                                 _writer.write("<li><b>");
1064                                 _writer.write(entry.getKey());
1065                                 _writer.write("</b> set from <b>");
1066                                 _writer.write(oldValue);
1067                                 _writer.write("</b> to <b>");
1068                                 _writer.write(newValue);
1069                                 _writer.write("</b> in Phase ");
1070                                 _writer.write(debugInfo[0].toString());
1071 
1072                                 // check if a call stack is available
1073                                 if (debugInfo[3] != null)
1074                                 {
1075                                     final String stackTraceId = fieldid + "_" + entry.getKey() + "_" + i;
1076                                     _writer.write("<div class=\"stacktrace_wrapper\">");
1077                                     _writer.write("<a href=\"#\" onclick=\"toggle('");
1078                                     _writer.write(stackTraceId);
1079                                     _writer.write("'); return false;\"><span id=\"");
1080                                     _writer.write(stackTraceId);
1081                                     _writer.write("Off\">+</span><span id=\"");
1082                                     _writer.write(stackTraceId);
1083                                     _writer.write("On\" style=\"display: none;\">-</span> Call Stack</a>");
1084                                     _writer.write("<div id=\"");
1085                                     _writer.write(stackTraceId);
1086                                     _writer.write("\" class=\"stacktrace_values\">");
1087                                     _writer.write("<ul>");
1088                                     for (StackTraceElement stackTraceElement
1089                                             : (List<StackTraceElement>) debugInfo[3])
1090                                     {
1091                                         _writer.write("<li>");
1092                                         _writer.write(stackTraceElement.toString());
1093                                         _writer.write("</li>");
1094                                     }
1095                                     _writer.write("</ul></div></div>");
1096                                 }
1097 
1098                                 _writer.write("</li>");
1099 
1100                                 i++;
1101                             }
1102                             _writer.write("</ol>");
1103                         }
1104 
1105                         _writer.write("</div></div>");
1106 
1107                         // now remove the debug info from the request map, 
1108                         // so that it does not appear in the scope values of the debug page 
1109                         requestMap.remove(DEBUG_INFO_KEY + target.getClientId());
1110 
1111                         if (!hasChildren)
1112                         {
1113                             // close the component
1114                             _writer.write("<dt>");
1115                             _writeEnd(_writer, target);
1116                             _writer.write("</dt>");
1117                         }
1118                     }
1119                 }
1120 
1121                 if (!hasChildren)
1122                 {
1123                     _writer.write("</dl>");
1124 
1125                     while (parent != null &&
1126                            ((parent.getChildCount()>0 && parent.getChildren().get(parent.getChildCount()-1) == target)
1127                                     || (parent.getFacetCount() != 0
1128                                             && _getVisitedFacetCount(context.getFacesContext(), parent) == 
1129                                                     parent.getFacetCount())))
1130                     {
1131                         // target is last child of parent or the "last" facet
1132 
1133                         // remove the visited facet count from the attribute map
1134                         _removeVisitedFacetCount(context.getFacesContext(), parent);
1135 
1136                         // check for componentes that visit their children multiple times
1137                         if (parent instanceof UIData)
1138                         {
1139                             UIData uidata = (UIData) parent;
1140                             if (uidata.getRowIndex() != uidata.getRowCount() - 1)
1141                             {
1142                                 // only continue if we're in the last row
1143                                 break;
1144                             }
1145                         }
1146                         else if (parent instanceof UIRepeat)
1147                         {
1148                             UIRepeat uirepeat = (UIRepeat) parent;
1149                             if (uirepeat.getIndex() + uirepeat.getStep() < uirepeat.getRowCount())
1150                             {
1151                                 // only continue if we're in the last row
1152                                 break;
1153                             }
1154                         }
1155 
1156                         _writer.write("</dd><dt>");
1157                         _writeEnd(_writer, parent);
1158                         _writer.write("</dt></dl>");
1159 
1160                         if (!(parent instanceof UIViewRoot))
1161                         {
1162                             _writer.write("</dd>");
1163                         }
1164 
1165                         target = parent;
1166                         parent = target.getParent();
1167                     }
1168                 }
1169             }
1170             catch (IOException ioe)
1171             {
1172                 throw new FacesException(ioe);
1173             }
1174 
1175             return VisitResult.ACCEPT;
1176         }
1177 
1178     }
1179 
1180     private static boolean _isFirstUIColumn(UIComponent uidata, UIColumn uicolumn)
1181     {
1182         for (int i = 0, childCount = uidata.getChildCount(); i < childCount; i++)
1183         {
1184             UIComponent child = uidata.getChildren().get(i);
1185             if (child instanceof UIColumn)
1186             {
1187                 return (child == uicolumn);
1188             }
1189         }
1190         return false;
1191     }
1192 
1193     private static String _getFacetName(UIComponent component)
1194     {
1195         UIComponent parent = component.getParent();
1196         if (parent != null)
1197         {
1198             if (parent.getFacetCount() > 0)
1199             {
1200                 for (Map.Entry<String, UIComponent> entry : parent.getFacets().entrySet())
1201                 {
1202                     if (entry.getValue() == component)
1203                     {
1204                         return entry.getKey();
1205                     }
1206                 }
1207             }
1208         }
1209         return null;
1210     }
1211 
1212     private static int _getVisitedFacetCount(FacesContext facesContext, UIComponent component)
1213     {
1214         Map<UIComponent, Integer> visitedFacetCount = (Map<UIComponent, Integer>)
1215             facesContext.getAttributes().get(VISITED_FACET_COUNT_KEY);
1216         if (visitedFacetCount == null)
1217         {
1218             return 0;
1219         }
1220         Integer count = visitedFacetCount.get(component);
1221         if (count != null)
1222         {
1223             return count;
1224         }
1225         return 0;
1226     }
1227 
1228     private static void _incrementVisitedFacetCount(FacesContext facesContext, UIComponent component)
1229     {
1230         Map<UIComponent, Integer> visitedFacetCount = (Map<UIComponent, Integer>)
1231             facesContext.getAttributes().get(VISITED_FACET_COUNT_KEY);
1232         if (visitedFacetCount == null)
1233         {
1234             visitedFacetCount = new HashMap<UIComponent, Integer>();
1235             facesContext.getAttributes().put(VISITED_FACET_COUNT_KEY, visitedFacetCount);
1236         }
1237         visitedFacetCount.put(component, _getVisitedFacetCount(facesContext, component) + 1);
1238     }
1239 
1240     private static void _removeVisitedFacetCount(FacesContext facesContext, UIComponent component)
1241     {
1242         Map<UIComponent, Integer> visitedFacetCount = (Map<UIComponent, Integer>)
1243             facesContext.getAttributes().get(VISITED_FACET_COUNT_KEY);
1244         if (visitedFacetCount == null)
1245         {
1246             return;
1247         }
1248         visitedFacetCount.remove(component);
1249     }
1250     
1251     private static void _clearVisitedFacetCountMap(FacesContext facesContext)
1252     {
1253         Map<UIComponent, Integer> visitedFacetCount = (Map<UIComponent, Integer>)
1254             facesContext.getAttributes().get(VISITED_FACET_COUNT_KEY);
1255         if (visitedFacetCount != null)
1256         {
1257             visitedFacetCount.clear();
1258             facesContext.getAttributes().remove(VISITED_FACET_COUNT_KEY);
1259         }
1260     }
1261 
1262     private static void _writeEnd(Writer writer, UIComponent c) throws IOException
1263     {
1264         if (!_isText(c))
1265         {
1266             writer.write(TS);
1267             writer.write('/');
1268             writer.write(_getName(c));
1269             writer.write('>');
1270         }
1271     }
1272 
1273     private static void _writeAttributes(Writer writer, UIComponent c, boolean valueExpressionValues)
1274     {
1275         try
1276         {
1277             BeanInfo info = Introspector.getBeanInfo(c.getClass());
1278             PropertyDescriptor[] pd = info.getPropertyDescriptors();
1279             Method m = null;
1280             Object v = null;
1281             ValueExpression valueExpression = null;
1282             String str = null;
1283             for (int i = 0; i < pd.length; i++)
1284             {
1285                 if ((pd[i].getWriteMethod() != null || Arrays.binarySearch(ALWAYS_WRITE, pd[i].getName()) > -1)
1286                     && Arrays.binarySearch(IGNORE, pd[i].getName()) < 0)
1287                 {
1288                     m = pd[i].getReadMethod();
1289                     try
1290                     {
1291                         // first check if the property is a ValueExpression
1292                         valueExpression = c.getValueExpression(pd[i].getName());
1293                         if (valueExpressionValues && valueExpression != null)
1294                         {
1295                             String expressionString = valueExpression.getExpressionString();
1296                             if (null == expressionString)
1297                             {
1298                                 expressionString = "";
1299                             }
1300                             _writeAttribute(writer, pd[i].getName(), expressionString);
1301                         }
1302                         else
1303                         {
1304                             v = m.invoke(c, null);
1305                             if (v != null)
1306                             {
1307                                 if (v instanceof Collection || v instanceof Map || v instanceof Iterator)
1308                                 {
1309                                     continue;
1310                                 }
1311                                 if (v instanceof Expression)
1312                                 {
1313                                     str = ((Expression)v).getExpressionString();
1314                                 }
1315                                 else if (v instanceof ValueBinding)
1316                                 {
1317                                     str = ((ValueBinding) v).getExpressionString();
1318                                 }
1319                                 else if (v instanceof MethodBinding)
1320                                 {
1321                                     str = ((MethodBinding) v).getExpressionString();
1322                                 }
1323                                 else
1324                                 {
1325                                     str = v.toString();
1326                                 }
1327 
1328                                 _writeAttribute(writer, pd[i].getName(), str);
1329                             }
1330                         }
1331                     }
1332                     catch (Exception e)
1333                     {
1334                         // do nothing
1335                     }
1336                 }
1337             }
1338 
1339             ValueExpression binding = c.getValueExpression("binding");
1340             if (binding != null)
1341             {
1342                 _writeAttribute(writer, "binding", binding.getExpressionString());
1343             }
1344 
1345             // write the location
1346             String location = _getComponentLocation(c);
1347             if (location != null)
1348             {
1349                 _writeAttribute(writer, "location", location);
1350             }
1351         }
1352         catch (Exception e)
1353         {
1354             // do nothing
1355         }
1356     }
1357 
1358     private static void _writeAttribute(Writer writer, String name, String value) throws IOException
1359     {
1360         writer.write(" ");
1361         writer.write(name);
1362         writer.write("=\"");
1363         writer.write(value.replaceAll("<", TS));
1364         writer.write("\"");
1365     }
1366 
1367     private static void _writeStart(Writer writer, UIComponent c,
1368             boolean children, boolean valueExpressionValues) throws IOException
1369     {
1370         if (_isText(c))
1371         {
1372             String str = c.toString().trim();
1373             writer.write(str.replaceAll("<", TS));
1374         }
1375         else
1376         {
1377             writer.write(TS);
1378             writer.write(_getName(c));
1379             _writeAttributes(writer, c, valueExpressionValues);
1380             if (children)
1381             {
1382                 writer.write('>');
1383             }
1384             else
1385             {
1386                 writer.write("/>");
1387             }
1388         }
1389     }
1390 
1391     private static String _getName(UIComponent c)
1392     {
1393         String nm = c.getClass().getName();
1394         return nm.substring(nm.lastIndexOf('.') + 1);
1395     }
1396 
1397     private static boolean _isText(UIComponent c)
1398     {
1399         return (c.getClass().getName().startsWith("org.apache.myfaces.view.facelets.compiler"));
1400     }
1401 
1402     private static void _prepareExceptionStack(Throwable ex)
1403     {
1404 
1405         if (ex == null)
1406         {
1407             return;
1408         }
1409 
1410         // check for getRootCause and getCause-methods
1411         if (!_initCausePerReflection(ex, "getRootCause"))
1412         {
1413             _initCausePerReflection(ex, "getCause");
1414         }
1415 
1416         _prepareExceptionStack(ex.getCause());
1417     }
1418 
1419     private static boolean _initCausePerReflection(Throwable ex, String methodName)
1420     {
1421         try
1422         {
1423             Method causeGetter = ex.getClass().getMethod(methodName, (Class[])null);
1424             Throwable rootCause = (Throwable)causeGetter.invoke(ex, (Object[])null);
1425             return _initCauseIfAvailable(ex, rootCause);
1426         }
1427         catch (Exception e1)
1428         {
1429             return false;
1430         }
1431     }
1432 
1433     private static boolean _initCauseIfAvailable(Throwable th, Throwable cause)
1434     {
1435         if (cause == null)
1436         {
1437             return false;
1438         }
1439 
1440         try
1441         {
1442             Method m = Throwable.class.getMethod("initCause", new Class[] { Throwable.class });
1443             m.invoke(th, new Object[] { cause });
1444             return true;
1445         }
1446         catch (Exception e)
1447         {
1448             return false;
1449         }
1450     }
1451 
1452     /**
1453      * Gets the Location of the given UIComponent from its attribute map.
1454      * @param component
1455      * @return
1456      */
1457     private static String _getComponentLocation(UIComponent component)
1458     {
1459         Location location = (Location) component.getAttributes()
1460                 .get(UIComponent.VIEW_LOCATION_KEY);
1461         if (location != null)
1462         {
1463             return location.toString();
1464         }
1465         return null;
1466     }
1467 }