Apache OFBiz Service Engine: Part 2

Using Services, Entities, and Widgets to build custom ERP and CRM systems

Calling Services from Java Code

So far, we have explored services invoked as events from the controller (example <event type="service" invoke="learningFirstService"/>). We now look at calling services explicitly from code.

To invoke services from code, we use the dispatcher object, which is an object of type org.ofbiz.service.ServiceDispatcher. Since this is obtainable from the DispatchContext we can invoke services from other services.

To demonstrate this we are going to create one simple service that calls another.

In our services.xml file in ${component:learning}servicedef add two new service definitions:

<service name="learningCallingServiceOne" engine="java"
location="org.ofbiz.learning.learning.LearningServices" invoke="callingServiceOne">
<description>First Service Called From The Controller</description>
<attribute name="firstName" type="String" mode="IN" optional="false"/>
<attribute name="lastName" type="String" mode="IN" optional="false"/>
<attribute name="planetId" type="String" mode="IN" optional="false"/>
<attribute name="fullName" type="String" mode="OUT" optional="true"/>
</service>
<service name="learningCallingServiceTwo" engine="java"
location="org.ofbiz.learning.learning.LearningServices" invoke="callingServiceTwo">
<description>Second Service Called From Service One</description>
<attribute name="planetId" type="String" mode="IN" optional="false"/>
</service>

In this simple example it is going to be the job of learningCallingServiceOne to prepare the parameter map and pass in the planetId parameter to learningCallingServiceTwo. The second service will determine if the input is EARTH, and return an error if not.

In the class org.ofbiz.learning.learning.LearningEvents, add the static method that is invoked by learningCallingServiceOne:

public static Map callingServiceOne(DispatchContext dctx, Map context){

LocalDispatcher dispatcher = dctx.getDispatcher();
Map resultMap = null;
String firstName = (String)context.get("firstName");
String lastName = (String)context.get("lastName");
String planetId = (String)context.get("planetId");
GenericValue userLogin = (GenericValue)context.get("userLogin");
Locale locale = (Locale)context.get("locale");

Map serviceTwoCtx = UtilMisc.toMap("planetId", planetId, "userLogin", userLogin, "locale", locale);
try{
resultMap = dispatcher.runSync("learningCallingServiceTwo", serviceTwoCtx);
}catch(GenericServiceException e){
Debug.logError(e, module);
}
resultMap.put("fullName", firstName + " " + lastName);

return resultMap;
}

and also the method invoked by learningServiceTwo:

public static Map callingServiceTwo(DispatchContext dctx, Map context){
String planetId = (String)context.get("planetId");
Map resultMap = null;
if(planetId.equals("EARTH")){
resultMap = ServiceUtil.returnSuccess("This planet is Earth");
}else{
resultMap = ServiceUtil.returnError("This planet is NOT Earth");
}
return resultMap;
}

To LearningScreens.xml add:

<screen name="TestCallingServices">
<section>
<actions><set field="formTarget" value="TestCallingServices"/></actions>
<widgets>
<include-screen name="TestFirstService"/>
</widgets>
</section>
</screen>

Finally add the request-map to the controller.xml file:

<request-map uri="TestCallingServices">
<security auth="false" https="false"/>
<event type="service" invoke="learningCallingServiceOne"/>
<response name="success" type="view" value="TestCallingServices"/>
<response name="error" type="view" value="TestCallingServices"/>
</request-map>

and also the view-map:

<view-map name="TestCallingServices" type="screen" 
page="component://learning/widget/learning/LearningScreens.xml#TestCallingServices"/>

Stop, rebuild, and restart, then fire an OFBiz http request TestCallingServices to webapp learning. Do not be alarmed if straight away you see error messages informing us that the required parameters are missing. By sending this request we have effectively called our service with none of our compulsory parameters present.

Enter your name and in the Planet Id, enter EARTH. You should see:

Apache OFBiz Service Engine: Part 2

Try entering MARS as the Planet Id.

Apache OFBiz Service Engine: Part 2

Notice how in the Java code for the static method callingServiceOne the line

