Convert core.database classes to Kotlin

pull/699/head
Alinson S. Xavier 5 years ago
parent feb3c98459
commit a02c7bdc44

@ -26,41 +26,41 @@ import java.io.*
class AndroidDatabase(
private val db: SQLiteDatabase,
private val file: File?,
override val file: File?,
) : Database {
override fun beginTransaction() = db.beginTransaction()
override fun setTransactionSuccessful() = db.setTransactionSuccessful()
override fun endTransaction() = db.endTransaction()
override fun close() = db.close()
override fun getVersion() = db.version
override fun getFile(): File? {
return file
}
override val version: Int
get() = db.version
override fun query(query: String, vararg params: String)
= AndroidCursor(db.rawQuery(query, params))
override fun query(q: String, vararg params: String) = AndroidCursor(db.rawQuery(q, params))
override fun execute(query: String, vararg params: Any)
= db.execSQL(query, params)
override fun execute(query: String, vararg params: Any) = db.execSQL(query, params)
override fun update(tableName: String,
map: Map<String, Any?>,
where: String,
vararg params: String): Int {
val values = mapToContentValues(map)
return db.update(tableName, values, where, params)
override fun update(
tableName: String,
values: Map<String, Any?>,
where: String,
vararg params: String,
): Int {
val contValues = mapToContentValues(values)
return db.update(tableName, contValues, where, params)
}
override fun insert(tableName: String, map: Map<String, Any?>): Long? {
val values = mapToContentValues(map)
return db.insert(tableName, null, values)
override fun insert(tableName: String, values: Map<String, Any?>): Long? {
val contValues = mapToContentValues(values)
return db.insert(tableName, null, contValues)
}
override fun delete(tableName: String,
where: String,
vararg params: String) {
override fun delete(
tableName: String,
where: String,
vararg params: String,
) {
db.delete(tableName, where, params)
}
@ -73,8 +73,7 @@ class AndroidDatabase(
is Long -> values.put(key, value)
is Double -> values.put(key, value)
is String -> values.put(key, value)
else -> throw IllegalStateException(
"unsupported type: " + value)
else -> throw IllegalStateException("unsupported type: $value")
}
}
return values

@ -16,13 +16,7 @@
* 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.core.database
package org.isoron.uhabits.core.database;
import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)
public @interface Column
{
String name() default "";
}
@Retention(AnnotationRetention.RUNTIME)
annotation class Column(val name: String = "")

@ -16,15 +16,13 @@
* 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.core.database
package org.isoron.uhabits.core.database;
import java.io.*
import androidx.annotation.*;
interface Cursor : Closeable {
public interface Cursor extends AutoCloseable
{
@Override
void close();
override fun close()
/**
* Moves the cursor forward one row from its current position. Returns
@ -32,37 +30,33 @@ public interface Cursor extends AutoCloseable
* past the last row. The cursor start at position -1, so this method must
* be called first.
*/
boolean moveToNext();
fun moveToNext(): Boolean
/**
* Retrieves the value of the designated column in the current row of this
* Cursor as an Integer. If the value is null, returns null. The first
* column has index zero.
*/
@Nullable
Integer getInt(int index);
fun getInt(index: Int): Int?
/**
* Retrieves the value of the designated column in the current row of this
* Cursor as a Long. If the value is null, returns null. The first
* column has index zero.
*/
@Nullable
Long getLong(int index);
fun getLong(index: Int): Long?
/**
* Retrieves the value of the designated column in the current row of this
* Cursor as a Double. If the value is null, returns null. The first
* column has index zero.
*/
@Nullable
Double getDouble(int index);
fun getDouble(index: Int): Double?
/**
* Retrieves the value of the designated column in the current row of this
* Cursor as a String. If the value is null, returns null. The first
* column has index zero.
*/
@Nullable
String getString(int index);
}
fun getString(index: Int): String?
}

@ -1,64 +0,0 @@
/*
* Copyright (C) 2017 Á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.core.database;
import java.io.*;
import java.util.*;
public interface Database
{
Cursor query(String query, String... params);
default void query(String query, ProcessCallback callback)
{
try (Cursor c = query(query)) {
c.moveToNext();
callback.process(c);
}
}
int update(String tableName,
Map<String, Object> values,
String where,
String... params);
Long insert(String tableName, Map<String, Object> values);
void delete(String tableName, String where, String... params);
void execute(String query, Object... params);
void beginTransaction();
void setTransactionSuccessful();
void endTransaction();
void close();
int getVersion();
File getFile();
interface ProcessCallback
{
void process(Cursor cursor);
}
}

@ -0,0 +1,62 @@
/*
* Copyright (C) 2017 Á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.core.database
import java.io.*
interface Database {
fun query(q: String, vararg params: String): Cursor
fun query(q: String, callback: ProcessCallback) {
query(q).use { c ->
c.moveToNext()
callback.process(c)
}
}
fun update(
tableName: String,
values: Map<String, Any?>,
where: String,
vararg params: String,
): Int
fun insert(tableName: String, values: Map<String, Any?>): Long?
fun delete(tableName: String, where: String, vararg params: String)
fun execute(query: String, vararg params: Any)
fun beginTransaction()
fun setTransactionSuccessful()
fun endTransaction()
fun close()
val version: Int
val file: File?
interface ProcessCallback {
fun process(cursor: Cursor)
}
}

@ -16,14 +16,10 @@
* 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.core.database
package org.isoron.uhabits.core.database;
import java.io.*
import androidx.annotation.*;
import java.io.*;
public interface DatabaseOpener
{
Database open(@NonNull File file);
}
interface DatabaseOpener {
fun open(file: File): Database?
}

@ -1,120 +0,0 @@
/*
* Copyright (C) 2017 Á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.core.database;
import java.sql.*;
public class JdbcCursor implements Cursor
{
private ResultSet resultSet;
public JdbcCursor(ResultSet resultSet)
{
this.resultSet = resultSet;
}
@Override
public void close()
{
try
{
resultSet.close();
}
catch (SQLException e)
{
throw new RuntimeException(e);
}
}
@Override
public boolean moveToNext()
{
try
{
return resultSet.next();
}
catch (SQLException e)
{
throw new RuntimeException(e);
}
}
@Override
public Integer getInt(int index)
{
try
{
Integer value = resultSet.getInt(index + 1);
if(resultSet.wasNull()) return null;
else return value;
}
catch (SQLException e)
{
throw new RuntimeException(e);
}
}
@Override
public Long getLong(int index)
{
try
{
Long value = resultSet.getLong(index + 1);
if(resultSet.wasNull()) return null;
else return value;
}
catch (SQLException e)
{
throw new RuntimeException(e);
}
}
@Override
public Double getDouble(int index)
{
try
{
Double value = resultSet.getDouble(index + 1);
if(resultSet.wasNull()) return null;
else return value;
}
catch (SQLException e)
{
throw new RuntimeException(e);
}
}
@Override
public String getString(int index)
{
try
{
String value = resultSet.getString(index + 1);
if(resultSet.wasNull()) return null;
else return value;
}
catch (SQLException e)
{
throw new RuntimeException(e);
}
}
}

@ -0,0 +1,77 @@
/*
* Copyright (C) 2017 Á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.core.database
import java.sql.*
class JdbcCursor(private val resultSet: ResultSet) : Cursor {
override fun close() {
try {
resultSet.close()
} catch (e: SQLException) {
throw RuntimeException(e)
}
}
override fun moveToNext(): Boolean {
return try {
resultSet.next()
} catch (e: SQLException) {
throw RuntimeException(e)
}
}
override fun getInt(index: Int): Int? {
return try {
val value = resultSet.getInt(index + 1)
if (resultSet.wasNull()) null else value
} catch (e: SQLException) {
throw RuntimeException(e)
}
}
override fun getLong(index: Int): Long? {
return try {
val value = resultSet.getLong(index + 1)
if (resultSet.wasNull()) null else value
} catch (e: SQLException) {
throw RuntimeException(e)
}
}
override fun getDouble(index: Int): Double? {
return try {
val value = resultSet.getDouble(index + 1)
if (resultSet.wasNull()) null else value
} catch (e: SQLException) {
throw RuntimeException(e)
}
}
override fun getString(index: Int): String? {
return try {
val value = resultSet.getString(index + 1)
if (resultSet.wasNull()) null else value
} catch (e: SQLException) {
throw RuntimeException(e)
}
}
}

@ -1,213 +0,0 @@
/*
* Copyright (C) 2017 Á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.core.database;
import org.apache.commons.lang3.*;
import java.io.*;
import java.sql.*;
import java.util.*;
public class JdbcDatabase implements Database
{
private Connection connection;
private boolean transactionSuccessful;
public JdbcDatabase(Connection connection)
{
this.connection = connection;
}
@Override
public Cursor query(String query, String... params)
{
try
{
PreparedStatement st = buildStatement(query, params);
return new JdbcCursor(st.executeQuery());
}
catch (SQLException e)
{
throw new RuntimeException(e);
}
}
@Override
public int update(String tableName,
Map<String, Object> map,
String where,
String... params)
{
try
{
ArrayList<String> fields = new ArrayList<>();
ArrayList<String> values = new ArrayList<>();
for (Map.Entry<String, Object> entry : map.entrySet())
{
if (entry.getValue() == null) continue;
fields.add(entry.getKey() + "=?");
values.add(entry.getValue().toString());
}
values.addAll(Arrays.asList(params));
String query = String.format("update %s set %s where %s", tableName,
StringUtils.join(fields, ", "), where);
PreparedStatement st = buildStatement(query, values.toArray());
return st.executeUpdate();
}
catch (SQLException e)
{
throw new RuntimeException(e);
}
}
@Override
public Long insert(String tableName, Map<String, Object> map)
{
try
{
ArrayList<String> fields = new ArrayList<>();
ArrayList<Object> params = new ArrayList<>();
ArrayList<String> questionMarks = new ArrayList<>();
for (Map.Entry<String, Object> entry : map.entrySet())
{
if (entry.getValue() == null) continue;
fields.add(entry.getKey());
params.add(entry.getValue());
questionMarks.add("?");
}
String query =
String.format("insert into %s(%s) values(%s)", tableName,
StringUtils.join(fields, ", "),
StringUtils.join(questionMarks, ", "));
PreparedStatement st = buildStatement(query, params.toArray());
st.execute();
Long id = null;
ResultSet keys = st.getGeneratedKeys();
if (keys.next()) id = keys.getLong(1);
return id;
}
catch (SQLException e)
{
throw new RuntimeException(e);
}
}
@Override
public void delete(String tableName, String where, String... params)
{
String query =
String.format("delete from %s where %s", tableName, where);
execute(query, (Object[]) params);
}
@Override
public void execute(String query, Object... params)
{
try
{
buildStatement(query, params).execute();
}
catch (SQLException e)
{
throw new RuntimeException(e);
}
}
private PreparedStatement buildStatement(String query, Object[] params)
throws SQLException
{
PreparedStatement st = connection.prepareStatement(query);
int index = 1;
for (Object param : params) st.setString(index++, param.toString());
return st;
}
@Override
public synchronized void beginTransaction()
{
try
{
connection.setAutoCommit(false);
transactionSuccessful = false;
}
catch (SQLException e)
{
throw new RuntimeException(e);
}
}
@Override
public synchronized void setTransactionSuccessful()
{
transactionSuccessful = true;
}
@Override
public synchronized void endTransaction()
{
try
{
if (transactionSuccessful) connection.commit();
else connection.rollback();
connection.setAutoCommit(true);
}
catch (SQLException e)
{
throw new RuntimeException(e);
}
}
@Override
public void close()
{
try
{
connection.close();
}
catch (SQLException e)
{
throw new RuntimeException(e);
}
}
@Override
public int getVersion()
{
try (Cursor c = query("PRAGMA user_version"))
{
c.moveToNext();
return c.getInt(0);
}
}
@Override
public File getFile()
{
return null;
}
}

@ -0,0 +1,157 @@
/*
* Copyright (C) 2017 Á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.core.database
import org.apache.commons.lang3.*
import java.io.*
import java.lang.IllegalArgumentException
import java.sql.*
import java.util.*
class JdbcDatabase(private val connection: Connection) : Database {
private var transactionSuccessful = false
override fun query(q: String, vararg params: String): Cursor {
return try {
val st = buildStatement(q, params)
JdbcCursor(st.executeQuery())
} catch (e: SQLException) {
throw RuntimeException(e)
}
}
override fun update(
tableName: String,
values: Map<String, Any?>,
where: String,
vararg params: String,
): Int {
return try {
val fields = ArrayList<String?>()
val valuesStr = ArrayList<String>()
for ((key, value) in values) {
fields.add("$key=?")
valuesStr.add(value.toString())
}
valuesStr.addAll(listOf(*params))
val query = String.format("update %s set %s where %s", tableName,
StringUtils.join(fields, ", "), where)
val st = buildStatement(query, valuesStr.toTypedArray())
st.executeUpdate()
} catch (e: SQLException) {
throw RuntimeException(e)
}
}
override fun insert(tableName: String, values: Map<String, Any?>): Long? {
return try {
val fields = ArrayList<String?>()
val params = ArrayList<Any?>()
val questionMarks = ArrayList<String?>()
for ((key, value) in values) {
fields.add(key)
params.add(value)
questionMarks.add("?")
}
val query = String.format("insert into %s(%s) values(%s)", tableName,
StringUtils.join(fields, ", "),
StringUtils.join(questionMarks, ", "))
val st = buildStatement(query, params.toTypedArray())
st.execute()
var id: Long? = null
val keys = st.generatedKeys
if (keys.next()) id = keys.getLong(1)
id
} catch (e: SQLException) {
throw RuntimeException(e)
}
}
override fun delete(tableName: String, where: String, vararg params: String) {
val query = String.format("delete from %s where %s", tableName, where)
execute(query, *params)
}
override fun execute(query: String, vararg params: Any) {
try {
buildStatement(query, params).execute()
} catch (e: SQLException) {
throw RuntimeException(e)
}
}
private fun buildStatement(query: String, params: Array<out Any?>): PreparedStatement {
val st = connection.prepareStatement(query)
var index = 1
for (param in params) {
when (param) {
null -> st.setNull(index++, Types.INTEGER)
is Int -> st.setInt(index++, param)
is Double -> st.setDouble(index++, param)
is String -> st.setString(index++, param)
is Long -> st.setLong(index++, param)
else -> throw IllegalArgumentException()
}
}
return st
}
@Synchronized
override fun beginTransaction() {
try {
connection.autoCommit = false
transactionSuccessful = false
} catch (e: SQLException) {
throw RuntimeException(e)
}
}
@Synchronized
override fun setTransactionSuccessful() {
transactionSuccessful = true
}
@Synchronized
override fun endTransaction() {
try {
if (transactionSuccessful) connection.commit() else connection.rollback()
connection.autoCommit = true
} catch (e: SQLException) {
throw RuntimeException(e)
}
}
override fun close() {
try {
connection.close()
} catch (e: SQLException) {
throw RuntimeException(e)
}
}
override val version: Int
get() {
query("PRAGMA user_version").use { c ->
c.moveToNext()
return c.getInt(0)!!
}
}
override val file: File?
get() = null
}

@ -1,71 +0,0 @@
/*
* Copyright (C) 2017 Á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.core.database;
import androidx.annotation.*;
import java.io.*;
import java.util.*;
import java.util.logging.*;
public class MigrationHelper
{
private static final Logger LOGGER =
Logger.getLogger(MigrationHelper.class.getName());
private final Database db;
public MigrationHelper(@NonNull Database db)
{
this.db = db;
}
public void migrateTo(int newVersion)
{
try
{
for (int v = db.getVersion() + 1; v <= newVersion; v++)
{
String fname = String.format(Locale.US, "/migrations/%02d.sql", v);
for (String command : SQLParser.parse(open(fname)))
db.execute(command);
}
}
catch (Exception e)
{
throw new RuntimeException(e);
}
}
@NonNull
private InputStream open(String fname) throws IOException
{
InputStream resource = getClass().getResourceAsStream(fname);
if(resource != null) return resource;
// Workaround for bug in Android Studio / IntelliJ. Removing this
// causes unit tests to fail when run from within the IDE, although
// everything works fine from the command line.
File file = new File("uhabits-core/src/main/resources/" + fname);
if(file.exists()) return new FileInputStream(file);
throw new RuntimeException("resource not found: " + fname);
}
}

@ -0,0 +1,49 @@
/*
* Copyright (C) 2017 Á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.core.database
import java.io.*
import java.util.*
class MigrationHelper(
private val db: Database,
) {
fun migrateTo(newVersion: Int) {
try {
for (v in db.version + 1..newVersion) {
val fname = String.format(Locale.US, "/migrations/%02d.sql", v)
for (command in SQLParser.parse(open(fname))) db.execute(command)
}
} catch (e: Exception) {
throw RuntimeException(e)
}
}
private fun open(fname: String): InputStream {
val resource = javaClass.getResourceAsStream(fname)
if (resource != null) return resource
// Workaround for bug in Android Studio / IntelliJ. Removing this
// causes unit tests to fail when run from within the IDE, although
// everything works fine from the command line.
val file = File("uhabits-core/src/main/resources/$fname")
if (file.exists()) return FileInputStream(file)
throw RuntimeException("resource not found: $fname")
}
}

@ -1,355 +0,0 @@
/*
* Copyright (C) 2017 Á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.core.database;
import androidx.annotation.*;
import org.apache.commons.lang3.*;
import org.apache.commons.lang3.tuple.*;
import java.lang.annotation.*;
import java.lang.reflect.*;
import java.util.*;
public class Repository<T>
{
@NonNull
private final Class klass;
@NonNull
private final Database db;
public Repository(@NonNull Class<T> klass, @NonNull Database db)
{
this.klass = klass;
this.db = db;
}
/**
* Returns the record that has the id provided.
* If no record is found, returns null.
*/
@Nullable
public T find(@NonNull Long id)
{
return findFirst(String.format("where %s=?", getIdName()),
id.toString());
}
/**
* Returns all records matching the given SQL query.
* <p>
* The query should only contain the "where" part of the SQL query, and
* optionally the "order by" part. "Group by" is not allowed. If no matching
* records are found, returns an empty list.
*/
@NonNull
public List<T> findAll(@NonNull String query, @NonNull String... params)
{
try (Cursor c = db.query(buildSelectQuery() + query, params))
{
return cursorToMultipleRecords(c);
}
}
/**
* Returns the first record matching the given SQL query.
* See findAll for more details about the parameters.
*/
@Nullable
public T findFirst(String query, String... params)
{
try (Cursor c = db.query(buildSelectQuery() + query, params))
{
if (!c.moveToNext()) return null;
return cursorToSingleRecord(c);
}
}
/**
* Executes the given SQL query on the repository.
* <p>
* The query can be of any kind. For example, complex deletes and updates
* are allowed. The repository does not perform any checks to guarantee
* that the query is valid, however the underlying database might.
*/
public void execSQL(String query, Object... params)
{
db.execute(query, params);
}
/**
* Executes the given callback inside a database transaction.
* <p>
* If the callback terminates without throwing any exceptions, the
* transaction is considered successful. If any exceptions are thrown,
* the transaction is aborted. Nesting transactions is not allowed.
*/
public void executeAsTransaction(Runnable callback)
{
db.beginTransaction();
try
{
callback.run();
db.setTransactionSuccessful();
}
catch (Exception e)
{
throw new RuntimeException(e);
}
finally
{
db.endTransaction();
}
}
/**
* Saves the record on the database.
* <p>
* If the id of the given record is null, it is assumed that the record has
* not been inserted in the repository yet. The record will be inserted, a
* new id will be automatically generated, and the id of the given record
* will be updated.
* <p>
* If the given record has a non-null id, then an update will be performed
* instead. That is, the previous record will be overwritten by the one
* provided.
*/
public void save(T record)
{
try
{
Field fields[] = getFields();
String columns[] = getColumnNames();
Map<String, Object> values = new HashMap<>();
for (int i = 0; i < fields.length; i++)
values.put(columns[i], fields[i].get(record));
Long id = (Long) getIdField().get(record);
int affectedRows = 0;
if (id != null) affectedRows =
db.update(getTableName(), values, getIdName() + "=?",
id.toString());
if (id == null || affectedRows == 0)
{
id = db.insert(getTableName(), values);
getIdField().set(record, id);
}
}
catch (Exception e)
{
throw new RuntimeException(e);
}
}
/**
* Removes the given record from the repository.
* The id of the given record is also set to null.
*/
public void remove(T record)
{
try
{
Long id = (Long) getIdField().get(record);
if (id == null) return;
db.delete(getTableName(), getIdName() + "=?", id.toString());
getIdField().set(record, null);
}
catch (Exception e)
{
throw new RuntimeException(e);
}
}
@NonNull
private List<T> cursorToMultipleRecords(Cursor c)
{
List<T> records = new LinkedList<>();
while (c.moveToNext()) records.add(cursorToSingleRecord(c));
return records;
}
@NonNull
private T cursorToSingleRecord(Cursor cursor)
{
try
{
Constructor constructor = klass.getDeclaredConstructors()[0];
constructor.setAccessible(true);
T record = (T) constructor.newInstance();
int index = 0;
for (Field field : getFields())
copyFieldFromCursor(record, field, cursor, index++);
return record;
}
catch (Exception e)
{
throw new RuntimeException(e);
}
}
private void copyFieldFromCursor(T record, Field field, Cursor c, int index)
throws IllegalAccessException
{
if (field.getType().isAssignableFrom(Integer.class))
field.set(record, c.getInt(index));
else if (field.getType().isAssignableFrom(Long.class))
field.set(record, c.getLong(index));
else if (field.getType().isAssignableFrom(Double.class))
field.set(record, c.getDouble(index));
else if (field.getType().isAssignableFrom(String.class))
field.set(record, c.getString(index));
else throw new RuntimeException(
"Type not supported: " + field.getType().getName() + " " +
field.getName());
}
private String buildSelectQuery()
{
return String.format("select %s from %s ",
StringUtils.join(getColumnNames(), ", "), getTableName());
}
private List<Pair<Field, Column>> getFieldColumnPairs()
{
List<Pair<Field, Column>> fields = new ArrayList<>();
for (Field field : klass.getDeclaredFields())
for (Annotation annotation : field.getAnnotations())
{
if (!(annotation instanceof Column)) continue;
Column column = (Column) annotation;
fields.add(new ImmutablePair<>(field, column));
}
return fields;
}
private Field[] cacheFields = null;
@NonNull
private Field[] getFields()
{
if (cacheFields == null)
{
List<Field> fields = new ArrayList<>();
List<Pair<Field, Column>> columns = getFieldColumnPairs();
for (Pair<Field, Column> pair : columns) fields.add(pair.getLeft());
cacheFields = fields.toArray(new Field[]{});
}
return cacheFields;
}
private String[] cacheColumnNames = null;
@NonNull
private String[] getColumnNames()
{
if (cacheColumnNames == null)
{
List<String> names = new ArrayList<>();
List<Pair<Field, Column>> columns = getFieldColumnPairs();
for (Pair<Field, Column> pair : columns)
{
String cname = pair.getRight().name();
if (cname.isEmpty()) cname = pair.getLeft().getName();
if (names.contains(cname))
throw new RuntimeException("duplicated column : " + cname);
names.add(cname);
}
cacheColumnNames = names.toArray(new String[]{});
}
return cacheColumnNames;
}
private String cacheTableName = null;
@NonNull
private String getTableName()
{
if (cacheTableName == null)
{
String name = getTableAnnotation().name();
if (name.isEmpty()) throw new RuntimeException("Table name is empty");
cacheTableName = name;
}
return cacheTableName;
}
private String cacheIdName = null;
@NonNull
private String getIdName()
{
if (cacheIdName == null)
{
String id = getTableAnnotation().id();
if (id.isEmpty()) throw new RuntimeException("Table id is empty");
cacheIdName = id;
}
return cacheIdName;
}
private Field cacheIdField = null;
@NonNull
private Field getIdField()
{
if (cacheIdField == null)
{
Field fields[] = getFields();
String idName = getIdName();
for (Field f : fields)
if (f.getName().equals(idName))
{
cacheIdField = f;
break;
}
if (cacheIdField == null)
throw new RuntimeException("Field not found: " + idName);
}
return cacheIdField;
}
@NonNull
private Table getTableAnnotation()
{
Table t = null;
for (Annotation annotation : klass.getAnnotations())
{
if (!(annotation instanceof Table)) continue;
t = (Table) annotation;
break;
}
if (t == null) throw new RuntimeException("Table annotation not found");
return t;
}
}

@ -0,0 +1,252 @@
/*
* Copyright (C) 2017 Á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.core.database
import org.apache.commons.lang3.*
import org.apache.commons.lang3.tuple.*
import java.lang.reflect.*
import java.util.*
class Repository<T>(
private val klass: Class<T>,
private val db: Database,
) {
/**
* Returns the record that has the id provided. If no record is found, returns null.
*/
fun find(id: Long): T? {
return findFirst(String.format("where %s=?", getIdName()), id.toString())
}
/**
* Returns all records matching the given SQL query.
*
* The query should only contain the "where" part of the SQL query, and optionally the "order
* by" part. "Group by" is not allowed. If no matching records are found, returns an empty list.
*/
fun findAll(query: String, vararg params: String): List<T> {
db.query(buildSelectQuery() + query, *params).use { c -> return cursorToMultipleRecords(c) }
}
/**
* Returns the first record matching the given SQL query. See findAll for more details about
* the parameters.
*/
fun findFirst(query: String, vararg params: String): T? {
db.query(buildSelectQuery() + query, *params).use { c ->
return if (!c.moveToNext()) null else cursorToSingleRecord(c)
}
}
/**
* Executes the given SQL query on the repository.
*
* The query can be of any kind. For example, complex deletes and updates are allowed. The
* repository does not perform any checks to guarantee that the query is valid, however the
* underlying database might.
*/
fun execSQL(query: String, vararg params: Any) {
db.execute(query, *params)
}
/**
* Executes the given callback inside a database transaction.
*
* If the callback terminates without throwing any exceptions, the transaction is considered
* successful. If any exceptions are thrown, the transaction is aborted. Nesting transactions
* is not allowed.
*/
fun executeAsTransaction(callback: Runnable) {
db.beginTransaction()
try {
callback.run()
db.setTransactionSuccessful()
} catch (e: Exception) {
throw RuntimeException(e)
} finally {
db.endTransaction()
}
}
/**
* Saves the record on the database.
*
* If the id of the given record is null, it is assumed that the record has not been inserted
* in the repository yet. The record will be inserted, a new id will be automatically generated,
* and the id of the given record will be updated.
*
* If the given record has a non-null id, then an update will be performed instead. That is,
* the previous record will be overwritten by the one provided.
*/
fun save(record: T) {
try {
val fields = getFields()
val columns = getColumnNames()
val values: MutableMap<String, Any?> = HashMap()
for (i in fields.indices) values[columns[i]] = fields[i][record]
var id = getIdField()[record] as Long?
var affectedRows = 0
if (id != null) {
affectedRows = db.update(getTableName(), values, "${getIdName()}=?", id.toString())
}
if (id == null || affectedRows == 0) {
id = db.insert(getTableName(), values)
getIdField()[record] = id
}
} catch (e: Exception) {
throw RuntimeException(e)
}
}
/**
* Removes the given record from the repository. The id of the given record is also set to null.
*/
fun remove(record: T) {
try {
val id = getIdField()[record] as Long?
db.delete(getTableName(), "${getIdName()}=?", id.toString())
getIdField()[record] = null
} catch (e: Exception) {
throw RuntimeException(e)
}
}
private fun cursorToMultipleRecords(c: Cursor): List<T> {
val records: MutableList<T> = LinkedList()
while (c.moveToNext()) records.add(cursorToSingleRecord(c))
return records
}
@Suppress("UNCHECKED_CAST")
private fun cursorToSingleRecord(cursor: Cursor): T {
return try {
val constructor = klass.declaredConstructors[0]
constructor.isAccessible = true
val record = constructor.newInstance() as T
var index = 0
for (field in getFields()) copyFieldFromCursor(record, field, cursor, index++)
record
} catch (e: Exception) {
throw RuntimeException(e)
}
}
private fun copyFieldFromCursor(record: T, field: Field, c: Cursor, index: Int) {
when {
field.type.isAssignableFrom(java.lang.Integer::class.java) -> field[record] = c.getInt(index)
field.type.isAssignableFrom(java.lang.Long::class.java) -> field[record] = c.getLong(index)
field.type.isAssignableFrom(java.lang.Double::class.java) -> field[record] = c.getDouble(index)
field.type.isAssignableFrom(java.lang.String::class.java) -> field[record] = c.getString(index)
else -> throw RuntimeException("Type not supported: ${field.type.name} ${field.name}")
}
}
private fun buildSelectQuery(): String {
return String.format("select %s from %s ", StringUtils.join(getColumnNames(), ", "), getTableName())
}
private val fieldColumnPairs: List<Pair<Field, Column>>
get() {
val fields: MutableList<Pair<Field, Column>> = ArrayList()
for (f in klass.declaredFields) {
for (annotation in f.annotations) {
if (annotation !is Column) continue
fields.add(ImmutablePair(f, annotation))
}
}
return fields
}
private var cacheFields: Array<Field>? = null
private fun getFields(): Array<Field> {
if (cacheFields == null) {
val fields: MutableList<Field> = ArrayList()
val columns = fieldColumnPairs
for (pair in columns) fields.add(pair.left)
cacheFields = fields.toTypedArray()
}
return cacheFields!!
}
private var cacheColumnNames: Array<String>? = null
private fun getColumnNames(): Array<String> {
if (cacheColumnNames == null) {
val names: MutableList<String> = ArrayList()
val columns = fieldColumnPairs
for (pair in columns) {
var cname = pair.right.name
if (cname.isEmpty()) cname = pair.left.name
if (names.contains(cname)) throw RuntimeException("duplicated column : $cname")
names.add(cname)
}
cacheColumnNames = names.toTypedArray()
}
return cacheColumnNames!!
}
private var cacheTableName: String? = null
private fun getTableName(): String {
if (cacheTableName == null) {
val name = getTableAnnotation().name
if (name.isEmpty()) throw RuntimeException("Table name is empty")
cacheTableName = name
}
return cacheTableName!!
}
private var cacheIdName: String? = null
private fun getIdName(): String {
if (cacheIdName == null) {
val id = getTableAnnotation().id
if (id.isEmpty()) throw RuntimeException("Table id is empty")
cacheIdName = id
}
return cacheIdName!!
}
private var cacheIdField: Field? = null
private fun getIdField(): Field {
if (cacheIdField == null) {
val fields = getFields()
val idName = getIdName()
for (f in fields) if (f.name == idName) {
cacheIdField = f
break
}
if (cacheIdField == null) throw RuntimeException("Field not found: $idName")
}
return cacheIdField!!
}
private fun getTableAnnotation(): Table {
var t: Table? = null
for (annotation in klass.annotations) {
if (annotation !is Table) continue
t = annotation
break
}
if (t == null) throw RuntimeException("Table annotation not found")
return t
}
}

