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