Skip to content

The Ultimate Guide to Creating a Boomi Custom Connector

In this article, or better to say, in this guide, we are going to explain in deep dive how to create a Boomi custom connector. Boomi comes with hundreds of built-in connectors, however, in many situations, you might find yourself in a position where there’s no out-of-the-box connector available. If the target system exposes an API layer, you can use the HTTP Connector to connect to it, however, using a packaged connector is always neater and encapsulates the functionality in a more professional and reusable fashion.

There’s a lot of resources available which explains how to create a custom connector, however, we found no single resource which gives a comprehensive tutorial on how to do that from A to Z, so we decided to tackle this problem in this article.

It’s worth noting that Boomi has created an article series on how to build a custom connector, however, we’ll go beyond that in this article, as well as go into more details into some steps which were not covered in enough details in this series. However, we always advise to go through that article series first to get a high-level idea about the process and understand the different components within the connector SDK and what they are used for.

Enough with the introductions, let’s get going into this.

Step 1: Setting up the IDE

This step is only required once, when you want to develop your custom connector for the first time. For any subsequent connectors, this is not required. Here, we will use Apache Maven to build our connector. You can use Gradle as well, however, we opted to use Maven as this is the more common way.

The following steps builds on the Building a connector with Apache Maven article with the aforementioned Boomi series, but goes into more details on how to achieve the steps mentioned in that article.

Setup the Environment Variables

First, we’ll need to set up some environment variables. To do that, follow the below steps. Note that these steps were performed on a Mac OS, so there might be slight differences if you are attempting them on a Linux OS:

  1. Enter the following command to locate the path of your JDK installation:

/usr/libexec/java_home -v 11

  1. Enter the following command to locate where maven is installed

which mvn

  1. Open the bash profile file (create if not existing)

open ~/.bash_profile

  1. Add the following. Replace values by your actual locations to Java and Maven

export JAVA_HOME=/Library/Java/JavaVirtualMachines/temurin-17.jdk/Contents/Home

export M2_HOME=/opt/homebrew/Cellar/maven/3.9.0/libexec

export PATH=”$M2_HOME/bin:$PATH”

  1. Apply changes to current terminal session

source ~/.bash_profile

Update Maven’s Settings File

Next, we need to update Maven’s settings file, as follows:

  1. Open settings file from ~/.m2/settings.xml
  2. Add the following to the file
<settings>
<profiles>
    <profile>
      <id>boomi-repository</id>
      <repositories>
        <repository>
          <id>boomi-maven-repository</id>
          <url>https://boomisdk.s3.amazonaws.com/releases</url>
          <releases>
            <enabled>true</enabled>
          </releases>
          <snapshots>
            <enabled>false</enabled>
          </snapshots>
        </repository>
      </repositories>
    </profile>
  </profiles>
  <activeProfiles>
    <activeProfile>boomi-repository</activeProfile>
  </activeProfiles>
</settings>
  1. Save and close the file

Step 2: Create the Connector Project

Now we get into creating our custom connector. To do that, Boomi uses a Maven command which creates the basic structure of the connector. Go on and follow the below steps to create the basic project. Replace the text between the <> with your specific connector’s name / company information

  1. Open terminal and navigate to the folder where you want your connector to reside in. Don’t create any folder as it will be created automatically.
  2. Run the following command:
    mvn archetype:generate -DarchetypeGroupId=com.boomi.connsdk -DarchetypeArtifactId=connector-sdk-archetype -DarchetypeVersion=RELEASE -DsdkVersion=RELEASE
  3. Follow the instructions by defining the following properties in the below format

‘groupId’: com.<your_company_name>.connector.<your_connector_name>

‘artifactId’: boomi-connector-<your_connector_name>

‘version’ 1.0-SNAPSHOT: : 1.0

‘package’ com.<your_company_name>.connector.<your_connector_name>: <Empty>

The project will be created automatically after that

  1. Open the project in your preferred IDE. We’ll use Visual Studio Code
How to Create a Boomi Custom Connector
  1. Open the pom.xml file. If the version of the com.boomi.util dependency is 2.3.0, change the version to 2.3.8 as the project will not build with that version.
  2. Open the src/main/assembly/assembly.xml and change the id value from “-car” to any abbreviation you want your generated package to be suffixed with. This is not an important step, however, I don’t like the default “-car” suffix, can’t see it relevant.
  1. Run the following command to build the project

mvn clean install

This downloads the dependencies and builds the initial JAR. 

How to Create a Boomi Custom Connector

Congratulations! You’ve got a basic custom connector that compiles successfully but doesn’t do anything! Let’s get into more details

Step 3: Implement the Basic Structure

In this step, we’ll create the basic foundational classes you need in any connector. We’ll keep them empty for now, we’ll perform a basic implementation later in the article. 

  1. First, define a skeleton connection class as follows:
package com.boomi.connector.<connector_name>connector;
import com.boomi.connector.api.BrowseContext;
import com.boomi.connector.util.BaseConnection;

public class <connector_name>Connection extends BaseConnection {
public <connector_name>Connection(BrowseContext context) {
super(context);
}
}

