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

This is a small guide for everybody involved in converting the Mini Language into Groovy.

Why is this important?

This tutorial is directly linked to the efforts of converting all scripts in Mini Language to newer Groovy Scripts.
All of this is done, because Groovy is much more readable and easier to review, more up to date and many other reasons, which can be found here: https://markmail.org/message/n2ybsyet5r4xcqsz

To contribute, or just be up to date with the current process, you can look at the existing JIRA issue OFBIZ-9350 - Getting issue details... STATUS

Content

Groovy DSL

Services

Getting started

Checking Fields

Setting Fields

Starting Services

Preparing Service Results

Database Communication

Permissions

Timestamp And System Time

Logging

General



Groovy DSL (dynamic scripting library)

How to get Groovy support in your IDE

The following paragraph is for Eclipse users.

It is possible to get Groovy support in Eclipse by converting the loaded project to a Groovy Project. The project itself will work as before.

To do this just follow these few steps:

  1. Right-click on the project that has to be converted
  2. Click on "Configure"
  3. Click on "Convert to Groovy Project"

Eclipse will automatically load the file OfbizDslDescriptorForEclipse.dsld , in which the known fields and methods used in Groovy Scripts are defined.



Known Fields

property name: 'parameters', type : 'java.util.Map'

These are the parameters given to the Groovy Script, when it is called as a service. It is equivalent to Map<String, Object> context in the Java-Service-Definition.

property name: 'context', type: 'java.util.Map'

More parameters, which are, for example, given through a screen or another Groovy Script. This is important when the script is called through an action segment of a screen.

property name: 'delegator', type: 'org.apache.ofbiz.entity.Delegator'

Normal instance of the Delegator, which is used for special database access.

property name: 'dispatcher', type: 'org.apache.ofbiz.service.LocalDispatcher'

Normal instance of the LocalDispatcher, which is used to call services and other service-like operations.

property name: 'security', type: 'org.apache.ofbiz.security.Security'

Normal instance of the Security-Interface with which permission checks are done.


Known Methods

method name: 'runService', type: 'java.util.Map', params: [serviceName: 'String', inputMap: 'java.util.Map']

Helping method to call services instead of dispatcher.runSync(serviceName, inputMap). Also possible: run service: serviceName, with: inputMap

method name: 'makeValue', type: 'java.util.Map', params: [entityName: 'String']

Helping method to make a GenericValue instead of delegator.makeValue(entityName). Creates an empty GenericValue of the specific entity.

method name: 'findOne', type: 'java.util.Map', params: [entityName: 'String', inputMap: 'java.util.Map']

Helping method to find one GenericValue in the database. Used instead of delegator.findOne(entityName, inputMap)

method name: 'findList', type: 'java.util.List', params: [entityName: 'String', inputMap: 'java.util.Map']

Helping method to find many GenericValue in the database. Used instead of delegator.findList(entityName, inputMap, null, null, null, false)

method name: 'select', type: 'org.apache.ofbiz.entity.util.EntityQuery', params: [entity: 'java.util.Set']

Helping method used instead of EntityQuery.use(delegator).select(...)

method name: 'select', type: 'org.apache.ofbiz.entity.util.EntityQuery', params: [entity: 'String...']

As above.

method name: 'from', type: 'org.apache.ofbiz.entity.util.EntityQuery', params: [entity: 'java.lang.Object']

Helping method used instead of EntityQuery.use(delegator).from(...)

method name: 'success', type: 'def', params: [message: 'String']

Helping method used instead of ServiceUtil.returnSuccess(message)

method name: 'failure', type: 'java.util.Map', params: [message: 'String']

Helping method used instead of ServiceUtil.returnFailure(message)

method name: 'error', type: 'def', params: [message: 'String']

Helping method used instead of ServiceUtil.returnError(message)

method name: 'logInfo', type: 'void', params: [message: 'String']

Helping method used instead of Debug.logInfo(message, fileName)

