Moving to root below branch label

git-svn-id: https://svn.alfresco.com/repos/alfresco-enterprise/alfresco/HEAD/root@2005 c4b6b30b-aa2e-2d43-bbcb-ca4b014f7261
This commit is contained in:
Derek Hulley
2005-12-08 07:13:07 +00:00
commit e1e6508fec
1095 changed files with 230566 additions and 0 deletions

View File

@@ -0,0 +1,373 @@
/*
* 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.IOException;
import java.lang.reflect.Method;
import java.nio.ByteBuffer;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.WritableByteChannel;
import java.util.List;
import org.alfresco.error.AlfrescoRuntimeException;
import org.alfresco.repo.transaction.TransactionUtil;
import org.alfresco.service.cmr.repository.ContentAccessor;
import org.alfresco.service.cmr.repository.ContentData;
import org.alfresco.service.cmr.repository.ContentStreamListener;
import org.alfresco.service.transaction.TransactionService;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.aop.AfterReturningAdvice;
/**
* Provides basic support for content accessors.
*
* @author Derek Hulley
*/
public abstract class AbstractContentAccessor implements ContentAccessor
{
private static Log logger = LogFactory.getLog(AbstractContentAccessor.class);
/** when set, ensures that listeners are executed within a transaction */
private TransactionService transactionService;
private String contentUrl;
private String mimetype;
private String encoding;
/**
* @param contentUrl the content URL
*/
protected AbstractContentAccessor(String contentUrl)
{
if (contentUrl == null || contentUrl.length() == 0)
{
throw new IllegalArgumentException("contentUrl must be a valid String");
}
this.contentUrl = contentUrl;
// the default encoding is Java's default encoding
encoding = "UTF-8";
}
public ContentData getContentData()
{
ContentData property = new ContentData(contentUrl, mimetype, getSize(), encoding);
return property;
}
/**
* Provides access to transactions for implementing classes
*
* @return Returns a source of user transactions
*/
protected TransactionService getTransactionService()
{
return transactionService;
}
/**
* Set the transaction provider to be used by {@link ContentStreamListener listeners}.
*
* @param transactionService the transaction service to wrap callback code in
*/
public void setTransactionService(TransactionService transactionService)
{
this.transactionService = transactionService;
}
public String toString()
{
StringBuilder sb = new StringBuilder(100);
sb.append("ContentAccessor")
.append("[ contentUrl=").append(getContentUrl())
.append(", mimetype=").append(getMimetype())
.append(", size=").append(getSize())
.append(", encoding=").append(getEncoding())
.append("]");
return sb.toString();
}
public String getContentUrl()
{
return contentUrl;
}
public String getMimetype()
{
return mimetype;
}
/**
* @param mimetype the underlying content's mimetype - null if unknown
*/
public void setMimetype(String mimetype)
{
this.mimetype = mimetype;
}
/**
* @return Returns the content encoding - null if unknown
*/
public String getEncoding()
{
return encoding;
}
/**
* @param encoding the underlying content's encoding - null if unknown
*/
public void setEncoding(String encoding)
{
this.encoding = encoding;
}
/**
* Advise that listens for the completion of specific methods on the
* {@link java.nio.channels.ByteChannel} interface.
*
* @author Derek Hulley
*/
protected class ByteChannelCallbackAdvise implements AfterReturningAdvice
{
private List<ContentStreamListener> listeners;
public ByteChannelCallbackAdvise(List<ContentStreamListener> listeners)
{
this.listeners = listeners;
}
/**
* Provides transactional callbacks to the listeners
*/
public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable
{
// check for specific events
if (method.getName().equals("close"))
{
fireChannelClosed();
}
}
private void fireChannelClosed()
{
if (listeners.size() == 0)
{
// nothing to do
return;
}
// ensure that we are in a transaction
if (transactionService == null)
{
throw new AlfrescoRuntimeException("A transaction service is required when there are listeners present");
}
TransactionUtil.TransactionWork<Object> work = new TransactionUtil.TransactionWork<Object>()
{
public Object doWork()
{
// call the listeners
for (ContentStreamListener listener : listeners)
{
listener.contentStreamClosed();
}
return null;
}
};
TransactionUtil.executeInUserTransaction(transactionService, work);
// done
if (logger.isDebugEnabled())
{
logger.debug("Content listeners called: close");
}
}
}
/**
* Wraps a <code>FileChannel</code> to provide callbacks to listeners when the
* channel is {@link java.nio.channels.Channel#close() closed}.
*
* @author Derek Hulley
*/
protected class CallbackFileChannel extends FileChannel
{
/** the channel to route all calls to */
private FileChannel delegate;
/** listeners waiting for the stream close */
private List<ContentStreamListener> listeners;
/**
* @param delegate the channel that will perform the work
* @param listeners listeners for events coming from this channel
*/
public CallbackFileChannel(
FileChannel delegate,
List<ContentStreamListener> listeners)
{
if (delegate == null)
{
throw new IllegalArgumentException("FileChannel delegate is required");
}
if (delegate instanceof CallbackFileChannel)
{
throw new IllegalArgumentException("FileChannel delegate may not be a CallbackFileChannel");
}
this.delegate = delegate;
this.listeners = listeners;
}
/**
* Closes the channel and makes the callbacks to the listeners
*/
@Override
protected void implCloseChannel() throws IOException
{
delegate.close();
fireChannelClosed();
}
/**
* Helper method to notify stream listeners
*/
private void fireChannelClosed()
{
if (listeners.size() == 0)
{
// nothing to do
return;
}
// create the work to update the listeners
TransactionUtil.TransactionWork<Object> work = new TransactionUtil.TransactionWork<Object>()
{
public Object doWork()
{
// call the listeners
for (ContentStreamListener listener : listeners)
{
listener.contentStreamClosed();
}
return null;
}
};
TransactionUtil.executeInUserTransaction(transactionService, work);
// done
if (logger.isDebugEnabled())
{
logger.debug("Content listeners called: close");
}
}
@Override
public void force(boolean metaData) throws IOException
{
delegate.force(metaData);
}
@Override
public FileLock lock(long position, long size, boolean shared) throws IOException
{
return delegate.lock(position, size, shared);
}
@Override
public MappedByteBuffer map(MapMode mode, long position, long size) throws IOException
{
return delegate.map(mode, position, size);
}
@Override
public long position() throws IOException
{
return delegate.position();
}
@Override
public FileChannel position(long newPosition) throws IOException
{
return delegate.position(newPosition);
}
@Override
public int read(ByteBuffer dst) throws IOException
{
return delegate.read(dst);
}
@Override
public int read(ByteBuffer dst, long position) throws IOException
{
return delegate.read(dst, position);
}
@Override
public long read(ByteBuffer[] dsts, int offset, int length) throws IOException
{
return delegate.read(dsts, offset, length);
}
@Override
public long size() throws IOException
{
return delegate.size();
}
@Override
public long transferFrom(ReadableByteChannel src, long position, long count) throws IOException
{
return delegate.transferFrom(src, position, count);
}
@Override
public long transferTo(long position, long count, WritableByteChannel target) throws IOException
{
return delegate.transferTo(position, count, target);
}
@Override
public FileChannel truncate(long size) throws IOException
{
return delegate.truncate(size);
}
@Override
public FileLock tryLock(long position, long size, boolean shared) throws IOException
{
return delegate.tryLock(position, size, shared);
}
@Override
public int write(ByteBuffer src) throws IOException
{
return delegate.write(src);
}
@Override
public int write(ByteBuffer src, long position) throws IOException
{
return delegate.write(src, position);
}
@Override
public long write(ByteBuffer[] srcs, int offset, int length) throws IOException
{
return delegate.write(srcs, offset, length);
}
}
}

View File

@@ -0,0 +1,624 @@
/*
* 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 <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());
// 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.
* <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
// with the stream open, attempt to delete the content
boolean deleted = store.delete(contentUrl);
assertFalse("Should not be able to delete content with open write stream", deleted);
// 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
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
* <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));
// 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
* <p>
* 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
* <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();
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);
}
}

View File

@@ -0,0 +1,332 @@
/*
* 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.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.Reader;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
import java.util.ArrayList;
import java.util.List;
import org.alfresco.error.AlfrescoRuntimeException;
import org.alfresco.service.cmr.repository.ContentAccessor;
import org.alfresco.service.cmr.repository.ContentIOException;
import org.alfresco.service.cmr.repository.ContentReader;
import org.alfresco.service.cmr.repository.ContentStreamListener;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.util.FileCopyUtils;
/**
* Implements all the convenience methods of the interface. The only methods
* that need to be implemented, i.e. provide low-level content access are:
* <ul>
* <li>{@link #getDirectReadableChannel()} to read content from the repository</li>
* </ul>
*
* @author Derek Hulley
*/
public abstract class AbstractContentReader extends AbstractContentAccessor implements ContentReader
{
private static final Log logger = LogFactory.getLog(AbstractContentReader.class);
private List<ContentStreamListener> listeners;
private ReadableByteChannel channel;
/**
* @param contentUrl the content URL - this should be relative to the root of the store
* and not absolute: to enable moving of the stores
*/
protected AbstractContentReader(String contentUrl)
{
super(contentUrl);
listeners = new ArrayList<ContentStreamListener>(2);
}
/**
* Adds the listener after checking that the output stream isn't already in
* use.
*/
public synchronized void addListener(ContentStreamListener listener)
{
if (channel != null)
{
throw new RuntimeException("Channel is already in use");
}
listeners.add(listener);
}
/**
* A factory method for subclasses to implement that will ensure the proper
* implementation of the {@link ContentReader#getReader()} method.
* <p>
* Only the instance need be constructed. The required mimetype, encoding, etc
* will be copied across by this class.
*
* @return Returns a reader onto the location referenced by this instance.
* The instance must <b>always</b> be a new instance.
* @throws ContentIOException
*/
protected abstract ContentReader createReader() throws ContentIOException;
/**
* Performs checks and copies required reader attributes
*/
public final ContentReader getReader() throws ContentIOException
{
ContentReader reader = createReader();
if (reader == null)
{
throw new AlfrescoRuntimeException("ContentReader failed to create new reader: \n" +
" reader: " + this);
}
else if (reader.getContentUrl() == null || !reader.getContentUrl().equals(getContentUrl()))
{
throw new AlfrescoRuntimeException("ContentReader has different URL: \n" +
" reader: " + this + "\n" +
" new reader: " + reader);
}
// copy across common attributes
reader.setMimetype(this.getMimetype());
reader.setEncoding(this.getEncoding());
// done
if (logger.isDebugEnabled())
{
logger.debug("Reader spawned new reader: \n" +
" reader: " + this + "\n" +
" new reader: " + reader);
}
return reader;
}
/**
* An automatically created listener sets the flag
*/
public synchronized final boolean isClosed()
{
if (channel != null)
{
return !channel.isOpen();
}
else
{
return false;
}
}
/**
* Provides low-level access to read content from the repository.
* <p>
* This is the only of the content <i>reading</i> methods that needs to be implemented
* by derived classes. All other content access methods make use of this in their
* underlying implementations.
*
* @return Returns a channel from which content can be read
* @throws ContentIOException if the channel could not be opened or the underlying content
* has disappeared
*/
protected abstract ReadableByteChannel getDirectReadableChannel() throws ContentIOException;
/**
* Optionally override to supply an alternate callback channel.
*
* @param directChannel the result of {@link #getDirectReadableChannel()}
* @param listeners the listeners to call
* @return Returns a channel
* @throws ContentIOException
*/
protected ReadableByteChannel getCallbackReadableChannel(
ReadableByteChannel directChannel,
List<ContentStreamListener> listeners)
throws ContentIOException
{
// introduce an advistor to handle the callbacks to the listeners
ByteChannelCallbackAdvise advise = new ByteChannelCallbackAdvise(listeners);
ProxyFactory proxyFactory = new ProxyFactory(directChannel);
proxyFactory.addAdvice(advise);
ReadableByteChannel callbackChannel = (ReadableByteChannel) proxyFactory.getProxy();
// done
return callbackChannel;
}
/**
* @see #getDirectReadableChannel()
* @see #getCallbackReadableChannel()
*/
public synchronized final ReadableByteChannel getReadableChannel() throws ContentIOException
{
// this is a use-once object
if (channel != null)
{
throw new RuntimeException("A channel has already been opened");
}
ReadableByteChannel directChannel = getDirectReadableChannel();
channel = getCallbackReadableChannel(directChannel, listeners);
// done
if (logger.isDebugEnabled())
{
logger.debug("Opened channel onto content: " + this);
}
return channel;
}
/**
* @see Channels#newInputStream(java.nio.channels.ReadableByteChannel)
*/
public InputStream getContentInputStream() throws ContentIOException
{
try
{
ReadableByteChannel channel = getReadableChannel();
InputStream is = new BufferedInputStream(Channels.newInputStream(channel));
// done
return is;
}
catch (Throwable e)
{
throw new ContentIOException("Failed to open stream onto channel: \n" +
" accessor: " + this,
e);
}
}
/**
* Copies the {@link #getContentInputStream() input stream} to the given
* <code>OutputStream</code>
*/
public final void getContent(OutputStream os) throws ContentIOException
{
try
{
InputStream is = getContentInputStream();
FileCopyUtils.copy(is, os); // both streams are closed
// done
}
catch (IOException e)
{
throw new ContentIOException("Failed to copy content to output stream: \n" +
" accessor: " + this,
e);
}
}
public final void getContent(File file) throws ContentIOException
{
try
{
InputStream is = getContentInputStream();
FileOutputStream os = new FileOutputStream(file);
FileCopyUtils.copy(is, os); // both streams are closed
// done
}
catch (IOException e)
{
throw new ContentIOException("Failed to copy content to file: \n" +
" accessor: " + this + "\n" +
" file: " + file,
e);
}
}
public final String getContentString(int length) throws ContentIOException
{
if (length < 0 || length > Integer.MAX_VALUE)
{
throw new IllegalArgumentException("Character count must be positive and within range");
}
Reader reader = null;
try
{
// just create buffer of the required size
char[] buffer = new char[length];
String encoding = getEncoding();
// create a reader from the input stream
if (encoding == null)
{
reader = new InputStreamReader(getContentInputStream());
}
else
{
reader = new InputStreamReader(getContentInputStream(), encoding);
}
// read it all, if possible
int count = reader.read(buffer, 0, length);
// there may have been fewer characters - create a new string
String result = new String(buffer, 0, count);
// done
return result;
}
catch (IOException e)
{
throw new ContentIOException("Failed to copy content to string: \n" +
" accessor: " + this + "\n" +
" length: " + length,
e);
}
finally
{
if (reader != null)
{
try { reader.close(); } catch (Throwable e) { logger.error(e); }
}
}
}
/**
* Makes use of the encoding, if available, to convert bytes to a string.
* <p>
* All the content is streamed into memory. So, like the interface said,
* be careful with this method.
*
* @see ContentAccessor#getEncoding()
*/
public final String getContentString() throws ContentIOException
{
try
{
// read from the stream into a byte[]
InputStream is = getContentInputStream();
ByteArrayOutputStream os = new ByteArrayOutputStream();
FileCopyUtils.copy(is, os); // both streams are closed
byte[] bytes = os.toByteArray();
// get the encoding for the string
String encoding = getEncoding();
// create the string from the byte[] using encoding if necessary
String content = (encoding == null) ? new String(bytes) : new String(bytes, encoding);
// done
return content;
}
catch (IOException e)
{
throw new ContentIOException("Failed to copy content to string: \n" +
" accessor: " + this,
e);
}
}
}

View File

@@ -0,0 +1,115 @@
/*
* 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.util.Calendar;
import java.util.GregorianCalendar;
import org.alfresco.error.AlfrescoRuntimeException;
import org.alfresco.service.cmr.repository.ContentIOException;
import org.alfresco.service.cmr.repository.ContentReader;
import org.alfresco.util.GUID;
/**
* Base class providing support for different types of content stores.
* <p>
* Since content URLs have to be consistent across all stores for
* reasons of replication and backup, the most important functionality
* provided is the generation of new content URLs and the checking of
* existing URLs.
*
* @author Derek Hulley
*/
public abstract class AbstractContentStore implements ContentStore
{
/**
* Simple implementation that uses the
* {@link ContentReader#exists() reader's exists} method as its implementation.
*/
public boolean exists(String contentUrl) throws ContentIOException
{
ContentReader reader = getReader(contentUrl);
return reader.exists();
}
/**
* Creates a new content URL. This must be supported by all
* stores that are compatible with Alfresco.
*
* @return Returns a new and unique content URL
*/
public static String createNewUrl()
{
Calendar calendar = new GregorianCalendar();
int year = calendar.get(Calendar.YEAR);
int month = calendar.get(Calendar.MONTH) + 1; // 0-based
int day = calendar.get(Calendar.DAY_OF_MONTH);
// create the URL
StringBuilder sb = new StringBuilder(20);
sb.append(STORE_PROTOCOL)
.append(year).append('/')
.append(month).append('/')
.append(day).append('/')
.append(GUID.generate()).append(".bin");
String newContentUrl = sb.toString();
// done
return newContentUrl;
}
/**
* This method can be used to ensure that URLs conform to the
* required format. If subclasses have to parse the URL,
* then a call to this may not be required - provided that
* the format is checked.
* <p>
* The protocol part of the URL (including legacy protocols)
* is stripped out and just the relative path is returned.
*
* @param contentUrl a URL of the content to check
* @return Returns the relative part of the URL
* @throws RuntimeException if the URL is not correct
*/
public static String getRelativePart(String contentUrl) throws RuntimeException
{
int index = 0;
if (contentUrl.startsWith(STORE_PROTOCOL))
{
index = 8;
}
else if (contentUrl.startsWith("file://"))
{
index = 7;
}
else
{
throw new AlfrescoRuntimeException(
"All content URLs must start with " + STORE_PROTOCOL + ": \n" +
" the invalid url is: " + contentUrl);
}
// extract the relative part of the URL
String path = contentUrl.substring(index);
// more extensive checks can be added in, but it seems overkill
if (path.length() < 10)
{
throw new AlfrescoRuntimeException(
"The content URL is invalid: \n" +
" content url: " + contentUrl);
}
return path;
}
}

View File

@@ -0,0 +1,315 @@
/*
* 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.BufferedOutputStream;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.channels.Channels;
import java.nio.channels.WritableByteChannel;
import java.util.ArrayList;
import java.util.List;
import org.alfresco.error.AlfrescoRuntimeException;
import org.alfresco.service.cmr.repository.ContentAccessor;
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.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.util.FileCopyUtils;
/**
* Implements all the convenience methods of the interface. The only methods
* that need to be implemented, i.e. provide low-level content access are:
* <ul>
* <li>{@link #getDirectWritableChannel()} to write content to the repository</li>
* </ul>
*
* @author Derek Hulley
*/
public abstract class AbstractContentWriter extends AbstractContentAccessor implements ContentWriter
{
private static final Log logger = LogFactory.getLog(AbstractContentWriter.class);
private List<ContentStreamListener> listeners;
private WritableByteChannel channel;
private ContentReader existingContentReader;
/**
* @param contentUrl the content URL
* @param existingContentReader a reader of a previous version of this content
*/
protected AbstractContentWriter(String contentUrl, ContentReader existingContentReader)
{
super(contentUrl);
this.existingContentReader = existingContentReader;
listeners = new ArrayList<ContentStreamListener>(2);
}
/**
* @return Returns a reader onto the previous version of this content
*/
protected ContentReader getExistingContentReader()
{
return existingContentReader;
}
/**
* Adds the listener after checking that the output stream isn't already in
* use.
*/
public synchronized void addListener(ContentStreamListener listener)
{
if (channel != null)
{
throw new RuntimeException("Channel is already in use");
}
listeners.add(listener);
}
/**
* A factory method for subclasses to implement that will ensure the proper
* implementation of the {@link ContentWriter#getReader()} method.
* <p>
* Only the instance need be constructed. The required mimetype, encoding, etc
* will be copied across by this class.
* <p>
*
* @return Returns a reader onto the location referenced by this instance.
* The instance must <b>always</b> be a new instance and never null.
* @throws ContentIOException
*/
protected abstract ContentReader createReader() throws ContentIOException;
/**
* Performs checks and copies required reader attributes
*/
public final ContentReader getReader() throws ContentIOException
{
if (!isClosed())
{
return null;
}
ContentReader reader = createReader();
if (reader == null)
{
throw new AlfrescoRuntimeException("ContentReader failed to create new reader: \n" +
" writer: " + this);
}
else if (reader.getContentUrl() == null || !reader.getContentUrl().equals(getContentUrl()))
{
throw new AlfrescoRuntimeException("ContentReader has different URL: \n" +
" writer: " + this + "\n" +
" new reader: " + reader);
}
// copy across common attributes
reader.setMimetype(this.getMimetype());
reader.setEncoding(this.getEncoding());
// done
if (logger.isDebugEnabled())
{
logger.debug("Writer spawned new reader: \n" +
" writer: " + this + "\n" +
" new reader: " + reader);
}
return reader;
}
/**
* An automatically created listener sets the flag
*/
public synchronized final boolean isClosed()
{
if (channel != null)
{
return !channel.isOpen();
}
else
{
return false;
}
}
/**
* Provides low-level access to write content to the repository.
* <p>
* This is the only of the content <i>writing</i> methods that needs to be implemented
* by derived classes. All other content access methods make use of this in their
* underlying implementations.
*
* @return Returns a channel with which to write content
* @throws ContentIOException if the channel could not be opened
*/
protected abstract WritableByteChannel getDirectWritableChannel() throws ContentIOException;
/**
* Optionally override to supply an alternate callback channel.
*
* @param directChannel the result of {@link #getDirectWritableChannel()}
* @param listeners the listeners to call
* @return Returns a callback channel
* @throws ContentIOException
*/
protected WritableByteChannel getCallbackWritableChannel(
WritableByteChannel directChannel,
List<ContentStreamListener> listeners)
throws ContentIOException
{
// proxy to add an advise
ByteChannelCallbackAdvise advise = new ByteChannelCallbackAdvise(listeners);
ProxyFactory proxyFactory = new ProxyFactory(directChannel);
proxyFactory.addAdvice(advise);
WritableByteChannel callbackChannel = (WritableByteChannel) proxyFactory.getProxy();
// done
return callbackChannel;
}
/**
* @see #getDirectWritableChannel()
* @see #getCallbackWritableChannel()
*/
public synchronized final WritableByteChannel getWritableChannel() throws ContentIOException
{
// this is a use-once object
if (channel != null)
{
throw new RuntimeException("A channel has already been opened");
}
WritableByteChannel directChannel = getDirectWritableChannel();
channel = getCallbackWritableChannel(directChannel, listeners);
// done
if (logger.isDebugEnabled())
{
logger.debug("Opened channel onto content: \n" +
" content: " + this + "\n" +
" channel: " + channel);
}
return channel;
}
/**
* @see Channels#newOutputStream(java.nio.channels.WritableByteChannel)
*/
public OutputStream getContentOutputStream() throws ContentIOException
{
try
{
WritableByteChannel channel = getWritableChannel();
OutputStream is = new BufferedOutputStream(Channels.newOutputStream(channel));
// done
return is;
}
catch (Throwable e)
{
throw new ContentIOException("Failed to open stream onto channel: \n" +
" writer: " + this,
e);
}
}
/**
* @see ContentReader#getContentInputStream()
* @see #putContent(InputStream)
*/
public void putContent(ContentReader reader) throws ContentIOException
{
try
{
// get the stream to read from
InputStream is = reader.getContentInputStream();
// put the content
putContent(is);
}
catch (Throwable e)
{
throw new ContentIOException("Failed to copy reader content to writer: \n" +
" writer: " + this + "\n" +
" source reader: " + reader,
e);
}
}
public final void putContent(InputStream is) throws ContentIOException
{
try
{
OutputStream os = getContentOutputStream();
FileCopyUtils.copy(is, os); // both streams are closed
// done
}
catch (IOException e)
{
throw new ContentIOException("Failed to copy content from input stream: \n" +
" writer: " + this,
e);
}
}
public final void putContent(File file) throws ContentIOException
{
try
{
OutputStream os = getContentOutputStream();
FileInputStream is = new FileInputStream(file);
FileCopyUtils.copy(is, os); // both streams are closed
// done
}
catch (IOException e)
{
throw new ContentIOException("Failed to copy content from file: \n" +
" writer: " + this + "\n" +
" file: " + file,
e);
}
}
/**
* Makes use of the encoding, if available, to convert the string to bytes.
*
* @see ContentAccessor#getEncoding()
*/
public final void putContent(String content) throws ContentIOException
{
try
{
// attempt to use the correct encoding
String encoding = getEncoding();
byte[] bytes = (encoding == null) ? content.getBytes() : content.getBytes(encoding);
// get the stream
OutputStream os = getContentOutputStream();
ByteArrayInputStream is = new ByteArrayInputStream(bytes);
FileCopyUtils.copy(is, os); // both streams are closed
// done
}
catch (IOException e)
{
throw new ContentIOException("Failed to copy content from string: \n" +
" writer: " + this +
" content length: " + content.length(),
e);
}
}
}

View File

@@ -0,0 +1,39 @@
/*
* 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 org.alfresco.repo.policy.ClassPolicy;
import org.alfresco.service.cmr.repository.NodeRef;
/**
* Content service policies interface
*
* @author Roy Wetherall
*/
public interface ContentServicePolicies
{
/**
* On content update policy interface
*/
public interface OnContentUpdatePolicy extends ClassPolicy
{
/**
* @param nodeRef the node reference
*/
public void onContentUpdate(NodeRef nodeRef);
}
}

View File

@@ -0,0 +1,134 @@
/*
* 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.util.Set;
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;
/**
* Provides low-level retrieval of content
* {@link org.alfresco.service.cmr.repository.ContentReader readers} and
* {@link org.alfresco.service.cmr.repository.ContentWriter writers}.
* <p>
* Implementations of this interface should be soley responsible for
* providing persistence and retrieval of the content against a
* <code>content URL</code>.
* <p>
* The URL format is <b>store://year/month/day/GUID.bin</b> <br>
* <ul>
* <li> <b>store://</b>: prefix identifying an Alfresco content stores
* regardless of the persistence mechanism. </li>
* <li> <b>year</b>: year </li>
* <li> <b>month</b>: 1-based month of the year </li>
* <li> <b>day</b>: 1-based day of the month </li>
* <li> <b>GUID</b>: A unique identifier </li>
* </ul>
* The old <b>file://</b> prefix must still be supported - and functionality
* around this can be found in the {@link org.alfresco.repo.content.AbstractContentStore}
* implementation.
*
* @author Derek Hulley
*/
public interface ContentStore
{
/** <b>store://</b> is the new prefix for all content URLs */
public static final String STORE_PROTOCOL = "store://";
/**
* Check for the existence of content in the store.
* <p>
* The implementation of this may be more efficient than first getting a
* reader to {@link ContentReader#exists() check for existence}, although
* that check should also be performed.
*
* @param contentUrl the path to the content
* @return Returns true if the content exists.
* @throws ContentIOException
*
* @see ContentReader#exists()
*/
public boolean exists(String contentUrl) throws ContentIOException;
/**
* Get the accessor with which to read from the content
* at the given URL. The reader is <b>stateful</b> and
* can <b>only be used once</b>.
*
* @param contentUrl the path to where the content is located
* @return Returns a read-only content accessor for the given URL. There may
* be no content at the given URL, but the reader must still be returned.
* The reader may implement the {@link RandomAccessContent random access interface}.
* @throws ContentIOException
*
* @see #exists(String)
* @see ContentReader#exists()
*/
public ContentReader getReader(String contentUrl) throws ContentIOException;
/**
* Get an accessor with which to write content to a location
* within the store. The writer is <b>stateful</b> and can
* <b>only be used once</b>. The location may be specified but must, in that case,
* be a valid and unused URL.
* <p>
* By supplying a reader to existing content, the store implementation may
* enable {@link RandomAccessContent random access}. The store implementation
* can enable this by copying the existing content into the new location
* before supplying a writer onto the new content.
*
* @param existingContentReader a reader onto any existing content for which
* a writer is required - may be null
* @param newContentUrl an unused, valid URL to use - may be null.
* @return Returns a write-only content accessor, possibly implementing
* the {@link RandomAccessContent random access interface}
* @throws ContentIOException if completely new content storage could not be
* created
*
* @see ContentWriter#addListener(ContentStreamListener)
* @see ContentWriter#getContentUrl()
*/
public ContentWriter getWriter(ContentReader existingContentReader, String newContentUrl) throws ContentIOException;
/**
* Get a set of all content in the store
*
* @return Returns a complete set of the unique URLs of all available content
* in the store
* @throws ContentIOException
*/
public Set<String> getUrls() throws ContentIOException;
/**
* Deletes the content at the given URL.
* <p>
* A delete cannot be forced since it is much better to have the
* file remain longer than desired rather than deleted prematurely.
* The store implementation should safeguard files for certain
* minimum period, in which case all files younger than a certain
* age will not be deleted.
*
* @param contentUrl the URL of the content to delete
* @return Return true if the content was deleted (either by this or
* another operation), otherwise false
* @throws ContentIOException
*/
public boolean delete(String contentUrl) throws ContentIOException;
}

