diff --git a/opensrp-app/src/main/java/org/smartregister/AllConstants.java b/opensrp-app/src/main/java/org/smartregister/AllConstants.java index 91d037d1b..494759bc8 100644 --- a/opensrp-app/src/main/java/org/smartregister/AllConstants.java +++ b/opensrp-app/src/main/java/org/smartregister/AllConstants.java @@ -140,6 +140,7 @@ public class AllConstants { public static final String COMBINE_CHECKBOX_OPTION_VALUES = "combine_checkbox_option_values"; public static final String GPS = "gps"; + public static final String MIGRATION_FILENAME_PATTERN = "(\\d)\\.(up|down)\\.sql"; public static class Immunizations { diff --git a/opensrp-app/src/main/java/org/smartregister/SyncConfiguration.java b/opensrp-app/src/main/java/org/smartregister/SyncConfiguration.java index a85e1ed65..1e68aa55d 100644 --- a/opensrp-app/src/main/java/org/smartregister/SyncConfiguration.java +++ b/opensrp-app/src/main/java/org/smartregister/SyncConfiguration.java @@ -214,4 +214,9 @@ public List> getGlobalSettingsQueryParams() { public boolean validateUserAssignments() { return true; } + // TODO: Rename this method + public boolean isMigrationsConfigurationEnabled() { + return false; + } + } diff --git a/opensrp-app/src/main/java/org/smartregister/repository/AllSharedPreferences.java b/opensrp-app/src/main/java/org/smartregister/repository/AllSharedPreferences.java index 2b083f3bb..ff795c7a2 100644 --- a/opensrp-app/src/main/java/org/smartregister/repository/AllSharedPreferences.java +++ b/opensrp-app/src/main/java/org/smartregister/repository/AllSharedPreferences.java @@ -33,6 +33,8 @@ public class AllSharedPreferences { public static final String FORMS_VERSION = "FORMS_VERSION"; private static final String ENCRYPTED_PASSPHRASE_KEY = "ENCRYPTED_PASSPHRASE_KEY"; private static final String DB_ENCRYPTION_VERSION = "DB_ENCRYPTION_VERSION"; + private static final String DB_VERSION = "DB_VERSION"; + private static final String DB_MIGRATION_REQUIRED = "DB_MIGRATION_REQUIRED"; private SharedPreferences preferences; public AllSharedPreferences(SharedPreferences preferences) { @@ -382,8 +384,24 @@ public int getDBEncryptionVersion() { return preferences.getInt(DB_ENCRYPTION_VERSION, 0); } - public void setDBEncryptionVersion(int encryptionVersion) { - preferences.edit().putInt(DB_ENCRYPTION_VERSION, encryptionVersion).commit(); + public boolean setDBEncryptionVersion(int encryptionVersion) { + return preferences.edit().putInt(DB_ENCRYPTION_VERSION, encryptionVersion).commit(); + } + + public int getDbVersion() { + return preferences.getInt(DB_VERSION, 0); + } + + public boolean setDbVersion(int dbVersion) { + return preferences.edit().putInt(DB_VERSION, dbVersion).commit(); + } + + public boolean isDbMigrationRequired() { + return preferences.getBoolean(DB_MIGRATION_REQUIRED, false); + } + + public boolean setDbMigrationRequired(boolean isRequired) { + return preferences.edit().putBoolean(DB_MIGRATION_REQUIRED, isRequired).commit(); } } diff --git a/opensrp-app/src/main/java/org/smartregister/repository/MigrationImpl.java b/opensrp-app/src/main/java/org/smartregister/repository/MigrationImpl.java new file mode 100644 index 000000000..16af603d9 --- /dev/null +++ b/opensrp-app/src/main/java/org/smartregister/repository/MigrationImpl.java @@ -0,0 +1,43 @@ +package org.smartregister.repository; + +import org.smartregister.repository.contract.MigrationSource; + +/** + * Created by Ephraim Kigamba - nek.eam@gmail.com on 29-01-2021. + */ +public class MigrationImpl implements MigrationSource.Migration { + + private int dbVersion; + private String[] migrations; + private MigrationType migrationType; + + @Override + public int getDbVersion() { + return dbVersion; + } + + @Override + public String[] getUpMigrationQueries() { + return migrations; + } + + @Override + public void setDbVersion(int dbVersion) { + this.dbVersion = dbVersion; + } + + @Override + public void setMigrations(String[] migrations) { + this.migrations = migrations; + } + + @Override + public MigrationType getMigrationType() { + return migrationType; + } + + @Override + public void setMigrationType(MigrationType migrationType) { + this.migrationType = migrationType; + } +} diff --git a/opensrp-app/src/main/java/org/smartregister/repository/Repository.java b/opensrp-app/src/main/java/org/smartregister/repository/Repository.java index 3ad11972f..24504a4f6 100755 --- a/opensrp-app/src/main/java/org/smartregister/repository/Repository.java +++ b/opensrp-app/src/main/java/org/smartregister/repository/Repository.java @@ -1,6 +1,7 @@ package org.smartregister.repository; import android.content.Context; +import android.text.TextUtils; import androidx.annotation.VisibleForTesting; @@ -12,8 +13,12 @@ import org.apache.commons.lang3.StringUtils; import org.smartregister.AllConstants; import org.smartregister.CoreLibrary; +import org.smartregister.SyncConfiguration; import org.smartregister.commonregistry.CommonFtsObject; import org.smartregister.exception.DatabaseMigrationException; +import org.smartregister.repository.contract.MigrationSource; +import org.smartregister.repository.dao.AppFolderMigrationSource; +import org.smartregister.repository.dao.AssetMigrationSource; import org.smartregister.repository.helper.OpenSRPDatabaseErrorHandler; import org.smartregister.util.DatabaseMigrationUtils; import org.smartregister.util.Session; @@ -23,6 +28,8 @@ import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; import java.util.LinkedHashSet; import java.util.Set; @@ -143,6 +150,40 @@ public void onCreate(SQLiteDatabase database) { @Override public void onUpgrade(SQLiteDatabase sqLiteDatabase, int oldVersion, int newVersion) { + SyncConfiguration syncConfiguration = CoreLibrary.getInstance().getSyncConfiguration(); + if (syncConfiguration != null && syncConfiguration.isMigrationsConfigurationEnabled()) { + AssetMigrationSource assetMigrationSource = new AssetMigrationSource(context); + AppFolderMigrationSource appFolderMigrationSource = new AppFolderMigrationSource(context); + + HashMap> assetMigrations + = assetMigrationSource.getMigrations(oldVersion); + HashMap> appFolderMigrationSourceMigrations + = appFolderMigrationSource.getMigrations(oldVersion); + + for (int version = oldVersion + 1; version <= newVersion; version++) { + ArrayList migrations = assetMigrations.get(version); + + if (migrations == null) { + migrations = appFolderMigrationSourceMigrations.get(version); + } + + if (migrations != null) { + for (MigrationSource.Migration migration: migrations) { + if (MigrationSource.Migration.MigrationType.UP.equals(migration.getMigrationType())) { + Timber.i("Running %s migration for v%d -> %d", migration.getMigrationType().name(), version, migrations.size()); + + for (String migrationQuery: migration.getUpMigrationQueries()) { + migrationQuery = migrationQuery.trim(); + if (!TextUtils.isEmpty(migrationQuery)){ + Timber.i("Running query [%s]", migrationQuery); + sqLiteDatabase.execSQL(migrationQuery); + } + } + } + } + } + } + } } public SQLiteDatabase getReadableDatabase() { diff --git a/opensrp-app/src/main/java/org/smartregister/repository/contract/MigrationSource.java b/opensrp-app/src/main/java/org/smartregister/repository/contract/MigrationSource.java new file mode 100644 index 000000000..225735d24 --- /dev/null +++ b/opensrp-app/src/main/java/org/smartregister/repository/contract/MigrationSource.java @@ -0,0 +1,34 @@ +package org.smartregister.repository.contract; + +import java.util.ArrayList; +import java.util.HashMap; + +/** + * Created by Ephraim Kigamba - nek.eam@gmail.com on 29-01-2021. + */ +public interface MigrationSource { + + HashMap> getMigrations(); + + HashMap> getMigrations(int fromDbVersion); + + interface Migration { + + enum MigrationType { + UP, + DOWN + } + + int getDbVersion(); + + String[] getUpMigrationQueries(); + + void setDbVersion(int dbVersion); + + void setMigrations(String[] migrations); + + MigrationType getMigrationType(); + + void setMigrationType(MigrationType migrationType); + } +} diff --git a/opensrp-app/src/main/java/org/smartregister/repository/dao/AppFolderMigrationSource.java b/opensrp-app/src/main/java/org/smartregister/repository/dao/AppFolderMigrationSource.java new file mode 100644 index 000000000..3c9b9757b --- /dev/null +++ b/opensrp-app/src/main/java/org/smartregister/repository/dao/AppFolderMigrationSource.java @@ -0,0 +1,90 @@ +package org.smartregister.repository.dao; + +import android.content.Context; +import android.os.Environment; +import android.text.TextUtils; + +import org.apache.commons.io.FileUtils; +import org.smartregister.AllConstants; +import org.smartregister.repository.MigrationImpl; +import org.smartregister.repository.contract.MigrationSource; +import org.smartregister.util.Utils; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import timber.log.Timber; + +/** + * Created by Ephraim Kigamba - nek.eam@gmail.com on 29-01-2021. + */ +public class AppFolderMigrationSource implements MigrationSource { + + public Context context; + + public AppFolderMigrationSource(Context context) { + this.context = context; + } + + @Override + public HashMap> getMigrations() { + return getMigrations(0); + } + + @Override + public HashMap> getMigrations(int fromDbVersion) { + HashMap> migrationMap = new HashMap<>(); + try { + File appFolderDirectory = new File(Environment.getDataDirectory(), "/data/" + Utils.getAppId(context) + "/files/migrations"); + String[] migrationFileNames = appFolderDirectory.list(); + + String regex = AllConstants.MIGRATION_FILENAME_PATTERN; + Pattern filePattern = Pattern.compile(regex); + + if (migrationFileNames != null) { + for (String migrationFile: migrationFileNames) { + // Check if migration file name matches the Regex + Matcher fileNameMatcher = filePattern.matcher(migrationFile); + if (fileNameMatcher.matches()) { + String versionString = fileNameMatcher.group(1); + String migrationType = fileNameMatcher.group(2); + + int version = Integer.parseInt(versionString); + + if (version >= fromDbVersion) { + + File file = new File(appFolderDirectory, migrationFile); + String queries = FileUtils.readFileToString(file); + + Migration versionMigration = new MigrationImpl(); + versionMigration.setDbVersion(version); + versionMigration.setMigrations(queries.split("\\n")); + + if (!TextUtils.isEmpty(migrationType)) { + versionMigration.setMigrationType(Migration.MigrationType.UP.name().equals(migrationType) + ? Migration.MigrationType.UP : Migration.MigrationType.DOWN); + } + + ArrayList versionMigrations = migrationMap.get(version); + if (versionMigrations == null) { + versionMigrations = new ArrayList<>(); + } + + versionMigrations.add(versionMigration); + migrationMap.put(version, versionMigrations); + } + } + } + } + + } catch (IOException e) { + Timber.e(e); + } + + return migrationMap; + } +} diff --git a/opensrp-app/src/main/java/org/smartregister/repository/dao/AssetMigrationSource.java b/opensrp-app/src/main/java/org/smartregister/repository/dao/AssetMigrationSource.java new file mode 100644 index 000000000..24e7851c4 --- /dev/null +++ b/opensrp-app/src/main/java/org/smartregister/repository/dao/AssetMigrationSource.java @@ -0,0 +1,85 @@ +package org.smartregister.repository.dao; + +import android.content.Context; +import android.text.TextUtils; + +import org.smartregister.AllConstants; +import org.smartregister.repository.MigrationImpl; +import org.smartregister.repository.contract.MigrationSource; +import org.smartregister.util.AssetHandler; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import timber.log.Timber; + +/** + * Created by Ephraim Kigamba - nek.eam@gmail.com on 29-01-2021. + */ +public class AssetMigrationSource implements MigrationSource { + + public Context context; + + public AssetMigrationSource(Context context) { + this.context = context; + } + + @Override + public HashMap> getMigrations() { + return getMigrations(0); + } + + @Override + public HashMap> getMigrations(int fromDbVersion) { + HashMap> migrationMap = new HashMap<>(); + try { + String[] migrationFileNames = context.getAssets().list("config/migrations"); + + String regex = AllConstants.MIGRATION_FILENAME_PATTERN; + Pattern filePattern = Pattern.compile(regex); + + if (migrationFileNames != null) { + for (String migrationFile: migrationFileNames) { + // Check if migration file name matches the Regex + Matcher fileNameMatcher = filePattern.matcher(migrationFile); + if (fileNameMatcher.matches()) { + String versionString = fileNameMatcher.group(1); + String migrationType = fileNameMatcher.group(2).toUpperCase(); + + int version = Integer.parseInt(versionString); + + if (version >= fromDbVersion) { + + String queries = AssetHandler.readFileFromAssetsFolder("config/migrations/" + migrationFile, context); + + Migration versionMigration = new MigrationImpl(); + versionMigration.setDbVersion(version); + versionMigration.setMigrations(queries.split("\\n")); + + if (!TextUtils.isEmpty(migrationType)) { + versionMigration.setMigrationType(Migration.MigrationType.UP.name().equals(migrationType) + ? Migration.MigrationType.UP : Migration.MigrationType.DOWN); + } + + ArrayList versionMigrations = migrationMap.get(version); + if (versionMigrations == null) { + versionMigrations = new ArrayList<>(); + } + + versionMigrations.add(versionMigration); + migrationMap.put(version, versionMigrations); + } + } + } + } + + } catch (IOException e) { + Timber.e(e); + } + + return migrationMap; + } +} diff --git a/opensrp-app/src/main/java/org/smartregister/view/activity/DrishtiApplication.java b/opensrp-app/src/main/java/org/smartregister/view/activity/DrishtiApplication.java index 35c28b859..826359847 100644 --- a/opensrp-app/src/main/java/org/smartregister/view/activity/DrishtiApplication.java +++ b/opensrp-app/src/main/java/org/smartregister/view/activity/DrishtiApplication.java @@ -173,4 +173,8 @@ public Context getContext() { return context; } + public int getDbVersion() { + return getContext().allSharedPreferences().getDbVersion(); + } + } \ No newline at end of file diff --git a/opensrp-app/src/test/assets/config/migrations/1.up.sql b/opensrp-app/src/test/assets/config/migrations/1.up.sql new file mode 100644 index 000000000..b62fc80f1 --- /dev/null +++ b/opensrp-app/src/test/assets/config/migrations/1.up.sql @@ -0,0 +1 @@ +CREATE IF NOT EXISTS TABLE key_value(key VARCHAR, val VARCHAR); \ No newline at end of file diff --git a/opensrp-app/src/test/assets/config/migrations/2.up.sql b/opensrp-app/src/test/assets/config/migrations/2.up.sql new file mode 100644 index 000000000..2063eb16d --- /dev/null +++ b/opensrp-app/src/test/assets/config/migrations/2.up.sql @@ -0,0 +1 @@ +CREATE IF NOT EXISTS TABLE clients(id INTEGER, full_name VARCHAR, age INTEGER, dob INTEGER); \ No newline at end of file diff --git a/opensrp-app/src/test/assets/config/migrations/3.up.sql b/opensrp-app/src/test/assets/config/migrations/3.up.sql new file mode 100644 index 000000000..699f150e4 --- /dev/null +++ b/opensrp-app/src/test/assets/config/migrations/3.up.sql @@ -0,0 +1 @@ +CREATE IF NOT EXISTS TABLE events(event_id INTEGER, json VARCHAR, server_version INTEGER); \ No newline at end of file diff --git a/opensrp-app/src/test/java/org/smartregister/TestSyncConfiguration.java b/opensrp-app/src/test/java/org/smartregister/TestSyncConfiguration.java index d018f9a55..dd60f4a24 100644 --- a/opensrp-app/src/test/java/org/smartregister/TestSyncConfiguration.java +++ b/opensrp-app/src/test/java/org/smartregister/TestSyncConfiguration.java @@ -72,4 +72,9 @@ public String getOauthClientSecret() { public Class getAuthenticationActivity() { return BaseLoginActivity.class; } + + @Override + public boolean isMigrationsConfigurationEnabled() { + return true; + } } diff --git a/opensrp-app/src/test/java/org/smartregister/repository/RepositoryRobolectricTest.java b/opensrp-app/src/test/java/org/smartregister/repository/RepositoryRobolectricTest.java index f105757ab..be8924212 100644 --- a/opensrp-app/src/test/java/org/smartregister/repository/RepositoryRobolectricTest.java +++ b/opensrp-app/src/test/java/org/smartregister/repository/RepositoryRobolectricTest.java @@ -141,4 +141,19 @@ public void canUseThisPasswordShouldCallIsDatabaseWritableAndReturnTrue() { Mockito.doReturn(true).when(repository).isDatabaseWritable(password); Assert.assertTrue(repository.canUseThisPassword(password)); } + + @Test + public void onUpgradeShouldApplyMigrationsFromAssetsFolder() { + Repository repository = Mockito.mock(Repository.class, Mockito.CALLS_REAL_METHODS); + ReflectionHelpers.setField(repository, "context", RuntimeEnvironment.application); + SQLiteDatabase database = Mockito.mock(SQLiteDatabase.class); + + // call the method under test + repository.onUpgrade(database, 0, 3); + + // Verify the migration calls + Mockito.verify(database).execSQL(Mockito.eq("CREATE IF NOT EXISTS TABLE key_value(key VARCHAR, val VARCHAR);")); + Mockito.verify(database).execSQL(Mockito.eq("CREATE IF NOT EXISTS TABLE clients(id INTEGER, full_name VARCHAR, age INTEGER, dob INTEGER);")); + Mockito.verify(database).execSQL(Mockito.eq("CREATE IF NOT EXISTS TABLE events(event_id INTEGER, json VARCHAR, server_version INTEGER);")); + } } diff --git a/opensrp-app/src/test/java/org/smartregister/repository/dao/AssetMigrationSourceTest.java b/opensrp-app/src/test/java/org/smartregister/repository/dao/AssetMigrationSourceTest.java new file mode 100644 index 000000000..1aaa01634 --- /dev/null +++ b/opensrp-app/src/test/java/org/smartregister/repository/dao/AssetMigrationSourceTest.java @@ -0,0 +1,57 @@ +package org.smartregister.repository.dao; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.robolectric.RuntimeEnvironment; +import org.smartregister.BaseRobolectricUnitTest; +import org.smartregister.repository.contract.MigrationSource; + +import java.util.ArrayList; +import java.util.HashMap; + +import static org.junit.Assert.*; + +/** + * Created by Ephraim Kigamba - nek.eam@gmail.com on 02-02-2021. + */ +public class AssetMigrationSourceTest extends BaseRobolectricUnitTest { + + private AssetMigrationSource assetMigrationSource; + + @Before + public void setUp() throws Exception { + assetMigrationSource = new AssetMigrationSource(RuntimeEnvironment.application); + } + + @After + public void tearDown() throws Exception { + } + + @Test + public void getMigrationsShouldReturnAllMigrationsInAssetsFolder() { + HashMap> migrations = + assetMigrationSource.getMigrations(); + + assertEquals(3, migrations.size()); + assertEquals(MigrationSource.Migration.MigrationType.UP, migrations.get(1).get(0).getMigrationType()); + assertEquals(MigrationSource.Migration.MigrationType.UP, migrations.get(2).get(0).getMigrationType()); + assertEquals("CREATE IF NOT EXISTS TABLE clients(id INTEGER, full_name VARCHAR, age INTEGER, dob INTEGER);" + , migrations.get(2).get(0).getUpMigrationQueries()[0]); + assertEquals(MigrationSource.Migration.MigrationType.UP, migrations.get(3).get(0).getMigrationType()); + } + + @Test + public void getMigrationsShouldReturnMigrationsFromV2() { + + HashMap> migrations = + assetMigrationSource.getMigrations(2); + + assertEquals(2, migrations.size()); + assertEquals(MigrationSource.Migration.MigrationType.UP, migrations.get(2).get(0).getMigrationType()); + assertEquals("CREATE IF NOT EXISTS TABLE clients(id INTEGER, full_name VARCHAR, age INTEGER, dob INTEGER);" + , migrations.get(2).get(0).getUpMigrationQueries()[0]); + assertEquals(MigrationSource.Migration.MigrationType.UP, migrations.get(3).get(0).getMigrationType()); + + } +} \ No newline at end of file