ИКТ-решения и сервисы для органов власти и бизнеса
75,05
рейтинг
13 января в 16:16

Разработка → Введение в преобразование моделей (или преобразование, которое создаёт преобразование, которое создаёт модель) tutorial



Сегодня напишем преобразование, которое создаёт преобразование. Лично мне это напоминает «Начало» Кристофера Нолана, где люди видели сны во снах.

Это 7-ая статья цикла по модельно-ориентированной разработке. Я уже полгода пытаюсь написать статью с серьёзным примером разработки, управляемой моделями. Но каждый раз пониманию, что сначала необходимо рассказать о технологиях в целом, разобрать какой-нибудь очень простой пример. Так и в этот раз, хотел только начать статью с «Hello World», а в итоге этот простой пример вырос в здоровенную статью.

Введение


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

Query/View/Transformation (QVT)

QVT – это семейство предметно-ориентированных языков, которые позволяют описывать преобразования моделей. В спецификации OMG QVT описаны три языка:

  • QVT Core (QVTc) – декларативный язык преобразования моделей. Например, есть UML- и ER-модели. С помощью QVT Core вы можете «сказать», что классы в UML-моделях соответствуют сущностям в ER-моделях, свойства классов в UML-моделях соответствуют атрибутам в ER-моделях и т.д. Описав такое отображение, вы можете преобразовывать некоторые UML-модели в ER-модели или, наоборот, ER-модели в UML-модели – отображение двунаправленное.
  • QVT Relations (QVTr) – примерно то же самое, что и QVT Core, только с синтаксическим сахаром, который позволяет писать преобразования более компактно.
  • QVT Operational (QVTo) – в отличие от двух предыдущих языков этот уже императивный и позволяет описывать только однонаправленные преобразования моделей.

Первоначально QVTo был реализован компанией Borland. А с 2007-2008 годов он развивается в рамках Eclipse.

Для QVTc и QVTr также существует Eclipse-реализация. Планируется, что к релизу Eclipse Neon ей можно будет пользоваться. Но пока она не очень рабочая, поэтому в данной статье на ней останавливаться не будем.

Очень важно отметить, что QVTo-преобразования можно запускать не только в Eclipse, но и в самостоятельных Java-приложениях. У нас, например, преобразования прекрасно работают на стороне веб-сервера.

Можно рассматривать QVT как надстройку над OCL. Вы можете использовать в QVT операторы для навигации: «.» и «->». Можете использовать стандартную библиотеку OCL.

ATL Transformation Language (ATL)

ATL – это гибридный (декларативно-императивный) язык преобразования моделей, который возник параллельно с QVT. Он во многом похож на QVT, но есть и отличия.

Минусы:

  • Он не был стандартизирован в отличие от QVT, для которого есть спецификация OMG QVT.
  • Немного непривычный синтаксис, хотя это субъективно.
  • Собственная, упрощенная реализация OCL, которая в чём-то может не соответствовать спецификации OMG OCL и может быть не такой хорошей как Eclipse OCL.
  • Не всегда понятные сообщения об ошибках.
  • Не очень удобные редактор и отладчик.
  • Для запуска преобразования приходится писать ANT-скрипты.

Плюсы:

  • Eclipse OCL, который используется в QVT, хотя и хорош, но относительно тяжелый. ATL в целом выглядит более шустрым, чем QVTo.
  • Поддержка уточняющих (refining) преобразований. Если необходимо внести небольшие изменения в существующую модель, а не создавать новую, то ATL удобней. Теоретически на QVT тоже можно писать такие преобразования, но практически всё сложно.
  • Поддержка преобразований высшего порядка. ATL позволяет работать с ATL-преобразованиями как с моделями. Т.е. с помощью него можно преобразовать ATL-преобразование во что-нибудь или, наоборот, сформировать ATL-преобразование из чего-нибудь. Теоретически и QVT должен позволять делать это, но на практике всё не так просто.

Henshin

