/*
 * Copyright (C) 2005 Alfresco, Inc.
 *
 * Licensed under the Mozilla Public License version 1.1 
 * with a permitted attribution clause. You may obtain a
 * copy of the License at
 *
 *   http://www.alfresco.org/legal/license.txt
 *
 * 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.alfresco.repo.content;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.ReadableByteChannel;
import java.util.Date;
import java.util.Set;

import javax.transaction.UserTransaction;

import junit.framework.TestCase;

import org.alfresco.repo.transaction.DummyTransactionService;
import org.alfresco.service.cmr.repository.ContentIOException;
import org.alfresco.service.cmr.repository.ContentReader;
import org.alfresco.service.cmr.repository.ContentStreamListener;
import org.alfresco.service.cmr.repository.ContentWriter;
import org.alfresco.service.transaction.TransactionService;
import org.alfresco.util.ApplicationContextHelper;
import org.springframework.context.ApplicationContext;

/**
 * Abstract base class that provides a set of tests for implementations
 * of the content readers and writers.
 * 
 * @see org.alfresco.service.cmr.repository.ContentReader
 * @see org.alfresco.service.cmr.repository.ContentWriter
 * 
 * @author Derek Hulley
 */
public abstract class AbstractContentReadWriteTest extends TestCase
{
    private static final ApplicationContext ctx = ApplicationContextHelper.getApplicationContext();
    
    protected TransactionService transactionService;
    private String contentUrl;
    private UserTransaction txn;
    
    public AbstractContentReadWriteTest()
    {
        super();
    }

    @Override
    public void setUp() throws Exception
    {
        contentUrl = AbstractContentStore.createNewUrl();
        transactionService = (TransactionService) ctx.getBean("TransactionService");
        txn = transactionService.getUserTransaction();
        txn.begin();
    }
    
    public void tearDown() throws Exception
    {
        txn.rollback();
    }
    
    /**
     * Fetch the store to be used during a test.  This method is invoked once per test - it is
     * therefore safe to use <code>setUp</code> to initialise resources.
     * <p>
     * Usually tests will construct a static instance of the store to use throughout all the
     * tests.
     * 
     * @return Returns the <b>same instance</b> of a store for all invocations.
     */
    protected abstract ContentStore getStore();
    
    /**
     * @see #getStore()
     */
    protected final ContentWriter getWriter()
    {
        return getStore().getWriter(null, contentUrl);
    }
    
    /**
     * @see #getStore()
     */
    protected final ContentReader getReader()
    {
        return getStore().getReader(contentUrl);
    }
    
    public void testSetUp() throws Exception
    {
        assertNotNull("setUp() not executed: no content URL present");
        
        // check that the store remains the same
        ContentStore store = getStore();
        assertNotNull("No store provided", store);
        assertTrue("The same instance of the store must be returned for getStore", store == getStore());
    }
    
    public void testContentUrl() throws Exception
    {
        ContentReader reader = getReader();
        ContentWriter writer = getWriter();
        
        // the contract is that both the reader and writer must refer to the same
        // content -> the URL must be the same
        String readerContentUrl = reader.getContentUrl();
        String writerContentUrl = writer.getContentUrl();
        assertNotNull("Reader url is invalid", readerContentUrl);
        assertNotNull("Writer url is invalid", writerContentUrl);
        assertEquals("Reader and writer must reference same content",
                readerContentUrl,
                writerContentUrl);
        
        // check that the content URL is correct
        assertTrue("Content URL doesn't start with correct prefix",
                readerContentUrl.startsWith(ContentStore.STORE_PROTOCOL));
    }
    
