mirror of https://github.com/iSoron/uhabits.git
parent
feb3c98459
commit
a02c7bdc44
@ -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)
|
||||||
|
}
|
||||||
|
}
|
@ -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 == ' '
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in new issue