resultMap = dispatcher.runSync("learningCallingServiceTwo", serviceTwoCtx);

is wrapped in a try/catch block. Similar to how the methods on the GenericDelegator object that accessed the database threw a GenericEntityException, methods on our dispatcher object throw a GenericServiceException which must be handled.

There are three main ways of invoking a service:

  • runSync—which runs a service synchronously and returns the result as a map.
  • runSyncIgnore—which runs a service synchronously and ignores the result. Nothing is passed back.
  • runAsync—which runs a service asynchronously. Again, nothing is passed back.

The difference between synchronously and asynchronously run services is discussed in more detail in the section called Synchronous and Asynchronous Services.

Implementing Interfaces

Open up the services.xml file in ${component:learning}servicedef and take a look at the service definitions for both learningFirstService and learningCallingServiceOne.

Do you notice that the <attribute> elements (parameters) are the same? To cut down on the duplication of XML code, services with similar parameters can implement an interface.

As the first service element in this file enter the following:

<service name="learningInterface" engine="interface">
<description>Interface to describe base parameters for Learning Services</description>
<attribute name="firstName" type="String" mode="IN" optional="false"/>
<attribute name="lastName" type="String" mode="IN" optional="false" />
<attribute name="planetId" type="String" mode="IN" optional="false"/>
<attribute name="fullName" type="String" mode="OUT" optional="true"/>
</service>

Notice that the engine attribute is set to interface.

Replace all of the <attribute> elements in the learningFirstService and learningCallingServiceOne service definitions with:

<implements service="learningInterface"/>

So the service definition for learningServiceOne becomes:

<service name="learningCallingServiceOne" engine="java"
location="org.ofbiz.learning.learning.LearningServices" invoke="callingServiceOne">
<description>First Service Called From The Controller</description>
<implements service="learningInterface"/>
</service>

Restart OFBiz and then fire an OFBiz http request TestCallingServices to webapp learning. Nothing should have changed—the services should run exactly as before, however our code is now somewhat tidier.

Overriding Implemented Attributes

It may be the case that the interface specifies an attribute as optional="false", however, our service does not need this parameter. We can simply override the interface and add the <attribute> element with whatever settings we wish.

For example, if we wish to make the planetId optional in the above example, the <implements> element could remain, but a new <attribute> element would be added like this:

<service name="learningCallingServiceOne" engine="java"
location="org.ofbiz.learning.learning.LearningServices" invoke="callingServiceOne">
<description>First Service Called From The Controller</description>
<implements service="learningInterface"/>
<attribute name="planetId" type="String" mode="IN" optional="false"/>
</service>

Synchronous and Asynchronous Services

The service engine allows us to invoke services synchronously or asynchronously. A synchronous service will be invoked in the same thread, and the thread will "wait" for the invoked service to complete before continuing. The calling service can obtain information from the synchronously run service, meaning its OUT parameters are accessible.

Asynchronous services run in a separate thread and the current thread will continue without waiting. The invoked service will effectively start to run in parallel to the service or event from which it was called. The current thread can therefore gain no information from a service that is run asynchronously. An error that occurs in an asynchronous service will not cause a failure or error in the service or event from which it is called.

A good example of an asynchronously called service is the sendOrderConfirmation service that creates and sends an order confirmation email. Once a customer has placed an order, there is no need to wait while the mail service is called and the mail sent. The mail server may be down, or busy, which may result in an error that would otherwise stop our customer form placing the order. It is much more preferable to allow the customer to continue to the Order Confirmation page and have our business receive the valuable order. By calling this service asynchronously, there is no delay to the customer in the checkout process, and while we log and fix any errors with the mail server, we still take the order.

Behind the scenes, an asynchronous service is actually added to the Job Scheduler. It is the Job Scheduler's task to invoke services that are waiting in the queue.

Using the Job Scheduler

Asynchronous services are added to the Job Scheduler automatically. However, we can see which services are waiting to run and which have already been invoked through the Webtools console. We can even schedule services to run once only or recur as often as we like.