View File

@@ -0,0 +1,117 @@
/*
* 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.util.Set;
import org.alfresco.error.AlfrescoRuntimeException;
import org.alfresco.service.cmr.repository.ContentReader;
import org.alfresco.service.cmr.search.SearchService;
import org.quartz.Job;
import org.quartz.JobDataMap;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
/**
* Removes all content form the store that is not referenced by any content node.
* <p>
* The following parameters are required:
* <ul>
* <li><b>contentStore</b>: The content store bean to clean up</li>
* <li><b>searcher</b>: The index searcher that searches for content in the store</li>
* <li><b>protectHours</b>: The number of hours to protect content that isn't referenced</li>
* </ul>
*
* @author Derek Hulley
*/
public class ContentStoreCleanupJob implements Job
{
/**
* Gets all content URLs from the store, checks if it is in use by any node
* and deletes those that aren't.
*/
public void execute(JobExecutionContext context) throws JobExecutionException
{
JobDataMap jobData = context.getJobDetail().getJobDataMap();
// extract the content store to use
Object contentStoreObj = jobData.get("contentStore");
if (contentStoreObj == null || !(contentStoreObj instanceof ContentStore))
{
throw new AlfrescoRuntimeException(
"ContentStoreCleanupJob data must contain valid 'contentStore' reference");
}
ContentStore contentStore = (ContentStore) contentStoreObj;
// extract the search to use
Object searcherObj = jobData.get("searcher");
if (searcherObj == null || !(searcherObj instanceof SearchService))
{
throw new AlfrescoRuntimeException(
"ContentStoreCleanupJob data must contain valid 'searcher' reference");
}
SearchService searcher = (SearchService) searcherObj;
// get the number of hourse to protect content
Object protectHoursObj = jobData.get("protectHours");
if (protectHoursObj == null || !(protectHoursObj instanceof String))
{
throw new AlfrescoRuntimeException(
"ContentStoreCleanupJob data must contain valid 'protectHours' value");
}
long protectHours = 24L;
try
{
protectHours = Long.parseLong((String) protectHoursObj);
}
catch (NumberFormatException e)
{
throw new AlfrescoRuntimeException(
"ContentStoreCleanupJob data 'protectHours' value is not a valid integer");
}
long protectMillis = protectHours * 3600L * 1000L; // 3600s in an hour; 1000ms in a second
long now = System.currentTimeMillis();
long lastModifiedSafeTimeMs = (now - protectMillis); // able to remove anything modified before this
// get all URLs in the store
Set<String> contentUrls = contentStore.getUrls();
for (String contentUrl : contentUrls)
{
// TODO here we need to get hold of all the orphaned content in this store
// not found - it is not in the repo, but check that it is old enough to delete
ContentReader reader = contentStore.getReader(contentUrl);
if (reader == null || !reader.exists())
{
// gone missing in the meantime
continue;
}
long lastModified = reader.getLastModified();
if (lastModified >= lastModifiedSafeTimeMs)
{
// not old enough
continue;
}
// it is not in the repo and is old enough
boolean result = contentStore.delete(contentUrl);
System.out.println(contentUrl + ": " + Boolean.toString(result));
}
// TODO for now throw this exception to ensure that this job does not get run until
// the orphaned content can be correctly retrieved
throw new UnsupportedOperationException();
}
}

View File

@@ -0,0 +1,114 @@
/*
* 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.util.Date;
import junit.framework.TestSuite;
import org.alfresco.service.cmr.repository.ContentReader;
import org.alfresco.service.cmr.repository.ContentWriter;
import org.alfresco.util.BaseSpringTest;
import org.quartz.JobDataMap;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.quartz.Scheduler;
import org.quartz.SchedulerFactory;
import org.quartz.impl.StdSchedulerFactory;
import org.quartz.impl.calendar.BaseCalendar;
import org.quartz.spi.TriggerFiredBundle;
import org.springframework.scheduling.quartz.SimpleTriggerBean;
/**
* Content store cleanup job unit test
*
* @author Roy Wetherall
*/
public class ContentStoreCleanupJobTest extends BaseSpringTest
{
private SimpleTriggerBean simpleTriggerBean;
private JobExecutionContext jobExecutionContext;
private ContentStoreCleanupJob job;
private ContentStore contentStore;
private String url;
/**
* This can be removed once the class being tested actually has a remote
* chance of working.
*/
public static TestSuite suite()
{
return new TestSuite();
}
/**
* On setup in transaction
*/
@Override
protected void onSetUpInTransaction() throws Exception
{
this.contentStore = (ContentStore)this.applicationContext.getBean("fileContentStore");
this.simpleTriggerBean = (SimpleTriggerBean)this.applicationContext.getBean("fileContentStoreCleanerTrigger");
SchedulerFactory factory = new StdSchedulerFactory();
Scheduler scheduler = factory.getScheduler();
// Set the protect hours to 0 for the purpose of this test
JobDataMap jobDataMap = this.simpleTriggerBean.getJobDetail().getJobDataMap();
jobDataMap.put("protectHours", "0");
this.simpleTriggerBean.getJobDetail().setJobDataMap(jobDataMap);
this.job = new ContentStoreCleanupJob();
TriggerFiredBundle triggerFiredBundle = new TriggerFiredBundle(
this.simpleTriggerBean.getJobDetail(),
this.simpleTriggerBean,
new BaseCalendar(),
false,
new Date(),
new Date(),
new Date(),
new Date());
this.jobExecutionContext = new JobExecutionContext(scheduler, triggerFiredBundle, job);
ContentWriter contentWriter = this.contentStore.getWriter(null, null);
contentWriter.putContent("This is some content that I am going to delete.");
this.url = contentWriter.getContentUrl();
}
/**
* Test execute method
*/
public void testExecute()
{
try
{
ContentReader before = this.contentStore.getReader(this.url);
assertTrue(before.exists());
this.job.execute(this.jobExecutionContext);
ContentReader after = this.contentStore.getReader(this.url);
assertFalse(after.exists());
}
catch (JobExecutionException exception)
{
fail("Exception raised!");
}
}
}

View File

@@ -0,0 +1,250 @@
/*
* 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.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.alfresco.config.Config;
import org.alfresco.config.ConfigElement;
import org.alfresco.config.ConfigLookupContext;
import org.alfresco.config.ConfigService;
import org.alfresco.error.AlfrescoRuntimeException;
import org.alfresco.service.cmr.repository.MimetypeService;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
* Provides a bidirectional mapping between well-known mimetypes and
* the registered file extensions. All mimetypes and extensions
* are stored and handled as lowercase.
*
* @author Derek Hulley
*/
public class MimetypeMap implements MimetypeService
{
public static final String MIMETYPE_TEXT_PLAIN = "text/plain";
public static final String MIMETYPE_TEXT_CSS = "text/css";
public static final String MIMETYPE_XML = "text/xml";
public static final String MIMETYPE_HTML = "text/html";
public static final String MIMETYPE_PDF = "application/pdf";
public static final String MIMETYPE_WORD = "application/msword";
public static final String MIMETYPE_EXCEL = "application/vnd.excel";
public static final String MIMETYPE_BINARY = "application/octet-stream";
public static final String MIMETYPE_PPT = "application/vnd.powerpoint";
public static final String MIMETYPE_FLASH = "application/x-shockwave-flash";
public static final String MIMETYPE_IMAGE_GIF = "image/gif";
public static final String MIMETYPE_IMAGE_JPEG = "image/jpeg";
public static final String MIMETYPE_IMAGE_RGB = "image/x-rgb";
public static final String MIMETYPE_OPENDOCUMENT_TEXT = "application/vnd.oasis.opendocument.text";
public static final String MIMETYPE_OPENDOCUMENT_TEXT_TEMPLATE = "application/vnd.oasis.opendocument.text-template";
public static final String MIMETYPE_OPENDOCUMENT_GRAPHICS = "application/vnd.oasis.opendocument.graphics";
public static final String MIMETYPE_OPENDOCUMENT_GRAPHICS_TEMPLATE= "application/vnd.oasis.opendocument.graphics-template";
public static final String MIMETYPE_OPENDOCUMENT_PRESENTATION= "application/vnd.oasis.opendocument.presentation";
public static final String MIMETYPE_OPENDOCUMENT_PRESENTATION_TEMPLATE= "application/vnd.oasis.opendocument.presentation-template";
public static final String MIMETYPE_OPENDOCUMENT_SPREADSHEET= "application/vnd.oasis.opendocument.spreadsheet";
public static final String MIMETYPE_OPENDOCUMENT_SPREADSHEET_TEMPLATE= "application/vnd.oasis.opendocument.spreadsheet-template";
public static final String MIMETYPE_OPENDOCUMENT_CHART= "application/vnd.oasis.opendocument.chart";
public static final String MIMETYPE_OPENDOCUMENT_CHART_TEMPLATE= "applicationvnd.oasis.opendocument.chart-template";
public static final String MIMETYPE_OPENDOCUMENT_IMAGE= "application/vnd.oasis.opendocument.image";
public static final String MIMETYPE_OPENDOCUMENT_IMAGE_TEMPLATE= "applicationvnd.oasis.opendocument.image-template";
public static final String MIMETYPE_OPENDOCUMENT_FORMULA= "application/vnd.oasis.opendocument.formula";
public static final String MIMETYPE_OPENDOCUMENT_FORMULA_TEMPLATE= "applicationvnd.oasis.opendocument.formula-template";
public static final String MIMETYPE_OPENDOCUMENT_TEXT_MASTER= "application/vnd.oasis.opendocument.text-master";
public static final String MIMETYPE_OPENDOCUMENT_TEXT_WEB= "application/vnd.oasis.opendocument.text-web";
public static final String MIMETYPE_OPENDOCUMENT_DATABASE= "application/vnd.oasis.opendocument.database";
public static final String MIMETYPE_OPENOFFICE_WRITER = "application/vnd.sun.xml.writer";
public static final String MIMETYPE_MP3 = "audio/x-mpeg";
public static final String MIMETYPE_ACP = "application/acp";
private static final String CONFIG_AREA = "mimetype-map";
private static final String CONFIG_CONDITION = "Mimetype Map";
private static final String ELEMENT_MIMETYPES = "mimetypes";
private static final String ATTR_MIMETYPE = "mimetype";
private static final String ATTR_DISPLAY = "display";
private static final String ATTR_DEFAULT = "default";
private static final Log logger = LogFactory.getLog(MimetypeMap.class);
private ConfigService configService;
private List<String> mimetypes;
private Map<String, String> extensionsByMimetype;
private Map<String, String> mimetypesByExtension;
private Map<String, String> displaysByMimetype;
private Map<String, String> displaysByExtension;
/**
* @param configService the config service to use to read mimetypes from
*/
public MimetypeMap(ConfigService configService)
{
this.configService = configService;
}
/**
* Initialises the map using the configuration service provided
*/
public void init()
{
this.mimetypes = new ArrayList<String>(40);
this.extensionsByMimetype = new HashMap<String, String>(59);
this.mimetypesByExtension = new HashMap<String, String>(59);
this.displaysByMimetype = new HashMap<String, String>(59);
this.displaysByExtension = new HashMap<String, String>(59);
Config config = configService.getConfig(CONFIG_CONDITION, new ConfigLookupContext(CONFIG_AREA));
ConfigElement mimetypesElement = config.getConfigElement(ELEMENT_MIMETYPES);
List<ConfigElement> mimetypes = mimetypesElement.getChildren();
int count = 0;
for (ConfigElement mimetypeElement : mimetypes)
{
count++;
// add to list of mimetypes
String mimetype = mimetypeElement.getAttribute(ATTR_MIMETYPE);
if (mimetype == null || mimetype.length() == 0)
{
logger.warn("Ignoring empty mimetype " + count);
continue;
}
// we store it as lowercase
mimetype = mimetype.toLowerCase();
if (this.mimetypes.contains(mimetype))
{
throw new AlfrescoRuntimeException("Duplicate mimetype definition: " + mimetype);
}
this.mimetypes.add(mimetype);
// add to map of mimetype displays
String mimetypeDisplay = mimetypeElement.getAttribute(ATTR_DISPLAY);
if (mimetypeDisplay != null && mimetypeDisplay.length() > 0)
{
this.displaysByMimetype.put(mimetype, mimetypeDisplay);
}
// get all the extensions
boolean isFirst = true;
List<ConfigElement> extensions = mimetypeElement.getChildren();
for (ConfigElement extensionElement : extensions)
{
// add to map of mimetypes by extension
String extension = extensionElement.getValue();
if (extension == null || extension.length() == 0)
{
logger.warn("Ignoring empty extension for mimetype: " + mimetype);
continue;
}
// put to lowercase
extension = extension.toLowerCase();
this.mimetypesByExtension.put(extension, mimetype);
// add to map of extension displays
String extensionDisplay = extensionElement.getAttribute(ATTR_DISPLAY);
if (extensionDisplay != null && extensionDisplay.length() > 0)
{
this.displaysByExtension.put(extension, extensionDisplay);
}
else if (mimetypeDisplay != null && mimetypeDisplay.length() > 0)
{
// no display defined for the extension - use the mimetype's display
this.displaysByExtension.put(extension, mimetypeDisplay);
}
// add to map of extensions by mimetype if it is the default or first extension
String isDefaultStr = extensionElement.getAttribute(ATTR_DEFAULT);
boolean isDefault = Boolean.parseBoolean(isDefaultStr);
if (isDefault || isFirst)
{
this.extensionsByMimetype.put(mimetype, extension);
}
isFirst = false;
}
// check that there were extensions defined
if (extensions.size() == 0)
{
logger.warn("No extensions defined for mimetype: " + mimetype);
}
}
// make the collections read-only
this.mimetypes = Collections.unmodifiableList(this.mimetypes);
this.extensionsByMimetype = Collections.unmodifiableMap(this.extensionsByMimetype);
this.mimetypesByExtension = Collections.unmodifiableMap(this.mimetypesByExtension);
this.displaysByMimetype = Collections.unmodifiableMap(this.displaysByMimetype);
this.displaysByExtension = Collections.unmodifiableMap(this.displaysByExtension);
}
/**
* @param mimetype a valid mimetype
* @return Returns the default extension for the mimetype
* @throws AlfrescoRuntimeException if the mimetype doesn't exist
*/
public String getExtension(String mimetype)
{
String extension = extensionsByMimetype.get(mimetype);
if (extension == null)
{
throw new AlfrescoRuntimeException("No extension available for mimetype: " + mimetype);
}
return extension;
}
public Map<String, String> getDisplaysByExtension()
{
return displaysByExtension;
}
public Map<String, String> getDisplaysByMimetype()
{
return displaysByMimetype;
}
public Map<String, String> getExtensionsByMimetype()
{
return extensionsByMimetype;
}
public List<String> getMimetypes()
{
return mimetypes;
}
public Map<String, String> getMimetypesByExtension()
{
return mimetypesByExtension;
}
/**
* @see #MIMETYPE_BINARY
*/
public String guessMimetype(String filename)
{
filename = filename.toLowerCase();
String mimetype = MIMETYPE_BINARY;
// extract the extension
int index = filename.lastIndexOf('.');
if (index > -1 && (index < filename.length() - 1))
{
String extension = filename.substring(index + 1);
if (mimetypesByExtension.containsKey(extension))
{
mimetype = mimetypesByExtension.get(extension);
}
}
return mimetype;
}
}

View File

@@ -0,0 +1,58 @@
/*
* 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.util.Map;
import org.alfresco.util.BaseSpringTest;
/**
* @see org.alfresco.repo.content.MimetypeMap
*
* @author Derek Hulley
*/
public class MimetypeMapTest extends BaseSpringTest
{
private MimetypeMap mimetypeMap;
public void setMimetypeMap(MimetypeMap mimetypeMap)
{
this.mimetypeMap = mimetypeMap;
}
public void testExtensions() throws Exception
{
Map<String, String> extensionsByMimetype = mimetypeMap.getExtensionsByMimetype();
Map<String, String> mimetypesByExtension = mimetypeMap.getMimetypesByExtension();
// plain text
assertEquals("txt", extensionsByMimetype.get("text/plain"));
assertEquals("text/plain", mimetypesByExtension.get("txt"));
assertEquals("text/plain", mimetypesByExtension.get("csv"));
assertEquals("text/plain", mimetypesByExtension.get("java"));
// JPEG
assertEquals("jpg", extensionsByMimetype.get("image/jpeg"));
assertEquals("image/jpeg", mimetypesByExtension.get("jpg"));
assertEquals("image/jpeg", mimetypesByExtension.get("jpeg"));
assertEquals("image/jpeg", mimetypesByExtension.get("jpe"));
// MS Word
assertEquals("doc", extensionsByMimetype.get("application/msword"));
assertEquals("application/msword", mimetypesByExtension.get("doc"));
}
}

View File

@@ -0,0 +1,50 @@
/*
* 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.nio.channels.FileChannel;
import org.alfresco.service.cmr.repository.ContentIOException;
/**
* Supplementary interface for content readers and writers that allow random-access to
* the underlying content.
* <p>
* The use of this interface by a client <b>may</b> preclude the use of any other
* access to the underlying content - this depends on the underlying implementation.
*
* @author Derek Hulley
*/
public interface RandomAccessContent
{
/**
* @return Returns true if the content can be written to
*/
public boolean canWrite();
/**
* Get a channel to access the content. The channel's behaviour is similar to that
* when a <tt>FileChannel</tt> is retrieved using {@link java.io.RandomAccessFile#getChannel()}.
*
* @return Returns a channel to access the content
* @throws ContentIOException
*
* @see #canWrite()
* @see java.io.RandomAccessFile#getChannel()
*/
public FileChannel getChannel() throws ContentIOException;
}

View File

@@ -0,0 +1,384 @@
/*
* 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.Serializable;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import org.alfresco.error.AlfrescoRuntimeException;
import org.alfresco.repo.content.ContentServicePolicies.OnContentUpdatePolicy;
import org.alfresco.repo.content.filestore.FileContentStore;
import org.alfresco.repo.content.transform.ContentTransformer;
import org.alfresco.repo.content.transform.ContentTransformerRegistry;
import org.alfresco.repo.policy.ClassPolicyDelegate;
import org.alfresco.repo.policy.JavaBehaviour;
import org.alfresco.repo.policy.PolicyComponent;
import org.alfresco.service.cmr.dictionary.DataTypeDefinition;
import org.alfresco.service.cmr.dictionary.DictionaryService;
import org.alfresco.service.cmr.dictionary.InvalidTypeException;
import org.alfresco.service.cmr.dictionary.PropertyDefinition;
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.ContentStreamListener;
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.namespace.NamespaceService;
import org.alfresco.service.namespace.QName;
import org.alfresco.service.transaction.TransactionService;
import org.alfresco.util.EqualsHelper;
import org.alfresco.util.TempFileProvider;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
* A content service that determines at runtime the store that the
* content associated with a node should be routed to.
*
* @author Derek Hulley
*/
public class RoutingContentService implements ContentService
{
private static Log logger = LogFactory.getLog(RoutingContentService.class);
private TransactionService transactionService;
private DictionaryService dictionaryService;
private NodeService nodeService;
/** a registry of all available content transformers */
private ContentTransformerRegistry transformerRegistry;
/** TEMPORARY until we have a map to choose from at runtime */
private ContentStore store;
/** the store for all temporarily created content */
private ContentStore tempStore;
/**
* The policy component
*/
private PolicyComponent policyComponent;
/**
* The onContentService policy delegate
*/
ClassPolicyDelegate<ContentServicePolicies.OnContentUpdatePolicy> onContentUpdateDelegate;
/**
* Default constructor sets up a temporary store
*/
public RoutingContentService()
{
this.tempStore = new FileContentStore(TempFileProvider.getTempDir().getAbsolutePath());
}
public void setTransactionService(TransactionService transactionService)
{
this.transactionService = transactionService;
}
public void setDictionaryService(DictionaryService dictionaryService)
{
this.dictionaryService = dictionaryService;
}
public void setNodeService(NodeService nodeService)
{
this.nodeService = nodeService;
}
public void setTransformerRegistry(ContentTransformerRegistry transformerRegistry)
{
this.transformerRegistry = transformerRegistry;
}
public void setStore(ContentStore store)
{
this.store = store;
}
public void setPolicyComponent(PolicyComponent policyComponent)
{
this.policyComponent = policyComponent;
}
/**
* Service initialise
*/
public void init()
{
// Bind on update properties behaviour
this.policyComponent.bindClassBehaviour(
QName.createQName(NamespaceService.ALFRESCO_URI, "onUpdateProperties"),
this,
new JavaBehaviour(this, "onUpdateProperties"));
// Register on content update policy
this.onContentUpdateDelegate = this.policyComponent.registerClassPolicy(OnContentUpdatePolicy.class);
}
/**
* Update properties policy behaviour
*
* @param nodeRef the node reference
* @param before the before values of the properties
* @param after the after values of the properties
*/
public void onUpdateProperties(
NodeRef nodeRef,
Map<QName, Serializable> before,
Map<QName, Serializable> after)
{
boolean fire = false;
// check if any of the content properties have changed
for (QName propertyQName : after.keySet())
{
// is this a content property?
PropertyDefinition propertyDef = dictionaryService.getProperty(propertyQName);
if (propertyDef == null)
{
// the property is not recognised
continue;
}
if (!propertyDef.getDataType().getName().equals(DataTypeDefinition.CONTENT))
{
// not a content type
continue;
}
try
{
ContentData beforeValue = (ContentData) before.get(propertyQName);
ContentData afterValue = (ContentData) after.get(propertyQName);
if (afterValue != null && afterValue.getContentUrl() == null)
{
// no URL - ignore
}
else if (!EqualsHelper.nullSafeEquals(beforeValue, afterValue))
{
// the content changed
// at the moment, we are only interested in this one change
fire = true;
break;
}
}
catch (ClassCastException e)
{
// properties don't conform to model
continue;
}
}
// fire?
if (fire)
{
// Fire the content update policy
Set<QName> types = new HashSet<QName>(this.nodeService.getAspects(nodeRef));
types.add(this.nodeService.getType(nodeRef));
OnContentUpdatePolicy policy = this.onContentUpdateDelegate.get(types);
policy.onContentUpdate(nodeRef);
}
}
public ContentReader getReader(NodeRef nodeRef, QName propertyQName)
{
// ensure that the node property is of type content
PropertyDefinition contentPropDef = dictionaryService.getProperty(propertyQName);
if (contentPropDef == null || !contentPropDef.getDataType().getName().equals(DataTypeDefinition.CONTENT))
{
throw new InvalidTypeException("The node property must be of type content: \n" +
" node: " + nodeRef + "\n" +
" property name: " + propertyQName + "\n" +
" property type: " + ((contentPropDef == null) ? "unknown" : contentPropDef.getDataType()),
propertyQName);
}
// get the content property
ContentData contentData = (ContentData) nodeService.getProperty(nodeRef, propertyQName);
// check that the URL is available
if (contentData == null || contentData.getContentUrl() == null)
{
// there is no URL - the interface specifies that this is not an error condition
return null;
}
String contentUrl = contentData.getContentUrl();
// TODO: Choose the store to read from at runtime
ContentReader reader = store.getReader(contentUrl);
// set extra data on the reader
reader.setMimetype(contentData.getMimetype());
reader.setEncoding(contentData.getEncoding());
// we don't listen for anything
// result may be null - but interface contract says we may return null
return reader;
}
public ContentWriter getWriter(NodeRef nodeRef, QName propertyQName, boolean update)
{
// check for an existing URL - the get of the reader will perform type checking
ContentReader existingContentReader = getReader(nodeRef, propertyQName);
// TODO: Choose the store to write to at runtime
// get the content using the (potentially) existing content - the new content
// can be wherever the store decides.
ContentWriter writer = store.getWriter(existingContentReader, null);
// set extra data on the reader if the property is pre-existing
ContentData contentData = (ContentData) nodeService.getProperty(nodeRef, propertyQName);
if (contentData != null)
{
writer.setMimetype(contentData.getMimetype());
writer.setEncoding(contentData.getEncoding());
}
// attach a listener if required
if (update)
{
// need a listener to update the node when the stream closes
WriteStreamListener listener = new WriteStreamListener(nodeService, nodeRef, propertyQName, writer);
writer.addListener(listener);
writer.setTransactionService(transactionService);
}
// give back to the client
return writer;
}
/**
* @return Returns a writer to an anonymous location
*/
public ContentWriter getTempWriter()
{
// there is no existing content and we don't specify the location of the new content
return tempStore.getWriter(null, null);
}
/**
* @see org.alfresco.repo.content.transform.ContentTransformerRegistry
* @see org.alfresco.repo.content.transform.ContentTransformer
*/
public void transform(ContentReader reader, ContentWriter writer)
throws NoTransformerException, ContentIOException
{
// check that source and target mimetypes are available
String sourceMimetype = reader.getMimetype();
if (sourceMimetype == null)
{
throw new AlfrescoRuntimeException("The content reader mimetype must be set: " + reader);
}
String targetMimetype = writer.getMimetype();
if (targetMimetype == null)
{
throw new AlfrescoRuntimeException("The content writer mimetype must be set: " + writer);
}
// look for a transformer
ContentTransformer transformer = transformerRegistry.getTransformer(sourceMimetype, targetMimetype);
if (transformer == null)
{
throw new NoTransformerException(sourceMimetype, targetMimetype);
}
// we have a transformer, so do it
transformer.transform(reader, writer);
// done
}
/**
* @see org.alfresco.repo.content.transform.ContentTransformerRegistry
* @see org.alfresco.repo.content.transform.ContentTransformer
*/
public boolean isTransformable(ContentReader reader, ContentWriter writer)
{
// check that source and target mimetypes are available
String sourceMimetype = reader.getMimetype();
if (sourceMimetype == null)
{
throw new AlfrescoRuntimeException("The content reader mimetype must be set: " + reader);
}
String targetMimetype = writer.getMimetype();
if (targetMimetype == null)
{
throw new AlfrescoRuntimeException("The content writer mimetype must be set: " + writer);
}
// look for a transformer
ContentTransformer transformer = transformerRegistry.getTransformer(sourceMimetype, targetMimetype);
return (transformer != null);
}
/**
* Ensures that, upon closure of the output stream, the node is updated with
* the latest URL of the content to which it refers.
* <p>
* The listener close operation does not need a transaction as the
* <code>ContentWriter</code> takes care of that.
*
* @author Derek Hulley
*/
private static class WriteStreamListener implements ContentStreamListener
{
private NodeService nodeService;
private NodeRef nodeRef;
private QName propertyQName;
private ContentWriter writer;
public WriteStreamListener(
NodeService nodeService,
NodeRef nodeRef,
QName propertyQName,
ContentWriter writer)
{
this.nodeService = nodeService;
this.nodeRef = nodeRef;
this.propertyQName = propertyQName;
this.writer = writer;
}
public void contentStreamClosed() throws ContentIOException
{
try
{
// set the full content property
ContentData contentData = writer.getContentData();
nodeService.setProperty(
nodeRef,
propertyQName,
contentData);
// done
if (logger.isDebugEnabled())
{
logger.debug("Stream listener updated node: \n" +
" node: " + nodeRef + "\n" +
" property: " + propertyQName + "\n" +
" value: " + contentData);
}
}
catch (Throwable e)
{
throw new ContentIOException("Failed to set content property on stream closure: \n" +
" node: " + nodeRef + "\n" +
" property: " + propertyQName + "\n" +
" writer: " + writer,
e);
}
}
}
}

View File

