/*
 * Copyright (C) 2005-2010 Alfresco Software Limited.
 *
 * This file is part of Alfresco
 *
 * Alfresco is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Alfresco is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with Alfresco. If not, see .
 */
package org.alfresco.repo.content;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.io.Serializable;
import java.util.Locale;
import java.util.Map;
import javax.transaction.RollbackException;
import javax.transaction.UserTransaction;
import junit.framework.TestCase;
import org.alfresco.model.ContentModel;
import org.alfresco.repo.content.filestore.FileContentWriter;
import org.alfresco.repo.content.transform.ContentTransformer;
import org.alfresco.repo.policy.JavaBehaviour;
import org.alfresco.repo.policy.PolicyComponent;
import org.alfresco.repo.security.authentication.AuthenticationComponent;
import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback;
import org.alfresco.service.ServiceRegistry;
import org.alfresco.service.cmr.repository.ChildAssociationRef;
import org.alfresco.service.cmr.repository.ContentData;
import org.alfresco.service.cmr.repository.ContentIOException;
import org.alfresco.service.cmr.repository.ContentReader;
import org.alfresco.service.cmr.repository.ContentService;
import org.alfresco.service.cmr.repository.ContentWriter;
import org.alfresco.service.cmr.repository.NoTransformerException;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.repository.NodeService;
import org.alfresco.service.cmr.repository.StoreRef;
import org.alfresco.service.namespace.NamespaceService;
import org.alfresco.service.namespace.QName;
import org.alfresco.service.transaction.TransactionService;
import org.alfresco.test_category.OwnJVMTestsCategory;
import org.alfresco.util.ApplicationContextHelper;
import org.alfresco.util.GUID;
import org.alfresco.util.PropertyMap;
import org.alfresco.util.TempFileProvider;
import org.junit.experimental.categories.Category;
import org.springframework.context.ApplicationContext;
/**
 * @see org.alfresco.repo.content.RoutingContentService
 * 
 * @author Derek Hulley
 */
@Category(OwnJVMTestsCategory.class)
public class RoutingContentServiceTest extends TestCase
{
    private static ApplicationContext ctx = ApplicationContextHelper.getApplicationContext();
    
    private static final String SOME_CONTENT = "ABC";
        
    private static final String TEST_NAMESPACE = "http://www.alfresco.org/test/RoutingContentServiceTest";
    
    private TransactionService transactionService;
    private ContentService contentService;
    private PolicyComponent policyComponent;
    private NodeService nodeService;
    private AuthenticationComponent authenticationComponent;
    private UserTransaction txn;
    private NodeRef rootNodeRef;
    private NodeRef contentNodeRef;
    
    public RoutingContentServiceTest()
    {
    }
    
    @Override
    public void setUp() throws Exception
    {
        transactionService = (TransactionService) ctx.getBean("TransactionService");
        nodeService = (NodeService) ctx.getBean("NodeService");
        contentService = (ContentService) ctx.getBean(ServiceRegistry.CONTENT_SERVICE.getLocalName());
        this.policyComponent = (PolicyComponent) ctx.getBean("policyComponent");
        this.authenticationComponent = (AuthenticationComponent) ctx.getBean("authenticationComponent");
        
        // authenticate
        this.authenticationComponent.setSystemUserAsCurrentUser();
        
        // start the transaction
        txn = getUserTransaction();
        txn.begin();
        
        // create a store and get the root node
        StoreRef storeRef = new StoreRef(StoreRef.PROTOCOL_WORKSPACE, getName());
        if (!nodeService.exists(storeRef))
        {
            storeRef = nodeService.createStore(storeRef.getProtocol(), storeRef.getIdentifier());
        }
        rootNodeRef = nodeService.getRootNode(storeRef);
        // create a content node
        ContentData contentData = new ContentData(null, "text/plain", 0L, "UTF-16", Locale.CHINESE);
        
        PropertyMap properties = new PropertyMap();
        properties.put(ContentModel.PROP_CONTENT, contentData);
        
        ChildAssociationRef assocRef = nodeService.createNode(
                rootNodeRef,
                ContentModel.ASSOC_CHILDREN,
                QName.createQName(TEST_NAMESPACE, GUID.generate()),
                ContentModel.TYPE_CONTENT,
                properties);
        contentNodeRef = assocRef.getChildRef();
    }
    
