/* * 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.Set; 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; /** * 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 String contentUrl; public AbstractContentReadWriteTest() { super(); } @Override public void setUp() throws Exception { contentUrl = AbstractContentStore.createNewUrl(); } /** * Fetch the store to be used during a test. This method is invoked once per test - it is * therefore safe to use setUp to initialise resources. *

* Usually tests will construct a static instance of the store to use throughout all the * tests. * * @return Returns the same instance 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.setTransactionService(new DummyTransactionService()); 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. *

* 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 // with the stream open, attempt to delete the content boolean deleted = store.delete(contentUrl); // close the stream os.close(); // get a reader ContentReader reader = store.getReader(contentUrl); assertNotNull(reader); // make sure that the underlying content still exists (the delete occured during a write) assertTrue("Underlying content was deleted during a write", reader.exists()); 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 deleted = store.delete(contentUrl); assertFalse("Content deletion failed to detect active reader", deleted); // close the reader stream is.close(); // get a fresh reader reader = store.getReader(contentUrl); assertNotNull(reader); 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 *

* Only applies when {@link #getStore()} returns a value. */ public void testListUrls() throws Exception { ContentStore store = getStore(); ContentWriter writer = getWriter(); Set contentUrls = store.getUrls(); String contentUrl = writer.getContentUrl(); assertTrue("Writer URL not listed by store", contentUrls.contains(contentUrl)); // write some data writer.putContent("The quick brown fox..."); // check again contentUrls = store.getUrls(); assertTrue("Writer URL not listed by store", 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 *

* Only executes if the writer implements {@link RandomAccessContent}. */ public void testRandomAccessWrite() throws Exception { ContentWriter writer = getWriter(); if (!(writer instanceof RandomAccessContent)) { // not much to do here return; } RandomAccessContent randomWriter = (RandomAccessContent) writer; // check that we are allowed to write assertTrue("Expected random access writing", randomWriter.canWrite()); FileChannel fileChannel = randomWriter.getChannel(); 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)); } } /** * Tests random access reading *

* 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(); if (!(reader instanceof RandomAccessContent)) { // not much to do here return; } RandomAccessContent randomReader = (RandomAccessContent) reader; // check that we are NOT allowed to write assertFalse("Expected read-only random access", randomReader.canWrite()); FileChannel fileChannel = randomReader.getChannel(); 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); } }