@@ -0,0 +1,585 @@
/*
* 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()
{
authenticationComponent.clearCurrentSecurityContext();
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, null);
// 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());
// check the indexing doesn't spank everthing
setComplete();
endTransaction();
// now attempt to get the reader for the node
ContentReader reader = contentService.getReader(contentNodeRef, ContentModel.PROP_CONTENT);
}
/**
* 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;
/**
* 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);
// 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)
{
assertEquals(this.contentNodeRef, nodeRef);
assertTrue(this.nodeService.hasAspect(nodeRef, ContentModel.ASPECT_VERSIONABLE));
this.policyFired = 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();
reader = contentService.getReader(contentNodeRef, ContentModel.PROP_CONTENT);
assertNull("Transaction was rolled back - no content should be visible", reader);
}
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
{
// 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
}
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);
}
/**
* 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()
{
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;
}
}
}
}

View File

@@ -0,0 +1,235 @@
/*
* 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.filestore;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.channels.FileChannel;
import java.nio.channels.ReadableByteChannel;
import java.text.MessageFormat;
import java.util.List;
import org.alfresco.error.AlfrescoRuntimeException;
import org.alfresco.repo.content.AbstractContentReader;
import org.alfresco.repo.content.MimetypeMap;
import org.alfresco.repo.content.RandomAccessContent;
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.util.TempFileProvider;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
* Provides direct access to a local file.
* <p>
* This class does not provide remote access to the file.
*
* @author Derek Hulley
*/
public class FileContentReader extends AbstractContentReader implements RandomAccessContent
{
private static final Log logger = LogFactory.getLog(FileContentReader.class);
private File file;
/**
* Checks the existing reader provided and replaces it with a reader onto some
* fake content if required. If the existing reader is invalid, an debug message
* will be logged under this classname category.
* <p>
* It is a convenience method that clients can use to cheaply get a reader that
* is valid, regardless of whether the initial reader is valid.
*
* @param existingReader a potentially valid reader
* @param msgTemplate the template message that will used to format the final <i>fake</i> content
* @param args arguments to put into the <i>fake</i> content
* @return Returns a the existing reader or a new reader onto some generated text content
*/
public static ContentReader getSafeContentReader(ContentReader existingReader, String msgTemplate, Object ... args)
{
ContentReader reader = existingReader;
if (existingReader == null || !existingReader.exists())
{
// the content was never written to the node or the underlying content is missing
String fakeContent = MessageFormat.format(msgTemplate, args);
// log it
if (logger.isDebugEnabled())
{
logger.debug(fakeContent);
}
// fake the content
File tempFile = TempFileProvider.createTempFile("getSafeContentReader_", ".txt");
ContentWriter writer = new FileContentWriter(tempFile);
writer.setMimetype(MimetypeMap.MIMETYPE_TEXT_PLAIN);
writer.setEncoding("UTF-8");
writer.putContent(fakeContent);
// grab the reader from the temp writer
reader = writer.getReader();
}
// done
if (logger.isDebugEnabled())
{
logger.debug("Created safe content reader: \n" +
" existing reader: " + existingReader + "\n" +
" safe reader: " + reader);
}
return reader;
}
/**
* Constructor that builds a URL based on the absolute path of the file.
*
* @param file the file for reading. This will most likely be directly
* related to the content URL.
*/
public FileContentReader(File file)
{
this(file, FileContentStore.STORE_PROTOCOL + file.getAbsolutePath());
}
/**
* Constructor that explicitely sets the URL that the reader represents.
*
* @param file the file for reading. This will most likely be directly
* related to the content URL.
* @param url the relative url that the reader represents
*/
public FileContentReader(File file, String url)
{
super(url);
this.file = file;
}
/**
* @return Returns the file that this reader accesses
*/
public File getFile()
{
return file;
}
public boolean exists()
{
return file.exists();
}
/**
* @see File#length()
*/
public long getSize()
{
if (!exists())
{
return 0L;
}
else
{
return file.length();
}
}
/**
* @see File#lastModified()
*/
public long getLastModified()
{
if (!exists())
{
return 0L;
}
else
{
return file.lastModified();
}
}
/**
* The URL of the write is known from the start and this method contract states
* that no consideration needs to be taken w.r.t. the stream state.
*/
@Override
protected ContentReader createReader() throws ContentIOException
{
return new FileContentReader(this.file, getContentUrl());
}
@Override
protected ReadableByteChannel getDirectReadableChannel() throws ContentIOException
{
try
{
// the file must exist
if (!file.exists())
{
throw new IOException("File does not exist");
}
// create the channel
RandomAccessFile randomAccessFile = new RandomAccessFile(file, "r"); // won't create it
FileChannel channel = randomAccessFile.getChannel();
// done
if (logger.isDebugEnabled())
{
logger.debug("Opened channel to file: " + file);
}
return channel;
}
catch (Throwable e)
{
throw new ContentIOException("Failed to open file channel: " + this, e);
}
}
/**
* @param directChannel a file channel
*/
@Override
protected ReadableByteChannel getCallbackReadableChannel(
ReadableByteChannel directChannel,
List<ContentStreamListener> listeners) throws ContentIOException
{
if (!(directChannel instanceof FileChannel))
{
throw new AlfrescoRuntimeException("Expected read channel to be a file channel");
}
FileChannel fileChannel = (FileChannel) directChannel;
// wrap it
FileChannel callbackChannel = new CallbackFileChannel(fileChannel, listeners);
// done
return callbackChannel;
}
/**
* @return Returns false as this is a reader
*/
public boolean canWrite()
{
return false; // we only allow reading
}
public FileChannel getChannel() throws ContentIOException
{
// go through the super classes to ensure that all concurrency conditions
// and listeners are satisfied
return (FileChannel) super.getReadableChannel();
}
}

View File

@@ -0,0 +1,319 @@
/*
* 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.filestore;
import java.io.File;
import java.io.IOException;
import java.util.HashSet;
import java.util.Set;
import org.alfresco.error.AlfrescoRuntimeException;
import org.alfresco.repo.content.AbstractContentStore;
import org.alfresco.service.cmr.repository.ContentIOException;
import org.alfresco.service.cmr.repository.ContentReader;
import org.alfresco.service.cmr.repository.ContentWriter;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
* Provides a store of node content directly to the file system.
* <p>
* The file names obey, as they must, the URL naming convention
* as specified in the {@link org.alfresco.repo.content.ContentStore}.
*
* @author Derek Hulley
*/
public class FileContentStore extends AbstractContentStore
{
private static final Log logger = LogFactory.getLog(FileContentStore.class);
private File rootDirectory;
private String rootAbsolutePath;
/**
* @param rootDirectory the root under which files will be stored. The
* directory will be created if it does not exist.
*/
public FileContentStore(String rootDirectoryStr)
{
rootDirectory = new File(rootDirectoryStr);
if (!rootDirectory.exists())
{
if (!rootDirectory.mkdirs())
{
throw new ContentIOException("Failed to create store root: " + rootDirectory, null);
}
}
rootDirectory = rootDirectory.getAbsoluteFile();
rootAbsolutePath = rootDirectory.getAbsolutePath();
}
public String toString()
{
StringBuilder sb = new StringBuilder(36);
sb.append("FileContentStore")
.append("[ root=").append(rootDirectory)
.append("]");
return sb.toString();
}
/**
* Generates a new URL and file appropriate to it.
*
* @return Returns a new and unique file
* @throws IOException if the file or parent directories couldn't be created
*/
private File createNewFile() throws IOException
{
String contentUrl = createNewUrl();
return createNewFile(contentUrl);
}
/**
* Creates a file for the specifically provided content URL. The URL may
* not already be in use.
* <p>
* The store prefix is stripped off the URL and the rest of the URL
* used directly to create a file.
*
* @param newContentUrl the specific URL to use, which may not be in use
* @return Returns a new and unique file
* @throws IOException if the file or parent directories couldn't be created or
* if the URL is already in use.
*/
public File createNewFile(String newContentUrl) throws IOException
{
File file = makeFile(newContentUrl);
// create the directory, if it doesn't exist
File dir = file.getParentFile();
if (!dir.exists())
{
dir.mkdirs();
}
// create a new, empty file
boolean created = file.createNewFile();
if (!created)
{
throw new ContentIOException(
"When specifying a URL for new content, the URL may not be in use already. \n" +
" store: " + this + "\n" +
" new URL: " + newContentUrl);
}
// done
return file;
}
/**
* Takes the file absolute path, strips off the root path of the store
* and appends the store URL prefix.
*
* @param file the file from which to create the URL
* @return Returns the equivalent content URL
* @throws Exception
*/
private String makeContentUrl(File file)
{
String path = file.getAbsolutePath();
// check if it belongs to this store
if (!path.startsWith(rootAbsolutePath))
{
throw new AlfrescoRuntimeException(
"File does not fall below the store's root: \n" +
" file: " + file + "\n" +
" store: " + this);
}
// strip off the file separator char, if present
int index = rootAbsolutePath.length();
if (path.charAt(index) == File.separatorChar)
{
index++;
}
// strip off the root path and adds the protocol prefix
String url = AbstractContentStore.STORE_PROTOCOL + path.substring(index);
// replace '\' with '/' so that URLs are consistent across all filesystems
url = url.replace('\\', '/');
// done
return url;
}
/**
* Creates a file from the given relative URL. The URL must start with
* the required {@link FileContentStore#STORE_PROTOCOL protocol prefix}.
*
* @param contentUrl the content URL including the protocol prefix
* @return Returns a file representing the URL - the file may or may not
* exist
*
* @see #checkUrl(String)
*/
private File makeFile(String contentUrl)
{
// take just the part after the protocol
String relativeUrl = getRelativePart(contentUrl);
// get the file
File file = new File(rootDirectory, relativeUrl);
// done
return file;
}
/**
* Performs a direct check against the file for its existence.
*/
@Override
public boolean exists(String contentUrl) throws ContentIOException
{
File file = makeFile(contentUrl);
return file.exists();
}
/**
* This implementation requires that the URL start with
* {@link FileContentStore#STORE_PROTOCOL }.
*/
public ContentReader getReader(String contentUrl)
{
try
{
File file = makeFile(contentUrl);
FileContentReader reader = new FileContentReader(file, contentUrl);
// done
if (logger.isDebugEnabled())
{
logger.debug("Created content reader: \n" +
" url: " + contentUrl + "\n" +
" file: " + file + "\n" +
" reader: " + reader);
}
return reader;
}
catch (Throwable e)
{
throw new ContentIOException("Failed to get reader for URL: " + contentUrl, e);
}
}
/**
* @return Returns a writer onto a location based on the date
*/
public ContentWriter getWriter(ContentReader existingContentReader, String newContentUrl)
{
try
{
File file = null;
String contentUrl = null;
if (newContentUrl == null) // a specific URL was not supplied
{
// get a new file with a new URL
file = createNewFile();
// make a URL
contentUrl = makeContentUrl(file);
}
else // the URL has been given
{
file = createNewFile(newContentUrl);
contentUrl = newContentUrl;
}
// create the writer
FileContentWriter writer = new FileContentWriter(file, contentUrl, existingContentReader);
// done
if (logger.isDebugEnabled())
{
logger.debug("Created content writer: \n" +
" writer: " + writer);
}
return writer;
}
catch (IOException e)
{
throw new ContentIOException("Failed to get writer", e);
}
}
public Set<String> getUrls()
{
// recursively get all files within the root
Set<String> contentUrls = new HashSet<String>(1000);
getUrls(rootDirectory, contentUrls);
// done
if (logger.isDebugEnabled())
{
logger.debug("Listed all content URLS: \n" +
" store: " + this + "\n" +
" count: " + contentUrls.size());
}
return contentUrls;
}
/**
* @param directory the current directory to get the files from
* @param contentUrls the list of current content URLs to add to
* @return Returns a list of all files within the given directory and all subdirectories
*/
private void getUrls(File directory, Set<String> contentUrls)
{
File[] files = directory.listFiles();
if (files == null)
{
// the directory has disappeared
throw new ContentIOException("Failed list files in folder: " + directory);
}
for (File file : files)
{
if (file.isDirectory())
{
// we have a subdirectory - recurse
getUrls(file, contentUrls);
}
else
{
// found a file - create the URL
String contentUrl = makeContentUrl(file);
contentUrls.add(contentUrl);
}
}
}
/**
* Attempts to delete the content. The actual deletion is optional on the interface
* so it just returns the success or failure of the underlying delete.
*/
public boolean delete(String contentUrl) throws ContentIOException
{
// ignore files that don't exist
File file = makeFile(contentUrl);
if (!file.exists())
{
return true;
}
// attempt to delete the file directly
boolean deleted = file.delete();
// done
if (logger.isDebugEnabled())
{
logger.debug("Delete content directly: \n" +
" store: " + this + "\n" +
" url: " + contentUrl);
}
return deleted;
}
}

View File

@@ -0,0 +1,93 @@
/*
* 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.filestore;
import java.io.File;
import org.alfresco.repo.content.AbstractContentReadWriteTest;
import org.alfresco.repo.content.ContentStore;
import org.alfresco.repo.content.MimetypeMap;
import org.alfresco.service.cmr.repository.ContentReader;
import org.alfresco.util.TempFileProvider;
/**
* Tests read and write functionality for the store.
*
* @see org.alfresco.repo.content.filestore.FileContentStore
*
* @author Derek Hulley
*/
public class FileContentStoreTest extends AbstractContentReadWriteTest
{
private FileContentStore store;
@Override
public void setUp() throws Exception
{
super.setUp();
// create a store that uses a subdirectory of the temp directory
File tempDir = TempFileProvider.getTempDir();
store = new FileContentStore(
tempDir.getAbsolutePath() +
File.separatorChar +
getName());
}
@Override
protected ContentStore getStore()
{
return store;
}
public void testGetSafeContentReader() throws Exception
{
String template = "ABC {0}{1}";
String arg0 = "DEF";
String arg1 = "123";
String fakeContent = "ABC DEF123";
// get a good reader
ContentReader reader = getReader();
assertFalse("No content has been written to the URL yet", reader.exists());
// now create a file for it
File file = store.createNewFile(reader.getContentUrl());
assertTrue("File store did not connect new file", file.exists());
assertTrue("Reader did not detect creation of the underlying file", reader.exists());
// remove the underlying content
file.delete();
assertFalse("File not missing", file.exists());
assertFalse("Reader doesn't show missing content", reader.exists());
// make a safe reader
ContentReader safeReader = FileContentReader.getSafeContentReader(reader, template, arg0, arg1);
// check it
assertTrue("Fake content doesn't exist", safeReader.exists());
assertEquals("Fake content incorrect", fakeContent, safeReader.getContentString());
assertEquals("Fake mimetype incorrect", MimetypeMap.MIMETYPE_TEXT_PLAIN, safeReader.getMimetype());
assertEquals("Fake encoding incorrect", "UTF-8", safeReader.getEncoding());
// now repeat with a null reader
reader = null;
safeReader = FileContentReader.getSafeContentReader(reader, template, arg0, arg1);
// check it
assertTrue("Fake content doesn't exist", safeReader.exists());
assertEquals("Fake content incorrect", fakeContent, safeReader.getContentString());
}
}

View File

@@ -0,0 +1,223 @@
/*
* 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.filestore;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.channels.FileChannel;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.WritableByteChannel;
import java.util.List;
import org.alfresco.error.AlfrescoRuntimeException;
import org.alfresco.repo.content.AbstractContentWriter;
import org.alfresco.repo.content.RandomAccessContent;
import org.alfresco.service.cmr.repository.ContentIOException;
import org.alfresco.service.cmr.repository.ContentReader;
import org.alfresco.service.cmr.repository.ContentStreamListener;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
* Provides direct access to a local file.
* <p>
* This class does not provide remote access to the file.
*
* @author Derek Hulley
*/
public class FileContentWriter extends AbstractContentWriter implements RandomAccessContent
{
private static final Log logger = LogFactory.getLog(FileContentWriter.class);
private File file;
/**
* Constructor that builds a URL based on the absolute path of the file.
*
* @param file the file for writing. This will most likely be directly
* related to the content URL.
*/
public FileContentWriter(File file)
{
this(
file,
FileContentStore.STORE_PROTOCOL + file.getAbsolutePath(),
null);
}
/**
* Constructor that builds a URL based on the absolute path of the file.
*
* @param file the file for writing. This will most likely be directly
* related to the content URL.
* @param existingContentReader a reader of a previous version of this content
*/
public FileContentWriter(File file, ContentReader existingContentReader)
{
this(
file,
FileContentStore.STORE_PROTOCOL + file.getAbsolutePath(),
existingContentReader);
}
/**
* Constructor that explicitely sets the URL that the reader represents.
*
* @param file the file for writing. This will most likely be directly
* related to the content URL.
* @param url the relative url that the reader represents
* @param existingContentReader a reader of a previous version of this content
*/
public FileContentWriter(File file, String url, ContentReader existingContentReader)
{
super(url, existingContentReader);
this.file = file;
}
/**
* @return Returns the file that this writer accesses
*/
public File getFile()
{
return file;
}
/**
* @return Returns the size of the underlying file or
*/
public long getSize()
{
if (file == null)
return 0L;
else if (!file.exists())
return 0L;
else
return file.length();
}
/**
* The URL of the write is known from the start and this method contract states
* that no consideration needs to be taken w.r.t. the stream state.
*/
@Override
protected ContentReader createReader() throws ContentIOException
{
return new FileContentReader(this.file, getContentUrl());
}
@Override
protected WritableByteChannel getDirectWritableChannel() throws ContentIOException
{
try
{
// we may not write to an existing file - EVER!!
if (file.exists() && file.length() > 0)
{
throw new IOException("File exists - overwriting not allowed");
}
// create the channel
RandomAccessFile randomAccessFile = new RandomAccessFile(file, "rw"); // will create it
FileChannel channel = randomAccessFile.getChannel();
// done
if (logger.isDebugEnabled())
{
logger.debug("Opened channel to file: " + file);
}
return channel;
}
catch (Throwable e)
{
throw new ContentIOException("Failed to open file channel: " + this, e);
}
}
/**
* @param directChannel a file channel
*/
@Override
protected WritableByteChannel getCallbackWritableChannel(
WritableByteChannel directChannel,
List<ContentStreamListener> listeners) throws ContentIOException
{
if (!(directChannel instanceof FileChannel))
{
throw new AlfrescoRuntimeException("Expected write channel to be a file channel");
}
FileChannel fileChannel = (FileChannel) directChannel;
// wrap it
FileChannel callbackChannel = new CallbackFileChannel(fileChannel, listeners);
// done
return callbackChannel;
}
/**
* @return Returns true always
*/
public boolean canWrite()
{
return true; // this is a writer
}
public FileChannel getChannel() throws ContentIOException
{
/*
* By calling this method, clients indicate that they wish to make random
* changes to the file. It is possible that the client might only want
* to update a tiny proportion of the file - or even none of it. Either
* way, the file must be as whole and complete as before it was accessed.
*/
// go through the super classes to ensure that all concurrency conditions
// and listeners are satisfied
FileChannel channel = (FileChannel) super.getWritableChannel();
// random access means that the the new content's starting point must be
// that of the existing content
ContentReader existingContentReader = getExistingContentReader();
if (existingContentReader != null)
{
ReadableByteChannel existingContentChannel = existingContentReader.getReadableChannel();
long existingContentLength = existingContentReader.getSize();
// copy the existing content
try
{
channel.transferFrom(existingContentChannel, 0, existingContentLength);
// copy complete
if (logger.isDebugEnabled())
{
logger.debug("Copied content for random access: \n" +
" writer: " + this + "\n" +
" existing: " + existingContentReader);
}
}
catch (IOException e)
{
throw new ContentIOException("Failed to copy from existing content to enable random access: \n" +
" writer: " + this + "\n" +
" existing: " + existingContentReader,
e);
}
finally
{
try { existingContentChannel.close(); } catch (IOException e) {}
}
}
// the file is now available for random access
return channel;
}
}

View File

@@ -0,0 +1,114 @@
/*
* 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.filestore;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import junit.framework.TestCase;
/**
* Some tests to check out the </code>java.lang.nio</code> functionality
*
* @author Derek Hulley
*/
public class FileIOTest extends TestCase
{
private static final String TEST_CONTENT = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
private File file;
public FileIOTest(String name)
{
super(name);
}
public void setUp() throws Exception
{
file = File.createTempFile(getName(), ".txt");
OutputStream os = new FileOutputStream(file);
os.write(TEST_CONTENT.getBytes());
os.flush();
os.close();
}
/**
* Attempt to read the same file using multiple channels concurrently
*/
public void testConcurrentFileReads() throws Exception
{
// open the file for a read
FileInputStream isA = new FileInputStream(file);
FileInputStream isB = new FileInputStream(file);
// get the channels
FileChannel channelA = isA.getChannel();
FileChannel channelB = isB.getChannel();
// buffers for reading
ByteBuffer bufferA = ByteBuffer.allocate(10);
ByteBuffer bufferB = ByteBuffer.allocate(10);
// read file into both buffers
int countA = 0;
int countB = 0;
do
{
countA = channelA.read((ByteBuffer)bufferA.clear());
countB = channelB.read((ByteBuffer)bufferB.clear());
assertEquals("Should read same number of bytes", countA, countB);
} while (countA > 6);
// both buffers should be at the same marker 6
assertEquals("BufferA marker incorrect", 6, bufferA.position());
assertEquals("BufferB marker incorrect", 6, bufferB.position());
}
public void testConcurrentReadWrite() throws Exception
{
// open file for a read
FileInputStream isRead = new FileInputStream(file);
// open file for write
FileOutputStream osWrite = new FileOutputStream(file);
// get channels
FileChannel channelRead = isRead.getChannel();
FileChannel channelWrite = osWrite.getChannel();
// buffers
ByteBuffer bufferRead = ByteBuffer.allocate(26);
ByteBuffer bufferWrite = ByteBuffer.wrap(TEST_CONTENT.getBytes());
// read - nothing will be read
int countRead = channelRead.read(bufferRead);
assertEquals("Expected nothing to be read", -1, countRead);
// write
int countWrite = channelWrite.write(bufferWrite);
assertEquals("Not all characters written", 26, countWrite);
// close the write side
channelWrite.close();
// reread
countRead = channelRead.read(bufferRead);
assertEquals("Expected full read", 26, countRead);
}
}

View File

@@ -0,0 +1,97 @@
/*
* Copyright (C) 2005 Jesper Steen M<>ller
*
* 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.metadata;
import java.io.Serializable;
import java.util.Collections;
import java.util.Map;
import java.util.Set;
import org.alfresco.service.namespace.QName;
/**
*
* @author Jesper Steen M<>ller
*/
abstract public class AbstractMetadataExtracter implements MetadataExtracter
{
private Set<String> mimetypes;
private double reliability;
private long extractionTime;
protected AbstractMetadataExtracter(String mimetype, double reliability, long extractionTime)
{
this.mimetypes = Collections.singleton(mimetype);
this.reliability = reliability;
this.extractionTime = extractionTime;
}
protected AbstractMetadataExtracter(Set<String> mimetypes, double reliability, long extractionTime)
{
this.mimetypes = mimetypes;
this.reliability = reliability;
this.extractionTime = extractionTime;
}
public double getReliability(String sourceMimetype)
{
if (mimetypes.contains(sourceMimetype))
return reliability;
else
return 0.0;
}
public long getExtractionTime()
{
return extractionTime;
}
/**
* Examines a value or string for nulls and adds it to the map (if
* non-empty)
*
* @param prop Alfresco's <code>ContentModel.PROP_</code> to set.
* @param value Value to set it to
* @param destination Map into which to set it
* @return true, if set, false otherwise
*/
protected boolean trimPut(QName prop, Object value, Map<QName, Serializable> destination)
{
if (value == null)
return false;
if (value instanceof String)
{
String svalue = ((String) value).trim();
if (svalue.length() > 0)
{
destination.put(prop, svalue);
return true;
}
return false;
}
else if (value instanceof Serializable)
{
destination.put(prop, (Serializable) value);
}
else
{
destination.put(prop, value.toString());
}
return true;
}
}

View File

@@ -0,0 +1,128 @@
/*
* Copyright (C) 2005 Jesper Steen M<>ller
*
* 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.metadata;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.Serializable;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;
import org.alfresco.model.ContentModel;
import org.alfresco.repo.content.MimetypeMap;
import org.alfresco.repo.content.filestore.FileContentReader;
import org.alfresco.service.cmr.repository.ContentReader;
import org.alfresco.service.namespace.QName;
import org.alfresco.util.BaseSpringTest;
import org.alfresco.util.TempFileProvider;
/**
* Provides a base set of tests for testing
* {@link org.alfresco.repo.content.metadata.MetadataExtracter} implementations.
*
* @author Jesper Steen M<>ller
*/
public abstract class AbstractMetadataExtracterTest extends BaseSpringTest
{
protected static final String QUICK_TITLE = "The quick brown fox jumps over the lazy dog";
protected static final String QUICK_DESCRIPTION = "Gym class featuring a brown fox and lazy dog";
protected static final String QUICK_CREATOR = "Nevin Nollop";
protected static final String[] QUICK_WORDS = new String[] { "quick", "brown", "fox", "jumps", "lazy", "dog" };
protected MimetypeMap mimetypeMap;
protected MetadataExtracter transformer;
public final void setMimetypeMap(MimetypeMap mimetypeMap)
{
this.mimetypeMap = mimetypeMap;
}
protected abstract MetadataExtracter getExtracter();
/**
* Ensures that the temp locations are cleaned out before the tests start
*/
@Override
protected void onSetUpInTransaction() throws Exception
{
// perform a little cleaning up
long now = System.currentTimeMillis();
TempFileProvider.TempFileCleanerJob.removeFiles(now);
}
/**
* Check that all objects are present
*/
public void testSetUp() throws Exception
{
assertNotNull("MimetypeMap not present", mimetypeMap);
// check that the quick resources are available
File sourceFile = AbstractMetadataExtracterTest.loadQuickTestFile("txt");
assertNotNull("quick.* files should be available from Tests", sourceFile);
}
/**
* Helper method to load one of the "The quick brown fox" files from the
* classpath.
*
* @param extension the extension of the file required
* @return Returns a test resource loaded from the classpath or
* <tt>null</tt> if no resource could be found.
* @throws IOException
*/
public static File loadQuickTestFile(String extension) throws IOException
{
URL url = AbstractMetadataExtracterTest.class.getClassLoader().getResource("quick/quick." + extension);
if (url == null)
{
return null;
}
File file = new File(url.getFile());
if (!file.exists())
{
return null;
}
return file;
}
public Map<QName, Serializable> extractFromExtension(String ext, String mimetype) throws Exception
{
Map<QName, Serializable> destination = new HashMap<QName, Serializable>();
// attempt to get a source file for each mimetype
File sourceFile = AbstractMetadataExtracterTest.loadQuickTestFile(ext);
if (sourceFile == null)
{
throw new FileNotFoundException("No quick." + ext + " file found for test");
}
// construct a reader onto the source file
ContentReader sourceReader = new FileContentReader(sourceFile);
sourceReader.setMimetype(mimetype);
getExtracter().extract(sourceReader, destination);
return destination;
}
public void testCommonMetadata(Map<QName, Serializable> destination)
{
assertEquals(QUICK_TITLE, destination.get(ContentModel.PROP_TITLE));
assertEquals(QUICK_DESCRIPTION, destination.get(ContentModel.PROP_DESCRIPTION));
assertEquals(QUICK_CREATOR, destination.get(ContentModel.PROP_CREATOR));
}
}

View File