    public void testMimetypeAndEncoding() throws Exception
    {
        ContentWriter writer = getWriter();
        // set mimetype and encoding
        writer.setMimetype("text/plain");
        writer.setEncoding("UTF-16");
        
        // create a UTF-16 string
        String content = "A little bit o' this and a little bit o' that";
        byte[] bytesUtf16 = content.getBytes("UTF-16");
        // write the bytes directly to the writer
        OutputStream os = writer.getContentOutputStream();
        os.write(bytesUtf16);
        os.close();
        
        // now get a reader from the writer
        ContentReader reader = writer.getReader();
        assertEquals("Writer -> Reader content URL mismatch", writer.getContentUrl(), reader.getContentUrl());
        assertEquals("Writer -> Reader mimetype mismatch", writer.getMimetype(), reader.getMimetype());
        assertEquals("Writer -> Reader encoding mismatch", writer.getEncoding(), reader.getEncoding());
        
        // now get the string directly from the reader
        String contentCheck = reader.getContentString();     // internally it should have taken care of the encoding
        assertEquals("Encoding and decoding of strings failed", content, contentCheck);
    }
    
    public void testExists() throws Exception
    {
        ContentStore store = getStore();
        
        // make up a URL
        String contentUrl = AbstractContentStore.createNewUrl();
        
        // it should not exist in the store
        assertFalse("Store exists fails with new URL", store.exists(contentUrl));
        
        // get a reader
        ContentReader reader = store.getReader(contentUrl);
        assertNotNull("Reader must be present, even for missing content", reader);
        assertFalse("Reader exists failure", reader.exists());
        
        // write something
        ContentWriter writer = store.getWriter(null, contentUrl);
        writer.putContent("ABC");
        
        assertTrue("Store exists should show URL to be present", store.exists(contentUrl));
    }
    
    public void testGetReader() throws Exception
    {
        ContentWriter writer = getWriter();
        
        // check that no reader is available from the writer just yet
        ContentReader nullReader = writer.getReader();
        assertNull("No reader expected", nullReader);
        
        String content = "ABC";
        // write some content
//        long before = System.currentTimeMillis();
        writer.setMimetype("text/plain");
        writer.setEncoding("UTF-8");
        writer.putContent(content);
//        long after = System.currentTimeMillis();
        
        // get a reader from the writer
        ContentReader readerFromWriter = writer.getReader();
        assertEquals("URL incorrect", writer.getContentUrl(), readerFromWriter.getContentUrl());
        assertEquals("Mimetype incorrect", writer.getMimetype(), readerFromWriter.getMimetype());
        assertEquals("Encoding incorrect", writer.getEncoding(), readerFromWriter.getEncoding());
        
        // get another reader from the reader
        ContentReader readerFromReader = readerFromWriter.getReader();
        assertEquals("URL incorrect", writer.getContentUrl(), readerFromReader.getContentUrl());
        assertEquals("Mimetype incorrect", writer.getMimetype(), readerFromReader.getMimetype());
        assertEquals("Encoding incorrect", writer.getEncoding(), readerFromReader.getEncoding());
        
        // check the content
        String contentCheck = readerFromWriter.getContentString();
        assertEquals("Content is incorrect", content, contentCheck);
        
        // check that the length is correct
        int length = content.getBytes(writer.getEncoding()).length;
        assertEquals("Reader content length is incorrect", length, readerFromWriter.getSize());

//
// This check has been disabled as Linux is out by some variable amount of time
//        // check that the last modified time is correct
//        long modifiedTimeCheck = readerFromWriter.getLastModified();
//        assertTrue("Reader last modified is incorrect", before <= modifiedTimeCheck);
//        assertTrue("Reader last modified is incorrect", modifiedTimeCheck <= after);
//
    }
    
    public void testClosedState() throws Exception
    {
        ContentReader reader = getReader();
        ContentWriter writer = getWriter();
        
        // check that streams are not flagged as closed
        assertFalse("Reader stream should not be closed", reader.isClosed());
        assertFalse("Writer stream should not be closed", writer.isClosed());
        
        // check that the write doesn't supply a reader
        ContentReader writerGivenReader = writer.getReader();
        assertNull("No reader should be available before a write has finished", writerGivenReader);
        
        // write some stuff
        writer.putContent("ABC");
        // check that the write has been closed
        assertTrue("Writer stream should be closed", writer.isClosed());
        
        // check that we can get a reader from the writer
        writerGivenReader = writer.getReader();
        assertNotNull("No reader given by closed writer", writerGivenReader);
        assertFalse("Readers should still be closed", reader.isClosed());
        assertFalse("Readers should still be closed", writerGivenReader.isClosed());
        
        // check that the instance is new each time
        ContentReader newReaderA = writer.getReader();
        ContentReader newReaderB = writer.getReader();
        assertFalse("Reader must always be a new instance", newReaderA == newReaderB);
        
        // check that the readers refer to the same URL
        assertEquals("Readers should refer to same URL",
                reader.getContentUrl(), writerGivenReader.getContentUrl());
        
        // read their content
        String contentCheck = reader.getContentString();
        assertEquals("Incorrect content", "ABC", contentCheck);
        contentCheck = writerGivenReader.getContentString();
        assertEquals("Incorrect content", "ABC", contentCheck);
        
        // check closed state of readers
        assertTrue("Reader should be closed", reader.isClosed());
        assertTrue("Reader should be closed", writerGivenReader.isClosed());
    }
    