Open up the Webtools console at https://localhost:8443/webtools/control/main and take a look under the Service Engine Tools heading. Select Job List to view a full list of jobs. Jobs without a Start Date/Time have not started yet. Those with an End Date/Time have completed. The Run Time is the time they are scheduled to run. All of the outstanding jobs in this list were added to the JobSandbox Entity when the initial seed data load was performed, along with the RecurrenceRule (also an Entity) information specifying how often they should be run. They are all maintenance jobs that are performed "offline".

The Pool these jobs are run from by default is set to pool. In an architecture where there may be multiple OFBiz instances connecting to the same database, this can be important. One OFBiz instance can be dedicated to performing certain jobs, and even though job schedulers may be running on each instance, this setting can be changed so we know only one of our instances will run this job.

The Service Engine settings can be configured in frameworkserviceconfigserviceengine.xml. By changing both the send-to-pool attribute and the name attribute on the <run-from-pool element>, we can ensure that only jobs created on an OFBiz instance are run by this OFBiz instance.

Click on the Schedule Job button and in the Service field enter learningCallingServiceOne, leave the Pool as pool and enter today's date/time by selecting the calendar icon and clicking on today's date. We will need to add 5 minutes onto this once it appears in the box. In the below example the Date appeared as 2008-06-18 14:11:24.265. This job is only going to be scheduled to run once, although we could specify any recurrence information we wish.

Select Submit and notice that scheduler is already aware of the parameters that can (or must, in this case) be entered. This information has been taken from the service definition in our services.xml file.

Apache OFBiz Service Engine: Part 2

Press Submit to schedule the job and find the entry in the list. This list is ordered by Run Time so it may not be the first. Recurring maintenance jobs are imported in the seed data and are scheduled to run overnight. These will more than likely be above the job we have just scheduled since their run-time is further in the future. The entered parameters are converted to a map and then serialized to the database. They are then fed to the service at run time.

Quickly Running a Service

Using the Webtools console it is also possible to run a service synchronously. This is quicker than going through the scheduler should you need to test a service or debug through a service. Select the Run Service button from the menu and enter the same service name, submit then enter the same parameters again. This time the service is run straight away and the OUT parameters and messages are passed back to the screen:

Apache OFBiz Service Engine: Part 2

Naming a Service and the Service Reference

Service names must be unique throughout the entire application. Because we do not need to specify a location when we invoke a service, if service names were duplicated we can not guarantee that the service we want to invoke is the one that is actually invoked. OFBiz comes complete with a full service reference, which is in fact a dictionary of services that we can use to check if a service exists with the name we are about to choose, or even if there is a service already written that we are about to duplicate.

From https://localhost:8443/webtools/control/main select the Service Reference and select "l" for learning. Here we can see all of our learning services, what engine they use and what method they invoke. By selecting the service learningCallingServiceOne, we can obtain complete information about this service as was defined in the service definition file services.xml. It even includes information about the parameters that are passed in and out automatically.

Careful selection of intuitive service names and use of the description tags in the service definition files are good practice since this allows other developers to reuse services that already exists, rather than duplicate work unnecessarily.

Event Condition Actions (ECA)

ECA refers to the structure of rules of a process. The Event is the trigger or the reason why the rule is being invoked. The condition is a check to see if we should continue and invoke the action, and the action is the final resulting change or modification. A real life example of an ECA could be "If you are leaving the house, check to see if it is raining. If so, fetch an umbrella". In this case the event is "leaving the house". The condition is "if it is raining" and the action is "fetch an umbrella".

There are two types of ECA rules in OFBiz: Service Event Condition Actions (SECAs) and Entity Event Condition Actions (EECAs).

Service Event Condition Actions (SECAs)

For SECAs the trigger (Event) is a service being invoked. A condition could be if a parameter equalled something (conditions are optional), and the action is to invoke another service.

SECAs are defined in the same directory as service definitions (servicedef). Inside files named secas.xml

Take a look at the existing SECAs in applicationsorderservicedefsecas.xml and we can see a simple ECA:

<eca service="changeOrderStatus" event="commit" run-on-error="false">
<condition field-name="statusId" operator="equals" value="ORDER_CANCELLED"/>
<action service="releaseOrderPayments" mode="sync"/>
</eca>

When the changeOrderStatus transaction is just about to be committed, a lookup is performed by the framework to see if there are any ECAs for this event. If there are, and the parameter statusId is ORDER_CANCELLED then the releaseOrderPayments service is run synchronously.

Most commonly, SECAs are triggered on commit or return; however, it is possible for the event to be in any of the following stages in the service's lifecycle:

  • auth—Before Authentication
  • in-validate—Before IN parameter validation
  • out-validate—Before OUT parameter validation
  • invoke—Before service invocation
  • commit—Just before the transaction is committed
  • return—Before the service returns
  • global-commit
  • global-rollback

The variables global-commit and i are a little bit different. If the service is part of a transaction, they will only run after a rollback or between the two phases (JTA implementation) of a commit.

There are also two specific attributes whose values are false by default:

  • run-on-failure
  • run-on-error

You can set them to true if you want the SECA to run in spite of a failure or error. A failure is the same thing as an error, except it doesn't represent a case where a rollback is required.

It should be noted that parameters passed into the trigger service are available, if need be, to the action service. The trigger services OUT parameters are also available to the action service.

Before using SECAs in a component, the component must be informed of the location of the ECA service-resources:

<service-resource type="eca" loader="main" location="servicedef/secas.xml"/>

This line must be added under the existing <service-resource> elements in the component's ofbiz-component.xml file.

Entity Event Condition Actions (EECAs)

For EECAs, the event is an operation on an entity and the action is a service being invoked.

EECAs are defined in the same directory as entity definitions (entitydef): inside files named eecas.xml.

They are used when it may not necessarily be a service that has initiated an operation on the entity, or you may wish that no matter what service operates on this entity, a certain course of action to be taken.

Open the eecas.xml file in the applicationsproductentitydef directory and take a look at the first <eca> element:

<eca entity="Product" operation="create-store" event="return">
<condition field-name="autoCreateKeywords" operator="not-equals" value="N"/>
<action service="indexProductKeywords" mode="sync" value-attr="productInstance"/>
</eca>

This ECA ensures that once any creation or update operation on a Product record has been committed, so long as the autoCreateKeywords field of this record is not N, then the indexProductKeywords service will be automatically invoked synchronously.

The operation can be any of the following self-explanatory operations:

  • create
  • store
  • remove
  • find
  • create-store (create or store/update)
  • create-remove
  • store-remove
  • create-store-remove
  • any

The return event is by far the most commonly used event in an EECA. But there are also validate, run, cache-check, cache-put, and cache-clear events. There is also the run-on-error attribute.

Before using EECAs in a component, the component must be informed of the location of the eca entity-resource:

<entity-resource type="eca" loader="main" location="entitydef/eecas.xml"/>

must be added under the existing <entity-resource> elements in the component's ofbiz-component.xml file.

ECAs can often catch people out! Since there is no apparent flow from the trigger to the service in the code they can be difficult to debug. When debugging always keep an eye on the logs. When an ECA is triggered, an entry is placed into the logs to inform us of the trigger and the action.

Summary

This brings us to the end of our investigation into the OFBiz Service Engine. We have discovered how useful the Service Oriented Architecture in OFBiz can be and we have learnt how the use of some of the built in Service Engine tools, like the Service Reference, can help us when we are creating new services.

In this article we have looked at:

  • Calling services from code (using dispatcher).
  • IN/OUT parameter mismatch when calling services
  • Sending feedback; standard return codes success, error and fail.
  • Implementing Service Interfaces
  • Synchronous and asynchronous services
  • Using the Service Engine tools
  • ECAs: Event Condition Actions

Books to Consider

Apache OFBiz Development: The Beginner's Tutorial
$ 21.00
Quickstart Apache Axis2
$ 15.60
comments powered by Disqus
X

An Introduction to 3D Printing

Explore the future of manufacturing and design  - read our guide to 3d printing for free