Моделирование при помощи среды Eclipse Modeling Framework: Часть 3. Настройка сгенерированных моделей и редакторов с применением Eclipse JMerge

Настройка сгенерированных моделей и редакторов с применением Eclipse JMerge

Eclipse Modeling Framework содержит инструмент open source JMerge, который делает процесс генерации кода гибким и настраиваемым. В этой статье приводится пример, иллюстрирующий, как добавить JMerge в приложение и настроить его для разных ситуаций.

Адриан Пауэлл, старший разработчик ПО, IBM  

Адриан Пауэлл (Adrian Powell) начинал свою деятельность с разработки инструментария Java в отделении VisualAge IBM в качестве члена группы Java Enterprise Tooling. Он потерял два года, программируя генератор кода вручную. С тех пор Адриан разработал инструменты и подключаемые модули почти для каждой версии Eclipse и VisualAge для Java. Сейчас он работает в Центре инноваций электронного бизнеса IBM в Ванкувере, где создает замену самому себе.



07.07.2009

Обзор

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

JMerge — это инструмент open source, который входит в состав Eclipse Modeling Framework. JMerge позволяет настраивать сгенерированные модели и редакторы без нарушения внесенных изменений из-за регенерации кода. JETEmitter поддерживает JMerge, если описать, как объединить свежеесгенерированный код с уже существующим, настроенным кодом. В этой статье приводится пример, на котором иллюстрируются некоторые из имеющихся возможностей.


Первые шаги

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

Код для вызова JMerge из подключаемого модуля довольно прямолинеен (см. листинг 1). Создаем новый экземпляр JMerger с URI merge.xml, устанавливаем источник и цель и вызываем merger.merge(). После этого можно использовать объединенный код как merger.getTargetCompilationUnit().

Листинг 1. Вызов JMerge
	// ...
	JMerger merger = getJMerger();
	
	// set source
	merger.setSourceCompilationUnit(
		merger.createCompilationUnitForContents(generated));
	
	// set target
	merger.setTargetCompilationUnit(
		merger.createCompilationUnitForInputStream( 
			new FileInputStream(target.getLocation().toFile())));
	
	// merge source and target
	merger.merge();

	// extract merged contents
	InputStream mergedContents = new ByteArrayInputStream(
		merger.getTargetCompilationUnit().getContents().getBytes());
		
	// overwrite the target with the merged contents
	target.setContents(mergedContents, true, false, monitor);
	// ...

// ...
private JMerger getJMerger() {
	// build URI for merge document
	String uri = 
	   Platform.getPlugin(PLUGIN_ID).getDescriptor().getInstallURL().toString();
	uri += "templates/merge.xml";
		
	JMerger jmerger = new JMerger();
	JControlModel controlModel = new JControlModel( uri );
	jmerger.setControlModel( controlModel );
	return jmerger;
}

Для начала воспользуемся минимальным вариантом merge.xml (листинг 2). Он декларирует тег <merge> и назначает декларацию по умолчанию namespace. Основная часть заключена в элементе merge:pull. В данном случае тело каждого метода в источнике будет заменяться на тело соответствующего метода в цели. Если в цели метод не существует, он будет создан. Если в источнике метод присутствует, а в цели — нет, он останется один.

Листинг 2. Простейший merge.xml
<?xml version="1.0" encoding="UTF-8"?>
<merge:options xmlns:merge=
   "http://www.eclipse.org/org/eclipse/emf/codegen/jmerge/Options">

    <merge:pull 
      sourceGet="Method/getBody"
      targetPut="Method/setBody"/>

</merge:options>

Различение сгенерированных методов

Проблема этого простого подхода выясняется быстро: при каждом изменении источника и регенерации все ваши изменения теряются. Нужно добавить какой-то механизм, чтобы сообщить JMerge, что некоторые методы были отредактированы и не должны переписываться. Для этого воспользуемся элементом <merge:dictionaryPattern>. Он позволяет использовать регулярные выражения, чтобы различать элементы Java.

Листинг 3. Простой dictionaryPattern
<merge:dictionaryPattern
   name="generatedMember" 
   select="Member/getComment" 
   match="\s*@\s*(gen)erated\s*\n"/>

