package com.WhiteRabbit.Codegen;
...
public class PatchGenerator {
private Context ctx;
private boolean isDataHeaderCreated = false;
private static final String[] VERSION_PROJECTION = new String[] {"id"};
private static final String[] TABLE_PROJECTION = new String[] {"id", "name", "alias"};
private static final String[] COLUMN_PROJECTION = new String[] {"id", "name", "alias", "type", "not_null"};
public PatchGenerator(Context ctx) {
this.ctx = ctx;
}
public void generate(OutputStreamWriter osw) throws Exception {
isDataHeaderCreated = false;
closeVersions();
genStage_1(osw);
genPatchList(osw);
genStage_2(osw);
genPatches(osw);
genStage_3(osw);
osw.flush();
}
private void genDataTable(OutputStreamWriter osw, long tableId, String tableAlias, Integer version) throws Exception {
Map<String, Column> columnList = new HashMap<String, Column>();
Uri uri = Db.Column.CONTENT_URI;
Cursor q = ctx.getContentResolver().query(
uri,
COLUMN_PROJECTION,
Db.Column.TABLE_ID_COLUMN + " = ?",
new String [] {Long.toString(tableId)},
null
);
for (q.moveToFirst();!q.isAfterLast();q.moveToNext()) {
int nameColumn = q.getColumnIndex(COLUMN_PROJECTION[1]);
int aliasColumn = q.getColumnIndex(COLUMN_PROJECTION[2]);
int typeColumn = q.getColumnIndex(COLUMN_PROJECTION[3]);
columnList.put(q.getString(nameColumn), new Column(q.getString(aliasColumn), q.getInt(typeColumn)));
}
uri = ContentUris.withAppendedId(Db.DATA_CONTENT_URI, tableId);
String[] DATA_PROJECTION = new String[columnList.size() + 1];
int ix = 0;
DATA_PROJECTION[ix++] = Db.ID_COLUMN_NAME;
for (String column: columnList.keySet()) {
DATA_PROJECTION[ix++] = column;
}
q = ctx.getContentResolver().query(
uri,
DATA_PROJECTION,
Db.VERSION_COLUMN_NAME + " = ?",
new String [] {version.toString()},
Db.ID_COLUMN_NAME
);
for (q.moveToFirst();!q.isAfterLast();q.moveToNext()) {
if (!isDataHeaderCreated) {
osw.write("\n ContentValues values;\n\n");
isDataHeaderCreated = true;
}
boolean isOnce = false;
for (String columnName: columnList.keySet()) {
Column c = columnList.get(columnName);
if (c == null) continue;
int col = q.getColumnIndex(columnName);
switch (c.getType()) {
case Db.DataType.INTEGER:
{
Long v = q.getLong(col);
if (v != null) {
if (!isOnce) {
osw.write(" values = new ContentValues();\n");
isOnce = true;
}
osw.write(" values.put(");
osw.write(c.getAlias());
osw.write(", ");
osw.write(v.toString());
osw.write(");\n");
}
}
break;
case Db.DataType.TEXT:
{
String v = q.getString(col);
if (v != null) {
if (!isOnce) {
osw.write(" values = new ContentValues();\n");
isOnce = true;
}
osw.write(" values.put(");
osw.write(c.getAlias());
osw.write(", ");
osw.write("\"");
osw.write(v.toString());
osw.write("\"");
osw.write(");\n");
}
}
break;
}
}
if (isOnce) {
osw.write(" addData(db, ");
osw.write(tableAlias);
osw.write(", values, ");
osw.write(version.toString());
osw.write(");\n\n");
}
}
}
private void genData(OutputStreamWriter osw, Integer version) throws Exception {
Uri uri = Db.Table.CONTENT_URI;
Cursor q = ctx.getContentResolver().query(
uri,
TABLE_PROJECTION,
null,
null,
TABLE_PROJECTION[0]
);
for (q.moveToFirst();!q.isAfterLast();q.moveToNext()) {
int idColumn = q.getColumnIndex(TABLE_PROJECTION[0]);
int aliasColumn = q.getColumnIndex(TABLE_PROJECTION[2]);
genDataTable(osw, q.getLong(idColumn), q.getString(aliasColumn), version);
}
}
private void genColumns(OutputStreamWriter osw, Integer version, Integer tableId) throws Exception {
Uri uri = Db.Column.CONTENT_URI;
Cursor q = ctx.getContentResolver().query(
uri,
COLUMN_PROJECTION,
Db.Column.TABLE_ID_COLUMN + " = ? and " + Db.VERSION_COLUMN_NAME + " = ?",
new String [] {tableId.toString(), version.toString()},
null
);
for (q.moveToFirst();!q.isAfterLast();q.moveToNext()) {
int aliasColumn = q.getColumnIndex(COLUMN_PROJECTION[2]);
int typeColumn = q.getColumnIndex(COLUMN_PROJECTION[3]);
int isNullColumn = q.getColumnIndex(COLUMN_PROJECTION[4]);
String alias = q.getString(aliasColumn);
int type = q.getInt(typeColumn);
int isNull = q.getInt(isNullColumn);
if (isNull > 0) {
osw.write(" addColumnNotNull(db, tableId, ");
} else {
osw.write(" addColumn(db, tableId, ");
}
osw.write(getColumnType(type));
osw.write(", ");
osw.write(alias);
osw.write(", \"");
osw.write(alias);
osw.write("\", ");
osw.write(version.toString());
osw.write(");\n");
}
}
private void genTables(OutputStreamWriter osw, Integer version) throws Exception {
Uri uri = Db.Table.CONTENT_URI;
Cursor q = ctx.getContentResolver().query(
uri,
TABLE_PROJECTION,
Db.VERSION_COLUMN_NAME + " = ?",
new String [] {version.toString()},
TABLE_PROJECTION[0]
);
for (q.moveToFirst();!q.isAfterLast();q.moveToNext()) {
int idColumn = q.getColumnIndex(TABLE_PROJECTION[0]);
int aliasColumn = q.getColumnIndex(TABLE_PROJECTION[2]);
Integer id = q.getInt(idColumn);
String alias = q.getString(aliasColumn);
osw.write(" tableId = addTable(db, ");
osw.write(alias);
osw.write(", \"");
osw.write(alias);
osw.write("\", ");
osw.write(version.toString());
osw.write(");\n");
genColumns(osw, version, id);
osw.write(" createTable(db, tableId);\n");
osw.write("\n");
}
}
private String getMaxVersion() {
Uri uri = ContentUris.withAppendedId(Db.Version.CONTENT_URI, 0);
Cursor q = ctx.getContentResolver().query(
uri,
VERSION_PROJECTION,
null,
null,
null
);
if (q.moveToFirst()) {
int idColumn = q.getColumnIndex(VERSION_PROJECTION[0]);
Integer id = q.getInt(idColumn);
return id.toString();
}
return "0";
}
private void closeVersions() {
Uri uri = Db.Version.CONTENT_URI;
ctx.getContentResolver().update(uri, null, null, null);
}
private void genPatchList(OutputStreamWriter osw) throws Exception {
Uri uri = Db.Version.CONTENT_URI;
Cursor q = ctx.getContentResolver().query(
uri,
VERSION_PROJECTION,
null,
null,
VERSION_PROJECTION[0]
);
for (q.moveToFirst();!q.isAfterLast();q.moveToNext()) {
int idColumn = q.getColumnIndex(VERSION_PROJECTION[0]);
Integer id = q.getInt(idColumn);
osw.write(" ");
osw.write("if (oldVersion < ");
osw.write(id.toString());
osw.write(") {patch_");
osw.write(id.toString());
osw.write("(db);}\n");
}
}
private void genPatches(OutputStreamWriter osw) throws Exception {
Uri uri = Db.Version.CONTENT_URI;
Cursor q = ctx.getContentResolver().query(
uri,
VERSION_PROJECTION,
null,
null,
VERSION_PROJECTION[0]
);
for (q.moveToFirst();!q.isAfterLast();q.moveToNext()) {
int idColumn = q.getColumnIndex(VERSION_PROJECTION[0]);
Integer id = q.getInt(idColumn);
osw.write(" private void patch_");
osw.write(id.toString());
osw.write("(SQLiteDatabase db) {\n\n");
osw.write(" long tableId;\n");
osw.write(" addVersion(db, ");
osw.write(id.toString());
osw.write(");\n\n");
genTables(osw, id);
genData(osw, id);
osw.write(" }\n\n");
}
}
private String getColumnType(int type) {
switch (type) {
case Db.DataType.INTEGER:
return "Db.DataType.INTEGER";
default:
return "Db.DataType.TEXT";
}
}
private void genStage_1(OutputStreamWriter osw) throws Exception {
osw.write("package ");
osw.write(Db.SITE);
osw.write(Db.APP);
osw.write(";\n\n");
osw.write("import android.content.ContentValues;\n");
osw.write("import android.content.Context;\n");
osw.write("import android.database.sqlite.SQLiteDatabase;\n\n");
osw.write("public abstract class ProviderPatches extends ProviderMetadata {\n\n");
osw.write(" protected static final int DATABASE_VERSION = ");
osw.write(getMaxVersion());
osw.write(";\n\n");
osw.write(" static class PatchedDatabaseHelper extends DatabaseHelper {\n\n");
osw.write(" PatchedDatabaseHelper(Context context, String name, int version) {\n");
osw.write(" super(context, name, version);\n");
osw.write(" }\n\n");
osw.write(" @Override\n");
osw.write(" public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {\n");
}
private void genStage_2(OutputStreamWriter osw) throws Exception {
osw.write(" }\n\n");
}
private void genStage_3(OutputStreamWriter osw) throws Exception {
osw.write(" }\n");
osw.write("}\n");
}
}
Реализация класса, используемого для хранения описаний столбцов, тривиальна:
package com.WhiteRabbit.Codegen;
public class Column {
private String alias;
private int type;
public Column(String alias, int type) {
this.alias = alias;
this.type = type;
}
public String getAlias() {
return alias;
}
public int getType() {
return type;
}
}
Поскольку, в процессе кодогенерации мы обращаемся к БД посредством Content Provider-а, реализуем необходимый функционал:
package com.WhiteRabbit.Codegen;
...
public class CodegenProvider extends ProviderPatches {
private static final String DATABASE_NAME = "Codegen.db";
private static final UriMatcher uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
private static final int VERSION_DIR_URI_INDICATOR = 1;
private static final int VERSION_ITEM_URI_INDICATOR = 2;
private static final int TABLE_DIR_URI_INDICATOR = 3;
private static final int COLUMN_DIR_URI_INDICATOR = 4;
private static final int DATA_ITEM_URI_INDICATOR = 5;
static {
uriMatcher.addURI(Db.AUTHORITY, Db.Version.PATH, VERSION_DIR_URI_INDICATOR);
uriMatcher.addURI(Db.AUTHORITY, Db.Version.PATH + "/#", VERSION_ITEM_URI_INDICATOR);
uriMatcher.addURI(Db.AUTHORITY, Db.Table.PATH, TABLE_DIR_URI_INDICATOR);
uriMatcher.addURI(Db.AUTHORITY, Db.Column.PATH, COLUMN_DIR_URI_INDICATOR);
uriMatcher.addURI(Db.AUTHORITY, Db.DATA_PATH + "/#", DATA_ITEM_URI_INDICATOR);
}
@Override
public boolean onCreate() {
dbHelper = new PatchedDatabaseHelper(getContext(), DATABASE_NAME, DATABASE_VERSION);
return true;
}
@Override
public String getType(Uri uri) {
switch (uriMatcher.match(uri)) {
case VERSION_DIR_URI_INDICATOR:
return Db.Version.CONTENT_DIR_TYPE;
case VERSION_ITEM_URI_INDICATOR:
return Db.Version.CONTENT_ITEM_TYPE;
case TABLE_DIR_URI_INDICATOR:
return Db.Table.CONTENT_DIR_TYPE;
case COLUMN_DIR_URI_INDICATOR:
return Db.Column.CONTENT_DIR_TYPE;
case DATA_ITEM_URI_INDICATOR:
return Db.DATA_ITEM_TYPE;
}
return null;
}
@Override
public Uri insert(Uri uri, ContentValues values) {
// Not Implemented
return null;
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
int r = 0;
// Not Implemented
return r;
}
@Override
public int update(Uri uri, ContentValues values, String selection,
String[] selectionArgs) {
int r = 0;
SQLiteDatabase db = dbHelper.getWritableDatabase();
switch (uriMatcher.match(uri)) {
case VERSION_DIR_URI_INDICATOR:
r = dbHelper.closeVersions(db);
break;
}
return r;
}
@Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
SQLiteDatabase db = dbHelper.getReadableDatabase();
StringBuilder sb = new StringBuilder();
sb.append("select ");
switch (uriMatcher.match(uri)) {
case DATA_ITEM_URI_INDICATOR:
String tableName = dbHelper.getTableName(db, uri.getPathSegments().get(1));
sb.append(Db.Column._ID);
for (String column: projection) {
sb.append(", ");
sb.append(column);
}
sb.append(" from ");
sb.append(tableName);
if (selection != null) {
sb.append(" where ");
sb.append(selection);
}
break;
case COLUMN_DIR_URI_INDICATOR:
sb.append(Db.Column._ID);
sb.append(" as ");
sb.append(projection[0]);
sb.append(", ");
sb.append(Db.Column.NAME_COLUMN);
sb.append(" as ");
sb.append(projection[1]);
sb.append(", ");
sb.append(Db.Column.ALIAS_COLUMN);
sb.append(" as ");
sb.append(projection[2]);
sb.append(", ");
sb.append(Db.Column.TYPE_ID_COLUMN);
sb.append(" as ");
sb.append(projection[3]);
sb.append(", ");
sb.append(Db.Column.IS_NOT_NULL);
sb.append(" as ");
sb.append(projection[4]);
sb.append(" from ");
sb.append(Db.Column.TABLE_NAME);
if (selection != null) {
sb.append(" where ");
sb.append(selection);
}
break;
case TABLE_DIR_URI_INDICATOR:
sb.append(Db.Table._ID);
sb.append(" as ");
sb.append(projection[0]);
sb.append(", ");
sb.append(Db.Table.NAME_COLUMN);
sb.append(" as ");
sb.append(projection[1]);
sb.append(", ");
sb.append(Db.Table.ALIAS_COLUMN);
sb.append(" as ");
sb.append(projection[2]);
sb.append(" from ");
sb.append(Db.Table.TABLE_NAME);
if (selection != null) {
sb.append(" where ");
sb.append(selection);
}
break;
case VERSION_DIR_URI_INDICATOR:
sb.append(Db.Version._ID);
sb.append(" as ");
sb.append(projection[0]);
sb.append(" from ");
sb.append(Db.Version.TABLE_NAME);
sb.append(" where not ");
sb.append(Db.Version.VER_DATE_COLUMN);
sb.append(" is null ");
if (selection != null) {
sb.append(" and ");
sb.append(selection);
}
break;
case VERSION_ITEM_URI_INDICATOR:
sb.append("max(");
sb.append(Db.Version._ID);
sb.append(") as ");
sb.append(projection[0]);
sb.append(" from ");
sb.append(Db.Version.TABLE_NAME);
sb.append(" where not ");
sb.append(Db.Version.VER_DATE_COLUMN);
sb.append(" is null ");
break;
}
if (sortOrder != null) {
sb.append(" order by ");
sb.append(sortOrder);
}
return db.rawQuery(sb.toString(), selectionArgs);
}
}
В нашем случае, нам не требуются реализации insert и delete, поскольку мы, в основном, только читаем БД. Разумеется в серьезном проекте, занимающемся чем-то полезным помимо собственной кодогенерации, эти методы потребуются.
Ну и последний штрих: в Activity нашего проекта создадим файл на файловой системе устройства и передадим поток вывода кодогенератору:
package com.WhiteRabbit.Codegen;
...
public class CodegenActivity extends Activity {
private PatchGenerator patchGenerator = new PatchGenerator(this);
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
try {
FileOutputStream os = new FileOutputStream("/sdcard/ProviderPatches.java");
OutputStreamWriter osw = new OutputStreamWriter(os);
try {
patchGenerator.generate(osw);
} finally {
os.close();
}
} catch (Exception e) {
Log.e(e.toString(), e.toString());
}
}
}
Запустив приложение на выполнение, мы сформируем файл ProviderPatches.java, в целом, повторяющий уже имеющийся в проекте. Если мы, каким либо способом, добавим в БД данные, привязанные к некоторой версии, то при очередной перегенерации, команды добавления этих данных будут добавлены в патч. Заменив сгенерированным файлом соответствующий файл в проекте и пересобрав приложение, мы, тем самым, осуществим тиражирование изменений, выполненных в БД.
Разумеется, кодогенератор, описанный в этом цикле статей, крайне (и вполне сознательно) упрощен. Полностью игнорируются такие вопросы как поддержка ограничений целостности БД (в том числе внешних ключей), индексов и т.п. Необходимые для поддержки этой функциональности изменения довольно примитивны, но достаточно объемны, чтобы сделать код полностью нечитаемым. Также, полностью игнорируются тот факт, что изменения в БД, это не обязательно только добавления :) Поддержка удаления устаревших объектов в версиях патча (и возможно последующего их пересоздания в последующих версиях) сделают код проекта еще менеее читаемым.
Целью этого цикла статей была иллюстрация того, как кодогенерация может помочь в решении каждодневных практических задач. Разумеется, описанным кейсом применение кодогенерации не ограничивается. Фактически, везде, где мы программно манипулируем некоторым языком (например SQL) кодогенерация, в том или ином виде, будет необходима. Нужно помнить, что это очень универсальный и полезный инструмент.
Комментариев нет:
Отправить комментарий