CXF-9145: Inconcise handling of logging features logMulitpart and logBinary (#2520)

diff --git a/rt/features/logging/src/main/java/org/apache/cxf/ext/logging/AbstractLoggingInterceptor.java b/rt/features/logging/src/main/java/org/apache/cxf/ext/logging/AbstractLoggingInterceptor.java
index 9713358..67e2763 100644
--- a/rt/features/logging/src/main/java/org/apache/cxf/ext/logging/AbstractLoggingInterceptor.java
+++ b/rt/features/logging/src/main/java/org/apache/cxf/ext/logging/AbstractLoggingInterceptor.java
@@ -153,16 +153,21 @@
                 String[] parts = originalLogString.split(Pattern.quote(boundary));
                 String payload = "";
                 for (String str : parts) {
-                    if (findContentType(str) != null
+                    final String contentType = findContentType(str);
+                    if (contentType != null
                         && eventMapper.isBinaryContent(
-                           findContentType(str).substring("Content-Type:".length()).trim())) {
-                        payload = payload + "\r\n" + CONTENT_SUPPRESSED + "\r\n";
+                                contentType.substring("Content-Type:".length()).trim())) {
+                        final String headers = extractHeaders(str);
+                        if (headers == null || headers.isEmpty()) {
+                            payload = payload + "\r\n" + CONTENT_SUPPRESSED + "\r\n";
+                        } else {
+                            payload = payload + "\r\n" + headers + "\r\n" + CONTENT_SUPPRESSED + "\r\n";
+                        }
                     } else {
                         payload = payload + str;
                         payload = payload + boundary;
                     }
                 }
-            
                 originalLogString = payload;
             }
         } catch (Exception ex) {
@@ -172,6 +177,43 @@
         
     }
     
