/*
 * 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.
 */
package org.apache.axiom.attachments;

import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.Random;
import java.util.Set;

import jakarta.activation.DataHandler;
import jakarta.activation.DataSource;
import jakarta.mail.Session;
import jakarta.mail.internet.MimeBodyPart;
import jakarta.mail.internet.MimeMessage;
import jakarta.mail.internet.MimeMultipart;
import jakarta.mail.util.ByteArrayDataSource;

import org.apache.axiom.attachments.lifecycle.DataHandlerExt;
import org.apache.axiom.blob.Blob;
import org.apache.axiom.blob.Blobs;
import org.apache.axiom.blob.MemoryBlob;
import org.apache.axiom.ext.activation.SizeAwareDataSource;
import org.apache.axiom.mime.ContentType;
import org.apache.axiom.mime.MIMEException;
import org.apache.axiom.om.AbstractTestCase;
import org.apache.axiom.om.OMException;
import org.apache.axiom.om.impl.MTOMConstants;
import org.apache.axiom.testutils.blob.RandomBlob;
import org.apache.axiom.testutils.io.ExceptionInputStream;
import org.apache.axiom.testutils.io.IOTestUtils;
import org.apache.axiom.ts.soap.MTOMSample;
import org.apache.axiom.ts.soap.SwASample;
import org.apache.axiom.ts.xml.MIMESample;
import org.apache.axiom.util.UIDGenerator;
import org.apache.axiom.util.activation.DataHandlerUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.io.output.NullOutputStream;

public class AttachmentsTest extends AbstractTestCase {
    private static String getAttachmentsDir() {
        File attachmentsDir = new File(System.getProperty("basedir", ".") + "/target/attachments");
        attachmentsDir.mkdirs();
        return attachmentsDir.getAbsolutePath();
    }
    
    public void testGetDataHandler() throws Exception {
        InputStream inStream = MTOMSample.SAMPLE1.getInputStream();
        Attachments attachments = new Attachments(inStream, MTOMSample.SAMPLE1.getContentType());

        DataHandler dh = attachments
                .getDataHandler("2.urn:uuid:A3ADBAEE51A1A87B2A11443668160994@apache.org");
        InputStream dataIs = dh.getDataSource().getInputStream();

        InputStream expectedDataIs = MTOMSample.SAMPLE1.getPart(2);

        // Compare data across streams
        IOTestUtils.compareStreams(dataIs, expectedDataIs);
    }

    public void testGetDataHandlerNonExistingMIMEPart() throws Exception {
        InputStream inStream = MTOMSample.SAMPLE1.getInputStream();
        Attachments attachments = new Attachments(inStream, MTOMSample.SAMPLE1.getContentType());

        DataHandler dh = attachments.getDataHandler("ThisShouldReturnNull");
        assertNull(dh);
    }

    public void testGetDataHandlerNonExistingMIMEPartWithoutStream() throws Exception {
        Attachments attachments = new Attachments();
        attachments.addDataHandler("id@apache.org", new DataHandler("test", "text/plain"));
        assertNull(attachments.getDataHandler("non-existing@apache.org"));
    }

    public void testGetAllContentIDs() throws Exception {
        InputStream inStream = MTOMSample.SAMPLE1.getInputStream();
        Attachments attachments = new Attachments(inStream, MTOMSample.SAMPLE1.getContentType());

        String[] contentIDs = attachments.getAllContentIDs();
        assertEquals(3, contentIDs.length);
        assertEquals("0.urn:uuid:A3ADBAEE51A1A87B2A11443668160702@apache.org", contentIDs[0]);
        assertEquals("1.urn:uuid:A3ADBAEE51A1A87B2A11443668160943@apache.org", contentIDs[1]);
        assertEquals("2.urn:uuid:A3ADBAEE51A1A87B2A11443668160994@apache.org", contentIDs[2]);
    }
    