Henshin – это тоже язык преобразования моделей. Но в отличие от QVT и ATL визуальный, а не текстовый. Он основан на теории категорий. Модели рассматриваются как графы, которые можно преобразовывать с помощью методов двойного кодекартова квадрата (double pushout — DPO) или одиночного кодекартова квадрата (single pushout — SPO).

Честно говоря, это безумно интересная тема, и в одной из следующих статей мы, наверное, вернёмся к теории категорий.

Henshin позволяет запускать преобразования на Apache Giraph, что позволяет преобразовывать очень большие модели. Хотя я никогда этого не делал, в основном я использую QVTo.

Другие инструменты

Также для преобразования моделей существуют и другие инструменты: Epsilon Transformation Language, EMorF, AGG (The Attributed Graph Grammar System), VIATRA (VIsual Automated model TRAnsformations) и т.п. Все они решают аналогичные задачи и основаны на аналогичных принципах. Если вам не подходит QVTo, ATL или Henshin, то альтернатив достаточно много.

Отдельно стоит отметить XSLT. Когда речь заходит о преобразованиях, то многие его вспоминают. Я и сам в начале 2000-х, на заре возникновения XSLT писал на нём и PHP движок для сайта. Он люто тормозил и требовал какое-то немыслимое по тем временам количество оперативной памяти. Но суть не в этом, технология не плохая, я до сих пор пишу на XSLT какие-то простенькие преобразования.

Первая проблема XSLT заключается в том, что он заточен именно на преобразование XML-документов. Конечно можно сериализовать модель в виде XML-документа и скормить её XSLT. Более того, есть даже соответствующая спецификация OMG XMI (далее будет пример XMI-файла). Но, во-первых, есть разные способы уложить модель в XML-файл. Например, атрибуты могут сериализовываться в виде XML-атрибутов или XML-элементов. Такая вариативность сильно усложняет XSLT-преобразования. Во-вторых, модель – это граф, а не дерево, обычно в моделях полно горизонтальных связей, в том числе и межмодельных. Искать нужные объекты по таким ссылкам – просто лютый ад. А если вспомнить ещё профили и стереотипы в UML-моделях, то это уже 9-ый круг ада.

Вторая проблема XSLT – это XML-синтаксис, он просто не удобен.

Наконец, в нормальных инструментах преобразования моделей есть вещи, которые создателям XSLT даже и не снились. Например, журнал преобразования и отложенное разрешение ссылок в QVTo, о чём я вскользь упомяну позже (в разделе «Отладка»).

Если мои доводы не достаточно убедительны, то в разделе «Пишем преобразование, которое за нас напишет «Hello World»-преобразование» как-раз есть пример модели в XMI-формате. А далее приводится ATL-преобразование, которое генерит такую модель. Представьте как выглядело бы аналогичное XSLT-преобразование.

Кстати, Eclipse Modeling Framework позволяет увязывать XML-схемы и Ecore-метамодели. Это позволяет преобразовывать почти произвольные XML-файлы в Ecore-модели и наоборот. Что, в свою очередь, позволяет использовать QVTo, ATL и другие инструменты для преобразования XML-документов. Возможно, мы рассмотрим такой пример в одной из следующих статей.

QVT Operational


Пишем «Hello world»

Как обычно, будем использовать Eclipse Modeling Tools.

Установите Operational QVT и ATL (Help -> Install Modeling Components).

Установите Henshin (Help -> Install New Software…) с сайта http://download.eclipse.org/modeling/emft/henshin/updates/release.

Вы можете взять готовый проект или создать новый (File -> New -> Other… -> Operational QVT Project).



Создайте новое преобразование (File -> New -> Other… -> Operational QVT Transformation).

Дополните оператор main следующим образом:

transformation HelloWorld1();

main() {
    log('Hello world');
}

Создайте конфигурацию для запуска преобразования (Run -> Run Configurations…) и запустите его:



Вы должны увидеть что-то подобное:



Пишем модельно-ориентированный «Hello world»

Для действительно модельно-ориентированного «Hello world» нужна тестовая модель, но в предыдущих статьях мы создали уже достаточно моделей. Хватит тратить на это время, пусть QVTo сам создаст её:

modeltype ECORE 'strict' uses 'http://www.eclipse.org/emf/2002/Ecore';

transformation HelloWorld2(out o : ECORE);

main() {
    object EPackage {
        name := 'World';
        eClassifiers += object EClass { name := 'Alice'; };
        eClassifiers += object EClass { name := 'Bob'; };
    };
}

Сначала (с помощью оператора modeltype) необходимо указать метамодель создаваемой модели. Затем, в третьей строке укажем, что у преобразования есть одна выходная модель. И, наконец, в оператор main добавим немного кода.

Интуитивно понятно, что преобразование создаёт пакет World с двумя классами (Alice и Bob).

Если вы кликните через Ctrl на имя класса или свойства, то откроете метамодель, в которой они определены. Также вы обнаружите рядом ещё сотню-другую разных метамоделей:



Примечание

Здесь и далее я не буду слишком подробно описывать синтаксис языка. С ним можно познакомиться в спецификации или в этой презентации. В следующей статье рассмотрим QVTo более подробно.

В конфигурации для запуска преобразования необходимо указать файл, в который будет сохраняться генерируемая модель:



После запуска вы получите такую модель:



Пишем труЪ модельно-ориентированный «Hello world»

Теперь преобразуем тестовую модель в новую модель. В 3-ей строке укажите, что у преобразования есть не только выходная, но и входная Ecore-модель. И добавьте немного кода:

modeltype ECORE 'strict' uses 'http://www.eclipse.org/emf/2002/Ecore';

transformation HelloWorld3(in i : ECORE, out o : ECORE);

main() {
    i.rootObjects()[EPackage]->toEPackage();
}

mapping EPackage::toEPackage() : EPackage
{
    name := 'Hello' + self.name;
    eClassifiers := self.eClassifiers->toEDataType();
}

mapping EClassifier::toEDataType() : EDataType
{
    name := 'Hello' + self.name;
    instanceClassName := 'some.ns.' + self.name + 'Class';
    serializable := false;
}

Суть преобразования следующая. Ищем во входной модели все корневые пакеты и преобразуем их тоже в пакеты, но другие. К имени добавляем префикс «Hello», а все классификаторы преобразуем в типы данных. Имена типов данных будут так же начинаться с префикса «Hello», ну, и, до кучи, установим ещё пару свойств.

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

После запуска у вас должно получиться что-то подобное:



Отладка

В Eclipse QVTo есть отладчик преобразований, который помимо обычных вещей показывает входные и выходные объекты всех отображений. Дело в том, что движок QVTo ведёт подробный журнал преобразования модели (на рисунке справа сверху). И нужно это не только для отладки. Во-первых, при повторном отображении тех же самых объектов результат будет взят из этого журнала (кэша). Это, кстати, позволяет преобразовывать модели инкрементально. Во-вторых, в коде можно явно обращаться к журналу с помощью операции resolve. А с помощью late resolve можно ссылаться на записи журнала, которых ещё нет! Журнал – это одна из ключевых фич QVTo.



ATL Transformation Language


Перепишем труЪ модельно-ориентированный «Hello world»

Теперь перепишем последнее преобразование на языке ATL. Создайте новый проект (File -> New -> Other… -> ATL Project). Создайте новое преобразование (File -> New -> Other… -> ATL File). Видно, что ATL очень похож на QVTo:

-- @nsURI Ecore = http://www.eclipse.org/emf/2002/Ecore

module HelloWorld3;
create OUT : Ecore from IN : Ecore;

rule toEPackage
{
    from
        package : Ecore!EPackage
    to 
        newPackage : Ecore!EPackage (
            name <- 'Hello' + package.name,
            eClassifiers <- package.eClassifiers
        )
}

rule toEDataType
{
    from
        classifier : Ecore!EClassifier
    to 
        dataType : Ecore!EDataType (
            name <- 'Hello' + classifier.name,
            instanceClassName <- 'some.ns.' + classifier.name + 'Class',
            serializable <- false
        )
}

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

Примечание

