# DSL for MongoDB

MongoDB is a very popular document-based database management system. In mongoDB, database entries (AKA documents) have fields, which are essentially (key, value) pairs. MongoDB is schema free, i.e. there are no rules, which fields have to be defined and of what type they are. This allows for very flexible and heterogeneous data structures and is a perfect match with JSON.

OTOH, Java is statically typed: The available types, their field names and field types are known and validated at compile time. The JavaBeans convention defines how instances can be manipulated. The standard mongoDB Java driver reflects the fact that mongoDB is schema-free by providing mongo documents as plain Java maps.

With this language you can describe statically typed Java-facades for MongoDB documents without hiding the dynamic nature of them. The language uses a tree-like syntax similar to JSON but lets you add static Java type information.

## Overview

In this project, we have created a small DSL mongoBeans based on Xtext that allows to create basic entity classes. These are backed by mongoDB objects but provide a statically typed JavaBeans API. Think of the language as a description how to map mongoDB documents to JavaBeans, in analogy to well known object relational mappers.

An example mongoBeans file looks like this:

    import java.util.*

    package org.musicdb {
        // a mongo bean
        Artist {
            String name    // single valued property
            Album* albums  // array property
            // an operation
            Iterable<Track> getOeuvre() {
                albums.map[track].flatten
            }
        }

        // another mongo bean
        Album {
            String title
            int year
            // inline definition of a mongo bean
            Track {
                String title
                int seconds
            }* tracks
        }
    }

For each MongoBean definition in a MongoFile file, we generate a Java class that wraps a DBObject. The class provides statically typed getter and setter methods for all defined MongoProperties. In the implementation of these accessor methods we delegate to the wrapped DBObject and do all the casting and conversion work. For the `Artist` in the above example, this would look like

    public class Artist implements IMongoBean {

      private DBObject _dbObject;
      ...
      public String getName() {
        return (String) _dbObject.get("name");
      }

      public void setName(final String name) {
         _dbObject.put("name", name);
      }
      ...
    }

By using the generated Java code, the rest of the application can use a type-safe and JavaBeans conformant API to access the data model. In addition, MongoBeans can define MongoOperations, which are translated to Java methods. We can use MongoProperties as well as Java types inside the operations’ bodies.

Client code could then look like this:

    Artist john = new Artist();
    john.setName("John Coltrane");
    Album album = new Album();
    album.setTitle("A Love Supreme");
    john.getAlbums().add(album);
    Track...  // create some tracks and add them to the album

    System.out.println(john.getName() + "'s Oeuvre");
    for(Track track: john.getOeuvre())
      System.out.println(track.getTitle());

    DBCollection dbCollection = ... // standard mongoDB driver code
    dbCollection.save(john.getDBObject())

## Running the Example

In addition to the common requirements, you need the mongoDB implementation for your platform. We have included the mongoDB Java driver from Eclipse Orbit in the code base.

Import the projects into an Eclipse workspace and run the launch configuration Run (org.eclipse.xtext.mongobeans). Import the example plug-in into the new workspace and run `MusicDBXtendTest` as a JUnit test.

## Grammar

The complete mongoBeans grammar looks like this:

    grammar org.xtext.mongobeans.MongoBeans with org.eclipse.xtext.xbase.Xbase

    generate mongoBeans "http://www.eclipse.org/xtext/mongobeans/MongoBeans"

    MongoFile:
    	importSection=XImportSection?
    	elements+=AbstractElement*;

    AbstractElement:
    	PackageDeclaration | MongoBean;

    PackageDeclaration:
    	'package' name=QualifiedName '{'
    		elements+=AbstractElement*
    	'}';

    MongoBean:
    	name=ValidID '{'
    		features+=AbstractFeature*
    	'}';

    AbstractFeature:
    	MongoOperation | MongoProperty;

    MongoProperty:
    	(type=JvmTypeReference | inlineType=MongoBean) (many?='*')? name=ValidID;

    MongoOperation:
    	=>(returnType=JvmTypeReference name=ValidID '(')
    		(parameters+=FullJvmFormalParameter
    			(',' parameters+=FullJvmFormalParameter)*
    		)?
    	')'
    	body=XBlockExpression;

The language inherits from the `Xbase` grammar in order to allow Xbase expressions and references to Java elements. A MongoFile starts with an import section (see Build DSL for details). The import section is followed by any number of AbstractElements, which can be PackageDeclarations or MongoBeans. Note that as opposed to Java, PackageDeclarations can be nested. MongoBeans define statically typed MongoProperties, which can be single-valued or multi-valued denoted by an `*` following the type name. The type of a MongoProperty can also be defined inline. MongoBeans can also define MongoOperations. The body of such an operation is an XBlockExpression from Xbase.

## Translation to Java

The JVM model inference is implemented in the MongoBeansJvmModelInferrer. As the generated code is quite rich, this is the most complex component of this language.

