понедельник, 25 июня 2012 г.

Об Архитектуре

Сегодня я хотел бы начать разговор об архитектурных паттернах и о том, каким образом они могут оказаться полезны в жизни. Я не буду ничего говорить о паттернах GoF  (которые, безусловно, нужны и полезны всем тем, кто вслед за Бандой разрабатывает “визуальные редакторы документов, построенные по принципу WYSIWYG  и аналогичные приложения”), а сосредоточусь на не менее интересных (но возможно несколько менее известных) документах TM Forum (а именно SID), позволяющих описать предметную область приложений, работающих в сфере телекоммуникаций.

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

Сущность



Исходя из SID-а, Сущность (Entity) представляет собой абстрактный класс, наследуемый от RootEntity, предоставляющий get-еры и set-еры к нескольким атрибутам, а также сохраняющий строковое значение версии. С RootEntity связаны следующие атрибуты:


Атрибут
Назначение
commonName
Обязательный атрибут сохраняющий “дружественное” описание объекта, составленное в соответствии с принятыми в разрабатываемой системе правилами именования объектов и используемое, например, при отображении объекта в GUI
description
Необязательный атрибут, описывающий, в свободной форме, назначение объекта
objectID
Обязательный атрибут, содержащий уникальный идентификатор объекта, используемый приложением во внутренних целях



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




Помимо интерфейсов (необходимость применения которых вполне обсуждаема), мы добавили (на уровне Entity) возможность хранения неких именованных значений (атрибутов). Пока что, для простоты изложения, будем считать, что эти значения строковые, но, строго говоря, ни строковыми, ни даже скалярными они быть совсем не обязаны.

Хочу специально обратить внимание, что IEntity не наследует IRootEntity. Поскольку аналогичное наследование есть на уровне абстрактных классов, наследование интерфейсов не даст нам ничего принципиально нового, но помешает создавать объекты хранящие списки именованных значений, но не имеющие идентификаторов.

Итак, наша сущность умеет хранить уникальный идентификатор и список именованных значений. Но как нам узнать какие именно именованные значения хранит Сущность? В этом нам поможет Спецификация.


Спецификация



 
Согласно SID-у абстрактный класс Specification наследуется от RootEntity (то есть имеет имя и уникальный идентификатор) и является родителем EntitySpecification (дополнительный уровень наследования введен, поскольку специфицировать может понадобиться не только сущности). С EntitySpecification может быть ассоциировано 0 и более Entity.


Абстрактный класс Specification определяет следующие атрибуты:
Атрибут
Назначение
specValidFor
Период действия спецификации
specLifeCycleStatus
Текущий статус спецификации (в рамках ее жизненного цикла)
specVersion
Версия спецификации
 
Согласно тому-же SID-у, спецификация предназначена для описания инвариантных характеристик Entity (таких как атрибуты, методы, ограничения и связи). Реализация этого хранения полностью отдана на откуп разработчику, чем мы немедленно и воспользуемся.


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


Метод
Назначение
getTypeName
Этот метод, у нас, будет возвращать строковый objectID из RootEntity, который мы будем называть типом сущности. Как будет показано далее, для типов сущностей имеет смысл определять иерархию.
Так, например, тип ‘ri может быть ассоциирован с корневой спецификацией, описывающей объекты Resource Inventory, а ‘ri.device’ ее дочерней спецификацией, описывающей устройства.
Можно пойти еще дальше и определить, например, спецификацию ‘ri.device.valid’ описывающую исправные устройства (мы вернемся к этому моменту, когда будем рассматривать взаимосвязь Specification и Policy)
getParentSpecification
Используя этот метод, мы всегда сможет получить родительскую спецификацию. При вызове этого метода у корневой спецификации, можно бросать исключение
getAttributeNames
Этот метод будет возвращать коллекцию имен атрибутов сохраняемых описываемой сущностью
isMandatoryAttribute
Зная имя атрибута, и используя этот метод, мы можем проверить, является ли атрибут обязательным. Ситуация кода Entity не сохраняет значение для обязательного атрибута должна рассматриваться как ошибка
isConstantAttribute
Этот метод показывает, разрешено ли изменение значения для данного атрибута. Константные атрибуты будут использоваться нами далее для идентификации сущностей
getDefaultValue
Используя этот метод, можно получить значение по умолчанию, которое следует использовать, если сущность не сохраняет значение для данного атрибута


Описанный выше интерфейс позволяет единообразно хранить описание сущностей (и соответственно работать с сущностями зная имена и типы их атрибутов). Несколько менее очевидно то, что одна и та же сущность может рассматриваться, фактически, как совершенно различные сущности (содержащие различные наборы значений). О том, зачем это может понадобиться, мы будем говорить, когда речь зайдет об архитектуре приложения. В настоящий момент нас более должен занимать другой вопрос. Как имея экземпляр Entity получить спецификацию, которая ее описывает? Решением этого вопроса и занимается идентификация, которую мы рассмотрим далее.


Идентификация




Попробуем прочитать эту загадочную картинку. Итак, с сущностью Entity связано некоторое количество EntityIdentification, содержащих значения, совокупность которых уникально идентифицирует экземпляр сущности. С другой стороны, EntityIdentificationSpecification ассоциирует наборы этих значений со спецификациями EntitySpecification. Для EntityIdentificationSpecification определены следующие атрибуты:
Атрибут
Назначение
name
Имя EntityIdentificationSpecification (как экземпляра RootEntity)
description
Описание, понятное человеку
validFor
Период действия
  
Какова главная цель использования EntityIdentificationSpecification? По всей видимости, он должен помочь нам, имея экземпляр сущности и некоторые априорные знания о ее типе (например, спецификацию родительского типа) получить точную спецификацию сущности. Определим это явно в форме интерфейса:
Зная имя родительской спецификации (например ‘ri’) и используя экземпляр, реализующий IEntity, мы получаем экземпляр спецификации (например, имеющий тип ‘ri.device’), наиболее полно описывающий сущность.
Как мы можем этого добиться? В этом нам сильно помогут константные атрибуты (фактически описывающие значения EntityIdentification). Действительно, мы можем определить правило проверки константного атрибута (например ‘TYPE_ID’), которое будет заключаться в том, что если значение TYPE_ID = 1 для экземпляра сущности Resource Inventory, то этот экземпляр следует рассматривать как устройство (‘ri.device’).
Другой важной задачей идентификации является валидация экземпляра сущности. Спецификация не возвращается до тех пор, пока EntityIdentificationSpecification не проверит корректность заполнения атрибутов (например, наличие всех обязательных полей и корректность константных значений атрибутов). В случае если экземпляр сущности не валиден, EntityIdentificationSpecification бросит исключение.
По завершении идентификации, мы можем связать сущность с полученной спецификацией:
 
Являясь наследником IEntity, SpecifiedEntity позволит работать лишь с теми значениями, имена атрибутов которых описаны в спецификации, а также подставит значения по умолчанию для отсутствующих не обязательных значений.

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

Граница на замке

Сегодня я предлагаю поговорить о второй моей любимой теме после Айкидо, об Oracle :) Мы займемся безопасностью (но не просто безопасностью, а безопасностью своими руками). Конечно, можно сказать, что это  "Закат Солнца вручную", но, если в процессе мы узнаем что-то новое, то, возможно, это не совсем бесполезное занятие?

Граница на замке

Итак, ни для кого не секрет, что в рамках TNS-протокола (служащего для общения клиента Oracle с сервером) данные (в том числе и пароль, используемый при авторизации) передаются по сети в открытом виде. Конечно, Oracle подумал об этом и поставляет всякие забавные штуки, типа Advanced Security Option, но наша сегодняшняя задача - показать, что мы и сами не без усов.

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

Выглядит все гладко, но как нам получить сеансовые данные с сервера ДО установления соединения??? Ладно, понятно, что авторизоваться в какой-то схеме Oracle все равно придется, но пусть это будет пустая схема, не имеющая прав ни на что, кроме выполнения двухфазной авторизации. Таким образом, будем считать, что утечка пароля к этой схеме не нанесет ущерба нашей безопасности (на самом деле, еще как нанесет, но тут я возвращаю всех к ASO, уже упомянутой выше).

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

Ну что-же, засучим рукава и создадим все что нам нужно:

connect sys/&&syspass@orcl as sysdba

create role ”STRT”; -- Минимальные права, для авторизации
create role ”DFLT”; -- Права авторизованного пользователя
create role ”ADMN”; -- Права авторизованного администратора

revoke ”STRT” from SYS;
revoke ”DFLT” from SYS;
revoke ”ADMN” from SYS;

create user MAIN    -- Схема владельца
identified by ”MAIN” account unlock;

grant CONNECT,RESOURCE to MAIN;
grant EXECUTE on DBMS_OBFUSCATION_TOOLKIT to MAIN;
grant EXECUTE on DBMS_APPLICATION_INFO to MAIN;
grant create any context to MAIN;
grant create any view to MAIN;
grant alter any role to MAIN;

create user ENTRY   -- Схема для выполнения первичного соединения
identified by ”&pass” account unlock;

grant CONNECT,STRT,DFLT,ADMN to ENTRY;
alter user ENTRY default role CONNECT,STRT;

Итак, у нас есть две схемы: ENTRY, используемая для первичного соединения и MAIN, собственно хранящая данные. Также определены три роли: STRT - используемая в процессе двухфазной авторизации, DFLT - определяющая права непривилегированного пользователя после завершения авторизации и ADMN - определяющая административные права. Создадим объекты в MAIN:


