Apache OFBiz Development : The Service Engine

August 26, 2009

Uncategorized

«»

Service Security and Access Control

Security-related programming in services is exactly like that in events.

In the class org.ofbiz.learning.learning.LearningServices, create a new static
method serviceWithAuth:


	public static Map serviceWithAuth(DispatchContext dctx, Map
	context){
		Security security = dctx.getSecurity();
		Map resultMap = null;
		if (context.get("userLogin") == null ||
			!security.hasPermission("LEARN_VIEW",
				(GenericValue)context.get("userLogin"))) {
			resultMap = ServiceUtil.returnError("You have no access
				here. You're not welcome!");
		}
		else {
			resultMap = ServiceUtil.returnSuccess("Welcome! You have
				access!");
		}
		return resultMap;
	}

Ensure that the correct imports have been added to the class:


	import java.util.Map;
	import org.ofbiz.entity.GenericValue;
	import org.ofbiz.security.Security;

In the file ${component:learning}\servicedef\services.xml, add a new
service definition:


	<service name="learningServiceWithAuth" engine="java"
			location="org.ofbiz.learning.learning.LearningServices"
			invoke="serviceWithAuth">
		<description>Service with some security-related codes</description>
	</service>

In the file ${webapp:learning}\WEB-INF\controller.xml, add a new request map:


	<request-map uri="TestServiceWithAuth">
		<security auth="true" https="true"/>
		<event type="service" invoke="learningServiceWithAuth"/>
		<response name="success" type="view" value="SimplestScreen"/>
		<response name="error" type="view" value="login"/>
	</request-map>

Rebuild and restart and then fire to webapp learning an http OFBiz request
TestServiceWithAuth, login with username allowed password ofbiz, and see the
welcome message displayed:

Logging in with username denied password zibfo will show an error message.
Thanks to the request-map’s response element named error having a value of
login, we are returned back to the login screen:

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, Mapcontext){
		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, Mapcontext){
		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.xmladd:


	<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:

Try entering MARS as the Planet Id.

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 framework\service\config\
serviceengine.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.

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:

Naming a Service and the Service Reference

Servic e 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 ref ers 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 a re 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 applications\order\servicedef\secas.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 global-rollback 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 a re 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 applications\product\entitydef 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 c an often catch people out! Since there is no apparent fl ow 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.

email

«»

Comments

comments