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             eos.getUnderlying().write( ByteConstantsHolder.CDATA_END_BYTES );
515             eos.flush();
516             xmlWriter.endElement();
517         }
518     }
519 
520     /**
521      * Adds system properties to the XML report.
522      * <br>
523      *
524      * @param xmlWriter The test suite to report to
525      */
526     private static void showProperties( XMLWriter xmlWriter, Map<String, String> systemProperties )
527         throws IOException
528     {
529         xmlWriter.startElement( "properties" );
530         for ( final Entry<String, String> entry : systemProperties.entrySet() )
531         {
532             final String key = entry.getKey();
533             String value = entry.getValue();
534 
535             if ( value == null )
536             {
537                 value = "null";
538             }
539 
540             xmlWriter.startElement( "property" );
541 
542             xmlWriter.addAttribute( "name", key );
543 
544             xmlWriter.addAttribute( "value", extraEscapeAttribute( value ) );
545 
546             xmlWriter.endElement();
547         }
548         xmlWriter.endElement();
549     }
550 
551     /**
552      * Handle stuff that may pop up in java that is not legal in xml.
553      *
554      * @param message   The string
555      * @return The escaped string or returns itself if all characters are legal
556      */
557     private static String extraEscapeAttribute( String message )
558     {
559         // Someday convert to xml 1.1 which handles everything but 0 inside string
560         return containsEscapesIllegalXml10( message ) ? escapeXml( message, true ) : message;
561     }
562 
563     /**
564      * Writes escaped string or the message within CDATA if all characters are legal.
565      *
566      * @param message   The string
567      */
568     private static void extraEscapeElementValue( String message, OutputStreamWriter outputStreamWriter,
569                                                  XMLWriter xmlWriter, OutputStream fw )
570         throws IOException
571     {
572         // Someday convert to xml 1.1 which handles everything but 0 inside string
573         if ( containsEscapesIllegalXml10( message ) )
574         {
575             xmlWriter.writeText( escapeXml( message, false ) );
576         }
577         else
578         {
579             EncodingOutputStream eos = new EncodingOutputStream( fw );
580             xmlWriter.writeText( "" ); // Cheat sax to emit element
581             outputStreamWriter.flush();
582             eos.getUnderlying().write( ByteConstantsHolder.CDATA_START_BYTES );
583             eos.write( message.getBytes( UTF_8 ) );
584             eos.getUnderlying().write( ByteConstantsHolder.CDATA_END_BYTES );
585             eos.flush();
586         }
587     }
588 
589     // todo: SUREFIRE-1986
590     private static void addCommentElementTestCase( String comment, OutputStreamWriter outputStreamWriter,
591                                                    XMLWriter xmlWriter, OutputStream fw )
592         throws IOException
593     {
594         xmlWriter.writeText( "" ); // Cheat sax to emit element
595         outputStreamWriter.flush();
596         fw.write( XML_NL.getBytes( UTF_8 ) );
597         fw.write( XML_INDENT.getBytes( UTF_8 ) );
598         fw.write( XML_INDENT.getBytes( UTF_8 ) );
599         fw.write( ByteConstantsHolder.COMMENT_START );
600         fw.write( comment.getBytes( UTF_8 ) );
601         fw.write( ByteConstantsHolder.COMMENT_END );
602         fw.write( XML_NL.getBytes( UTF_8 ) );
603         fw.write( XML_INDENT.getBytes( UTF_8 ) );
604         fw.flush();
605     }
606 
607     private static final class EncodingOutputStream
608         extends FilterOutputStream
609     {
610         private int c1;
611 
612         private int c2;
613 
614         EncodingOutputStream( OutputStream out )
615         {
616             super( out );
617         }
618 
619         OutputStream getUnderlying()
620         {
621             return out;
622         }
623 
624         private boolean isCdataEndBlock( int c )
625         {
626             return c1 == ']' && c2 == ']' && c == '>';
627         }
628 
629         @Override
630         public void write( int b )
631             throws IOException
632         {
633             if ( isCdataEndBlock( b ) )
634             {
635                 out.write( ByteConstantsHolder.CDATA_ESCAPE_STRING_BYTES );
636             }
637             else if ( isIllegalEscape( b ) )
638             {
639                 // uh-oh!  This character is illegal in XML 1.0!
640                 // http://www.w3.org/TR/1998/REC-xml-19980210#charsets
641                 // we're going to deliberately doubly-XML escape it...
642                 // there's nothing better we can do! :-(
643                 // SUREFIRE-456
644                 out.write( ByteConstantsHolder.AMP_BYTES );
645                 out.write( String.valueOf( b ).getBytes( UTF_8 ) );
646                 out.write( ';' ); // & Will be encoded to amp inside xml encodingSHO
647             }
648             else
649             {
650                 out.write( b );
651             }
652             c1 = c2;
653             c2 = b;
654         }
655     }
656 
657     private static boolean containsEscapesIllegalXml10( String message )
658     {
659         int size = message.length();
660         for ( int i = 0; i < size; i++ )
661         {
662             if ( isIllegalEscape( message.charAt( i ) ) )
663             {
664                 return true;
665             }
666 
667         }
668         return false;
669     }
670 
671     private static boolean isIllegalEscape( char c )
672     {
673         return isIllegalEscape( (int) c );
674     }
675 
676     private static boolean isIllegalEscape( int c )
677     {
678         return c >= 0 && c < 32 && c != '\n' && c != '\r' && c != '\t';
679     }
680 
681     /**
682      * escape for XML 1.0
683      *
684      * @param text      The string
685      * @param attribute true if the escaped value is inside an attribute
686      * @return The escaped string
687      */
688     private static String escapeXml( String text, boolean attribute )
689     {
690         StringBuilder sb = new StringBuilder( text.length() * 2 );
691         for ( int i = 0; i < text.length(); i++ )
692         {
693             char c = text.charAt( i );
694             if ( isIllegalEscape( c ) )
695             {
696                 // uh-oh!  This character is illegal in XML 1.0!
697                 // http://www.w3.org/TR/1998/REC-xml-19980210#charsets
698                 // we're going to deliberately doubly-XML escape it...
699                 // there's nothing better we can do! :-(
700                 // SUREFIRE-456
701                 sb.append( attribute ? "&#" : "&amp#" ).append( (int) c ).append(
702                     ';' ); // & Will be encoded to amp inside xml encodingSHO
703             }
704             else
705             {
706                 sb.append( c );
707             }
708         }
709         return sb.toString();
710     }
711 
712     private static final class ByteConstantsHolder
713     {
714         private static final byte[] CDATA_START_BYTES = "<![CDATA[".getBytes( UTF_8 );
715 
716         private static final byte[] CDATA_END_BYTES = "]]>".getBytes( UTF_8 );
717 
718         private static final byte[] CDATA_ESCAPE_STRING_BYTES = "]]><![CDATA[>".getBytes( UTF_8 );
719 
720         private static final byte[] AMP_BYTES = "&amp#".getBytes( UTF_8 );
721 
722         private static final byte[] COMMENT_START = "<!-- ".getBytes( UTF_8 );
723 
724         private static final byte[] COMMENT_END = " --> ".getBytes( UTF_8 );
725     }
726 }