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:
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
, Book
, List Book
). From where do they come?
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
, Book
, List 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.
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.
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.