пятница, 1 июня 2012 г.

Кодогенерация как она есть

Поскольку мы уже вполне хорошо представляем, что нам требуется генерировать, самое время взяться за собственно процесс кодогенерации. Разработка  кодогенерирующих приложений не столько сложна, сколько трудоемка, в плане отладки. Сам по себе кодогенератор довольно примитивен и удручающе многословен:

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) кодогенерация, в том или ином виде, будет необходима. Нужно помнить, что это очень универсальный и полезный инструмент.

пятница, 25 мая 2012 г.

Кодогенерация - какая она?

В прошлой статье мы разобрались, для чего нам нужна кодогенерация, но пока несколько смутно представляем, что именно мы собираемся кодогенерировать. Исправим это.

Кодогенерация - какая она?


Не знаю как у Вас, но у меня вновь созданный, абсолютно пустой проект вызывает чувство необузданного первобытного ужаса :( Чтобы его пересилить и начать писать хоть что-то, проще всего начинать с самого простого и понятного. Например, мне абсолютно понятно, что наш проект реализует Content Provider и никак не сможет обойтись без трех служебных таблиц, описанных в последней диаграмме прошлой статьи. Опишем эти таблицы в Java-коде:

package com.WhiteRabbit.Codegen;

...
public final class Db {

    private final static String SCHEME              = "content://";
    public  final static String SITE                = "com.WhiteRabbit.";
    public  final static String APP                 = "Codegen";
    public  final static String AUTHORITY           = SITE + "provider." + APP;
    public  final static String DIR_TYPE            = "vnd.android.cursor.dir/vnd." + AUTHORITY + ".";
    public  final static String ITEM_TYPE           = "vnd.android.cursor.item/vnd." + AUTHORITY + ".";
   
    public  final static String DATA_PATH           = "data";
    public  final static Uri    DATA_CONTENT_URI    = Uri.parse(SCHEME + AUTHORITY + "/" + DATA_PATH);
    public  final static String DATA_ITEM_TYPE      = ITEM_TYPE + DATA_PATH;
    public  final static String ID_COLUMN_NAME      = "_id";
    public  final static String VERSION_COLUMN_NAME = "version";
   
    private Db() {}
   
    public final static class DataType {
        public final static int INTEGER = 1;
        public final static int TEXT = 2;
    }
   
    public final static class Table implements BaseColumns {
       
        public final static String PATH = "Table";
        public final static Uri    CONTENT_URI =  Uri.parse(SCHEME + AUTHORITY + "/" + PATH);
        public final static String CONTENT_DIR_TYPE = DIR_TYPE + PATH;
        public final static String CONTENT_ITEM_TYPE = ITEM_TYPE + PATH;
       
        private Table() {}
       
        public final static String TABLE_NAME = "_table";
       
        public final static String NAME_COLUMN = "table_name";
        public final static String ALIAS_COLUMN = "alias_name";
    }

    public final static class Version implements BaseColumns {
       
        public final static String PATH = "Version";
        public final static Uri    CONTENT_URI =  Uri.parse(SCHEME + AUTHORITY + "/" + PATH);
        public final static String CONTENT_DIR_TYPE = DIR_TYPE + PATH;
        public final static String CONTENT_ITEM_TYPE = ITEM_TYPE + PATH;
       
        private Version() {}
       
        public final static String TABLE_NAME = "_version";
       
        public final static String VER_DATE_COLUMN = "version_date";
    }

    public final static class Column implements BaseColumns {
       
        public final static String PATH = "Column";
        public final static Uri    CONTENT_URI =  Uri.parse(SCHEME + AUTHORITY + "/" + PATH);
        public final static String CONTENT_DIR_TYPE = DIR_TYPE + PATH;
        public final static String CONTENT_ITEM_TYPE = ITEM_TYPE + PATH;
       
        private Column() {}
       
        public final static String TABLE_NAME = "_column";
       
        public final static String TABLE_ID_COLUMN = "table_id";
        public final static String TYPE_ID_COLUMN = "data_type";
        public final static String IS_NOT_NULL = "is_not_null";
        public final static String NAME_COLUMN = "column_name";
        public final static String ALIAS_COLUMN = "alias_name";
    }
}


Далее тривиально создаем эти таблицы в ProviderMetadata:

package com.WhiteRabbit.Codegen;

...
public abstract class ProviderMetadata extends ContentProvider {
   
    protected DatabaseHelper dbHelper;
   
    static abstract class DatabaseHelper extends SQLiteOpenHelper {
       
        private int version;

        DatabaseHelper(Context context, String name, int version) {
            super(context, name, null, version);
            this.version = version;
        }

        @Override
        public void onCreate(SQLiteDatabase db) {
           
            db.execSQL("CREATE TABLE " + Db.Version.TABLE_NAME + " (" +
                       Db.Version._ID + " INTEGER PRIMARY KEY," +
                       Db.Version.VER_DATE_COLUMN + " TEXT);");
           
            db.execSQL("CREATE TABLE " + Db.Table.TABLE_NAME + " (" +
                       Db.Table._ID + " INTEGER PRIMARY KEY," +
                       Db.VERSION_COLUMN_NAME + " INTEGER NOT NULL," +
                       Db.Table.ALIAS_COLUMN + " TEXT NOT NULL," +
                       Db.Table.NAME_COLUMN + " TEXT NOT NULL);");
           
            db.execSQL("CREATE TABLE " + Db.Column.TABLE_NAME + " (" +
                       Db.Column._ID + " INTEGER PRIMARY KEY," +
                       Db.VERSION_COLUMN_NAME + " INTEGER NOT NULL," +
                       Db.Column.TABLE_ID_COLUMN + " INTEGER NOT NULL," +
                       Db.Column.TYPE_ID_COLUMN + " INTEGER NOT NULL," +
                       Db.Column.IS_NOT_NULL + " INTEGER NOT NULL," +
                       Db.Column.ALIAS_COLUMN + " TEXT NOT NULL," +
                       Db.Column.NAME_COLUMN + " TEXT NOT NULL);");
           
            onUpgrade(db, 0, version);
        }
    }
}


Теперь начинается самое интересное. Создаем файл, который впоследствии будем генерировать автоматически (соответственно, он должен быть построен таким образом, чтобы максимально облегчить эту кодогенерацию):

package com.WhiteRabbit.Codegen;

import android.content.ContentValues;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;

public abstract class ProviderPatches extends ProviderMetadata {

    protected static final int DATABASE_VERSION = 1;
   
    static class PatchedDatabaseHelper extends DatabaseHelper {

        PatchedDatabaseHelper(Context context, String name, int version) {
            super(context, name, version);
        }
       
        @Override
        public void onUpgrade(SQLiteDatabase db, int oldVersion, 
                              int newVersion) {
            if (oldVersion < 1)  {patch_1(db);}
        }
       
        private void patch_1(SQLiteDatabase db) {
           
            long tableId;
            addVersion(db, 1);
           
            tableId = addTable(db, 
                          Db.TestTable.TABLE_NAME, 
                         "Db.TestTable.TABLE_NAME", 1);
            addColumnNotNull(db, tableId, 
                          Db.DataType.TEXT, Db.TestTable.FIELD_COLUMN, 
                         "Db.TestTable.FIELD_COLUMN", 1);
            createTable(db, tableId);
           
            ContentValues values;
            values = new ContentValues();
            values.put(Db.TestTable.FIELD_COLUMN, "test");
            addData(db, Db.TestTable.TABLE_NAME, values, 1);
        }
    }
}

Класс этот абстрактный (от него будет наследоваться класс, реализующий функционал нашего Content Provider-а). Также можно заметить, что он определяет номер версии БД DATABASE_VERSION, но имя БД мы до сих пор пока нигде не указывали (поскольку весь ранее описанный код достаточно универсален и может использоваться в различных проектах).
Для создания таблиц и заполнения их данными мы не используем SQL-запросы непосредственно, а используя некое API, создаем описание таблиц или данных, после чего генерируем их в соответствии с этим описанием (такой подход существенно обегчит нам задачу кодогенерации). Определим функции, испольованные нами для генерации данных в ранее созданном классе ProviderMetadata:

package com.WhiteRabbit.Codegen;

...
public abstract class ProviderMetadata extends ContentProvider {

    ...
    static abstract class DatabaseHelper extends SQLiteOpenHelper {

        protected void addVersion(SQLiteDatabase db, long id) {
            Date now = new Date();
            SimpleDateFormat formatter = new SimpleDateFormat("MM/dd/yyyy");
            db.execSQL("insert into " + Db.Version.TABLE_NAME + "(" +
                    Db.Version._ID + "," +
                    Db.Version.VER_DATE_COLUMN + ") values " +
                    "(" + Long.toString(id) + ", '" + 
                    formatter.format(now) + "')");
        }
       
        protected long getVersion(SQLiteDatabase db) {
            Cursor q = db.rawQuery("select " + Db.Version._ID + " " +
                    "from " + Db.Version.TABLE_NAME + " " +
                    "where " + Db.Version.VER_DATE_COLUMN + " is null",
                    null);
            if (q.moveToFirst()) {
                int idColumn = q.getColumnIndex(Db.Version._ID);
                return q.getInt(idColumn);
            }           
            ContentValues values = new ContentValues();
            return db.insert(Db.Version.TABLE_NAME, Db.Version._ID, values);
        }
       
        public int closeVersions(SQLiteDatabase db) {
            Date now = new Date();
            SimpleDateFormat formatter = new SimpleDateFormat("MM/dd/yyyy");
            ContentValues values = new ContentValues();
            values.put(Db.Version.VER_DATE_COLUMN, formatter.format(now));
            return db.update(Db.Version.TABLE_NAME, values,
                        Db.Version.VER_DATE_COLUMN + " is null", null);
        }

        protected long addTable(SQLiteDatabase db, String name, 
            String alias, int ver) {
            ContentValues values = new ContentValues();
            values.put(Db.Table.NAME_COLUMN, name);
            values.put(Db.Table.ALIAS_COLUMN, alias);
            values.put(Db.VERSION_COLUMN_NAME, ver);
            return db.insert(Db.Table.TABLE_NAME, Db.Table._ID, values);
        }
       
        protected void addColumn(SQLiteDatabase db, long tableId, int typeId, 
            String name, String alias, int ver, boolean isNotNull, 
            boolean isDefNull) {
            ContentValues values = new ContentValues();
            values.put(Db.Column.TABLE_ID_COLUMN, tableId);
            values.put(Db.Column.TYPE_ID_COLUMN, typeId);
            values.put(Db.Column.NAME_COLUMN, name);
            values.put(Db.Column.ALIAS_COLUMN, alias);
            values.put(Db.Column.IS_NOT_NULL, isNotNull?1:0);
            values.put(Db.VERSION_COLUMN_NAME, ver);
            db.insert(Db.Column.TABLE_NAME, Db.Column._ID, values);
        }
       
        protected void addColumn(SQLiteDatabase db, long tableId, 
            int typeId, String name, String alias, int ver) {
            addColumn(db, tableId, typeId, name, alias, ver, false, false);
        }
        protected void addColumnNotNull(SQLiteDatabase db, long tableId, 
            int typeId, String name, String alias, int ver) {
            addColumn(db, tableId, typeId, name, alias, ver, true, false);
        }

        protected void addData(SQLiteDatabase db, String tableName, 
            ContentValues values, int ver) {
            values.put(Db.VERSION_COLUMN_NAME, ver);
            db.insert(tableName, "_id", values);
        }
       
        public String getTableName(SQLiteDatabase db, String tableId) {
            String r = "";
            Cursor q = db.rawQuery("select " + Db.Table.NAME_COLUMN + " " +
                    "from " + Db.Table.TABLE_NAME + " " +
                    "where " + Db.Table._ID + " = ?",
                    new String [] {tableId});
            if (q.moveToFirst()) {
                int nameColumn = q.getColumnIndex(Db.Table.NAME_COLUMN);
                r = q.getString(nameColumn);
            }           
            return r;
        }
       
        protected void createTable(SQLiteDatabase db, long tableId) {
            StringBuilder sb = new StringBuilder();
            sb.append("CREATE TABLE ");
            sb.append(getTableName(db, Long.toString(tableId)));
            sb.append(" (");
            sb.append(Db.ID_COLUMN_NAME);
            sb.append(" INTEGER PRIMARY KEY, ");
            sb.append(Db.VERSION_COLUMN_NAME);
            sb.append(" INTEGER NOT NULL ");
            Cursor q = db.rawQuery("select " + Db.Column.NAME_COLUMN + ", " +
                    Db.Column.TYPE_ID_COLUMN + ", " +
                    Db.Column.IS_NOT_NULL + " " +
                    "from " + Db.Column.TABLE_NAME + " " +
                    "where " + Db.Column.TABLE_ID_COLUMN + " = ?",
                    new String [] {Long.toString(tableId)});
            for (q.moveToFirst();!q.isAfterLast();q.moveToNext()) {
                sb.append(", ");
                int nameColumn = q.getColumnIndex(Db.Column.NAME_COLUMN);
                int typeColumn = q.getColumnIndex(Db.Column.TYPE_ID_COLUMN);
                int notNullColumn = q.getColumnIndex(Db.Column.IS_NOT_NULL);
                String name = q.getString(nameColumn);
                int type = q.getInt(typeColumn);
                int notNull = q.getInt(notNullColumn);
                sb.append(name);
                switch (type) {
                    case Db.DataType.INTEGER:
                        sb.append(" INTEGER");
                        break;
                    case Db.DataType.TEXT:
                        sb.append(" TEXT");
                        break;
                }
                if (notNull > 0) {
                    sb.append(" NOT NULL");
                }
            }
            sb.append(");");
            db.execSQL(sb.toString());
        }
        ...
       
    }
}

Реализация API описания метаданных достаточно прямолинейна. Некоторую сложность здесь представляет лишь реализация createTable, представляющая собой решение задачи кодогенерации "в миниатюре", создающая запрос create table и выполняющая его "на лету". Также, не забываем привязывать данные, создаваемые в addData к версии патча.

Таким образом, мы разработали (пока абстрактную) заготовку нашего Content Provider-а. В следующей статье, мы займемся непосредственно кодогенератором, попутно дописав необходимый функционал в Content Provider-е.

воскресенье, 20 мая 2012 г.

Кодогенерация - для чего она?

Программы, которые пишут программы ... звучит интригующе, но для чего это может понадобиться в реальной жизни??? Сегодня, я постараюсь рассказать о практической задаче, при решении которой применение кодогенерации оказалось единственным разумным подходом.

Кодогенерация - для чего она?

Давайте представим себе, что мы разрабатываем некоторое тиражируемое приложение (например игру), использующее БД для хранения своих данных. Android не балует нас разнообразием, и когда мы говорим "встроенная БД", мы имеем в виду SQLite и только её :)