<merge:pull 
   targetMarkup="^gen$"
   sourceGet="Method/getBody"
   targetPut="Method/setBody"/>

dictionaryPattern определяет регулярное выражение, которое сопоставляет Member с "@generated" где-нибудь в элементе. Атрибут select определяет, какой аспект Member будет сравниваться с регулярным выражением, указанным в атрибуте match. dictionaryPattern идентифицируется по строке gen, которая помечается круглыми скобками в значении атрибута match.

Элемент merge:pull содержит дополнительный атрибут targetMarkup. Он совпадает с dictionaryPattern, который должен совмещаться с целевым кодом до применения правила слияния. Здесь мы проверяем целевой код, а не исходный, так как пользователи могут редактировать код. Когда пользователь удаляет тег "@generated" в комментарии, dictionaryPattern не будет совпадать с целевым кодом, и тело метода не будет объединяться.

Листинг 4. Редактирование кода
/**
 * test case for getName
 * @generated
 */
public void testSimpleGetName() {
	// because of the @generated tag,
	// any code in this method will be overridden
}

/**
 * test case for getName
 */
public void testSimpleSetName() {
	// code in this method will not be regenerated
}

Можно заметить, что некоторые элементы не должны редактироваться, и нужно исключить любую попытку их изменения. Для этого определим еще один dictionaryPattern, который будет следить за каким-нибудь другим тегом в исходном коде (а не в целевом), например, @unmodifiable. Затем определим правило pull, которое проверяет sourceMarkup, вместо targetMarkup, так что пользователь не сможет удалить тег и заблокировать слияние.

Листинг 5. merge.xml для немодифицируемого кода
<merge:dictionaryPattern
   name="generatedUnmodifiableMembers" 
   select="Member/getComment" 
   match="\s*@\s*(unmod)ifiable\s*\n"/>

<merge:pull 
   sourceMarkup="^unmod$"
   sourceGet="Member/getBody"
   targetPut="Member/setBody"/>

Детальная настройка

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

Для этого замените прежнюю цель pull на код из листинга 6.

Листинг 6. Детальная настройка кода
<!-- if target is generated, transfer -->
<!-- change to sourceMarkup if the source is the standard -->
<merge:pull 
   targetMarkup="^gen$"
   sourceGet="Method/getBody"
   sourceTransfer="(\s*//\s*begin-user-code.*?//\s*end-user-code\s*)\n"
   targetPut="Method/setBody"/>

В этом случае переписывается все до строки "// begin-user-code" и после "// end user-code", а весь настраиваемый промежуточный код сохраняется. В приведенном выше регулярном выражении знаками "?" помечен код, который останется в целевом коде, хотя все остальное будет заменено. Нечто аналогичное можно сделать с комментариями JavaDoc, чтобы сами комментарии копировались, но оставалось место для настройки пользователем.

Листинг 7. Детальная настройка JavaDoc
<!-- copy comments except between the begin-user-doc
     and end-user-doc tags -->
<merge:pull 
  sourceMarkup="^gen$"
  sourceGet="Member/getComment"
  sourceTransfer="(\s*<!--\s*begin-user-doc.*?end-user-doc\s*-->\s*)\n"
  targetMarkup="^gen$"
  targetPut="Member/setComment"/>

Чтобы поддерживать комментарии, сначала заменим теги начала и конца в соответствии с синтаксисом комментариев HTML, так чтобы они не появлялись в генерируемом JavaDoc, и заменим атрибуты sourceGet и targetPut на "Member/getComment" и "Member/setComment". JMerge позволяет удалять и добавлять многие разные аспекты кода Java на детальном уровне (см. Приложение A).


Следующие шаги

До сих пор мы рассматривали преобразование тела методов, но JMerge может управлять и полями, инициализаторами, исключениями, возвращаемыми значениями, операторами импорта и другими элементами. К ним применимы те же основные идеи, с небольшими изменениями. Из plugins/org.eclipse.emf.codegen_1.1.0/test/merge.xml видно, как их можно применять (я использую Eclipse 2.1, и для другой версии Eclipse версия подключаемого модуля ecore может быть другой). Это довольно простой пример без использования тега sourceTransfer, но он показывает один из способов работы с исключениями, флагами и другими элементами Java.