    public void testGetAllContentIDsWithoutStream() {
        Attachments attachments = new Attachments();
        // The choice of content IDs here makes sure that we test that the attachments are returned
        // in the order in which they have been added (instead of sorted by content ID as in
        // earlier Axiom versions)
        attachments.addDataHandler("contentB@apache.org", new DataHandler("content1", "text/plain"));
        attachments.addDataHandler("contentA@apache.org", new DataHandler("content2", "text/plain"));
        
        String[] contentIDs = attachments.getAllContentIDs();
        assertEquals(2, contentIDs.length);
        assertEquals("contentB@apache.org", contentIDs[0]);
        assertEquals("contentA@apache.org", contentIDs[1]);
    }
    
    public void testGetContentIDSet() {
        InputStream inStream = MTOMSample.SAMPLE1.getInputStream();
        Attachments attachments = new Attachments(inStream, MTOMSample.SAMPLE1.getContentType());

        Set<String> idSet = attachments.getContentIDSet();
        assertTrue(idSet.contains("0.urn:uuid:A3ADBAEE51A1A87B2A11443668160702@apache.org"));
        assertTrue(idSet.contains("2.urn:uuid:A3ADBAEE51A1A87B2A11443668160994@apache.org"));
        assertTrue(idSet.contains("1.urn:uuid:A3ADBAEE51A1A87B2A11443668160943@apache.org"));
    }
    
    public void testGetContentIDSetWithoutStream() {
        Attachments attachments = new Attachments();
        attachments.addDataHandler("id1@apache.org", new DataHandler("content1", "text/plain"));
        attachments.addDataHandler("id2@apache.org", new DataHandler("content2", "text/plain"));
        attachments.addDataHandler("id3@apache.org", new DataHandler("content3", "text/plain"));
        
        Set<String> idSet = attachments.getContentIDSet();
        assertTrue(idSet.contains("id1@apache.org"));
        assertTrue(idSet.contains("id2@apache.org"));
        assertTrue(idSet.contains("id3@apache.org"));
    }
    
    public void testGetContentLength() throws IOException {
        InputStream inStream = MTOMSample.SAMPLE1.getInputStream();
        Attachments attachments = new Attachments(inStream, MTOMSample.SAMPLE1.getContentType());
        
        // Make sure the length is correct
        long length = attachments.getContentLength();
        long fileSize = IOUtils.toByteArray(MTOMSample.SAMPLE1.getInputStream()).length;
        assertEquals("Return value of getContentLength()", fileSize, length);
    }
    
    /**
     * Tests that {@link Attachments#getContentLength()} returns <code>-1</code> if the object is
     * not stream backed.
     * 
     * @throws IOException
     */
    public void testGetContentLengthWithoutStream() throws IOException {
        Attachments attachments = new Attachments();
        attachments.addDataHandler(UIDGenerator.generateContentId(), new DataHandler("test", "text/plain"));
        assertEquals(-1, attachments.getContentLength());
    }

    private void testGetRootPartContentID(String contentTypeStartParam, String contentId)
            throws Exception {
        MimeMessage message = new MimeMessage((Session)null);
        MimeMultipart mp = new MimeMultipart("related");
        MimeBodyPart rootPart = new MimeBodyPart();
        rootPart.setDataHandler(new DataHandler(new TextDataSource("<root/>", "utf-8", "xml")));
        rootPart.addHeader("Content-Transfer-Encoding", "binary");
        rootPart.addHeader("Content-ID", "<" + contentId + ">");
        mp.addBodyPart(rootPart);
        message.setContent(mp);
        message.saveChanges();
        MemoryBlob blob = Blobs.createMemoryBlob();
        OutputStream out = blob.getOutputStream();
        mp.writeTo(out);
        out.close();

        Attachments attachments = new Attachments(
                blob.getInputStream(),
                new ContentType(message.getContentType())
                        .toBuilder()
                        .setParameter("start", contentTypeStartParam)
                        .build()
                        .toString());
        assertEquals("Did not obtain correct content ID", contentId,
                attachments.getRootPartContentID());
    }
    
    public void testGetRootPartContentIDWithoutBrackets() throws Exception {
        testGetRootPartContentID("my-content-id@localhost", "my-content-id@localhost");
    }
    
    public void testGetRootPartContentIDWithBrackets() throws Exception {
        testGetRootPartContentID("<my-content-id@localhost>", "my-content-id@localhost");
    }
    
