In rev. 1298461 and 1298908 I have committed some enhancements to the Groovy service engine and event handling to let the system inject a base class for all the Groovy scripts. The classname is configurable in the service engine.xml file: a first version of it with an initial but already usable implementation of a OFBiz DSL (Domain Specific Language) for Groovy services.
Main goal: make available to all Groovy services and events a 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 be implemented following a programming style very similar to the one provided by Minilang but leveraging all the features of a complete programming language.
Example
An example is worth of 1000 words: in the following two sections Minilang service and a Java event have been converted to Groovy
Services
This is the original Minilang service:
<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>
Here is the equivalent in Groovy:
<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 = findOne('InventoryItem', [inventoryItemId:parameters.inventoryItemId]) 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 = findList('ProductFacility', [productId:inventoryItem.productId, facilityId:inventoryItem.facilityId]) 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}.") }
Some highlights:
the 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:
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 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
Events
Here is the original event in Java:
public static String updateProductCategoryMember(HttpServletRequest request, HttpServletResponse response) { Delegator delegator = (Delegator) request.getAttribute("delegator"); String productId = request.getParameter("productId"); String productCategoryId = request.getParameter("productCategoryId"); String thruDate = request.getParameter("thruDate"); if ((thruDate == null) || (thruDate.trim().length() == 0)) { thruDate = UtilDateTime.nowTimestamp().toString(); } try { List<GenericValue> prodCatMembs = delegator.findByAnd("ProductCategoryMember", UtilMisc.toMap("productCategoryId", productCategoryId, "productId", productId)); prodCatMembs = EntityUtil.filterByDate(prodCatMembs); if (prodCatMembs.size() > 0) { // there is one to modify GenericValue prodCatMemb = prodCatMembs.get(0); prodCatMemb.setString("thruDate", thruDate); prodCatMemb.store(); } } catch (GenericEntityException e) { String errMsg = "Error adding to category: " + e.toString(); request.setAttribute("_ERROR_MESSAGE_", errMsg); return "error"; } return "success"; }
Here is the equivalent in Groovy:
def updateProductCategoryMember() { if (!parameters.thruDate) { parameters.thruDate = UtilDateTime.nowTimestamp() } try { productCategoryMember = EntityUtil.getFirst(EntityUtil.filterByDate(findList('ProductCategoryMember', [productCategoryId: parameters.productCategoryId, productId: parameters.productId]))) 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() }
Some highlights:
- 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
calling services:
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
GenericValue findOne(String entityName, Map inputMap)
List findList(String entityName, Map inputMap)
modifying data:
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):
logInfo(String message)
logWarning(String message)
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):
def success(String message)
Map failure(String message)
def error(String message)