    @Override
    public void tearDown() throws Exception
    {
        try
        {
            authenticationComponent.clearCurrentSecurityContext();
        }
        catch (Throwable e)
        {
            // ignore
        }
        try
        {
            if (txn != null)
            {
                txn.rollback();
            }
        }
        catch (Throwable e)
        {
            // ignore
        }
    }
    
    private UserTransaction getUserTransaction()
    {
        return (UserTransaction) transactionService.getUserTransaction();
    }
    
    public void testSetUp() throws Exception
    {
        assertNotNull(contentService);
        assertNotNull(nodeService);
        assertNotNull(rootNodeRef);
        assertNotNull(contentNodeRef);
        assertNotNull(getUserTransaction());
        assertFalse(getUserTransaction() == getUserTransaction());  // ensure txn instances aren't shared 
    }
    
    /**
     * Check that a valid writer into the content store can be retrieved and used.
     */
    public void testSimpleNonTempWriter() throws Exception
    {
        ContentWriter writer = contentService.getWriter(null, null, false);
        assertNotNull("Writer should not be null", writer);
        assertNotNull("Content URL should not be null", writer.getContentUrl());
        
        // write some content
        writer.putContent(SOME_CONTENT);
        writer.setMimetype(MimetypeMap.MIMETYPE_TEXT_PLAIN);
        writer.setEncoding("UTF-16");
        writer.setLocale(Locale.CHINESE);
        
        // set the content property manually
        nodeService.setProperty(contentNodeRef, ContentModel.PROP_CONTENT, writer.getContentData());
        
        // get the reader
        ContentReader reader = contentService.getReader(contentNodeRef, ContentModel.PROP_CONTENT);
        assertNotNull("Reader should not be null", reader);
        assertNotNull("Content URL should not be null", reader.getContentUrl());
        assertEquals("Content Encoding was not set", "UTF-16", reader.getEncoding());
        assertEquals("Content Locale was not set", Locale.CHINESE, reader.getLocale());
    }
    
    /**
     * Checks that the URL, mimetype and encoding are automatically set on the readers
     * and writers
     */
    public void testAutoSettingOfProperties() throws Exception
    {
        // get a writer onto the node
        ContentWriter writer = contentService.getWriter(contentNodeRef, ContentModel.PROP_CONTENT, true);
        assertNotNull("Writer should not be null", writer);
        assertNotNull("Content URL should not be null", writer.getContentUrl());
        assertNotNull("Content mimetype should not be null", writer.getMimetype());
        assertNotNull("Content encoding should not be null", writer.getEncoding());
        assertNotNull("Content locale should not be null", writer.getLocale());
        
        // write some content
        writer.putContent(SOME_CONTENT);
        
        // get the reader
        ContentReader reader = contentService.getReader(contentNodeRef, ContentModel.PROP_CONTENT);
        assertNotNull("Reader should not be null", reader);
        assertNotNull("Content URL should not be null", reader.getContentUrl());
        assertNotNull("Content mimetype should not be null", reader.getMimetype());
        assertNotNull("Content encoding should not be null", reader.getEncoding());
        assertNotNull("Content locale should not be null", reader.getLocale());
        
        // check that the content length is correct
        // - note encoding is important as we get the byte length
        long length = SOME_CONTENT.getBytes(reader.getEncoding()).length;  // ensures correct decoding
        long checkLength = reader.getSize();
        assertEquals("Content length incorrect", length, checkLength);
        // check the content - the encoding will come into effect here
        String contentCheck = reader.getContentString();
        assertEquals("Content incorrect", SOME_CONTENT, contentCheck);
    }
    
