Jump to: navigation, search

JS4EMF/Developer Guide

< JS4EMF
Revision as of 08:07, 9 June 2011 by Hal.idi.ntnu.no (Talk | contribs)

(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)

Javascript for EMF lets you use EMF objects in Javascript code, as if they were native Javascript objects. Here's a short list of nice features:

Each of these is covered in the sections below. But, first we give a small introduction to the Javascript object model.

The Javascript object model

In most object-oriented languages, the behavior (attributes and methods) of an object is determined by the class, from which it is instantiated. Both attributes and methods are defined in the class, and the instances created from a class will have a value for each defined attribute and the methods you may call are those defined in the class. If inheritance is supported, a class can be seen as the composition of its own attributes and those defined in its superclasses.

In Javascript, there is no classes and class-based instantiation. Instead you create empty objects and dynamically add properties to them. Methods are just properties with function values. While properties usually have simple Java-like names, they are in general String or integer keys. The dot notation is used for simple names, e.g. obj.simpleName, while brackets ([]) is used for the general case, e.g. obj[1] or obj['strange name'].

To create two objects with similar behavior, you simply add the same properties and methods to them. Alternatively, a mechanism for inheriting behavior from other objects is provided, by means of an object's prototype. If you try to access the value of a property that has not (yet) been added to an object, the property of the object's prototype is used instead. Hence, if two empty objects share the same prototype, they will effectively have the same default property values and methods. Note that since the prototype is a normal object with its own prototype, there really is a prototype chain providing default property values and methods. By carefully constructing partly shared prototype chains, you can get an effect similar to Java's single inheritance class hierarchy. This is what we do in our Javascript support for EObjects. The general prototype chain mechanism is, however, a lot more flexible than Java's inheritance, since the prototype chain and prototype objects may be modified at any point. It can also be very confusing, and messing with the prototype structure requires great care.

Note that properties that are not handled by the EMF-specific mechanisms described below, are handled by Rhino's default behavior for Java objects.

Using Ecore attributes and references as Javascript properties

When EObjects are used as Javascript objects, the attributes (EAttribute) and references (EReference) may be referred to as properties, using the dot notation. This means that the Javascript code used for accessing them, may be similar to its Java counterpart. E.g. to access the name attribute of obj, you write obj.name. However, you may also use the bracket notation, as in obj['name']. Since features in Ecore must have Java-like names, the bracket notation is typically used for arrays or when you compute the the property name as in obj[useFullName ? 'fullName' : 'name'].

A second kind of Javascript property is supported as a convenience. It is fairly common for an EObject to contain other EObjects, where the contained EObjects have a 'name' attribute that is unique among siblings. E.g. Ecore EPackages contain EClasses, EClasses contain EStructuralFeatures and EOperations contain EParameters. In such a case, a contained EObject may be retrieved using the name prefixed by '$' as the property. E.g. anObj.$aName will retrieve the EObject named 'aName' contained in anObj. This makes it easier to navigate in some EObject structures, particularly the Ecore meta-object structure. Note that this is limited to getting the object as a property, not setting it.

Adding Javascript functions as methods to EClasses

While the annotation mechanism allows implementing Ecore operations, it is not very convenient, since you must edit them individually using the property sheet and not as a whole in a code editor. Therefore we support implementing all the operations of Ecore classes as Javascript functions in separate .js files, one for each EClass. The .js file for an EClass is located by construction a URI from the EClass' and using the URIConverter of the ResourceSet that loaded the EClass, so the location is configurable. Typically, it will be placed in the folder and have the same name as the EClass with a .js file extension.

The .js file for an EClass will define the set of Javascript functions that will be callable as methods of EObjects of that EClass. Hence, the this keyword will refer to instances of that EClass inside the body of each function. The same technique is used for defining methods on existing classes, like EObject, EClass and EList (see below). In the core plugin there are .js files for EObject, EClass, EEList etc. that define methods for (instances of) these classes. E.g. in EClass.js the create methods is defined as follows:

function create() {
	var factory = this.ePackage.eFactoryInstance;
	var eObject = factory.create(this);
	return eObject;
}

So to create an instance of a class named 'C' in package 'p', you can write $p.$C.create();.

Adding Javascript functions to individual EObjects

Built-in Javascript methods

Many of the existing Ecore classes have been extended with methods that make them easier to use for scripting. Below you'll find a list of the supported classes and their methods. Note that these methods are in addition to those provided by Rhino's default handling of Java objects. Hence, you may call eClass() to get an EObject's EClass, even though eClass() isn't in the list below.

The methods _() and __(), which returns the container and contents respectively, are generic in the sense that they have sensible implementations in all classes.

EObject methods

  • _(): returns the container of this EObject
  • __(): returns the contents of this EObject
  • isA(eClass): returns true of this is an instance of eClass, false otherwise
  • copy(): returns a copy of this using EcoreUtil.copy
  • findContainer(predicate): returns the first container satisfying predicate

EClass methods

  • create(): creates and returns an instance of this EClass

EDataType methods

  • create(string): creates an instance of this EDataType from string, using the EDataType's EFactory's createFromString method
  • toString(object): returns the string representation of object, using the EDataType's EFactory's convertToString method

EEnum methods

  • literal(value): returns the literal for value

EModelElement methods

  • getAnnotation(source, key): returns the EAnnotation for source and key