    // Not sure when exactly somebody uses the "cid:" prefix in the start parameter, but
    // this is how the code currently works.
    public void testGetRootPartContentIDWithCidPrefix() throws Exception {
        testGetRootPartContentID("cid:my-content-id@localhost", "my-content-id@localhost");
    }
    
    // Regression test for AXIOM-195
    public void testGetRootPartContentIDWithCidPrefix2() throws Exception {
        testGetRootPartContentID("<cid-73920@192.168.0.1>", "cid-73920@192.168.0.1");
    }
    
    public void testGetRootPartContentIDShort() throws Exception {
        testGetRootPartContentID("bbb", "bbb");
    }
    
    public void testGetRootPartContentIDShortWithBrackets() throws Exception {
        testGetRootPartContentID("<b>", "b");
    }
    
    public void testGetRootPartContentIDBorderline() throws Exception {
        testGetRootPartContentID("cid:", "cid:");
    }
    
    /**
     * Tests that {@link Attachments#getRootPartContentType()} throws a meaningful exception if it
     * is unable to determine the content type.
     */
    public void testGetRootPartContentTypeWithContentIDMismatch() {
        String contentType = "multipart/related; boundary=\"" + MTOMSample.SAMPLE1.getBoundary() +
                "\"; type=\"text/xml\"; start=\"<wrong-content-id@example.org>\"";
        assertThrows(MIMEException.class, () -> {
            Attachments attachments = new Attachments(MTOMSample.SAMPLE1.getInputStream(), contentType);
            attachments.getRootPartContentType();
        });
    }
    
    public void testGetIncomingAttachmentStreams() throws Exception {
        InputStream inStream = MTOMSample.SAMPLE1.getInputStream();
        Attachments attachments = new Attachments(inStream, MTOMSample.SAMPLE1.getContentType());

        // Get the inputstream container
        IncomingAttachmentStreams ias = attachments.getIncomingAttachmentStreams();

        IncomingAttachmentInputStream dataIs;

        // Img1 stream
        dataIs = ias.getNextStream();

        // Make sure it was the first attachment
        assertEquals("<1.urn:uuid:A3ADBAEE51A1A87B2A11443668160943@apache.org>",
                     dataIs.getContentId());

        // Consume the stream
        while (dataIs.read() != -1) ;

        // Img2 stream
        dataIs = ias.getNextStream();
        assertEquals("<2.urn:uuid:A3ADBAEE51A1A87B2A11443668160994@apache.org>",
                     dataIs.getContentId());

        // Test if getContentType() works..
        assertEquals("image/jpeg", dataIs.getContentType());

        // Test if a adding/getting a header works
        dataIs.addHeader("new-header", "test-value");
        assertEquals("test-value", dataIs.getHeader("new-header"));
    }
    
    public void testGetIncomingAttachmentStreams2() throws Exception {
        MTOMSample sample = MTOMSample.SAMPLE1;

        IncomingAttachmentInputStream dataIs;
        InputStream expectedDataIs;

        InputStream inStream = sample.getInputStream();
        Attachments attachments = new Attachments(inStream, sample.getContentType());

        // Since SOAP part operated independently of other streams, access it
        // directly, and then get to the streams. If this sequence throws an
        // error, something is wrong with the stream handling code.
        InputStream is = attachments.getRootPartInputStream();
        while (is.read() != -1) ;

        // Get the inputstream container
        IncomingAttachmentStreams ias = attachments.getIncomingAttachmentStreams();

        dataIs = ias.getNextStream();
        expectedDataIs = sample.getPart(1);
        IOTestUtils.compareStreams(dataIs, expectedDataIs);

        dataIs = ias.getNextStream();
        expectedDataIs = sample.getPart(2);
        IOTestUtils.compareStreams(dataIs, expectedDataIs);

        // Confirm that no more streams are left
        assertNull(ias.getNextStream());
        
        // After all is done, we should *still* be able to access and
        // re-consume the SOAP part stream, as it should be cached.. can we?
        is = attachments.getRootPartInputStream();
        while (is.read() != -1) ;  
    }
    