    public void testWriteToNodeWithoutAnyContentProperties() throws Exception
    {
        // previously, the node was populated with the mimetype, etc
        // check that the write has these
        ContentWriter writer = contentService.getWriter(contentNodeRef, ContentModel.PROP_CONTENT, true);
        assertEquals(MimetypeMap.MIMETYPE_TEXT_PLAIN, writer.getMimetype());
        assertEquals("UTF-16", writer.getEncoding());
        assertEquals(Locale.CHINESE, writer.getLocale());
        // now remove the content property from the node
        nodeService.setProperty(contentNodeRef, ContentModel.PROP_CONTENT, null);
        
        writer = contentService.getWriter(contentNodeRef, ContentModel.PROP_CONTENT, true);
        assertNull(writer.getMimetype());
        assertEquals("UTF-8", writer.getEncoding());
        assertEquals(Locale.getDefault(), writer.getLocale());
        
        // now set it on the writer
        writer.setMimetype("text/plain");
        writer.setEncoding("UTF-16");
        writer.setLocale(Locale.FRENCH);
        
        String content = "The quick brown fox ...";
        writer.putContent(content);
        
        // the properties should have found their way onto the node
        ContentData contentData = (ContentData) nodeService.getProperty(contentNodeRef, ContentModel.PROP_CONTENT);
        assertEquals("metadata didn't get onto node", writer.getContentData(), contentData);
        
        // check that the reader's metadata is set
        ContentReader reader = contentService.getReader(contentNodeRef, ContentModel.PROP_CONTENT);
        assertEquals("Metadata didn't get set on reader", writer.getContentData(), reader.getContentData());
    }
    
    public void testNullReaderForNullUrl() throws Exception
    {
        // set the property, but with a null URL
        ContentData contentData = new ContentData(null, null, 0L, null);
        nodeService.setProperty(contentNodeRef, ContentModel.PROP_CONTENT, contentData);
        // get the reader
        ContentReader reader = contentService.getReader(contentNodeRef, ContentModel.PROP_CONTENT);
        assertNull("Reader must be null if the content URL is null", reader);
    }
    
    @SuppressWarnings("unused")
    public void testContentStoreSizes() throws Exception
    {
        long contentTotalSize = contentService.getStoreFreeSpace();
        long contentAvailableSize = contentService.getStoreTotalSpace();
    }
    
    public void testGetRawReader() throws Exception
    {
        ContentReader reader = contentService.getRawReader("test://non-existence");
        assertNotNull("A reader is expected with content URL referencing no content", reader);
        assertFalse("Reader should not have any content", reader.exists());
        // Now write something
        ContentWriter writer = contentService.getWriter(contentNodeRef, ContentModel.PROP_CONTENT, false);
        writer.putContent("ABC from " + getName());
        // Try again
        String contentUrl = writer.getContentUrl();
        reader = contentService.getRawReader(contentUrl);
        assertNotNull("Expected reader for live, raw content", reader);
        assertEquals("Content sizes don't match", writer.getSize(), reader.getSize());
    }
    
    /**
     * Checks what happens when the physical content disappears
     */
    public void testMissingContent() throws Exception
    {
        File tempFile = TempFileProvider.createTempFile(getName(), ".txt");
        
        ContentWriter writer = new FileContentWriter(tempFile);
        writer.setMimetype(MimetypeMap.MIMETYPE_TEXT_PLAIN);
        writer.setEncoding("UTF-8");
        writer.putContent("What about the others?  Buckwheats!");
        // check
        assertTrue("File does not exist", tempFile.exists());
        assertTrue("File not written to", tempFile.length() > 0);
        
        // update the node with this new info 
        ContentData contentData = writer.getContentData();
        nodeService.setProperty(contentNodeRef, ContentModel.PROP_CONTENT, contentData);
        
        // delete the content
        tempFile.delete();
        assertFalse("File not deleted", tempFile.exists());
        
        // now attempt to get the reader for the node
        ContentReader reader = contentService.getReader(contentNodeRef, ContentModel.PROP_CONTENT);
        assertFalse("Reader should indicate that content is missing", reader.exists());
        
        // check the indexing doesn't spank everthing
        txn.commit();
        txn = null;
        // cleanup
        txn = getUserTransaction();
        txn.begin();
        nodeService.deleteNode(contentNodeRef);
        txn.commit();
        txn = null;
    }
	
	/**
	 * Tests simple writes that don't automatically update the node content URL
	 */
	public void testSimpleWrite() throws Exception
	{
		// get a writer to an arbitrary node
		ContentWriter writer = contentService.getWriter(contentNodeRef, ContentModel.PROP_CONTENT, false);   // no updating of URL
		assertNotNull("Writer should not be null", writer);
		
		// put some content
		writer.putContent(SOME_CONTENT);
		
		// get the reader for the node
		ContentReader reader = contentService.getReader(contentNodeRef, ContentModel.PROP_CONTENT);
		assertNull("No reader should yet be available for the node", reader);
	}
	
