mirror of https://github.com/iSoron/uhabits.git
commit
96c1a046d4
@ -0,0 +1,85 @@
|
|||||||
|
/*
|
||||||
|
* 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.androidbase.storage;
|
||||||
|
|
||||||
|
import android.content.*;
|
||||||
|
import android.database.sqlite.*;
|
||||||
|
|
||||||
|
import org.isoron.androidbase.*;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
|
||||||
|
public class BaseSQLiteOpenHelper extends SQLiteOpenHelper
|
||||||
|
{
|
||||||
|
private final Context context;
|
||||||
|
|
||||||
|
private final int version;
|
||||||
|
|
||||||
|
public BaseSQLiteOpenHelper(@AppContext Context context,
|
||||||
|
String databaseFilename,
|
||||||
|
int version)
|
||||||
|
{
|
||||||
|
super(context, databaseFilename, null, version);
|
||||||
|
this.context = context;
|
||||||
|
this.version = version;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate(SQLiteDatabase db)
|
||||||
|
{
|
||||||
|
executeMigrations(db, -1, version);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion)
|
||||||
|
{
|
||||||
|
executeMigrations(db, oldVersion, newVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void executeMigrations(SQLiteDatabase db,
|
||||||
|
int oldVersion,
|
||||||
|
int newVersion)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
for (int v = oldVersion + 1; v <= newVersion; v++)
|
||||||
|
{
|
||||||
|
String fname = String.format(Locale.US, "migrations/%d.sql", v);
|
||||||
|
InputStream stream = context.getAssets().open(fname);
|
||||||
|
for (String command : SQLParser.parse(stream))
|
||||||
|
db.execSQL(command);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion)
|
||||||
|
{
|
||||||
|
throw new UnsupportedDatabaseVersionException();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,30 @@
|
|||||||
|
/*
|
||||||
|
* 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.androidbase.storage;
|
||||||
|
|
||||||
|
import java.lang.annotation.*;
|
||||||
|
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
public @interface Column
|
||||||
|
{
|
||||||
|
String name() default "";
|
||||||
|
}
|
@ -0,0 +1,163 @@
|
|||||||
|
/*
|
||||||
|
* 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.androidbase.storage;
|
||||||
|
|
||||||
|
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,282 @@
|
|||||||
|
/*
|
||||||
|
* 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.androidbase.storage;
|
||||||
|
|
||||||
|
import android.content.*;
|
||||||
|
import android.database.*;
|
||||||
|
import android.database.sqlite.*;
|
||||||
|
import android.support.annotation.*;
|
||||||
|
import android.util.*;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.*;
|
||||||
|
|
||||||
|
import java.lang.annotation.*;
|
||||||
|
import java.lang.reflect.*;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
public class SQLiteRepository<T>
|
||||||
|
{
|
||||||
|
@NonNull
|
||||||
|
private final Class klass;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private final SQLiteDatabase db;
|
||||||
|
|
||||||
|
public SQLiteRepository(@NonNull Class<T> klass, @NonNull SQLiteDatabase db)
|
||||||
|
{
|
||||||
|
this.klass = klass;
|
||||||
|
this.db = db;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public T find(@NonNull Long id)
|
||||||
|
{
|
||||||
|
return findFirst(String.format("where %s=?", getIdName()),
|
||||||
|
id.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public List<T> findAll(String query, String... params)
|
||||||
|
{
|
||||||
|
try (Cursor c = db.rawQuery(buildSelectQuery() + query, params))
|
||||||
|
{
|
||||||
|
return cursorToMultipleRecords(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public T findFirst(String query, String... params)
|
||||||
|
{
|
||||||
|
try (Cursor c = db.rawQuery(buildSelectQuery() + query, params))
|
||||||
|
{
|
||||||
|
if (!c.moveToNext()) return null;
|
||||||
|
return cursorToSingleRecord(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void execSQL(String query, Object... params)
|
||||||
|
{
|
||||||
|
db.execSQL(query, params);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void save(T record)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Field fields[] = getFields();
|
||||||
|
String columns[] = getColumnNames();
|
||||||
|
|
||||||
|
ContentValues values = new ContentValues();
|
||||||
|
for (int i = 0; i < fields.length; i++)
|
||||||
|
fieldToContentValue(values, columns[i], fields[i], record);
|
||||||
|
|
||||||
|
Long id = (Long) getIdField().get(record);
|
||||||
|
int affectedRows = 0;
|
||||||
|
|
||||||
|
if (id != null) affectedRows =
|
||||||
|
db.update(getTableName(), values, getIdName() + "=?",
|
||||||
|
new String[]{ id.toString() });
|
||||||
|
|
||||||
|
if (id == null || affectedRows == 0)
|
||||||
|
{
|
||||||
|
id = db.insertOrThrow(getTableName(), null, values);
|
||||||
|
getIdField().set(record, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void remove(T record)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Long id = (Long) getIdField().get(record);
|
||||||
|
if (id == null) return;
|
||||||
|
|
||||||
|
db.delete(getTableName(), getIdName() + "=?",
|
||||||
|
new String[]{ 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 void fieldToContentValue(ContentValues values,
|
||||||
|
String columnName,
|
||||||
|
Field field,
|
||||||
|
T record)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (field.getType().isAssignableFrom(Integer.class))
|
||||||
|
values.put(columnName, (Integer) field.get(record));
|
||||||
|
else if (field.getType().isAssignableFrom(Long.class))
|
||||||
|
values.put(columnName, (Long) field.get(record));
|
||||||
|
else if (field.getType().isAssignableFrom(Double.class))
|
||||||
|
values.put(columnName, (Double) field.get(record));
|
||||||
|
else if (field.getType().isAssignableFrom(String.class))
|
||||||
|
values.put(columnName, (String) field.get(record));
|
||||||
|
else throw new RuntimeException(
|
||||||
|
"Type not supported: " + field.getName());
|
||||||
|
}
|
||||||
|
catch (IllegalAccessException e)
|
||||||
|
{
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 Pair<>(field, column));
|
||||||
|
}
|
||||||
|
return fields;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private Field[] getFields()
|
||||||
|
{
|
||||||
|
List<Field> fields = new ArrayList<>();
|
||||||
|
List<Pair<Field, Column>> columns = getFieldColumnPairs();
|
||||||
|
for (Pair<Field, Column> pair : columns) fields.add(pair.first);
|
||||||
|
return fields.toArray(new Field[]{});
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private String[] getColumnNames()
|
||||||
|
{
|
||||||
|
List<String> names = new ArrayList<>();
|
||||||
|
List<Pair<Field, Column>> columns = getFieldColumnPairs();
|
||||||
|
for (Pair<Field, Column> pair : columns)
|
||||||
|
{
|
||||||
|
String cname = pair.second.name();
|
||||||
|
if (cname.isEmpty()) cname = pair.first.getName();
|
||||||
|
if (names.contains(cname))
|
||||||
|
throw new RuntimeException("duplicated column : " + cname);
|
||||||
|
names.add(cname);
|
||||||
|
}
|
||||||
|
|
||||||
|
return names.toArray(new String[]{});
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private String getTableName()
|
||||||
|
{
|
||||||
|
String name = getTableAnnotation().name();
|
||||||
|
if (name.isEmpty()) throw new RuntimeException("Table name is empty");
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private String getIdName()
|
||||||
|
{
|
||||||
|
String id = getTableAnnotation().id();
|
||||||
|
if (id.isEmpty()) throw new RuntimeException("Table id is empty");
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private Field getIdField()
|
||||||
|
{
|
||||||
|
Field fields[] = getFields();
|
||||||
|
String idName = getIdName();
|
||||||
|
for (Field f : fields)
|
||||||
|
if (f.getName().equals(idName)) return f;
|
||||||
|
throw new RuntimeException("Field not found: " + idName);
|
||||||
|
}
|
||||||
|
|
||||||
|
@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,32 @@
|
|||||||
|
/*
|
||||||
|
* 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.androidbase.storage;
|
||||||
|
|
||||||
|
import java.lang.annotation.*;
|
||||||
|
|
||||||
|
@Target(ElementType.TYPE)
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
public @interface Table
|
||||||
|
{
|
||||||
|
String name();
|
||||||
|
String id() default "id";
|
||||||
|
}
|
@ -0,0 +1,189 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||||
|
*
|
||||||
|
* This file is part of Loop Habit Tracker.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by the
|
||||||
|
* Free Software Foundation, either version 3 of the License, or (at your
|
||||||
|
* option) any later version.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||||
|
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||||
|
* more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along
|
||||||
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.isoron.uhabits.models.sqlite;
|
||||||
|
|
||||||
|
import android.database.sqlite.*;
|
||||||
|
import android.support.test.runner.*;
|
||||||
|
import android.test.suitebuilder.annotation.*;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.builder.*;
|
||||||
|
import org.isoron.androidbase.storage.*;
|
||||||
|
import org.isoron.uhabits.*;
|
||||||
|
import org.isoron.uhabits.utils.*;
|
||||||
|
import org.junit.*;
|
||||||
|
import org.junit.runner.*;
|
||||||
|
|
||||||
|
import static org.hamcrest.core.IsEqual.*;
|
||||||
|
import static org.junit.Assert.assertThat;
|
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4.class)
|
||||||
|
@MediumTest
|
||||||
|
public class SQLiteRepositoryTest extends BaseAndroidTest
|
||||||
|
{
|
||||||
|
private SQLiteRepository<ThingRecord> repository;
|
||||||
|
|
||||||
|
private SQLiteDatabase db;
|
||||||
|
|
||||||
|
@Before
|
||||||
|
@Override
|
||||||
|
public void setUp()
|
||||||
|
{
|
||||||
|
super.setUp();
|
||||||
|
this.db = DatabaseUtils.openDatabase();
|
||||||
|
repository = new SQLiteRepository<>(ThingRecord.class, db);
|
||||||
|
|
||||||
|
db.execSQL("drop table if exists tests");
|
||||||
|
db.execSQL("create table tests(" +
|
||||||
|
"id integer not null primary key autoincrement, " +
|
||||||
|
"color_number integer not null, score float not null, " +
|
||||||
|
"name string not null)");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testFind() throws Exception
|
||||||
|
{
|
||||||
|
db.execSQL("insert into tests(id, color_number, name, score) " +
|
||||||
|
"values (10, 20, 'hello', 8.0)");
|
||||||
|
|
||||||
|
ThingRecord record = repository.find(10L);
|
||||||
|
|
||||||
|
assertNotNull(record);
|
||||||
|
assertThat(record.id, equalTo(10L));
|
||||||
|
assertThat(record.color, equalTo(20));
|
||||||
|
assertThat(record.name, equalTo("hello"));
|
||||||
|
assertThat(record.score, equalTo(8.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSave_withId() throws Exception
|
||||||
|
{
|
||||||
|
ThingRecord record = new ThingRecord();
|
||||||
|
record.id = 50L;
|
||||||
|
record.color = 10;
|
||||||
|
record.name = "hello";
|
||||||
|
record.score = 5.0;
|
||||||
|
repository.save(record);
|
||||||
|
assertThat(record, equalTo(repository.find(50L)));
|
||||||
|
|
||||||
|
record.name = "world";
|
||||||
|
record.score = 128.0;
|
||||||
|
repository.save(record);
|
||||||
|
assertThat(record, equalTo(repository.find(50L)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSave_withoutId() throws Exception
|
||||||
|
{
|
||||||
|
ThingRecord r1 = new ThingRecord();
|
||||||
|
r1.color = 10;
|
||||||
|
r1.name = "hello";
|
||||||
|
r1.score = 16.0;
|
||||||
|
repository.save(r1);
|
||||||
|
|
||||||
|
ThingRecord r2 = new ThingRecord();
|
||||||
|
r2.color = 20;
|
||||||
|
r2.name = "world";
|
||||||
|
r2.score = 2.0;
|
||||||
|
repository.save(r2);
|
||||||
|
|
||||||
|
assertThat(r1.id, equalTo(1L));
|
||||||
|
assertThat(r2.id, equalTo(2L));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testRemove() throws Exception
|
||||||
|
{
|
||||||
|
ThingRecord rec1 = new ThingRecord();
|
||||||
|
rec1.color = 10;
|
||||||
|
rec1.name = "hello";
|
||||||
|
rec1.score = 16.0;
|
||||||
|
repository.save(rec1);
|
||||||
|
|
||||||
|
ThingRecord rec2 = new ThingRecord();
|
||||||
|
rec2.color = 20;
|
||||||
|
rec2.name = "world";
|
||||||
|
rec2.score = 32.0;
|
||||||
|
repository.save(rec2);
|
||||||
|
|
||||||
|
long id = rec1.id;
|
||||||
|
assertThat(rec1, equalTo(repository.find(id)));
|
||||||
|
assertThat(rec2, equalTo(repository.find(rec2.id)));
|
||||||
|
|
||||||
|
repository.remove(rec1);
|
||||||
|
assertThat(rec1.id, equalTo(null));
|
||||||
|
assertNull(repository.find(id));
|
||||||
|
assertThat(rec2, equalTo(repository.find(rec2.id)));
|
||||||
|
|
||||||
|
repository.remove(rec1); // should have no effect
|
||||||
|
assertNull(repository.find(id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Table(name = "tests")
|
||||||
|
class ThingRecord
|
||||||
|
{
|
||||||
|
@Column
|
||||||
|
public Long id;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
public String name;
|
||||||
|
|
||||||
|
@Column(name = "color_number")
|
||||||
|
public Integer color;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
public Double score;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o)
|
||||||
|
{
|
||||||
|
if (this == o) return true;
|
||||||
|
|
||||||
|
if (o == null || getClass() != o.getClass()) return false;
|
||||||
|
|
||||||
|
ThingRecord record = (ThingRecord) o;
|
||||||
|
|
||||||
|
return new EqualsBuilder()
|
||||||
|
.append(id, record.id)
|
||||||
|
.append(name, record.name)
|
||||||
|
.append(color, record.color)
|
||||||
|
.isEquals();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode()
|
||||||
|
{
|
||||||
|
return new HashCodeBuilder(17, 37)
|
||||||
|
.append(id)
|
||||||
|
.append(name)
|
||||||
|
.append(color)
|
||||||
|
.toHashCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString()
|
||||||
|
{
|
||||||
|
return new ToStringBuilder(this)
|
||||||
|
.append("id", id)
|
||||||
|
.append("name", name)
|
||||||
|
.append("color", color)
|
||||||
|
.toString();
|
||||||
|
}
|
||||||
|
}
|
@ -1 +1 @@
|
|||||||
alter table habits add column reminder_days integer not null default 127;
|
alter table Habits add column reminder_days integer not null default 127;
|
@ -1,4 +1,4 @@
|
|||||||
create index idx_score_habit_timestamp on score(habit, timestamp);
|
create index idx_score_habit_timestamp on Score(habit, timestamp);
|
||||||
create index idx_checkmark_habit_timestamp on checkmarks(habit, timestamp);
|
create index idx_checkmark_habit_timestamp on Checkmarks(habit, timestamp);
|
||||||
create index idx_repetitions_habit_timestamp on repetitions(habit, timestamp);
|
create index idx_repetitions_habit_timestamp on Repetitions(habit, timestamp);
|
||||||
create index idx_streak_habit_end on streak(habit, end);
|
create index idx_streak_habit_end on Streak(habit, end);
|
@ -1,5 +1,11 @@
|
|||||||
DROP TABLE Score;
|
drop table Score;
|
||||||
CREATE TABLE Score (Id INTEGER PRIMARY KEY AUTOINCREMENT, habit INTEGER REFERENCES Habits(Id), score REAL, timestamp INTEGER);
|
create table Score (
|
||||||
CREATE INDEX idx_score_habit_timestamp on score(habit, timestamp);
|
id integer primary key autoincrement,
|
||||||
delete from Streak;
|
habit integer references habits(id),
|
||||||
delete from Checkmarks;
|
score real,
|
||||||
|
timestamp integer);
|
||||||
|
|
||||||
|
create index idx_score_habit_timestamp on Score(habit, timestamp);
|
||||||
|
|
||||||
|
delete from streak;
|
||||||
|
delete from checkmarks;
|
@ -0,0 +1,6 @@
|
|||||||
|
create table Events (
|
||||||
|
id integer primary key autoincrement,
|
||||||
|
timestamp integer,
|
||||||
|
message text,
|
||||||
|
server_id integer
|
||||||
|
);
|
@ -1,2 +0,0 @@
|
|||||||
alter table habits add column reminder_hour integer;
|
|
||||||
alter table habits add column reminder_min integer;
|
|
@ -1 +0,0 @@
|
|||||||
alter table habits add column highlight integer not null default 0;
|
|
@ -1 +0,0 @@
|
|||||||
alter table habits add column archived integer not null default 0;
|
|
@ -0,0 +1,41 @@
|
|||||||
|
create table Habits (
|
||||||
|
id integer primary key autoincrement,
|
||||||
|
archived integer,
|
||||||
|
color integer,
|
||||||
|
description text,
|
||||||
|
freq_den integer,
|
||||||
|
freq_num integer,
|
||||||
|
highlight integer,
|
||||||
|
name text,
|
||||||
|
position integer,
|
||||||
|
reminder_hour integer,
|
||||||
|
reminder_min integer
|
||||||
|
);
|
||||||
|
|
||||||
|
create table Checkmarks (
|
||||||
|
id integer primary key autoincrement,
|
||||||
|
habit integer references habits(id),
|
||||||
|
timestamp integer,
|
||||||
|
value integer
|
||||||
|
);
|
||||||
|
|
||||||
|
create table Repetitions (
|
||||||
|
id integer primary key autoincrement,
|
||||||
|
habit integer references habits(id),
|
||||||
|
timestamp integer
|
||||||
|
);
|
||||||
|
|
||||||
|
create table Streak (
|
||||||
|
id integer primary key autoincrement,
|
||||||
|
end integer,
|
||||||
|
habit integer references habits(id),
|
||||||
|
length integer,
|
||||||
|
start integer
|
||||||
|
);
|
||||||
|
|
||||||
|
create table Score (
|
||||||
|
id integer primary key autoincrement,
|
||||||
|
habit integer references habits(id),
|
||||||
|
score integer,
|
||||||
|
timestamp integer
|
||||||
|
);
|
@ -0,0 +1,54 @@
|
|||||||
|
/*
|
||||||
|
* 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;
|
||||||
|
|
||||||
|
import android.content.*;
|
||||||
|
import android.database.sqlite.*;
|
||||||
|
|
||||||
|
import org.isoron.androidbase.storage.*;
|
||||||
|
|
||||||
|
|
||||||
|
public class HabitsDatabaseOpener extends BaseSQLiteOpenHelper
|
||||||
|
{
|
||||||
|
private final int version;
|
||||||
|
|
||||||
|
public HabitsDatabaseOpener(Context context,
|
||||||
|
String databaseFilename,
|
||||||
|
int version)
|
||||||
|
{
|
||||||
|
super(context, databaseFilename, version);
|
||||||
|
this.version = version;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate(SQLiteDatabase db)
|
||||||
|
{
|
||||||
|
onUpgrade(db, 8, version);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion)
|
||||||
|
{
|
||||||
|
if(oldVersion < 8) throw new UnsupportedDatabaseVersionException();
|
||||||
|
super.onUpgrade(db, oldVersion, newVersion);
|
||||||
|
}
|
||||||
|
}
|
@ -1,84 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
|
||||||
*
|
|
||||||
* This file is part of Loop Habit Tracker.
|
|
||||||
*
|
|
||||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by the
|
|
||||||
* Free Software Foundation, either version 3 of the License, or (at your
|
|
||||||
* option) any later version.
|
|
||||||
*
|
|
||||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
|
||||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
|
||||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
|
||||||
* more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License along
|
|
||||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.isoron.uhabits.models.sqlite;
|
|
||||||
|
|
||||||
import android.database.*;
|
|
||||||
import android.database.sqlite.*;
|
|
||||||
import android.support.annotation.*;
|
|
||||||
|
|
||||||
import com.activeandroid.*;
|
|
||||||
|
|
||||||
import org.isoron.uhabits.models.sqlite.records.*;
|
|
||||||
|
|
||||||
import java.util.*;
|
|
||||||
|
|
||||||
public class SQLiteUtils<T extends SQLiteRecord>
|
|
||||||
{
|
|
||||||
private Class klass;
|
|
||||||
|
|
||||||
public SQLiteUtils(Class klass)
|
|
||||||
{
|
|
||||||
this.klass = klass;
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
public List<T> query(String query, String params[])
|
|
||||||
{
|
|
||||||
SQLiteDatabase db = Cache.openDatabase();
|
|
||||||
try (Cursor c = db.rawQuery(query, params))
|
|
||||||
{
|
|
||||||
return cursorToMultipleRecords(c);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
public T querySingle(String query, String params[])
|
|
||||||
{
|
|
||||||
SQLiteDatabase db = Cache.openDatabase();
|
|
||||||
try(Cursor c = db.rawQuery(query, params))
|
|
||||||
{
|
|
||||||
if (!c.moveToNext()) return null;
|
|
||||||
return cursorToSingleRecord(c);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@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 c)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
T record = (T) klass.newInstance();
|
|
||||||
record.copyFrom(c);
|
|
||||||
return record;
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in new issue