Table of Contents |
---|
Introduction
In rev. 1298461 and 1298908 I have committed some Recently a series of enhancements to the Groovy service engine and event handling integration has been introduced to let the system inject a base class for all the Groovy scripts. The classname is configurable in the service enginegroovy.xml file: a first version of it with an initial but already usable implementation of a properties file (located in in the framework/base/conf/ folder): the base class currently available provides an implementation of an OFBiz DSL (Domain Specific Language) for Groovy servicesbased on Groovy specialized in the implementation of business logic (services, events and data preparation scripts).
Main goal: make available to all Groovy services and events a DSL , events, data preparation scripts and scriptles the same DSL, specific for OFBiz, and focused on simplifying common tasks like calling other services, performing entity operations, logging and returning messages, error handling. The end result is that Groovy servicesand events can all the business logic in OFBiz can now be implemented following a programming style very similar to the one provided by Minilang but leveraging all the features of a complete programming language.
Info |
---|
As mentioned above, the DSL for services and events is available out-of-the-box. In order to enable the DSL also for data preparation scripts you'll have to apply the patch that is attached to this page (groovy-dsl-for-data-prep.patch); this work is still experimental and will be committed to the trunk once we will be sure that it doesn't affect the performance of script execution. |
Example
In the following two sections we will compare examples of business logic implemented as a Minilang service and as a Java event to the equivalent version implemented with the Groovy DSL.
Services
This is the original Minilang service:
The complete support for the OFBiz DSL is available since r. 1636346, committed to trunk in early November 2014. |
Example
In the following two sections we will compare examples of business logic implemented as a Minilang service and as a Java event to the equivalent version implemented with the Groovy DSL.
Services
This is the original Minilang service:
Code Block |
---|
<service |
Code Block |
<service name="setLastInventoryCount" engine="simple"
location="component://product/script/org/ofbiz/product/inventory/InventoryServices.xml" invoke="setLastInventoryCount">
<description>Service which run as EECA (on InventoryItemDetail entity) and updates lastInventoryCount for products available in facility in ProductFacility entity</description>
<attribute name="inventoryItemId" mode="IN" type="String" optional="false"/>
</service>
...
<simple-method method-name="setLastInventoryCount" short-description="Service that updates stock availability of products">
<entity-one value-field="inventoryItem" entity-name="InventoryItem" auto-field-map="false">
<field-map field-name="inventoryItemId" from-field="parameters.inventoryItemId"/>
</entity-one>
<entity-and list="productFacilities" entity-name="ProductFacility">
<field-map field-name="productId" from-field="inventoryItem.productId" />
</entity-and>
<if-not-empty field="productFacilities">
<iterate list="productFacilities" entry="productFacility">
<set field="serviceInMap.productId" from-field="productFacility.productId"/>
<set field="serviceInMap.facilityId" from-field="productFacility.facilityId"/>
<call-service service-name="getInventoryAvailableByFacility" in-map-name="serviceInMap">
<result-to-field result-name="availableToPromiseTotal"/>
</call-service>
<clear-field field="serviceInMap"/>
<set field="productFacility.lastInventoryCount" from-field="availableToPromiseTotal"/>
<set-service-fields service-name="updateProductFacility" map="productFacility" to-map="serviceInMap"/>
<call-service service-name="updateProductFacility" in-map-name="serviceInMap"/>
<clear-field field="productFacility"/>
<clear-field field="serviceInMap"/>
</iterate>
</if-not-empty>
</simple-method>
|
...
Code Block |
---|
<service name="setLastInventoryCount" engine="groovy" location="component://product/script/org/ofbiz/product/inventory/InventoryServices.groovy" invoke="setLastInventoryCount"> <description>Service which run as EECA (on InventoryItemDetail entity) and updates lastInventoryCount for products available in facility in ProductFacility entity</description> <attribute name="inventoryItemId" mode="IN" type="String" optional="false"/> </service> ... def setLastInventoryCount() { inventoryItem = findOneselect().from('InventoryItem', ).where([inventoryItemId:parameters.inventoryItemId]).queryOne() if (!inventoryItem) { logWarning("The InventoryItem with inventoryItemId=${parameters.inventoryItemId} doesn't exist.") return failure("Inventory item with id ${parameters.inventoryItemId} was not found.") } List productFacilities = findListselect().from('ProductFacility', ).where([productId:inventoryItem.productId, facilityId:inventoryItem.facilityId]).queryList() productFacilities.each { countResult = runService('getInventoryAvailableByFacility', [productId:it.productId, facilityId: it.facilityId]) result = runService('updateProductFacility', [productId:it.productId, facilityId:it.facilityId, lastInventoryCount:countResult.availableToPromiseTotal]) } return success("Updated inventory count for product ${inventoryItem.productId}.") } |
...
the code block:
Code Block if (!inventoryItem) { logWarning("The InventoryItem with inventoryItemId=${parameters.inventoryItemId} doesn't exist.") return failure("Inventory item with id ${parameters.inventoryItemId} was not found.") }
is not really necessary (there is no equivalent in the Minilang version) but I have added it because it seems useful and also to show how you can log a warning message in the console and how you can prematurely return from a service (here we use a "failure" that is still a success, no rollback; but this nice feature is not used much in OFBiz)
- the code is really expressive and easy to read; no technical stuff that is not part of the business logic is needed (similar to Minilang); the code is also very concise (50% of the Minilang equivalent)
error handling: as in Minilang error handling related code is not necessary in the 90% of the cases; when a service call or an entity operation fail the service execution is stopped and the engine takes care of returning the "error" and rolling back the transactions; if you want to avoid this behavior for a special handling (equivalent of the Minilang's break-on-error="false" attribute) you can simply wrap the call in a try/catch block; for example:
Code Block try { result = runService('updateProduct', [productId: 'CodeThatDoesntExist']) } catch(Exception e) { return error('something wrong happened: ${e.getMessage()}') }
However in most of the cases you shouldn't worry about errors returned by services and entity operations because the framework will take care of returning the proper error map for you (as it happens in Minilang)
- dispatcher and delegator objects are available with all their rich api (just use them as they are already in the context) but not necessary for the most common cases (calling sync services, fetching and manipulating simple data etc...) because for them you can use the DSL language: all the calls like runService, findOne, findList, makeValue etc... fetch the dispatcher/delegator from the context behind the lines
- runService accepts and an input map and there is no need to add to it the userLogin object because the method will automatically fetch it from the context if not already in the map (same as in Minilang)
- the methods error(), failure(), success() (you can optionally pass a string to them for the message) all return a valid service output map; success/failure represent a "success" (no rollback) while error will cause a rollback; however in most of the cases you will not need to call "error" because if something goes wrong the framework will do it for you (similar to Minilang)
- with IDEs that support Groovy (I am using Idea) you will be able to debug groovy services like in Java; assisted completion features are also pretty good for Groovy
...
Code Block |
---|
def updateProductCategoryMember() { if (!parameters.thruDate) { parameters.thruDate = UtilDateTime.nowTimestamp() } try { productCategoryMember = EntityUtil.getFirstselect(EntityUtil).filterByDate(findListfrom('ProductCategoryMember', ).where([productCategoryId: parameters.productCategoryId, productId: parameters.productId]).filterByDate().queryFirst() if (productCategoryMember) { productCategoryMember.setString('thruDate', parameters.thruDate) productCategoryMember.store() } } catch(Exception e) { return error("The following error occurred setting thruDate on category ${parameters.productCategoryId} for product ${parameters.productId}: ${e.getMessage()}") } return success() } |
...
- the Groovy method that implements the event is identical to a Groovy service: all the event specific code is hidden by the usage of DSL
- now the success() method returns the "success" string as required by OFBiz events rather than the "success" map for services
- similarly the error() method returns the "error" string and adds the error message to the proper attribute of the request object (but this detail is hidden and the method looks exactly the same as in a service)
- you can now have a Groovy script file containing several methods, each method representing an event
- the try/catch block was not required because in case of error the "error" string would have been returned by the framework; however I have used it to return a custom error message (and show the usage of the "error" method)
Short reference of DSL operations
For now the DSL is intentionally simple because it is focused on most common tasks/behavior (for more complex and less common tasks you can use the dispatcher/delegator). This section provides a summary of the main methods; for a full reference please refer to the source file: http://svn.apache.org/repos/asf/ofbiz/trunk/framework/service/src/org/ofbiz/service/engine/GroovyBaseScript.groovy
- return a custom error message (and show the usage of the "error" method)
Short reference of DSL operations
For now the DSL is intentionally simple because it is focused on most common tasks/behavior (for more complex and less common tasks you can use the dispatcher/delegator). This section provides a summary of the main methods; for a full reference please refer to the source file: http://svn.apache.org/repos/asf/ofbiz/ofbiz-framework/trunk/framework/service/src/main/java/org/apache/ofbiz/service/engine/GroovyBaseScript.groovy
calling services:
Code Block Map runService(String serviceName, Map inputMap)
Code Block Map run(Map args)
Info The "run" method is implemented using named parameters and supports service calls with the following style:
Code Block result = run service: 'createProduct', with: [productId: 'WG-1111']
The DSL valid the map passed before run the service, so you can call directly like
Code Block result = run service: 'createProduct', with: parameters
retrieving data
Code Block EntityQuery select(...)
Code Block EntityQuery from(...)
Info The "select" and "from" methods can be used in the following ways:
Code Block record = select().from('Product').where(productId, 'WG-1111').queryOne() // selects all the columns of the record record = from('Product').where(productId, 'WG-1111').queryOne() // same as above record = from('Product').where(context).queryOne() // same as above if productId is present in context or present in context.parameters record = select('internalName').from('Product').where(productId, 'WG-1111').queryOne() // selects one column of the record
For more details refer to the Javadocs of EntityQuery.
modifying data:
Code Block GenericValue makeValue(String entityName)
and then call the methods on the GenericValue object (remove/store etc...)
logging (they all accept a GString i.e. $notation):
Code Block logInfo(String message)
Code Block logWarning(String message)
Code Block logError(String message)
returning from the service or event (the methods simply return a Map for services or a string for events but you still have to use the "return" keyword to return the map back; when used by events the error method adds also the error message, if specified, to the request object):
Code Block def success
calling services:
Code Block Map runService(String serviceName, Map inputMap)
retrieving data TODO: expand to support conditions; TODO: for more complex queries use the entity builder (work in progress); TODO: properly manage EntityListIterator for larger sets of data
Code Block GenericValue findOne(String entityName, Map inputMap)
Code Block List findList(String entityName, Map inputMap)
modifying data:
Code Block GenericValue makeValue(String entityName)
and then call the methods on the GenericValue object (remove/store etc...)
logging (they all accept a GString i.e. $notation):
Code Block logInfo(String message)
Code Block logWarningMap failure(String message)
Code Block logErrordef error(String message)
returning from the service or event (the methods simply return a Map for services or a string for events but you still have to use the "return" keyword to return the map back; when used by events the error method adds also the error message, if specified, to the request object):
Code Block def success(String message)
Code Block Map failure(String message)
def error(String message)Code Block
IDE Integration
DSL descriptor files for Eclipse and IntelliJ are included in OFBiz trunk since r1643183 in December 2014. For older versions, attached to this page you will find the DSL Descriptor files that, once added to the classpath (the framework/base/src/main/java/org/apache/ofbiz/base/ folder is probably the best place) of your OFBiz project, will add DSL support to all the Groovy scripts in OFBiz (autocompletion etc...):
- Eclipse: OfbizDslDescriptorForEclipse.dsld (seems to work in ~/.groovy/greclipse/global_dsld_support/dsld)
- IntelliJ: OfbizDslDescriptorForIntelliJ.gdsl