	private boolean policyFired = false;
    private boolean readPolicyFired = false;
    private boolean newContent = true;
	
	/**
	 * Tests that the content update policy firs correctly
	 */
	public void testOnContentUpdatePolicy()
	{
		// Register interest in the content update event for a versionable node
		this.policyComponent.bindClassBehaviour(
                QName.createQName(NamespaceService.ALFRESCO_URI, "onContentUpdate"),
				ContentModel.ASPECT_VERSIONABLE,
				new JavaBehaviour(this, "onContentUpdateBehaviourTest"));
		
		// First check that the policy is not fired when the versionable aspect is not present
		ContentWriter contentWriter = this.contentService.getWriter(contentNodeRef, ContentModel.PROP_CONTENT, true);
		contentWriter.putContent("content update one");
		assertFalse(this.policyFired);
        
        this.newContent = false;
		
		// Now check that the policy is fired when the versionable aspect is present
		this.nodeService.addAspect(this.contentNodeRef, ContentModel.ASPECT_VERSIONABLE, null);
		ContentWriter contentWriter2 = this.contentService.getWriter(contentNodeRef, ContentModel.PROP_CONTENT, true);
		contentWriter2.putContent("content update two");
		assertTrue(this.policyFired);
		this.policyFired = false;
		
		// Check that the policy is not fired when using a non updating content writer
		ContentWriter contentWriter3 = this.contentService.getWriter(contentNodeRef, ContentModel.PROP_CONTENT, false);
		contentWriter3.putContent("content update three");
		assertFalse(this.policyFired);
	}
	
	public void onContentUpdateBehaviourTest(NodeRef nodeRef, boolean newContent)
	{
		assertEquals(this.contentNodeRef, nodeRef);
        assertEquals(this.newContent, newContent);
		assertTrue(this.nodeService.hasAspect(nodeRef, ContentModel.ASPECT_VERSIONABLE));
		this.policyFired = true;
	}
    
    public void testOnContentReadPolicy()
    {
        // Register interest in the content read event for a versionable node
        this.policyComponent.bindClassBehaviour(
                QName.createQName(NamespaceService.ALFRESCO_URI, "onContentRead"),
                ContentModel.ASPECT_VERSIONABLE,
                new JavaBehaviour(this, "onContentReadBehaviourTest"));
        
        // First check that the policy is not fired when the versionable aspect is not present
        this.contentService.getReader(contentNodeRef, ContentModel.PROP_CONTENT);
        assertFalse(this.readPolicyFired);
        
        // Write some content and check that the policy is still not fired
        ContentWriter contentWriter2 = this.contentService.getWriter(contentNodeRef, ContentModel.PROP_CONTENT, true);
        contentWriter2.putContent("content update two");
        this.contentService.getReader(contentNodeRef, ContentModel.PROP_CONTENT);
        assertFalse(this.readPolicyFired);
        
        // Now check that the policy is fired when the versionable aspect is present
        this.nodeService.addAspect(this.contentNodeRef, ContentModel.ASPECT_VERSIONABLE, null);
        this.contentService.getReader(contentNodeRef, ContentModel.PROP_CONTENT);
        assertTrue(this.readPolicyFired);
    }
    
    public void onContentReadBehaviourTest(NodeRef nodeRef)
    {
        this.readPolicyFired = true;
    }
    
    public void testTempWrite() throws Exception
    {
        // get a temporary writer
        ContentWriter writer1 = contentService.getTempWriter();
        // and another
        ContentWriter writer2 = contentService.getTempWriter();
        
        // check
        assertNotSame("Temp URLs must be different",
                writer1.getContentUrl(), writer2.getContentUrl());
    }
    
