Membaca File Excel Dengan Groovy

Salah satu library yang paling terkenal untuk membaca file Excel melalui Java adalah Apache POI. Tentu saja pengguna Groovy juga dapat memakai library tersebut untuk membaca file Excel yang masih dalam format binary (berakhiran xls). Akan tetapi, Microsoft Excel mulai memperkenalkan format XML sejak dirilisnya Microsoft Office XP. Format dokumen Excel yang diadopsi sejak Microsoft Office 2007 adalah Office Open XML (berakhir xlsx). Lalu, apa dampak peralihan format binary ke dalam bentuk XML? Perubahan format ini membuat saya bisa membaca file xlsx yang dibuat dengan Excel 2007 ke atas dengan gampang asalkan bahasa yang dipakai dapat membaca file XML. Sebagai contoh, saya bisa membaca file xlsx di Groovy melalui XmlSlurper-nya tanpa melibatkan library tambahan.

Sebuah file xlsx (Excel 2007 ke atas) adalah file yang mengikuti spesifikasi Open Packaging Conventions (OPC). Dengan kata lain, file xlsx pada dasarnya adalah sebuah arsip yang terkompresi dengan menggunakan format ZIP. Sebagai contoh, bila saya membuka file sample.xlsx dengan menggunakan 7-Zip, saya akan menemukan isi arsip seperti berikut ini:

Isi file xlsx yang dibuat oleh Microsoft Excel

Isi file xlsx yang dibuat oleh Microsoft Excel

File [Content_Types].xml berisi daftar seluruh XML yang ada dan jenis-nya (ContentType). Karena file xlsx adalah file ZIP, maka saya bisa membacanya dengan menggunakan class ZipFile bawaan Java. Untuk membuktikannya, saya dapat membuka Groovy Console dan mengerjakan perintah berikut ini:

Membaca XML melalui Groovy

Membaca XML melalui Groovy

Setiap file yang berada dalam arsip ZIP tersebut disebut sebagai part (karena pada dasarnya secara logika hanya ada 1 file xlsx). Workbook di Excel dideskripsikan dalam sebuah part dengan format XML yang disebut sebagai SpreadsheetML. Untuk mendapatkan lokasi part yang berisi workbook, saya perlu mencarinya di [Content_Types].xml. Saya bisa membaca XML tersebut dengan XmlSlurper dari Groovy. Sebagai contoh, kode program berikut akan mencari workbook di [Content_Types].xml:

import java.util.zip.ZipFile

ZipFile zipFile = new ZipFile('C:/sample.xlsx')
def contentTypesStream = zipFile.getInputStream(zipFile.getEntry('[Content_Types].xml'))
def typesXml = new XmlSlurper().parse(contentTypesStream)
def contentType = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml' 
def workbook = typesXml.Override.find { it.@ContentType.text() == contentType }.@PartName.text()

Output:

/xl/workbook.xml

Seperti apa isi part /xl/workbook.xml tersebut? Berikut adalah contoh isi part tersebut:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"
          xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
    <fileVersion appName="xl" lastEdited="5" lowestEdited="5" rupBuild="9302"/>
    <workbookPr filterPrivacy="1" defaultThemeVersion="124226"/>
    <bookViews>
        <workbookView xWindow="240" yWindow="120" windowWidth="9555" windowHeight="5700"/>
    </bookViews>
    <sheets>
        <sheet name="student" sheetId="1" r:id="rId1"/>
        <sheet name="teacher" sheetId="2" r:id="rId2"/>
        <sheet name="classroom" sheetId="3" r:id="rId3"/>
    </sheets>
    <definedNames>
        <definedName name="LOCAL_MYSQL_DATE_FORMAT" hidden="1">REPT(LOCAL_YEAR_FORMAT,4)&LOCAL_DATE_SEPARATOR&REPT(LOCAL_MONTH_FORMAT,2)&LOCAL_DATE_SEPARATOR&REPT(LOCAL_DAY_FORMAT,2)&"
            "&REPT(LOCAL_HOUR_FORMAT,2)&LOCAL_TIME_SEPARATOR&REPT(LOCAL_MINUTE_FORMAT,2)&LOCAL_TIME_SEPARATOR&REPT(LOCAL_SECOND_FORMAT,2)
        </definedName>
    </definedNames>
    <calcPr calcId="144525"/>
</workbook>

Setiap kali membaca sebuah part, saya juga perlu membaca part lain yang disebut sebagai relationship part bila ada. Relationship part mendeklarasikan hubungan part tersebut dengan part lainnya (mirip seperti sebagai join table di database). Saya dapat menemukan relationship part pada folder dengan nama seperti _rels dimana didalamnya terdapat part dengan akhiran .rels. Sebagai contoh, relationship part untuk xl/workbook.xml adalah xl/_rels/workbook.xml.rels yang isinya terlihat seperti berikut ini:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
    <Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet"
                  Target="worksheets/sheet3.xml"/>
    <Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet"
                  Target="worksheets/sheet2.xml"/>
    <Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet"
                  Target="worksheets/sheet1.xml"/>
    <Relationship Id="rId6" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings"
                  Target="sharedStrings.xml"/>
    <Relationship Id="rId5" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles"
                  Target="styles.xml"/>
    <Relationship Id="rId4" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme"
                  Target="theme/theme1.xml"/>
</Relationships>

Sekarang, saya bisa membuat kode program Groovy untuk menampilkan daftar sheet di workbook, misalnya dengan kode program seperti berikut ini:

import java.util.zip.ZipFile

ZipFile zipFile = new ZipFile('C:/sample.xlsx')
def workbookStream = zipFile.getInputStream(zipFile.getEntry('xl/workbook.xml'))
def workbookXml = new XmlSlurper().parse(workbookStream)
def referenceStream = zipFile.getInputStream(zipFile.getEntry('xl/_rels/workbook.xml.rels'))
def referenceXml = new XmlSlurper().parse(referenceStream)

workbookXml.sheets.sheet.each {
    def rId = it.@'r:id'.text()
    def targetSheet = 'xl/' + referenceXml.Relationship.find { it.@Id.text() == rId }.@Target.text()
    println "Menemukan sheet ${it.@name.text()} dengan referensi $rId ($targetSheet)"    
}

Output:

Menemukan sheet student dengan referensi rId1 (xl/worksheets/sheet1.xml)
Menemukan sheet teacher dengan referensi rId2 (xl/worksheets/sheet2.xml)
Menemukan sheet classroom dengan referensi rId3 (xl/worksheets/sheet3.xml)

Masing-masing sheet diwakili oleh part (file XML) tersendiri. Sebagai contoh, seandainya saya membuat sheet dengan isi seperti berikut ini:

Contoh isi file yang dibuat dengan Microsoft Excel

Contoh isi file yang dibuat dengan Microsoft Excel

maka worksheet tersebut akan disimpan dalam bentuk XML seperti berikut ini:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"
           xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
           xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="x14ac"
           xmlns:x14ac="http://schemas.microsoft.com/office/spreadsheetml/2009/9/ac">
    <dimension ref="A1:D4"/>
    <sheetViews>
        <sheetView tabSelected="1" workbookViewId="0">
            <selection activeCell="D2" sqref="D2"/>
        </sheetView>
    </sheetViews>
    <sheetFormatPr defaultRowHeight="15" x14ac:dyDescent="0.25"/>
    <cols>
        <col min="4" max="4" width="13.85546875" customWidth="1"/>
    </cols>
    <sheetData>
        <row r="1" spans="1:4" x14ac:dyDescent="0.25">
            <c r="A1" t="s">
                <v>0</v>
            </c>
            <c r="B1" t="s">
                <v>1</v>
            </c>
            <c r="C1" t="s">
                <v>2</v>
            </c>
            <c r="D1" t="s">
                <v>18</v>
            </c>
        </row>
        <row r="2" spans="1:4" x14ac:dyDescent="0.25">
            <c r="A2">
                <v>1</v>
            </c>
            <c r="B2" t="s">
                <v>3</v>
            </c>
            <c r="C2">
                <v>30</v>
            </c>
            <c r="D2" s="1">
                <v>31119</v>
            </c>
        </row>
        <row r="3" spans="1:4" x14ac:dyDescent="0.25">
            <c r="A3">
                <v>2</v>
            </c>
            <c r="B3" t="s">
                <v>4</v>
            </c>
            <c r="C3">
                <v>28</v>
            </c>
            <c r="D3" s="1">
                <v>31825</v>
            </c>
        </row>
        <row r="4" spans="1:4" x14ac:dyDescent="0.25">
            <c r="A4">
                <v>3</v>
            </c>
            <c r="B4" t="s">
                <v>5</v>
            </c>
            <c r="C4">
                <v>25</v>
            </c>
            <c r="D4" s="1">
                <v>16689</v>
            </c>
        </row>
    </sheetData>
    <pageMargins left="0.7" right="0.7" top="0.75" bottom="0.75" header="0.3" footer="0.3"/>
    <pageSetup paperSize="9" orientation="portrait" r:id="rId1"/>
    <tableParts count="1">
        <tablePart r:id="rId2"/>
    </tableParts>
</worksheet>

Setiap baris di sheet diwakili oleh <row>. Setiap baris memiliki satu atau lebih <c> yang mewakili sel. Sebuah <c> memiliki nilai yang didefinisikan oleh <v>. Karena semua ini didefinisikan dalam bentuk XML, maka saya dapat membacanya melalui XmlSlurper seperti pada contoh kode program Groovy berikut ini:

import java.util.zip.ZipFile

ZipFile zipFile = new ZipFile('C:/sample.xlsx')
def sheet1Stream = zipFile.getInputStream(zipFile.getEntry('xl/worksheets/sheet1.xml'))
def sheet1Xml = new XmlSlurper().parse(sheet1Stream)
def referenceStream = zipFile.getInputStream(zipFile.getEntry('xl/worksheets/_rels/sheet1.xml.rels'))
def referenceXml = new XmlSlurper().parse(referenceStream)

sheet1Xml.sheetData.row.each { row ->
    row.c.each { c ->
        print "${c.v.text()}; "
    }
    println ''
}

Output:

0; 1; 2; 18; 
1; 3; 30; 31119; 
2; 4; 28; 31825; 
3; 5; 25; 16689;

Ada yang aneh, bukan? Teks yang saya ketik tidak muncul melainkan diganti dengan angka. Selain itu, kolom untuk tanggal juga berada dalam bentuk numerik. Hanya kolom ke-3 yang memang dalam bentuk angka yang berhasil dibaca dengan baik.

Untuk menghemat ukuran file, Excel menyimpan seluruh teks (string) yang unik pada xl/sharedStrings.xml. Bila atribut t dari <c> bernilai s, maka hal tersebut berarti nilai dari <v> adalah referensi (berdasarkan indeks) ke teks yang ada di xl/sharedStrings.xml. Contoh isi xl/sharedStrings.xml adalah:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<sst xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" count="23" uniqueCount="19">
    <si>
        <t>id</t>
    </si>
    <si>
        <t>name</t>
    </si>
    <si>
        <t>age</t>
    </si>
    <si>
        <t>jocki</t>
    </si>
    <si>
        <t>lena</t>
    </si>
    ...
</sst>

Dengan demikian, agar dapat menampilkan teks, saya perlu mengubah kode program saya menjadi seperti berikut ini:

import java.util.zip.ZipFile

ZipFile zipFile = new ZipFile('C:/sample.xlsx')
def sheet1Stream = zipFile.getInputStream(zipFile.getEntry('xl/worksheets/sheet1.xml'))
def sheet1Xml = new XmlSlurper().parse(sheet1Stream)
def referenceStream = zipFile.getInputStream(zipFile.getEntry('xl/worksheets/_rels/sheet1.xml.rels'))
def referenceXml = new XmlSlurper().parse(referenceStream)
def sharedStringStream = zipFile.getInputStream(zipFile.getEntry('xl/sharedStrings.xml'))
def sharedStringXml = new XmlSlurper().parse(sharedStringStream)

sheet1Xml.sheetData.row.each { row ->
    row.c.each { c ->
        def nilai = c.v.text()
        if (c.@t.text() == 's') {
            nilai = sharedStringXml.si[nilai as Integer].t.text()
        }
        print "${nilai}; "
    }
    println ''
}

Output:

id; name; age; birthdate; 
1; jocki; 30; 31119; 
2; lena; 28; 31825; 
3; snake; 25; 16689;

Teks sudah berhasil dibaca dengan baik. Lalu, bagaimana dengan tanggal? Untuk mendeteksi sebuah kolom berupa tanggal atau bukan ternyata tidak mudah! Saya harus mulai dengan memeriksa nilai atribut s dari <c> bersangkutan. Nilai ini adalah referensi ke index <xf> yang ada di tag <cellXfs> di part xl/styles.xml. Contoh isi file xl/styles.xml:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"
            xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="x14ac"
            xmlns:x14ac="http://schemas.microsoft.com/office/spreadsheetml/2009/9/ac">
    ...
    <cellXfs count="2">
        <xf numFmtId="0" fontId="0" fillId="0" borderId="0" xfId="0"/>
        <xf numFmtId="14" fontId="0" fillId="0" borderId="0" xfId="0" applyNumberFormat="1"/>
    </cellXfs>
    ...
