Статьи

Xtext 2.1: использование выражений Xbase

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

Если вы посмотрите на Пять простых шагов к языку JVM , вы увидите мощь Xbase! Более того, в Xtext 2.1 использование Xbase еще проще, поскольку, написав только AbstractModelInferrer, вы получите полную интеграцию с Java (путем сопоставления элементов вашей модели AST с концепциями Java) и генератор (который создаст код Java, соответствующий этому отображению). ). Таким образом, вам даже не понадобится генератор вообще.

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

  • как интегрировать выражения Xbase (XExpression) в ваш DSL
  • написать генератор для вашего DSL и повторно использовать XbaseCompiler для кода XExpressions

Следуя духу предыдущего поста , я не буду использовать пример Domainmodel, но что-то действительно простое, например пример Greeting, т. Е. Базовый DSL, который вы получите при создании проекта Xtext внутри Eclipse. В частности, вы можете захотеть просмотреть предыдущий пост перед прочтением этого поста (поскольку мы повторно используем некоторые из представленных здесь концепций).

Итак, прежде всего, создайте новый проект Xtext; Я назову этот проект org.xtext.example.helloxbase (и файлы будут иметь расширение helloxbase ). Теперь мы можем начать генерировать артефакты Xtext. С настройками по умолчанию вы также получите HelloXbaseGenerator.xtend , который мы сейчас генерируем для кода Java. Первая реализация генератора: для каждого элемента Greeting мы получим пакет и класс, названный в соответствии с функцией имени Greeting (упаковывать все строчные буквы и имя класса с заглавной буквой). Кроме того, сгенерированный метод main выведет соответствующее приветствие:

package org.xtext.example.helloxbase.generator

import org.eclipse.emf.ecore.resource.Resource
import org.eclipse.xtext.generator.IFileSystemAccess
import org.eclipse.xtext.generator.IGenerator
import org.xtext.example.helloxbase.helloXbase.Greeting

import static extension org.eclipse.xtext.xtend2.lib.ResourceExtensions.*

class HelloXbaseGenerator implements IGenerator {

	override void doGenerate(Resource resource, IFileSystemAccess fsa) {
		for(greeting: resource.allContentsIterable.filter(typeof(Greeting))) {
			fsa.generateFile(
				greeting.packageName + "/" + // package
				greeting.className + ".java", // class name
				greeting.compile)
		}
	}

	def compile(Greeting greeting) '''
	package «greeting.packageName»;

	public class «greeting.className» {
		public static void main(String args[]) {
			System.out.println("Hello «greeting.name»");
		}
	}
	'''

	def className(Greeting greeting) {
		greeting.name.toFirstUpper
	}

	def packageName(Greeting greeting) {
		greeting.name.toLowerCase
	}
}

Если вы уже знакомы с Xtend2 (или читали предыдущий пост), вы должны иметь представление о том, что делает генератор; давайте попробуем генератор: перезапустите ваш второй экземпляр eclipse и убедитесь, что в проекте, который вы создали с помощью файла helloxbase, у вас есть папка с именем src-gen , и эта папка настроена как исходная папка. Если вы наберете что-то вроде

Hello foo!
Hello bar!

Вы должны увидеть некоторый код Java, сгенерированный в папке src-gen (согласно схеме, описанной выше).

Теперь идея состоит в том, чтобы обогатить приветствующий DSL HelloXbase некоторыми выражениями Xbase, поэтому мы изменим грамматику языка (HelloXbase.xtext) следующим образом (вы должны быть знакомы с функциями импорта Xtext):

grammar org.xtext.example.helloxbase.HelloXbase with
	org.eclipse.xtext.xbase.Xbase

generate helloXbase "http://www.xtext.org/example/helloxbase/HelloXbase"

Model:
	imports += Import*
	greetings+=Greeting*;

Import:
  'import' importedNamespace = QualifiedNameWithWildcard
;

QualifiedNameWithWildcard:
  QualifiedName '.*'?
; 

Greeting:
    'Hello' name=ID 'from' expression = XExpression '!'
;

Обратите внимание, что теперь мы получаем нашу грамматику из грамматики Xbase, поэтому теперь мы можем полагаться на правила грамматики Xbase; идея состоит в том, чтобы иметь возможность написать в нашем DSL что-то вроде

Попробуйте сами: восстановите артефакты Xtext и перезапустите экземпляр eclipse во время выполнения. Чтобы полностью насладиться синтаксисом Xbase, убедитесь, что в проекте, который вы создали в рабочей среде времени выполнения, у вас также есть org.eclipse.xtext.xbase.lib в качестве зависимости в MANIFEST.