	/**
	 * Tests the automatic updating of nodes' content URLs
	 */
    public void testUpdatingWrite() throws Exception
    {
        // check that the content URL property has not been set
        ContentData contentData = (ContentData) nodeService.getProperty(
                contentNodeRef,
                ContentModel.PROP_CONTENT); 
        assertNull("Content URL should be null", contentData.getContentUrl());
        
        // before the content is written, there should not be any reader available
        ContentReader reader = contentService.getReader(contentNodeRef, ContentModel.PROP_CONTENT);
        assertNull("No reader should be available for new node", reader);
        
        // get the writer
        ContentWriter writer = contentService.getWriter(contentNodeRef, ContentModel.PROP_CONTENT, true);
        assertNotNull("No writer received", writer);
        // write some content directly
        writer.putContent(SOME_CONTENT);
        
        // make sure that we can't reuse the writer
        try
        {
            writer.putContent("DEF");
            fail("Failed to prevent repeated use of the content writer");
        }
        catch (ContentIOException e)
        {
            // expected
        }
        
        // check that there is a reader available
        reader = contentService.getReader(contentNodeRef, ContentModel.PROP_CONTENT);
        assertNotNull("No reader available for node", reader);
        String contentCheck = reader.getContentString();
        assertEquals("Content fetched doesn't match that written", SOME_CONTENT, contentCheck);
        // check that the content data was set
        contentData = (ContentData) nodeService.getProperty(
                contentNodeRef,
                ContentModel.PROP_CONTENT);
        assertNotNull("Content data not set", contentData);
        assertEquals("Mismatched URL between writer and node",
                writer.getContentUrl(), contentData.getContentUrl());
        
        // check that the content size was set
        assertEquals("Reader content length and node content length different",
                reader.getSize(), contentData.getSize());
        
        // check that the mimetype was set
        assertEquals("Mimetype not set on content data", "text/plain", contentData.getMimetype());
        // check encoding
        assertEquals("Encoding not set", "UTF-16", contentData.getEncoding());
    }
    
    /**
     * Checks that multiple writes can occur to the same node outside of any transactions.
     * 
     * It is only when the streams are closed that the node is updated.
     */
    public void testConcurrentWritesNoTxn() throws Exception
    {
        // ensure that the transaction is ended - ofcourse, we need to force a commit
        txn.commit();
        txn = null;
        
        ContentWriter writer1 = contentService.getWriter(contentNodeRef, ContentModel.PROP_CONTENT, true);
        ContentWriter writer2 = contentService.getWriter(contentNodeRef, ContentModel.PROP_CONTENT, true);
        ContentWriter writer3 = contentService.getWriter(contentNodeRef, ContentModel.PROP_CONTENT, true);
        
        writer1.putContent("writer1 wrote this");
        writer2.putContent("writer2 wrote this");
        writer3.putContent("writer3 wrote this");
        // get the content
        ContentReader reader = contentService.getReader(contentNodeRef, ContentModel.PROP_CONTENT);
        String contentCheck = reader.getContentString();
        assertEquals("Content check failed", "writer3 wrote this", contentCheck);
    }
    
    public void testConcurrentWritesWithSingleTxn() throws Exception
    {
        // want to operate in a user transaction
        txn.commit();
        txn = null;
        
        UserTransaction txn = getUserTransaction();
        txn.begin();
        txn.setRollbackOnly();
        ContentWriter writer1 = contentService.getWriter(contentNodeRef, ContentModel.PROP_CONTENT, true);
        ContentWriter writer2 = contentService.getWriter(contentNodeRef, ContentModel.PROP_CONTENT, true);
        ContentWriter writer3 = contentService.getWriter(contentNodeRef, ContentModel.PROP_CONTENT, true);
        
        writer1.putContent("writer1 wrote this");
        writer2.putContent("writer2 wrote this");
        writer3.putContent("writer3 wrote this");
        // get the content
        ContentReader reader = contentService.getReader(contentNodeRef, ContentModel.PROP_CONTENT);
        String contentCheck = reader.getContentString();
        assertEquals("Content check failed", "writer3 wrote this", contentCheck);
        
        try
        {
            txn.commit();
            fail("Transaction has been marked for rollback");
        }
        catch (RollbackException e)
        {
            // expected
        }
        
        // rollback and check that the content has 'disappeared'
        txn.rollback();
        
        // need a new transaction
        txn = getUserTransaction();
        txn.begin();
        txn.setRollbackOnly();
        reader = contentService.getReader(contentNodeRef, ContentModel.PROP_CONTENT);
        assertNull("Transaction was rolled back - no content should be visible", reader);
        
        txn.rollback();
    }
    
