Merge branch 'replace-db'

This commit is contained in:
Felix Ableitner 2016-04-01 00:58:02 +02:00
commit cfb0723c1f
19 changed files with 294 additions and 383 deletions

View file

@ -1,139 +0,0 @@
package com.nutomic.ensichat.util
import java.util.GregorianCalendar
import java.util.concurrent.CountDownLatch
import android.content._
import android.database.DatabaseErrorHandler
import android.database.sqlite.SQLiteDatabase
import android.support.v4.content.LocalBroadcastManager
import android.test.AndroidTestCase
import com.nutomic.ensichat.core.body.{CryptoData, Text}
import com.nutomic.ensichat.core.header.ContentHeader
import com.nutomic.ensichat.core.{Address, Message, User}
import com.nutomic.ensichat.util.DatabaseTest._
import junit.framework.Assert._
import scala.collection.SortedSet
import scala.collection.immutable.TreeSet
object DatabaseTest {
/**
* Provides a temporary database file that can be deleted easily.
*/
private class DatabaseContext(context: Context) extends ContextWrapper(context) {
private val dbFile = "database-test.db"
override def openOrCreateDatabase(file: String, mode: Int, factory:
SQLiteDatabase.CursorFactory, errorHandler: DatabaseErrorHandler) =
context.openOrCreateDatabase(dbFile, mode, factory, errorHandler)
def deleteDbFile() = context.deleteDatabase(dbFile)
}
private val a1 = new Address("A51B74475EE622C3C924DB147668F85E024CA0B44CA146B5E3D3C31A54B34C1E")
private val a2 = new Address("222229685A73AB8F2F853B3EA515633B7CD5A6ABDC3210BC4EF38F955A14AAF6")
private val a3 = new Address("3333359893F8810C4024CFC951374AABA1F4DE6347A3D7D8E44918AD1FF2BA36")
private val a4 = new Address("4444459893F8810C4024CFC951374AABA1F4DE6347A3D7D8E44918AD1FF2BA36")
private val h1 = new ContentHeader(a1, a2, 1234, Text.Type, Some(123),
Some(new GregorianCalendar(1970, 1, 1).getTime), 5)
private val h2 = new ContentHeader(a1, a3, 30000, Text.Type, Some(8765),
Some(new GregorianCalendar(2014, 6, 10).getTime), 20)
private val h3 = new ContentHeader(a4, a2, 250, Text.Type, Some(77),
Some(new GregorianCalendar(2020, 11, 11).getTime), 123)
private val m1 = new Message(h1, new Text("first"))
private val m2 = new Message(h2, new Text("second"))
private val m3 = new Message(h3, new Text("third"))
private val u1 = new User(a1, "one", "s1")
private val u2 = new User(a2, "two", "s2")
}
class DatabaseTest extends AndroidTestCase {
private lazy val context = new DatabaseTest.DatabaseContext(getContext)
private lazy val database = new Database(context)
override def setUp(): Unit = {
super.setUp()
database.onMessageReceived(m1)
database.onMessageReceived(m2)
database.onMessageReceived(m3)
}
override def tearDown(): Unit = {
super.tearDown()
database.close()
context.deleteDbFile()
}
/**
* Calls [[Database.getMessagesCursor]] with parameters and converts the result to sorted set.
*/
private def getMessages(address: Address): SortedSet[Message] = {
val c = database.getMessagesCursor(address)
var messages = new TreeSet[Message]()(Message.Ordering)
while (c.moveToNext()) {
messages += Database.messageFromCursor(c)
}
c.close()
messages
}
def testMessageOrder(): Unit = {
val msg = getMessages(m1.header.target).firstKey
assertEquals(m1.body, msg.body)
}
def testMessageSelect(): Unit = {
val msg = getMessages(m1.header.target)
assertTrue(msg.contains(m1))
assertFalse(msg.contains(m2))
assertTrue(msg.contains(m3))
}
def testMessageFields(): Unit = {
val msg = getMessages(m2.header.target).firstKey
val header = msg.header.asInstanceOf[ContentHeader]
assertEquals(h2.origin, header.origin)
assertEquals(h2.target, header.target)
assertEquals(-1, msg.header.seqNum)
assertEquals(h2.contentType, header.contentType)
assertEquals(h2.messageId, header.messageId)
assertEquals(h2.time, header.time)
assertEquals(new CryptoData(None, None), msg.crypto)
assertEquals(m2.body, msg.body)
}
def testAddContact(): Unit = {
database.addContact(u1)
val contacts = database.getContacts
assertEquals(1, contacts.size)
assertEquals(Option(u1), database.getContact(u1.address))
}
def testAddContactCallback(): Unit = {
val latch = new CountDownLatch(1)
val lbm = LocalBroadcastManager.getInstance(context)
val receiver = new BroadcastReceiver {
override def onReceive(context: Context, intent: Intent): Unit = latch.countDown()
}
lbm.registerReceiver(receiver, new IntentFilter(Database.ActionContactsUpdated))
database.addContact(u1)
latch.await()
lbm.unregisterReceiver(receiver)
}
def testGetContact(): Unit = {
database.addContact(u2)
assertTrue(database.getContact(u1.address).isEmpty)
val c = database.getContact(u2.address)
assertTrue(c.nonEmpty)
assertEquals(Option(u2), c)
}
}

View file

@ -14,7 +14,6 @@ import com.google.zxing.integration.android.IntentIntegrator
import com.nutomic.ensichat.R
import com.nutomic.ensichat.core.Address
import com.nutomic.ensichat.service.CallbackHandler
import com.nutomic.ensichat.util.Database
import com.nutomic.ensichat.views.UsersAdapter
/**
@ -22,8 +21,6 @@ import com.nutomic.ensichat.views.UsersAdapter
*/
class ConnectionsActivity extends EnsichatActivity with OnItemClickListener {
private lazy val database = new Database(this)
private lazy val adapter = new UsersAdapter(this)
/**
@ -41,7 +38,7 @@ class ConnectionsActivity extends EnsichatActivity with OnItemClickListener {
val filter = new IntentFilter()
filter.addAction(CallbackHandler.ActionConnectionsChanged)
filter.addAction(Database.ActionContactsUpdated)
filter.addAction(CallbackHandler.ActionContactsUpdated)
LocalBroadcastManager.getInstance(this)
.registerReceiver(onContactsUpdatedReceiver, filter)
}
@ -120,7 +117,7 @@ class ConnectionsActivity extends EnsichatActivity with OnItemClickListener {
val user = service.get.getUser(parsedAddress)
if (database.getContacts.map(_.address).contains(user.address)) {
if (database.get.getContacts.map(_.address).contains(user.address)) {
val text = getString(R.string.contact_already_added, user.name)
Toast.makeText(this, text, Toast.LENGTH_SHORT).show()
return
@ -130,7 +127,7 @@ class ConnectionsActivity extends EnsichatActivity with OnItemClickListener {
.setMessage(getString(R.string.dialog_add_contact, user.name))
.setPositiveButton(android.R.string.yes, new OnClickListener {
override def onClick(dialog: DialogInterface, which: Int): Unit = {
database.addContact(user)
database.get.addContact(user)
Toast.makeText(ConnectionsActivity.this, R.string.toast_contact_added, Toast.LENGTH_SHORT)
.show()
}

View file

@ -62,4 +62,6 @@ class EnsichatActivity extends AppCompatActivity with ServiceConnection {
*/
def service = chatService.map(_.getConnectionHandler)
def database = chatService.map(_.database)
}

View file

