пятница, 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-е.

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

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