+    private static String extractHeaders(String str) {
+        // We know that content header is present, let's start from that
+        final Matcher m = CONTENT_TYPE_PATTERN.matcher(str);
+        if  (m.find()) {
+            int payloadStart = 0;
+
+            // Look for empty line to find out where the actual payload starts (it should
+            // follow headers)
+            int buffer = 0;
+            for (int i = m.start(0); i < str.length(); ++i) {
+                final char c = str.charAt(i);
+                // a linefeed is a terminator, always.
+                if (c == '\n') {
+                    if (buffer == 0) {
+                        payloadStart = i;
+                        break;
+                    } else {
+                        buffer = 0;
+                    }
+                } else if (c == '\r') {
+                    //just ignore the CR.  The next character SHOULD be an NL.  If not, we're
+                    //just going to discard this
+                    continue;
+                } else {
+                    // just count is as a buffer
+                    ++buffer;
+                }
+            }
+
+            if (payloadStart > 0 && payloadStart < str.length()) {
+                return str.substring(0, payloadStart).trim();
+            }
+        }
+
+        return null;
+    }
+
     private String findContentType(String payload) {
         // Use regex to get the Content-Type and return null if it's not found
         Matcher m = CONTENT_TYPE_PATTERN.matcher(payload);
diff --git a/rt/features/logging/src/main/java/org/apache/cxf/ext/logging/event/DefaultLogEventMapper.java b/rt/features/logging/src/main/java/org/apache/cxf/ext/logging/event/DefaultLogEventMapper.java
index 4e8ee74..ee3a386 100644
--- a/rt/features/logging/src/main/java/org/apache/cxf/ext/logging/event/DefaultLogEventMapper.java
+++ b/rt/features/logging/src/main/java/org/apache/cxf/ext/logging/event/DefaultLogEventMapper.java
@@ -234,11 +234,22 @@
 
     private boolean isBinaryContent(Message message) {
         String contentType = safeGet(message, Message.CONTENT_TYPE);
-        return contentType != null && binaryContentMediaTypes.contains(contentType);
+        return isBinaryContent(contentType);
     }
 
     public boolean isBinaryContent(String contentType) {
-        return contentType != null && binaryContentMediaTypes.contains(contentType);
+        if (contentType == null) {
+            return false;
+        } else {
+            // Consider compound header values, like: 
+            //    Content-Type: application/xop+xml; charset=UTF-8; type="text/xml"
+            final int index = contentType.indexOf(';');
+            if (index > 0) {
+                return binaryContentMediaTypes.contains(contentType.substring(0, index).trim());
+            } else {
+                return binaryContentMediaTypes.contains(contentType);
+            }
+        }
     }
     
     private boolean isMultipartContent(Message message) {
diff --git a/rt/features/logging/src/test/java/org/apache/cxf/ext/logging/LoggingInInterceptorTest.java b/rt/features/logging/src/test/java/org/apache/cxf/ext/logging/LoggingInInterceptorTest.java
index fda5b7b..d7b7578 100644
--- a/rt/features/logging/src/test/java/org/apache/cxf/ext/logging/LoggingInInterceptorTest.java
+++ b/rt/features/logging/src/test/java/org/apache/cxf/ext/logging/LoggingInInterceptorTest.java
@@ -19,6 +19,9 @@
 
 package org.apache.cxf.ext.logging;
 
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashMap;
@@ -27,6 +30,7 @@
 import java.util.Set;
 
 import org.apache.cxf.ext.logging.event.LogEvent;
+import org.apache.cxf.io.CachedOutputStream;
 import org.apache.cxf.message.ExchangeImpl;
 import org.apache.cxf.message.Message;
 import org.apache.cxf.message.MessageImpl;
@@ -36,6 +40,7 @@
 
 import static org.apache.cxf.ext.logging.event.DefaultLogEventMapper.MASKED_HEADER_VALUE;
 import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.equalToIgnoringCase;
 import static org.hamcrest.Matchers.hasSize;
 import static org.junit.Assert.assertEquals;
 
@@ -114,4 +119,119 @@
 
         assertEquals(TEST_HEADER_VALUE, event.getHeaders().get(TEST_HEADER_NAME));
     }
+
+    @Test
+    public void shouldLogMultipartPayload() throws IOException {
+        message.put(Message.ENDPOINT_ADDRESS, "http://localhost:9001/");
+        message.put(Message.REQUEST_URI, "/api");
+
+        StringBuilder buf = new StringBuilder(512);
+        buf.append("------=_Part_0_2180223.1203118300920\n");
+        buf.append("Content-Type: application/xop+xml; charset=UTF-8; type=\"text/xml\"\n");
+        buf.append("Content-Transfer-Encoding: 8bit\n");
+        buf.append("Content-ID: <soap.xml@xfire.codehaus.org>\n");
+        buf.append('\n');
+        buf.append("<soap:Envelope xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\" "
+                   + "xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" "
+                   + "xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\">"
+                   + "<soap:Body><getNextMessage xmlns=\"http://foo.bar\" /></soap:Body>"
+                   + "</soap:Envelope>\n");
+        buf.append("------=_Part_0_2180223.1203118300920--\n");
+
+        String ct = "multipart/related; type=\"application/xop+xml\"; "
+                + "boundary=\"----=_Part_0_2180223.1203118300920\"";
+
+        final byte[] bytes = buf.toString().getBytes(StandardCharsets.UTF_8);
+        final OutputStream os = new CachedOutputStream();
+        os.write(bytes, 0, bytes.length);
+        message.setContent(CachedOutputStream.class, os);
+        message.put(Message.CONTENT_TYPE, ct);
+
+        interceptor.addBinaryContentMediaTypes("application/xop+xml");
+        interceptor.setLogMultipart(true);
+        interceptor.setLogBinary(true);
+        interceptor.handleMessage(message);
+        
+        assertThat(sender.getEvents(), hasSize(1));
+        final LogEvent event = sender.getEvents().get(0);
+
+        assertThat(event.getPayload(), equalToIgnoringCase(buf.toString()));
+    }
+
+    @Test
+    public void shouldLogMultipartHeadersOnly() throws IOException {
+        message.put(Message.ENDPOINT_ADDRESS, "http://localhost:9001/");
+        message.put(Message.REQUEST_URI, "/api");
+
+        final StringBuilder headers = new StringBuilder(512);
+        headers.append("------=_Part_0_2180223.1203118300920\n");
+        headers.append("Content-Type: application/xop+xml; charset=UTF-8; type=\"text/xml\"\n");
+        headers.append("Content-Transfer-Encoding: 8bit\n");
+        headers.append("Content-ID: <soap.xml@xfire.codehaus.org>\n");
+
+        final StringBuilder buf = new StringBuilder(512);
+        buf.append(headers);
+        buf.append('\n');
+        buf.append("<soap:Envelope xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\" "
+                   + "xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" "
+                   + "xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\">"
+                   + "<soap:Body><getNextMessage xmlns=\"http://foo.bar\" /></soap:Body>"
+                   + "</soap:Envelope>\n");
+        buf.append("------=_Part_0_2180223.1203118300920--\n");
+
+        String ct = "multipart/related; type=\"application/xop+xml\"; "
+                + "boundary=\"----=_Part_0_2180223.1203118300920\"";
+
+        final byte[] bytes = buf.toString().getBytes(StandardCharsets.UTF_8);
+        final OutputStream os = new CachedOutputStream();
+        os.write(bytes, 0, bytes.length);
+        message.setContent(CachedOutputStream.class, os);
+        message.put(Message.CONTENT_TYPE, ct);
+
+        interceptor.addBinaryContentMediaTypes("application/xop+xml");
+        interceptor.setLogMultipart(true);
+        interceptor.setLogBinary(false);
+        interceptor.handleMessage(message);
+        
+        assertThat(sender.getEvents(), hasSize(1));
+        final LogEvent event = sender.getEvents().get(0);
+
+        assertThat(event.getPayload().replaceAll("\r\n", "\n"), equalToIgnoringCase(headers.toString()
+            + "--- Content suppressed ---\n--\n------=_Part_0_2180223.1203118300920"));
+    }
+
+    @Test
+    public void shouldLogMultipartPayloadNoHeaders() throws IOException {
+        message.put(Message.ENDPOINT_ADDRESS, "http://localhost:9001/");
+        message.put(Message.REQUEST_URI, "/api");
+
+        StringBuilder buf = new StringBuilder(512);
+        buf.append("------=_Part_0_2180223.1203118300920\n");
+        buf.append('\n');
+        buf.append("<soap:Envelope xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\" "
+                   + "xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" "
+                   + "xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\">"
+                   + "<soap:Body><getNextMessage xmlns=\"http://foo.bar\" /></soap:Body>"
+                   + "</soap:Envelope>\n");
+        buf.append("------=_Part_0_2180223.1203118300920--\n");
+
+        String ct = "multipart/related; type=\"application/xop+xml\"; "
+                + "boundary=\"----=_Part_0_2180223.1203118300920\"";
+
+        final byte[] bytes = buf.toString().getBytes(StandardCharsets.UTF_8);
+        final OutputStream os = new CachedOutputStream();
+        os.write(bytes, 0, bytes.length);
+        message.setContent(CachedOutputStream.class, os);
+        message.put(Message.CONTENT_TYPE, ct);
+
+        interceptor.addBinaryContentMediaTypes("application/xop+xml");
+        interceptor.setLogMultipart(true);
+        interceptor.setLogBinary(true);
+        interceptor.handleMessage(message);
+        
+        assertThat(sender.getEvents(), hasSize(1));
+        final LogEvent event = sender.getEvents().get(0);
+
+        assertThat(event.getPayload(), equalToIgnoringCase(buf.toString()));
+    }
 }