method name: 'logWarning', type: 'void', params: [message: 'String']

Helping method used instead of Debug.logWarning(message, fileName)

method name: 'logError', type: 'void', params: [message: 'String']

Helping method used instead of Debug.logError(message, fileName)

method name: 'logVerbose', type: 'void', params: [message: 'String']

Helping method used instead of Debug.logVerbose(message, fileName)


The actual definition of the methods can be found in /framework/service/src/main/java/org/apache/ofbiz/service/engine/GroovyBaseScript.groovy,
the variables dctx, dispatcher and delegator are set in the file GroovyEngine.java which can be found in the same location.



Services

 From MiniLang to Groovy

To see additional examples and finished conversions, which may help with occurring questions, click:  OFBIZ-9350 - Getting issue details... STATUS
There is a chance that a similar case has already been converted.


IMPORTANT: When a simple-method ends, it will automatically at least return a success-map.
All the Groovy Services have to return success at least, too.

 return success()

Getting started

MiniLang files consist of services, which, in most cases, implement services.

The get converted to Groovy like the following:

<!-- This is MiniLang -->
<simple-method method-name="createProductCategory" short-description="Create an ProductCategory">
   <!-- Code -->
</simple-method>


// This is the converted Groovy equivalent
/**
 * Create an ProductCategory
 */
def createProductCategory() {
    // Code
}

It will be useful for future developers, and everybody who has to check something in the code, to put at least the short-description as the new Groovydoc. This will hopefully more or less explain, what the method should or shouldn't do.
If the short-description isn't helpful enough, feel free  complete it.

The structure of if and else in MiniLang is a little different than the one from Groovy or Java and can be a bit confusing when first seen, so here is an example:

<if-empty field="parameters.productCategoryId">
    <sequenced-id sequence-name="ProductCategory" field="newEntity.productCategoryId"/>
<else>
    <set field="newEntity.productCategoryId" from-field="parameters.productCategoryId"/>
    <check-id field="newEntity.productCategoryId"/>
    <check-errors/>
</else>
</if-empty>

Notice, that the else always starts before the if-tag is closed, but sometimes isn't indented as one would expect it.
When navigating through bigger if-phrases, the navigation itself will be much easier through just clicking in the opening or closing if-tag; Eclipse will automatically mark the matching opening or closing if-tag for you.


There are two possibilities to initialize a field/variable in Groovy.

  1. To define a field/variable with its correct typing:

    String fieldName = "value"
  2. To just "define" a field/variable. The IDE you are working with may not recognize the typing, but OFBiz can work with it:

    def fieldName = "value"

Checking Fields

<if-empty field="fieldName"></if-empty>


// checks if fieldName is existent and/or empty
if (!fieldName) {}
<if-empty field="fieldName.property"></if-empty>


// fieldName has to be existent, property doesn't need to
// if known, that property does exist, the ? can be left out
if (!fieldName?.property) {}
// CAUTION: every query like this in Groovy evaluates to a Boolean type
// everything that is empty or false will turn into false:
// null, [], [:], "", false -> false

// if you want to check if the field really is empty
if (UtilValidate.isEmpty(fieldName)) {}
<if>
    <condition>
        <or>
            <if-empty field="field1"/>
            <if-empty field="field2"/>
        </or>
    </condition>
    <then>
        <!-- code in if -->
    </then>
    <else>
        <!-- code in else -->
    </else>
</if>


if (!field1 || !field2) {
    // code in if
} else {
    // code in else
}
<if-compare-field field="product.primaryProductCategoryId" to-field="parameters.productCategoryId" operator="equals">
    <!-- code -->
</if-compare-field>


// this will even work, if product is not existent or null
if (UtilValidate.areEqual(product?.primaryProductCategoryId, parameters.productCategoryId)) {
    // code
}
<if-instance-of field="parameters.categories" class="java.util.List"></if-instance-of>


if (parameters.categories instanceof java.util.List) {}

Setting Fields

