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 
359         ppw.addAttribute( "name", report.getReportName( reportNameSuffix ) );
360 
361         if ( report.getGroup() != null )
362         {
363             ppw.addAttribute( "group", report.getGroup() );
364         }
365 
366         ppw.addAttribute( "time", timeAsString );
367 
368         ppw.addAttribute( "tests", String.valueOf( testSetStats.getCompletedCount() ) );
369 
370         ppw.addAttribute( "errors", String.valueOf( testSetStats.getErrors() ) );
371 
372         ppw.addAttribute( "skipped", String.valueOf( testSetStats.getSkipped() ) );
373 
374         ppw.addAttribute( "failures", String.valueOf( testSetStats.getFailures() ) );
375     }
376 
377     private static void getTestProblems( OutputStreamWriter outputStreamWriter, XMLWriter ppw,
378                                          WrappedReportEntry report, boolean trimStackTrace, OutputStream fw,
379                                          String testErrorType, boolean createOutErrElementsInside )
380     {
381         ppw.startElement( testErrorType );
382 
383         String stackTrace = report.getStackTrace( trimStackTrace );
384 
385         if ( report.getMessage() != null && !report.getMessage().isEmpty() )
386         {
387             ppw.addAttribute( "message", extraEscape( report.getMessage(), true ) );
388         }
389 
390         if ( report.getStackTraceWriter() != null )
391         {
392             //noinspection ThrowableResultOfMethodCallIgnored
393             SafeThrowable t = report.getStackTraceWriter().getThrowable();
394             if ( t != null )
395             {
396                 if ( t.getMessage() != null )
397                 {
398                     int delimiter = stackTrace.indexOf( ":" );
399                     String type = delimiter == -1 ? stackTrace : stackTrace.substring( 0, delimiter );
400                     ppw.addAttribute( "type", type );
401                 }
402                 else
403                 {
404                     ppw.addAttribute( "type", new StringTokenizer( stackTrace ).nextToken() );
405                 }
406             }
407         }
408 
409         boolean hasNestedElements = createOutErrElementsInside & stackTrace != null;
410 
411         if ( stackTrace != null )
412         {
413             if ( hasNestedElements )
414             {
415                 ppw.startElement( "stackTrace" );
416             }
417 
418             ppw.writeText( extraEscape( stackTrace, false ) );
419 
420             if ( hasNestedElements )
421             {
422                 ppw.endElement();
423             }
424         }
425 
426         if ( createOutErrElementsInside )
427         {
428             createOutErrElements( outputStreamWriter, ppw, report, fw );
429         }
430 
431         ppw.endElement(); // entry type
432     }
433 
434     // Create system-out and system-err elements
435     private static void createOutErrElements( OutputStreamWriter outputStreamWriter, XMLWriter ppw,
436                                               WrappedReportEntry report, OutputStream fw )
437     {
438         EncodingOutputStream eos = new EncodingOutputStream( fw );
439         addOutputStreamElement( outputStreamWriter, eos, ppw, report.getStdout(), "system-out" );
440         addOutputStreamElement( outputStreamWriter, eos, ppw, report.getStdErr(), "system-err" );
441     }
442 
443     private static void addOutputStreamElement( OutputStreamWriter outputStreamWriter,
444                                          EncodingOutputStream eos, XMLWriter xmlWriter,
445                                          Utf8RecodingDeferredFileOutputStream utf8RecodingDeferredFileOutputStream,
446                                          String name )
447     {
448         if ( utf8RecodingDeferredFileOutputStream != null && utf8RecodingDeferredFileOutputStream.getByteCount() > 0 )
449         {
450             xmlWriter.startElement( name );
451 
452             try
453             {
454                 xmlWriter.writeText( "" ); // Cheat sax to emit element
455                 outputStreamWriter.flush();
456                 utf8RecodingDeferredFileOutputStream.close();
457                 eos.getUnderlying().write( ByteConstantsHolder.CDATA_START_BYTES ); // emit cdata
458                 utf8RecodingDeferredFileOutputStream.writeTo( eos );
459                 eos.getUnderlying().write( ByteConstantsHolder.CDATA_END_BYTES );
460                 eos.flush();
461             }
462             catch ( IOException e )
463             {
464                 throw new ReporterException( "When writing xml report stdout/stderr", e );
465             }
466             xmlWriter.endElement();
467         }
468     }
469 
470     /**
471      * Adds system properties to the XML report.
472      * <br>
473      *
474      * @param xmlWriter The test suite to report to
475      */
476     private static void showProperties( XMLWriter xmlWriter, Map<String, String> systemProperties )
477     {
478         xmlWriter.startElement( "properties" );
479         for ( final Entry<String, String> entry : systemProperties.entrySet() )
480         {
481             final String key = entry.getKey();
482             String value = entry.getValue();
483 
484             if ( value == null )
485             {
486                 value = "null";
487             }
488 
489             xmlWriter.startElement( "property" );
490 
491             xmlWriter.addAttribute( "name", key );
492 
493             xmlWriter.addAttribute( "value", extraEscape( value, true ) );
494 
495             xmlWriter.endElement();
496         }
497         xmlWriter.endElement();
498     }
499 
500     /**
501      * Handle stuff that may pop up in java that is not legal in xml
502      *
503      * @param message   The string
504      * @param attribute true if the escaped value is inside an attribute
505      * @return The escaped string
506      */
507     private static String extraEscape( String message, boolean attribute )
508     {
509         // Someday convert to xml 1.1 which handles everything but 0 inside string
510         return containsEscapesIllegalXml10( message ) ? escapeXml( message, attribute ) : message;
511     }
512 
513     private static final class EncodingOutputStream
514         extends FilterOutputStream
515     {
516         private int c1;
517 
518         private int c2;
519 
520         EncodingOutputStream( OutputStream out )
521         {
522             super( out );
523         }
524 
525         OutputStream getUnderlying()
526         {
527             return out;
528         }
529 
530         private boolean isCdataEndBlock( int c )
531         {
532             return c1 == ']' && c2 == ']' && c == '>';
533         }
534 
535         @Override
536         public void write( int b )
537             throws IOException
538         {
539             if ( isCdataEndBlock( b ) )
540             {
541                 out.write( ByteConstantsHolder.CDATA_ESCAPE_STRING_BYTES );
542             }
543             else if ( isIllegalEscape( b ) )
544             {
545                 // uh-oh!  This character is illegal in XML 1.0!
546                 // http://www.w3.org/TR/1998/REC-xml-19980210#charsets
547                 // we're going to deliberately doubly-XML escape it...
548                 // there's nothing better we can do! :-(
549                 // SUREFIRE-456
550                 out.write( ByteConstantsHolder.AMP_BYTES );
551                 out.write( String.valueOf( b ).getBytes( UTF_8 ) );
552                 out.write( ';' ); // & Will be encoded to amp inside xml encodingSHO
553             }
554             else
555             {
556                 out.write( b );
557             }
558             c1 = c2;
559             c2 = b;
560         }
561     }
562 
563     private static boolean containsEscapesIllegalXml10( String message )
564     {
565         int size = message.length();
566         for ( int i = 0; i < size; i++ )
567         {
568             if ( isIllegalEscape( message.charAt( i ) ) )
569             {
570                 return true;
571             }
572 
573         }
574         return false;
575     }
576 
577     private static boolean isIllegalEscape( char c )
578     {
579         return isIllegalEscape( (int) c );
580     }
581 
582     private static boolean isIllegalEscape( int c )
583     {
584         return c >= 0 && c < 32 && c != '\n' && c != '\r' && c != '\t';
585     }
586 
587     private static String escapeXml( String text, boolean attribute )
588     {
589         StringBuilder sb = new StringBuilder( text.length() * 2 );
590         for ( int i = 0; i < text.length(); i++ )
591         {
592             char c = text.charAt( i );
593             if ( isIllegalEscape( c ) )
594             {
595                 // uh-oh!  This character is illegal in XML 1.0!
596                 // http://www.w3.org/TR/1998/REC-xml-19980210#charsets
597                 // we're going to deliberately doubly-XML escape it...
598                 // there's nothing better we can do! :-(
599                 // SUREFIRE-456
600                 sb.append( attribute ? "&#" : "&amp#" ).append( (int) c ).append(
601                     ';' ); // & Will be encoded to amp inside xml encodingSHO
602             }
603             else
604             {
605                 sb.append( c );
606             }
607         }
608         return sb.toString();
609     }
610 
611     private static final class ByteConstantsHolder
612     {
613         private static final byte[] CDATA_START_BYTES;
614 
615         private static final byte[] CDATA_END_BYTES;
616 
617         private static final byte[] CDATA_ESCAPE_STRING_BYTES;
618 
619         private static final byte[] AMP_BYTES;
620 
621         static
622         {
623             CDATA_START_BYTES = "<![CDATA[".getBytes( UTF_8 );
624             CDATA_END_BYTES = "]]>".getBytes( UTF_8 );
625             CDATA_ESCAPE_STRING_BYTES = "]]><![CDATA[>".getBytes( UTF_8 );
626             AMP_BYTES = "&amp#".getBytes( UTF_8 );
627         }
628     }
629 }