This commit is contained in:
2019-03-23 06:29:32 -05:00
parent 7cab0a39e5
commit a546f6de73
39 changed files with 974 additions and 331 deletions

Binary file not shown.

View File

@@ -19,26 +19,27 @@
package org.isoron.uhabits
import org.isoron.uhabits.models.HabitList
import org.isoron.uhabits.models.*
import org.isoron.uhabits.utils.*
class Backend(var databaseOpener: DatabaseOpener,
var fileOpener: FileOpener,
var log: Log) {
var db: Database
var habits: HabitList
var database: Database
var habitsRepository: HabitRepository
var habits = mutableMapOf<Int, Habit>()
init {
val dbFile = fileOpener.openUserFile("uhabits.sqlite3")
db = databaseOpener.open(dbFile)
db.migrateTo(LOOP_DATABASE_VERSION, fileOpener, log)
habits = HabitList(db)
val dbFile = fileOpener.openUserFile("uhabits.db")
database = databaseOpener.open(dbFile)
database.migrateTo(LOOP_DATABASE_VERSION, fileOpener, log)
habitsRepository = HabitRepository(database)
habits = habitsRepository.findAll()
}
fun getHabitList(): List<Map<String, *>> {
return habits.getActive().map { h ->
return habits.values.sortedBy { h -> h.position }.map { h ->
mapOf("key" to h.id.toString(),
"name" to h.name,
"color" to h.color.paletteIndex)
@@ -46,11 +47,30 @@ class Backend(var databaseOpener: DatabaseOpener,
}
fun createHabit(name: String) {
val id = habitsRepository.nextId()
val habit = Habit(id = id,
name = name,
description = "",
frequency = Frequency(1, 1),
color = Color(3),
isArchived = false,
position = habits.size,
unit = "",
target = 0.0,
type = HabitType.YES_NO_HABIT)
habitsRepository.insert(habit)
habits[id] = habit
}
fun deleteHabit(id: Int) {
val habit = habits[id]!!
habitsRepository.delete(habit)
habits.remove(id)
}
fun updateHabit(id: Int, name: String) {
val habit = habits[id]!!
habit.name = name
habitsRepository.update(habit)
}
}

View File

@@ -19,4 +19,4 @@
package org.isoron.uhabits.models
class Color(val paletteIndex: Int)
data class Color(val paletteIndex: Int)

View File

@@ -27,5 +27,5 @@ data class Habit(var id: Int,
var isArchived: Boolean,
var position: Int,
var unit: String,
var target: Int,
var target: Double,
var type: HabitType)

View File

@@ -1,57 +0,0 @@
/*
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker 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 General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.models
import org.isoron.uhabits.utils.Database
import org.isoron.uhabits.utils.StepResult
class HabitList(var db: Database) {
var habits = mutableListOf<Habit>()
init {
loadHabitsFromDatabase()
}
fun getActive(): List<Habit> {
return habits.filter { h -> !h.isArchived }
}
private fun loadHabitsFromDatabase() {
val stmt = db.prepareStatement(
"select id, name, description, freq_num, freq_den, color, " +
"archived, position, unit, target_value, type " +
"from habits")
while (stmt.step() == StepResult.ROW) {
habits.add(Habit(id = stmt.getInt(0),
name = stmt.getText(1),
description = stmt.getText(2),
frequency = Frequency(stmt.getInt(3),
stmt.getInt(4)),
color = Color(stmt.getInt(5)),
isArchived = (stmt.getInt(6) != 0),
position = stmt.getInt(7),
unit = stmt.getText(8),
target = 0,
type = HabitType.YES_NO_HABIT))
}
stmt.finalize()
}
}

View File

@@ -0,0 +1,99 @@
/*
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker 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 General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.models
import org.isoron.uhabits.utils.Database
import org.isoron.uhabits.utils.PreparedStatement
import org.isoron.uhabits.utils.StepResult
import org.isoron.uhabits.utils.nextId
class HabitRepository(var db: Database) {
companion object {
const val SELECT_COLUMNS = "id, name, description, freq_num, freq_den, color, archived, position, unit, target_value, type"
const val SELECT_PLACEHOLDERS = "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?"
const val UPDATE_COLUMNS = "id=?, name=?, description=?, freq_num=?, freq_den=?, color=?, archived=?, position=?, unit=?, target_value=?, type=?"
}
private val findAllStatement = db.prepareStatement("select $SELECT_COLUMNS from habits order by position")
private val insertStatement = db.prepareStatement("insert into Habits($SELECT_COLUMNS) values ($SELECT_PLACEHOLDERS)")
private val updateStatement = db.prepareStatement("update Habits set $UPDATE_COLUMNS where id=?")
private val deleteStatement = db.prepareStatement("delete from Habits where id=?")
fun nextId(): Int {
return db.nextId("Habits")
}
fun findAll(): MutableMap<Int, Habit> {
val result = mutableMapOf<Int, Habit>()
while (findAllStatement.step() == StepResult.ROW) {
val habit = buildHabitFromStatement(findAllStatement)
result[habit.id] = habit
}
findAllStatement.reset()
return result
}
fun insert(habit: Habit) {
bindHabitToStatement(habit, insertStatement)
insertStatement.step()
insertStatement.reset()
}
fun update(habit: Habit) {
bindHabitToStatement(habit, updateStatement)
updateStatement.bindInt(11, habit.id)
updateStatement.step()
updateStatement.reset()
}
private fun buildHabitFromStatement(stmt: PreparedStatement): Habit {
return Habit(id = stmt.getInt(0),
name = stmt.getText(1),
description = stmt.getText(2),
frequency = Frequency(stmt.getInt(3), stmt.getInt(4)),
color = Color(stmt.getInt(5)),
isArchived = stmt.getInt(6) != 0,
position = stmt.getInt(7),
unit = stmt.getText(8),
target = stmt.getReal(9),
type = if (stmt.getInt(10) == 0) HabitType.YES_NO_HABIT else HabitType.NUMERICAL_HABIT)
}
private fun bindHabitToStatement(habit: Habit, statement: PreparedStatement) {
statement.bindInt(0, habit.id)
statement.bindText(1, habit.name)
statement.bindText(2, habit.description)
statement.bindInt(3, habit.frequency.numerator)
statement.bindInt(4, habit.frequency.denominator)
statement.bindInt(5, habit.color.paletteIndex)
statement.bindInt(6, if (habit.isArchived) 1 else 0)
statement.bindInt(7, habit.position)
statement.bindText(8, habit.unit)
statement.bindReal(9, habit.target)
statement.bindInt(10, habit.type.code)
}
fun delete(habit: Habit) {
deleteStatement.bindInt(0, habit.id)
deleteStatement.step()
deleteStatement.reset()
}
}

View File

@@ -19,7 +19,7 @@
package org.isoron.uhabits.models
enum class HabitType {
YES_NO_HABIT,
NUMERICAL_HABIT,
enum class HabitType(val code: Int) {
YES_NO_HABIT(0),
NUMERICAL_HABIT(1),
}

View File

@@ -29,8 +29,10 @@ interface PreparedStatement {
fun finalize()
fun getInt(index: Int): Int
fun getText(index: Int): String
fun getReal(index: Int): Double
fun bindInt(index: Int, value: Int)
fun bindText(index: Int, value: String)
fun bindReal(index: Int, value: Double)
fun reset()
}
@@ -57,25 +59,38 @@ fun Database.queryInt(sql: String): Int {
return result
}
fun Database.nextId(tableName: String): Int {
val stmt = prepareStatement("select seq from sqlite_sequence where name='$tableName'")
if(stmt.step() == StepResult.ROW) {
val result = stmt.getInt(0)
stmt.finalize()
return result + 1
} else {
return 0
}
}
fun Database.begin() = execute("begin")
fun Database.commit() = execute("commit")
fun Database.rollback() = execute("rollback")
fun Database.getVersion() = queryInt("pragma user_version")
fun Database.setVersion(v: Int) = execute("pragma user_version = $v")
fun Database.migrateTo(newVersion: Int, fileOpener: FileOpener, log: Log) {
val currentVersion = getVersion()
log.debug("Current database version: $currentVersion")
log.debug("Database", "Current database version: $currentVersion")
if (currentVersion == newVersion) return
log.debug("Upgrading to version: $newVersion")
log.debug("Database", "Upgrading to version: $newVersion")
if (currentVersion > newVersion)
throw RuntimeException("database produced by future version of the application")
begin()
for (v in (currentVersion + 1)..newVersion) {
log.debug("Running migration $v")
log.debug("Database", "Running migration $v")
val filename = sprintf("migrations/%03d.sql", v)
val migrationFile = fileOpener.openResourceFile(filename)
for (line in migrationFile.readLines()) {

View File

@@ -20,6 +20,20 @@
package org.isoron.uhabits.utils
interface Log {
fun info(msg: String)
fun debug(msg: String)
fun info(tag: String, msg: String)
fun debug(tag: String, msg: String)
}
/**
* A Log that prints to the standard output.
*/
class StandardLog : Log {
override fun info(tag: String, msg: String) {
println(sprintf("I/%-20s %s", tag, msg))
}
override fun debug(tag: String, msg: String) {
println(sprintf("D/%-20s %s", tag, msg))
}
}

View File

@@ -0,0 +1,55 @@
/*
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker 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 General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.utils
import platform.Foundation.*
class IosResourceFile(val path: String) : ResourceFile {
private val fileManager = NSFileManager.defaultManager()
override fun readLines(): List<String> {
val contents = NSString.stringWithContentsOfFile(path) as NSString
return contents.componentsSeparatedByCharactersInSet(NSCharacterSet.newlineCharacterSet()) as List<String>
}
}
class IosUserFile(val path: String) : UserFile {
override fun exists(): Boolean {
return NSFileManager.defaultManager().fileExistsAtPath(path)
}
override fun delete() {
NSFileManager.defaultManager().removeItemAtPath(path, null)
}
}
class IosFileOpener : FileOpener {
override fun openResourceFile(filename: String): ResourceFile {
val root = NSBundle.mainBundle.resourcePath!!
return IosResourceFile("$root/$filename")
}
override fun openUserFile(filename: String): UserFile {
val manager = NSFileManager.defaultManager()
val root = manager.URLForDirectory(NSDocumentDirectory,
NSUserDomainMask,
null, false, null)!!.path
return IosUserFile("$root/$filename")
}
}

View File

@@ -17,17 +17,16 @@
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.database
package org.isoron.uhabits.utils
import org.isoron.uhabits.utils.*
import java.sql.Connection
import java.sql.DriverManager
import java.sql.PreparedStatement
import java.sql.ResultSet
class JavaPreparedStatement(private var stmt : PreparedStatement) : org.isoron.uhabits.utils.PreparedStatement {
class JavaPreparedStatement(private var stmt: PreparedStatement) : org.isoron.uhabits.utils.PreparedStatement {
private var rs: ResultSet? = null
private var hasExecuted = false
override fun step(): StepResult {
@@ -40,7 +39,6 @@ class JavaPreparedStatement(private var stmt : PreparedStatement) : org.isoron.u
if (rs == null || !rs!!.next()) return StepResult.DONE
return StepResult.ROW
}
override fun finalize() {
stmt.close()
}
@@ -53,16 +51,25 @@ class JavaPreparedStatement(private var stmt : PreparedStatement) : org.isoron.u
return rs!!.getString(index + 1)
}
override fun getReal(index: Int): Double {
return rs!!.getDouble(index + 1)
}
override fun bindInt(index: Int, value: Int) {
stmt.setInt(index, value)
stmt.setInt(index + 1, value)
}
override fun bindText(index: Int, value: String) {
stmt.setString(index, value)
stmt.setString(index + 1, value)
}
override fun bindReal(index: Int, value: Double) {
stmt.setDouble(index + 1, value)
}
override fun reset() {
stmt.clearParameters()
hasExecuted = false
}
}
@@ -70,7 +77,7 @@ class JavaDatabase(private var conn: Connection,
private val log: Log) : Database {
override fun prepareStatement(sql: String): org.isoron.uhabits.utils.PreparedStatement {
log.debug("Running SQL: ${sql}")
log.debug("Database", "Preparing: $sql")
return JavaPreparedStatement(conn.prepareStatement(sql))
}
override fun close() {

View File

@@ -1,30 +0,0 @@
/*
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker 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 General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.utils
class JavaLog : Log {
override fun info(msg: String) {
println("[I] $msg")
}
override fun debug(msg: String) {
println("[D] $msg")
}
}

View File

@@ -19,11 +19,21 @@
package org.isoron.uhabits
import org.isoron.uhabits.utils.JavaFileOpener
import org.isoron.uhabits.database.JavaDatabaseOpener
import org.isoron.uhabits.utils.JavaLog
import org.isoron.uhabits.models.HabitRepository
import org.isoron.uhabits.utils.*
import org.junit.Before
open class BaseTest {
val fileOpener = JavaFileOpener()
val databaseOpener = JavaDatabaseOpener(JavaLog())
val log = StandardLog()
val databaseOpener = JavaDatabaseOpener(log)
lateinit var db: Database
@Before
open fun setUp() {
val dbFile = fileOpener.openUserFile("test.sqlite3")
if (dbFile.exists()) dbFile.delete()
db = databaseOpener.open(dbFile)
db.migrateTo(LOOP_DATABASE_VERSION, fileOpener, log)
}
}

View File

@@ -26,15 +26,6 @@ import org.junit.Test
import kotlin.test.assertEquals
class JavaDatabaseTest : BaseTest() {
private lateinit var db: Database
@Before
fun setup() {
val dbFile = fileOpener.openUserFile("test.sqlite3")
if (dbFile.exists()) dbFile.delete()
db = databaseOpener.open(dbFile)
}
@Test
fun testUsage() {
db.setVersion(0)
@@ -51,14 +42,14 @@ class JavaDatabaseTest : BaseTest() {
stmt.step()
stmt.finalize()
stmt = db.prepareStatement("insert into demo(key, value) values (?1, ?2)")
stmt.bindInt(1, 42)
stmt.bindText(2, "Hello World")
stmt = db.prepareStatement("insert into demo(key, value) values (?, ?)")
stmt.bindInt(0, 42)
stmt.bindText(1, "Hello World")
stmt.step()
stmt.finalize()
stmt = db.prepareStatement("select * from demo where key > ?1")
stmt.bindInt(1, 10)
stmt = db.prepareStatement("select * from demo where key > ?")
stmt.bindInt(0, 10)
var result = stmt.step()
assertEquals(result, StepResult.ROW)
@@ -71,12 +62,4 @@ class JavaDatabaseTest : BaseTest() {
stmt.finalize()
db.close()
}
@Test
fun testMigrateTo() {
assertEquals(0, db.getVersion())
db.migrateTo(22, fileOpener)
assertEquals(22, db.getVersion())
db.execute("select * from habits")
}
}

View File

@@ -0,0 +1,102 @@
/*
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker 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 General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.models
import junit.framework.Assert.assertEquals
import org.isoron.uhabits.BaseTest
import org.junit.Before
import org.junit.Test
class HabitRepositoryTest : BaseTest() {
lateinit var repository: HabitRepository
lateinit private var original0: Habit
lateinit private var original1: Habit
lateinit private var original2: Habit
@Before
override fun setUp() {
super.setUp()
original0 = Habit(id = 0,
name = "Wake up early",
description = "Did you wake up before 6am?",
frequency = Frequency(1, 1),
color = Color(3),
isArchived = false,
position = 0,
unit = "",
target = 0.0,
type = HabitType.YES_NO_HABIT)
original1 = Habit(id = 1,
name = "Exercise",
description = "Did you exercise for at least 20 minutes?",
frequency = Frequency(1, 2),
color = Color(4),
isArchived = false,
position = 1,
unit = "",
target = 0.0,
type = HabitType.YES_NO_HABIT)
original2 = Habit(id = 2,
name = "Learn Japanese",
description = "Did you study Japanese today?",
frequency = Frequency(1, 1),
color = Color(3),
isArchived = false,
position = 2,
unit = "",
target = 0.0,
type = HabitType.YES_NO_HABIT)
repository = HabitRepository(db)
}
@Test
fun testFindActive() {
var habits = repository.findAll()
assertEquals(0, repository.nextId())
assertEquals(0, habits.size)
repository.insert(original0)
repository.insert(original1)
repository.insert(original2)
habits = repository.findAll()
assertEquals(3, habits.size)
assertEquals(original0, habits[0])
assertEquals(original1, habits[1])
assertEquals(original2, habits[2])
assertEquals(3, repository.nextId())
original0.description = "New description"
repository.update(original0)
habits = repository.findAll()
assertEquals(original0, habits[0])
repository.delete(original0)
habits = repository.findAll()
assertEquals(2, habits.size)
assertEquals(original1, habits[1])
assertEquals(original2, habits[2])
}
}