    public void testSimultaneousStreamAccess1() throws Exception {
        InputStream inStream = MTOMSample.SAMPLE1.getInputStream();
        Attachments attachments = new Attachments(inStream, MTOMSample.SAMPLE1.getContentType());
    
        attachments.getDataHandler("2.urn:uuid:A3ADBAEE51A1A87B2A11443668160994@apache.org");
    
        // This should throw an error
        assertThrows(IllegalStateException.class, attachments::getIncomingAttachmentStreams);
    
        inStream.close();
    }

    public void testSimultaneousStreamAccess2() throws Exception {
        // Try the other way around.
        InputStream inStream = MTOMSample.SAMPLE1.getInputStream();
        Attachments attachments = new Attachments(inStream, MTOMSample.SAMPLE1.getContentType());
    
        attachments.getIncomingAttachmentStreams();
    
        // These should NOT throw error even though they are using part based access
        String contentType = attachments.getRootPartContentType();
        assertTrue(contentType.indexOf("application/xop+xml;") >=0);
        assertTrue(contentType.indexOf("charset=UTF-8;") >=0);
        assertTrue(contentType.indexOf("type=\"application/soap+xml\";") >=0);
    
        attachments.getRootPartInputStream();
    
        // These should throw an error
        assertThrows(IllegalStateException.class, () ->
            attachments.getDataHandler("2.urn:uuid:A3ADBAEE51A1A87B2A11443668160994@apache.org"));
    
        // Additionally, we also need to ensure mutual exclusion if someone
        // tries to access part data directly
    
        assertThrows(IllegalStateException.class, attachments::getAllContentIDs);
    
        assertThrows(IllegalStateException.class, () ->
            attachments.getDataHandler("2.urn:uuid:A3ADBAEE51A1A87B2A11443668160994@apache.org"));
    }

    public void testRemoveDataHandlerAfterParsing() {
        InputStream inStream = MTOMSample.SAMPLE1.getInputStream();
        Attachments attachments = new Attachments(inStream, MTOMSample.SAMPLE1.getContentType());

        Set<String> list = attachments.getContentIDSet();
        assertEquals(3, list.size());
        
        assertTrue(list.contains("1.urn:uuid:A3ADBAEE51A1A87B2A11443668160943@apache.org"));
        assertTrue(list.contains("2.urn:uuid:A3ADBAEE51A1A87B2A11443668160994@apache.org"));
        
        attachments.removeDataHandler("1.urn:uuid:A3ADBAEE51A1A87B2A11443668160943@apache.org");

        List<String> list2 = attachments.getContentIDList();
        assertEquals(2, list2.size());
        assertEquals(2, attachments.getMap().size());

        assertFalse(list2.contains("1.urn:uuid:A3ADBAEE51A1A87B2A11443668160943@apache.org"));
        assertTrue(list2.contains("2.urn:uuid:A3ADBAEE51A1A87B2A11443668160994@apache.org"));
    }

    /**
     * Tests the behavior of {@link Attachments#removeDataHandler(String)} for a MIME part that has
     * not yet been processed.
     */
    public void testRemoveDataHandlerBeforeParsing() {
        InputStream inStream = MTOMSample.SAMPLE1.getInputStream();
        Attachments attachments = new Attachments(inStream, MTOMSample.SAMPLE1.getContentType());
        attachments.removeDataHandler("1.urn:uuid:A3ADBAEE51A1A87B2A11443668160943@apache.org");
        Set<String> idSet = attachments.getContentIDSet();
        assertEquals(2, idSet.size());
        assertTrue(idSet.contains("0.urn:uuid:A3ADBAEE51A1A87B2A11443668160702@apache.org"));
        assertFalse(idSet.contains("1.urn:uuid:A3ADBAEE51A1A87B2A11443668160943@apache.org"));
        assertTrue(idSet.contains("2.urn:uuid:A3ADBAEE51A1A87B2A11443668160994@apache.org"));
    }
    
    /**
     * Tests that {@link Attachments#removeDataHandler(String)} returns silently if the message
     * doesn't contain a MIME part with the specified content ID.
     */
    public void testRemoveDataHandlerNonExistingWithStream() {
        InputStream inStream = MTOMSample.SAMPLE1.getInputStream();
        Attachments attachments = new Attachments(inStream, MTOMSample.SAMPLE1.getContentType());
        attachments.removeDataHandler("non-existing@apache.org");
        assertEquals(3, attachments.getContentIDSet().size());
    }

