Module 2 - Creating extensions in Java

Before you begin this module, you should complete Module 1 - Building the application. In this module, you will add a Java-based service to the work-order-lib library to compute the cost of a work order.

You will learn to access these features using REST clients, or by adding integrated actions to processes.

Writing a custom Java service

In this module we will write some Java code that directly supports the application logic for our Work Order library. For this tutorial, the example is that we want to have a custom calculation of the cost of a Work Order, taking into account the total hours worked and the hard-coded hourly rate (we will make this configurable later). We will make it available as a custom Service Task in the Process Designer. This is representative of much more complex kinds of logic that is better suited to a language like Java.

We will also take this opportunity to demonstrate how to bring in a dependency on third-party Java code and package it within our project.

Along the way we will see how to debug this code in Eclipse, as an example of using standard tools with your Java code.

Also, we will expose this logic by using annotations that turn it into a custom RESTful API, showing an example of how your application can be integrated with external systems in a standard way.

Preparing the project in Eclipse

Get the Sample Code

Download and and unzip the Innovation Suite Development Tutorial Sample Code archive (get the latest version). It should be provided alongside this document. It is important to understand that these files are additive to certain projects that you have already created in the tutorial. They do not replace the projects you have already generated; rather, they provide additional files that need to be merged into the project structure. They do not modify settings files such as pom.xml that are specific to you; the only assumption is that you have not started coding Java for this project. In a few cases, sample files do overwrite the originals and the copy operation will ask you to confirm this.

First, copy over the additional source code files from the sample code into your project. In this Lesson we are adding Java code to the work-order-lib project.

  1. Select the work-order-lib folder from the sample code and initiate a COPY operation (for example, on Windows, use Control-C).

    images/docs/download/attachments/669196710/image2016-11-22_12_54_31.png

  2. Select the project root, such as \projects. PASTE the work-order-lib folder from sample source into this

    images/docs/download/attachments/669196710/snip.PNG

  3. This will trigger a merge. In Windows, the Explorer will ask if you want to merge the files and apply "Copy and Replace" to all the files (because of the few cases where a file is updated and not just added).

images/docs/download/attachments/669196710/image2016-11-18_14_54_11.png

images/docs/download/attachments/669196710/Capture.PNG

Import the Maven Project into Eclipse

In Eclipse, use the "Import…" and "Existing Maven Projects" option to import the following POM file.

projects/work-order-lib/bundle/pom.xml

Note: if you had already imported the Maven project before copying over the sample code (see above), then you will have to refresh the navigator with F5 in Eclipse to see the code.

Introducing a Work Order service class

If you have followed the previous step to set up Eclipse, you probably also copied in the additional files to implement the Work Order Service. This Lesson does not ask you to code it yourself line-by-line; rather, it assumes you have added in the custom Java code and takes you on a tour of it so you will understand what it is doing.The purpose of the Work Order Service, like all implementations of the Service interface, is to provide a context for Java code that can easily be re-used by other services, and by Innovation Suite rules and processes directly.Using Eclipse to browser through the code, let's begin by looking just at the WorkOrderService class itself in the com.example.service Java package.

