diff --git a/config/alfresco/repository.properties b/config/alfresco/repository.properties
index 0101978710..e4965525a4 100644
--- a/config/alfresco/repository.properties
+++ b/config/alfresco/repository.properties
@@ -8,6 +8,7 @@ dir.root=./alf_data
dir.contentstore=${dir.root}/contentstore
dir.contentstore.deleted=${dir.root}/contentstore.deleted
+dir.contentstore.bucketsPerMinute=0
# ContentStore subsystem: default choice
filecontentstore.subsystem.name=unencryptedContentStore
diff --git a/config/alfresco/subsystems/ContentStore/unencrypted/unencrypted-store-context.xml b/config/alfresco/subsystems/ContentStore/unencrypted/unencrypted-store-context.xml
index c3a029528c..2d81e13d5a 100644
--- a/config/alfresco/subsystems/ContentStore/unencrypted/unencrypted-store-context.xml
+++ b/config/alfresco/subsystems/ContentStore/unencrypted/unencrypted-store-context.xml
@@ -6,6 +6,7 @@
+
diff --git a/source/java/org/alfresco/repo/content/filestore/FileContentStore.java b/source/java/org/alfresco/repo/content/filestore/FileContentStore.java
index f55fd46fc7..e5b8dc311e 100644
--- a/source/java/org/alfresco/repo/content/filestore/FileContentStore.java
+++ b/source/java/org/alfresco/repo/content/filestore/FileContentStore.java
@@ -42,13 +42,12 @@ import org.alfresco.repo.content.ContentStoreCreatedEvent;
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.Deleter;
-import org.alfresco.util.GUID;
-import org.alfresco.util.Pair;
-import org.apache.commons.io.FilenameUtils;
-import org.apache.commons.logging.Log;
+import org.alfresco.service.cmr.repository.ContentReader;
+import org.alfresco.service.cmr.repository.ContentWriter;
+import org.alfresco.util.Deleter;
+import org.alfresco.util.Pair;
+import org.apache.commons.io.FilenameUtils;
+import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
@@ -91,6 +90,7 @@ public class FileContentStore
private boolean readOnly;
private ApplicationContext applicationContext;
private boolean deleteEmptyDirs = true;
+ private FileContentUrlProvider fileContentUrlProvider = new TimeBasedFileContentUrlProvider();
/**
* Private: for Spring-constructed instances only.
@@ -207,6 +207,10 @@ public class FileContentStore
{
this.readOnly = readOnly;
}
+
+ public void setFileContentUrlProvider(FileContentUrlProvider fileContentUrlProvider) {
+ this.fileContentUrlProvider = fileContentUrlProvider;
+ }
/**
* Generates a new URL and file appropriate to it.
@@ -216,7 +220,7 @@ public class FileContentStore
*/
/*package*/ File createNewFile() throws IOException
{
- String contentUrl = FileContentStore.createNewFileStoreUrl();
+ String contentUrl = fileContentUrlProvider.createNewFileStoreUrl();
return createNewFile(contentUrl);
}
@@ -587,25 +591,7 @@ public class FileContentStore
*/
public static String createNewFileStoreUrl()
{
- 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;
+ return TimeBasedFileContentUrlProvider.createNewFileStoreUrl(0);
}
/**
diff --git a/source/java/org/alfresco/repo/content/filestore/FileContentUrlProvider.java b/source/java/org/alfresco/repo/content/filestore/FileContentUrlProvider.java
new file mode 100644
index 0000000000..06d41b484e
--- /dev/null
+++ b/source/java/org/alfresco/repo/content/filestore/FileContentUrlProvider.java
@@ -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 .
+ * #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();
+}
diff --git a/source/java/org/alfresco/repo/content/filestore/TimeBasedFileContentUrlProvider.java b/source/java/org/alfresco/repo/content/filestore/TimeBasedFileContentUrlProvider.java
new file mode 100644
index 0000000000..d5af2a3630
--- /dev/null
+++ b/source/java/org/alfresco/repo/content/filestore/TimeBasedFileContentUrlProvider.java
@@ -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 .
+ * #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 store://year/month/day/hour/minute/GUID.bin,
+ * but can be configured to include provision for splitting data into
+ * buckets within minute range through bucketsPerMinute property :
+ * store://year/month/day/hour/minute/bucket/GUID.bin
+ *
+ * - store://: prefix identifying an Alfresco content stores
+ * regardless of the persistence mechanism.
+ * - year: year
+ * - month: 1-based month of the year
+ * - day: 1-based day of the month
+ * - hour: 0-based hour of the day
+ * - minute: 0-based minute of the hour
+ * - bucket: 0-based bucket depending second of minute
+ * - GUID: A unique identifier
+ *
+ *
+ * @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();
+ }
+}
diff --git a/source/java/org/alfresco/repo/content/filestore/VolumeAwareContentUrlProvider.java b/source/java/org/alfresco/repo/content/filestore/VolumeAwareContentUrlProvider.java
new file mode 100644
index 0000000000..d06c7743bb
--- /dev/null
+++ b/source/java/org/alfresco/repo/content/filestore/VolumeAwareContentUrlProvider.java
@@ -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 .
+ * #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 store://volume/year/month/day/hour/minute/GUID.bin,
+ * As {@link TimeBasedFileContentUrlProvider TimeBasedFileContentUrlProvider} can be configured to include provision for
+ * splitting data into buckets within minute 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)];
+ }
+
+}
diff --git a/source/java/org/alfresco/repo/tenant/TenantRoutingFileContentStore.java b/source/java/org/alfresco/repo/tenant/TenantRoutingFileContentStore.java
index 8729dfd856..843ba71a16 100644
--- a/source/java/org/alfresco/repo/tenant/TenantRoutingFileContentStore.java
+++ b/source/java/org/alfresco/repo/tenant/TenantRoutingFileContentStore.java
@@ -33,7 +33,8 @@ import java.util.Map;
import org.alfresco.repo.content.ContentLimitProvider;
import org.alfresco.repo.content.ContentLimitProvider.NoLimitProvider;
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;
/**
@@ -41,7 +42,8 @@ import org.springframework.context.ApplicationContext;
*/
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.
@@ -49,6 +51,14 @@ public class TenantRoutingFileContentStore extends AbstractTenantRoutingContentS
public void setContentLimitProvider(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)
@@ -66,7 +76,11 @@ public class TenantRoutingFileContentStore extends AbstractTenantRoutingContentS
{
fileContentStore.setContentLimitProvider(contentLimitProvider);
}
-
+
+ if(fileContentUrlProvider != null)
+ {
+ fileContentStore.setFileContentUrlProvider(fileContentUrlProvider);
+ }
return fileContentStore;
}
}
diff --git a/source/test-java/org/alfresco/repo/content/filestore/BucketAwareFileContentStoreTest.java b/source/test-java/org/alfresco/repo/content/filestore/BucketAwareFileContentStoreTest.java
new file mode 100644
index 0000000000..18a3360333
--- /dev/null
+++ b/source/test-java/org/alfresco/repo/content/filestore/BucketAwareFileContentStoreTest.java
@@ -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 .
+ * #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
+ * minute 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);
+ }
+
+}
diff --git a/source/test-java/org/alfresco/repo/content/filestore/FileContentStoreTest.java b/source/test-java/org/alfresco/repo/content/filestore/FileContentStoreTest.java
index 4e743a9fb0..2e63f557f3 100644
--- a/source/test-java/org/alfresco/repo/content/filestore/FileContentStoreTest.java
+++ b/source/test-java/org/alfresco/repo/content/filestore/FileContentStoreTest.java
@@ -28,7 +28,7 @@ package org.alfresco.repo.content.filestore;
import java.io.File;
import java.nio.ByteBuffer;
import java.util.Locale;
-
+
import org.alfresco.repo.content.AbstractWritableContentStoreTest;
import org.alfresco.repo.content.ContentContext;
import org.alfresco.repo.content.ContentExistsException;
@@ -45,7 +45,7 @@ import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.experimental.categories.Category;
-
+
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
@@ -62,7 +62,7 @@ import static org.junit.Assert.fail;
@Category(OwnJVMTestsCategory.class)
public class FileContentStoreTest extends AbstractWritableContentStoreTest
{
- private FileContentStore store;
+ protected FileContentStore store;
@Before
public void before() throws Exception
diff --git a/source/test-java/org/alfresco/repo/content/filestore/VolumeAwareFileContentStoreTest.java b/source/test-java/org/alfresco/repo/content/filestore/VolumeAwareFileContentStoreTest.java
new file mode 100644
index 0000000000..c3677e6a38
--- /dev/null
+++ b/source/test-java/org/alfresco/repo/content/filestore/VolumeAwareFileContentStoreTest.java
@@ -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 .
+ * #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);
+ }
+}