How to Create a Custom Mule Connector With DataSense

In my previous article, I explained how to create a custom connector using Mule DevKit. In this article, I will explain how to add DataSense in a connector. 

Normally, when creating a Connector, it is not mandatory to have configuration declarations (i.e., a @Configuration class). You can create a fine connector without declaring any configuration.

However, when you are going to add DataSense to a custom connector, having configuration declaration is mandatory irrespective of if the configuration is mandatory or not. 

In this article, I am going to create a very simple connector with three processors (operations). The processors are simply returning some hard coded values. You can find the complete source code here.

Connector Classes

In this part, I will try to explain every class of the connector briefly. 

Model Classes

I have created two simple model classes. Metadata of these classes will be returned upon request.

1. Author

A simple author POJO class:

package com.anupam.snake.model;

/**
 * Created by ac-agogoi on 2/21/17.
 */
public class Author {

 private String firstName;
 private String lastName;

 public Author() {}

 public Author(String firstName, String lastName) {
  this.firstName = firstName;
  this.lastName = lastName;
 }

 public String getFirstName() {
  return firstName;
 }

 public void setFirstName(String firstName) {
  this.firstName = firstName;
 }

 public String getLastName() {
  return lastName;
 }

 public void setLastName(String lastName) {
  this.lastName = lastName;
 }
}

2. Book

A simple book POJO class:

package com.anupam.snake.model;

/**
 * Created by ac-agogoi on 2/21/17.
 */
public class Author {

 private String firstName;
 private String lastName;

 public Author() {}

 public Author(String firstName, String lastName) {
  this.firstName = firstName;
  this.lastName = lastName;
 }

 public String getFirstName() {
  return firstName;
 }

 public void setFirstName(String firstName) {
  this.firstName = firstName;
 }

 public String getLastName() {
  return lastName;
 }

 public void setLastName(String lastName) {
  this.lastName = lastName;
 }
}

MetaDataCategory Class

To add DataSense, the very first thing you will need is a class annotated with @MetaDataCategory. In my case, the class is DataSenseResolver:

package com.anupam.snake.metadata;

import java.util.ArrayList;
import java.util.List;

import org.mule.api.annotations.MetaDataKeyRetriever;
import org.mule.api.annotations.MetaDataRetriever;
import org.mule.api.annotations.components.MetaDataCategory;
import org.mule.common.metadata.DefaultListMetaDataModel;
import org.mule.common.metadata.DefaultMetaData;
import org.mule.common.metadata.DefaultMetaDataKey;
import org.mule.common.metadata.MetaData;
import org.mule.common.metadata.MetaDataKey;
import org.mule.common.metadata.MetaDataModel;
import org.mule.common.metadata.builder.DefaultMetaDataBuilder;

import com.anupam.snake.model.Author;
import com.anupam.snake.model.Book;

/**
 * Category which can differentiate between input or output MetaDataRetriever
 */
@MetaDataCategory
public class DataSenseResolver {

 @MetaDataKeyRetriever
 public List < MetaDataKey > getMetaDataKeys() throws Exception {
  List < MetaDataKey > keys = new ArrayList < MetaDataKey > ();

  // Generate the keys
  keys.add(new DefaultMetaDataKey("author_id", "Author"));
  keys.add(new DefaultMetaDataKey("book_id", "Book"));
  keys.add(new DefaultMetaDataKey("book_list_id", "List Book"));
  return keys;
 }