    /**
     * Checks that the store disallows concurrent writers to be issued to the same URL.
     */
    public void testConcurrentWriteDetection() throws Exception
    {
        String contentUrl = AbstractContentStore.createNewUrl();
        ContentStore store = getStore();

        ContentWriter firstWriter = store.getWriter(null, contentUrl);
        try
        {
            ContentWriter secondWriter = store.getWriter(null, contentUrl);
            fail("Store issued two writers for the same URL: " + store);
        }
        catch (ContentIOException e)
        {
            // expected
        }
    }
    
    /**
     * Checks that the writer can have a listener attached
     */
    public void testWriteStreamListener() throws Exception
    {
        ContentWriter writer = getWriter();
        
        final boolean[] streamClosed = new boolean[] {false};  // has to be final
        ContentStreamListener listener = new ContentStreamListener()
        {
            public void contentStreamClosed() throws ContentIOException
            {
                streamClosed[0] = true;
            }
        };
        writer.setRetryingTransactionHelper(null);
        writer.addListener(listener);
        
        // write some content
        writer.putContent("ABC");
        
        // check that the listener was called
        assertTrue("Write stream listener was not called for the stream close", streamClosed[0]);
    }
    
    /**
     * The simplest test.  Write a string and read it again, checking that we receive the same values.
     * If the resource accessed by {@link #getReader()} and {@link #getWriter()} is not the same, then
     * values written and read won't be the same.
     */
    public void testWriteAndReadString() throws Exception
    {
        ContentReader reader = getReader();
        ContentWriter writer = getWriter();
        
        String content = "ABC";
        writer.putContent(content);
        assertTrue("Stream close not detected", writer.isClosed());

        String check = reader.getContentString();
        assertTrue("Read and write may not share same resource", check.length() > 0);
        assertEquals("Write and read didn't work", content, check);
    }
    
    public void testStringTruncation() throws Exception
    {
        String content = "1234567890";
        
        ContentWriter writer = getWriter();
        writer.setMimetype(MimetypeMap.MIMETYPE_TEXT_PLAIN);
        writer.setEncoding("UTF-8");  // shorter format i.t.o. bytes used
        // write the content
        writer.putContent(content);
        
        // get a reader - get it in a larger format i.t.o. bytes
        ContentReader reader = writer.getReader();
        String checkContent = reader.getContentString(5);
        assertEquals("Truncated strings don't match", "12345", checkContent);
    }
    
    public void testReadAndWriteFile() throws Exception
    {
        ContentReader reader = getReader();
        ContentWriter writer = getWriter();
        
        File sourceFile = File.createTempFile(getName(), ".txt");
        sourceFile.deleteOnExit();
        // dump some content into the temp file
        String content = "ABC";
        FileOutputStream os = new FileOutputStream(sourceFile);
        os.write(content.getBytes());
        os.flush();
        os.close();
        
        // put our temp file's content
        writer.putContent(sourceFile);
        assertTrue("Stream close not detected", writer.isClosed());
        
        // create a sink temp file
        File sinkFile = File.createTempFile(getName(), ".txt");
        sinkFile.deleteOnExit();
        
        // get the content into our temp file
        reader.getContent(sinkFile);
        
        // read the sink file manually
        FileInputStream is = new FileInputStream(sinkFile);
        byte[] buffer = new byte[100];
        int count = is.read(buffer);
        assertEquals("No content read", 3, count);
        is.close();
        String check = new String(buffer, 0, count);
        
        assertEquals("Write out of and read into files failed", content, check);
    }
    