    /**
     * Tests that {@link Attachments#removeDataHandler(String)} returns silently if no data handler
     * with the given content ID has been added to the object.
     */
    public void testRemoveDataHandlerNonExistingWithoutStream() {
        Attachments attachments = new Attachments();
        attachments.addDataHandler("id@apache.org", new DataHandler("test", "text/plain"));
        attachments.removeDataHandler("non-existing@apache.org");
        assertEquals(1, attachments.getContentIDSet().size());
    }

    private void testReadBase64EncodedAttachment(boolean useFile) throws Exception {
        // Note: We are only interested in the MimeMultipart, but we need to create a
        //       MimeMessage to be able to calculate the correct content type
        MimeMessage message = new MimeMessage((Session)null);
        MimeMultipart mp = new MimeMultipart("related");
        
        // Prepare the "SOAP" part
        MimeBodyPart bp1 = new MimeBodyPart();
        // Obviously this is not SOAP, but this is irrelevant for this test
        bp1.setDataHandler(new DataHandler(new TextDataSource("<root/>", "utf-8", "xml")));
        bp1.addHeader("Content-Transfer-Encoding", "binary");
        bp1.addHeader("Content-ID", "part1@apache.org");
        mp.addBodyPart(bp1);
        
        // Prepare the attachment
        MimeBodyPart bp2 = new MimeBodyPart();
        byte[] content = new byte[8192];
        new Random().nextBytes(content);
        bp2.setDataHandler(new DataHandler(new ByteArrayDataSource(content, "application/octet-stream")));
        bp2.addHeader("Content-Transfer-Encoding", "base64");
        bp2.addHeader("Content-ID", "part2@apache.org");
        mp.addBodyPart(bp2);
        
        message.setContent(mp);
        // Compute the correct content type
        message.saveChanges();
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        mp.writeTo(baos);
        String contentType = message.getContentType();
        
        InputStream in = new ByteArrayInputStream(baos.toByteArray());
        Attachments attachments;
        if (useFile) {
            attachments = new Attachments(in, contentType, true, getAttachmentsDir(), "1024");
        } else {
            attachments = new Attachments(in, contentType);
        }
        DataHandler dh = attachments.getDataHandler("part2@apache.org");
        byte[] content2 = IOUtils.toByteArray(dh.getInputStream());
        assertTrue(Arrays.equals(content, content2));
    }

    public void testReadBase64EncodedAttachmentWithPartOnMemory() throws Exception {
        testReadBase64EncodedAttachment(false);
    }

    public void testReadBase64EncodedAttachmentWithPartOnFile() throws Exception {
        testReadBase64EncodedAttachment(true);
    }

    /**
     * Tests that {@link Attachments} can successfully read an attachment with zero length. This is
     * a regression test for
     * <a href="https://issues.apache.org/jira/browse/AXIOM-383">AXIOM-383</a>.
     * 
     * @throws Exception
     */
    public void testZeroLengthAttachment() throws Exception {
        InputStream in = getTestResource("mtom/zero-length-attachment.bin");
        try {
            Attachments attachments = new Attachments(in,
                    "multipart/related; " +
                    "boundary=MIMEBoundaryurn_uuid_0549F3F826EC3041861188639371825; " +
                    "type=\"application/xop+xml\"; " +
                    "start=\"0.urn:uuid:0549F3F826EC3041861188639371826@apache.org\"; " +
                    "start-info=\"application/soap+xml\"; action=\"urn:test\"");
            DataHandler dh = attachments.getDataHandler("1.urn:uuid:0549F3F826EC3041861188639371827@apache.org");
            InputStream content = dh.getInputStream();
            assertEquals(-1, content.read());
        } finally {
            in.close();
        }
    }
    
    public void testPurgeDataSource() throws Exception {
        InputStream in = getTestResource("mtom/msg-soap-wls81.txt");
        MyLifecycleManager manager = new MyLifecycleManager();
        Attachments attachments = new Attachments(manager, in,
                "multipart/related;type=\"text/xml\";boundary=\"----=_Part_0_3437046.1188904239130\";start=__WLS__1188904239161__SOAP__",
                true, getAttachmentsDir(), "1024");
        
        // Read the attachment once to make sure it is buffered
        DataHandler dh = attachments.getDataHandler("__WLS__1188904239162__SOAP__");
        assertTrue(dh instanceof DataHandlerExt);
        InputStream content = dh.getInputStream();
        IOUtils.copy(content, NullOutputStream.NULL_OUTPUT_STREAM);
        content.close();
        
        assertEquals(1, manager.getFileCount());
        ((DataHandlerExt)dh).purgeDataSource();
        assertEquals(0, manager.getFileCount());
        
        in.close();
    }
    
    /**
     * Tests that after consuming the input stream returned by {@link DataHandlerExt#readOnce()} for
     * an attachment that has been buffered on disk, the temporary file for that attachment is
     * deleted.
     * 
     * @throws Exception
     */
    public void testReadOnceOnBufferedPart() throws Exception {
        InputStream in = getTestResource("mtom/msg-soap-wls81.txt");
        MyLifecycleManager manager = new MyLifecycleManager();
        Attachments attachments = new Attachments(manager, in,
                "multipart/related;type=\"text/xml\";boundary=\"----=_Part_0_3437046.1188904239130\";start=__WLS__1188904239161__SOAP__",
                true, getAttachmentsDir(), "1024");
        
        // Read the attachment once to make sure it is buffered
        DataHandler dh = attachments.getDataHandler("__WLS__1188904239162__SOAP__");
        InputStream content = dh.getInputStream();
        IOUtils.copy(content, NullOutputStream.NULL_OUTPUT_STREAM);
        content.close();
        
        assertEquals(1, manager.getFileCount());

        // Now consume the content of the attachment
        content = ((DataHandlerExt)dh).readOnce();
        IOUtils.copy(content, NullOutputStream.NULL_OUTPUT_STREAM);
        content.close();
        
        // The temporary file should have been deleted
        assertEquals(0, manager.getFileCount());
        
        in.close();
    }
    
    /**
     * Tests that attachments are correctly buffered on file if the threshold is very low. This is a
     * regression test for <a href="https://issues.apache.org/jira/browse/AXIOM-61">AXIOM-61</a>.
     * 
     * @throws Exception
     */
    public void testFileBufferingWithLowThreshold() throws Exception {
        InputStream in = getTestResource("mtom/msg-soap-wls81.txt");
        Attachments attachments = new Attachments(in,
                "multipart/related;type=\"text/xml\";boundary=\"----=_Part_0_3437046.1188904239130\";start=__WLS__1188904239161__SOAP__",
                true, getAttachmentsDir(), "1");
        
        DataHandler dh = attachments.getDataHandler("__WLS__1188904239162__SOAP__");
        BufferedReader reader = new BufferedReader(new InputStreamReader(dh.getInputStream(), StandardCharsets.UTF_8));
        assertEquals("%PDF-1.3", reader.readLine());
        reader.close();
        
        in.close();
    }