    /**
     * Create several threads that will attempt to write to the same node property.
     * The ContentWriter is handed to the thread, so this checks that the stream closure
     * uses the transaction that called close and not the transaction that
     * fetched the ContentWriter.
     */
    public synchronized void testConcurrentWritesWithMultipleTxns() throws Exception
    {
        // ensure that there is no content to read on the node
        ContentReader reader = contentService.getReader(contentNodeRef, ContentModel.PROP_CONTENT);
        assertNull("Reader should not be available", reader);
        // commit node so that threads can see node
        txn.commit();
        txn = null;
        
        String threadContent = "Thread content";
        WriteThread[] writeThreads = new WriteThread[5];
        for (int i = 0; i < writeThreads.length; i++)
        {
            ContentWriter threadWriter = contentService.getWriter(contentNodeRef, ContentModel.PROP_CONTENT, true);
            writeThreads[i] = new WriteThread(threadWriter, threadContent);
            // Kick each thread off
            writeThreads[i].start();
        }
        
        // Wait for all threads to be waiting
        outer:
        while (true)
        {
            // Wait for each thread to be in a transaction
            for (int i = 0; i < writeThreads.length; i++)
            {
                if (!writeThreads[i].isWaiting())
                {
                    wait(10);
                    continue outer;
                }
            }
            // All threads were waiting
            break outer;
        }
        
        // Kick each thread into the stream close phase
        for (int i = 0; i < writeThreads.length; i++)
        {
            synchronized(writeThreads[i])
            {
                writeThreads[i].notifyAll();
            }
        }
        // Wait for the threads to complete (one way or another)
        for (int i = 0; i < writeThreads.length; i++)
        {
            while (!writeThreads[i].isDone())
            {
                wait(10);
            }
        }
        // check content has taken on thread's content
        reader = contentService.getReader(contentNodeRef, ContentModel.PROP_CONTENT);
        assertNotNull("Reader should now be available", reader);
        String checkContent = reader.getContentString();
        assertEquals("Content check failed", threadContent, checkContent);
    }
    
    public void testTransformation() throws Exception
    {
        // commit node so that threads can see node
        txn.commit();
        txn = null;
        
        UserTransaction txn = getUserTransaction();
        txn.begin();
        txn.setRollbackOnly();
        
        // get a regular writer
        ContentWriter writer = contentService.getTempWriter();
        writer.setMimetype("text/xml");
        // write some stuff
        String content = "";
        writer.putContent(content);
        // get a reader onto the content
        ContentReader reader = writer.getReader();
        
        // get a new writer for the transformation
        writer = contentService.getTempWriter();
        writer.setMimetype("audio/x-wav");     // no such conversion possible
        try
        {
            contentService.transform(reader, writer);
            fail("Transformation attempted with invalid mimetype");
        }
        catch (NoTransformerException e)
        {
            // expected
        }
        
        // at this point, the transaction is unusable
        txn.rollback();
        
        txn = getUserTransaction();
        txn.begin();
        txn.setRollbackOnly();
        
        writer.setMimetype("text/plain");
        ContentTransformer transformer = contentService.getTransformer(reader.getMimetype(), writer.getMimetype());
        assertNotNull("Expected a valid transformer", transformer);
        contentService.transform(reader, writer);
        // get the content from the writer
        reader = writer.getReader();
        assertEquals("Mimetype of target reader incorrect",
                writer.getMimetype(), reader.getMimetype());
        String contentCheck = reader.getContentString();
        assertEquals("Content check failed", content, contentCheck);
        
        txn.rollback();
    }
    
    /**
     * Writes some content to the writer's output stream and then aquires
     * a lock on the writer, waits until notified and then closes the
     * output stream before terminating.
     * 
     * When firing thread up, be sure to call notify on the
     * Thread instance in order to let the thread run to completion.
     */
    private class WriteThread extends Thread
    {
        private ContentWriter writer;
        private String content;
        private volatile boolean isWaiting;
        private volatile boolean isDone;
        private volatile Throwable error;
        
        public WriteThread(ContentWriter writer, String content)
        {
            this.writer = writer;
            this.content = content;
            isWaiting = false;
            isDone = false;
            error = null;
        }
        
        public boolean isWaiting()
        {
            return isWaiting;
        }
        
        public boolean isDone()
        {
            return isDone;
        }
        