Следует отдать должное разработчикам Android, использование SQLite на этой платформе весьма продуманно, особенно в том, что касается тиражирования изменений. Разработчику достаточно увеличить номер версии БД в исходном коде и внести необходимые дополнения в метод onUpgrade, чтобы, при последующем запуске обновленного приложения, необходимые изменения были внесены во встроенную БД каждого пользователя автоматически.

Это действительно очень удобно, но ... что делать если мы используем какой-то визуальный редактор (например при проектировании уровней)? Наш редактор уровней, в процессе работы, внесет изменения в ЛОКАЛЬНУЮ встроенную БД, используемого нами устройства, минуя исходный код нашего приложения! Как нам передать эти изменения пользователям?

Конечно, если мы используем эмулятор, не составляет труда скопировать с него сгенерированную нашим приложением БД, но, во первых, пользоваться эмулятором может быть неудобно, и во вторых, что нам делать с полученным файлом БД дальше?

В этом месте нам на помощь приходит кодогенерация. Действительно решение тривиально - если для тиражирования изменений БД необходимо внести новый КОД в метод onUpgrade, почему бы не сделать это автоматически, учтя при этом все изменения, внесенные в локальную БД приложением?
Android позволяет записать текстовый файл в файловую систему устройства (при наличии WRITE_EXTERNAL_STORAGE uses-permission в манифесте), вот мы и сформируем файл, содержащий код на языке Java. Впоследствии этот файл можно будет забрать с устройства и добавить его в исходный код проекта, решив, тем самым, проблему тиражирования изменений.