<set field="fieldName" value="value"/>


// if fieldName is not initialized
String fieldName = "value" 
// if fieldName is initialized
fieldName = "value" 
<set field="otherFieldName.property" value="value"/>
<set field="otherFieldName.otherProperty" value="true" type="Boolean"/>
<set field="otherFieldName.otherProperty" from-field="parameters.property/>


// if otherFieldName is not yet initialized, you have to do it first
// MiniLang does that automatically
Map otherFieldName = [:] // empty Map
// now put the values in
otherFieldName = [
    property: "value",
    otherProperty: true
]
// or the less efficient way
otherFieldName.property = "value"
otherFieldName.otherProperty = true

// it is possible to put different values in later:
otherFieldName.property = parameters.property
<set field="thisFieldName" value="${groovy: []}" type="List"/>


// this is easier in Groovy
List thisFieldName = []
<property-to-field resource="CommonUiLabels" property="CommonGenericPermissionError" field="failMessage"/>
<!-- there are different cases of this, which are not distinguished in MiniLang -->
<property-to-field resource="general.properties" property="currency.uom.id.default" field="parameters.rateCurrencyUomId"/>

// The property-to-field minilang method treated retrieved property as input to FlexibleStringExpander#expandString and passed it the 
// current environment map. This behaviour can be replicated by passing the variables Map to UtilProperties#getMessage.
String failMessage = UtilProperties.getMessage("CommonUiLabels", "CommonGenericPermissionError", binding.variables, parameters.locale)
// in Groovy there can is a difference for the second case
parameters.rateCurrencyUomId = UtilProperties.getPropertyValue('general.properties', 'currency.uom.id.default')
<clear-field field="product.primaryProductCategoryId"/>


product.primaryProductCategoryId = null

Starting Services

<set field="relatedCategoryContext.parentProductCategoryId"  from-field="defaultTopCategoryId"/>
<call-service service-name="getRelatedCategories" in-map-name="relatedCategoryContext">
    <result-to-field result-name="categories" field="resCategories"/>
</call-service>


def relatedCategoryContext = [parentProductCategoryId: defaultTopCategoryId]
def serviceResult = run service: "getRelatedCategoryies", with: relatedCategoryContext
def resCategories = serviceResult.categories
// if it is not too confusing to read you can leave out the extra variable
run service: "getRelatedCategoryies", with: [parentProductCategoryId: defaultTopCategoryId]
<set-service-fields service-name="productCategoryGenericPermission" map="parameters" to-map="productCategoryGenericPermissionMap"/>
<call-service service-name="productCategoryGenericPermission" in-map-name="productCategoryGenericPermissionMap">
    <results-to-map map-name="genericResult"/>
</call-service>

// instead of setting the service fields from parameters, it is possible to run the service with the parameters map
Map genericResult = run service: "productCategoryGenericPermission", with: parameters

Preparing Service Results

<field-to-result field="fieldBudgetId" result-name="budgetId"/>


// MiniLang knows this implizitly
def result = success()
result.budgetId = fieldBudgetId
return result

Database Communication

<make-value entity-name="FinAccountTrans" value-field="newEntity"/>
<set-nonpk-fields map="parameters" value-field="newEntity"/>
<set-pk-fields map="parameters" value-field="newEntity"/>


// this is the easy way
GenericValue newEntity = makeValue("FinAccountTrans", parameters)
// this is also possible
GenericValue newEntity = makeValue("FinAccountTrans")
newEntity.setPKFields(parameters)
newEntity.setNonPKFields(parameters)
<entity-and entity-name="BudgetStatus" list="budgetStatuses">
    <field-map field-name="budgetId" from-field="parameters.budgetId"/>
    <order-by field-name="-statusDate"/>
</entity-and>


// this can also be done in one line, but it can easily become unreadable
def budgetStatuses = from("BudgetStatus")
    .where("budgetId", paramters.budgetId)
    .orderBy("-statusDate")
    .queryList()