@ -1,163 +0,0 @@
/*
* Copyright (C) 2014 Markus Pfeiffer
*
* 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 org.isoron.uhabits.core.database;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
class Tokenizer {
private final InputStream mStream;
private boolean mIsNext;
private int mCurrent;
public Tokenizer(final InputStream in) {
this.mStream = in;
}
public boolean hasNext() throws IOException {
if (!this.mIsNext) {
this.mIsNext = true;
this.mCurrent = this.mStream.read();
}
return this.mCurrent != -1;
}
public int next() throws IOException {
if (!this.mIsNext) {
this.mCurrent = this.mStream.read();
}
this.mIsNext = false;
return this.mCurrent;
}
public boolean skip(final String s) throws IOException {
if (s == null || s.length() == 0) {
return false;
}
if (s.charAt(0) != this.mCurrent) {
return false;
}
final int len = s.length();
this.mStream.mark(len - 1);
for (int n = 1; n < len; n++) {
final int value = this.mStream.read();
if (value != s.charAt(n)) {
this.mStream.reset();
return false;
}
}
return true;
}
}
public class SQLParser {
public final static int STATE_NONE = 0;
public final static int STATE_STRING = 1;
public final static int STATE_COMMENT = 2;
public final static int STATE_COMMENT_BLOCK = 3;
public static List<String> parse(final InputStream stream) throws IOException {
final BufferedInputStream buffer = new BufferedInputStream(stream);
final List<String> commands = new ArrayList<String>();
final StringBuffer sb = new StringBuffer();
try {
final Tokenizer tokenizer = new Tokenizer(buffer);
int state = STATE_NONE;
while (tokenizer.hasNext()) {
final char c = (char) tokenizer.next();
if (state == STATE_COMMENT_BLOCK) {
if (tokenizer.skip("*/")) {
state = STATE_NONE;
}
continue;
} else if (state == STATE_COMMENT) {
if (isNewLine(c)) {
state = STATE_NONE;
}
continue;
} else if (state == STATE_NONE && tokenizer.skip("/*")) {
state = STATE_COMMENT_BLOCK;
continue;
} else if (state == STATE_NONE && tokenizer.skip("--")) {
state = STATE_COMMENT;
continue;
} else if (state == STATE_NONE && c == ';') {
final String command = sb.toString().trim();
commands.add(command);
sb.setLength(0);
continue;
} else if (state == STATE_NONE && c == '\'') {
state = STATE_STRING;
} else if (state == STATE_STRING && c == '\'') {
state = STATE_NONE;
}
if (state == STATE_NONE || state == STATE_STRING) {
if (state == STATE_NONE && isWhitespace(c)) {
if (sb.length() > 0 && sb.charAt(sb.length() - 1) != ' ') {
sb.append(' ');
}
} else {
sb.append(c);
}
}
}
} finally {
buffer.close();
}
if (sb.length() > 0) {
commands.add(sb.toString().trim());
}
return commands;
}
private static boolean isNewLine(final char c) {
return c == '\r' || c == '\n';
}
private static boolean isWhitespace(final char c) {
return c == '\r' || c == '\n' || c == '\t' || c == ' ';
}
}

