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