<entity-one entity-name="StatusValidChange" value-field="statusValidChange">
    <field-map field-name="statusId" from-field="budgetStatus.statusId"/>
    <field-map field-name="statusIdTo" from-field="parameters.statusId"/>
</entity-one>
<!-- entity-one can be called without child elements, too -->
<entity-one entity-name="Product" value-field="product" auto-field-map="true"/>


// MiniLang has false set for useCache as the default value
statusValidChange = findOne("StatusValidChange", [statusId: budgetStatus.statusId, statusIdTo: parameters.statusId], false)
// this is also possible
statusValidChange = from("StatusValidChange")
    .where("statusId", budgetStatus.statusId, "statusIdTo", parameters.statusId)
    .queryOne()
// if there are no child elements, this can be used
GenericValue product = from("Product").where(parameters).queryOne()
<find-by-primary-key entity-name="ProductCategoryMember" map="lookupPKMap" value-field="lookedUpValue"/>


GenericValue lookedUpValue = findOne("ProductCategoryMember", lookupPKMap, false)
// this is also possible
lookedUpValue = from("ProductCategoryRole")
    .where(lookupPKMap)
    .queryOne()
<entity-condition entity-name="ProductCategoryContentAndInfo" list="productCategoryContentAndInfoList" filter-by-date="true" use-cache="true">
    <condition-list combine="and">
        <condition-expr field-name="productCategoryId" from-field="productCategoryList.productCategoryId"/>
        <condition-expr field-name="prodCatContentTypeId" value="ALTERNATIVE_URL"/>
    </condition-list>
    <order-by field-name="-fromDate"/>
</entity-condition>
<!-- entity-condition can also be used with the "or" operator -->
<entity-condition entity-name="ProdCatalogCategory" list="prodCatalogCategoryList" filter-by-date="true">
    <condition-list combine="and">
        <condition-expr field-name="productCategoryId" from-field="parameters.productCategoryId"/>
        <condition-list combine="or">
            <condition-expr field-name="prodCatalogCategoryTypeId" value="PCCT_VIEW_ALLW"/>
            <condition-expr field-name="prodCatalogCategoryTypeId" value="PCCT_PURCH_ALLW"/>
        </condition-list>
    </condition-list>
</entity-condition>


// the Groovy methods use the "and" and "equals" operator as default values
List productCategoryContentAndInfoList = from("ProductCategoryContentAndInfo")
    .where("productCategoryId", productCategoryList.productCategoryId, "prodCatContentTypeId", "ALTERNATIVE_URL")
    .cache().orderBy("-fromDate")
    .filterByDate()
    .queryList()
// with the use of the "or" operator you have to build your condition like this
EntityCondition condition = EntityCondition.makeCondition([
    EntityCondition.makeCondition([
        EntityCondition.makeCondition("prodCatalogCategoryTypeId", "PCCT_VIEW_ALLW"),
        EntityCondition.makeCondition("prodCatalogCategoryTypeId", "PCCT_PURCH_ALLW")
    ], EntityOperator.OR),
    EntityCondition.makeCondition("productCategoryId", parameters.productCategoryId)
])
List prodCatalogCategoryList = from("ProdCatalogCategory").where(condition).filterByDate().queryList()
<make-value entity-name="FinAccountTrans" value-field="newEntity"/>
<set-nonpk-fields map="parameters" value-field="newEntity"/>
<!-- In this case multiple fields of the GenericValue are set --> 
<make-value entity-name="ProductCategoryRollup" value-field="newLimitRollup"/>
<set field="newLimitRollup.productCategoryId" from-field="newEntity.productCategoryId"/>
<set field="newLimitRollup.parentProductCategoryId" from-field="productCategoryRole.productCategoryId"/>
<set field="newLimitRollup.fromDate" from-field="nowTimestamp"/>