Вы можете увидеть много интересных вещей здесь! В основном у вас есть весь мощный Java-подобный синтаксис выражений Xbase, доступ к статическим методам с использованием ::, замыканий и методов расширения ( nullOrEmpty из StringExtensions )! Все бесплатно!

Теперь, что случилось с нашим генератором? Это все еще вызывается автоматически при изменении ресурса? Боюсь, что нет: как только вы вывели свою грамматику из Xbase, в вашем проекте появился новый файл HelloXbaseJvmModelInferrer.xtend , поскольку в Xtext 2.1, как было указано в начале, стратегия генерации изменилась. В частности, в src-gen AbstractHelloXbaseRuntimeModule IGenerator теперь связан с org.eclipse.xtext.xbase.compiler.JvmModelGenerator ; так как мы хотим вернуть наш собственный генератор, мы просто переопределяем эту привязку в HelloXbaseRuntimeModule (той, которую нам разрешено изменять):

public class HelloXbaseRuntimeModule extends org.xtext.example.helloxbase.AbstractHelloXbaseRuntimeModule {

	/**
	 * Avoids to use the default org.eclipse.xtext.xbase.compiler.JvmModelGenerator
	 * when using xbase.
	 * @see org.xtext.example.helloxbase.AbstractHelloXbaseRuntimeModule#bindIGenerator()
	 */
	@Override
	public Class<? extends IGenerator> bindIGenerator() {
		return HelloXbaseGenerator.class;
	}

}

Если вы перезапустите свой экземпляр времени выполнения, вы увидите, что наш генератор вернулся!

Теперь мы хотим сгенерировать код Java, соответствующий Xbase XExpression; Затем мы просто повторно используем XbaseCompiler в нашем генераторе (что, как вы знаете, в Xtext означает «впрыскивать его»). В генераторе мы теперь будем использовать также ImportManager (который объясняется в предыдущем посте ) и начнем проводить некоторые эксперименты с XbaseCompiler:

package org.xtext.example.helloxbase.generator

import com.google.inject.Inject
import org.eclipse.emf.ecore.resource.Resource
import org.eclipse.xtext.common.types.TypesFactory
import org.eclipse.xtext.generator.IFileSystemAccess
import org.eclipse.xtext.generator.IGenerator
import org.eclipse.xtext.xbase.XExpression
import org.eclipse.xtext.xbase.compiler.ImportManager
import org.eclipse.xtext.xbase.compiler.StringBuilderBasedAppendable
import org.eclipse.xtext.xbase.compiler.XbaseCompiler
import org.xtext.example.helloxbase.helloXbase.Greeting

import static extension org.eclipse.xtext.xtend2.lib.ResourceExtensions.*

class HelloXbaseGenerator implements IGenerator {

	@Inject
	protected XbaseCompiler xbaseCompiler

	override void doGenerate(Resource resource, IFileSystemAccess fsa) {
		for(greeting: resource.allContentsIterable.filter(typeof(Greeting))) {
			fsa.generateFile(
				greeting.packageName + "/" + // package
				greeting.className + ".java", // class name
				greeting.compile)
		}
	}

	def getJvmType(Greeting greeting) {
		val declaredType = TypesFactory::eINSTANCE.createJvmGenericType
		declaredType.setSimpleName(greeting.className)
		declaredType.setPackageName(greeting.packageName)
		declaredType
	}

	def compile(Greeting greeting) '''
	«val importManager = new ImportManager(true, getJvmType(greeting))»
	«val mainMethod = compile(greeting, importManager)»
	package «greeting.packageName»;
	«IF !importManager.imports.empty»

	«FOR i : importManager.imports»
		import «i»;
	«ENDFOR»
	«ENDIF»

	«mainMethod»
	'''

	def compile(Greeting greeting, ImportManager importManager) '''
	public class «greeting.className» {
		public static void main(String args[]) {
			«compile(greeting.expression, importManager)»

			System.out.println("Hello «greeting.name» from ");
		}
	}
	'''

	def compile(XExpression xExpression, ImportManager importManager) {
		val result = new StringBuilderBasedAppendable(importManager)
		xbaseCompiler.toJavaStatement(xExpression, result, true)
		result
	}

	def className(Greeting greeting) {
		greeting.name.toFirstUpper
	}

	def packageName(Greeting greeting) {
		greeting.name.toLowerCase
	}
}

Давайте сосредоточимся на этих частях:

	def compile(Greeting greeting, ImportManager importManager) '''
	public class «greeting.className» {
		public static void main(String args[]) {
			«compile(greeting.expression, importManager)»

			System.out.println("Hello «greeting.name» from ");
		}
	}
	'''

	def compile(XExpression xExpression, ImportManager importManager) {
		val result = new StringBuilderBasedAppendable(importManager)
		xbaseCompiler.toJavaStatement(xExpression, result, true)
		result
	}

Мы попытались использовать toJavaStatement из XbaseCompiler, который кажется хорошим кандидатом на то, что мы хотим; в частности, передавая true в качестве последнего аргумента, мы говорим, что «результат» генерации должен быть присвоен переменной Java, чтобы мы могли ссылаться на нее; давайте посмотрим, что происходит в рабочей среде времени выполнения с этим поколением:

Эй, круто! XbaseCompiler генерирует все операторы Java, соответствующие исходному выражению XExpression (включая замыкания)! Теперь нам хотелось бы получить доступ только к последней сгенерированной переменной (в приведенных выше примерах _isNullOrEmpty и _apply соответственно), чтобы мы могли сами сгенерировать некоторый код Java с использованием этой переменной; но как мы можем знать его имя?

Ну, класс StringBuilderBasedAppendable не только содержит результат генерации; он также содержит карту со всеми промежуточными локальными переменными Java, сгенерированными компилятором. Каждая переменная на карте имеет в качестве ключа соответствующий объект Xbase. Таким образом, нам нужно знать только имя переменной, соответствующее нашему выражению XExpression, и все готово:

result.getName(greeting.expression)

Итак, финальный генератор выглядит так:

package org.xtext.example.helloxbase.generator

import com.google.inject.Inject
import org.eclipse.emf.ecore.resource.Resource
import org.eclipse.xtext.common.types.TypesFactory
import org.eclipse.xtext.generator.IFileSystemAccess
import org.eclipse.xtext.generator.IGenerator
import org.eclipse.xtext.xbase.XExpression
import org.eclipse.xtext.xbase.compiler.ImportManager
import org.eclipse.xtext.xbase.compiler.StringBuilderBasedAppendable
import org.eclipse.xtext.xbase.compiler.XbaseCompiler
import org.xtext.example.helloxbase.helloXbase.Greeting

import static extension org.eclipse.xtext.xtend2.lib.ResourceExtensions.*

class HelloXbaseGenerator implements IGenerator {

	@Inject
	protected XbaseCompiler xbaseCompiler

	override void doGenerate(Resource resource, IFileSystemAccess fsa) {
		for(greeting: resource.allContentsIterable.filter(typeof(Greeting))) {
			fsa.generateFile(
				greeting.packageName + "/" + // package
				greeting.className + ".java", // class name
				greeting.compile)
		}
	}

	def getJvmType(Greeting greeting) {
		val declaredType = TypesFactory::eINSTANCE.createJvmGenericType
		declaredType.setSimpleName(greeting.className)
		declaredType.setPackageName(greeting.packageName)
		declaredType
	}

	def compile(Greeting greeting) '''
	«val importManager = new ImportManager(true, getJvmType(greeting))»
	«val mainMethod = compile(greeting, importManager)»
	package «greeting.packageName»;
	«IF !importManager.imports.empty»

	«FOR i : importManager.imports»
		import «i»;
	«ENDFOR»
	«ENDIF»

	«mainMethod»
	'''

	def compile(Greeting greeting, ImportManager importManager) '''
	«val result = compile(greeting.expression, importManager)»
	public class «greeting.className» {
		public static void main(String args[]) {
			«result»

			Object expression = «result.getName(greeting.expression)»;
			System.out.println("Hello «greeting.name» from " +
				expression.toString());
		}
	}
	'''

	def compile(XExpression xExpression, ImportManager importManager) {
		val result = new StringBuilderBasedAppendable(importManager)
		xbaseCompiler.toJavaStatement(xExpression, result, true)
		result
	}

	def className(Greeting greeting) {
		greeting.name.toFirstUpper
	}

	def packageName(Greeting greeting) {
		greeting.name.toLowerCase
	}
}

И во время выполнения вы получите то, что хотели:

Вы можете найти источники для проекта helloxbase на

https://github.com/LorenzoBettini/Xtext2-experiments

Надеюсь, вы найдете этот пост полезным и следите за новыми сообщениями о Xtext ?

 

С http://www.rcp-vision.com/?p=1640&lang=en