Menambah atau mengubah endpoint yang dihasilkan Spring Data Rest

Pada artikel Membuat Restful Web Service Dengan Spring Data REST, saya menghasilkan REST API secara otomatis berdasarkan domain model dan Spring Data Repository. API yang dihasilkan pun telah mendukung HATEOAS dengan implementasi HAL. Dengan Spring Data REST, saya tidak perlu lagi menulis kode program controller dan Spring HATEOAS secara manual untuk setiap domain model yang ada.

Sebagai latihan, kali ini saya akan memakai Spring Data REST melalui Spring Boot. Pada saat membuat proyek baru, saya menambahkan dependency berupa Web, Rest Repositories dan JPA. Setelah proyek selesai dibuat, saya menambahkan entity baru seperti berikut ini:

package com.example.demo.domain;

import org.hibernate.validator.constraints.Email;
import org.hibernate.validator.constraints.NotBlank;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;

@Entity
public class Pelanggan {

    @Id
    @GeneratedValue
    private Long id;

    @NotBlank @Email
    private String email;

    @NotBlank
    private String nama;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public String getNama() {
        return nama;
    }

    public void setNama(String nama) {
        this.nama = nama;
    }
}

Setelah itu, saya membuat sebuah repository untuk mengakses dan melakukan operasi CRUD terhadap Pelanggan seperti berikut ini:

package com.example.demo.repository;

import com.example.demo.domain.Pelanggan;
import org.springframework.data.jpa.repository.JpaRepository;

public interface PelangganRepository extends JpaRepository<Pelanggan, Long> {
}

Sebagai langkah terakhir, saya akan menggunakan embedded database pada latihan kali ini. Untuk itu, saya menambahkan baris berikut ini pada file build.gradle:

...
dependencies {
  compile('com.h2database:h2:1.4.196')
  ...
}

Sekarang, bila saya menjalankan aplikasi Spring Boot ini, sesuai dengan spesifikasi HATEOAS, saya bisa mengakses http://localhost:8080/ untuk melihat daftar endpoints yang tersedia seperti yang terlihat pada gambar berikut ini:

Daftar endpoints yang tersedia

Daftar endpoints yang tersedia

Saya bisa melakukan operasi CRUD terhadap pelanggan melalui URI http://localhost:8080/pelanggans. Spring Data REST menggunakan nama plural (pelanggan menjadi pelanggans) sesuai spesifikasi REST, walaupun hal ini sebenarnya tidak relevan untuk Bahasa Indonesia. Selain itu, API yang dihasilkan juga mendukung operasi halaman (pagination) dan pengurutan (sorting).

Saya akan mencoba menyimpan pelanggan baru dengan memberikan request POST ke http://localhost:8080/pelanggans berisi JSON berikut ini (pastikan menyertakan Content-Type berupa application/json):

{
  "email": "phantom@diamondogs.pain",
  "nama": "Venom"
}

Saya akan memperoleh kembalian seperti pada gambar berikut ini:

JSON yang dikembalikan saat menyimpan pelanggan baru

JSON yang dikembalikan saat menyimpan pelanggan baru

Sesuai dengan spesifikasi HAL, JSON yang dikembalikan mengandung _links yang berisi referensi ke operasi berikutnya yang bisa dilakukan terhadap entity ini. Sampai disini, entity juga sudah tersimpan ke database.

Bayangkan bila saya harus melakukan langkah di atas secara manual. Saya perlu membuat sebuah controller baru. Selain itu, untuk tetap mendukung HATEOAS, saya perlu memakai Spring HATEOAS dan mendaftarkan _links secara manual. Cukup merepotkan, bukan?

Walaupung Spring Data REST sangat membantu, ada kalanya saya perlu menyediakan operasi lain selain CRUD yang belum disediakan oleh Spring Data REST secara otomatis. Selain itu saya juga mungkin perlu memodifikasi operasi yang tersedia. Sebagai contoh, saya umumnya perlu mengirim email ke pelanggan setelah menyimpannya ke database. Beruntungnya, Spring Data REST tetap memungkinkan saya untuk mengubah operasi yang sudah ada.

Untuk itu, saya perlu membuat controller baru seperti berikut ini:

package com.example.demo.web;

import com.example.demo.domain.Pelanggan;
import com.example.demo.repository.PelangganRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.rest.webmvc.PersistentEntityResource;
import org.springframework.data.rest.webmvc.PersistentEntityResourceAssembler;
import org.springframework.data.rest.webmvc.RepositoryRestController;
import org.springframework.hateoas.Resource;
import org.springframework.http.ResponseEntity;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.mail.Address;
import javax.mail.internet.MimeMessage;

@RepositoryRestController
public class PelangganController {

    private final PelangganRepository pelangganRepository;
    private final JavaMailSender mailSender;

    @Autowired
    public PelangganController(PelangganRepository pelangganRepository, JavaMailSender mailSender) {
        this.pelangganRepository = pelangganRepository;
        this.mailSender = mailSender;
    }

    @RequestMapping(method = RequestMethod.POST, value = "/pelanggans")
    public @ResponseBody ResponseEntity<?> save(@RequestBody Resource<Pelanggan> pelangganResource, PersistentEntityResourceAssembler assembler) {
        // Menyimpan pelanggan
        Pelanggan pelanggan = pelangganResource.getContent();
        pelanggan = pelangganRepository.save(pelanggan);

        // Mengirim email
        SimpleMailMessage email = new SimpleMailMessage();
        email.setTo(pelanggan.getEmail());
        email.setSubject("Selamat datang");
        email.setText(String.format("Hi, selamatt datang, %s", pelanggan.getNama()));
        mailSender.send(email);

        // Mempersiapkan kembalian
        PersistentEntityResource resource = assembler.toResource(pelanggan);
        return ResponseEntity.ok(resource);
    }
}

Kali ini, controller yang saya buat tidak menggunakan annotation @Controller melainkan @RepositoryRestController yang menandakan bahwa saya ingin memodifikasi hasil dari Spring Data REST. Method di controller saya akan menerima request dalam bentuk Resource dan juga mengembalikan resource agar mendukung HATEOAS. Agar tidak repot menghasilkan _links dengan Spring HATEOAS, saya cukup menggunakan PersistentEntityResourceAssembler untuk menghasilkan resource yang sama persis seperti yang dihasilkan oleh Spring Data REST sebelum dimodifikasi.

Iklan

Menghindari CORS saat memakai Spring Boot bersama dengan Webpack

Pada artikel Menyatukan Frontend dan Backend Pada Satu Proyek Yang Sama, saya membuat proyek baru yang menggabungkan back end dan front end. Untuk menjalankan back end, saya memberikan perintah ./gradlew bootRun yang akan membuat instance server Tomcat baru. Saya bisa meng-eksekusi end points milik back end di http://localhost:8080/api. Setiap kali kode back end berubah, server ini akan diperbaharui. Sementara itu, untuk menjalankan front end, saya memberikan perintah yarn dev yang juga akan menjalankan server baru di http://localhost:9090. Setiap kali kode front end berubah, server ini akan diperbaharui.

Apa yang salah bila server front end berbeda dengan back end? Hampir semua browser modern sudah mendukung fasilitas Cross-origin Resource Sharing (CORS) yang akan memblokir akses end points milik front end dari domain berbeda. Tentunya saya tidak ingin setiap API milik back end saya bisa diakses oleh front end milik orang lain sesuka hati. Akan tetapi, saat mengembangkan kode program, server front end dan back end adalah dua server berbeda. Fasilitas CORS akan membuat front end saya gagal memanggil back end. Memang Spring Boot memiliki annotation @CrossOrigin yang bisa membuat back end saya dipakai semua orang sesuka hati sehingga front end saya yang berada di server berbeda bisa memanggilnya. Akan tetapi, ini membuat saya merasa kurang aman.

Apakah ada solusi lain? Yup! Saya beruntung karena template webpack yang dipakai oleh vue-cli sudah memperhitungkan hal ini. Bila saya memperhatikan isi file dev-server.js yang dijalankan oleh yarn dev, terlihat bahwa http-proxy-middleware sudah disertakan. Proyek ini pada dasarnya adalah sebuah HTTP proxy (memakai http-proxy) berbasis JavaScript. Dengan http-proxy-middleware, saya ingin pada saat front end mengakses http://localhost:9090/api, secara transparan, ia akan memanggil http://localhost:8080/api. Karena proxy ini transparan dari sisi browser, saya tidak akan menemukan masalah CORS lagi.

Untuk itu, saya akan mengubah file config/index.js pada bagian dev.proxyTable sehingga terlihat seperti berikut ini:

module.exports = {
  ...
  dev: {
    ...
    proxyTable: {
      '/api': 'http://localhost:8080'
    }
  }
}

Pada konfigurasi di atas, setiap kali saya mengakses url yang diawali oleh /api di server front end seperti http://localhost:9090/api/snakes, akses tersebut akan diteruskan ke server back end di http://localhost:8080/api/snakes secara transparan. Dengan demikian, saya tidak akan mendapatkan kesalahan CORS lagi.

Memakai Bootstrap bersama dengan Vue.js dan Webpack

Pada artikel Menyatukan Frontend dan Backend Pada Satu Proyek Yang Sama, saya membuat sebuah website kosong yang menggunakan Vue.js dan berbasis Webpack. Salah satu hal menarik yang saya sukai dari Vue.js adalah setiap komponen (single file component) memiliki deklarasi CSS-nya masing-masing. Agar CSS hanya berlaku untuk sebuah komponen (dan juga komponen lain yang dikandunginya), saya tinggal menambahkan <style scoped>. Selain itu, saya juga bisa menggunakan CSS Module dengan menggunakan <style module>.

Walaupun deklarasi CSS per komponen adalah hal yang baik, akan tetapi, ada saatnya saya juga membutuhkan CSS yang global untuk seluruh bagian di website. Sebagai contoh, akan sulit sekali bagi saya untuk menulis ulang CSS seperti Bootstrap grid di setiap komponen. Bagaimana cara menambahkan Bootstrap pada proyek Vue.js yang berbasis Webpack?

Satu hal yang perlu saya pertimbangkan adalah komponen JavaScript di Bootstrap masih menggunakan jQuery sementara filosofi Vue.js yang reaktif membuat saya ingin menghindari jQuery sebisa mungkin. Beruntungnya, saya menemukan https://bootstrap-vue.js.org yang menyediakan Bootstrap 4 dimana komponen-nya telah ditulis ulang sebagai komponen Vue.js yang tidak menggunakan jQuery. Untuk menggunakan library tersebut, saya segera menambahkan baris berikut ini pada package.json:

"dependencies": {
    ...
    "bootstrap-vue": "^0.18.0"
},

Library ini akan menyertakan bootstrap@^4.0.0-alpha.6 yang berisi Bootstrap 4. Mengapa tidak mengubah file index.html dan menambahkan baris seperti <link rel="stylesheet" href="..."> dan <script src="...">? Salah satu alasannya adalah akan lebih konsisten bila seluruh dependensi proyek diletakkan pada file package.json. Bila saya ingin memakai versi Bootstrap yang berbeda, misalnya, saya hanya perlu memperbaharui package.json tanpa mengubah index.html.

Berikutnya, saya menambahkan baris berikut ini pada awal file main.js:

import BootstrapVue from 'bootstrap-vue'
import 'bootstrap/dist/css/bootstrap.css'
import 'bootstrap-vue/dist/bootstrap-vue.css'

Proyek Webpack yang dihasilkan oleh vue-cli sudah dilengkapi konfigurasi yang memakai css-loader (lihat file buildScripts\util.js) sehingga saya bisa men-import file css di dalam kode program JavaScript.

Berikutnya, saya perlu mengaktifkan bootstrap-vue dengan menambahkan kode program berikut ini di main.js:

Vue.use(BootstrapVue)

Sekarang, bila saya menjalankan website ini dengan yarn run dev, saya akan menemukan bahwa CSS Bootstrap (dan juga CSS lain yang saya import dikemudian hari) akan diletakkan secara inline ke dalam tag <style> seperti yang terlihat pada gambar berikut ini:

Inline CSS pada saat `run dev`

Inline CSS pada saat `run dev`

Walaupun CSS inline lebih sederhana, ia tidak disarankan untuk dipakai di lingkungan produksi. Beruntungnya, proyek Webpack yang dihasilkan oleh vue-cli sudah memperhitungkan hal ini. Pada file buildScripts\util.js, terlihat bahwa tersedia option extract yang akan menggunakan plugin extract-text-webpack-plugin untuk menghasilkan file CSS terpisah. Seperti yang bisa ditebak, extract akan bernilai true bila perintah ini dikerjakan pada lingkungan produksi (lihat file buildScripts/vue-loader.conf.js). Untuk membuktikannya, saya akan menjalankan perintah yarn run build. Output dari perintah ini dapat dijumpai di folder dist. Kali ini, CSS akan diletakkan pada file terpisah seperti yang terlihat pada gambar berikut ini:

File CSS terpisah saat memakai `yarn run build`

File CSS terpisah saat memakai `yarn run build`

Memperbaharui Output Webpack Di Browser Secara Otomatis

Pada artikel Menyatukan Frontend dan Backend Pada Satu Proyek Yang Sama, saya membuat proyek Spring Boot yang menggunakan Webpack di front end. Webpack adalah bundler yang juga berfungsi melakukan transformasi konten. Sebagai contoh, Webpack akan melakukan transpiling kode program ES2016 saya ke dalam kode program JavaScript ‘lama’ yang bisa dimengerti semua browser berkat babel-loader. Selain itu, saya bisa menulis style dalam format Scss (Sassy CSS) yang kemudian akan diterjemahkan oleh sass-loader menjadi CSS biasa yang bisa dimengerti oleh semua browser. Semua hasil dari Webpack ini akan disimpan kedalam sebuah Jar.

Apa yang terjadi bila saya menjalankan Spring Boot dengan gradlew bootRun lalu mengubah kode program HTML atau JavaScript yang ada? Hasil perubahan ini tidak akan terlihat pada browser! Mengapa demikian? Karena gradlew bootRun hanya memanggil Webpack pada saat pertama kali dijalankan. Saat saya mengubah kode program di HTML atau JavaScript, Webpack tidak akan dipanggil. Hal ini tentu akan sangat merepotkan karena saya perlu menjalankan gradlew bootRun berkali-kali setiap kali melakukan perubahan di front end.

Beruntungnya, konfigurasi Webpack yang dihasilkan vue-cli sudah mendukung live reload. Template yang saya pakai memiliki script buildScripts/dev-server.js yang menggunakan fasilitas dari webpack-dev-middleware untuk menghasilkan live reload. Ini hampir sama seperti apa yang dilakukan oleh webpack-dev-server. Bila saya membuka file package.json, terlihat bahwa scripts dev atau start akan melakukan hal yang sama yaitu mengerjakan buildScripts/dev-server.js.

Agar bisa memanggil script ini secara mudah, saya bisa menambahkan kode program berikut ini pada build.gradle (milik proyek front end):

...
task runDev(dependsOn: yarn, type: YarnTask) {
    args = ['run', 'dev']
}
...

Sekarang, saya bisa menjalankan proyek front end (tanpa harus menjalankan Spring Boot) dengan menggunakan perintah berikut ini:

$ ./gradlew runDev
:frontend:nodeSetup UP-TO-DATE
:frontend:yarnSetup UP-TO-DATE
:frontend:yarn UP-TO-DATE
:frontend:runDev
yarn run v0.27.5
$ node buildScripts/dev-server.js
> Starting dev server...
 DONE  Compiled successfully in 1904ms00:08:40

> Listening at http://localhost:8080

 75% EXECUTING
> :frontend:runDev

Perintah di atas akan membuka browser yang menampilkan halaman front end saya. Sekarang, bila saya melakukan perubahan pada HTML atau JavaScript, halaman tersebut akan langsung diperbaharui secara otomatis. Saya juga bisa menemukan output seperti berikut ini di console:

 WAIT  Compiling...00:00:12

 DONE  Compiled successfully in 154ms00:00:13

Sekarang, saya bisa mengerjakan front end dan melihat hasilnya secara cepat.

Bagaimana bila saya ingin menjalankan Spring Boot dan dev-server.js secara bersamaan? Karena keduanya sama-sama menggunakan port 8080, salah satu akan gagal. Untuk itu, saya akan menggubah port yang dipakai oleh dev-server.js menjadi 9090 dengan mengubah port di config/index.js.

Belajar menilai seberapa sempurna rancangan REST API

Membuat API berbasis REST pada dasarnya adalah sesuatu yang sangat mudah: buat sebuah method di controller dan kembalikan hasil berupa JSON. Selesai! Yup, cukup sampai disini bagi programmer yang sedang belajar atau membuat proyek jangka pendek yang tidak ingin berkembang. Pada tulisan kali ini, saya akan melihat dari sisi arsitektur, dimana REST API tersebut akan dipakai dalam jangka lama dan mungkin akan dipanggil oleh klien baru yang belum terpikirkan saat ini.

Salah satu cara untuk menilai implementasi REST API adalah dengan membandingkannya dengan Richardson Maturity Model. Gambar berikut ini memperlihatkan perkembangan REST menurut model tersebut:

Richardson Maturity Model

Richardson Maturity Model

Pada Level 0, REST API dianggap sebagai API jarak jauh yang dipanggil melalui Web (Http) dan mengembalikan JSON. Tidak lebih dari sekedar pengganti function atau method (yang dikerjakan jarak jauh)! Sebagai contoh, API berikut ini masuk dalam kategori Level 0:

POST http://api.server.com/buatProduk
{
   nama: "produk1",
   harga: 1000
}

POST http://api.server.com/hapusProduk
{
   id: 1
}

POST http://api.server.com/daftarPelanggan
{
   email: "ocelat@dd.com",
   nama: "ocelot"
}

POST http://api.server.com/hapusPelanggan
{
   id: 1
}

Pada Level 1, terdapat konsep resource. Setiap API merupakan operasi yang akan melakukan sesuatu pada resource yang ada. Sebagai contoh, API berikut ini termasuk dalam kategori Level 1:

POST http://api.server.com/produk/buat
{
   nama: "produk1",
   harga: 1000
}

POST http://api.server.com/produk/hapus
{
   id: 1
}

POST http://api.server.com/pelanggan/daftar
{
   email: "ocelat@dd.com",
   nama: "ocelot"
}

POST http://api.server.com/pelanggan/hapus
{
   id: 1
}

Pada contoh di atas, setiap API masih dipanggil dengan request POST yang populer dari dulu. Biasanya, GET dipakai bila kita tidak ingin mengirim data tambahan seperti JSON. Bila ingin mengirimkan JSON atau mengirim data dalam jumlah besar, POST akan digunakan. Padahal selain GET dan POST, HTTP juga memiliki request method lainnya seperti HEAD, PUT, DELETE, TRACE, OPTIONS, CONNECT, dan PATCH.

Sebuah request yang sukses akan mengembalikan respon 200. Selain respon 200, HTTP juga memiliki definisi response status code yang lain seperti 500 untuk menandakan kesalahan, 201 untuk menandakan resource berhasil dibuat, dan sebagainya.

Pada Level 2, REST API akan memanfaatkan HTTP request method dan response status code. Sebagai contoh, API berikut ini masuk dalam kategori Level 2:

POST http://api.server.com/produk
{
   nama: "produk1",
   harga: 1000
}

PUT http://api.server.com/produk/1
{
   nama: "produk1 dengan nama baru",
   harga: 2000
}

DELETE http://api.server.com/produk/1

POST http://api.server.com/pelanggan
{
   email: "ocelat@dd.com",
   nama: "ocelot"
}

DELETE http://api.server.com/pelanggan/1

Pada Level 3, REST API akan menggunakan HATEOAS. Sebagai contoh, saya membuat REST API yang mendukung HATEOAS di artikel Membuat RESTFul Web Service Dengan Spring Data REST. Kenapa memakai HATEOAS? Beberapa keuntungan yang ditawarkannya antara lain:

  1. Klien tidak perlu men-hardcode link untuk melakukan operasi terhadap resource yang diterima.  Dengan demikian, perubahan nama atau lokasi API tidak menimbulkan perubahan di kode program klien.
  2. Klien dapat mengetahui operasi apa yang diperbolehkan dan tidak diperbolehkan terhadap resource.   Sebagai contoh, pada UI yang berisi daftar produk, bila tidak ada link untuk menghapus produk yang dikembalikan bersamaan dengan produk tersebut (misalnya karena user yang login tidak berhak untuk operasi tersebut), maka saya bisa menyembunyikan tombol hapus.

Sayangnya, hingga saat ini tidak ada implementasi yang standar untuk HATEOAS.  Salah satu yang menjanjikan adalah HAL (Hypertext Application Language).  Daftar library yang mengimplementasikan HAL dapat dijumpai di https://github.com/mikekelly/hal_specification/wiki/Libraries.

REST API dari Paypal dan Netflix adalah contoh API yang sudah mendukung HATEOAS.  Sementara itu, kebanyakan penyedia REST API publik lainnya hanya berada pada Level 2 karena mereka berfokus pada kemudahan penggunaan (sementara HATEOAS tidak begitu populer).   Perlu diingat ini bahwa mereka adalah API publik yang ditujukan untuk dikonsumsi oleh developer luar dari berbagai kalangan.  Fokus ini tentu akan berbeda, misalnya, pada saat merancang arsitektur microservices untuk dipakai dalam perusahaan.

Menyatukan Frontend dan Backend Pada Satu Proyek Yang Sama

Salah satu pertanyaan yang sering muncul pada saat akan memulai proyek baru dari awal adalah apakah kode program front end dan back end harus dipisahkan menjadi dua proyek yang berbeda? Proyek front end umumnya adalah proyek berbasis JavaScript yang menggunakan framework seperti Angular, Vue.js, React, dan sebagainya. Build tool yang umumnya digunakan oleh proyek front end meliputi Grunt, Yarn, Webpack, npm, dan sejenisnya. Sementara itu, proyek back end umumnya ditulis dalam Java, PHP, C#, dan sebagainya. Build tool yang umum meliputi Gradle, Ant, Composer, CMake, dan sebagainya. Karena dunia keduanya yang berbeda jauh, apakah pemisahan proyek secara fisik selalu merupakan pilihan yang tepat?

Tidak selamanya front end dan back end harus dipisahkan menjadi dua proyek berbeda. Bila anggota tim lebih banyak yang full stack, pemisahan proyek justru malah akan merepotkan. Metodologi pengembangan software seperti Scrum, misalnya, memiliki unit pekerjaan berupa story seperti “pelanggan bisa mendaftar baru” dan “pelanggan bisa melihat laporan laba rugi per tahun”. Agar bisa menyelesaikan story yang sedang dikerjakannya, developer umumnya harus mengubah front end dan juga back end.

Beberapa framework yang dogmatis mempermudah penggabungan kode program front end dan back end pada satu proyek yang sama. Sebagai contoh, Laravel 5.4 yang dilengkapi dengan Laravel Mix (berbasis Webpack) sangat mempermudah penggunaan front end Vue.js di dalam satu proyek Laravel yang sama. Lalu bagaimana dengan framework di dunia Java seperti Spring Boot? Walaupun tidak semudah di Laravel, saya akan mencoba membuat proyek Spring Boot (back end) + Webpack + Vue.js (front end) pada artikel ini.

