# Creating a new extension * [Initial setup](#initial-setup) * [Obtain network address and port information](#obtain-network-address-and-port-information) * [Implement the _`Extension`_ interface](#implement-the-extension-interface) * [Implement other interfaces and extension points](#implement-other-interfaces-and-extension-points) * [Use the OpenSearch Java client to implement functionality](#use-the-opensearch-java-client-to-implement-functionality) * [Defining a Document class](#defining-a-document-class) * [Creating (PUT) a document in an index](#creating-put-a-document-in-an-index) * [Reading (GET) a document in an index](#reading-get-a-document-in-an-index) * [Updating (POST) a document in an index](#updating-post-a-document-in-an-index) * [Deleting (DELETE) a document in an index](#deleting-delete-a-document-in-an-index) *Note*: This document is evolving and is in draft state. This document outlines how to create a new custom extension. For migration of existing plugins, see [PLUGIN_MIGRATION](PLUGIN_MIGRATION.md). For this example, you will create a CRUD extension, demonstrating the create, read, update, and delete operations on an index. ## Initial setup Create a new repository at a location of your choice. In your dependency management, set up a dependency on the OpenSearch SDK for Java. Here is the required key information: - Group ID: `org.opensearch.sdk` - Artifact ID: `opensearch-sdk-java` - Version: `1.0.0-SNAPSHOT` (compatible with OpenSearch 2.x) or `2.0.0-SNAPSHOT` (compatible with OpenSearch 3.x) At general availability, dependencies will be released to the Central Repository. To use SNAPSHOT versions, add these repositories: - OpenSearch SNAPSHOT repository: https://aws.oss.sonatype.org/content/repositories/snapshots/ - Lucene snapshot repository: https://d1nvenhzbhpy0q.cloudfront.net/snapshots/lucene/ If you use Maven, the following POM entries will work: ```xml opensearch.snapshots OpenSearch Snapshot Repository https://aws.oss.sonatype.org/content/repositories/snapshots/ lucene.snapshots Lucene Snapshot Repository https://d1nvenhzbhpy0q.cloudfront.net/snapshots/lucene/ org.opensearch.sdk opensearch-sdk-java 2.0.0-SNAPSHOT ``` For Gradle, specify dependencies as follows: ```groovy repositories { mavenCentral() maven { url "https://aws.oss.sonatype.org/content/repositories/snapshots/" } maven { url "https://d1nvenhzbhpy0q.cloudfront.net/snapshots/lucene/"} } dependencies { implementation("org.opensearch.sdk:opensearch-sdk-java:2.0.0-SNAPSHOT") } ``` ## Obtain network address and port information An extension requires host and port information for both the extension and OpenSearch. You may either define these in code in an `ExtensionSettings` object, or in a YAML file. The following are equivalent: - Java import and instantiation: ```java import org.opensearch.sdk.ExtensionSettings; new ExtensionSettings("crud", "127.0.0.1", "4532", "127.0.0.1", "9200") ``` - A `crud.yml` file: ```yml extensionName: crud hostAddress: 127.0.0.1 hostPort: 4532 opensearchAddress: 127.0.0.1 opensearchPort: 9200 ``` ## Implement the _`Extension`_ interface Create a class that implements _`Extension`_. You may prefer to create a class that extends `BaseExtension`, which provides some helper methods. Implementing the _`Extension`_ interface requires you to implement the `getExtensionSettings()` and `setExtensionsRunner()` methods. The `BaseExtension` class implements these and only requires that you call `super()` with either the `ExtensionSettings` object you created or a path to the YAML file (either absolute or classpath-based). Implement a `main()` method that instantiates your object and passes an instance of itself to `ExtensionsRunner`. You will need to either handle or throw an `IOException` from this method. The following Java code accomplishes the preceding steps: ```java import java.io.IOException; import org.opensearch.sdk.BaseExtension; import org.opensearch.sdk.ExtensionSettings; import org.opensearch.sdk.ExtensionsRunner; public class CRUDExtension extends BaseExtension { public CRUDExtension() { // Optionally, pass a String path to a YAML file with these settings super(new ExtensionSettings("crud", "127.0.0.1", "4532", "127.0.0.1", "9200")); } public static void main(String[] args) throws IOException { ExtensionsRunner.run(new CRUDExtension()); } } ``` At this point, you have a working extension! Start it by executing the `main()` method, and then start your OpenSearch cluster. But it doesn't _do_ anything yet. Here is where you can start defining your own functionality. ## Implement other interfaces and extension points If you want to handle REST requests, implement the `ActionExtension` interface and override the `getExtensionRestHandlers()` method. Pass a list of classes that will handle those requests: ```java import org.opensearch.sdk.api.ActionExtension; public class CRUDExtension extends BaseExtension implements ActionExtension { // keep the constructor and main method from before and add the following code @Override public List getExtensionRestHandlers() { // you need to create this class next! return List.of(new CrudAction()); } } ``` These classes must implement _`ExtensionRestHandler`_, which is a functional interface that requires the implementation of the `handleRequest()` method with the signature `public ExtensionRestResponse handleRequest(RestRequest request)`. The `BaseExtensionRestHandler` class provides many useful methods for exception handling in requests. For the CRUD extension example, you'll implement one REST route for each option and delegate it to the appropriate handler function. Each route is an instance of `NamedRoute` and requires at least a method, path, and globally unique name. ```java import java.util.List; import java.util.function.Function; import org.opensearch.rest.NamedRoute; import org.opensearch.rest.RestRequest; import org.opensearch.rest.RestRequest.Method; import org.opensearch.rest.RestResponse; import org.opensearch.rest.RestStatus; import org.opensearch.sdk.rest.BaseExtensionRestHandler; public class CrudAction extends BaseExtensionRestHandler { @Override public List routes() { return List.of( new NamedRoute.Builder().method(Method.PUT) .path("/sample") .uniqueName("crud_extension:sample/create") .handler(createHandler) .build(), new NamedRoute.Builder().method(Method.GET) .path("/sample/{id}") .uniqueName("crud_extension:sample/get") .handler(readHandler) .build(), new NamedRoute.Builder().method(Method.POST) .path("/sample/{id}") .uniqueName("crud_extension:sample/post") .handler(updateHandler) .build(), new NamedRoute.Builder().method(Method.DELETE) .path("/sample/{id}") .uniqueName("crud_extension:sample/delete") .handler(deleteHandler) .build() ); } Function createHandler = (request) -> { return new ExtensionRestResponse(request, RestStatus.OK, "To be implemented"); }; Function readHandler = (request) -> { return new ExtensionRestResponse(request, RestStatus.OK, "To be implemented"); }; Function updateHandler = (request) -> { return new ExtensionRestResponse(request, RestStatus.OK, "To be implemented"); }; Function deleteHandler = (request) -> { return new ExtensionRestResponse(request, RestStatus.OK, "To be implemented"); }; } ``` ## Use the OpenSearch Java client to implement functionality To use the OpenSearch REST API, you will need an instance of the OpenSearch Java client. Refer to the OpenSearch Java client documentation for either [Apache HttpClient 5 Transport](https://opensearch.org/docs/latest/clients/java/#initializing-the-client-with-ssl-and-tls-enabled-using-apache-httpclient-5-transport) or [OpenSearch RestClient Transport](https://opensearch.org/docs/latest/clients/java/#initializing-the-client-with-ssl-and-tls-enabled-using-restclient-transport). The remainder of this example assumes you have implemented one of the preceding options in your constructor: ```java private final OpenSearchClient client; public CrudAction() { final OpenSearchTransport transport = // implement per documentation this.client = new OpenSearchClient(transport); } ``` ### Defining a Document class For your CRUD sample you will create a simple Java class with a single field: ```java public static class CrudData { private String value; public String getValue() { return value; } public void setValue(String value) { this.value = value; } } ``` ### Creating (PUT) a document in an index Now in the create handler function, create an index (if it doesn't exist): ```java BooleanResponse exists = client.indices().exists(new ExistsRequest.Builder().index("crudsample").build()); if (!exists.value()) { client.indices().create(new CreateIndexRequest.Builder().index("crudsample").build()); } ``` Next, you add a document to it: ```java CrudData crudData = new CrudData(); crudData.setValue("value"); IndexResponse response = client.index(new IndexRequest.Builder().index("crudsample").document(crudData).build()); ``` The `BaseExtensionRestHandler` provides an `exceptionalRequest()` method to handle exceptions: ```java return exceptionalRequest(request, e); ``` The user needs the ID of the created document (`response.id()`) for further handling. The `BaseExtensionRestHandler` provides a `createJsonResponse()` method for this: ```java return createJsonResponse(request, RestStatus.OK, "_id", response.id()); ``` Finally, you have the following code for the create handler method: ```java Function createHandler = (request) -> { IndexResponse response; try { // Create index if it doesn't exist BooleanResponse exists = client.indices().exists(new ExistsRequest.Builder().index("crudsample").build()); if (!exists.value()) { client.indices().create(new CreateIndexRequest.Builder().index("crudsample").build()); } // Now add our document CrudData crudData = new CrudData(); crudData.setValue("value"); response = client.index(new IndexRequest.Builder().index("crudsample").document(crudData).build()); } catch (OpenSearchException | IOException e) { return exceptionalRequest(request, e); } if (response.result() == Result.Created) { return createJsonResponse(request, RestStatus.OK, "_id", response.id()); } return createJsonResponse(request, RestStatus.INTERNAL_SERVER_ERROR, "failed", response.result().toString()); }; ``` ### Reading (GET) a document in an index You can now use the read handler function to get the document you just created, using its ID, which you will pass as a named parameter in the path. You can then get the document by ID. ```java String id = request.param("id"); GetResponse response = client.get(new GetRequest.Builder().index("crudsample").id(id).build(), CrudData.class); ``` Adding exception handling, the following is the full handler method: ```java Function readHandler = (request) -> { GetResponse response; // Parse ID from request String id = request.param("id"); try { response = client.get(new GetRequest.Builder().index("crudsample").id(id).build(), CrudData.class); } catch (OpenSearchException | IOException e) { return exceptionalRequest(request, e); } if (response.found()) { return createJsonResponse(request, RestStatus.OK, "value", response.source().getValue()); } return createJsonResponse(request, RestStatus.NOT_FOUND, "error", "not_found"); }; ``` ### Updating (POST) a document in an index You will create a new document similar to the one you created in the create handler, parse the ID as you did in the read handler, and then update that document. With exception handling, the following is the update handler method: ```java Function updateHandler = (request) -> { UpdateResponse response; // Parse ID from request String id = request.param("id"); // Now create the new document to update with CrudData crudData = new CrudData(); crudData.setValue("new value"); try { response = client.update( new UpdateRequest.Builder().index("crudsample").id(id).doc(crudData).build(), CrudData.class ); } catch (OpenSearchException | IOException e) { return exceptionalRequest(request, e); } if (response.result() == Result.Updated) { return createEmptyJsonResponse(request, RestStatus.OK); } return createJsonResponse(request, RestStatus.INTERNAL_SERVER_ERROR, "failed", response.result().toString()); }; ``` ### Deleting (DELETE) a document in an index You only need the ID to delete a document, so the delete handler method is implemented as follows: ```java Function deleteHandler = (request) -> { DeleteResponse response; // Parse ID from request String id = request.param("id"); try { response = client.delete(new DeleteRequest.Builder().index("crudsample").id(id).build()); } catch (OpenSearchException | IOException e) { return exceptionalRequest(request, e); } if (response.result() == Result.Deleted) { return createEmptyJsonResponse(request, RestStatus.OK); } return createJsonResponse(request, RestStatus.INTERNAL_SERVER_ERROR, "failed", response.result().toString()); }; ```