Creating custom REST action

Let's start the journey of extending Elasticsearch by creating a custom REST action. We've chosen this as the first extension, because we wanted to take the simplest approach as the introduction to extending Elasticsearch.

Note

We assume that you already have a Java project created and that you are using Maven, just like we did in the Creating the Apache Maven project structure section in the beginning of this chapter. If you would like to use an already created and working example and start from there, please look at the code for Chapter 9, Developing Elasticsearch Plugins that is available with the book.

The assumptions

In order to illustrate how to develop a custom REST action, we need to have an idea of how it should work. Our REST action will be really simple—it should return names of all the nodes or names of the nodes that start with the given prefix if the prefix parameter is passed to it. In addition to that, it should only be available when using the HTTP GET method, so POST requests, for example, shouldn't be allowed.

Implementation details

We will need to develop two Java classes:

  • A class that extends the BaseRestHandler Elasticsearch abstract class from the org.elasticsearch.rest package that will be responsible for handling the REST action code—we will call it a CustomRestAction.
  • A class that will be used by Elasticsearch to load the plugin—this class needs to extend the Elasticsearch AbstractPlugin class from the org.elasticsearch.plugin package—we will call it CustomRestActionPlugin.

In addition to the preceding two, we will need a simple text file that we will discuss after implementing the two mentioned Java classes.

Using the REST action class

The most interesting class is the one that will be used to handle the user's requests—we will call it CustomRestAction. In order to work, it needs to extend the BaseRestHandler class from the org.elasticsearch.rest package—the base class for REST actions in Elasticsearch. In order to extend this class, we need to implement the handleRequest method in which we will process the user request and a three argument constructor that will be used to initialize the base class and register the appropriate handler under which our REST action will be visible.

The whole code for the CustomRestAction class looks as follows:

public class CustomRestAction extends BaseRestHandler {
  @Inject
  public CustomRestAction(Settings settings, RestController
  controller, Client client) {
    super(settings, controller, client);
    controller.registerHandler(Method.GET,"/_mastering/nodes", this);
  }   
  @Override
  public void handleRequest(RestRequest request, RestChannel 
  channel, Client client) {
    final String prefix = request.param("prefix", "");
    client.admin().cluster().prepareNodesInfo().all().execute(new 
    RestBuilderListener<NodesInfoResponse>(channel) {
      @Override
      public RestResponse buildResponse(
      NodesInfoResponse response, XContentBuilder builder)
      throws Exception {
        List<String> nodes = new ArrayList<String>();
        for (NodeInfo nodeInfo : response.getNodes()) {
          String nodeName = nodeInfo.getNode().getName();
          if (prefix.isEmpty()) {
            nodes.add(nodeName);
          } else if (nodeName.startsWith(prefix)) {
            nodes.add(nodeName);
          }
        }
      builder.startObject()
      .field("nodes", nodes)
      .endObject();
      return new BytesRestResponse(RestStatus.OK, builder);
      }
    });
  }
}

The constructor

For each custom REST class, Elasticsearch will pass three arguments when creating an object of such type: the Settings type object, which holds the settings; the RestController type object that we will use to bind our REST action to the REST endpoint; and the Client type object, which is an Elasticsearch client and entry point for cooperation with it. All of these arguments are also required by the super class, so we invoke the base class constructor and pass them.

There is one more thing: the @Inject annotation. It allows us to inform Elasticsearch that it should put the objects in the constructor during the object creation. For more information about it, please refer to the Javadoc of the mentioned annotation, which is available at https://github.com/elasticsearch/elasticsearch/blob/master/src/main/java/org/elasticsearch/common/inject/Inject.java.

Now, let's focus on the following code line:

controller.registerHandler(Method.GET, "/_mastering/nodes", this);

What it does is that it registers our custom REST action implementation and binds it to the endpoint of our choice. The first argument is the HTTP method type, the REST action will be able to work with. As we said earlier, we only want to respond to GET requests. If we would like to respond to multiple types of HTTP methods, we should just include multiple registerHandler method invocations with each HTTP method. The second argument specifies the actual REST endpoint our custom action will be available at; in our case, it will available under the /_mastering/nodes endpoint. The third argument tells Elasticsearch which class should be responsible for handling the defined endpoint; in our case, this is the class we are developing, thus we are passing this.

Handling requests

Although the handleRequest method is the longest one in our code, it is not complicated. We start by reading the request parameter with the following line of code:

String prefix = request.param("prefix", "");

We store the prefix request parameter in the variable called prefix. By default, we want an empty String object to be assigned to the prefix variable if there is no prefix parameter passed to the request (the default value is defined by the second parameter of the param method of the request object).

Next, we retrieve the NodesInfoResponse object using the Elasticsearch client object and its abilities to run administrative commands. In this case, we have used the possibility of sending queries to Elasticsearch in an asynchronous way. Instead of the call execute().actionGet() part, which waits for a response and returns it, we have used the execute() call, which takes a future object that will be informed when the query finishes. So, the rest of the method is in the buildResponse() callback of the RestBuilderListener object. The NodesInfoResponse object will contain an array of NodeInfo objects, which we will use to get node names. What we need to do is return all the node names that start with a given prefix or all if the prefix parameter was not present in the request. In order to do this, we create a new array:

List<String> nodes = new ArrayList<String>();

We iterate over the available nodes using the following for loop:

for (NodeInfo nodeInfo : response.getNodes())

We get the node name using the getName method of the DiscoveryNode object, which is returned after invoking the getNode method of NodeInfo:

String nodeName = nodeInfo.getNode().getName();

If prefix is empty or if it starts with the given prefix, we add the name of the node to the array we've created. After we iterate through all the NodeInfo objects, we call the are starting build the response and sent it through the HTTP.