Saya akan mulai dengan membuat sebuah proyek Spring Boot baru melalui Spring Initializr (https://start.spring.io). Setelah menjalankan IntelliJ IDEA, saya segera memilih New, Project…. Setelah memilih Spring Initializr, saya men-klik tombol Next. Karena saya ingin menggunakan Gradle, saya mengisi dialog yang muncul seperti yang terlihat pada gambar berikut ini:

Membuat Proyek Spring Boot Baru

Membuat Proyek Spring Boot Baru

Pada langkah berikutnya, saya memilih dependency yang diperlukan. Karena ini merupakan sebuah percobaan, saya hanya memberikan tanda centang pada Web. Setelah men-klik Next, saya bisa mengisi nama proyek dan lokasi penyimpanannya. Setelah men-klik Finish, IDEA akan men-download proyek dan menampilkan sebuah proyek Spring Boot yang masih kosong seperti yang terlihat pada gambar berikut ini:

Struktur Proyek Spring Boot Yang Baru Dibuat

Struktur Proyek Spring Boot Yang Baru Dibuat

Seandainya saja saya menggunakan front end yang ramah terhadap Java seperti Thymeleaf dan JSF, maka saya hanya perlu meletakkan kode program mereka ke direktori /src/main/webapp. Akan tetapi, ada kalanya saya perlu menggunakan front end yang lebih berat seperti Angular dan Vue.js. Keuntungannya adalah saya bisa memanfaatkan teknologi yang umum dipakai programmer front end seperti npm, Yarn, dan Webpack. Walaupun pada dunia Java sudah ada upaya menjadikan library front end ke dalam apa yang disebut sebagai Webjars, saya tetap merasa penggunaan npm lebih alami bagi pencinta JavaScript.

Bagaimana cara memasukkan proyek Vue.js ke dalam proyek Spring Boot yang baru saja dibuat ini? Cara yang paling adalah dengan menggunakan Vue Cli (https://github.com/vuejs/vue-cli). Jika ini adalah pertamakalinya saya menggunakan Vue Cli, saya perlu men-install-nya dengan memberikan perintah berikut ini:

$ npm install -g vue-cli

Setelah itu, masih berada di direktori proyek Spring Boot, saya memberikan perintah berikut ini:

$ vue init webpack frontend

Vue Cli akan menghasilkan sebuah proyek kosong untuk Vue.js berbasis Webpack seperti yang terlihat pada gambar berikut ini:

Proyek Vue.js di dalam proyek Spring Boot

Proyek Vue.js di dalam proyek Spring Boot

Perlu diperhatikan bahwa frontend hanyalah sebuah folder biasa. Sampai disini, saya bisa saja mulai bekerja dengan Vue.js dan memberikan perintah npm pada saat berada dalam folder frontend. Walaupun demikian, akan lebih baik bila saya bisa sama-sama mengendalikan front end dan back end melalui Gradle. Berhubung Gradle mendukung multi-projects, saya akan menjadikan folder frontend sebagai sebuah sub-project.

Langkah pertama yang saya lakukan adalah membuat sebuah file baru bernama build.gradle di folder frontend dengan isi seperti berikut ini:

plugins {
  id 'com.moowork.node' version '1.2.0'
  id 'java'
}

repositories {
  mavenCentral()
}

node {
  version = '8.1.3'
  yarnVersion = '0.27.5'
  download = true
}

task runBuild(dependsOn: yarn, type: YarnTask) {
  args = ['run', 'build']
}

task runDev(dependsOn: yarn, type: YarnTask) {
  args = ['run', 'dev']
}

jar {
  from project.projectDir.absolutePath + '/dist'
  eachFile { details ->
    details.path = details.path.startsWith('META-INF')?: 'static/' + details.path
  }
  includeEmptyDirs = false
}

jar.dependsOn runBuild

Pada file di atas, saya menggunakan plugin com.moowork.node yang akan men-download Node.js dan Yarn ke dalam folder .gradle. Sebagai informasi, Yarn adalah sebuah klien npm yang masih bisa menggunakan file package.json seperti klien npm standard. Salah satu kelebihan Yarn adalah ia memiliki penyimpanan global di folder ~/.cache/yarn (sama halnya dengan Gradle yang memiliki folder ~/.gradle). Bila saya membuat proyek baru yang memakai library yang sudah di-download sebelumnya, Yarn tidak akan membutuhkan koneksi internet lagi karena ia akan memakai yang sudah tersimpan di ~/.cache/yarn (proses build pun terasa lebih cepat karena tidak perlu men-download ulang). Ini berbeda dengan cache klien npm standard (~/.npm) yang hanya sementara dan tidak permanen. Karena Yarn kompatibel dengan klien npm standard, saya bisa mengganti perintah npm run build dengan yarn run build.

Spring Boot secara otomatis akan menyajikan file yang berada di folder /public dan /static sehingga bisa diakses oleh pengguna. Oleh sebab itu, file Gradle di atas akan menyalin seluruh file yang ada di folder dist ke dalam folder static di dalam file Jar yang dihasilkan nantinya.

Salah satu masalah yang saya jumpai pada proyek Vue.js yang dihasilkan oleh vue-cli adalah build scripts yang diletakkan di folder build. Hal ini akan menimbulkan sedikit kekacauan karena folder build memiliki arti berbeda di Gradle yang akan digunakan sebagai tempat output (misalnya file Jar yang dihasilkan Gradle akan diletakkan di folder ini). Oleh sebab itu, saya mengganti nama folder build menjadi buildScripts. Tidak lupa juga, saya men-update file package.json supaya menggunakan nama folder yang baru.

Berikutnya, pada proyek utama (Spring Boot), saya menambahkan sebuah file baru bernama settings.gradle yang isinya berupa:

rootProject.name = 'latihan'
include 'frontend'

Sebagai langkah terakhir, saya menambahkan dependency ke subproyek frontend di file build.gradle milik proyek utama, seperti pada:

dependencies {
   compile project(':frontend')
   ...
}

Sekarang, bila saya melihat daftar perintah Gradle yang bisa dijalankan untuk proyek ini, saya akan menjumpai seperti yang terlihat pada gambar berikut ini:

Daftar perintah Gradle untuk proyek ini

Daftar perintah Gradle untuk proyek ini

Mari jalankan proyek ini dengan men-double click bootRun dibagian application atau dengan memberikan perintah berikut ini:

$ ./gradlew bootRun
Starting a Gradle Daemon (subsequent builds will be faster)
:frontend:compileJava NO-SOURCE
:frontend:processResources NO-SOURCE
:frontend:classes UP-TO-DATE
:frontend:nodeSetup UP-TO-DATE
:frontend:yarnSetup UP-TO-DATE
:frontend:yarn UP-TO-DATE
:frontend:runBuild
yarn run v0.27.5
$ node buildScripts/build.js

Starting to optimize CSS...
Processing static/css/app.c6d9c9fc12c1dbaee77703a4dd731a8b.css...
Processed static/css/app.c6d9c9fc12c1dbaee77703a4dd731a8b.css, before: 431, after: 363, ratio: 84.22%
Hash: 424b3c987da691d210b2ING
Version: webpack 2.6.1ECUTING
Time: 5139msrunBuild
                                                  Asset       Size  Chunks             Chunk Names
                  static/js/app.eaedbed942638ee1e5dd.js    11.7 kB       0  [emitted]  app
               static/js/vendor.4e561224e1f68ab595ac.js     107 kB       1  [emitted]  vendor
             static/js/manifest.4373dcd698dd3ea60ed7.js    1.51 kB       2  [emitted]  manifest
    static/css/app.c6d9c9fc12c1dbaee77703a4dd731a8b.css  363 bytes       0  [emitted]  app
              static/js/app.eaedbed942638ee1e5dd.js.map    36.7 kB       0  [emitted]  app
static/css/app.c6d9c9fc12c1dbaee77703a4dd731a8b.css.map  910 bytes       0  [emitted]  app
           static/js/vendor.4e561224e1f68ab595ac.js.map     857 kB       1  [emitted]  vendor
         static/js/manifest.4373dcd698dd3ea60ed7.js.map    14.6 kB       2  [emitted]  manifest
                                             index.html  445 bytes          [emitted]  

  Build complete.

  Tip: built files are meant to be served over an HTTP server.
  Opening index.html over file:// won't work.

Done in 9.22s.
:frontend:jar UP-TO-DATE
:compileJava UP-TO-DATE
:processResources UP-TO-DATE
:classes UP-TO-DATE
:findMainClass
:bootRun

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v1.5.4.RELEASE)

2017-07-10 20:07:40.825  INFO 4098 --- [           main] com.example.latihan.LatihanApplication   : Starting LatihanApplication on desktop with PID 4098 (/home/user/IdeaProjects/latihan/build/classes/main started by user in /home/userIdeaProjects/latihan)
2017-07-10 20:07:40.828  INFO 4098 --- [           main] com.example.latihan.LatihanApplication   : No active profile set, falling back to default profiles: default
2017-07-10 20:07:40.913  INFO 4098 --- [           main] ationConfigEmbeddedWebApplicationContext : Refreshing org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@5f71c76a: startup date [Mon Jul 10 20:07:40 WIB 2017]; root of context hierarchy
2017-07-10 20:07:42.200  INFO 4098 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat initialized with port(s): 8080 (http)
2017-07-10 20:07:42.213  INFO 4098 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2017-07-10 20:07:42.215  INFO 4098 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet Engine: Apache Tomcat/8.5.15
2017-07-10 20:07:42.364  INFO 4098 --- [ost-startStop-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2017-07-10 20:07:42.366  INFO 4098 --- [ost-startStop-1] o.s.web.context.ContextLoader            : Root WebApplicationContext: initialization completed in 1456 ms
2017-07-10 20:07:42.478  INFO 4098 --- [ost-startStop-1] o.s.b.w.servlet.ServletRegistrationBean  : Mapping servlet: 'dispatcherServlet' to [/]
2017-07-10 20:07:42.482  INFO 4098 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'characterEncodingFilter' to: [/*]
2017-07-10 20:07:42.482  INFO 4098 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'hiddenHttpMethodFilter' to: [/*]
2017-07-10 20:07:42.482  INFO 4098 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'httpPutFormContentFilter' to: [/*]
2017-07-10 20:07:42.482  INFO 4098 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'requestContextFilter' to: [/*]
2017-07-10 20:07:42.776  INFO 4098 --- [           main] s.w.s.m.m.a.RequestMappingHandlerAdapter : Looking for @ControllerAdvice: org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@5f71c76a: startup date [Mon Jul 10 20:07:40 WIB 2017]; root of context hierarchy
2017-07-10 20:07:42.839  INFO 4098 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error],produces=[text/html]}" onto public org.springframework.web.servlet.ModelAndView org.springframework.boot.autoconfigure.web.BasicErrorController.errorHtml(javax.servlet.http.HttpServletRequest,javax.servlet.http.HttpServletResponse)
2017-07-10 20:07:42.841  INFO 4098 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error]}" onto public org.springframework.http.ResponseEntity<java.util.Map> org.springframework.boot.autoconfigure.web.BasicErrorController.error(javax.servlet.http.HttpServletRequest)
2017-07-10 20:07:42.867  INFO 4098 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/webjars/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2017-07-10 20:07:42.868  INFO 4098 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2017-07-10 20:07:42.902  INFO 4098 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**/favicon.ico] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2017-07-10 20:07:42.922  INFO 4098 --- [           main] oConfiguration$WelcomePageHandlerMapping : Adding welcome page: class path resource [static/index.html]
2017-07-10 20:07:43.017  INFO 4098 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Registering beans for JMX exposure on startup
2017-07-10 20:07:43.074  INFO 4098 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8080 (http)
2017-07-10 20:07:43.081  INFO 4098 --- [           main] com.example.latihan.LatihanApplication   : Started LatihanApplication in 2.57 seconds (JVM running for 3.034)
<============-> 92% EXECUTING
> :bootRun

Bila perintah ini dikerjakan pertama kalinya, ia akan men-download Node.js dan Yarn. Selain itu, ia juga akan men-download library di package.json yang belum ada di folder node_modules. Setelah itu, ia akan menjalankan Webpack yang secara tidak langsung bekerja melalui loader-nya. Sebagai contoh, vue-loader akan bekerja menerjemahkan komponen Vue dalam file .vue dan babel-loader akan melakukan transpiling sehingga saya bisa memakai ES2016 tanpa khawatir tidak bisa dijalankan oleh browser-browser populer. Setelah selesai, saya bisa membuka http://localhost:8080 untuk mengakses aplikasi.

Sampai disini, saya sudah membuat proyek baru yang menggabungkan kode program front end dan back end. Saya juga memakai Gradle untuk mengendalikan kode program front end (yang secara tidak langsung akan memakai Yarn dan Webpack yang dipakai oleh proyek front end).

Bagaimana dengan deployment? Bila saya memberikan perintah:

$ ./gradlew war

Saya akan memperoleh sebuah file War bernama latihan-0.0.1-SNAPSHOT.war di lokasi build/libs. File ini dapat langsung di-deploy di application server produksi (misalnya Tomcat atau JBoss). Di dalam file War ini sudah termasuk proyek front end saya yang bisa ditemukan di WEB-INF\lib\frontend.jar. Jadi, Gradle akan membuat Jar untuk front end terlebih dahulu. Setelah itu, Jar akan dipaketkan bersama-sama dengan Jar lain yang dibutuhkan oleh back end di WEB-INF\lib.

Memakai Graph Database Dengan Neo4j

Selama beberapa dekade terakhir ini, database relasional berbasis tabel boleh dibilang merupakan jenis database yang paling sering dipakai dan paling dewasa. Database relasional tidak akan hilang dalam jangka waktu dekat ini, tapi mereka memiliki keterbatasan sendiri. Sebagai contoh, database relasional akan kewalahan melakukan query yang dibutuhkan untuk keperluan jejaring sosial seperti Facebook dan Twitter. Database relasional memang memiliki JOIN untuk menggabungkan tabel, tapi JOIN yang mereka miliki tidak memiliki arah dan label. Salah satu jenis database yang diciptakan untuk mengembalikan makna ‘relasional’ tersebut adalah graph database.

Graph yang dimaksud pada graph database adalah definisi graph pada graph theory yang merupakan bagian dari ilmu matematika diskrit. Paper pertama yang membahas graph theory ditulis oleh Leonhard Eulear pada tahun 1736 yang membahas tentang permasalahan Konigsberg Bridge.

Pada kesempatan ini, saya akan mencoba menggunakan sebuah graph database populer, Neo4j. Saya harus mulai dari mana? Pada database relasional, saya bisa mulai dengan merancang tabel dengan merancang skema dengan menggunakan ERD. Berbeda dari database relasional, sebuah graph database tidak memiliki skema seperti tabel dan kolom yang berbeda dengan isi (record). Segala sesuatunya adalah data! Oleh sebab itu, pada saat merancang untuk graph database, saya bisa langsung memakai contoh data. Sebagai latihan, saya akan membuat database yang menampung produk yang dibeli oleh pengguna seperti berikut ini:

Contoh rancangan untuk graph database

Contoh rancangan untuk graph database

Pada rancangan di atas, masing-masing lingkaran mewakili sebuah node. Setiap node terhubung ke node lain melalui garis yang memiliki arah yang disebut sebagai edge. Baik node maupun edge boleh memiliki properties. Berkebalikan dari database relasional, nilai properties pada node atau edge akan lebih baik bila sebisa mungkin di-‘normalisasi’ menjadi node sehingga bisa dipakai secara efisien di query.

Setelah menentukan rancangan database, sekarang saatnya untuk memasukkan data. Saya sudah menyiapkan beberapa data dalam bentuk CSV untuk dimasukkan sebagai isi dari database Neo4j. Saya akan mulai dengan membuat seluruh node yang mewakili produk. Untuk itu, setelah menjalankan Neo4j, saya membuka browser dan menampilkan http://localhost:7474. Ini adalah halaman web dimana saya bisa mengelola database Neo4j dan juga mengerjakan query (dalam bentuk Cypher). Sebagai contoh, saya memberikan perintah seperti pada gambar berikut ini:

Mengisi data dari file CSV

Mengisi data dari file CSV

Saya juga melakukan hal yang sama untuk menciptakan node yang mewakili pengguna, misalnya dengan memberikan perintah Cypher berikut ini:

USING PERIODIC COMMIT
LOAD CSV WITH HEADERS FROM "file:C:/user.csv" AS row
CREATE (:User {id: row.id});

Karena kedua node ini akan sering dicari berdasarkan id, maka saya bisa memberikan index. id bersifat unik sehingga saya bisa menggunakan perintah CREATE CONSTRAINTS yang akan menciptakan unique index yang lebih cepat dibandingkan index biasa. Sebagai contoh, saya memberikan perintah berikut ini:

CREATE CONSTRAINT ON (p:Produk) ASSERT p.id IS UNIQUE;
CREATE CONSTRAINT ON (u:User) ASSERT u.id IS UNIQUE

Sekarang adalah saatnya untuk membuat edge BELI dari User ke Produk. Saya melakukannya dengan memberikan perintah seperti berikut ini:

USING PERIODIC COMMIT
LOAD CSV WITH HEADERS FROM "file:C:/buys.csv" AS row
MATCH (user:User {id: row.user_id})
MATCH (produk:Produk {id: row.produk_id})
MERGE (user)-[:BELI]->(produk);

Setelah ini, saya bisa mulai belajar mencari data. Bila pada database relasional terdapat SQL, maka Neo4j menggunakan query khusus yang disebut sebagai Cypher. Bahasa query ini juga deklaratif seperti SQL dan dirancang agar mudah dipahami. Sebagai contoh, untuk menampilkan 25 node user pertama, saya dapat menggunakan Cyhper seperti:

MATCH (n:User) RETURN n LIMIT 25;
Hasil query dalam bentuk tabel

Hasil query dalam bentuk tabel

Tidak ada yang berbeda dari database relasional bila node dikembalikan dalam bentuk tabel. Agar lebih menarik, saya bisa menampilkan hasil query dalam bentuk grafis seperti yang terlihat pada gambar berikut ini:

Hasil query dalam bentuk graph

Hasil query dalam bentuk graph

Tidak seperti hasil query dari database relasional yang sulit dicerna secara langsung, hasil query pada graph database cukup menarik untuk diamati. Pengguna juga bisa menemukan pola data secara visual.

Contoh lain dari query Cypher adalah:

MATCH (u:User {id:"xxx"})-[:BELI]->(p:Produk)
RETURN p;

Klausa MATCH pada query di atas akan mencari node User yang memiliki id "xxx" dan memiliki asosiasi BELI terhadap node Produk. Setelah itu, klausa RETURN akan mengembalikan seluruh node Produk yang ada. Dengan kata lain, query ini akan mengembalikan seluruh produk yang dibeli oleh pengguna dengan id "xxx". Tentu saja query seperti ini juga bisa dilakukan secara mudah dengan menggunakan SQL di database relasional.

Untuk menunjukkan sesuatu yang tidak gampang dilakukan melalui SQL, saya akan memberikan query Cypher seperti berikut ini:

MATCH (u1:User)-[:BELI]->(p:Produk)<-[:BELI]-(u2:User) 
RETURN p;

Hasilnya akan terlihat seperti pada gambar berikut ini:

Hasil query dalam bentuk graph

Hasil query dalam bentuk graph

Dengan menggunakan arah panah seperti -[:BELI]-> dan <-[:BELI]-, query di atas akan mengembalikan seluruh node Produk yang minimal sudah dibeli oleh 2 pengguna. Karena saya tidak peduli dengan node User pada query di atas, saya bisa mengabaikannya dengan mengubah query menjadi seperti berikut ini:

MATCH ()-[:BELI]->(p:Produk)<-[:BELI]-()
RETURN p;

Selain mengembalikan node, saya juga bisa mengembalikan property sehingga memperoleh hasil dalam bentuk tabel. Sebagai contoh, saya bisa memberikan query seperti berikut ini:

MATCH (u1:User)-[:BELI]->(p:Produk)<-[:BELI]-(u2:User)
RETURN u1.id AS user1, p.url AS produk, u2.id AS user2;

Ingin sesuatu yang lebih sulit dilakukan melalui query SQL? Sebagai contoh, saya akan mencari produk lain yang dibeli oleh pengguna lain yang juga membeli produk yang sama dengan seorang pengguna dengan menggunakan query seperti berikut ini:

MATCH (u1:User)-[:BELI]->(p1:Produk)<-[:BELI]-(u2:User),
      (u2)-[:BELI]->(p2:Produk)
RETURN p1, p2;
Hasil query dalam bentuk graph

Hasil query dalam bentuk graph

Karena graph database memang dirancang untuk keperluan seperti Cypher di atas, ia bukan hanya menawarkan kemudahan, tapi juga kinerja yang lebih baik dibandingkan query serupa yang memakai database relasional. Walaupun penelusuran tetapi dilakukan satu per satu seperti pada JOIN di database relasional, setiap node bisa memiliki path yang berbeda. Begitu path untuk node sudah melenceng dari yang diharapkan, ia bisa segera diabaikan sehingga penelusuran bisa segera dilanjutkan.

Saya juga menggabungkan MATCH di Cypher di atas sehingga menjadi seperti berikut ini:

MATCH (u1:User)-[:BELI]->(p1:Produk)<-[:BELI]-(u2:User)-[:BELI]->(p2:Produk)
RETURN p1, p2;

Tergantung pada selera, versi yang terakhir ini mungkin lebih mudah dipahami dibandingkan dengan versi sebelumnya.

Sama seperti di SQL, untuk mebatasi query di Cypher saya juga dapat menggunakan klausa WHERE. Sebagai contoh, bila saya hanya tertarik pada produk dengan id 'xxx', maka saya bisa memberikan query seperti berikut ini:

MATCH (u1:User)-[:BELI]->(p1:Produk)<-[:BELI]-(u2:User)-[:BELI]->(p2:Produk)
WHERE p1.id = "xxx"
RETURN u1, p1, u2, p2;
Hasil query dalam bentuk graph

Hasil query dalam bentuk graph

Pada gambar di atas, id 5969 adalah nilai internal yang diberikan untuk produk 'xxx'. Terlihat bahwa user seperti user 329, 3953, 5149 dan sejenisnya membeli produk 'xxx' bersamaan dengan produk lain.

Pada query diatas, akan ada banyak produk ganda yang ditampilkan karena beberapa pengguna berbeda bisa saja melihat banyak produk yang sama. Sama seperti di SQL, untuk mengembalikan hanya produk yang unik, saya dapat menggunakan klausa DISTINCT seperti pada contoh berikut ini:

MATCH (u1:User)-[:BELI]->(p1:Produk)<-[:BELI]-(u2:User)-[:BELI]->(p2:Produk)
WHERE p1.id = "xxx" 
RETURN DISTINCT p1, u2, p2;

Membuat Fitur “Produk Serupa” Dengan PredictionIO

PredictionIO (https://prediction.io) adalah sebuah engine yang dirancang untuk mudah dipakai dalam menerapkan machine learning. Lalu apa manfaatnya? Produk ini tidak hanya berguna untuk peneliti, tetapi juga bisa diterapkan langsung pada situs e-commerce. Salah satunya adalah PredictionIO bisa digunakan untuk menghasilkan daftar “produk serupa” berdasarkan perilaku dari pengunjung lain. Tanpa machine learning, pembuat situs biasanya menampilkan produk yang acak berdasarkan tag tertentu. Dengan menghasilkan daftar “produk serupa” secara pintar, pengguna akan memperoleh saran yang lebih akurat sehingga penjualan bisa meningkat, terutama bagi situs e-commerce yang memiliki banyak produk bervariasi.

Sebagai latihan, saya akan menjalankan engine PredictionIO pada sebuah server Linux yang terpisah dari server web. Salah satu kelebihan PredictionIO adalah masing-masing algoritma dikelompokkan dalam apa yang disebut dengan engine template. Saya dapat menemukan daftar engine template siap pakai di https://templates.prediction.io. Sesungguhnya sebuah engine template bukan hanya mengandung algoritma, tetapi komponen lengkap yang disebut DASE (Data, Algorithm, Serving, Evaluator). Developer juga bisa memodifikasi kode program engine template (dalam bahasa Scala) sesuai dengan kebutuhan.

Karena ingin memunculkan “produk serupa”, saya akan menggunakan template PredictionIO/template-scala-parallel-complementarypurchase yang dapat dijumpai di https://templates.prediction.io/PredictionIO/template-scala-parallel-complementarypurchase. Berdasarkan informasi dari dokumentasi, algoritma yang dipakai oleh engine template ini mengikuti konsep association rule learning (https://en.wikipedia.org/wiki/Association_rule_learning).

Sebagai langkah awal, saya memberikan perintah berikut ini untuk men-download kode program engine template:

$ pio template get PredictionIO/template-scala-parallel-complementarypurchase latihanEngine
Please enter author's name: latihan
Please enter the template's Scala package name (e.g. com.mycompany): com.latihan
Please enter author's e-mail address: 
Author's name:         latihan
Author's e-mail:       
Author's organization: com.latihan
Would you like to be informed about new bug fixes and security updates of this template? (Y/n) n
Retrieving PredictionIO/template-scala-parallel-complementarypurchase
There are 5 tags
Using tag v0.3.1
Going to download https://github.com/PredictionIO/template-scala-parallel-complementarypurchase/archive/v0.3.1.zip
Redirecting to https://codeload.github.com/PredictionIO/template-scala-parallel-complementarypurchase/zip/v0.3.1
Replacing org.template.complementarypurchase with com.latihan...
Processing latihanEngine/build.sbt...
Processing latihanEngine/engine.json...
Processing latihanEngine/src/main/scala/Algorithm.scala...
Processing latihanEngine/src/main/scala/DataSource.scala...
Processing latihanEngine/src/main/scala/Engine.scala...
Processing latihanEngine/src/main/scala/Preparator.scala...
Processing latihanEngine/src/main/scala/Serving.scala...
Engine template PredictionIO/template-scala-parallel-complementarypurchase is now ready at latihanEngine

Perintah di atas akan menciptakan sebuah folder bernama latihanEngine yang berisi kode program yang bisa saya modifikasi sesuai keperluan. Karena akan bekerja pada folder ini, maka saya perlu pindah ke folder ini dengan memberikan perintah:

$ cd latihanEngine

Setelah itu, saya siap untuk menjalankan PredictionIO dengan memberikan perintah seperti berikut ini:

$ pio-start-all

Saya bisa memerika apakah semua komponen dijalankan dengan baik dengan menggunakan perintah seperti berikut ini:

$ pio status
PredictionIO
  Installed at: /home/snake/PredictionIO
  Version: 0.9.2

Apache Spark
  Installed at: /home/snake/PredictionIO/vendors/spark-1.3.0
  Version: 1.3.0 (meets minimum requirement of 1.3.0)

Storage Backend Connections
  Verifying Meta Data Backend
  Verifying Model Data Backend
  Verifying Event Data Backend
  Test write Event Store (App Id 0)
[INFO] [HBLEvents] The table predictionio_eventdata:events_0 doesn't exist yet. Creating now...
[INFO] [HBLEvents] Removing table predictionio_eventdata:events_0...

(sleeping 5 seconds for all messages to show up...)
Your system is all ready to go.

Perintah di atas memerika apakah komponen yang dipakai oleh PredictionIO semuanya sudah siap dipakai. Terlihat bahwa PredictionIO memakai Apache Spark untuk mengerjakan algoritma secara paralel dan scalable. Selain itu, data yang dikumpulkan akan disimpan ke dalam Apache HBase. Ini adalah sebuah database No-SQL yang dirancang untuk dipakai pada HDFS (Hadoop File System).

Langkah awal pada machine learning adalah mengumpulkan data sebanyak mungkin untuk keperluan training. PredictionIO akan membuat sebuah web service server berbasis REST yang dapat dipakai oleh aplikasi web untuk memberikan data. Untuk itu, saya perlu memperoleh sebuah access key terlebih dahulu dengan memberikan perintah:

$ pio app new LatihanWeb
[INFO] [HBLEvents] The table predictionio_eventdata:events_2 doesn't exist yet. Creating now...
[INFO] [App$] Initialized Event Store for this app ID: 2.
[INFO] [App$] Created new app:
[INFO] [App$]       Name: LatihanWeb
[INFO] [App$]         ID: 2
[INFO] [App$] Access Key: A7iLxxkf7RGBpiTtxMGu8zi4EZ0Kfcox05HTwYDCteCn5weqtdnoGyEaRCRYX2CM

Bagian yang paling penting dari output di atas adalah access key yang perlu saya berikan saat memanggil event server yang diciptakan oleh PredictionIO. Untuk memastikan event server berjalan dengan baik, saya bisa mengakses URL seperti http://192.168.10.100:7070 dari browser dimana 192.168.10.100 adalah IP server yang menjalankan PredictionIO. Saya akan memperoleh hasil seperti {"status":"alive"}. Bila memakai terminal, saya juga bisa menggunakan perintah curl seperti berikut ini:

$ curl 192.168.10.100:7070
{"status":"alive"}

Sekarang, saya siap untuk mengirimkan event kepada event server. Sebagai latihan, saya akan memakai access logs untuk web distribution dari Amazon CloudFront milik sebuah situs. Saya hanya tertarik pada kolom ke-12 (cs-uri-query) dimana saya mengambil nilai parameter item_id yang mewakili item yang dilihat pengguna dan visitor_id yang mewakili pengenal unik untuk pengguna tersebut. Engine complementary purchase yang saya pakai secara default mendukung event buy yang membutuhkan parameter berupa user dan item. Agar sederhana, saya tidak akan melakukan perubahan kode program dan menganggap event buy tersebut sama seperti view. Saya kemudian membuat sebuah script Perl untuk membaca dan mengakses event server seperti berikut ini:

#!/usr/bin/perl

$LOGFILE = "access.log";
open(LOGFILE) or die("Tidak dapat membaca $LOGFILE");
while () {
  next if 1..2;
  $uri = (split(' ', $_))[11];
  if ($uri =~ m/item_id%253D(\w+)%2526/) {
    $itemId = $1;
  } else {
    print "Tidak dapat menemukan item_id di $_\n";
  }
  if ($uri =~ m/visitor_id%253D([\w-]+)&/) {
    $browserId = $1;
  } else {
    print "Tidak dapat menemukan visitor_id di $_\n";
  }
  ($postEvent = <<"CURL") =~ s/\n+//gm;
curl -i -X POST http://192.168.10.100:7070/events.json?accessKey=A7iLxxkf7RGBpiTtxMGu8zi4EZ0Kfcox05HTwYDCteCn5weqtdnoGyEaRCRYX2CM \
-H "Content-Type: application/json" \
-d '{
  "event": "buy",
  "entityType": "user",
  "entityId": "$browserId",
  "targetEntityType": "item",
  "targetEntityId": "$itemId"
}'
CURL
  system($postEvent);
}

Script di atas pada dasarnya akan menggunakan curl untuk menambahkan event pada event server. Seluruh event yang terkumpul akan diletakkan pada database Apache HBase. Untuk melihat apakah event sudah tersimpan dengan baik, saya bisa membuka URL seperti http://192.168.10.100:7070/events.json?accessKey=A7iLxxkf7RGBpiTtxMGu8zi4EZ0Kfcox05HTwYDCteCn5weqtdnoGyEaRCRYX2CM melalui browser.

Bila selama percobaan, event yang diberikan tidak benar atau perlu dihapus, saya bisa menggunakan perintah seperti berikut ini:

$ pio app data-delete LatihanWeb
[INFO] [App$] Data of the following app (default channel only) will be deleted. Are you sure?
[INFO] [App$]     App Name: LatihanWeb
[INFO] [App$]       App ID: 2
[INFO] [App$]  Description: None
Enter 'YES' to proceed: YES
[INFO] [HBLEvents] Removing table predictionio_eventdata:events_2...
[INFO] [App$] Removed Event Store for this app ID: 2
[INFO] [HBLEvents] The table predictionio_eventdata:events_2 doesn't exist yet. Creating now...
[INFO] [App$] Initialized Event Store for this app ID: 2.
[INFO] [App$] Done.

Setelah event terkumpul, sekarang saatnya untuk menjalankan engine. Tapi sebelumnya, saya perlu membuka file engine.json dan menguna nilai appName menjadi latihanWeb (sesuai dengan nama yang sama berikan saat mengerjakan pio app new). Setelah itu, saya memberikan perintah berikut ini:

$ pio build --verbose
...
[INFO] [Console$] Your engine is ready for training.

Perintah di atas akan men-download dependency Ivy yang dibutuhkan (dengan menggunakan Scala sbt, sejenis Gradle di Groovy) dan men-compile kode program yang sebelumnya dihasilkan oleh engine template. Setelah proses building selesai, langkah berikutnya adalah melakukan training untuk predictive model dengan memberikan perintah ini:

$ pio train
...
[INFO] [CoreWorkflow$] Training completed successfully.

Perintah ini akan menjalankan proses training di Apache Spark. Bila terdapat kesalahan yang berkaitan dengan java.lang.StackOverflowError, maka stack size bisa ditingkatkan menjadi lebih besar, misalnya 16 MB dengan membuat environment variable bernama SPARK_JAVA_OPTS yang memiliki isi seperti -Xss16M.

Setelah proses training selesai, saya bisa men-deploy engine agar hasilnya dapat diakses melalui REST. Sebagai contoh, saya memberikan perintah berikut ini:

$ pio deploy
...
[INFO] [MasterActor] Bind successful. Ready to serve.

Untuk memastikan engine sudah berjalan dengan baik, saya bisa mencoba mengakses URL seperti http://192.168.10.100:8000. Bila hasilnya muncul seperti harapan, maka saya siap memodifikasi aplikasi web untuk mengakses engine ini.

Untuk mendapatkan rekomendasi produk sejenis, server web (atau server lain yang membutuhkan) perlu mengirimkan JSON yang mengandung sebuah id item di key "items" dan jumlah rekomendasi yang dibutuhkan di key "num". Sebagai contoh, saya bisa mengakses engine seperti berikut ini:

$ curl -H "Content-Type: application/json" -d '{"items": ["xxx"], "num": 3}' http://192.168.10.100:8000/queries.json
{
  "rules": [
    {
     "cond": ["xxx"], 
     "itemScores":[
        {"item":"hasil1", "support":3.92541707556427E-4, "confidence":0.166666,"lift":424.583333},
        {"item":"hasil2","support":3.925417075564279E-4,"confidence":0.166666,"lift":424.58333333},
        {"item":"hasil3", "support":3.925417075564279E-4,"confidence":0.1666666,"lift":424.58333333}]
    }
  ]
}

Sebagai contoh, pada hasil yang saya peroleh, 3 rekomendasi untuk sebuah produk iPhone 5s adalah produk ZenFone 2, iPhone 5s dari penjual berbeda, dan iPhone 6. Hasil ini cukup masuk akal secara sekilas. Yang perlu diperhatikan adalah hasil ini tidak menyertakan riwayat produk yang sudah pernah dilihat oleh user tersebut, melainkan rekomendasi produk serupa berdasarkan pola kunjungan pengguna lain.

Bila mencoba dengan data dengan variasi kunjungan yang terbatas, misalnya snapshot untuk beberapa menit dimana masing-masing item barang hanya dikunjungi satu dua kali oleh pengguna, saya bisa mengubah parameter algoritma di engine.json menjadi seperti berikut ini:

"algorithms": [
    {
      "name": "algo",
      "params": {
        "basketWindow" : 300,
        "maxRuleLength" : 2,
        "minSupport": 0,
        "minConfidence": 0,
        "minLift" : 0,
        "minBasketSize" : 2,
        "maxNumRulesPerCond": 5
      }
    }
]

Mengubah nilai minSupport, minConfidence dan minLift menjadi 0 akan mengurangi kualitas hasil yang diperoleh. Sebagai contoh, nilai default untuk minSupport adalah 0.1 yang berarti item harus muncul minimal 10% untuk seluruh transaksi agar ia disertakan pada hasil. Oleh sebab itu, pengaturan seperti ini sebaiknya tidak dipakai pada kasus nyata.

Belajar Memakai Specification Di Spring Data JPA

Pada artikel Memakai Querydsl Dengan Spring Data JPA, saya menunjukkan bagaimana memakai Querydsl di Spring Data JPA. Pada kesempatan kali ini, saya akan mencoba membuat query dinamis dengan menggunakan fasilitas bawaan Spring Data JPA yaitu Specification. Karena interface ini merupakan bawaan dari Spring Data JPA, saya tidak perlu lagi menambahkan referensi ke Jar eksternal.

Mengapa perlu membuat query secara dinamis? Sebagai contoh, saya memiliki sebuah tabel PrimeFaces di facelet seperti berikut ini:

<p:dataTable var="vProduk" value="#{entityList}" selection="#{entityList.selected}" selectionMode="single" rowKey="#{vProduk.id}" 
    paginator="true" rows="10" id="tabelProduk" lazy="true" filterDelay="1000" resizableColumns="true">
    <p:column headerText="Kode" sortBy="#{vProduk.kode}" filterBy="#{vProduk.kode}">
        <h:outputText value="#{vProduk.kode}" />
    </p:column>
    <p:column headerText="Nama" sortBy="#{vProduk.nama}" filterBy="#{vProduk.nama}">
    <h:outputText value="#{vProduk.nama}" />
    </p:column>
    <p:column headerText="Jenis" sortBy="#{vProduk.jenis.nama}" filterBy="#{vProduk.jenis}" filterOptions="#{filterJenisProduk}" width="150">              
        <h:outputText value="#{vProduk.jenis.nama}" />
    </p:column>
    <p:column headerText="Harga (Rp)" sortBy="#{vProduk.harga}" filterBy="#{vProduk.harga}">
        <h:outputText value="#{vProduk.harga}" style="float: right">
            <f:convertNumber type="number" />
        </h:outputText>
    </p:column>
    <p:column headerText="Qty" sortBy="#{vProduk.qty}" filterBy="#{vProduk.qty}" width="100">
        <h:outputText value="#{vProduk.qty}" style="float: right">
            <f:convertNumber type="number" />
        </h:outputText>
    </p:column>               
    <p:ajax event="rowSelect" update=":mainForm:buttonPanel" />               
</p:dataTable>

Tampilan HTML untuk tabel di atas akan terlihat seperti pada gambar berikut ini:

Tabel dengan fasilitas filtering per kolom

Tabel dengan fasilitas filtering per kolom

Pada tampilan di atas, pengguna bisa melakukan pencarian secara bebas di setiap kolom. Sebagai contoh, pengguna bisa mencari berdasarkan kode dan jenis. Selain itu, pengguna juga bisa mencari berdasarkan kode dan jenis dan harga. Atau, pengguna juga bisa memilih mencari berdasarkan jenis dan harga. Kombinasi seperti ini akan sangat banyak bila ingin dibuat query-nya secara manual satu per satu. Ini adalah contoh kasus yang tepat untuk memakai fasilitas seperti Querydsl dan Specification.

Karena filter table seperti ini sering digunakan pada tabel yang berbeda, saya akan mulai dengan membuat sebuah implementasi Specification yang bisa dipakai ulang seperti pada kode program berikut ini:

public class FilterSpecification<T> implements Specification<T> {

  public enum Operation { EQ, LIKE, MIN };

  private List<Filter> filters = new ArrayList<>();

  public void and(String name, Operation op, Object...args) {
    filters.add(new Filter(name, op, args));
  }

  public void and(Filter filter) {
    filters.add(filter);
  }

  @Override
  public Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder cb) {              
    Predicate result = cb.conjunction();
    for (Filter filter: filters) {
      result = cb.and(result, filter.toPredicate(cb, root));            
    }
    return result;      
  }

  class Filter {

    private String name;
    private Operation op;
    private Object[] args;

    public Filter(String name, Operation op, Object... args) {
      this.name = name;
      this.op = op;
      this.args = args;
    }

    @SuppressWarnings({ "unchecked", "rawtypes" })
    public Predicate toPredicate(CriteriaBuilder cb, Root<T> root) {
      Expression path = toPath(root, name);
      switch (op) {
      case EQ:
        return cb.equal(path, args[0]);
      case LIKE:                                
        return cb.like(path, (String) args[0]);
      case MIN: 
        return cb.greaterThanOrEqualTo(path, Integer.parseInt((String) args[0]));
      default:
        return null;
      }
    }

    @SuppressWarnings("rawtypes")
    public Expression toPath(Root root, String name) {
      Path result = null;
      if (name.contains("_")) {
        for (String node: name.split("_")) {
          node = StringUtils.uncapitalize(node);                    
          if (result == null) {
            result = root.get(node);
          } else {
            result = result.get(node);
          }
        }
      } else {
        result = root.get(name);
      }
      return (Expression) result;
    }

  }     

}

Setiap kali pengguna mengetik (atau memilih) kriteria filter di tabel, setelah delay selama 1000 ms, method load() di LazyDataModel akan dipanggil. Method ini perlu melakukan query di database untuk memperoleh hasil sesuai kriteria yang diberikan. Untuk itu, saya bisa menggunakan FilterSpecification yang saya buat seperti pada kode program berikut ini:

FilterSpecification<Produk> filterSpecification = new FilterSpecification<>();
if (getFilter("kode") != null) {
  filterSpecification.and("kode", Operation.LIKE, getFilterAsSearchExpr("kode"));
}
if (getFilter("nama") != null) {
  filterSpecification.and("nama", Operation.LIKE, getFilterAsSearchExpr("nama"));
}
if (getFilter("jenis") != null) {
  filterSpecification.and("jenis_id", Operation.EQ, getFilter("jenis"));
}
if (getFilter("harga") != null) {
  filterSpecification.and("harga", Operation.MIN, (String) getFilter("harga"));
}
if (getFilter("qty") != null) {
  filterSpecification.and("qty", Operation.MIN, (String) getFilter("qty"));
}

Method and() akan mendaftarkan sebuah Filter baru yang berisi nama path, operasi dan nilai pencarian. Untuk mengakses path di dalam path lain, saya menggunakan tanda garis bawah. Kode program yang memungkinkan hal ini terjadi dapat dilihat di method FilterSpecification.toPath().

Method toPredicate() hanya akan dipanggil pada saat Specification ini dipakai oleh salah satu method di repository Spring Data JPA. Agar bisa memakai Specification di repository Spring Data JPA, saya perlu menurunkan interface repository dari JpaSpecificationExecutor seperti yang terlihat pada kode program berikut ini:

public interface ProdukRepository extends JpaRepository<Produk, Long>, JpaSpecificationExecutor<Produk> {}

Sekarang, saya dapat memakai salah satu method yang mendukung Specification di ProdukRepository. Sebagai contoh, saya bisa melakukan filtering dengan menggunakan Specification sekaligus memperoleh hasil per halaman dengan Pageable dengan memanggil repository seperti berikut ini:

produkRepository.findAll(filterSpecification, pageable);

Dengan kode program yang sederhana dan mudah dipahami ini, saya kini bisa melakukan pencarian untuk kombinasi kolom berbeda. Sebagai contoh, bila saya mencari nama produk yang mengandung kata RING KEH dengan jenis HGP dan harga minimal Rp 100.000, saya akan memperoleh hasil seperti pada gambar berikut ini:

Melakukan filtering pada tabel

Melakukan filtering pada tabel

Belajar Membuat Aplikasi Web Dengan Spring Framework

Banyak sekali pemula yang kewalahan dalam mempelajari pemograman web di Java karena kewalahan dengan sekian banyak pilihan yang ada. Walaupun Java EE 7 kini sudah semakin mirip dengan Spring, untuk saat ini ia masih perlu banyak belajar lagi agar semakin dewasa. Saat ini tidak semua server Java EE 7 memiliki perilaku yang sama. Sebagai contoh, unit testing yang melibatkan CDI dan EJB begitu mudah dilakukan di server GlassFish karena ia mendukung EJBContainer untuk menjalankan embeddable container. Server WildFly tampaknya tidak menganut filosofi serupa dan lebih menyarankan penggunaan Arquillian untuk keperluan pengujian. Masalah perbedaan akan lebih sering muncul bila memakai Java EE API yang jarang dibahas seperti JMS.

Bila Java EE 7 adalah jalur resmi, maka teknologi dari Spring adalah jalur tidak resmi yang masih tetap bertahan sampai sekarang. Boleh dibilang CDI dari Java EE 7 menawarkan banyak fasilitas yang bersaing dengan Spring Container. Lalu kenapa masih memakai Spring? Karena Spring memiliki ekosistem yang luas dan dewasa. Memakai Spring ibarat masuk ke sebuah jeratan dunia baru yang beda sendiri. Spring dapat dijalankan pada server apa saja asalkan mendukung servlet (misalnya server Tomcat yang lawas).

Pada kesempatan ini, saya akan membuat sebuah aplikasi web sederhana dengan menggunakan Spring dan beberapa API dari Java EE 7 yang di-deploy pada server Tomcat. Aplikasi ini tidak membutuhkan server khusus untuk Java EE 7. Kode program lengkap untuk aplikasi latihan ini dapat dijumpai di https://github.com/JockiHendry/basic-inventory-web. Hasil akhirnya dapat dijumpai di https://basicinventory-latihanjocki.rhcloud.com/app/home.

Aplikasi ini memakai Gradle untuk mengelola proses siklus hidup seperti building. Sebagai contoh, bila saya memberikan perintah gradle build, sebuah file bernama ROOT.war akan dihasilkan untuk di-deploy pada servlet container seperti Tomcat. Sebagai informasi, tidak seperti di PHP yang berbasis file, sebuah aplikasi web di Java berada dalam bentuk file tunggal yang berakhir *.war (web archive). File ini adalah sebuah file ZIP yang berisi seluruh file yang dibutuhkan untuk menjalankan aplikasi dan juga metadata seperti nama dan versi aplikasi. Gradle juga secara otomatis men-download JAR yang dibutuhkan. Pada latihan ini, seluruh JAR yang saya pakai dapat dilihat di build.gradle di bagian dependencies.

Di dunia ini tidak ada teknologi yang sempurna! Suatu hari nanti, saya mungkin perlu mengganti salah satu teknologi yang dipakai dengan yang berbeda. Saya mungkin suatu hari ini ingin beralih dari JSF ke GWT. Atau, mungkin saya ingin berpindah dari database relational menjadi database NoSQL seperti MongoDB. Tentu saja saya tidak ingin sampai harus membuat ulang aplikasi dari awal. Untuk itu, saya berjaga-jaga dengan menggunakan arsitektur layering dimana saya mengelompokkan kode program ke dalam layer yang berbeda sesuai dengan perannya. Sebagai contoh, saya membuat package seperti pada gambar berikut ini:

Struktur package di aplikasi web

Struktur package di aplikasi web

Seluruh kode program yang berkaitan dengan presentation layer terletak di package com.jocki.inventory.view. Sementara itu, seluruh kode program yang berkaitan dengan service layer berada di package com.jocki.inventory.service. Sisanya, kode program yang berkaitan dengan persistence layer (mengakses database secara langsung) berada di package com.jocki.inventory.repository.

Salah satu syarat dalam layering adalah layer yang satu hanya bisa mengakses layer sesudahnya atau sebelumnya. Pada contoh ini, presentation layer hanya bisa mengakses service layer secara langsung. Setelah itu, service layer mengakses persistence layer guna membaca data dari database. Walaupun demikian, saya juga tidak begitu kaku. Misalnya, karena memakai Spring Data JPA pada persistence layer, saya mengizinkan presentation layer untuk memanggil finders secara langsung dari persistence layer tanpa melalui service layer.

Package com.jocki.inventory.domain berisi domain class yang bisa diakses oleh layer mana saja. Isi dari package ini adalah JPA entity yang mewakili domain, misalnya Konsumen, Supplier dan Sales. Karena saya ingin mengakses domain class secara langsung termasuk di presentation layer, ada baiknya saya membuat seluruh domain class menjadi Serializable.

Sama seperti domain class, masalah validasi adalah persoalan cross cutting yang dibutuhkan semua layer. Saya menyerahkan validasi pada Bean Validation yang menggunakan annotation seperti @NotBlank, @Size dan @NotNull pada domain class. Dengan demikian, validasi yang sama dan konsisten dapat diterapkan mulai dari presentation layer hingga persistence layer. Validasi ini juga dipakai oleh JPA untuk menghasilkan tabel lengkap dengan konstrain sehingga data yang tidak valid tidak bisa disimpan di database.

Bahkan, berkat PrimeFaces yang menyediakan komponen tambahan bagi JSF, validasi ini juga akan dikerjakan oleh kode program JavaScript sebelum nilai dikirim ke server. PrimeFaces memungkinkan validasi client side bila saya menambahkan atribut validateClient="true" pada <p:commandButton>. Hasil validasi client side akan terlihat seperti pada gambar berikut ini:

Validasi secara otomatis

Validasi secara otomatis

Untuk menampilkan pesan kesalahan, saya menggunakan <p:message>. Agar label untuk setiap pesan kesalahan juga ikut menjadi berwarna merah, saya tidak menggunakan <h:outputLabel> biasa melainkan <p:outputLabel>. Secara default, pesan kesalahan berada dalam bahasa Inggris. Karena validasi dilakukan di sisi klien, pesan kesalahan di ValidationMessage.properties tidak dapat dipakai. Oleh sebab itu, saya perlu membuat file JavaScript yang berisi pesan kesalahan dalam bahasa Indonesia seperti:

PrimeFaces.locales['en_US'] = {
  ...
  messages: {
    ...
    "javax.validation.constraints.NotNull.message": "tidak boleh kosong",
    "javax.validation.constraints.Size.message": "harus diantara {0} dan {1}",
    "javax.faces.validator.BeanValidator.MESSAGE": "{0}"
    ...
  }
}

Package com.jocki.inventory.config berisi konfigurasi aplikasi web. Salah satu pergerakan yang terlihat di Spring adalah peralihan konfigurasi berbasis XML menjadi konfigurasi programmatis melalui annotation. Bukan hanya Spring, tapi juga di Java. Sejak Servlet 3.0 (bagian dari JEE 6), file konfigurasi web.xml tidak wajib ada. Sebagai informasi, dulunya, setiap servlet wajib didaftarkan di web.xml. Sebagai gantinya, developer bisa membuat file javax.servlet.ServletContainerInitializer di folder META-INF/services untuk memberikan servlet container agar mengerjakan sebuah class untuk mencari definisi servlet dan sebagainya. Beruntungnya, semua ini sudah diurus oleh Spring WebMVC sehingga saya tinggal membuat turunan AbstractAnnotationConfigDispatcherServletInitializer seperti berikut ini:

public class DispatcherServletInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

  @Override
  protected Class<?>[] getRootConfigClasses() {
    return new Class[] { RootConfig.class, WebMvcConfig.class, WebFlowConfig.class };
  }

  @Override
  protected Class<?>[] getServletConfigClasses() {
    return null;
  }

  @Override
  protected String[] getServletMappings() {
    return new String[] { "/app/*" };
  }

  @Override
  public void onStartup(ServletContext servletContext) throws ServletException {
    servletContext.setInitParameter("javax.faces.DEFAULT_SUFFIX", ".xhtml");
    servletContext.setInitParameter("primefaces.FONT_AWESOME", "true");
    servletContext.setInitParameter("primefaces.CLIENT_SIDE_VALIDATION", "true");
    // Tambahkan konfigurasi development bila sedang tidak di-deploy di OpenShift
    if (System.getenv("OPENSHIFT_APP_NAME") == null) {
      servletContext.setInitParameter("javax.faces.PROJECT_STAGE", "Development");
      servletContext.setInitParameter("javax.faces.FACELETS_REFRESH_PERIOD", "1");
    } else {
      servletContext.setInitParameter("javax.faces.PROJECT_STAGE", "Production");
    }
    servletContext.addListener(ConfigureListener.class);
    super.onStartup(servletContext);
  }

}

