Memakai Reverse Ajax “Comet” Dengan Spring Web MVC 3.2

Hujan turun tanpa henti di malam yang ceriah ini.   Karena hari ini adalah hari terakhir di tahun 2012.  Dan tulisan ini akan menjadi tulisan terakhir di tahun 2012.  Selamat merayakan tahun baru dan selamat datang, 2013!  Kode program terakhir yang saya ubah di tahun 2012 berkaitan dengan sebuah aplikasi web yang saya buat dimana tampilannya seperti berikut ini:

Permintaan Perubahan

Permintaan Perubahan

Halaman tersebut menampilkan data termasuk status data yang ada. Perubahan status data, seperti “Belum Disajikan” menjadi “Sudah Disajikan” dilakukan oleh pengguna lain di komputer lain. Perubahan ini perlu diperbaharui oleh halaman tersebut.

Tetapi yang terjadi adalah bila pengguna membuka halaman ini, maka sampai selama-lamanya halaman akan terlihat sama, walaupun pengguna lain telah men-update status data.   Informasi yang ditampilkan menjadi tidak up-to-date lagi.

Lalu bagaimana cara memperbaharui tampilan? Pengguna bisa men-klik tombol Refresh atau menekan tombol F5!!   Bayangkan jika pengguna harus memantau halaman ini terus menerus selama jam kerja (misalnya seorang kasir), maka tombol F5 bisa jadi tombol yang paling cepat pudar di keyboard 😉

Lalu apakah ada solusi untuk membuat halaman ini terlihat lebih profesional dimana perubahan data oleh user lain bisa langsung diperbaharui?  Yup!  Saya bisa menggunakan apa yang disebut sebagai teknik Reverse Ajax.   Bila biasanya client yang duluan menghubungi server, maka pada Reverse Ajax, seolah-olah server yang akan menghubungi client.   Solusi lain adalah dengan memakai WebSocket (bagian dari HTML5) dengan syarat browser pengguna sudah mendukung.

Teknik Reverse Ajax terdiri atas polling, piggyback, dan Comet (long polling).

Polling adalah cara yang paling sederhana, yaitu dengan membuat sebuah timer JavaScript yang memeriksa perubahan data di server secara periodik. Cara ini akan sangat membebani server karena selalu ada data yang dikirim dari server biarpun tidak ada perubahan.

Piggyback akan mengembalikan status perubahan bersamaan dengan sebuah request normal. Misalnya, pada saat user men-klik sesuatu di halaman, sekalian ikut kembalian event perubahan.   Cara ini lebih hemat bandwidth dibandingkan dengan teknik polling. Kelemahannya adalah harus ada sebuah request yang normal terlebih dahulu, baru event perubahan yang terakumulasi di server dikirim balik bersamaan dengan reponse.   Saya tidak bisa menggunakan piggyback karena saya ingin halaman tetap diperbaharui biarpun pengguna hanya duduk diam menatap monitor.

Teknik Reverse Ajax yang lebih efisien adalah Comet atau sering disebut juga dengan long polling. Pada Comet, client akan melakukan request, tetapi server tidak akan langsung mengembalikan nilai.   Request ini akan terbuka untuk waktu yang lama.   Selama selang waktu tersebut, server dapat mengirim data ke client kapan saja.   Teknik ini adalah teknik yang paling efisien karena bandwidth hanya akan dipakai bila server perlu mengembalikan data ke client.

Saya akan memakai teknik Comet.   Untuk itu, saya harus memenuhi persyaratan, yaitu memiliki server yang mendukung asynchronous reponse.  Selain itu saya juga perlu memakai bahasa pemograman yang mendukung multithreading. Sebagai contoh, bila saya memakai Apache + PHP ‘murni’ dan berusaha menghentikan request dengan sleep(), maka selama thread di-sleep(), seluruh client lain tidak akan bisa mengakses web!!   Kinerja web malah akan jauh lebih buruk dibanding memakai teknik polling.  Server apa yang sudah mendukung asynchronous response?   Tomcat 6 ke atas, Jetty 6 ke atas, Glassfish dengan Grizzly, dan sebagainya.   Saya akan memakai Tomcat 7.

