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
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:
- Right-click on the project that has to be converted
- Click on "Configure"
- 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']
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:
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.
To define a field/variable with its correct typing:
String fieldName = "value"
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.
Related articles
2 Comments
Jacques Le Roux
Thanks Dennis for the tip about Eclipse!
Dennis Balkir
No problem! Somebody told me that and it makes working with Groovy much easier, so I thought sharing would be the best idea