REPO-1219: Allow file content URLs to be generated by a provider

- merged implementation from ACE-5093
   - marked the FileContentUrlProvider as public
   - added the default implementation bean, and injected it in the TenantRoutingFileContentStore 

git-svn-id: https://svn.alfresco.com/repos/alfresco-enterprise/alfresco/BRANCHES/DEV/5.2.N/root@132105 c4b6b30b-aa2e-2d43-bbcb-ca4b014f7261
This commit is contained in:
Alexandra Leahu
2016-11-02 15:43:31 +00:00
parent 6451543233
commit 6c41a4f779
11 changed files with 439 additions and 33 deletions

View File

@@ -60,6 +60,9 @@
</property> </property>
</bean> </bean>
<bean id="defaultFileContentUrlProvider" class="org.alfresco.repo.content.filestore.TimeBasedFileContentUrlProvider">
<property name="bucketsPerMinute" value="${dir.contentstore.bucketsPerMinute}"/>
</bean>
<!-- This content limit provider is used above (and can also be overriden, eg. by modules). --> <!-- This content limit provider is used above (and can also be overriden, eg. by modules). -->
<bean id="defaultContentLimitProvider" class="org.alfresco.repo.content.ContentLimitProvider$SimpleFixedLimitProvider"> <bean id="defaultContentLimitProvider" class="org.alfresco.repo.content.ContentLimitProvider$SimpleFixedLimitProvider">
<property name="sizeLimitString" value="${system.content.maximumFileSizeLimit}"/> <property name="sizeLimitString" value="${system.content.maximumFileSizeLimit}"/>

View File

@@ -8,6 +8,7 @@ dir.root=./alf_data
dir.contentstore=${dir.root}/contentstore dir.contentstore=${dir.root}/contentstore
dir.contentstore.deleted=${dir.root}/contentstore.deleted dir.contentstore.deleted=${dir.root}/contentstore.deleted
dir.contentstore.bucketsPerMinute=0
# ContentStore subsystem: default choice # ContentStore subsystem: default choice
filecontentstore.subsystem.name=unencryptedContentStore filecontentstore.subsystem.name=unencryptedContentStore

View File

@@ -6,6 +6,7 @@
<bean id="fileContentStore" class="org.alfresco.repo.tenant.TenantRoutingFileContentStore" parent="baseTenantRoutingContentStore"> <bean id="fileContentStore" class="org.alfresco.repo.tenant.TenantRoutingFileContentStore" parent="baseTenantRoutingContentStore">
<property name="rootLocation" value="${dir.contentstore}" /> <property name="rootLocation" value="${dir.contentstore}" />
<property name="contentLimitProvider" ref="defaultContentLimitProvider" /> <property name="contentLimitProvider" ref="defaultContentLimitProvider" />
<property name="fileContentUrlProvider" ref="defaultFileContentUrlProvider"/>
</bean> </bean>
</beans> </beans>

View File

