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