Belajar Membuat Dialog Login Di Griffon

Pada panduan sederhana ini, saya akan membuat sebuah aplikasi desktop yang menampilkan dialog login. Program baru akan ditampilkan bila dialog di-isi dengan nama pengguna dan password yang benar. Untuk keperluan tersebut, saya akan menggunakan Griffon 1.5 dan JXLoginPane dari SwingX. Untuk melakukan akses database, saya akan memakai simple-jpa 0.6. Karena simple-jpa sudah menyertakan SwingX builder, saya tidak perlu men-install swingx-builder lagi.

Langkah pertama yang saya lakukan adalah membuat sebuah proyek baru dengan perintah berikut ini:

C:\> griffon create-app login
C:\> cd login
C:\login> _

Setelah proyek baru dibuat, saya akan men-install plugin simple-jpa 0.6 dengan menggunakan perintah berikut ini:

C:\login> griffon install-plugin simple-jpa 0.6

Saya perlu menyiapkan koneksi ke sebuah database MySQL di komputer lokal dengan memberikan perintah berikut ini:

C:\login> griffon create-simple-jpa -user=snake -password=12345 -database=latihanLogin -rootPassword=admin12345

Bila ingin memakai database yang sudah ada, saya perlu menambahkan perintah -skip-database pada perintah di atas.

Karena terbiasa memakai IDE, saya akan membuat proyek IntelliJ IDEA dengan memberikan perintah berikut ini:

C:\login> griffon integrate-with --idea

Setelah ini, saya membuka file proyek yang dihasilkan dengan memilih menu File, Open… di IntelliJ IDEA.

Saya kemudian membuka file LoginController.groovy dan mengubahnya menjadi seperti berikut ini:

package login

import domain.DatabaseLoginService
import org.jdesktop.swingx.JXLoginPane

class LoginController {

    def view

    void mvcGroupInit(Map args) {
        DatabaseLoginService loginService = new DatabaseLoginService()
        loginService.buatUserDefault()
        execInsideUISync {
            JXLoginPane panel = new JXLoginPane(loginService)
            JXLoginPane.Status status = JXLoginPane.showLoginDialog(app.windowManager.getStartingWindow(), panel)
            if (status != JXLoginPane.Status.SUCCEEDED) {
                app.shutdown()
            }
        }
    }

}

Pada kode program di atas, saya memakai JXLoginPane dari SwingX. Selain itu, saya menggunakan app.windowManager.getStartingWindow() untuk mendapatkan JFrame yang biasanya ditampilkan pertama kali (sesuai dengan konfigurasi di Application.groovy).

Berikutnya, saya perlu membuat kode program untuk DatabaseLoginService yang akan memeriksa apakah login sukses atau tidak berdasarkan informasi yang tersimpan di database. Ini adalah kanditat yang tepat untuk services. Griffon mendukung application services dengan perintah create-service. Di Griffon, services adalah artifact sama seperti controller. Akan tetapi, agar sederhana, saya tidak akan menggunakan fasilitas dari Griffon tersebut. Saya akan menganggap DatabaseLoginService sebagai domain service (bila mengikuti domain driven design, domain juga memiliki services). Untuk itu, saya membuat file src\main\domain\DatabaseLoginService.groovy yang isinya seperti berikut ini:

package domain

import org.jdesktop.swingx.auth.LoginService
import simplejpa.transaction.Transaction

@Transaction
class DatabaseLoginService extends LoginService {

    @Override
    boolean authenticate(String nama, char[] password, String server) throws Exception {
        return false
    }

    void buatUserDefault() {}

}

Setelah itu, saya menambahkan import domain.DatabaseLoginService pada file LoginController.groovy.

Sampai disini, bila saya menjalankan kode program, saya akan memperoleh tampilan seperti pada gambar berikut ini:

Tampilan Dialog Login

Tampilan Dialog Login

Bila saya mengisi dengan nama pengguna atau password yang saya, saya akan memperoleh tampilan seperti pada gambar berikut ini:

Tampilan Dialog Login Bila Terjadi Kesalahan

Tampilan Dialog Login Bila Terjadi Kesalahan

Apapun yang saya isi, login tidak akan pernah sukses, karena saat ini method DatabaseLoginService.authenticate() selalu mengembalikan nilai false.

Saya akan membuat sebuah domain class yang mewakili pengguna. Class ini akan saya beri nama Pengguna. Untuk itu, saya memilih menu Tools, Griffon, Run Target, lalu mengisinya dengan:

create-domain-class Pengguna

Setelah itu, saya mengubah kode program Pengguna.groovy yang dihasilkan menjadi seperti berikut ini:

package domain

import groovy.transform.*
import simplejpa.DomainClass
import javax.persistence.*
import javax.validation.constraints.*
import org.hibernate.validator.constraints.*
import java.security.MessageDigest

@DomainClass @Entity @Canonical
class Pengguna {

    @NotBlank @Size(min=2, max=50)
    String nama

    @NotNull
    byte[] passwordHash

    private byte[] getMD5(String plain) {
        MessageDigest digester = MessageDigest.getInstance('MD5')
        digester.update(plain.bytes)
        digester.digest()
    }

    void setPassword(String password) {
        passwordHash = getMD5(password)
    }

    boolean login(String password) {
        if (password.isAllWhitespace()) return false 
        Arrays.equals(passwordHash, getMD5(password))
    }

}

Pada class di atas, saya tidak menyimpan password secara langsung melainkan hanya MD5 hash-nya saja. Walaupun tidak sangat aman, menyimpan dalam bentuk MD5 hash jauh lebih baik daripada menyimpan password apa adanya. Hal ini karena orang yang melihat isi tabel secara langsung tetap tidak bisa mengetahui apa password yang harus diketik. Saya memilih menyimpan passwordHash sebagai byte[]. Secara default, Hibernate JPA akan memetakan byte[] dengan sebuah kolom BLOB di tabel MySQL.