@@ -42,13 +42,12 @@ import org.alfresco.repo.content.ContentStoreCreatedEvent;
import org.alfresco.repo.content.EmptyContentReader; import org.alfresco.repo.content.EmptyContentReader;
import org.alfresco.repo.content.UnsupportedContentUrlException; import org.alfresco.repo.content.UnsupportedContentUrlException;
import org.alfresco.service.cmr.repository.ContentIOException; import org.alfresco.service.cmr.repository.ContentIOException;
import org.alfresco.service.cmr.repository.ContentReader; import org.alfresco.service.cmr.repository.ContentReader;
import org.alfresco.service.cmr.repository.ContentWriter; import org.alfresco.service.cmr.repository.ContentWriter;
import org.alfresco.util.Deleter; import org.alfresco.util.Deleter;
import org.alfresco.util.GUID; import org.alfresco.util.Pair;
import org.alfresco.util.Pair; import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.FilenameUtils; import org.apache.commons.logging.Log;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory; import org.apache.commons.logging.LogFactory;
import org.springframework.beans.BeansException; import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContext;
@@ -91,6 +90,7 @@ public class FileContentStore
private boolean readOnly; private boolean readOnly;
private ApplicationContext applicationContext; private ApplicationContext applicationContext;
private boolean deleteEmptyDirs = true; private boolean deleteEmptyDirs = true;
private FileContentUrlProvider fileContentUrlProvider = new TimeBasedFileContentUrlProvider();
/** /**
* Private: for Spring-constructed instances only. * Private: for Spring-constructed instances only.
@@ -207,6 +207,10 @@ public class FileContentStore
{ {
this.readOnly = readOnly; this.readOnly = readOnly;
} }
public void setFileContentUrlProvider(FileContentUrlProvider fileContentUrlProvider) {
this.fileContentUrlProvider = fileContentUrlProvider;
}
/** /**
* Generates a new URL and file appropriate to it. * Generates a new URL and file appropriate to it.
@@ -216,7 +220,7 @@ public class FileContentStore
*/ */
/*package*/ File createNewFile() throws IOException /*package*/ File createNewFile() throws IOException
{ {
String contentUrl = FileContentStore.createNewFileStoreUrl(); String contentUrl = fileContentUrlProvider.createNewFileStoreUrl();
return createNewFile(contentUrl); return createNewFile(contentUrl);
} }
@@ -587,25 +591,7 @@ public class FileContentStore
*/ */
public static String createNewFileStoreUrl() public static String createNewFileStoreUrl()
{ {
Calendar calendar = new GregorianCalendar(); return TimeBasedFileContentUrlProvider.createNewFileStoreUrl(0);
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

@@ -0,0 +1,43 @@
/*
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2016 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
*
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Alfresco is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/
package org.alfresco.repo.content.filestore;
import org.alfresco.api.AlfrescoPublicApi;
/**
* Provider API for file content store URL implementations
* @author Andreea Dragoi
*/
@AlfrescoPublicApi
public interface FileContentUrlProvider
{
/**
* Content URLs must consist of a prefix or protocol followed by an implementation-specific identifier
* @return file content store URL
*/
public String createNewFileStoreUrl();
}

View File

@@ -0,0 +1,104 @@
/*
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2016 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
*
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Alfresco is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/
package org.alfresco.repo.content.filestore;
import java.util.Calendar;
import java.util.GregorianCalendar;
import org.alfresco.repo.content.ContentStore;
import org.alfresco.util.GUID;
/**
* Default Content URL provider for file stores.
* Content URL format is <b>store://year/month/day/hour/minute/GUID.bin</b>,
* but can be configured to include provision for splitting data into
* buckets within <b>minute</b> range through bucketsPerMinute property :
* <b>store://year/month/day/hour/minute/bucket/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>hour</b>: 0-based hour of the day </li>
* <li> <b>minute</b>: 0-based minute of the hour </li>
* <li> <b>bucket</b>: 0-based bucket depending second of minute </li>
* <li> <b>GUID</b>: A unique identifier </li>
* </ul>
* <p>
* @author Andreea Dragoi
*/
class TimeBasedFileContentUrlProvider implements FileContentUrlProvider
{
protected int bucketsPerMinute = 0;
public void setBucketsPerMinute(int bucketsPerMinute)
{
this.bucketsPerMinute = bucketsPerMinute;
}
@Override
public String createNewFileStoreUrl()
{
return createNewFileStoreUrl(bucketsPerMinute);
}
public static String createTimeBasedPath(int bucketsPerMinute){
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(year).append('/')
.append(month).append('/')
.append(day).append('/')
.append(hour).append('/')
.append(minute).append('/');
if (bucketsPerMinute != 0)
{
long seconds = System.currentTimeMillis() % (60 * 1000);
int actualBucket = (int) seconds / ((60 * 1000) / bucketsPerMinute);
sb.append(actualBucket).append('/');
}
//done
return sb.toString();
}
public static String createNewFileStoreUrl(int minuteBucketCount)
{
StringBuilder sb = new StringBuilder(20);
sb.append(FileContentStore.STORE_PROTOCOL);
sb.append(ContentStore.PROTOCOL_DELIMITER);
sb.append(createTimeBasedPath(minuteBucketCount));
sb.append(GUID.generate()).append(".bin");
return sb.toString();
}
}

View File

@@ -0,0 +1,78 @@
/*
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2016 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
*
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Alfresco is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/
package org.alfresco.repo.content.filestore;
import java.util.Random;
import org.alfresco.repo.content.ContentStore;
import org.alfresco.util.GUID;
/**
* Content URL provider for file stores which allows routing content from a store to a selection of filesystem volumes.
* Content is randomly distributed on configured volumes.
* Content URL format is <b>store://volume/year/month/day/hour/minute/GUID.bin</b>,
* As {@link TimeBasedFileContentUrlProvider TimeBasedFileContentUrlProvider} can be configured to include provision for
* splitting data into buckets within <b>minute</b> range
* @author Andreea Dragoi
*/
class VolumeAwareContentUrlProvider extends TimeBasedFileContentUrlProvider
{
private String[] volumes;
private Random random = new Random();
/**
* @param volumeNames(name of volumes separated by comma)
*/
public VolumeAwareContentUrlProvider(String volumeNames)
{
if (volumeNames == null || volumeNames.isEmpty())
{
throw new IllegalArgumentException("Invalid volumeNames argument");
}
this.volumes = volumeNames.split(",");
}
@Override
public String createNewFileStoreUrl()
{
StringBuilder sb = new StringBuilder(20);
sb.append(FileContentStore.STORE_PROTOCOL)
.append(ContentStore.PROTOCOL_DELIMITER)
.append(chooseVolume()).append("/")
.append(TimeBasedFileContentUrlProvider.createTimeBasedPath(bucketsPerMinute))
.append(GUID.generate()).append(".bin");
String newContentUrl = sb.toString();
return newContentUrl;
}
private String chooseVolume()
{
int volumesNum = volumes.length;
return volumes[random.nextInt(volumesNum)];
}
}

View File

@@ -33,7 +33,8 @@ import java.util.Map;
import org.alfresco.repo.content.ContentLimitProvider; import org.alfresco.repo.content.ContentLimitProvider;
import org.alfresco.repo.content.ContentLimitProvider.NoLimitProvider; import org.alfresco.repo.content.ContentLimitProvider.NoLimitProvider;
import org.alfresco.repo.content.ContentStore; import org.alfresco.repo.content.ContentStore;
import org.alfresco.repo.content.filestore.FileContentStore; import org.alfresco.repo.content.filestore.FileContentStore;
import org.alfresco.repo.content.filestore.FileContentUrlProvider;
import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContext;
/** /**
@@ -41,7 +42,8 @@ import org.springframework.context.ApplicationContext;
*/ */
public class TenantRoutingFileContentStore extends AbstractTenantRoutingContentStore public class TenantRoutingFileContentStore extends AbstractTenantRoutingContentStore
{ {
private ContentLimitProvider contentLimitProvider = new NoLimitProvider(); private ContentLimitProvider contentLimitProvider = new NoLimitProvider();
private FileContentUrlProvider fileContentUrlProvider;
/** /**
* Sets a new {@link ContentLimitProvider} which will provide a maximum filesize for content. * Sets a new {@link ContentLimitProvider} which will provide a maximum filesize for content.
@@ -49,6 +51,14 @@ public class TenantRoutingFileContentStore extends AbstractTenantRoutingContentS
public void setContentLimitProvider(ContentLimitProvider contentLimitProvider) public void setContentLimitProvider(ContentLimitProvider contentLimitProvider)
{ {
this.contentLimitProvider = contentLimitProvider; this.contentLimitProvider = contentLimitProvider;
}
/**
* Sets a new {@link FileContentUrlProvider} which will build the content url.
*/
public void setFileContentUrlProvider(FileContentUrlProvider fileContentUrlProvider)
{
this.fileContentUrlProvider = fileContentUrlProvider;
} }
protected ContentStore initContentStore(ApplicationContext ctx, String contentRoot) protected ContentStore initContentStore(ApplicationContext ctx, String contentRoot)
@@ -66,7 +76,11 @@ public class TenantRoutingFileContentStore extends AbstractTenantRoutingContentS
{ {
fileContentStore.setContentLimitProvider(contentLimitProvider); fileContentStore.setContentLimitProvider(contentLimitProvider);
} }
if(fileContentUrlProvider != null)
{
fileContentStore.setFileContentUrlProvider(fileContentUrlProvider);
}
return fileContentStore; return fileContentStore;
} }
} }

