1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 package org.apache.maven.plugin.surefire.report;
20
21 import java.io.BufferedOutputStream;
22 import java.io.File;
23 import java.io.FilterOutputStream;
24 import java.io.IOException;
25 import java.io.OutputStream;
26 import java.io.OutputStreamWriter;
27 import java.io.PrintWriter;
28 import java.nio.file.Files;
29 import java.time.Instant;
30 import java.util.ArrayList;
31 import java.util.Deque;
32 import java.util.LinkedHashMap;
33 import java.util.List;
34 import java.util.Map;
35 import java.util.Map.Entry;
36 import java.util.StringTokenizer;
37 import java.util.concurrent.ConcurrentLinkedDeque;
38
39 import org.apache.maven.plugin.surefire.booterclient.output.InPluginProcessDumpSingleton;
40 import org.apache.maven.surefire.api.report.SafeThrowable;
41 import org.apache.maven.surefire.extensions.StatelessReportEventListener;
42 import org.apache.maven.surefire.shared.utils.xml.PrettyPrintXMLWriter;
43 import org.apache.maven.surefire.shared.utils.xml.XMLWriter;
44
45 import static java.nio.charset.StandardCharsets.UTF_8;
46 import static org.apache.maven.plugin.surefire.report.DefaultReporterFactory.TestResultType;
47 import static org.apache.maven.plugin.surefire.report.FileReporterUtils.stripIllegalFilenameChars;
48 import static org.apache.maven.plugin.surefire.report.ReportEntryType.SKIPPED;
49 import static org.apache.maven.plugin.surefire.report.ReportEntryType.SUCCESS;
50 import static org.apache.maven.surefire.shared.utils.StringUtils.isBlank;
51 import static org.apache.maven.surefire.shared.utils.StringUtils.isNotBlank;
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87 @SuppressWarnings({"javadoc", "checkstyle:javadoctype"})
88
89 public class StatelessXmlReporter implements StatelessReportEventListener<WrappedReportEntry, TestSetStats> {
90 private static final float ONE_SECOND = 1000.0f;
91
92 private static final String XML_INDENT = " ";
93
94 private static final String XML_NL = "\n";
95
96 private final File reportsDirectory;
97
98 private final String reportNameSuffix;
99
100 private final boolean trimStackTrace;
101
102 private final int rerunFailingTestsCount;
103
104 private final String xsdSchemaLocation;
105
106 private final String xsdVersion;
107
108
109
110 private final Map<String, Deque<WrappedReportEntry>> testClassMethodRunHistoryMap;
111
112 private final boolean phrasedFileName;
113
114 private final boolean phrasedSuiteName;
115
116 private final boolean phrasedClassName;
117
118 private final boolean phrasedMethodName;
119
120 private final boolean enableOutErrElements;
121
122 private final boolean enablePropertiesElement;
123
124 private final boolean reportTestTimestamp;
125
126
127
128
129
130 @Deprecated
131 public StatelessXmlReporter(
132 File reportsDirectory,
133 String reportNameSuffix,
134 boolean trimStackTrace,
135 int rerunFailingTestsCount,
136 Map<String, Deque<WrappedReportEntry>> testClassMethodRunHistoryMap,
137 String xsdSchemaLocation,
138 String xsdVersion,
139 boolean phrasedFileName,
140 boolean phrasedSuiteName,
141 boolean phrasedClassName,
142 boolean phrasedMethodName,
143 boolean enableOutErrElements,
144 boolean enablePropertiesElement,
145 boolean reportTestTimestamp) {
146 this.reportsDirectory = reportsDirectory;
147 this.reportNameSuffix = reportNameSuffix;
148 this.trimStackTrace = trimStackTrace;
149 this.rerunFailingTestsCount = rerunFailingTestsCount;
150 this.testClassMethodRunHistoryMap = testClassMethodRunHistoryMap;
151 this.xsdSchemaLocation = xsdSchemaLocation;
152 this.xsdVersion = xsdVersion;
153 this.phrasedFileName = phrasedFileName;
154 this.phrasedSuiteName = phrasedSuiteName;
155 this.phrasedClassName = phrasedClassName;
156 this.phrasedMethodName = phrasedMethodName;
157 this.enableOutErrElements = enableOutErrElements;
158 this.enablePropertiesElement = enablePropertiesElement;
159 this.reportTestTimestamp = reportTestTimestamp;
160 }
161
162 @Override
163 public void testSetCompleted(WrappedReportEntry testSetReportEntry, TestSetStats testSetStats) {
164 Map<String, Map<String, List<WrappedReportEntry>>> classMethodStatistics =
165 arrangeMethodStatistics(testSetReportEntry, testSetStats);
166
167
168
169 try (OutputStream outputStream = getOutputStream(testSetReportEntry);
170 OutputStreamWriter fw = getWriter(outputStream)) {
171 XMLWriter ppw = new PrettyPrintXMLWriter(new PrintWriter(fw), XML_INDENT, XML_NL, UTF_8.name(), null);
172
173 createTestSuiteElement(ppw, testSetReportEntry, classMethodStatistics);
174
175 if (enablePropertiesElement) {
176 showProperties(ppw, testSetReportEntry.getSystemProperties());
177 } else {
178 boolean hasNonSuccess = false;
179 for (Map<String, List<WrappedReportEntry>> statistics : classMethodStatistics.values()) {
180 for (List<WrappedReportEntry> thisMethodRuns : statistics.values()) {
181 if (thisMethodRuns.stream()
182 .anyMatch(entry -> entry.getReportEntryType() != ReportEntryType.SUCCESS)) {
183 hasNonSuccess = true;
184 break;
185 }
186 }
187 if (hasNonSuccess) {
188 break;
189 }
190 }
191
192 if (hasNonSuccess) {
193 showProperties(ppw, testSetReportEntry.getSystemProperties());
194 }
195 }
196
197 for (Entry<String, Map<String, List<WrappedReportEntry>>> statistics : classMethodStatistics.entrySet()) {
198 Map<String, List<WrappedReportEntry>> methodStatistics = statistics.getValue();
199 for (Entry<String, List<WrappedReportEntry>> thisMethodRuns : methodStatistics.entrySet()) {
200 serializeTestClass(outputStream, fw, ppw, thisMethodRuns.getValue(), methodStatistics);
201 }
202 }
203
204 ppw.endElement();
205 } catch (IOException e) {
206
207
208
209 InPluginProcessDumpSingleton.getSingleton().dumpException(e, e.getLocalizedMessage(), reportsDirectory);
210 }
211 }
212
213 private Map<String, Map<String, List<WrappedReportEntry>>> arrangeMethodStatistics(
214 WrappedReportEntry testSetReportEntry, TestSetStats testSetStats) {
215 Map<String, Map<String, List<WrappedReportEntry>>> classMethodStatistics = new LinkedHashMap<>();
216 for (WrappedReportEntry methodEntry : aggregateCacheFromMultipleReruns(testSetReportEntry, testSetStats)) {
217 String testClassName = methodEntry.getSourceName();
218 Map<String, List<WrappedReportEntry>> stats =
219 classMethodStatistics.computeIfAbsent(testClassName, k -> new LinkedHashMap<>());
220 String methodName = methodEntry.getName();
221 List<WrappedReportEntry> methodRuns = stats.computeIfAbsent(methodName, k -> new ArrayList<>());
222 methodRuns.add(methodEntry);
223 }
224 return classMethodStatistics;
225 }
226
227 private Deque<WrappedReportEntry> aggregateCacheFromMultipleReruns(
228 WrappedReportEntry testSetReportEntry, TestSetStats testSetStats) {
229 String suiteClassName = testSetReportEntry.getSourceName();
230 Deque<WrappedReportEntry> methodRunHistory = getAddMethodRunHistoryMap(suiteClassName);
231 methodRunHistory.addAll(testSetStats.getReportEntries());
232 return methodRunHistory;
233 }
234
235 private void serializeTestClass(
236 OutputStream outputStream,
237 OutputStreamWriter fw,
238 XMLWriter ppw,
239 List<WrappedReportEntry> methodEntries,
240 Map<String, List<WrappedReportEntry>> methodStatistics)
241 throws IOException {
242 if (rerunFailingTestsCount > 0) {
243 serializeTestClassWithRerun(outputStream, fw, ppw, methodEntries, methodStatistics);
244 } else {
245
246
247 serializeTestClassWithoutRerun(outputStream, fw, ppw, methodEntries);
248 }
249 }
250
251 private void serializeTestClassWithoutRerun(
252 OutputStream outputStream, OutputStreamWriter fw, XMLWriter ppw, List<WrappedReportEntry> methodEntries)
253 throws IOException {
254 for (WrappedReportEntry methodEntry : methodEntries) {
255 startTestElement(ppw, methodEntry);
256 if (methodEntry.getReportEntryType() != SUCCESS) {
257 getTestProblems(
258 fw,
259 ppw,
260 methodEntry,
261 trimStackTrace,
262 outputStream,
263 methodEntry.getReportEntryType().getXmlTag(),
264 false);
265 }
266 if (methodEntry.getReportEntryType() != SUCCESS || enableOutErrElements) {
267 createOutErrElements(fw, ppw, methodEntry, outputStream);
268 }
269 ppw.endElement();
270 }
271 }
272
273 private void serializeTestClassWithRerun(
274 OutputStream outputStream,
275 OutputStreamWriter fw,
276 XMLWriter ppw,
277 List<WrappedReportEntry> methodEntries,
278 Map<String, List<WrappedReportEntry>> methodStatistics)
279 throws IOException {
280 WrappedReportEntry firstMethodEntry = methodEntries.get(0);
281
282 TestResultType resultType =
283 getTestResultTypeWithBeforeAllHandling(firstMethodEntry.getName(), methodEntries, methodStatistics);
284
285 switch (resultType) {
286 case SUCCESS:
287 for (WrappedReportEntry methodEntry : methodEntries) {
288 if (methodEntry.getReportEntryType() == SUCCESS) {
289 startTestElement(ppw, methodEntry);
290 ppw.endElement();
291 }
292 }
293 break;
294 case ERROR:
295 case FAILURE:
296
297 startTestElement(ppw, firstMethodEntry);
298 boolean firstRun = true;
299 for (WrappedReportEntry singleRunEntry : methodEntries) {
300 if (firstRun) {
301 firstRun = false;
302 getTestProblems(
303 fw,
304 ppw,
305 singleRunEntry,
306 trimStackTrace,
307 outputStream,
308 singleRunEntry.getReportEntryType().getXmlTag(),
309 false);
310 createOutErrElements(fw, ppw, singleRunEntry, outputStream);
311 } else if (singleRunEntry.getReportEntryType() == SKIPPED) {
312
313
314
315
316 addCommentElementTestCase("a skipped test execution in re-run phase", fw, ppw, outputStream);
317 } else {
318 getTestProblems(
319 fw,
320 ppw,
321 singleRunEntry,
322 trimStackTrace,
323 outputStream,
324 singleRunEntry.getReportEntryType().getRerunXmlTag(),
325 true);
326 }
327 }
328 ppw.endElement();
329 break;
330 case FLAKE:
331 WrappedReportEntry successful = null;
332
333 for (WrappedReportEntry singleRunEntry : methodEntries) {
334 if (singleRunEntry.getReportEntryType() == SUCCESS) {
335 successful = singleRunEntry;
336 break;
337 }
338 }
339 WrappedReportEntry firstOrSuccessful = successful == null ? methodEntries.get(0) : successful;
340 startTestElement(ppw, firstOrSuccessful);
341 for (WrappedReportEntry singleRunEntry : methodEntries) {
342 if (singleRunEntry.getReportEntryType() != SUCCESS) {
343 getTestProblems(
344 fw,
345 ppw,
346 singleRunEntry,
347 trimStackTrace,
348 outputStream,
349 singleRunEntry.getReportEntryType().getFlakyXmlTag(),
350 true);
351 }
352 }
353 ppw.endElement();
354 break;
355 case SKIPPED:
356 startTestElement(ppw, firstMethodEntry);
357 getTestProblems(
358 fw,
359 ppw,
360 firstMethodEntry,
361 trimStackTrace,
362 outputStream,
363 firstMethodEntry.getReportEntryType().getXmlTag(),
364 false);
365 ppw.endElement();
366 break;
367 default:
368 throw new IllegalStateException("Get unknown test result type");
369 }
370 }
371
372
373
374
375 public void cleanTestHistoryMap() {
376 testClassMethodRunHistoryMap.clear();
377 }
378
379
380
381
382
383
384
385 private TestResultType getTestResultType(List<WrappedReportEntry> methodEntryList) {
386 List<ReportEntryType> testResultTypeList = new ArrayList<>();
387 for (WrappedReportEntry singleRunEntry : methodEntryList) {
388 testResultTypeList.add(singleRunEntry.getReportEntryType());
389 }
390
391 return DefaultReporterFactory.getTestResultType(testResultTypeList, rerunFailingTestsCount);
392 }
393
394
395
396
397
398
399
400
401
402
403 private TestResultType getTestResultTypeWithBeforeAllHandling(
404 String methodName,
405 List<WrappedReportEntry> methodRuns,
406 Map<String, List<WrappedReportEntry>> methodStatistics) {
407 TestResultType resultType = getTestResultType(methodRuns);
408
409
410
411 if ((methodName == null || methodName.equals("null"))
412 && (resultType == TestResultType.ERROR || resultType == TestResultType.FAILURE)) {
413
414 boolean hasSuccessfulTestMethods = methodStatistics.entrySet().stream()
415 .filter(entry ->
416 entry.getKey() != null && !entry.getKey().equals("null"))
417 .anyMatch(entry -> entry.getValue().stream()
418 .anyMatch(reportEntry -> reportEntry.getReportEntryType() == SUCCESS));
419
420 if (hasSuccessfulTestMethods) {
421 resultType = TestResultType.FLAKE;
422 }
423 }
424
425 return resultType;
426 }
427
428 private Deque<WrappedReportEntry> getAddMethodRunHistoryMap(String testClassName) {
429 Deque<WrappedReportEntry> methodRunHistory = testClassMethodRunHistoryMap.get(testClassName);
430 if (methodRunHistory == null) {
431 methodRunHistory = new ConcurrentLinkedDeque<>();
432 testClassMethodRunHistoryMap.put(testClassName == null ? "null" : testClassName, methodRunHistory);
433 }
434 return methodRunHistory;
435 }
436
437 private OutputStream getOutputStream(WrappedReportEntry testSetReportEntry) throws IOException {
438 File reportFile = getReportFile(testSetReportEntry);
439 File reportDir = reportFile.getParentFile();
440
441 reportFile.delete();
442
443 reportDir.mkdirs();
444 return new BufferedOutputStream(Files.newOutputStream(reportFile.toPath()), 64 * 1024);
445 }
446
447 private static OutputStreamWriter getWriter(OutputStream fos) {
448 return new OutputStreamWriter(fos, UTF_8);
449 }
450
451 private File getReportFile(WrappedReportEntry report) {
452 String reportName = "TEST-" + (phrasedFileName ? report.getReportSourceName() : report.getSourceName());
453 String customizedReportName = isBlank(reportNameSuffix) ? reportName : reportName + "-" + reportNameSuffix;
454 return new File(reportsDirectory, stripIllegalFilenameChars(customizedReportName + ".xml"));
455 }
456
457 private void startTestElement(XMLWriter ppw, WrappedReportEntry report) throws IOException {
458 ppw.startElement("testcase");
459 String name = phrasedMethodName ? report.getReportName() : report.getName();
460 ppw.addAttribute("name", name == null ? "" : extraEscapeAttribute(name));
461
462 if (report.getGroup() != null) {
463 ppw.addAttribute("group", report.getGroup());
464 }
465
466 String className = phrasedClassName
467 ? report.getReportSourceName(reportNameSuffix)
468 : report.getSourceText() != null ? report.getSourceText() : report.getSourceName(reportNameSuffix);
469 if (className != null) {
470 ppw.addAttribute("classname", extraEscapeAttribute(className));
471 }
472
473 if (report.getElapsed() != null) {
474 ppw.addAttribute("time", String.valueOf(report.getElapsed() / ONE_SECOND));
475 }
476
477 if (reportTestTimestamp && report.getStartTime() > 0L) {
478 ppw.addAttribute(
479 "timestamp", Instant.ofEpochMilli(report.getStartTime()).toString());
480 }
481 }
482
483 private void createTestSuiteElement(
484 XMLWriter ppw,
485 WrappedReportEntry report,
486 Map<String, Map<String, List<WrappedReportEntry>>> classMethodStatistics)
487 throws IOException {
488 ppw.startElement("testsuite");
489
490 ppw.addAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance");
491 ppw.addAttribute("xsi:noNamespaceSchemaLocation", xsdSchemaLocation);
492 ppw.addAttribute("version", xsdVersion);
493
494 String reportName = phrasedSuiteName
495 ? report.getReportSourceName(reportNameSuffix)
496 : report.getSourceName(reportNameSuffix);
497 ppw.addAttribute("name", reportName == null ? "" : extraEscapeAttribute(reportName));
498
499 if (report.getGroup() != null) {
500 ppw.addAttribute("group", report.getGroup());
501 }
502
503 if (report.getElapsed() != null) {
504 ppw.addAttribute("time", String.valueOf(report.getElapsed() / ONE_SECOND));
505 }
506
507 if (reportTestTimestamp && report.getStartTime() > 0L) {
508 ppw.addAttribute(
509 "timestamp", Instant.ofEpochMilli(report.getStartTime()).toString());
510 }
511
512
513
514 int actualTestCount = 0;
515 int errors = 0;
516 int failures = 0;
517 int skipped = 0;
518 int flakes = 0;
519
520 for (Map<String, List<WrappedReportEntry>> methodStats : classMethodStatistics.values()) {
521 actualTestCount += methodStats.size();
522 for (Map.Entry<String, List<WrappedReportEntry>> methodEntry : methodStats.entrySet()) {
523 String methodName = methodEntry.getKey();
524 List<WrappedReportEntry> methodRuns = methodEntry.getValue();
525 TestResultType resultType = getTestResultTypeWithBeforeAllHandling(methodName, methodRuns, methodStats);
526
527 switch (resultType) {
528 case ERROR:
529 errors++;
530 break;
531 case FAILURE:
532 failures++;
533 break;
534 case SKIPPED:
535 skipped++;
536 break;
537 case FLAKE:
538 flakes++;
539 break;
540 case SUCCESS:
541 default:
542 break;
543 }
544 }
545 }
546
547 ppw.addAttribute("tests", String.valueOf(actualTestCount));
548 ppw.addAttribute("errors", String.valueOf(errors));
549 ppw.addAttribute("skipped", String.valueOf(skipped));
550 ppw.addAttribute("failures", String.valueOf(failures));
551 ppw.addAttribute("flakes", String.valueOf(flakes));
552 }
553
554 private static void getTestProblems(
555 OutputStreamWriter outputStreamWriter,
556 XMLWriter ppw,
557 WrappedReportEntry report,
558 boolean trimStackTrace,
559 OutputStream fw,
560 String testErrorType,
561 boolean createNestedOutErrElements)
562 throws IOException {
563 ppw.startElement(testErrorType);
564
565 String stackTrace = report.getStackTrace(trimStackTrace);
566
567 if (report.getMessage() != null && !report.getMessage().isEmpty()) {
568 ppw.addAttribute("message", extraEscapeAttribute(report.getMessage()));
569 }
570
571 if (report.getStackTraceWriter() != null) {
572
573 SafeThrowable t = report.getStackTraceWriter().getThrowable();
574 if (t != null) {
575 if (t.getMessage() != null) {
576 int delimiter = stackTrace.indexOf(":");
577 String type = delimiter == -1 ? stackTrace : stackTrace.substring(0, delimiter);
578 ppw.addAttribute("type", type);
579 } else {
580 if (isNotBlank(stackTrace)) {
581 ppw.addAttribute("type", new StringTokenizer(stackTrace).nextToken());
582 }
583 }
584 }
585 }
586
587
588
589
590
591 if (createNestedOutErrElements) {
592 ppw.startElement("stackTrace");
593 if (stackTrace != null) {
594 extraEscapeElementValue(stackTrace, outputStreamWriter, ppw, fw);
595 }
596 ppw.endElement();
597
598 createOutErrElements(outputStreamWriter, ppw, report, fw);
599 } else if (stackTrace != null) {
600 extraEscapeElementValue(stackTrace, outputStreamWriter, ppw, fw);
601 }
602
603 ppw.endElement();
604 }
605
606
607 private static void createOutErrElements(
608 OutputStreamWriter outputStreamWriter, XMLWriter ppw, WrappedReportEntry report, OutputStream fw)
609 throws IOException {
610 EncodingOutputStream eos = new EncodingOutputStream(fw);
611 addOutputStreamElement(outputStreamWriter, eos, ppw, report.getStdout(), "system-out");
612 addOutputStreamElement(outputStreamWriter, eos, ppw, report.getStdErr(), "system-err");
613 }
614
615 private static void addOutputStreamElement(
616 OutputStreamWriter outputStreamWriter,
617 EncodingOutputStream eos,
618 XMLWriter xmlWriter,
619 Utf8RecodingDeferredFileOutputStream utf8RecodingDeferredFileOutputStream,
620 String name)
621 throws IOException {
622 if (utf8RecodingDeferredFileOutputStream != null && utf8RecodingDeferredFileOutputStream.getByteCount() > 0) {
623 xmlWriter.startElement(name);
624 xmlWriter.writeText("");
625 outputStreamWriter.flush();
626 eos.getUnderlying().write(ByteConstantsHolder.CDATA_START_BYTES);
627 utf8RecodingDeferredFileOutputStream.writeTo(eos);
628 utf8RecodingDeferredFileOutputStream.free();
629 eos.getUnderlying().write(ByteConstantsHolder.CDATA_END_BYTES);
630 eos.flush();
631 xmlWriter.endElement();
632 }
633 }
634
635
636
637
638
639
640
641 private static void showProperties(XMLWriter xmlWriter, Map<String, String> systemProperties) throws IOException {
642 xmlWriter.startElement("properties");
643 for (final Entry<String, String> entry : systemProperties.entrySet()) {
644 final String key = entry.getKey();
645 String value = entry.getValue();
646
647 if (value == null) {
648 value = "null";
649 }
650
651 xmlWriter.startElement("property");
652
653 xmlWriter.addAttribute("name", key);
654
655 xmlWriter.addAttribute("value", extraEscapeAttribute(value));
656
657 xmlWriter.endElement();
658 }
659 xmlWriter.endElement();
660 }
661
662
663
664
665
666
667
668 private static String extraEscapeAttribute(String message) {
669
670 return containsEscapesIllegalXml10(message) ? escapeXml(message, true) : message;
671 }
672
673
674
675
676
677
678 private static void extraEscapeElementValue(
679 String message, OutputStreamWriter outputStreamWriter, XMLWriter xmlWriter, OutputStream fw)
680 throws IOException {
681
682 if (containsEscapesIllegalXml10(message)) {
683 xmlWriter.writeText(escapeXml(message, false));
684 } else {
685 EncodingOutputStream eos = new EncodingOutputStream(fw);
686 xmlWriter.writeText("");
687 outputStreamWriter.flush();
688 eos.getUnderlying().write(ByteConstantsHolder.CDATA_START_BYTES);
689 eos.write(message.getBytes(UTF_8));
690 eos.getUnderlying().write(ByteConstantsHolder.CDATA_END_BYTES);
691 eos.flush();
692 }
693 }
694
695
696 private static void addCommentElementTestCase(
697 String comment, OutputStreamWriter outputStreamWriter, XMLWriter xmlWriter, OutputStream fw)
698 throws IOException {
699 xmlWriter.writeText("");
700 outputStreamWriter.flush();
701 fw.write(XML_NL.getBytes(UTF_8));
702 fw.write(XML_INDENT.getBytes(UTF_8));
703 fw.write(XML_INDENT.getBytes(UTF_8));
704 fw.write(ByteConstantsHolder.COMMENT_START);
705 fw.write(comment.getBytes(UTF_8));
706 fw.write(ByteConstantsHolder.COMMENT_END);
707 fw.write(XML_NL.getBytes(UTF_8));
708 fw.write(XML_INDENT.getBytes(UTF_8));
709 fw.flush();
710 }
711
712 private static final class EncodingOutputStream extends FilterOutputStream {
713 private int c1;
714
715 private int c2;
716
717 EncodingOutputStream(OutputStream out) {
718 super(out);
719 }
720
721 OutputStream getUnderlying() {
722 return out;
723 }
724
725 private boolean isCdataEndBlock(int c) {
726 return c1 == ']' && c2 == ']' && c == '>';
727 }
728
729 @Override
730 public void write(int b) throws IOException {
731 if (isCdataEndBlock(b)) {
732 out.write(ByteConstantsHolder.CDATA_ESCAPE_STRING_BYTES);
733 } else if (isIllegalEscape(b)) {
734
735
736
737
738
739 out.write(ByteConstantsHolder.AMP_BYTES);
740 out.write(String.valueOf(b).getBytes(UTF_8));
741 out.write(';');
742 } else {
743 out.write(b);
744 }
745 c1 = c2;
746 c2 = b;
747 }
748 }
749
750 private static boolean containsEscapesIllegalXml10(String message) {
751 int size = message.length();
752 for (int i = 0; i < size; i++) {
753 if (isIllegalEscape(message.charAt(i))) {
754 return true;
755 }
756 }
757 return false;
758 }
759
760 private static boolean isIllegalEscape(char c) {
761 return isIllegalEscape((int) c);
762 }
763
764 private static boolean isIllegalEscape(int c) {
765 return c >= 0 && c < 32 && c != '\n' && c != '\r' && c != '\t';
766 }
767
768
769
770
771
772
773
774
775 private static String escapeXml(String text, boolean attribute) {
776 StringBuilder sb = new StringBuilder(text.length() * 2);
777 for (int i = 0; i < text.length(); i++) {
778 char c = text.charAt(i);
779 if (isIllegalEscape(c)) {
780
781
782
783
784
785 sb.append(attribute ? "&#" : "&#")
786 .append((int) c)
787 .append(';');
788 } else {
789 sb.append(c);
790 }
791 }
792 return sb.toString();
793 }
794
795 private static final class ByteConstantsHolder {
796 private static final byte[] CDATA_START_BYTES = "<![CDATA[".getBytes(UTF_8);
797
798 private static final byte[] CDATA_END_BYTES = "]]>".getBytes(UTF_8);
799
800 private static final byte[] CDATA_ESCAPE_STRING_BYTES = "]]><![CDATA[>".getBytes(UTF_8);
801
802 private static final byte[] AMP_BYTES = "&#".getBytes(UTF_8);
803
804 private static final byte[] COMMENT_START = "<!-- ".getBytes(UTF_8);
805
806 private static final byte[] COMMENT_END = " --> ".getBytes(UTF_8);
807 }
808 }