Allow other types of content URLs other than store://...

Enforce restriction that all content URLs must be of form protocol://identifier
Allow for read-only stores.
Improved tests so that it is easier, when writing a new store, to determine if the store is compliant or not.


git-svn-id: https://svn.alfresco.com/repos/alfresco-enterprise/alfresco/HEAD/root@5899 c4b6b30b-aa2e-2d43-bbcb-ca4b014f7261
This commit is contained in:
Derek Hulley
2007-06-09 00:43:02 +00:00
parent 9b03b15674
commit f30ccf8d6c
28 changed files with 1685 additions and 628 deletions

View File

@@ -34,6 +34,7 @@ import java.nio.channels.ReadableByteChannel;
import java.text.MessageFormat;
import org.alfresco.repo.content.AbstractContentReader;
import org.alfresco.repo.content.ContentStore;
import org.alfresco.repo.content.MimetypeMap;
import org.alfresco.service.cmr.repository.ContentIOException;
import org.alfresco.service.cmr.repository.ContentReader;
@@ -119,7 +120,9 @@ public class FileContentReader extends AbstractContentReader
*/
public FileContentReader(File file)
{
this(file, FileContentStore.STORE_PROTOCOL + file.getAbsolutePath());
this(
file,
FileContentStore.STORE_PROTOCOL + ContentStore.PROTOCOL_DELIMITER + file.getAbsolutePath());
}
/**

View File

@@ -26,16 +26,23 @@ package org.alfresco.repo.content.filestore;
import java.io.File;
import java.io.IOException;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.HashSet;
import java.util.Set;
import org.alfresco.error.AlfrescoRuntimeException;
import org.alfresco.repo.content.AbstractContentStore;
import org.alfresco.repo.content.ContentContext;
import org.alfresco.repo.content.ContentStore;
import org.alfresco.repo.content.EmptyContentReader;
import org.alfresco.repo.content.UnsupportedContentUrlException;
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.GUID;
import org.alfresco.util.Pair;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
@@ -50,11 +57,18 @@ import org.apache.commons.logging.LogFactory;
*/
public class FileContentStore extends AbstractContentStore
{
/**
* <b>store</b> is the new prefix for file content URLs
* @see ContentStore#PROTOCOL_DELIMITER
*/
public static final String STORE_PROTOCOL = "store";
private static final Log logger = LogFactory.getLog(FileContentStore.class);
private File rootDirectory;
private String rootAbsolutePath;
private boolean allowRandomAccess;
private boolean readOnly;
/**
* @param rootDirectoryStr the root under which files will be stored.
@@ -83,6 +97,7 @@ public class FileContentStore extends AbstractContentStore
this.rootDirectory = rootDirectory.getAbsoluteFile();
rootAbsolutePath = rootDirectory.getAbsolutePath();
allowRandomAccess = true;
readOnly = false;
}
public String toString()
@@ -90,6 +105,8 @@ public class FileContentStore extends AbstractContentStore
StringBuilder sb = new StringBuilder(36);
sb.append("FileContentStore")
.append("[ root=").append(rootDirectory)
.append(", allowRandomAccess=").append(allowRandomAccess)
.append(", readOnly=").append(readOnly)
.append("]");
return sb.toString();
}
@@ -110,6 +127,18 @@ public class FileContentStore extends AbstractContentStore
this.allowRandomAccess = allowRandomAccess;
}
/**
* File stores may optionally be declared read-only. This is useful when configuring
* a store, possibly temporarily, to act as a source of data but to preserve it against
* any writes.
*
* @param readOnly <tt>true</tt> to force the store to only allow reads.
*/
public void setReadOnly(boolean readOnly)
{
this.readOnly = readOnly;
}
/**
* Generates a new URL and file appropriate to it.
*
@@ -118,7 +147,7 @@ public class FileContentStore extends AbstractContentStore
*/
private File createNewFile() throws IOException
{
String contentUrl = createNewUrl();
String contentUrl = FileContentStore.createNewFileStoreUrl();
return createNewFile(contentUrl);
}
@@ -131,11 +160,20 @@ public class FileContentStore extends AbstractContentStore
*
* @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.
* @throws IOException
* if the file or parent directories couldn't be created or if the URL is already in use.
* @throws UnsupportedOperationException
* if the store is read-only
*
* @see #setReadOnly(boolean)
*/
public File createNewFile(String newContentUrl) throws IOException
{
if (readOnly)
{
throw new UnsupportedOperationException("This store is currently read-only: " + this);
}
File file = makeFile(newContentUrl);
// create the directory, if it doesn't exist
@@ -185,7 +223,7 @@ public class FileContentStore extends AbstractContentStore
index++;
}
// strip off the root path and adds the protocol prefix
String url = AbstractContentStore.STORE_PROTOCOL + path.substring(index);
String url = FileContentStore.STORE_PROTOCOL + ContentStore.PROTOCOL_DELIMITER + path.substring(index);
// replace '\' with '/' so that URLs are consistent across all filesystems
url = url.replace('\\', '/');
// done
@@ -195,34 +233,45 @@ public class FileContentStore extends AbstractContentStore
/**
* Creates a file from the given relative URL.
*
* @param contentUrl the content URL including the protocol prefix
* @return Returns a file representing the URL - the file may or may not
* exist
* @param contentUrl the content URL including the protocol prefix
* @return Returns a file representing the URL - the file may or may not
* exist
* @throws UnsupportedContentUrlException
* if the URL is invalid and doesn't support the
* {@link FileContentStore#STORE_PROTOCOL correct protocol}
*
* @see #checkUrl(String)
*/
private File makeFile(String contentUrl)
{
// take just the part after the protocol
String relativeUrl = FileContentStore.getRelativePart(contentUrl);
if (relativeUrl == null)
Pair<String, String> urlParts = super.getContentUrlParts(contentUrl);
String protocol = urlParts.getFirst();
String relativePath = urlParts.getSecond();
// Check the protocol
if (!protocol.equals(FileContentStore.STORE_PROTOCOL))
{
throw new ContentIOException(
"The content URL is not valid for this store: \n" +
" Store: " + this + "\n" +
" Content URL: " + contentUrl);
throw new UnsupportedContentUrlException(this, contentUrl);
}
// get the file
File file = new File(rootDirectory, relativeUrl);
File file = new File(rootDirectory, relativePath);
// done
return file;
}
/**
* @return Returns <tt>true</tt> always
*/
public boolean isWriteSupported()
{
return true;
}
/**
* Performs a direct check against the file for its existence.
*/
@Override
public boolean exists(String contentUrl) throws ContentIOException
public boolean exists(String contentUrl)
{
File file = makeFile(contentUrl);
return file.exists();
@@ -237,8 +286,17 @@ public class FileContentStore extends AbstractContentStore
try
{
File file = makeFile(contentUrl);
FileContentReader reader = new FileContentReader(file, contentUrl);
reader.setAllowRandomAccess(allowRandomAccess);
ContentReader reader = null;
if (file.exists())
{
FileContentReader fileContentReader = new FileContentReader(file, contentUrl);
fileContentReader.setAllowRandomAccess(allowRandomAccess);
reader = fileContentReader;
}
else
{
reader = new EmptyContentReader(contentUrl);
}
// done
if (logger.isDebugEnabled())
@@ -250,6 +308,11 @@ public class FileContentStore extends AbstractContentStore
}
return reader;
}
catch (UnsupportedContentUrlException e)
{
// This can go out directly
throw e;
}
catch (Throwable e)
{
throw new ContentIOException("Failed to get reader for URL: " + contentUrl, e);
@@ -259,10 +322,8 @@ public class FileContentStore extends AbstractContentStore
/**
* @return Returns a writer onto a location based on the date
*/
public ContentWriter getWriter(ContentContext ctx)
public ContentWriter getWriterInternal(ContentReader existingContentReader, String newContentUrl)
{
ContentReader existingContentReader = ctx.getExistingContentReader();
String newContentUrl = ctx.getContentUrl();
try
{
File file = null;
@@ -358,9 +419,17 @@ public class FileContentStore extends AbstractContentStore
/**
* 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.
*
* @throws UnsupportedOperationException if the store is read-only
*
* @see #setReadOnly(boolean)
*/
public boolean delete(String contentUrl) throws ContentIOException
public boolean delete(String contentUrl)
{
if (readOnly)
{
throw new UnsupportedOperationException("This store is currently read-only: " + this);
}
// ignore files that don't exist
File file = makeFile(contentUrl);
boolean deleted = false;
@@ -384,45 +453,31 @@ public class FileContentStore extends AbstractContentStore
}
/**
* 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. If no known prefix is
* found, or if the relative part is empty, then <tt>null</tt> is returned.
* Creates a new content URL. This must be supported by all
* stores that are compatible with Alfresco.
*
* @param contentUrl a URL of the content to check
* @return Returns the relative part of the URL. If there is no
* prefix, then the URL is assumed to be the relative part.
* @return Returns a new and unique content URL
*/
public static String getRelativePart(String contentUrl)
public static String createNewFileStoreUrl()
{
int index = 0;
if (contentUrl.startsWith(STORE_PROTOCOL))
{
index = 8;
}
else if (contentUrl.startsWith("file://"))
{
index = 7;
}
else
{
if (contentUrl.length() == 0)
{
throw new IllegalArgumentException("Invalid FileStore content URL: " + contentUrl);
}
return 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() == 0)
{
throw new IllegalArgumentException("Invalid FileStore content URL: " + contentUrl);
}
return path;
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);
int hour = calendar.get(Calendar.HOUR_OF_DAY);
int minute = calendar.get(Calendar.MINUTE);
// create the URL
StringBuilder sb = new StringBuilder(20);
sb.append(FileContentStore.STORE_PROTOCOL)
.append(ContentStore.PROTOCOL_DELIMITER)
.append(year).append('/')
.append(month).append('/')
.append(day).append('/')
.append(hour).append('/')
.append(minute).append('/')
.append(GUID.generate()).append(".bin");
String newContentUrl = sb.toString();
// done
return newContentUrl;
}
}

View File

@@ -25,11 +25,13 @@
package org.alfresco.repo.content.filestore;
import java.io.File;
import java.nio.ByteBuffer;
import org.alfresco.repo.content.AbstractContentReadWriteTest;
import org.alfresco.repo.content.AbstractWritableContentStoreTest;
import org.alfresco.repo.content.ContentContext;
import org.alfresco.repo.content.ContentExistsException;
import org.alfresco.repo.content.ContentStore;
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;
/**
@@ -39,7 +41,7 @@ import org.alfresco.util.TempFileProvider;
*
* @author Derek Hulley
*/
public class FileContentStoreTest extends AbstractContentReadWriteTest
public class FileContentStoreTest extends AbstractWritableContentStoreTest
{
private FileContentStore store;
@@ -55,47 +57,35 @@ public class FileContentStoreTest extends AbstractContentReadWriteTest
File.separatorChar +
getName());
}
@Override
protected ContentStore getStore()
{
return store;
}
public void testGetSafeContentReader() throws Exception
/**
* Checks that the store disallows concurrent writers to be issued to the same URL.
*/
@SuppressWarnings("unused")
public void testConcurrentWriteDetection() throws Exception
{
String template = "ABC {0}{1}";
String arg0 = "DEF";
String arg1 = "123";
String fakeContent = "ABC DEF123";
ByteBuffer buffer = ByteBuffer.wrap("Something".getBytes());
ContentStore store = getStore();
// 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());
ContentContext firstContentCtx = ContentStore.NEW_CONTENT_CONTEXT;
ContentWriter firstWriter = store.getWriter(firstContentCtx);
String contentUrl = firstWriter.getContentUrl();
ContentContext secondContentCtx = new ContentContext(null, contentUrl);
try
{
ContentWriter secondWriter = store.getWriter(secondContentCtx);
fail("Store must disallow more than one writer onto the same content URL: " + store);
}
catch (ContentExistsException e)
{
// expected
}
}
}

