Статьи

Использование типов JVM в Xtext 2.1 и ImportManager

Xtext 2.1 облегчает доступ к типам Java из вашего DSL; Вы можете найти некоторые параграфы в документации. В частности, новые возможности Xbase делают эту интеграцию еще более мощной!

Этот параграф в документации кратко описывает, как обращаться к элементам Java с использованием типов JVM, а затем посвящает гораздо больше места доступу к типам Java с помощью Xbase.

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

Таким образом, для этого простого эксперимента я буду использовать в качестве отправной точки пример Приветствие, т. Е. Самый базовый DSL, который вы получите при создании проекта Xtext внутри Eclipse. Итак, продолжайте и создайте такой проект (посмотрите строки, которые я использовал на скриншоте):

Значения по умолчанию для созданного проекта уже подходят для доступа к типам JVM, поэтому нам не нужно настраивать файл mwe2.

Теперь, следуя документации Xtext, мы импортируем пакет ecore, определяющий типы JVM, в наш HelloJvmTypes.xtext

import "http://www.eclipse.org/xtext/common/JavaVMTypes" as jvmTypes

и теперь мы готовы к доступу к JvmTypes из нашей грамматики.

Я не хочу иметь полезный DSL, я просто хочу поэкспериментировать с доступом к типам Java, поэтому скажем, я просто хочу закончить DSL, который позволяет мне писать предложения вроде

Hello world from java.util.List, org.eclipse.emf.ecore.EClass!
Hello foobar from java.io.IOException!

и из этих предложений сгенерируйте некоторые классы Java, которые просто выводят одинаковые строки.

Поэтому мы изменим наш HelloJvmTypes.xtext следующим образом

grammar org.xtext.example.hellojvmtypes.HelloJvmTypes with org.eclipse.xtext.common.Terminals

import "http://www.eclipse.org/xtext/common/JavaVMTypes" as jvmTypes 

generate helloJvmTypes "http://www.xtext.org/example/hellojvmtypes/HelloJvmTypes"

Model:
 greetings+=Greeting*;

Greeting:
 'Hello' name=ID 'from'
 javaTypes+=[jvmTypes::JvmType|QualifiedName]
 (',' javaTypes+=[jvmTypes::JvmType|QualifiedName])* '!'
;

QualifiedName: ID ('.' ID)* ;

Теперь мы можем восстановить все артефакты и запустить другой экземпляр Eclipse; здесь мы создаем новый проект плагина (скажем, «hellojvmtypes»), а в папке с исходным кодом мы создаем файл .hellojvmtypes (скажем, «My.hellojvmtypes»). Мы должны в конечном итоге с редактором, как на скриншоте

Обратите внимание, что также работает помощник по содержимому для типов Java! .

Также обратите внимание, что на самом деле будут видны только те типы Java, которые «достижимы» из пути к классам вашего проекта. Например, попробуйте использовать org.eclipse.emf.ecore.EClass, и вы получите ошибку (если вы создали простой проект плагина).

Теперь попробуйте добавить org.eclipse.emf.ecore в качестве зависимости в ваш файл MANIFEST проекта, и вы сможете получить доступ к его классам Java.

Прежде чем мы продолжим генерацию кода, давайте проведем некоторое модульное тестирование! Создайте класс Xtend2 в проекте плагина тестов org.xtext.example.hellojvmtypes.tests и добавьте в MANIFEST org.eclipse.xtext.xtend2.lib в качестве зависимости.

package org.xtext.example.hellojvmtypes.tests

import com.google.inject.Inject
import junit.framework.Assert
import org.eclipse.xtext.common.types.JvmGenericType
import org.eclipse.xtext.junit4.InjectWith
import org.eclipse.xtext.junit4.XtextRunner
import org.eclipse.xtext.junit4.util.ParseHelper
import org.eclipse.xtext.junit4.validation.ValidationTestHelper
import org.junit.Test
import org.junit.runner.RunWith
import org.xtext.example.hellojvmtypes.HelloJvmTypesInjectorProvider
import org.xtext.example.hellojvmtypes.helloJvmTypes.Greeting
import org.xtext.example.hellojvmtypes.helloJvmTypes.Model

@InjectWith(typeof(HelloJvmTypesInjectorProvider))
@RunWith(typeof(XtextRunner))
class ParserTest {

    @Inject
    ParseHelper<Model> parser

    @Inject extension ValidationTestHelper

    @Test
    def void testParsingAndLinking() {
        parser.parse('''Hello foo from java.util.List!''').assertNoErrors
    }

    @Test
    def void testJvmTypeAccess() {
        val model = parser.parse(
            "Hello foo from java.util.List!")
        val greeting = model.greetings.head as Greeting
        val jvmType = greeting.javaTypes.get(0) as JvmGenericType
        Assert::assertEquals("java.util.List", jvmType.identifier)
    }

}

Если вы запустите соответствующий сгенерированный класс Java в качестве теста Junit, вы увидите зеленую линию!

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

package org.xtext.example.hellojvmtypes.generator

import org.eclipse.emf.ecore.resource.Resource
import org.eclipse.xtext.generator.IGenerator
import org.eclipse.xtext.generator.IFileSystemAccess
import org.xtext.example.hellojvmtypes.helloJvmTypes.Greeting

