/*
* Tweetings - Twitter client for Android
*
* Copyright (C) 2012-2013 RBD Solutions Limited <apps@tweetings.net>
* Copyright (C) 2012 Mariotaku Lee <mariotaku.lee@gmail.com>
*
* This program 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.
*
* This program 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 com.dwdesign.tweetings.util;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Set;
import android.content.ContentValues;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.provider.BaseColumns;
public final class DatabaseUpgradeHelper {
private static final int FIELD_TYPE_NULL = 0;
private static final int FIELD_TYPE_INTEGER = 1;
private static final int FIELD_TYPE_FLOAT = 2;
private static final int FIELD_TYPE_STRING = 3;
private static final int FIELD_TYPE_BLOB = 4;
public static void safeUpgrade(final SQLiteDatabase db, final String table, final String[] new_cols, final String[] new_types,
final boolean fast_upgrade, final boolean drop_directly) {
if (new_cols == null || new_types == null || new_cols.length != new_types.length)
throw new IllegalArgumentException("Invalid parameters, length of columns and types not match.");
final List<ContentValues> values_list = new ArrayList<ContentValues>();
// First, create the table if not exists.
db.execSQL(createTable(table, new_cols, new_types, true));
// We need to get all data from old table.
final Cursor cur = db.query(table, null, null, null, null, null, null);
cur.moveToFirst();
final String[] old_cols = cur.getColumnNames();
if (fast_upgrade) {
final String[] old_types = getBatchTypeString(db, table, old_cols);
if (!shouldUpgrade(old_cols, old_types, new_cols, new_types)) {
if (cur != null) {
cur.close();
}
return;
}
}
// If drop_directly set to true, we will not backup any data actually.
if (!drop_directly) {
while (!cur.isAfterLast()) {
final ContentValues values = new ContentValues();
final int length = new_cols.length;
for (int i = 0; i < length; i++) {
final String new_col = new_cols[i];
final String new_type = new_types[i];
if (BaseColumns._ID.equals(new_col)) {
continue;
}
final int idx = cur.getColumnIndex(new_col);
if (ArrayUtils.contains(old_cols, new_col)) {
final String old_type = getTypeString(db, table, new_col);
final boolean compatible = isTypeCompatible(old_type, new_type, false);
if (compatible && idx > -1) {
switch (getTypeInt(new_type)) {
case FIELD_TYPE_INTEGER:
values.put(new_col, cur.getLong(idx));
break;
case FIELD_TYPE_FLOAT:
values.put(new_col, cur.getFloat(idx));
break;
case FIELD_TYPE_STRING:
values.put(new_col, cur.getString(idx));
break;
case FIELD_TYPE_BLOB:
values.put(new_col, cur.getBlob(idx));
break;
case FIELD_TYPE_NULL:
default:
break;
}
}
}
}
values_list.add(values);
cur.moveToNext();
}
}
cur.close();
// OK, now we got all data can be moved from old table, so we will
// delete the old table and create a new one.
db.execSQL("DROP TABLE IF EXISTS " + table);
db.execSQL(createTable(table, new_cols, new_types, false));
// Now, insert all data backuped into new table.
db.beginTransaction();
for (final ContentValues values : values_list) {
db.insert(table, null, values);
}
db.setTransactionSuccessful();
db.endTransaction();
}
private static String createTable(final String tableName, final String[] columns, final String[] types, final boolean create_if_not_exists) {
if (tableName == null || columns == null || types == null || types.length != columns.length
|| types.length == 0)
throw new IllegalArgumentException("Invalid parameters for creating table " + tableName);
final StringBuilder stringBuilder = new StringBuilder(create_if_not_exists ? "CREATE TABLE IF NOT EXISTS "
: "CREATE TABLE ");
stringBuilder.append(tableName);
stringBuilder.append(" (");
final int length = columns.length;
for (int i = 0; i < length; i++) {
if (i > 0) {
stringBuilder.append(", ");
}
stringBuilder.append(columns[i]).append(' ').append(types[i]);
}
return stringBuilder.append(");").toString();
}
private static String[] getBatchTypeString(final SQLiteDatabase db, final String table, final String[] columns) {
if (columns == null || columns.length == 0) return new String[0];
final String[] types = new String[columns.length];
final StringBuilder builder = new StringBuilder();
builder.append("SELECT ");
final int columns_length = columns.length;
for (int i = 0, len = columns_length; i < len; i++) {
builder.append("typeof(" + columns[i] + ")");
if (i != columns.length - 1) {
builder.append(", ");
}
}
builder.append(" FROM " + table);
final Cursor cur = db.rawQuery(builder.toString(), null);
if (cur == null) return null;
if (cur.getCount() > 0) {
cur.moveToFirst();
final int types_length = types.length;
for (int i = 0; i < types_length; i++) {
types[i] = cur.getString(i);
}
} else {
Arrays.fill(types, "NULL");
}
cur.close();
return types;
}
private static int getTypeInt(final String type) {
final int idx = type.contains("(") ? type.indexOf("(") : type.indexOf(" ");
final String type_main = idx > -1 ? type.substring(0, idx) : type;
if ("NULL".equalsIgnoreCase(type_main))
return FIELD_TYPE_NULL;
else if ("INTEGER".equalsIgnoreCase(type_main))
return FIELD_TYPE_INTEGER;
else if ("FLOAT".equalsIgnoreCase(type_main))
return FIELD_TYPE_FLOAT;
else if ("TEXT".equalsIgnoreCase(type_main))
return FIELD_TYPE_STRING;
else if ("BLOB".equalsIgnoreCase(type_main)) return FIELD_TYPE_BLOB;
throw new IllegalStateException("Unknown field type " + type + " !");
}
private static String getTypeString(final SQLiteDatabase db, final String table, final String column) {
final String sql = "SELECT typeof(" + column + ") FROM " + table;
final Cursor cur = db.rawQuery(sql, null);
if (cur == null) return null;
cur.moveToFirst();
final String type = cur.getString(0);
cur.close();
return type;
}
private static boolean isTypeCompatible(final String old_type, final String new_type, final boolean treat_null_as_compatible) {
if (old_type != null && new_type != null) {
final int old_idx = old_type.contains("(") ? old_type.indexOf("(") : old_type.indexOf(" ");
final int new_idx = new_type.contains("(") ? new_type.indexOf("(") : new_type.indexOf(" ");
final String old_type_main = old_idx > -1 ? old_type.substring(0, old_idx) : old_type;
final String new_type_main = new_idx > -1 ? new_type.substring(0, new_idx) : new_type;
if (treat_null_as_compatible)
return "NULL".equalsIgnoreCase(old_type_main) || "NULL".equalsIgnoreCase(new_type_main)
|| old_type_main.equalsIgnoreCase(new_type_main);
return old_type_main.equalsIgnoreCase(new_type_main);
}
return false;
}
private static boolean shouldUpgrade(final String[] old_cols, final String[] old_types, final String[] new_cols, final String[] new_types) {
if (old_cols == null || old_types == null || new_cols == null || new_types == null)
throw new IllegalArgumentException("All arguments cannot be null!");
if (old_cols.length != old_types.length || new_cols.length != new_types.length)
throw new IllegalArgumentException("Length of columns and types not match!");
if (old_cols.length != new_cols.length) return true;
if (!ArrayUtils.contentMatch(old_cols, new_cols)) return true;
final HashMap<String, String> old_map = new HashMap<String, String>(), new_map = new HashMap<String, String>();
// I'm sure the length of four arrays are equal.
final int length = old_cols.length;
for (int i = 0; i < length; i++) {
old_map.put(old_cols[i], old_types[i]);
new_map.put(new_cols[i], new_types[i]);
}
final Set<String> old_keyset = old_map.keySet();
for (final String col_name : old_keyset) {
if (!isTypeCompatible(old_map.get(col_name), new_map.get(col_name), true)) return true;
}
return false;
}
}