View Javadoc
1   package org.apache.maven.plugin.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 org.apache.maven.plugin.surefire.booterclient.output.InPluginProcessDumpSingleton;
23  import org.apache.maven.surefire.shared.utils.xml.PrettyPrintXMLWriter;
24  import org.apache.maven.surefire.shared.utils.xml.XMLWriter;
25  import org.apache.maven.surefire.extensions.StatelessReportEventListener;
26  import org.apache.maven.surefire.api.report.SafeThrowable;
27  
28  import java.io.BufferedOutputStream;
29  import java.io.File;
30  import java.io.FileOutputStream;
31  import java.io.FilterOutputStream;
32  import java.io.IOException;
33  import java.io.OutputStream;
34  import java.io.OutputStreamWriter;
35  import java.io.PrintWriter;
36  import java.util.ArrayList;
37  import java.util.Deque;
38  import java.util.LinkedHashMap;
39  import java.util.List;
40  import java.util.Map;
41  import java.util.Map.Entry;
42  import java.util.StringTokenizer;
43  import java.util.concurrent.ConcurrentLinkedDeque;
44  
45  import static java.nio.charset.StandardCharsets.UTF_8;
46  import static org.apache.maven.plugin.surefire.report.DefaultReporterFactory.TestResultType;
47  import static org.apache.maven.plugin.surefire.report.FileReporterUtils.stripIllegalFilenameChars;
48  import static org.apache.maven.plugin.surefire.report.ReportEntryType.SKIPPED;
49  import static org.apache.maven.plugin.surefire.report.ReportEntryType.SUCCESS;
50  import static org.apache.maven.surefire.shared.utils.StringUtils.isBlank;
51  
52  @SuppressWarnings( { "javadoc", "checkstyle:javadoctype" } )
53  // CHECKSTYLE_OFF: LineLength
54  /*
55   * XML format reporter writing to <code>TEST-<i>reportName</i>[-<i>suffix</i>].xml</code> file like written and read
56   * by Ant's <a href="http://ant.apache.org/manual/Tasks/junit.html"><code>&lt;junit&gt;</code></a> and
57   * <a href="http://ant.apache.org/manual/Tasks/junitreport.html"><code>&lt;junitreport&gt;</code></a> tasks,
58   * then supported by many tools like CI servers.
59   * <br>
60   * <pre>&lt;?xml version="1.0" encoding="UTF-8"?>
61   * &lt;testsuite name="<i>suite name</i>" [group="<i>group</i>"] tests="<i>0</i>" failures="<i>0</i>" errors="<i>0</i>" skipped="<i>0</i>" time="<i>0,###.###</i>">
62   *  &lt;properties>
63   *    &lt;property name="<i>name</i>" value="<i>value</i>"/>
64   *    [...]
65   *  &lt;/properties>
66   *  &lt;testcase time="<i>0,###.###</i>" name="<i>test name</i> [classname="<i>class name</i>"] [group="<i>group</i>"]"/>
67   *  &lt;testcase time="<i>0,###.###</i>" name="<i>test name</i> [classname="<i>class name</i>"] [group="<i>group</i>"]">
68   *    &lt;<b>error</b> message="<i>message</i>" type="<i>exception class name</i>"><i>stacktrace</i>&lt;/error>
69   *    &lt;system-out><i>system out content (present only if not empty)</i>&lt;/system-out>
70   *    &lt;system-err><i>system err content (present only if not empty)</i>&lt;/system-err>
71   *  &lt;/testcase>
72   *  &lt;testcase time="<i>0,###.###</i>" name="<i>test name</i> [classname="<i>class name</i>"] [group="<i>group</i>"]">
73   *    &lt;<b>failure</b> message="<i>message</i>" type="<i>exception class name</i>"><i>stacktrace</i>&lt;/failure>
74   *    &lt;system-out><i>system out content (present only if not empty)</i>&lt;/system-out>
75   *    &lt;system-err><i>system err content (present only if not empty)</i>&lt;/system-err>
76   *  &lt;/testcase>
77   *  &lt;testcase time="<i>0,###.###</i>" name="<i>test name</i> [classname="<i>class name</i>"] [group="<i>group</i>"]">
78   *    &lt;<b>skipped</b>/>
79   *  &lt;/testcase>
80   *  [...]</pre>
81   *
82   * @author Kristian Rosenvold
83   * @see <a href="http://wiki.apache.org/ant/Proposals/EnhancedTestReports">Ant's format enhancement proposal</a>
84   *      (not yet implemented by Ant 1.8.2)
85   */
86  //todo this is no more stateless due to existence of testClassMethodRunHistoryMap since of 2.19.
87  public class StatelessXmlReporter
88          implements StatelessReportEventListener<WrappedReportEntry, TestSetStats>
89  {
90      private static final String XML_INDENT = "  ";
91  
92      private static final String XML_NL = "\n";
93  
94      private final File reportsDirectory;
95  
96      private final String reportNameSuffix;
97  
98      private final boolean trimStackTrace;
99  
100     private final int rerunFailingTestsCount;
101 
102     private final String xsdSchemaLocation;
103 
104     private final String xsdVersion;
105 
106     // Map between test class name and a map between test method name
107     // and the list of runs for each test method
108     private final Map<String, Deque<WrappedReportEntry>> testClassMethodRunHistoryMap;
109 
110     private final boolean phrasedFileName;
111 
112     private final boolean phrasedSuiteName;
113 
114     private final boolean phrasedClassName;
115 
116     private final boolean phrasedMethodName;
117 
118     public StatelessXmlReporter( File reportsDirectory, String reportNameSuffix, boolean trimStackTrace,
119                                  int rerunFailingTestsCount,
120                                  Map<String, Deque<WrappedReportEntry>> testClassMethodRunHistoryMap,
121                                  String xsdSchemaLocation, String xsdVersion, boolean phrasedFileName,
122                                  boolean phrasedSuiteName, boolean phrasedClassName, boolean phrasedMethodName )
123     {
124         this.reportsDirectory = reportsDirectory;
125         this.reportNameSuffix = reportNameSuffix;
126         this.trimStackTrace = trimStackTrace;
127         this.rerunFailingTestsCount = rerunFailingTestsCount;
128         this.testClassMethodRunHistoryMap = testClassMethodRunHistoryMap;
129         this.xsdSchemaLocation = xsdSchemaLocation;
130         this.xsdVersion = xsdVersion;
131         this.phrasedFileName = phrasedFileName;
132         this.phrasedSuiteName = phrasedSuiteName;
133         this.phrasedClassName = phrasedClassName;
134         this.phrasedMethodName = phrasedMethodName;
135     }
136 
137     @Override
138     public void testSetCompleted( WrappedReportEntry testSetReportEntry, TestSetStats testSetStats )
139     {
140         Map<String, Map<String, List<WrappedReportEntry>>> classMethodStatistics =
141                 arrangeMethodStatistics( testSetReportEntry, testSetStats );
142 
143         // The Java Language Spec:
144         // "Note that the close methods of resources are called in the opposite order of their creation."
145         try ( OutputStream outputStream = getOutputStream( testSetReportEntry );
146               OutputStreamWriter fw = getWriter( outputStream ) )
147         {
148             XMLWriter ppw = new PrettyPrintXMLWriter( new PrintWriter( fw ), XML_INDENT, XML_NL, UTF_8.name(), null );
149 
150             createTestSuiteElement( ppw, testSetReportEntry, testSetStats ); // TestSuite
151 
152             showProperties( ppw, testSetReportEntry.getSystemProperties() );
153 
154             for ( Entry<String, Map<String, List<WrappedReportEntry>>> statistics : classMethodStatistics.entrySet() )
155             {
156                 for ( Entry<String, List<WrappedReportEntry>> thisMethodRuns : statistics.getValue().entrySet() )
157                 {
158                     serializeTestClass( outputStream, fw, ppw, thisMethodRuns.getValue() );
159                 }
160             }
161 
162             ppw.endElement(); // TestSuite
163         }
164         catch ( IOException e )
165         {
166             // It's not a test error.
167             // This method must be sail-safe and errors are in a dump log.
168             // The control flow must not be broken in TestSetRunListener#testSetCompleted.
169             InPluginProcessDumpSingleton.getSingleton()
170                     .dumpException( e, e.getLocalizedMessage(), reportsDirectory );
171         }
172     }
173 
174     private Map<String, Map<String, List<WrappedReportEntry>>> arrangeMethodStatistics(
175             WrappedReportEntry testSetReportEntry, TestSetStats testSetStats )
176     {
177         Map<String, Map<String, List<WrappedReportEntry>>> classMethodStatistics = new LinkedHashMap<>();
178         for ( WrappedReportEntry methodEntry : aggregateCacheFromMultipleReruns( testSetReportEntry, testSetStats ) )
179         {
180             String testClassName = methodEntry.getSourceName();
181             Map<String, List<WrappedReportEntry>> stats = classMethodStatistics.get( testClassName );
182             if ( stats == null )
183             {
184                 stats = new LinkedHashMap<>();
185                 classMethodStatistics.put( testClassName, stats );
186             }
187             String methodName = methodEntry.getName();
188             List<WrappedReportEntry> methodRuns = stats.get( methodName );
189             if ( methodRuns == null )
190             {
191                 methodRuns = new ArrayList<>();
192                 stats.put( methodName, methodRuns );
193             }
194             methodRuns.add( methodEntry );
195         }
196         return classMethodStatistics;
197     }
198 
199     private Deque<WrappedReportEntry> aggregateCacheFromMultipleReruns( WrappedReportEntry testSetReportEntry,
200                                                                        TestSetStats testSetStats )
201     {
202         String suiteClassName = testSetReportEntry.getSourceName();
203         Deque<WrappedReportEntry> methodRunHistory = getAddMethodRunHistoryMap( suiteClassName );
204         methodRunHistory.addAll( testSetStats.getReportEntries() );
205         return methodRunHistory;
206     }
207 
208     private void serializeTestClass( OutputStream outputStream, OutputStreamWriter fw, XMLWriter ppw,
209                                      List<WrappedReportEntry> methodEntries )
210         throws IOException
211     {
212         if ( rerunFailingTestsCount > 0 )
213         {
214             serializeTestClassWithRerun( outputStream, fw, ppw, methodEntries );
215         }
216         else
217         {
218             // rerunFailingTestsCount is smaller than 1, but for some reasons a test could be run
219             // for more than once
220             serializeTestClassWithoutRerun( outputStream, fw, ppw, methodEntries );
221         }
222     }
223 
224     private void serializeTestClassWithoutRerun( OutputStream outputStream, OutputStreamWriter fw, XMLWriter ppw,
225                                                  List<WrappedReportEntry> methodEntries )
226         throws IOException
227     {
228         for ( WrappedReportEntry methodEntry : methodEntries )
229         {
230             startTestElement( ppw, methodEntry );
231             if ( methodEntry.getReportEntryType() != SUCCESS )
232             {
233                 getTestProblems( fw, ppw, methodEntry, trimStackTrace, outputStream,
234                         methodEntry.getReportEntryType().getXmlTag(), false );
235             }
236             createOutErrElements( fw, ppw, methodEntry, outputStream );
237             ppw.endElement();
238         }
239     }
240 
241     private void serializeTestClassWithRerun( OutputStream outputStream, OutputStreamWriter fw, XMLWriter ppw,
242                                               List<WrappedReportEntry> methodEntries )
243         throws IOException
244     {
245         WrappedReportEntry firstMethodEntry = methodEntries.get( 0 );
246         switch ( getTestResultType( methodEntries ) )
247         {
248             case success:
249                 for ( WrappedReportEntry methodEntry : methodEntries )
250                 {
251                     if ( methodEntry.getReportEntryType() == SUCCESS )
252                     {
253                         startTestElement( ppw, methodEntry );
254                         ppw.endElement();
255                     }
256                 }
257                 break;
258             case error:
259             case failure:
260                 // When rerunFailingTestsCount is set to larger than 0
261                 startTestElement( ppw, firstMethodEntry );
262                 boolean firstRun = true;
263                 for ( WrappedReportEntry singleRunEntry : methodEntries )
264                 {
265                     if ( firstRun )
266                     {
267                         firstRun = false;
268                         getTestProblems( fw, ppw, singleRunEntry, trimStackTrace, outputStream,
269                                 singleRunEntry.getReportEntryType().getXmlTag(), false );
270                         createOutErrElements( fw, ppw, singleRunEntry, outputStream );
271                     }
272                     else if ( singleRunEntry.getReportEntryType() == SKIPPED )
273                     {
274                         // The version 3.1.0 should produce a new XSD schema with version 3.1.0, see SUREFIRE-1986,
275                         // and the XSD schema should add a new element "rerunSkipped"
276                         // then ReportEntryType should update the enum to SKIPPED( "skipped", "", "rerunSkipped" ).
277                         // The teams should be notified - Jenkins reports.
278                         addCommentElementTestCase( "a skipped test execution in re-run phase", fw, ppw, outputStream );
279                     }
280                     else
281                     {
282                         getTestProblems( fw, ppw, singleRunEntry, trimStackTrace, outputStream,
283                                 singleRunEntry.getReportEntryType().getRerunXmlTag(), true );
284                     }
285                 }
286                 ppw.endElement();
287                 break;
288             case flake:
289                 WrappedReportEntry successful = null;
290                 // Get the run time of the first successful run
291                 for ( WrappedReportEntry singleRunEntry : methodEntries )
292                 {
293                     if ( singleRunEntry.getReportEntryType() == SUCCESS )
294                     {
295                         successful = singleRunEntry;
296                         break;
297                     }
298                 }
299                 WrappedReportEntry firstOrSuccessful = successful == null ? methodEntries.get( 0 ) : successful;
300                 startTestElement( ppw, firstOrSuccessful );
301                 for ( WrappedReportEntry singleRunEntry : methodEntries )
302                 {
303                     if ( singleRunEntry.getReportEntryType() != SUCCESS )
304                     {
305                         getTestProblems( fw, ppw, singleRunEntry, trimStackTrace, outputStream,
306                                 singleRunEntry.getReportEntryType().getFlakyXmlTag(), true );
307                     }
308                 }
309                 ppw.endElement();
310                 break;
311             case skipped:
312                 startTestElement( ppw, firstMethodEntry );
313                 getTestProblems( fw, ppw, firstMethodEntry, trimStackTrace, outputStream,
314                         firstMethodEntry.getReportEntryType().getXmlTag(), false );
315                 ppw.endElement();
316                 break;
317             default:
318                 throw new IllegalStateException( "Get unknown test result type" );
319         }
320     }
321 
322     /**
323      * Clean testClassMethodRunHistoryMap
324      */
325     public void cleanTestHistoryMap()
326     {
327         testClassMethodRunHistoryMap.clear();
328     }
329 
330     /**
331      * Get the result of a test from a list of its runs in WrappedReportEntry
332      *
333      * @param methodEntryList the list of runs for a given test
334      * @return the TestResultType for the given test
335      */
336     private TestResultType getTestResultType( List<WrappedReportEntry> methodEntryList )
337     {
338         List<ReportEntryType> testResultTypeList = new ArrayList<>();
339         for ( WrappedReportEntry singleRunEntry : methodEntryList )
340         {
341             testResultTypeList.add( singleRunEntry.getReportEntryType() );
342         }
343 
344         return DefaultReporterFactory.getTestResultType( testResultTypeList, rerunFailingTestsCount );
345     }
346 
347     private Deque<WrappedReportEntry> getAddMethodRunHistoryMap( String testClassName )
348     {
349         Deque<WrappedReportEntry> methodRunHistory = testClassMethodRunHistoryMap.get( testClassName );
350         if ( methodRunHistory == null )
351         {
352             methodRunHistory = new ConcurrentLinkedDeque<>();
353             testClassMethodRunHistoryMap.put( testClassName == null ? "null" : testClassName, methodRunHistory );
354         }
355         return methodRunHistory;
356     }
357 
358     private OutputStream getOutputStream( WrappedReportEntry testSetReportEntry )
359         throws IOException
360     {
361         File reportFile = getReportFile( testSetReportEntry );
362         File reportDir = reportFile.getParentFile();
363         //noinspection ResultOfMethodCallIgnored
364         reportFile.delete();
365         //noinspection ResultOfMethodCallIgnored
366         reportDir.mkdirs();
367         return new BufferedOutputStream( new FileOutputStream( reportFile ), 64 * 1024 );
368     }
369 
370     private static OutputStreamWriter getWriter( OutputStream fos )
371     {
372         return new OutputStreamWriter( fos, UTF_8 );
373     }
374 
375     private File getReportFile( WrappedReportEntry report )
376     {
377         String reportName = "TEST-" + ( phrasedFileName ? report.getReportSourceName() : report.getSourceName() );
378         String customizedReportName = isBlank( reportNameSuffix ) ? reportName : reportName + "-" + reportNameSuffix;
379         return new File( reportsDirectory, stripIllegalFilenameChars( customizedReportName + ".xml" ) );
380     }
381 
382     private void startTestElement( XMLWriter ppw, WrappedReportEntry report )
383         throws IOException
384     {
385         ppw.startElement( "testcase" );
386         String name = phrasedMethodName ? report.getReportName() : report.getName();
387         ppw.addAttribute( "name", name == null ? "" : extraEscapeAttribute( name ) );
388 
389         if ( report.getGroup() != null )
390         {
391             ppw.addAttribute( "group", report.getGroup() );
392         }
393 
394         String className = phrasedClassName ? report.getReportSourceName( reportNameSuffix )
395                 : report.getSourceName( reportNameSuffix );
396         if ( className != null )
397         {
398             ppw.addAttribute( "classname", extraEscapeAttribute( className ) );
399         }
400 
401         ppw.addAttribute( "time", report.elapsedTimeAsString() );
402     }
403 
404     private void createTestSuiteElement( XMLWriter ppw, WrappedReportEntry report, TestSetStats testSetStats )
405         throws IOException
406     {
407         ppw.startElement( "testsuite" );
408 
409         ppw.addAttribute( "xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance" );
410         ppw.addAttribute( "xsi:noNamespaceSchemaLocation", xsdSchemaLocation );
411         ppw.addAttribute( "version", xsdVersion );
412 
413         String reportName = phrasedSuiteName ? report.getReportSourceName( reportNameSuffix )
414                 : report.getSourceName( reportNameSuffix );
415         ppw.addAttribute( "name", reportName == null ? "" : extraEscapeAttribute( reportName ) );
416 
417         if ( report.getGroup() != null )
418         {
419             ppw.addAttribute( "group", report.getGroup() );
420         }
421 
422         ppw.addAttribute( "time", report.elapsedTimeAsString() );
423 
424         ppw.addAttribute( "tests", String.valueOf( testSetStats.getCompletedCount() ) );
425 
426         ppw.addAttribute( "errors", String.valueOf( testSetStats.getErrors() ) );
427 
428         ppw.addAttribute( "skipped", String.valueOf( testSetStats.getSkipped() ) );
429 
430         ppw.addAttribute( "failures", String.valueOf( testSetStats.getFailures() ) );
431     }
432 
433     private static void getTestProblems( OutputStreamWriter outputStreamWriter, XMLWriter ppw,
434                                          WrappedReportEntry report, boolean trimStackTrace, OutputStream fw,
435                                          String testErrorType, boolean createOutErrElementsInside )
436         throws IOException
437     {
438         ppw.startElement( testErrorType );
439 
440         String stackTrace = report.getStackTrace( trimStackTrace );
441 
442         if ( report.getMessage() != null && !report.getMessage().isEmpty() )
443         {
444             ppw.addAttribute( "message", extraEscapeAttribute( report.getMessage() ) );
445         }
446 
447         if ( report.getStackTraceWriter() != null )
448         {
449             //noinspection ThrowableResultOfMethodCallIgnored
450             SafeThrowable t = report.getStackTraceWriter().getThrowable();
451             if ( t != null )
452             {
453                 if ( t.getMessage() != null )
454                 {
455                     int delimiter = stackTrace.indexOf( ":" );
456                     String type = delimiter == -1 ? stackTrace : stackTrace.substring( 0, delimiter );
457                     ppw.addAttribute( "type", type );
458                 }
459                 else
460                 {
461                     ppw.addAttribute( "type", new StringTokenizer( stackTrace ).nextToken() );
462                 }
463             }
464         }
465 
466         boolean hasNestedElements = createOutErrElementsInside & stackTrace != null;
467 
468         if ( stackTrace != null )
469         {
470             if ( hasNestedElements )
471             {
472                 ppw.startElement( "stackTrace" );
473             }
474 
475             extraEscapeElementValue( stackTrace, outputStreamWriter, ppw, fw );
476 
477             if ( hasNestedElements )
478             {
479                 ppw.endElement();
480             }
481         }
482 
483         if ( createOutErrElementsInside )
484         {
485             createOutErrElements( outputStreamWriter, ppw, report, fw );
486         }
487 
488         ppw.endElement(); // entry type
489     }
490 
491     // Create system-out and system-err elements
492     private static void createOutErrElements( OutputStreamWriter outputStreamWriter, XMLWriter ppw,
493                                               WrappedReportEntry report, OutputStream fw )
494         throws IOException
495     {
496         EncodingOutputStream eos = new EncodingOutputStream( fw );
497         addOutputStreamElement( outputStreamWriter, eos, ppw, report.getStdout(), "system-out" );
498         addOutputStreamElement( outputStreamWriter, eos, ppw, report.getStdErr(), "system-err" );
499     }
500 
501     private static void addOutputStreamElement( OutputStreamWriter outputStreamWriter,
502                                          EncodingOutputStream eos, XMLWriter xmlWriter,
503                                          Utf8RecodingDeferredFileOutputStream utf8RecodingDeferredFileOutputStream,
504                                          String name )
505         throws IOException
506     {
507         if ( utf8RecodingDeferredFileOutputStream != null && utf8RecodingDeferredFileOutputStream.getByteCount() > 0 )
508         {
509             xmlWriter.startElement( name );
510             xmlWriter.writeText( "" ); // Cheat sax to emit element
511             outputStreamWriter.flush();
512             eos.getUnderlying().write( ByteConstantsHolder.CDATA_START_BYTES ); // emit cdata
513             utf8RecodingDeferredFileOutputStream.writeTo( eos );
514             utf8RecodingDeferredFileOutputStream.free();
515             eos.getUnderlying().write( ByteConstantsHolder.CDATA_END_BYTES );
516             eos.flush();
517             xmlWriter.endElement();
518         }
519     }
520 
521     /**
522      * Adds system properties to the XML report.
523      * <br>
524      *
525      * @param xmlWriter The test suite to report to
526      */
527     private static void showProperties( XMLWriter xmlWriter, Map<String, String> systemProperties )
528         throws IOException
529     {
530         xmlWriter.startElement( "properties" );
531         for ( final Entry<String, String> entry : systemProperties.entrySet() )
532         {
533             final String key = entry.getKey();
534             String value = entry.getValue();
535 
536             if ( value == null )
537             {
538                 value = "null";
539             }
540 
541             xmlWriter.startElement( "property" );
542 
543             xmlWriter.addAttribute( "name", key );
544 
545             xmlWriter.addAttribute( "value", extraEscapeAttribute( value ) );
546 
547             xmlWriter.endElement();
548         }
549         xmlWriter.endElement();
550     }
551 
552     /**
553      * Handle stuff that may pop up in java that is not legal in xml.
554      *
555      * @param message   The string
556      * @return The escaped string or returns itself if all characters are legal
557      */
558     private static String extraEscapeAttribute( String message )
559     {
560         // Someday convert to xml 1.1 which handles everything but 0 inside string
561         return containsEscapesIllegalXml10( message ) ? escapeXml( message, true ) : message;
562     }
563 
564     /**
565      * Writes escaped string or the message within CDATA if all characters are legal.
566      *
567      * @param message   The string
568      */
569     private static void extraEscapeElementValue( String message, OutputStreamWriter outputStreamWriter,
570                                                  XMLWriter xmlWriter, OutputStream fw )
571         throws IOException
572     {
573         // Someday convert to xml 1.1 which handles everything but 0 inside string
574         if ( containsEscapesIllegalXml10( message ) )
575         {
576             xmlWriter.writeText( escapeXml( message, false ) );
577         }
578         else
579         {
580             EncodingOutputStream eos = new EncodingOutputStream( fw );
581             xmlWriter.writeText( "" ); // Cheat sax to emit element
582             outputStreamWriter.flush();
583             eos.getUnderlying().write( ByteConstantsHolder.CDATA_START_BYTES );
584             eos.write( message.getBytes( UTF_8 ) );
585             eos.getUnderlying().write( ByteConstantsHolder.CDATA_END_BYTES );
586             eos.flush();
587         }
588     }
589 
590     // todo: SUREFIRE-1986
591     private static void addCommentElementTestCase( String comment, OutputStreamWriter outputStreamWriter,
592                                                    XMLWriter xmlWriter, OutputStream fw )
593         throws IOException
594     {
595         xmlWriter.writeText( "" ); // Cheat sax to emit element
596         outputStreamWriter.flush();
597         fw.write( XML_NL.getBytes( UTF_8 ) );
598         fw.write( XML_INDENT.getBytes( UTF_8 ) );
599         fw.write( XML_INDENT.getBytes( UTF_8 ) );
600         fw.write( ByteConstantsHolder.COMMENT_START );
601         fw.write( comment.getBytes( UTF_8 ) );
602         fw.write( ByteConstantsHolder.COMMENT_END );
603         fw.write( XML_NL.getBytes( UTF_8 ) );
604         fw.write( XML_INDENT.getBytes( UTF_8 ) );
605         fw.flush();
606     }
607 
608     private static final class EncodingOutputStream
609         extends FilterOutputStream
610     {
611         private int c1;
612 
613         private int c2;
614 
615         EncodingOutputStream( OutputStream out )
616         {
617             super( out );
618         }
619 
620         OutputStream getUnderlying()
621         {
622             return out;
623         }
624 
625         private boolean isCdataEndBlock( int c )
626         {
627             return c1 == ']' && c2 == ']' && c == '>';
628         }
629 
630         @Override
631         public void write( int b )
632             throws IOException
633         {
634             if ( isCdataEndBlock( b ) )
635             {
636                 out.write( ByteConstantsHolder.CDATA_ESCAPE_STRING_BYTES );
637             }
638             else if ( isIllegalEscape( b ) )
639             {
640                 // uh-oh!  This character is illegal in XML 1.0!
641                 // http://www.w3.org/TR/1998/REC-xml-19980210#charsets
642                 // we're going to deliberately doubly-XML escape it...
643                 // there's nothing better we can do! :-(
644                 // SUREFIRE-456
645                 out.write( ByteConstantsHolder.AMP_BYTES );
646                 out.write( String.valueOf( b ).getBytes( UTF_8 ) );
647                 out.write( ';' ); // & Will be encoded to amp inside xml encodingSHO
648             }
649             else
650             {
651                 out.write( b );
652             }
653             c1 = c2;
654             c2 = b;
655         }
656     }
657 
658     private static boolean containsEscapesIllegalXml10( String message )
659     {
660         int size = message.length();
661         for ( int i = 0; i < size; i++ )
662         {
663             if ( isIllegalEscape( message.charAt( i ) ) )
664             {
665                 return true;
666             }
667 
668         }
669         return false;
670     }
671 
672     private static boolean isIllegalEscape( char c )
673     {
674         return isIllegalEscape( (int) c );
675     }
676 
677     private static boolean isIllegalEscape( int c )
678     {
679         return c >= 0 && c < 32 && c != '\n' && c != '\r' && c != '\t';
680     }
681 
682     /**
683      * escape for XML 1.0
684      *
685      * @param text      The string
686      * @param attribute true if the escaped value is inside an attribute
687      * @return The escaped string
688      */
689     private static String escapeXml( String text, boolean attribute )
690     {
691         StringBuilder sb = new StringBuilder( text.length() * 2 );
692         for ( int i = 0; i < text.length(); i++ )
693         {
694             char c = text.charAt( i );
695             if ( isIllegalEscape( c ) )
696             {
697                 // uh-oh!  This character is illegal in XML 1.0!
698                 // http://www.w3.org/TR/1998/REC-xml-19980210#charsets
699                 // we're going to deliberately doubly-XML escape it...
700                 // there's nothing better we can do! :-(
701                 // SUREFIRE-456
702                 sb.append( attribute ? "&#" : "&amp#" ).append( (int) c ).append(
703                     ';' ); // & Will be encoded to amp inside xml encodingSHO
704             }
705             else
706             {
707                 sb.append( c );
708             }
709         }
710         return sb.toString();
711     }
712 
713     private static final class ByteConstantsHolder
714     {
715         private static final byte[] CDATA_START_BYTES = "<![CDATA[".getBytes( UTF_8 );
716 
717         private static final byte[] CDATA_END_BYTES = "]]>".getBytes( UTF_8 );
718 
719         private static final byte[] CDATA_ESCAPE_STRING_BYTES = "]]><![CDATA[>".getBytes( UTF_8 );
720 
721         private static final byte[] AMP_BYTES = "&amp#".getBytes( UTF_8 );
722 
723         private static final byte[] COMMENT_START = "<!-- ".getBytes( UTF_8 );
724 
725         private static final byte[] COMMENT_END = " --> ".getBytes( UTF_8 );
726     }
727 }