        @SuppressWarnings("unused")
        public Throwable getError()
        {
            return error;
        }
        public void run()
        {
            authenticationComponent.setSystemUserAsCurrentUser();
            
            synchronized (this)
            {
                isWaiting = true;
                try { this.wait(); } catch (InterruptedException e) {};   // wait until notified
            }
            final OutputStream os = writer.getContentOutputStream();
            // Callback to write to the content in a new transaction
            RetryingTransactionCallback callback = new RetryingTransactionCallback()
            {
                public Void execute() throws Throwable
                {
                    try
                    {
                        // put the content
                        if (writer.getEncoding() == null)
                        {
                            os.write(content.getBytes());
                        }
                        else
                        {
                            os.write(content.getBytes(writer.getEncoding()));
                        }
                        os.close();
                    }
                    finally
                    {
                        if (os != null)
                        {
                            try { os.close(); } catch (IOException e) {}
                        }
                    }
                    return null;
                }
            };
            try
            {
                transactionService.getRetryingTransactionHelper().doInTransaction(callback);
            }
            catch (Throwable e)
            {
                e.printStackTrace();
                error = e;
            }
            finally
            {
                isDone = true;
            }
        }
    }
    /**
     * Check that the system is able to handle the uploading of content with an unknown mimetype.
     * The unknown mimetype should be preserved, but treated just like an octet stream.
     */
    public void testUnknownMimetype() throws Exception
    {
        String bogusMimetype = "text/bamboozle";
        // get a writer onto the node
        ContentWriter writer = contentService.getWriter(contentNodeRef, ContentModel.PROP_CONTENT, true);
        writer.setMimetype(bogusMimetype);
        
        // write something in
        writer.putContent(SOME_CONTENT);
        
        // commit the transaction to ensure that it goes in OK
        txn.commit();
        
        // so far, so good
        ContentReader reader = contentService.getReader(contentNodeRef, ContentModel.PROP_CONTENT);
        assertNotNull("Should be able to get reader", reader);
        assertEquals("Unknown mimetype was changed", bogusMimetype, reader.getMimetype());
    }
    
    /**
     * Checks that node copy and delete behaviour behaves correctly w.r.t. cleanup and shared URLs
     */
    public void testPostCopyContentRetrieval() throws Exception
    {
        ContentWriter writer = contentService.getWriter(contentNodeRef, ContentModel.PROP_CONTENT, true);
        writer.setMimetype(MimetypeMap.MIMETYPE_TEXT_PLAIN);
        writer.putContent("Some content");
        ContentData writerContentData = writer.getContentData();
        ContentData nodeContentData = (ContentData) nodeService.getProperty(contentNodeRef, ContentModel.PROP_CONTENT);
        assertNotNull(nodeContentData);
        assertEquals("ContentData not the same from NodeService and from ContentWriter", writerContentData, nodeContentData);
        
        Map copyProperties = nodeService.getProperties(contentNodeRef);
        copyProperties.remove(ContentModel.PROP_NODE_UUID);
        // Copy the node
        NodeRef contentCopyNodeRef = nodeService.createNode(
                rootNodeRef,
                ContentModel.ASSOC_CHILDREN,
                QName.createQName(TEST_NAMESPACE, GUID.generate()),
                ContentModel.TYPE_CONTENT,
                copyProperties).getChildRef();
        // Now get and check the ContentData for the copy
        ContentData copyNodeContentData = (ContentData) nodeService.getProperty(contentCopyNodeRef, ContentModel.PROP_CONTENT);
        assertNotNull(copyNodeContentData);
        // The copy should share the same URL even
        assertEquals("Copied node's cm:content ContentData was different", writerContentData, copyNodeContentData);
        
        // Delete the first node and ensure that the second valud remains good and the content is editable
        nodeService.deleteNode(contentNodeRef);
        copyNodeContentData = (ContentData) nodeService.getProperty(contentCopyNodeRef, ContentModel.PROP_CONTENT);
        assertNotNull(copyNodeContentData);
        assertEquals("Post-delete value didn't remain the same", writerContentData, copyNodeContentData);
        ContentReader copyNodeContentReader = contentService.getReader(contentCopyNodeRef, ContentModel.PROP_CONTENT);
        assertTrue("Physical content was removed", copyNodeContentReader.exists());
        
        txn.commit();
        txn = null;
    }
    
