In this article, we will cover the following topics:

  1. A detailed, step-by-step guide on migrating a MuleSoft connector from Mule 3 to Mule 4, utilizing the Devkit Migration Tool (DMT).
  2. An overview of the Mule 4 connector project structure, including insights into DataSense and error handling mechanisms.
  3. Practical migration tips and a compilation of helpful resources to facilitate the successful completion of the connector migration process.

Introduction

In January 2018, Mulesoft unveiled the first stable release of Mule 4, version 4.1. This new version brought significant advancements, including the integration of Dataweave 2.0, which replaced the Mule Expression Language (MEL), along with a revamped message structure and a more streamlined error-handling mechanism.

Following this release, the focus for developers shifted towards migrating Mule 3 connectors to the updated Mule 4 platform. The development process under Mule 3 involved the use of the Mule Development Kit (DevKit), whereas Mule 4 utilizes the Mule SDK.

To facilitate this transition, MuleSoft introduced the DevKit Migration Tool (DMT), designed to serve as an initial step in the migration process. After utilizing this tool, the migrated Mule connector typically requires additional manual adjustments and error resolutions to ensure full compatibility with Mule 4.

Migrating to Mule 4 using the DevKit Migration Tool

The following steps will show how to use the DMT to start the migration process.

  • Open the Maven POM file in Anypoint Studio v6.5.x (Mule 3) and replace the parent tag with the following one:
<parent>
<groupId>org.mule.tools.dmt</groupId>
<artifactId>mule-dmt</artifactId>
<version>1.0.0</version>
</parent>

  • Open the terminal, go to the project directory and execute the following command to convert the connector to a Mule 4 one. The converted application will be saved in the Mule 3 Project > target > generated-sources > extension directory.
mvn clean package
  • Copy the contents of the extension directory to the desired location. (Note: The tests are not migrated by the DMT.)
  • Open Anypoint Studio v7.3.x (Mule 4) and select File > Import > Anypoint Studio Project from File System​ then click Next. Click on  and then from the Finder window locate the copied extension directory and click Finish.
  • Close Anypoint Studio 7 and open the .project file in the project root directory. Then check that the following configuration is present within the projectDescription tag. Then, reopen the connector project in Anypoint Studio 7.
<buildSpec>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments></arguments>
</buildCommand>
<buildCommand>
<name>org.mule.tooling.core.muleStudioBuilder</name>
<arguments></arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.mule.tooling.core.muleStudioNature</nature>
<nature>org.eclipse.jdt.core.javanature</nature>
</natures>
  • view rawproject.xml hosted with GitHub Open the pom.xml file and replace the parent tag with the following one:
<parent>
<groupId>org.mule.connectors</groupId>
<artifactId>mule-certified-parent</artifactId>
<version>1.4.1</version>
</parent>
  • Open the Problems tab from Window > Show View > Problems to determine the pending issues:
  • Additionally, the DMT creates some TODO tasks which can be viewed by selecting Window > Show View > Tasks:

Mule 4 Connector Project

After employing the DMT tool to upgrade the Mule 3 connector to Mule 4, it’s pertinent to delve into the project structure of the newly created Mule 4 connector. A notable aspect in Mule 4 is the alteration of the Anypoint Studio Palette by MuleSoft.

The Mule 4 Anypoint Studio Palette

In Mule 4, the Anypoint Studio Palette has been reorganized to group a connector’s operations under its name. This enhancement streamlines the development process by allowing application developers to directly drag and drop the desired operations onto the canvas. Previously, it was necessary to first place the connector on the canvas before selecting an operation. For example, the following image illustrates the HTTP connector positioned on the left side of the palette, with its respective operations displayed on the right side.

 

Class Loading Isolation

To grasp the project structure created by the DMT, it’s crucial to understand that Mule 4 employs class loading isolation. This technique segregates modules, applications, and the runtime, utilizing the “api” and “internal” packages to manage visibility and interaction.