Untuk memastikan tidak ada yang salah pada class di atas, saya akan membuat sebuah unit test dengan memberikan perintah berikut ini:

create-unit-test Pengguna

Saya kemudian mengisi file PenggunaTests yang dihasilkan menjadi seperti berikut ini:

package login

import domain.Pengguna
import griffon.test.*

class PenggunaTests extends GriffonUnitTestCase {

    protected void setUp() {
        super.setUp()
    }

    protected void tearDown() {
        super.tearDown()
    }

    void testLogin() {
        Pengguna pengguna = new Pengguna(nama: 'Solid')
        pengguna.setPassword('Snake')

        // Kasus password benar
        assertTrue(pengguna.login('Snake'))

        // Kasus password salah
        assertFalse(pengguna.login('Liquid Snake'))
        assertFalse(pengguna.login(''))
    }
}

Saya kemudian menghapus file LoginControllerTests.groovy dan LoginModelTests.groovy (yang dihasilkan oleh Griffon) karena mereka tidak berisi test case dan tidak diperlukan pada proyek latihan ini.

Berikutnya saya perlu menjalankan unit test. Untuk itu, saya memberikan perintah berikut ini:

test-app unit:

Bila semuanya sesuai dengan harapan, saya akan harus memperoleh hasil seperti berikut ini:

...
-------------------------------------------------------
Running 1 unit test...
Running test login.PenggunaTests...PASSED
Tests Completed in 562ms ...
-------------------------------------------------------
Tests passed: 1
Tests failed: 0
-------------------------------------------------------

Setelah yakin bahwa kode program Pengguna sudah dapat berfungsi sebagaimana seharusnya, kini saat yang tepat untuk mengubah kode program DatabaseLoginService menjadi seperti berikut ini:

package domain

import org.jdesktop.swingx.auth.LoginService
import simplejpa.transaction.Transaction

@Transaction
class DatabaseLoginService extends LoginService {

    @Override
    boolean authenticate(String nama, char[] password, String server) throws Exception {
        Pengguna pengguna = findPenggunaByNama(nama)
        pengguna?.login(String.valueOf(password))

    }

    void buatUserDefault() {
        if (!findPenggunaByNama('Solid')) {
            Pengguna me = new Pengguna(nama: 'Solid')
            me.password = 'Snake'
            persist(me)
        }
    }

}

Pada kode program di atas, method authenticate() akan dipanggil untuk memeriksa apakah password benar atau salah. Selain itu, juga ada method buatUserDefault() yang akan membuat sebuah pengguna dengan nama dan password default (bila belum ada) sehingga setidaknya saya bisa login ke aplikasi walaupun database masih kosong.

Sekarang, saya dapat mencoba menjalankan aplikasi. Pada proyek latihan ini, saya bisa masuk ke dialog utama hanya bila mengisi nama pengguna dengan Solid dan password berupa Snake.

Iklan

Menggunakan DateTime Di simple-jpa

simple-jpa 0.2.1 memiliki komponen DateTimePicker.  Apa kegunaannya?

Menyimpan dan men-query tipe data tanggal sekilas terlihat gampang, tapi kerap menimbulkan permasalahan pada prakteknya.   Hal ini karena Date di Java menyimpan tanggal  dan jam secara bersamaan.  Sebagai contoh, saya memiliki sebuah tabel dengan data bertipe tanggal seperti yang terlihat berikut ini:

mysql> select * from tagihan;
+------+---------------------+
| kode | jatuhTempo          |
+------+---------------------+
|    1 | 2013-04-11 10:00:00 |
|    2 | 2013-04-11 12:00:00 |
|    3 | 2013-04-12 09:00:00 |
|    4 | 2013-04-12 00:00:00 |
+------+---------------------+

Pada tabel di atas, nilai jatuhTempo mewakili tanggal jatuh tempo yang dipilih oleh pengguna melalui sebuah picker atau di-isi secara otomatis melalui kode program.  Bila tidak hati-hati, nilai jam secara otomatis juga akan ikut terisi (biasanya sesuai dengan jam saat pengguna memilih tanggal).

Apa akibatnya bila nilai jam ikut terisi?

Berikut ini adalah sebuah SQL yang ‘polos’ untuk mengambil seluruh tagihan yang jatuh tempo pada tanggal 2013-04-11:

mysql> select * from tagihan where jatuhTempo = '2013-04-11';
Empty set (0.00 sec)

Kenapa tidak ada hasil yang dikembalikan?  Karena query tersebut hanya mencari tagihan yang jatuh tempo pada waktu 2013-04-11 00:00:00.  Hal ini cukup menjebak dan bisa menjadi sumber kesalahan bila developer tidak berhati-hati.  Bila query dilakukan secara lengkap berdasarkan tanggal dan jam, maka hasil akan dikembalikan dengan baik, seperti berikut ini:

mysql> select * from tagihan where jatuhTempo = '2013-04-11 12:00:00';
+------+---------------------+
| kode | jatuhTempo          |
+------+---------------------+
|    2 | 2013-04-11 12:00:00 |
+------+---------------------+
1 row in set (0.00 sec)

Contoh ketidak-konsistenan lainnya adalah bila pengguna memilih untuk melihat tagihan yang jatuh tempo setelah tanggal 2013-04-11, perintah SQL akan menampilkan hasil seperti berikut ini:

mysql> select * from tagihan where jatuhTempo > '2013-04-11';
+------+---------------------+
| kode | jatuhTempo          |
+------+---------------------+
|    1 | 2013-04-11 10:00:00 |
|    2 | 2013-04-11 12:00:00 |
|    3 | 2013-04-12 09:00:00 |
|    4 | 2013-04-12 00:00:00 |
+------+---------------------+
4 rows in set (0.00 sec)

Bukankah harusnya hanya tanggal 12 saja yang disertakan?  Tapi yang dikerjakan adalah seluruh tagihan yang jatuh tempo setelah tanggal 2013-04-11 00:00:00 tengah malam (tagihan pada tanggal 2013-04-11 yang jam-nya bukan 00:00:00 akan ditampilkan)! Demikian juga bila pengguna menampilkan tagihan yang jatuh tempo setelah tanggal 2013-04-12, ia akan memperoleh hasil seperti berikut ini:

mysql> select * from tagihan where jatuhTempo > '2013-04-12';
+------+---------------------+
| kode | jatuhTempo          |
+------+---------------------+
|    3 | 2013-04-12 09:00:00 |
+------+---------------------+
1 row in set (0.00 sec)

Akan terasa aneh, bukan?

Untuk menghindari permasalahan seperti di atas, simple-jpa memakai Joda Time.  Komponen DateTimePicker mendukung object Joda Time berikut ini:

  • LocalDate – hanya berisi tanggal tanpa ada informasi jam.  Pada contoh di atas, jatuhTempo merupakan kandidat yang tepat untuk menggunakan LocalDate.
  • LocalTime – hanya berisi jam tanpa informasi tanggal.  Bisa digunakan pada atribut seperti batasJamTerlambat, jamMulaiLembur, dsb.
  • LocalDateTime – berisi informasi tanggal dan jam.  Bisa digunakan pada atribut seperti jamMasukKerja (berisi tanggal dan jam saat karyawan absensi masuk setiap harinya),  tanggalDaftar (tanggal plus jam pendaftaran), dsb.
  • DateTime – mirip seperti LocalDateTime tetapi menyertakan informasi time zone sehingga waktu dapat ditampilkan sesuai dengan time zone masing-masing pengguna.

Untuk melihat perbedaan masing-masing object di atas, saya akan membuat sebuah proyek sederhana yang memakai simple-jpa 0.2.1. Saya akan membuat domain class dengan nama Latihan yang terlihat seperti berikut ini:

package domain

import ...

@DomainModel @Entity
@TupleConstructor @ToString
class Latihan {

    @Size(min=2, max=50)
    String nama

    @Type(type="org.jadira.usertype.dateandtime.joda.PersistentLocalDate")
    LocalDate tanggal

}

Pada domain class di atas, saya wajib menyertakan @Type karena LocalDate bukanlah tipe data yang di-dukung secara native oleh Hibernate JPA.  simple-jpa memakai Jadira User Type yang menyediakan class PersistentLocalDate, PersistentLocalTime, PersistentLocalDateTime, dan PersistentDateTime untuk men-konversi setiap object Joda Time ke dalam field database (dan sebaliknya).  Bila saya memakai perintah generate-all, pada view akan terdapat baris seperti berikut ini:

dateTimePicker(id: 'tanggal', localDate: bind('tanggal', target: model, mutual: true), 
   errorPath: 'tanggal', dateVisible: true, timeVisible: false)

Binding dilakukan terhadap nilai property localDate.  Selain itu, timeVisible diberi nilai false sehingga JSpinner untuk memilih jam tidak akan ditampilkan.  Tampilannya akan terlihat seperti berikut ini:

Tampilan DateTimePicker Untuk LocalDate

Tampilan DateTimePicker Untuk LocalDate

Bila saya melihat isi tabel, data akan tersimpan seperti berikut ini:

mysql> select tanggal from latihan;
+------------+
| tanggal    |
+------------+
| 2013-04-11 |
| 2013-04-14 |
| 2013-04-23 |
+------------+

Selanjutnya, saya akan mengubah definisi tanggal di file Latihan.groovy menjadi seperti berikut ini:

@Type(type="org.jadira.usertype.dateandtime.joda.PersistentLocalTime")
LocalTime tanggal

Setiap perubahan pada domain class umumnya juga akan mempengaruhi view model (atau disebut hanya model).  Agar tidak bingung, anggap saja domain class berhubungan dengan keseluruhan business logic aplikasi yang dapat dipakai dimana saja, sementara view model dipakai untuk mewakili data pada view yang sedang aktif.  Karena setiap form umumnya menampilkan atribut sebuah domain class, maka biasanya atribut di domain class juga ada di view model.

Perubahan bisa disesuaikan secara otomatis oleh perintah generate-all, akan tetapi kali ini saya akan mencoba mengubah secara manual tanpa melalui perintah tersebut.  Saya perlu mengubah file LatihanModel.groovy yang mendefinisikan tanggal menjadi seperti berikut ini:

@Bindable LocalTime tanggal

Lalu, saya juga mengubah definisi tanggal di view sehingga terlihat menjadi seperti berikut ini:

dateTimePicker(id: 'tanggal', localTime: bind('tanggal', target: model, mutual: true), 
   errorPath: 'tanggal', dateVisible: true, timeVisible: false)

Bila saya menjalankan aplikasi, sekarang akan terlihat seperti pada gambar berikut ini:

Tampilan DateTimePicker Untuk LocalTime

Tampilan DateTimePicker Untuk LocalTime

Selanjutnya, akan kembali mengubah Latihan.groovy sehingga definisi tanggal menjadi seperti berikut ini:

@Type(type="org.jadira.usertype.dateandtime.joda.PersistentLocalDateTime")
LocalDateTime tanggal

Lalu, saya mengubah LatihanModel.groovy pada definisi tanggal menjadi seperti berikut ini:

@Bindable LocalDateTime tanggal

Terakhir, saya mengubah definisi di view menjadi seperti berikut ini:

dateTimePicker(id: 'tanggal', localDateTime: bind('tanggal', target: model, mutual: true), 
   errorPath: 'tanggal', dateVisible: true, timeVisible: true)

Bila saya menjalankan aplikasi, tampilan akan terlihat seperti berikut ini:

Tampilan DateTimePicker Untuk LocalDateTime

Tampilan DateTimePicker Untuk LocalDateTime

Bila saya melihat data yang tersimpan di tabel,  saya akan menemukan seperti berikut ini:

mysql> select tanggal from latihan;
+---------------------+
| tanggal             |
+---------------------+
| 2013-04-02 20:14:00 |
| 2013-04-30 16:22:00 |
| 2013-04-16 22:11:00 |
+---------------------+