    public void testReadAndWriteStreamByPull() throws Exception
    {
        ContentReader reader = getReader();
        ContentWriter writer = getWriter();

        String content = "ABC";
        // put the content using a stream
        InputStream is = new ByteArrayInputStream(content.getBytes());
        writer.putContent(is);
        assertTrue("Stream close not detected", writer.isClosed());
        
        // get the content using a stream
        ByteArrayOutputStream os = new ByteArrayOutputStream(100);
        reader.getContent(os);
        byte[] bytes = os.toByteArray();
        String check = new String(bytes);
        
        assertEquals("Write out and read in using streams failed", content, check);
    }
    
    public void testReadAndWriteStreamByPush() throws Exception
    {
        ContentReader reader = getReader();
        ContentWriter writer = getWriter();

        String content = "ABC";
        // get the content output stream
        OutputStream os = writer.getContentOutputStream();
        os.write(content.getBytes());
        assertFalse("Stream has not been closed", writer.isClosed());
        // close the stream and check again
        os.close();
        assertTrue("Stream close not detected", writer.isClosed());
        
        // pull the content from a stream
        InputStream is = reader.getContentInputStream();
        byte[] buffer = new byte[100];
        int count = is.read(buffer);
        assertEquals("No content read", 3, count);
        is.close();
        String check = new String(buffer, 0, count);
        
        assertEquals("Write out of and read into files failed", content, check);
    }
    
    /**
     * Tests deletion of content.
     * <p>
     * Only applies when {@link #getStore()} returns a value.
     */
    public void testDelete() throws Exception
    {
        ContentStore store = getStore();
        ContentWriter writer = getWriter();
        
        String content = "ABC";
        String contentUrl = writer.getContentUrl();

        // write some bytes, but don't close the stream
        OutputStream os = writer.getContentOutputStream();
        os.write(content.getBytes());
        os.flush();                  // make sure that the bytes get persisted
        
        // close the stream
        os.close();
        
        // get a reader
        ContentReader reader = store.getReader(contentUrl);
        assertNotNull(reader);
        
        ContentReader readerCheck = writer.getReader();
        assertNotNull(readerCheck);
        assertEquals("Store and write provided readers onto different URLs",
                writer.getContentUrl(), reader.getContentUrl());
        
        // open the stream onto the content
        InputStream is = reader.getContentInputStream();
        
        // attempt to delete the content
        boolean deleted = store.delete(contentUrl);

        // close the reader stream
        is.close();
        
        // get a fresh reader
        reader = store.getReader(contentUrl);
        assertNotNull(reader);
        
        // the underlying system may or may not have deleted the content
        if (deleted)
        {
            assertFalse("Content should not exist", reader.exists());
            // drop out here
            return;
        }
        else
        {
            assertTrue("Content should exist", reader.exists());
        }
        
        // delete the content
        store.delete(contentUrl);
        
        // attempt to read from the reader
        try
        {
            is = reader.getContentInputStream();
            fail("Reader failed to detect underlying content deletion");
        }
        catch (ContentIOException e)
        {
            // expected
        }
        
        // get another fresh reader
        reader = store.getReader(contentUrl);
        assertNotNull("Reader must be returned even when underlying content is missing",
                reader);
        assertFalse("Content should not exist", reader.exists());
        try
        {
            is = reader.getContentInputStream();
            fail("Reader opened stream onto missing content");
        }
        catch (ContentIOException e)
        {
            // expected
        }
    }
    