Pada class di atas, saya menginstruksikan agar Spring selanjutnya membaca konfigurasi yang ada di class RootConfig, WebMvcConfig dan WebFlowConfig. Karena getServletMappings() mengembalikan /app/*, maka URL untuk aplikasi ini harus memiliki pola serupa seperti http://localhost/app/konsumen. Saya juga melakukan konfigurasi JSF pada class ini.

Aplikasi web biasanya membutuhkan state padahal protokol HTTP yang dipakai oleh website bersifat stateless. Oleh sebab itu, saya menggunakan Spring Web Flow (SWF) untuk memuluskan transisi antar halaman. SWF juga secara otomatis menerapkan POST-REDIRECT-GET sehingga tombol Back di browser dapat digunakan secara baik dan aman. Sebenarnya JSF 2.2 telah dilengkapi dengan dukungan deklarasi flow seperti pada SWF. Walaupun demikian, karena ini adalah fitur baru, saya merasa flow pada JSF masih terbatas sehingga saya memutuskan untuk tetap menggunakan SWF. Konfigurasi SWF dapat dilihat pada class WebFlowConfig yang merupakan turunan dari AbstractFacesFlowConfiguration. Class ini secara otomatis mendeklarasi mapping untuk akses ke URL seperti /javax.faces.resources/** untuk keperluan JSF.

Sebagai informasi, JavaServer Faces (JSF) adalah bagian dari Java EE yang memungkinkan developer memakai facelet dalam bentuk tag XML untuk menghasilkan komponen UI. Tujuannya adalah agar developer tidak perlu berhubungan langsung dengan semua operasi tingkat rendah yang melibatkan JavaScript, Ajax dan CSS styling. Bila komponen UI yang disediakan oleh JSF tidak cukup, pengguna bisa membuat komponen baru sendiri. Atau, pengguna juga dapat memakai komponen dari pihak ketiga seperti PrimeFaces, RichFaces, ICEfaces dan sebagainya.

Pengguna yang terbiasa membuat situs dengan PHP yang berorientasi file mungkin akan bertanya dimana sesungguhnya lokasi file dan bagaimana pemetaan file pada aplikasi web di Java. Berbeda jauh dari situs berorientasi file, sebuah servlet Java akan menerima input berupa URL tujuan lalu mengembalikan output sesuai dengan URL tersebut. Yup! Apapun URL-nya hanya akan ditangani oleh 1 servlet. Pada contoh di atas, saya secara tidak langsung hanya mendaftarkan org.springframework.web.servlet.DispatcherServlet dari Spring dan javax.faces.webapp.FacesServlet (dari Mojarra, implementasi JSF yang saya pakai). Setiap request URL akan ditangani oleh salah satu servlet di atas, lebih tepatnya adalah oleh DispatcherServlet milik Spring karena FacesServlet hanya dibutuhkan untuk inisialisasi JSF. Method yang menangani request akan terlihat seperti void service(HttpServletRequest request, HttpServletResponse response). Apa yang dikirim oleh pengguna mulai dari URL, parameter hingga cookie bisa dibaca melalui request. Untuk menulis hasil yang dikirim kepada pengguna, baik HTML, gambar, JSON, dan sebagainya, cukup tulis byte per byte melalui response.

Terlihat rumit? Saya tidak perlu melakukan pemograman tingkat rendah sampai ke servlet karena semua ini sudah ditangani oleh Spring WebMVC. Pada konfigurasi WebMvcConfig, saya membuat kode program seperti berikut ini:

@Configuration
@EnableWebMvc
@ComponentScan
public class WebMvcConfig extends WebMvcConfigurerAdapter {

  @Inject
  private WebFlowConfig webFlowConfig;

  @Override
  public void addViewControllers(ViewControllerRegistry registry) {
    registry.addViewController("/error").setViewName("error");
  }

  @Bean
  public UrlBasedViewResolver faceletsViewResolver() {
    UrlBasedViewResolver resolver = new UrlBasedViewResolver();
    resolver.setViewClass(JsfView.class);
    resolver.setPrefix("/WEB-INF/views/");
    resolver.setSuffix(".xhtml");
    return resolver;
  }

  ...
}

Pada konfigurasi di atas, saya memetakan URL /error dengan sebuah view bernama error di addViewControllers(). Dengan demikian, bila pengguna membuka URL seperti http://localhost/app/error, maka view dengan nama error akan ditampilkan. UrlBasedViewResolver yang saya pakai akan mencari view error di lokasi /WEB-INF/views/error.xhtml yang harus dalam bentuk facelet (JSF). Bila facelet tidak ditemukan, maka Spring mencari file dengan nama yang sama persis di src/main/webapp. Bila file tidak juga ditemukan, pesan kesalahan 404 akan diberikan pada pengguna. Pengguna tidak akan pernah bisa mengakses isi folder WEB-INF secara langsung walaupun folder ini terletak di dalam src/main/webapp.

Konfigurasi pada class RootConfig berisi hal diluar presentation layer seperti berikut ini:

@Configuration
@EnableJpaRepositories(basePackages="com.jocki.inventory.repository")
@EnableTransactionManagement
@ComponentScan("com.jocki.inventory")
public class RootConfig {

  @Bean
  public LocalContainerEntityManagerFactoryBean entityManagerFactory() throws NamingException {
    LocalContainerEntityManagerFactoryBean emf = new LocalContainerEntityManagerFactoryBean();
    emf.setDataSource(dataSource());
    emf.setJpaVendorAdapter(new HibernateJpaVendorAdapter());
    emf.setPackagesToScan("com.jocki.inventory.domain");

    Map<String,? super Object> jpaProperties = new HashMap<>();
    jpaProperties.put("hibernate.dialect", "org.hibernate.dialect.MySQL5Dialect");
    emf.setJpaPropertyMap(jpaProperties);

    return emf;
  }

  @Bean
  public DataSource dataSource() throws NamingException {
    JndiTemplate jndiTemplate = new JndiTemplate();
    DataSource dataSource = (DataSource) jndiTemplate.lookup("java:comp/env/jdbc/inventory");
    return dataSource;
  }

  @Bean
  public JpaTransactionManager transactionManager(EntityManagerFactory emf) {
    JpaTransactionManager transactionManager = new JpaTransactionManager();
    transactionManager.setEntityManagerFactory(emf);
    return transactionManager;
  }

}

Pada konfigurasi di atas, saya melakukan pengaturan JPA dan data source yang dipakai. Saya menambahkan annotation @EnableTransactionManagement sehingga transaksi pada service layer dapat diaktifkan. Selain itu, saya menambahkan @EnableJpaRepositories agar Spring Data JPA bisa secara otomatis menghasilkan implementasi dari interface repository yang ada. Saya juga menggunakan JNDI sehingga saya tidak perlu mendeklarasikan lokasi, nama pengguna dan password untuk database secara langsung di dalam kode program. Sebagai gantinya, server yang menjalankan aplikasi ini harus mendeklarasikan resource JNDI dengan nama jdbc/inventory yang berisi DataSource yang mewakili database yang hendak dipakai. Pada Tomcat, deklarasi ini dapat dilakukan dengan menambahkan baris seperti berikut ini pada file context.xml di folder conf:

<Resource auth="Container" driverClassName="com.mysql.jdbc.Driver" name="jdbc/inventory"
password="12345" type="javax.sql.DataSource" url="jdbc:mysql://localhost/latihandb"
username="namauser" validationQuery="/* ping */ SELECT 1"/>

Pada server percobaan, saya membuat resource jdbc/inventory yang merujuk pada database lokal. Untuk live server, tentunya resource ini harus merujuk pada database di live server. Peralihan ke database yang berbeda ini dapat berlangsung secara transparan tanpa harus mengubah kode program karena informasi koneksi database tidak tersimpan di dalam aplikasi.

Walaupun idealnya konfigurasi web.xml bisa dibuang sepenuhnya, untuk saat ini, saya masih tidak bisa lepas dari web.xml. Sebagai contoh, deklarasi halaman kesalahanan dan referensi JNDI tetap dibutuhkan pada web.xml:

<error-page>
  <location>/app/error</location>
</error-page>
<resource-ref>
  <description>Default Datasource</description>
  <res-ref-name>jdbc/inventory</res-ref-name>
  <res-type>javax.sql.DataSource</res-type>
  <res-auth>Container</res-auth>
</resource-ref>

Pada deklarasi di atas, bila terjadi kesalahan pada aplikasi web, maka URL /app/error akan ditampilkan. Sejak Servlet 3.0, saya bisa menggunakan 1 halaman error generik untuk seluruh jenis kesalahan yang ada. Sesuai dengan konfigurasi yang saya lakukan pada WebMvcConfig, URL ini akan menampilkan facelet yang terletak di src/main/webapp/WEB-INF/views/error.xhtml. Pada facelet ini, saya bisa mengakses informasi lebih lengkap mengenai kesalahan yang terjadi melalui EL seperti #{request.getAttribute('javax.servlet.error.message')} atau #{request.getAttribute('javax.servlet.error.status_code')}.

Sebuah halaman web biasanya memiliki template yang konsisten, misalnya mengandung bagian header dan footer. Oleh sebab itu, daripada mengetik ulang bagian yang sama berkali-kali (dan mengubahnya berkali-kali bila salah!), saya akan menggunakan fasilitas template dari JSF. Untuk itu, saya mendeklarasikan sebuah template di src/main/webapp/WEB-INF/layouts/standard.xml yang isinya seperti berikut ini:

<html ...>
<f:view contentType="text/html">
    <h:head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> 
        <title>Basic Inventory Web Demo</title>
        <h:outputStylesheet name="css/basic.css" /> 
        <h:outputScript name="js/locales.js" /> 
        <ui:insert name="headIncludes" />   
    </h:head>
    <h:body>
        <p:layout fullPage="true" style="border: 0px;">
            <p:layoutUnit position="west" resizable="true" collapsible="true" styleClass="main-menu">
                ...
            </p:layoutUnit>
            <p:layoutUnit position="center" styleClass="main-panel">
                <h:form id="mainForm">
                    <ui:insert name="content" />
                </h:form>
            </p:layoutUnit>
            <p:layoutUnit position="south" styleClass="main-panel">
                ...
            </p:layoutUnit>                       
        </p:layout>               
    </h:body>
</f:view>
<p:ajaxStatus onerror="PF('dialogKesalahan').show();"/>                                                   
<p:dialog header="Terjadi Kesalahan" id="dialogKesalahan" widgetVar="dialogKesalahan" modal="true" resizable="false">   
    <p>Telah terjadi kesalahan saat berkomunikasi dengan server.</p>
    <p>Silahkan perbaharui halaman atau ulangi beberapa saat kemudian!</p>