 @MetaDataRetriever
 public MetaData getMetaData(MetaDataKey entityKey) throws Exception {
  if ("author_id".equals(entityKey.getId())) {
   MetaDataModel authorModel = new DefaultMetaDataBuilder().createPojo(Author.class).build();
   return new DefaultMetaData(authorModel);
  } else if ("book_id".equals(entityKey.getId())) {
   MetaDataModel bookModel = new DefaultMetaDataBuilder().createPojo(Book.class).build();
   return new DefaultMetaData(bookModel);
  } else if ("book_list_id".equals(entityKey.getId())) {
   //MetaDataModel bookListModel = new DefaultMetaDataBuilder().createList().ofPojo(Book.class).build();
   MetaDataModel mm = new DefaultMetaDataBuilder().createPojo(Book.class).build();
   DefaultListMetaDataModel listModel = new DefaultListMetaDataModel(mm);
   return new DefaultMetaData(listModel);
  } else {
   MetaDataModel defaultModel = new DefaultMetaDataBuilder().createPojo(String.class).build();
   return new DefaultMetaData(defaultModel);
  }
 }

} -

The first method, getMetaDataKeys(), is annonated with @MetaDataKeyRetriever. Here, we are defining theMetaDataKey. The concept of MetaDataKey is very simple. We are creating a list of MetaDataKey and adding objects of class DefaultMetaDataKey, which is nothing but an implementation of the MetaDataKey interface.

