/*******************************************************************************
* Copyright 2012 Crazywater
*
* 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 de.knufficast.logic.db;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.SQLException;
import android.database.sqlite.SQLiteDatabase;
import android.util.Log;
/**
* A connection to the Android-internal SQLite3 database. Caches writes so that
* multiple writes only write the last result. Also caches reads, so multiple
* reads don't go to the database.
*
* @author crazywater
*
*/
public class Database {
private final Map<ColId, String> cache = new ConcurrentHashMap<ColId, String>();
private final SQLiteHelper dbHelper;
private final DBUpdater dbUpdater;
private SQLiteDatabase database;
private class ColId {
ColId(String table, String column, long id) {
this.table = table;
this.column = column;
this.id = id;
}
String table;
String column;
long id;
@Override
public boolean equals(Object other) {
if (!(other instanceof ColId)) {
return false;
}
ColId c = (ColId) other;
return id == c.id && table.equals(c.table) && column.equals(c.column);
}
@Override
public int hashCode() {
return table.hashCode() ^ column.hashCode() ^ Long.valueOf(id).hashCode();
}
}
public Database(Context context) {
dbHelper = new SQLiteHelper(context);
dbUpdater = new DBUpdater();
dbUpdater.start();
}
public void open() throws SQLException {
database = dbHelper.getWritableDatabase();
}
public void close() {
dbHelper.close();
}
/**
* Gets all row ids from the table.
*/
public List<Long> getIds(String table) {
String[] id = { SQLiteHelper.C_ID };
Cursor cursor = database.query(table, id, null, null, null, null,
SQLiteHelper.C_ID + " DESC");
return getAllIds(cursor);
}
/**
* Querys the database for rows which have column=value.
*/
public List<Long> query(String table, String column, String value) {
String[] id = { SQLiteHelper.C_ID };
String[] values = { value };
Cursor cursor = database.query(table, id, column + " = ?", values, null,
null, SQLiteHelper.C_ID + " DESC");
return getAllIds(cursor);
}
public void delete(String table, long id) {
database.delete(table, SQLiteHelper.C_ID + " = " + id, null);
}
/**
* Gets a value from the database. Might be cached.
*/
public String get(String table, long id, String column) {
String[] col = { column };
ColId key = new ColId(table, column, id);
if (cache.containsKey(key)) {
String result = cache.get(key);
return result;
}
Cursor cursor = database.query(table, col, SQLiteHelper.C_ID + " = " + id,
null, null, null, null);
if (cursor.getCount() != 1) {
Log.e("Database", "Weird number of results: Table " + table + ", id "
+ id
+ ", column " + column + ": results: " + cursor.getCount());
return "";
}
cursor.moveToFirst();
String result = cursor.getString(0);
cursor.close();
return result;
}
/**
* Returns a long value. Is only used for referencing rows of other tables.
*/
public long getLong(String table, long id, String column) {
String[] col = { column };
Cursor cursor = database.query(table, col, SQLiteHelper.C_ID + " = " + id,
null, null, null, null);
cursor.moveToFirst();
long result = cursor.getLong(0);
cursor.close();
return result;
}
/**
* Set a value in the table.
*
* @param table
* the table
* @param id
* the row ID
* @param column
* the column name
* @param value
* the value
*/
public void put(String table, long id, String column, String value) {
ColId colId = new ColId(table, column, id);
cache.put(colId, value);
dbUpdater.postUpdate(colId, value);
}
/**
* Creates a new row in the table.
*
* @return the ID of the row
*/
public long create(String table, Collection<String> columns,
Iterable<String> values) {
ContentValues cvs = new ContentValues();
Iterator<String> it = values.iterator();
for (String col : columns) {
String val = it.next();
cvs.put(col, val == null ? "" : val);
}
long id = database.insert(table, null, cvs);
return id;
}
private List<Long> getAllIds(Cursor cursor) {
cursor.moveToFirst();
List<Long> results = new ArrayList<Long>();
while (!cursor.isAfterLast()) {
results.add(cursor.getLong(0));
cursor.moveToNext();
}
cursor.close();
return results;
}
/**
* A simple updater thread that does writes to the database in the background.
* "Batches up" writes to the same location, so that only the last write is
* executed. Writes are generally executed only after a time of WAIT_TIME.
*/
private class DBUpdater extends Thread {
private Map<ColId, String> toUpdate = new ConcurrentHashMap<ColId, String>();
private static final long WAIT_TIME = 10 * 1000; // 10s
private long lastWrite = 0;
@Override
public void run() {
while (true) {
try {
synchronized (this) {
this.wait();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
long diff = System.currentTimeMillis() - lastWrite;
if (diff < WAIT_TIME) {
try {
Thread.sleep(WAIT_TIME - diff);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
lastWrite = System.currentTimeMillis();
while (!toUpdate.isEmpty()) {
Iterator<Entry<ColId, String>> it = toUpdate.entrySet().iterator();
while (it.hasNext()) {
Entry<ColId, String> entry = it.next();
ColId id = entry.getKey();
ContentValues cvs = new ContentValues();
cvs.put(id.column, entry.getValue());
database.update(id.table, cvs, SQLiteHelper.C_ID + " = " + id.id,
null);
it.remove();
}
}
}
}
/**
* Request a database update from this thread.
*/
void postUpdate(ColId id, String value) {
toUpdate.put(id, value);
synchronized (this) {
this.notify();
}
}
}
}