К созданию ATL приложился INRIA и, честно говоря, это можно увидеть в синтаксисе :) От их Caml у меня тоже двоится в глазах. Отличная контора, но есть ощущение, что их языки отличаются от других языков также как французский отличается от английского. Буквы вроде похожи, но что-то не так.

После сохранения преобразования в проекте должен появиться asm-файл. Это то же самое преобразование, но в форме, предназначенной для запуска на виртуальной машине ATL.

Примечание

Иногда бывает, что вы что-то меняете в преобразовании, но оно работает по-старому. В этом случае удалите asm-файл и, если он автоматически не пересоздастся, значит в преобразовании что-то не так. Например, я использовал в преобразовании оператор drop, который поддерживается только в новой версии компилятора ATL, которую нужно явно включать с помощью специальной директивы. При этом я не получал никаких ошибок, а asm-файл просто молча не перегенерировался.

Можно запустить это преобразование с помощью Run -> Run Configurations… Однако, у такого способа есть некоторые ограничения, поэтому напишем для запуска сразу ANT-скрипт.

Создайте в проекте файл build.xml со следующим содержимым:

<?xml version="1.0"?>
<project name="HelloWorldATL">
    <target name="HelloWorld3">
        <!-- Loading metamodels -->
        <atl.loadModel name="Ecore" metamodel="MOF" nsURI="http://www.eclipse.org/emf/2002/Ecore" />
        <!-- Loading models -->
        <atl.loadModel name="IN" metamodel="Ecore" path="output/MyModel2.xmi" />
        <!-- Transformation -->
        <atl.launch path="HelloWorld3.atl">
            <inmodel name="IN" model="IN" />
            <outmodel name="OUT" model="OUT" metamodel="Ecore" />
        </atl.launch>
        <!-- Saving models -->
        <atl.saveModel model="OUT" path="output/MyModel3.xmi" />
    </target>
</project>

Тестовую входную модель можно взять из QVTo-проекта.

Создайте конфигурацию для запуска (Run -> External Tools -> External Tools Configurations…).



Укажите путь к build.xml.

На вкладке Targets отметьте HelloWorld3.

И, самое главное, на вкладке JRE выберите «Run in the same JRE as the workspace», иначе получите ошибку «The name is undefined».

После запуска вы должны увидеть что-то подобное:



Пишем уточняющее преобразование

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

-- @atlcompiler atl2010
-- @nsURI Ecore = http://www.eclipse.org/emf/2002/Ecore

module HelloWorld4;
create OUT : Ecore refining IN : Ecore;

rule sayHello
{
    from
        s : Ecore!ENamedElement (s.name <> 'Bob')
    to
        t : Ecore!ENamedElement (
            name <- 'Hello' + s.name
        )
}

rule killBob
{
    from
        s : Ecore!ENamedElement (s.name = 'Bob')
    to
        drop
}

Скрипт для запуска.
<?xml version="1.0"?>
<project name="HelloWorldATL">
    <target name="HelloWorld4">
        <!-- Loading metamodels -->
        <atl.loadModel name="Ecore" metamodel="MOF" nsURI="http://www.eclipse.org/emf/2002/Ecore" />
        <!-- Loading models -->
        <atl.loadModel name="IN" metamodel="Ecore" path="output/MyModel2.xmi" />
        <!-- Transformation -->
        <atl.launch path="HelloWorld4.atl" refining="true">
            <inoutmodel name="IN" model="IN" />
        </atl.launch>
        <!-- Saving models -->
        <atl.saveModel model="IN" path="output/MyModel4.xmi" />
    </target>
</project>


Модель сохраняется в отдельный файл, однако по структуре она повторяет исходную модель за исключением описанных в преобразовании изменений.

Пишем преобразование, которое за нас напишет «Hello World»-преобразование

Я думаю, настало время для небольшого выноса мозга. Преобразования, которые за нас создают или изменяют модели мы уже написали. Осталось написать преобразование, которое за нас напишет преобразование.

Дело в том, что преобразования сами являются моделями. Чтобы убедиться в этом напишем небольшое преобразование, которое персонально приветствует Алису и Боба:

-- @nsURI Ecore = http://www.eclipse.org/emf/2002/Ecore

module HelloWorld5;
create OUT : Ecore from IN : Ecore;

rule SayHelloToAlice {
    from
        classifier : Ecore!EClassifier (
            classifier.name = 'Alice'
        )
    to
        datatype : Ecore!EDataType (
            name <- 'Hello' + classifier.name
        )
}

rule SayHelloToBob {
    from
        classifier : Ecore!EClassifier (
            classifier.name = 'Bob'
        )
    to
        datatype : Ecore!EDataType (
            name <- 'Hello' + classifier.name
        )
}

С помощью этого скрипта сохраните преобразование в XMI-формате.
<?xml version="1.0"?>
<project name="HelloWorldATL">
    <target name="ATLCopy">
        <!-- Loading metamodels -->
        <atl.loadModel name="ATL" metamodel="MOF" nsURI="platform:/plugin/org.eclipse.m2m.atl.common/org/eclipse/m2m/atl/common/resources/ATL.ecore" />
        <!-- Loading models -->
        <atl.loadModel name="IN" metamodel="ATL" path="HelloWorld5.atl">
            <injector name="ATL" />
        </atl.loadModel>
        <!-- Saving models -->
        <atl.saveModel model="IN" path="output/HelloWorld5.atl.xmi" />
    </target>
</project>


