| package org.apache.maven.plugin.surefire.report; |
| |
| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| |
| import org.apache.maven.plugin.surefire.booterclient.output.InPluginProcessDumpSingleton; |
| import org.apache.maven.surefire.shared.utils.xml.PrettyPrintXMLWriter; |
| import org.apache.maven.surefire.shared.utils.xml.XMLWriter; |
| import org.apache.maven.surefire.extensions.StatelessReportEventListener; |
| import org.apache.maven.surefire.api.report.ReporterException; |
| import org.apache.maven.surefire.api.report.SafeThrowable; |
| |
| import java.io.BufferedOutputStream; |
| import java.io.File; |
| import java.io.FileOutputStream; |
| import java.io.FilterOutputStream; |
| import java.io.IOException; |
| import java.io.OutputStream; |
| import java.io.OutputStreamWriter; |
| import java.util.ArrayList; |
| import java.util.Deque; |
| import java.util.LinkedHashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Map.Entry; |
| import java.util.StringTokenizer; |
| import java.util.concurrent.ConcurrentLinkedDeque; |
| |
| import static java.nio.charset.StandardCharsets.UTF_8; |
| import static org.apache.maven.plugin.surefire.report.DefaultReporterFactory.TestResultType; |
| import static org.apache.maven.plugin.surefire.report.FileReporterUtils.stripIllegalFilenameChars; |
| import static org.apache.maven.plugin.surefire.report.ReportEntryType.SUCCESS; |
| import static org.apache.maven.surefire.shared.utils.StringUtils.isBlank; |
| |
| @SuppressWarnings( { "javadoc", "checkstyle:javadoctype" } ) |
| // CHECKSTYLE_OFF: LineLength |
| /* |
| * XML format reporter writing to <code>TEST-<i>reportName</i>[-<i>suffix</i>].xml</code> file like written and read |
| * by Ant's <a href="http://ant.apache.org/manual/Tasks/junit.html"><code><junit></code></a> and |
| * <a href="http://ant.apache.org/manual/Tasks/junitreport.html"><code><junitreport></code></a> tasks, |
| * then supported by many tools like CI servers. |
| * <br> |
| * <pre><?xml version="1.0" encoding="UTF-8"?> |
| * <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>"> |
| * <properties> |
| * <property name="<i>name</i>" value="<i>value</i>"/> |
| * [...] |
| * </properties> |
| * <testcase time="<i>0,###.###</i>" name="<i>test name</i> [classname="<i>class name</i>"] [group="<i>group</i>"]"/> |
| * <testcase time="<i>0,###.###</i>" name="<i>test name</i> [classname="<i>class name</i>"] [group="<i>group</i>"]"> |
| * <<b>error</b> message="<i>message</i>" type="<i>exception class name</i>"><i>stacktrace</i></error> |
| * <system-out><i>system out content (present only if not empty)</i></system-out> |
| * <system-err><i>system err content (present only if not empty)</i></system-err> |
| * </testcase> |
| * <testcase time="<i>0,###.###</i>" name="<i>test name</i> [classname="<i>class name</i>"] [group="<i>group</i>"]"> |
| * <<b>failure</b> message="<i>message</i>" type="<i>exception class name</i>"><i>stacktrace</i></failure> |
| * <system-out><i>system out content (present only if not empty)</i></system-out> |
| * <system-err><i>system err content (present only if not empty)</i></system-err> |
| * </testcase> |
| * <testcase time="<i>0,###.###</i>" name="<i>test name</i> [classname="<i>class name</i>"] [group="<i>group</i>"]"> |
| * <<b>skipped</b>/> |
| * </testcase> |
| * [...]</pre> |
| * |
| * @author Kristian Rosenvold |
| * @see <a href="http://wiki.apache.org/ant/Proposals/EnhancedTestReports">Ant's format enhancement proposal</a> |
| * (not yet implemented by Ant 1.8.2) |
| */ |
| //todo this is no more stateless due to existence of testClassMethodRunHistoryMap since of 2.19. |
| public class StatelessXmlReporter |
| implements StatelessReportEventListener<WrappedReportEntry, TestSetStats> |
| { |
| private final File reportsDirectory; |
| |
| private final String reportNameSuffix; |
| |
| private final boolean trimStackTrace; |
| |
| private final int rerunFailingTestsCount; |
| |
| private final String xsdSchemaLocation; |
| |
| private final String xsdVersion; |
| |
| // Map between test class name and a map between test method name |
| // and the list of runs for each test method |
| private final Map<String, Deque<WrappedReportEntry>> testClassMethodRunHistoryMap; |
| |
| private final boolean phrasedFileName; |
| |
| private final boolean phrasedSuiteName; |
| |
| private final boolean phrasedClassName; |
| |
| private final boolean phrasedMethodName; |
| |
| public StatelessXmlReporter( File reportsDirectory, String reportNameSuffix, boolean trimStackTrace, |
| int rerunFailingTestsCount, |
| Map<String, Deque<WrappedReportEntry>> testClassMethodRunHistoryMap, |
| String xsdSchemaLocation, String xsdVersion, boolean phrasedFileName, |
| boolean phrasedSuiteName, boolean phrasedClassName, boolean phrasedMethodName ) |
| { |
| this.reportsDirectory = reportsDirectory; |
| this.reportNameSuffix = reportNameSuffix; |
| this.trimStackTrace = trimStackTrace; |
| this.rerunFailingTestsCount = rerunFailingTestsCount; |
| this.testClassMethodRunHistoryMap = testClassMethodRunHistoryMap; |
| this.xsdSchemaLocation = xsdSchemaLocation; |
| this.xsdVersion = xsdVersion; |
| this.phrasedFileName = phrasedFileName; |
| this.phrasedSuiteName = phrasedSuiteName; |
| this.phrasedClassName = phrasedClassName; |
| this.phrasedMethodName = phrasedMethodName; |
| } |
| |
| @Override |
| public void testSetCompleted( WrappedReportEntry testSetReportEntry, TestSetStats testSetStats ) |
| { |
| Map<String, Map<String, List<WrappedReportEntry>>> classMethodStatistics = |
| arrangeMethodStatistics( testSetReportEntry, testSetStats ); |
| |
| OutputStream outputStream = getOutputStream( testSetReportEntry ); |
| try ( OutputStreamWriter fw = getWriter( outputStream ) ) |
| { |
| XMLWriter ppw = new PrettyPrintXMLWriter( fw ); |
| ppw.setEncoding( UTF_8.name() ); |
| |
| createTestSuiteElement( ppw, testSetReportEntry, testSetStats ); // TestSuite |
| |
| showProperties( ppw, testSetReportEntry.getSystemProperties() ); |
| |
| for ( Entry<String, Map<String, List<WrappedReportEntry>>> statistics : classMethodStatistics.entrySet() ) |
| { |
| for ( Entry<String, List<WrappedReportEntry>> thisMethodRuns : statistics.getValue().entrySet() ) |
| { |
| serializeTestClass( outputStream, fw, ppw, thisMethodRuns.getValue() ); |
| } |
| } |
| |
| ppw.endElement(); // TestSuite |
| } |
| catch ( Exception e ) |
| { |
| // It's not a test error. |
| // This method must be sail-safe and errors are in a dump log. |
| // The control flow must not be broken in TestSetRunListener#testSetCompleted. |
| InPluginProcessDumpSingleton.getSingleton() |
| .dumpException( e, e.getLocalizedMessage(), reportsDirectory ); |
| } |
| } |
| |
| private Map<String, Map<String, List<WrappedReportEntry>>> arrangeMethodStatistics( |
| WrappedReportEntry testSetReportEntry, TestSetStats testSetStats ) |
| { |
| Map<String, Map<String, List<WrappedReportEntry>>> classMethodStatistics = new LinkedHashMap<>(); |
| for ( WrappedReportEntry methodEntry : aggregateCacheFromMultipleReruns( testSetReportEntry, testSetStats ) ) |
| { |
| String testClassName = methodEntry.getSourceName(); |
| Map<String, List<WrappedReportEntry>> stats = classMethodStatistics.get( testClassName ); |
| if ( stats == null ) |
| { |
| stats = new LinkedHashMap<>(); |
| classMethodStatistics.put( testClassName, stats ); |
| } |
| String methodName = methodEntry.getName(); |
| List<WrappedReportEntry> methodRuns = stats.get( methodName ); |
| if ( methodRuns == null ) |
| { |
| methodRuns = new ArrayList<>(); |
| stats.put( methodName, methodRuns ); |
| } |
| methodRuns.add( methodEntry ); |
| } |
| return classMethodStatistics; |
| } |
| |
| private Deque<WrappedReportEntry> aggregateCacheFromMultipleReruns( WrappedReportEntry testSetReportEntry, |
| TestSetStats testSetStats ) |
| { |
| String suiteClassName = testSetReportEntry.getSourceName(); |
| Deque<WrappedReportEntry> methodRunHistory = getAddMethodRunHistoryMap( suiteClassName ); |
| methodRunHistory.addAll( testSetStats.getReportEntries() ); |
| return methodRunHistory; |
| } |
| |
| private void serializeTestClass( OutputStream outputStream, OutputStreamWriter fw, XMLWriter ppw, |
| List<WrappedReportEntry> methodEntries ) |
| { |
| if ( rerunFailingTestsCount > 0 ) |
| { |
| serializeTestClassWithRerun( outputStream, fw, ppw, methodEntries ); |
| } |
| else |
| { |
| // rerunFailingTestsCount is smaller than 1, but for some reasons a test could be run |
| // for more than once |
| serializeTestClassWithoutRerun( outputStream, fw, ppw, methodEntries ); |
| } |
| } |
| |
| private void serializeTestClassWithoutRerun( OutputStream outputStream, OutputStreamWriter fw, XMLWriter ppw, |
| List<WrappedReportEntry> methodEntries ) |
| { |
| for ( WrappedReportEntry methodEntry : methodEntries ) |
| { |
| startTestElement( ppw, methodEntry ); |
| if ( methodEntry.getReportEntryType() != SUCCESS ) |
| { |
| getTestProblems( fw, ppw, methodEntry, trimStackTrace, outputStream, |
| methodEntry.getReportEntryType().getXmlTag(), false ); |
| } |
| createOutErrElements( fw, ppw, methodEntry, outputStream ); |
| ppw.endElement(); |
| } |
| } |
| |
| private void serializeTestClassWithRerun( OutputStream outputStream, OutputStreamWriter fw, XMLWriter ppw, |
| List<WrappedReportEntry> methodEntries ) |
| { |
| WrappedReportEntry firstMethodEntry = methodEntries.get( 0 ); |
| switch ( getTestResultType( methodEntries ) ) |
| { |
| case success: |
| for ( WrappedReportEntry methodEntry : methodEntries ) |
| { |
| if ( methodEntry.getReportEntryType() == SUCCESS ) |
| { |
| startTestElement( ppw, methodEntry ); |
| ppw.endElement(); |
| } |
| } |
| break; |
| case error: |
| case failure: |
| // When rerunFailingTestsCount is set to larger than 0 |
| startTestElement( ppw, firstMethodEntry ); |
| boolean firstRun = true; |
| for ( WrappedReportEntry singleRunEntry : methodEntries ) |
| { |
| if ( firstRun ) |
| { |
| firstRun = false; |
| getTestProblems( fw, ppw, singleRunEntry, trimStackTrace, outputStream, |
| singleRunEntry.getReportEntryType().getXmlTag(), false ); |
| createOutErrElements( fw, ppw, singleRunEntry, outputStream ); |
| } |
| else |
| { |
| getTestProblems( fw, ppw, singleRunEntry, trimStackTrace, outputStream, |
| singleRunEntry.getReportEntryType().getRerunXmlTag(), true ); |
| } |
| } |
| ppw.endElement(); |
| break; |
| case flake: |
| WrappedReportEntry successful = null; |
| // Get the run time of the first successful run |
| for ( WrappedReportEntry singleRunEntry : methodEntries ) |
| { |
| if ( singleRunEntry.getReportEntryType() == SUCCESS ) |
| { |
| successful = singleRunEntry; |
| break; |
| } |
| } |
| WrappedReportEntry firstOrSuccessful = successful == null ? methodEntries.get( 0 ) : successful; |
| startTestElement( ppw, firstOrSuccessful ); |
| for ( WrappedReportEntry singleRunEntry : methodEntries ) |
| { |
| if ( singleRunEntry.getReportEntryType() != SUCCESS ) |
| { |
| getTestProblems( fw, ppw, singleRunEntry, trimStackTrace, outputStream, |
| singleRunEntry.getReportEntryType().getFlakyXmlTag(), true ); |
| } |
| } |
| ppw.endElement(); |
| break; |
| case skipped: |
| startTestElement( ppw, firstMethodEntry ); |
| getTestProblems( fw, ppw, firstMethodEntry, trimStackTrace, outputStream, |
| firstMethodEntry.getReportEntryType().getXmlTag(), false ); |
| ppw.endElement(); |
| break; |
| default: |
| throw new IllegalStateException( "Get unknown test result type" ); |
| } |
| } |
| |
| /** |
| * Clean testClassMethodRunHistoryMap |
| */ |
| public void cleanTestHistoryMap() |
| { |
| testClassMethodRunHistoryMap.clear(); |
| } |
| |
| /** |
| * Get the result of a test from a list of its runs in WrappedReportEntry |
| * |
| * @param methodEntryList the list of runs for a given test |
| * @return the TestResultType for the given test |
| */ |
| private TestResultType getTestResultType( List<WrappedReportEntry> methodEntryList ) |
| { |
| List<ReportEntryType> testResultTypeList = new ArrayList<>(); |
| for ( WrappedReportEntry singleRunEntry : methodEntryList ) |
| { |
| testResultTypeList.add( singleRunEntry.getReportEntryType() ); |
| } |
| |
| return DefaultReporterFactory.getTestResultType( testResultTypeList, rerunFailingTestsCount ); |
| } |
| |
| private Deque<WrappedReportEntry> getAddMethodRunHistoryMap( String testClassName ) |
| { |
| Deque<WrappedReportEntry> methodRunHistory = testClassMethodRunHistoryMap.get( testClassName ); |
| if ( methodRunHistory == null ) |
| { |
| methodRunHistory = new ConcurrentLinkedDeque<>(); |
| testClassMethodRunHistoryMap.put( testClassName == null ? "null" : testClassName, methodRunHistory ); |
| } |
| return methodRunHistory; |
| } |
| |
| private OutputStream getOutputStream( WrappedReportEntry testSetReportEntry ) |
| { |
| File reportFile = getReportFile( testSetReportEntry ); |
| |
| File reportDir = reportFile.getParentFile(); |
| |
| //noinspection ResultOfMethodCallIgnored |
| reportDir.mkdirs(); |
| |
| try |
| { |
| return new BufferedOutputStream( new FileOutputStream( reportFile ), 64 * 1024 ); |
| } |
| catch ( Exception e ) |
| { |
| throw new ReporterException( "When writing report", e ); |
| } |
| } |
| |
| private static OutputStreamWriter getWriter( OutputStream fos ) |
| { |
| return new OutputStreamWriter( fos, UTF_8 ); |
| } |
| |
| private File getReportFile( WrappedReportEntry report ) |
| { |
| String reportName = "TEST-" + ( phrasedFileName ? report.getReportSourceName() : report.getSourceName() ); |
| String customizedReportName = isBlank( reportNameSuffix ) ? reportName : reportName + "-" + reportNameSuffix; |
| return new File( reportsDirectory, stripIllegalFilenameChars( customizedReportName + ".xml" ) ); |
| } |
| |
| private void startTestElement( XMLWriter ppw, WrappedReportEntry report ) |
| { |
| ppw.startElement( "testcase" ); |
| String name = phrasedMethodName ? report.getReportName() : report.getName(); |
| ppw.addAttribute( "name", name == null ? "" : extraEscapeAttribute( name ) ); |
| |
| if ( report.getGroup() != null ) |
| { |
| ppw.addAttribute( "group", report.getGroup() ); |
| } |
| |
| String className = phrasedClassName ? report.getReportSourceName( reportNameSuffix ) |
| : report.getSourceName( reportNameSuffix ); |
| if ( className != null ) |
| { |
| ppw.addAttribute( "classname", extraEscapeAttribute( className ) ); |
| } |
| |
| ppw.addAttribute( "time", report.elapsedTimeAsString() ); |
| } |
| |
| private void createTestSuiteElement( XMLWriter ppw, WrappedReportEntry report, TestSetStats testSetStats ) |
| { |
| ppw.startElement( "testsuite" ); |
| |
| ppw.addAttribute( "xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance" ); |
| ppw.addAttribute( "xsi:noNamespaceSchemaLocation", xsdSchemaLocation ); |
| ppw.addAttribute( "version", xsdVersion ); |
| |
| String reportName = phrasedSuiteName ? report.getReportSourceName( reportNameSuffix ) |
| : report.getSourceName( reportNameSuffix ); |
| ppw.addAttribute( "name", reportName == null ? "" : extraEscapeAttribute( reportName ) ); |
| |
| if ( report.getGroup() != null ) |
| { |
| ppw.addAttribute( "group", report.getGroup() ); |
| } |
| |
| ppw.addAttribute( "time", report.elapsedTimeAsString() ); |
| |
| ppw.addAttribute( "tests", String.valueOf( testSetStats.getCompletedCount() ) ); |
| |
| ppw.addAttribute( "errors", String.valueOf( testSetStats.getErrors() ) ); |
| |
| ppw.addAttribute( "skipped", String.valueOf( testSetStats.getSkipped() ) ); |
| |
| ppw.addAttribute( "failures", String.valueOf( testSetStats.getFailures() ) ); |
| } |
| |
| private static void getTestProblems( OutputStreamWriter outputStreamWriter, XMLWriter ppw, |
| WrappedReportEntry report, boolean trimStackTrace, OutputStream fw, |
| String testErrorType, boolean createOutErrElementsInside ) |
| { |
| ppw.startElement( testErrorType ); |
| |
| String stackTrace = report.getStackTrace( trimStackTrace ); |
| |
| if ( report.getMessage() != null && !report.getMessage().isEmpty() ) |
| { |
| ppw.addAttribute( "message", extraEscapeAttribute( report.getMessage() ) ); |
| } |
| |
| if ( report.getStackTraceWriter() != null ) |
| { |
| //noinspection ThrowableResultOfMethodCallIgnored |
| SafeThrowable t = report.getStackTraceWriter().getThrowable(); |
| if ( t != null ) |
| { |
| if ( t.getMessage() != null ) |
| { |
| int delimiter = stackTrace.indexOf( ":" ); |
| String type = delimiter == -1 ? stackTrace : stackTrace.substring( 0, delimiter ); |
| ppw.addAttribute( "type", type ); |
| } |
| else |
| { |
| ppw.addAttribute( "type", new StringTokenizer( stackTrace ).nextToken() ); |
| } |
| } |
| } |
| |
| boolean hasNestedElements = createOutErrElementsInside & stackTrace != null; |
| |
| if ( stackTrace != null ) |
| { |
| if ( hasNestedElements ) |
| { |
| ppw.startElement( "stackTrace" ); |
| } |
| |
| extraEscapeElementValue( stackTrace, outputStreamWriter, ppw, fw ); |
| |
| if ( hasNestedElements ) |
| { |
| ppw.endElement(); |
| } |
| } |
| |
| if ( createOutErrElementsInside ) |
| { |
| createOutErrElements( outputStreamWriter, ppw, report, fw ); |
| } |
| |
| ppw.endElement(); // entry type |
| } |
| |
| // Create system-out and system-err elements |
| private static void createOutErrElements( OutputStreamWriter outputStreamWriter, XMLWriter ppw, |
| WrappedReportEntry report, OutputStream fw ) |
| { |
| EncodingOutputStream eos = new EncodingOutputStream( fw ); |
| addOutputStreamElement( outputStreamWriter, eos, ppw, report.getStdout(), "system-out" ); |
| addOutputStreamElement( outputStreamWriter, eos, ppw, report.getStdErr(), "system-err" ); |
| } |
| |
| private static void addOutputStreamElement( OutputStreamWriter outputStreamWriter, |
| EncodingOutputStream eos, XMLWriter xmlWriter, |
| Utf8RecodingDeferredFileOutputStream utf8RecodingDeferredFileOutputStream, |
| String name ) |
| { |
| if ( utf8RecodingDeferredFileOutputStream != null && utf8RecodingDeferredFileOutputStream.getByteCount() > 0 ) |
| { |
| xmlWriter.startElement( name ); |
| |
| try |
| { |
| xmlWriter.writeText( "" ); // Cheat sax to emit element |
| outputStreamWriter.flush(); |
| utf8RecodingDeferredFileOutputStream.close(); |
| eos.getUnderlying().write( ByteConstantsHolder.CDATA_START_BYTES ); // emit cdata |
| utf8RecodingDeferredFileOutputStream.writeTo( eos ); |
| eos.getUnderlying().write( ByteConstantsHolder.CDATA_END_BYTES ); |
| eos.flush(); |
| } |
| catch ( IOException e ) |
| { |
| throw new ReporterException( "When writing xml report stdout/stderr", e ); |
| } |
| xmlWriter.endElement(); |
| } |
| } |
| |
| /** |
| * Adds system properties to the XML report. |
| * <br> |
| * |
| * @param xmlWriter The test suite to report to |
| */ |
| private static void showProperties( XMLWriter xmlWriter, Map<String, String> systemProperties ) |
| { |
| xmlWriter.startElement( "properties" ); |
| for ( final Entry<String, String> entry : systemProperties.entrySet() ) |
| { |
| final String key = entry.getKey(); |
| String value = entry.getValue(); |
| |
| if ( value == null ) |
| { |
| value = "null"; |
| } |
| |
| xmlWriter.startElement( "property" ); |
| |
| xmlWriter.addAttribute( "name", key ); |
| |
| xmlWriter.addAttribute( "value", extraEscapeAttribute( value ) ); |
| |
| xmlWriter.endElement(); |
| } |
| xmlWriter.endElement(); |
| } |
| |
| /** |
| * Handle stuff that may pop up in java that is not legal in xml. |
| * |
| * @param message The string |
| * @return The escaped string or returns itself if all characters are legal |
| */ |
| private static String extraEscapeAttribute( String message ) |
| { |
| // Someday convert to xml 1.1 which handles everything but 0 inside string |
| return containsEscapesIllegalXml10( message ) ? escapeXml( message, true ) : message; |
| } |
| |
| /** |
| * Writes escaped string or the message within CDATA if all characters are legal. |
| * |
| * @param message The string |
| */ |
| private static void extraEscapeElementValue( String message, OutputStreamWriter outputStreamWriter, |
| XMLWriter xmlWriter, OutputStream fw ) |
| { |
| // Someday convert to xml 1.1 which handles everything but 0 inside string |
| if ( containsEscapesIllegalXml10( message ) ) |
| { |
| xmlWriter.writeText( escapeXml( message, false ) ); |
| } |
| else |
| { |
| try |
| { |
| EncodingOutputStream eos = new EncodingOutputStream( fw ); |
| xmlWriter.writeText( "" ); // Cheat sax to emit element |
| outputStreamWriter.flush(); |
| eos.getUnderlying().write( ByteConstantsHolder.CDATA_START_BYTES ); |
| eos.write( message.getBytes( UTF_8 ) ); |
| eos.getUnderlying().write( ByteConstantsHolder.CDATA_END_BYTES ); |
| eos.flush(); |
| } |
| catch ( IOException e ) |
| { |
| throw new ReporterException( "When writing xml element", e ); |
| } |
| } |
| } |
| |
| private static final class EncodingOutputStream |
| extends FilterOutputStream |
| { |
| private int c1; |
| |
| private int c2; |
| |
| EncodingOutputStream( OutputStream out ) |
| { |
| super( out ); |
| } |
| |
| OutputStream getUnderlying() |
| { |
| return out; |
| } |
| |
| private boolean isCdataEndBlock( int c ) |
| { |
| return c1 == ']' && c2 == ']' && c == '>'; |
| } |
| |
| @Override |
| public void write( int b ) |
| throws IOException |
| { |
| if ( isCdataEndBlock( b ) ) |
| { |
| out.write( ByteConstantsHolder.CDATA_ESCAPE_STRING_BYTES ); |
| } |
| else if ( isIllegalEscape( b ) ) |
| { |
| // uh-oh! This character is illegal in XML 1.0! |
| // http://www.w3.org/TR/1998/REC-xml-19980210#charsets |
| // we're going to deliberately doubly-XML escape it... |
| // there's nothing better we can do! :-( |
| // SUREFIRE-456 |
| out.write( ByteConstantsHolder.AMP_BYTES ); |
| out.write( String.valueOf( b ).getBytes( UTF_8 ) ); |
| out.write( ';' ); // & Will be encoded to amp inside xml encodingSHO |
| } |
| else |
| { |
| out.write( b ); |
| } |
| c1 = c2; |
| c2 = b; |
| } |
| } |
| |
| private static boolean containsEscapesIllegalXml10( String message ) |
| { |
| int size = message.length(); |
| for ( int i = 0; i < size; i++ ) |
| { |
| if ( isIllegalEscape( message.charAt( i ) ) ) |
| { |
| return true; |
| } |
| |
| } |
| return false; |
| } |
| |
| private static boolean isIllegalEscape( char c ) |
| { |
| return isIllegalEscape( (int) c ); |
| } |
| |
| private static boolean isIllegalEscape( int c ) |
| { |
| return c >= 0 && c < 32 && c != '\n' && c != '\r' && c != '\t'; |
| } |
| |
| /** |
| * escape for XML 1.0 |
| * |
| * @param text The string |
| * @param attribute true if the escaped value is inside an attribute |
| * @return The escaped string |
| */ |
| private static String escapeXml( String text, boolean attribute ) |
| { |
| StringBuilder sb = new StringBuilder( text.length() * 2 ); |
| for ( int i = 0; i < text.length(); i++ ) |
| { |
| char c = text.charAt( i ); |
| if ( isIllegalEscape( c ) ) |
| { |
| // uh-oh! This character is illegal in XML 1.0! |
| // http://www.w3.org/TR/1998/REC-xml-19980210#charsets |
| // we're going to deliberately doubly-XML escape it... |
| // there's nothing better we can do! :-( |
| // SUREFIRE-456 |
| sb.append( attribute ? "&#" : "&#" ).append( (int) c ).append( |
| ';' ); // & Will be encoded to amp inside xml encodingSHO |
| } |
| else |
| { |
| sb.append( c ); |
| } |
| } |
| return sb.toString(); |
| } |
| |
| private static final class ByteConstantsHolder |
| { |
| private static final byte[] CDATA_START_BYTES; |
| |
| private static final byte[] CDATA_END_BYTES; |
| |
| private static final byte[] CDATA_ESCAPE_STRING_BYTES; |
| |
| private static final byte[] AMP_BYTES; |
| |
| static |
| { |
| CDATA_START_BYTES = "<![CDATA[".getBytes( UTF_8 ); |
| CDATA_END_BYTES = "]]>".getBytes( UTF_8 ); |
| CDATA_ESCAPE_STRING_BYTES = "]]><![CDATA[>".getBytes( UTF_8 ); |
| AMP_BYTES = "&#".getBytes( UTF_8 ); |
| } |
| } |
| } |