@@ -0,0 +1,173 @@
/*
* Copyright (C) 2005 Jesper Steen M<>ller
*
* 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.metadata;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
import javax.swing.text.ChangedCharSetException;
import javax.swing.text.MutableAttributeSet;
import javax.swing.text.html.HTML;
import javax.swing.text.html.HTMLEditorKit;
import javax.swing.text.html.parser.ParserDelegator;
import org.alfresco.model.ContentModel;
import org.alfresco.repo.content.MimetypeMap;
import org.alfresco.service.cmr.repository.ContentIOException;
import org.alfresco.service.cmr.repository.ContentReader;
import org.alfresco.service.namespace.QName;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
*
* @author Jesper Steen M<>ller
*/
public class HtmlMetadataExtracter extends AbstractMetadataExtracter
{
private static final Log logger = LogFactory.getLog(HtmlMetadataExtracter.class);
public HtmlMetadataExtracter()
{
super(MimetypeMap.MIMETYPE_HTML, 1.0, 1000);
}
public void extract(ContentReader reader, Map<QName, Serializable> destination) throws ContentIOException
{
final Map<QName, Serializable> tempDestination = new HashMap<QName, Serializable>();
try
{
HTMLEditorKit.ParserCallback callback = new HTMLEditorKit.ParserCallback()
{
StringBuffer title = null;
boolean inHead = false;
public void handleText(char[] data, int pos)
{
if (title != null)
{
title.append(data);
}
}
public void handleComment(char[] data, int pos)
{
// Perhaps sniff for Office 9+ metadata in here?
}
public void handleStartTag(HTML.Tag t, MutableAttributeSet a, int pos)
{
if (HTML.Tag.HEAD.equals(t))
{
inHead = true;
}
else if (HTML.Tag.TITLE.equals(t) && inHead)
{
title = new StringBuffer();
}
else
handleSimpleTag(t, a, pos);
}
public void handleEndTag(HTML.Tag t, int pos)
{
if (HTML.Tag.HEAD.equals(t))
{
inHead = false;
}
else if (HTML.Tag.TITLE.equals(t))
{
trimPut(ContentModel.PROP_TITLE, title.toString(), tempDestination);
title = null;
}
}
public void handleSimpleTag(HTML.Tag t, MutableAttributeSet a, int pos)
{
if (HTML.Tag.META.equals(t))
{
Object nameO = a.getAttribute(HTML.Attribute.NAME);
Object valueO = a.getAttribute(HTML.Attribute.CONTENT);
if (nameO == null || valueO == null)
return;
String name = nameO.toString();
if (name.equalsIgnoreCase("creator") || name.equalsIgnoreCase("author")
|| name.equalsIgnoreCase("dc.creator"))
{
trimPut(ContentModel.PROP_CREATOR, valueO, tempDestination);
}
if (name.equalsIgnoreCase("description") || name.equalsIgnoreCase("dc.description"))
{
trimPut(ContentModel.PROP_DESCRIPTION, valueO, tempDestination);
}
}
}
public void handleError(String errorMsg, int pos)
{
}
};
String charsetGuess = "UTF-8";
int tries = 0;
while (tries < 3)
{
tempDestination.clear();
Reader r = null;
InputStream cis = null;
try
{
cis = reader.getContentInputStream();
// TODO: for now, use default charset; we should attempt to map from html meta-data
r = new InputStreamReader(cis);
HTMLEditorKit.Parser parser = new ParserDelegator();
parser.parse(r, callback, tries > 0);
destination.putAll(tempDestination);
break;
}
catch (ChangedCharSetException ccse)
{
tries++;
charsetGuess = ccse.getCharSetSpec();
int begin = charsetGuess.indexOf("charset=");
if (begin > 0)
charsetGuess = charsetGuess.substring(begin + 8, charsetGuess.length());
reader = reader.getReader();
}
finally
{
if (r != null)
r.close();
if (cis != null)
cis.close();
}
}
}
catch (IOException e)
{
throw new ContentIOException("HTML metadata extraction failed: \n" + " reader: " + reader, e);
}
}
}

View File

@@ -0,0 +1,60 @@
/*
* Copyright (C) 2005 Jesper Steen M<>ller
*
* 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.metadata;
import org.alfresco.repo.content.MimetypeMap;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
* @see org.alfresco.repo.content.transform.OfficeMetadataExtracter
* @author Jesper Steen M<>ller
*/
public class HtmlMetadataExtracterTest extends AbstractMetadataExtracterTest
{
private static final Log logger = LogFactory.getLog(HtmlMetadataExtracterTest.class);
private MetadataExtracter extracter;
public void onSetUpInTransaction() throws Exception
{
extracter = new HtmlMetadataExtracter();
}
/**
* @return Returns the same transformer regardless - it is allowed
*/
protected MetadataExtracter getExtracter()
{
return extracter;
}
public void testReliability() throws Exception
{
double reliability = 0.0;
reliability = extracter.getReliability(MimetypeMap.MIMETYPE_TEXT_PLAIN);
assertEquals("Mimetype text should not be supported", 0.0, reliability);
reliability = extracter.getReliability(MimetypeMap.MIMETYPE_HTML);
assertEquals("HTML should be supported", 1.0, reliability);
}
public void testHtmlExtraction() throws Exception
{
testCommonMetadata(extractFromExtension("html", MimetypeMap.MIMETYPE_HTML));
}
}

View File

@@ -0,0 +1,245 @@
/*
* 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.metadata;
import java.io.File;
import java.io.IOException;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
import org.alfresco.model.ContentModel;
import org.alfresco.repo.content.MimetypeMap;
import org.alfresco.service.cmr.repository.ContentIOException;
import org.alfresco.service.cmr.repository.ContentReader;
import org.alfresco.service.namespace.QName;
import org.alfresco.util.GUID;
import org.farng.mp3.AbstractMP3FragmentBody;
import org.farng.mp3.MP3File;
import org.farng.mp3.TagException;
import org.farng.mp3.id3.AbstractID3v2;
import org.farng.mp3.id3.AbstractID3v2Frame;
import org.farng.mp3.id3.ID3v1;
import org.farng.mp3.lyrics3.AbstractLyrics3;
import org.farng.mp3.lyrics3.Lyrics3v2;
import org.farng.mp3.lyrics3.Lyrics3v2Field;
/**
* @author Roy Wetherall
*/
public class MP3MetadataExtracter extends AbstractMetadataExtracter
{
private static final QName PROP_ALBUM_TITLE = QName.createQName("{music}albumTitle");
private static final QName PROP_SONG_TITLE = QName.createQName("{music}songTitle");;
private static final QName PROP_ARTIST = QName.createQName("{music}artist");;
private static final QName PROP_COMMENT = QName.createQName("{music}comment");;
private static final QName PROP_YEAR_RELEASED = QName.createQName("{music}yearReleased");;
private static final QName PROP_TRACK_NUMBER = QName.createQName("{music}trackNumber");;
private static final QName PROP_GENRE = QName.createQName("{music}genre");;
private static final QName PROP_COMPOSER = QName.createQName("{music}composer");;
private static final QName PROP_LYRICS = QName.createQName("{music}lyrics");;
public MP3MetadataExtracter()
{
super(MimetypeMap.MIMETYPE_MP3, 1.0, 1000);
}
/**
* @see org.alfresco.repo.content.metadata.MetadataExtracter#extract(org.alfresco.service.cmr.repository.ContentReader, java.util.Map)
*/
public void extract(ContentReader reader,
Map<QName, Serializable> destination) throws ContentIOException
{
try
{
Map<QName, Serializable> props = new HashMap<QName, Serializable>();
// Create a temp file
File tempFile = File.createTempFile(GUID.generate(), ".tmp");
try
{
reader.getContent(tempFile);
// Create the MP3 object from the file
MP3File mp3File = new MP3File(tempFile);
ID3v1 id3v1 = mp3File.getID3v1Tag();
if (id3v1 != null)
{
setTagValue(props, PROP_ALBUM_TITLE, id3v1.getAlbum());
setTagValue(props, PROP_SONG_TITLE, id3v1.getTitle());
setTagValue(props, PROP_ARTIST, id3v1.getArtist());
setTagValue(props, PROP_COMMENT, id3v1.getComment());
setTagValue(props, PROP_YEAR_RELEASED, id3v1.getYear());
// TODO sort out the genre
//setTagValue(props, MusicModel.PROP_GENRE, id3v1.getGenre());
// TODO sort out the size
//setTagValue(props, MusicModel.PROP_SIZE, id3v1.getSize());
}
AbstractID3v2 id3v2 = mp3File.getID3v2Tag();
if (id3v2 != null)
{
setTagValue(props, PROP_SONG_TITLE, getID3V2Value(id3v2, "TIT2"));
setTagValue(props, PROP_ARTIST, getID3V2Value(id3v2, "TPE1"));
setTagValue(props, PROP_ALBUM_TITLE, getID3V2Value(id3v2, "TALB"));
setTagValue(props, PROP_YEAR_RELEASED, getID3V2Value(id3v2, "TDRC"));
setTagValue(props, PROP_COMMENT, getID3V2Value(id3v2, "COMM"));
setTagValue(props, PROP_TRACK_NUMBER, getID3V2Value(id3v2, "TRCK"));
setTagValue(props, PROP_GENRE, getID3V2Value(id3v2, "TCON"));
setTagValue(props, PROP_COMPOSER, getID3V2Value(id3v2, "TCOM"));
// TODO sort out the lyrics
//System.out.println("Lyrics: " + getID3V2Value(id3v2, "SYLT"));
//System.out.println("Lyrics: " + getID3V2Value(id3v2, "USLT"));
}
AbstractLyrics3 lyrics3Tag = mp3File.getLyrics3Tag();
if (lyrics3Tag != null)
{
System.out.println("Lyrics3 tag found.");
if (lyrics3Tag instanceof Lyrics3v2)
{
setTagValue(props, PROP_SONG_TITLE, getLyrics3v2Value((Lyrics3v2)lyrics3Tag, "TIT2"));
setTagValue(props, PROP_ARTIST, getLyrics3v2Value((Lyrics3v2)lyrics3Tag, "TPE1"));
setTagValue(props, PROP_ALBUM_TITLE, getLyrics3v2Value((Lyrics3v2)lyrics3Tag, "TALB"));
setTagValue(props, PROP_COMMENT, getLyrics3v2Value((Lyrics3v2)lyrics3Tag, "COMM"));
setTagValue(props, PROP_LYRICS, getLyrics3v2Value((Lyrics3v2)lyrics3Tag, "SYLT"));
setTagValue(props, PROP_COMPOSER, getLyrics3v2Value((Lyrics3v2)lyrics3Tag, "TCOM"));
}
}
}
finally
{
tempFile.delete();
}
// Set the destination values
if (props.get(PROP_SONG_TITLE) != null)
{
destination.put(ContentModel.PROP_TITLE, props.get(PROP_SONG_TITLE));
}
if (props.get(PROP_ARTIST) != null)
{
destination.put(ContentModel.PROP_CREATOR, props.get(PROP_ARTIST));
}
String description = getDescription(props);
if (description != null)
{
destination.put(ContentModel.PROP_DESCRIPTION, description);
}
}
catch (IOException ioException)
{
// TODO sort out exception handling
throw new RuntimeException("Error reading mp3 file.", ioException);
}
catch (TagException tagException)
{
// TODO sort out exception handling
throw new RuntimeException("Error reading mp3 tag information.", tagException);
}
}
/**
* Generate the description
*
* @param props the properties extracted from the file
* @return the description
*/
private String getDescription(Map<QName, Serializable> props)
{
StringBuilder result = new StringBuilder();
if (props.get(PROP_SONG_TITLE) != null && props.get(PROP_ARTIST) != null && props.get(PROP_ALBUM_TITLE) != null)
{
result
.append(props.get(PROP_SONG_TITLE))
.append(" - ")
.append(props.get(PROP_ALBUM_TITLE))
.append(" (")
.append(props.get(PROP_ARTIST))
.append(")");
}
return result.toString();
}
/**
*
* @param props
* @param propQName
* @param propvalue
*/
private void setTagValue(Map<QName, Serializable> props, QName propQName, String propvalue)
{
if (propvalue != null && propvalue.length() != 0)
{
trimPut(propQName, propvalue, props);
}
}
/**
*
* @param lyrics3Tag
* @param name
* @return
*/
private String getLyrics3v2Value(Lyrics3v2 lyrics3Tag, String name)
{
String result = "";
Lyrics3v2Field field = lyrics3Tag.getField(name);
if (field != null)
{
AbstractMP3FragmentBody body = field.getBody();
if (body != null)
{
result = (String)body.getObject("Text");
}
}
return result;
}
/**
* Get the ID3V2 tag value in a safe way
*
* @param id3v2
* @param name
* @return
*/
private String getID3V2Value(AbstractID3v2 id3v2, String name)
{
String result = "";
AbstractID3v2Frame frame = id3v2.getFrame(name);
if (frame != null)
{
AbstractMP3FragmentBody body = frame.getBody();
if (body != null)
{
result = (String)body.getObject("Text");
}
}
return result;
}
}

View File

@@ -0,0 +1,72 @@
/*
* Copyright (C) 2005 Jesper Steen M<>ller
*
* 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.metadata;
import java.io.Serializable;
import java.util.Map;
import org.alfresco.service.cmr.repository.ContentIOException;
import org.alfresco.service.cmr.repository.ContentReader;
import org.alfresco.service.namespace.QName;
/**
*
* @author Jesper Steen M<>ller
*/
public interface MetadataExtracter
{
/**
* Provides the approximate accuracy with which this extracter can extract
* metadata for the mimetype.
* <p>
*
* @param sourceMimetype the source mimetype
* @return Returns a score 0.0 to 1.0. 0.0 indicates that the extraction
* cannot be performed at all. 1.0 indicates that the extraction can
* be performed perfectly.
*/
public double getReliability(String sourceMimetype);
/**
* Provides an estimate, usually a worst case guess, of how long an
* extraction will take.
* <p>
* This method is used to determine, up front, which of a set of equally
* reliant transformers will be used for a specific extraction.
*
* @return Returns the approximate number of milliseconds per transformation
*/
public long getExtractionTime();
/**
* Extracts the metadata from the content provided by the reader and source
* mimetype to the supplied map.
* <p>
* The extraction viability can be determined by an up front call to
* {@link #getReliability(String)}.
* <p>
* The source mimetype <b>must</b> be available on the
* {@link org.alfresco.service.cmr.repository.ContentAccessor#getMimetype()} method
* of the reader.
*
* @param reader the source of the content
* @param destination the destination of the extraction
* @throws ContentIOException if an IO exception occurs
*/
public void extract(ContentReader reader, Map<QName, Serializable> destination) throws ContentIOException;
}

View File

@@ -0,0 +1,180 @@
/*
* Copyright (C) 2005 Jesper Steen M<>ller
*
* 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.metadata;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import org.alfresco.error.AlfrescoRuntimeException;
import org.alfresco.repo.content.MimetypeMap;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.util.Assert;
/**
* Holds and provides the most appropriate metadate extracter for a particular
* mimetype.
* <p>
* The extracters themselves know how well they are able to extract metadata.
*
* @see org.alfresco.repo.content.metadata.MetadataExtracter
* @author Jesper Steen M<>ller
*/
public class MetadataExtracterRegistry
{
private static final Log logger = LogFactory.getLog(MetadataExtracterRegistry.class);
private List<MetadataExtracter> extracters;
private Map<String, MetadataExtracter> extracterCache;
private MimetypeMap mimetypeMap;
/** Controls read access to the cache */
private Lock extracterCacheReadLock;
/** controls write access to the cache */
private Lock extracterCacheWriteLock;
/**
* @param mimetypeMap all the mimetypes available to the system
*/
public MetadataExtracterRegistry(MimetypeMap mimetypeMap)
{
Assert.notNull(mimetypeMap, "The MimetypeMap is mandatory");
this.mimetypeMap = mimetypeMap;
extracters = Collections.emptyList(); // just in case it isn't set
extracterCache = new HashMap<String, MetadataExtracter>(17);
// create lock objects for access to the cache
ReadWriteLock extractionCacheLock = new ReentrantReadWriteLock();
extracterCacheReadLock = extractionCacheLock.readLock();
extracterCacheWriteLock = extractionCacheLock.writeLock();
}
/**
* Gets the best metadata extracter. This is a combination of the most
* reliable and the most performant extracter.
* <p>
* The result is cached for quicker access next time.
*
* @param mimetype the source MIME of the extraction
* @return Returns a metadata extracter that can extract metadata from the
* chosen MIME type.
*/
public MetadataExtracter getExtracter(String sourceMimetype)
{
// check that the mimetypes are valid
if (!mimetypeMap.getMimetypes().contains(sourceMimetype))
{
throw new AlfrescoRuntimeException("Unknown extraction source mimetype: " + sourceMimetype);
}
MetadataExtracter extracter = null;
extracterCacheReadLock.lock();
try
{
if (extracterCache.containsKey(sourceMimetype))
{
// the translation has been requested before
// it might have been null
return extracterCache.get(sourceMimetype);
}
}
finally
{
extracterCacheReadLock.unlock();
}
// the translation has not been requested before
// get a write lock on the cache
// no double check done as it is not an expensive task
extracterCacheWriteLock.lock();
try
{
// find the most suitable transformer - may be empty list
extracter = findBestExtracter(sourceMimetype);
// store the result even if it is null
extracterCache.put(sourceMimetype, extracter);
return extracter;
}
finally
{
extracterCacheWriteLock.unlock();
}
}
/**
* @param sourceMimetype The MIME type under examination
* @return The fastest of the most reliable extracters in
* <code>extracters</code> for the given MIME type.
*/
private MetadataExtracter findBestExtracter(String sourceMimetype)
{
double bestReliability = -1;
long bestTime = Long.MAX_VALUE;
logger.debug("Finding best extracter for " + sourceMimetype);
MetadataExtracter bestExtracter = null;
for (MetadataExtracter ext : extracters)
{
double r = ext.getReliability(sourceMimetype);
if (r == bestReliability)
{
long time = ext.getExtractionTime();
if (time < bestTime)
{
bestExtracter = ext;
bestTime = time;
}
}
else if (r > bestReliability)
{
bestExtracter = ext;
bestReliability = r;
bestTime = ext.getExtractionTime();
}
}
return bestExtracter;
}
/**
* Provides a list of self-discovering extracters.
*
* @param transformers all the available extracters that the registry can
* work with
*/
public void setExtracters(List<MetadataExtracter> extracters)
{
logger.debug("Setting " + extracters.size() + "new extracters.");
extracterCacheWriteLock.lock();
try
{
this.extracters = extracters;
this.extracterCache.clear();
}
finally
{
extracterCacheWriteLock.unlock();
}
}
}

View File

@@ -0,0 +1,108 @@
/*
* Copyright (C) 2005 Jesper Steen M<>ller
*
* 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.metadata;
import java.io.IOException;
import java.io.Serializable;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Map;
import org.alfresco.model.ContentModel;
import org.alfresco.repo.content.MimetypeMap;
import org.alfresco.service.cmr.repository.ContentIOException;
import org.alfresco.service.cmr.repository.ContentReader;
import org.alfresco.service.namespace.QName;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.poi.hpsf.DocumentSummaryInformation;
import org.apache.poi.hpsf.PropertySet;
import org.apache.poi.hpsf.PropertySetFactory;
import org.apache.poi.hpsf.SummaryInformation;
import org.apache.poi.poifs.eventfilesystem.POIFSReader;
import org.apache.poi.poifs.eventfilesystem.POIFSReaderEvent;
import org.apache.poi.poifs.eventfilesystem.POIFSReaderListener;
/**
*
* @author Jesper Steen M<>ller
*/
public class OfficeMetadataExtracter extends AbstractMetadataExtracter
{
private static final Log logger = LogFactory.getLog(OfficeMetadataExtracter.class);
private static String[] mimeTypes = new String[] { MimetypeMap.MIMETYPE_WORD, MimetypeMap.MIMETYPE_EXCEL,
MimetypeMap.MIMETYPE_PPT };
public OfficeMetadataExtracter()
{
super(new HashSet<String>(Arrays.asList(mimeTypes)), 1.0, 1000);
}
public void extract(ContentReader reader, final Map<QName, Serializable> destination) throws ContentIOException
{
POIFSReaderListener readerListener = new POIFSReaderListener()
{
public void processPOIFSReaderEvent(final POIFSReaderEvent event)
{
try
{
PropertySet ps = PropertySetFactory.create(event.getStream());
if (ps instanceof SummaryInformation)
{
SummaryInformation si = (SummaryInformation) ps;
// Titled aspect
trimPut(ContentModel.PROP_TITLE, si.getTitle(), destination);
trimPut(ContentModel.PROP_DESCRIPTION, si.getSubject(), destination);
// Auditable aspect
trimPut(ContentModel.PROP_CREATED, si.getCreateDateTime(), destination);
trimPut(ContentModel.PROP_CREATOR, si.getAuthor(), destination);
trimPut(ContentModel.PROP_MODIFIED, si.getLastSaveDateTime(), destination);
trimPut(ContentModel.PROP_MODIFIER, si.getLastAuthor(), destination);
}
else if (ps instanceof DocumentSummaryInformation)
{
DocumentSummaryInformation dsi = (DocumentSummaryInformation) ps;
// These are not really interesting to any aspect:
// trimPut(ContentModel.PROP_xxx, dsi.getCompany(),
// destination);
// trimPut(ContentModel.PROP_yyy, dsi.getManager(),
// destination);
}
}
catch (Exception ex)
{
throw new ContentIOException("Property set stream: " + event.getPath() + event.getName(), ex);
}
}
};
try
{
POIFSReader r = new POIFSReader();
r.registerListener(readerListener, SummaryInformation.DEFAULT_STREAM_NAME);
r.read(reader.getContentInputStream());
}
catch (IOException e)
{
throw new ContentIOException("Compound Document SummaryInformation metadata extraction failed: \n"
+ " reader: " + reader,
e);
}
}
}

View File

@@ -0,0 +1,60 @@
package org.alfresco.repo.content.metadata;
import org.alfresco.repo.content.MimetypeMap;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
* @see org.alfresco.repo.content.transform.OfficeMetadataExtracter
* @author Jesper Steen M<>ller
*/
public class OfficeMetadataExtracterTest extends AbstractMetadataExtracterTest
{
private static final Log logger = LogFactory.getLog(OfficeMetadataExtracterTest.class);
private MetadataExtracter extracter;
public void onSetUpInTransaction() throws Exception
{
extracter = new OfficeMetadataExtracter();
}
/**
* @return Returns the same transformer regardless - it is allowed
*/
protected MetadataExtracter getExtracter()
{
return extracter;
}
public void testReliability() throws Exception
{
double reliability = 0.0;
reliability = extracter.getReliability(MimetypeMap.MIMETYPE_TEXT_PLAIN);
assertEquals("Mimetype text should not be supported", 0.0, reliability);
reliability = extracter.getReliability(MimetypeMap.MIMETYPE_WORD);
assertEquals("Word should be supported", 1.0, reliability);
reliability = extracter.getReliability(MimetypeMap.MIMETYPE_EXCEL);
assertEquals("Excel should be supported", 1.0, reliability);
reliability = extracter.getReliability(MimetypeMap.MIMETYPE_PPT);
assertEquals("PowerPoint should be supported", 1.0, reliability);
}
public void testWordExtraction() throws Exception
{
testCommonMetadata(extractFromExtension("doc", MimetypeMap.MIMETYPE_WORD));
}
public void testExcelExtraction() throws Exception
{
testCommonMetadata(extractFromExtension("xls", MimetypeMap.MIMETYPE_EXCEL));
}
public void testPowerPointExtraction() throws Exception
{
testCommonMetadata(extractFromExtension("ppt", MimetypeMap.MIMETYPE_PPT));
}
}

View File

@@ -0,0 +1,95 @@
/*
* Copyright (C) 2005 Antti Jokipii
*
* 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.metadata;
import java.io.Serializable;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Map;
import org.alfresco.model.ContentModel;
import org.alfresco.repo.content.MimetypeMap;
import org.alfresco.service.cmr.repository.ContentIOException;
import org.alfresco.service.cmr.repository.ContentReader;
import org.alfresco.service.namespace.QName;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import com.catcode.odf.ODFMetaFileAnalyzer;
import com.catcode.odf.OpenDocumentMetadata;
/**
* Metadata extractor for the
* {@link org.alfresco.repo.content.MimetypeMap#MIMETYPE_OPENDOCUMENT_TEXT MIMETYPE_OPENDOCUMENT_XXX}
* mimetypes.
*
* @author Antti Jokipii
*/
public class OpenDocumentMetadataExtracter extends AbstractMetadataExtracter
{
private static final Log logger = LogFactory.getLog(OpenDocumentMetadataExtracter.class);
private static String[] mimeTypes = new String[] {
MimetypeMap.MIMETYPE_OPENDOCUMENT_TEXT,
MimetypeMap.MIMETYPE_OPENDOCUMENT_TEXT_TEMPLATE,
MimetypeMap.MIMETYPE_OPENDOCUMENT_GRAPHICS,
MimetypeMap.MIMETYPE_OPENDOCUMENT_GRAPHICS_TEMPLATE,
MimetypeMap.MIMETYPE_OPENDOCUMENT_PRESENTATION,
MimetypeMap.MIMETYPE_OPENDOCUMENT_PRESENTATION_TEMPLATE,
MimetypeMap.MIMETYPE_OPENDOCUMENT_SPREADSHEET,
MimetypeMap.MIMETYPE_OPENDOCUMENT_SPREADSHEET_TEMPLATE,
MimetypeMap.MIMETYPE_OPENDOCUMENT_CHART,
MimetypeMap.MIMETYPE_OPENDOCUMENT_CHART_TEMPLATE,
MimetypeMap.MIMETYPE_OPENDOCUMENT_IMAGE,
MimetypeMap.MIMETYPE_OPENDOCUMENT_IMAGE_TEMPLATE,
MimetypeMap.MIMETYPE_OPENDOCUMENT_FORMULA,
MimetypeMap.MIMETYPE_OPENDOCUMENT_FORMULA_TEMPLATE,
MimetypeMap.MIMETYPE_OPENDOCUMENT_TEXT_MASTER,
MimetypeMap.MIMETYPE_OPENDOCUMENT_TEXT_WEB,
MimetypeMap.MIMETYPE_OPENDOCUMENT_DATABASE, };
public OpenDocumentMetadataExtracter()
{
super(new HashSet<String>(Arrays.asList(mimeTypes)), 1.00, 1000);
}
public void extract(ContentReader reader, Map<QName, Serializable> destination) throws ContentIOException
{
ODFMetaFileAnalyzer analyzer = new ODFMetaFileAnalyzer();
try
{
// stream the document in
OpenDocumentMetadata docInfo = analyzer.analyzeZip(reader.getContentInputStream());
if (docInfo != null)
{
// set the metadata
destination.put(ContentModel.PROP_CREATOR, docInfo.getCreator());
destination.put(ContentModel.PROP_TITLE, docInfo.getTitle());
destination.put(ContentModel.PROP_DESCRIPTION, docInfo.getDescription());
destination.put(ContentModel.PROP_CREATED, docInfo.getCreationDate());
}
}
catch (Throwable e)
{
String message = "Metadata extraction failed: \n" +
" reader: " + reader;
logger.debug(message, e);
throw new ContentIOException(message, e);
}
}
}

View File

@@ -0,0 +1,91 @@
/*
* Copyright (C) 2005 Jesper Steen M<>ller
*
* 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.metadata;
import java.io.IOException;
import java.io.Serializable;
import java.util.Calendar;
import java.util.Map;
import org.alfresco.model.ContentModel;
import org.alfresco.repo.content.MimetypeMap;
import org.alfresco.service.cmr.repository.ContentIOException;
import org.alfresco.service.cmr.repository.ContentReader;
import org.alfresco.service.namespace.QName;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.pdfbox.pdmodel.PDDocument;
import org.pdfbox.pdmodel.PDDocumentInformation;
/**
*
* @author Jesper Steen M<>ller
*/
public class PdfBoxMetadataExtracter extends AbstractMetadataExtracter
{
private static final Log logger = LogFactory.getLog(PdfBoxMetadataExtracter.class);
public PdfBoxMetadataExtracter()
{
super(MimetypeMap.MIMETYPE_PDF, 1.0, 1000);
}
public void extract(ContentReader reader, Map<QName, Serializable> destination) throws ContentIOException
{
if (!MimetypeMap.MIMETYPE_PDF.equals(reader.getMimetype()))
{
logger.debug("No metadata extracted for " + reader.getMimetype());
return;
}
PDDocument pdf = null;
try
{
// stream the document in
pdf = PDDocument.load(reader.getContentInputStream());
// Scoop out the metadata
PDDocumentInformation docInfo = pdf.getDocumentInformation();
trimPut(ContentModel.PROP_CREATOR, docInfo.getAuthor(), destination);
trimPut(ContentModel.PROP_TITLE, docInfo.getTitle(), destination);
trimPut(ContentModel.PROP_DESCRIPTION, docInfo.getSubject(), destination);
Calendar created = docInfo.getCreationDate();
if (created != null)
destination.put(ContentModel.PROP_CREATED, created.getTime());
}
catch (IOException e)
{
throw new ContentIOException("PDF metadata extraction failed: \n" +
" reader: " + reader);
}
finally
{
if (pdf != null)
{
try
{
pdf.close();
}
catch (Throwable e)
{
e.printStackTrace();
}
}
}
}
}

View File

@@ -0,0 +1,43 @@
package org.alfresco.repo.content.metadata;
import org.alfresco.repo.content.MimetypeMap;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
* @see org.alfresco.repo.content.transform.PdfBoxContentTransformer
* @author Jesper Steen M<>ller
*/
public class PdfBoxMetadataExtracterTest extends AbstractMetadataExtracterTest
{
private static final Log logger = LogFactory.getLog(PdfBoxMetadataExtracterTest.class);
private MetadataExtracter extracter;
public void onSetUpInTransaction() throws Exception
{
extracter = new PdfBoxMetadataExtracter();
}
/**
* @return Returns the same transformer regardless - it is allowed
*/
protected MetadataExtracter getExtracter()
{
return extracter;
}
public void testReliability() throws Exception
{
double reliability = 0.0;
reliability = extracter.getReliability(MimetypeMap.MIMETYPE_TEXT_PLAIN);
assertEquals("Mimetype should not be supported", 0.0, reliability);
reliability = extracter.getReliability(MimetypeMap.MIMETYPE_PDF);
assertEquals("Mimetype should be supported", 1.0, reliability);
}
public void testPdfExtraction() throws Exception
{
testCommonMetadata(extractFromExtension("pdf", MimetypeMap.MIMETYPE_PDF));
}
}