    /**
     * Tests that a call to {@link DataHandlerExt#readOnce()} on a {@link DataHandler} returned by
     * the {@link Attachments} object streams the content of the MIME part.
     * 
     * @throws Exception
     */
    public void testDataHandlerStreaming() throws Exception {
        // Note: We are only interested in the MimeMultipart, but we need to create a
        //       MimeMessage to be able to calculate the correct content type
        MimeMessage message = new MimeMessage((Session)null);
        final MimeMultipart mp = new MimeMultipart("related");
        
        // Prepare the "SOAP" part
        MimeBodyPart bp1 = new MimeBodyPart();
        // Obviously this is not SOAP, but this is irrelevant for this test
        bp1.setDataHandler(new DataHandler(new TextDataSource("<root/>", "utf-8", "xml")));
        bp1.addHeader("Content-Transfer-Encoding", "binary");
        bp1.addHeader("Content-ID", "part1@apache.org");
        mp.addBodyPart(bp1);
        
        // Create an attachment that is larger than the maximum heap
        Blob blob = new RandomBlob((int)Math.min(Runtime.getRuntime().maxMemory(), Integer.MAX_VALUE));
        MimeBodyPart bp2 = new MimeBodyPart();
        bp2.setDataHandler(DataHandlerUtils.toDataHandler(blob));
        bp2.addHeader("Content-Transfer-Encoding", "binary");
        bp2.addHeader("Content-ID", "part2@apache.org");
        mp.addBodyPart(bp2);
        
        message.setContent(mp);
        // Compute the correct content type
        message.saveChanges();
        
        // We use a pipe (with a producer running in a separate thread) because obviously we can't
        // store the multipart in memory.
        PipedOutputStream pipeOut = new PipedOutputStream();
        PipedInputStream pipeIn = new PipedInputStream(pipeOut);
        
        Thread producerThread = new Thread(() -> {
            try {
                try {
                    mp.writeTo(pipeOut);
                } finally {
                    pipeOut.close();
                }
            } catch (Exception ex) {
                ex.printStackTrace();
            }
        });
        producerThread.start();
        
        try {
            // We configure Attachments to buffer MIME parts in memory. If the part content is not
            // streamed, then this will result in an OOM error.
            Attachments attachments = new Attachments(pipeIn, message.getContentType());
            DataHandlerExt dh = (DataHandlerExt)attachments.getDataHandler("part2@apache.org");
            IOTestUtils.compareStreams(blob.getInputStream(), dh.readOnce());
        } finally {
            pipeIn.close();
        }
    }

    private void testTurkishLocale(String contentIDHeaderName) throws Exception {
        Locale locale = Locale.getDefault();
        Locale.setDefault(new Locale("tr", "TR"));
        try {
            MimeMessage message = new MimeMessage((Session)null);
            MimeMultipart mp = new MimeMultipart("related");
            
            MimeBodyPart bp1 = new MimeBodyPart();
            bp1.setDataHandler(new DataHandler(new TextDataSource("<root/>", "utf-8", "xml")));
            bp1.addHeader("Content-Transfer-Encoding", "binary");
            mp.addBodyPart(bp1);
            
            MimeBodyPart bp2 = new MimeBodyPart();
            byte[] content = new byte[8192];
            new Random().nextBytes(content);
            bp2.setDataHandler(new DataHandler(new TextDataSource("Test", "utf-8", "plain")));
            bp2.addHeader("Content-Transfer-Encoding", "binary");
            bp2.addHeader(contentIDHeaderName, "part@apache.org");
            mp.addBodyPart(bp2);
            
            message.setContent(mp);
            message.saveChanges();
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            mp.writeTo(baos);
            String contentType = message.getContentType();
            
            InputStream in = new ByteArrayInputStream(baos.toByteArray());
            Attachments attachments = new Attachments(in, contentType);
            assertNotNull(attachments.getDataHandler("part@apache.org"));
        } finally {
            Locale.setDefault(locale);
        }
    }

    // Regression test for AXIOM-389
    public void testTurkishLocale1() throws Exception {
        testTurkishLocale("Content-ID");
    }

    public void testTurkishLocale2() throws Exception {
        testTurkishLocale("content-id");
    }
    
    private void testGetAttachmentSpecType(MIMESample resource, String expectedResult) throws Exception {
        InputStream in = resource.getInputStream();
        try {
            Attachments attachments = new Attachments(in, resource.getContentType());
            assertEquals(expectedResult, attachments.getAttachmentSpecType());
        } finally {
            in.close();
        }
    }
    
    public void testGetAttachmentSpecTypeMTOM() throws Exception {
        testGetAttachmentSpecType(MTOMSample.SAMPLE1, MTOMConstants.MTOM_TYPE);
    }
    
    public void testGetAttachmentSpecTypeSWA() throws Exception {
        testGetAttachmentSpecType(SwASample.SAMPLE1, MTOMConstants.SWA_TYPE);
    }
    
    public void testGetAttachmentSpecTypeWithoutStream() {
        assertThrows(OMException.class, () -> new Attachments().getAttachmentSpecType());
    }

