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

@@ -45,7 +45,6 @@ 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;
@@ -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.
@@ -208,6 +208,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

@@ -34,6 +34,7 @@ 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;
/** /**
@@ -42,6 +43,7 @@ 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.
@@ -51,6 +53,14 @@ public class TenantRoutingFileContentStore extends AbstractTenantRoutingContentS
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)
{ {
Map<String, Serializable> extendedEventParams = new HashMap<String, Serializable>(); Map<String, Serializable> extendedEventParams = new HashMap<String, Serializable>();
@@ -67,6 +77,10 @@ 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

@@ -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);
}
}