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