PS: You can define reusable methods here as the connection will be referenced in all operations. We’ll do that later when we implement the classes with an actual scenario.

  1. Second, define the skeleton browser class as follows. The browser is needed if you need the browse / generate profiles capability while building your operations.
package com.boomi.connector.<connector_name>connector;
import com.boomi.connector.api.BrowseContext;
import com.boomi.connector.api.ConnectorException;
import com.boomi.connector.api.ContentType;
import com.boomi.connector.api.ObjectDefinition;
import com.boomi.connector.api.ObjectDefinitionRole;
import com.boomi.connector.api.ObjectDefinitions;
import com.boomi.connector.api.ObjectType;
import com.boomi.connector.api.ObjectTypes;
import com.boomi.connector.util.BaseBrowser;
import com.boomi.util.ClassUtil;
import com.boomi.util.IOUtil;
import com.boomi.util.StreamUtil;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.util.Collection;

public class <connector_name>Browser extends BaseBrowser {
   protected <connctor_name>Browser(BrowseContext context) {
       super(context);
   }

   @Override
   public ObjectDefinitions getObjectDefinitions(String objectTypeId
, Collection<ObjectDefinitionRole> roles) {
       // TODO Auto-generated method stub
       throw new UnsupportedOperationException("Unimplemented method 'getObjectDefinitions'");
   }

   @Override
   public ObjectTypes getObjectTypes() {
       // TODO Auto-generated method stub
       throw new UnsupportedOperationException("Unimplemented method 'getObjectTypes'");
   }
}
  1. If you want to expose a get operation, define the Get Operation class which exposes a reference to the connection class built previously, and implements the executeGet operation. This operation gets invoked when you run your integration and the document reaches the Get operation you created in your process. Keep it unimplemented for now, we’ll do that later. This will be used to implement the “get” functionality with a parameter within our connector.
package com.boomi.connector.<connector_name>connector;
import com.boomi.connector.api.GetRequest;
import com.boomi.connector.api.OperationResponse;
import com.boomi.connector.util.BaseGetOperation;

public class GetOperation extends BaseGetOperation {
   protected GetOperation(<connector_name>Connection connection) {
       super(connection);
   }

   @Override
   public <connector_name>Connection getConnection() {
       return (<connector_name>Connection) super.getConnection();
   }

   @Override
   protected void executeGet(GetRequest request, OperationResponse operationResponse) {
       throw new UnsupportedOperationException("Unimplemented method 'executeGet'");
   }
}
  1. If you want to expose an execute operation, define the Execute Operation class. Similar to the Get operation, let’s keep this unimplemented for now, we’ll do that later. This will be used to implement the “get” functionality without a parameter within our connector
package com.boomi.connector.<connector_name>connector;
import com.boomi.connector.api.OperationResponse;
import com.boomi.connector.api.UpdateRequest;
import com.boomi.connector.util.BaseUpdateOperation;

public class ExecuteOperation extends BaseUpdateOperation {
   protected ExecuteOperation(<connector_name>Connection connection) {
       super(connection);
   }

   @Override
   public <connector_name>Connection getConnection() {
       return (<connector_name>Connection) super.getConnection();
   }

   @Override
   protected void executeUpdate(UpdateRequest request, OperationResponse response)  
  {
      throw new UnsupportedOperationException("Unimplemented method 'executeUpdate'");
   }
}
  1. If you want to expose a create operation, define the create operation class in similar fashion to the previous definitions. This will be used to implement the “inserts” functionality within our connector
package com.boomi.connector.<connector_name>connector;
import java.io.InputStream;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.boomi.connector.api.ObjectData;
import com.boomi.connector.api.OperationResponse;
import com.boomi.connector.api.OperationStatus;
import com.boomi.connector.api.PayloadMetadata;
import com.boomi.connector.api.PayloadUtil;
import com.boomi.connector.api.ResponseUtil;
import com.boomi.connector.api.UpdateRequest;
import com.boomi.connector.util.BaseUpdateOperation;
import com.boomi.util.IOUtil;

public class CreateOperation extends BaseUpdateOperation{
   public CreateOperation(<connector_name>Connection connection) {
       super(connection);
   }

   @Override
   public <connector_name>Connection getConnection() {
       return (DemoConnection) super.getConnection();
   }

   @Override
   protected void executeUpdate(UpdateRequest updateRequest, OperationResponse operationResponse) {
       throw new UnsupportedOperationException("Unimplemented method 'executeUpdate'");
   }
}
  1. Now that the browser, connection, and operations are defined, we can define the connector class. This hooks everything together and basically invokes your defined classes for the operations triggered.
package com.boomi.connector.<connector_name>connector;
import com.boomi.connector.api.BrowseContext;
import com.boomi.connector.api.Browser;
import com.boomi.connector.api.Operation;
import com.boomi.connector.api.OperationContext;
import com.boomi.connector.util.BaseConnector;

public class <connector_name>Connector extends BaseConnector {
   public Browser createBrowser(BrowseContext context) {
       return new <connector_name>Browser(context);
   }

