View Javadoc
1   package org.apache.maven.plugins.surefire.report;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *     http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  
22  import java.io.File;
23  import java.text.NumberFormat;
24  import java.util.Iterator;
25  import java.util.List;
26  import java.util.Locale;
27  import java.util.Map;
28  import java.util.ResourceBundle;
29  import java.util.StringTokenizer;
30  import org.apache.maven.doxia.markup.HtmlMarkup;
31  import org.apache.maven.doxia.sink.Sink;
32  import org.apache.maven.doxia.sink.SinkEventAttributeSet;
33  import org.apache.maven.doxia.sink.SinkEventAttributes;
34  import org.apache.maven.doxia.util.DoxiaUtils;
35  import org.apache.maven.reporting.MavenReportException;
36  
37  /**
38   *
39   */
40  public class SurefireReportGenerator
41  {
42      private final SurefireReportParser report;
43  
44      private List<ReportTestSuite> testSuites;
45  
46      private final boolean showSuccess;
47  
48      private final String xrefLocation;
49  
50      private static final int LEFT = Sink.JUSTIFY_LEFT;
51  
52      public SurefireReportGenerator( List<File> reportsDirectories, Locale locale, boolean showSuccess,
53                                      String xrefLocation )
54      {
55          report = new SurefireReportParser( reportsDirectories, locale );
56  
57          this.xrefLocation = xrefLocation;
58  
59          this.showSuccess = showSuccess;
60      }
61  
62      public void doGenerateReport( ResourceBundle bundle, Sink sink )
63          throws MavenReportException
64      {
65          testSuites = report.parseXMLReportFiles();
66  
67          sink.head();
68  
69          sink.title();
70          sink.text( bundle.getString( "report.surefire.header" ) );
71          sink.title_();
72  
73          sink.head_();
74  
75          sink.body();
76  
77          SinkEventAttributeSet atts = new SinkEventAttributeSet();
78          atts.addAttribute( SinkEventAttributes.TYPE, "text/javascript" );
79          sink.unknown( "script", new Object[]{ HtmlMarkup.TAG_TYPE_START }, atts );
80          sink.unknown( "cdata", new Object[]{ HtmlMarkup.CDATA_TYPE, javascriptToggleDisplayCode() }, null );
81          sink.unknown( "script", new Object[]{ HtmlMarkup.TAG_TYPE_END }, null );
82  
83          sink.section1();
84          sink.sectionTitle1();
85          sink.text( bundle.getString( "report.surefire.header" ) );
86          sink.sectionTitle1_();
87          sink.section1_();
88  
89          constructSummarySection( bundle, sink );
90  
91          Map<String, List<ReportTestSuite>> suitePackages = report.getSuitesGroupByPackage( testSuites );
92          if ( !suitePackages.isEmpty() )
93          {
94              constructPackagesSection( bundle, sink, suitePackages );
95          }
96  
97          if ( !testSuites.isEmpty() )
98          {
99              constructTestCasesSection( bundle, sink );
100         }
101 
102         List<ReportTestCase> failureList = report.getFailureDetails( testSuites );
103         if ( !failureList.isEmpty() )
104         {
105             constructFailureDetails( sink, bundle, failureList );
106         }
107 
108         sink.body_();
109 
110         sink.flush();
111 
112         sink.close();
113     }
114 
115     private void constructSummarySection( ResourceBundle bundle, Sink sink )
116     {
117         Map<String, String> summary = report.getSummary( testSuites );
118 
119         sink.section1();
120         sink.sectionTitle1();
121         sink.text( bundle.getString( "report.surefire.label.summary" ) );
122         sink.sectionTitle1_();
123 
124         sinkAnchor( sink, "Summary" );
125 
126         constructHotLinks( sink, bundle );
127 
128         sinkLineBreak( sink );
129 
130         sink.table();
131 
132         sink.tableRows( new int[]{ LEFT, LEFT, LEFT, LEFT, LEFT, LEFT }, true );
133 
134         sink.tableRow();
135 
136         sinkHeader( sink, bundle.getString( "report.surefire.label.tests" ) );
137 
138         sinkHeader( sink, bundle.getString( "report.surefire.label.errors" ) );
139 
140         sinkHeader( sink, bundle.getString( "report.surefire.label.failures" ) );
141 
142         sinkHeader( sink, bundle.getString( "report.surefire.label.skipped" ) );
143 
144         sinkHeader( sink, bundle.getString( "report.surefire.label.successrate" ) );
145 
146         sinkHeader( sink, bundle.getString( "report.surefire.label.time" ) );
147 
148         sink.tableRow_();
149 
150         sink.tableRow();
151 
152         sinkCell( sink, summary.get( "totalTests" ) );
153 
154         sinkCell( sink, summary.get( "totalErrors" ) );
155 
156         sinkCell( sink, summary.get( "totalFailures" ) );
157 
158         sinkCell( sink, summary.get( "totalSkipped" ) );
159 
160         sinkCell( sink, summary.get( "totalPercentage" ) + "%" );
161 
162         sinkCell( sink, summary.get( "totalElapsedTime" ) );
163 
164         sink.tableRow_();
165 
166         sink.tableRows_();
167 
168         sink.table_();
169 
170         sink.lineBreak();
171 
172         sink.paragraph();
173         sink.text( bundle.getString( "report.surefire.text.note1" ) );
174         sink.paragraph_();
175 
176         sinkLineBreak( sink );
177 
178         sink.section1_();
179     }
180 
181     private void constructPackagesSection( ResourceBundle bundle, Sink sink,
182                                            Map<String, List<ReportTestSuite>> suitePackages )
183     {
184         NumberFormat numberFormat = report.getNumberFormat();
185 
186         sink.section1();
187         sink.sectionTitle1();
188         sink.text( bundle.getString( "report.surefire.label.packagelist" ) );
189         sink.sectionTitle1_();
190 
191         sinkAnchor( sink, "Package_List" );
192 
193         constructHotLinks( sink, bundle );
194 
195         sinkLineBreak( sink );
196 
197         sink.table();
198 
199         sink.tableRows( new int[]{ LEFT, LEFT, LEFT, LEFT, LEFT, LEFT, LEFT }, true );
200 
201         sink.tableRow();
202 
203         sinkHeader( sink, bundle.getString( "report.surefire.label.package" ) );
204 
205         sinkHeader( sink, bundle.getString( "report.surefire.label.tests" ) );
206 
207         sinkHeader( sink, bundle.getString( "report.surefire.label.errors" ) );
208 
209         sinkHeader( sink, bundle.getString( "report.surefire.label.failures" ) );
210 
211         sinkHeader( sink, bundle.getString( "report.surefire.label.skipped" ) );
212 
213         sinkHeader( sink, bundle.getString( "report.surefire.label.successrate" ) );
214 
215         sinkHeader( sink, bundle.getString( "report.surefire.label.time" ) );
216 
217         sink.tableRow_();
218 
219         for ( Map.Entry<String, List<ReportTestSuite>> entry : suitePackages.entrySet() )
220         {
221             sink.tableRow();
222 
223             String packageName = entry.getKey();
224 
225             List<ReportTestSuite> testSuiteList = entry.getValue();
226 
227             Map<String, String> packageSummary = report.getSummary( testSuiteList );
228 
229             sinkCellLink( sink, packageName, "#" + packageName );
230 
231             sinkCell( sink, packageSummary.get( "totalTests" ) );
232 
233             sinkCell( sink, packageSummary.get( "totalErrors" ) );
234 
235             sinkCell( sink, packageSummary.get( "totalFailures" ) );
236 
237             sinkCell( sink, packageSummary.get( "totalSkipped" ) );
238 
239             sinkCell( sink, packageSummary.get( "totalPercentage" ) + "%" );
240 
241             sinkCell( sink, packageSummary.get( "totalElapsedTime" ) );
242 
243             sink.tableRow_();
244         }
245 
246         sink.tableRows_();
247 
248         sink.table_();
249 
250         sink.lineBreak();
251 
252         sink.paragraph();
253         sink.text( bundle.getString( "report.surefire.text.note2" ) );
254         sink.paragraph_();
255 
256         for ( Map.Entry<String, List<ReportTestSuite>> entry : suitePackages.entrySet() )
257         {
258             String packageName = entry.getKey();
259 
260             List<ReportTestSuite> testSuiteList = entry.getValue();
261 
262             sink.section2();
263             sink.sectionTitle2();
264             sink.text( packageName );
265             sink.sectionTitle2_();
266 
267             sinkAnchor( sink, packageName );
268 
269             boolean showTable = false;
270 
271             for ( ReportTestSuite suite : testSuiteList )
272             {
273                 if ( showSuccess || suite.getNumberOfErrors() != 0 || suite.getNumberOfFailures() != 0 )
274                 {
275                     showTable = true;
276 
277                     break;
278                 }
279             }
280 
281             if ( showTable )
282             {
283                 sink.table();
284 
285                 sink.tableRows( new int[]{ LEFT, LEFT, LEFT, LEFT, LEFT, LEFT, LEFT, LEFT }, true );
286 
287                 sink.tableRow();
288 
289                 sinkHeader( sink, "" );
290 
291                 sinkHeader( sink, bundle.getString( "report.surefire.label.class" ) );
292 
293                 sinkHeader( sink, bundle.getString( "report.surefire.label.tests" ) );
294 
295                 sinkHeader( sink, bundle.getString( "report.surefire.label.errors" ) );
296 
297                 sinkHeader( sink, bundle.getString( "report.surefire.label.failures" ) );
298 
299                 sinkHeader( sink, bundle.getString( "report.surefire.label.skipped" ) );
300 
301                 sinkHeader( sink, bundle.getString( "report.surefire.label.successrate" ) );
302 
303                 sinkHeader( sink, bundle.getString( "report.surefire.label.time" ) );
304 
305                 sink.tableRow_();
306 
307                 for ( ReportTestSuite suite : testSuiteList )
308                 {
309                     if ( showSuccess || suite.getNumberOfErrors() != 0 || suite.getNumberOfFailures() != 0 )
310                     {
311                         constructTestSuiteSection( sink, numberFormat, suite );
312                     }
313                 }
314 
315                 sink.tableRows_();
316 
317                 sink.table_();
318             }
319 
320             sink.section2_();
321         }
322 
323         sinkLineBreak( sink );
324 
325         sink.section1_();
326     }
327 
328     private void constructTestSuiteSection( Sink sink, NumberFormat numberFormat, ReportTestSuite suite )
329     {
330         sink.tableRow();
331 
332         sink.tableCell();
333 
334         sink.link( "#" + suite.getPackageName() + suite.getName() );
335 
336         if ( suite.getNumberOfErrors() > 0 )
337         {
338             sinkIcon( "error", sink );
339         }
340         else if ( suite.getNumberOfFailures() > 0 )
341         {
342             sinkIcon( "junit.framework", sink );
343         }
344         else if ( suite.getNumberOfSkipped() > 0 )
345         {
346             sinkIcon( "skipped", sink );
347         }
348         else
349         {
350             sinkIcon( "success", sink );
351         }
352 
353         sink.link_();
354 
355         sink.tableCell_();
356 
357         sinkCellLink( sink, suite.getName(), "#" + suite.getPackageName() + suite.getName() );
358 
359         sinkCell( sink, Integer.toString( suite.getNumberOfTests() ) );
360 
361         sinkCell( sink, Integer.toString( suite.getNumberOfErrors() ) );
362 
363         sinkCell( sink, Integer.toString( suite.getNumberOfFailures() ) );
364 
365         sinkCell( sink, Integer.toString( suite.getNumberOfSkipped() ) );
366 
367         String percentage =
368             report.computePercentage( suite.getNumberOfTests(), suite.getNumberOfErrors(),
369                                       suite.getNumberOfFailures(), suite.getNumberOfSkipped() );
370         sinkCell( sink, percentage + "%" );
371 
372         sinkCell( sink, numberFormat.format( suite.getTimeElapsed() ) );
373 
374         sink.tableRow_();
375     }
376 
377     private void constructTestCasesSection( ResourceBundle bundle, Sink sink )
378     {
379         NumberFormat numberFormat = report.getNumberFormat();
380 
381         sink.section1();
382         sink.sectionTitle1();
383         sink.text( bundle.getString( "report.surefire.label.testcases" ) );
384         sink.sectionTitle1_();
385 
386         sinkAnchor( sink, "Test_Cases" );
387 
388         constructHotLinks( sink, bundle );
389 
390         for ( ReportTestSuite suite : testSuites )
391         {
392             List<ReportTestCase> testCases = suite.getTestCases();
393 
394             if ( testCases != null && !testCases.isEmpty() )
395             {
396                 sink.section2();
397                 sink.sectionTitle2();
398                 sink.text( suite.getName() );
399                 sink.sectionTitle2_();
400 
401                 sinkAnchor( sink, suite.getPackageName() + suite.getName() );
402 
403                 boolean showTable = false;
404 
405                 for ( ReportTestCase testCase : testCases )
406                 {
407                     if ( testCase.getFailure() != null || showSuccess )
408                     {
409                         showTable = true;
410 
411                         break;
412                     }
413                 }
414 
415                 if ( showTable )
416                 {
417                     sink.table();
418 
419                     sink.tableRows( new int[]{ LEFT, LEFT, LEFT }, true );
420 
421                     for ( ReportTestCase testCase : testCases )
422                     {
423                         if ( testCase.getFailure() != null || showSuccess )
424                         {
425                             constructTestCaseSection( sink, numberFormat, testCase );
426                         }
427                     }
428 
429                     sink.tableRows_();
430 
431                     sink.table_();
432                 }
433 
434                 sink.section2_();
435             }
436         }
437 
438         sinkLineBreak( sink );
439 
440         sink.section1_();
441     }
442 
443     private void constructTestCaseSection( Sink sink, NumberFormat numberFormat, ReportTestCase testCase )
444     {
445         sink.tableRow();
446 
447         sink.tableCell();
448 
449         Map<String, Object> failure = testCase.getFailure();
450 
451         if ( failure != null )
452         {
453             sink.link( "#" + toHtmlId( testCase.getFullName() ) );
454 
455             sinkIcon( (String) failure.get( "type" ), sink );
456 
457             sink.link_();
458         }
459         else
460         {
461             sinkIcon( "success", sink );
462         }
463 
464         sink.tableCell_();
465 
466         if ( failure != null )
467         {
468             sink.tableCell();
469 
470             sinkLink( sink, testCase.getName(), "#" + toHtmlId( testCase.getFullName() ) );
471 
472             SinkEventAttributeSet atts = new SinkEventAttributeSet();
473             atts.addAttribute( SinkEventAttributes.CLASS, "detailToggle" );
474             atts.addAttribute( SinkEventAttributes.STYLE, "display:inline" );
475             sink.unknown( "div", new Object[]{ HtmlMarkup.TAG_TYPE_START }, atts );
476 
477             sink.link( "javascript:toggleDisplay('" + toHtmlId( testCase.getFullName() ) + "');" );
478 
479             atts = new SinkEventAttributeSet();
480             atts.addAttribute( SinkEventAttributes.STYLE, "display:inline;" );
481             atts.addAttribute( SinkEventAttributes.ID, toHtmlId( testCase.getFullName() ) + "off" );
482             sink.unknown( "span", new Object[]{ HtmlMarkup.TAG_TYPE_START }, atts );
483             sink.text( " + " );
484             sink.unknown( "span", new Object[]{ HtmlMarkup.TAG_TYPE_END }, null );
485 
486             atts = new SinkEventAttributeSet();
487             atts.addAttribute( SinkEventAttributes.STYLE, "display:none;" );
488             atts.addAttribute( SinkEventAttributes.ID, toHtmlId( testCase.getFullName() ) + "on" );
489             sink.unknown( "span", new Object[]{ HtmlMarkup.TAG_TYPE_START }, atts );
490             sink.text( " - " );
491             sink.unknown( "span", new Object[]{ HtmlMarkup.TAG_TYPE_END }, null );
492 
493             sink.text( "[ Detail ]" );
494             sink.link_();
495 
496             sink.unknown( "div", new Object[]{ HtmlMarkup.TAG_TYPE_END }, null );
497 
498             sink.tableCell_();
499         }
500         else
501         {
502             sinkCell( sink, testCase.getName() );
503         }
504 
505         sinkCell( sink, numberFormat.format( testCase.getTime() ) );
506 
507         sink.tableRow_();
508 
509         if ( failure != null )
510         {
511             sink.tableRow();
512 
513             sinkCell( sink, "" );
514             sinkCell( sink, (String) failure.get( "message" ) );
515             sinkCell( sink, "" );
516             sink.tableRow_();
517 
518             List<String> detail = (List<String>) failure.get( "detail" );
519             if ( detail != null )
520             {
521 
522                 sink.tableRow();
523                 sinkCell( sink, "" );
524 
525                 sink.tableCell();
526                 SinkEventAttributeSet atts = new SinkEventAttributeSet();
527                 atts.addAttribute( SinkEventAttributes.ID,
528                                    toHtmlId( testCase.getFullName() ) + "error" );
529                 atts.addAttribute( SinkEventAttributes.STYLE, "display:none;" );
530                 sink.unknown( "div", new Object[]{ HtmlMarkup.TAG_TYPE_START }, atts );
531 
532                 sink.verbatim( null );
533                 for ( String line : detail )
534                 {
535                     sink.text( line );
536                     sink.lineBreak();
537                 }
538                 sink.verbatim_();
539 
540                 sink.unknown( "div", new Object[]{ HtmlMarkup.TAG_TYPE_END }, null );
541                 sink.tableCell_();
542 
543                 sinkCell( sink, "" );
544 
545                 sink.tableRow_();
546             }
547         }
548     }
549 
550 
551     private String toHtmlId( String id )
552     {
553         if ( DoxiaUtils.isValidId( id ) )
554         {
555             return id;
556         }
557         else
558         {
559             return DoxiaUtils.encodeId( id, true );
560         }
561     }
562 
563     private void constructFailureDetails( Sink sink, ResourceBundle bundle, List<ReportTestCase> failureList )
564     {
565         Iterator<ReportTestCase> failIter = failureList.iterator();
566 
567         if ( failIter != null )
568         {
569             sink.section1();
570             sink.sectionTitle1();
571             sink.text( bundle.getString( "report.surefire.label.failuredetails" ) );
572             sink.sectionTitle1_();
573 
574             sinkAnchor( sink, "Failure_Details" );
575 
576             constructHotLinks( sink, bundle );
577 
578             sinkLineBreak( sink );
579 
580             sink.table();
581 
582             sink.tableRows( new int[]{ LEFT, LEFT }, true );
583 
584             while ( failIter.hasNext() )
585             {
586                 ReportTestCase tCase = failIter.next();
587 
588                 Map<String, Object> failure = tCase.getFailure();
589 
590                 sink.tableRow();
591 
592                 sink.tableCell();
593 
594                 String type = (String) failure.get( "type" );
595                 sinkIcon( type, sink );
596 
597                 sink.tableCell_();
598 
599                 sinkCellAnchor( sink, tCase.getName(), toHtmlId( tCase.getFullName() ) );
600 
601                 sink.tableRow_();
602 
603                 String message = (String) failure.get( "message" );
604 
605                 sink.tableRow();
606 
607                 sinkCell( sink, "" );
608 
609                 StringBuilder sb = new StringBuilder();
610                 sb.append( type );
611 
612                 if ( message != null )
613                 {
614                     sb.append( ": " );
615                     sb.append( message );
616                 }
617 
618                 sinkCell( sink, sb.toString() );
619 
620                 sink.tableRow_();
621 
622                 List<String> detail = (List<String>) failure.get( "detail" );
623                 if ( detail != null )
624                 {
625                     boolean firstLine = true;
626 
627                     String techMessage = "";
628                     for ( String line : detail )
629                     {
630                         techMessage = line;
631                         if ( firstLine )
632                         {
633                             firstLine = false;
634                         }
635                         else
636                         {
637                             sink.text( "    " );
638                         }
639                     }
640 
641                     sink.tableRow();
642 
643                     sinkCell( sink, "" );
644 
645                     sink.tableCell();
646                     SinkEventAttributeSet atts = new SinkEventAttributeSet();
647                     atts.addAttribute( SinkEventAttributes.ID, tCase.getName() + "error" );
648                     sink.unknown( "div", new Object[]{ HtmlMarkup.TAG_TYPE_START }, atts );
649 
650                     if ( xrefLocation != null )
651                     {
652                         String path = tCase.getFullClassName().replace( '.', '/' );
653 
654                         sink.link( xrefLocation + "/" + path + ".html#"
655                                         + getErrorLineNumber( tCase.getFullName(), techMessage ) );
656                     }
657                     sink.text(
658                         tCase.getFullClassName() + ":" + getErrorLineNumber( tCase.getFullName(), techMessage ) );
659 
660                     if ( xrefLocation != null )
661                     {
662                         sink.link_();
663                     }
664                     sink.unknown( "div", new Object[]{ HtmlMarkup.TAG_TYPE_END }, null );
665 
666                     sink.tableCell_();
667 
668                     sink.tableRow_();
669                 }
670             }
671 
672             sink.tableRows_();
673 
674             sink.table_();
675         }
676 
677         sinkLineBreak( sink );
678 
679         sink.section1_();
680     }
681 
682     private String getErrorLineNumber( String className, String source )
683     {
684         StringTokenizer tokenizer = new StringTokenizer( source );
685 
686         while ( tokenizer.hasMoreTokens() )
687         {
688             String token = tokenizer.nextToken();
689             if ( token.startsWith( className ) )
690             {
691                 int idx = token.indexOf( ":" );
692                 if ( idx >= 0 )
693                 {
694                     int closeIdx = token.lastIndexOf( ")" );
695 
696                     if ( closeIdx > idx + 1 )
697                     {
698                         return token.substring( idx + 1, closeIdx );
699                     }
700                 }
701             }
702         }
703         return "";
704     }
705 
706     private void constructHotLinks( Sink sink, ResourceBundle bundle )
707     {
708         if ( !testSuites.isEmpty() )
709         {
710             sink.paragraph();
711 
712             sink.text( "[" );
713             sinkLink( sink, bundle.getString( "report.surefire.label.summary" ), "#Summary" );
714             sink.text( "]" );
715 
716             sink.text( " [" );
717             sinkLink( sink, bundle.getString( "report.surefire.label.packagelist" ), "#Package_List" );
718             sink.text( "]" );
719 
720             sink.text( " [" );
721             sinkLink( sink, bundle.getString( "report.surefire.label.testcases" ), "#Test_Cases" );
722             sink.text( "]" );
723 
724             sink.paragraph_();
725         }
726     }
727 
728     private void sinkLineBreak( Sink sink )
729     {
730         sink.lineBreak();
731     }
732 
733     private void sinkIcon( String type, Sink sink )
734     {
735         sink.figure();
736 
737         if ( type.startsWith( "junit.framework" ) || "skipped".equals( type ) )
738         {
739             sink.figureGraphics( "images/icon_warning_sml.gif" );
740         }
741         else if ( type.startsWith( "success" ) )
742         {
743             sink.figureGraphics( "images/icon_success_sml.gif" );
744         }
745         else
746         {
747             sink.figureGraphics( "images/icon_error_sml.gif" );
748         }
749 
750         sink.figure_();
751     }
752 
753     private void sinkHeader( Sink sink, String header )
754     {
755         sink.tableHeaderCell();
756         sink.text( header );
757         sink.tableHeaderCell_();
758     }
759 
760     private void sinkCell( Sink sink, String text )
761     {
762         sink.tableCell();
763         sink.text( text );
764         sink.tableCell_();
765     }
766 
767     private void sinkLink( Sink sink, String text, String link )
768     {
769         sink.link( link );
770         sink.text( text );
771         sink.link_();
772     }
773 
774     private void sinkCellLink( Sink sink, String text, String link )
775     {
776         sink.tableCell();
777         sinkLink( sink, text, link );
778         sink.tableCell_();
779     }
780 
781     private void sinkCellAnchor( Sink sink, String text, String anchor )
782     {
783         sink.tableCell();
784         sinkAnchor( sink, anchor );
785         sink.text( text );
786         sink.tableCell_();
787     }
788 
789     private void sinkAnchor( Sink sink, String anchor )
790     {
791         sink.anchor( anchor );
792         sink.anchor_();
793     }
794 
795     private static String javascriptToggleDisplayCode()
796     {
797         final StringBuilder str = new StringBuilder( 64 );
798 
799         // the javascript code is emitted within a commented CDATA section
800         // so we have to start with a newline and comment the CDATA closing in the end
801         str.append( "\n" );
802         str.append( "function toggleDisplay(elementId) {\n" );
803         str.append( " var elm = document.getElementById(elementId + 'error');\n" );
804         str.append( " if (elm && typeof elm.style != \"undefined\") {\n" );
805         str.append( " if (elm.style.display == \"none\") {\n" );
806         str.append( " elm.style.display = \"\";\n" );
807         str.append( " document.getElementById(elementId + 'off').style.display = \"none\";\n" );
808         str.append( " document.getElementById(elementId + 'on').style.display = \"inline\";\n" );
809         str.append( " }" );
810         str.append( " else if (elm.style.display == \"\") {" );
811         str.append( " elm.style.display = \"none\";\n" );
812         str.append( " document.getElementById(elementId + 'off').style.display = \"inline\";\n" );
813         str.append( " document.getElementById(elementId + 'on').style.display = \"none\";\n" );
814         str.append( " } \n" );
815         str.append( " } \n" );
816         str.append( " }\n" );
817         str.append( "//" );
818 
819         return str.toString();
820     }
821 }