connect MAIN/MAIN@orcl

CREATE TABLE ROLE_LIST (
  ID  NUMBER NOT NULL,
  NM  VARCHAR2(10) NOT NULL,
  PS  VARCHAR2(30) NOT NULL
);

ALTER TABLE ROLE_LIST ADD (
  CONSTRAINT ROLE_LIST_PK PRIMARY KEY (ID));

create or replace view ROLES as
select ID,NM,'******************************' PS
from ROLE_LIST;

grant SELECT on ROLES to ADMN;

create sequence USERS_SEQ;

CREATE TABLE USER_LIST (
  ID  NUMBER        NOT NULL,
  NM  VARCHAR2(30)  NOT NULL,
  PS  VARCHAR2(30)  NOT NULL,
  TP  NUMBER        DEFAULT 0,
  SP  DATE          DEFAULT NULL
);

ALTER TABLE USER_LIST ADD (
  CONSTRAINT USER_LIST_PK PRIMARY KEY (ID));

ALTER TABLE USER_LIST ADD (
  CONSTRAINT USER_LIST_UK UNIQUE (NM));

create sequence LOGIN_SEQ
INCREMENT BY 1 START WITH 1 MAXVALUE 1000 CYCLE CACHE 5;

create sequence LOG_SEQ;

CREATE TABLE USER_LOG (
  ID  NUMBER        NOT NULL,
  DT  DATE          DEFAULT sysdate NOT NULL,
  NM  VARCHAR2(30)  NOT NULL,
  RS  NUMBER(1)     NOT NULL
);
 
ALTER TABLE USER_LOG ADD (
  CONSTRAINT USER_LOG_PK PRIMARY KEY (ID));

В таблице USER_LISTS будем хранить логины и пароли пользователей, имеющих право работать в нашей системе. ROLE_LIST будет использоваться для хранения секретных паролей, используемых для включения определенных нами ролей. Для выполнения авторизации, знать нам эти пароли не понадобиться, соответсвенно и доступа к ним, у пользователя, не будет .

create or replace package SYSAUTH as
    function  TestPass(P_US in varchar2,
                       P_PS in varchar2,
                       P_SS in varchar2) return varchar2;
    function md5(P_IN in varchar2) return varchar2;
end SYSAUTH;
/

create or replace package body SYSAUTH as

    function my_role(P_ID in number) return varchar2
    as L_SS varchar2(100) default NULL;
    begin
      select ’,”’||NM||’” identified by ”’||PS||’”’
      into L_SS from ROLE_LIST where ID = P_ID;
      return L_SS;
    exception
      when others then
        return NULL;
    end;

    procedure SET_APP(P_US in varchar2)
    as L_ID number default NULL;
    begin
      DBMS_APPLICATION_INFO.SET_MODULE('SEC','');
      DBMS_APPLICATION_INFO.SET_CLIENT_INFO(P_US);
      select max(ID) into L_ID from USER_LIST
      where NM = P_US and bitand(TP,1)+0>0;
      if not L_ID is NULL then
         DBMS_SESSION.SET_CONTEXT(’MYCTX’,’ID’,TO_CHAR(L_ID));
      end if;
    end;

    procedure Activate(P_US in varchar2,                       
                       L_TP in number,
                       O_SS out NOCOPY varchar2) as
    begin O_SS := ’CONNECT ’||my_role(0);
      if bitand(L_TP,1)+0>0 then
         O_SS := O_SS||my_role(1);
      end if;
      SET_APP(P_US);
    end;

    procedure Log(P_US in varchar2, P_RS in number)
    as pragma autonomous_transaction;
    begin
       insert into USER_LOG(ID,NM,RS)
       values (LOG_SEQ.nextval,P_US,P_RS);
       commit;
    exception
       when others then
         rollback;
    end;
    function md5(P_IN in varchar2) return varchar2
    as L_SS varchar2(100) default NULL;
       L_RR varchar2(200) default NULL;
       L_CC varchar2(1) default NULL;
    begin
      DBMS_OBFUSCATION_TOOLKIT.MD5(INPUT_STRING    => P_IN,
                                   CHECKSUM_STRING => L_SS);
      loop
        L_CC := substr(L_SS,1,1);
        L_RR := L_RR||Trim(TO_CHAR(ascii(L_CC),’XX’));
        exit when length(L_SS) = 1;
        L_SS := substr(L_SS,2,length(L_SS)-1);
      end loop;
      return L_RR;
    end;

    function TestPass(P_US in varchar2, P_PS in varchar2,
                      P_SS in varchar2) return varchar2
    as L_TP number default NULL;
       L_SS varchar2(2000) default NULL;
    begin
      select max(TP) into L_TP from USER_LIST
      where NM = P_US and nvl(SP,sysdate+1)>sysdate;
      if not L_TP is NULL and md5(P_SS) = P_PS then
         Activate(P_US,L_TP,L_SS);
         Log(P_US,1);
      else
         Log(P_US,0);
      end if;
      return L_SS;
    end;

