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