   @Override
   public Operation createGetOperation(OperationContext context) {
       return new GetOperation(new <connector_name>Connection(context));
   }

   @Override
   public Operation createCreateOperation(OperationContext context)
   {
       return new CreateOperation(new DemoConnection(context));
   }

   @Override
   protected Operation createExecuteOperation(OperationContext context) {
       return new ExecuteOperation(new <connector_name>Connection(context));
   }
}
  1. Before we continue developing our connector, we need to enter the fully qualified name of the connector class in the connector-config.xml file and change the sdkApiVersion. If you don’t change the version, the connector will not work. Open this file and do that:
<?xml version="1.0" encoding="UTF-8"?>
<GenericConnector sdkApiVersion="2.8.1">
<connectorClassName>com.<company_name>.connector.<connector_name>connector.<connector_name>Connector</connectorClassName>
</GenericConnector>
  1. As of now, we have a skeleton project with all base files needed. In the next sections, we’ll go into details of how to implement the code showcasing many common coding snippets.

Step 4: Implement Our Scenario

Our demo connector will use the GET, EXECUTE, and CREATE operations to get an order, all orders by status, and insert an order into a mock API. We’ll also implement the Browse class to provide design-time browsing capability in order to automatically create the relevant profiles. The following sections implements our connector’s functionality in full.

Implement the Connector Descriptor

Open the connector-desriptor.xml file. This file contains the definition of the connector from a user-interface perspective. You define the connector properties, the operations, the fields in those operations, any incoming document properties, and any outgoing document properties as well. Whatever you define here, you can access in the classes you previously created. Here’s the implementation for our connector. The implementation is thoroughly commented to explain all included sections.

<?xml version="1.0" encoding="UTF-8"?>
<GenericConnectorDescriptor>
   <!-- Define the connector properties -->
  <field id="host" label="Host" type="string">
      <helpText>The host of the target service</helpText>
      <defaultValue>your_api_host</defaultValue>
  </field>

  <field id="username" label="Username" type="password"/>
  <field id="password" label="Password" type="password"/>

  <!-- Define the Get operation, this required an input Id parameter to get a single resource -->
  <operation types="GET" customTypeId="GET"/>
  <!-- Define the CREATE operation -->
  <operation types="CREATE" customTypeId="INSERT"/>
  <!-- Define the Execute operation and use it to get data (without a need for Id parameter)-->

  <!-- Define properties for the operation -->
  <operation types="EXECUTE" customTypeId="GET_ALL" customTypeLabel="QUERY">
      <field type="string" id="query" label="Query Parameter">
          <helpText>The query parameter value, such as retrieve success or failures.</helpText>
          <defaultValue>successes</defaultValue>
          <allowedValue label="Successes">
              <value>successes</value>
          </allowedValue>
          <allowedValue label="Failures">
              <value>failures</value>
          </allowedValue>
      </field>
  </operation>

  <!-- Define an incoming document property to send a source value to the destination -->
  <dynamicProperty type="string" id="source" label="Source" />
  <!-- Define an outgoing document property to return the operation status code -->
  <trackedProperty id="status_code" label="Status Code" />
 </GenericConnectorDescriptor>

Create the Schema Files

In order for the Browser class to be able to successfully create the profiles at design-time, we need to define the schemas of the profiles to be created. 

We need two profiles to be created, one for getting or inserting a single order, here’s how it looks like:

{
     "order_number": "ORD001",
     "customer_id": "CUST001",
     "customer_name": "Alice Smith",
     "order_date": "2024-03-01T09:30:00Z",
     "total_amount": 149.99,
     "status": "Processing",
     "payment_method": "Credit Card",
     "shipping_method": "Standard"
}

The other is to retrieve multiple orders, here’s how it will look like