end SYSAUTH;
/

create or replace context MYCTX using SYSAUTH
/

Этот пакет и контекст мы используем для проверки пароля. Пользователь не будет иметь доступа к этому пакету.

 
create or replace package SECAUTH as
    auth_application_error EXCEPTION;
    PRAGMA EXCEPTION_INIT(auth_application_error,-20101);
    function  GetSalt return varchar2;
    function  CheckPass(P_US in varchar2,
                        P_PS in varchar2) return varchar2;
end SECAUTH;
/

grant EXECUTE on SECAUTH  to STRT;

create or replace view USERS as
select ID,NM,TP,SP from USER_LIST
where nvl(TO_NUMBER(sys_context('MYCTX','ID')),ID) = ID;

grant SELECT on USERS to DFLT;

create or replace trigger USERS_MDF
instead of insert or update or delete
on "USERS"
begin
  raise SECAUTH.auth_application_error;
end;
/

create or replace package body SECAUTH as

    G_SALT varchar2(100) default null;

    function  GetSalt return varchar2
    as L_NN number default NULL;
    begin
      select LOGIN_SEQ.nextval into L_NN from dual;
      G_SALT:=SYSAUTH.md5(TO_CHAR(L_NN)||
                          TO_CHAR(sysdate,’MISS’));
      return G_SALT;
    end;

    function  CheckPass(P_US in varchar2,
                        P_PS in varchar2) return varchar2
    as L_SS varchar2(300) default NULL;
    begin
      select PS||G_SALT into L_SS
      from USER_LIST where NM = P_US;
      return SYSAUTH.TestPass(P_US,P_PS,L_SS);
    end;

end SECAUTH;
/

Пакет SECAUTH, напротив, будет использоваться при выполнении авторизации, что впрочем никак не поможет злоумышленнику авторизоваться, не зная пароля.

create or replace trigger ROLES_DEL
instead of delete
on "ROLES"
begin
  raise SECAUTH.auth_application_error;
end;
/

create or replace trigger ROLES_MDF
instead of insert or update
on "ROLES"
declare pragma autonomous_transaction;
begin
  if inserting then
     insert into ROLE_LIST(ID,NM,PS)
     values(:new.ID,:new.NM,Trim(:new.PS));
  else
     update ROLE_LIST set PS=Trim(:new.PS) where ID=:new.ID;
  end if;
  execute immediate 'alter role "'||:new.NM||
                    '" identified by "'||Trim(:new.PS)||'"';
  commit;
exception
  when others then
    rollback;
    raise;
end;
/

Эти триггеры помогут нам изменять пароли включаемых ролей.


create function Login(P_US in varchar2, P_PS in varchar2)
return number AUTHID CURRENT_USER
as pragma autonomous_transaction;
   L_SS varchar2(2000) default SECAUTH.CheckPass(P_US,P_PS);
begin
  if not L_SS is NULL then
    execute immediate ’set role ’||L_SS;
    execute immediate 'alter session set current_schema = MAIN';
    return 1;
  else
    return 0;
  end if;
end Login;
/

grant EXECUTE on Login to STRT;

insert into ROLES(ID,NM,PS) values(0,'DFLT','default');
insert into ROLES(ID,NM,PS) values(1,'ADMN','admin');

insert into USER_LIST(ID,NM,PS,TP)
values(USERS_SEQ.nextval,’admin’,’admin’,1);
commit;

connect sys/&&syspass@orcl as sysdba

alter user MAIN account lock;

Ну и наконец самое интересное. Функция Login будет доступна пользователю, зашедшему в схему ENTRY. Как нам все это использовать? Очень просто:

SQL> connect ENTRY/ENTRY@orcl
Connected.
SQL> select * from main.users;
select * from main.users
                   *
ERROR at line 1:
ORA-00942: table or view does not exist


SQL> select MAIN.SECAUTH.GetSalt from dual;

GETSALT
----------------------------------------------
791A86ECB24B3CC72E214AF874C8BDA6

SQL> select MAIN.Login('admin','58A283B2DDAC84C122965216410B27') from dual;

MAIN.LOGIN('ADMIN','58A283B2DDAC84C122965216410B27')
----------------------------------------------------
                                                   1

SQL> select * from users;

        ID NM                                     TP SP
---------- ------------------------------ ---------- ---------
         1 admin                                   1

Зайдя в ENTRY, наша программа запросит уникальный Salt, затем выполнит MD5 преобразование, аналогично тому, как это делается в серверном коде и передаст результат в функцию Login.Функция вернет код результата авторизации, предоставит необходимые права и переключит текущую схему на MAIN.

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