Groovy là ngôn ngữ lập trình chạy theo mô hình lập trình chức năng (functional programming) thực thi các câu lệnh trên JVM (Java Virtual Machine – máy ảo Java). Để tự động hóa các nghiệp vụ kinh doanh trong phần mềm ERP, như trong Apache Ofbiz, có rất nhiều đoạn chương trình viết bằng *.groovy
để thực hiện các nghiệp vụ một cách tự động theo nhiều bước. Cú pháp viết file build.gradle
là sử dụng cú pháp của Apache Groovy.
Môi trường phát triển
- IntelliJ IDEA 2020.2.3 (Ultimate Edition)
- Windows 10 x64 version 2004 – Enterprise edition
- Groovy 3.0.6
- JDK 1.8
Ví dụ: File ofbiz-frameworkapplicationsordergroovyScriptsallocationplanCreateAllocationPlan.groovy
(Apache Ofbiz version 17.12.04, bản quyền Apache Software Foundation)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 |
import org.apache.ofbiz.entity.condition.EntityOperator import org.apache.ofbiz.entity.condition.EntityCondition import org.apache.ofbiz.order.order.OrderReadHelper import org.apache.ofbiz.party.party.PartyHelper allocationPlanInfo = itemList = [] isPlanAlreadyExists = false productId = parameters.productId planName = parameters.planName if (productId) { orderedQuantityTotal = 0.0 orderedValueTotal = 0.0 reservedQuantityTotal = 0.0 ecl = EntityCondition.makeCondition([ EntityCondition.makeCondition("productId", EntityOperator.EQUALS, productId), EntityCondition.makeCondition("statusId", EntityOperator.IN, ["ALLOC_PLAN_CREATED", "ALLOC_PLAN_APPROVED"]), EntityCondition.makeCondition("planTypeId", EntityOperator.EQUALS, "SALES_ORD_ALLOCATION")], EntityOperator.AND) allocationPlanHeader = from("AllocationPlanHeader").where(ecl).queryFirst() if (allocationPlanHeader == null) { ecl = EntityCondition.makeCondition([ EntityCondition.makeCondition("productId", EntityOperator.EQUALS, productId), EntityCondition.makeCondition("orderStatusId", EntityOperator.EQUALS, "ORDER_APPROVED"), EntityCondition.makeCondition("orderTypeId", EntityOperator.EQUALS, "SALES_ORDER")], EntityOperator.AND) orderAndItemList = from("OrderHeaderAndItems").where(ecl).queryList() orderAndItemList.each { orderAndItem -> itemMap = salesChannelEnumId = orderAndItem.salesChannelEnumId itemMap.salesChannelEnumId = salesChannelEnumId salesChannel = from("Enumeration").where("enumId", salesChannelEnumId).queryOne() if (salesChannel) { itemMap.salesChannel = salesChannel.description } orh = new OrderReadHelper(delegator, orderAndItem.orderId) placingParty = orh.getPlacingParty() if (placingParty != null) { itemMap.partyId = placingParty.partyId itemMap.partyName = PartyHelper.getPartyName(placingParty) } itemMap.orderId = orderAndItem.orderId itemMap.orderItemSeqId = orderAndItem.orderItemSeqId itemMap.estimatedShipDate = orderAndItem.estimatedShipDate unitPrice = orderAndItem.unitPrice cancelQuantity = orderAndItem.cancelQuantity quantity = orderAndItem.quantity if (cancelQuantity != null) { orderedQuantity = quantity.subtract(cancelQuantity) } else { orderedQuantity = quantity } orderedValue = orderedQuantity.multiply(unitPrice) orderedQuantityTotal = orderedQuantityTotal.add(orderedQuantity) orderedValueTotal = orderedValueTotal.add(orderedValue) itemMap.orderedQuantity = orderedQuantity itemMap.orderedValue = orderedValue // Reserved quantity reservedQuantity = 0.0 reservations = from("OrderItemShipGrpInvRes").where("orderId", orderAndItem.orderId, "orderItemSeqId", orderAndItem.orderItemSeqId).queryList() reservations.each { reservation -> if (reservation.quantity) { reservedQuantity += reservation.quantity } } reservedQuantityTotal = reservedQuantityTotal.add(reservedQuantity) itemMap.reservedQuantity = reservedQuantity itemList.add(itemMap) } } else { isPlanAlreadyExists = true } allocationPlanInfo.orderedQuantityTotal = orderedQuantityTotal allocationPlanInfo.orderedValueTotal = orderedValueTotal allocationPlanInfo.reservedQuantityTotal = reservedQuantityTotal } allocationPlanInfo.isPlanAlreadyExists = isPlanAlreadyExists allocationPlanInfo.itemList = itemList context.allocationPlanInfo = allocationPlanInfo |
Đoạn code trên tạo kế hoạch phan bổ dựa trên Mã sản phẩm, Mã trạng thái, Mã kế hoạch; phân bổ đơn hàng vào các kênh bán hàng với số lượng cụ thể. Liên kết tải về https://dl.bintray.com/groovy/maven/apache-groovy-sdk-3.0.6.zip (71,2 MB), giải nén ra (thành khoảng 242 MB). Source code: https://github.com/apache/groovy Groovy 3.0.6 yêu cầu phải có JDK phiên bản từ 1.8 trở lên. Trong thư mục sau giải nén, tìm thư mục bin
, giả sử D:toolsgroovy-3.0.6bin
, khai báo đường dẫn này trong biến môi trường của Windows.
Chạy lệnh kiểm tra phiên bản của Groovy
1 2 |
groovy --version |
Kết quả là
1 2 3 |
Picked up _JAVA_OPTIONS: -Xmx512M Groovy Version: 3.0.6 JVM: 1.8.0_261 Vendor: Oracle Corporation OS: Windows 10 |
Khai báo Groovy SDK trong IntelliJ IDEA 2020
Viết chương trình Groovy đầu tiên, file Hello.groovy
1 2 |
println("Hello, Hanoi!"); |
Kết quả:
1 2 |
Hello, Hanoi! |
Xin chúc mừng, vậy là bạn đã viết chương trình Groovy đầu tiên.
Các từ khóa (keyword) trong Groovy là as
, assert
, break
, case
, catch
, class
, const
, continue
, def
, default
, do
, else
, enum
, extends
, false
, finally
, for
, goto
, if
, implements
, import
, in
, instanceof
, interface
, new
, null
, package
, return
, super
, switch
, this
, throws
, throw
, trait
, true
, try
, var
, while
.
Các định danh hợp lệ và không hợp lệ
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
# Hợp lệ def name def item3 def with_under_score def $startByDollarSign // Không hợp lệ def 3abc def a+b def a#b // Hợp lệ foo.as2 foo.asertAA foo.breakXYZ foo.case99 foo.catch42 |
Kiểu dữ liệu map
1 2 3 4 5 6 7 |
def aha = aha.'xx' = "XXX many XX"; aha.'girl' = "BIG GIRL"; assert aha.'xx' == "XXX many XX"; assert aha.'girl' == 'BIG GIRL' |
Quy định đặt tên định danh: Bắt đầu bởi chữ cái, ký hiệu dollar sign, hoặc dấu gạch dưới. Không được phép bắt đầu bởi số.
Chữ cái là các ký tự nằm trong khoảng
- từ
a
đếnz
(ký tự ASCII viết thường) - từ
A
đếnZ
(ký tự ASCIII viết hoa) - từ
u00c0
đếnu00d6
- từ
u00d8
đếnu00f6
- từ
u00f8
đếnu00ff
- từ
u0100
đếnufffe
Một số định danh đặc biệt
1 2 3 4 |
// https://www.fileformat.info/info/unicode/char/00c0/index.htm def À = 1 assert À == 1 |
Cách sử dụng map
File Hello.groovy
có nội dung
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
map = map.'''triple abc''' map."""triple xyz""" map./love string/ map.$/doll love string/$ def something = def firstName = "Van" something."NguyenBich-${firstName}" = "Nguoi yeu cu" assert something."NguyenBich-Van" == "Nguoi yeu cu" println(something.'NguyenBich-Van') println(something."NguyenBich-Van") |
Lưu ý với cách đặt tên key trong kiểu dữ liệu map, những thứ dưới đây là cho phép
1 2 3 4 |
foo = foo./yeu em/ = "Yêu em Vân" foo.$/bo em trang/$ = "Bỏ em Trang" |
Chạy lệnh
1 2 |
groovy Hello.groovy |
Kết quả là
1 2 3 4 |
Picked up _JAVA_OPTIONS: -Xmx512M Nguoi yeu cu Nguoi yeu cu |
Cái này gọi là Groovy GString, khả năng nội suy xâu ký tự của Groovy. Javadoc https://docs.groovy-lang.org/latest/html/api/groovy/lang/GString.html
Các ký tự đặc biệt cho phép trong string là
b
f
n
r
s
\
'
"
Ví dụ liên quan đến GString
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
def sum = "The sum of 2 and 3 equals ${2 + 3}" assert sum.toString() == 'The sum of 2 and 3 equals 5' def sum2 = "The sum of 2 and 3 equals ${def a = 1; def b = 6; a + b}" assert sum2.toString() == 'The sum of 2 and 3 equals 7' def nguoi_nao_do = [ten: 'Van', tuoi: 33] assert "$nguoi_nao_do.ten hien tai $nguoi_nao_do.tuoi tuoi" == "Van hien tai 33 tuoi" def so_pi = 3.1415926535 def sParameterLessClosure = "1 + 2 == ${-> 3}" assert sParameterLessClosure == '1 + 2 == 3' def sOneParameterClosure = "1 + 2 == ${w -> w << 3}" assert sOneParameterClosure == '1 + 2 == 3' |
Ví dụ liên quan đến eager GString và lazy GString
1 2 3 4 5 6 7 8 9 10 11 |
def number = 1 // Mục 1 def eagerGString = "value == ${number}" def lazyGString = "value == ${-> number}" assert eagerGString == "value == 1" // Mục 2 assert lazyGString == "value == 1" // Mục 3 number = 2 // Mục 4 assert eagerGString == "value == 1" // Mục 5 assert lazyGString == "value == 2" // Mục 6 |
- Mục 1. Định ngĩa biến
number
chứa giá trị 1 ở đó chúng ta nội suy bên trong 2 GString, như là một biểu thức trongeagerGString
và một closure tronglazyGString
. - Mục 2. Chúng ta kỳ vọng rằng xâu kết quả chứa cùng giá trị là 1 cho
eagerGString
- Mục 3. Chúng ta kỳ vọng rằng xâu kết quả chứa cùng giá trị là 1 cho
lazyGString
- Mục 4. Sau đó, chúng ta thay đổi giá trị của biến thành một giá trị khác.
- Mục 5. Với biểu thức nội suy thuần, giá trị thực sự ràng buộc vào thời điểm khởi tạo GString.
- Mục 6. Nhưng với biểu thức closure, closure được gọi dựa trên mỗi lần ép giá trị của GSTring vào String, kết quả là một chuỗi được cập nhật chưuá giá trị số mới.
Một số dạng đặc biệt của string
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
def fooPattern = /.*chu_cuoi.*/ assert fooPattern == '.*chu_cuoi.*' def escapeSlash = /The character / is a forward slash/ assert escapeSlash == 'The character / is a forward slash' def nhieu_dong_chuoi_tron = /mot hai ba bon/ assert nhieu_dong_chuoi_tron.contains('n') def mau_sac = 'xanh' def noi_suy_chuoi_tron = /mot chiec xe mau ${mau_sac}/ assert noi_suy_chuoi_tron == 'mot chiec xe mau xanh' |
itemList = []
isPlanAlreadyExists = false
productId = parameters.productId
planName = parameters.planName
if (productId) {
orderedQuantityTotal = 0.0
orderedValueTotal = 0.0
reservedQuantityTotal = 0.0
ecl = EntityCondition.makeCondition([
EntityCondition.makeCondition(“productId”, EntityOperator.EQUALS, productId),
EntityCondition.makeCondition(“statusId”, EntityOperator.IN, [“ALLOC_PLAN_CREATED”, “ALLOC_PLAN_APPROVED”]),
EntityCondition.makeCondition(“planTypeId”, EntityOperator.EQUALS, “SALES_ORD_ALLOCATION”)],
EntityOperator.AND)
allocationPlanHeader = from(“AllocationPlanHeader”).where(ecl).queryFirst()
if (allocationPlanHeader == null) {
ecl = EntityCondition.makeCondition([
EntityCondition.makeCondition(“productId”, EntityOperator.EQUALS, productId),
EntityCondition.makeCondition(“orderStatusId”, EntityOperator.EQUALS, “ORDER_APPROVED”),
EntityCondition.makeCondition(“orderTypeId”, EntityOperator.EQUALS, “SALES_ORDER”)],
EntityOperator.AND)
orderAndItemList = from(“OrderHeaderAndItems”).where(ecl).queryList()
orderAndItemList.each { orderAndItem ->
itemMap =
salesChannelEnumId = orderAndItem.salesChannelEnumId
itemMap.salesChannelEnumId = salesChannelEnumId
salesChannel = from(“Enumeration”).where(“enumId”, salesChannelEnumId).queryOne()
if (salesChannel) {
itemMap.salesChannel = salesChannel.description
}
orh = new OrderReadHelper(delegator, orderAndItem.orderId)
placingParty = orh.getPlacingParty()
if (placingParty != null) {
itemMap.partyId = placingParty.partyId
itemMap.partyName = PartyHelper.getPartyName(placingParty)
}
itemMap.orderId = orderAndItem.orderId
itemMap.orderItemSeqId = orderAndItem.orderItemSeqId
itemMap.estimatedShipDate = orderAndItem.estimatedShipDate
unitPrice = orderAndItem.unitPrice
cancelQuantity = orderAndItem.cancelQuantity
quantity = orderAndItem.quantity
if (cancelQuantity != null) {
orderedQuantity = quantity.subtract(cancelQuantity)
} else {
orderedQuantity = quantity
}
orderedValue = orderedQuantity.multiply(unitPrice)
orderedQuantityTotal = orderedQuantityTotal.add(orderedQuantity)
orderedValueTotal = orderedValueTotal.add(orderedValue)
itemMap.orderedQuantity = orderedQuantity
itemMap.orderedValue = orderedValue
// Reserved quantity
reservedQuantity = 0.0
reservations = from(“OrderItemShipGrpInvRes”).where(“orderId”, orderAndItem.orderId, “orderItemSeqId”, orderAndItem.orderItemSeqId).queryList()
reservations.each { reservation ->
if (reservation.quantity) {
reservedQuantity += reservation.quantity
}
}
reservedQuantityTotal = reservedQuantityTotal.add(reservedQuantity)
itemMap.reservedQuantity = reservedQuantity
itemList.add(itemMap)
}
} else {
isPlanAlreadyExists = true
}
allocationPlanInfo.orderedQuantityTotal = orderedQuantityTotal
allocationPlanInfo.orderedValueTotal = orderedValueTotal
allocationPlanInfo.reservedQuantityTotal = reservedQuantityTotal
}
allocationPlanInfo.isPlanAlreadyExists = isPlanAlreadyExists
allocationPlanInfo.itemList = itemList
context.allocationPlanInfo = allocationPlanInfo
The above code creates a attribution plan based on Product Code, Status Code, Plan Code; allocate orders to sales channels with specific quantities. Download link https://dl.bintray.com/groovy/maven/apache-groovy-sdk-3.0.6.zip (71.2 MB), extract it (into about 242 MB). Source code: https://github.com/apache/groovy Groovy 3.0.6 requires JDK version 1.8 or higher. In the following folder unzip, find the bin
directory, assuming D:toolsgroovy-3.0.6bin
, declare this path in the Windows environment variable.
Run Groovy’s version check command
1 2 |
groovy --version |
The result is
1 2 3 |
Picked up _JAVA_OPTIONS: -Xmx512M Groovy Version: 3.0.6 JVM: 1.8.0_261 Vendor: Oracle Corporation OS: Windows 10 |
Declare the Groovy SDK in IntelliJ IDEA 2020
Write your first Groovy program, file Hello.groovy
1 2 |
println("Hello, Hanoi!"); |
Result:
1 2 |
Hello, Hanoi! |
Congratulations, you’ve written your first Groovy program.
The keywords (keyword) in Groovy are as
, assert
, break
, case
, catch
, class
, const
, continue
, def
, default
, do
, else
, enum
, extends
, false
, finally
, for
, goto
, if
, implements
, import
, in
, instanceof
, interface
, new
, null
, package
, return
, super
, switch
, this
, throws
, throw
, trait
, true
, try
, var
, while
.
Valid and invalid identifiers
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
# Hợp lệ def name def item3 def with_under_score def $startByDollarSign // Không hợp lệ def 3abc def a+b def a#b // Hợp lệ foo.as2 foo.asertAA foo.breakXYZ foo.case99 foo.catch42 |
Map data type
1 2 3 4 5 6 7 |
def aha = aha.'xx' = "XXX many XX"; aha.'girl' = "BIG GIRL"; assert aha.'xx' == "XXX many XX"; assert aha.'girl' == 'BIG GIRL' |
Identification naming rules: Start with a letter, a dollar sign, or an underscore. Not allowed to start with numbers.
Letters are the characters in the range
- from
a
toz
(lowercase ASCII characters) - from
A
toZ
(uppercase ASCIII characters) - from
u00c0
tou00d6
- from
u00d8
tou00f6
- from
u00f8
tou00ff
- from
u0100
toufffe
Some special identifiers
1 2 3 4 |
// https://www.fileformat.info/info/unicode/char/00c0/index.htm def À = 1 assert À == 1 |
How to use map
Hello.groovy
file has content
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
map = map.'''triple abc''' map."""triple xyz""" map./love string/ map.$/doll love string/$ def something = def firstName = "Van" something."NguyenBich-${firstName}" = "Nguoi yeu cu" assert something."NguyenBich-Van" == "Nguoi yeu cu" println(something.'NguyenBich-Van') println(something."NguyenBich-Van") |
Note with the naming of keys in map data type, the following is permissible
1 2 3 4 |
foo = foo./yeu em/ = "Yêu em Vân" foo.$/bo em trang/$ = "Bỏ em Trang" |
Run the command
1 2 |
groovy Hello.groovy |
The result is
1 2 3 4 |
Picked up _JAVA_OPTIONS: -Xmx512M Nguoi yeu cu Nguoi yeu cu |
This is called Groovy GString, Groovy’s ability to interpolate string of characters. Javadoc https://docs.groovy-lang.org/latest/html/api/groovy/lang/GString.html
Special characters allowed in string are
b
f
n
r
s
\
'
"
Example related to GString
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
def sum = "The sum of 2 and 3 equals ${2 + 3}" assert sum.toString() == 'The sum of 2 and 3 equals 5' def sum2 = "The sum of 2 and 3 equals ${def a = 1; def b = 6; a + b}" assert sum2.toString() == 'The sum of 2 and 3 equals 7' def nguoi_nao_do = [ten: 'Van', tuoi: 33] assert "$nguoi_nao_do.ten hien tai $nguoi_nao_do.tuoi tuoi" == "Van hien tai 33 tuoi" def so_pi = 3.1415926535 def sParameterLessClosure = "1 + 2 == ${-> 3}" assert sParameterLessClosure == '1 + 2 == 3' def sOneParameterClosure = "1 + 2 == ${w -> w << 3}" assert sOneParameterClosure == '1 + 2 == 3' |
The example involves eager GString and lazy GString
1 2 3 4 5 6 7 8 9 10 11 |
def number = 1 // Mục 1 def eagerGString = "value == ${number}" def lazyGString = "value == ${-> number}" assert eagerGString == "value == 1" // Mục 2 assert lazyGString == "value == 1" // Mục 3 number = 2 // Mục 4 assert eagerGString == "value == 1" // Mục 5 assert lazyGString == "value == 2" // Mục 6 |
- Item 1. Define the variable
number
containing the value 1 where we interpolate inside 2 GString, as an expression ineagerGString
and a closure onlazyGString
. - Item 2. We expect that the resulting string contains the same value 1 for
eagerGString
- Item 3. We expect that the resulting string contains the same value of 1 for
lazyGString
- Section 4. After that, we change the value of the variable to something else.
- Section 5. For pure interpolation expression, value is actually constrained at GString initialization.
- Section 6. But for the closure expression, the closure is called based on each force of GSTring’s value into String, the result is an updated string containing a new numeric value.
Some special form of string
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
def fooPattern = /.*chu_cuoi.*/ assert fooPattern == '.*chu_cuoi.*' def escapeSlash = /The character / is a forward slash/ assert escapeSlash == 'The character / is a forward slash' def nhieu_dong_chuoi_tron = /mot hai ba bon/ assert nhieu_dong_chuoi_tron.contains('n') def mau_sac = 'xanh' def noi_suy_chuoi_tron = /mot chiec xe mau ${mau_sac}/ assert noi_suy_chuoi_tron == 'mot chiec xe mau xanh' |