Child pages
  • Groovy DSL for OFBiz business logic

Access to add and change pages is restricted. See: https://cwiki.apache.org/confluence/display/OFBIZ/Wiki+access

Skip to end of metadata
Go to start of metadata

Introduction

Recently a series of enhancements to the Groovy integration has been introduced to let the system inject a base class for all the Groovy scripts. The classname is configurable in the groovy.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) based on Groovy specialized in the implementation of business logic (services, events and data preparation scripts).

Main goal: make available to all Groovy services, 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 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.

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:

    <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 = select().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 = select().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}.")
}

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 = select().from('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()
}

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/ofbiz-framework/trunk/framework/service/src/main/java/org/apache/ofbiz/service/engine/GroovyBaseScript.groovy

  • calling services:

    Map runService(String serviceName, Map inputMap)
    Map run(Map args)

    The "run" method is implemented using named parameters and supports service calls with the following style:

    result = run service: 'createProduct', with: [productId: 'WG-1111']

    The DSL valid the map passed before run the service, so you can call directly like

    result = run service: 'createProduct', with: parameters
  • retrieving data

    EntityQuery select(...)
    EntityQuery from(...)

    The "select" and "from" methods can be used in the following ways:

    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:

    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)

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
  • No labels

1 Comment

  1. Thanks Jacopo, I confirm the autocompletion works in Eclipse (Kepler here) after adding  OfbizDslDescriptorForEclipse.dsld to the user folder (here C:\Users\Jacques\.groovy\greclipse\global_dsld_support\dsld in Windows 7) (smile)