View File

@@ -0,0 +1,58 @@
/*
* Copyright (C) 2005 Jesper Steen M<>ller
*
* 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.metadata;
import java.io.Serializable;
import java.util.Map;
import org.alfresco.service.cmr.repository.ContentIOException;
import org.alfresco.service.cmr.repository.ContentReader;
import org.alfresco.service.namespace.QName;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
*
* @author Jesper Steen M<>ller
*/
public class StringMetadataExtracter implements MetadataExtracter
{
public static final String PREFIX_TEXT = "text/";
private static final Log logger = LogFactory.getLog(StringMetadataExtracter.class);
public double getReliability(String sourceMimetype)
{
if (sourceMimetype.startsWith(PREFIX_TEXT))
return 0.1;
else
return 0.0;
}
public long getExtractionTime()
{
return 1000;
}
public void extract(ContentReader reader, Map<QName, Serializable> destination) throws ContentIOException
{
if (logger.isDebugEnabled())
{
logger.debug("No metadata extracted for " + reader.getMimetype());
}
}
}

View File

@@ -0,0 +1,205 @@
/*
* Copyright (C) 2005 Jesper Steen M<>ller
*
* 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.metadata;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.Serializable;
import java.net.ConnectException;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Map;
import net.sf.joott.uno.UnoConnection;
import org.alfresco.model.ContentModel;
import org.alfresco.repo.content.MimetypeMap;
import org.alfresco.service.cmr.repository.ContentIOException;
import org.alfresco.service.cmr.repository.ContentReader;
import org.alfresco.service.namespace.QName;
import org.alfresco.util.TempFileProvider;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import com.sun.star.beans.PropertyValue;
import com.sun.star.beans.XPropertySet;
import com.sun.star.document.XDocumentInfoSupplier;
import com.sun.star.frame.XComponentLoader;
import com.sun.star.lang.XComponent;
import com.sun.star.ucb.XFileIdentifierConverter;
import com.sun.star.uno.UnoRuntime;
/**
*
* @author Jesper Steen M<>ller
*/
public class UnoMetadataExtracter extends AbstractMetadataExtracter
{
private static final Log logger = LogFactory.getLog(UnoMetadataExtracter.class);
private static String[] mimeTypes = new String[] {
MimetypeMap.MIMETYPE_OPENDOCUMENT_TEXT,
MimetypeMap.MIMETYPE_OPENOFFICE_WRITER,
// Add the other OpenOffice.org stuff here
// In fact, other types may apply as well, but should be counted as lower
// quality since they involve conversion.
};
public UnoMetadataExtracter(MimetypeMap mimetypeMap, String connectionUrl)
{
super(new HashSet<String>(Arrays.asList(mimeTypes)), 1.00, 10000);
this.mimetypeMap = mimetypeMap;
init(connectionUrl);
}
public UnoMetadataExtracter(MimetypeMap mimetypeMap)
{
this(mimetypeMap, UnoConnection.DEFAULT_CONNECTION_STRING);
}
private MimetypeMap mimetypeMap;
private MyUnoConnection connection;
private boolean isConnected;
/**
* @param unoConnectionUrl the URL of the Uno server
*/
private synchronized void init(String unoConnectionUrl)
{
connection = new MyUnoConnection(unoConnectionUrl);
// attempt to make an connection
try
{
connection.connect();
isConnected = true;
}
catch (ConnectException e)
{
isConnected = false;
}
}
/**
* @return Returns true if a connection to the Uno server could be
* established
*/
public boolean isConnected()
{
return isConnected;
}
public void extract(ContentReader reader, final Map<QName, Serializable> destination) throws ContentIOException
{
String sourceMimetype = reader.getMimetype();
// create temporary files to convert from and to
File tempFromFile = TempFileProvider.createTempFile("UnoContentTransformer", "."
+ mimetypeMap.getExtension(sourceMimetype));
// download the content from the source reader
reader.getContent(tempFromFile);
String sourceUrl = tempFromFile.toString();
try
{
sourceUrl = toUrl(tempFromFile, connection);
// UNO Interprocess Bridge *should* be thread-safe, but...
synchronized (connection)
{
XComponentLoader desktop = connection.getDesktop();
XComponent document = desktop.loadComponentFromURL(
sourceUrl,
"_blank",
0,
new PropertyValue[] { property("Hidden", Boolean.TRUE) });
if (document == null)
{
throw new FileNotFoundException("could not open source document: " + sourceUrl);
}
try
{
XDocumentInfoSupplier infoSupplier = (XDocumentInfoSupplier) UnoRuntime.queryInterface(
XDocumentInfoSupplier.class, document);
XPropertySet propSet = (XPropertySet) UnoRuntime.queryInterface(
XPropertySet.class,
infoSupplier
.getDocumentInfo());
// Titled aspect
trimPut(ContentModel.PROP_TITLE, propSet.getPropertyValue("Title"), destination);
trimPut(ContentModel.PROP_DESCRIPTION, propSet.getPropertyValue("Subject"), destination);
// Auditable aspect
// trimPut(ContentModel.PROP_CREATED,
// si.getCreateDateTime(), destination);
trimPut(ContentModel.PROP_CREATOR, propSet.getPropertyValue("Author"), destination);
// trimPut(ContentModel.PROP_MODIFIED,
// si.getLastSaveDateTime(), destination);
// trimPut(ContentModel.PROP_MODIFIER, si.getLastAuthor(),
// destination);
}
finally
{
document.dispose();
}
}
}
catch (Throwable e)
{
throw new ContentIOException("Conversion failed: \n" +
" source: " + sourceUrl + "\n",
e);
}
}
public String toUrl(File file, MyUnoConnection connection) throws ConnectException
{
Object fcp = connection.getFileContentService();
XFileIdentifierConverter fic = (XFileIdentifierConverter) UnoRuntime.queryInterface(
XFileIdentifierConverter.class, fcp);
return fic.getFileURLFromSystemPath("", file.getAbsolutePath());
}
public double getReliability(String sourceMimetype)
{
if (isConnected())
return super.getReliability(sourceMimetype);
else
return 0.0;
}
private static PropertyValue property(String name, Object value)
{
PropertyValue property = new PropertyValue();
property.Name = name;
property.Value = value;
return property;
}
static class MyUnoConnection extends UnoConnection
{
public MyUnoConnection(String url)
{
super(url);
}
public Object getFileContentService() throws ConnectException
{
return getService("com.sun.star.ucb.FileContentProvider");
}
}
}

View File

@@ -0,0 +1,79 @@
/*
* Copyright (C) 2005 Jesper Steen M<>ller
*
* 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.metadata;
import org.alfresco.repo.content.MimetypeMap;
/**
* @see org.alfresco.repo.content.transform.UnoMetadataExtracter
* @author Jesper Steen M<>ller
*/
public class UnoMetadataExtracterTest extends AbstractMetadataExtracterTest
{
private UnoMetadataExtracter extracter;
public void onSetUpInTransaction() throws Exception
{
extracter = new UnoMetadataExtracter(mimetypeMap);
}
/**
* @return Returns the same extracter regardless - it is allowed
*/
protected MetadataExtracter getExtracter()
{
return extracter;
}
public void testReliability() throws Exception
{
if (!extracter.isConnected())
{
return;
}
double reliability = 0.0;
reliability = extracter.getReliability(MimetypeMap.MIMETYPE_TEXT_PLAIN);
assertEquals("Mimetype text should not be supported", 0.0, reliability);
reliability = extracter.getReliability(MimetypeMap.MIMETYPE_OPENDOCUMENT_TEXT);
assertEquals("OpenOffice 2.0 Writer (OpenDoc) should be supported", 1.0, reliability);
reliability = extracter.getReliability(MimetypeMap.MIMETYPE_OPENOFFICE_WRITER);
assertEquals("OpenOffice 1.0 Writer should be supported", 1.0, reliability);
}
public void testOOo20WriterExtraction() throws Exception
{
if (!extracter.isConnected())
{
return;
}
testCommonMetadata(extractFromExtension("odt", MimetypeMap.MIMETYPE_OPENDOCUMENT_TEXT));
}
public void testOOo10WriterExtraction() throws Exception
{
if (!extracter.isConnected())
{
return;
}
testCommonMetadata(extractFromExtension("sxw", MimetypeMap.MIMETYPE_OPENOFFICE_WRITER));
}
}

View File

@@ -0,0 +1,242 @@
/*
* 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.transform;
import java.util.Collections;
import java.util.Map;
import org.alfresco.error.AlfrescoRuntimeException;
import org.alfresco.service.cmr.repository.ContentAccessor;
import org.alfresco.service.cmr.repository.ContentIOException;
import org.alfresco.service.cmr.repository.ContentReader;
import org.alfresco.service.cmr.repository.ContentWriter;
import org.alfresco.service.cmr.repository.MimetypeService;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
* Provides basic services for {@link org.alfresco.repo.content.transform.ContentTransformer}
* implementations.
* <p>
* This class maintains the performance measures for the transformers as well, making sure that
* there is an extra penalty for transformers that fail regularly.
*
* @author Derek Hulley
*/
public abstract class AbstractContentTransformer implements ContentTransformer
{
private static final Log logger = LogFactory.getLog(AbstractContentTransformer.class);
private MimetypeService mimetypeService;
private double averageTime = 0.0;
private long count = 0L;
/**
* All transformers start with an average transformation time of 0.0ms.
*/
protected AbstractContentTransformer()
{
averageTime = 0.0;
}
/**
* Helper setter of the mimetype service. This is not always required.
*
* @param mimetypeService
*/
public void setMimetypeService(MimetypeService mimetypeService)
{
this.mimetypeService = mimetypeService;
}
/**
* @return Returns the mimetype helper
*/
protected MimetypeService getMimetypeService()
{
return mimetypeService;
}
@Override
public String toString()
{
StringBuilder sb = new StringBuilder();
sb.append(this.getClass().getSimpleName())
.append("[ average=").append((long)averageTime).append("ms")
.append("]");
return sb.toString();
}
/**
* Convenience to fetch and check the mimetype for the given content
*
* @param content the reader/writer for the content
* @return Returns the mimetype for the content
* @throws AlfrescoRuntimeException if the content doesn't have a mimetype
*/
protected String getMimetype(ContentAccessor content)
{
String mimetype = content.getMimetype();
if (mimetype == null)
{
throw new AlfrescoRuntimeException("Mimetype is mandatory for transformation: " + content);
}
// done
return mimetype;
}
/**
* Convenience method to check the reliability of a transformation
*
* @param reader
* @param writer
* @throws AlfrescoRuntimeException if the reliability isn't > 0
*/
protected void checkReliability(ContentReader reader, ContentWriter writer)
{
String sourceMimetype = getMimetype(reader);
String targetMimetype = getMimetype(writer);
if (getReliability(sourceMimetype, targetMimetype) <= 0.0)
{
throw new AlfrescoRuntimeException("Zero scoring transformation attempted: \n" +
" reader: " + reader + "\n" +
" writer: " + writer);
}
// it all checks out OK
}
/**
* Method to be implemented by subclasses wishing to make use of the common infrastructural code
* provided by this class.
*
* @param reader the source of the content to transform
* @param writer the target to which to write the transformed content
* @param options a map of options to use when performing the transformation. The map
* will never be null.
* @throws Exception exceptions will be handled by this class - subclasses can throw anything
*/
protected abstract void transformInternal(
ContentReader reader,
ContentWriter writer,
Map<String, Object> options) throws Exception;
/**
* @see #transform(ContentReader, ContentWriter, Map)
* @see #transformInternal(ContentReader, ContentWriter, Map)
*/
public final void transform(ContentReader reader, ContentWriter writer) throws ContentIOException
{
transform(reader, writer, null);
}
/**
* Performs the following:
* <ul>
* <li>Times the transformation</li>
* <li>Ensures that the transformation is allowed</li>
* <li>Calls the subclass implementation of {@link #transformInternal(ContentReader, ContentWriter)}</li>
* <li>Transforms any exceptions generated</li>
* <li>Logs a successful transformation</li>
* </ul>
* Subclass need only be concerned with performing the transformation.
* <p>
* If the options provided are null, then an empty map will be created.
*/
public final void transform(
ContentReader reader,
ContentWriter writer,
Map<String, Object> options) throws ContentIOException
{
// begin timing
long before = System.currentTimeMillis();
// check the reliability
checkReliability(reader, writer);
// check options map
if (options == null)
{
options = Collections.emptyMap();
}
try
{
transformInternal(reader, writer, options);
}
catch (Throwable e)
{
// Make sure that this transformation gets set back i.t.o. time taken.
// This will ensure that transformers that compete for the same transformation
// will be prejudiced against transformers that tend to fail
recordTime(10000); // 10 seconds, i.e. rubbish
throw new ContentIOException("Content conversion failed: \n" +
" reader: " + reader + "\n" +
" writer: " + writer + "\n" +
" options: " + options,
e);
}
// record time
long after = System.currentTimeMillis();
recordTime(after - before);
// done
if (logger.isDebugEnabled())
{
logger.debug("Completed transformation: \n" +
" reader: " + reader + "\n" +
" writer: " + writer + "\n" +
" options: " + options + "\n" +
" transformer: " + this);
}
}
/**
* @return Returns the calculated running average of the current transformations
*/
public synchronized long getTransformationTime()
{
return (long) averageTime;
}
/**
* Records and updates the average transformation time for this transformer.
* <p>
* Subclasses should call this after every transformation in order to keep
* the running average of the transformation times up to date.
* <p>
* This method is thread-safe. The time spent in this method is negligible
* so the impact will be minor.
*
* @param transformationTime the time it took to perform the transformation.
* The value may be 0.
*/
protected final synchronized void recordTime(long transformationTime)
{
if (count == Long.MAX_VALUE)
{
// we have reached the max count - reduce it by half
// the average fluctuation won't be extreme
count /= 2L;
}
// adjust the average
count++;
double diffTime = ((double) transformationTime) - averageTime;
averageTime += diffTime / (double) count;
}
}

View File

@@ -0,0 +1,214 @@
/*
* 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.transform;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.List;
import org.alfresco.repo.content.MimetypeMap;
import org.alfresco.repo.content.filestore.FileContentReader;
import org.alfresco.repo.content.filestore.FileContentWriter;
import org.alfresco.service.cmr.repository.ContentReader;
import org.alfresco.service.cmr.repository.ContentWriter;
import org.alfresco.util.BaseSpringTest;
import org.alfresco.util.TempFileProvider;
/**
* Provides a base set of tests for testing
* {@link org.alfresco.repo.content.transform.ContentTransformer}
* implementations.
*
* @author Derek Hulley
*/
public abstract class AbstractContentTransformerTest extends BaseSpringTest
{
private static String QUICK_CONTENT = "The quick brown fox jumps over the lazy dog";
private static String[] QUICK_WORDS = new String[] {
"quick", "brown", "fox", "jumps", "lazy", "dog"};
protected MimetypeMap mimetypeMap;
protected ContentTransformer transformer;
public final void setMimetypeMap(MimetypeMap mimetypeMap)
{
this.mimetypeMap = mimetypeMap;
}
/**
* Fetches a transformer to test for a given transformation. The transformer
* does not <b>have</b> to be reliable for the given format - if it isn't
* then it will be ignored.
*
* @param sourceMimetype the sourceMimetype to be tested
* @param targetMimetype the targetMimetype to be tested
* @return Returns the <tt>ContentTranslators</tt> that will be tested by
* the methods implemented in this class. A null return value is
* acceptable if the source and target mimetypes are not of interest.
*/
protected abstract ContentTransformer getTransformer(String sourceMimetype, String targetMimetype);
/**
* Ensures that the temp locations are cleaned out before the tests start
*/
@Override
protected void onSetUpInTransaction() throws Exception
{
// perform a little cleaning up
long now = System.currentTimeMillis();
TempFileProvider.TempFileCleanerJob.removeFiles(now);
}
/**
* Check that all objects are present
*/
public void testSetUp() throws Exception
{
assertNotNull("MimetypeMap not present", mimetypeMap);
// check that the quick resources are available
File sourceFile = AbstractContentTransformerTest.loadQuickTestFile("txt");
assertNotNull(sourceFile);
}
/**
* Helper method to load one of the "The quick brown fox" files from the
* classpath.
*
* @param extension the extension of the file required
* @return Returns a test resource loaded from the classpath or <tt>null</tt> if
* no resource could be found.
* @throws IOException
*/
public static File loadQuickTestFile(String extension) throws IOException
{
URL url = AbstractContentTransformerTest.class.getClassLoader().getResource("quick/quick." + extension);
if (url == null)
{
return null;
}
File file = new File(url.getFile());
if (!file.exists())
{
return null;
}
return file;
}
/**
* Tests the full range of transformations available on the
* {@link #getTransformer(String, String) transformer} subject to the
* {@link org.alfresco.util.test.QuickFileTest available test files}
* and the {@link ContentTransformer#getReliability(String, String) reliability} of
* the {@link #getTransformer(String, String) transformer} itself.
* <p>
* Each transformation is repeated several times, with a transformer being
* {@link #getTransformer(String, String) requested} for each transformation. In the
* case where optimizations are being done around the selection of the most
* appropriate transformer, different transformers could be used during the iteration
* process.
*/
public void testAllConversions() throws Exception
{
// get all mimetypes
List<String> mimetypes = mimetypeMap.getMimetypes();
for (String sourceMimetype : mimetypes)
{
// attempt to get a source file for each mimetype
String sourceExtension = mimetypeMap.getExtension(sourceMimetype);
File sourceFile = AbstractContentTransformerTest.loadQuickTestFile(sourceExtension);
if (sourceFile == null)
{
continue; // no test file available for that extension
}
// attempt to convert to every other mimetype
for (String targetMimetype : mimetypes)
{
ContentWriter targetWriter = null;
// construct a reader onto the source file
ContentReader sourceReader = new FileContentReader(sourceFile);
// perform the transformation several times so that we get a good idea of performance
int count = 0;
for (int i = 0; i < 5; i++)
{
// must we test the transformation?
ContentTransformer transformer = getTransformer(sourceMimetype, targetMimetype);
if (transformer == null)
{
break; // test is not required
}
else if (transformer.getReliability(sourceMimetype, targetMimetype) <= 0.0)
{
break; // not reliable for this transformation
}
// make a writer for the target file
String targetExtension = mimetypeMap.getExtension(targetMimetype);
File targetFile = TempFileProvider.createTempFile(
getClass().getSimpleName() + "_" + getName() + "_" + sourceExtension + "_",
"." + targetExtension);
targetWriter = new FileContentWriter(targetFile);
// do the transformation
sourceReader.setMimetype(sourceMimetype);
targetWriter.setMimetype(targetMimetype);
transformer.transform(sourceReader.getReader(), targetWriter);
// if the target format is any type of text, then it must contain the 'quick' phrase
if (targetMimetype.equals(MimetypeMap.MIMETYPE_TEXT_PLAIN))
{
ContentReader targetReader = targetWriter.getReader();
String checkContent = targetReader.getContentString();
assertTrue("Quick phrase not present in document converted to text: \n" +
" transformer: " + transformer + "\n" +
" source: " + sourceReader + "\n" +
" target: " + targetWriter,
checkContent.contains(QUICK_CONTENT));
}
else if (targetMimetype.startsWith(StringExtractingContentTransformer.PREFIX_TEXT))
{
ContentReader targetReader = targetWriter.getReader();
String checkContent = targetReader.getContentString();
// essentially check that FTS indexing can use the conversion properly
for (int word = 0; word < QUICK_WORDS.length; word++)
{
assertTrue("Quick phrase word not present in document converted to text: \n" +
" transformer: " + transformer + "\n" +
" source: " + sourceReader + "\n" +
" target: " + targetWriter + "\n" +
" word: " + word,
checkContent.contains(QUICK_WORDS[word]));
}
}
// increment count
count++;
}
if (logger.isDebugEnabled())
{
logger.debug("Transformation performed " + count + " time: " +
sourceMimetype + " --> " + targetMimetype + "\n" +
" source: " + sourceReader + "\n" +
" target: " + targetWriter + "\n" +
" transformer: " + transformer);
}
}
}
}
}

View File

@@ -0,0 +1,74 @@
/*
* 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.transform;
import java.util.Map;
import org.alfresco.service.cmr.repository.ContentReader;
import org.alfresco.service.cmr.repository.ContentWriter;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
* Allows direct streaming from source to target when the respective mimetypes
* are identical, except where the mimetype is text.
* <p>
* Text has to be transformed based on the encoding even if the mimetypes don't
* reflect it.
*
* @see org.alfresco.repo.content.transform.StringExtractingContentTransformer
*
* @author Derek Hulley
*/
public class BinaryPassThroughContentTransformer extends AbstractContentTransformer
{
private static final Log logger = LogFactory.getLog(BinaryPassThroughContentTransformer.class);
/**
* @return Returns 1.0 if the formats are identical and not text
*/
public double getReliability(String sourceMimetype, String targetMimetype)
{
if (sourceMimetype.startsWith(StringExtractingContentTransformer.PREFIX_TEXT))
{
// we can only stream binary content through
return 0.0;
}
else if (!sourceMimetype.equals(targetMimetype))
{
// no transformation is possible so formats must be exact
return 0.0;
}
else
{
// formats are the same and are not text
return 1.0;
}
}
/**
* Performs a direct stream provided the preconditions are met
*/
public void transformInternal(
ContentReader reader,
ContentWriter writer,
Map<String, Object> options) throws Exception
{
// just stream it
writer.putContent(reader.getContentInputStream());
}
}

View File

@@ -0,0 +1,59 @@
/*
* 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.transform;
import org.alfresco.repo.content.MimetypeMap;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
* @see org.alfresco.repo.content.transform.BinaryPassThroughContentTransformer
*
* @author Derek Hulley
*/
public class BinaryPassThroughContentTransformerTest extends AbstractContentTransformerTest
{
private static final Log logger = LogFactory.getLog(BinaryPassThroughContentTransformerTest.class);
private ContentTransformer transformer;
public void onSetUpInTransaction() throws Exception
{
transformer = new BinaryPassThroughContentTransformer();
}
/**
* @return Returns the same transformer regardless - it is allowed
*/
protected ContentTransformer getTransformer(String sourceMimetype, String targetMimetype)
{
return transformer;
}
public void testReliability() throws Exception
{
double reliability = 0.0;
reliability = transformer.getReliability(MimetypeMap.MIMETYPE_TEXT_PLAIN, MimetypeMap.MIMETYPE_TEXT_PLAIN);
assertEquals("Mimetype should not be supported", 0.0, reliability);
reliability = transformer.getReliability(MimetypeMap.MIMETYPE_XML, MimetypeMap.MIMETYPE_XML);
assertEquals("Mimetype should not be supported", 0.0, reliability);
reliability = transformer.getReliability(MimetypeMap.MIMETYPE_WORD, MimetypeMap.MIMETYPE_WORD);
assertEquals("Mimetype should be supported", 1.0, reliability);
reliability = transformer.getReliability(MimetypeMap.MIMETYPE_EXCEL, MimetypeMap.MIMETYPE_EXCEL);
assertEquals("Mimetype should be supported", 1.0, reliability);
}
}

View File

@@ -0,0 +1,149 @@
package org.alfresco.repo.content.transform;
import java.io.File;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.alfresco.error.AlfrescoRuntimeException;
import org.alfresco.repo.content.filestore.FileContentWriter;
import org.alfresco.service.cmr.repository.ContentReader;
import org.alfresco.service.cmr.repository.ContentWriter;
import org.alfresco.util.TempFileProvider;
import org.springframework.beans.factory.InitializingBean;
/**
* Transformer that passes a document through several nested transformations
* in order to accomplish its goal.
*
* @author Derek Hulley
*/
public class ComplexContentTransformer extends AbstractContentTransformer implements InitializingBean
{
private List<ContentTransformer> transformers;
private List<String> intermediateMimetypes;
public ComplexContentTransformer()
{
}
/**
* The list of transformers to use.
* <p>
* If a single transformer is supplied, then it will still be used.
*
* @param transformers list of <b>at least one</b> transformer
*/
public void setTransformers(List<ContentTransformer> transformers)
{
this.transformers = transformers;
}
/**
* Set the intermediate mimetypes that the transformer must take the content
* through. If the transformation <b>A..B..C</b> is performed in order to
* simulate <b>A..C</b>, then <b>B</b> is the intermediate mimetype. There
* must always be <b>n-1</b> intermediate mimetypes, where <b>n</b> is the
* number of {@link #setTransformers(List) transformers} taking part in the
* transformation.
*
* @param intermediateMimetypes intermediate mimetypes to transition the content
* through.
*/
public void setIntermediateMimetypes(List<String> intermediateMimetypes)
{
this.intermediateMimetypes = intermediateMimetypes;
}
/**
* Ensures that required properties have been set
*/
public void afterPropertiesSet() throws Exception
{
if (transformers == null || transformers.size() == 0)
{
throw new AlfrescoRuntimeException("At least one inner transformer must be supplied: " + this);
}
if (intermediateMimetypes == null || intermediateMimetypes.size() != transformers.size() - 1)
{
throw new AlfrescoRuntimeException(
"There must be n-1 intermediate mimetypes, where n is the number of transformers");
}
if (getMimetypeService() == null)
{
throw new AlfrescoRuntimeException("'mimetypeService' is a required property");
}
}
/**
* @return Returns the multiple of the reliabilities of the chain of transformers
*/
public double getReliability(String sourceMimetype, String targetMimetype)
{
double reliability = 1.0;
String currentSourceMimetype = sourceMimetype;
Iterator<ContentTransformer> transformerIterator = transformers.iterator();
Iterator<String> intermediateMimetypeIterator = intermediateMimetypes.iterator();
while (transformerIterator.hasNext())
{
ContentTransformer transformer = transformerIterator.next();
// determine the target mimetype. This is the final target if we are on the last transformation
String currentTargetMimetype = null;
if (!transformerIterator.hasNext())
{
currentTargetMimetype = targetMimetype;
}
else
{
// use an intermediate transformation mimetype
currentTargetMimetype = intermediateMimetypeIterator.next();
}
// the reliability is a multiple
reliability *= transformer.getReliability(currentSourceMimetype, currentTargetMimetype);
// move the source on
currentSourceMimetype = currentTargetMimetype;
}
// done
return reliability;
}
@Override
public void transformInternal(
ContentReader reader,
ContentWriter writer,
Map<String, Object> options) throws Exception
{
ContentReader currentReader = reader;
Iterator<ContentTransformer> transformerIterator = transformers.iterator();
Iterator<String> intermediateMimetypeIterator = intermediateMimetypes.iterator();
while (transformerIterator.hasNext())
{
ContentTransformer transformer = transformerIterator.next();
// determine the target mimetype. This is the final target if we are on the last transformation
ContentWriter currentWriter = null;
if (!transformerIterator.hasNext())
{
currentWriter = writer;
}
else
{
String nextMimetype = intermediateMimetypeIterator.next();
// make a temp file writer with the correct extension
String sourceExt = getMimetypeService().getExtension(currentReader.getMimetype());
String targetExt = getMimetypeService().getExtension(nextMimetype);
File tempFile = TempFileProvider.createTempFile(
"ComplextTransformer_intermediate_" + sourceExt + "_",
"." + targetExt);
currentWriter = new FileContentWriter(tempFile);
currentWriter.setMimetype(nextMimetype);
}
// transform
transformer.transform(currentReader, currentWriter, options);
// move the source on
currentReader = currentWriter.getReader();
}
// done
}
}

View File