В качестве более сложного примера можно привести способ, которым EMF использует JMerge: plugins/org.eclipse.emf.codegen.ecore_1.1.0/templates/emf-merge.xml. Из него видно, что EMF позволяет лишь частично настраивать JavaDoc, но пользуясь приведенными выше советами, вы можете добавить собственную поддержку для тела методов (путем модификации шаблонов JET с добавлением новой разметки).


Приложение A: Допустимые цели

Внутри правил dictionaryPattern и pull мы использовали "Member/getComment" и "Member/getBody" и их установщики, но существует много других возможностей. JMerge поддерживает совмещение и замену для любого класса, определенного в org.eclipse.jdt.core.jdom.IDOM*. Возможные варианты приведены в табл. 1.

Таблица 1. Допустимые цели

ТипМетодКомментарий
CompilationUnitgetHeader/setHeader
getName/setName
FieldgetInitializer/setInitializerНе содержит "="
getName/setNameИмя переменной
getName/setNameИмя класса
ImportgetName/setNameПолностью подходящее имя типа или пакет по требованию
InitializergetName/setName
getBody/setBody
MembergetComment/setComment
getFlags/setFlagsПримеры: abstract, final, native, и т.д.
MethodaddException
addParameter
getBody/setBody
getName/setName
getParameterNames/setParameterNames
getParameterTypes/setParameterTypes
getReturnType/setReturnType
PackagegetName/setName
TypeaddSuperInterface
getName/setName
getSuperclass/setSuperclass
getSuperInterfaces/setSuperInterfaces

Приложение B: Атрибуты merge:pull

В табл. 2 приведены атрибуты элемента merge:pull.

Таблица 2. Атрибуты merge:pull

АтрибутУсловия
sourceGetОбязательный. Значение должно быть одним из вариантов, перечисленных в Приложении A, например, "Member/getBody".
targetPutОбязательный. Значение должно быть одним из вариантов, перечисленных в Приложении A, например, "Member/setBody".
sourceMarkupНеобязятельный. Используется для выбора того, какие dictionaryPatterns должны совпадать с источником перед запуском этого правила merge:pull. Используйте форму "^dictionaryName$" и соедините несколько dictionaryPatterns в одну строку с применением символа "|".
targetMarkupНеобязятельный. Используется для выбора того, какие dictionaryPatterns должны совпадать с целью перед запуском этого правила merge:pull. Используйте форму "^dictionaryName$" и соедините несколько dictionaryPatterns в одну строку с применением символа "|".
sourceTransferНеобязятельный. Регулярное выражение, которое определяет количество исходного кода, преобразуемого в целевой.

Загрузка

ОписаниеИмяРазмер
Образец кодаos-ecemf3/com.ibm.pdc.example.jet_1.0.0.zip---

Ресурсы

Научиться

Получить продукты и технологии

Обсудить

Комментарии

developerWorks: Войти

Обязательные поля отмечены звездочкой (*).


Нужен IBM ID?
Забыли Ваш IBM ID?


Забыли Ваш пароль?
Изменить пароль

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


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

Вся введенная информация защищена.

Выберите имя, которое будет отображаться на экране



При первом входе в developerWorks для Вас будет создан профиль и Вам нужно будет выбрать Отображаемое имя. Оно будет выводиться рядом с контентом, опубликованным Вами в developerWorks.

Отображаемое имя должно иметь длину от 3 символов до 31 символа. Ваше Имя в системе должно быть уникальным. В качестве имени по соображениям приватности нельзя использовать контактный e-mail.

Обязательные поля отмечены звездочкой (*).

(Отображаемое имя должно иметь длину от 3 символов до 31 символа.)

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Вся введенная информация защищена.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Open source, Технология Java
ArticleID=407075
ArticleTitle=Моделирование при помощи среды Eclipse Modeling Framework: Часть 3. Настройка сгенерированных моделей и редакторов с применением Eclipse JMerge
publish-date=07072009