Resource methods

  • _(): returns the resource set of this resource
  • __(): returns the contents of this resource

ResourceSet methods

  • _(): returns null, since a ResourceSet does not have a container
  • __(): returns the resources of this ResourceSet

(E)List methods

The (E)List methods are heavily inspired by Common Lisp and OCL

Generic functions

  • __(): returns itself (for completeness)

Search

  • findOne(predicate, depth): returns the first contained object (recursively) satisfying predicate, limited by depth (-1 means no limit)
  • findMany(predicate, depth, count, result): returns all contained objects (recursively) satisfying predicate, limited by depth and count
  • findInstance(eClassifier): finds the first contained object (recursively) that is instance of eClassifier
  • findInstances(eClassifier): finds all contained objects (recursively) that are instances of eClassifier

Standard

  • mapList(fun) and map(fun): returns a list of the results from calling fun on this List's contents
  • mapProperty(prop): returns a list of the prop properties of this List's contents
  • mapMethod(prop, args): returns a list of the calling the prop method on this List's contents with additional args
  • reduce(fun, result): accumulates the result of calling the binary function fun on this List's contents, using result as the initial value
  • sum(fun): sums the results of calling fun on this List's contents
  • filterList(predicate, logic): returns the list of this List's contents that satisfies predicate (or not, depending on logic)
  • select(predicate): returns the list of this List's contents that satisfies predicate
  • reject(predicate): returns the list of this List's contents that does not satisfy predicate

Counting and testing

  • countList(predicate, logic, count): counts the number of object in this List's contents that satisfies predicate (or not, depending on logic), upto count
  • count(predicate): counts the number of object in this List's contents that satisfies predicate
  • one(predicate): returns true if one and only one object in this List's contents satisfies predicate
  • exists(predicate): returns true if at least one object in this List's contents satisfies predicate
  • every(predicate): returns true if every object in this List's contents satisfies predicate
  • none(predicate): returns true if no object in this List's contents satisfies predicate

New lists

  • listWith(element): returns this List if it contains element, or a new one with List's contents and element added
  • listWithout(element): returns this List if it does not contain element, or a new one with List's contents and element removed

Copy

  • copy(): copies and returns this List using EcoreUtil.copy

Methods from java.util.Collections

  • reverseOrder(comparator): returns a java.util.Comparator that is the reverse of comparator
  • fill(anything): replaces every element in this List with anything
  • reverse(): reverses this List
  • rotate(n): rotates this List n times (n defaults to 1)
  • asComparator(fun, comparator): returns a java.util.Comparator that calls fun
  • min(fun, comparator): returns the minimum value resulting from calling fun on this List's content according to comparator
  • max(fun, comparator): returns the maximum value resulting from calling fun on this List's content according to comparator
  • sort(fun, comparator): returns the list of values resulting from calling fun on this List's content, sorted according to comparator

(E)Map methods

  • __(): returns this Map's values

Global methods

Convenience functions corresponding to (E)List methods

  • findOne(container, test): calls findOne on container's contents
  • findInstance(container, eClassifier): calls findInstance on container's contents
  • findInstances(container, eClassifier): calls findInstances on container's contents
  • mapList(container, fun) or map(container, fun): calls mapList or map on container's contents
  • reduce(container, fun): calls reduce on container's contents
  • filterList(container, fun, logic): calls filterList on container's contents
  • select(container, fun): calls select on container's contents
  • reject(container, fun): calls reduce on container's contents
  • count(container, fun): calls count on container's contents
  • one(container, fun): calls one on container's contents
  • some(container, fun): calls some on container's contents
  • every(container, fun): calls every on container's contents
  • none(container, fun): calls none on container's contents

Create a new EList

  • newList(): creates a new EList (BasicEList)

Listening to change notifications and calling event-handling methods

In many applications, a lot of behavior is implemented be means of change notifications and handlers (called adapters in Ecore). EObjects have built-in support for notifications and we provide support for handling change notifications with Javascript code. There are two ways of attaching handlers:

  1. You explicitly attach a specific handler to a specific notifier by means of the adaptTo(notifier, handler) method. This is an EMF-specific Javascript method that may be called in Javascript code.
  2. You attach a single handler to an EObject hierarchy that makes the EObject handle its own notifications. This is achieved by means of JavascriptSupport.supportNotifications method, which must be called in Java code, typically after loading a resource.

In the first case, a specific handler is used for handling notifications on some other source object, while in the second case the object will handle notifications with itself as the source. But which method will be called to handle the notification? In both cases, the name of the method to be called is constructed from the pattern 'on'<eventType><featureName>, where eventType is one of 'Set', 'Unset', 'Add', 'Remove', 'AddMany', 'RemoveMany', and 'Move', and featureName is the name of the changed feature. E.g. if the notification resulted from setting an attribute named 'date', the 'onSetDate' method will be called to handle the notification. If the method doesn't exist, it tries again with eventType set to 'Change', e.g. 'onChangeDate'.

Dependency recording and databinding

To support Databinding, there is support for recording all read accesses to EMF properties during the execution of Javascript code and reacting to changes to those properties. Suppose you 1) computed some values based on the state of some EObjects and 2) change the state of some other EObjects. You would like to redo step 2) when the values from step 1) changes. This may be done by recording the dependencies during step 1), attaching listeners to the appropriate objects and redo both steps (while re-recording dependencies) when the appropriate change notifications are received.