@@ -0,0 +1,89 @@
/*
* 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.transform;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.alfresco.repo.content.MimetypeMap;
/**
* Tests a transformation from Powerpoint->PDF->Text.
*
* @see org.alfresco.repo.content.transform.ComplexContentTransformer
*
* @author Derek Hulley
*/
public class ComplexContentTransformerTest extends AbstractContentTransformerTest
{
private ComplexContentTransformer transformer;
private boolean isAvailable;
public void onSetUpInTransaction() throws Exception
{
ContentTransformer unoTransformer = (ContentTransformer) applicationContext.getBean("transformer.OpenOffice");
ContentTransformer pdfBoxTransformer = (ContentTransformer) applicationContext.getBean("transformer.PdfBox");
// make sure that they are working for this test
if (unoTransformer.getReliability(MimetypeMap.MIMETYPE_PPT, MimetypeMap.MIMETYPE_PDF) == 0.0)
{
isAvailable = false;
return;
}
else if (pdfBoxTransformer.getReliability(MimetypeMap.MIMETYPE_PDF, MimetypeMap.MIMETYPE_TEXT_PLAIN) == 0.0)
{
isAvailable = false;
return;
}
else
{
isAvailable = true;
}
transformer = new ComplexContentTransformer();
transformer.setMimetypeService(mimetypeMap);
// set the transformer list
List<ContentTransformer> transformers = new ArrayList<ContentTransformer>(2);
transformers.add(unoTransformer);
transformers.add(pdfBoxTransformer);
transformer.setTransformers(transformers);
// set the intermediate mimetypes
List<String> intermediateMimetypes = Collections.singletonList(MimetypeMap.MIMETYPE_PDF);
transformer.setIntermediateMimetypes(intermediateMimetypes);
}
/**
* @return Returns the same transformer regardless - it is allowed
*/
protected ContentTransformer getTransformer(String sourceMimetype, String targetMimetype)
{
return transformer;
}
public void testReliability() throws Exception
{
if (!isAvailable)
{
return;
}
double reliability = 0.0;
reliability = transformer.getReliability(MimetypeMap.MIMETYPE_PPT, MimetypeMap.MIMETYPE_PDF);
assertEquals("Mimetype should not be supported", 0.0, reliability);
reliability = transformer.getReliability(MimetypeMap.MIMETYPE_PPT, MimetypeMap.MIMETYPE_TEXT_PLAIN);
assertEquals("Mimetype should be supported", 1.0, reliability);
}
}

View File

@@ -0,0 +1,248 @@
/*
* 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.transform;
import java.io.File;
import java.util.LinkedList;
import java.util.Map;
import org.alfresco.error.AlfrescoRuntimeException;
import org.alfresco.repo.content.filestore.FileContentWriter;
import org.alfresco.service.cmr.repository.ContentIOException;
import org.alfresco.service.cmr.repository.ContentReader;
import org.alfresco.service.cmr.repository.ContentWriter;
import org.alfresco.util.TempFileProvider;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
* A chain of transformations that is able to produce non-zero reliability
* transformation from one mimetype to another.
* <p>
* The reliability of the chain is the product of all the individual
* transformations.
*
* @author Derek Hulley
*/
public class CompoundContentTransformer implements ContentTransformer
{
private static final Log logger = LogFactory.getLog(CompoundContentTransformer.class);
/** a sequence of transformers to apply */
private LinkedList<Transformation> chain;
/** the combined reliability of all the transformations in the chain */
private double reliability;
public CompoundContentTransformer()
{
chain = new LinkedList<Transformation>();
reliability = 1.0;
}
/**
* Adds a transformation to the chain. The reliability of each transformation
* added must be greater than 0.0.
*
* @param sourceMimetype
* @param targetMimetype
* @param transformer the transformer that will transform from the source to
* the target mimetype
*/
public void addTransformation(String sourceMimetype, String targetMimetype, ContentTransformer transformer)
{
// create a transformation that aggregates the transform info
Transformation transformation = new Transformation(
transformer,
sourceMimetype,
targetMimetype);
// add to the chain
chain.add(transformation);
// recalculate combined reliability
double transformerReliability = transformer.getReliability(sourceMimetype, targetMimetype);
if (transformerReliability <= 0.0 || transformerReliability > 1.0)
{
throw new AlfrescoRuntimeException(
"Reliability of transformer must be between 0.0 and 1.0: \n" +
" transformer: " + transformer + "\n" +
" source: " + sourceMimetype + "\n" +
" target: " + targetMimetype + "\n" +
" reliability: " + transformerReliability);
}
this.reliability *= transformerReliability;
}
/**
* In order to score anything, the source mimetype must match the source
* mimetype of the first transformer and the target mimetype must match
* the target mimetype of the last transformer in the chain.
*
* @return Returns the product of the individual reliability scores of the
* transformations in the chain
*/
public double getReliability(String sourceMimetype, String targetMimetype)
{
if (chain.size() == 0)
{
// no transformers therefore no transformation possible
return 0.0;
}
Transformation first = chain.getFirst();
Transformation last = chain.getLast();
if (!first.getSourceMimetype().equals(sourceMimetype)
&& last.getTargetMimetype().equals(targetMimetype))
{
// the source type of the first transformation must match the source
// the target type of the last transformation must match the target
return 0.0;
}
return reliability;
}
/**
* @return Returns 0 if there are no transformers in the chain otherwise
* returns the sum of all the individual transformation times
*/
public long getTransformationTime()
{
long transformationTime = 0L;
for (Transformation transformation : chain)
{
ContentTransformer transformer = transformation.transformer;
transformationTime += transformer.getTransformationTime();
}
return transformationTime;
}
/**
*
*/
public void transform(ContentReader reader, ContentWriter writer) throws ContentIOException
{
transform(reader, writer, null);
}
/**
* Executes each transformer in the chain, passing the content between them
*/
public void transform(ContentReader reader, ContentWriter writer, Map<String, Object> options)
throws ContentIOException
{
if (chain.size() == 0)
{
throw new AlfrescoRuntimeException("No transformations present in chain");
}
// check that the mimetypes of the transformation are valid for the chain
String sourceMimetype = reader.getMimetype();
String targetMimetype = writer.getMimetype();
Transformation firstTransformation = chain.getFirst();
Transformation lastTransformation = chain.getLast();
if (!firstTransformation.getSourceMimetype().equals(sourceMimetype)
&& lastTransformation.getTargetMimetype().equals(targetMimetype))
{
throw new AlfrescoRuntimeException("Attempting to perform unreliable transformation: \n" +
" reader: " + reader + "\n" +
" writer: " + writer);
}
ContentReader currentReader = reader;
ContentWriter currentWriter = null;
int currentIndex = 0;
for (Transformation transformation : chain)
{
boolean last = (currentIndex == chain.size() - 1);
if (last)
{
// we are on the last transformation so use the final output writer
currentWriter = writer;
}
else
{
// have to create an intermediate writer - just use a file writer
File tempFile = TempFileProvider.createTempFile("transform", ".tmp");
currentWriter = new FileContentWriter(tempFile);
// set the writer's mimetype to conform to the transformation we are using
currentWriter.setMimetype(transformation.getTargetMimetype());
}
// transform from the current reader to the current writer
transformation.execute(currentReader, currentWriter, options);
if (!currentWriter.isClosed())
{
throw new AlfrescoRuntimeException("Writer not closed by transformation: \n" +
" transformation: " + transformation + "\n" +
" writer: " + currentWriter);
}
// if we have more transformations, then use the written content
// as the next source
if (!last)
{
currentReader = currentWriter.getReader();
}
}
// done
if (logger.isDebugEnabled())
{
logger.debug("Executed complex transformation: \n" +
" chain: " + chain + "\n" +
" reader: " + reader + "\n" +
" writer: " + writer);
}
}
/**
* A transformation that contains the transformer as well as the
* transformation mimetypes to be used
*/
public static class Transformation extends ContentTransformerRegistry.TransformationKey
{
private ContentTransformer transformer;
public Transformation(ContentTransformer transformer, String sourceMimetype, String targetMimetype)
{
super(sourceMimetype, targetMimetype);
this.transformer = transformer;
}
/**
* Executs the transformation
*
* @param reader the reader from which to read the content
* @param writer the writer to write content to
* @param options the options to execute with
* @throws ContentIOException if the transformation fails
*/
public void execute(ContentReader reader, ContentWriter writer, Map<String, Object> options)
throws ContentIOException
{
String sourceMimetype = getSourceMimetype();
String targetMimetype = getTargetMimetype();
// check that the source and target mimetypes of the reader and writer match
if (!sourceMimetype.equals(reader.getMimetype()))
{
throw new AlfrescoRuntimeException("The source mimetype doesn't match the reader's mimetype: \n" +
" source mimetype: " + sourceMimetype + "\n" +
" reader: " + reader);
}
if (!targetMimetype.equals(writer.getMimetype()))
{
throw new AlfrescoRuntimeException("The target mimetype doesn't match the writer's mimetype: \n" +
" target mimetype: " + targetMimetype + "\n" +
" writer: " + writer);
}
transformer.transform(reader, writer, options);
}
}
}

View File

@@ -0,0 +1,84 @@
/*
* 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.transform;
import java.util.Map;
import org.alfresco.service.cmr.repository.ContentIOException;
import org.alfresco.service.cmr.repository.ContentReader;
import org.alfresco.service.cmr.repository.ContentWriter;
/**
* Interface for class that allow content transformation from one mimetype to another.
*
* @author Derek Hulley
*/
public interface ContentTransformer
{
/**
* Provides the approximate accuracy with which this transformer can
* transform from one mimetype to another.
* <p>
* This method is used to determine, up front, which of a set of
* transformers will be used to perform a specific transformation.
*
* @param sourceMimetype the source mimetype
* @param targetMimetype the target mimetype
* @return Returns a score 0.0 to 1.0. 0.0 indicates that the
* transformation cannot be performed at all. 1.0 indicates that
* the transformation can be performed perfectly.
*/
public double getReliability(String sourceMimetype, String targetMimetype);
/**
* Provides an estimate, usually a worst case guess, of how long a transformation
* will take.
* <p>
* This method is used to determine, up front, which of a set of
* equally reliant transformers will be used for a specific transformation.
*
* @return Returns the approximate number of milliseconds per transformation
*/
public long getTransformationTime();
/**
* @see #transform(ContentReader, ContentWriter, Map)
*/
public void transform(ContentReader reader, ContentWriter writer) throws ContentIOException;
/**
* Transforms the content provided by the reader and source mimetype
* to the writer and target mimetype.
* <p>
* The transformation viability can be determined by an up front call
* to {@link #getReliability(String, String)}.
* <p>
* The source and target mimetypes <b>must</b> be available on the
* {@link org.alfresco.service.cmr.repository.ContentAccessor#getMimetype()} methods of
* both the reader and the writer.
*
* @param reader the source of the content
* @param writer the destination of the transformed content
* @param options options to pass to the transformer. These are transformer dependent
* and may be null.
* @throws ContentIOException if an IO exception occurs
*/
public void transform(
ContentReader reader,
ContentWriter writer,
Map<String, Object> options) throws ContentIOException;
}

View File

@@ -0,0 +1,362 @@
/*
* 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.transform;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import org.alfresco.error.AlfrescoRuntimeException;
import org.alfresco.repo.content.MimetypeMap;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.util.Assert;
/**
* Holds and provides the most appropriate content transformer for
* a particular source and target mimetype transformation request.
* <p>
* The transformers themselves are used to determine the applicability
* of a particular transformation.
*
* @see org.alfresco.repo.content.transform.ContentTransformer
*
* @author Derek Hulley
*/
public class ContentTransformerRegistry
{
private static final Log logger = LogFactory.getLog(ContentTransformerRegistry.class);
private List<ContentTransformer> transformers;
private MimetypeMap mimetypeMap;
/** Cache of previously used transactions */
private Map<TransformationKey, List<ContentTransformer>> transformationCache;
private short accessCount;
/** Controls read access to the transformation cache */
private Lock transformationCacheReadLock;
/** controls write access to the transformation cache */
private Lock transformationCacheWriteLock;
/**
* @param mimetypeMap all the mimetypes available to the system
*/
public ContentTransformerRegistry(MimetypeMap mimetypeMap)
{
Assert.notNull(mimetypeMap, "The MimetypeMap is mandatory");
this.mimetypeMap = mimetypeMap;
this.transformers = Collections.emptyList(); // just in case it isn't set
transformationCache = new HashMap<TransformationKey, List<ContentTransformer>>(17);
accessCount = 0;
// create lock objects for access to the cache
ReadWriteLock transformationCacheLock = new ReentrantReadWriteLock();
transformationCacheReadLock = transformationCacheLock.readLock();
transformationCacheWriteLock = transformationCacheLock.writeLock();
}
/**
* Provides a list of explicit transformers to use.
*
* @param transformations list of ( list of ( (from-mimetype)(to-mimetype)(transformer) ) )
*/
public void setExplicitTransformations(List<List<Object>> transformations)
{
for (List<Object> list : transformations)
{
if (list.size() != 3)
{
throw new AlfrescoRuntimeException(
"Explicit transformation is 'from-mimetype', 'to-mimetype' and 'transformer': \n" +
" list: " + list);
}
try
{
String sourceMimetype = (String) list.get(0);
String targetMimetype = (String) list.get(1);
ContentTransformer transformer = (ContentTransformer) list.get(2);
// create the transformation
TransformationKey key = new TransformationKey(sourceMimetype, targetMimetype);
// bypass all discovery and plug this directly into the cache
transformationCache.put(key, Collections.singletonList(transformer));
}
catch (ClassCastException e)
{
throw new AlfrescoRuntimeException(
"Explicit transformation is 'from-mimetype', 'to-mimetype' and 'transformer': \n" +
" list: " + list);
}
}
}
/**
* Provides a list of self-discovering transformers that the registry will fall
* back on if a transformation is not available from the explicitly set
* transformations.
*
* @param transformers all the available transformers that the registry can
* work with
*/
public void setTransformers(List<ContentTransformer> transformers)
{
this.transformers = transformers;
}
/**
* Resets the transformation cache. This allows a fresh analysis of the best
* conversions based on actual average performance of the transformers.
*/
public void resetCache()
{
// get a write lock on the cache
transformationCacheWriteLock.lock();
try
{
transformationCache.clear();
accessCount = 0;
}
finally
{
transformationCacheWriteLock.unlock();
}
// done
if (logger.isDebugEnabled())
{
logger.debug("Content transformation cache reset");
}
}
/**
* Gets the best transformer possible. This is a combination of the most reliable
* and the most performant transformer.
* <p>
* The result is cached for quicker access next time.
*
* @param sourceMimetype the source mimetype of the transformation
* @param targetMimetype the target mimetype of the transformation
* @return Returns a content transformer that can perform the desired
* transformation or null if no transformer could be found that would do it.
*/
public ContentTransformer getTransformer(String sourceMimetype, String targetMimetype)
{
// check that the mimetypes are valid
if (!mimetypeMap.getMimetypes().contains(sourceMimetype))
{
throw new AlfrescoRuntimeException("Unknown source mimetype: " + sourceMimetype);
}
if (!mimetypeMap.getMimetypes().contains(targetMimetype))
{
throw new AlfrescoRuntimeException("Unknown target mimetype: " + targetMimetype);
}
TransformationKey key = new TransformationKey(sourceMimetype, targetMimetype);
List<ContentTransformer> transformers = null;
transformationCacheReadLock.lock();
try
{
if (transformationCache.containsKey(key))
{
// the translation has been requested before
// it might have been null
transformers = transformationCache.get(key);
}
}
finally
{
transformationCacheReadLock.unlock();
}
if (transformers == null)
{
// the translation has not been requested before
// get a write lock on the cache
// no double check done as it is not an expensive task
transformationCacheWriteLock.lock();
try
{
// find the most suitable transformer - may be empty list
transformers = findTransformers(sourceMimetype, targetMimetype);
// store the result even if it is null
transformationCache.put(key, transformers);
}
finally
{
transformationCacheWriteLock.unlock();
}
}
// select the most performant transformer
long bestTime = -1L;
ContentTransformer bestTransformer = null;
for (ContentTransformer transformer : transformers)
{
long transformationTime = transformer.getTransformationTime();
// is it better?
if (bestTransformer == null || transformationTime < bestTime)
{
bestTransformer = transformer;
bestTime = transformationTime;
}
}
// done
return bestTransformer;
}
/**
* Gets all transformers, of equal reliability, that can perform the requested transformation.
*
* @return Returns best transformer for the translation - null if all
* score 0.0 on reliability
*/
private List<ContentTransformer> findTransformers(String sourceMimetype, String targetMimetype)
{
// search for a simple transformer that can do the job
List<ContentTransformer> transformers = findDirectTransformers(sourceMimetype, targetMimetype);
// get the complex transformers that can do the job
List<ContentTransformer> complexTransformers = findComplexTransformer(sourceMimetype, targetMimetype);
transformers.addAll(complexTransformers);
// done
if (logger.isDebugEnabled())
{
logger.debug("Searched for transformer: \n" +
" source mimetype: " + sourceMimetype + "\n" +
" target mimetype: " + targetMimetype + "\n" +
" transformers: " + transformers);
}
return transformers;
}
/**
* Loops through the content transformers and picks the ones with the highest reliabilities.
* <p>
* Where there are several transformers that are equally reliable, they are all returned.
*
* @return Returns the most reliable transformers for the translation - empty list if there
* are none.
*/
private List<ContentTransformer> findDirectTransformers(String sourceMimetype, String targetMimetype)
{
double maxReliability = 0.0;
long leastTime = 100000L; // 100 seconds - longer than anyone would think of waiting
List<ContentTransformer> bestTransformers = new ArrayList<ContentTransformer>(2);
// loop through transformers
for (ContentTransformer transformer : this.transformers)
{
double reliability = transformer.getReliability(sourceMimetype, targetMimetype);
if (reliability <= 0.0)
{
// it is unusable
continue;
}
else if (reliability < maxReliability)
{
// it is not the best one to use
continue;
}
else if (reliability == maxReliability)
{
// it is as reliable as a previous transformer
}
else
{
// it is better than any previous transformer - wipe them
bestTransformers.clear();
maxReliability = reliability;
}
// add the transformer to the list
bestTransformers.add(transformer);
}
// done
return bestTransformers;
}
/**
* Uses a list of known mimetypes to build transformations from several direct transformations.
*/
private List<ContentTransformer> findComplexTransformer(String sourceMimetype, String targetMimetype)
{
// get a complete list of mimetypes
// TODO: Build complex transformers by searching for transformations by mimetype
return Collections.emptyList();
}
/**
* Recursive method to build up a list of content transformers
*/
private void buildTransformer(List<ContentTransformer> transformers,
double reliability,
List<String> touchedMimetypes,
String currentMimetype,
String targetMimetype)
{
throw new UnsupportedOperationException();
}
/**
* A key for a combination of a source and target mimetype
*/
public static class TransformationKey
{
private final String sourceMimetype;
private final String targetMimetype;
private final String key;
public TransformationKey(String sourceMimetype, String targetMimetype)
{
this.key = (sourceMimetype + "_" + targetMimetype);
this.sourceMimetype = sourceMimetype;
this.targetMimetype = targetMimetype;
}
public String getSourceMimetype()
{
return sourceMimetype;
}
public String getTargetMimetype()
{
return targetMimetype;
}
@Override
public boolean equals(Object obj)
{
if (obj == null)
{
return false;
}
else if (this == obj)
{
return true;
}
else if (!(obj instanceof TransformationKey))
{
return false;
}
TransformationKey that = (TransformationKey) obj;
return this.key.equals(that.key);
}
@Override
public int hashCode()
{
return key.hashCode();
}
}
}

View File

@@ -0,0 +1,237 @@
/*
* 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.transform;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import org.alfresco.repo.content.MimetypeMap;
import org.alfresco.repo.content.filestore.FileContentReader;
import org.alfresco.repo.content.filestore.FileContentWriter;
import org.alfresco.service.cmr.repository.ContentReader;
import org.alfresco.service.cmr.repository.ContentWriter;
import org.alfresco.util.TempFileProvider;
/**
* @see org.alfresco.repo.content.transform.ContentTransformerRegistry
*
* @author Derek Hulley
*/
public class ContentTransformerRegistryTest extends AbstractContentTransformerTest
{
private static final String A = MimetypeMap.MIMETYPE_TEXT_PLAIN;
private static final String B = MimetypeMap.MIMETYPE_XML;
private static final String C = MimetypeMap.MIMETYPE_WORD;
private static final String D = MimetypeMap.MIMETYPE_HTML;
/** a real registry with real transformers */
private ContentTransformerRegistry registry;
/** a fake registry with fake transformers */
private ContentTransformerRegistry dummyRegistry;
private ContentReader reader;
private ContentWriter writer;
/**
* Allows dependency injection
*/
public void setContentTransformerRegistry(ContentTransformerRegistry registry)
{
this.registry = registry;
}
@Override
public void onSetUpInTransaction() throws Exception
{
reader = new FileContentReader(TempFileProvider.createTempFile(getName(), ".txt"));
reader.setMimetype(A);
writer = new FileContentWriter(TempFileProvider.createTempFile(getName(), ".txt"));
writer.setMimetype(D);
byte[] bytes = new byte[256];
for (int i = 0; i < 256; i++)
{
bytes[i] = (byte)i;
}
List<ContentTransformer> transformers = new ArrayList<ContentTransformer>(5);
// create some dummy transformers for reliability tests
transformers.add(new DummyTransformer(A, B, 0.3, 10L));
transformers.add(new DummyTransformer(A, B, 0.6, 10L));
transformers.add(new DummyTransformer(A, C, 0.5, 10L));
transformers.add(new DummyTransformer(A, C, 1.0, 10L));
transformers.add(new DummyTransformer(B, C, 0.2, 10L));
// create some dummy transformers for speed tests
transformers.add(new DummyTransformer(A, D, 1.0, 20L));
transformers.add(new DummyTransformer(A, D, 1.0, 20L));
transformers.add(new DummyTransformer(A, D, 1.0, 10L)); // the fast one
transformers.add(new DummyTransformer(A, D, 1.0, 20L));
transformers.add(new DummyTransformer(A, D, 1.0, 20L));
// create the dummyRegistry
dummyRegistry = new ContentTransformerRegistry(mimetypeMap);
dummyRegistry.setTransformers(transformers);
}
/**
* Checks that required objects are present
*/
public void testSetUp() throws Exception
{
super.testSetUp();
assertNotNull(registry);
}
/**
* @return Returns the transformer provided by the <b>real</b> registry
*/
protected ContentTransformer getTransformer(String sourceMimetype, String targetMimetype)
{
return registry.getTransformer(sourceMimetype, targetMimetype);
}
public void testNullRetrieval() throws Exception
{
ContentTransformer transformer = null;
transformer = dummyRegistry.getTransformer(C, B);
assertNull("No transformer expected", transformer);
transformer = dummyRegistry.getTransformer(C, A);
assertNull("No transformer expected", transformer);
transformer = dummyRegistry.getTransformer(B, A);
assertNull("No transformer expected", transformer);
}
public void testSimpleRetrieval() throws Exception
{
ContentTransformer transformer = null;
// B -> C expect 0.2
transformer = dummyRegistry.getTransformer(B, C);
transformer = dummyRegistry.getTransformer(B, C);
assertNotNull("No transformer found", transformer);
assertEquals("Incorrect reliability", 0.2, transformer.getReliability(B, C));
assertEquals("Incorrect reliability", 0.0, transformer.getReliability(C, B));
}
/**
* Force some equally reliant transformers to do some work and develop
* different average transformation times. Check that the registry
* copes with the new averages after a reset.
*/
public void testPerformanceRetrieval() throws Exception
{
// A -> D expect 1.0, 10ms
ContentTransformer transformer1 = dummyRegistry.getTransformer(A, D);
assertEquals("Incorrect reliability", 1.0, transformer1.getReliability(A, D));
assertEquals("Incorrect reliability", 0.0, transformer1.getReliability(D, A));
assertEquals("Incorrect transformation time", 10L, transformer1.getTransformationTime());
}
public void testScoredRetrieval() throws Exception
{
ContentTransformer transformer = null;
// A -> B expect 0.6
transformer = dummyRegistry.getTransformer(A, B);
assertNotNull("No transformer found", transformer);
assertEquals("Incorrect reliability", 0.6, transformer.getReliability(A, B));
assertEquals("Incorrect reliability", 0.0, transformer.getReliability(B, A));
// A -> C expect 1.0
transformer = dummyRegistry.getTransformer(A, C);
assertNotNull("No transformer found", transformer);
assertEquals("Incorrect reliability", 1.0, transformer.getReliability(A, C));
assertEquals("Incorrect reliability", 0.0, transformer.getReliability(C, A));
}
/**
* Set an explicit, and bizarre, transformation. Check that it is used.
*
*/
public void testExplicitTransformation()
{
ContentTransformer dummyTransformer = new DummyTransformer(
MimetypeMap.MIMETYPE_FLASH, MimetypeMap.MIMETYPE_EXCEL, 1.0, 12345);
List<Object> transform = new ArrayList<Object>(3);
transform.add(MimetypeMap.MIMETYPE_FLASH);
transform.add(MimetypeMap.MIMETYPE_EXCEL);
transform.add(dummyTransformer);
List<List<Object>> explicitTransformers = Collections.singletonList(transform);
// add it to the registry
dummyRegistry.setExplicitTransformations(explicitTransformers);
// get the appropriate transformer for the bizarre mapping
ContentTransformer checkTransformer = dummyRegistry.getTransformer(
MimetypeMap.MIMETYPE_FLASH, MimetypeMap.MIMETYPE_EXCEL);
assertNotNull("No explicit transformer found", checkTransformer);
assertTrue("Expected explicit transformer", dummyTransformer == checkTransformer);
}
/**
* Dummy transformer that does no transformation and scores exactly as it is
* told to in the constructor. It enables the tests to be sure of what to expect.
*/
private static class DummyTransformer extends AbstractContentTransformer
{
private String sourceMimetype;
private String targetMimetype;
private double reliability;
private long transformationTime;
public DummyTransformer(String sourceMimetype, String targetMimetype,
double reliability, long transformationTime)
{
this.sourceMimetype = sourceMimetype;
this.targetMimetype = targetMimetype;
this.reliability = reliability;
this.transformationTime = transformationTime;
}
public double getReliability(String sourceMimetype, String targetMimetype)
{
if (this.sourceMimetype.equals(sourceMimetype)
&& this.targetMimetype.equals(targetMimetype))
{
return reliability;
}
else
{
return 0.0;
}
}
/**
* Just notches up some average times
*/
public void transformInternal(
ContentReader reader,
ContentWriter writer,
Map<String, Object> options) throws Exception
{
// just update the transformation time
super.recordTime(transformationTime);
}
/**
* @return Returns the fixed dummy average transformation time
*/
public synchronized long getTransformationTime()
{
return transformationTime;
}
}
}

View File

@@ -0,0 +1,76 @@
/*
* 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.transform;
import java.io.File;
import java.util.Map;
import org.alfresco.repo.content.MimetypeMap;
import org.alfresco.service.cmr.repository.ContentReader;
import org.alfresco.service.cmr.repository.ContentWriter;
import org.alfresco.util.TempFileProvider;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.htmlparser.beans.StringBean;
/**
* @see http://htmlparser.sourceforge.net/
* @see org.htmlparser.beans.StringBean
*
* @author Derek Hulley
*/
public class HtmlParserContentTransformer extends AbstractContentTransformer
{
private static final Log logger = LogFactory.getLog(HtmlParserContentTransformer.class);
/**
* Only support HTML to TEXT.
*/
public double getReliability(String sourceMimetype, String targetMimetype)
{
if (!MimetypeMap.MIMETYPE_HTML.equals(sourceMimetype) ||
!MimetypeMap.MIMETYPE_TEXT_PLAIN.equals(targetMimetype))
{
// only support HTML -> TEXT
return 0.0;
}
else
{
return 1.0;
}
}
public void transformInternal(ContentReader reader, ContentWriter writer, Map<String, Object> options)
throws Exception
{
// we can only work from a file
File htmlFile = TempFileProvider.createTempFile("HtmlParserContentTransformer_", ".html");
reader.getContent(htmlFile);
// create the extractor
StringBean extractor = new StringBean();
extractor.setCollapse(false);
extractor.setLinks(false);
extractor.setReplaceNonBreakingSpaces(false);
extractor.setURL(htmlFile.getAbsolutePath());
// get the text
String text = extractor.getStrings();
// write it to the writer
writer.putContent(text);
}
}

View File

@@ -0,0 +1,58 @@
/*
* 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.transform;
import org.alfresco.repo.content.MimetypeMap;
/**
* @see org.alfresco.repo.content.transform.HtmlParserContentTransformer
*
* @author Derek Hulley
*/
public class HtmlParserContentTransformerTest extends AbstractContentTransformerTest
{
private static final String SOME_CONTENT = "azAz10!<21>$%^&*()\t\r\n";
private ContentTransformer transformer;
@Override
public void onSetUpInTransaction() throws Exception
{
transformer = new HtmlParserContentTransformer();
}
protected ContentTransformer getTransformer(String sourceMimetype, String targetMimetype)
{
return transformer;
}
public void testSetUp() throws Exception
{
assertNotNull(transformer);
}
public void checkReliability() throws Exception
{
// check reliability
double reliability = transformer.getReliability(MimetypeMap.MIMETYPE_HTML, MimetypeMap.MIMETYPE_TEXT_PLAIN);
assertEquals("Reliability incorrect", 1.0, reliability); // plain text to plain text is 100%
// check other way around
reliability = transformer.getReliability(MimetypeMap.MIMETYPE_TEXT_PLAIN, MimetypeMap.MIMETYPE_HTML);
assertEquals("Reliability incorrect", 0.0, reliability); // plain text to plain text is 0%
}
}