Мы уже почти наполовину решили нашу задачу :) Осталось только спроектировать как все это будет выглядеть.


Мы разделим реализацию нашего Content Provider-а на три Java-класса. ProviderMetadata будет содержать реализацию DatabaseHelper, предоставляющую базовую функциональность, ProviderPatches будет содержать тиражируемые изменения и формироваться кодогенератором, а CodegenProvider реализует API используемое приложением для взаимодействия с БД.

Здесь следует обратить внимание, что константу DATABASE_NAME содержащую имя используемой нами БД, мы определяем на самом верхнем уровне в CodegenProvider, а DATABASE_VERSION в классе, содержащем реализацию патчей. Класс ProviderMetadata свободен от какой-то конкретике и может повторно использоваться в любых наших приложениях.

Все это прекрасно, но с чем именно будет работать наш обобщенный и до предела отвязанный от конкретики (почти сферический) ProviderMetadata? Как бы смешно это не звучало, но для того чтобы программа могла писать программы, ей необходимы данные, которые описывают данные :) то есть МЕТАданные.

В этом учебном примере я постарался до предела упростить структуру метаданных. В реальном приложении необходимо описывать индексы, взаимосвязи таблиц, ограничения уникальности и много чего другого. Нам из всего этого понадобиться только описание таблиц и их столбцов.


Таблица version будет содержать список патчей, сформированных для нашего приложения. В таблице table мы будем хранить описание таблиц, а в таблице column - столбцов (привязанные к патчам БД). Для таблиц мы будем сохранять их имена, а для столбцов, дополнительно описания типов данных (data_type) и признака NOT NULL (is_not_null). Для чего нам потребуется поле alias_name, я расскажу позже.

Подведем итог


В этой статье мы спроектировали систему описания данных и автоматического тиражирования их изменений. В следующей статье мы реализуем Content Provider, в соответствии с разработанной спецификацией.