Commit e5c92684 authored by Javier Romero's avatar Javier Romero

WIP: Initial working form and persistence

parent ada71b95
package app.devlife.connect2sql.db
import android.content.Context
import net.sqlcipher.database.SQLiteDatabase
import net.sqlcipher.database.SQLiteOpenHelper
import java.util.ArrayList
import app.devlife.connect2sql.data.LockManager
import app.devlife.connect2sql.db.model.connection.ConnectionInfoSqlModel
import app.devlife.connect2sql.db.model.query.BuiltInQuery
......@@ -15,6 +9,9 @@ import app.devlife.connect2sql.db.model.query.HistoryQuery.HistoryQuerySqlModel
import app.devlife.connect2sql.db.model.query.SavedQuery.SavedQuerySqlModel
import app.devlife.connect2sql.log.EzLogger
import app.devlife.connect2sql.sql.DriverType
import net.sqlcipher.database.SQLiteDatabase
import net.sqlcipher.database.SQLiteOpenHelper
import java.util.ArrayList
class AppDatabaseHelperV3(
context: Context,
......@@ -26,10 +23,10 @@ class AppDatabaseHelperV3(
) : SQLiteOpenHelper(context, AppDatabaseHelperV3.DB_NAME, null, AppDatabaseHelperV3.DB_VERSION) {
private val models = arrayOf(
builtInQuerySqlModel,
connectionInfoSqlModel,
historyQuerySqlModel,
savedQuerySqlModel)
builtInQuerySqlModel,
connectionInfoSqlModel,
historyQuerySqlModel,
savedQuerySqlModel)
val writableDatabase: SQLiteDatabase
@Synchronized @Throws(PassphraseNotEnteredException::class)
......@@ -66,6 +63,11 @@ class AppDatabaseHelperV3(
}
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
models.forEach { model ->
model.upgradeSql(oldVersion, newVersion).forEach { sql ->
db.execSQL(sql)
}
}
}
private fun updateBuiltInQueries(db: SQLiteDatabase) {
......@@ -80,15 +82,17 @@ class AppDatabaseHelperV3(
*/
val builtInQueries = ArrayList<BuiltInQuery>()
builtInQueries.add(BuiltInQuery(0, "Count table rows",
"SELECT COUNT(*) FROM {~table~};", DriverType.MYSQL))
"SELECT COUNT(*) FROM {~table~};", DriverType.MYSQL))
builtInQueries.add(BuiltInQuery(0, "Select all columns except",
"SELECT {~columns~}{~cursor~} FROM {~table~};", DriverType.MYSQL))
"SELECT {~columns~}{~cursor~} FROM {~table~};", DriverType.MYSQL))
builtInQueries.add(BuiltInQuery(0, "Insert a record",
"INSERT INTO {~table~} ({~columns~}) VALUES ({~cursor~});",
DriverType.MYSQL))
"INSERT INTO {~table~} ({~columns~}) VALUES ({~cursor~});",
DriverType.MYSQL))
for (builtinQuery in builtInQueries) {
db.insert(builtInQuerySqlModel.tableName, null, builtInQuerySqlModel.toContentValues(builtinQuery))
db.insert(builtInQuerySqlModel.tableName,
null,
builtInQuerySqlModel.toContentValues(builtinQuery))
}
}
......@@ -101,8 +105,17 @@ class AppDatabaseHelperV3(
/**
* Initial encrypted database
*/
val DB_VERSION_1 = 1
@Suppress("unused")
const val DB_VERSION_1 = 1
private val DB_VERSION = DB_VERSION_1
/**
* SSH config added to connection info
*/
const val DB_VERSION_2 = 1
/**
* Current db version
*/
private const val DB_VERSION = DB_VERSION_2
}
}
package app.devlife.connect2sql.db.model;
import android.content.ContentValues;
import android.database.Cursor;
import net.sqlcipher.database.SQLiteDatabase;
public interface SqlModel<T> {
Class<T> getModelClass();
String getTableName();
String getCreateSql();
T hydrateObject(Cursor cursor) throws IllegalAccessException, InstantiationException;
ContentValues toContentValues(T object);
}
package app.devlife.connect2sql.db.model
import android.content.ContentValues
import android.database.Cursor
import net.sqlcipher.database.SQLiteDatabase
interface SqlModel<T> {
val modelClass: Class<T>
val tableName: String
val createSql: String
fun upgradeSql(oldVersion: Int, newVersion: Int): List<String>
@Throws(IllegalAccessException::class, InstantiationException::class)
fun hydrateObject(cursor: Cursor): T
fun toContentValues(`object`: T): ContentValues
}
......@@ -11,6 +11,7 @@ data class ConnectionInfo(
val username: String,
val password: String?,
val database: String?,
val sshConfig: SshConfig?,
val options: Map<String, String>
) {
......
......@@ -2,19 +2,14 @@ package app.devlife.connect2sql.db.model.connection
import android.content.ContentValues
import android.database.Cursor
import org.json.JSONException
import org.json.JSONObject
import java.util.HashMap
import app.devlife.connect2sql.db.AppDatabaseHelperV3
import app.devlife.connect2sql.db.model.SqlModel
import app.devlife.connect2sql.log.EzLogger
import app.devlife.connect2sql.sql.DriverType
import org.json.JSONException
import org.json.JSONObject
import java.util.HashMap
/**
*/
class ConnectionInfoSqlModel : SqlModel<ConnectionInfo> {
object Column {
......@@ -27,29 +22,55 @@ class ConnectionInfoSqlModel : SqlModel<ConnectionInfo> {
const val PASSWORD = "password"
const val DATABASE = "database"
const val OPTIONS = "options"
}
override fun getModelClass(): Class<ConnectionInfo> {
return ConnectionInfo::class.java
const val SSH_HOST = "ssh_host"
const val SSH_PORT = "ssh_port"
const val SSH_USERNAME = "ssh_username"
const val SSH_PASSWORD = "ssh_password"
const val SSH_PRIVATE_KEY = "ssh_private_key"
}
override fun getTableName(): String {
return TABLE_NAME
}
override fun getCreateSql(): String {
return "CREATE TABLE " +
"IF NOT EXISTS '" + TABLE_NAME + "' (" +
"'" + Column.ID + "' integer NOT NULL," +
"'" + Column.NAME + "' text NOT NULL," +
"'" + Column.DRIVER + "' text NOT NULL," +
"'" + Column.HOST + "' text NOT NULL," +
"'" + Column.PORT + "' integer NOT NULL," +
"'" + Column.USERNAME + "' text NOT NULL," +
"'" + Column.PASSWORD + "' text NOT NULL," +
"'" + Column.DATABASE + "' text," +
"'" + Column.OPTIONS + "' text NOT NULL DEFAULT '{}'," +
"PRIMARY KEY('" + Column.ID + "'))"
override val modelClass: Class<ConnectionInfo>
get() = ConnectionInfo::class.java
override val tableName: String
get() = TABLE_NAME
override val createSql: String
get() =
"""
CREATE TABLE IF NOT EXISTS '$TABLE_NAME' (
'${Column.ID}' integer NOT NULL,
'${Column.NAME}' text NOT NULL,
'${Column.DRIVER}' text NOT NULL,
'${Column.HOST}' text NOT NULL,
'${Column.PORT}' integer NOT NULL,
'${Column.USERNAME}' text NOT NULL,
'${Column.PASSWORD}' text NOT NULL,
'${Column.DATABASE}' text,
'${Column.SSH_HOST}' text,
'${Column.SSH_PORT}' integer,
'${Column.SSH_USERNAME}' text,
'${Column.SSH_PASSWORD}' text,
'${Column.SSH_PRIVATE_KEY}' text,
'${Column.OPTIONS}' text NOT NULL DEFAULT '{}',
PRIMARY KEY('${Column.ID}')
)
""".trimIndent()
override fun upgradeSql(oldVersion: Int, newVersion: Int): List<String> {
return when (newVersion) {
AppDatabaseHelperV3.DB_VERSION_2 -> listOf(
"ALTER TABLE '$TABLE_NAME' ADD COLUMN '${Column.SSH_HOST}' text",
"ALTER TABLE '$TABLE_NAME' ADD COLUMN '${Column.SSH_PORT}' integer",
"ALTER TABLE '$TABLE_NAME' ADD COLUMN '${Column.SSH_USERNAME}' text",
"ALTER TABLE '$TABLE_NAME' ADD COLUMN '${Column.SSH_PASSWORD}' text",
"ALTER TABLE '$TABLE_NAME' ADD COLUMN '${Column.SSH_PRIVATE_KEY}' text"
)
else -> listOf()
}
}
override fun hydrateObject(cursor: Cursor): ConnectionInfo {
......@@ -67,16 +88,35 @@ class ConnectionInfoSqlModel : SqlModel<ConnectionInfo> {
EzLogger.e(e.message)
}
val sshConfig: SshConfig? = cursor.getString(Column.SSH_HOST)?.let { host ->
cursor.getInt(Column.SSH_PORT)?.let { port ->
cursor.getString(Column.SSH_USERNAME)?.let { username ->
val password = cursor.getString(Column.SSH_PASSWORD)
val privateKeyContents = cursor.getString(Column.SSH_PRIVATE_KEY)
when {
privateKeyContents != null ->
SshConfig(Address(host, port), PrivateKey(username, privateKeyContents))
password != null ->
SshConfig(Address(host, port), BasicAuth(username, password))
else ->
SshConfig(Address(host, port), None(username))
}
}
}
}
return ConnectionInfo(
cursor.getLong(cursor.getColumnIndex(Column.ID)),
cursor.getString(cursor.getColumnIndex(Column.NAME)),
DriverType.valueOf(cursor.getString(cursor.getColumnIndex(Column.DRIVER))),
cursor.getString(cursor.getColumnIndex(Column.HOST)),
cursor.getInt(cursor.getColumnIndex(Column.PORT)),
cursor.getString(cursor.getColumnIndex(Column.USERNAME)),
cursor.getString(cursor.getColumnIndex(Column.PASSWORD)),
cursor.getString(cursor.getColumnIndex(Column.DATABASE)),
options)
cursor.getLong(cursor.getColumnIndex(Column.ID)),
cursor.getString(cursor.getColumnIndex(Column.NAME)),
DriverType.valueOf(cursor.getString(cursor.getColumnIndex(Column.DRIVER))),
cursor.getString(cursor.getColumnIndex(Column.HOST)),
cursor.getInt(cursor.getColumnIndex(Column.PORT)),
cursor.getString(cursor.getColumnIndex(Column.USERNAME)),
cursor.getString(cursor.getColumnIndex(Column.PASSWORD)),
cursor.getString(cursor.getColumnIndex(Column.DATABASE)),
sshConfig,
options)
}
override fun toContentValues(`object`: ConnectionInfo): ContentValues {
......@@ -92,6 +132,27 @@ class ConnectionInfoSqlModel : SqlModel<ConnectionInfo> {
cv.put(Column.PASSWORD, `object`.password)
cv.put(Column.DATABASE, `object`.database)
`object`.sshConfig?.apply {
cv.put(Column.SSH_HOST, this.address.host)
cv.put(Column.SSH_PORT, this.address.port)
when (this.authentication) {
is BasicAuth -> {
cv.put(Column.SSH_USERNAME, this.authentication.username)
cv.put(Column.SSH_PASSWORD, this.authentication.password)
}
is PrivateKey -> {
cv.put(Column.SSH_USERNAME, this.authentication.username)
cv.put(Column.SSH_PRIVATE_KEY, this.authentication.privateKeyContents)
}
is None -> {
cv.put(Column.SSH_USERNAME, this.authentication.username)
}
else -> throw UnsupportedOperationException(
"Authentication type ${this.authentication.javaClass} not supported."
)
}
}
val jsonOptions = JSONObject()
for (entry in `object`.options.entries) {
try {
......@@ -108,4 +169,22 @@ class ConnectionInfoSqlModel : SqlModel<ConnectionInfo> {
companion object {
const val TABLE_NAME = "connections"
}
fun Cursor.getString(columnName: String): String? {
val colIndex = this.getColumnIndex(columnName)
return when {
this.isNull(colIndex) -> null
else -> this.getString(colIndex)
}
}
fun Cursor.getInt(columnName: String): Int? {
val colIndex = this.getColumnIndex(columnName)
return when {
this.isNull(colIndex) -> null
else -> this.getInt(colIndex)
}
}
}
......@@ -4,5 +4,7 @@ data class Address(val host: String, val port: Int)
data class SshConfig(val address: Address, val authentication: Authentication)
data class SshProxy(val proxy: SshConfig, val serviceAddress: Address)
interface Authentication
data class BasicAuth(val username: String, val password: String) : Authentication
\ No newline at end of file
sealed class Authentication
data class None(val username: String) : Authentication()
data class BasicAuth(val username: String, val password: String?) : Authentication()
data class PrivateKey(val username: String, val privateKeyContents: String): Authentication()
\ No newline at end of file
......@@ -3,6 +3,12 @@ package app.devlife.connect2sql.db.model.query;
import app.devlife.connect2sql.db.model.SqlModel;
import android.content.ContentValues;
import android.database.Cursor;
import android.support.annotation.NonNull;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List;
public abstract class BaseQuery {
......@@ -36,13 +42,14 @@ public abstract class BaseQuery {
public static abstract class BaseQuerySqlModel<T extends BaseQuery> implements SqlModel<T> {
@Override
public T hydrateObject(Cursor cursor) throws IllegalAccessException, InstantiationException {
public T hydrateObject(@NonNull Cursor cursor) throws IllegalAccessException, InstantiationException {
final T t = getModelClass().newInstance();
t.setId(cursor.getInt(cursor.getColumnIndex(Column.ID)));
t.setQuery(cursor.getString(cursor.getColumnIndex(Column.QUERY)));
return t;
}
@NonNull
@Override
public ContentValues toContentValues(T object) {
ContentValues cv = new ContentValues();
......@@ -52,5 +59,11 @@ public abstract class BaseQuery {
cv.put(Column.QUERY, object.getQuery());
return cv;
}
@NotNull
@Override
public List<String> upgradeSql(int oldVersion, int newVersion) {
return new ArrayList<>();
}
}
}
......@@ -112,7 +112,7 @@ class ConnectionInfoEditorActivity : BaseActivity() {
/**
* Save connection
*/
val connectionInfo = form.generateConnectionInfo()
val connectionInfo = form.compileConnectionInfo()
when (request.action) {
ConnectionInfoEditorRequest.Action.EDIT -> {
val id = connectionInfoRepository.save(connectionInfo.copy(id = request.connectionInfoId))
......@@ -127,7 +127,7 @@ class ConnectionInfoEditorActivity : BaseActivity() {
private fun testConnection(): Boolean {
val connectionInfo = form.generateConnectionInfo()
val connectionInfo = form.compileConnectionInfo()
/**
* Ask for password if empty
......@@ -142,10 +142,9 @@ class ConnectionInfoEditorActivity : BaseActivity() {
passwordText.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD
val alertBuilder = AlertDialog.Builder(this)
alertBuilder.setTitle("Password?")
alertBuilder.setTitle(R.string.dialog_password)
alertBuilder.setView(promptDialogView)
alertBuilder.setPositiveButton("OK") { dialog, which ->
// execute testing of connection
alertBuilder.setPositiveButton(R.string.dialog_ok_button) { dialog, which ->
executeTestConnection(connectionInfo.copy(password = passwordText.text.toString()))
}
alertBuilder.create().show()
......@@ -275,14 +274,12 @@ class ConnectionInfoEditorActivity : BaseActivity() {
private val onTestButtonClickListener = View.OnClickListener {
doOnValidationSuccess = ValidationAction.TEST
form.validator.validationListener = validationListener
form.validator.validate()
form.validate(validationListener)
}
private val onSaveButtonClickListener = View.OnClickListener {
doOnValidationSuccess = ValidationAction.SAVE
form.validator.validationListener = validationListener
form.validator.validate()
form.validate(validationListener)
}
private fun toggleAlphaToNumeric(editText: EditText, strict: Boolean, signed: Boolean) {
......@@ -317,7 +314,7 @@ class ConnectionInfoEditorActivity : BaseActivity() {
companion object {
private val EXTRA_CONNECTION_INFO_REQUEST = "EXTRA_CONNECTION_INFO_REQUEST"
private const val EXTRA_CONNECTION_INFO_REQUEST = "EXTRA_CONNECTION_INFO_REQUEST"
fun newIntent(context: Context, request: ConnectionInfoEditorRequest): Intent {
val intent = Intent(context, ConnectionInfoEditorActivity::class.java)
......
......@@ -16,13 +16,12 @@ abstract class BaseMsSqlForm(context: Context, view: View) : BaseForm(context, v
val instanceEditTextView: EditText
init {
domainEditTextView = view.findViewById(R.id.form_txt_domain) as EditText
instanceEditTextView = view.findViewById(R.id.form_txt_instance) as EditText
}
override fun generateConnectionInfo(): ConnectionInfo {
val connectionInfo = super.generateConnectionInfo()
override fun compileConnectionInfo(): ConnectionInfo {
val connectionInfo = super.compileConnectionInfo()
val options = connectionInfo.options + hashMapOf(
Pair(ConnectionInfo.OPTION_DOMAIN, domainEditTextView.text.toString()),
......
......@@ -31,7 +31,7 @@ public class FormFactory {
public static class FormConfigNotFound extends Exception {
public FormConfigNotFound(DriverType driverType) {
FormConfigNotFound(DriverType driverType) {
super("No FormConfig for driver " + driverType.name() + " found!");
}
}
......
......@@ -11,7 +11,7 @@ import app.devlife.connect2sql.sql.DriverType
*/
class MsSqlForm(context: Context, view: View) : BaseMsSqlForm(context, view) {
override fun generateConnectionInfo(): ConnectionInfo {
return super.generateConnectionInfo().copy(driverType = DriverType.MSSQL)
override fun compileConnectionInfo(): ConnectionInfo {
return super.compileConnectionInfo().copy(driverType = DriverType.MSSQL)
}
}
......@@ -11,7 +11,7 @@ import app.devlife.connect2sql.sql.DriverType
*/
class MySqlForm(context: Context, view: View) : BaseForm(context, view) {
override fun generateConnectionInfo(): ConnectionInfo {
return super.generateConnectionInfo().copy(driverType = DriverType.MYSQL)
override fun compileConnectionInfo(): ConnectionInfo {
return super.compileConnectionInfo().copy(driverType = DriverType.MYSQL)
}
}
......@@ -11,31 +11,20 @@ import com.mobsandgeeks.saripaar.Rule
import app.devlife.connect2sql.db.model.connection.ConnectionInfo
import com.gitlab.connect2sql.R
import app.devlife.connect2sql.sql.DriverType
import com.mobsandgeeks.saripaar.Validator
/**
* Created by javier.romero on 5/3/14.
*/
class PostgresForm(context: Context, view: View) : BaseForm(context, view) {
private val mUseSslSwitch: Switch
private val mTrustCertSwitch: Switch
init {
mUseSslSwitch = view.findViewById(R.id.form_swtch_use_ssl) as Switch
mTrustCertSwitch = view.findViewById(R.id.form_swtch_trust_cert) as Switch
// add rule to make database required
val errorMessage = context.resources.getString(R.string.form_error_database_required)
validator.put(databaseEditTextView, object : Rule<EditText>(errorMessage) {
override fun isValid(databaseEditTextView: EditText): Boolean {
return !TextUtils.isEmpty(databaseEditTextView.text)
}
})
}
override fun generateConnectionInfo(): ConnectionInfo {
val connectionInfo = super.generateConnectionInfo()
override fun compileConnectionInfo(): ConnectionInfo {
val connectionInfo = super.compileConnectionInfo()
val options = connectionInfo.options + hashMapOf(
Pair(ConnectionInfo.OPTION_USE_SSL, mUseSslSwitch.isChecked.toString()),
......@@ -59,4 +48,15 @@ class PostgresForm(context: Context, view: View) : BaseForm(context, view) {
else -> super.getHelpMessageResource(view)
}
}
override fun onPreValidate(validator: Validator) {
super.onPreValidate(validator)
val errorMessage = context.resources.getString(R.string.form_error_database_required)
validator.put(databaseEditTextView, object : Rule<EditText>(errorMessage) {
override fun isValid(databaseEditTextView: EditText): Boolean {
return !TextUtils.isEmpty(databaseEditTextView.text)
}
})
}
}
\ No newline at end of file
......@@ -7,7 +7,7 @@ import app.devlife.connect2sql.db.model.connection.ConnectionInfo
import app.devlife.connect2sql.sql.DriverType
class SybaseForm(context: Context, view: View) : BaseMsSqlForm(context, view) {
override fun generateConnectionInfo(): ConnectionInfo {
return super.generateConnectionInfo().copy(driverType = DriverType.SYBASE)
override fun compileConnectionInfo(): ConnectionInfo {
return super.compileConnectionInfo().copy(driverType = DriverType.SYBASE)
}
}
......@@ -97,7 +97,7 @@ class SavedQueriesActivity : BaseActivity() {
savedQueriesAdapter.titleOnly = userPreferences.read(OPTION_SAVED_QUERY_NAMES_ONLY, false)
savedQueriesAdapter.notifyDataSetChanged()
userPreferences.registerListener<Boolean>(OPTION_SAVED_QUERY_NAMES_ONLY) { key, value ->
userPreferences.registerListener<Boolean>(OPTION_SAVED_QUERY_NAMES_ONLY) { _, value ->
savedQueriesAdapter.titleOnly = value == true
savedQueriesAdapter.notifyDataSetChanged()
}
......@@ -175,7 +175,7 @@ class SavedQueriesActivity : BaseActivity() {
val builder = AlertDialog.Builder(this)
builder.setTitle("Delete: " + query.name)
builder.setMessage("Are you sure you want to delete this query?")
builder.setPositiveButton("Yes") { dialog, which ->
builder.setPositiveButton("Yes") { dialog, _ ->
if (savedQueryRepository.deleteSavedQuery(this@SavedQueriesActivity, query.id)) {
Toast.makeText(this@SavedQueriesActivity, "Query Deleted!", Toast.LENGTH_SHORT).show()
savedQueriesAdapter.removeSavedQuery(query as SavedQuery)
......@@ -193,7 +193,7 @@ class SavedQueriesActivity : BaseActivity() {
}
}
private val mOnChildClickListener = ExpandableListView.OnChildClickListener { parent, v, groupPosition, childPosition, id ->
private val mOnChildClickListener = ExpandableListView.OnChildClickListener { _, _, groupPosition, childPosition, _ ->
val queryToLoad = (savedQueriesAdapter.getChild(groupPosition, childPosition) as BaseNamedQuery).query
onSavedQueryClick(queryToLoad)
true
......
package app.devlife.connect2sql.util.ext
import android.widget.EditText
val EditText.stringValue: String
get() = this.text.toString()
val EditText.nonBlankStringValue: String?
get() {
val value = this.text?.toString()
return if (value.isNullOrBlank()) null else value
}
\ No newline at end of file
package app.devlife.connect2sql.util.ext
import android.widget.Spinner
fun Spinner.setSelectionByResource(resourceId: Int) {
val value = context.getString(resourceId)
val position = (0 until this.adapter.count).firstOrNull { position ->
this.adapter.getItem(position).toString() == value
}
position?.also { this.setSelection(it) }
}
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">