def newEntity = makeValue("FinAccountTrans", parameters)
// you can set multiple fields of a GenericValue like this
def newLimitRollup = makeValue("ProductCategoryRollup", [
    productCategoryId: newEntity.productCategoryId,
    parentProductCategoryId: productCategoryRole.productCategoryId,
    fromDate: nowTimestamp
])
<set field="statusValidChange.prop" value="value"/>


statusValidChange.prop = "value" 
<create-value value-field="newEntity"/>


newEntity.create()
<store-value value-field="newEntity"/>
<store-list list="listToStore"/>


newEntity.store()
delegator.storeAll(listToStore)
<clone-value value-field="productCategoryMember" new-value-field="newProductCategoryMember"/>


def newProductCategoryMember = productCategoryMember.clone()
<remove-value value-field="lookedUpValue"/>


lookedUpValue.remove()
<sequenced-id sequence-name="ProductCategory" field="newEntity.productCategoryId"/>


newEntity.productCategoryId = delegator.getNextSeqId("ProductCategory")
<check-id field="newEntity.productCategoryId"/>


UtilValidate.checkValidDatabaseId(newEntity.productCategoryId)
<make-next-seq-id value-field="newEntity" seq-field-name="linkSeqId"/>


delegator.setNextSubSeqId(newEntity, "linkSeqId", 5, 1)
// the numbers 5 and 1 are used in the Java implementation of the MiniLang method
// and can also be found as the default values in the MiniLang documentation

Permissions

CAUTION: To also check for admin-permissions, this method has to be used:

hasEntityPermission(permission, action, userLogin)

If the method is used with wildcards, it is important to not forget the underscore, which comes before the parameter action!

<check-permission permission="CATALOG" action="_CREATE">
    <alt-permission permission="CATALOG_ROLE" action="_CREATE"/>
    <fail-property resource="ProductUiLabels" property="ProductCatalogCreatePermissionError"/>
</check-permission>
<check-errors/>


if (!(security.hasEntityPermission("CATALOG", "_CREATE", parameters.userLogin)
    || security.hasEntityPermission("CATALOG_ROLE", "_CREATE", parameters.userLogin))) {
    return error(UtilProperties.getMessage("ProductUiLabels", "ProductCatalogCreatePermissionError", parameters.locale))
}
<set field="hasCreatePermission" value="false" type="Boolean"/>
<if-has-permission permission="${primaryPermission}" action="${mainAction}">
    <set field="hasCreatePermission" value="true" type="Boolean"/>
</if-has-permission>


// this will automatically be set to false if the user doesn't have the permission
def hasCreatePermission = security.hasEntityPermission(primaryPermission, "_${mainAction}", parameters.userLogin)

Timestamp And System Time

The first two simple-method are deprecated; the third method should have been used instead.

<now-timestamp field="nowTimestamp"/>


Timestamp nowTimestamp = UtilDateTime.nowTimestamp()
<now-date-to-env field="nowDate"/>


Timestamp nowDate = UtilDateTime.nowTimestamp()
<!-- this method also has the parameter "type", which is set to 'java.sql.timestamp' as default -->
<now field="fooNow"/>


Timestamp fooNow = UtilDateTime.nowTimestamp()
<if-compare-field field="productCategoryMember.thruDate" to-field="expireTimestamp" operator="less" type="Timestamp">
    <!-- code -->
</if-compare-field>


Timestamp thruDate = productCategoryMember.thruDate
if (thruDate && thruDate.before(expireTimestamp)) {
    // code
}

Logging

Since all of the log methods are know to the Groovy Language, it is possible to just nearly use them as they are in MiniLang.
For further explanation, here are some examples:

<log level="verbose" message="Permission check failed, user does not have permission"/>


logVerbose("Permission check failed, user does not have the correct permission.")
<log level="info" message="Applying feature [${productFeatureId}] of type [${productFeatureTypeId}] to product [${productId}]"/>


logInfo("Applying feature [${productFeatureId}] of type [${productFeatureTypeId}] to product [${productId}]")

General