@ -15,7 +15,6 @@ import com.nutomic.ensichat.activities.EnsichatActivity
import com.nutomic.ensichat.core.body.Text
import com.nutomic.ensichat.core.{Address, ConnectionHandler, Message}
import com.nutomic.ensichat.service.CallbackHandler
import com.nutomic.ensichat.util.Database
import com.nutomic.ensichat.views.{DatesAdapter, MessagesAdapter}
/**
@ -31,8 +30,6 @@ class ChatFragment extends ListFragment with OnClickListener {
this.address = address
}
private lazy val database = new Database(getActivity)
private lazy val activity = getActivity.asInstanceOf[EnsichatActivity]
private var address: Address = _
@ -53,10 +50,10 @@ class ChatFragment extends ListFragment with OnClickListener {
activity.runOnServiceConnected(() => {
chatService = activity.service.get
database.getContact(address).foreach(c => getActivity.setTitle(c.name))
activity.database.get.getContact(address).foreach(c => getActivity.setTitle(c.name))
adapter = new DatesAdapter(getActivity,
new MessagesAdapter(getActivity, database.getMessagesCursor(address), address))
new MessagesAdapter(getActivity, activity.database.get.getMessages(address), address))
if (listView != null) {
listView.setAdapter(adapter)
@ -131,7 +128,8 @@ class ChatFragment extends ListFragment with OnClickListener {
msg.body match {
case _: Text =>
adapter.changeCursor(database.getMessagesCursor(address))
val messages = activity.database.get.getMessages(address)
adapter.replaceItems(messages)
case _ =>
}
}

View file

@ -18,7 +18,6 @@ import com.nutomic.ensichat.R
import com.nutomic.ensichat.activities.{ConnectionsActivity, EnsichatActivity, MainActivity, SettingsActivity}
import com.nutomic.ensichat.core.interfaces.SettingsInterface
import com.nutomic.ensichat.service.{CallbackHandler, ChatService}
import com.nutomic.ensichat.util.Database
import com.nutomic.ensichat.views.UsersAdapter
import scala.collection.JavaConversions._
@ -30,7 +29,7 @@ class ContactsFragment extends ListFragment with OnClickListener {
private lazy val adapter = new UsersAdapter(getActivity)
private lazy val database = new Database(getActivity)
private lazy val database = activity.database.get
private lazy val lbm = LocalBroadcastManager.getInstance(getActivity)
@ -44,7 +43,7 @@ class ContactsFragment extends ListFragment with OnClickListener {
setListAdapter(adapter)
setHasOptionsMenu(true)
lbm.registerReceiver(onContactsUpdatedListener, new IntentFilter(Database.ActionContactsUpdated))
lbm.registerReceiver(onContactsUpdatedListener, new IntentFilter(CallbackHandler.ActionContactsUpdated))
lbm.registerReceiver(onConnectionsChangedListener, new IntentFilter(CallbackHandler.ActionConnectionsChanged))
}
@ -52,7 +51,7 @@ class ContactsFragment extends ListFragment with OnClickListener {
super.onResume()
activity.runOnServiceConnected(() => {
adapter.clear()
database.getContacts.foreach(adapter.add)
adapter.addAll(database.getContacts)
updateConnections()
})
}

View file

@ -9,7 +9,6 @@ import com.nutomic.ensichat.core.body.UserInfo
import com.nutomic.ensichat.core.interfaces.SettingsInterface._
import com.nutomic.ensichat.fragments.SettingsFragment._
import com.nutomic.ensichat.service.ChatService
import com.nutomic.ensichat.util.Database
import com.nutomic.ensichat.{BuildConfig, R}
object SettingsFragment {
@ -21,7 +20,7 @@ object SettingsFragment {
*/
class SettingsFragment extends PreferenceFragment with OnSharedPreferenceChangeListener {
private lazy val database = new Database(getActivity)
private lazy val activity = getActivity.asInstanceOf[EnsichatActivity]
private lazy val maxConnections = findPreference(KeyMaxConnections)
private lazy val version = findPreference(Version)
@ -52,9 +51,8 @@ class SettingsFragment extends PreferenceFragment with OnSharedPreferenceChangeL
override def onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) {
key match {
case KeyUserName | KeyUserStatus =>
val service = getActivity.asInstanceOf[EnsichatActivity].service
val ui = new UserInfo(prefs.getString(KeyUserName, ""), prefs.getString(KeyUserStatus, ""))
database.getContacts.foreach(c => service.get.sendTo(c.address, ui))
activity.database.get.getContacts.foreach(c => activity.service.get.sendTo(c.address, ui))
case KeyServers =>
val intent = new Intent(getActivity, classOf[ChatService])
intent.setAction(ChatService.ActionNetworkChanged)

View file

@ -10,6 +10,7 @@ object CallbackHandler {
val ActionMessageReceived = "message_received"
val ActionConnectionsChanged = "connections_changed"
val ActionContactsUpdated = "contacts_updated"
val ExtraMessage = "extra_message"
@ -38,4 +39,10 @@ class CallbackHandler(chatService: ChatService, notificationHandler: Notificatio
.updatePersistentNotification(chatService.getConnectionHandler.connections().size)
}
def onContactsUpdated(): Unit = {
val i = new Intent(ActionContactsUpdated)
LocalBroadcastManager.getInstance(chatService)
.sendBroadcast(i)
}
}

View file

@ -8,8 +8,9 @@ import android.content.{Context, Intent, IntentFilter}
import android.net.ConnectivityManager
import android.os.Handler
import com.nutomic.ensichat.bluetooth.BluetoothInterface
import com.nutomic.ensichat.core.util.Database
import com.nutomic.ensichat.core.{ConnectionHandler, Crypto}
import com.nutomic.ensichat.util.{Database, NetworkChangedReceiver, SettingsWrapper}
import com.nutomic.ensichat.util.{NetworkChangedReceiver, SettingsWrapper}
object ChatService {
@ -30,8 +31,10 @@ class ChatService extends Service {
private val callbackHandler = new CallbackHandler(this, notificationHandler)
lazy val database = new Database(getDatabasePath("database"), callbackHandler)
private lazy val connectionHandler =
new ConnectionHandler(new SettingsWrapper(this), new Database(this), callbackHandler,
new ConnectionHandler(new SettingsWrapper(this), database, callbackHandler,
ChatService.newCrypto(this), 1)
private val networkReceiver = new NetworkChangedReceiver()

View file

@ -1,151 +0,0 @@
package com.nutomic.ensichat.util
import java.util.Date
import android.content.{ContentValues, Context, Intent}
import android.database.Cursor
import android.database.sqlite.{SQLiteDatabase, SQLiteOpenHelper}
import android.support.v4.content.LocalBroadcastManager
import com.nutomic.ensichat.core.body.Text
import com.nutomic.ensichat.core.header.ContentHeader
import com.nutomic.ensichat.core.interfaces.DatabaseInterface
import com.nutomic.ensichat.core.{Address, Message, User}
object Database {
val ActionContactsUpdated = "contacts_updated"
private val DatabaseName = "message_store.db"
private val DatabaseVersion = 2
// NOTE: We could make origin/target foreign keys to contacts, but:
// - they don't change anyway
// - we'd have to insert the local user into contacts
private val CreateMessagesTable = "CREATE TABLE messages(" +
"_id INTEGER PRIMARY KEY," +
"origin TEXT NOT NULL," +
"target TEXT NOT NULL," +
"message_id INT NOT NULL," +
"text TEXT NOT NULL," +
"date INT NOT NULL);" // Unix timestamp
private val CreateContactsTable = "CREATE TABLE contacts(" +
"_id INTEGER PRIMARY KEY," +
"address TEXT NOT NULL," +
"name TEXT NOT NULL," +
"status TEXT NOT NULL)"
def messageFromCursor(c: Cursor): Message = {
val header = new ContentHeader(new Address(
c.getString(c.getColumnIndex("origin"))),
new Address(c.getString(c.getColumnIndex("target"))),
-1,
Text.Type,
Some(c.getLong(c.getColumnIndex("message_id"))),
Some(new Date(c.getLong(c.getColumnIndex("date")))))
val body = new Text(new String(c.getString(c.getColumnIndex ("text"))))
new Message(header, body)
}
}
/**
* Stores all messages and contacts in SQL database.
*/
class Database(context: Context) extends DatabaseInterface {
private class Helper
extends SQLiteOpenHelper(context, Database.DatabaseName, null, Database.DatabaseVersion) {
override def onCreate(db: SQLiteDatabase): Unit = {
db.execSQL(Database.CreateContactsTable)
db.execSQL(Database.CreateMessagesTable)
}
override def onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int): Unit = {
if (oldVersion < 2) {
db.execSQL("ALTER TABLE contacts ADD COLUMN status TEXT")
val cv = new ContentValues()
cv.put("status", "")
db.update("contacts", cv, null, null)
}
}
}
private val helper = new Helper()
def close() = helper.close()
def getMessagesCursor(address: Address): Cursor = {
helper.getReadableDatabase.query(true,
"messages", Array("_id", "origin", "target", "message_id", "text", "date"),
"origin = ? OR target = ?", Array(address.toString, address.toString),
null, null, "date ASC", null)
}
def onMessageReceived(msg: Message): Unit = msg.body match {
case text: Text =>
val cv = new ContentValues()
cv.put("origin", msg.header.origin.toString)
cv.put("target", msg.header.target.toString)
// Need to use [[Long#toString]] because of https://issues.scala-lang.org/browse/SI-2991
cv.put("message_id", msg.header.messageId.get.toString)
cv.put("date", msg.header.time.get.getTime.toString)
cv.put("text", text.text)
helper.getWritableDatabase.insert("messages", null, cv)
}
def getContacts: Set[User] = {
val c = helper.getReadableDatabase.query(true, "contacts", Array("address", "name", "status"), "", Array(),
null, null, null, null)
var contacts = Set[User]()
while (c.moveToNext()) {
contacts += new User(new Address(c.getString(c.getColumnIndex("address"))),
c.getString(c.getColumnIndex("name")),
c.getString(c.getColumnIndex("status")))
}
c.close()
contacts
}
def getContact(address: Address): Option[User] = {
val c = helper.getReadableDatabase.query(true, "contacts", Array("address", "name", "status"),
"address = ?", Array(address.toString), null, null, null, null)
if (c.getCount != 0) {
c.moveToNext()
val s = Option(new User(new Address(c.getString(c.getColumnIndex("address"))),
c.getString(c.getColumnIndex("name")),
c.getString(c.getColumnIndex("status"))))
c.close()
s
} else {
c.close()
None
}
}
def addContact(contact: User): Unit = {
val cv = new ContentValues()
cv.put("address", contact.address.toString())
cv.put("name", contact.name)
cv.put("status", contact.status)
helper.getWritableDatabase.insert("contacts", null, cv)
contactsUpdated()
}
def updateContact(contact: User): Unit = {
val cv = new ContentValues()
cv.put("name", contact.name)
cv.put("status", contact.status)
helper.getWritableDatabase.update("contacts", cv, "address = ?", Array(contact.address.toString()))
contactsUpdated()
}
private def contactsUpdated(): Unit = {
LocalBroadcastManager.getInstance(context)
.sendBroadcast(new Intent(Database.ActionContactsUpdated))
}
}

View file

@ -3,18 +3,19 @@ package com.nutomic.ensichat.views
import java.text.DateFormat
import android.content.Context
import android.database.Cursor
import com.mobsandgeeks.adapters.{Sectionizer, SimpleSectionAdapter}
import com.nutomic.ensichat.R
import com.nutomic.ensichat.util.Database
import com.nutomic.ensichat.core.Message
import scala.collection.JavaConverters._
object DatesAdapter {
private val Sectionizer = new Sectionizer[Cursor]() {
override def getSectionTitleForItem(item: Cursor): String = {
private val Sectionizer = new Sectionizer[Message]() {
override def getSectionTitleForItem(item: Message): String = {
DateFormat
.getDateInstance(DateFormat.MEDIUM)
.format(Database.messageFromCursor(item).header.time.get)
.format(item.header.time.get)
}
}
@ -24,11 +25,12 @@ object DatesAdapter {
* Wraps [[MessagesAdapter]] and shows date between messages.
*/
class DatesAdapter(context: Context, messagesAdapter: MessagesAdapter)
extends SimpleSectionAdapter[Cursor](context, messagesAdapter, R.layout.item_date, R.id.date,
extends SimpleSectionAdapter[Message](context, messagesAdapter, R.layout.item_date, R.id.date,
DatesAdapter.Sectionizer) {
def changeCursor(cursor: Cursor): Unit = {
messagesAdapter.changeCursor(cursor)
def replaceItems(items: Seq[Message]): Unit = {
messagesAdapter.clear()
messagesAdapter.addAll(items.asJava)
notifyDataSetChanged()
}

View file

@ -1,16 +1,26 @@
package com.nutomic.ensichat.views
import java.text.DateFormat
import java.util
import android.content.Context
import android.database.Cursor
import android.view._
import android.widget._
import com.mobsandgeeks.adapters.{InstantCursorAdapter, SimpleSectionAdapter, ViewHandler}
import com.mobsandgeeks.adapters.{InstantAdapter, SimpleSectionAdapter, ViewHandler}
import com.nutomic.ensichat.R
import com.nutomic.ensichat.core.body.Text
import com.nutomic.ensichat.core.{Address, Message}
import com.nutomic.ensichat.util.Database
import com.nutomic.ensichat.views.MessagesAdapter._
object MessagesAdapter {
private def itemsAsMutableList(items: Seq[Message]): util.List[Message] = {
val list = new util.ArrayList[Message]()
items.foreach(list.add)
list
}
}
/**
* Displays [[Message]]s in ListView.
@ -18,8 +28,9 @@ import com.nutomic.ensichat.util.Database
* We just use the instant adapter for compatibility with [[SimpleSectionAdapter]], but don't use
* the annotations (as it breaks separation of presentation and content).
*/
class MessagesAdapter(context: Context, cursor: Cursor, remoteAddress: Address) extends
InstantCursorAdapter[Message](context, R.layout.item_message, classOf[Message], cursor) {
class MessagesAdapter(context: Context, items: Seq[Message], remoteAddress: Address) extends
InstantAdapter[Message](context, R.layout.item_message, classOf[Message],
itemsAsMutableList(items)) {
private val MessagePaddingLarge = 50
private val MessagePaddingSmall = 10
@ -52,6 +63,4 @@ class MessagesAdapter(context: Context, cursor: Cursor, remoteAddress: Address)
}
})
override def getInstance (cursor: Cursor) = Database.messageFromCursor(cursor)
}

View file

@ -2,6 +2,8 @@ apply plugin: 'scala'
dependencies {
compile 'org.scala-lang:scala-library:2.11.7'
compile 'com.h2database:h2:1.4.191'
compile 'com.typesafe.slick:slick_2.11:3.1.1'
testCompile 'junit:junit:4.12'
}

View file

@ -6,7 +6,7 @@ import com.nutomic.ensichat.core.body.{ConnectionInfo, MessageBody, UserInfo}
import com.nutomic.ensichat.core.header.ContentHeader
import com.nutomic.ensichat.core.interfaces._
import com.nutomic.ensichat.core.internet.InternetInterface
import com.nutomic.ensichat.core.util.FutureHelper
import com.nutomic.ensichat.core.util.{Database, FutureHelper}
import scala.concurrent.ExecutionContext.Implicits.global
@ -16,7 +16,7 @@ import scala.concurrent.ExecutionContext.Implicits.global
* @param maxInternetConnections Maximum number of concurrent connections that should be opened by
* [[InternetInterface]].
*/
final class ConnectionHandler(settings: SettingsInterface, database: DatabaseInterface,
final class ConnectionHandler(settings: SettingsInterface, database: Database,
callbacks: CallbackInterface, crypto: Crypto,
maxInternetConnections: Int) {
@ -51,6 +51,7 @@ final class ConnectionHandler(settings: SettingsInterface, database: DatabaseInt
def stop(): Unit = {
transmissionInterfaces.foreach(_.destroy())
database.close()
}
/**

View file

@ -8,4 +8,6 @@ trait CallbackInterface {
def onConnectionsChanged(): Unit
def onContactsUpdated(): Unit
}

View file

@ -1,29 +0,0 @@
package com.nutomic.ensichat.core.interfaces
import com.nutomic.ensichat.core.{Address, Message, User}
trait DatabaseInterface {
/**
* Inserts the given new message into the database.
*/
def onMessageReceived(msg: Message): Unit
/**
* Returns all contacts of this user.
*/
def getContacts: Set[User]
/**
* Returns the contact with the given address if it exists.
*/
def getContact(address: Address): Option[User]
/**
* Inserts the given device into contacts.
*/
def addContact(contact: User): Unit
def updateContact(contact: User): Unit
}

View file

@ -0,0 +1,124 @@
package com.nutomic.ensichat.core.util
import java.io.File
import java.util.Date
import com.nutomic.ensichat.core.body.Text
import com.nutomic.ensichat.core.header.ContentHeader
import com.nutomic.ensichat.core.interfaces.{Log, CallbackInterface}
import com.nutomic.ensichat.core.{Address, Message, User}
import slick.driver.H2Driver.api._
import slick.jdbc.meta.MTable
import scala.concurrent.Await
import scala.concurrent.duration.Duration
/**
* Handles persistent data storage.
*
* @param path The database file.
*/
class Database(path: File, callbackInterface: CallbackInterface) {
private val Tag = "Database"
private class Messages(tag: Tag) extends Table[Message](tag, "MESSAGES") {
def id = primaryKey("id", (origin, messageId))
def origin = column[String]("origin")
def target = column[String]("target")
def messageId = column[Long]("message_id")
def text = column[String]("text")
def date = column[Long]("date")
def * = (origin, target, messageId, text, date).<> [Message, (String, String, Long, String, Long)]( { tuple =>
val header = new ContentHeader(new Address(tuple._1),
new Address(tuple._2),
-1,
Text.Type,
Some(tuple._3),
Some(new Date(tuple._5)))
val body = new Text(tuple._4)
new Message(header, body)
}, { message =>
Option((message.header.origin.toString(), message.header.target.toString(),
message.header.messageId.get, message.body.asInstanceOf[Text].text,
message.header.time.get.getTime))
})
}
private val messages = TableQuery[Messages]
private class Contacts(tag: Tag) extends Table[User](tag, "CONTACTS") {
def address = column[String]("address", O.PrimaryKey)
def name = column[String]("name")
def status = column[String]("status")
def wrappedAddress = address.<> [Address, String](new Address(_), a => Option(a.toString()))
def * = (wrappedAddress, name, status) <> (User.tupled, User.unapply)
}
private val contacts = TableQuery[Contacts]
private val db = Database.forURL("jdbc:h2:" + path.getAbsolutePath, driver = "org.h2.Driver")
// Create tables if database doesn't exist.
{
// H2 appends a .mv.db suffix to the path which we can't change, so we have to check that file.
val dbFile = new File(path.getAbsolutePath + ".mv.db")
if (!dbFile.exists()) {
Log.i(Tag, "Database does not exist, creating tables")
Await.result(db.run((messages.schema ++ contacts.schema).create), Duration.Inf)
}
}
def close(): Unit = {
Await.result(db.shutdown, Duration.Inf)
}
/**
* Inserts the given new message into the database.
*/
def onMessageReceived(msg: Message): Unit = {
Await.result(db.run(messages += msg), Duration.Inf)
}
def getMessages(address: Address): Seq[Message] = {
val query = messages.filter { m =>
m.origin === address.toString || m.target === address.toString
}
Await.result(db.run(query.result), Duration.Inf)
}
/**
* Returns all contacts of this user.
*/
def getContacts: Set[User] = {
val f = db.run(contacts.result)
Await.result(f, Duration.Inf).toSet
}
/**
* Returns the contact with the given address if it exists.
*/
def getContact(address: Address): Option[User] = {
val query = contacts.filter { c =>
c.address === address.toString
}
Await.result(db.run(query.result), Duration.Inf).headOption
}
/**
* Inserts the user as a new contact.
*/
def addContact(contact: User): Unit = {
Await.result(db.run(contacts += contact), Duration.Inf)
callbackInterface.onContactsUpdated()
}
/**
* Updates an existing contact.
*/
def updateContact(contact: User): Unit = {
assert(getContact(contact.address).nonEmpty)
Await.result(db.run(contacts.insertOrUpdate(contact)), Duration.Inf)
callbackInterface.onContactsUpdated()
}
}

View file

@ -0,0 +1,105 @@
package com.nutomic.ensichat.core.util
import java.io.File
import java.util.GregorianCalendar
import java.util.concurrent.CountDownLatch
import com.nutomic.ensichat.core.body.Text
import com.nutomic.ensichat.core.header.ContentHeader
import com.nutomic.ensichat.core.interfaces.CallbackInterface
import com.nutomic.ensichat.core.util.DatabaseTest._
import com.nutomic.ensichat.core.{Address, Message, User}
import junit.framework.Assert._
import junit.framework.TestCase
object DatabaseTest {
private val a1 = new Address("A51B74475EE622C3C924DB147668F85E024CA0B44CA146B5E3D3C31A54B34C1E")
private val a2 = new Address("222229685A73AB8F2F853B3EA515633B7CD5A6ABDC3210BC4EF38F955A14AAF6")
private val a3 = new Address("3333359893F8810C4024CFC951374AABA1F4DE6347A3D7D8E44918AD1FF2BA36")
private val a4 = new Address("4444459893F8810C4024CFC951374AABA1F4DE6347A3D7D8E44918AD1FF2BA36")
private val h1 = new ContentHeader(a2, a1, -1, Text.Type, Some(123),
Some(new GregorianCalendar(1970, 1, 1).getTime), 0)
private val h2 = new ContentHeader(a1, a3, -1, Text.Type, Some(8765),
Some(new GregorianCalendar(2014, 6, 10).getTime), 0)
private val h3 = new ContentHeader(a4, a2, -1, Text.Type, Some(77),
Some(new GregorianCalendar(2020, 11, 11).getTime), 0)
private val m1 = new Message(h1, new Text("first"))
private val m2 = new Message(h2, new Text("second"))
private val m3 = new Message(h3, new Text("third"))
private val u1 = new User(a1, "one", "s1")
private val u2 = new User(a2, "two", "s2")
private val u3 = new User(a2, "two-updated", "s2-updated")
}
class DatabaseTest extends TestCase {
private val databaseFile = File.createTempFile("ensichat-test", ".db")
private val latch = new CountDownLatch(1)
private val database = new Database(databaseFile, new CallbackInterface {
override def onConnectionsChanged(): Unit = {}
override def onContactsUpdated(): Unit = {
latch.countDown()
}
override def onMessageReceived(msg: Message): Unit = {}
})
override def tearDown(): Unit = {
super.tearDown()
database.close()
databaseFile.delete()
}
def testMessageSelect(): Unit = {
database.onMessageReceived(m1)
database.onMessageReceived(m2)
database.onMessageReceived(m3)
val msg = database.getMessages(a2)
assertEquals(Seq(m1, m3), msg)
}
def testAddContact(): Unit = {
assertEquals(0, database.getContacts.size)
database.addContact(u1)
val contacts = database.getContacts
assertEquals(1, contacts.size)
assertEquals(Option(u1), database.getContact(u1.address))
}
def testAddContactCallback(): Unit = {
database.addContact(u1)
latch.await()
}
def testGetContact(): Unit = {
assertFalse(database.getContact(u2.address).isDefined)
database.addContact(u2)
val c = database.getContact(u2.address)
assertEquals(u2, c.get)
}
def testUpdateContact(): Unit = {
database.addContact(u2)
database.updateContact(u3)
val c = database.getContact(u2.address)
assertEquals(u3, c.get)
}
def testUpdateNonExistingContact(): Unit = {
try {
database.updateContact(u3)
fail()
} catch {
case _: AssertionError =>
}
}
}

View file

@ -1,23 +0,0 @@
package com.nutomic.ensichat.server
import com.nutomic.ensichat.core.interfaces.DatabaseInterface
import com.nutomic.ensichat.core.{Address, Message, User}
class Database extends DatabaseInterface {
private var contacts = Set[User]()
def onMessageReceived(msg: Message): Unit = {}
def getContacts: Set[User] = contacts
def getContact(address: Address): Option[User] = contacts.find(_.address == address)
def addContact(contact: User): Unit = contacts += contact
def updateContact(contact: User): Unit = {
contacts = contacts.filterNot(_.address == contact.address)
contacts += contact
}
}

View file

@ -7,6 +7,7 @@ import java.util.concurrent.TimeUnit
import com.nutomic.ensichat.core.body.Text
import com.nutomic.ensichat.core.interfaces.SettingsInterface._
import com.nutomic.ensichat.core.interfaces.{CallbackInterface, Log, SettingsInterface}
import com.nutomic.ensichat.core.util.Database
import com.nutomic.ensichat.core.{ConnectionHandler, Crypto, Message}
import scopt.OptionParser
@ -16,6 +17,7 @@ object Main extends App with CallbackInterface {
private val ConfigFolder = Paths.get("").toFile.getAbsoluteFile
private val ConfigFile = new File(ConfigFolder, "config.properties")
private val DatabaseFile = new File(ConfigFolder, "database")
private val KeyFolder = new File(ConfigFolder, "keys")
private val LogInterval = TimeUnit.SECONDS.toMillis(1)
@ -23,8 +25,8 @@ object Main extends App with CallbackInterface {
private lazy val logInstance = new Logging()
private lazy val settings = new Settings(ConfigFile)
private lazy val crypto = new Crypto(settings, KeyFolder)
private lazy val connectionHandler =
new ConnectionHandler(settings, new Database(), this, crypto, 7)
private lazy val database = new Database(DatabaseFile, this)
private lazy val connectionHandler = new ConnectionHandler(settings, database, this, crypto, 7)
init()
@ -85,4 +87,6 @@ object Main extends App with CallbackInterface {
def onConnectionsChanged(): Unit = {}
def onContactsUpdated(): Unit = {}
}