В итоге вы получите такую модель.
<?xml version="1.0" encoding="ISO-8859-1"?>
<xmi:XMI xmi:version="2.0" xmlns:xmi="http://www.omg.org/XMI" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:atl="http://www.eclipse.org/gmt/2005/ATL" xmlns:ocl="http://www.eclipse.org/gmt/2005/OCL">
  <atl:Module location="3:1-26:2" name="HelloWorld5">
    <commentsBefore>-- @nsURI Ecore = http://www.eclipse.org/emf/2002/Ecore</commentsBefore>
    <inModels location="4:25-4:35" name="IN" metamodel="/2"/>
    <outModels location="4:8-4:19" name="OUT" metamodel="/1"/>
    <elements xsi:type="atl:MatchedRule" location="6:1-15:2" name="SayHelloToAlice">
      <outPattern location="11:2-14:4">
        <elements xsi:type="atl:SimpleOutPatternElement" location="12:3-14:4" varName="datatype">
          <type xsi:type="ocl:OclModelElement" location="12:14-12:29" name="EDataType" model="/4"/>
          <bindings location="13:4-13:37" propertyName="name">
            <value xsi:type="ocl:OperatorCallExp" location="13:12-13:37" operationName="+">
              <source xsi:type="ocl:StringExp" location="13:12-13:19" stringSymbol="Hello"/>
              <arguments xsi:type="ocl:NavigationOrAttributeCallExp" location="13:22-13:37" name="name">
                <source xsi:type="ocl:VariableExp" location="13:22-13:32" referredVariable="/0/@elements.0/@inPattern/@elements.0"/>
              </arguments>
            </value>
          </bindings>
        </elements>
      </outPattern>
      <inPattern location="7:2-10:4">
        <elements xsi:type="atl:SimpleInPatternElement" location="8:3-8:33" varName="classifier" variableExp="/0/@elements.0/@inPattern/@filter/@source/@source /0/@elements.0/@outPattern/@elements.0/@bindings.0/@value/@arguments.0/@source">
          <type xsi:type="ocl:OclModelElement" location="8:16-8:33" name="EClassifier" model="/3"/>
        </elements>
        <filter xsi:type="ocl:OperatorCallExp" location="9:4-9:29" operationName="=">
          <source xsi:type="ocl:NavigationOrAttributeCallExp" location="9:4-9:19" name="name">
            <source xsi:type="ocl:VariableExp" location="9:4-9:14" referredVariable="/0/@elements.0/@inPattern/@elements.0"/>
          </source>
          <arguments xsi:type="ocl:StringExp" location="9:22-9:29" stringSymbol="Alice"/>
        </filter>
      </inPattern>
    </elements>
    <elements xsi:type="atl:MatchedRule" location="17:1-26:2" name="SayHelloToBob">
      <outPattern location="22:2-25:4">
        <elements xsi:type="atl:SimpleOutPatternElement" location="23:3-25:4" varName="datatype">
          <type xsi:type="ocl:OclModelElement" location="23:14-23:29" name="EDataType" model="/6"/>
          <bindings location="24:4-24:37" propertyName="name">
            <value xsi:type="ocl:OperatorCallExp" location="24:12-24:37" operationName="+">
              <source xsi:type="ocl:StringExp" location="24:12-24:19" stringSymbol="Hello"/>
              <arguments xsi:type="ocl:NavigationOrAttributeCallExp" location="24:22-24:37" name="name">
                <source xsi:type="ocl:VariableExp" location="24:22-24:32" referredVariable="/0/@elements.1/@inPattern/@elements.0"/>
              </arguments>
            </value>
          </bindings>
        </elements>
      </outPattern>
      <inPattern location="18:2-21:4">
        <elements xsi:type="atl:SimpleInPatternElement" location="19:3-19:33" varName="classifier" variableExp="/0/@elements.1/@inPattern/@filter/@source/@source /0/@elements.1/@outPattern/@elements.0/@bindings.0/@value/@arguments.0/@source">
          <type xsi:type="ocl:OclModelElement" location="19:16-19:33" name="EClassifier" model="/5"/>
        </elements>
        <filter xsi:type="ocl:OperatorCallExp" location="20:4-20:27" operationName="=">
          <source xsi:type="ocl:NavigationOrAttributeCallExp" location="20:4-20:19" name="name">
            <source xsi:type="ocl:VariableExp" location="20:4-20:14" referredVariable="/0/@elements.1/@inPattern/@elements.0"/>
          </source>
          <arguments xsi:type="ocl:StringExp" location="20:22-20:27" stringSymbol="Bob"/>
        </filter>
      </inPattern>
    </elements>
  </atl:Module>
  <ocl:OclModel location="4:14-4:19" name="Ecore" model="/0/@outModels.0"/>
  <ocl:OclModel location="4:30-4:35" name="Ecore" model="/0/@inModels.0"/>
  <ocl:OclModel location="8:16-8:21" name="Ecore" elements="/0/@elements.0/@inPattern/@elements.0/@type"/>
  <ocl:OclModel location="12:14-12:19" name="Ecore" elements="/0/@elements.0/@outPattern/@elements.0/@type"/>
  <ocl:OclModel location="19:16-19:21" name="Ecore" elements="/0/@elements.1/@inPattern/@elements.0/@type"/>
  <ocl:OclModel location="23:14-23:19" name="Ecore" elements="/0/@elements.1/@outPattern/@elements.0/@type"/>
</xmi:XMI>


Примечание

К сожалению, из-за одной багофичи ATL в ANT-скрипте приходится писать какой-то сомнительный путь к ATL.ecore. А полученный XMI-файл нельзя открыть в нормальном древовидном редакторе моделей, потому что пространства имен www.eclipse.org/gmt/2005/ATL и www.eclipse.org/gmt/2005/OCL не зарегистрированы в Eclipse. Это можно исправить, но не будем отвлекаться, для целей статьи это не принципиально. Главное, что вы видите, что ATL-преобразование можно представить в виде модели.

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

-- @nsURI Ecore = http://www.eclipse.org/emf/2002/Ecore
-- @path  ATL = platform:/plugin/org.eclipse.m2m.atl.common/org/eclipse/m2m/atl/common/resources/ATL.ecore

module GenerateHelloWorld;
create OUT : ATL from IN : Ecore;

rule EPackageToModule {
    from
        package : Ecore!EPackage
    to
        _module : ATL!Module (
            name <- 'HelloWorld5',
            inModels <- thisModule.createEcoreModel('IN'),
            outModels <- thisModule.createEcoreModel('OUT'),
            elements <- package.eClassifiers
        )
}

