ODF 2 integration with Java Native Worker
This article delves into the technical specifics of migrating a Digital Worker (DW) project built with the ODF 2 framework to the solution offered by the Java Native Worker (JNW).
note
This article primarily focuses on the essentials for migrating an existing project based on the ODF 2 framework. However, when initiating a fresh implementation, our suggestion is to employ the JNW exclusively, without involving the ODF 2 framework.
To begin with, you can utilize the JNW archetype to create a new project.
Upgrade ODF 2 framework
To use the Java Native Worker and run Bot Tasks implemented using the ODF 2 framework, migrate your Digital Worker project to the ODF 2 v10.2.8 or later. See the migration guide for detailed instructions.
You should be aware of some alterations related to the JNW.
Web-Harvest integration
Starting from version 10.2.8, ODF 2 introduces a new Maven module called odf2-webharvest-integration. This module conveniently packages all code, service implementations, and dependencies crucial for the Web-Harvest runtime.
However, with the transition towards the JNW approach, there's no need to employ the odf2-webharvest-integration module. In fact, it's imperative that you avoid using it, especially since Web-Harvest-based services, such as SecretsVaultService, won't function within the JNW runtime.
Instead of adding the odf2-webharvest-integration module, you should adjust your BCB code to ensure it can compile without relying on Web-Harvest dependencies. This means the deprecation of any direct Web-Harvest components. The ODF 2 framework provides comprehensive abstractions for services, such as Secret Vault, Billing, S3, and others.
The next step is to stop using ControlTowerServicesModule. This Feather module now comes directly from odf2-webharvest-integration and is specifically tailored for services within the Web-Harvest runtime. It's crucial to confirm that none of your Bot Tasks depend on it. For instance:
// before
@Requires({CustomModule.class, DataModule.class, ControlTowerServicesModule.class})
@BotTask
public class MyBotTask implements AdHocTask {}
// after
@Requires({CustomModule.class, DataModule.class})
@BotTask
public class MyBotTask implements AdHocTask {}
Billing service
Starting from v10.2.8, the ODF 2 framework brings in an additional level of abstraction for managing the billing information service. If you are currently utilizing BillingInfoService obtained from the Web-Harvest context, transition to the com.workfusion.odf2.service.billing.BillingService abstraction within the ODF 2 framework:
@BotTask
public class BillingBotTask implements AdHocTask {
private final BillingService billingService;
@Inject
public BillingBotTask(BillingService billingService) {
this.billingService = billingService;
}
@Override
public TaskRunnerOutput run(TaskInput taskInput) {
final String jsonData = taskInput.getRequiredVariable("json_billing_data");
final boolean billingResult = billingService.sendInfo(jsonData);
return taskInput.asResult().withColumn("billing_result", String.valueOf(billingResult));
}
}
Java Native Worker module
In the majority of cases, a Digital Worker project takes the form of a standard Maven multi-module project with the following layout:
- dw-project-bcb: Bot Config Bundle (BCB) containing all production Java classes and unit tests.
- dw-project-package: Asset Bundle containing Business Processes and database migrations.
- dw-project-test: integration test module.
- pom.xml: root configuration of the Maven project.
For incorporating the code relevant to the Java Native Worker, it is recommended to establish a distinct Maven sub-module within your DW project. It's crucial to position the JNW module between the BCB and the package modules. This arrangement is necessary because the JNW module will rely on the BCB module and will subsequently be integrated into the package module.
For the majority of DW projects, the project's structure appears as follows:
- dw-project-bcb
- dw-project-njw: new JNW sub-module
- dw-project-package
- dw-project-test
pom.xml
To achieve this, take the following steps:
- Generate a project using the JNW archetype, selecting the most recent JNW version available. You can do this in any non-project location of your choice. It's worth noting that the JNW archetype creates a fresh single-module Maven project. While the project is not an exact match for our needs, it provides a correct JNW project structure.
- Utilize an IDE to prepare a new Maven sub-module within your existing DW project.
- Copy and paste all the code generated by the JNW archetype into the new sub-module.
- Adjust the sub-module's
pom.xmlfile to match the structure detailed in the provided listing:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<!-- Refers to the root pom.xml with 'com.workfusion.dw:my-dw-project:1.0.0' GAV coordinates -->
<parent>
<groupId>com.workfusion.dw</groupId>
<artifactId>my-dw-project</artifactId>
<version>1.0.0</version>
</parent>
<!-- JNW sub-module artifact ID -->
<artifactId>my-dw-project-njw</artifactId>
<!-- Prefix name for all bot configs created from this module -->
<name>My DW Project JNW</name>
<properties>
<!-- Use latest versions available -->
<wf.java-native-worker-parent-bom.version>1.0.1.14</wf.java-native-worker-parent-bom.version>
<wf.java-native-worker-base.version>1.0.1.15</wf.java-native-worker-base.version>
<wf.runtime-logger-configurer.version>1.0.136</wf.runtime-logger-configurer.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.workfusion.spa.java.native.worker</groupId>
<artifactId>java-native-worker-parent</artifactId>
<version>${wf.java-native-worker-parent-bom.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.workfusion.spa.java.native.worker</groupId>
<artifactId>worker-base</artifactId>
<version>${wf.java-native-worker-base.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- Refers to project's BCB module -->
<dependency>
<groupId>com.workfusion.dw</groupId>
<artifactId>my-dw-project-bcb</artifactId>
<version>${project.version}</version>
</dependency>
<!-- ODF2-JNW integration dependencies -->
<dependency>
<groupId>com.workfusion.odf2</groupId>
<artifactId>odf2-jnw-integration</artifactId>
</dependency>
<dependency>
<groupId>com.workfusion.odf2</groupId>
<artifactId>odf2-jnw-compiler</artifactId>
<scope>provided</scope>
</dependency>
<!-- JNW dependencies -->
<dependency>
<groupId>com.workfusion.spa.java.native.worker</groupId>
<artifactId>java-native-worker-core</artifactId>
<version>${wf.java-native-worker-base.version}</version>
</dependency>
<dependency>
<groupId>com.workfusion.util</groupId>
<artifactId>runtime-logger-configurer</artifactId>
<version>${wf.runtime-logger-configurer.version}</version>
</dependency>
<!-- JDBC driver for Data Store operations -->
<dependency>
<groupId>com.microsoft.sqlserver</groupId>
<artifactId>mssql-jdbc</artifactId>
<version>7.0.0.jre8</version>
</dependency>
<!-- Test dependencies -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>*</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.workfusion.odf2</groupId>
<artifactId>odf2-junit</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<!-- Use the same 'spring-boot' version as in 'java-native-worker-base' project -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.7.12</version>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
<!-- Settings required by the 'odf2-jnw-compiler' -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<compilerArgs>
<arg>-AgroupId=${project.groupId}</arg>
<arg>-AartifactId=${project.artifactId}</arg>
<arg>-Aversion=${project.version}</arg>
</compilerArgs>
</configuration>
</plugin>
</plugins>
</build>
</project>
- Execute a project build to make sure there are no issues. At this point, your DW project should be successfully built, along with the integrated JNW module.
Task Processors
In the JNW environment, the equivalent of an ODF 2 Bot Task is referred to as a Task Processor.
For each Bot Task you intend to employ within the JNW runtime, it's essential to have a corresponding JNW Task Processor.
The odf2-jnw-integration module furnishes all requisite dependencies, configurations, and tools to facilitate a seamless transition to JNW Task Processors.
Let's consider a scenario of migrating a simple Bot Task as illustrated below:
@BotTask
public class MyBotTask implements AdHocTask {
@Override
public TaskRunnerOutput run(TaskInput taskInput) {
return taskInput.asResult();
}
}
To create a Task Processor for the MyBotTask class, follow the steps below:
- Create a new Java class within the JNW sub-module that implements the
ITaskProcessorinterface. - Apply the
@TaskProcessorannotation to the newly created class. - Inject the
OdfJnwIntegrationbean (already available within the Spring context) through the class constructor. - Invoke the
executeOdfTaskmethod and supply theMyBotTaskclass along with theTaskInputDataobject.
Here's a sample implementation:
@TaskProcessor(id = "my-bot-task")
public class MyBotTaskProcessor implements ITaskProcessor {
private final OdfJnwIntegration odfJnwIntegration;
public MyBotTaskProcessor(OdfJnwIntegration odfJnwIntegration) {
this.odfJnwIntegration = odfJnwIntegration;
}
@Override
public TaskOutputData process(TaskInputData inputData) {
return odfJnwIntegration.executeOdfTask(MyBotTask.class, inputData);
}
}
By following the above steps, you can generate distinct Task Processors for each Bot Task you intend to incorporate within your Business Process.
note
If a Bot Task is annotated with @SendToExternalConnector, add the @SendToExternalConnector annotation to the respective Task Processor, too.
XML bot configurations
Each Task Processor requires an XML bot configuration, typically in the following form:
<?xml version="1.0" encoding="UTF-8"?>
<config type="java">
<worker>${GAV}</worker>
<processor>${processorId}</processor>
</config>
The ODF 2 framework is equipped with the odf2-jnw-compiler module that automatically generates XML bot configurations for all classes annotated with @TaskProcessor during the compilation phase of your project.
You can find these generated XML files within the target/classes/configs/main directory of your JNW sub-module.
By default, the XML bot configuration file takes its name from the class name (for example, for MyBotTaskProcessor.class, the name will be my-bot-task-processor.xml).
To specify a customized name for a generated XML file, use the @FileName annotation as demonstrated below:
@FileName("my-custom-name.xml")
@TaskProcessor(id = "my-bot-task")
public class MyBotTaskProcessor implements ITaskProcessor {}
Testing Task Processors
As JNW fundamentally operates as a Spring Boot application, you can take advantage of the utilities, such as spring-boot-test and other test libraries. The JNW archetype offers an initial configuration for testing Task Processors.
Below is a practical test example for the MyTaskProcessor Task Processor. In this scenario, the test runs using a fresh Transaction object and verifies the successful status by the end.
class MyTaskProcessorTest extends BaseProcessorTest {
@Autowired TaskProcessingRouter router;
@Autowired OrmSupport ormSupport;
@BeforeEach
void setUp() {
ormSupport.createTables(Transaction.class, CustomEntity.class);
}
@AfterEach
void tearDown() {
ormSupport.dropAllCreatedTables();
}
@Test
@DisplayName("should run task processor")
void shouldRunTaskProcessor() {
// given
Transaction transaction = ormSupport.getTransactionRepository().startNewTransaction("NEW");
TaskInput<TaskInputWithContext> taskInput = new TaskDataPreparation(MyTaskProcessor.class)
.withTransaction(transaction)
.withInputData("key", "value")
.build();
// when
TaskResult<TaskOutputData> result = router.execute(taskInput);
// then
TaskResultAssert.assertThat(result).isSuccessful();
}
}
The BaseProcessorTest class is provided by the JNW archetype, and usually it looks as follows:
@ExtendWith(SpringExtension.class)
@SpringBootTest(classes = IntegrationTestConfiguration.class, webEnvironment = SpringBootTest.WebEnvironment.NONE)
public class BaseProcessorTest {}
As you can see, the BaseProcessorTest follows the conventional Spring Boot testing approach.
Additionally, the IntegrationTestConfiguration class, supplied by the JNW archetype, is responsible for configuring the Spring context for testing purposes.
In our case, a minor adjustment was made to the configuration to incorporate the OrmSupport object within the Spring context.
@ComponentScan(value = {
"com.workfusion.mydwproject",
"com.workfusion.odf2.jnw",
"com.workfusion.spa.core",
"com.workfusion.spa.jnative.worker",
"com.workfusion.task.execution.configuration"
})
public class IntegrationTestConfiguration {
@Bean
public OrmSupport ormSupport(DataSource dataSource) throws SQLException {
return new Odf2OrmSupport(dataSource, new GlobalSettings());
}
@Bean
public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer(ApplicationContext context) {
MutablePropertySources propertySources = ((ConfigurableEnvironment) context.getEnvironment()).getPropertySources();
PropertySourcesPlaceholderConfigurer configurer = new PropertySourcesPlaceholderConfigurer();
configurer.setPropertySources(propertySources);
return configurer;
}
}
BCB changes
If your BCB is configured to function with the PF4J classloading isolation, you can eliminate the corresponding configurations. This is because, inherently, JNW doesn't require any form of classloading isolation. Although it is not an absolute requirement, taking this step reduces the build time and results in a smaller size of the final artifact. The size reduction can be quite noticeable depending on the dependencies your BCB relies on.
To get rid of PF4J-related configurations, locate the declaration for the maven-assembly-plugin within the BCB's pom.xml file and completely remove it, along with the src/main/resources/assembly.xml file.
Typically, the section you need to delete appears as follows:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<descriptors>
<descriptor>src/main/resources/assembly.xml</descriptor>
</descriptors>
<archiverConfig>
<compress>false</compress>
</archiverConfig>
<archive>
<index>true</index>
<manifest>
<addClasspath>true</addClasspath>
<classpathPrefix>lib/</classpathPrefix>
</manifest>
<manifestEntries>
<Plugin-Id>${project.groupId}:${project.artifactId}:${project.version}</Plugin-Id>
<Plugin-Cache-Supported>true</Plugin-Cache-Supported>
</manifestEntries>
</archive>
<appendAssemblyId>false</appendAssemblyId>
</configuration>
<executions>
<execution>
<id>create-archive</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
Package structure changes
The outcome of the JNW sub-module is a Spring Boot application–a single JAR file that incorporates all necessary dependencies, including the project's BCB.
As a result, there's no longer a need to package the BCB itself within a DW bundle (dw-project-package module). Instead, a JNW JAR file must be packaged.
Below, you'll find an example of a package.xml file that packages JNW's JAR file into the artifactory-dependency/workers folder:
<?xml version="1.0" encoding="UTF-8"?>
<assembly xmlns="http://maven.apache.org/ASSEMBLY/2.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/ASSEMBLY/2.0.0 http://maven.apache.org/xsd/assembly-2.0.0.xsd">
<id>assembly-id</id>
<includeBaseDirectory>false</includeBaseDirectory>
<formats>
<format>zip</format>
</formats>
<fileSets>
<fileSet>
<directory>src/main/resources</directory>
<outputDirectory/>
</fileSet>
</fileSets>
<dependencySets>
<dependencySet>
<outputDirectory>artifactory-dependency/workers/com/workfusion/dw/my-dw-project-njw/${project.version}</outputDirectory>
<includes>
<include>com.workfusion.dw:my-dw-project-njw:jar:${project.version}</include>
</includes>
<useTransitiveDependencies>false</useTransitiveDependencies>
<useStrictFiltering>true</useStrictFiltering>
<useProjectArtifact>false</useProjectArtifact>
</dependencySet>
</dependencySets>
</assembly>
The package.xml needs to be configured within the pom.xml file of a DW's package module as follows:
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<appendAssemblyId>false</appendAssemblyId>
<descriptors>
<descriptor>assembly/package.xml</descriptor>
</descriptors>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
Business Process changes
The final step involves transitioning Bot Tasks to Task Processors within your Business Process. Here's how you can do it:
- Import the migrated DW project to a Control Tower instance.
- Open the Business Process you intend to convert.
- Modify each bot configuration in the Business Process, replacing the ODF 2 Bot Task with the corresponding JNW Task Processor.
- Save and export the updated Business Process.
- Store the exported Business Process within the DW’s package module.
note
Once a project is migrated to JNW, it's no longer possible to continue using ODF 2 Bot Tasks in their original form.
Even if you attempt to integrate ODF 2 Bot Tasks into a Business Process, it's unlikely to function properly due to the alterations made during the JNW migration process.