View File

@@ -0,0 +1,87 @@
/*
* 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.transform;
import java.io.IOException;
import java.util.Map;
import org.alfresco.repo.content.MimetypeMap;
import org.alfresco.service.cmr.repository.ContentIOException;
import org.alfresco.service.cmr.repository.ContentReader;
import org.alfresco.service.cmr.repository.ContentWriter;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.pdfbox.pdmodel.PDDocument;
import org.pdfbox.util.PDFTextStripper;
/**
* Makes use of the {@link http://www.pdfbox.org/ PDFBox} library to
* perform conversions from PDF files to text.
*
* @author Derek Hulley
*/
public class PdfBoxContentTransformer extends AbstractContentTransformer
{
private static final Log logger = LogFactory.getLog(PdfBoxContentTransformer.class);
/**
* Currently the only transformation performed is that of text extraction from PDF documents.
*/
public double getReliability(String sourceMimetype, String targetMimetype)
{
// TODO: Expand PDFBox usage to convert images to PDF and investigate other conversions
if (!MimetypeMap.MIMETYPE_PDF.equals(sourceMimetype) ||
!MimetypeMap.MIMETYPE_TEXT_PLAIN.equals(targetMimetype))
{
// only support PDF -> Text
return 0.0;
}
else
{
return 1.0;
}
}
public void transformInternal(ContentReader reader, ContentWriter writer, Map<String, Object> options)
{
PDDocument pdf = null;
try
{
// stream the document in
pdf = PDDocument.load(reader.getContentInputStream());
// strip the text out
PDFTextStripper stripper = new PDFTextStripper();
String text = stripper.getText(pdf);
// dump it all to the writer
writer.putContent(text);
}
catch (IOException e)
{
throw new ContentIOException("PDF text stripping failed: \n" +
" reader: " + reader);
}
finally
{
if (pdf != null)
{
try { pdf.close(); } catch (Throwable e) {e.printStackTrace(); }
}
}
}
}

View File

@@ -0,0 +1,55 @@
/*
* 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.transform;
import org.alfresco.repo.content.MimetypeMap;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
* @see org.alfresco.repo.content.transform.PdfBoxContentTransformer
*
* @author Derek Hulley
*/
public class PdfBoxContentTransformerTest extends AbstractContentTransformerTest
{
private static final Log logger = LogFactory.getLog(PdfBoxContentTransformerTest.class);
private ContentTransformer transformer;
public void onSetUpInTransaction() throws Exception
{
transformer = new PdfBoxContentTransformer();
}
/**
* @return Returns the same transformer regardless - it is allowed
*/
protected ContentTransformer getTransformer(String sourceMimetype, String targetMimetype)
{
return transformer;
}
public void testReliability() throws Exception
{
double reliability = 0.0;
reliability = transformer.getReliability(MimetypeMap.MIMETYPE_TEXT_PLAIN, MimetypeMap.MIMETYPE_PDF);
assertEquals("Mimetype should not be supported", 0.0, reliability);
reliability = transformer.getReliability(MimetypeMap.MIMETYPE_PDF, MimetypeMap.MIMETYPE_TEXT_PLAIN);
assertEquals("Mimetype should be supported", 1.0, reliability);
}
}

View File

@@ -0,0 +1,251 @@
/*
* 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.transform;
import java.io.OutputStream;
import java.util.Map;
import org.alfresco.repo.content.MimetypeMap;
import org.alfresco.service.cmr.repository.ContentReader;
import org.alfresco.service.cmr.repository.ContentWriter;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.poi.hssf.usermodel.HSSFCell;
import org.apache.poi.hssf.usermodel.HSSFRow;
import org.apache.poi.hssf.usermodel.HSSFSheet;
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
/**
* Makes use of the {@link http://jakarta.apache.org/poi/ POI} library to
* perform conversions from Excel spreadsheets to text (comma separated).
* <p>
* While most text extraction from spreadsheets only extract the first sheet of
* the workbook, the method used here extracts the text from <b>all the sheets</b>.
* This is more useful, especially when it comes to indexing spreadsheets.
* <p>
* In the case where there is only one sheet in the document, the results will be
* exactly the same as most extractors. Where there are multiple sheets, the results
* will differ, but meaningful reimporting of the text document is not possible
* anyway.
*
* @author Derek Hulley
*/
public class PoiHssfContentTransformer extends AbstractContentTransformer
{
/**
* Windows carriage return line feed pair.
*/
private static final String LINE_BREAK = "\r\n";
private static final Log logger = LogFactory.getLog(PoiHssfContentTransformer.class);
/**
* Currently the only transformation performed is that of text extraction from XLS documents.
*/
public double getReliability(String sourceMimetype, String targetMimetype)
{
if (!MimetypeMap.MIMETYPE_EXCEL.equals(sourceMimetype) ||
!MimetypeMap.MIMETYPE_TEXT_PLAIN.equals(targetMimetype))
{
// only support XLS -> Text
return 0.0;
}
else
{
return 1.0;
}
}
public void transformInternal(ContentReader reader, ContentWriter writer, Map<String, Object> options)
throws Exception
{
OutputStream os = writer.getContentOutputStream();
String encoding = writer.getEncoding();
try
{
// open the workbook
HSSFWorkbook workbook = new HSSFWorkbook(reader.getContentInputStream());
// how many sheets are there?
int sheetCount = workbook.getNumberOfSheets();
// transform each sheet
for (int i = 0; i < sheetCount; i++)
{
HSSFSheet sheet = workbook.getSheetAt(i);
String sheetName = workbook.getSheetName(i);
writeSheet(os, sheet, encoding);
// write the sheet name
PoiHssfContentTransformer.writeString(os, encoding, LINE_BREAK, false);
PoiHssfContentTransformer.writeString(os, encoding, "End of sheet: " + sheetName, true);
PoiHssfContentTransformer.writeString(os, encoding, LINE_BREAK, false);
PoiHssfContentTransformer.writeString(os, encoding, LINE_BREAK, false);
}
}
finally
{
if (os != null)
{
try { os.close(); } catch (Throwable e) {}
}
}
}
/**
* Dumps the text from the sheet to the stream in CSV format
*/
private void writeSheet(OutputStream os, HSSFSheet sheet, String encoding) throws Exception
{
int rows = sheet.getLastRowNum();
// transform each row
for (int i = 0; i <= rows; i++)
{
HSSFRow row = sheet.getRow(i);
if (row != null)
{
writeRow(os, row, encoding);
}
// break between rows
if (i < rows)
{
PoiHssfContentTransformer.writeString(os, encoding, LINE_BREAK, false);
}
}
}
private void writeRow(OutputStream os, HSSFRow row, String encoding) throws Exception
{
short firstCellNum = row.getFirstCellNum();
short lastCellNum = row.getLastCellNum();
// pad out to first cell
for (short i = 0; i < firstCellNum; i++)
{
PoiHssfContentTransformer.writeString(os, encoding, ",", false); // CSV up to first cell
}
// write each cell
for (short i = 0; i <= lastCellNum; i++)
{
HSSFCell cell = row.getCell(i);
if (cell != null)
{
StringBuilder sb = new StringBuilder(10);
switch (cell.getCellType())
{
case HSSFCell.CELL_TYPE_BLANK:
// ignore
break;
case HSSFCell.CELL_TYPE_BOOLEAN:
sb.append(cell.getBooleanCellValue());
break;
case HSSFCell.CELL_TYPE_ERROR:
sb.append("ERROR");
break;
case HSSFCell.CELL_TYPE_FORMULA:
double dataNumber = cell.getNumericCellValue();
if (Double.isNaN(dataNumber))
{
// treat it as a string
sb.append(cell.getStringCellValue());
}
else
{
// treat it as a number
sb.append(dataNumber);
}
break;
case HSSFCell.CELL_TYPE_NUMERIC:
sb.append(cell.getNumericCellValue());
break;
case HSSFCell.CELL_TYPE_STRING:
sb.append(cell.getStringCellValue());
break;
default:
throw new RuntimeException("Unknown HSSF cell type: " + cell);
}
String data = sb.toString();
PoiHssfContentTransformer.writeString(os, encoding, data, true);
}
// comma separate if required
if (i < lastCellNum)
{
PoiHssfContentTransformer.writeString(os, encoding, ",", false);
}
}
}
/**
* Writes the given data to the stream using the encoding specified. If the encoding
* is not given, the default <tt>String</tt> to <tt>byte[]</tt> conversion will be
* used.
* <p>
* The given data string will be escaped appropriately.
*
* @param os the stream to write to
* @param encoding the encoding to use, or null if the default encoding is acceptable
* @param value the string to write
* @param isData true if the value represents a human-readable string, false if the
* value represents formatting characters, separating characters, etc.
* @throws Exception
*/
public static void writeString(OutputStream os, String encoding, String value, boolean isData) throws Exception
{
if (value == null)
{
// nothing to do
return;
}
int dataLength = value.length();
if (dataLength == 0)
{
// nothing to do
return;
}
// escape the string
StringBuilder sb = new StringBuilder(dataLength + 5); // slightly longer than the data
for (int i = 0; i < dataLength; i++)
{
char currentChar = value.charAt(i);
if (currentChar == '\"') // inverted commas
{
sb.append("\""); // CSV escaping of inverted commas
}
// append the char
sb.append(currentChar);
}
// enclose in inverted commas for safety
if (isData)
{
sb.insert(0, "\"");
sb.append("\"");
}
// escaping complete
value = sb.toString();
byte[] bytes = null;
if (encoding == null)
{
// use default encoding
bytes = value.getBytes();
}
else
{
bytes = value.getBytes(encoding);
}
// write to the stream
os.write(bytes);
// done
}
}

View File

@@ -0,0 +1,90 @@
/*
* 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.transform;
import java.io.File;
import java.io.InputStream;
import org.alfresco.repo.content.MimetypeMap;
import org.alfresco.repo.content.filestore.FileContentWriter;
import org.alfresco.service.cmr.repository.ContentReader;
import org.alfresco.util.TempFileProvider;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
* @see org.alfresco.repo.content.transform.PoiHssfContentTransformer
*
* @author Derek Hulley
*/
public class PoiHssfContentTransformerTest extends AbstractContentTransformerTest
{
private static final Log logger = LogFactory.getLog(PoiHssfContentTransformerTest.class);
private ContentTransformer transformer;
public void onSetUpInTransaction() throws Exception
{
transformer = new PoiHssfContentTransformer();
}
/**
* @return Returns the same transformer regardless - it is allowed
*/
protected ContentTransformer getTransformer(String sourceMimetype, String targetMimetype)
{
return transformer;
}
public void testReliability() throws Exception
{
double reliability = 0.0;
reliability = transformer.getReliability(MimetypeMap.MIMETYPE_TEXT_PLAIN, MimetypeMap.MIMETYPE_EXCEL);
assertEquals("Mimetype should not be supported", 0.0, reliability);
reliability = transformer.getReliability(MimetypeMap.MIMETYPE_EXCEL, MimetypeMap.MIMETYPE_TEXT_PLAIN);
assertEquals("Mimetype should be supported", 1.0, reliability);
}
/**
* Tests a specific failure in the library
*/
public void xtestBugFixAR114() throws Exception
{
File tempFile = TempFileProvider.createTempFile(
getClass().getSimpleName() + "_" + getName() + "_",
".xls");
FileContentWriter writer = new FileContentWriter(tempFile);
writer.setMimetype(MimetypeMap.MIMETYPE_EXCEL);
// get the test resource and write it (Excel)
InputStream is = getClass().getClassLoader().getResourceAsStream("Plan270904b.xls");
assertNotNull("Test resource not found: Plan270904b.xls");
writer.putContent(is);
// get the source of the transformation
ContentReader reader = writer.getReader();
// make a new location of the transform output (plain text)
tempFile = TempFileProvider.createTempFile(
getClass().getSimpleName() + "_" + getName() + "_",
".txt");
writer = new FileContentWriter(tempFile);
writer.setMimetype(MimetypeMap.MIMETYPE_TEXT_PLAIN);
// transform it
transformer.transform(reader, writer);
}
}

View File

@@ -0,0 +1,287 @@
/*
* 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.transform;
import java.io.File;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;
import org.alfresco.error.AlfrescoRuntimeException;
import org.alfresco.service.cmr.repository.ContentIOException;
import org.alfresco.service.cmr.repository.ContentReader;
import org.alfresco.service.cmr.repository.ContentWriter;
import org.alfresco.service.cmr.repository.MimetypeService;
import org.alfresco.util.TempFileProvider;
import org.alfresco.util.exec.RuntimeExec;
import org.alfresco.util.exec.RuntimeExec.ExecutionResult;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
* This configurable wrapper is able to execute any command line transformation that
* accepts an input and an output file on the command line.
* <p>
* The following parameters are use:
* <ul>
* <li><b>{@link #VAR_SOURCE target}</b> - full path to the source file</li>
* <li><b>{@link #VAR_TARGET source}</b> - full path to the target file</li>
* </ul>
* Provided that the command executed ultimately transforms the source file
* and leaves the result in the target file, the transformation should be
* successful.
* <p>
* <b>NOTE</b>: It is only the contents of the files that can be transformed.
* Any attempt to modify the source or target file metadata will, at best, have
* no effect, but may ultimately lead to the transformation failing. This is
* because the files provided are both temporary files that reside in a location
* outside the system's content store.
*
* @see org.alfresco.util.exec.RuntimeExec
*
* @since 1.1
* @author Derek Hulley
*/
public class RuntimeExecutableContentTransformer extends AbstractContentTransformer
{
public static final String VAR_SOURCE = "source";
public static final String VAR_TARGET = "target";
private static Log logger = LogFactory.getLog(RuntimeExecutableContentTransformer.class);
private boolean available;
private MimetypeService mimetypeService;
private RuntimeExec checkCommand;
private RuntimeExec transformCommand;
private Set<Integer> errCodes;
public RuntimeExecutableContentTransformer()
{
this.errCodes = new HashSet<Integer>(2);
errCodes.add(1);
errCodes.add(2);
}
/**
* @param mimetypeService the mapping from mimetype to extensions
*/
public void setMimetypeService(MimetypeService mimetypeService)
{
this.mimetypeService = mimetypeService;
}
/**
* Set the runtime executer that will be called as part of the initialisation
* to determine if the transformer is able to function. This is optional, but allows
* the transformer registry to detect and avoid using this instance if it is not working.
* <p>
* The command will be considered to have failed if the
*
* @param checkCommand the initialisation check command
*/
public void setCheckCommand(RuntimeExec checkCommand)
{
this.checkCommand = checkCommand;
}
/**
* Set the runtime executer that will called to perform the actual transformation.
*
* @param transformCommand the runtime transform command
*/
public void setTransformCommand(RuntimeExec transformCommand)
{
this.transformCommand = transformCommand;
}
/**
* A comma or space separated list of values that, if returned by the executed command,
* indicate an error value. This defaults to <b>"1, 2"</b>.
*
* @param erroCodesStr
*/
public void setErrorCodes(String errCodesStr)
{
StringTokenizer tokenizer = new StringTokenizer(errCodesStr, " ,");
while(tokenizer.hasMoreElements())
{
String errCodeStr = tokenizer.nextToken();
// attempt to convert it to an integer
try
{
int errCode = Integer.parseInt(errCodeStr);
this.errCodes.add(errCode);
}
catch (NumberFormatException e)
{
throw new AlfrescoRuntimeException("Error codes string must be integers: " + errCodesStr);
}
}
}
/**
* @param exitValue the command exit value
* @return Returns true if the code is a listed failure code
*
* @see #setErrorCodes(String)
*/
private boolean isFailureCode(int exitValue)
{
return errCodes.contains((Integer)exitValue);
}
/**
* Executes the check command, if present. Any errors will result in this component
* being rendered unusable within the transformer registry, but may still be called
* directly.
*/
public void init()
{
if (transformCommand == null)
{
throw new AlfrescoRuntimeException("Mandatory property 'transformCommand' not set");
}
else if (mimetypeService == null)
{
throw new AlfrescoRuntimeException("Mandatory property 'mimetypeService' not set");
}
// execute the command
if (checkCommand != null)
{
ExecutionResult result = checkCommand.execute();
// check the return code
available = !isFailureCode(result.getExitValue());
}
else
{
// no check - just assume it is available
available = true;
}
}
/**
* Unless otherwise configured, this component supports all mimetypes.
* If the {@link #init() initialization} failed,
*/
public double getReliability(String sourceMimetype, String targetMimetype)
{
if (!available)
{
return 0.0;
}
else
{
return 1.0;
}
}
/**
* Converts the source and target content to temporary files with the
* correct extensions for the mimetype that they map to.
*
* @see #transformInternal(File, File)
*/
protected final void transformInternal(
ContentReader reader,
ContentWriter writer,
Map<String, Object> options) throws Exception
{
// get mimetypes
String sourceMimetype = getMimetype(reader);
String targetMimetype = getMimetype(writer);
// get the extensions to use
String sourceExtension = mimetypeService.getExtension(sourceMimetype);
String targetExtension = mimetypeService.getExtension(targetMimetype);
if (sourceExtension == null || targetExtension == null)
{
throw new AlfrescoRuntimeException("Unknown extensions for mimetypes: \n" +
" source mimetype: " + sourceMimetype + "\n" +
" source extension: " + sourceExtension + "\n" +
" target mimetype: " + targetMimetype + "\n" +
" target extension: " + targetExtension);
}
// if the source mimetype is the same as the target's then just stream it
if (sourceMimetype.equals(targetMimetype))
{
writer.putContent(reader.getContentInputStream());
return;
}
// create required temp files
File sourceFile = TempFileProvider.createTempFile(
getClass().getSimpleName() + "_source_",
"." + sourceExtension);
File targetFile = TempFileProvider.createTempFile(
getClass().getSimpleName() + "_target_",
"." + targetExtension);
Map<String, String> properties = new HashMap<String, String>(5);
// copy options over
for (Map.Entry<String, Object> entry : options.entrySet())
{
String key = entry.getKey();
Object value = entry.getValue();
properties.put(key, (value == null ? null : value.toString()));
}
// add the source and target properties
properties.put(VAR_SOURCE, sourceFile.getAbsolutePath());
properties.put(VAR_TARGET, targetFile.getAbsolutePath());
// pull reader file into source temp file
reader.getContent(sourceFile);
// execute the transformation command
ExecutionResult result = null;
try
{
result = transformCommand.execute(properties);
}
catch (Throwable e)
{
throw new ContentIOException("Transformation failed during command execution: \n" + transformCommand, e);
}
// check
if (isFailureCode(result.getExitValue()))
{
throw new ContentIOException("Transformation failed - status indicates an error: \n" + result);
}
// check that the file was created
if (!targetFile.exists())
{
throw new ContentIOException("Transformation failed - target file doesn't exist: \n" + result);
}
// copy the target file back into the repo
writer.putContent(targetFile);
// done
if (logger.isDebugEnabled())
{
logger.debug("Transformation completed: \n" +
" source: " + reader + "\n" +
" target: " + writer + "\n" +
" options: " + options + "\n" +
" result: \n" + result);
}
}
}

View File

@@ -0,0 +1,82 @@
/*
* 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.transform;
import java.io.File;
import java.util.HashMap;
import java.util.Map;
import org.alfresco.repo.content.MimetypeMap;
import org.alfresco.repo.content.filestore.FileContentWriter;
import org.alfresco.service.cmr.repository.ContentReader;
import org.alfresco.service.cmr.repository.ContentWriter;
import org.alfresco.util.BaseAlfrescoTestCase;
import org.alfresco.util.TempFileProvider;
import org.alfresco.util.exec.RuntimeExec;
/**
* @see org.alfresco.repo.content.transform.RuntimeExecutableContentTransformer
*
* @author Derek Hulley
*/
public class RuntimeExecutableContentTransformerTest extends BaseAlfrescoTestCase
{
private RuntimeExecutableContentTransformer transformer;
@Override
protected void setUp() throws Exception
{
super.setUp();
transformer = new RuntimeExecutableContentTransformer();
// the command to execute
RuntimeExec transformCommand = new RuntimeExec();
Map<String, String> commandMap = new HashMap<String, String>(5);
commandMap.put("Linux", "mv -f ${source} ${target}");
commandMap.put("*", "cmd /c copy /Y \"${source}\" \"${target}\"");
transformCommand.setCommandMap(commandMap);
transformer.setTransformCommand(transformCommand);
transformer.setMimetypeService(serviceRegistry.getMimetypeService());
transformer.setErrorCodes("1, 2");
// initialise so that it doesn't score 0
transformer.init();
}
public void testCopyCommand() throws Exception
{
String content = "<A><B></B></A>";
// create the source
File sourceFile = TempFileProvider.createTempFile(getName() + "_", ".txt");
ContentWriter tempWriter = new FileContentWriter(sourceFile);
tempWriter.setMimetype(MimetypeMap.MIMETYPE_TEXT_PLAIN);
tempWriter.putContent(content);
ContentReader reader = tempWriter.getReader();
// create the target
File targetFile = TempFileProvider.createTempFile(getName() + "_", ".xml");
ContentWriter writer = new FileContentWriter(targetFile);
writer.setMimetype(MimetypeMap.MIMETYPE_XML);
// do the transformation
transformer.transform(reader, writer); // no options on the copy
// make sure that the content was copied over
ContentReader checkReader = writer.getReader();
String checkContent = checkReader.getContentString();
assertEquals("Content not copied", content, checkContent);
}
}

View File

@@ -0,0 +1,141 @@
/*
* 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.transform;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.Writer;
import java.util.Map;
import org.alfresco.repo.content.MimetypeMap;
import org.alfresco.service.cmr.repository.ContentReader;
import org.alfresco.service.cmr.repository.ContentWriter;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
* Converts any textual format to plain text.
* <p>
* The transformation is sensitive to the source and target string encodings.
*
* @author Derek Hulley
*/
public class StringExtractingContentTransformer extends AbstractContentTransformer
{
public static final String PREFIX_TEXT = "text/";
private static final Log logger = LogFactory.getLog(StringExtractingContentTransformer.class);
/**
* Gives a high reliability for all translations from <i>text/sometype</i> to
* <i>text/plain</i>. As the text formats are already text, the characters
* are preserved and no actual conversion takes place.
* <p>
* Extraction of text from binary data is wholly unreliable.
*/
public double getReliability(String sourceMimetype, String targetMimetype)
{
if (!targetMimetype.equals(MimetypeMap.MIMETYPE_TEXT_PLAIN))
{
// can only convert to plain text
return 0.0;
}
else if (sourceMimetype.equals(MimetypeMap.MIMETYPE_TEXT_PLAIN))
{
// conversions from any plain text format are very reliable
return 1.0;
}
else if (sourceMimetype.startsWith(PREFIX_TEXT))
{
// the source is text, but probably with some kind of markup
return 0.1;
}
else
{
// extracting text from binary is not useful
return 0.0;
}
}
/**
* Text to text conversions are done directly using the content reader and writer string
* manipulation methods.
* <p>
* Extraction of text from binary content attempts to take the possible character
* encoding into account. The text produced from this will, if the encoding was correct,
* be unformatted but valid.
*/
@Override
public void transformInternal(ContentReader reader, ContentWriter writer, Map<String, Object> options)
throws Exception
{
// is this a straight text-text transformation
transformText(reader, writer);
}
/**
* Transformation optimized for text-to-text conversion
*/
private void transformText(ContentReader reader, ContentWriter writer) throws Exception
{
// get a char reader and writer
Reader charReader = null;
Writer charWriter = null;
try
{
if (reader.getEncoding() == null)
{
charReader = new InputStreamReader(reader.getContentInputStream());
}
else
{
charReader = new InputStreamReader(reader.getContentInputStream(), reader.getEncoding());
}
if (writer.getEncoding() == null)
{
charWriter = new OutputStreamWriter(writer.getContentOutputStream());
}
else
{
charWriter = new OutputStreamWriter(writer.getContentOutputStream(), writer.getEncoding());
}
// copy from the one to the other
char[] buffer = new char[1024];
int readCount = 0;
while (readCount > -1)
{
// write the last read count number of bytes
charWriter.write(buffer, 0, readCount);
// fill the buffer again
readCount = charReader.read(buffer);
}
}
finally
{
if (charReader != null)
{
try { charReader.close(); } catch (Throwable e) { logger.error(e); }
}
if (charWriter != null)
{
try { charWriter.close(); } catch (Throwable e) { logger.error(e); }
}
}
// done
}
}

View File

@@ -0,0 +1,162 @@
/*
* 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.transform;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.util.Random;
import org.alfresco.repo.content.MimetypeMap;
import org.alfresco.repo.content.filestore.FileContentReader;
import org.alfresco.repo.content.filestore.FileContentWriter;
import org.alfresco.service.cmr.repository.ContentReader;
import org.alfresco.service.cmr.repository.ContentWriter;
import org.alfresco.util.TempFileProvider;
/**
* @see org.alfresco.repo.content.transform.StringExtractingContentTransformer
*
* @author Derek Hulley
*/
public class StringExtractingContentTransformerTest extends AbstractContentTransformerTest
{
private static final String SOME_CONTENT = "azAz10!<21>$%^&*()\t\r\n";
private ContentTransformer transformer;
/** the final destination of transformations */
private ContentWriter targetWriter;
@Override
public void onSetUpInTransaction() throws Exception
{
transformer = new StringExtractingContentTransformer();
targetWriter = new FileContentWriter(getTempFile());
targetWriter.setMimetype("text/plain");
targetWriter.setEncoding("UTF-8");
}
protected ContentTransformer getTransformer(String sourceMimetype, String targetMimetype)
{
return transformer;
}
public void testSetUp() throws Exception
{
assertNotNull(transformer);
}
/**
* @return Returns a new temp file
*/
private File getTempFile()
{
return TempFileProvider.createTempFile(getName(), ".txt");
}
/**
* Writes some content using the mimetype and encoding specified.
*
* @param mimetype
* @param encoding
* @return Returns a reader onto the newly written content
*/
private ContentReader writeContent(String mimetype, String encoding)
{
ContentWriter writer = new FileContentWriter(getTempFile());
writer.setMimetype(mimetype);
writer.setEncoding(encoding);
// put content
writer.putContent(SOME_CONTENT);
// return a reader onto the new content
return writer.getReader();
}
public void testDirectTransform() throws Exception
{
ContentReader reader = writeContent("text/plain", "latin1");
// check reliability
double reliability = transformer.getReliability(reader.getMimetype(), targetWriter.getMimetype());
assertEquals("Reliability incorrect", 1.0, reliability); // plain text to plain text is 100%
// transform
transformer.transform(reader, targetWriter);
// get a reader onto the transformed content and check
ContentReader checkReader = targetWriter.getReader();
String checkContent = checkReader.getContentString();
assertEquals("Content check failed", SOME_CONTENT, checkContent);
}
public void testInterTextTransform() throws Exception
{
ContentReader reader = writeContent("text/xml", "UTF-16");
// check reliability
double reliability = transformer.getReliability(reader.getMimetype(), targetWriter.getMimetype());
assertEquals("Reliability incorrect", 0.1, reliability); // markup to plain text not 100%
// transform
transformer.transform(reader, targetWriter);
// get a reader onto the transformed content and check
ContentReader checkReader = targetWriter.getReader();
String checkContent = checkReader.getContentString();
assertEquals("Content check failed", SOME_CONTENT, checkContent);
}
/**
* Generate a large file and then transform it using the text extractor.
* We are not creating super-large file (1GB) in order to test the transform
* as it takes too long to create the file in the first place. Rather,
* this test can be used during profiling to ensure that memory is not
* being consumed.
*/
public void testLargeFileStreaming() throws Exception
{
File sourceFile = TempFileProvider.createTempFile(getName(), ".txt");
int chars = 1000000; // a million characters should do the trick
Random random = new Random();
Writer charWriter = new OutputStreamWriter(new BufferedOutputStream(new FileOutputStream(sourceFile)));
for (int i = 0; i < chars; i++)
{
char next = (char)(random.nextDouble() * 93D + 32D);
charWriter.write(next);
}
charWriter.close();
// get a reader and a writer
ContentReader reader = new FileContentReader(sourceFile);
reader.setMimetype(MimetypeMap.MIMETYPE_TEXT_PLAIN);
File outputFile = TempFileProvider.createTempFile(getName(), ".txt");
ContentWriter writer = new FileContentWriter(outputFile);
writer.setMimetype(MimetypeMap.MIMETYPE_TEXT_PLAIN);
// transform
transformer.transform(reader, writer);
// delete files
sourceFile.delete();
outputFile.delete();
}
}