Berikutnya, saya akan melihat perbedaan antara LocalDateTime dan DateTime.  Saya akan mulai dengan mengubah definisi tanggal di Latihan.groovy menjadi seperti berikut ini:

@Type(type="org.jadira.usertype.dateandtime.joda.PersistentDateTime")
DateTime tanggal

Setelah itu, saya mengubah file LatihanModel.groovy yang mendefinisikan tanggal menjadi seperti berikut ini:

@Bindable DateTime tanggal

Lalu, saya mengubah file LatihanView.groovy yang mendefinisikan tanggal menjadi seperti berikut:

dateTimePicker(id: 'tanggal', dateTime: bind('tanggal', target: model, mutual: true), errorPath: 'tanggal')

Saya boleh menghilangkan bagian yang mengubah property dateVisible dan timeVisible menjadi true karena secara default kedua nilai tersebut adalah true.

Pada saat dijalankan, aplikasi akan terlihat seperti berikut ini:

Tampilan DateTimePicker Untuk Data DateTime

Tampilan DateTimePicker Untuk Data DateTime

Secara sekilas, tidak ada yang terlihat berbeda dengan LocalDateTime (kecuali ada tambahan +7 yang menunjukkan informasi time zone).  Akan tetapi, bila saya melihat data yang tersimpan di database, saya akan menemukan seperti berikut ini:

mysql> select tanggal from latihan;
+---------------------+
| tanggal             |
+---------------------+
| 2013-04-01 05:33:00 |
| 2013-04-08 07:33:00 |
| 2013-04-30 13:44:00 |
+---------------------+

Kenapa data yang tersimpan berbeda jauh dengan data yang ditampilkan?  Karena Jadira User Type akan men-konversi DateTime dan menyimpannya di database dalam timezone UTC (+0).  Saat sebuah aplikasi menbaca data DateTime, ia akan menerjemahkan data dari database yang dalam UTC  ke time zone yang dipakai oleh aplikasi.  Bila Jadira User Type tidak menyimpan data dalam bentuk UTC, maka proses menerjemahkan kembali tanggal sesuai dengan time zone pengguna akan menjadi kompleks.

Sekarang, saya akan mensimulasikan seorang pengguna dari time zone berbeda, misalnya seorang pengguna dari time zone berbeda dengan cara mengubah Time Zone setting di komputer saya.  Tetap berdasarkan data yang sama (saya tidak menghapus data yang sebelumnya ada di tabel!), saat saya menjalankan aplikasi, saya akan memperoleh tampilan seperti berikut ini:

Tanggal yang sama di TimeZone yang berbeda

Tanggal yang sama di TimeZone yang berbeda

Untuk memeriksa apakah nilai yang ditampilkan adalah benar, saya akan menggunakan rumus konversi time zone pada baris pertama seperti berikut ini:

"time in zone A" − "UTC offset for zone A" = "time in zone B" − "UTC offset for zone B"
2013-04-01T12:33:00 - (+07:00) = 2013-04-01T07:33:00 - (+02:00)
2013-04-01T05:33:00 = 2013-04-01T05:33:00

Dengan menggunakan DateTime, tanggal dan jam dapat ditampilkan dengan benar sesuai dengan time zone yang dipakai oleh komputer yang menjalankan aplikasi (atau time zone yang diatur untuk Java). Hal ini yang membedakan antara DateTime dan LocalDateTime.

Memakai SwingXBuilder Di Griffon: Part 3

JXStatusBar

JXStatusBar adalah komponen yang mewakili status bar yang berada di baris paling bawah dari sebuah container. Berikut ini adalah contoh kode program yang memakai JXStatusBar:

import org.jdesktop.swingx.JXSearchField
import static org.jdesktop.swingx.JXStatusBar.*
import org.jdesktop.swingx.JXStatusBar

application(title: 'Komponen SwingX',  
  id: 'frameUtama',
  preferredSize: [320, 240],
  pack: true,
  locationByPlatform:true,    
  iconImage: imageIcon('/griffon-icon-48x48.png').image) {

  	statusBar() {
	  label("Selamat datang di aplikasi saya.", constraints: 
		  new Constraint(Constraint.ResizeBehavior.FILL))
	  widget(new JXSearchField(), constraints: new Constraint(200))
	  jxbusyLabel(busy: true)
	}
}

Tampilan program di atas dapat dilihat pada gambar berikut ini:

JXStatusBar

JXStatusBar

JXTable

JXTable adalah sebuah JTable yang dengan beberapa fitur tambahan seperti rollover, highlighting, dan pencarian. Berikut ini adalah contoh kode program yang memakai fitur pencarian dari JXTable:

application(title: 'Komponen SwingX',  
  id: 'frameUtama',
  preferredSize: [320, 240],
  pack: true,
  locationByPlatform:true,    
  iconImage: imageIcon('/griffon-icon-48x48.png').image) {

  	borderLayout()
	scrollPane(constraints: CENTER) {
		jxtable(id: "tabel") {
			tableModel(list:[[nim: "1122334455", nama: "Solid", nilai: "A"],
					 [nim: "1122334466", nama: "Snake", nilai: "B"],
					 [nim: "1122334477", nama: "Way",   nilai: "C"]]) {
				propertyColumn(header: "NIM", propertyName: "nim")
				propertyColumn(header: "Nama", propertyName: "nama")
				propertyColumn(header: "Nilai", propertyName: "nilai")
			}						
	  	}
  	}
	jxfindBar(searchable: tabel.getSearchable(), constraints: PAGE_START)
}

Tampilan kode program di atas akan terlihat seperti:

JXTable

JXTable

JXTaskPane

JXTaskPane memakai JXCollapsiblePane yang telah dilengkapi dengan title untuk menyembunyikan dan menampilkan sebuah JXCollapsiblePane. Berikut ini adalah contoh kode program yang membuat JXTaskPane:

application(title: 'Komponen SwingX',  
  id: 'frameUtama',
  preferredSize: [320, 240],
  pack: true,
  locationByPlatform:true,    
  iconImage: imageIcon('/griffon-icon-48x48.png').image) {

  	borderLayout()
  	jxtaskPaneContainer(constraints: CENTER) {
	  jxtaskPane(title: "JXTaskPane #1") {
		 label("Ini adalah JXTaskPane #1")  
	  }
	  jxtaskPane(title: "JXTaskPane #2") {
		 label("Ini adalah JXTaskPane #2")  
	  }
	  jxtaskPane(title: "JXTaskPane #3", special: true) {
		  label("Yang ini spesial!")
	  }
  	}
}

Tampilan kode program di atas akan terlihat seperti pada gambar berikut ini:

JXTaskPane

JXTaskPane

JXTitledPanel

JXTitledPanel adalah sebuah JXPanel yang telah dilengkapi judul. Berikut ini adalah contoh kode program yang membuat JXPanel:

application(title: 'Komponen SwingX',  
  id: 'frameUtama',
  preferredSize: [320, 240],
  pack: true,
  locationByPlatform:true,    
  iconImage: imageIcon('/griffon-icon-48x48.png').image) {

    borderLayout()
  	jxtitledPanel(title: "Judul", leftDecoration: button(text: "MENU"),
           rightDecoration: jxbusyLabel(busy:true)) {
       label("Isi JXTitledPanel!")
    }
}

Tampilan dari kode program di atas akan terlihat seperti pada gambar berikut ini:

JXTitledPanel

JXTitledPanel

JXTitledSeparator

JXTitledSeparator adalah sebuah JXPanel yang dapat dipakai sebagai pemisah (bisa dilengkapi judul dan icon). Berikut ini adalah contoh kode program yang memakai JXTitledSeparator:

import javax.swing.SwingConstants;

application(title: 'Komponen SwingX',  
  id: 'frameUtama',
  preferredSize: [320, 240],
  pack: true,
  locationByPlatform:true,    
  iconImage: imageIcon('/griffon-icon-48x48.png').image) {

    borderLayout()
    titledSeparator(title: "Sebuah Pemisah", 
      horizontalAlignment: SwingConstants.CENTER)

}

Tampilannya akan terlihat seperti pada gambar berikut ini:

JXTitledSeparator

JXTitledSeparator

JXTree

JXTree adalah sebuah JTree yang dilengkapi beberapa fitur tambahan seperti highlightning dan pencarian. Berikut ini adalah contoh kode program yang menampilkan isi folder dalam bentuk JXTree dan disertai dengan fitur pencarian:

import org.jdesktop.swingx.treetable.FileSystemModel

application(title: 'Komponen SwingX',  
  id: 'frameUtama',
  preferredSize: [320, 240],
  pack: true,
  locationByPlatform:true,    
  iconImage: imageIcon('/griffon-icon-48x48.png').image) {

    borderLayout()
    jxtree(id: "tree", model: new FileSystemModel(
        new File("C:\\Users\\Public\\Pictures\\Sample Pictures")), constraints: CENTER)
    jxfindBar(searchable: tree.getSearchable(), constraints: PAGE_START)

}

Tampilan kode program di atas akan terlihat seperti pada gambar berikut ini:

JXTree

JXTree

JXTreeTable

JXTreeTable adalah sebuah JXTable yang didalamnya terdapat JXTree. Untuk menampilkan JXTreeTable, saya membuat kode program seperti berikut ini:

import org.jdesktop.swingx.treetable.FileSystemModel

application(title: 'Komponen SwingX',  
  id: 'frameUtama',
  preferredSize: [320, 240],
  pack: true,
  locationByPlatform:true,    
  iconImage: imageIcon('/griffon-icon-48x48.png').image) {

    borderLayout()
    scrollPane(constraints: CENTER) {
        jxtreeTable(id: "treeTable", treeTableModel: new FileSystemModel(
           new File("C:\\Users\\Public\\Pictures\\Sample Pictures")))
    }
    jxfindBar(searchable: treeTable.getSearchable(), constraints: PAGE_START)

}

Tampilan dari kode program di atas akan terlihat seperti pada gambar berikut ini:

JXTreeTable

JXTreeTable

Memakai SwingXBuilder Di Griffon: Part 2

JXHeader

JXHeader adalah sebuah komponen untuk menampilkan judul, keterangan dan icon. Berikut ini adalah contoh kode program yang memakai JXHeader:

application(title: 'Komponen SwingX',  
  id: 'frameUtama',
  preferredSize: [320, 240],
  pack: true,
  locationByPlatform:true,    
  iconImage: imageIcon('/griffon-icon-48x48.png').image) {

  	flowLayout()
  	jxheader(title: "The Solid Snake", description: "Catatan Harian Seorang Developer")
}

Tampilannya akan terlihat seperti pada gambar berikut ini:

JXHeader

JXHeader

JXImageView

JXImageView dapat dipakai untuk menampilkan gambar dimana pengguna bisa men-drag gambar yang berada di dalam JXImageView. JXImageView juga menyediakan method setScale() untuk zooming gambar. Berikut ini adalah contoh kode program yang memakai JXImageView:

def zoom = 0.2

application(title: 'Komponen SwingX',  
  id: 'frameUtama',
  preferredSize: [320, 240],
  pack: true,
  locationByPlatform:true,    
  iconImage: imageIcon('/griffon-icon-48x48.png').image) {

  	borderLayout()
	jximageView(constraints: CENTER, id: "imageView").with {
		setImage(new File("C:/Users/Public/Pictures/Sample Pictures/Koala.jpg"))
		setScale(zoom)
  	}		
  	jxpanel(constraints: PAGE_START) {
	  	jxbutton(text: "Perbesar",  actionPerformed: { 
			  zoom = [10, zoom+0.01].min(); 
			  imageView.setScale(zoom); 
		})
		jxbutton(text: "Perkecil", actionPerformed: { 
			  zoom = [-10, zoom-0.01].max();
			  imageView.setScale(zoom);
		})
	}

}

Tampilannya akan terlihat seperti pada gambar berikut ini:

JXImageView

JXImageView

JXList

Pada dasarnya JXList adalah sebuah JList yang ditambah dengan berbagai fitur SwingX seperti rollover, highlighter dan searchable. Berikut ini adalah contoh kode program yang memakai JXList:

import java.awt.Color
import javax.swing.border.TitledBorder
import org.jdesktop.swingx.*
import org.jdesktop.swingx.decorator.*

application(title: 'Komponen SwingX',  
  id: 'frameUtama',
  preferredSize: [320, 240],
  pack: true,
  locationByPlatform:true,    
  iconImage: imageIcon('/griffon-icon-48x48.png').image) {

  	borderLayout()

  	jxlist(new JXList(['The', 'Solid', 'Snake', 'Blog'].toArray()), id: 'list', 
		  constraints: CENTER, border: new TitledBorder("Pilih:")).with {
	  setRolloverEnabled(true)
	  addHighlighter(new ColorHighlighter(HighlightPredicate.ROLLOVER_ROW,
	  	null, Color.GREEN))
	}

	jxfindBar(new JXFindBar(list.getSearchable()), constraints: PAGE_START)

}

Tampilan kode program di atas akan terlihat seperti pada gambar berikut ini:

JXList

JXList

JXLoginPane

Sesuai dengan namanya, JXLoginPane dipakai untuk menampilkan dialog login. Berikut ini adalah contoh kode program yang menampilkan dialog login:

import org.jdesktop.swingx.*
import org.jdesktop.swingx.auth.LoginService
import org.jdesktop.swingx.decorator.*

JXLoginPane panel = new JXLoginPane(controller)
JFrame frame = JXLoginPane.showLoginFrame(panel)
frame.setVisible(true)

JXLoginPane akan secara otomatis memanggil kode yang memeriksa apakah data login yang dimasukkan pengguna benar atau salah. Oleh sebab itu, saya perlu memberikan kode program seperti berikut ini di controller:

import org.jdesktop.swingx.auth.LoginService

class LatihanController extends LoginService {
    def view

	@Override
	public boolean authenticate(String name, char[] password, String server)
			throws Exception {

		println "Login Info: name = [$name] password = [$password] " +
			"server = [$server]"
		if (name=="solid" && password.toString()=="snake")
			return true
		else
			return false
	}

}

Tampilkan kode program di atas akan terlihat seperti pada gambar berikut ini:

JXLoginPane

JXLoginPane

JXMonthView

JXMonthView adalah sebuah kalendar yang ditampilkan saat user memilih sebuah JXDatePicker. Dengan JXMonthView, pengguna bisa tidak hanya terbatas memilih 1 tanggal, tetapi juga range tanggal, seperti yang diperlihatkan pada kode program berikut ini:

import static org.jdesktop.swingx.calendar.DateSelectionModel.SelectionMode.*
import org.jdesktop.swingx.calendar.*

application(title: 'Komponen SwingX',  
  id: 'frameUtama',
  preferredSize: [320, 240],
  pack: true,
  locationByPlatform:true,    
  iconImage: imageIcon('/griffon-icon-48x48.png').image) {

  	borderLayout()
  	jxmonthView(constraints: CENTER, id: "monthView", 
		  selectionMode: SINGLE_INTERVAL_SELECTION).with {		
		getSelectionModel().valueChanged = { e ->
			terpilih.setText(monthView.getFirstSelectionDate()?.format("dd/MM/yyyy") +
				" s/d " + monthView.getLastSelectionDate()?.format("dd/MM/yyyy"))
		}
	}
	panel(constraints: PAGE_END) {
		label(text: 'Anda memilih: ')
		label(id: 'terpilih')	
	}
}

Tampilan program diatas diperlihatkan oleh gambar berikut ini:

JXMonthView

JXMonthView

JXSearchField

JXSearchField adalah turunan dari JXTextField yang dilengkapi dengan icon untuk pencarian. Sebagai contoh, berikut ini adalah kode program yang menggunakan JXSearchField:

import org.jdesktop.swingx.JXSearchField

application(title: 'Komponen SwingX',  
  id: 'frameUtama',
  preferredSize: [320, 240],
  pack: true,
  locationByPlatform:true,    
  iconImage: imageIcon('/griffon-icon-48x48.png').image) {

  	borderLayout()
  	widget(new JXSearchField("Cari:"), constraints: PAGE_START, 
		 actionPerformed: { e ->
			 txtOutput.append("Mencari [${e.getActionCommand()}]\n")
		 })
	textArea(constraints: CENTER, id: "txtOutput")
}

Tampilan program diatas diperlihatkan oleh gambar berikut ini:

JXSearchField

JXSearchField

JXSearchPanel

JXSearchPanel adalah panel untuk pencarian kompleks. Kode program berikut ini akan menampilkan JXSearchPanel:

import org.jdesktop.swingx.JXSearchField
application(title: 'Komponen SwingX',  
  id: 'frameUtama',
  preferredSize: [320, 240],
  pack: true,
  locationByPlatform:true,    
  iconImage: imageIcon('/griffon-icon-48x48.png').image) {

  	borderLayout()
    jxsearchPanel(constraints: PAGE_START)
	textArea(constraints: CENTER)
}

Tampilan JXSearchPanel dapat dilihat pada gambar berikut ini:

JXSearchPanel

JXSearchPanel

Memakai SwingXBuilder Di Griffon: Part 1

Bila komponen di Swing dirasa belum cukup “canggih”, maka salah satu alternatif yang dapat dipakai adalah SwingX. Sayangnya, dokumentasi resmi-nya di http://swingx.java.net/documentation terasa sangat sederhana dan tanpa screenshot!   Oleh sebab itu, untuk mengetahui bagaimana tampilan dan cara pakai setiap komponen yang ada, saya akan mencoba menjalankannya di Griffon (framework untuk bahasa pemograman Groovy).

Untuk memakai SwingX, saya akan meng-install plugin SwingXBuilder dengan memberikan perintah berikut:

griffon install-plugin swingx-builder

Perintah di atas juga akan men-download dan menyertakan library SwingX terbaru yang dibutuhkan.   Pada kasus saya, versi SwingX yang dipakai adalah versi 1.6.4.

Node application() di Griffon secara otomatis akan memakai JXFrame bila terdapat SwingX; bila tidak terdapat SwingX, node tersebut akan memakai JFrame dari Swing.

JXBusyLabel

Komponen ini pada dasarnya adalah sebuah label yang dilengkapi dengan animasi ‘sedang diproses’. Untuk mengaktifkan animasi, method setBusy() harus dipanggil dengan nilai true. Berikut ini adalah contoh kode program yang memakai JXBusyLabel:

application(title: 'Komponen SwingX',
  id: 'frameUtama',
  preferredSize: [320, 240],
  pack: true,
  locationByPlatform:true,    
  iconImage: imageIcon('/griffon-icon-48x48.png').image) {

  	flowLayout()
  	jxbusyLabel(text: 'Permintaan Anda sedang diproses!', busy: true)

}

Tampilannya akan terlihat seperti pada gambar berikut ini:

Tampilan JXBusyLabel

Tampilan JXBusyLabel

JXCollapsiblePane

JXCollapsiblePane adalah turunan dari JXPanel yang juga merupakan turunan dari JPanel. Isi dari JXCollapsiblePane dapat disembunyikan dan dimunculkan (dilengkapi animasi fade-in dan fade-out) dengan memanggil method setCollapsed() atau TOGGLE_ACTION yang disediakan. Berikut ini adalah contoh kode program yang memakai JXCollapsiblePane:

import javax.swing.border.TitledBorder
import org.jdesktop.swingx.JXCollapsiblePane

application(title: 'Komponen SwingX',
  id: 'frameUtama',
  preferredSize: [320, 240],
  pack: true,
  locationByPlatform:true,    
  iconImage: imageIcon('/griffon-icon-48x48.png').image) {

    borderLayout()
    jxcollapsiblePane (id: "cp", border: new TitledBorder("JXCollapsiblePane"), constraints: CENTER) {		
	label("Ini isi JXCollapsiblePane")
    }

    button(cp.getActionMap().get(JXCollapsiblePane.TOGGLE_ACTION), 
	constraints: PAGE_START, text: "Tampil/Sembunyikan")

}

Tampilan program diatas akan terlihat seperti pada gambar berikut ini:

JXCollapsiblePane

JXCollapsiblePane

JXColorSelectionButton

Ini adalah sebuah tombol yang bisa dipakai untuk memilih warna, seperti yang diperlihatkan dalam kode program berikut ini:

application(title: 'Komponen SwingX',  
  preferredSize: [320, 240],
  pack: true,
  locationByPlatform:true,    
  iconImage: imageIcon('/griffon-icon-48x48.png').image) {

    flowLayout()		
    jxcolorSelectionButton(id: "colorSelection")
    label(text: "Periksa Warna Tulisan Ini", foreground: 
    bind(source: colorSelection, sourceProperty: "background"))
}

Tab pada dialog yang dimunculkan oleh JXColorSelectionButton sedikit lebih banyak bila dibandingkan dengan JColorChooser milik Swing, seperti yang terlihat pada gambar berikut ini:

JXColorSelectionButton

JXColorSelectionButton

JXDatePicker

JXDatePicker adalah sebuah combo box yang menampilkan pilihan tanggal. Berikut ini adalah contoh kode program yang memakai JXDatePicker:

import java.text.SimpleDateFormat;

SimpleDateFormat dateFormat = new SimpleDateFormat("dd/MM/yyyy")

application(title: 'Komponen SwingX',  
  preferredSize: [320, 240],
  pack: true,
  locationByPlatform:true,    
  iconImage: imageIcon('/griffon-icon-48x48.png').image) {

    flowLayout()		
    jxdatePicker(id: "datePicker")
    label("Anda memilih tanggal: ")
    label(text: bind(source: datePicker, sourceProperty: "date", 
	converter: { v -> dateFormat.format(v) }))
}

Tampilannya akan terlihat seperti pada gambar berikut ini:

JXDatePicker

JXDatePicker

JXErrorPane

JXErrorPane mirip seperti JOptionPane tetapi dirancang khusus untuk menampilkan pesan kesalahan. Berikut ini adalah contoh kode program yang memakai JXErrorPane:

import org.jdesktop.swingx.JXErrorPane;

application(title: 'Komponen SwingX',  
  id: 'frameUtama',
  preferredSize: [320, 240],
  pack: true,
  locationByPlatform:true,    
  iconImage: imageIcon('/griffon-icon-48x48.png').image) {

    flowLayout()		
	button(text: "Klik Disini!", actionPerformed: {
	try {
	  throw new RuntimeException("Oh tidak! Saya mengalami kesalahan!!")
	} catch (Exception ex) {				
	  JXErrorPane.showDialog(ex)
	}
    })  	
}

Tampilannya akan terlihat seperti pada gambar berikut ini:

JXErrorPane

JXErrorPane

JXFindBar

Komponen ini merupakan sebuah JXFindPanel yang dilengkapi implementasi untuk pencarian incremental. Berikut ini adalah contoh kode program yang memakai JXFindBar:

import java.awt.BorderLayout;
import javax.swing.BorderFactory;
import org.jdesktop.swingx.JXFindBar;

application(title: 'Komponen SwingX',  
  id: 'frameUtama',
  preferredSize: [320, 240],
  pack: true,
  locationByPlatform:true,    
  iconImage: imageIcon('/griffon-icon-48x48.png').image) {

    borderLayout()
    jxeditorPane(id: 'editorPane', constraints: BorderLayout.CENTER,
 	border: BorderFactory.createEtchedBorder())
    jxfindBar(new JXFindBar(editorPane.getSearchable()), 
	constraints: BorderLayout.PAGE_START)			
}

Tampilannya akan terlihat seperti pada gambar berikut ini:

JXFindBar

JXFindBar

JXGradientChooser

Komponen ini adalah sebuah JXPanel yang dirancang khusus untuk memilih gradien warna, seperti yang diperlihatkan oleh kode program berikut ini:

import java.awt.BorderLayout;

application(title: 'Komponen SwingX',  
  id: 'frameUtama',
  preferredSize: [320, 240],
  pack: true,
  locationByPlatform:true,    
  iconImage: imageIcon('/griffon-icon-48x48.png').image) {

    borderLayout()
    jxgradientChooser(constraints: BorderLayout.CENTER)    	  

}

Tampilannya akan terlihat seperti pada gambar berikut ini:

JXGradientChooser

JXGradientChooser

Pengenalan SwingX

Akhirnya liburan semester tiba juga.. Tidak terasa sudah menjadi seorang dosen untuk satu semester.. Mendidik mahasiswa memang tidak mudah, sulit untuk membuat mereka aktif meneliti dan terus mencari pengetahuan dari berbagai sumber. Banyak yang hanya ingin meraih nilai tertinggi saja, sampai melupakan tujuan mereka belajar. Padahal dunia IT adalah dunia yang luas dan terus berkembang, sehingga tidak mungkin hanya mengandalkan orang lain untuk bisa tetap mengikuti perkembangan. Lagipula, waktu yang terbatas selama satu semester, membuat aku tidak sempat mengajarkan topik-topik tambahan.  Misalnya, di pemograman Java, aku mengajarkan Swing. Jika bosan dengan Swing, atau menginginkan fitur-fitur tambahan pada Swing, seorang programmer Java bisa menggunakan SwingX, salah satu ‘turunan‘ dari Swing yang dapat di-download terpisah di http://www.swinglabs.org/

Apa kelebihan SwingX?  SwingX menawarkan fitur-fitur ekstra yang tidak ditemui di Swing secara siap jadi. Sebagai contoh, ada yang disebut sebagai Highlighter, untuk memberi highlight (bisa berupa warna background berbeda dan sebagainya) pada sel tertentu di JXTable, JXList, JXTree, dan sebagainya.  Komponen yang diawali JX kebanyakan adalah turunan dari komponen Swing standar, misalnya JXTable adalah turunan dari JTable.  Demikian juga, JXComboBox adalah turunan dari JComboBoxJXComboBox sudah mendukung fitur highlight dengan adanya fungsi addHighlighter().  Jika kita ingin menampilkan nilai dibawah 50 dengan background merah di JXComboBox, kita dapat menggunakan ColorHighlighter, salah satu implementasi dari Highlighter, seperti pada contoh berikut ini:

import java.awt.Color;
import java.awt.Component;
import java.awt.FlowLayout;
import javax.swing.JButton;
import javax.swing.JFrame;
import org.jdesktop.swingx.JXComboBox;
import org.jdesktop.swingx.decorator.ColorHighlighter;
import org.jdesktop.swingx.decorator.ComponentAdapter;
import org.jdesktop.swingx.decorator.HighlightPredicate;

public class App extends JFrame
{    
    private JXComboBox cboTest;

    public App() {
        super("Latihan SwingX");

        cboTest = new JXComboBox(new Integer[] {100, 50, 30, 80, 70, 45, 50, 60});    
        ColorHighlighter colorHighlight = new ColorHighlighter();
        colorHighlight.setBackground(Color.RED);
        colorHighlight.setForeground(Color.WHITE);        
        colorHighlight.setHighlightPredicate(new HighlightPredicate() {

            @Override
            public boolean isHighlighted(Component renderer, ComponentAdapter adapter) {
                if ((Integer)adapter.getValue() < 50) {
                    return true;
                } else {
                    return false;
                }
            }

        });

        cboTest.addHighlighter(colorHighlight);

        setLayout(new FlowLayout());        
        add(cboTest);
        add(new JButton("TEST"));

        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setSize(500,500);
        setVisible(true);
    }

    public static void main( String[] args )
    {
        new App();
    }
}

Hasil dari tampilan program di atas akan terlihat seperti:

ComboBox dengan ColorHighlighter

ComboBox dengan ColorHighlighter

Pada program di atas, fungsi setHighlightPredicate() di ColorHighlighter akan memeriksa apakah suatu nilai perlu di-highlight atau tidak (berdasarkan nilai kembalian true atau false).

Selain itu, SwingX juga menawarkan komponen baru, seperti JXDatePicker. Ini adalah contoh komponen yang sering dibutuhkan oleh aplikasi, tetapi tidak disediakan oleh Swing secara langsung.  Berikut ini adalah contoh tampilan JXDatePicker:

Contoh Tampilan JXDatePicker

Tampilan JXDatePicker

SwingX juga memiliki kotak dialog siap pakai, seperti JXTipOfTheDay, yang umum dipakai untuk menampilkan tips di program.  Berikut ini adalah contoh potongan kode program yang mempergunakan JXTipOfTheDay:

tipOfTheDay = new JXTipOfTheDay();
tipOfTheDay.setModel(new TipOfTheDayModel() {

    private Tip[] tips = {
       new Tip() {

           @Override
           public String getTipName() {
               return "Tips 1";
           }

           @Override
           public Object getTip() {
               return "Tahukah Anda SwingX menyediakan komponen siap jadi?";
           }
       },

       new Tip() {

          @Override
          public String getTipName() {
               return "Tip 2";
          }

          @Override
          public Object getTip() {
               return "Anda bisa membuat table dengan highlighter di SwingX";
          }
       } 
   };     

   @Override
   public int getTipCount() {
       return tips.length;
   }

   @Override
   public Tip getTipAt(int index) {                                
       return tips[index];
   }
});

tipOfTheDay.setCurrentTip(1);        
tipOfTheDay.showDialog(this);

Contoh tampilan program di atas akan terlihat seperti:

Contoh Tampilan JXTipOfTheDay

Tampilan JXTipOfTheDay