@ -0,0 +1,130 @@
/*
* Copyright (C) 2014 Markus Pfeiffer
*
* 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 org.isoron.uhabits.core.database
import java.io.*
import java.util.*
internal class Tokenizer(
private val mStream: InputStream,
) {
private var mIsNext = false
private var mCurrent = 0
operator fun hasNext(): Boolean {
if (!mIsNext) {
mIsNext = true
mCurrent = mStream.read()
}
return mCurrent != -1
}
operator fun next(): Int {
if (!mIsNext) {
mCurrent = mStream.read()
}
mIsNext = false
return mCurrent
}
fun skip(s: String?): Boolean {
if (s == null || s.isEmpty()) {
return false
}
if (s[0].toInt() != mCurrent) {
return false
}
val len = s.length
mStream.mark(len - 1)
for (n in 1 until len) {
val value = mStream.read()
if (value != s[n].toInt()) {
mStream.reset()
return false
}
}
return true
}
}
object SQLParser {
private const val STATE_NONE = 0
private const val STATE_STRING = 1
private const val STATE_COMMENT = 2
private const val STATE_COMMENT_BLOCK = 3
fun parse(stream: InputStream): List<String> {
val buffer = BufferedInputStream(stream)
val commands: MutableList<String> = ArrayList()
val sb = StringBuffer()
try {
val tokenizer = Tokenizer(buffer)
var state = STATE_NONE
while (tokenizer.hasNext()) {
val c = tokenizer.next().toChar()
if (state == STATE_COMMENT_BLOCK) {
if (tokenizer.skip("*/")) {
state = STATE_NONE
}
continue
} else if (state == STATE_COMMENT) {
if (isNewLine(c)) {
state = STATE_NONE
}
continue
} else if (state == STATE_NONE && tokenizer.skip("/*")) {
state = STATE_COMMENT_BLOCK
continue
} else if (state == STATE_NONE && tokenizer.skip("--")) {
state = STATE_COMMENT
continue
} else if (state == STATE_NONE && c == ';') {
val command = sb.toString().trim { it <= ' ' }
commands.add(command)
sb.setLength(0)
continue
} else if (state == STATE_NONE && c == '\'') {
state = STATE_STRING
} else if (state == STATE_STRING && c == '\'') {
state = STATE_NONE
}
if (state == STATE_NONE || state == STATE_STRING) {
if (state == STATE_NONE && isWhitespace(c)) {
if (sb.length > 0 && sb[sb.length - 1] != ' ') {
sb.append(' ')
}
} else {
sb.append(c)
}
}
}
} finally {
buffer.close()
}
if (sb.isNotEmpty()) {
commands.add(sb.toString().trim { it <= ' ' })
}
return commands
}
private fun isNewLine(c: Char): Boolean {
return c == '\r' || c == '\n'
}
private fun isWhitespace(c: Char): Boolean {
return c == '\r' || c == '\n' || c == '\t' || c == ' '
}
}

@ -16,15 +16,8 @@
* 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.core.database
package org.isoron.uhabits.core.database;
import java.lang.annotation.*;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Table
{
String name();
String id() default "id";
}
@Target(AnnotationTarget.ANNOTATION_CLASS, AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
annotation class Table(val name: String, val id: String = "id")

@ -16,9 +16,6 @@
* 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.core.database
package org.isoron.uhabits.core.database;
public class UnsupportedDatabaseVersionException extends RuntimeException
{
}
class UnsupportedDatabaseVersionException : RuntimeException()

@ -89,7 +89,7 @@ class SyncManager @Inject constructor(
}
logger.info("Encrypting local database...")
val encryptedDB = db.file.encryptToString(encryptionKey)
val encryptedDB = db.file!!.encryptToString(encryptionKey)
val size = encryptedDB.length / 1024
try {

Loading…
Cancel
Save