This isolation leads to the usage of two primary packages, which determine the visibility of Java classes within the component:

  1. src/main/java/package_name/api: This package contains Java classes that are visible externally to a Mule application using the connector. For instance, these include classes that define return types.
  2. src/main/java/package_name/internal: Here, you’ll find Java classes that are integral to the connector’s functionality, such as those defining the Extension and Operation classes, which will be discussed in more detail subsequently.

Extensions, Operations, and Connection Providers

With an understanding of the package structure in place, we can now explore the key classes within a Mule 4 connector project. Primarily, the project revolves around three types of classes:

  1. Extension Class: This class is the core of the Mule 4 connector, taking the place of the Mule 3 Connector class. It is annotated with several important markers, including @Extension and @Configurations. The @Extension annotation defines the connector and vendor names, while the @Configurations annotation points to the configuration class. The configuration class, in turn, links to the operations and connection providers Java classes through the @Operations and @ConnectionProviders annotations, respectively. Below are examples of an extension class (MyConnectorExtension.java) and a corresponding configuration class (BasicConfiguration.java):
import org.mule.runtime.extension.api.annotation.Extension;
import org.mule.runtime.extension.api.annotation.Operations;
import org.mule.runtime.extension.api.annotation.connectivity.ConnectionProviders;

@Xml(prefix = "conn-name")
@Extension(name="Connector Name", vendor="MyCompany")
@Configurations(BasicConfiguration.class)
@ErrorTypes(CustomConnectorErrorType.class)
public class MyConnectorExtension {
}
import org.mule.runtime.extension.api.annotation.Operations;
import org.mule.runtime.extension.api.annotation.connectivity.ConnectionProviders;
import org.mule.runtime.extension.api.annotation.param.Parameter;

@Operations(MyConnectorOperations.class)
@ConnectionProviders({ 
    MyFirstConnectorConnectionProvider.class, 
    MySecondConnectorConnectionProvider.class 
})
public class BasicConfiguration {

}

Operation Classes Defining Connector Operations

A Mule 4 connector project typically includes one or more Operation classes. These classes are crucial as they define the connector’s operations, which in Mule 3 were previously outlined in the *Connector.java class. To properly integrate these Operation classes into the connector’s framework, they must be declared within the @Operations annotation of the configuration class. Each Operation class should consist of one or more public Java methods that encapsulate the functionalities of the connector.

The code snippet below from MyConnectorOperations.java demonstrates how annotations are employed to define the behavior of these components within a Mule Application. Key annotations include:

  • @DisplayName: This annotation is used to set the operation’s name as it will appear in the Mule Palette and flows.
  • @Summary: Provides a brief description of the component’s functionality, offering developers an understanding of what the operation accomplishes.
  • @Placement and @DisplayName: These are used in conjunction to determine the presentation and labeling of the operation’s parameters during the connector configuration process.
import org.mule.runtime.extension.api.annotation.param.display.DisplayName;
import org.mule.runtime.extension.api.annotation.param.display.Summary;
import org.mule.runtime.extension.api.annotation.param.Connection;

public class MyConnectorOperations {
    @DisplayName("Add")
    @Summary("Adds the firstNumber and secondNumber and returns the result")
    public int add(@Connection CustomConnection connection,
                    @Placement(order = 1) @DisplayName("First") int firstNumber,
                    @Placement(order = 2) @DisplayName("Second") int secondNumber)
    {
        return firstNumber + secondNumber;
    }
}

Connection Provider Classes Managing Connections

In a Mule 4 connector project, one or more Connection Provider classes play a pivotal role. Each class is tasked with overseeing a specific type of connection, such as OAuth.

These classes are integral in defining the connection configuration aspect of the component. For example, the image below illustrates the configuration section of an HTTP Listener.

To effectively manage connections, each Connection Provider class must implement the PoolingConnectionProvider<T> class, where ‘T’ represents the connection strategy. These classes consist of various variables, each marked with the @Parameter annotation. Furthermore, they override essential methods like connect, disconnect, and validate to ensure proper connection management.

