- 1 Modeling for Programmers and Programming for Modelers
- 2 Getting Started
- 3 Creating an Xcore Project
- 4 Creating an Xcore model
- 4.1 Specifying a Package
- 4.2 Specifying a Class
- 4.3 Specifying an Attribute
- 4.4 Specifying a Containment Reference
- 4.5 Specifying a Container Reference
- 4.6 Specifying a Cross Reference
- 4.7 Specifying an Enumeration
- 4.8 Specifying a Data Type
- 4.9 Specifying an Operation
- 4.10 Specifying a Derived Feature
- 4.11 Specifying an Annotation
- 5 Creating a Dynamic Instance
- 6 Configuring GenModel Properties
- 7 Converting a GenModel to an Xcore Model
- 8 Converting an Xcore Model to a GenModel
Modeling for Programmers and Programming for Modelers
Xcore is an extended concrete syntax for Ecore that, in combination with Xbase, transforms it into a fully fledged programming language with high quality tools reminiscent of the Java Development Tools. You can use it not only to specify the structure of your model, but also the behavior of your operations and derived features as well as the conversion logic of your data types. It eliminates the dividing line between modeling and programming, combining the advantages of each. All this is supported for both generated and dynamic EMF models.
The first version of Xcore will be part of the Juno release train and is currently available in the Juno p2 repository. To get started, you'll need to install the hardware-appropriate platform version of Eclipse 4.2 or Eclipse 3.8. Once you have that installed and running, you're ready to install Xcore.
- Use "Help → Install New Software..." to bring up the "Install" wizard; the Juno repository should be available in the "Work with" dropdown.
- Select Juno and wait for the content of the wizard to populate.
- Either enter "Xcore" in the filter field or locate "Modeling → EMF - Eclipse Modeling Framework Xcore SDK" in the content and check mark it.
- Drive the wizard to completion.
Xcore and everything it needs will be automatically installed. Of course you'll need to restart Eclipse for the changes to property take effect.
Please keep in mind that this is all work in progress, so there are bound to be bugs. I've fixed a few of them while writing this tutorial; those fixes won't be available until the next build (M6) is promoted to Juno. For bleeding edge changes, you can use the Xtext nightly builds and EMF nightly builds; the install wizard supports drag and drop so you can drag the Xtext link first and then the EMF link before following the steps above to locate Xcore and install its latest nightly version. If you have questions, the EMF newsgroup (eclipse.tools.emf) is the best place to ask them; please use "[xcore]" as the prefix for your subject line.
Creating an Xcore Project
Xcore can be used in any properly-configured Java project. There's a convenient wizard for creating an empty pre-configured project.
- Use "File → Project..." and enter "Xcore" in the filter field or locate "Xcore → Xcore Project".
- Use "Next" to advance the "New Project" wizard and enter the name of your project. It's best to use a qualified name that will be appropriate as a plug-in ID and as a prefix for your Java packages, i.e, org.example.library.
- Use "Finish" to complete the wizard.
In your workspace you'll see your new project.
It's a project with a Java, PDE, and Xtext natures that contains the following:
- A "src" folder for your hand written Java code.
- A "src-gen" folder in which your model code will be generated by default.
- A MANIFEST.MF with prepopulated dependencies for EMF Ecore and Xtext Xbase.
- And an empty model folder.
Of course it's possible to start with any Java project and use the "Convert → Add Xtext Nature" or "Convert → Convert to Plug-in Projects to introduce the missing natures and to edit the MANIFEST.MF to add missing dependencies.
Creating an Xcore model
An Xcore model is created in the "model" folder via its context menu using "New → File" to create an empty new file with extension ".xcore", e.g., "Company.xcore". This will open the Xcore-aware editor. It starts out with an error marker, because an empty file isn't a valid Xcore instance.
Specifying a Package
The editor supports syntax completion so if you enter Ctrl-Space, you'll see it fills in the "package" keyword. Next you need to enter a package name, e.g., org.example.company. If you save, you'll see the error markers go away. You've created an empty Xcore package. As you can see, like Java, an Xcore model starts with a package declaration but note that that there's no semicolon. Note too that nothing is generated in the "src-gen" folder yet; EMF doesn't generate anything for empty packages.
Specifying a Class
Now you're ready to create something more meaningful. Add two blank lines and try hitting Ctrl-Space again to see what you're allowed to enter next.
We'll start by defining a class called "Library". After entering the "class" keyword and the name of the class, type a curly brace: the closing curly brace is automatically inserted. If you now save the editor, you'll see the following:
Noticed that the model code has been generated automatically.
Specifying an Attribute
Now you're ready to define the structure of the "Library" class. Within the curly braces, hit Ctrl-Space to see what you're allowed to enter next.
To specify an attribute, you need to specify the name of a data type followed by the name of the feature. So if you choose "String", enter "name", and save, you'll see the following:
Of course the corresponding feature accessor methods, i.e., getName() and setName(String), will immediately be generated in the "Library" interface. Note that while the Xcore source shows the use of "String", which in the completion proposal you saw earlier was listed as "java.lang.String", from the hover information you can see that the reference actually resolves to the "EString" data type from the built-in "Ecore" package. Xcore provides familiar aliases for all of the Ecore data types that correspond to Java built-in and mathematical types.
Specifying a Containment Reference
We'll want libraries to contain books and authors, so let's add two more empty classes, "Book" and "Writer" so that we can specify references to them in the library. If we try the same approach as defining the name feature, you'll end up with the following:
That's because Xcore interprets this as an attribute and the type of an attribute must be a data type, not a class. If you hover over the error indicator, you'll see how to fix the problem.
In this case, we want to define a containment reference, so choose that option. Notice that if you select the reference to "Book" you can use "F3" to navigate to the definition for the "Book" class. Of course we wanted to define this to be a multi-valued reference and here have specified a single-valued reference. The multiplicity of a feature is specified with the bounds in square brackets, where "" is used as a short-hand for "[0..*]" as follows:
Let's define another multi-valued containment reference called "authors" and save the result, which will look as follows:
Again, the expected results are immediately generated. Notice that the outline view uses icons are much like the GenModel's icons, though with a purplish cast rather than a bluish one.
Specifying a Container Reference
If you wanted a convenient API for navigating from a "Book" or "Writer" to its containing "Library" you'd specify container reference with an opposite like this:
Notice that completion support is available for specifying the opposite. When you select a choice, the opposite is automatically updated to refer back.
Let's add a container feature for "Writer" as well, and let's define "title" and "pages" features for "Book" and a "name" feature for "Writer" to produce the following:
Note how we've made use of "int" as an alias for "EInt" to define the type for "pages".
Specifying a Cross Reference
The most important thing left to complete the picture is specifying the bidirectional relationship between "Books" and "Writers". Specifying a cross reference is done much like we did for containment and container references, but using the "refers" keyword. We start by specifying one of the two references, i.e, a "Book" has "authors", then the other, i.e., a "Writer" has "books", at which point we can also specify the opposite.
Notice that opposite completion is supported here as well and that upon completion, both opposites are properly paired.
Of course "F3" navigation is supported for the reference to an opposite.
Specifying an Enumeration
Supposed we wanted to categorize the kinds of books we have in the library. We could specify an enumeration named "BookCategory" for that and use it to specify a feature named "bookCategory" in "Book" as follows:
Specifying a Data Type
Suppose we wanted to specify the copyright date of a book. Of course we could use "EDate" from Ecore, but its serialization format is more like date and time, so that might not exactly fit our needs. Instead we could define our own "Date" data type to wrap "java.util.Date" as follows:
A data type acts as a wrapper for an existing Java type, so the completion proposal supports choosing a Java class that matches the name we've started typing. Upon selection, an import is added, so we can use the short form of the name.
Because classifier names and Java type names can never be used interchangeably in any Xcore scope, it's not a conflict to import the Java name in a scope that also defines a classifier with the same name.
So far everything we've shown are things you could do with Ecore directly, so we've only seen how Xcore provides a concise textual syntax for Ecore. To complete the support for our own "Date" type, it would normally be necessary to modify the generated "LibraryFactoryImpl" class' methods that implement string conversion for this data type. With Xcore, we can specify this logic directly in the model. Note that if we look at the proposals for what may follow the type specification, we can see that the "create" and "convert" keywords are expected.
So we can choose to start specifying the "create" logic for creating an instance of the data type from a "String" value which is specifying within the curly braces of a block. We can using Java's "java.text.SimpleDateFormat" class as follows:
Notice that completion proposals understand what's on the classpath so we can use that for calling the constructor. To refer to the instance of the data type within the body we use "it", which acts much like the implicit "this" variable in Java. We also need to consider that "parse" can throw a "ParseException" that we need to handle properly. In addition, the value of the data type might be null, so best we guard for that case too. And finally, we can use a similar approach to specify the "convert" logic to produce the following complete result:
Notice we've also used the new data type to define a "copyright" features in the "Book" class and that all the Java classes we've used were automatically imported by completion. The notation for expressing behavior in Xcore uses the model defined by Xbase. It's very similar to Java, but is expression oriented, so even things that are statements in Java, return a value in Xcore/Xbase. That's why you don't see a "return" statement.
Specifying an Operation
Suppose you wanted convenience methods on Library, e.g., an operation to retrieve a "Book" give its title. We could specify that as follows:
Again, the syntax is very Java-like and of course completion proposals are aware of the Java APIs implied by the Xcore model definition so feature names such as "books" are in the proposals. Here's how we would complete the definition:
We return "null" if we reach the end of the loop without a match. That's all there is to it. If you look at the generated implementation class for "Library" you'll see it compiles to the following Java code:
Notice that the "==" comparison for title is compiled to a null safe "Object.equals" test. Notice also that all the things that look like simple field accesses in the Xcore code actually compile down to proper calls to the generated API accessor methods. One of the nice things about Xcore is that you get the advantages of writing what look like simple Java classes with simple fields declarations, including the ability to write a notation as if you had simple fields, but you end up with properly defined APIs and proper uses of them.
Specifying a Derived Feature
With Xcore it's even possible to define the behavior of a derived feature. You only need to mark the feature as "derived" and, using the "get" keyword, define within a block how the value is completed. For example, you could specify a derived attribute to compute the last name of the "Writer" as follows:
Specifying an Annotation
Ecore supports general-purpose annotations comprising a source string (typically a URI) for specifying identification and a map of string-based key-value pairs for specifying the details. Rather than requiring the typically large source URI to be repeated on each use, Xcore provide support for specifying a simple alias for it as follows:
We can then use it as follows:
Annotations with the GenModel's nsURI have specialized support in Xcore. Any key that matches the name of a feature in the GenModel, will be used to populate that feature. If you have a look at your generated model after saving the above, you will notice that the base class of each modeled class's implementation class has changed to use the one we've just specified.
Note that even the annotation definition itself is not necessary for this case because this "alias" is defined in "xcore.lang" and is therefore visible everywhere by default:
Creating a Dynamic Instance
Not only does Xcore generate you model code as you'd expect, everything we've specified works in dynamic models as well. Let's create a dynamic instance of "Library" by selecting it and bringing up the context menu for it as follows:
This brings up the following wizard, which lets you control the name and location of the new instance:
The defaults should be fine for now do drive the wizard to completion. This will open the "Sample Reflective Editor" as follows:
Expanding the first resource and double clicking on the "Library" instance will bring up the properties view. Notice that the "Library.xcore" resource is loaded in order to load the dynamic Library instance. If you expand it, you'll notice it contains the Xcore model, the GenModel, the Ecore model, and a bunch of JVM model instances. The GenModel and Ecore model are derived from the details in Xcore model. The various JVM model instances in turn are derived from the GenModel, i.e, all the information about the Java artifacts that are generated by the GenModel. Additional resources contain things that are referenced directly and indirectly from our model. Let's make the model more complete by creating a "Book".
We can do the same thing to create a "Writer". Let's have a look at the properties for a "Writer". We can given the "Writer" a "Name", say "Arthur C Clark" using the properties view.
Notice that the "Last Name" updates correctly whenever we change the full "Name".
Let's populate some of the information about the "Book". We'll call it "2001: A Space Odyssey" and set the copyright date to September 1st, 1968.
Notice that as we start entering a date, we get feedback about parse failures demonstrating that the data type conversion logic we implemented is hard at work.
Finally, we can have a look at how the logic for finding a book from its title is working in the "Library" instance. Let's select it and use "Windows → Show View → Other..." and locate "Eclipse Modeling Framework → Operation Invocation View". This view responds to the selection in the active editor and provides a drop-down for choosing which operation to invoke. If the operation has arguments, it provides properties for setting those values. And of course there is an "Invoke" button for actually invoking the operation. We'd can use it as follows:
After invoking the operation, the result will be selected in the editor, so when the right book is selected, we can see that this too is working well.
Configuring GenModel Properties
The Xcore editor directly supports the same Properties view as you're familiar with from the Generator. Use "Windows → Show View → Other..." followed by "General → Properties" to bring up the Properties view. It responds to selection it the editor and the Outline, so selecting the package produces the following:
Note that the properties for the GenModel and the GenPackage are merged into a single set of properties.
Changing the values in the Properties view will modify the source with the corresponding @GenModel annotations.
Converting a GenModel to an Xcore Model
To make it easy to migrate existing artifacts to Xcore, we've added migration support. It is integrated with the GenModel's exporter framework, so from the context menu of the Generator editor opened for a "*.genmodel" resource, you can invoke "Export Model...". Suppose we did that for the XML Schema-based library model.
That will bring up the "Export EMF Model" wizard from which you can choose Xcore as the target.
The next page allows you to choose where the new Xcore resources will be saved.
The final page allows you to choose which packages to export and the name of the Xcore resource in which to save them.
Completing that page produces the following resource:
Notice all the annotations that capture details that would otherwise be missing:
- The "nsURI" of the model, which, if unspecified by an "Ecore" annotation, is just the fully qualified package name.
- The "ExtendedMetaData" annotations which record the mapping onto XML Schema.
Notice too in the import directives that keywords can be used as identifiers when escaped with a "^".
Converting an Xcore Model to a GenModel
If for some reason you need the legacy format, e.g., to use a tool which expects that format, you can convert your Xcore model to a GenModel using the importer framework. From the context menu for the "*.xcore" resource, you can create a new "*.genmodel" resource as follows:
This brings up the following wizard in which we can choose to create a new EMF Generator model:
Proceeding to the next page, where we can choose the location and name of the new "*.genmodel" resource.
Proceeding from there, we can choose which importer to use.
After that, we can choose which "*.xcore" resource to import. Note that one is already suggested because we have it selected in the package explore. Also note that you must hit the "Load" button in order to proceed from this page
That brings us to the final page where we can choose which packages to import and which to reuse from existing sources.
Given that the Xcore resource does physically contain a GenModel and an Ecore model, it should be possible in the future for any tool to consume Xcore resources directly.