View File

@@ -0,0 +1,102 @@
/*
* Copyright (C) 2005-2010 Alfresco Software Limited./*
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2016 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
*
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Alfresco is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/
package org.alfresco.repo.content.filestore;
import java.io.File;
import org.alfresco.test_category.OwnJVMTestsCategory;
import org.junit.Before;
import org.junit.Test;
import org.junit.experimental.categories.Category;
import static org.junit.Assert.assertTrue;
/**
* Tests for {@link FileContentStore FileContentStore} which uses
* {@link TimeBasedFileContentUrlProvider TimeBasedFileContentUrlProvider}
* configured for provisioning splitting data into buckets within the
* <b>minute</b> range
*
* @author Andreea Dragoi
*
*/
@Category(OwnJVMTestsCategory.class)
public class BucketAwareFileContentStoreTest extends FileContentStoreTest
{
private static final int BUCKETS_PER_MINUTE = 20;
private static final int ITERATIONS = 5;
@Before
public void before() throws Exception
{
super.before();
TimeBasedFileContentUrlProvider fileContentUrlProvider = new TimeBasedFileContentUrlProvider();
// configure url provider to create buckets on minute range on a
// interval of 3 seconds (60/MINUTE_BUCKET_COUNT)
fileContentUrlProvider.setBucketsPerMinute(BUCKETS_PER_MINUTE);
store.setFileContentUrlProvider(fileContentUrlProvider);
}
@Test
public void testBucketCreation() throws Exception
{
// create several files in a interval of ~15 seconds
// depending when the test is started files can be created on same
// minute or not
File firstFile = store.createNewFile();
for (int i = 0; i < ITERATIONS; i++)
{
store.createNewFile();
Thread.sleep(3000);
}
File lastFile = store.createNewFile();
// check the minute for first and last file created
File firstFileMinute = firstFile.getParentFile().getParentFile();
File lastFileMinute = lastFile.getParentFile().getParentFile();
int createdBuckets;
int firstFileMinuteBuckets = firstFileMinute.list().length;
if (!firstFileMinute.equals(lastFileMinute))
{
// files are created in different minutes
int lastFileMinutesBuckets = lastFileMinute.list().length;
createdBuckets = firstFileMinuteBuckets + lastFileMinutesBuckets;
}
else
{
// files are created on same minute
createdBuckets = firstFileMinuteBuckets;
}
// Interval of 15s + time for file creation, expecting (ITERATIONS + 1)
// buckets
assertTrue("Unexpected number of buckets created", createdBuckets == ITERATIONS + 1);
}
}