Additionally, the parameters within these classes can be further annotated for enhanced usability and clarity:

  • @DisplayName: This annotation is employed to dictate how the parameters will appear in the Global Element Properties window, enhancing the user interface and making it more intuitive.
  • @Optional: By using this annotation, it’s indicated that a parameter is not mandatory. In such cases, a default value can be provided, allowing for greater flexibility and customization in the connection setup.
import org.mule.runtime.api.connection.ConnectionException;
import org.mule.runtime.extension.api.annotation.param.Parameter;
import org.mule.runtime.extension.api.annotation.param.Optional;
import org.mule.runtime.api.connection.ConnectionValidationResult;
import org.mule.runtime.api.connection.ConnectionProvider;
import org.mule.runtime.extension.api.annotation.param.display.DisplayName;

public class MyFirstConnectorConnectionProvider 
        implements PoolingConnectionProvider {

    @Parameter
    private String requiredParameter;

    @Parameter
    @DisplayName("Optional Parameter")
    @Optional(defaultValue = "100")
    private int optionalParam;

    @Override
    public MyFirstConnectorConnection connect() throws ConnectionException {
        //TODO: Connect
    }

    @Override
    public void disconnect(MyFirstConnectorConnection connection) {
        //TODO: Disconnect
    }

    @Override
    public ConnectionValidationResult validate(MyFirstConnectorConnection connection) {
        //TODO: Validate the connection
        return ConnectionValidationResult.success();
    }
}

DataSense

DataSense[6] is used to assist the developer in determining what the input and output types of a particular operation are (this is also referred to as metadata). For instance, below we can see an example of an operation with the input DataSense of type Number and output DataSense of type MyCustomObject.

The simplest method to implement DataSense for a particular operation is to create a Java class which defines both of the corresponding input and output metadata.

This is achieved by having the class implement the InputTypeResolver and OutputTypeResolver interfaces as in the example below. Note that the getInputMetadata method will return the input metadata, while the getOutputMetadata method will return the output metadata.

import org.mule.metadata.api.builder.BaseTypeBuilder;
import org.mule.metadata.api.model.MetadataFormat;
import org.mule.metadata.api.model.MetadataType;
import org.mule.runtime.api.metadata.MetadataContext;
import org.mule.runtime.api.metadata.resolving.InputTypeResolver;
import org.mule.runtime.api.metadata.resolving.OutputTypeResolver;

public class MyDataSenseResolver
        implements InputTypeResolver, OutputTypeResolver {

    @Override
    public String getResolverName() {
        return "MyDataSenseResolver";
    }
    
    @Override
    public String getCategoryName() {
        return "MyDataSenseResolver";
    }

    @Override
    public MetadataType getInputMetadata(MetadataContext metadataContext, String entityKeyId){
        return new BaseTypeBuilder(MetadataFormat.JAVA).numberType().build();
    }

    @Override
    public MetadataType getOutputType(MetadataContext metadataContext, String entityKeyId){
        return metadataContext.getTypeLoader().load(MyCustomObject.class);
    }
}

Once the above class has been defined, the operation can be annotated with the @TypeResolver(MyDataSenseResolver.class) annotation to reference the input DataSense, and with the @OutputResolver(output = MyDataSenseResolver.class) annotation to reference the output DataSense.

A more complex scenario where DataSense may be required is when a selector’s choice is dependent on another one — this is referred to as multi-level DataSense[7]. As an example, the image below depicts an operation with two selectors, where the Second Selector values are dependent on the chosen First Selector value.

In order to handle such cases, a Java class corresponding to a selector group must be created as follows:

public class SelectorGroup {
    @Parameter
    @Alias("FirstSelector")
    @DisplayName("First Selector")
    @MetadataKeyPart(order = 1)
    private String service;

    @Parameter
    @Alias("SecondSelector")
    @DisplayName("Second Selector")
    @MetadataKeyPart(order = 2)
    private String action;

    public SelectorGroup() {

    }

    public SelectorGroup(String service, String action) {
        this.service = service;
        this.action = action;
    }