</styleSheet>

Bila nilai numFmtId di <xf> berada di kisaran 14 sampai 22, maka sel tersebut memakai format tanggal bawaan. Akan tetapi bila nilai numFmtId di atas 163, maka sel tersebut di-format secara custom dimana format tersebut tersimpan di <numFmts>. Agar sederhana, saya hanya akan mengenali format tanggal bawaan Microsoft Excel dengan mengubah kode program saya menjadi seperti berikut ini:

import java.util.zip.ZipFile

ZipFile zipFile = new ZipFile('C:/sample.xlsx')
def sheet1Stream = zipFile.getInputStream(zipFile.getEntry('xl/worksheets/sheet1.xml'))
def sheet1Xml = new XmlSlurper().parse(sheet1Stream)
def referenceStream = zipFile.getInputStream(zipFile.getEntry('xl/worksheets/_rels/sheet1.xml.rels'))
def referenceXml = new XmlSlurper().parse(referenceStream)
def sharedStringStream = zipFile.getInputStream(zipFile.getEntry('xl/sharedStrings.xml'))
def sharedStringXml = new XmlSlurper().parse(sharedStringStream)
def styleStream = zipFile.getInputStream(zipFile.getEntry('xl/styles.xml'))
def styleXml = new XmlSlurper().parse(styleStream)

sheet1Xml.sheetData.row.each { row ->
    row.c.each { c ->
        def nilai = c.v.text()
        if (c.@t.text() == 's') {
            nilai = sharedStringXml.si[nilai as Integer].t.text()
        } else if (!c.@s.isEmpty()) {
            def numFmtId = styleXml.cellXfs.xf[c.@s.text() as Integer].@numFmtId.text()
            if ((14..22).contains(numFmtId as Integer)) {
                Calendar cal = Calendar.getInstance()
                cal.set(1900, 0, (nilai as Integer) - 1, 0, 0, 0)
                nilai = cal.getTime()
            }
        }
        print "${nilai}; "
    }
    println ''
}

Output:

id; name; age; birthdate; 
1; jocki; 30; Wed Mar 13 00:00:00 ICT 1985; 
2; lena; 28; Tue Feb 17 00:00:00 ICT 1987; 
3; snake; 25; Sun Sep 09 00:00:00 ICT 1945;

Pada kode program di atas, kerumitan lain yang saya jumpai adalah mengubah nilai tanggal dari Excel menjadi nilai tanggal di Java. Excel menyimpan tanggal sebagai selisih hari sejak tanggal 0 Januari 1900 (yup, itu sebabnya saya perlu -1 di kode program). Bagian desimal-nya (dibelakang koma) akan dianggap informasi waktu (jam, menit dan detik). Sementara itu, Java menyimpan tanggal sebagai selisih detik sejak 1 Januari 1970 (disebut juga Unix time) dalam satuan milidetik (ms).

Bila ingin membaca bagian waktu (jam dan menit), saya bisa meniru teknik yang dipakai oleh org.apache.poi.ss.usermodel.DateUtil milik Apache POI, misalnya dengan membuat kode proram seperti berikut ini:

...
value = value as Double
int days = (int) Math.floor(value)
int time = (int)((value - days) * 86400 + 0.5) * 1000
Calendar cal = Calendar.getInstance()
cal.set(1900, 0, (value as Integer) - 1, 0, 0, 0)
cal.set(Calendar.MILLISECOND, time)
value = cal.getTime()
Iklan

Memakai Fork/Join Framework Di Groovy

Salah satu masalah yang timbul pada percobaan saya di artikel sebelumnya (Multithreading Di Groovy) adalah walaupun saya membuat beberapa thread berbeda, thread yang sudah menyelesaikan tugasnya terlebih dahulu akan nganggur (berada dalam kondisi wait). Untuk mengatasi permasalahan tersebut, Java 7 telah dilengkapi dengan framework Fork/Join dengan kemampuan work-stealing. Groovy kemudian menyediakan cara mudah memakai Fork/Join Framework melalui GPars.

Sebagai latihan, saya akan mencoba membuat kode program sebelumnya yang memakai framework Executor menjadi menggunakan framework Fork/Join seperti berikut ini:

import groovy.transform.CompileStatic
import javax.swing.JOptionPane
import static groovyx.gpars.GParsPool.runForkJoin
import static groovyx.gpars.GParsPool.withPool

JOptionPane.showMessageDialog(null, 'Attach Terlebih Dahulu Profiler Bila Perlu...')

long waktuMulai = System.nanoTime()

@CompileStatic
boolean isPrimary(long angka) {
    for (int i=2; i<angka-1; i++) {
        if ((angka % i) == 0) return false
    }
    true
}

List hasil

withPool(4) {
    hasil = runForkJoin(2, 100000) { angka1, angka2 ->
        List hasilSementara = []
        if ((angka2 - angka1) > 500) {
            def tengah = (int)((angka2-angka1)/2)
            forkOffChild(angka1, angka1+tengah-1)
            forkOffChild(angka1+tengah, angka2)
            childrenResults.each { hasilSementara.addAll(it) }
        } else {
            for (long angka=angka1; angka<=angka2; angka++) {
                if (isPrimary(angka)) {
                    hasilSementara << angka
                }
            }
        }
        hasilSementara
    }
}

hasil.sort()
long waktuSelesai = System.nanoTime()
println "Waktu Eksekusi: ${waktuSelesai - waktuMulai}"

Pada kode program di atas, saya perlu meletakkan kode program dalam withPool() yang akan memakai framework Fork/Join. Saya juga dapat menentukan jumlah thread yang akan dipakai dengan memberikan parameter angka seperti withPool(4) { ... }. Setelah itu, saya meletakkan kode program yang memakai algoritma divide and conquer pada closure di runForkJoin(). Di dalam closure untuk runForkJoin(), saya dapat memanggil method forkOffChild() dan getChildrenResults(). Method forkOffChild() akan menjadwalkan pemanggilan dan langsung lanjut ke perintah berikutnya (setara dengan fork). Method getChildrenResults() akan menunggu hingga seluruh child selesai di-eksekusi (setara dengan join) dan mengembalikan hasilnya dalam bentuk List. Tanpa @CompileStatic di method isPrimary(), kode program akan berjalan sangat lambat, bahkan lebih lambat dari versi yang hanya memakai thread tunggal.

Bila saya menjalankan kode program di atas dan melihat daftar thread melalui JVisualVM, saya akan memperoleh hasil seperti pada gambar berikut ini:

Tampilan thread yang sibuk semua

Tampilan thread yang sibuk semua

Kali ini terlihat bahwa thread yang ada semuanya sibuk dan tidak ada lagi yang ‘santai’. Walaupun demikian, secara garis besar, kinerjanya tidak lebih baik daripada versi sebelumnya yang memakai framework Executor. Hal ini karena kode program harus melakukan hal baru yang sebelumnya tidak ada seperti menjadwalkan tugas (dengan forkOffChild()) dan mengisi/menggabungkan List.

Setidaknya bila dibandingkan dengan kode program versi Java di artikel Seberapa Jauh Bedanya Program Yang Multithreading?, versi Groovy-nya terasa lebih sederhana dan lebih mudah dipahami. Bukan hanya itu, kode program di atas sebenarnya masih dapat disederhanakan lagi menjadi seperti:

import groovy.transform.CompileStatic
import javax.swing.JOptionPane
import static groovyx.gpars.GParsPool.withPool

JOptionPane.showMessageDialog(null, 'Attach Terlebih Dahulu Profiler Bila Perlu...')

long waktuMulai = System.nanoTime()

@CompileStatic
boolean isPrimary(long angka) {
    for (int i=2; i<angka-1; i++) {
        if ((angka % i) == 0) return false
    }
    true
}

List hasil

withPool(4) {
    hasil = (2..100000).findAllParallel { isPrimary(it) }
    hasil.sort()
}

long waktuSelesai = System.nanoTime()
println "Waktu Eksekusi: ${waktuSelesai - waktuMulai}"

Kode program di atas memakai findAllParallel() yang akan menggunakan framework Fork/Join untuk mengerjakan operasi pada List secara paralel. Pada Groovy, range seperti (2..100000) adalah sebuah List (Range diturunkan dari List).

Bila saya ingin mencetak setiap bilangan prima ke layar, saya dapat menggunakan kode program seperti:

withPool {
    (2..100000).eachParallel {
        if (isPrimary(it)) println it
    }
}

Multithreading Di Groovy

Karena Groovy dapat mengakses class Java, maka secara tidak langsung, Groovy juga mendukung multithreading. Java 8 memiliki banyak fitur tambahan yang berkaitan dengan multithreading (concurrency), misalnya tambahan method forEach(), search(), reduce(), Array.parallelSort(), dan sebagainya. Groovy, tanpa Java 8, juga memiliki fasilitas yang serupa melalui GPars. Sebelumnya GPars adalah proyek terpisah tapi kini sudah menjadi bagian dari Groovy sehingga tidak perlu di-download secara terpisah lagi.

Sebagai latihan, saya akan membuat sebuah program yang mencari bilangan prima. Program ini akan memakai algoritma yang paling buruk dan paling lambat, tapi setidaknya sangat mudah dimengerti. Ini adalah versi kode program yang hanya berjalan pada thread tunggal:

List hasil = []

long waktuMulai = System.nanoTime()

boolean isPrimary(long angka) {
    for (int i=2; i<angka-1; i++) {
        if ((angka % i) == 0) return false
    }
    true
}

(2..100000).each { angka ->
    if (isPrimary(angka)) hasil << angka
}

long waktuSelesai = System.nanoTime()

println "Waktu Eksekusi: ${waktuSelesai - waktuMulai}"

Kode program tersebut membutuhkan waktu sekitar 9.356.485.040 ns. Kode program tersebut hanya bekerja pada satu thread yaitu thread main yang terlihat seperti pada gambar hasil di JVisualVM berikut ini:

Kode program yang hanya dijalankan di thread main

Kode program yang hanya dijalankan di thread main

Thread dengan nama main adalah thread yang diciptakan secara otomatis pada saat kode program Java dikerjakan. Karena kode program di atas tidak membuat thread baru, maka hingga selesai dikerjakan, program yang saya buat di atas akan berjalan di thread main.

Pada Groovy, saya tetap dapat menggunakan instance dari Thread untuk mewakili sebuah thread baru. Akan tetapi, saya akan memakai cara yang lebih mudah dengan menggunakan task dari GPars. Sebagai contoh, saya akan mengubah kode program di atas agar membagi proses looping menjadi 2 bagian yang paralel, seperti berikut ini:

import javax.swing.JOptionPane
import java.util.concurrent.ConcurrentSkipListSet
import static groovyx.gpars.dataflow.Dataflow.task

ConcurrentSkipListSet hasil = new ConcurrentSkipListSet()

JOptionPane.showMessageDialog(null, 'Attach Terlebih Dahulu Profiler Bila Perlu...')

long waktuMulai = System.nanoTime()

boolean isPrimary(long angka) {
    for (int i=2; i<angka-1; i++) {
        if ((angka % i) == 0) return false
    }
    true
}

def t1 = task {
    (2..50000).each {
        if (isPrimary(it)) hasil << it
    }
}

def t2 = task {
    (50001..100000).each {
        if (isPrimary(it)) hasil << it
    }
}

[t1,t2]*.join()

long waktuSelesai = System.nanoTime()

println "Waktu Eksekusi: ${waktuSelesai - waktuMulai}"

Kode program di atas membutuhkan waktu sekitar 7.585.801.575 ns atau lebih cepat 19% dibanding versi thread tunggal sebelumnya. Pada saat program ini dijalankan, t1 dan t2 masing-masing akan dikerjakan pada thread terpisah dengan nama Actor Thread 1 dan Actor Thread 2 seperti yang terlihat pada gambar hasil JVisualVM berikut ini:

Kode program yang dijalankan pada 2 thread

Kode program yang dijalankan pada 2 thread

Terlihat bahwa thread main kini menghabiskan lebih banyak waktunya untuk menunggu. Actor Thread 2 bekerja lebih keras sementara Actor Thread 1 lebih duluan selesai. Walaupun sudah selesai, Actor Thread 1 harus menunggu hingga thread lain selesai, karena saya memanggil join() dengan [t1,t2]*.join(). Thread yang nganggur adalah pemborosan, oleh sebab itu Java 7 dilengkapi dengan fork/join framework yang memiliki algoritma work-stealing untuk mengurangi thread yang nganggur. Saya tidak akan memakainya disini.

Saya akan mencoba meningkatkan jumlah task menjadi lebih banyak dengan mengubah kode program menjadi seperti berikut ini:

import javax.swing.JOptionPane
import java.util.concurrent.ConcurrentSkipListSet
import static groovyx.gpars.dataflow.Dataflow.task

ConcurrentSkipListSet hasil = new ConcurrentSkipListSet()

JOptionPane.showMessageDialog(null, 'Attach Terlebih Dahulu Profiler Bila Perlu...')

long waktuMulai = System.nanoTime()

boolean isPrimary(long angka) {
    for (int i=2; i<angka-1; i++) {
        if ((angka % i) == 0) return false
    }
    true
}

def t1 = task {
    (1..25000).each {
        if (isPrimary(it)) hasil << it
    }
}

def t2 = task {
    (25001..50000).each {
        if (isPrimary(it)) hasil << it
    }
}

def t3 = task {
    (50001..75000).each {
        if (isPrimary(it)) hasil << it
    }
}

def t4 = task {
    (75001..100000).each {
        if (isPrimary(it)) hasil << it
    }
}

[t1,t2,t3,t4]*.join()

long waktuSelesai = System.nanoTime()

println "Waktu Eksekusi: ${waktuSelesai - waktuMulai}"

Waktu eksekusi kini menjadi 5.899.754.797 atau lebih cepat 37% dibanding versi thread tunggal. Gambar hasil JVisualVM menunjukkan bahwa kini ada 4 thread yang bekerja secara paralel:

Kode program yang dijalankan pada 4 thread

Kode program yang dijalankan pada 4 thread

Terlihat pada thread yang memproses bilangan yang kecil akan lebih cepat selesai dan lebih banyak menunggu (warna kuning menunjukkan thread sedang nganggur). Hal ini karena bilangan kecil membutuhkan looping yang lebih singkat, sementara bilang besar membutuhkan lebih banyak looping.

Bila saya hanya ingin menampilkan bilangan prima yang ditemukan langsung pada layar, saya dapat membuat sebuah task baru seperti berikut ini:

def outputTask = task {
    while (true) {
        println hasil.last()
    }
}

Task di atas akan menampilkan hasil perhitungan terakhir. Tapi karena ia dijalankan tidak tersinkronisasi dengan task lainnya, maka sering kali nilai yang sama ditampilkan berulang kali. Groovy mengatasi hal ini dengan cara yang sangat sederhana yang disebut dataflow. Dengan dataflow, sebuah task menulis nilai, dan task lain akan mengerjakan sesuatu hanya jika nilai tersebut sudah terisi. Sebagai contoh, saya dapat menggunakan DataflowQueues seperti pada kode program berikut ini:

import groovyx.gpars.dataflow.DataflowQueue
import static groovyx.gpars.dataflow.Dataflow.task

DataflowQueue buffer = new DataflowQueue()

boolean isPrimary(long angka) {
    for (int i=2; i<angka-1; i++) {
        if ((angka % i) == 0) return false
    }
    true
}

def t1 = task {
    (2..25000).each {
        if (isPrimary(it)) buffer << it
    }
}

def t2 = task {
    (25001..50000).each {
        if (isPrimary(it)) buffer << it
    }
}

def t3 = task {
    (50001..75000).each {
        if (isPrimary(it)) buffer << it
    }
}

def t4 = task {
    (75001..100000).each {
        if (isPrimary(it)) buffer << it
    }
}

def output = task {
    while (true) {
        println buffer.val
    }
}

[t1,t2,t3,t4]*.join()

Untuk mengisi nilai pada dataflow, saya menggunakan kode program seperti buffer << it. Untuk mengakses nilainya, saya menggunakan kode program seperti buffer.val yang akan menunggu hingga ada nilai yang dapat dibaca.

Memakai Currying Pada Closure Di Groovy

Berdasarkan informasi dari Wikipedia, currying adalah sebuah teknik transformasi fungsi pada bidang matematika dan ilmu komputer yang diperkenalkan oleh Moses Schonfinkel dan dipopulerkan oleh Haskell Curry.   Closure pada Groovy mendukung currying, tapi sebenarnya apa itu currying?   Saya akan mengawali ilustrasi dengan sebuah pemanggilan closure biasa seperti berikut ini:

def query = { String namaModel, boolean barisTunggal, Map where ->
   println "Melakukan query untuk mencari $namaModel"
   where.each { k,v -> println "Dengan kondisi $k = $v" }
   if (barisTunggal) {
       "Data $namaModel"
   } else {
       ["Data $namaModel #1", "Data $namaModel #2"]
   }
}

//
// Output:
// Melakukan query untuk mencari mahasiswa
// Dengan kondisi nama = Jocki
// Dengan kondisi lulus = true
// Data mahasiswa
//
println query('mahasiswa', true, [nama: 'Jocki', lulus: true])

//
// Output:
// Melakukan query untuk mencari mahasiswa
// Dengan kondisi nama = Jocki
// [Data mahasiswa #1, Data mahasiswa #2]
//
println query('mahasiswa', false, [nama: 'Jocki'])

//
// Output:
// Melakukan query untuk mencari kelas
// Dengan kondisi kode = 1AC
// Data kelas
//
println query('kelas', true, [kode: '1AC'])

Pada kode program di atas, anggap saja closure query dipakai untuk membaca data dari database.   Ia membutuhkan argumen berupa sebuah namaModel yang mewakili nama tabel yang akan dibaca.   Ia juga memiliki argumen barisTunggal yang jika bernilai true akan selalu mengembalikan sebuah record; sebaliknya jika bernilai false, maka yang dikembalikan adalah sebuah List berisi satu atau lebih record.   Saya juga dapat menyertakan kondisi pencarian dalam bentuk sebuah Map.

Sekilas, pemanggilan closure query terlihat kompleks karena harus memberikan banyak parameter.   Oleh sebab itu, saya dapat menggunakan currying untuk me-‘reduksi‘ closure tersebut menjadi lebih terspesialisasi (dengan cara memastikan parameter tertentu selalu sama nilainya).   Sebagai contoh, saya dapat menghasilkan queryMahasiswa dan queryKelas yang khusus untuk mencari tabel-nya masing-masing, seperti pada kode program berikut ini:

def query = { String namaModel, boolean barisTunggal, Map where ->
   println "Melakukan query untuk mencari $namaModel"
   where.each { k,v -> println "Dengan kondisi $k = $v" }
   if (barisTunggal) {
       "Data $namaModel"
   } else {
       ["Data $namaModel #1", "Data $namaModel #2"]
   }
}

def queryMahasiswa = query.curry('mahasiswa')
println queryMahasiswa(true, [nama: 'Jocki', lulus: true])
println queryMahasiswa(false, [nama: 'Jocki'])

def queryKelas = query.curry('kelas')
println queryKelas(true, [kode: '1AC'])

Kode program di atas menghasilkan output yang sama seperti sebelumnya, tetapi kini terdapat queryMahasiswa dan queryKelas yang dihasilkan dari query, dimana parameter pertama-nya selalu di-isi dengan nilai yang telah ditentukan (yakni 'mahasiswa' dan 'kelas').   Ini disebut juga dengan partial function application.

Pada contoh berikut ini, saya kembali melakukan ‘spesialisasi’ lagi dimana terdapat closure yang hanya mengembalikan record tunggal dan juga terdapat closure yang mengembalikan lebih dari satu record:

def query = { String namaModel, boolean barisTunggal, Map where ->
   println "Melakukan query untuk mencari $namaModel"
   where.each { k,v -> println "Dengan kondisi $k = $v" }
   if (barisTunggal) {
       "Data $namaModel"
   } else {
       ["Data $namaModel #1", "Data $namaModel #2"]
   }
}

def cariSeluruhMahasiswa = query.curry('mahasiswa', false)
def cariMahasiswa = query.curry('mahasiswa', true)
def cariSeluruhKelas = query.curry('kelas', false)
def cariKelas = query.curry('kelas', true)

println cariMahasiswa([nama: 'Jocki', lulus: true])
println cariSeluruhMahasiswa([nama: 'Jocki'])
println cariKelas([kode: '1AC'])
Contoh Penggunaan Currying

Contoh Penggunaan Currying

Seluruh contoh program di atas mengembalikan hasil yang sama dan memiliki implementasi kode program yang satu (dimana isi closure query tidak berubah).   Walaupun demikian, versi terakhir di atas terlihat lebih mudah dimengerti dan lebih rapi karena argumen yang dilewatkan lebih sedikit.

Mengungkap Misteri Lenyapnya Looping ‘for’ Di Groovy

Memakai struktur data Collection,  termasuk juga array,  adalah hal yang sering saya alami.   Pada bahasa pemograman Java, untuk membaca dan menulis Collection,  saya harus melakukan looping dengan keyword for.  Sebagai contoh, ini adalah kode program Java yang membaca dan menampilkan ArrayList:

Collection bahasa = new ArrayList();
bahasa.add("Java");
bahasa.add("Groovy");
bahasa.add("C");
bahasa.add("C++");
bahasa.add("PHP");
for (String b: bahasa) {
   System.out.println(b);
}

Sebenarnya kode program di atas juga dapat dipakai pada Groovy, karena for tetap ada di Groovy.   Walaupun demikian, ada cara yang lebih mudah dalam melakukan looping,  yaitu dengan menggunakan method each() dari Collection tersebut.   Method each() menerima argumen berupa sebuah closure yang akan dikerjakan untuk setiap item dalam Collection.   Berikut ini adalah contoh kode program Groovy yang melakukan hal serupa:

def bahasa = ["Java", "Groovy", "C", "C++", "PHP"]
bahasa.each { println it }

Cukup dua baris, tanpa memakai looping for!   Pada closure untuk each(),  variabel it merujuk pada item yang sedang ditelusuri.   Bila ingin memperjelas makna it, misalnya memberi tipe data dan nama berbeda, saya dapat mengubahnya menjadi seperti berikut ini:

def bahasa = ["Java", "Groovy", "C", "C++", "PHP"]
bahasa.each { String b -> println b }

Bagaimana bila saya ingin melakukan perulangan angka berurut,  apakah tetap harus memakai for?   Misalnya seperti pada kode program Java berikut ini:

for (int i=0; i<10; i++) {
   System.out.println("Nilai: " + i);
}

Groovy memiliki Collection khusus yang disebut Range untuk mewakili nilai berurut.   Misalnya,  kode program di atas akan mencetak 0 sampai 9, sehingga dapat diwakili dengan range 0..<10.   Notasi ..< menunjukkan bahwa ’10’  tidak menjadi bagian dari range tersebut.   0..<10 pada dasarnya sama dengan 0..9 (versi ini lebih jelas).   Karena Range juga adalah sebuah Collection, maka looping bisa dilakukan dengan method each() seperti yang terlihat pada kode program berikut ini:

(0..9).each { println "Nilai: $it" }

Ok, bagaimana bila saya ingin mencetak nilai index untuk Collection,  misalnya pada kode program Java berikut ini:

List bahasa = new ArrayList();
bahasa.add("Java");
bahasa.add("Groovy");
bahasa.add("C");
bahasa.add("C++");
bahasa.add("PHP");
for (int i=0; i<bahasa.size(); i++) { 
   System.out.println("Index " + i + " adalah " + bahasa.get(i)) 
}

Pada Groovy, saya bisa memakai method eachWithIndex() untuk melakukan hal serupa,  seperti yang terlihat pada kode program berikut ini:

def bahasa = ["Java", "Groovy", "C", "C++", "PHP"] 
bahasa.eachWithIndex { String entry, int i ->
   println "Index $i adalah $entry"
}

Semua telah dilalui tanpa memakai syntax for di Groovy.   Lalu, bagaimana bila saya ingin mengubah nilai yang ada pada Collection tersebut?  Parameter yang dilewatkan oleh each() pastinya tidak dapat dipakai untuk mengubah nilai original, bukan?   Misalnya,  dengan syntax for di Java, saya dapat melakukan hal berikut ini:

List bahasa = new ArrayList();
bahasa.add("Java");
bahasa.add("Groovy");
bahasa.add("C");
bahasa.add("C++");
bahasa.add("PHP");
for (int i=0; i<bahasa.size(); i++) {
   String b = bahasa.get(i);
   bahasa.set(i, "Bahasa " + b);
}
for (String b: bahasa) {
   System.out.println(b);
}

Pada kode program di atas,  saya menambahkan “Bahasa” pada setiap item yang ada di dalam Collection.   Apakah Groovy bisa melakukannya tanpa harus menggunakan syntax for?   Yup, bisa!   Saya bisa menggunakan method collect() yang akan menghasilkan sebuah Collection baru setelah modifikasi setiap itemnya,  seperti yang terlihat pada kode program berikut ini:

def bahasa = ["Java", "Groovy", "C", "C++", "PHP"]
bahasa = bahasa.collect { "Bahasa $it" }
bahasa.each { println it}

Groovy juga menambahkan banyak “gula pemanis” pada Collection sehingga kode program bisa lebih ringkas tanpa kehilangan makna.   Berikut ini adalah contoh “manisan” pada kode program Groovy yang bisa membuat ketagihan:

def bahasa = ["Java", "Groovy", "C", "C++", "PHP"]

// membuang elemen di Collection
bahasa -= ["C++", "PHP"]

// menambah elemen baru di Collection
bahasa += ["Assembly", "C#"]

// mengurutkan elemen
bahasa.sort()

// mencari elemen yang diawakili huruf C atau G
// output:
// C
// C#
// Groovy
bahasa.findAll { it ==~ /[CG].*/ }.each { println it }

// memakai masing-masing item Collection sebagai argumen dengan spread operator
// Output:
// Saya terbiasa memakai Assembly, baru belajar C, tertarik pada C# dan Groovy, sering mendengar Java
printf("Saya terbiasa memakai %s, baru belajar %s, tertarik pada %s dan %s, sering mendengar %s\n", *bahasa)

Melakukan Binding Terhadap ObservableList Di Groovy

Melakukan binding terhadap ObservableList di Groovy dan Griffon terkadang bisa menjebak.  Sebagai contoh, program sederhana berikut terlihat tidak bersalah:

import groovy.swing.SwingBuilder
import javax.swing.*
import java.awt.*
import groovy.beans.Bindable

class Mahasiswa {
    @Bindable String nama   
    @Bindable ObservableList kelas
}

Mahasiswa mahasiswa = new Mahasiswa(nama: "None", kelas: ["A1", "A2"])

new SwingBuilder().edt {
    frame(title: 'Latihan', pack: true, show: true) {
        flowLayout()
        label("Nama")
        textField(text: bind(source: mahasiswa, sourceProperty: 'nama'), columns: 10)
        label("Kelas")
        textField(text: bind(source: mahasiswa, sourceProperty: 'kelas', 
            sourceValue: {mahasiswa.kelas.join(", ")}), columns: 20)                

        button("Ubah", actionPerformed: { 
            mahasiswa.nama = "Solid Snake"
            mahasiswa.kelas << "AX"
        })
    }
}

Akan tetapi bila tombol “Ubah” di-klik, binding untuk ObservableList tidak akan bekerja sesuai dengan harapan, seperti yang ditunjukkan oleh gambar berikut ini:

Contoh Binding Yang Tidak Bekerja

Contoh Binding Yang Tidak Bekerja

Hal ini terjadi karena kode program tersebut melakukan binding terhadap PropertyChangeSupport milik class Mahasiswa yang dihasilkan oleh annotation @Bindable.  Dengan demikian, perubahan pada text field hanya akan terjadi bila variabel kelas diganti dengan sebuah ObservableList yang baru.   Tentunya ini tidak sesuai dengan harapan karena yang diinginkan adalah binding terjadi saat isi ObservableList berubah.

Agar program bekerja, binding harus dilakukan terhadap PropertyChangeSupport milik ObservableList, bukan milik Mahasiswa! Sebagai contoh, saya mengubah kode program di atas menjadi seperti berikut ini:

import groovy.swing.SwingBuilder
import javax.swing.*
import java.awt.*
import groovy.beans.Bindable

class Mahasiswa {
    @Bindable String nama   
    ObservableList kelas
}

Mahasiswa mahasiswa = new Mahasiswa(nama: "None", kelas: ["A1", "A2"])

new SwingBuilder().edt {
    frame(title: 'Latihan', pack: true, show: true) {
        flowLayout()
        label("Nama")
        textField(text: bind(source: mahasiswa, sourceProperty: 'nama'), columns: 10)
        label("Kelas")
        textField(text: bind(source: mahasiswa.kelas, sourceProperty: 'content', 
            sourceValue: {mahasiswa.kelas.join(", ")}), columns: 20)                

        button("Ubah", actionPerformed: { 
            mahasiswa.nama = "Solid Snake"
            mahasiswa.kelas << "AX"
        })
    }
}

Sekarang, program akan bekerja sesuai harapan seperti yang ditunjukkan oleh gambar berikut ini:

Contoh Binding Yang Bekerja

Contoh Binding Yang Bekerja

Memahami Closure Di Groovy

Groovy memungkinkan developer untuk mendefinisikan closure, misalnya seperti pada kode program berikut ini:

class Latihan {
  def sebuahClosure = {
    println "Ini di dalam closure"
  }
}

def latihan = new Latihan()
latihan.sebuahClosure()

// outputnya adalah:
// Ini di dalam closure

Salah satu yang hal yang bisa menjebak adalah fakta bahwa def sebuahClosure adalah sebuah variabel yang menampung closure, bukan sebuah method!  Biar lebih jelas, saya akan menambahkan sebuah method di class tersebut sehingga terlihat seperti pada kode program berikut ini:

class Latihan {
  def sebuahClosure = {
    println "Ini di dalam closure"
  }

  void sebuahMethod() {
    println "Ini di dalam method"
  }
}

def latihan = new Latihan()
latihan.sebuahClosure()
latihan.sebuahMethod()

// outputnya adalah:
// Ini di dalam closure
// Ini di dalam method

Loh, terus apa bedanya sebuah closure dan sebuah method biasa?  Biar jelas, saya akan mengubah definisi class di atas menjadi seperti berikut ini:

class Latihan {
  def sebuahClosure = {
    println "Ini di dalam closure"
  }

  void sebuahMethod() {
    println "Ini di dalam method"
  }

  void proses(Closure argumen) {
    print "PROSES: "
    argumen()
  }
}

def latihan = new Latihan()

Berdasarkan kode program di atas, bila saya memberikan perintah seperti berikut ini:

latihan.proses(latihan.sebuahClosure)

Saya akan memperoleh hasil berupa:  PROSES: Ini di dalam closure. Hal ini memperlihatkan bahwa saya dapat melewatkan sebuah closure sebagai argumen dalam sebuah method karena closure tersebut di-“simpan” oleh sebuah variabel.

Tetapi bila saya memberikan perintah berikut ini:

latihan.proses(latihan.sebuahMethod)

Saya akan mendapatkan pesan kesalahan!!!  Apakah tidak bisa melewatkan sebuah method sebagai argumen?  Bisa, tetapi harus dalam bentuk seperti berikut ini:

latihan.proses(latihan.&sebuahMethod)

Bagaimana bila sebuah closure memiliki parameter?  Saya dapat menggunakan operator -> untuk memberikan parameter pada closure.   Hal ini terlihat pada kode program berikut ini:

def tambah = { nilai1, nilai2 ->
  println "${nilai1} + ${nilai2} = ${nilai1+nilai2}"
}

def kurang = { nilai1, nilai2 ->
  println "${nilai1} - ${nilai2} = ${nilai1-nilai2}"
}

void proses(nilai1, nilai2, Closure argumen) {
  print "PROSES: "
  argumen(nilai1, nilai2)
}

proses(10, 20, tambah)
proses(40, 30, kurang)

// Hasilnya adalah:
// PROSES: 10 + 20 = 30
// PROSES: 40 - 30 = 10

Ok, saya akan mengingat ini bila suatu saat nanti saya menemukan kode program dengan operator ->.

Btw, saya juga sering menemukan apa yang disebut dengan inline closure.  Melanjutkan dari kode program di atas, saya menambah baris seperti berikut ini:

proses(10, 20, tambah)
proses(40, 30, kurang)
proses(50, 60) {  nilai1, nilai2 ->
  println("${nilai1} * ${nilai2} = ${nilai1*nilai2}"
}

// Hasilnya adalah:
// PROSES: 10 + 20 = 30
// PROSES: 40 - 30 = 10
// PROSES: 50 * 60 = 3000

Bila parameter terakhir adalah sebuah Closure, saya dapat langsung memberikan definisi closure tersebut setelah pemanggilan method.

Berbekal pemahaman berdasarkan contoh di atas, saya akhirnya bisa memahami contoh kode program Griffon yang awalnya seolah penuh blok ajaib, misalnya yang terlihat berikut ini:

void mvcGroupInit(Map args) {
  ...
  execOutsideUI {
     String text = model.loadedFile.text
     execInsideUIAsync {
        model.fileText = text
     }
  }
}

Memahami Cara Kerja Builder Di Groovy

Saya akan mengawali tulisan pertama di tahun 2013 ini dengan topik yang belum pernah saya bahas sebelumnya yaitu bahasa pemograman Groovy.  Bahasa pemograman Groovy adalah sebuah bahasa pemograman yang berjalan di atas platform Java.  Dengan demikian, Groovy dapat memakai semua class yang sudah ada di Java.   Hal ini akan sangat membantu, karena Java memiliki sangat banyak API yang beragam; sebuah bahasa yang baru lahir mustahil memiliki daftar class selengkap Java yang teruji selama bertahun-tahun (baik yang resmi maupun oleh komunitas open-source).  Sebenarnya tidak hanya Groovy yang dapat memanggil class Java, JRuby (implementasi bahasa pemograman Ruby di Java) dan Jython (implementasi bahasa pemograman Python di Java) juga dapat melakukannya.

Kelebihan Groovy dibanding bahasa pemograman Java adalah syntax-nya yang lebih sederhana sehingga coding bisa lebih ringkas.  Tapi bukan hanya itu saja, Groovy juga memiliki fitur yang tidak ada di Java seperti GString (seperti String di PHP), operator untuk regex seperti di Perl,  mendukung Invoke Dynamic Java 7 (memanggil method tanpa perlu mengetahui apa method tersebut), dan sebagainya.

Salah fitur Groovy adalah ia memiliki Builder yang menerapkan Builder pattern.  Sebagai contoh, saya bisa menghasilkan XML secara singkat dengan memakai class MarkupBuilder, seperti yang terlihat pada kode program Groovy berikut ini:

def writer = new StringWriter()
def builder = new groovy.xml.MarkupBuilder(writer)
builder.daftar {
    mahasiswa(nim: "99887766", nama: "Sandu Kosaso") {
        matakuliah(nama: "data structure", nilai: "D")
        matakuliah(nama: "character building", nilai: "E")
        matakuliah(nama: "business plan", nilai: "A")
    }
    mahasiswa(nim: "99887755", nama: "Ide O") {
        matakuliah(nama: "java programming", nilai: "A")
        matakuliah(nama: "business plan", nilai: "B")
        matakuliah(nama: "system architecture", nilai: "C")
    }
}
print writer

Hasilnya adalah sebuah XML yang terlihat seperti berikut ini:

<daftar>
  <mahasiswa nim='99887766' nama='Sandu Kosaso'>
    <matakuliah nama='data structure' nilai='D' />
    <matakuliah nama='character building' nilai='E' />
    <matakuliah nama='business plan' nilai='A' />
  </mahasiswa>
  <mahasiswa nim='99887755' nama='Ide O'>
    <matakuliah nama='java programming' nilai='A' />
    <matakuliah nama='business plan' nilai='B' />
    <matakuliah nama='system architecture' nilai='C' />
  </mahasiswa>
</daftar>

Kode program ini benar-benar sangat singkat bila dibandingkan dengan cara yang harus ditempuh melalui Java.

Tapi pertanyaannya adalah kenapa bisa demikian?  Tidak ada method daftar di MarkupBuilder, bukan? Juga tidak ada definisi method mahasiswa dan matakuliah, bukan?  Lalu kenapa bisa jadi XML??

Hal ini berkaitan dengan apa yang disebut dengan Meta Object Protocol (MOP) yang memungkinkan programmer untuk mengubah atau menambah method pada class secara dinamis.   Setiap class milik Groovy selalu mengimplementasikan interface groovy.lang.GroovyObject.   Terdapat juga sebuah class GroovyObjectSupport sebagai class utility yang mengimplementasikan interface GroovyObjectSupport.   Sebagai contoh, class MarkupBuilder diturunkan dari BuilderSupport yang diturunkan dari GroovyObjectSupport. UML Class Diagram berikut ini memperlihatkan struktur class yang ada:

Hierari Class MarkupBuilder

Hierari Class MarkupBuilder

Jika pada saat program dikerjakan, sebuah method yang dipanggil tidak ditemukan dalam definisi class tersebut, maka method getProperty(), setProperty(), atau invokeMethod() milik GroovyObject tersebut akan dikerjakan.

Dengan demikian, pada kode program saya, method daftar(), mahasiswa(), dan matakuliah() yang pada dasarnya tidak ada di MarkupBuilder, akan menyebabkan method invokeMethod() di class BuilderSupport dikerjakan,  yang dimana selanjutnya akan memanggil method createNode()untuk membentuk XML.

Dengan MOP, saya dapat mengubah perilaku sebuah object pada saat program dijalankan.  Proses membuat kode program di sebuah bahasa pemograman agar bahasa pemograman tersebut berperilaku sesuai dengan yang saya harapkan disebut sebagai metaprogramming.

Hal yang membedakan metaprogramming dan membuat bahasa pemograman baru adalah pada metaprogramming, saya memakai kemampuan MOP dari bahasa pemograman tersebut untuk mengubah perilaku bahasa pemograman itu sendiri dimana perubahan perilaku ini akan terjadi setelah program dijalankan.

Hasil akhir metaprogramming bisa saja membentuk sebuah bahasa pemograman mini yang spesifik sesuai kebutuhan dan nantinya dipakai oleh metaprogrammer.  Bahasa seperti ini disebut sebagai Domain-specific Language(DSL).