{
 "orders": [
   {
     "order_number": "ORD001",
     "customer_id": "CUST001",
     "customer_name": "Alice Smith",
     "order_date": "2024-03-01T09:30:00Z",
     "total_amount": 149.99,
     "status": "Processing",
     "payment_method": "Credit Card",
     "shipping_method": "Standard"
   },
   {
     "order_number": "ORD002",
     "customer_id": "CUST002",
     "customer_name": "Bob Johnson",
     "order_date": "2024-03-02T14:15:00Z",
     "total_amount": 279.95,
     "status": "Shipped",
     "payment_method": "PayPal",
     "shipping_method": "Express"
   }
}

We need to generate schemas for both. To do that, we can use any online tool such as transform.tools to do that

Once the schemas are generated, we can save them as json files under the resources folder of our project’s structure.

How to Create a Boomi Custom Connector

Implement the Browse Functionality

In order to implement the Browse functionality for auto-creation of profiles, we need to implement the two overridden methods created previously. The two methods are:

  1. getObjectDefinitions: This method is invoked in the first step of the Import Operation to display the list of object types and allow you to select one. So your implementation here is to populate this list of “supported” object types. Here’s a sample of the Amazon S3 connector object types when importing an operation.
  1. getObjectTypes: This method is invoked when attempting to generate the profiles, so your implementation here should point out to the correct schemas so that the right profiles would be generated. This method takes as input the objectTypeId, which could be either GET, EXECUTE, or CREATE (as defined in the descriptor file), and the list of roles, which would be INPUT and OUTPUT. These parameters allow you to know the context of the call in order to point out to the right schema.

Go on and replace the getObjectDefinitions implementation with the following one. This implementation adds two types to the list, one is Order and the other is Orders

@Override
   public ObjectTypes getObjectTypes() {
       ObjectTypes types = new ObjectTypes();
       ObjectType ordertype = new ObjectType();
       ordertype.setId(ORDER);
       ordertype.setLabel(ORDER);
       types.getTypes().add(ordertype);

       ObjectType orderstype = new ObjectType();
       orderstype.setId(ORDERS);
       orderstype.setLabel(ORDERS);
       types.getTypes().add(orderstype);
       return types;
   }

Then replace the getObjectDefinitions implementation by the below one

@Override
   public ObjectDefinitions getObjectDefinitions(String objectTypeId, Collection<ObjectDefinitionRole> roles) {
       String jsonSchema = null;
       ObjectDefinitions definitions = new ObjectDefinitions();
       ObjectDefinition objdef = new ObjectDefinition();
// Get the custom operation schema as per the descriptor operation definition (ex. GET_ALL, INSERT, GET)
       String customOpsType = getContext().getCustomOperationType();
// Get the operation schema as per the descriptor operation definition (ex. GET, EXECUTE, CREATE) -- NOT USED
       OperationType opsType = getContext().getOperationType();
       switch (customOpsType) {
           case "INSERT":
               jsonSchema = getJsonOrderSchema();
               break;
           case "GET_ALL":
           jsonSchema = getJsonOrdersSchema();
               break;
           case "GET":
               jsonSchema = getJsonOrderSchema();
               break;
           default:
               break;
       }

       if (jsonSchema == null) {
           definitions = this.getUnstructuredSchema(objdef, definitions);
       } else {
           definitions = this.getJsonStructure(jsonSchema, objdef, definitions);
       }
       //return definitions;
       return definitions;
   }

This implementation captures the custom operation type as defined in the descriptor file, and based on the values reads the appropriate json schema and add it to the collection of object definitions. You could use the operation schema value as well, both works, and we’ve retrieved that as well in case you want to change the implementation.

Also, add the below implementations for the private methods used in the getObjectDefinitions new implementation

private static String getJsonOrdersSchema() {
       String schema;
       InputStream is = ClassUtil.getResourceAsStream(ORDERS_SCHEMA);
       try {
           schema = StreamUtil.toString(is, Charset.forName(UTF8));
       } catch (IOException ex) {
           throw new ConnectorException("Error reading schema", ex);
       } finally {
           IOUtil.closeQuietly(is);
       }
       return schema;
   }

   private static String getJsonOrderSchema() {
       String schema;
       InputStream is = ClassUtil.getResourceAsStream(ORDER_SCHEMA);
       try {
           schema = StreamUtil.toString(is, Charset.forName(UTF8));
       } catch (IOException ex) {
           throw new ConnectorException("Error reading schema", ex);
       } finally {
           IOUtil.closeQuietly(is);
       }
       return schema;
   }

   private ObjectDefinitions getUnstructuredSchema(ObjectDefinition objdef, ObjectDefinitions objdefs) {
       objdef.setElementName("");
       objdef.setOutputType(ContentType.BINARY);
       objdef.setInputType(ContentType.NONE);
       objdefs.getDefinitions().add(objdef);
       return objdefs;
   }

   private ObjectDefinitions getJsonStructure(String jsonSchema, ObjectDefinition objdef, ObjectDefinitions objdefs) {
       objdef.setElementName("");
       objdef.setJsonSchema(jsonSchema);
       objdef.setOutputType(ContentType.JSON);
       objdef.setInputType(ContentType.JSON);
       objdefs.getDefinitions().add(objdef);
       return objdefs;
   }

To close the Browser implementation off, build the project again using the mvn clean install command to verify your changes haven’t caused any compilation issues.

Create Helper Classes for HTTP Client Functionality

Since in this scenario we’ll be performing some HTTP Gets and Posts to external APIs, we need to create a couple of helper classes. If your scenario doesn’t include such a requirement, you can skip this section.

  1. Create a folder called “client” under the democonnector folder
  2. Create a file called “RepeatableInputStreamEntity.java” and populate it with the below code. Neglect the compilation errors for now.
package com.boomi.connector.<connector_name>connector.client;
import org.apache.http.entity.InputStreamEntity;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

public class RepeatableInputStreamEntity extends InputStreamEntity {
   public RepeatableInputStreamEntity(InputStream input, long length) {
       super(input, length);
       if (!input.markSupported()) {
           throw new IllegalArgumentException("input does not support mark/reset");
       }
   }

   @Override
   public boolean isRepeatable() {
       return true;
   }

   @Override
   public InputStream getContent() throws IOException {
       InputStream content = super.getContent();
       content.reset();
       return content;
   }

   @Override
   public void writeTo(OutputStream outstream) throws IOException {
       super.getContent().reset();
       super.writeTo(outstream);
   }
}
  1. Create a file called “RESTClient.java” and populate it with the below code. Neglect the compilation errors for now.
package com.boomi.connector.<connector_name>connector.client;

import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.impl.client.CloseableHttpClient;
import java.io.Closeable;
import java.io.IOException;

public class RESTClient implements Closeable {
   private final CloseableHttpClient _client;
   private final HttpClientContext _context;

   public RESTClient(CloseableHttpClient client, HttpClientContext context){
       _client = client;
       _context = context;
   }

   public void close() throws IOException {
       _client.close();
   }

   public CloseableHttpResponse executeRequest(HttpUriRequest request) throws Exception{
       return _client.execute(request,_context);
   }
}
  1. Open the pom.xml file located in the project root
  2. Add the following dependency section under the <dependencies> element
<dependency>
           <groupId>org.apache.httpcomponents</groupId>
           <artifactId>httpclient</artifactId>
           <version>4.5.14</version>
</dependency>
  1. Rebuild the project to verify no compilation errors are introduced

mvn clean install

Implement the Connection Class

Let’s move to the next step and implement the Connection class. This class will be referenced from within all operation classes, so it’s a good place to put any code that needs to be reused across different operations. Let’s start by defining three static strings that correspond to the host, username, and password fields defined in the descriptor file.

private static final String USERNAME_FIELD = "username";
private static final String PASSWORD_FIELD = "password";
private static final String BASE_HOST_FIELD = "host";

We’ll read those values set by the user in the connection properties into these three fields. To do that, we can use the below command:

getContext().getConnectionProperties().getProperty(BASE_HOST_FIELD);

Ok, add the below method to set the base URL of the API we will be invoking

/**
    * Fetch the base url field, this is defined in the descriptor. the id should always match to fetch the correct field.
    * @return the baseURL defined by the user
    */
   public String getHost() {
       String host = getContext().getConnectionProperties().getProperty(BASE_HOST_FIELD);
       if (host.startsWith("http")) {
           host = host.replace("https://", "").replace("http://", "");
       }
       host = "https://" + host;
       return host;
   }

Also, add the below methods to set up a rest client that can be easily used from within the operations. Note how we retrieve the username and password using the command we mentioned above

/**
    * transport library, this can be anything you want it to be.
    * @return the restClient
    */
   public RESTClient getRESTClient() {
       return new RESTClient(HttpClientBuilder.create().build(), getHttpContext());
   }

   private HttpClientContext getHttpContext() {
       HttpClientContext httpContext = HttpClientContext.create();
       httpContext.setCredentialsProvider(getCredentialsProvider());
       return httpContext;
   }

   private CredentialsProvider getCredentialsProvider() {
       CredentialsProvider credsProvider = new BasicCredentialsProvider();
       credsProvider.setCredentials(AuthScope.ANY,
               new UsernamePasswordCredentials(
getContext().getConnectionProperties().getProperty(USERNAME_FIELD),
getContext().getConnectionProperties().getProperty(PASSWORD_FIELD)));
       return credsProvider;
   }

Add also a definition for a utility method to convert a stream to a string. We’ll be using that later as well

/**
    * Utility to convert InputStream to String
    * @param is
    * @return
    * @throws IOException
    */
   public static String inputStreamToString(InputStream is) throws IOException {
       try (BufferedReader buffer = new BufferedReader(new InputStreamReader(is)))   
   {
           return buffer.lines().collect(Collectors.joining("\n"));
       }
   }

Imagine, all we’ve done until now is prep work! Next we’ll start the core implementation of the connector’s functionality, i.e, the code that will run once the documents enter the scope of the connector. Let’s start by implementing the operations one by one.

Implementing the Execute Operation

As we’ve seen in the configuration of the descriptor file, and the implementation of the connector file, the execute operation class will be invoked when the user selects the QUERY operation when configuring a connector in his process. Here, we’ll be implementing the logic that gets triggered in that operation.

First, add the following imports to the top of the ExecuteOperation.java file

import java.util.logging.Logger;
import java.util.logging.Level;
import org.json.JSONObject;

We need to add a dependency for the org.json component, so open the pom.xml and add the following dependency under the dependencies element.

<dependency>
           <groupId>org.json</groupId>
           <artifactId>json</artifactId>
           <version>20240205</version>
</dependency>

As we go, those versions used in the dependencies configuration might change, so try to always keep using the latest versions.

Ok, now we are ready to replace the executeUpdate implementation by the below one. We have included detailed commenting so that you would understand each of the steps involved

@Override
protected void executeUpdate(UpdateRequest updateRequest, OperationResponse operationResponse) {
       // Get the logger, used for logging purposes
       Logger logger   = operationResponse.getLogger();
       // Get the query parameter value defined in the descriptor, as per the user's selection in the operation
       String query = getContext().getOperationProperties().getProperty("query");
       CloseableHttpResponse response = null;
       RESTClient client = null;
       String responseString = null;
       String message = null;
       // Since we are issuing a GET request, we neglect all incoming documents and only get a reference to one
       // So that we don't issue a GET request for each incoming document
       ObjectData input = updateRequest.iterator().next();
       // Get a reference to the dynamic properties defined in the descriptor file
       Map<String, String> dynamicProps    = input.getDynamicProperties();
       // Get the value of the source dynamic document property, as set by the user in the process
       String source = dynamicProps.get("source");
       try {
           // Set the URL           
           String uri = getConnection().getHost() + "/orders";
           // Create a GET request, add a source header and the query param as defined in the descriptor
           HttpUriRequest httpRequest = RequestBuilder.create("GET")
                                                       .addHeader("source", source)
                                                       .addParameter("status", query)
                                                       .setUri(uri).build();
           logger.log(Level.INFO, "REQUEST: " + httpRequest.toString());
           client = getConnection().getRESTClient();
           // Call the API
           response = client.executeRequest(httpRequest);
           // Get the response status code
           int statusCode = response.getStatusLine().getStatusCode();
           // Save the response as string, use the utility function we previously defined in the Connection class
           responseString = getConnection().inputStreamToString(response.getEntity().getContent());
           // Instantiate the JsonObject in case we need to parse the JSON response
           JSONObject jsonresponse = new JSONObject(responseString);
           // Not used here, however if the API returns back a status message, you can retrieve it and use it in the
           // connector result
           //message = jsonresponse.getString("some_element");
           if (statusCode >= 200 && statusCode < 300) {
               message = "OK";
               if (!responseString.contains("[]")) {
                   // Return success and continue processing with the API response data
                   operationResponse.addResult(input, OperationStatus.SUCCESS, String.valueOf(statusCode), message, PayloadUtil.toPayload(responseString));
               } else {
                   // In case the API returns an empty array
                   logger.log(Level.INFO, "EMPTY RESPONSE: " + responseString);
                   // Return success but stop further processing. Documents stop at this stage and don't continue
                   // further in the process
                   ResponseUtil.addEmptySuccess(operationResponse, input,
                       String.valueOf(response.getStatusLine().getStatusCode()));
               }
           } else {
               message = "FAILURE";
               logger.log(Level.SEVERE, "API ERROR: " + responseString);
               // Use APPLICATION_ERROR Operation Status instead of FAILURE to allow setting as
               // per the operation's "Error Behavior" property
               operationResponse.addResult(input, OperationStatus.APPLICATION_ERROR, String.valueOf(statusCode), message, PayloadUtil.toPayload(responseString));
           }
       } catch (Exception e) {
           logger.log(Level.SEVERE, "CONNECTOR EXCEPTION: " + e.toString());
           ResponseUtil.addExceptionFailure(operationResponse, input, e);
       } finally {
           IOUtil.closeQuietly(response, client);
           logger.log(Level.INFO, "FINISHED PROCESSING...");
       }
   }

This should be enough to invoke the external service and return an appropriate response based on the external API response.

Implementing the Create Operation

Similar to the Execute operation, we’ll implement the create operation to create a new record using the target’s API post method. We’ll replace the executeUpdate implementation by the one below

@Override
protected void executeUpdate(UpdateRequest updateRequest, OperationResponse operationResponse) {
       Logger logger   = operationResponse.getLogger();
       PayloadMetadata metadata    = operationResponse.createMetadata();
       String responseString = null;
       String message = null;
       Map<String, String> dynamicProps    = null;
       String source = null;

       // For all of the incoming documents, perform a POST to create a record
       RESTClient client = null;

       for (ObjectData data : updateRequest) {
           // Get a reference to the dynamic properties defined in the descriptor file
           dynamicProps    = data.getDynamicProperties();
           // Get the value of the source dynamic document property, as set by the user in the process
           source = dynamicProps.get("source");
           CloseableHttpResponse response = null;
           InputStream dataStream = null;

           try {
               // fetch the document data as a stream
               dataStream = data.getData();
               String uri = getConnection().getHost() + "/orders";
               // Create a POST request, add a source header from the document property
               HttpUriRequest request = RequestBuilder.create("POST")
                                                       .addHeader("source", source)
                                                       .setUri(uri).setEntity(
                                       new RepeatableInputStreamEntity(dataStream, data.getDataSize()) {
                                       }).build();
               logger.log(Level.INFO, "REQUEST: " + request.toString());
               client = getConnection().getRESTClient();
                // Call the API
               response = client.executeRequest(request);
               InputStream responseStream = response.getEntity().getContent();
               // Save the response as string, use the utility function we previously defined in the Connection class
               responseString = getConnection().inputStreamToString(responseStream);
               // Get the response status code
               int statusCode = response.getStatusLine().getStatusCode();
               // Instantiate the JsonObject to parse the JSON response
               JSONObject jsonresponse = new JSONObject(responseString);
               // The API returns back a status message, retrieve it and use it in the connector result
               message = jsonresponse.getString("message");
               if (statusCode >= 200 && statusCode < 300) {
                   operationResponse.addResult(data, OperationStatus.SUCCESS, String.valueOf(statusCode), message, PayloadUtil.toPayload(responseString));
               } else {
                   // Unsuccessful call, return status code of why
                   logger.log(Level.SEVERE, "API ERROR: " + responseString);
                   // Use APPLICATION_ERROR Operation Status instead of FAILURE to allow setting as per the operation's "Error Behavior" property
                   operationResponse.addResult(data, OperationStatus.APPLICATION_ERROR, String.valueOf(statusCode), message, PayloadUtil.toPayload(responseString));
               }
           } catch (Exception e) {
               logger.log(Level.SEVERE, "CONNECTOR EXCEPTION: " + e.toString());
               //Exception occurred, add failure.
               ResponseUtil.addExceptionFailure(operationResponse, data, e);
           } finally {
               IOUtil.closeQuietly(response, dataStream);
               logger.log(Level.INFO, "FINISHED PROCESSING...");
           }
       }
       IOUtil.closeQuietly(client);
   }

Implementing the Get Operation

Last operation to implement is the Get operation that will be used to get a single order. Open the GetOperation.java file and replace the executeGet method with the below one.   

@Override
protected void executeGet(GetRequest request, OperationResponse operationResponse) {
       //Fetch the object ID
       ObjectIdData objectId = request.getObjectId();
       Logger logger   = operationResponse.getLogger();
       CloseableHttpResponse response = null;
       RESTClient client = null;
       String responseString = null;
       String message = null;

       try {
           String uri = getConnection().getHost() + "/orders/" + objectId.getObjectId();
           // Create the GET request
           HttpUriRequest httpRequest = RequestBuilder.create("GET").setUri(uri).build();
           client = getConnection().getRESTClient();
           // Call the API
           response = client.executeRequest(httpRequest);
           // Get the response status code
           int statusCode = response.getStatusLine().getStatusCode();
           // Save the response as string, use the utility function we previously defined in the Connection class
           responseString = getConnection().inputStreamToString(response.getEntity().getContent());
           // Instantiate the JsonObject in case we need to parse the JSON response
           JSONObject jsonresponse = new JSONObject(responseString);
           // Not used here, however if the API returns back a status message, you can retrieve it and use it in the
           // connector result
           //message = jsonresponse.getString("some_element");
           if (statusCode >= 200 && statusCode < 300) {
               message = "OK";
               if (!responseString.contains("{}")) {
                   // Return success and continue processing with the API response data
                   operationResponse.addResult(objectId, OperationStatus.SUCCESS, String.valueOf(statusCode), message, PayloadUtil.toPayload(responseString));
               } else {
                   // In case the API returns an empty array
                   logger.log(Level.INFO, "EMPTY RESPONSE: " + responseString);
                   // Return success but stop further processing. Documents stop at this stage and don't continue
                   // further in the process
                   ResponseUtil.addEmptySuccess(operationResponse, objectId,
                       String.valueOf(response.getStatusLine().getStatusCode()));
               }
           } else {
               message = "FAILURE";
               logger.log(Level.SEVERE, "API ERROR: " + responseString);
               // Use APPLICATION_ERROR Operation Status instead of FAILURE to allow setting as
               // per the operation's "Error Behavior" property
               operationResponse.addResult(objectId, OperationStatus.APPLICATION_ERROR, String.valueOf(statusCode), message, PayloadUtil.toPayload(responseString));
           }
       } catch (Exception e) {
           ResponseUtil.addExceptionFailure(operationResponse, objectId, e);
       } finally {
           IOUtil.closeQuietly(response, client);
       }
   }

Finally, build the project again using the maven clean install command to verify there’s no compilation errors.

Step 5: Test the Connector

Before deploying the connector, it’s a good idea to unit test your code from within the IDE. To do that, perform the following:

  1. Add the below dependency to the pom.xml file
<dependency>
           <groupId>com.boomi.connsdk</groupId>
           <artifactId>connector-sdk-test-util</artifactId>
</dependency>
  1. Create a test file under the test directory and populate it with test methods for each operation as follows:
@Test
public void testGetOperation() throws Exception
   {
       DemoConnector connector = new DemoConnector();
       ConnectorTester tester = new ConnectorTester(connector);
       // setup the operation context for a GET operation
       Map<String, Object> connProps = new HashMap<String, Object>();
       connProps.putIfAbsent("host", "your_api_host");
       connProps.putIfAbsent("username", "test");
       connProps.putIfAbsent("password", "test");
       Map<String, Object> opProps = new HashMap<String, Object>();
       tester.setOperationContext(OperationType.GET, connProps, opProps, "ORD001", null);
       List<SimpleOperationResult> results = tester.executeGetOperation("ORD001");

       for (SimpleOperationResult result : results) {
           assertEquals("Wrong result", OperationStatus.SUCCESS, result.getStatus());
       }
   }

   @Test
   public void testExecuteOperation() throws Exception
   {
       DemoConnector connector = new DemoConnector();
       ConnectorTester tester = new ConnectorTester(connector);

       // setup the operation context for a GET operation
       Map<String, Object> connProps = new HashMap<String, Object>();
       connProps.putIfAbsent("host", "your_api_host");
       connProps.putIfAbsent("username", "test");
       connProps.putIfAbsent("password", "test");
       Map<String, Object> opProps = new HashMap<String, Object>();
       opProps.putIfAbsent("query", "successes");
       tester.setOperationContext(OperationType.EXECUTE, connProps, opProps, null, null);
       // Create the list of input streams, needed as an input for the Execute operation
       String testinput = "not_needed";
       InputStream targetStream = new ByteArrayInputStream(testinput.getBytes());
       List<InputStream> list = new ArrayList<InputStream>();
       list.add(targetStream);
       List<SimpleOperationResult> results = tester.executeExecuteOperation(list);

       for (SimpleOperationResult result : results) {
           assertEquals("Wrong result", OperationStatus.SUCCESS, result.getStatus());
       }
   }

   @Test
   public void testCreateOperation() throws Exception
   {
       DemoConnector connector = new DemoConnector();
       ConnectorTester tester = new ConnectorTester(connector);
       // setup the operation context for a GET operation
       Map<String, Object> connProps = new HashMap<String, Object>();
       connProps.putIfAbsent("host", "your_api_host");
       connProps.putIfAbsent("username", "test");
       connProps.putIfAbsent("password", "test");
       Map<String, Object> opProps = new HashMap<String, Object>();
       tester.setOperationContext(OperationType.CREATE, connProps, opProps, null, null);
       // Create the list of input streams, needed as an input for the Execute operation
       String testinput = "{\"order_number\":\"ORD001\",\"customer_id\":\"CUST001\",\"customer_name\":\"Alice Smith\",\"order_date\":\"2024-03-01T09:30:00Z\",\"total_amount\":149.99,\"status\":\"Processing\",\"payment_method\":\"Credit Card\",\"shipping_method\":\"Standard\"}";

       InputStream targetStream = new ByteArrayInputStream(testinput.getBytes());
       List<InputStream> list = new ArrayList<InputStream>();
       list.add(targetStream);
       List<SimpleOperationResult> results = tester.executeCreateOperation(list);

       for (SimpleOperationResult result : results) {
           assertEquals("Wrong result", OperationStatus.SUCCESS, result.getStatus());
       }
   }
  1. Compiling the project using the mvn clean install command would run all the tests before generating the packages. 

Another option is to right-click any method and select “Debug Java”, this will allow you to step into the code and trace it step-by-step. Don’t forget to set some breakpoints along the way.

How to Create a Boomi Custom Connector

Step 6: Deploy the Connector

Deployment of the connector is the standard one as explained in Boomi’s official article. However, again, for completeness, here’s the steps you need to perform:

  1. Ensure you build the project to compile the latest version of your changes

mvn clean install

  1. Copy two files into a separate directory. The zip archive generated, and the descriptor file. This is an optional step, however, we always like to copy them to a deployment-specific folder.
  1. Head to Boomi platform now, into the Settings >> Developer page
  2. If no suitable connector group exists, create a connector group, and select the descriptor and archive file you saved in the previous step
How to Create a Boomi Custom Connector
  1. Click Ok
  2. For any subsequent installations (any bug fixes, improvements), you can click on the “Add Version” button and reselect the updated descriptor and archive files
How to Create a Boomi Custom Connector
  1. Now you can go on and use the connector similar to any other out-of-the-box connector. Here’s we’re creating a connection and setting it’s properties
How to Create a Boomi Custom Connector
  1. Here, we’re creating a GET operation. Click the Import Operation to test the Browse functionality we implemented
How to Create a Boomi Custom Connector
  1. After selecting the Atom, you will be presented with the available object types, which is the result of your implementation of the “getObjectTypes” browser method. Select “Order” in this case and click “Next
How to Create a Boomi Custom Connector
  1. Click finish to invoke the “getObjectDefinitions” method and generated the order profile
How to Create a Boomi Custom Connector
  1. Here’s the order profile generated
How to Create a Boomi Custom Connector
  1. Similarly, you can create the query and insert operations. Here, we’re running the Query operation and retrieving the results
How to Create a Boomi Custom Connector
  1. Here, we’ve created a sample process that tests the three operations created within our connector
How to Create a Boomi Custom Connector
  1. We can also view the logs to see the connector-specific log messages we printed out in our implementation
How to Create a Boomi Custom Connector
  1. Finally, let’s inspect the document property we created in the descriptor file. We can set that via a “Set Properties” shape as you can see below
How to Create a Boomi Custom Connector
How to Create a Boomi Custom Connector

Conclusion

In this guide, we went above and beyond to provide a full implementation of a Boomi custom connector covering a lot of details around the implementation and covering a lot of aspects that you would normally need a lot of research and many articles to cover. We grouped all that in a single guide that you can use as a reference for any future development of any Boomi custom connector. Better off, we’ve attached the custom connector implementation here, to make your life easier and give you a kickstart to any custom connector you might need to create. Hope you have enjoyed it. Happy learning!

Leave a Reply

Discover more from ihub4us

Subscribe now to keep reading and get access to the full archive.

Continue reading