rule EClassifierToRule {
    from
        classifier : Ecore!EClassifier
    to
        _rule : ATL!MatchedRule (
            name <- 'SayHelloTo' + classifier.name,
            inPattern <- _in,
            outPattern <- _out
        ),
        -- InPattern
        _in : ATL!InPattern (
            elements <- inElement,
            filter <- inFilter
        ),
        inElement : ATL!SimpleInPatternElement (
            varName <- 'classifier',
            type <- thisModule.createEcoreModelElement('EClassifier')
        ),
        inFilter : ATL!"OCL::OperatorCallExp" (
            operationName <- '=',
            source <- thisModule.createAttributeCallExp(inElement, 'name'),
            arguments <- thisModule.createStringExp(classifier.name)
        ),
        -- OutPattern
        _out : ATL!OutPattern (
            elements <- outElement
        ),
        outElement : ATL!SimpleOutPatternElement (
            varName <- 'datatype',
            type <- thisModule.createEcoreModelElement('EDataType'),
            bindings <- nameBinding
        ),
        nameBinding : ATL!Binding (
            propertyName <- 'name',
            value <- helloPrefixOperatorExp
        ),
        helloPrefixOperatorExp : ATL!"OCL::OperatorCallExp" (
            operationName <- '+',
            source <- thisModule.createStringExp('Hello'),
            arguments <- thisModule.createAttributeCallExp(inElement, 'name')
        )
}

lazy rule createEcoreModel {
    from
        name : String
    to
        model : ATL!OclModel (
            name <- name,
            metamodel <- ecoreMM
        ),
        ecoreMM : ATL!OclModel (
            name <- 'Ecore'
        )
}

lazy rule createEcoreModelElement {
    from
        name : String
    to
        element : ATL!"OCL::OclModelElement" (
               model <- model,
            name <- name
        ),
        model : ATL!OclModel (
            name <- 'Ecore'
        )
}

lazy rule createAttributeCallExp {
    from
        var : ATL!SimpleInPatternElement,
        name : String
    to
        expr : ATL!"OCL::NavigationOrAttributeCallExp" (
            name <- name,
            source <- variableExp
        ),
        variableExp : ATL!"OCL::VariableExp" (
            referredVariable <- var
        )
}

lazy rule createStringExp {
    from
        str : String
    to
        expr : ATL!"OCL::StringExp" (
            stringSymbol <- str
        )
}

Представьте как выглядело бы аналогичное XSLT-преобразование.

Скрипт для запуска.
<?xml version="1.0"?>
<project name="HelloWorldATL">
    <target name="GenerateHelloWorld">
        <!-- Loading metamodels -->
        <atl.loadModel name="Ecore" metamodel="MOF" nsURI="http://www.eclipse.org/emf/2002/Ecore" />
        <atl.loadModel name="ATL" metamodel="MOF" nsURI="platform:/plugin/org.eclipse.m2m.atl.common/org/eclipse/m2m/atl/common/resources/ATL.ecore" />
        <!-- Loading models -->
        <atl.loadModel name="IN" metamodel="Ecore" path="output/MyModel2.xmi" />
        <!-- Transformation -->
        <atl.launch path="GenerateHelloWorld.atl">
            <inmodel name="IN" model="IN" />
            <outmodel name="OUT" model="OUT" metamodel="ATL" />
        </atl.launch>
        <!-- Saving models -->
        <atl.saveModel model="OUT" path="output/HelloWorld5.atl">
            <extractor name="ATL" />
        </atl.saveModel>
    </target>
</project>


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

Пишем преобразование, которое напишет преобразование, которое напишет преобразование...

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

Henshin


Чтобы немного прийти в себя после жуткого синтаксиса ATL, нарисуем преобразование мышкой.

Создайте новый проект: обычный или Java (File -> New -> Other… -> Java Project).

Создайте Henshin-диаграмму, мастер создания также предложит вам создать и модель. К слову, в статье про Sirius мы учились создавать подобные редакторы диаграмм.

Создайте такое преобразование:



Смысл должен быть интуитивно понятен. Сначала убиваем Боба и внедряем Карлоса. Потом приветствуем выживших.

Примечание

