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