The constructor of DefaultMetaDataKey receives two arguments (ID and displayName). The displayName is what that will appear in the drop down box (I'll explain this later).

The second method, getMetaData(), is annonated with @MetaDataRetriever. The purpose of this method is to return the metadata depending on the MetaDataKey that it receives as an argument. 

Configuration Class

In fact, I do not need any configuration for this connector — but as I said, to add DataSense, it is mandatory to have a configuration class. Here is my class, ConnectorConfig:

package com.anupam.snake.config;

import org.mule.api.annotations.Configurable;
import org.mule.api.annotations.components.Configuration;
import org.mule.api.annotations.param.Default;

@Configuration(friendlyName = "Configuration")
public class ConnectorConfig {

 @Configurable
 @Default("admin")
 private String username;

 public String getUsername() {
  return username;
 }

 public void setUsername(String username) {
  this.username = username;
 }

}

Connector Class

Here is our connector class, SnakeConnector. Please do remember that we have referred to the DataSenseResolver class at the class level using the @MetaDataScope annotation. You can use this annotation at the individual @Processor level, also.

package com.anupam.snake.connector;

import java.util.ArrayList;
import java.util.List;

import org.mule.api.annotations.Config;
import org.mule.api.annotations.Connector;
import org.mule.api.annotations.MetaDataScope;
import org.mule.api.annotations.Processor;
import org.mule.api.annotations.lifecycle.Start;
import org.mule.api.annotations.param.Default;
import org.mule.api.annotations.param.MetaDataKeyParam;
import org.mule.api.annotations.param.MetaDataKeyParamAffectsType;
import org.mule.api.annotations.param.MetaDataStaticKey;

import com.anupam.snake.config.ConnectorConfig;
import com.anupam.snake.metadata.DataSenseResolver;
import com.anupam.snake.model.Author;
import com.anupam.snake.model.Book;

@Connector(name = "snake", friendlyName = "Snake")
@MetaDataScope(DataSenseResolver.class)
public class SnakeConnector {

 @Config
 ConnectorConfig config;

 @Start
 public void init() {

 }

 @Processor
 public Object getByType(@MetaDataKeyParam(affects = MetaDataKeyParamAffectsType.BOTH) String entityType,
  @Default("#[payload]") Object entityData) {

  if ("Author".equals(entityType)) {
   return new Author("Anupam", "Gogoi");
  } else if ("Book".equals(entityType)) {
   return new Book("Mule", "ISBN-1234");
  } else {
   return null;
  }
 }

 @Processor
 @MetaDataStaticKey(type = "book_id")
 public Object getBook() {
  return new Book("Java", "ISBN-333");
 }

 @Processor
 @MetaDataStaticKey(type = "book_list_id")
 public Object getListBooks() {
  List < Book > list = new ArrayList < > ();
  list.add(new Book("Book 1", "ISBN-1234"));
  list.add(new Book("Book 2", "ISBN-3333"));
  list.add(new Book("Book 3", "ISBN-4454"));
  return list;
 }

 public ConnectorConfig getConfig() {
  return config;
 }

 public void setConfig(ConnectorConfig config) {
  this.config = config;
 }

}

Discussion About the SnakeConnector 

The connector has three methods or processors: getByType(), getBook(), and getListBooks(). When you install the connector and configure it providing the necessary configuration, you can see how these methods (processors) are mapped in the Operation drop-down box as shown below:

Image title

Here, I am going to give a brief of each operation.

Get by Type

Select the Get by type operation and you will be provided with a drop-down list of three entity types (Author, BookList Book). From where do they come? 

Image title

If you observe carefully the method getByType(), you can see that I am passing two arguments (entityType and entityData). The second argument is optional.

@Processor
public Object getByType(@MetaDataKeyParam(affects = MetaDataKeyParamAffectsType.BOTH) String entityType,
 @Default("#[payload]") Object entityData) {

 if ("Author".equals(entityType)) {
  return new Author("Anupam", "Gogoi");
 } else if ("Book".equals(entityType)) {
  return new Book("Mule", "ISBN-1234");
 } else {
  return null;
 }
}

Most importantly, the first argument (entityType) is annonated with @MetaDataKeyParam(affects=MetaDataKeyParamAffectsType.BOTH). This directs Mule to create a drop-down list of the MetaDataKeys that we have defined in our DataSenseResolver class. In the drop-down list, it shows only the display names of our declared MetaDataKeys.

@MetaDataKeyRetriever
public List < MetaDataKey > getMetaDataKeys() throws Exception {
 List < MetaDataKey > keys = new ArrayList < MetaDataKey > ();

 // Generate the keys
 keys.add(new DefaultMetaDataKey("author_id", "Author"));
 keys.add(new DefaultMetaDataKey("book_id", "Book"));
 keys.add(new DefaultMetaDataKey("book_list_id", "List Book"));
 return keys;
}

Now, when you select the Book option, it automatically loads the metadata (on the right-hand side) for the Book POJO that we have defined. If it does not load properly, just hit Refresh Metadata.

So, what exactly happens inside? When you select Book from the drop-down list, Mule internally applies some Java reflection mechanism to search for the ID associated with Book (book_id) and creates a MetaDataKey. Then, Mule looks for the metadata associated with this MetaDataKey using the following method that we have declared in our DataSenseResolver class:

@MetaDataRetriever
public MetaData getMetaData(MetaDataKey entityKey) throws Exception {
 if ("author_id".equals(entityKey.getId())) {
  MetaDataModel authorModel = new DefaultMetaDataBuilder().createPojo(Author.class).build();
  return new DefaultMetaData(authorModel);
 } else if ("book_id".equals(entityKey.getId())) {
  MetaDataModel bookModel = new DefaultMetaDataBuilder().createPojo(Book.class).build();
  return new DefaultMetaData(bookModel);
 } else if ("book_list_id".equals(entityKey.getId())) {
  //MetaDataModel bookListModel = new DefaultMetaDataBuilder().createList().ofPojo(Book.class).build();
  MetaDataModel mm = new DefaultMetaDataBuilder().createPojo(Book.class).build();
  DefaultListMetaDataModel listModel = new DefaultListMetaDataModel(mm);
  return new DefaultMetaData(listModel);
 } else {
  MetaDataModel defaultModel = new DefaultMetaDataBuilder().createPojo(String.class).build();
  return new DefaultMetaData(defaultModel);
 }
}

The attribute affects=MetaDataKeyParamAffectsType.BOTH signifies that we want metadata in both input and output.

Now, another question arises. What if we declare duplicate keys and/or names? For example: 

@MetaDataKeyRetriever
public List < MetaDataKey > getMetaDataKeys() throws Exception {
 List < MetaDataKey > keys = new ArrayList < MetaDataKey > ();

 // Duplicate keys
 keys.add(new DefaultMetaDataKey("author_id", "Author"));
 keys.add(new DefaultMetaDataKey("author_id", "Author"));

 keys.add(new DefaultMetaDataKey("book_id", "Book"));
 keys.add(new DefaultMetaDataKey("book_list_id", "List Book"));
 return keys;
}

We have declared identical author_id and Author. In this case, Mule will simply ignore the identical entry and in the dropdown list will show only (Author, BookList Book). 

Let's assume another situation,

@MetaDataKeyRetriever
public List<MetaDataKey> getMetaDataKeys() throws Exception {
List<MetaDataKey> keys = new ArrayList<MetaDataKey>();

// Identical displayname
keys.add(new DefaultMetaDataKey("author_id", "Author"));
keys.add(new DefaultMetaDataKey("user_id", "Author"));

      keys.add(new DefaultMetaDataKey("book_id", "Book"));
        keys.add(new DefaultMetaDataKey("book_list_id", "List Book"));
return keys;
}

In this case, the dropdown list will contain Author, Book, and List Book.

Get Book

Please select the Get book operation. Observe that it does not provide you the drop-down list to choose metadata. However, when you click the Output tab, you can see the Book metadata.Please do not forget to refresh metadata.

Image title

So, how does it happen? Please check the method getBook() in the SnakeConnector class:

@Processor
@MetaDataStaticKey(type = "book_id")
public Object getBook() {
 return new Book("Java", "ISBN-333");
}

Here, we have provided the key with the annotation @MetaDataStaticKey(type="book_id") . 

Get List Books

Now, choose the Get list book option and check the output metadata. Please do not forget to refresh the metadata.

Image title

Now, you have a List payload as metadata. Check out the code:

@Processor
@MetaDataStaticKey(type = "book_list_id")
public Object getListBooks() {
 List < Book > list = new ArrayList < > ();
 list.add(new Book("Book 1", "ISBN-1234"));
 list.add(new Book("Book 2", "ISBN-3333"));
 list.add(new Book("Book 3", "ISBN-4454"));
 return list;
}

An important point to be noted is how the List metadata has been declared in the DataSenseResolver. Here is the code snippet:

@MetaDataRetriever
public MetaData getMetaData(MetaDataKey entityKey) throws Exception {
 if ("author_id".equals(entityKey.getId())) {
  MetaDataModel authorModel = new DefaultMetaDataBuilder().createPojo(Author.class).build();
  return new DefaultMetaData(authorModel);
 } else if ("book_id".equals(entityKey.getId())) {
  MetaDataModel bookModel = new DefaultMetaDataBuilder().createPojo(Book.class).build();
  return new DefaultMetaData(bookModel);
 } else if ("book_list_id".equals(entityKey.getId())) {
  //MetaDataModel bookListModel = new DefaultMetaDataBuilder().createList().ofPojo(Book.class).build();
  MetaDataModel mm = new DefaultMetaDataBuilder().createPojo(Book.class).build();
  DefaultListMetaDataModel listModel = new DefaultListMetaDataModel(mm);
  return new DefaultMetaData(listModel);
 } else {
  MetaDataModel defaultModel = new DefaultMetaDataBuilder().createPojo(String.class).build();
  return new DefaultMetaData(defaultModel);
 }
}

As per the DevKit documentation for List metadata, you have to use the createList() method of the DefaultMetaDataBuilder class, as shown below:

MetaDataModel bookListModel = new DefaultMetaDataBuilder().createList().ofPojo(Book.class).build();

But in practice creating metadata with this method does not create List metadata. So to create the List metadata please use the DefaultListMetaDataModel class as shown below,

MetaDataModel mm = new DefaultMetaDataBuilder().createPojo(Book.class).build();
DefaultListMetaDataModel listModel = new DefaultListMetaDataModel(mm);

Conclusion

In this tutorial, I have shown how to add DataSense in a connector. In the next tutorial, I will explain how to add security to the connector.

 

 

 

 

Top