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