Writing response

The last thing regarding our CustomRestAction class is the response handling, which is the responsibility of the last part of the buildResponse() method that we created. It is simple because an appropriate response builder is already provided by Elasticsearch under the builder argument. It takes into consideration the format parameter used by the client in the call, so by default, we send the response in a proper JSON format just like Elasticsearch does and also take the YAML (http://en.wikipedia.org/wiki/YAML) format for free.

Now, we use the builder object we got to start the response object (using the startObject method) and start a nodes field (because the value of the field is a collection, it will automatically be formatted as an array). The nodes field is created inside the initial object, and we will use it to return matching nodes names. Finally, we close the object using the endObject method.

After we have our object ready to be sent as a response, we return the BytesRestResponse object. We do this in the following line:

return new BytesRestResponse(RestStatus.OK, builder);

As you can see, to create the object, we need to pass two parameters: RestStatus and the XContentBuilder, which holds our response. The RestStatus class allows us to specify the response code, which is RestStatus.OK in our case, because everything went smoothly.

The plugin class

The CustomRestActionPlugin class will hold the code that is used by Elasticsearch to initialize the plugin itself. It extends the AbstractPlugin class from the org.elasticsearch.plugin package. Because we are creating an extension, we are obliged to implement the following code parts:

  • constructor: This is a standard constructor that will take a single argument; in our case, it will be empty
  • The onModule method: This is the method that includes the code that will add our custom REST action so that Elasticsearch will know about it
  • The name method: This is the name of our plugin
  • The description method: This is a short description of our plugin

The code of the whole class looks as follows:

public class CustomRestActionPlugin extends AbstractPlugin {
  @Inject
  public CustomRestActionPlugin(Settings settings) {
  }

  public void onModule(RestModule module) {
    module.addRestAction(CustomRestAction.class);
  }

  @Override
  public String name() {
    return "CustomRestActionPlugin";
  }

  @Override
  public String description() {
    return "Custom REST action";
  }
}

The constructor, name, and description methods are very simple, and we will just skip discussing them, and we will focus on the onModule method. This method takes a single argument: the RestModule class object, which is the class that allows us to register our custom REST action. Elasticsearch will call the onModule method for all the modules that are available and eligible (all REST actions). What we do is just a simple call to the RestModule addRestAction method, passing in our CustomRestAction class as an argument. That's all when it comes to Java development.

Informing Elasticsearch about our REST action

We have our code ready, but we need one additional thing; we need to let Elasticsearch know what the class registering our plugin is—the one we've called CustomRestActionPlugin. In order to do this, we create an es-plugin.properties file in the src/main/resources directory with the following content:

plugin=pl.solr.rest.CustomRestActionPlugin

We just specify the plugin property there, which should have a value of the class we use to register our plugins (the one that extends the Elasticsearch AbstractPlugin class). This file will be included in the jar file that will be created during the build process and will be used by Elasticsearch during the plugin load process.

Time for testing

Of course, we could leave it now and say that we are done, but we won't. We would like to show you how to build each of the plugins, install it, and finally, test it to see whether it actually works. Let's start with building our plugin.

Building the REST action plugin

We start with the easiest part—building our plugin. In order to do this, we run a simple command:

mvn compile package

We tell Maven that we want the code to be compiled and packaged. After the command finishes, we can find the archive with the plugin in the target/release directory (assuming you are using a project setup similar to the one we've described at the beginning of the chapter).

Installing the REST action plugin

In order to install the plugin, we will use the plugin command that is located in the bin directory of the Elasticsearch distributable package. Assuming that we have our plugin archive stored in the /home/install/es/plugins directory, we will run the following command (we run it from the Elasticsearch home directory):

bin/plugin --install rest --url file:/home/install/es/plugins/elasticsearch-rest-1.4.1.zip

We need to install the plugin on all the nodes in our cluster, because we want to be able to run our custom REST action on each Elasticsearch instance.

Note

In order to learn more about installing Elasticsearch plugins, please refer to our previous book, Elasticsearch Server Second Edition, or check out the official Elasticsearch documentation at http://www.elasticsearch.org/guide/reference/modules/plugins/.

After we have the plugin installed, we need to restart our Elasticsearch instance we were making the installation on. After the restart, we should see something like this in the logs:

[2014-12-12 21:04:48,348][INFO ][plugins                  ] [Archer] loaded [CustomRestActionPlugin], sites []

As you can see, Elasticsearch informed us that the plugin named CustomRestActionPlugin was loaded.

Checking whether the REST action plugin works

We can finally check whether the plugin works. In order to do that, we will run the following command:

curl -XGET 'localhost:9200/_mastering/nodes?pretty'

As a result, we should get all the nodes in the cluster, because we didn't provide the prefix parameter and this is exactly what we've got from Elasticsearch:

{
  "nodes" : [ "Archer" ]
}

Because we only had one node in our Elasticsearch cluster, we've got the nodes array with only a single entry.

Now, let's test what will happen if we add the prefix=Are parameter to our request. The exact command we've used was as follows:

curl -XGET 'localhost:9200/_mastering/nodes?prefix=Are&pretty'

The response from Elasticsearch was as follows:

{
  "nodes" : [ ]
}

As you can see, the nodes array is empty, because we don't have any node in the cluster that would start with the Are prefix. At the end, let's check another format of response:

curl -XGET 'localhost:9200/_mastering/nodes?pretty&format=yaml'

Now the response is not in a JSON format. Look at the example output for a cluster consisting of two nodes:

---
nodes:
- "Atalon"
- "Slapstick"

As we can see, our REST plugin is not so complicated but already has several features.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset