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