    public String getService() {
        return service;
    }

    public String getAction() {
        return action;
    }

    public void setService(String service) {
        this.service = service;
    }

    public void setAction(String action) {
        this.action = action;
    }
}

The next step is to create a new Java class that implements the TypeKeysResolver interface and defines the selector keys/options. In this case, MetadataKeyBuilder is used to create a hierarchy of keys, where the parent keys are the first selector keys, while the child keys are the second selector keys (i.e. dependent on the parent key/first selector selection).

public class DataSenseResolver implements TypeKeysResolver {
            
    @Override
    public String getCategoryName() {
        return "DataSenseResolver";
    }

    @Override
    public Set getKeys(MetadataContext metadataContext) {
        Set keys = new HashSet<>();

        MetadataKeyBuilder key1 = MetadataKeyBuilder.newKey(ProcessKeys.KEY1.toString());
        key1.withChild(MetadataKeyBuilder.newKey(SubProcessAKeys.SUB_KEY_1A.toString()).build());
        key1.withChild(MetadataKeyBuilder.newKey(SubProcessAKeys.SUB_KEY_2A.toString()).build());
        key1.withChild(MetadataKeyBuilder.newKey(SubProcessAKeys.SUB_KEY_3A.toString()).build());
        keys.add(key1.build());

        MetadataKeyBuilder key2 = MetadataKeyBuilder.newKey(ProcessKeys.KEY2.toString());
        key2.withChild(MetadataKeyBuilder.newKey(SubProcessBKeys.SUB_KEY_1B.toString()).build());
        key2.withChild(MetadataKeyBuilder.newKey(SubProcessBKeys.SUB_KEY_2B.toString()).build());
        key2.withChild(MetadataKeyBuilder.newKey(SubProcessBKeys.SUB_KEY_3B.toString()).build());
        keys.add(key2.build());

        MetadataKeyBuilder key3 = MetadataKeyBuilder.newKey(ProcessKeys.KEY3.toString());
        key3.withChild(MetadataKeyBuilder.newKey(SubProcessCKeys.SUB_KEY_1C.toString()).build());
        key3.withChild(MetadataKeyBuilder.newKey(SubProcessCKeys.SUB_KEY_2C.toString()).build());
        key3.withChild(MetadataKeyBuilder.newKey(SubProcessCKeys.SUB_KEY_3C.toString()).build());
        keys.add(key3.build());

        return keys;
    }
}

Finally, the following must be added (as a parameter) to the respective operation:

@ParameterGroup(name = "Operation Name") 
@MetadataKeyId(DataSenseResolver.class) SelectorGroup selector
public enum CustomConnectorErrorType implements ErrorTypeDefinition {
    
    CUSTOM_ERROR_1, CUSTOM_ERROR_2, CUSTOM_ERROR_3;

    private ErrorTypeDefinition> parent;

    CustomConnectorErrorType() {
    }

    CustomConnectorErrorType(ErrorTypeDefinition> parent) {
        this.parent = parent;
    }

    @Override
    public Optional>> getParent() {
        return Optional.ofNullable(parent);
    }
}

Subsequently, each operation must define which of the errors defined above it may throw using the @Throws(CustomErrorProvider.class) annotation.

This annotation references a class that implements ErrorTypeProvider and specifies the respective error types. (Note that each error provider may be used by multiple operations that throw the same set of errors.)

public class CustomErrorProvider implements ErrorTypeProvider {
    @Override
    public Set getErrorTypes() {
        HashSet errors = new HashSet<>();
        errors.add(CustomConnectorErrorType.CUSTOM_ERROR_1);
        errors.add(CustomConnectorErrorType.CUSTOM_ERROR_2);
        return errors;
    }
}
throw new ModuleException(CustomConnectorErrorType.CUSTOM_ERROR_1, new MyCustomException("..."));

Loading

Subscribe To Our Newsletter

Subscribe To Our Newsletter

Join our mailing list to receive the latest news and updates from our team.

You have Successfully Subscribed!