For each MongoBean, we create a Java class implementing the interface IMongoBean. This interface is the first type of a small runtime library that has to be on the classpath at runtime.

- DSL:

      package org.musicdb {
        Artist { ...

- Java:

      package org.musicdb;
      ...
      public class Artist implements IMongoBean {  ...

The inferrer code responsible for this section looks like this:

    @Inject extension JvmTypesBuilder
    @Inject extension IQualifiedNameProvider
    ...
    def dispatch void infer(MongoFile file,
                            IJvmDeclaredTypeAcceptor acceptor,
                            boolean isPreIndexingPhase) {
      for(bean : file.eAllOfType(MongoBean)) {
           acceptor.accept(bean.toClass(bean.fullyQualifiedName))[
               documentation = bean.documentation
               superTypes += typeRef(IMongoBean)
    ... // calling various methods to create Java members
        // from the AbstractFeatures
          ]
      }
    }

First, it finds all elements of type MongoBean in the given MongoFile. For each of these, it creates a new Java class. Then the documentation is copied and the interface IMongoBean is added to the list of supertypes. This will also insert a Java import at the appropriate location.

Each MongoBean wraps a DBObject, which is represented as a Java field with a getter. There are two constructors, one for a given DBObject and one that creates a new one. We have to store the class name in the DB object, if we want to be able to restore JavaBeans from query results.

- DSL:

      Artist { ...

- Java:

      public class Artist implements IMongoBean {
        private DBObject _dbObject;

        public DBObject getDbObject() {
          return this._dbObject;
        }

        public Artist(final DBObject dbObject) {
          this._dbObject = dbObject;
        }

        public Artist() {
          _dbObject = new BasicDBObject();
          _dbObject.put(JAVA_CLASS_KEY, "org.musicdb.Artist");
        }
      ...

The inferrer code does this in two separate methods: One for the property `_dbObject` and another for the constructors.

    def protected addDbObjectProperty(JvmDeclaredType
                                      inferredType,
                                      MongoBean bean) {
      inferredType.members += bean.toField('_dbObject', typeRef(DBObject))
      inferredType.members += bean.toGetter('dbObject', '_dbObject', typeRef(DBObject))
    }

    def protected addConstructors(JvmDeclaredType inferredType,
                                  MongoBean bean) {
      inferredType.members += bean.toConstructor [
        documentation = '''...'''
        parameters += bean.toParameter("dbObject", typeRef(DBObject))
        body = '''
          this._dbObject = dbObject;
        '''
      ]
      inferredType.members += bean.toConstructor [
        documentation = '''...'''
        body = '''
          _dbObject = new com.mongodb.BasicDBObject();
          _dbObject.put(JAVA_CLASS_KEY, "«inferredType.identifier»");
        '''
      ]
    }

Next on our list are the getters and setters delegating to the `_dbObject`. We have to handle four cases: Properties with a type that can be handled by the mongoDB Java-driver directly (most primitive types, String, Date, etc.), IMongoBean properties, and their respective multi-valued counterparts:

- DSL:

      ...
        String name     // primitive property
        Artist friend   // bean-type property
        String* aliases // multi-valued primitive property
        Album* albums   // multi-valued bean-type property
      ...

- Java:

      ...
        public String getName() {
          return (String) _dbObject.get("name");
        }
        public void setName(final String name) {
           _dbObject.put("name", name);
        }

        public Artist getFriend() {
          return WrappingUtil.wrapAndCast(
              (DBObject) _dbObject.get("friend"));
        }
        public void setFriend(final Artist friend) {
           _dbObject.put("friend", WrappingUtil.unwrap(friend));
        }

        public List<String> getAliases() {
          return (List<String>) _dbObject.get("aliases");
        }

        private MongoBeanList<Album> _albums;
        public List<Album> getAlbums() {
          if(_albums==null)
            _albums = new MongoBeanList<Album>(_dbObject, "albums");
          return _albums;
        }
      ...

The runtime helper class WrappingUtil does the conversion between DBObject and IMongoBean. For multi-valued MongoProperties we need getters only. If they have a primitive type, they can be handled by the Java-driver directly. Multi-valued MongoBean typed properties require a special MongoBeanList to automatically wrap/unwrap the elements.

The corresponding inferrer code does not show anything particularly new so we skip most of it for brevity. To detect whether a type is an IMongoBean or a primitive mongoDB type, we use the last helper class MongoTypes. The following snippet shows the inference of the getter for multi-valued properties:

    def protected addListAccessor(JvmDeclaredType inferredType,
                                  MongoProperty property) {
      val propertyType = property.jvmType.asWrapperTypeIfPrimitive
      if(propertyType.isMongoPrimitiveType) {
        inferredType.members += property.toMethod(
          'get' + property.name.toFirstUpper,
           typeRef(List, propertyType)
        ) [
    ...
        ]
      } else {
        inferredType.members += property.toField(
          '_' + property.name, typeRef(MongoBeanList, propertyType))
        inferredType.members += property.toMethod(
          'get' + property.name.toFirstUpper, typeRef(List, propertyType)
        ) [
     ...

Last but not least, we infer Java methods for MongoOperations.

- DSL:

      ...
        Iterable<Track> getOeuvre() {
            albums.map[tracks].flatten
        }
      ...

- Java:

      ...
        public Iterable<Track> getOeuvre() {
          // some java code you really don't care about
          // but it should just do the right thing
        }
      ...

The inferrer code for this particular task is surprisingly simple, as we can directly associate the body of the MongoOperation to the generated Java method. The Xbase compiler will automatically transform that to Java.

    def protected addMethod(JvmDeclaredType inferredType,
                            MongoOperation operation) {
      inferredType.members += operation.toMethod(operation.name,
          operation.returnType) [
        documentation = operation.documentation
        for(parameter: operation.parameters)
          parameters += parameter.toParameter(parameter.name,
              parameter.parameterType)
        body = operation.body
      ]
    }

## Qualified Name Provider

By default, the qualified name of an element is calculated by joining all the simple names of its containers with a dot. In our example, the MongoBean `Track` would consequently be named `org.musicdb.Album.track.Track`. To ignore properties and beans on the path, we implemented our own MongoQualifiedNameProvider.

    class MongoQualifiedNameProvider extends XbaseQualifiedNameProvider {

      def qualifiedName(MongoBean mongoBean) {
        val packageDeclaration =
          mongoBean.getContainerOfType(PackageDeclaration)
        if(packageDeclaration != null)
          packageDeclaration.fullyQualifiedName.append(mongoBean.name)
        else
          return QualifiedName.create(mongoBean.name)
      }
    }

To make the framework pick up our customization, we have to add a binding in the respective Guice module.

    @Override
    public Class<? extends IQualifiedNameProvider> bindIQualifiedNameProvider() {
      return MongoQualifiedNameProvider.class;
    }

See Dependency Injection to learn more about Xtext’s dependency injection.

## Validation

The Java driver for mongoDB cannot map all Java types to mongoDB types. To enforce that constraint, we have added the MongoBeansValidator. It also checks for missing types and avoids name collisions in the generated code with the implicitly defined property `dbObject`.

    class MongoBeansValidator extends XbaseJavaValidator {
    ...
      @Inject extension MongoTypes mongoTypes

      @Check def checkMongoProperty(MongoProperty it) {
        if (name == 'dbObject')
          error("Illegal property name 'dbObject'",
                ABSTRACT_FEATURE__NAME,
                ILLEGAL_PROPERTY_NAME,
                '_' + name)
        if (type != null) {
          if (!type.isMongoType)
            error('Only MongoBeans and mappable types are allowed',
                  MONGO_PROPERTY__TYPE, ILLEGAL_TYPE)
        } else if (inlineType == null) {
          error('Type must be set', ABSTRACT_FEATURE__NAME, MISSING_TYPE)
        }
      }
    ...

## IDE Enhancements

The validator from the previous section raises an error `ILLEGAL_PROPERTY_NAME` when a property is named DBObject. We have implemented a quick fix to replace the invalid name:

    class MongoBeansQuickfixProvider extends XbaseWithAnnotationsQuickfixProvider {

      @Fix(MongoBeansValidator.ILLEGAL_PROPERTY_NAME)
      def void capitalizeName(Issue issue, IssueResolutionAcceptor acceptor) {
        acceptor.accept(issue,
            'Rename to ' + issue.data.head,
            '''Rename property to «issue.data.head».''',
            null) [
          xtextDocument.replace(issue.offset, issue.length, issue.data.head)
        ]
      }
    }

To improve the language IDE visually, we have tuned the outline a bit

    class MongoBeansOutlineTreeProvider extends DefaultOutlineTreeProvider {
      // don't show children of operations
      def _isLeaf(MongoOperation operation) {
        true
      }

      // show inline declared MongoBeans
      def _createChildren(IOutlineNode parentNode, MongoProperty property) {
        if(property.inlineType != null)
          parentNode.createNode(property.inlineType)
      }

      def _isLeaf(MongoProperty property) {
        property.inlineType == null
      }
    }

and the label provider as well:

    class MongoBeansLabelProvider extends XbaseLabelProvider {
    ...
      override image(Object element) {
        // icons are stored in the 'icons' folder of this project
        switch element {
          MongoBean: 'Letter-B-blue-icon.png'
          MongoProperty: 'Letter-P-orange-icon.png'
          MongoOperation: 'Letter-O-red-icon.png'
          Import: 'imp_obj.gif'
          PackageDeclaration: 'package_obj.gif'
          default:
            super.image(element)
        }
      }

Next Chapter: DSL for Guice