View File

@@ -28,7 +28,7 @@ package org.alfresco.repo.content.filestore;
import java.io.File; import java.io.File;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.Locale; import java.util.Locale;
import org.alfresco.repo.content.AbstractWritableContentStoreTest; import org.alfresco.repo.content.AbstractWritableContentStoreTest;
import org.alfresco.repo.content.ContentContext; import org.alfresco.repo.content.ContentContext;
import org.alfresco.repo.content.ContentExistsException; import org.alfresco.repo.content.ContentExistsException;
@@ -45,7 +45,7 @@ import org.junit.After;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.junit.experimental.categories.Category; import org.junit.experimental.categories.Category;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotNull;
@@ -62,7 +62,7 @@ import static org.junit.Assert.fail;
@Category(OwnJVMTestsCategory.class) @Category(OwnJVMTestsCategory.class)
public class FileContentStoreTest extends AbstractWritableContentStoreTest public class FileContentStoreTest extends AbstractWritableContentStoreTest
{ {
private FileContentStore store; protected FileContentStore store;
@Before @Before
public void before() throws Exception public void before() throws Exception

View File

@@ -0,0 +1,74 @@
/*
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2016 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
*
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Alfresco is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/
package org.alfresco.repo.content.filestore;
import java.io.File;
import java.io.IOException;
import org.alfresco.test_category.OwnJVMTestsCategory;
import org.junit.Before;
import org.junit.Test;
import org.junit.experimental.categories.Category;
import static org.junit.Assert.assertTrue;
/**
* Tests for {@link FileContentStore} that uses {@link VolumeAwareContentUrlProvider}
* to route content from a store to a selection of filesystem volumes
* @author Andreea Dragoi
*/
@Category(OwnJVMTestsCategory.class)
public class VolumeAwareFileContentStoreTest extends FileContentStoreTest{
private static final String VOLUMES = "volumeA,volumeB,volumeC";
@Before
public void before() throws Exception
{
super.before();
VolumeAwareContentUrlProvider volumeAwareContentUrlProvider = new VolumeAwareContentUrlProvider(VOLUMES);
store.setFileContentUrlProvider(volumeAwareContentUrlProvider);
}
@Test
public void testVolumeCreation() throws IOException
{
int volumesNumber = VOLUMES.split(",").length;
// create several files
for (int i = 0; i < volumesNumber * 5 ; i++)
{
store.createNewFile();
}
File root = new File(store.getRootLocation());
String[] folders = root.list();
// check if root folders contains configured volumes
for (String file : folders)
{
assertTrue("Unknown volume", VOLUMES.contains(file));
}
assertTrue("Not all configured volumes were created", folders.length == volumesNumber);
}
}