    /**
     * Tests retrieval of all content URLs
     * <p>
     * Only applies when {@link #getStore()} returns a value.
     */
    public void testListUrls() throws Exception
    {
        ContentStore store = getStore();

        ContentWriter writer = getWriter();
        
        Set<String> contentUrls = store.getUrls();
        String contentUrl = writer.getContentUrl();
        assertTrue("Writer URL not listed by store", contentUrls.contains(contentUrl));

        Date yesterday = new Date(System.currentTimeMillis() - 3600L * 1000L * 24L);
        
        // write some data
        writer.putContent("The quick brown fox...");

        // check again
        contentUrls = store.getUrls();
        assertTrue("Writer URL not listed by store", contentUrls.contains(contentUrl));
        
        // check that the query for content created before this time yesterday doesn't return the URL
        contentUrls = store.getUrls(null, yesterday);
        assertFalse("URL was younger than required, but still shows up", contentUrls.contains(contentUrl));
        
        // delete the content
        boolean deleted = store.delete(contentUrl);
        if (deleted)
        {
            contentUrls = store.getUrls();
            assertFalse("Successfully deleted URL still shown by store", contentUrls.contains(contentUrl));
        }
    }
    
    /**
     * Tests random access writing
     * <p>
     * Only executes if the writer implements {@link RandomAccessContent}.
     */
    public void testRandomAccessWrite() throws Exception
    {
        ContentWriter writer = getWriter();
        
        FileChannel fileChannel = writer.getFileChannel(true);
        assertNotNull("No channel given", fileChannel);
        
        // check that no other content access is allowed
        try
        {
            writer.getWritableChannel();
            fail("Second channel access allowed");
        }
        catch (RuntimeException e)
        {
            // expected
        }
        
        // write some content in a random fashion (reverse order)
        byte[] content = new byte[] {1, 2, 3};
        for (int i = content.length - 1; i >= 0; i--)
        {
            ByteBuffer buffer = ByteBuffer.wrap(content, i, 1);
            fileChannel.write(buffer, i);
        }
        
        // close the channel
        fileChannel.close();
        assertTrue("Writer not closed", writer.isClosed());
        
        // check the content
        ContentReader reader = writer.getReader();
        ReadableByteChannel channelReader = reader.getReadableChannel();
        ByteBuffer buffer = ByteBuffer.allocateDirect(3);
        int count = channelReader.read(buffer);
        assertEquals("Incorrect number of bytes read", 3, count);
        for (int i = 0; i < content.length; i++)
        {
            assertEquals("Content doesn't match", content[i], buffer.get(i));
        }
        
        // get a new writer from the store, using the existing content and perform a truncation check
        ContentWriter writerTruncate = getStore().getWriter(writer.getReader(), AbstractContentStore.createNewUrl());
        assertEquals("Content size incorrect", 0, writerTruncate.getSize());
        // get the channel with truncation
        FileChannel fcTruncate = writerTruncate.getFileChannel(true);
        fcTruncate.close();
        assertEquals("Content not truncated", 0, writerTruncate.getSize());
        
        // get a new writer from the store, using the existing content and perform a non-truncation check
        ContentWriter writerNoTruncate = getStore().getWriter(writer.getReader(), AbstractContentStore.createNewUrl());
        assertEquals("Content size incorrect", 0, writerNoTruncate.getSize());
        // get the channel without truncation
        FileChannel fcNoTruncate = writerNoTruncate.getFileChannel(false);
        fcNoTruncate.close();
        assertEquals("Content was truncated", writer.getSize(), writerNoTruncate.getSize());
    }
    
    /**
     * Tests random access reading
     * <p>
     * Only executes if the reader implements {@link RandomAccessContent}.
     */
    public void testRandomAccessRead() throws Exception
    {
        ContentWriter writer = getWriter();
        // put some content
        String content = "ABC";
        byte[] bytes = content.getBytes();
        writer.putContent(content);
        ContentReader reader = writer.getReader();
        
        FileChannel fileChannel = reader.getFileChannel();
        assertNotNull("No channel given", fileChannel);
        
        // check that no other content access is allowed
        try
        {
            reader.getReadableChannel();
            fail("Second channel access allowed");
        }
        catch (RuntimeException e)
        {
            // expected
        }
        
        // read the content
        ByteBuffer buffer = ByteBuffer.allocate(bytes.length);
        int count = fileChannel.read(buffer);
        assertEquals("Incorrect number of bytes read", bytes.length, count);
        // transfer back to array
        buffer.rewind();
        buffer.get(bytes);
        String checkContent = new String(bytes);
        assertEquals("Content read failure", content, checkContent);
        fileChannel.close();
    }
}