    /**
     * Ensure that content URLs outside of a transaction are not touched on rollback.
     */
    public void testRollbackCleanup_ALF2890() throws Exception
    {
        ContentWriter updatingWriter = contentService.getWriter(contentNodeRef, ContentModel.PROP_CONTENT, true);
        updatingWriter.putContent("STEP 1");
        txn.commit();
        txn = null;
        
        ContentReader readerStep1 = contentService.getReader(contentNodeRef, ContentModel.PROP_CONTENT);
        assertEquals("Incorrect content", "STEP 1", readerStep1.getContentString());
        
        ContentWriter simpleWriter = contentService.getWriter(contentNodeRef, ContentModel.PROP_CONTENT, false);
        simpleWriter.putContent("STEP 2");
        readerStep1 = contentService.getReader(contentNodeRef, ContentModel.PROP_CONTENT);
        assertEquals("Incorrect content", "STEP 1", readerStep1.getContentString());
        
        // Update the content
        nodeService.setProperty(contentNodeRef, ContentModel.PROP_CONTENT, simpleWriter.getContentData());
        ContentReader readerStep2 = contentService.getReader(contentNodeRef, ContentModel.PROP_CONTENT);
        assertEquals("Incorrect content", "STEP 2", readerStep2.getContentString());
        
        simpleWriter = contentService.getWriter(contentNodeRef, ContentModel.PROP_CONTENT, false);
        simpleWriter.putContent("STEP 3");
        ContentReader readerStep3 = simpleWriter.getReader();
        assertEquals("Incorrect content", "STEP 3", readerStep3.getContentString());
        readerStep2 = contentService.getReader(contentNodeRef, ContentModel.PROP_CONTENT);
        assertEquals("Incorrect content", "STEP 2", readerStep2.getContentString());
        
        // Now get a ex-transaction writer but set the content property in a failing transaction
        // Notice that we have already written "STEP 3" to an underlying binary
        final ContentData simpleWriterData = simpleWriter.getContentData();
        RetryingTransactionCallback failToSetPropCallback = new RetryingTransactionCallback()
        {
            public Void execute() throws Throwable
            {
                nodeService.setProperty(contentNodeRef, ContentModel.PROP_CONTENT, simpleWriterData);
                throw new RuntimeException("aaa");
            }
        };
        try
        {
            transactionService.getRetryingTransactionHelper().doInTransaction(failToSetPropCallback);
        }
        catch (RuntimeException e)
        {
            if (!e.getMessage().equals("aaa"))
            {
                throw e;
            }
            // Expected
        }
        // The writer data should not have been cleaned up
        readerStep3 = simpleWriter.getReader();
        assertTrue("Content was cleaned up when it originated outside of the transaction", readerStep3.exists());
        assertEquals("Incorrect content", "STEP 3", readerStep3.getContentString());
        // The node's content must be unchanged
        readerStep2 = contentService.getReader(contentNodeRef, ContentModel.PROP_CONTENT);
        assertEquals("Incorrect content", "STEP 2", readerStep2.getContentString());
        // Test that rollback cleanup works for writers fetched in the same transaction
        final ContentReader[] readers = new ContentReader[1];
        RetryingTransactionCallback rollbackCallback = new RetryingTransactionCallback()
        {
            public Void execute() throws Throwable
            {
                ContentWriter writer = contentService.getWriter(contentNodeRef, ContentModel.PROP_CONTENT, true);
                writer.putContent("UNLUCKY CONTENT");
                ContentReader reader = contentService.getReader(contentNodeRef, ContentModel.PROP_CONTENT);
                assertEquals("Incorrect content", "UNLUCKY CONTENT", reader.getContentString());
                assertEquals("Incorrect content", "UNLUCKY CONTENT", writer.getReader().getContentString());
                readers[0] = reader;
                
                throw new RuntimeException("aaa");
            }
        };
        try
        {
            transactionService.getRetryingTransactionHelper().doInTransaction(rollbackCallback);
        }
        catch (RuntimeException e)
        {
            if (!e.getMessage().equals("aaa"))
            {
                throw e;
            }
            // Expected
        }
        // Make sure that the content has been cleaned up
        assertFalse("Content was not cleaned up after having been created in-transaction", readers[0].exists());
    }
}