Честно говоря, к редактору диаграмм нужно привыкнуть. Например, если вам не удаётся изменить порядок правил в Sequential Unit, то вы можете изменить его в древовидном редакторе модели.

Слева в дереве проектов вызовите контекстное меню у henshin-файла. И выберите Henshin -> Apply Transformation. Запускать можно как отдельные правила, так и модули.

В качестве модели можете указать тестовую модель, которую для нас ранее любезно сгенерило QVTo-преобразование. Обратите внимание на то, что мастер предлагает сравнить модели после преобразования.

Если всё нормально, то после запуска вы увидите что-то подобное:



Как видите, переименование Алисы, убийство Боба и внедрение в модель Карлоса не остались незамеченными.

Но, скорее всего, вы увидите что-то подобное:



Если это произошло, то чтобы разобраться в причине ошибки придётся запустить преобразование вручную с помощью подобного класса:

HelloWorldHenshin.Main
package HelloWorldHenshin;

import org.eclipse.emf.henshin.interpreter.EGraph;
import org.eclipse.emf.henshin.interpreter.Engine;
import org.eclipse.emf.henshin.interpreter.UnitApplication;
import org.eclipse.emf.henshin.interpreter.impl.EGraphImpl;
import org.eclipse.emf.henshin.interpreter.impl.EngineImpl;
import org.eclipse.emf.henshin.interpreter.impl.UnitApplicationImpl;
import org.eclipse.emf.henshin.model.Module;
import org.eclipse.emf.henshin.model.resource.HenshinResourceSet;

public class Main {

    public static void main(String[] args) {
        HenshinResourceSet resourceSet = new HenshinResourceSet("model");
        Module module = resourceSet.getModule("HelloWorld.henshin", false);
        EGraph graph = new EGraphImpl(resourceSet.getResource("MyModel2.xmi"));
        Engine engine = new EngineImpl();

        UnitApplication app = new UnitApplicationImpl(engine);
        app.setEGraph(graph);
        app.setUnit(module.getUnit("main"));

        if (!app.execute(null)) {
            throw new RuntimeException("Execution error");
        }

        resourceSet.saveEObject(graph.getRoots().get(0), "MyModel3.xmi");
    }

}


В этом случае есть шанс увидеть более осмысленное сообщение об ошибке.

Рекомендую посмотреть примеры Henshin-преобразований. Особенно про треугольник Серпинского и обедающих философов.

Также есть несколько аналогичных инструментов: EMorF, AGG, VIATRA и другие.

Заключение


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

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

Увидели интересное практическое применение теории категорий (SPO, DPO), к которой возможно вернёмся позже.

В очередной раз услышали о некоторых спецификациях Object Management Group (XMI, OCL, QVT, UML).

Вскользь познакомились с инструментом сравнения моделей EMF Compare.

Исходный код доступен тут.

В следующей статье я опишу уже реальное и сложное QVTo-преобразование.
Автор: @Ares_ekb
ИКТ-решения и сервисы для органов власти и бизнеса

Комментарии (4)

  • +3
    Думаю многие были бы признательны, если бы вы ещё написали статью с объяснением самых основ и назначения всего этого «на уровне пятилетнего ребёнка». Первая статья про OCL очень хорошая, однако предполагает что читатель представляет назначение этого всего.

    P.S. В википедии статьи про OCL на русском нет.
    • 0
      Спасибо!

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

      А заключительная статья будет про модельно-ориентированную разработку в целом (что это и зачем).
      • 0
        Спасибо! Буду ждать.
  • 0
    В разделе про Henshin не сразу сообразил, как нарисовать диаграмму :) Может, я что-то пропустил, но раздела «ecore» в палитре инструментов у меня не оказалось. Как выяснилось, надо щёлкнуть правой кнопкой в поле диаграммы, выбрать Import Package -> From Registry и в появившемся окне импортировать пакет http://www.eclipse.org/emf/2002/Ecore, чтобы этот раздел появился. Либо то же самое можно было сделать на последнем шаге создания диаграммы.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.

Самое читаемое Разработка