    private void testGetSizeOnDataSource(boolean useFiles) throws Exception {
        InputStream in = MTOMSample.SAMPLE1.getInputStream();
        try {
            Attachments attachments;
            if (useFiles) {
                attachments = new Attachments(in, MTOMSample.SAMPLE1.getContentType(),
                        true, getAttachmentsDir(), "4096");
            } else {
                attachments = new Attachments(in, MTOMSample.SAMPLE1.getContentType());
            }
            DataHandler dh = attachments
                    .getDataHandler("2.urn:uuid:A3ADBAEE51A1A87B2A11443668160994@apache.org");
            DataSource ds = dh.getDataSource();
            assertTrue(ds instanceof SizeAwareDataSource);
            assertEquals(13887, ((SizeAwareDataSource)ds).getSize());
        } finally {
            in.close();
        }
    }
    
    public void testGetSizeOnDataSourceOnMemory() throws Exception {
        testGetSizeOnDataSource(false);
    }
    
    public void testGetSizeOnDataSourceOnFile() throws Exception {
        testGetSizeOnDataSource(true);
    }
    
    public void testIOExceptionInPartHeaders() throws Exception {
        InputStream in = MTOMSample.SAMPLE1.getInputStream();
        try {
            Attachments attachments = new Attachments(new ExceptionInputStream(in, 1050), MTOMSample.SAMPLE1.getContentType());
            // TODO: decide what exception should be thrown exactly here
            assertThrows(MIMEException.class, () -> attachments.getDataHandler("1.urn:uuid:A3ADBAEE51A1A87B2A11443668160943@apache.org"));
        } finally {
            in.close();
        }
    }
    
    public void testIOExceptionInPartContent() throws Exception {
        InputStream in = MTOMSample.SAMPLE1.getInputStream();
        try {
            Attachments attachments = new Attachments(new ExceptionInputStream(in, 1500), MTOMSample.SAMPLE1.getContentType());
            DataHandler dh = attachments.getDataHandler("1.urn:uuid:A3ADBAEE51A1A87B2A11443668160943@apache.org");
            // TODO: decide what exception should be thrown exactly here
            assertThrows(MIMEException.class, dh::getInputStream);
        } finally {
            in.close();
        }
    }

    /**
     * Regression test for <a href="https://issues.apache.org/jira/browse/AXIOM-467">AXIOM-467</a>.
     */
    public void testQuotedPrintable() throws Exception {
        Attachments attachments = new Attachments(
                MTOMSample.QUOTED_PRINTABLE.getInputStream(),
                MTOMSample.QUOTED_PRINTABLE.getContentType());
        DataHandler dh = attachments.getDataHandler("SDESS_COREP_00000_KO_SNG.xml");
        IOTestUtils.compareStreams(
                MTOMSample.QUOTED_PRINTABLE.getPart(1),
                dh.getInputStream());
    }

    /**
     * Tests access to a root part that doesn't have a content ID. In this case, the
     * {@link Attachments} API generates a fake content ID.
     * 
     * @throws Exception
     */
    public void testFakeRootPartContentID() throws Exception {
        MimeMessage message = new MimeMessage((Session)null);
        MimeMultipart mp = new MimeMultipart("related");

        DataHandler expectedRootPart = new DataHandler(new TextDataSource("<root/>", "utf-8", "xml"));
        MimeBodyPart bp1 = new MimeBodyPart();
        bp1.setDataHandler(expectedRootPart);
        bp1.addHeader("Content-Transfer-Encoding", "binary");
        mp.addBodyPart(bp1);
        
        MimeBodyPart bp2 = new MimeBodyPart();
        bp2.setDataHandler(new DataHandler(new TextDataSource("Test", "utf-8", "plain")));
        bp2.addHeader("Content-Transfer-Encoding", "binary");
        mp.addBodyPart(bp2);
        
        message.setContent(mp);
        message.saveChanges();
        
        MemoryBlob blob = Blobs.createMemoryBlob();
        OutputStream out = blob.getOutputStream();
        mp.writeTo(out);
        out.close();
        String contentType = message.getContentType();
        
        Attachments attachments = new Attachments(blob.getInputStream(), contentType);
        String rootPartContentID = attachments.getRootPartContentID();
        assertThat(rootPartContentID).isNotNull();
        DataHandler rootPart = attachments.getDataHandler(rootPartContentID);
        IOTestUtils.compareStreams(rootPart.getInputStream(), expectedRootPart.getInputStream());
    }
}
