Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.

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

Info
titleWhy 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

Jira
serverASF JIRA
serverId5aa69414-a9e9-3523-82ec-879b028fb15b
keyOFBIZ-9350

Content

Groovy DSL

Services

Getting started

Checking Fields

Setting Fields

Starting Services

Preparing Service Results

Database Communication

Permissions

Timestamp And System Time

Logging

General

 

 



Anchor
groovyDsl
groovyDsl
Groovy DSL (dynamic scripting library)

Section


Column
width20%

How to get Groovy support in your IDE


Column
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.

 





Section


Column
width20%

Known Fields


Column

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. 




Section


Column
width20%

Known Methods


Column

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.



 
Section


Anchor
services
services
Services

 From MiniLang to Groovy

To see additional examples and finished conversions, which may help with occurring questions, click: 

Jira
serverASF JIRA
serverId5aa69414-a9e9-3523-82ec-879b028fb15b
keyOFBIZ-9350

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.

Code Block
languagegroovy
 return success()

Anchor
gettingStarted
gettingStarted
Getting started

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

The get converted to Groovy like the following:

Code Block
<!-- 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:

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

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

    Code Block
    def fieldName = "value"



Anchor
checkingFields
checkingFields
Checking Fields

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


// checks if fieldName is existent and/or empty
if (!fieldName) {}


Code Block
<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)) {}


Code Block
<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
}


Code Block
<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
}


Code Block
<if-instance-of field="parameters.categories" class="java.util.List"></if-instance-of>


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

Anchor
settingFields
settingFields
Setting Fields

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


// if fieldName is not initialized
String fieldName = "value" 
// if fieldName is initialized
fieldName = "value" 


Code Block
<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


Code Block
<set field="thisFieldName" value="${groovy: []}" type="List"/>


// this is easier in Groovy
List thisFieldName = []


Code Block
<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"/>


String failMessage = UtilProperties.getMessage("CommonUiLabels", "CommonGenericPermissionError", parameters.locale)
// in Groovy there can is a difference for the second case
parameters.rateCurrencyUomId = UtilProperties.getPropertyValue('general.properties', 'currency.uom.id.default')


Code Block
<clear-field field="product.primaryProductCategoryId"/>


product.primaryProductCategoryId = null

Anchor
startingServices
startingServices
Starting Services

Code Block
<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]


Code Block
<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

Anchor
preparingServiceResults
preparingServiceResults
Preparing Service Results

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


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

Anchor
databaseCommunication
databaseCommunication
Database Communication

Code Block
<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)


Code Block
<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()


Code Block
<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()


Code Block
<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()


Code Block
<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()


Code Block
<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
])


Code Block
<set field="statusValidChange.prop" value="value"/>


statusValidChange.prop = "value" 


Code Block
<create-value value-field="newEntity"/>


newEntity.create()


Code Block
<store-value value-field="newEntity"/>
<store-list list="listToStore"/>


newEntity.store()
delegator.storeAll(listToStore)


Code Block
<clone-value value-field="productCategoryMember" new-value-field="newProductCategoryMember"/>


def newProductCategoryMember = productCategoryMember.clone()


Code Block
<remove-value value-field="lookedUpValue"/>


lookedUpValue.remove()


Code Block
<sequenced-id sequence-name="ProductCategory" field="newEntity.productCategoryId"/>


newEntity.productCategoryId = delegator.getNextSeqId("ProductCategory")


Code Block
<check-id field="newEntity.productCategoryId"/>


UtilValidate.checkValidDatabaseId(newEntity.productCategoryId)


Code Block
<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

Anchor
permissions
permissions
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!

Code Block
<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))
}


Code Block
<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)

Anchor
timestampAndSystemTime
timestampAndSystemTime
Timestamp And System Time

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

Code Block
<now-timestamp field="nowTimestamp"/>


Timestamp nowTimestamp = UtilDateTime.nowTimestamp()


Code Block
<now-date-to-env field="nowDate"/>


Timestamp nowDate = UtilDateTime.nowTimestamp()


Code Block
<!-- this method also has the parameter "type", which is set to 'java.sql.timestamp' as default -->
<now field="fooNow"/>


Timestamp fooNow = UtilDateTime.nowTimestamp()


Code Block
<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
}

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

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


logVerbose("Permission check failed, user does not have the correct permission.")


Code Block
<log level="info" message="Applying feature [${productFeatureId}] of type [${productFeatureTypeId}] to product [${productId}]"/>


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

Anchor
general
general
General

Code Block
<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
}


Code Block
<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
}


Code Block
<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
}


Code Block
<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
}


Code Block
<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()


Code Block
<order-map-list list="productsList">
    <order-by field-name="sequenceNum"/>
</order-map-list>


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

 

 



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

Code Block
languagegroovy
@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:

Code Block
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.

 

 



Content by Label
showLabelsfalse
max5
spacesOFBIZ
showSpacefalse
sortmodified
reversetrue
typepage
cqllabel in ("minilang","converison","ofbiztutorial","tutorial","mini-lang","convert","language","mini","groovy","ofbiz","dsl") and type = "page" and space = "OFBIZ"
labelsminilang mini language mini-lang groovy dsl convert converison tutorial ofbiz ofbiztutorial

 
Page properties
hiddentrue


Related issues