Введение в преобразование моделей (или преобразование, которое создаёт преобразование, которое создаёт модель)13.01.2016 17:03
Сегодня напишем преобразование, которое создаёт преобразование. Лично мне это напоминает «Начало» Кристофера Нолана, где люди видели сны во снах.
Это 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).
Создайте конфигурацию для запуска преобразования (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-модель. И добавьте немного кода:
Суть преобразования следующая. Ищем во входной модели все корневые пакеты и преобразуем их тоже в пакеты, но другие. К имени добавляем префикс «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 со следующим содержимым:
Тестовую входную модель можно взять из QVTo-проекта.
И, самое главное, на вкладке 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
}
Скрипт для запуска.
Модель сохраняется в отдельный файл, однако по структуре она повторяет исходную модель за исключением описанных в преобразовании изменений.Пишем преобразование, которое за нас напишет «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-формате.
К сожалению, из-за одной багофичи 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-преобразование.
Скрипт для запуска.
После запуска преобразования вы получите на выходе преобразование, с которого мы начали этот подраздел (с правилами 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-преобразование.