Apa contoh teknologi bahasa pemograman yang mendukung asynchronous response?   Servlet 3 di Java EE!   Ada juga framework yang mempermudah seperti CometD, DWR (Direct Web Remoting), dan Atmosphere.   Selain itu,  Spring Web MVC sejak versi 3.2 telah mendukung asynchronous response.   Karnea aplikasi saya dari awal dikembangkan dengan Spring Web MVC, maka yang perlu saya lakukan adalah men-upgrade versi Spring yang dipakai ke versi 3.2.   Dengan memakai Apache Maven, yang perlu saya lakukan hanya mengganti nilai property org.springframework-version menjadi 3.2.0.RELEASE, dan Maven akan mendownload JAR yang dibutuhkan.

Setelah men-upgrade versi Spring ke 3.2.0.RELEASE, saya perlu mengubah file web.xml. Perubahan yang saya lakukan adalah:

<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" version="3.0">

Selain itu, saya perlu menambahkan <async-supported> di definisi org.springframework.web.servlet.DispatcherServlet milik Spring Web MVC seperti yang terlihat berikut ini:

<servlet>
    <servlet-name>appServlet</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>    
    <init-param>      
      <param-name>contextConfigLocation</param-name>
      <param-value>/WEB-INF/spring/appServlet/servlet-context.xml</param-value>
    </init-param>    
    <load-on-startup>1</load-on-startup>
    <async-supported>true</async-supported>
</servlet>

Karena saya memakai Spring Security yang mendefinisikan sebuah filter di web.xml yaitu org.springframework.web.filter.DelegatingFilterProxy, maka saya juga perlu menambahkan <async-supported> pada definisi filter tersebut. Berikut ini adalah perubahan yang saya lakukan:

<filter>
    <filter-name>springSecurityFilterChain</filter-name>
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
    <async-supported>true</async-supported>    
  </filter>
  <filter-mapping>
    <filter-name>springSecurityFilterChain</filter-name>
    <url-pattern>/*</url-pattern>
    <dispatcher>REQUEST</dispatcher>
    <dispatcher>ASYNC</dispatcher>
  </filter-mapping>
</filter>

Setelah ini, saya bisa mulai membuat kode program.  Saya akan mulai dari service layer.  Saya sedikit frustasi karena tidak menemukan pendekatan yang lebih rapi yang tidak mengotori service layer dengan objek DeferredResult milik presentation layer.  Saya selalu berusaha menghindari coupling seperti ini, tapi sepertinya tidak ada pilihan yang lebih gampang.   Pada service layer, saya mendefisinikan sebuah  struktur untuk menampung DeferredResult yang mewakili asynchronous request seperti berikut ini:

private List<DeferredResult<Boolean>> listDeferredResult = new Vector<DeferredResult<Boolean>>();

Saya memakai Vector karena class tersebut thread safe.  Pada asynchronous request yang diberikan halaman kasir, saya hanya mengembalikan sebuah nilai true bila halaman perlu diperbaharui atau false bila tidak.  Hal ini karena tabel dibuat dengan jqGrid dan saya tidak ingin repot mengelola data.

Pada satu atau lebih service yang menyebabkan perubahan di halaman kasir, saya memberikan kode program seperti berikut ini:

public Pemesanan prosesTambahPemesanan(Pemesanan pemesanan) {
   ...
   prosesNotifikasiPembayaran();
}

public Pemesanan prosesUpdatePemesanan(Pemesanan pemesanan) {
   ...
   prosesNotifikasiPembayaran();
}

private void prosesNotifikasiPembayaran() {
   for (DeferredResult<Boolean> deferredResult: listDeferredResult) {
      deferredResult.setResult(Boolean.TRUE);
   }
}

Method prosesNotifikasiPembayaran() akan menyebabkan asynchronous response selesai dengan nilai kembali berupa true.

Tidak lupa saya juga menyediakan method untuk mendaftar dan menghapus setiap DeferredResult yang ada di Vector, seperti berikut ini:

public void daftarNotifikasiPembayaran(DeferredResult<Boolean> deferredResult) {
  listDeferredResult.add(deferredResult);
}

public void hapusNotifikasiPembayaran(DeferredResult<Boolean> deferredResult) {
  listDeferredResult.remove(deferredResult);
}

Ok, service layer sudah selesai, sekarang saya akan ke presentation layer.  Saya menambahkan method berikut ini di controller:

@RequestMapping(value="pembaharuan")
@ResponseBody
public DeferredResult<Boolean> notifikasiPembayaran() {
  final DeferredResult<Boolean> deferredResult = new DeferredResult<Boolean>(30000l, Boolean.FALSE);
  pemesananService.daftarNotifikasiPembayaran(deferredResult);
  deferredResult.onCompletion(new Runnable() {
    @Override
    public void run() {
      pemesananService.hapusNotifikasiPembayaran(deferredResult);
    }
  });
  return deferredResult;
}

Pada kode program controller tersebut, saya memberikan waktu tunggu 30.000 ms atau 30 detik.  Bila setelah 30 detik, belum ada yang memanggil method DeferredResult.setResult(), maka nilai false akan dikembalikan.

Langkah terakhir, menambahkan kode program ini di view JSP:

...
<spring:url value="/pembayaran/pembaharuan" var="urlPembaharuan" />
...
function comet() {
  $.getJSON("${urlPembaharuan}", function(data) {
     if (data==true) {
        gridUtama.trigger('reloadGrid');
     }
     comet();
  });
}
...

Pada kode program di atas, bila hasil asynchronous request adalah true, maka dialog jqGrid akan di-refresh.   Setelah itu, tidak peduli nilai kembali adalah true atau false, fungsi tersebut akan kembali memanggil dirinya sendiri.

Untuk mencobanya, saya membuka tiga browser yang berbeda, dimana pada 2 browser, saya membuka halaman untuk kasir.  Satu browser-nya lagi untuk perubahan yang harus di-update oleh halaman kasir, seperti pada gambar berikut ini:

Pengujian

Pengujian

Pada gambar di atas, pada saat tombol “Iya” di-klik, maka tabel di browser Kasir A maupun browser Kasir B akan di-reload secara otomatis.

Teknik Comet (long polling) memang agak mirip polling, dimana client harus melakukan request secara periodik.  Tetapi penggunaan asynchronous request membuat Comet jauh lebih efisien dibanding polling:

  1. Pada  polling,  bila interval adalah 30 detik, maka client benar-benar harus menunggu selama 30 detik baru ada hasil yang diperoleh.  Pada Comet, bila interval adalah 30 detik, lalu pada detik ke-15 sudah ada event, maka hasil akan segera dikembalikan pada saat itu juga.
  2. Karena alasan di atas, untuk memperoleh hasil yang real-time, maka polling biasanya memiliki interval yang singkat, misalnya 10ms.   Hal ini menyebabkan semakin banyak request yang dilakukan ke server secara periodik dan akan membebani server.
Iklan

Menguji Tabel jqGrid Melalui Selenium

Pada sebuah halaman yang harus diuji, saya dihadapkan pada sebuah tabel jqGrid yang menampilkan data dari database, yang terlihat seperti pada gambar berikut ini:

Tampilan Yang Akan DiUji

Tampilan Yang Akan DiUji

Kode HTML yang membentuk halaman tersebut, bila dilihat melalui inspector Firefox, adalah:

Source  HTML Dilihat Dari Inspector Firefox

Source HTML Dilihat Dari Inspector Firefox

Pada dasarnya ini adalah sebuah elemen <table> diikuti <tbody>, didalamnya terdapat <tr> dan <td>.  Pertanyaannya adalah bagaimana cara menguji tabel di atas melalui Selenium?

Untuk 5 kolom pertama, saya bisa menggunakan method getTable() milik object WebDriverBackedSelenium.  Method tersebut menerima parameter dalam bentuk idtabel.indexbaris.indexkolom.  Sebagai contoh, nilai selenium.getTable(“grid.0.0”) akan mengembalikan nilai text yang terlihat untuk baris pertama dan kolom pertama dari tabel dengan id berupa “grid”.

Karena jqGrid selalu menyisakan baris pertama untuk disembunyikan, maka saya harus mulai menguji mulai dari baris kedua atau index baris 1 (bukan 0).  Demikian juga, karena  kolom pertama berisi id dan disembunyikan, maka saya  harus mulai dari index baris 1 (bukan 0).  Berikut ini contoh kode program pengujiannya:

assertEquals("10-10-2012", selenium.getTable("grid.1.1"));
assertEquals("13-10-2012 16:10", selenium.getTable("grid.1.2"));
assertEquals("01PQVS3KY5", selenium.getTable("grid.1.3"));
assertEquals("160.000,00", selenium.getTable("grid.1.4"));
assertEquals("Konfirmasi Sukses", selenium.getTable("grid.1.5"));

Nilai yang dipakai pada kode program di atas harus sesuai dengan nilai yang tersimpan di database!  Dengan demikian, bila suatu hari nanti, setelah perjalanan panjang coding sana sini, tiba-tiba halaman ini menampilkan nilai yang tidak sesuai, saya akan segera mengetahuinya.

Berikutnya, menguji kolom terakhir yang berisi tombol akan sedikit sulit.  Hal ini karena saya tidak bisa memakai getTable() berhubung getTable() sama seperti getText() mengembalikan apa yang dilihat oleh user!   Saya harus memakai getValue() yang mengembalikan nilai atribut value pada elemen yang ditentukan.  

Pertanyaannya adalah bagaimana cara memilih kolom kelima dari setiap baris?  Salah satu solusi yang mungkin adalah dengan memakai ekpresi XPath.  Berbeda dengan getTable(), pada ekspresi XPath, index dimulai dari 1, bukan dimulai dari 0.  Berikut ini adalah contoh mengambil atribut value (yang mewakili caption) dari sebuah <input type=”button”> dengan memakai XPath:

assertEquals("Cetak", selenium.getValue("//table[@id='grid']/tbody/tr[2]/td[7]/input[1]"));

Kode program diatas akan memeriksa apakah baris ke-dua (yang pertama kali terlihat, karena baris pertama di jqGrid disembunyikan) dan kolom ke-7 (karena pada kode program tampilan saya, setiap baris terdiri atas 7 kolom, dimana kolom pertama disembunyikan!) memiliki elemen <input> dengan atribut value berupa “Cetak”.

Untuk menguji apakah XPath yang saya masukkan benar atau salah, saya bisa menggunakan fungsi $x() di console Firebug .  Sebagai contoh, bila saya mengetik $x(“//table[@id=’grid’]”) di console Firebug, maka Firebug akan mengembalikan objek  <table> dengan id “grid”.

Mengatur Dialog Bawaan jqGrid Yang Tertimpa

jqGrid selain dapat menampilkan tabel, juga dilengkapi berbagai fitur seperti fitur untuk edit dan pencarian.   Terkadang dialog bawaan jqGrid saat ditampilkan terlihat tertimpa oleh hasil rancangan kita, seperti pada gambar berikut ini:

Dialog jqGrid Yang Tertimpa

Dialog jqGrid Yang Tertimpa

Hal ini terjadi karena pengaturan property z-index yang tidak tepat. z-index adalah salah satu property CSS yang dipakai untuk menentukan elemen mana yang akan ditampilkan bila dua atau lebih elemen saling menimpa.  Elemen dengan nilai z-index yang lebih besar akan menimpa elemen dengan nilai z-index yang lebih kecil.

Lalu, bagaimana caranya mengatur z-index untuk dialog jqGrid?   Saya dapat memberikan nilai property zIndex saat melakukan inisialisasi navigasi jqGrid.   Sebagai contoh, saya tidak memakai fitur untuk add, edit dan delete melalui jqGrid, tetapi saya meminjam dialog search jqGrid, sehingga untuk mengatur z-index di dialog pencarian, inisialisasi jqGrid saya akan terlihat seperti berikut ini:

grid.jqGrid(parameter).navGrid("#pager",
  {add: false, edit: false, del: false}, {}, {}, {},
  {closeAfterSearch: true, zIndex: 1000}, {}
);

Setelah menambahkan parameter zIndex menjadi 1000,  maka dialog pencarian jqGrid tidak terlihat tertimpa lagi, seperti pada gambar berikut ini:

Dialog Pencarian Dengan Parameter zIndex = 1000

Dialog Pencarian Dengan Parameter zIndex = 1000

 

Menambah Tombol Delete Untuk Setiap Baris Di jqGrid

jqGrid (http://www.trirand.com/blog/) adalah sebuah plugin untuk JQuery yang dapat dipakai untuk menampilkan data dalam bentuk tabel/grid.  jqGrid dapat melakukan proses filter dan pagination (navigasi halaman) secara otomatis, sementara programmer tinggal memberikan data dari server dalam bentuk JSON atau XML.

Salah satu pertanyaan yang muncul pada saat saya memakai jqGrid adalah bagaimana membuat tombol delete (hapus) untuk setiap baris di tabel?  Apa saya harus memodifikasi data yang dikirim dari server untuk membuat kolom ‘semu’?  Setelah melakukan google sejenak, saya menemukan beberapa contoh penyelesaian yang agak ‘rumit’ yaitu dengan melakukan data setelah di-load (dengan demikian membutuhkan JavaScript pada event gridComplete).  Tapi tidak lama kemudian, saya menemukan jawaban yang lebih sederhana namun tetap elegan, yaitu dengan menggunakan formatter dan  formatoptions.

Pada saat saya membuat jqGrid, saya menambahkan sebuah kolom kosong di colNames.  Saya tidak perlu mengirimkan data dari server untuk kolom ini, karena nantinya kolom ini akan berisi tombol hapus.   Lalu, pada colModel, saya menambahkan formatter dan formatOptions untuk kolom kosong ini seperti berikut ini:

...
colNames: ['First Name', 'Last Name','Birth Date', ''],

colModel: [
  {name:'firstName', index:'firstName', width:150},
  {name:'lastName', index:'lastName', width:100},
  {name:'birthDateString', index:'birthDate', width:100},
  {name: '', index: 'action', width:75, sortable:false, formatter:'actions',
  formatoptions:{
     editbutton: false, delbutton:true, mtype: "GET",
     delOptions:{
        caption: 'Hapus',
        msg: 'Anda ingin menghapus record ini?',
        bSubmit: 'Hapus',
        mtype: 'GET',
        url: 'contacts/listrowdelete',
        bCancel: 'Batal'
     }
  }} ],
  ...

Dengan memberi nilai formatter dengan actions, maka kolom ini akan di-isi dengan tombol-tombol yang mewakili aksi tertentu.  Untuk melakukan pengaturan lebih lanjut, saya melakukan formatoptions.   Nilai false pada editbutton menyebabkan tombol tersebut tidak akan ditampilkan.    Nilai mtype secara default adalah POST.  Nilai ini menentukan bagaimana data dikirim ke server pada tombol delete ditekan.  Saya memilih GET untuk mempermudah debugging.   Berikut ini adalah contoh tampilannya:

Tampilan jqGrid dengan Tombol Delete Per Baris

Tampilan jqGrid dengan Tombol Delete Per Baris

Nilai delOptions menentukan bagaimana karakteristik dialog yang muncul pada saat tombol hapus ditekan.  Nilai ini secara internal akan dikirimkan pada saat memanggil fungsi delGridRow().  Dokumentasi untuk nilai yang ada dapat dilihat di http://www.trirand.com/jqgridwiki/doku.php?id=wiki:form_editing#delgridrow.

Berikut ini adalah contoh dialog yang muncul pada saat tombol hapus ditekan:

Dialog Hapus

Dialog Hapus

Bila tombol Delete ditekan, maka URL yang ditentukan di url akan dipanggil.  Pada contoh ini, URL yang dipanggil adalah http://localhost:8080/test/contacts/listrowdelete?oper=del&id=1.  jqGrid secara otomatis menambahkan parameter oper dan id, dimana id adalah baris yang sedang terpilih.

Dengan demikian, pada sisi server, di controller, saya tinggal membuat sebuah handler seperti:

@RequestMapping(value="/listrowdelete", params="oper=del", method=RequestMethod.GET)
public String listRowDelete(@RequestParam(value="id", required=true) Long id, Model uiModel) {
  // kode program hapus dari database disini
}