</p:dialog>
...
</html>

Pada facelet di atas, selain layout, saya juga menambahkan p:ajaxStatus secara global untuk setiap halaman yang ada sehingga pesan kesalahan akan selalu muncul bila terjadi kesalahan Ajax pada halaman mana saja seperti yang terlihat pada gambar berikut ini:

Tampilan pesan kesalahan pada Ajax

Tampilan pesan kesalahan pada Ajax

Salah satu kasus dimana pesan kesalahan ini akan selalu muncul adalah bila pengguna membuka sebuah halaman JSF dan meninggalkannya untuk jangka waktu yang lama. JSF menyimpan informasi komponen view yang dihasilkannya ke dalam session. Secara default, session di Tomcat diakhiri bila tidak ada aktifitas dari pengguna setelah 30 menit. Hal ini perlu dilakukan untuk mengirit memori. Bila pengguna mencoba memakai komponen JSF setelah session berakhir, ia akan menemukan setiap tombol tidak akan bekerja. Solusi untuk masalah ini cukup sederhana: pengguna harus me-refresh halaman untuk memulai session baru. PrimeFaces 5 sebenarnya memiliki komponen <p:ajaxExceptionHandler> untuk menghasilkan informasi kesalahan yang lebih jelas tetapi ia membutuhkan deklarasi halaman kesalahan secara eksplisit di web.xml.

File yang memakai template perlu menggunakan ui:composition dan mengisi nilai atribut template sesuai dengan lokasi file template yang hendak dipakai. Selain itu, pengguna template juga perlu menggunakan ui:define untuk menyisipkan nilai pada ui:insert di template. Sebagai contoh, facelet untuk pesan kesalahan akan terlihat seperti berikut ini:

<ui:composition ...  template="/WEB-INF/layouts/standard.xhtml">
    <ui:define name="content">
        <div class="ui-message-error ui-widget ui-corner-all">          
            <div class="ui-message-error-detail">
                <p><strong>Telah terjadi kesalahan saat menampilkan halaman ini!</strong></p>               
                <p>Status kesalahan: <strong><h:outputText value="#{request.getAttribute('javax.servlet.error.status_code')}"/></strong></p>
                ...
            </div>
        </div>
    </ui:define>
</ui:composition>

Saya mendeklarasikan setiap flow SWF dalam folder masing-masing di src/main/webapp/WEB-INF/flows. Sebagai contoh, URL seperti app/konsumen akan mengakses flow yang didefinisikan di src/main/webapp/WEB-INF/flows/konsumen/konsumen-flow.xml. SWF mendukung flow inheritance sehingga saya dapat memakai ulang pola flow yang sama berkali-kali. Sebagai contoh, semua flow untuk operasi CRUD diturunkan dari src/main/webapp/WEB-INF/flows/crud/crud-flow.xml yang memiliki aliran seperti:

Contoh flow di SWF

Contoh flow di SWF

Selain flow inheritance, SWF juga mendukung embedded flow. Ini adalah solusi yang tepat untuk Ajax. Sebagai contoh, pada flow di atas, bila pengguna melakukan aksi edit atau tambah, maka flow src/main/webapp/WEB-INF/flows/crud/entry/entry-flow.xml akan dikerjakan:

<flow ... abstract="true">

    <input name="selectedEntity" />    

    <view-state id="entry">
        <on-render>
            <set name="viewScope.updateMode" value="(selectedEntity != null) and (selectedEntity.id != null)" />
        </on-render>  
        <transition on="simpan" />
        <transition on="kembali" to="kembali" bind="false" validate="false" />
        <transition on="update" to="updateAction" />
    </view-state>     

    <action-state id="updateAction">                                        
        <transition on="success" to="kembali" />
        <transition on="error" to="entry" />  
    </action-state>

    <end-state id="kembali" />

</flow>

Karena saya menambahkan atribut abstract dengan nilai true, flow di atas tidak akan pernah bisa dipanggil secara langsung dengan URL seperti /app/crud/entry. Bila dipakai sebagai embedded flow, setiap perpindahan view-state pada flow di atas tidak akan menyebabkan request halaman baru (tidak ada POST-REDIRECT-GET) sehingga semuanya bisa berlangsung tanpa merefresh halaman secara penuh.

Contoh flow yang mengimplementasikan crud/entry adlaah konsumen/entry yang definisinya terlihat seperti berikut ini:

<flow ... parent="crud/entry">

  <view-state id="entry">

    <on-entry>
      <evaluate expression="kotaRepository.findAll()" result="viewScope.daftarKota" />
    </on-entry>

    <transition on="simpan">
      <evaluate expression="konsumenAction.simpan(selectedEntity, flowRequestContext)" />
    </transition>

    <transition on="kembali" to="kembali" bind="false" validate="false" />

    <transition on="update" to="updateAction" />

  </view-state>       

  <action-state id="updateAction">
     <evaluate expression="konsumenAction.update(selectedEntity, flowRequestContext)" />        
  </action-state>

</flow>

Pada saat view entry pertama kali dikerjakan, method findAll() dari kotaRepository akan dikerjakan dan hasilnya akan ditampung sebagai variabel di daftarKota (dengan view scope). Secara default, id dari view-state dipakai untuk menentukan facelet yang hendak ditampilkan, dalam hal ini adalah src/main/webapp/WEB-INF/konsumen/entry/entry.xhtml yang isinya seperti berikut ini:

<ui:composition ... template="/WEB-INF/layouts/standard.xhtml">
    <ui:define name="content">              
        <p:growl for="pesanInformasi" globalOnly="true" showDetail="true" />
        <p:panel id="panel" columns="3" styleClass="entry" header="#{updateMode? 'Edit Konsumen': 'Tambah Konsumen'}">
            <p:focus />
            <h:panelGrid columns="3" cellpadding="5">

                <p:outputLabel for="id" value="Id:" rendered="#{updateMode}"/>
                <p:inputText id="id" size="50" value="#{selectedEntity.id}" disabled="true" rendered="#{updateMode}"/>
                <p:message for="id" display="text" rendered="#{updateMode}"/>

                <p:outputLabel for="nama" value="Nama:" />
                <p:inputText id="nama" size="50" value="#{selectedEntity.nama}">
                    <p:clientValidator event="keyup" />
                </p:inputText>
                <p:message for="nama" display="text" />

                <p:outputLabel for="alamat" value="Alamat:" />
                <p:inputTextarea id="alamat" rows="5" cols="50" value="#{selectedEntity.alamat}">
                    <p:clientValidator event="keyup" />
                </p:inputTextarea>                                                    
                <p:message for="alamat" display="text" />

                <p:outputLabel for="kota" value="Kota:" />
                <p:selectOneMenu id="kota" value="#{selectedEntity.kota}" filter="true" converter="#{kotaConverter}">
                    <f:selectItems value="#{daftarKota}" var="vKota" itemValue="#{vKota}"/>
                  </p:selectOneMenu>  
                <p:message for="kota" display="text" />           

            </h:panelGrid>
            <div class="buttonPanel">
                <p:commandButton id="simpan" value="Simpan" icon="fa fa-floppy-o" action="simpan" validateClient="true" rendered="#{!updateMode}" update="mainForm"/>
                <p:commandButton id="update" value="Update" icon="fa fa-edit" action="update" validateClient="true" rendered="#{updateMode}" update="mainForm"/>                    
                <p:commandButton id="kembali" value="Kembali" icon="fa fa-arrow-left" action="kembali" immediate="true" />
            </div>                                    
        </p:panel>                            
    </ui:define>  
</ui:composition>

Pada facelet di atas, saya melakukan binding dari setiap komponen input ke variabel selectedEntity di flow melalui atribut value. Saya menambahkan p:clientValidator agar validasi JavaScript langsung dilakukan pada saat saya mengetik di komponen input. Khusus untuk p:selectOneMenu yang menampilkan pilihan daftar kota, saya perlu memberikan variabel daftar kota yang sudah saya query sebelumnya. Selain itu, saya juga perlu memberikan sebuah converter yang bisa menerjemahkan id kota menjadi nama kota dan sebaliknya (dapat dijumpai di src/main/java/view/converter/KotaConverter). Bila saya menampilkan view ini di browser dan nilai selectedEntity adalah null, saya akan memperoleh tampilan seperti berikut ini:

Contoh tampilan data entry

Contoh tampilan data entry

Bila seandainya selectedEntity tidak bernilai null, maka saya akan memperoleh tampilan seperti berikut ini:

Contoh tampilan edit

Contoh tampilan edit

Pada <p:commandButton>, saya menambahkan atribut update dengan nilai mainForm supaya saat tombol di-klik, hanya panel ini saja yang perlu diperbaharui melalui Ajax. Nilai action seperti 'simpan', 'update' atau 'kembali' harus sesuai dengan yang saya pakai di &lt;transition&gt; di deklarasi flow. Khusus untuk tombol kembali, saya menambahkan atribute immediate dengan nilai true agar validasi dan binding tidak dilakukan.

Pada view display, saya menggunakan sebuah <p:dataTable> yang dilengkapi dengan halaman, filter, dan pengurutan seperti yang terlihat pada gambar berikut ini:

Contoh tampilan tampilan tabel

Contoh tampilan tampilan tabel

Untuk meningkatkan kinerja, daripada men-query isi tabel sekaligus, saya hanya men-query 10 record saja per halaman. Begitu pengguna men-klik nomor halaman yang berbeda, kode program akan men-query 10 record lain yang dibutuhkan (melalui Ajax). Hal ini dapat dicapai dengan memberikan nilai "true" pada atribut lazy. Selain itu, saya juga perlu sebuah turunan LazyDataModel. Pada class ini, terdapat method seperti:

List load(int first, int pageSize, String sortField, SortOrder sortOrder, Map<String, Object> filters)

yang perlu di-override. Beruntungnya, Spring Data JPA mendukung pembatasan query per halaman dengan menggunakan interface Pageable (bentuk konkrit-nya adalah PageRequest). Pageable juga dapat dipakai untuk menyimpan informasi pengurutan.

Untuk membuat persistence layer, saya cukup membuat interface seperti:

public interface KonsumenRepository extends JpaRepository<Konsumen, Long> {

  Page findByNamaLike(String nama, Pageable pageable);

  Page findByAlamatLike(String alamat, Pageable pageable);

  Page findByKota_NamaLike(String namaKota, Pageable pageable);

}

Spring Data JPA akan membuat implementasi dari interface ini secara otomatis. Saya hanya perlu menambahkan @Inject pada class lain yang perlu memakai interface ini. Seluruh method yang ada mengembalikan object Page. Object ini tidak hanya berisi daftar Konsumen yang di-query, tetapi juga informasi lain seperti jumlah seluruh konsumen yang ada. Informasi seperti ini dibutuhkan untuk membuat daftar halaman, menentukan apakah ini adalah halaman pertama atau halaman terakhir, dan sebagainya. Selain finders, JpaRepository sudah menyediakan method seperti save() dan delete() untuk melakukan operasi CRUD pada sebuah JPA entity.

Pada service layer, saya membuat class seperti:

@Service @Transactional
public class KonsumenService {

  @Inject
  private transient KonsumenRepository konsumenRepository;

  @Override
  public Konsumen simpan(Konsumen konsumen) {
    return konsumenRepository.save(konsumen);
  }

  @Override
  public Konsumen update(Konsumen konsumenBaru) {
    Konsumen konsumen = konsumenRepository.findOne(konsumenBaru.getId());
    konsumen.setNama(konsumenBaru.getNama());
    konsumen.setAlamat(konsumenBaru.getAlamat());
    konsumen.setKota(konsumenBaru.getKota());
    return konsumen;
  }

  @Override
  public void hapus(Konsumen konsumen) {
    konsumenRepository.delete(konsumen.getId());
  }

}

Saya menggunakan annotation @Service untuk memberi tanda bahwa class di atas adalah bagian dari service layer. Selain itu, saya juga memberikan annotation @Transactional agar setiap method di dalam class ini dikerjakan dalam sebuah transaksi database. Bila terjadi kesalahan selama eksekusi sebuah method yang diindikasi oleh sebuah Exception, maka proses rollback harus dilakukan sehingga tidak ada perubahan yang terjadi pada database. Saya tidak menggunakan save() pada method update() melainkan men-update satu per satu atribut yang ada karena pada domain class yang kompleks, sering kali method update() hanya boleh memperbaharui sebagian atribut saja.

Berkat Spring container, saya bisa memakai class service layer di presentation layer dengan menggunakan @Inject seperti pada:

@Component
public class KonsumenAction {

  @Inject
  private KonsumenService konsumenService;

  ...

}

Saya kemudian bisa memakainya seperti pada:

public String update(@NotNull Konsumen entity, RequestContext context) {
  try {
    konsumenService.update(entity);
    return "success";
  } catch (Exception ex) {
    addErrorMessage(ex.getMessage());
    return "error";
  }
}

public void addInfoMessage(String message) {
  FacesMessage facesMessage = new FacesMessage(FacesMessage.SEVERITY_INFO, "Sukses", message);
  FacesContext.getCurrentInstance().addMessage("pesanInformasi", facesMessage);
}

public void addErrorMessage(String message) {
  FacesMessage facesMessage = new FacesMessage(FacesMessage.SEVERITY_ERROR, "Terjadi kesalahan", message);
  FacesContext.getCurrentInstance().addMessage("pesanInformasi", facesMessage);
}

Method simpan() mengembalikan sebuah String yang mewakili transisi yang akan dilakukan pada SWF. Selain itu, saya juga bisa menambahkan pesan informasi dalam bentuk FacesMessage yang bisa ditampilkan oleh PrimeFaces melalui p:growl seperti yang terlihat pada gambar berikut ini:

Tampilan pesan informasi melalui growl

Tampilan pesan informasi melalui growl

Karena KonsumenAction memiliki annotation @Component, maka singleton-nya sudah disediakan oleh Spring sehingga saya bisa langsung memakainya di SWF seperti pada contoh berikut ini:

<action-state id="updateAction">
    <evaluate expression="konsumenAction.update(selectedEntity, flowRequestContext)" />
    <transition on="success" to="kembali" />
    <transition on="error" to="entry" />          
</action-state>

Sebagai langkah terakhir, saya perlu men-deploy aplikasi web ini ke sebuah server. Bila saya menjalankan perintah gradle build, saya akan memperoleh sebuah file bernama ROOT.war di folder webapps. Masalahnya adalah kemana saya harus meletakkan file ini? Salah satu solusi gratis yang bisa saya pakai adalah dengan menggunakan OpenShift (https://www.openshift.com/) yang menyediakan layanan PaaS. Saya mulai dengan membuat sebuah cartridge DIY (Do It Yourself) baru dan men-install Tomcat 8 pada cartridge tersebut. Saya juga menambahkan sebuah cartridge MySQL untuk memperoleh sebuah database MySQL. Setelah men-setup script di folder .openshift dan memindahkan kode program ke server OpenShift dengan menggunakan Git, aplikasi bisa di-deploy dan dijalankan.

Salah satu hal yang harus diperhatikan saat memakai OpenShift adalah hanya port 15000 – 35530 yang boleh digunakan. Port 8080 adalah satu-satunya yang diperbolehkan diluar port yang diizinkan. Setiap request HTTP (di port 80) akan di forward ke port 8080 di server virtual saya. Saya juga perlu mengubah context.xml di server Tomcat 8 yang saya install agar memiliki resource bernama jdbc/inventory yang merujuk pada database MySQL. Saya bisa memperoleh informasi mengenai alamat host, port, user dan ip dengan melihat isi environment variable seperti OPENSHIFT_MYSQL_DB_HOST, OPENSHIFT_MYSQL_DB_PORT, OPENSHIFT_MYSQL_DB_USERNAME dan OPENSHIFT_MYSQL_DB_PASSWORD. Selain itu, sebagai pengguna gratis, aplikasi yang saya deploy akan memasuki idling state bila tidak pernah diakses selama 24 jam. Ini akan menyebabkan pengguna yang pertama kali mengakses aplikasi setelah idling state harus mengalami sedikit delay.