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