View File

@@ -33,6 +33,7 @@ import java.nio.channels.Channels;
import java.nio.channels.WritableByteChannel;
import org.alfresco.repo.content.AbstractContentWriter;
import org.alfresco.repo.content.ContentStore;
import org.alfresco.service.cmr.repository.ContentIOException;
import org.alfresco.service.cmr.repository.ContentReader;
import org.apache.commons.logging.Log;
@@ -60,10 +61,7 @@ public class FileContentWriter extends AbstractContentWriter
*/
public FileContentWriter(File file)
{
this(
file,
FileContentStore.STORE_PROTOCOL + file.getAbsolutePath(),
null);
this(file, null);
}
/**
@@ -77,7 +75,7 @@ public class FileContentWriter extends AbstractContentWriter
{
this(
file,
FileContentStore.STORE_PROTOCOL + file.getAbsolutePath(),
FileContentStore.STORE_PROTOCOL + ContentStore.PROTOCOL_DELIMITER + file.getAbsolutePath(),
existingContentReader);
}

View File

@@ -26,7 +26,7 @@ package org.alfresco.repo.content.filestore;
import java.io.File;
import org.alfresco.repo.content.AbstractContentReadWriteTest;
import org.alfresco.repo.content.AbstractWritableContentStoreTest;
import org.alfresco.repo.content.ContentStore;
import org.alfresco.util.TempFileProvider;
@@ -35,9 +35,10 @@ import org.alfresco.util.TempFileProvider;
*
* @see org.alfresco.repo.content.filestore.FileContentStore
*
* @since 2.1
* @author Derek Hulley
*/
public class NoRandomAccessFileContentStoreTest extends AbstractContentReadWriteTest
public class NoRandomAccessFileContentStoreTest extends AbstractWritableContentStoreTest
{
private FileContentStore store;

View File

@@ -0,0 +1,66 @@
/*
* Copyright (C) 2005-2007 Alfresco Software Limited.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
* As a special exception to the terms and conditions of version 2.0 of
* the GPL, you may redistribute this Program in connection with Free/Libre
* and Open Source Software ("FLOSS") applications as described in Alfresco's
* FLOSS exception. You should have recieved a copy of the text describing
* the FLOSS exception, and it is also available here:
* http://www.alfresco.com/legal/licensing"
*/
package org.alfresco.repo.content.filestore;
import java.io.File;
import org.alfresco.repo.content.AbstractReadOnlyContentStoreTest;
import org.alfresco.repo.content.AbstractWritableContentStoreTest;
import org.alfresco.repo.content.ContentStore;
import org.alfresco.util.TempFileProvider;
/**
* Tests the file-based store when in read-only mode.
*
* @see org.alfresco.repo.content.filestore.FileContentStore
*
* @since 2.1
* @author Derek Hulley
*/
public class ReadOnlyFileContentStoreTest extends AbstractReadOnlyContentStoreTest
{
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());
// disallow random access
store.setReadOnly(true);
}
@Override
protected ContentStore getStore()
{
return store;
}
}