The WorkOrderService.java class has several interesting methods to note:

  1. It implements the Innovation Suite Service interface.

    import com.bmc.arsys.rx.services.common.Service;
    
    public class WorkOrderService implements Service {
    
    . . .
    
  2. The code contains constants that refer to the design objects created in Innovation Studio. If these do not agree with the definitions, then the code will obviously not run properly.

    private static final String WORK_ORDER_RECORD_DEFINITION = "com.example.work-order-lib:Work Order";
    private static final Integer WORK_ORDER_STATUS_FIELD_ID = 7;
    private static final String WORK_ORDER_STATUS_CANCELLED = "50";
    private static final String WORK_TASK_WORKED_HOURS_FIELD_ID = "536882001";
    private static final Integer WORK_TASK_STATUS_FIELD_ID = 7;
    private static final String WORK_TASK_STATUS_REJECTED = "3";
    private static final String WORK_TASK_ID_FIELD_ID = "379";
    private static final String WORK_TASK_RECORD_DEFINITION = "com.example.work-order-lib:Work Task";
    private static String WORK_ORDER_TASK_ASSOCIATION_DEFINITION = "com.example.work-order-lib:Associated Work Tasks";
    
  3. The getCost() and cancelWorkOrder() methods are marked as Service Tasks, using the @Action annotation. This means they will appear in the palette of Process Designer and Rule Designer and can be directly used within the business logic of any application that depends on the Work Order library. Note the other annotations that tell Innovation Studio how to map parameters into the method.

    /*
    * This Service Task returns the cost of a work order.
    * @param workOrderId
    * @return cost
    */
    @Action
    public float getCost(
     @ActionParameter(name = "workOrderId") @NotBlank @NotNull String workOrderId) {
    
     . . .
    
  4. The Record Service is used to manipulate records. It is obtained using the ServiceLocator.

    RecordService recordService = ServiceLocator.getRecordService();

  5. It uses the common logger to write statements into arextension.log.

    ServiceLocator.getLogger().info("Computed cost for work order " + workOrderId + " as total hours (" + totalTaskHours + ") + rate (" + rate + ") = " + cost);

  6. The Association Service is used to find all the associated Work Task records in the private method getTaskAssociations(). It uses the common DataPageQuery implementation exposed by that service, called getAssociationInstancesByIdDataPage(). This utility method is shared by both @Action methods.

    private List<?> getTaskAssociations(String workOrderId) {
       // Retrieve the tasks associated to this work order.
       AssociationService associationService = ServiceLocator.getAssociationService();
    
       Map<String, QueryPredicate> queryPredicatesByName = new HashMap<String, QueryPredicate>();
     queryPredicatesByName.put(ASSOCIATION_DEFINITION_QUERY_PARAMETER_NAME,
         new QueryPredicate(ASSOCIATION_DEFINITION_QUERY_PARAMETER_NAME, WORK_ORDER_TASK_ASSOCIATION_DEFINITION));
      queryPredicatesByName.put(ASSOCIATION_NODE_TO_QUERY_PARAMETER_NAME,
            new QueryPredicate(ASSOCIATION_NODE_TO_QUERY_PARAMETER_NAME, NODE_B_NAME));
        queryPredicatesByName.put(ASSOCIATION_RECORD_INSTANCE_ID_PARAMETER_NAME,
           new QueryPredicate(ASSOCIATION_RECORD_INSTANCE_ID_PARAMETER_NAME, workOrderId));        
    
      DataPageQueryParameters params = new DataPageQueryParameters(50, 0, null, null, queryPredicatesByName); 
       DataPage result = associationService.getAssociationInstancesByIdDataPage(params);   
       return result.getData();
    }
    
  7. There is a call to get the hourly rate. Until this is configured in a later Lesson, this will always return the default value of "10.00".

  8. Note that in order for the@Action methods to be discovered by Innovation Studio, the service class must be registered with the bundle. You will see that this has been done in the bundle class (MyLibrary.java), found in the com.example.package Java package, register() callback method, using the registerService() method.

    Hint: use Control-Shift-O in Eclipse to fix the import statements for you.

    registerService(new WorkOrderService());

  9. Assuming that you have either imported the sample code, or written some Java classes yourselves, this needs to be built and deployed to Innovation Suite. After saving all files, be sure to use BOTH the -Pexport and -Pdeploy maven profiles to preserve your definitions. Otherwise, your definitions will be replaced by whatever happens to be in the DEF file!

    projects\work-order-lib> mvn clean install -Pexport -Pdeploy

  10. In Innovation Studio

    1. The Process Designer will show the new Service Tasks in its palette. Note that the "camel-case" names of the methods are transformed into mixed case and space-separated words.
    2. A nice way to test the Java code is to introduce getCost Service Task to the process: configure it to take the workOrderId as input.

      images/docs/download/attachments/669196709/image2016-11-18_17_23_3.png

    3. It's output can be used to enhance the notification information in the next step.

images/docs/download/attachments/669196709/image2016-11-18_17_24_53.png

The next time you test the work order process, it should correctly add up the hours of each task, and multiply the default 10.00 rate. If the task hours add up to 15, you should see this notification:

images/docs/download/attachments/669196709/image2016-11-18_17_55_5.png

Using the Admin Settings service

In the last lesson, we noted that there is a static helper class WorkOrderSettings but that it's getHourlyRate() method always returns 10.00. This is because we have not yet created the setting on which it relies, which requires the use of Innovation Studio. Let's do that now.

  1. In Innovation Studio, navigate to the Work Order library and the the Configurations tab.

  2. Click New->Shared Settings

  3. Under General properties, specify the following:

    1. Component Name: WorkOrders
    2. Show In: Both
    3. Component Label: Work Orders
    4. Turn on "Available in Navigation Sidebar"
    5. First menu: "Work Order Settings". You can leave "Second Menu" blank.

      images/docs/download/attachments/668422821/worddavb7323d13e4d00b47ee2f11820e76477c.png

  4. Over on the left hand side of the screen above the Grid, click New Field->Text, and configure the field properties:

    1. Field Name: HourlyRate
    2. Field Label: Hourly Rate

      images/docs/download/attachments/668422821/worddav42f6463c1d967b26966718453288b7a8.png

  5. Click Save.

Testing Custom Settings in Innovation Studio

We have defined the setting for Work Order library called HourlyRate. Now let's set a value for this setting for testing purposes.

  1. On the tab navigation bar of Innovation Studio, click on the Administration tab.

  2. You should now see "Work Order Settings" appearing in the navigator on the left (if not, you may need to refresh your browser).

  3. Navigate to the Work Orders item and set the hourly rate to some number.

    images/docs/download/attachments/668422821/worddav2ae969adde674478346df091778c80eb.png

    Take another look at the sample code that you have already deployed, specifically the WorkOrderSettings class. You can see from the implementation that it uses the AdminSettingsService to look up our setting, within our component, within the bundle scope of com.example.work-order-lib.

    package com.example.service;
    import java.util.Map;
    import com.bmc.arsys.rx.application.common.ServiceLocator;
    import com.bmc.arsys.rx.services.admin.domain.AdminHeader;
    import com.bmc.arsys.rx.services.admin.domain.AdminSettingData;
    import com.bmc.arsys.rx.services.admin.domain.AdminSettingDataContainer;
    /**
     * This is a utility class that works with the Settings Service to retrieve configuration information 
     * within the Work Order library.
     * @author dsulcer
     *
     */
    public class WorkOrderSettings {
    
        /**
         * The assumption is that the WorkOrders component and the HourlyRate setting exist in the context o
         * the work order library bundle.
         */
        private final static String SETTINGS_WORKORDERS_COMPONENT_WORKORDERS = "WorkOrders";
        private final static String SETTINGS_WORKORDERS_SETTING_HOURLY_RATE = "HourlyRate";
        private final static String WORK_ORDER_LIB_BUNDLE_SCOPE = "com.example.work-order-lib";
        /**
        * Look up the configured hourly rate, which should be a setting definition created.
        */
        public static float getHourlyRate() {    
            float rate = 0;    
            String rateString = getValue(SETTINGS_WORKORDERS_SETTING_HOURLY_RATE);
            try {
                rate = new Float(rateString);
            }
            catch (Exception e) {
                ServiceLocator.getLogger().error("could not parse config setting for rate of " + SETTINGS_WORKORDERS_COMPONENT_WORKORDERS + "/" + SETTINGS_WORKORDERS_SETTING_HOURLY_RATE + " : " + rateString + "'");
            }
            return rate;
        }
        /**
         * Utility method to get some setting for the WorkOrders setting component.
         * @param setting
         * @return
         */
        private static String getValue(String setting) {    
    
            AdminHeader adminHeader = new AdminHeader();
            adminHeader.setBundleScope(WORK_ORDER_LIB_BUNDLE_SCOPE);
            adminHeader.setGlobalSpace(false);
            AdminSettingDataContainer dataHolder = null;
            try {
                 dataHolder = ServiceLocator.getAdminSettingsService().
                    getAdminSettingData(SETTINGS_WORKORDERS_COMPONENT_WORKORDERS, adminHeader);
            }
            catch (Exception e) {
                ServiceLocator.getLogger().error("Could not access " + SETTINGS_WORKORDERS_COMPONENT_WORKORDERS + " component");
            }
            Map<String, AdminSettingData> dataMap = null;
            try {
                dataMap = dataHolder.getAdminSettingDataMap();
            }
            catch (Exception e) {}
            if (dataMap == null) {
                ServiceLocator.getLogger().error("no dataMap found for " + SETTINGS_WORKORDERS_COMPONENT_WORKORDERS + " component");    
                return null;
            }
    
            AdminSettingData data = null;
            try {
                data = dataMap.get(setting);
            }
            catch (Exception e) {}
            if (data == null) {
                ServiceLocator.getLogger().error("could not get data for '" + SETTINGS_WORKORDERS_COMPONENT_WORKORDERS + "/" + setting + "'");
                return null;
            }
            return data.getSettingValue();
        }
    }
    

    If all of this is working perfectly so far, congratulations! Otherwise, you may need to debug the code, the process, or both. The next Lesson describes how to do that from the Java code perspective.

Setting up a debug configuration in Eclipse

  1. Make sure a server port for debugging is enabled

  2. Make sure the project is loaded into Eclipse as described above.

  3. Right-click on the project node, and select Debug As > Debug Configurations

  4. Create a new configuration for Remote Java Application. Set the port number (by default it is 12444).

  5. Click "Debug" to connect

    images/docs/download/attachments/668422821/worddavc1bed078abec735aecf07726f64f6926.png

    images/docs/download/attachments/668422821/worddav4baf4c2f790610b3140874fd6562f40b.png

    images/docs/download/attachments/668422821/worddav79eafa194cf497d7077778d588693333.png

    NOTE: if you cannot connect, make sure the port is enabled in arserver.config on the Innovation Suite instance.

Summary

We have successfully introduced custom Java code into our project. It obtains service handles and calls other Java code, including framework services like RecordService, and uses the @Action annotation to easily integrate Service Tasks into Process and Rule definitions.

In the next module we will add more Java code, in order to create new REST endpoints that can be used by custom clients.

Creating REST endpoints

As you probably know, REST (Representational State Transfer) is a type of web service that defines resources and the HTTP operations that operate on them, such as GET, PUT, POST, and DELETE. Innovation Studio, and the applications created by Innovation Studio out of the box use the built-in REST-based application services, such as Login, Record, and Process to do standard operations like working with records and views.

The Innovation Suite SDK supports creating additional REST endpoints for your own purposes - typically either integration into the system, or supporting custom view components that have special data needs. It also introduces two very useful standard resources that can be used when the standard "CRUD" (Create/Read/Update/Delete) operations do not fit the problem cleanly. These are known as DataPageQuery, and Command.

DataPageQuery is a standard resource, provided by the framework, that allows your custom Java classes to easily return a set of paginated data to a client, without having having to define special parameterization around a GET syntax. By "paginated", we mean that the syntax already supports the notion of a "page size" and "start index" for returning data in chunks to the client.

The Command resource provides a very simple way of providing a custom execute method, with parameters, like a SOAP call to do something in the system that is not well-described by "state transfer".

Finally, the framework supports creating a completely new REST-based resource in your own code, for which you can support any of or all of the standard HTTP operations: GET, PUT, POST, and DELETE.

To make this development easy, it's a good idea to use some tool that can send any kind of HTTP request to the server. A particularly good free tool for this is Postman (https://www.getpostman.com) which can be installed standalone and as a Chrome app. We will show this tool being used to demonstrate the REST APIs created in this lesson.

Creating a simple custom data page query

This topic is covered in the official Innovation Suite SDK documentation, found at Creating a custom Data Page Query.

Creating a work order data page query

We can use this same technique to massage or mingle data stored using the Record Service. This may not always be a good idea, because custom REST queries will not work with the out of the box UI Components, and could potentially be affected or even broken by customization of the underlying Record Definition. To avoid this, definition aspects that could break code (like certain customizations) can be disallowed by you as the developer.

In some cases it can be a very effective way of combining custom code with Record-based information.

Let's say that we want to return a list of work order information, and use the getCost() implementation we implemented in the previous lesson to add cost information to each one. In order to do this, we can implement a custom WorkOrderDataPageQuery. It's implementation does two things:

  1. Uses the built-in Record Service Data Page Query to retrieve work order records.

  2. For each one, call the Work Order Service to get cost information

That's exactly what the WorkOrderDataPageQuery implementation in the sample code does. Note that in the code, you have complete control over what parameters are used, how they are interpreted, whether filtering or sorting is done, how much information is returned, etc.

To test this, first make you have some valid work orders in the system, which have associated tasks with Hours Worked information. Now use Postman to send the query and check out the result. It should look something like this.

images/docs/download/attachments/668422834/worddav79b9596a983365c5c1e0d4319c1814c0.png

With result something like this.

{
  "totalSize": 2,
  "data": [
    {
      "workOrderId": "AGGAA5V0GFI2VAO4L3JGO3PQMXBZPA",
      "cost": 140
    },
    {
      "workOrderId": "AGGAA5V0GFI2VAO4U60VO3YT182VTB",
      "cost": 805
    }
  ]
}

Creating a custom command

Another very useful REST pattern, also very easy to use with the Innovation SDK, is a Command. A Command is a built-in resource, mapped to your custom class (much like DataPageQuery). It's purpose is simply to execute some code using parameters supplied with the PUT operation.

For Work Order Service, let's say there is a service method cancelWorkOrder().

/**
     * This action cancels an open work order.
     * @param workOrderId
     * @return cost
     */
    @Action
    public boolean cancelWorkOrder(
        @ActionParameter(name = "workOrderId") @NotBlank @NotNull String workOrderId) {

        ServiceLocator.getLogger().info("cancelWorkOrder called for  work order " + workOrderId);    
        RecordService recordService = ServiceLocator.getRecordService();
        RecordInstance workOrderRecordInstance = 
recordService.getRecordInstance(WORK_ORDER_RECORD_DEFINITION, workOrderId);
        if (workOrderRecordInstance == null) {
            ServiceLocator.getLogger().info("cancelWorkOrder work order " + workOrderId + " not found");    
            return false;
        }
        ServiceLocator.getLogger().info("cancelWorkOrder found work order " + workOrderId);    

        // Update associated tasks to mark them as Rejected.
        List<?> taskRecords = getAssociatedTaskRecords(workOrderRecordInstance, workOrderId);
        if (taskRecords != null) {
            for (Object taskRecord : taskRecords) {
                HashMap<String, Object> mappedRecord = (HashMap<String, Object>) taskRecord;
                String taskId = (String)mappedRecord.get(WORK_TASK_ID_FIELD_ID);
                ServiceLocator.getLogger().info("getting associated task " + taskId);    
                RecordInstance taskRecordInstance = 
recordService.getRecordInstance(WORK_TASK_RECORD_DEFINITION, taskId);
                if (taskRecordInstance == null) {
                    continue;
                }
                ServiceLocator.getLogger().info("updating associated task " + taskId 
+ " to Rejected");
                taskRecordInstance.setFieldValue(WORK_TASK_STATUS_FIELD_ID, 
WORK_TASK_STATUS_REJECTED);
                recordService.updateRecordInstance(taskRecordInstance);                    
            } 
        }

        // Update the work order itself to be Cancelled.
        ServiceLocator.getLogger().info("cancelWorkOrder updating status for " + workOrderId 
+ " to Cancelled");
        workOrderRecordInstance.setFieldValue(WORK_ORDER_STATUS_FIELD_ID, WORK_ORDER_STATUS_CANCELLED);
        recordService.updateRecordInstance(workOrderRecordInstance);
        ServiceLocator.getLogger().info("cancelWorkOrder updated status for " + workOrderId + " done.");            return true;
    }

We would like to provide direct access through REST to invoking this. The solution is to implement a custom Command class to wrap this method.

Please see the Innovation Suite SDK documentation at Creating a custom Command in Java for full information about this design pattern. The implementation for Work Order is included in the sample code for this tutorial.

Creating a custom REST API

We have exposed REST endpoints using pre-built mechanisms like DataPageQuery and Command. What is left is to create a completely custom resource with its own GET, PUT, POST, and DELETE operations as desired.

The sample code contains this already in the form of the WorkOrderCostResource class in the com.example.resource Java package, that contains the following.

  1. The class implements rx.service.common.RestfulResource

  2. It is annotated with @Path that describes the URL prefix

    @Path("example/workordercost")

  3. It contains a get() method annotated with HTTP verb @GET and authorization directive.

  4. It obtains a handle to the WorkOrderService using the ServiceLocator in a private method.

It has business logic to generate the cost as a CSV string. This method will later be re-implemented to use a 3rd party library to achieve this.

package com.example.resource;

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;

import com.bmc.arsys.rx.application.common.ServiceLocator;
import com.bmc.arsys.rx.services.bundle.BundleService;
import com.bmc.arsys.rx.services.common.RestfulResource;
import com.bmc.arsys.rx.services.common.annotation.AccessControlledMethod;
import com.bmc.arsys.rx.services.common.annotation.AccessControlledMethod.AuthorizationLevel;
import com.bmc.arsys.rx.services.common.annotation.RxDefinitionTransactional;
import com.example.service.WorkOrderService;

/**
 * The Class WorkOrderCost
 */
@Path("example/workordercost")
public class WorkOrderCostResource implements RestfulResource {

    private WorkOrderService workOrderService = null;

    /**
     * Gets the cost associated with a work order.
     *
     * @return the cost.
     */
    @GET
    @Path("/{workorderid}")
    @RxDefinitionTransactional(readOnly = true)
    @AccessControlledMethod(authorization = AuthorizationLevel.ValidUser)
    public String get(@PathParam("workorderid") String workOrderId) {

        // Calculate up the cost using the work order service.
        float cost = getWorkOrderService().getCost(workOrderId);

        // Format it
        String csv = WorkOrderCostFormatter.formatCostCSV(workOrderId, cost);

        return csv;
    }


    /**
     * This is the recommended technique for obtaining a handle to a deployed service using
     * OSGi.  In this case, it is a service deployed in this same package.  Do not keep global 
     * variables in code.
     * @return
     */
    private WorkOrderService getWorkOrderService() {
        if (workOrderService == null) {
            BundleService bundleService = ServiceLocator.getBundleService();
            workOrderService = (WorkOrderService) 
bundleService.getService("com.example.service.WorkOrderService");
          }
          return workOrderService;
        }

}

Note: the WorkOrderCostFormatter.java class, in the same package, contains a utility method such as this:

package com.example.resource;

public class WorkOrderCostFormatter {

    /** 
     * Basic version of formatting that does not use 3rd-party CSV library 
    */
    public static String formatCostCSV(String workOrderId, float cost) {

        StringBuilder response = new StringBuilder();   
        response.append("workorder, cost\n");
        response.append(workOrderId);
        response.append(",");
        response.append(cost);
        response.append("\n");
        return response.toString();

    }
}
  1. The class is registered in the bundle class's register() callback method, using the registerRestfulResource() method in MyLibrary.java. Hint: use Control-Shift-O in Eclipse to fix the import statements for you.

    registerRestfulResource(new WorkOrderCostResource());

  2. Test your service by putting a breakpoint in Eclipse and manually exercising the API from POSTMAN (or directly from your browser)

    1. Make sure the browser has an active, authenticated session: refresh / log in if needed to Innovation Studio
    2. From the same browser instance, invoke the REST resource from the URL. Note - be sure the session is still active.
    3. In order to test this, you will need to

      1. Select a work order that has a value populated in the Total Hours field
      2. Obtain the ID (not DisplayID) of that Work Order. The easiest way to do this is to copy and paste it from the Work Order Edit view.
    4. Invoke the URL, using an example Work Order ID - note that /api/BUNDLE-ID needs to be prefixed to the @Path statements in the Resource class.

images/docs/download/attachments/669196771/image2016-11-29_19_42_32.png

Summary

We have now covered all the most useful ways to extend an application and exposing this through REST.

  1. Register a custom DataPageQuery class (we created both a simple "hello world" implementation, as well as one that uses the Record Service itself to work with persisted data).

  2. Register a custom Command class.

  3. Register a completely custom REST endpoint.

Using third-party Java libraries

In this example, we bring in a library from Apache Commons to perform CSV parsing. We will use it in enhancing the HTTP WorkOrderCostResource coded in the earlier module, so that it formats CSV using 3rd party code. This is a trivial example of using third party libraries, and not a terribly practical one, but it does illustrate how to go about adding such dependencies to your project.

Introducing the library by adding a dependency

  1. Add the dependency in the work-order-lib\bundle\pom.xml:

    <dependencies>
    
            <!-- Add a custom dependency to bring in 3rd party code.  -->
            <dependency>
                <groupId>org.apache.commons</groupId>
                <artifactId>commons-csv</artifactId>
                <version>1.1</version>
                <!-- Use the default scope of “compile” since this code is not in the AR Server -->
                  <scope>compile</scope>
            </dependency>
    
  2. Build the code bundle. Be sure to close your zip tool before rebuilding.

    projects> cd work-order-lib\bundle
    bundle> mvn clean install
    
  3. Rebuild the code bundle.

    To check this, open the bundle found in work-order-lib\bundle\target\com.example.work-order-lib.1.0-SNAPSHOT.jar (for example, with 7zip) and check that the dependent library is now distributed at the root of the bundle

    images/docs/download/attachments/668422843/worddavdaa9c6268e2f6e60ac207b04607536af.png

  4. Reload the POM file into Eclipse

    Make sure the project for work-order-lib is updated to understand the new POM file settings. This will enable use of the CSV library.

    images/docs/download/attachments/668422843/worddavf463b038f8cd0f6af039f16275089da4.png

Using the third party library in code

  1. Modify the WorkOrderCostFormatter class to import Apache Commons CSV. You will need some other imports

    HINT: after adding the implementation, Eclipse will find the imports for you using Control-Shift-O.

    import java.io.StringWriter;
    import java.io.Writer;
    import org.apache.commons.csv.CSVFormat;
    import org.apache.commons.csv.CSVPrinter;
    
  2. Update the implementation of formatCostCSV to use the Apache commons CSV support. You can remove the commented out alternative method, and comment out the original one.

    The resulting method should look like this:

    private String formatCostCSV(String workOrderId, float cost) {
    
        Writer writer = new StringWriter();
        try {
            CSVPrinter csvPrinter = new CSVPrinter(writer, CSVFormat.EXCEL);
            csvPrinter.printRecord("workOrderId", "cost"); 
            csvPrinter.printRecord(workOrderId, cost); 
            csvPrinter.close();
        }
        catch (Exception e) {
            return "";
        }
        return writer.toString();
    }
    

    Note the references to the 3rd party library introduced in the POM file. If Eclipse has reloaded the maven settings, you should be able to use the full capabilities of Eclipse to autocomplete the methods, etc

  3. Build and deploy the new code.

    projects\work-order-lib> mvn clean install -Pexport -Pdeploy

  4. Test from a Browser or Postman. This result is the same as when using the code not using the CSV library.

Conclusion - Module 2

Upon completion of this module, you discovered how to extend a Java library to query a custom REST service that you created, as well as leverage third party services.