<call-simple-method method-name="checkCategoryRelatedPermission"/>
<check-errors/>


// simple-methods inside of classes, as long as they are not services, will be called like normal methods
Map res = checkCategoryRelatedPermission("updateProductCategory", "UPDATE", null, null)
if (!ServiceUtil.isSuccess(res)) {
    return res
}
<iterate list="subCategories" entry="subCategory">
    <!-- code -->
</iterate>


for (def subCategory : subCategories) {
    // code
}
// this is also possible (CAUTION: Eclipse sometimes doesn't know, that it already knows methods inside of closures)
subCategories.each { subCategory ->
    // code
}
<iterate-map map="parameters.productFeatureIdByType" key="productFeatureTypeId" value="productFeatureId">
    <!-- in here something should happen with value and key -->
</iterate-map>

// Map.Entry<String, String> should be changed to desired Object Type
for (Map.Entry<String, String> entry : parameters.productFeatureIdByType.entrySet()) {
    def productFeatureTypeId = entry.getKey()
    def productFeatureId = entry.getValue()
    // in here something should happen with value and key
}
<if>
    <condition>
        <not>
            <or>
                <if-has-permission permission="CATALOG" action="_${checkAction}"/>
                <and>
                    <if-has-permission permission="CATALOG_ROLE" action="_${checkAction}"/>
                    <not><if-empty field="roleCategories"/></not>
                </and>
            </or>
        </not>
    </condition>
    <then>
        <!-- code -->
    </then>
</if>


if (!security.hasEntityPermission("CATALOG", "_${checkAction}", parameters.userLogin)
    && !(security.hasEntityPermission("CATALOG_ROLE", "_${checkAction}", parameters.userLogin)
    && roleCategories)) {
    // code
}
<set field="validDate" from-field="parameters.validDate"/>
<if-not-empty field="validDate">
    <filter-list-by-date list="productCategoryMembers" valid-date="validDate"/>
</if-not-empty>


def query = from("ProductCategoryMember").where("productCategoryId", parameters.productCategoryId)
if (parameters.validDate) {
    query.filterByDate()
}
List productCategoryMembers = query.queryList()
<order-map-list list="productsList">
    <order-by field-name="sequenceNum"/>
</order-map-list>


productsList = EntityUtil.orderBy(productsList, ["sequenceNum"])



Where to find MiniLang implementation

If you find yourself in a position, where you don't know how to convert a certain tag from MiniLang to Groovy, you can always check the Java implementation of the MiniLang method.
All of the methods have an existing Java implementation and you can find all of them in this folder: /ofbiz/trunk/framework/minilang/src/main/java/org/apache/ofbiz/minilang/method

The interesting part of this implementation is the method exec(), which actually runs the MiniLang tag.

The tag <remove-by-and> for example is realized using this part of code here:

@Override

public boolean exec(MethodContext methodContext) throws MiniLangException {
    @Deprecated
    String entityName = entityNameFse.expandString(methodContext.getEnvMap());
    if (entityName.isEmpty()) {
        throw new MiniLangRuntimeException("Entity name not found.", this);
    }
    try {
        Delegator delegator = getDelegator(methodContext);
        delegator.removeByAnd(entityName, mapFma.get(methodContext.getEnvMap()));
    } catch (GenericEntityException e) {
        String errMsg = "Exception thrown while removing entities: " + e.getMessage();
        Debug.logWarning(e, errMsg, module);
        simpleMethod.addErrorMessage(methodContext, errMsg);
        return false;
    }
    return true;
}

In this you can find one important part of code, which is:

delegator.removeByAnd(entityName, mapFma.get(methodContext.getEnvMap()));

This tells you, that, if you're trying to convert the tag <remove-by-and>, you can use delegator.removeByAnd() in Groovy.





2 Comments

  1. Thanks Dennis for the tip about Eclipse!

    1. No problem! Somebody told me that and it makes working with Groovy much easier, so I thought sharing would be the best idea