/*
 * 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.File;
import java.io.IOException;
import java.io.OutputStream;

import javax.transaction.RollbackException;
import javax.transaction.UserTransaction;

import org.alfresco.model.ContentModel;
import org.alfresco.repo.content.filestore.FileContentWriter;
import org.alfresco.repo.policy.JavaBehaviour;
import org.alfresco.repo.policy.PolicyComponent;
import org.alfresco.repo.security.authentication.AuthenticationComponent;
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.util.BaseSpringTest;
import org.alfresco.util.GUID;
import org.alfresco.util.PropertyMap;
import org.alfresco.util.TempFileProvider;

/**
 * @see org.alfresco.repo.content.RoutingContentService
 * 
 * @author Derek Hulley
 */
public class RoutingContentServiceTest extends BaseSpringTest
{
    private static final String SOME_CONTENT = "ABC";
        
    private static final String TEST_NAMESPACE = "http://www.alfresco.org/test/RoutingContentServiceTest";
    
    private ContentService contentService;
    private PolicyComponent policyComponent;
    private NodeService nodeService;
    private NodeRef rootNodeRef;
    private NodeRef contentNodeRef;
    private AuthenticationComponent authenticationComponent;
    
    public RoutingContentServiceTest()
    {
    }
    
    @Override
    public void onSetUpInTransaction() throws Exception
    {
        super.onSetUpInTransaction();
        nodeService = (NodeService) applicationContext.getBean("dbNodeService");
        contentService = (ContentService) applicationContext.getBean(ServiceRegistry.CONTENT_SERVICE.getLocalName());
        this.policyComponent = (PolicyComponent)this.applicationContext.getBean("policyComponent");
        this.authenticationComponent = (AuthenticationComponent)this.applicationContext.getBean("authenticationComponent");
        
        this.authenticationComponent.setSystemUserAsCurrentUser();
        // 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");
        
        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
    protected void onTearDownInTransaction() throws Exception
    {
        try
        {
            authenticationComponent.clearCurrentSecurityContext();
        }
        catch (Throwable e)
        {
            // ignore
        }
        super.onTearDownInTransaction();
    }
    
    private UserTransaction getUserTransaction()
    {
        TransactionService transactionService = (TransactionService)applicationContext.getBean("transactionComponent");
        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 
    }
    
    /**
     * 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());
        
        // 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());
        
        // 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);
        assertNotNull(writer.getMimetype());
        assertNotNull(writer.getEncoding());

        // 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());
        
        // now set it on the writer
        writer.setMimetype("text/plain");
        writer.setEncoding("UTF-8");
        
        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);
    }
    
    /**
     * 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
        setComplete();
        endTransaction();
    }
	
	/**
	 * 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.
     * <p>
     * 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
        setComplete();
        endTransaction();
        
        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
        setComplete();
        endTransaction();
        
        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();
    }
    
    public synchronized void testConcurrentWritesWithMultipleTxns() throws Exception
    {
        // commit node so that threads can see node
        setComplete();
        endTransaction();
        
        UserTransaction txn = getUserTransaction();
        txn.begin();
        
        // 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);
        
        ContentWriter threadWriter = contentService.getWriter(contentNodeRef, ContentModel.PROP_CONTENT, true);
        String threadContent = "Thread content";
        WriteThread thread = new WriteThread(threadWriter, threadContent);
        // kick off thread
        thread.start();
        // wait for thread to get to its wait points
        while (!thread.isWaiting())
        {
            wait(10);
        }
        
        // write to the content
        ContentWriter writer = contentService.getWriter(contentNodeRef, ContentModel.PROP_CONTENT, true);
        writer.putContent(SOME_CONTENT);
        
        // fire thread up again
        synchronized(threadWriter)
        {
            threadWriter.notifyAll();
        }
        // thread is released - but we have to wait for it to complete
        while (!thread.isDone())
        {
            wait(10);
        }

        // the thread has finished and has committed its changes - check the visibility
        reader = contentService.getReader(contentNodeRef, ContentModel.PROP_CONTENT);
        assertNotNull("Reader should now be available", reader);
        String checkContent = reader.getContentString();
        assertEquals("Content check failed", SOME_CONTENT, checkContent);
        
        // rollback the txn
        txn.rollback();
        
        // check content has taken on thread's content
        reader = contentService.getReader(contentNodeRef, ContentModel.PROP_CONTENT);
        assertNotNull("Reader should now be available", reader);
        checkContent = reader.getContentString();
        assertEquals("Content check failed", threadContent, checkContent);
    }
    
    public void testTransformation() throws Exception
    {
        // commit node so that threads can see node
        setComplete();
        endTransaction();
        
        UserTransaction txn = getUserTransaction();
        txn.begin();
        txn.setRollbackOnly();
        
        // get a regular writer
        ContentWriter writer = contentService.getTempWriter();
        writer.setMimetype("text/xml");
        // write some stuff
        String content = "<blah></blah>";
        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");
        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.
     * <p>
     * When firing thread up, be sure to call <code>notify</code> on the
     * writer in order to let the thread run to completion.
     */
    private class WriteThread extends Thread
    {
        private ContentWriter writer;
        private String content;
        private boolean isWaiting;
        private boolean isDone;
        
        public WriteThread(ContentWriter writer, String content)
        {
            this.writer = writer;
            this.content = content;
            isWaiting = false;
            isDone = false;
        }
        
        public boolean isWaiting()
        {
            return isWaiting;
        }
        
        public boolean isDone()
        {
            return isDone;
        }

        public void run()
        {
            authenticationComponent.setSystemUserAsCurrentUser();
            
            isWaiting = false;
            isDone = false;
            UserTransaction txn = getUserTransaction();
            OutputStream os = writer.getContentOutputStream();
            try
            {
                txn.begin();    // not testing transactions - this is not a safe pattern
                // put the content
                if (writer.getEncoding() == null)
                {
                    os.write(content.getBytes());
                }
                else
                {
                    os.write(content.getBytes(writer.getEncoding()));
                }
                synchronized (writer)
                {
                    isWaiting = true;
                    writer.wait();   // wait until notified
                }
                os.close();
                os = null;
                txn.commit();
            }
            catch (Throwable e)
            {
                try {txn.rollback(); } catch (Exception ee) {}
                e.printStackTrace();
                throw new RuntimeException("Failed writing to output stream for writer: " + writer, e);
            }
            finally
            {
                if (os != null)
                {
                    try { os.close(); } catch (IOException e) {}
                }
                isDone = true;
            }
        }
    }
}