import static extension org.eclipse.xtext.xtend2.lib.ResourceExtensions.*
import org.eclipse.xtext.xbase.compiler.ImportManager

class HelloJvmTypesGenerator 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) '''
	«val importManager = new ImportManager(true)»
	«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[]) {
			«FOR javaType : greeting.javaTypes»
			System.out.println("Hello «greeting.name» from " +
				«importManager.serialize(javaType)».class.getName());
			«ENDFOR»
		}
	}
	'''

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

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

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

Теперь вы должны увидеть код, сгенерированный автоматически в src-gen: в частности, вы окажетесь в конце каждого элемента Greeting с пакетом и классом, названным в соответствии с функцией имени Greeting (упакуйте все строчные буквы и имя класса с первой буквой капитал). Кроме того, сгенерированный метод main распечатает соответствующее приветствие.

Важными частями в генераторе являются следующие:

	def compile(Greeting greeting) '''
	«val importManager = new ImportManager(true)»
	«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[]) {
			«FOR javaType : greeting.javaTypes»
			System.out.println("Hello «greeting.name» from " +
				«importManager.serialize(javaType)».class.getName());
			«ENDFOR»
		}
	}
	'''

Здесь мы используем действительно классный класс, предоставляемый Xtext для генерации операторов импорта и, в частности, для генерации обращений к классам Java: цитирование из документации Xtext:

ImportManager укорачивает полностью квалифицированные имена, отслеживает импортируемые пространства имен, позволяет избежать конфликтов имен

Таким образом, если вы вызовите importManager.serialize (JvmType), у вас будет не только строка для переданного JvmType: ImportManager будет отслеживать оператор импорта, который необходимо будет добавить, чтобы гарантировать, что сгенерированная строка приведет к действительному Java-коду. Конечно, в случае конфликтов ImportManager сгенерирует полное имя Java (и оператор импорта не будет записан). Таким образом, в нашем генераторе кода мы сначала генерируем код для класса и метода main и сохраняем его в String;

def compile(Greeting greeting) '''
	«val importManager = new ImportManager(true)»
	«val mainMethod = compile(greeting, importManager)»
... continued in (2)

во время генерации importManager записал требуемый импорт,

def compile(Greeting greeting, ImportManager importManager) '''
	public class «greeting.className» {
		public static void main(String args[]) {
			«FOR javaType : greeting.javaTypes»
			System.out.println("Hello «greeting.name» from " +
				«importManager.serialize(javaType)».class.getName());
			«ENDFOR»
		}
	}
	'''

таким образом, мы затем генерируем все операторы импорта Java, а затем фактический класс Java (который мы буферизировали в строку).

(2)... continuation
	package «greeting.packageName»;
	«IF !importManager.imports.empty»

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

	«mainMethod»
	'''

Например, посмотрите, как ImportManager правильно (и прозрачно) обрабатывает возможные конфликты классов Java в сгенерированном коде (из-за URI класса, который появляется в разных пакетах).

Все идет нормально! Но что произойдет, если мы ссылаемся на тип Java, который, случайно, имеет то же имя класса, которое мы генерируем? Например, в папке src создайте класс hello.Foobar и обратитесь к нему в своем My.hellojvmtypes и посмотрите сгенерированный код… argh! Мы получаем ошибку!

Это потому, что ImportManager знает о типах, к которым вы обращаетесь при создании класса Foobar (в данном случае), но не о самом сгенерированном классе Foobar! Мы можем решить эту проблему, используя другую форму конструктора ImportManager

public ImportManager(boolean organizeImports, JvmDeclaredType thisType)

где вы указываете JvmDeclaredType элемента Java, который будет содержать доступ к элементам Java, которые мы генерируем через сам ImportManager. Таким образом, нам нужно только создать на лету JvmDeclaredType, соответствующий классу Java, который мы генерируем для элемента Greeting!

Вот модификация генератора:

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

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

	«mainMethod»
	'''

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

Теперь перезапустите другой экземпляр Eclipse, и заново сгенерируйте код для My.hellojvmtypes , и посмотрите теперь правильный сгенерированный класс Java!

Итак, это полная окончательная версия генератора:

package org.xtext.example.hellojvmtypes.generator

import org.eclipse.emf.ecore.resource.Resource
import org.eclipse.xtext.generator.IGenerator
import org.eclipse.xtext.generator.IFileSystemAccess
import org.xtext.example.hellojvmtypes.helloJvmTypes.Greeting

import static extension org.eclipse.xtext.xtend2.lib.ResourceExtensions.*
import org.eclipse.xtext.xbase.compiler.ImportManager
import org.eclipse.xtext.common.types.TypesFactory

class HelloJvmTypesGenerator 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) '''
	«val importManager = new ImportManager(true, createJvmType(greeting))»
	«val mainMethod = compile(greeting, importManager)»
	package «greeting.packageName»;
	«IF !importManager.imports.empty»

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

	«mainMethod»
	'''

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

	def compile(Greeting greeting, ImportManager importManager) '''
	public class «greeting.className» {
		public static void main(String args[]) {
			«FOR javaType : greeting.javaTypes»
			System.out.println("Hello «greeting.name» from " +
				«importManager.serialize(javaType)».class.getName());
			«ENDFOR»
		}
	}
	'''

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

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

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

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

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

 

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