View File

@@ -0,0 +1,86 @@
/*
* 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.transform;
import java.io.IOException;
import java.io.InputStream;
import java.util.Map;
import org.alfresco.repo.content.MimetypeMap;
import org.alfresco.service.cmr.repository.ContentReader;
import org.alfresco.service.cmr.repository.ContentWriter;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.textmining.text.extraction.WordExtractor;
/**
* Makes use of the {@link http://www.textmining.org/ TextMining} library to
* perform conversions from MSWord documents to text.
*
* @author Derek Hulley
*/
public class TextMiningContentTransformer extends AbstractContentTransformer
{
private static final Log logger = LogFactory.getLog(TextMiningContentTransformer.class);
private WordExtractor wordExtractor;
public TextMiningContentTransformer()
{
this.wordExtractor = new WordExtractor();
}
/**
* Currently the only transformation performed is that of text extraction from Word documents.
*/
public double getReliability(String sourceMimetype, String targetMimetype)
{
if (!MimetypeMap.MIMETYPE_WORD.equals(sourceMimetype) ||
!MimetypeMap.MIMETYPE_TEXT_PLAIN.equals(targetMimetype))
{
// only support DOC -> Text
return 0.0;
}
else
{
return 1.0;
}
}
public void transformInternal(ContentReader reader, ContentWriter writer, Map<String, Object> options)
throws Exception
{
InputStream is = reader.getContentInputStream();
String text = null;
try
{
text = wordExtractor.extractText(is);
}
catch (IOException e)
{
// check if this is an error caused by the fact that the .doc is in fact
// one of Word's temp non-documents
if (e.getMessage().contains("Unable to read entire header"))
{
// just assign an empty string
text = "";
}
}
// dump the text out
writer.putContent(text);
}
}

View File

@@ -0,0 +1,90 @@
/*
* 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.transform;
import java.io.File;
import java.io.InputStream;
import org.alfresco.repo.content.MimetypeMap;
import org.alfresco.repo.content.filestore.FileContentWriter;
import org.alfresco.service.cmr.repository.ContentReader;
import org.alfresco.util.TempFileProvider;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
* @see org.alfresco.repo.content.transform.TextMiningContentTransformer
*
* @author Derek Hulley
*/
public class TextMiningContentTransformerTest extends AbstractContentTransformerTest
{
private static final Log logger = LogFactory.getLog(TextMiningContentTransformerTest.class);
private ContentTransformer transformer;
public void onSetUpInTransaction() throws Exception
{
transformer = new TextMiningContentTransformer();
}
/**
* @return Returns the same transformer regardless - it is allowed
*/
protected ContentTransformer getTransformer(String sourceMimetype, String targetMimetype)
{
return transformer;
}
public void testReliability() throws Exception
{
double reliability = 0.0;
reliability = transformer.getReliability(MimetypeMap.MIMETYPE_TEXT_PLAIN, MimetypeMap.MIMETYPE_WORD);
assertEquals("Mimetype should not be supported", 0.0, reliability);
reliability = transformer.getReliability(MimetypeMap.MIMETYPE_WORD, MimetypeMap.MIMETYPE_TEXT_PLAIN);
assertEquals("Mimetype should be supported", 1.0, reliability);
}
/**
* Tests a specific failure in the library
*/
public void testBugFixAR1() throws Exception
{
File tempFile = TempFileProvider.createTempFile(
getClass().getSimpleName() + "_" + getName() + "_",
".doc");
FileContentWriter writer = new FileContentWriter(tempFile);
writer.setMimetype(MimetypeMap.MIMETYPE_WORD);
// get the test resource and write it (MS Word)
InputStream is = getClass().getClassLoader().getResourceAsStream("farmers_markets_list_2003.doc");
assertNotNull("Test resource not found: farmers_markets_list_2003.doc");
writer.putContent(is);
// get the source of the transformation
ContentReader reader = writer.getReader();
// make a new location of the transform output (plain text)
tempFile = TempFileProvider.createTempFile(
getClass().getSimpleName() + "_" + getName() + "_",
".txt");
writer = new FileContentWriter(tempFile);
writer.setMimetype(MimetypeMap.MIMETYPE_TEXT_PLAIN);
// transform it
transformer.transform(reader, writer);
}
}

View File

@@ -0,0 +1,279 @@
/*
* 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.transform;
import java.io.File;
import java.io.IOException;
import java.net.ConnectException;
import java.util.HashMap;
import java.util.Map;
import net.sf.joott.uno.DocumentConverter;
import net.sf.joott.uno.DocumentFormat;
import net.sf.joott.uno.UnoConnection;
import org.alfresco.repo.content.MimetypeMap;
import org.alfresco.service.cmr.repository.ContentIOException;
import org.alfresco.service.cmr.repository.ContentReader;
import org.alfresco.service.cmr.repository.ContentWriter;
import org.alfresco.util.TempFileProvider;
/**
* Makes use of the OpenOffice Uno interfaces to convert the content.
* <p>
* The conversions are slow but reliable.
*
* @author Derek Hulley
*/
public class UnoContentTransformer extends AbstractContentTransformer
{
/** map of <tt>DocumentFormat</tt> instances keyed by mimetype conversion */
private static Map<ContentTransformerRegistry.TransformationKey, DocumentFormatWrapper> formatsByConversion;
static
{
// Build the map of known Uno document formats and store by conversion key
formatsByConversion = new HashMap<ContentTransformerRegistry.TransformationKey, DocumentFormatWrapper>(17);
formatsByConversion.put(
new ContentTransformerRegistry.TransformationKey(MimetypeMap.MIMETYPE_TEXT_PLAIN, MimetypeMap.MIMETYPE_HTML),
new DocumentFormatWrapper(DocumentFormat.HTML_WRITER, 1.0));
formatsByConversion.put(
new ContentTransformerRegistry.TransformationKey(MimetypeMap.MIMETYPE_TEXT_PLAIN, MimetypeMap.MIMETYPE_PDF),
new DocumentFormatWrapper(DocumentFormat.PDF_WRITER, 1.0));
formatsByConversion.put(
new ContentTransformerRegistry.TransformationKey(MimetypeMap.MIMETYPE_TEXT_PLAIN, MimetypeMap.MIMETYPE_WORD),
new DocumentFormatWrapper(DocumentFormat.TEXT, 1.0));
formatsByConversion.put(
new ContentTransformerRegistry.TransformationKey(MimetypeMap.MIMETYPE_WORD, MimetypeMap.MIMETYPE_TEXT_PLAIN),
new DocumentFormatWrapper(DocumentFormat.TEXT, 1.0));
formatsByConversion.put(
new ContentTransformerRegistry.TransformationKey(MimetypeMap.MIMETYPE_WORD, MimetypeMap.MIMETYPE_PDF),
new DocumentFormatWrapper(DocumentFormat.PDF_WRITER, 1.0));
formatsByConversion.put(
new ContentTransformerRegistry.TransformationKey(MimetypeMap.MIMETYPE_EXCEL, MimetypeMap.MIMETYPE_TEXT_PLAIN),
new DocumentFormatWrapper(DocumentFormat.TEXT_CALC, 0.8)); // only first sheet extracted
formatsByConversion.put(
new ContentTransformerRegistry.TransformationKey(MimetypeMap.MIMETYPE_EXCEL, MimetypeMap.MIMETYPE_PDF),
new DocumentFormatWrapper(DocumentFormat.PDF_CALC, 1.0));
formatsByConversion.put(
new ContentTransformerRegistry.TransformationKey(MimetypeMap.MIMETYPE_PPT, MimetypeMap.MIMETYPE_FLASH),
new DocumentFormatWrapper(DocumentFormat.FLASH_IMPRESS, 1.0));
formatsByConversion.put(
new ContentTransformerRegistry.TransformationKey(MimetypeMap.MIMETYPE_PPT, MimetypeMap.MIMETYPE_PDF),
new DocumentFormatWrapper(DocumentFormat.PDF_IMPRESS, 1.0));
formatsByConversion.put(
new ContentTransformerRegistry.TransformationKey(MimetypeMap.MIMETYPE_WORD, MimetypeMap.MIMETYPE_HTML),
new DocumentFormatWrapper(DocumentFormat.HTML_WRITER, 1.0));
formatsByConversion.put(
new ContentTransformerRegistry.TransformationKey(MimetypeMap.MIMETYPE_HTML, MimetypeMap.MIMETYPE_PDF),
new DocumentFormatWrapper(DocumentFormat.PDF_WRITER_WEB, 1.0));
// there are many more formats available and therefore many more transformation combinations possible
// DocumentFormat.FLASH_IMPRESS
// DocumentFormat.HTML_CALC
// DocumentFormat.HTML_WRITER
// DocumentFormat.MS_EXCEL_97
// DocumentFormat.MS_POWERPOINT_97
// DocumentFormat.MS_WORD_97
// DocumentFormat.PDF_CALC
// DocumentFormat.PDF_IMPRESS
// DocumentFormat.PDF_WRITER
// DocumentFormat.PDF_WRITER_WEB
// DocumentFormat.RTF
// DocumentFormat.TEXT
// DocumentFormat.TEXT_CALC
// DocumentFormat.XML_CALC
// DocumentFormat.XML_IMPRESS
// DocumentFormat.XML_WRITER
// DocumentFormat.XML_WRITER_WEB
}
private String connectionUrl = UnoConnection.DEFAULT_CONNECTION_STRING;
private UnoConnection connection;
private boolean isConnected;
/**
* Constructs the default transformer that will attempt to connect to the
* Uno server using the default connect string.
*
* @see UnoConnection#DEFAULT_CONNECTION_STRING
*/
public UnoContentTransformer()
{
}
/**
* Override the default connection URL with a new one.
*
* @param connectionUrl the connection string
*
* @see UnoConnection#DEFAULT_CONNECTION_STRING
*/
public void setConnectionUrl(String connectionUrl)
{
this.connectionUrl = connectionUrl;
}
/**
* Perform bean initialization
*/
public synchronized void init()
{
connection = new UnoConnection(connectionUrl);
// attempt to make an connection
try
{
connection.connect();
isConnected = true;
}
catch (ConnectException e)
{
isConnected = false;
}
}
/**
* @return Returns true if a connection to the Uno server could be established
*/
public boolean isConnected()
{
return isConnected;
}
/**
* @param sourceMimetype
* @param targetMimetype
* @return Returns a document format wrapper that is valid for the given source and target mimetypes
*/
private static DocumentFormatWrapper getDocumentFormatWrapper(String sourceMimetype, String targetMimetype)
{
// get the well-known document format for the specific conversion
ContentTransformerRegistry.TransformationKey key =
new ContentTransformerRegistry.TransformationKey(sourceMimetype, targetMimetype);
DocumentFormatWrapper wrapper = UnoContentTransformer.formatsByConversion.get(key);
return wrapper;
}
/**
* Checks how reliable the conversion will be when performed by the Uno server.
* <p>
* The connection for the Uno server is checked in order to have any chance of
* being reliable.
* <p>
* The conversions' reliabilities are set up statically based on prior tests that
* included checking performance as well as accuracy.
*/
public double getReliability(String sourceMimetype, String targetMimetype)
{
// check if a connection to the Uno server can be established
if (!isConnected())
{
// no connection means that conversion is not possible
return 0.0;
}
// check if the source and target mimetypes are supported
DocumentFormatWrapper docFormatWrapper = getDocumentFormatWrapper(sourceMimetype, targetMimetype);
if (docFormatWrapper == null)
{
return 0.0;
}
else
{
return docFormatWrapper.getReliability();
}
}
public void transformInternal(ContentReader reader, ContentWriter writer, Map<String, Object> options)
throws Exception
{
String sourceMimetype = getMimetype(reader);
String targetMimetype = getMimetype(writer);
// create temporary files to convert from and to
File tempFromFile = TempFileProvider.createTempFile(
"UnoContentTransformer",
"." + getMimetypeService().getExtension(sourceMimetype));
File tempToFile = TempFileProvider.createTempFile(
"UnoContentTransformer",
"." + getMimetypeService().getExtension(targetMimetype));
// download the content from the source reader
reader.getContent(tempFromFile);
// get the document format that should be used
DocumentFormatWrapper docFormatWrapper = getDocumentFormatWrapper(sourceMimetype, targetMimetype);
try
{
docFormatWrapper.execute(tempFromFile, tempToFile, connection);
// conversion success
}
catch (ConnectException e)
{
throw new ContentIOException("Connection to Uno server failed: \n" +
" reader: " + reader + "\n" +
" writer: " + writer,
e);
}
catch (IOException e)
{
throw new ContentIOException("Uno server conversion failed: \n" +
" reader: " + reader + "\n" +
" writer: " + writer + "\n" +
" from file: " + tempFromFile + "\n" +
" to file: " + tempToFile,
e);
}
// upload the temp output to the writer given us
writer.putContent(tempToFile);
}
/**
* Wraps a document format as well the reliability. The source and target mimetypes
* are not kept, but will probably be closely associated with the reliability.
*/
private static class DocumentFormatWrapper
{
/*
* Source and target mimetypes not kept -> class is private as it doesn't keep
* enough info to be used safely externally
*/
private DocumentFormat documentFormat;
private double reliability;
public DocumentFormatWrapper(DocumentFormat documentFormat, double reliability)
{
this.documentFormat = documentFormat;
this.reliability = reliability;
}
public double getReliability()
{
return reliability;
}
/**
* Executs the transformation
*/
public void execute(File fromFile, File toFile, UnoConnection connection) throws ConnectException, IOException
{
DocumentConverter converter = new DocumentConverter(connection);
converter.convert(fromFile, toFile, documentFormat);
}
}
}

View File

@@ -0,0 +1,71 @@
/*
* 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.transform;
import org.alfresco.repo.content.MimetypeMap;
/**
* @see org.alfresco.repo.content.transform.UnoContentTransformer
*
* @author Derek Hulley
*/
public class UnoContentTransformerTest extends AbstractContentTransformerTest
{
private static String MIMETYPE_RUBBISH = "text/rubbish";
private UnoContentTransformer transformer;
public void onSetUpInTransaction() throws Exception
{
transformer = new UnoContentTransformer();
transformer.setMimetypeService(mimetypeMap);
}
/**
* @return Returns the same transformer regardless - it is allowed
*/
protected ContentTransformer getTransformer(String sourceMimetype, String targetMimetype)
{
return transformer;
}
public void testSetUp() throws Exception
{
super.testSetUp();
assertNotNull(mimetypeMap);
}
public void testReliability() throws Exception
{
if (!transformer.isConnected())
{
// no connection
return;
}
double reliability = 0.0;
reliability = transformer.getReliability(MIMETYPE_RUBBISH, MimetypeMap.MIMETYPE_TEXT_PLAIN);
assertEquals("Mimetype should not be supported", 0.0, reliability);
reliability = transformer.getReliability(MimetypeMap.MIMETYPE_TEXT_PLAIN, MIMETYPE_RUBBISH);
assertEquals("Mimetype should not be supported", 0.0, reliability);
reliability = transformer.getReliability(MimetypeMap.MIMETYPE_TEXT_PLAIN, MimetypeMap.MIMETYPE_WORD);
assertEquals("Mimetype should be supported", 1.0, reliability);
reliability = transformer.getReliability(MimetypeMap.MIMETYPE_WORD, MimetypeMap.MIMETYPE_TEXT_PLAIN);
assertEquals("Mimetype should be supported", 1.0, reliability);
reliability = transformer.getReliability(MimetypeMap.MIMETYPE_EXCEL, MimetypeMap.MIMETYPE_TEXT_PLAIN);
assertEquals("Mimetype should be supported", 0.8, reliability);
}
}

View File

@@ -0,0 +1,256 @@
/*
* 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.transform.magick;
import java.io.File;
import java.io.InputStream;
import java.util.Collections;
import java.util.Map;
import org.alfresco.error.AlfrescoRuntimeException;
import org.alfresco.repo.content.MimetypeMap;
import org.alfresco.repo.content.filestore.FileContentWriter;
import org.alfresco.repo.content.transform.AbstractContentTransformer;
import org.alfresco.service.cmr.repository.ContentIOException;
import org.alfresco.service.cmr.repository.ContentReader;
import org.alfresco.service.cmr.repository.ContentWriter;
import org.alfresco.util.TempFileProvider;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
* Abstract helper for transformations based on <b>ImageMagick</b>
*
* @author Derek Hulley
*/
public abstract class AbstractImageMagickContentTransformer extends AbstractContentTransformer
{
/** the prefix for mimetypes supported by the transformer */
public static final String MIMETYPE_IMAGE_PREFIX = "image/";
private static final Log logger = LogFactory.getLog(AbstractImageMagickContentTransformer.class);
private MimetypeMap mimetypeMap;
private boolean available;
public AbstractImageMagickContentTransformer()
{
this.available = false;
}
/**
* Set the mimetype map to resolve mimetypes to file extensions.
*
* @param mimetypeMap
*/
public void setMimetypeMap(MimetypeMap mimetypeMap)
{
this.mimetypeMap = mimetypeMap;
}
/**
* @return Returns true if the transformer is functioning otherwise false
*/
public boolean isAvailable()
{
return available;
}
/**
* Make the transformer available
* @param available
*/
protected void setAvailable(boolean available)
{
this.available = available;
}
/**
* Checks for the JMagick and ImageMagick dependencies, using the common
* {@link #transformInternal(File, File) transformation method} to check
* that the sample image can be converted.
*/
public void init()
{
if (mimetypeMap == null)
{
throw new AlfrescoRuntimeException("MimetypeMap not present");
}
try
{
// load, into memory the sample gif
String resourcePath = "org/alfresco/repo/content/transform/magick/alfresco.gif";
InputStream imageStream = getClass().getClassLoader().getResourceAsStream(resourcePath);
if (imageStream == null)
{
throw new AlfrescoRuntimeException("Sample image not found: " + resourcePath);
}
// dump to a temp file
File inputFile = TempFileProvider.createTempFile(
getClass().getSimpleName() + "_init_source_",
".gif");
FileContentWriter writer = new FileContentWriter(inputFile);
writer.putContent(imageStream);
// create the output file
File outputFile = TempFileProvider.createTempFile(
getClass().getSimpleName() + "_init_target_",
".png");
// execute it
Map<String, Object> options = Collections.emptyMap();
transformInternal(inputFile, outputFile, options);
// check that the file exists
if (!outputFile.exists())
{
throw new Exception("Image conversion failed: \n" +
" from: " + inputFile + "\n" +
" to: " + outputFile);
}
// we can be sure that it works
setAvailable(true);
}
catch (Throwable e)
{
logger.error(
getClass().getSimpleName() + " not available: " +
(e.getMessage() != null ? e.getMessage() : ""));
// debug so that we can trace the issue if required
logger.debug(e);
}
}
/**
* Some image formats are not supported by ImageMagick, or at least appear not to work.
*
* @param mimetype the mimetype to check
* @return Returns true if ImageMagic can handle the given image format
*/
public static boolean isSupported(String mimetype)
{
if (!mimetype.startsWith(MIMETYPE_IMAGE_PREFIX))
{
return false; // not an image
}
else if (mimetype.equals(MimetypeMap.MIMETYPE_IMAGE_RGB))
{
return false; // rgb extension doesn't work
}
else
{
return true;
}
}
/**
* Supports image to image conversion, but only if the JMagick library and required
* libraries are available.
*/
public double getReliability(String sourceMimetype, String targetMimetype)
{
if (!available)
{
return 0.0;
}
if (!AbstractImageMagickContentTransformer.isSupported(sourceMimetype) ||
!AbstractImageMagickContentTransformer.isSupported(targetMimetype))
{
// only support IMAGE -> IMAGE (excl. RGB)
return 0.0;
}
else
{
return 1.0;
}
}
/**
* @see #transformInternal(File, File)
*/
protected final void transformInternal(
ContentReader reader,
ContentWriter writer,
Map<String, Object> options) throws Exception
{
// get mimetypes
String sourceMimetype = getMimetype(reader);
String targetMimetype = getMimetype(writer);
// get the extensions to use
String sourceExtension = mimetypeMap.getExtension(sourceMimetype);
String targetExtension = mimetypeMap.getExtension(targetMimetype);
if (sourceExtension == null || targetExtension == null)
{
throw new AlfrescoRuntimeException("Unknown extensions for mimetypes: \n" +
" source mimetype: " + sourceMimetype + "\n" +
" source extension: " + sourceExtension + "\n" +
" target mimetype: " + targetMimetype + "\n" +
" target extension: " + targetExtension);
}
// if the source mimetype is the same as the target's then just stream it
if (sourceMimetype.equals(targetMimetype))
{
writer.putContent(reader.getContentInputStream());
return;
}
// create required temp files
File sourceFile = TempFileProvider.createTempFile(
getClass().getSimpleName() + "_source_",
"." + sourceExtension);
File targetFile = TempFileProvider.createTempFile(
getClass().getSimpleName() + "_target_",
"." + targetExtension);
// pull reader file into source temp file
reader.getContent(sourceFile);
// transform the source temp file to the target temp file
transformInternal(sourceFile, targetFile, options);
// check that the file was created
if (!targetFile.exists())
{
throw new ContentIOException("JMagick transformation failed to write output file");
}
// upload the output image
writer.putContent(targetFile);
// done
if (logger.isDebugEnabled())
{
logger.debug("Transformation completed: \n" +
" source: " + reader + "\n" +
" target: " + writer + "\n" +
" options: " + options);
}
}
/**
* Transform the image content from the source file to the target file
*
* @param sourceFile the source of the transformation
* @param targetFile the target of the transformation
* @param options the transformation options supported by ImageMagick
* @throws Exception
*/
protected abstract void transformInternal(
File sourceFile,
File targetFile,
Map<String, Object> options) throws Exception;
}

View File

@@ -0,0 +1,110 @@
/*
* 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.transform.magick;
import java.io.File;
import java.util.HashMap;
import java.util.Map;
import org.alfresco.error.AlfrescoRuntimeException;
import org.alfresco.service.cmr.repository.ContentIOException;
import org.alfresco.util.exec.RuntimeExec;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
* Executes a statement to implement
*
* @author Derek Hulley
*/
public class ImageMagickContentTransformer extends AbstractImageMagickContentTransformer
{
/** the command options, such as <b>--resize</b>, etc. */
public static final String KEY_OPTIONS = "options";
/** source variable name */
public static final String VAR_OPTIONS = "options";
/** source variable name */
public static final String VAR_SOURCE = "source";
/** target variable name */
public static final String VAR_TARGET = "target";
private static final Log logger = LogFactory.getLog(ImageMagickContentTransformer.class);
/** the system command executer */
private RuntimeExec executer;
public ImageMagickContentTransformer()
{
}
/**
* Set the runtime command executer that must be executed in order to run
* <b>ImageMagick</b>. Whether or not this is the full path to the convertCommand
* or just the convertCommand itself depends the environment setup.
* <p>
* The command must contain the variables <code>${source}</code> and
* <code>${target}</code>, which will be replaced by the names of the file to
* be transformed and the name of the output file respectively.
* <pre>
* convert ${source} ${target}
* </pre>
*
* @param executer the system command executer
*/
public void setExecuter(RuntimeExec executer)
{
this.executer = executer;
}
/**
* Checks for the JMagick and ImageMagick dependencies, using the common
* {@link #transformInternal(File, File) transformation method} to check
* that the sample image can be converted.
*/
public void init()
{
if (executer == null)
{
throw new AlfrescoRuntimeException("System runtime executer not set");
}
super.init();
}
/**
* Transform the image content from the source file to the target file
*/
protected void transformInternal(File sourceFile, File targetFile, Map<String, Object> options) throws Exception
{
Map<String, String> properties = new HashMap<String, String>(5);
// set properties
properties.put(KEY_OPTIONS, (String) options.get(KEY_OPTIONS));
properties.put(VAR_SOURCE, sourceFile.getAbsolutePath());
properties.put(VAR_TARGET, targetFile.getAbsolutePath());
// execute the statement
RuntimeExec.ExecutionResult result = executer.execute(properties);
if (result.getExitValue() != 0 && result.getStdErr() != null && result.getStdErr().length() > 0)
{
throw new ContentIOException("Failed to perform ImageMagick transformation: \n" + result);
}
// success
if (logger.isDebugEnabled())
{
logger.debug("ImageMagic executed successfully: \n" + executer);
}
}
}

View File

@@ -0,0 +1,67 @@
/*
* 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.transform.magick;
import java.util.Collections;
import org.alfresco.repo.content.MimetypeMap;
import org.alfresco.repo.content.transform.AbstractContentTransformerTest;
import org.alfresco.repo.content.transform.ContentTransformer;
import org.alfresco.util.exec.RuntimeExec;
/**
* @see org.alfresco.repo.content.transform.magick.JMagickContentTransformer
*
* @author Derek Hulley
*/
public class ImageMagickContentTransformerTest extends AbstractContentTransformerTest
{
private ImageMagickContentTransformer transformer;
public void onSetUpInTransaction() throws Exception
{
RuntimeExec executer = new RuntimeExec();
executer.setCommand("imconvert.exe ${source} ${options} ${target}");
executer.setDefaultProperties(Collections.singletonMap("options", ""));
transformer = new ImageMagickContentTransformer();
transformer.setMimetypeMap(mimetypeMap);
transformer.setExecuter(executer);
transformer.init();
}
/**
* @return Returns the same transformer regardless - it is allowed
*/
protected ContentTransformer getTransformer(String sourceMimetype, String targetMimetype)
{
return transformer;
}
public void testReliability() throws Exception
{
if (!transformer.isAvailable())
{
return;
}
double reliability = 0.0;
reliability = transformer.getReliability(MimetypeMap.MIMETYPE_IMAGE_GIF, MimetypeMap.MIMETYPE_TEXT_PLAIN);
assertEquals("Mimetype should not be supported", 0.0, reliability);
reliability = transformer.getReliability(MimetypeMap.MIMETYPE_IMAGE_GIF, MimetypeMap.MIMETYPE_IMAGE_JPEG);
assertEquals("Mimetype should be supported", 1.0, reliability);
}
}

View File

@@ -0,0 +1,57 @@
/*
* 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.transform.magick;
import java.io.File;
import java.util.Map;
import magick.ImageInfo;
import magick.MagickImage;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
* Makes use of the {@link http://www.textmining.org/ TextMining} library to
* perform conversions from MSWord documents to text.
*
* @author Derek Hulley
*/
public class JMagickContentTransformer extends AbstractImageMagickContentTransformer
{
private static final Log logger = LogFactory.getLog(JMagickContentTransformer.class);
public JMagickContentTransformer()
{
}
/**
* Uses the <b>JMagick</b> library to perform the transformation
*
* @param sourceFile
* @param targetFile
* @throws Exception
*/
@Override
protected void transformInternal(File sourceFile, File targetFile, Map<String, Object> options) throws Exception
{
ImageInfo imageInfo = new ImageInfo(sourceFile.getAbsolutePath());
MagickImage image = new MagickImage(imageInfo);
image.setFileName(targetFile.getAbsolutePath());
image.writeImage(imageInfo);
}
}

View File

@@ -0,0 +1,63 @@
/*
* 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.transform.magick;
import org.alfresco.repo.content.MimetypeMap;
import org.alfresco.repo.content.transform.AbstractContentTransformerTest;
import org.alfresco.repo.content.transform.ContentTransformer;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
* @see org.alfresco.repo.content.transform.magick.JMagickContentTransformer
*
* @author Derek Hulley
*/
public class JMagickContentTransformerTest extends AbstractContentTransformerTest
{
private static final Log logger = LogFactory.getLog(JMagickContentTransformerTest.class);
private JMagickContentTransformer transformer;
public void onSetUpInTransaction() throws Exception
{
transformer = new JMagickContentTransformer();
transformer.setMimetypeMap(mimetypeMap);
transformer.init();
}
/**
* @return Returns the same transformer regardless - it is allowed
*/
protected ContentTransformer getTransformer(String sourceMimetype, String targetMimetype)
{
return transformer;
}
public void testReliability() throws Exception
{
if (!transformer.isAvailable())
{
return;
}
double reliability = 0.0;
reliability = transformer.getReliability(MimetypeMap.MIMETYPE_IMAGE_GIF, MimetypeMap.MIMETYPE_TEXT_PLAIN);
assertEquals("Mimetype should not be supported", 0.0, reliability);
reliability = transformer.getReliability(MimetypeMap.MIMETYPE_IMAGE_GIF, MimetypeMap.MIMETYPE_IMAGE_JPEG);
assertEquals("Mimetype should be supported", 1.0, reliability);
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB