From 051f1cacf5a2b45d358b05d42dab01f8c66d19a4 Mon Sep 17 00:00:00 2001
From: kaloyane <kaloyane>
Date: Mon, 24 Oct 2011 16:09:26 +0000
Subject: [PATCH] [LMS-2583] new IFreeSpaceProvider implementation respecting
 the free space within a PostgreSQL data source, IFreespaceProvider is now
 configurable in ExperimentBaseArchivingTask

SVN: 23403
---
 .../plugins/ExperimentBasedArchivingTask.java |  70 ++++++--
 .../api/v1/impl/DataSourceQueryService.java   |   4 +-
 .../generic/shared/DataSourceProvider.java    |   4 +-
 .../generic/shared/IDataSourceProvider.java   |  43 +++++
 .../dss/generic/shared/ServiceProvider.java   |   4 +-
 ...stgresPlusFileSystemFreeSpaceProvider.java | 149 +++++++++++++++
 .../ExperimentBasedArchivingTaskTest.java     |  17 +-
 .../shared/ServiceProviderTestWrapper.java    |   1 +
 ...esPlusFileSystemFreeSpaceProviderTest.java | 169 ++++++++++++++++++
 9 files changed, 440 insertions(+), 21 deletions(-)
 create mode 100644 datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/shared/IDataSourceProvider.java
 create mode 100644 datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/shared/utils/PostgresPlusFileSystemFreeSpaceProvider.java
 create mode 100644 datastore_server/sourceTest/java/ch/systemsx/cisd/openbis/dss/generic/shared/utils/PostgresPlusFileSystemFreeSpaceProviderTest.java

diff --git a/datastore_server/source/java/ch/systemsx/cisd/etlserver/plugins/ExperimentBasedArchivingTask.java b/datastore_server/source/java/ch/systemsx/cisd/etlserver/plugins/ExperimentBasedArchivingTask.java
index 37082eeb250..6921db5a03f 100644
--- a/datastore_server/source/java/ch/systemsx/cisd/etlserver/plugins/ExperimentBasedArchivingTask.java
+++ b/datastore_server/source/java/ch/systemsx/cisd/etlserver/plugins/ExperimentBasedArchivingTask.java
@@ -40,6 +40,8 @@ import ch.systemsx.cisd.common.filesystem.SimpleFreeSpaceProvider;
 import ch.systemsx.cisd.common.logging.LogCategory;
 import ch.systemsx.cisd.common.logging.LogFactory;
 import ch.systemsx.cisd.common.maintenance.IDataStoreLockingMaintenanceTask;
+import ch.systemsx.cisd.common.utilities.ClassUtils;
+import ch.systemsx.cisd.common.utilities.ExtendedProperties;
 import ch.systemsx.cisd.common.utilities.PropertyParametersUtil;
 import ch.systemsx.cisd.common.utilities.PropertyUtils;
 import ch.systemsx.cisd.openbis.dss.generic.shared.IEncapsulatedOpenBISService;
@@ -77,12 +79,14 @@ public class ExperimentBasedArchivingTask implements IDataStoreLockingMaintenanc
 
     static final String MONITORED_DIR = "monitored-dir";
 
+    static final String FREE_SPACE_PROVIDER_PREFIX = "free-space-provider.";
+
     private static final EnumSet<DataSetArchivingStatus> ARCHIVE_STATES = EnumSet.of(
             DataSetArchivingStatus.ARCHIVE_PENDING, DataSetArchivingStatus.ARCHIVED);
 
     private final IEncapsulatedOpenBISService service;
 
-    private final IFreeSpaceProvider freeSpaceProvider;
+    private IFreeSpaceProvider freeSpaceProvider;
 
     private File storeRoot;
 
@@ -98,15 +102,12 @@ public class ExperimentBasedArchivingTask implements IDataStoreLockingMaintenanc
 
     public ExperimentBasedArchivingTask()
     {
-        this(ServiceProvider.getOpenBISService(), new SimpleFreeSpaceProvider(), ServiceProvider
-                .getShareIdManager());
+        this(ServiceProvider.getOpenBISService(), ServiceProvider.getShareIdManager());
     }
 
-    ExperimentBasedArchivingTask(IEncapsulatedOpenBISService service,
-            IFreeSpaceProvider freeSpaceProvider, IShareIdManager shareIdManager)
+    ExperimentBasedArchivingTask(IEncapsulatedOpenBISService service, IShareIdManager shareIdManager)
     {
         this.service = service;
-        this.freeSpaceProvider = freeSpaceProvider;
         this.shareIdManager = shareIdManager;
     }
 
@@ -116,15 +117,33 @@ public class ExperimentBasedArchivingTask implements IDataStoreLockingMaintenanc
     }
 
     public void setUp(String pluginName, Properties properties)
+    {
+        storeRoot = setUpStoreRoot(properties);
+        freeSpaceProvider = setUpFreeSpaceProvider(properties);
+        setUpMonitoredShareOrPath(properties);
+        minimumFreeSpace =
+                FileUtils.ONE_MB * PropertyUtils.getLong(properties, MINIMUM_FREE_SPACE_KEY, 1024);
+        excludedDataSetTypes =
+                new HashSet<String>(Arrays.asList(PropertyParametersUtil.parseItemisedProperty(
+                        properties.getProperty(EXCLUDED_DATA_SET_TYPES_KEY, ""),
+                        EXCLUDED_DATA_SET_TYPES_KEY)));
+    }
+
+    private File setUpStoreRoot(Properties properties)
     {
         String storeRootFileName =
                 PropertyUtils.getMandatoryProperty(properties, STOREROOT_DIR_KEY);
-        storeRoot = new File(storeRootFileName);
-        if (storeRoot.isDirectory() == false)
+        File resultStoreRoot = new File(storeRootFileName);
+        if (resultStoreRoot.isDirectory() == false)
         {
             throw new ConfigurationFailureException(
                     "Store root doesn't exists or isn't a directory: " + storeRoot);
         }
+        return resultStoreRoot;
+    }
+
+    private void setUpMonitoredShareOrPath(Properties properties)
+    {
         final String monitoredDirPath = PropertyUtils.getProperty(properties, MONITORED_DIR);
         if (monitoredDirPath == null)
         {
@@ -147,12 +166,35 @@ public class ExperimentBasedArchivingTask implements IDataStoreLockingMaintenanc
                         + "' doesn't exists or isn't a directory.");
             }
         }
-        minimumFreeSpace =
-                FileUtils.ONE_MB * PropertyUtils.getLong(properties, MINIMUM_FREE_SPACE_KEY, 1024);
-        excludedDataSetTypes =
-                new HashSet<String>(Arrays.asList(PropertyParametersUtil.parseItemisedProperty(
-                        properties.getProperty(EXCLUDED_DATA_SET_TYPES_KEY, ""),
-                        EXCLUDED_DATA_SET_TYPES_KEY)));
+    }
+
+    private IFreeSpaceProvider setUpFreeSpaceProvider(Properties properties)
+    {
+        Properties providerProps =  ExtendedProperties.getSubset(properties, FREE_SPACE_PROVIDER_PREFIX, true);
+        String freeSpaceProviderClassName =
+                PropertyUtils.getProperty(providerProps, "class",
+                        SimpleFreeSpaceProvider.class.getName());
+        
+        Class<?> clazz = null;
+        try
+        {
+            clazz = Class.forName(freeSpaceProviderClassName);
+        } catch (ClassNotFoundException cnfe)
+        {
+            throw ConfigurationFailureException.fromTemplate(
+                    "Cannot find configured free space provider class '%s'",
+                    freeSpaceProviderClassName);
+        }
+        
+        if (ClassUtils.hasConstructor(clazz, properties))
+        {
+            return ClassUtils.create(IFreeSpaceProvider.class, clazz, properties);
+
+        } else
+        {
+            return ClassUtils.create(IFreeSpaceProvider.class, clazz);
+        }
+        
     }
 
     public void execute()
diff --git a/datastore_server/source/java/ch/systemsx/cisd/etlserver/registrator/api/v1/impl/DataSourceQueryService.java b/datastore_server/source/java/ch/systemsx/cisd/etlserver/registrator/api/v1/impl/DataSourceQueryService.java
index 2941f639fa4..5939d362365 100644
--- a/datastore_server/source/java/ch/systemsx/cisd/etlserver/registrator/api/v1/impl/DataSourceQueryService.java
+++ b/datastore_server/source/java/ch/systemsx/cisd/etlserver/registrator/api/v1/impl/DataSourceQueryService.java
@@ -28,7 +28,7 @@ import org.apache.log4j.Logger;
 
 import ch.systemsx.cisd.common.logging.LogCategory;
 import ch.systemsx.cisd.common.logging.LogFactory;
-import ch.systemsx.cisd.openbis.dss.generic.shared.DataSourceProvider;
+import ch.systemsx.cisd.openbis.dss.generic.shared.IDataSourceProvider;
 import ch.systemsx.cisd.openbis.dss.generic.shared.ServiceProvider;
 import ch.systemsx.cisd.openbis.dss.generic.shared.api.internal.v1.IDataSourceQueryService;
 
@@ -40,7 +40,7 @@ public class DataSourceQueryService implements IDataSourceQueryService
     private static final Logger operationLog = LogFactory.getLogger(LogCategory.OPERATION,
             DataSourceQueryService.class);
 
-    private DataSourceProvider getDataSourceProvider()
+    private IDataSourceProvider getDataSourceProvider()
     {
         return ServiceProvider.getDataSourceProvider();
     }
diff --git a/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/shared/DataSourceProvider.java b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/shared/DataSourceProvider.java
index a16dddf9c7d..6a433b9b451 100644
--- a/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/shared/DataSourceProvider.java
+++ b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/shared/DataSourceProvider.java
@@ -29,8 +29,8 @@ import ch.systemsx.cisd.common.logging.LogCategory;
 import ch.systemsx.cisd.common.logging.LogFactory;
 import ch.systemsx.cisd.common.utilities.ClassUtils;
 import ch.systemsx.cisd.common.utilities.PropertyParametersUtil;
-import ch.systemsx.cisd.common.utilities.PropertyUtils;
 import ch.systemsx.cisd.common.utilities.PropertyParametersUtil.SectionProperties;
+import ch.systemsx.cisd.common.utilities.PropertyUtils;
 import ch.systemsx.cisd.openbis.dss.generic.shared.utils.DssPropertyParametersUtil;
 
 /**
@@ -68,7 +68,7 @@ import ch.systemsx.cisd.openbis.dss.generic.shared.utils.DssPropertyParametersUt
  * 
  * @author Izabela Adamczyk
  */
-public class DataSourceProvider
+public class DataSourceProvider implements IDataSourceProvider
 {
     static final String DATA_SOURCES_KEY = "data-sources";
 
diff --git a/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/shared/IDataSourceProvider.java b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/shared/IDataSourceProvider.java
new file mode 100644
index 00000000000..b0cf222503d
--- /dev/null
+++ b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/shared/IDataSourceProvider.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2010 ETH Zuerich, CISD
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * 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 ch.systemsx.cisd.openbis.dss.generic.shared;
+
+import java.util.Properties;
+
+import javax.sql.DataSource;
+
+/**
+ * A provider for data sources.
+ * 
+ * @author Kaloyan Enimanev
+ */
+public interface IDataSourceProvider
+{
+
+    /**
+     * Returns data source configured with given name or throws {@link IllegalArgumentException} if
+     * not configured.
+     */
+    public DataSource getDataSource(String name);
+
+    /**
+     * Extracts the data source name from the specified properties and returns the requested data
+     * source by calling {@link #getDataSource(String)}.
+     */
+    public DataSource getDataSource(Properties properties);
+
+}
diff --git a/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/shared/ServiceProvider.java b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/shared/ServiceProvider.java
index 5956f81078d..c7ef22cbe10 100644
--- a/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/shared/ServiceProvider.java
+++ b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/shared/ServiceProvider.java
@@ -150,9 +150,9 @@ public class ServiceProvider
                 "data-store-rpc-service-generic"));
     }
 
-    public static DataSourceProvider getDataSourceProvider()
+    public static IDataSourceProvider getDataSourceProvider()
     {
-        return ((DataSourceProvider) getApplicationContext().getBean("data-source-provider"));
+        return ((IDataSourceProvider) getApplicationContext().getBean("data-source-provider"));
     }
 
     public static IConfigProvider getConfigProvider()
diff --git a/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/shared/utils/PostgresPlusFileSystemFreeSpaceProvider.java b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/shared/utils/PostgresPlusFileSystemFreeSpaceProvider.java
new file mode 100644
index 00000000000..9345a681ee5
--- /dev/null
+++ b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/shared/utils/PostgresPlusFileSystemFreeSpaceProvider.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright 2011 ETH Zuerich, CISD
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * 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 ch.systemsx.cisd.openbis.dss.generic.shared.utils;
+
+import java.io.IOException;
+import java.sql.Connection;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.Properties;
+
+import javax.sql.DataSource;
+
+import org.apache.log4j.Logger;
+
+import ch.systemsx.cisd.base.exceptions.CheckedExceptionTunnel;
+import ch.systemsx.cisd.common.filesystem.HostAwareFile;
+import ch.systemsx.cisd.common.filesystem.IFreeSpaceProvider;
+import ch.systemsx.cisd.common.filesystem.SimpleFreeSpaceProvider;
+import ch.systemsx.cisd.common.logging.LogCategory;
+import ch.systemsx.cisd.common.logging.LogFactory;
+import ch.systemsx.cisd.common.utilities.PropertyUtils;
+import ch.systemsx.cisd.openbis.dss.generic.shared.ServiceProvider;
+
+/**
+ * PostgreSQL database files are designed to grow until they take up the entire available disk
+ * space. It is therefore impossible to estimate what part of a hard disk is "free" by just asking
+ * the file system.
+ * <p>
+ * The {@link PostgresPlusFileSystemFreeSpaceProvider} estimates the free space on a drive as the
+ * sum of the free disk space and the free space of a PostgreSQL database as returned by its
+ * "pgstattuple" extension.
+ * 
+ * <pre>
+ * IMPORTANT: The class requires that the extension 'pgstattuple' is installed on the target
+ * PostgreSQL database. For PostgreSQL 9.1 this can be done by executing :
+ *     
+ *     psql -d DB_NAME -c "CREATE EXTENSION pgstattuple;"
+ * 
+ * @author Kaloyan Enimanev
+ */
+public class PostgresPlusFileSystemFreeSpaceProvider implements IFreeSpaceProvider
+{
+    
+    static final String EXECUTE_VACUUM_KEY = "execute-vacuum";
+
+    static final String DATA_SOURCE_KEY = "monitored-data-source";
+
+    private static final String VACUUM_QUERY = "VACUUM;";
+
+    private static final String CREATE_TMP_FREE_SPACE_TABLE =
+            "CREATE TEMPORARY TABLE freespace ON COMMIT DROP AS "
+                    + "   (SELECT relname, (SELECT free_space FROM pgstattuple(relid)) "
+                    + "       AS free_space FROM pg_catalog.pg_statio_user_tables);";
+
+    private static final String SELECT_FREE_SPACE_QUERY = CREATE_TMP_FREE_SPACE_TABLE
+            + " SELECT sum(free_space) FROM freespace;";
+
+    private static final Logger operationLog = LogFactory.getLogger(LogCategory.OPERATION,
+            PostgresPlusFileSystemFreeSpaceProvider.class);
+
+    private final boolean executeVacuum;
+
+    private final DataSource dataSource;
+
+    private final IFreeSpaceProvider fileSystemFreeSpaceProvider = new SimpleFreeSpaceProvider();
+
+    public PostgresPlusFileSystemFreeSpaceProvider(Properties properties)
+    {
+        executeVacuum = PropertyUtils.getBoolean(properties, EXECUTE_VACUUM_KEY, false);
+        
+        String dataSourceName = PropertyUtils.getMandatoryProperty(properties, DATA_SOURCE_KEY);
+        dataSource = ServiceProvider.getDataSourceProvider().getDataSource(dataSourceName);
+    }
+
+    public long freeSpaceKb(HostAwareFile path) throws IOException
+    {
+        long dataSourceFreeSpace = calculateDataSourceFreeSpace();
+        long fsFreeSpace = fileSystemFreeSpaceProvider.freeSpaceKb(path);
+        return dataSourceFreeSpace + fsFreeSpace;
+    }
+
+    private long calculateDataSourceFreeSpace()
+    {
+        Connection connection = null;
+        try
+        {
+            connection = createConnection();
+            if (executeVacuum)
+            {
+                executeVacuumQuery(connection);
+            }
+
+            return calculateFreeSpace(connection);
+
+        } catch (SQLException sqlEx)
+        {
+            throw CheckedExceptionTunnel.wrapIfNecessary(sqlEx);
+        } finally
+        {
+            closeConnection(connection);
+        }
+    }
+
+    private void executeVacuumQuery(Connection connection) throws SQLException
+    {
+        connection.createStatement().execute(VACUUM_QUERY);
+    }
+
+    private long calculateFreeSpace(Connection connection) throws SQLException
+    {
+        ResultSet result = connection.createStatement().executeQuery(SELECT_FREE_SPACE_QUERY);
+        return result.getLong(1);
+    }
+
+    private Connection createConnection() throws SQLException
+    {
+        return dataSource.getConnection();
+    }
+
+    private void closeConnection(Connection connection)
+    {
+        if (connection != null)
+        {
+            try
+            {
+                connection.close();
+            } catch (SQLException ex)
+            {
+                // suppress this exception
+                operationLog.error(ex);
+            }
+        }
+    }
+
+}
diff --git a/datastore_server/sourceTest/java/ch/systemsx/cisd/etlserver/plugins/ExperimentBasedArchivingTaskTest.java b/datastore_server/sourceTest/java/ch/systemsx/cisd/etlserver/plugins/ExperimentBasedArchivingTaskTest.java
index ce2017a5ab5..66ab3308ed7 100644
--- a/datastore_server/sourceTest/java/ch/systemsx/cisd/etlserver/plugins/ExperimentBasedArchivingTaskTest.java
+++ b/datastore_server/sourceTest/java/ch/systemsx/cisd/etlserver/plugins/ExperimentBasedArchivingTaskTest.java
@@ -54,6 +54,17 @@ import ch.systemsx.cisd.openbis.generic.shared.dto.identifier.ProjectIdentifier;
  */
 public class ExperimentBasedArchivingTaskTest extends AbstractFileSystemTestCase
 {
+    public static class MockFreeSpaceProvider implements IFreeSpaceProvider
+    {
+
+        static IFreeSpaceProvider mock;
+
+        public long freeSpaceKb(HostAwareFile path) throws IOException
+        {
+            return mock.freeSpaceKb(path);
+        }
+    }
+
     private static final String LOG_ENTRY_PREFIX_TEMPLATE =
             "INFO  %s.ExperimentBasedArchivingTask - ";
 
@@ -121,7 +132,9 @@ public class ExperimentBasedArchivingTaskTest extends AbstractFileSystemTestCase
         service = context.mock(IEncapsulatedOpenBISService.class);
         freeSpaceProvider = context.mock(IFreeSpaceProvider.class);
         shareIdManager = context.mock(IShareIdManager.class);
-        task = new ExperimentBasedArchivingTask(service, freeSpaceProvider, shareIdManager);
+        MockFreeSpaceProvider.mock = freeSpaceProvider;
+
+        task = new ExperimentBasedArchivingTask(service, shareIdManager);
         assertEquals(true, task.requiresDataStoreLock());
 
         e1 = new ExperimentBuilder().id(41).identifier("/S/P/E1").getExperiment();
@@ -149,6 +162,8 @@ public class ExperimentBasedArchivingTaskTest extends AbstractFileSystemTestCase
         properties.setProperty(ExperimentBasedArchivingTask.MONITORED_SHARE_KEY, SHARE_ID);
         properties.setProperty(ExperimentBasedArchivingTask.MINIMUM_FREE_SPACE_KEY, "100");
         properties.setProperty(ExperimentBasedArchivingTask.EXCLUDED_DATA_SET_TYPES_KEY, "ABC, B");
+        properties.setProperty(ExperimentBasedArchivingTask.FREE_SPACE_PROVIDER_PREFIX + "class",
+                MockFreeSpaceProvider.class.getName());
     }
 
     private DataSetBuilder dataSet(String code)
diff --git a/datastore_server/sourceTest/java/ch/systemsx/cisd/openbis/dss/generic/shared/ServiceProviderTestWrapper.java b/datastore_server/sourceTest/java/ch/systemsx/cisd/openbis/dss/generic/shared/ServiceProviderTestWrapper.java
index 3312e094d68..b734cbfe0dc 100644
--- a/datastore_server/sourceTest/java/ch/systemsx/cisd/openbis/dss/generic/shared/ServiceProviderTestWrapper.java
+++ b/datastore_server/sourceTest/java/ch/systemsx/cisd/openbis/dss/generic/shared/ServiceProviderTestWrapper.java
@@ -43,6 +43,7 @@ public class ServiceProviderTestWrapper
         classNameToBeanName.put(IEncapsulatedOpenBISService.class, "openBIS-service");
         classNameToBeanName.put(IShareIdManager.class, "share-id-manager");
         classNameToBeanName.put(IConfigProvider.class, "config-provider");
+        classNameToBeanName.put(IDataSourceProvider.class, "data-source-provider");
         classNameToBeanName
                 .put(IHierarchicalContentProvider.class, "hierarchical-content-provider");
     }
diff --git a/datastore_server/sourceTest/java/ch/systemsx/cisd/openbis/dss/generic/shared/utils/PostgresPlusFileSystemFreeSpaceProviderTest.java b/datastore_server/sourceTest/java/ch/systemsx/cisd/openbis/dss/generic/shared/utils/PostgresPlusFileSystemFreeSpaceProviderTest.java
new file mode 100644
index 00000000000..77c76990b1b
--- /dev/null
+++ b/datastore_server/sourceTest/java/ch/systemsx/cisd/openbis/dss/generic/shared/utils/PostgresPlusFileSystemFreeSpaceProviderTest.java
@@ -0,0 +1,169 @@
+/*
+ * Copyright 2011 ETH Zuerich, CISD
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * 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 ch.systemsx.cisd.openbis.dss.generic.shared.utils;
+
+import static ch.systemsx.cisd.openbis.dss.generic.shared.utils.PostgresPlusFileSystemFreeSpaceProvider.DATA_SOURCE_KEY;
+
+import java.io.File;
+import java.sql.Connection;
+import java.sql.ResultSet;
+import java.sql.Statement;
+import java.util.Properties;
+
+import javax.sql.DataSource;
+
+import org.apache.commons.io.FileSystemUtils;
+import org.jmock.Expectations;
+import org.jmock.Mockery;
+import org.springframework.beans.factory.BeanFactory;
+import org.testng.AssertJUnit;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import ch.systemsx.cisd.common.filesystem.HostAwareFile;
+import ch.systemsx.cisd.openbis.dss.generic.shared.IDataSourceProvider;
+import ch.systemsx.cisd.openbis.dss.generic.shared.ServiceProviderTestWrapper;
+
+/**
+ * @author Kaloyan Enimanev
+ */
+public class PostgresPlusFileSystemFreeSpaceProviderTest extends AssertJUnit
+{
+
+    private final static String DATA_SOURCE = "data-source";
+
+    private PostgresPlusFileSystemFreeSpaceProvider provider;
+
+    private Mockery context;
+    private Connection connection;
+
+    private BeanFactory mockApplicationContext;
+
+    @BeforeMethod
+    public void setUp() throws Exception
+    {
+        context = new Mockery();
+        mockApplicationContext = context.mock(BeanFactory.class);
+        connection = context.mock(Connection.class);
+
+        ServiceProviderTestWrapper.setApplicationContext(mockApplicationContext);
+        
+        final IDataSourceProvider dsProvider =
+                ServiceProviderTestWrapper.mock(context, IDataSourceProvider.class);
+        final DataSource dataSource = context.mock(DataSource.class);
+
+        context.checking(new Expectations()
+            {
+                {
+                    allowing(dsProvider).getDataSource(DATA_SOURCE);
+                    will(returnValue(dataSource));
+
+                    one(dataSource).getConnection();
+                    will(returnValue(connection));
+
+                    one(connection).close();
+                }
+            });
+    }
+
+    @AfterMethod
+    public void tearDown()
+    {
+        ServiceProviderTestWrapper.restoreApplicationContext();
+    }
+
+    private Properties createProperties(boolean executeVacuum) {
+        Properties props = new Properties();
+        props.put(DATA_SOURCE_KEY, DATA_SOURCE);
+        if (executeVacuum)
+        {
+            props.put(PostgresPlusFileSystemFreeSpaceProvider.EXECUTE_VACUUM_KEY, "true");
+        }
+        return props;
+    }
+
+    @Test
+    public void testNoVacuum() throws Exception
+    {
+        Properties props = createProperties(false);
+        provider = new PostgresPlusFileSystemFreeSpaceProvider(props);
+
+        final long postgresFreeSpace = 1000L;
+        prepareFreeSpaceExpectations(postgresFreeSpace);
+        
+        File workDir = new File(".");
+        HostAwareFile file = new HostAwareFile(workDir);
+        long fsFreeSpace = FileSystemUtils.freeSpaceKb(workDir.getAbsolutePath());
+        long totalFreeSpace = provider.freeSpaceKb(file);
+        
+        assertEquals(fsFreeSpace + postgresFreeSpace, totalFreeSpace);
+    }
+
+    @Test
+    public void testWithVacuum() throws Exception
+    {
+        Properties props = createProperties(true);
+        provider = new PostgresPlusFileSystemFreeSpaceProvider(props);
+
+        final long postgresFreeSpace = 1000L;
+        prepareVacuumExpectations();
+        prepareFreeSpaceExpectations(postgresFreeSpace);
+
+        File workDir = new File(".");
+        HostAwareFile file = new HostAwareFile(workDir);
+        long fsFreeSpace = FileSystemUtils.freeSpaceKb(workDir.getAbsolutePath());
+        long totalFreeSpace = provider.freeSpaceKb(file);
+
+        assertEquals(fsFreeSpace + postgresFreeSpace, totalFreeSpace);
+    }
+
+    private void prepareVacuumExpectations() throws Exception
+    {
+        context.checking(new Expectations()
+            {
+                {
+                    Statement statement = context.mock(Statement.class, "vacuumStatement");
+                    one(connection).createStatement();
+                    will(returnValue(statement));
+
+                    one(statement).execute("VACUUM;");
+                }
+            });
+    }
+
+    private void prepareFreeSpaceExpectations(final long freeSpace) throws Exception
+    {
+        context.checking(new Expectations()
+            {
+                {
+                    Statement statement = context.mock(Statement.class, "freeSpaceStatement");
+                    one(connection).createStatement();
+                    will(returnValue(statement));
+
+                    one(statement).executeQuery(with(any(String.class)));
+
+                    ResultSet rs = context.mock(ResultSet.class);
+                    will(returnValue(rs));
+
+                    one(rs).getLong(1);
+                    will(returnValue(freeSpace));
+                }
+            });
+    }
+
+}
-- 
GitLab