/*******************************************************************************
* Copyright (c) 2010 Denis Solonenko.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Public License v2.0
* which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
*
* Contributors:
* Denis Solonenko - initial API and implementation
******************************************************************************/
package ru.orangesoftware.financisto2.db;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.Scanner;
import android.content.ContentValues;
import android.content.Context;
import android.content.res.AssetManager;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import android.database.sqlite.SQLiteOpenHelper;
import android.util.Log;
/**
* Schema evolution helper.
* Put sql files into assets/database directory as following:
* - All create scripts into /create directory,
* - All view scripts into /view directory,
* - All alter scripts in /alter directory.
* The algorithm is as follows:
* On initial database create (when SQLiteOpenHelper.onCreate invoked),
* the helper executes scripts in the following order: /create, /alter, /view.
* On every database upgrade, the helper executes scripts from /alter which
* haven't been yet executed, then all scripts from /view.
*
* @author Denis Solonenko
*/
public class DatabaseSchemaEvolution extends SQLiteOpenHelper {
private static final String TAG = "DatabaseSchemaEvolution";
private static final String ALTERLOG = "alterlog";
private static final String DATABASE_PATH = "database";
private static final String CREATE_PATH = DATABASE_PATH + "/create";
private static final String VIEW_PATH = DATABASE_PATH + "/view";
private static final String ALTER_PATH = DATABASE_PATH + "/alter";
private final AssetManager assetManager;
private boolean autoDropViews = false;
public DatabaseSchemaEvolution(Context context, String name, SQLiteDatabase.CursorFactory factory, int version) {
super(context, name, factory, version);
this.assetManager = context.getAssets();
}
public boolean isAutoDropViews() {
return autoDropViews;
}
public void setAutoDropViews(boolean autoDropViews) {
this.autoDropViews = autoDropViews;
}
@Override
public void onCreate(SQLiteDatabase db) {
try {
Log.i(TAG, "Creating ALTERLOG table");
db.execSQL("create table "+ALTERLOG+" (script text not null, datetime long not null);");
db.execSQL("create index "+ALTERLOG+"_script_idx on "+ALTERLOG+" (script);");
Log.i(TAG, "Running create scripts...");
runAllScripts(db, CREATE_PATH, false);
Log.i(TAG, "Running alter scripts...");
runAllScripts(db, ALTER_PATH, true);
Log.i(TAG, "Running create view scripts...");
runAllScripts(db, VIEW_PATH, false);
} catch (Exception ex) {
throw new RuntimeException("Failed to create database", ex);
}
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
try {
Log.i(TAG, "Upgrading database from version "+oldVersion+" to version "+newVersion+"...");
Log.i(TAG, "Running alter scripts...");
runAllScripts(db, ALTER_PATH, true);
Log.i(TAG, "Running create view scripts...");
runAllScripts(db, VIEW_PATH, false);
} catch (Exception ex) {
throw new RuntimeException("Failed to upgrade database", ex);
}
}
public void runAlterScript(SQLiteDatabase db, String name)
throws IOException {
runAlterScript(db, ALTER_PATH, name);
}
private void runAlterScript(SQLiteDatabase db, String path, String name)
throws IOException {
String script = path + "/" + name;
runScript(db, script);
}
private void runAllScripts(SQLiteDatabase db, String path, boolean checkAlterlog)
throws IOException {
String[] scripts = sortScripts(assetManager.list(path));
for (String scriptFile : scripts) {
String script = path + "/" + scriptFile;
if (checkAlterlog) {
if (alreadyRun(db, script)) {
Log.d("DatabaseSchema", "Skipping " + script);
continue;
}
}
if (autoDropViews && VIEW_PATH.equals(path)) {
String viewName = getViewNameFromScriptName(scriptFile);
db.execSQL("DROP VIEW IF EXISTS "+viewName);
}
Log.i("DatabaseSchema", "Running " + script);
runScript(db, script);
if (checkAlterlog) {
saveScriptToAlterlog(db, script);
}
}
}
private void runScript(SQLiteDatabase db, String script) throws IOException {
String[] content = readFile(script).split(";");
for (String s : content) {
String sql = s.trim();
if (sql.length() > 1) {
try {
db.execSQL(sql);
} catch (SQLiteException ex) {
Log.e("DatabaseSchema", "Unable to run sql: "+sql, ex);
throw ex;
}
}
}
}
/**
* Sorts array of scripts' names
* @param scripts scripts list
* @return scripts array sorted with natural order
*/
protected String[] sortScripts(String[] scripts) {
Arrays.sort(scripts);
return scripts;
}
protected String getViewNameFromScriptName(String scriptFileName) {
int i = scriptFileName.indexOf('.');
return i == -1 ? scriptFileName : scriptFileName.substring(0, i);
}
private static final String[] projection = {"1"};
private boolean alreadyRun(SQLiteDatabase db, String script) {
Cursor c = db.query(ALTERLOG, projection, "script=?", new String[]{script}, null, null, null);
try {
return c.moveToFirst();
} finally {
c.close();
}
}
private void saveScriptToAlterlog(SQLiteDatabase db, String script) {
ContentValues values = new ContentValues();
values.put("script", script);
values.put("datetime", System.currentTimeMillis());
db.insert(ALTERLOG, null, values);
}
private String readFile(String scriptFile) throws IOException {
StringBuilder sb = new StringBuilder();
InputStream is = assetManager.open(scriptFile);
Scanner scanner = new Scanner(is);
try {
while (scanner.hasNextLine()) {
sb.append(scanner.nextLine().trim()).append(" ");
}
} finally {
scanner.close();
is.close();
}
return sb.toString().trim();
}
}