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

Комментариев нет:

Отправить комментарий